DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on • Originally published at mvpfactory.io

Diagnosing Android Jank with FrameTimeline API

---
title: "Diagnose Android Jank with FrameTimeline API"
published: true
description: "Use Android 12+ FrameTimeline API to pinpoint exact dropped frames in Jetpack Compose  from Perfetto traces to automated CI regression detection with Macrobenchmark."
tags: android, mobile, performance, architecture
canonical_url: https://blog.mvpfactory.co/diagnose-android-jank-with-frametimeline-api
---

## What We Will Build

By the end of this tutorial, you will have a repeatable workflow for diagnosing jank in Jetpack Compose apps using Android 12's FrameTimeline API. You will capture a Perfetto trace, trace a dropped frame back to the exact recomposition or layout pass that broke the 16.6ms budget, and set up a Macrobenchmark test that catches regressions in CI before they ship.

Let me show you a pattern I use in every project — one that turns hours of guessing into a five-minute lookup.

## Prerequisites

- Android 12+ device or emulator (API 31+)
- Android Studio with Perfetto UI access
- A Jetpack Compose project with scrollable content
- Macrobenchmark module configured in your project

## Step 1: Understand How FrameTimeline Connects the Pipeline

FrameTimeline assigns each frame a unique **token** that flows through the entire rendering pipeline:

Enter fullscreen mode Exit fullscreen mode

Choreographer.doFrame() → Compose recomposition → HWUI RenderThread → SurfaceFlinger


For every frame, SurfaceFlinger records `expectedPresentTime`, `actualPresentTime`, `jankType`, and `frameDuration`. When `actualPresentTime` exceeds `expectedPresentTime`, you missed the deadline. The `jankType` field tells you whether your app or the compositor caused it — `AppDeadlineMissed`, `SurfaceFlinger`, `PredictionError`, or `None`.

Here is the minimal setup to get this working.

## Step 2: Capture a Perfetto Trace with FrameTimeline

Enter fullscreen mode Exit fullscreen mode


bash
adb shell perfetto -o /data/misc/perfetto-traces/trace.pb -t 10s \
-c - <<EOF
buffers: { size_kb: 65536 }
data_sources: { config { name: "android.surfaceflinger.frametimeline" } }
data_sources: { config { name: "android.gpu.memory" } }
EOF


Open the trace in Perfetto UI. Look for the `Expected Timeline` vs `Actual Timeline` lanes — they render side-by-side per surface. Red slices are deadline misses.

## Step 3: Trace the Frame Token Back to Its Source

Click a red frame slice. The detail panel shows the frame token, jank classification, and duration breakdown. If `jankType = AppDeadlineMissed`, the bottleneck is in your app process.

Now cross-reference the frame token with Choreographer slices. In the app process tracks, find the matching `Choreographer#doFrame` with the same VSYNC ID. Expand its children — you will see `compose:recomposition`, `measure`, `layout`, and `draw` phases with individual durations.

This is usually where you get your answer. A recomposition triggered by a `derivedStateOf` recalculation or an unbounded `LazyColumn` item composition shows up as an abnormally long child slice.

## Step 4: Check HWUI RenderThread Contention

Even when the main thread finishes under budget, the HWUI `RenderThread` can miss the SurfaceFlinger deadline on its own. Watch for these signatures:

- **Long `syncFrameState` slice** — large display list sync adding 2-8ms
- **`Upload` slices on RenderThread** — GPU texture uploads costing 3-12ms for large bitmaps
- **`compile` slices** — shader compilation hitting 20-80ms on first occurrence
- **`Lock contention` markers** — main thread contention causing 1-5ms stalls

The docs do not mention this, but shader compilation jank during first-launch animations is a particular headache in Compose. I have seen 60ms+ hits from a single shader compile on mid-range devices.

## Step 5: Automate Regression Detection in CI

Enter fullscreen mode Exit fullscreen mode


kotlin
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

@test
fun scrollJankMetrics() {
benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
setupBlock = { pressHome(); startActivityAndWait() }
) {
val list = device.findObject(By.res("item_list"))
list.setGestureMargin(device.displayWidth / 5)
list.fling(Direction.DOWN)
}
}


`FrameTimingMetric` reports `frameDurationCpuMs` and `frameOverrunMs` at P50, P90, P95, and P99. Set your CI gates around these thresholds:

| Metric | Green | Yellow | Red |
|---|---|---|---|
| `frameOverrunMs` P50 | < 0ms | 0-5ms | > 5ms |
| `frameOverrunMs` P99 | < 8ms | 8-16ms | > 16ms |
| `frameDurationCpuMs` P90 | < 12ms | 12-16ms | > 16ms |

Store results in a time-series database and alert on week-over-week P99 regressions exceeding 3ms.

## Gotchas

- **The number one mistake teams make is assuming jank is a GPU problem.** In practice, most frame drops in Compose-based UIs originate on the main thread or during recomposition, not during draw.
- **Do not measure averages.** Averages hide the worst frames, and the worst frames are what users actually feel. Gate CI on `frameOverrunMs` P99, not average frame duration.
- **A clean main thread does not guarantee smooth frames.** Profile the RenderThread separately — shader compilation, texture uploads, and display list sync are independent sources of deadline misses that bite hardest on first launch.
- **Before Android 12, connecting a specific Choreographer callback to a SurfaceFlinger deadline miss meant manually aligning timelines.** FrameTimeline tokens eliminate that pain entirely.

## Conclusion

Here is the gotcha that will save you hours: stop guessing which frame dropped and why. Open Perfetto, find the red `Actual Timeline` slices, trace the frame token back to the exact `Choreographer#doFrame` callback and its child recomposition phases. Then put `FrameTimingMetric` in your CI pipeline with hard P99 thresholds so regressions never ship silently.

FrameTimeline tokens, not averages. Percentiles, not gut feelings. That is how you ship smooth Compose UIs.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)