This article introduces Choreographer, a class that app developers rarely encounter but is crucial in the Android Framework rendering pipeline. It covers the background of Choreographer‘s introduction, an overview, source code analysis, its relationship with MessageQueue, its use in APM, and optimization ideas from mobile manufacturers.
Choreographer coordinates animations, some input processing, and View updates with display timing. It requests frame callbacks when work is pending rather than unconditionally waking the app on every physical refresh. A 60Hz display, common in 2019, has an interval of about 16.67ms; 90Hz and 120Hz correspond to about 11.11ms and 8.33ms. Modern devices can also change refresh rates. A timely callback does not mean the image has been presented: RenderThread, GPU, composition, and display work remain. Understanding this flow helps explain Message, Handler, Looper, MessageQueue, Measure, Layout, and Draw.
Version note — reviewed 2026-10-13: Historical excerpts use AOSP
android-10.0.0_r1Choreographer as a reference and retain the 2019 screenshots. Updates cover Android 16 and API 24/31/33 observation interfaces. Source excerpts are not standalone compilable apps; vendor implementations and capture configurations are incomplete. Obsolete private interfaces are marked below.
This is the eighth article in the Systrace series, providing a brief introduction to Choreographer within Systrace.
The purpose of this series is to view the overall operation of the Android system from a different perspective using Systrace, while also providing an alternative angle for learning the Framework. Perhaps you’ve read many articles about the Framework but can never remember the code, or you’re unclear about the execution flow. Maybe from Systrace’s graphical perspective, you can gain a deeper understanding.
Series Article Index
- Introduction to Systrace
- Systrace Basics - Prerequisites for Systrace
- Systrace Basics - Why 60 fps?
- Android Systrace Basics - SystemServer Explained
- Systrace Basics - SurfaceFlinger Explained
- Systrace Basics - Input Explained
- Systrace Basics - Vsync Explained
- Systrace Basics - Vsync-App: Detailed Explanation of Choreographer-Based Rendering Mechanism
- Systrace Basics - MainThread and RenderThread Explained
- Systrace Basics - Triple Buffer Explained
- Systrace Basics - CPU Info Explained
- Systrace Smoothness in Action 1: Understanding Jank Principles
- Systrace Smoothness in Action 2: Case Analysis - MIUI Launcher Scroll Jank Analysis
- Systrace Smoothness in Action 3: FAQs During Jank Analysis
- Systrace Responsiveness in Action 1: Understanding Responsiveness Principles
- Systrace Responsiveness in Action 2: Responsiveness Analysis - Using App Startup as an Example
- Systrace Responsiveness in Action 3: Extended Knowledge on Responsiveness
- Systrace Thread CPU State Analysis Tips - Runnable
- Systrace Thread CPU State Analysis Tips - Running
- Systrace Thread CPU State Analysis Tips - Sleep and Uninterruptible Sleep
Essence of the Main Thread Mechanism
This article connects to Perfetto 5: Choreographer-based rendering flow. Looper dispatches Messages; MessageQueue does not itself execute app code. Native polling can also process file-descriptor events while obtaining the next message, including display events. “Every main-thread operation is a Message” is therefore an oversimplification.
In Perfetto, correlate the frame with the main thread, RenderThread, GPU, and SurfaceFlinger. On Android 12 / API 31 or later, with the relevant data captured, FrameTimeline expected/actual tracks help locate missed deadlines. Missing tracks do not mean zero jank. Investigate Sleeping, Runnable, and Running separately; a slice’s wall duration is not CPU time. These historical images show spaced dispatches, and method instrumentation itself affects timing.
MethodTrace Illustration

Systrace Illustration

Evolution
Choreographer arrived in Android 4.1 / API 16 to coordinate work with display timing. Both images below already contain Choreographer.doFrame and RenderThread. They cannot prove behavior before Choreographer existed. They show a different capture with denser rendering and waits; its precise configuration is unavailable.
MethodTrace Illustration


The crop includes RenderThread dequeueBuffer intervals, but does not establish the sole bottleneck or a fixed consumption rate. Buffer availability, consumer progress, and fences require investigation; SurfaceFlinger does not simply consume a “Front Buffer” at an invariant rate.
Consistent presentation matters, but stable 50fps is not universally smoother than fluctuating 40–60fps. On a fixed 60Hz display, 50fps itself cannot be presented uniformly in whole refresh intervals. VSync coordination and buffering improve pacing opportunities; they do not guarantee a stable rate or remove the workload.
Introducing Choreographer
Choreographer requests the next applicable VSync event and schedules callbacks on its Looper thread. It does not set the panel’s refresh rate, and app VSync callbacks are not hardware interrupt handlers. A normal View window with no pending work need not continuously produce new frames.

Read this as a simplified historical diagram. RenderThread is a CPU thread, so labeling its entire box “GPU” is inaccurate. Buffer acquisition, GPU submission, queuing, and composition are not always serialized as drawn. The 16.6ms interval approximates 60Hz, not every stage’s complete budget or end-to-end input latency. See the 90Hz discussion.
Choreographer Overview
Choreographer executes due frame callbacks in stages and requests display events when needed. Android 10 orders them INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT. It does not collect every app Message, nor does every frame necessarily perform full measure/layout/draw.
Choreographer, SurfaceFlinger, and buffering participate in scheduling; they cannot guarantee 20, 60, or 90fps. Monitoring must distinguish public FrameCallback, hidden FrameInfo, Looper dispatch timing, and presentation data. IdleHandler runs under queue-idle conditions, not reliably at the end of every frame.
Diagrams are useful, but I don’t particularly enjoy drawing them. I spend more time looking at Systrace and MethodTrace in my day-to-day work: CPU, SurfaceFlinger, SystemServer, and App activity laid out from left to right. Once you know the code, it becomes easier to connect the trace with what is happening on the phone. So I’ll mainly use Systrace here.
Choreographer Workflow from a Systrace Perspective
This Launcher scroll is a historical example. A green F is an old Systrace frame-analysis marker, not independent proof of successful on-screen presentation. The alternating background represents that trace’s VSync reference; check the axis and configuration instead of assuming 16.6ms from its colors.
On a typical hardware-accelerated View path, the main thread updates drawing state and RenderThread prepares/submits rendering work. Buffer queuing must still be correlated with GPU completion, SurfaceFlinger composition, and presentation. UI and RenderThread work can overlap; adding their wall durations does not give display latency.

Zooming into one historical frame shows Choreographer’s callbacks and the handoff to RenderThread. The old annotations are qualified below:

Choreographer Workflow
- Create the thread’s Choreographer, bind Looper/FrameHandler, and initialize its display receiver and callback queues.
- When due work needs a frame and none is scheduled, request VSync.
onVsyncposts an asynchronous Runnable to that Looper; its laterrun()invokesdoFrame(). doFramechecks whether work remains scheduled, measures its start delay relative to frame time, and adjusts animation time when needed.- Execute due INPUT, ANIMATION, INSETS_ANIMATION, TRAVERSAL, and COMMIT callbacks in order. Traversal may synchronize drawing state with RenderThread.
- RenderThread submits GPU work and a Buffer. Submission returning does not imply GPU completion; fences communicate completion/dependencies for subsequent composition and presentation.
- Schedule another frame only if work such as animation continues.
The old annotation above calls Traversal the third callback and omits Insets Animation; it is the fourth category in Android 10. Its “rendering complete, send Buffer” caption also needs the asynchronous GPU/fence qualification above.
Here is the corresponding MethodTrace (detailed view following):

Now, let’s examine the implementation via source code.
Source Code Analysis
These are Android 10 excerpts, with omitted checks and state maintenance marked in comments. They are not complete classes to assemble verbatim. Android 16 doFrame also receives VsyncEventData and uses its interval, vsync id, and deadline; see the versioned source.
Choreographer Initialization
Singleton Initialization
1 | // Thread local storage for the choreographer. |
Choreographer Constructor
1 | private Choreographer(Looper looper, int vsyncSource) { |
FrameHandler
1 | private final class FrameHandler extends Handler { |
Choreographer Initialization Chain
For a new Activity window, the resume/display path can add the window, create ViewRootImpl, and obtain the current thread’s Choreographer. Animation code may initialize it earlier, and resuming an existing root does not necessarily recreate it. This is an illustrative call chain:
1 | ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) |
FrameDisplayEventReceiver Overview
FrameDisplayEventReceiver handles Vsync registration, requests, and reception. It inherits from DisplayEventReceiver and has three key methods:
onVsync– Vsync signal callbackrun– ExecutesdoFramescheduleVsync– Requests the Vsync signal
1 | private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { |
scheduleVsync() is implemented by the parent DisplayEventReceiver; code accessing its private mReceiverPtr does not belong inside the subclass. FrameHandler’s MSG_DO_FRAME is principally the timer fallback when VSync is disabled; the normal VSync path uses the Runnable above.
Vsync Registration in Choreographer
The FrameDisplayEventReceiver.onVsync inner class receives Vsync callbacks and notifies the UIThread. It achieves this by listening to file descriptors initialized as follows:
android/view/Choreographer.java
1 | private Choreographer(Looper looper, int vsyncSource) { |
android/view/Choreographer.java
1 | public FrameDisplayEventReceiver(Looper looper, int vsyncSource) { |
android/view/DisplayEventReceiver.java
1 | public DisplayEventReceiver(Looper looper, int vsyncSource) { |
Separate control requests from event delivery. DisplayEventReceiver::requestNextVsync() requests the next event through the Binder IDisplayEventConnection; events are read back over BitTube.
DisplayEventDispatcher::initialize() registers the receiving FD with the native Looper. A readable event is dispatched to Java onVsync(), which posts an asynchronous Runnable. onVsync and doFrame are distinct steps, not an app hardware-interrupt handler:

Logic for Processing One Frame
The core logic resides in Choreographer.doFrame. FrameDisplayEventReceiver.onVsync posts itself, and its run method calls doFrame:
android/view/Choreographer.java
1 | public void onVsync(long timestampNanos, long physicalDisplayId, int frame) { |
doFrame performs three main tasks:
- Estimates dispatch-start delay relative to frame time.
- Records frame drawing information.
- Executes
CALLBACK_INPUT,CALLBACK_ANIMATION,CALLBACK_INSETS_ANIMATION,CALLBACK_TRAVERSAL, andCALLBACK_COMMIT.
Dropped Frame Logic
1 | void doFrame(long frameTimeNanos, int frame) { |
The calculation is startNanos - frameTimeNanos: how late this doFrame starts relative to the supplied frame timestamp. Integer division estimates missed scheduling intervals, not this frame’s rendering duration or the full count of missed presentations. The surrounding source also adjusts animation frame time and handles backward time.

The diagram’s start_time should mean the timestamp carried by the event, not when Java happens to receive it. Its divisor must not be hardcoded to 16ms.

Earlier work can delay the next callback, but this is not simply a detector for “the previous frame.” GPU, composition, or presentation misses may produce no such warning. Runnable scheduling delays can contribute without the app continuously running on CPU. The final annotation also reverses the subtraction; the source subtracts the frame timestamp from dispatch-start time.
Recording Frame Drawing Info
Internal android.graphics.FrameInfo records some stage timestamps, and HWUI adds later fields. It is marked @hide, not a stable app SDK class. Stage boundaries are not per-business-callback timings. The following Android 10 indices must not be used blindly for newer output formats.
1 | // Various flags set to provide extra metadata about the current frame |
doFrame records times from Vsync to markPerformTraversalsStart:
1 | void doFrame(long frameTimeNanos, int frame) { |
Executing Callbacks
1 | void doFrame(long frameTimeNanos, int frame) { |
Input Callback Stack
input callback typically executes ViewRootImpl.ConsumeBatchedInputRunnable:
android/view/ViewRootImpl.java
1 | final class ConsumeBatchedInputRunnable implements Runnable { |
This illustrates a batched-input path into DecorView/Activity/View dispatch. Not all input waits for the INPUT callback; keys, unbatched events, and other paths need separate analysis:

Animation Callback Stack
Commonly used via View.postOnAnimation:
1 | public void postOnAnimation(Runnable action) { |
Used for operations like startScroll and Fling:

The actual stack depends on the posted callback. This image shows AnimationHandler and ValueAnimator/AnimatorSet evaluation; it does not by itself establish an AbsListView FlingRunnable:

FrameCallback also uses CALLBACK_ANIMATION:
1 | public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { |
Traversal Stack
1 | void scheduleTraversals() { |
These ViewRootImpl excerpts retain a conditional outline, with local variables supplied by omitted surrounding code. Not every frame performs measure/layout/draw. A barrier does not raise CPU scheduling priority; its queue semantics are explained below.
doTraversal TraceView Example

Requesting the Next Frame’s Vsync
Continuous operations like animations or scrolling need a stable frame output. Further work may request another frame. invalidate and requestLayout can schedule traversal, but state checks and coalescing mean they do not each generate a separate VSync.

ObjectAnimator Animation Logic
android/animation/ObjectAnimator.java
1 | public void start() { |
android/animation/ValueAnimator.java
1 | private void start(boolean playBackwards) { |
android/animation/AnimationHandler.java
1 | public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { |
The provider forwards to Choreographer; the internal path queues due work and schedules a frame, or posts a delayed scheduling message:
1 | public void postFrameCallback(Choreographer.FrameCallback callback) { |
1 | private void postCallbackDelayedInternal(int callbackType, |
AnimationHandler reposts while animation callbacks remain. Public FrameCallback is one-shot; a continuous loop must explicitly repost and be cancelled when no longer needed:

Source Code Summary
getInstance()returns the calling Looper thread’s instance, not a process-wide singleton; a missing Looper causes an exception.- Requests go through the display connection, events through FD/Looper dispatch, and doFrame through an asynchronous Message. None is an app hardware-interrupt handler.
- FrameCallback is one-shot and runs in ANIMATION. One callback does not guarantee one new Buffer.
- Android 10/16 order due work as INPUT, ANIMATION, INSETS_ANIMATION, TRAVERSAL, COMMIT. Earlier updates can affect the later traversal.
- COMMIT runs after traversal, not after GPU/SF completion or presentation. ActivityThread.scheduleTrimMemory() uses it when the main Choreographer exists, but this is one use, not an onTrimMemory call every frame.
- ListView obtain/setup can be driven by input scrolling, continuing animation, or layout. Read the actual stack rather than inferring the cause from a stage name.
APM and Choreographer
These interfaces observe different stages; their numbers are not interchangeable FPS or dropped-frame counts:
- FrameCallback observes callback pacing on its Looper, not Buffer/presentation counts.
gfxinfo ... framestatsexposes window HWUI statistics with version-specific fields.SurfaceFlinger --latencyis a historical diagnostic whose target layer and implementation must match.- The private PageFlip query is historical and unavailable under its old code in Android 16.
- Choreographer skip warnings indicate dispatch-start lateness, not complete presentation behavior.
- Looper/BlockCanary measures message dispatch, not displayed frames.
Android 7 / API 24 provides Window.OnFrameMetricsAvailableListener. Its dropCountSinceLastInvocation counts dropped metric notifications, not dropped display frames. On Android 12 / API 31 and later, where captured, Perfetto FrameTimeline links expected/actual app and SF frames to help investigate CPU, GPU, and composition delays.
Using FrameCallback.doFrame
1 | public interface FrameCallback { |
Use it on a thread with a Looper, normally the main thread for View apps:
1 | Choreographer choreographer = Choreographer.getInstance(); |
postFrameCallback() is public from API 16 and one-shot. A continuous observer must repost and stop appropriately; the observer itself requests more frames, so callback counts are not necessarily app draw counts. frameTimeNanos is the animation frame time, possibly jitter-adjusted, not the instant execution starts.
TinyDancer was the historical callback-based pacing project referenced here. Its displayed rate is not automatically composited screen FPS. API 33 adds postVsyncCallback() with FrameData/FrameTimeline information such as candidate deadlines and expected presentation times. These are predictions, not measured presentation; use the data within the callback’s valid lifetime.
Using FrameInfo
1 | adb shell dumpsys gfxinfo com.meizu.flyme.launcher framestats |
Substitute the actual package. The preserved sample below says Window: StatusBar; it must not be presented as verified output of that Launcher command in the same run.
1 | Window: StatusBar |
Keep and parse the CSV header: Android/HWUI versions change fields. Timestamps use the implementation’s monotonic clock in nanoseconds; Duration columns are durations, not timestamps. The old OldestInputEvent=9223372036854775807 and NewestInputEvent=0 combination indicates no valid input timestamps, not enormous input latency. FrameCompleted is not screen presentation. Slow-frame categories can overlap and must not be added as disjoint bad-frame counts.
Using SurfaceFlinger
These are Android 10-era diagnostic examples. List actual layers and query the relevant one; names change with windows, recreation, and versions:
1 | adb shell dumpsys SurfaceFlinger --list |
In Android 10 SurfaceFlinger::dumpStatsLocked() and FrameTracker::dumpStats(), the first line is the refresh interval, followed by columns:
- A:
desiredPresentTime, desired presentation time. - B:
actualPresentTime, recorded actual presentation time. - C:
frameReadyTime, when contents became ready for presentation, possibly obtained from a ready fence.
These are not app draw start, pre-submission VSync, and hardware acceptance. That implementation uses a 128-entry ring but dumps 127 slots per tracker. Zero and pending INT64_MAX entries are not valid frames. Check the target, time range, validity, monotonicity, gaps, and refresh changes rather than assuming 128 new frames per command.
The old ceil((C - A) / refresh_period) formula and “a change means jank” rule are not universal. Desired, ready, and actual times measure different things; even persistently high latency can leave that value unchanged. Analyze presentation intervals and expected deadlines according to the implementation, preferably correlating FrameTimeline where supported.
Using SurfaceFlinger PageFlip
Historical private interface, not a current app-monitoring recipe. Android 10’s private debug transaction 1013 returned the default display’s PageFlip count. Even with access, that was display-level activity, not one app’s frame count. The old command also omitted shell; correcting it to adb shell service call SurfaceFlinger 1013 does not make it public or remove permission restrictions.
Android 16 source explicitly contains:
1 | case 1013: // Unused. |
The old example that read transact(1013) as a count and calculated app FPS is therefore removed. Use public window metrics or version-appropriate trace data. Never ignore transaction/return status or treat unsupported output as zero frames.
Choreographer Skip Warnings
Android 10 defaults SKIPPED_FRAME_WARNING_LIMIT to 30 via debug.choreographer.skipwarning, read when the class initializes. This is a logging threshold, not proof that smaller delays are smooth or that every frame has been measured.
1 | if (jitterNanos >= mFrameIntervalNanos) { |
Frame time, dispatch start, work completion, and presentation remain separate. Changing the warning threshold does not fix latency.
BlockCanary
Tools such as BlockCanary time the boundaries of Looper message dispatch. This is an excerpt of the older dispatch path:
1 | // Excerpt inside Looper.loop(); obtaining the next message is omitted. |
It measures dispatch wall time including waits, not pure CPU time or the complete RenderThread/GPU/SF chain. It also does not cover every native-poll callback outside dispatch. A backlog of individually short messages can delay work without one long-message warning. Logging, string construction, and sampling have overhead, and setMessageLogging must coexist with other observers. This is not zero-overhead, complete jank detection.
MessageQueue and Choreographer
In Android 10 MessageQueue.next(), a queue-head barrier with target == null makes the queue search forward for an asynchronous message, still respecting its when. Synchronous messages behind the barrier wait for removal. This neither preempts a running message nor raises CPU scheduling priority.
Message.setAsynchronous() is public from API 22, and Handler.createAsync() from API 28. “Asynchronous” means exempt from synchronization barriers, not automatically first, on another thread, or immediate. Without a barrier, normal queue timing/order still applies. The framework’s barrier insertion/removal APIs are not public app SDK contracts.
SyncBarrier Example in Choreographer
This ViewRootImpl outline omits other state management. scheduleTraversals() inserts the barrier and registers traversal work. An asynchronous VSync message can bypass the barrier and let doFrame run that callback:
1 | void scheduleTraversals() { |
doTraversal() removes the barrier before performTraversals(), not after GPU completion:
1 | void doTraversal() { |
Asynchronous eligibility cannot bypass a long task already executing. Private barriers or arbitrary message reordering are not general app jank fixes.
Manufacturer Optimizations
Working on the system side gives us room to change source code and try scheduling policies. I can’t share the proprietary implementation here, but I can outline the ideas. If you’re interested, we can discuss them privately. The examples below do not include controlled results.
Input-event Optimization
AOSP already has CALLBACK_INPUT and batched-input consumption. The historical idea concerns changing input scheduling or responding earlier, not adding input support to a Choreographer that has none. Earlier work must be evaluated alongside batching, resampling, power, and presentation pacing. It is not automatically better.
Background Animation Optimization
An app’s self-reposting callbacks or animations can continue creating work when no longer useful. Decisions to stop them should account for lifecycle, window visibility, multi-window operation, and the actual feature; not every background callback is meaningless.

I recorded this historical com.netease.newsreader.activity capture as a background case. It shows dense frame activity but no independent visibility or power evidence, and makes no claim about the current app. Apps should cancel unneeded animations/callbacks; platform policies must also preserve legitimate visible-window and background behavior.
Frame Scheduling Optimization
“Compose without waiting for VSync” was an OEM exploration direction, not a public app switch. Scheduling still depends on buffers, fences, display hardware, and refresh constraints. Immediate scheduling can increase power or disrupt pacing without reducing end-to-end latency. No implementation or comparison is provided, so this is not a verified optimization result.
App Startup Optimization
First locate long work, locks, and queues on the launch critical path. System-level message reordering must preserve lifecycle and dependencies; moving arbitrary “important” messages to the front risks incorrect order and starvation. Apps should reduce critical-path work, defer unnecessary initialization, and measure first-frame and interactive readiness separately.
High-refresh-rate Optimization
60/90/120Hz correspond to about 16.67/11.11/8.33ms; the original 8.8ms for 120Hz was incorrect. Refresh intervals are not a full independent budget for every thread. Evaluate app/game load, rate transitions, thermals, power, and pacing together rather than requiring every app to render at maximum rate continuously.
References
Versioned primary sources are linked in the relevant sections. These are the original background references; they do not establish current platform behavior:
- Historical VSync explanation
- Gityuan’s Choreographer article
- Choreographer API reference
- Historical Jishuwen reference
- Historical Juejin entry
- Android development course
Article Discussion
Original Chinese discussion pages: Zhihu and Juejin.
About Me && Blog
I welcome discussion with fellow developers:
- About me: personal introduction and contact information.
- Blog navigation.
- Android performance resources: recommendations are welcome.
- Android performance community: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.
One person can go faster; a group can go further.
