Skip to content

Commit d3f3604

Browse files
jushpengdev
authored andcommitted
Calculate more fine-grained FPS statistics in Android renderer (#10591)
## Summary - Improves FPS statistics logging in `FpsManager` to distinguish between different types of frame skips and rendering delays - Adds tracking for frames missed due to frame pacing vs frames missed due to slow rendering - Provides more detailed performance metrics for debugging rendering issues ## Changes - Added `choreographerPacingSkips` counter to track VSYNC skips specifically due to intentional frame pacing - Added `missedMapRenderFrames` counter to track map render frames missed due to rendering taking too long (calculated based on the configured map render frame rate, not just screen refresh rate) - Enhanced logging to report: - Average map core rendering time in milliseconds and FPS - Number of map render frames missed - VSYNC skips broken down by total and pacing-related - Added documentation comments explaining the purpose of each counter - Improved clarity of log messages to differentiate between different performance metrics ## Context The previous FPS statistics lumped all frame skips together, making it difficult to distinguish between: 1. Intentional skips due to frame pacing (when rendering at lower than screen refresh rate) 2. Missed frames due to rendering taking too long This change provides clearer visibility into rendering performance for debugging and optimization. cc @mapbox/maps-android cc @mapbox/sdk-platform --------- Co-authored-by: Peng Liu <[email protected]> GitOrigin-RevId: 54119ce0ea8f24177d0b3e955c6b00b668ee53cc
1 parent f225eb7 commit d3f3604

File tree

2 files changed

+61
-15
lines changed

2 files changed

+61
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Mapbox welcomes participation and contributions from everyone.
88

99
## Features ✨ and improvements 🏁
1010
* [compose] Add declarative animation API to experimental `Marker` composable with two animation triggers: `appearAnimation` and `disappearAnimation`. Each trigger accepts a list of `MarkerAnimationEffect` including `wiggle` (pendulum rotation), `scale`, `fadeIn`, and `fadeOut`. Effects can be customized with parameters (e.g., `scale(from = 0.5f, to = 1.5f)`, `fade(from = 0.5f, to = 1.0f)`) and combined for rich animations. See `MarkersActivity` example for usage.
11+
* Improve FPS statistics logging when `mapView.setOnFpsChangedListener()` is used with separate tracking for frame pacing skips and missed render frames for better performance debugging.
1112

1213
## Bug fixes 🐞
1314
* Fix NPE crash in `PointAnnotationClusterActivity` example when the remote GeoJSON endpoint returns a non-successful HTTP response.

maps-sdk/src/main/java/com/mapbox/maps/renderer/FpsManager.kt

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,23 @@ internal class FpsManager(
2626

2727
private var preRenderTimeNs = -1L
2828
private var choreographerTicks = 0
29+
30+
/**
31+
* VSYNC skipped, either due to taking too long (see [updateFrameStats]) or due to frame pacing
32+
* (see [performPacing])
33+
*/
2934
private var choreographerSkips = 0
3035

36+
/**
37+
* VSYNC skipped due to frame pacing (see [performPacing])
38+
*/
39+
private var choreographerPacingSkips = 0
40+
41+
/**
42+
* Amount of render frames missed (based on map render frame rate) due to map render taking too long
43+
*/
44+
private var missedMapRenderFrames = 0
45+
3146
internal var fpsChangedListener: OnFpsChangedListener? = null
3247

3348
@Suppress("PrivatePropertyName")
@@ -97,18 +112,19 @@ internal class FpsManager(
97112
fun postRender() {
98113
val frameRenderTimeNs = System.nanoTime() - preRenderTimeNs
99114
frameRenderTimeAccumulatedNs += frameRenderTimeNs
100-
// normally we update FPS counter and reset counters once a second
115+
// normally we update FPS counter and reset counters once a second (since screenRefreshRate is FPS)
101116
if (choreographerTicks >= screenRefreshRate) {
102117
calculateFpsAndReset()
103118
} else {
104119
// however to produce correct values we also update FPS after IDLE_TIMEOUT_MS
105120
// otherwise when updating the map after it was IDLE first update will report
106121
// huge delta between new frame and last frame (as we're using dirty rendering)
122+
val delayMillis = VSYNC_COUNT_TILL_IDLE * (screenRefreshPeriodNs / ONE_MILLISECOND_NS)
107123
HandlerCompat.postDelayed(
108124
handler,
109125
onRenderingPausedRunnable,
110126
fpsManagerToken,
111-
VSYNC_COUNT_TILL_IDLE * (screenRefreshPeriodNs / ONE_MILLISECOND_NS)
127+
delayMillis
112128
)
113129
}
114130
preRenderTimeNs = -1L
@@ -123,14 +139,32 @@ internal class FpsManager(
123139
fpsChangedListener = null
124140
}
125141

142+
/**
143+
* This is called on every VSYNC (screenRefreshRate)
144+
*/
126145
private fun updateFrameStats(frameTimeNs: Long) {
127146
preRenderTimeNs = System.nanoTime()
128147
skippedNow = 0
129-
// check if we did miss VSYNC deadline meaning too much work was done in previous doFrame
130-
if (previousFrameTimeNs != -1L && frameTimeNs - previousFrameTimeNs > screenRefreshPeriodNs + ONE_MILLISECOND_NS) {
131-
skippedNow =
132-
((frameTimeNs - previousFrameTimeNs) / (screenRefreshPeriodNs + ONE_MILLISECOND_NS)).toInt()
133-
choreographerSkips += skippedNow
148+
if (previousFrameTimeNs != -1L) {
149+
val frameElapsedTimeNs = frameTimeNs - previousFrameTimeNs
150+
151+
// check if we did miss VSYNC deadline meaning too much work was done in previous doFrame
152+
if (frameElapsedTimeNs > screenRefreshPeriodNs + ONE_MILLISECOND_NS) {
153+
// Figure out how many VSYNC (`screenRefreshPeriod`) we missed due to the map rendering taking too long
154+
skippedNow =
155+
(frameElapsedTimeNs / (screenRefreshPeriodNs + ONE_MILLISECOND_NS)).toInt()
156+
choreographerSkips += skippedNow
157+
}
158+
159+
// Check if we did miss map render frames
160+
// The map render frame time is based on the userToScreenRefreshRateRatio or the screen refresh rate (1.0)
161+
val mapRenderFrameRatio = userToScreenRefreshRateRatio ?: 1.0
162+
val mapRenderTimeNs = screenRefreshPeriodNs / mapRenderFrameRatio
163+
val missedMapRenderFramesNow =
164+
(frameElapsedTimeNs / (mapRenderTimeNs + ONE_MILLISECOND_NS)).toInt()
165+
if (missedMapRenderFramesNow > 0) {
166+
missedMapRenderFrames += missedMapRenderFramesNow
167+
}
134168
}
135169
previousFrameTimeNs = frameTimeNs
136170
// we always increase choreographer tick by one + add number of skipped frames for consistent results
@@ -172,6 +206,8 @@ internal class FpsManager(
172206
* // reset counters
173207
* 1 | 0.4 | false
174208
* 2 | 0.8 | false
209+
*
210+
* @return true if we should render this frame, false otherwise
175211
*/
176212
private fun performPacing(userToScreenRefreshRateRatio: Double): Boolean {
177213
val drawnFrameIndex = (choreographerTicks * userToScreenRefreshRateRatio).toInt()
@@ -186,6 +222,9 @@ internal class FpsManager(
186222
previousDrawnFrameIndex = drawnFrameIndex
187223
return true
188224
}
225+
choreographerPacingSkips++
226+
// We also increase choreographerSkips when performing pacing. Note this is not really a
227+
// missed map render frame since we're doing pacing.
189228
choreographerSkips++
190229
return false
191230
}
@@ -201,26 +240,32 @@ internal class FpsManager(
201240
if (choreographerTicks == choreographerSkips) {
202241
logI(
203242
TAG,
204-
"VSYNC based FPS is $fps, missed $choreographerSkips out of $choreographerTicks VSYNC pulses"
243+
"VSYNC based FPS is $fps, " +
244+
"skipped $choreographerSkips ($choreographerPacingSkips due to pacing) " +
245+
"out of $choreographerTicks VSYNC pulses"
205246
)
206247
} else {
248+
val actualAmountOfFramesRendered = choreographerTicks - choreographerSkips
207249
val averageRenderTimeNs =
208-
frameRenderTimeAccumulatedNs.toDouble() / (choreographerTicks - choreographerSkips)
209-
val fps =
210-
String.format("%.2f", screenRefreshPeriodNs / averageRenderTimeNs * screenRefreshRate)
250+
frameRenderTimeAccumulatedNs.toDouble() / actualAmountOfFramesRendered
251+
val averageFps =
252+
String.format("%.2f", (screenRefreshPeriodNs / averageRenderTimeNs) * screenRefreshRate)
211253
logI(
212254
TAG,
213-
"VSYNC based FPS is $fps," +
214-
" average core rendering time is ${averageRenderTimeNs / ONE_MILLISECOND_NS} ms" +
215-
" (or $fps FPS)," +
216-
" missed $choreographerSkips out of $choreographerTicks VSYNC pulses"
255+
"Average map core rendering time is " +
256+
"${averageRenderTimeNs / ONE_MILLISECOND_NS} ms (or $averageFps FPS), " +
257+
"missed $missedMapRenderFrames map render frames, " +
258+
"skipped $choreographerSkips ($choreographerPacingSkips due to render pacing) " +
259+
"out of $choreographerTicks VSYNC pulses"
217260
)
218261
}
219262
}
220263
previousDrawnFrameIndex = 0
221264
frameRenderTimeAccumulatedNs = 0L
222265
choreographerTicks = 0
223266
choreographerSkips = 0
267+
choreographerPacingSkips = 0
268+
missedMapRenderFrames = 0
224269
}
225270

226271
internal companion object {

0 commit comments

Comments
 (0)