Skip to content

Commit

Permalink
MAPSIOS-1170: Expose performance statistics API (#1946)
Browse files Browse the repository at this point in the history
* MAPSIOS-1170: Expose performance statistics API

* Add tests and changelog

* Add `isOn` parameter to `collectPerformanceStatistics` in SwiftUI
Sync UIKit example to what Android have

* Bump versions and sync example with Android

* Add tests, adjust the example

* Adopt fixes from gl-native

* Bump CoreMaps version to build on CI

* Update Package.resolved with snapshot

* Fix doc and rname handler to callback to sync with gl-native naming

* Use snapshot with proper Equatable PerformanceStatisticsOptions and fix comments
  • Loading branch information
aleksproger authored Jan 29, 2024
1 parent d83e2e3 commit 1a5fd6f
Show file tree
Hide file tree
Showing 13 changed files with 418 additions and 87 deletions.
9 changes: 0 additions & 9 deletions Apps/Apps.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@
"version" : "24.1.0"
}
},
{
"identity" : "mapbox-core-maps-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mapbox/mapbox-core-maps-ios.git",
"state" : {
"revision" : "d9cdf51cf4b897811a5c5d5135d3c772830e2b7c",
"version" : "11.1.0"
}
},
{
"identity" : "turf-swift",
"kind" : "remoteSourceControl",
Expand Down
154 changes: 105 additions & 49 deletions Apps/Examples/Examples/All Examples/DebugMapExample.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import UIKit
@_spi(Experimental) import MapboxMaps

private protocol DebugOptionSettingsDelegate: AnyObject {
func debugOptionSettingsDidChange(_ controller: SettingsViewController)
}

private struct MapDebugOptionSetting {
let debugOption: MapViewDebugOptions
let displayTitle: String
}

final class DebugMapExample: UIViewController, ExampleProtocol, DebugOptionSettingsDelegate {
final class DebugMapExample: UIViewController, ExampleProtocol {
private var collectStatisticsButton = UIButton(type: .system)
private var mapView: MapView!
private var performanceStatisticsCancelable: AnyCancelable?
private let settings: [Setting] = [
Setting(option: .debug(.collision), title: "Debug collision"),
Setting(option: .debug(.depthBuffer), title: "Show depth buffer"),
Setting(option: .debug(.overdraw), title: "Debug overdraw"),
Setting(option: .debug(.parseStatus), title: "Show tile coordinate"),
Setting(option: .debug(.stencilClip), title: "Show stencil buffer"),
Setting(option: .debug(.tileBorders), title: "Debug tile clipping"),
Setting(option: .debug(.timestamps), title: "Show tile loaded time"),
Setting(option: .debug(.modelBounds), title: "Show 3D model bounding boxes"),
Setting(option: .debug(.light), title: "Show light conditions"),
Setting(option: .debug(.camera), title: "Show camera debug view"),
Setting(option: .debug(.padding), title: "Camera padding"),
Setting(option: .performance(.init([.perFrame, .cumulative], samplingDurationMillis: 5000)), title: "Performance statistics"),
]

override func viewDidLoad() {
super.viewDidLoad()
Expand Down Expand Up @@ -52,7 +59,7 @@ final class DebugMapExample: UIViewController, ExampleProtocol, DebugOptionSetti
}

@objc private func openDebugOptionsMenu(_ sender: UIBarButtonItem) {
let settingsViewController = SettingsViewController(debugOptions: mapView.debugOptions)
let settingsViewController = SettingsViewController(settings: settings)
settingsViewController.delegate = self

let navigationController = UINavigationController(rootViewController: settingsViewController)
Expand All @@ -68,34 +75,28 @@ final class DebugMapExample: UIViewController, ExampleProtocol, DebugOptionSetti
showAlert(withTitle: "Displayed tiles", and: message)
}

fileprivate func debugOptionSettingsDidChange(_ controller: SettingsViewController) {
controller.dismiss(animated: true, completion: nil)
mapView.debugOptions = controller.enabledDebugOptions
private func handle(statistics: PerformanceStatistics) {
showAlert(with: "\(statistics.topRenderedGroupDescription)\n\(statistics.renderingDurationStatisticsDescription)")
}
}

private final class SettingsViewController: UIViewController, UITableViewDataSource {
extension DebugMapExample: DebugOptionSettingsDelegate {
func settingsDidChange(debugOptions: MapViewDebugOptions, performanceOptions: PerformanceStatisticsOptions?) {
mapView.debugOptions = debugOptions

guard let performanceOptions else { return performanceStatisticsCancelable = nil }
performanceStatisticsCancelable?.cancel()
performanceStatisticsCancelable = mapView.mapboxMap.collectPerformanceStatistics(performanceOptions, callback: handle(statistics:))
}
}

final class SettingsViewController: UIViewController, UITableViewDataSource {
weak var delegate: DebugOptionSettingsDelegate?
private var listView: UITableView!
private let settings: [Setting]

private(set) var enabledDebugOptions: MapViewDebugOptions
private let allSettings: [MapDebugOptionSetting] = [
MapDebugOptionSetting(debugOption: .collision, displayTitle: "Debug collision"),
MapDebugOptionSetting(debugOption: .depthBuffer, displayTitle: "Show depth buffer"),
MapDebugOptionSetting(debugOption: .overdraw, displayTitle: "Debug overdraw"),
MapDebugOptionSetting(debugOption: .parseStatus, displayTitle: "Show tile coordinate"),
MapDebugOptionSetting(debugOption: .stencilClip, displayTitle: "Show stencil buffer"),
MapDebugOptionSetting(debugOption: .tileBorders, displayTitle: "Debug tile clipping"),
MapDebugOptionSetting(debugOption: .timestamps, displayTitle: "Show tile loaded time"),
MapDebugOptionSetting(debugOption: .modelBounds, displayTitle: "Show 3D model bounding boxes"),
MapDebugOptionSetting(debugOption: .light, displayTitle: "Show light conditions"),
MapDebugOptionSetting(debugOption: .camera, displayTitle: "Show camera debug view"),
MapDebugOptionSetting(debugOption: .padding, displayTitle: "Camera padding")
]

init(debugOptions: MapViewDebugOptions) {
enabledDebugOptions = debugOptions
fileprivate init(settings: [Setting]) {
self.settings = settings
super.init(nibName: nil, bundle: nil)
}

Expand Down Expand Up @@ -134,27 +135,31 @@ private final class SettingsViewController: UIViewController, UITableViewDataSou
}

@objc private func saveSettings(_ sender: UIBarButtonItem) {
delegate?.debugOptionSettingsDidChange(self)
let debugOptions = settings
.filter(\.isEnabled)
.compactMap(\.option.debugOption)
.reduce(MapViewDebugOptions()) { result, next in result.union(next) }

let performanceOptions = settings
.filter(\.isEnabled)
.compactMap(\.option.performanceOption)

delegate?.settingsDidChange(debugOptions: debugOptions, performanceOptions: performanceOptions.first)
dismiss(animated: true, completion: nil)
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
allSettings.count
settings.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellID = String(describing: DebugOptionCell.self)
// swiftlint:disable:next force_cast
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! DebugOptionCell

let setting = allSettings[indexPath.row]
cell.configure(with: setting, isOptionEnabled: enabledDebugOptions.contains(setting.debugOption))
cell.onToggled { [unowned self] isEnabled in
if isEnabled {
self.enabledDebugOptions.insert(setting.debugOption)
} else {
self.enabledDebugOptions.remove(setting.debugOption)
}
}
let setting = settings[indexPath.row]
cell.configure(with: setting.title, isOptionEnabled: setting.isEnabled)
cell.onToggled(setting.toggle)

return cell
}
Expand All @@ -163,9 +168,9 @@ private final class SettingsViewController: UIViewController, UITableViewDataSou
// MARK: Cell

private class DebugOptionCell: UITableViewCell {

private let titleLabel = UILabel()
private let toggle = UISwitch()
private var onToggleHandler: (() -> Void)?

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
Expand Down Expand Up @@ -193,18 +198,69 @@ private class DebugOptionCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}

func configure(with setting: MapDebugOptionSetting, isOptionEnabled: Bool) {
titleLabel.text = setting.displayTitle
func configure(with title: String, isOptionEnabled: Bool) {
titleLabel.text = title
toggle.isOn = isOptionEnabled
}

private var onToggleHandler: ((Bool) -> Void)?

func onToggled(_ handler: @escaping (Bool) -> Void) {
func onToggled(_ handler: @escaping () -> Void) {
onToggleHandler = handler
}

@objc private func didToggle(_ sender: UISwitch) {
onToggleHandler?(sender.isOn)
onToggleHandler?()
}
}

protocol DebugOptionSettingsDelegate: AnyObject {
func settingsDidChange(debugOptions: MapViewDebugOptions, performanceOptions: PerformanceStatisticsOptions?)
}

private final class Setting {
enum Option {
case debug(MapViewDebugOptions)
case performance(PerformanceStatisticsOptions)
}

let option: Option
let title: String
private(set) var isEnabled: Bool

init(option: Option, title: String, isEnabled: Bool = false) {
self.option = option
self.title = title
self.isEnabled = isEnabled
}

func toggle() { isEnabled.toggle() }
}

extension Setting.Option {
var debugOption: MapViewDebugOptions? {
if case let .debug(option) = self { return option }
else { return nil }
}

var performanceOption: PerformanceStatisticsOptions? {
if case let .performance(option) = self { return option }
else { return nil }
}
}

extension PerformanceStatistics {
fileprivate var topRenderedGroupDescription: String {
if let topRenderedGroup = perFrameStatistics?.topRenderGroups.first {
return "Top rendered group: `\(topRenderedGroup.name)` took \(topRenderedGroup.durationMillis)ms."
} else {
return "No information about topRenderedLayer."
}
}

fileprivate var renderingDurationStatisticsDescription: String {
guard let drawCalls = cumulativeStatistics?.drawCalls else { return "Cumulative statistics haven't been collected." }
return """
Number of draw calls: \(drawCalls).
"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,69 @@ struct Settings {
var constrainMode: ConstrainMode = .heightOnly
var ornamentSettings = OrnamentSettings()
var debugOptions: MapViewDebugOptions = [.camera]

var performance = PerformanceSettings()

struct OrnamentSettings {
var isScaleBarVisible = true
var isCompassVisible = true
}

struct PerformanceSettings {
var samplerOptions = PerformanceStatisticsOptions.SamplerOptions([.perFrame, .cumulative])
var samplingDurationMillis: UInt32 = 5000
var isStatisticsEnabled = false

var statisticsOptions: PerformanceStatisticsOptions {
PerformanceStatisticsOptions(samplerOptions, samplingDurationMillis: Double(samplingDurationMillis))
}
}
}

@available(iOS 14.0, *)
struct MapSettingsExample : View {
@State private var settingsOpened = false
@State private var settings = Settings()
@State private var performanceStatistics: PerformanceStatistics? = nil

var body: some View {
Map(initialViewport: .camera(center: .berlin, zoom: 12))
.cameraBounds(settings.cameraBounds)
.mapStyle(settings.mapStyle)
.gestureOptions(settings.gestureOptions)
.gestureHandlers(gestureHandlers)
.northOrientation(settings.orientation)
.constrainMode(settings.constrainMode)
.ornamentOptions(OrnamentOptions(
scaleBar: ScaleBarViewOptions(visibility: settings.ornamentSettings.isScaleBarVisible ? .visible : .hidden),
compass: CompassViewOptions(visibility: settings.ornamentSettings.isCompassVisible ? .visible : .hidden)
))
.debugOptions(settings.debugOptions)
.ignoresSafeArea()
.sheet(isPresented: $settingsOpened) {
SettingsView(settings: $settings)
.defaultDetents()
}
.safeOverlay(alignment: .trailing, content: {
MapStyleSelectorButton(mapStyle: $settings.mapStyle)
})
.toolbar {
Button("Settings") {
settingsOpened.toggle()
ZStack(alignment: .bottom) {
Map(initialViewport: .camera(center: .berlin, zoom: 12))
.cameraBounds(settings.cameraBounds)
.mapStyle(settings.mapStyle)
.gestureOptions(settings.gestureOptions)
.gestureHandlers(gestureHandlers)
.northOrientation(settings.orientation)
.constrainMode(settings.constrainMode)
.collectPerformanceStatistics(settings.performance.isStatisticsEnabled ? settings.performance.statisticsOptions : nil) { stats in
performanceStatistics = stats
}
.ornamentOptions(OrnamentOptions(
scaleBar: ScaleBarViewOptions(visibility: settings.ornamentSettings.isScaleBarVisible ? .visible : .hidden),
compass: CompassViewOptions(visibility: settings.ornamentSettings.isCompassVisible ? .visible : .hidden)
))
.debugOptions(settings.debugOptions)
.ignoresSafeArea()
.sheet(isPresented: $settingsOpened) {
SettingsView(settings: $settings)
.defaultDetents()
}
.safeOverlay(alignment: .trailing) {
MapStyleSelectorButton(mapStyle: $settings.mapStyle)
}
.toolbar {
Button("Settings") {
settingsOpened.toggle()
}
}

if settings.performance.isStatisticsEnabled, let stats = performanceStatistics {
VStack(alignment: .leading) {
Text(stats.topRenderedLayerDescription).font(.safeMonospaced)
Text(stats.renderingDurationStatisticsDescription).font(.safeMonospaced)
}
.floating()
}
}
}

var gestureHandlers: MapGestureHandlers {
Expand Down Expand Up @@ -121,6 +146,22 @@ struct SettingsView : View {
} header: {
Text("Debug Options")
}
Section {
let samplerOptions = [
("Per Frame", PerformanceStatisticsOptions.SamplerOptions.perFrame),
("Cumulative", .cumulative)
]
Toggle("Collect Statistics", isOn: $settings.performance.isStatisticsEnabled)

if settings.performance.isStatisticsEnabled {
Stepper("Sampling Duration, \(settings.performance.samplingDurationMillis) ms", value: $settings.performance.samplingDurationMillis, step: 1000)
ForEach(samplerOptions, id: \.0) { option in
Toggle(option.0, isOn: $settings.performance.samplerOptions.contains(option: option.1))
}
}
} header: {
Text("Performance Statistics")
}
}
}
}
Expand Down Expand Up @@ -149,3 +190,17 @@ struct MapSettingsExample_Preveiw: PreviewProvider {
}
}
}

extension PerformanceStatistics {
fileprivate var topRenderedLayerDescription: String {
if let topRenderedLayer = perFrameStatistics?.topRenderLayers.first {
return "Top rendered layer: `\(topRenderedLayer.name)` for \(topRenderedLayer.durationMillis)ms."
} else {
return "No information about topRenderedLayer."
}
}

fileprivate var renderingDurationStatisticsDescription: String {
"Max rendering call duration: \(mapRenderDurationStatistics.maxMillis)ms.\nMedian rendering call duration: \(mapRenderDurationStatistics.medianMillis)ms"
}
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Use them to configure respective map options after creating a map view.
* Expose experimental `CustomRasterSource` and non-experimental `CustomGeometrySource` as regular `Source`'s providing a better way to work with them and also allow for using them in Style DSL.
* Introduce `SymbolLayer.iconColorSaturation` API.
* Introduce experimental `RasterLayer.rasterElevation` API.
* Introduce experimental `MapboxMap.collectPerformanceStatistics` allowing to collect map rendering performance statistics, both for UIKit and SwiftUI.

## 11.1.0 - 17 January, 2024

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Maps rendering SDK also known as GL-Native
- <doc:Offline>

### Free Camera
- <doc:Free-Camera>
- <doc:Free-Camera>
1 change: 1 addition & 0 deletions Sources/MapboxMaps/Foundation/CoreAliases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ typealias CoreRuntimeStylingOptions = MapboxCoreMaps_Private.RuntimeStylingOptio
typealias CoreObservable = MapboxCoreMaps_Private.Observable
typealias CoreViewAnnotationPositionsUpdateListener = MapboxCoreMaps_Private.ViewAnnotationPositionsUpdateListener
typealias CoreMapSnapshotter = MapboxCoreMaps_Private.MapSnapshotter
typealias CorePerformanceSamplerOptions = MapboxCoreMaps_Private.PerformanceSamplerOptions
Loading

0 comments on commit 1a5fd6f

Please sign in to comment.