Render Diagnostics May 18, 2026 • 7 min read

Debugging UI Thread Jank: Choreographer Locks & CADisplayLink Cadence

How to isolate dropped frames, main-thread blocking operations, and high-refresh-rate pacing issues using Perfetto and Xcode Instruments.

Debugging UI Thread Jank: Choreographer Locks & CADisplayLink Cadence

When mobile users interact with scrolling lists, carousels, or gesture transitions, they expect unbroken visual smoothness. On standard 60Hz displays, the rendering pipeline has exactly 16.6 milliseconds to complete layout, measure, draw, and GPU command buffer submission. On modern 120Hz ProMotion and dynamic Android panels, that budget drops to just 8.33 milliseconds.

When a single frame exceeds this deadline, the display hardware re-presents the previous frame, creating a perceptible stutter known as jank.


Android Choreographer & VSYNC Cadence

On Android, the Choreographer coordinates the timing of animations, input handling, and view drawing with the display hardware VSYNC signal. When the main thread is occupied by an expensive task when VSYNC fires, logcat emits the familiar warning:

I/Choreographer: Skipped 14 frames! The application may be doing too much work on its main thread.

Capturing Jank with Perfetto Tracing

To isolate what blocked the thread, we record a system trace with perfetto capturing:

  • sched: CPU scheduler slices and thread states (Running vs Blocked / Sleeping).
  • view: Android view layout, measure, and draw passes.
  • gfx: RenderThread GPU queueing.
# Capture a 10-second Perfetto trace during list scroll:
adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/trace.perfetto-trace <<EOF
buffers: { size_kb: 65536 }
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "sched_switch"
            ftrace_events: "sched_wakeup"
            atrace_categories: "view"
            atrace_categories: "res"
            atrace_categories: "gfx"
        }
    }
}
duration_ms: 10000
EOF

When analyzing the flame graph in the Perfetto UI, inspect the doFrame slice. If traversal takes 25ms, look for:

  1. Excessive View Inflation: Creating view holders during fast flings instead of recycling existing views.
  2. Synchronous JSON Parsing: Deserializing nested API responses directly inside view binding adapters.
  3. Overdraw: Multiple overlapping opaque backgrounds causing the GPU to write pixels multiple times.

On iOS, views backed by CALayer are submitted to the render server process. If your main thread blocks during layoutSubviews() or when rendering custom draw(rect:) Quartz graphics, the render server cannot commit the frame to the display controller on time.

Common Pitfalls in Declarative UI (SwiftUI & Compose)

Modern declarative UI frameworks simplify UI construction but introduce distinct performance traps:

  • Unstable Parameters: Passing unmemoized lambda functions or mutable collections to composable functions in Jetpack Compose, forcing unnecessary recompositions across the entire tree.
  • Heavy View Builders: Performing database queries or array sorting inside SwiftUI var body: some View closures.

Lab Recommendations for Frame-Perfect Pacing

  1. Offload All Heavy Parsing: Move JSON serialization, image compression, and cryptographic calculations to background actor pools.
  2. Pre-Calculate Layout Dimensions: For dynamic text in long feeds, compute text heights asynchronously before items enter the visible viewport.
  3. Automate Frame Timing CI Checks: Run automated scroll journeys with Macrobenchmark on Android and XCTOSSignpostMetric on iOS to block pull requests that introduce frame drops.

For hands-on profiling of your app’s rendering pipeline, explore our UI Jank & Frame Rate Diagnostic Service.

Lead Diagnostic Engineer

Published by Adapter Canvas Point Diagnostic Lab

Our mobile performance engineering team publishes findings derived directly from physical test harness runs and production telemetry triage in Bangkok, Thailand.

Request an engineering review for your app →