Scroll Jank

Measuring Scroll Jank and Frame Timing

The same Macrobenchmark 234 rule measures frames. FrameTimingMetric reads each frame of the target app from a Perfetto 440,880 trace recorded around the measureBlock and reports two distributions: frameDurationCpuMs, the time the UI thread and RenderThread spent producing the frame, and, on Android 12 and later, frameOverrunMs, how far the frame missed (positive) or beat (negative) its deadline. Watch P90 to P99: the median frame is rarely the one users notice.

benchmark/.../BookNestBenchmarks.kt: a scroll benchmark (excerpt)Kotlin
private fun scroll(mode: CompilationMode) = rule.measureRepeated(
  packageName = PACKAGE,
  metrics = listOf(FrameTimingMetric()),
  compilationMode = mode,
  startupMode = StartupMode.COLD,
  iterations = 5,
  setupBlock = {
    pressHome()
    startActivityAndWait { it.putExtra("stage", "lazy") }
    device.wait(Until.hasObject(By.scrollable(true)), 10_000)
  },
) {
  val x = device.displayWidth / 2                          // only this part is measured
  repeat(3) {
    device.swipe(x, device.displayHeight * 4 / 5, x, device.displayHeight / 5, 10)
    device.waitForIdle()
  }
}

Only the measureBlock is timed; the setupBlock starts the 600-book catalog of Lists with LazyColumn. The first version found the list with device.findObject(By.scrollable(true)) and called fling() on it three times, and failed with StaleObjectException: after the first fling, the accessibility node it held was gone. Re-finding the list each time then returned null under load, so the final version swipes by coordinates.

In production, the JankStats 234 library (androidx.metrics:metrics-performance:1.0.0) reports janky frames from users' devices with your own state labels, and Android vitals shows slow rendering.