Interview QuestionPractical QuestionFollow-up Questions

Tracing UI Jank on Android

skydovesJaewoong Eum (skydoves)||12 min read

Tracing UI Jank on Android

UI jank occurs when the main thread cannot complete its work within a single frame deadline, causing visible stuttering or dropped frames for the user. On modern Android devices targeting 60, 90, or 120 Hz refresh rates, the budget per frame can be as tight as 8 milliseconds. Diagnosing jank requires understanding the full rendering pipeline from Choreographer callbacks through Compose's three phases to the final surface submission, and knowing which profiling tools reveal which bottleneck.

By the end of this lesson, you will be able to:

  • Explain how the Android rendering pipeline schedules frame work through Choreographer and VSYNC signals.
  • Trace a janky frame in Perfetto by reading the Frames track, doFrame slices, and Compose phase markers.
  • Describe how the CPU Profiler's flame chart isolates expensive method calls on the main thread.
  • Identify unnecessary recompositions using the Layout Inspector and Compose Compiler Metrics.
  • Apply custom trace sections and benchmarking strategies to measure and resolve jank in production code.

The Android Frame Pipeline and Choreographer

Every frame on Android begins with a VSYNC signal delivered by the display subsystem. The Choreographer class receives this signal and dispatches work in a fixed order: input handling, animation callbacks, traversal (measure, layout, draw), and commit. For Compose, AndroidComposeView hooks into this mechanism with its own phase ordering.

The Recomposer synchronizes composition with the Choreographer frame clock via parentFrameClock.withFrameNanos, which suspends until the next VSYNC-aligned callback. Inside that callback, it performs recomposition and applies changes to the node tree. After composition, the View system calls AndroidComposeView.dispatchDraw(), which runs layout via measureAndLayout() followed by drawing via root.draw().

// Inside AndroidComposeView
override fun dispatchDraw(canvas: android.graphics.Canvas) {
    measureAndLayout()
    Snapshot.notifyObjectsInitialized()
    isDrawingContent = true
    canvasHolder.drawInto(canvas) {
        root.draw(canvas = this, graphicsLayer = null)
    }
}

A frame is janky when the total time from VSYNC to buffer submission exceeds the deadline: approximately 16.6 ms at 60 Hz, 11.1 ms at 90 Hz, or 8.3 ms at 120 Hz. The overrun can happen in recomposition, layout, drawing, or unrelated main-thread work that starves the frame callback.

System Tracing with Perfetto

The primary tool for diagnosing jank is system tracing, captured through Android Studio's Profiler or the on-device System Tracing developer option and analyzed in Perfetto UI, which provides a microsecond-level timeline of all thread activity.

The Frames track for the application process is the starting point. Green frames met the deadline, yellow frames were close, and red frames missed it. Below the Frames track, Choreographer doFrame slices on the main thread show the work done per frame. Within a Compose application, the runtime emits trace markers for Recompose, Layout, and Draw nested inside doFrame. The relative durations of these slices immediately narrow the bottleneck to a specific phase.

The RenderThread is also visible in the trace. It handles GPU command submission after the main thread finishes drawing. If the draw phase produces excessive display list operations or large bitmaps, the RenderThread can become a bottleneck even when the main thread finishes on time.

For deeper investigation, you can add custom trace sections using the androidx.tracing library:

import androidx.tracing.trace

@Composable
fun MessageList(messages: List<Message>) {
    trace("MessageList-Composition") {
        LazyColumn {
            items(messages, key = { it.id }) { message ->
                trace("MessageBubble-${message.id}") {
                    MessageBubble(message)
                }
            }
        }
    }
}

These custom sections appear as named slices in Perfetto, letting you correlate specific composable functions with frame timing when the automatic Compose markers are insufficient.

CPU Profiler and Flame Charts

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor