Add rendering stats tracker example (#3680)

This commit is contained in:
Adrian Cojocaru
2025-07-31 14:04:01 +03:00
committed by GitHub
parent 4af690dcb3
commit c0193bf0df
3 changed files with 251 additions and 10 deletions

View File

@@ -1,28 +1,20 @@
package org.maplibre.android.testapp.activity.events
import android.app.ActivityManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.MapLibre
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.Style
import org.maplibre.android.tile.TileOperation
import org.maplibre.android.testapp.R
import org.maplibre.android.testapp.styles.TestStyles
import timber.log.Timber
import java.util.*
import kotlin.time.TimeMark
import kotlin.time.TimeSource
import kotlin.time.TimeSource.Monotonic
import org.maplibre.android.log.Logger
import org.maplibre.android.log.Logger.INFO
import org.maplibre.android.maps.RenderingStats
import org.maplibre.android.maps.MapLibreMap
// # --8<-- [start:ObserverActivity]
/**
* Test activity showcasing logging observer actions from the core
@@ -39,6 +31,7 @@ class ObserverActivity : AppCompatActivity(),
// # --8<-- [end:ObserverActivity]
private lateinit var mapView: MapView
private val renderStatsTracker = RenderStatsTracker()
companion object {
const val TAG = "ObserverActivity"
@@ -61,6 +54,33 @@ class ObserverActivity : AppCompatActivity(),
mapView.addOnDidFinishRenderingFrameListener(this)
// # --8<-- [end:addListeners]
// # --8<-- [start:renderStatsTracker]
renderStatsTracker.setReportFields(listOf(
"encodingTime",
"renderingTime",
"numDrawCalls",
"numActiveTextures",
"numBuffers",
"memTextures",
"memBuffers"
))
renderStatsTracker.setReportListener { _, _, avg ->
Logger.i(TAG, "RenderStatsReport - avg - ${avg.nonZeroValuesString()}")
}
renderStatsTracker.setThresholds(hashMapOf(
"numDrawCalls" to 1000,
"totalBuffers" to 1000L
))
renderStatsTracker.setThresholdExceededListener { exceededValues, _ ->
Logger.i(TAG, "Exceeded render values $exceededValues")
}
renderStatsTracker.startReports(10L)
// # --8<-- [end:renderStatsTracker]
mapView.getMapAsync {
it.setStyle(
Style.Builder().fromUri(TestStyles.DEMOTILES)
@@ -78,7 +98,7 @@ class ObserverActivity : AppCompatActivity(),
}
// # --8<-- [start:printActionJournal]
fun printActionJournal(map: MapLibreMap) {
private fun printActionJournal(map: MapLibreMap) {
// configure using `MapLibreMapOptions.actionJournal*` methods
Logger.i(TAG,"ActionJournal files: \n${map.actionJournalLogFiles.joinToString("\n")}")
@@ -136,7 +156,7 @@ class ObserverActivity : AppCompatActivity(),
}
override fun onDidFinishRenderingFrame(fully: Boolean, stats: RenderingStats) {
renderStatsTracker.addFrame(stats)
}
// # --8<-- [end:mapEvents]
@@ -168,6 +188,7 @@ class ObserverActivity : AppCompatActivity(),
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
renderStatsTracker.stopReports()
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@@ -0,0 +1,214 @@
package org.maplibre.android.testapp.activity.events
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.RenderingStats
import java.lang.reflect.Field
import java.util.Timer
import kotlin.concurrent.fixedRateTimer
/**
* Example class that tracks rendering statistics values (based on `RenderingStats` fields) and offers:
* - periodic reports with min/max/average values
* - threshold exceeded triggers
* @see RenderingStats
*/
class RenderStatsTracker {
private var min = RenderingStats()
private var max = RenderingStats()
private var total = RenderingStats()
private var frameCount = 0
private var reportTimer: Timer? = null
private var reportFields = RenderingStats::class.java.fields
private var reportListener: ((RenderingStats, RenderingStats, RenderingStats) -> Unit)? = null
private var thresholds = HashMap<Int, Number>()
private var thresholdExceededListener: ((HashMap<String, Number>,RenderingStats) -> Unit)? = null
/**
* Set fields to be tracked and updated every frame. An empty list will track all fields.
* The report listener will provide `RenderingStats` objects with the tracked values.
*/
@Synchronized fun setReportFields(values: List<String>?) {
if (values == null) {
reportFields = RenderingStats::class.java.fields
return
}
reportFields = values.map { RenderingStats::class.java.getField(it) }.toTypedArray()
}
/**
* Callback that provides min/max/average values for the configured interval (`start(interval)`)
*/
@Synchronized fun setReportListener(listener: ((RenderingStats, RenderingStats, RenderingStats) -> Unit)?) {
if (reportFields.isEmpty()) {
setReportFields(null)
}
reportListener = listener
}
/**
* Set the thresholds to be checked
*/
@Synchronized fun setThresholds(values: HashMap<String, Number>) {
thresholds.clear()
val fields = RenderingStats::class.java.fields
// cache index instead of name
values.map { value ->
val index = fields.indexOfFirst { it.name == value.key }
if (index != -1) {
thresholds[index] = value.value
}
}
}
/**
* Callback that provides the exceeded threshold values in the last frame.
*/
@Synchronized fun setThresholdExceededListener(listener: ((HashMap<String, Number>,RenderingStats) -> Unit)?) {
thresholdExceededListener = listener
}
/**
* Start periodic reports every `interval` seconds
*/
@Synchronized fun startReports(interval: Long) {
reset()
reportTimer = fixedRateTimer(
name = "RenderStatsReportTimer",
initialDelay = interval * 1000L,
period = interval * 1000L
) {
reportListener?.invoke(min, max, getAvg())
reset()
}
}
/**
* Stop periodic reports. Must be called before the object is destroyed
*/
@Synchronized fun stopReports() {
reportTimer?.cancel()
reportTimer = null
}
/**
* Add the last frame data to the report.
* @see MapView.OnDidFinishRenderingFrameWithStatsListener(fully: Boolean, stats: RenderingStats)
*/
@Synchronized fun addFrame(frameStats: RenderingStats) {
updateReportValues(frameStats)
checkThresholds(frameStats)
}
private fun updateReportValues(frameStats: RenderingStats) {
if (reportListener == null || reportTimer == null) {
return
}
++frameCount;
reportFields.map { field: Field ->
when (val frameValue = field.get(frameStats)) {
is Int -> {
if (field.getInt(min) > frameValue) {
field.setInt(min, frameValue)
}
if (field.getInt(max) < frameValue) {
field.setInt(max, frameValue)
}
field.setInt(total, frameValue + field.getInt(total))
}
is Long -> {
if (field.getLong(min) > frameValue) {
field.setLong(min, frameValue)
}
if (field.getLong(max) < frameValue) {
field.setLong(max, frameValue)
}
field.setLong(total, frameValue + field.getLong(total))
}
is Double -> {
if (field.getDouble(min) > frameValue) {
field.setDouble(min, frameValue)
}
if (field.getDouble(max) < frameValue) {
field.setDouble(max, frameValue)
}
field.setDouble(total, frameValue + field.getDouble(total))
}
else -> {}
}
}
}
private fun checkThresholds(frameStats: RenderingStats) {
if (thresholdExceededListener == null && thresholds.isEmpty()) {
return
}
val fields = RenderingStats::class.java.fields
val exceededValues = HashMap<String, Number>()
thresholds.map {
when (val frameValue = fields[it.key].get(frameStats)) {
is Int -> if (frameValue > it.value as Int) exceededValues[fields[it.key].name] = frameValue
is Long -> if (frameValue > it.value as Long) exceededValues[fields[it.key].name] = frameValue
is Double -> if (frameValue > it.value as Double) exceededValues[fields[it.key].name] = frameValue
else -> {}
}
}
if (exceededValues.isNotEmpty()) {
thresholdExceededListener?.invoke(exceededValues, frameStats)
}
}
private fun getAvg(): RenderingStats {
val avg = RenderingStats()
if (frameCount == 0) {
return avg;
}
reportFields.map { field: Field ->
when (val value = field.get(total)) {
is Int -> field.setInt(avg, value / frameCount)
is Long -> field.setLong(avg, value / frameCount)
is Double -> field.setDouble(avg, value / frameCount)
else -> {}
}
}
return avg
}
private fun reset() {
reportFields.map { field: Field -> field.set(min, Int.MAX_VALUE) }
reportFields.map { field: Field -> field.set(max, -Int.MAX_VALUE) }
reportFields.map { field: Field -> field.set(total, 0) }
frameCount = 0
}
}
fun RenderingStats.nonZeroValuesString(): String {
return this::class.java.fields
.filter { (it.get(this) as? Number)?.toDouble() != 0.0 }
.joinToString(", ") { field ->
"(${field.name}=${field.get(this)})"
}
}

View File

@@ -19,3 +19,9 @@ In this case we implement them by implementing the interfaces below in the activ
```kotlin
--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:ObserverActivity"
```
`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.
```kotlin
--8<-- "MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/events/ObserverActivity.kt:renderStatsTracker"
```