Skip to content

Commit c0193bf

Browse files
Add rendering stats tracker example (#3680)
1 parent 4af690d commit c0193bf

File tree

3 files changed

+251
-10
lines changed

3 files changed

+251
-10
lines changed

platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
package org.maplibre.android.testapp.activity.events
22

3-
import android.app.ActivityManager
4-
import android.os.Build
53
import android.os.Bundle
64
import android.view.View
7-
import android.view.WindowManager
85
import androidx.appcompat.app.AppCompatActivity
9-
import org.maplibre.android.MapLibre
106
import org.maplibre.android.maps.MapView
117
import org.maplibre.android.maps.Style
128
import org.maplibre.android.tile.TileOperation
139
import org.maplibre.android.testapp.R
1410
import org.maplibre.android.testapp.styles.TestStyles
15-
import timber.log.Timber
1611
import java.util.*
1712
import kotlin.time.TimeMark
1813
import kotlin.time.TimeSource
19-
import kotlin.time.TimeSource.Monotonic
2014
import org.maplibre.android.log.Logger
21-
import org.maplibre.android.log.Logger.INFO
2215
import org.maplibre.android.maps.RenderingStats
2316
import org.maplibre.android.maps.MapLibreMap
2417

25-
2618
// # --8<-- [start:ObserverActivity]
2719
/**
2820
* Test activity showcasing logging observer actions from the core
@@ -39,6 +31,7 @@ class ObserverActivity : AppCompatActivity(),
3931
// # --8<-- [end:ObserverActivity]
4032

4133
private lateinit var mapView: MapView
34+
private val renderStatsTracker = RenderStatsTracker()
4235

4336
companion object {
4437
const val TAG = "ObserverActivity"
@@ -61,6 +54,33 @@ class ObserverActivity : AppCompatActivity(),
6154
mapView.addOnDidFinishRenderingFrameListener(this)
6255
// # --8<-- [end:addListeners]
6356

57+
// # --8<-- [start:renderStatsTracker]
58+
renderStatsTracker.setReportFields(listOf(
59+
"encodingTime",
60+
"renderingTime",
61+
"numDrawCalls",
62+
"numActiveTextures",
63+
"numBuffers",
64+
"memTextures",
65+
"memBuffers"
66+
))
67+
68+
renderStatsTracker.setReportListener { _, _, avg ->
69+
Logger.i(TAG, "RenderStatsReport - avg - ${avg.nonZeroValuesString()}")
70+
}
71+
72+
renderStatsTracker.setThresholds(hashMapOf(
73+
"numDrawCalls" to 1000,
74+
"totalBuffers" to 1000L
75+
))
76+
77+
renderStatsTracker.setThresholdExceededListener { exceededValues, _ ->
78+
Logger.i(TAG, "Exceeded render values $exceededValues")
79+
}
80+
81+
renderStatsTracker.startReports(10L)
82+
// # --8<-- [end:renderStatsTracker]
83+
6484
mapView.getMapAsync {
6585
it.setStyle(
6686
Style.Builder().fromUri(TestStyles.DEMOTILES)
@@ -78,7 +98,7 @@ class ObserverActivity : AppCompatActivity(),
7898
}
7999

80100
// # --8<-- [start:printActionJournal]
81-
fun printActionJournal(map: MapLibreMap) {
101+
private fun printActionJournal(map: MapLibreMap) {
82102
// configure using `MapLibreMapOptions.actionJournal*` methods
83103

84104
Logger.i(TAG,"ActionJournal files: \n${map.actionJournalLogFiles.joinToString("\n")}")
@@ -136,7 +156,7 @@ class ObserverActivity : AppCompatActivity(),
136156
}
137157

138158
override fun onDidFinishRenderingFrame(fully: Boolean, stats: RenderingStats) {
139-
159+
renderStatsTracker.addFrame(stats)
140160
}
141161
// # --8<-- [end:mapEvents]
142162

@@ -168,6 +188,7 @@ class ObserverActivity : AppCompatActivity(),
168188
override fun onDestroy() {
169189
super.onDestroy()
170190
mapView.onDestroy()
191+
renderStatsTracker.stopReports()
171192
}
172193

173194
override fun onSaveInstanceState(outState: Bundle) {
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package org.maplibre.android.testapp.activity.events
2+
3+
import org.maplibre.android.maps.MapView
4+
import org.maplibre.android.maps.RenderingStats
5+
import java.lang.reflect.Field
6+
import java.util.Timer
7+
import kotlin.concurrent.fixedRateTimer
8+
9+
/**
10+
* Example class that tracks rendering statistics values (based on `RenderingStats` fields) and offers:
11+
* - periodic reports with min/max/average values
12+
* - threshold exceeded triggers
13+
* @see RenderingStats
14+
*/
15+
class RenderStatsTracker {
16+
private var min = RenderingStats()
17+
private var max = RenderingStats()
18+
private var total = RenderingStats()
19+
private var frameCount = 0
20+
21+
private var reportTimer: Timer? = null
22+
private var reportFields = RenderingStats::class.java.fields
23+
private var reportListener: ((RenderingStats, RenderingStats, RenderingStats) -> Unit)? = null
24+
25+
private var thresholds = HashMap<Int, Number>()
26+
private var thresholdExceededListener: ((HashMap<String, Number>,RenderingStats) -> Unit)? = null
27+
28+
/**
29+
* Set fields to be tracked and updated every frame. An empty list will track all fields.
30+
* The report listener will provide `RenderingStats` objects with the tracked values.
31+
*/
32+
@Synchronized fun setReportFields(values: List<String>?) {
33+
if (values == null) {
34+
reportFields = RenderingStats::class.java.fields
35+
return
36+
}
37+
38+
reportFields = values.map { RenderingStats::class.java.getField(it) }.toTypedArray()
39+
}
40+
41+
/**
42+
* Callback that provides min/max/average values for the configured interval (`start(interval)`)
43+
*/
44+
@Synchronized fun setReportListener(listener: ((RenderingStats, RenderingStats, RenderingStats) -> Unit)?) {
45+
if (reportFields.isEmpty()) {
46+
setReportFields(null)
47+
}
48+
49+
reportListener = listener
50+
}
51+
52+
/**
53+
* Set the thresholds to be checked
54+
*/
55+
@Synchronized fun setThresholds(values: HashMap<String, Number>) {
56+
thresholds.clear()
57+
val fields = RenderingStats::class.java.fields
58+
59+
// cache index instead of name
60+
values.map { value ->
61+
val index = fields.indexOfFirst { it.name == value.key }
62+
if (index != -1) {
63+
thresholds[index] = value.value
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Callback that provides the exceeded threshold values in the last frame.
70+
*/
71+
@Synchronized fun setThresholdExceededListener(listener: ((HashMap<String, Number>,RenderingStats) -> Unit)?) {
72+
thresholdExceededListener = listener
73+
}
74+
75+
/**
76+
* Start periodic reports every `interval` seconds
77+
*/
78+
@Synchronized fun startReports(interval: Long) {
79+
reset()
80+
81+
reportTimer = fixedRateTimer(
82+
name = "RenderStatsReportTimer",
83+
initialDelay = interval * 1000L,
84+
period = interval * 1000L
85+
) {
86+
reportListener?.invoke(min, max, getAvg())
87+
reset()
88+
}
89+
}
90+
91+
/**
92+
* Stop periodic reports. Must be called before the object is destroyed
93+
*/
94+
@Synchronized fun stopReports() {
95+
reportTimer?.cancel()
96+
reportTimer = null
97+
}
98+
99+
/**
100+
* Add the last frame data to the report.
101+
* @see MapView.OnDidFinishRenderingFrameWithStatsListener(fully: Boolean, stats: RenderingStats)
102+
*/
103+
@Synchronized fun addFrame(frameStats: RenderingStats) {
104+
updateReportValues(frameStats)
105+
checkThresholds(frameStats)
106+
}
107+
108+
private fun updateReportValues(frameStats: RenderingStats) {
109+
if (reportListener == null || reportTimer == null) {
110+
return
111+
}
112+
113+
++frameCount;
114+
115+
reportFields.map { field: Field ->
116+
when (val frameValue = field.get(frameStats)) {
117+
is Int -> {
118+
if (field.getInt(min) > frameValue) {
119+
field.setInt(min, frameValue)
120+
}
121+
122+
if (field.getInt(max) < frameValue) {
123+
field.setInt(max, frameValue)
124+
}
125+
126+
field.setInt(total, frameValue + field.getInt(total))
127+
}
128+
129+
is Long -> {
130+
if (field.getLong(min) > frameValue) {
131+
field.setLong(min, frameValue)
132+
}
133+
134+
if (field.getLong(max) < frameValue) {
135+
field.setLong(max, frameValue)
136+
}
137+
138+
field.setLong(total, frameValue + field.getLong(total))
139+
}
140+
141+
is Double -> {
142+
if (field.getDouble(min) > frameValue) {
143+
field.setDouble(min, frameValue)
144+
}
145+
146+
if (field.getDouble(max) < frameValue) {
147+
field.setDouble(max, frameValue)
148+
}
149+
150+
field.setDouble(total, frameValue + field.getDouble(total))
151+
}
152+
153+
else -> {}
154+
}
155+
}
156+
}
157+
158+
private fun checkThresholds(frameStats: RenderingStats) {
159+
if (thresholdExceededListener == null && thresholds.isEmpty()) {
160+
return
161+
}
162+
163+
val fields = RenderingStats::class.java.fields
164+
val exceededValues = HashMap<String, Number>()
165+
166+
thresholds.map {
167+
when (val frameValue = fields[it.key].get(frameStats)) {
168+
is Int -> if (frameValue > it.value as Int) exceededValues[fields[it.key].name] = frameValue
169+
is Long -> if (frameValue > it.value as Long) exceededValues[fields[it.key].name] = frameValue
170+
is Double -> if (frameValue > it.value as Double) exceededValues[fields[it.key].name] = frameValue
171+
else -> {}
172+
}
173+
}
174+
175+
if (exceededValues.isNotEmpty()) {
176+
thresholdExceededListener?.invoke(exceededValues, frameStats)
177+
}
178+
}
179+
180+
private fun getAvg(): RenderingStats {
181+
val avg = RenderingStats()
182+
183+
if (frameCount == 0) {
184+
return avg;
185+
}
186+
187+
reportFields.map { field: Field ->
188+
when (val value = field.get(total)) {
189+
is Int -> field.setInt(avg, value / frameCount)
190+
is Long -> field.setLong(avg, value / frameCount)
191+
is Double -> field.setDouble(avg, value / frameCount)
192+
else -> {}
193+
}
194+
}
195+
196+
return avg
197+
}
198+
199+
private fun reset() {
200+
reportFields.map { field: Field -> field.set(min, Int.MAX_VALUE) }
201+
reportFields.map { field: Field -> field.set(max, -Int.MAX_VALUE) }
202+
reportFields.map { field: Field -> field.set(total, 0) }
203+
204+
frameCount = 0
205+
}
206+
}
207+
208+
fun RenderingStats.nonZeroValuesString(): String {
209+
return this::class.java.fields
210+
.filter { (it.get(this) as? Number)?.toDouble() != 0.0 }
211+
.joinToString(", ") { field ->
212+
"(${field.name}=${field.get(this)})"
213+
}
214+
}

platform/android/docs/observability/observe-map-events.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ In this case we implement them by implementing the interfaces below in the activ
1919
```kotlin
2020
--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:ObserverActivity"
2121
```
22+
23+
`ObserverActivity.onDidFinishRenderingFrame` uses `RenderStatsTracker` as an example for tracking rendering statistics over time. This offers periodic reports of minimum, maximum, average values and callbacks when predefined thresholds are exceeded.
24+
25+
```kotlin
26+
--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:renderStatsTracker"
27+
```

0 commit comments

Comments
 (0)