Perfetto’s vsync-app, vsync-sf, and app doFrame tracks can look like the same event. This article follows a frame request from SurfaceFlinger scheduling to the app, then relates it to RenderThread, buffers, and FrameTimeline.
The examples mainly concern 120 Hz operation. Phase settings, hardware-callback enablement, and pending frame requests affect the tracks; separate those meanings before interpreting latency.
Version note (reviewed October 2026): Java, JNI, and scheduling code are checked against
android-16.0.0_r1; app/appSf roles also reference Android 13 source. Historical screenshots are not universal Android 16/17 configurations. Excerpts omit surrounding state where noted and come from different classes, not one compilable program.
Table of Contents
- Series Catalog
- What is Vsync
- Basic Working Principles of Vsync in Android
- Observing Vsync in Perfetto
- How Android App Frames Work Based on Vsync
- References
- About Me && Blog
Series Catalog
- Android Perfetto Series Catalog
- Android Perfetto Series 1: Introduction to Perfetto
- Android Perfetto Series 2: Capturing Perfetto Traces
- Android Perfetto Series 3: Familiarizing with Perfetto View
- Android Perfetto Series 4: Opening Large Traces via Command Line
- Android Perfetto Series 5: Android App Rendering Flow Based on Choreographer
- Android Perfetto Series 6: Why 120Hz? Advantages and Challenges of High Refresh Rates
- Android Perfetto Series 7: MainThread and RenderThread Deep Dive
- Android Perfetto Series 8: Understanding Vsync Mechanism and Performance Analysis
- Android Perfetto Series 9: CPU Information Interpretation
- Android Perfetto Series 10: Binder Scheduling and Lock Contention
- Android Perfetto Series 11: PerfettoSQL, Trace Processor and Regression Detection
- Android Perfetto Series 12: Trace Dataflow and Data Loss
- Android Perfetto Series 13: Perfetto SDK, Track Event and App Field Traces
- Android Perfetto Series 14: heapprofd and Memory Profiling
- Android Perfetto Series 15: Boot Traces and Long-running Field Tracing
- Android Perfetto Series 16: GPU, Power Counters and Hardware Bottlenecks
- Android Perfetto Series 17: Scenario Automation and Platform Tracing
- Android Perfetto Series 18: Input Response Latency
- Video (Bilibili) - Android Perfetto Basics and Case Sharing
- Video (Bilibili) - Android Perfetto: Trace Graph Types - AOSP, WebView, Flutter + OEM System Optimization
If you haven’t read the Systrace series yet, here are the links:
- Systrace Series Catalog: A systematic introduction to Systrace, Perfetto’s predecessor, and learning about Android performance optimization and Android system operation basics through Systrace.
- Personal Blog: My personal blog, mainly Android-related content, with some life and work-related content as well.
Find ways to get in touch on the About Me page to discuss your questions, what you’d most like to see about Perfetto, and all Android development-related topics with fellow group members.
What is Vsync
Vsync (Vertical Synchronization) is the core mechanism of the Android graphics system. Its existence is to solve a fundamental problem: how to keep the software rendering rhythm synchronized with the hardware display rhythm.
Writing new content into a buffer while scanout reads it can mix frames and cause tearing. Scheduling, buffer ownership, and fences work together; this is not a simple pre-/post-Choreographer divide.
What Problems Does Vsync Solve?
Vsync supplies a shared timing reference, while buffering and fences coordinate access:
- Hardware timing can inform predictions of later display opportunities.
- Choreographer processes due callbacks for requested frames; GPU work may continue asynchronously.
- SurfaceFlinger schedules composition, acquire fences govern safe reads, and presentation feedback helps assess the result.
A fixed 120 Hz mode has an 8.33 ms nominal interval. Pipeline phases and target deadlines determine whether a frame is timely; queueBuffer alone is not proof of presentation.
Basic Working Principles of Vsync in Android
Android system’s Vsync implementation is much more complex than the basic concept, needing to consider multiple different rendering components and their coordinated work.
Layered Architecture of Vsync Signals
Separate hardware callbacks, software scheduling, and trace counters:
Hardware Vsync: Display timing reaches SurfaceFlinger through HWC. Fixed modes commonly use 60/90/120 Hz intervals, but callback rate need not always equal physical refresh cadence. Supported Android 15+ ARR devices can decouple TE/Vsync and refresh rate; see part six.
SurfaceFlinger can enable/disable HWC callbacks and predict subsequent timing in software. Disabling callbacks does not stop panel refresh. A screenshot cannot establish how often the device disables them or how much power it saves.
Vsync-app: Choreographer requests notifications for pending callback work. This is a simplified Android 16 Vsync path; fuller scheduling branches appear later:
1 | // Android 16 Vsync path, timer fallback/logging omitted. |
A static UI can still register FrameCallbacks. Pending work, not just touch state or visible animation, determines requests.
Vsync-sf: Drives SF frame scheduling, with its own work and timing targets; it is not a callback delivered to every app.
Vsync-appSf: Android 13 SurfaceFlinger already creates separate app/appSf connections and initializes SF scheduling separately. Android 16 likewise distinguishes Render and LastComposite client timing. appSf is not another hardware pulse; check clients and budgets against the branch.
Observing Vsync in Perfetto
Perfetto traces contain multiple Vsync-related Tracks. Understanding the meaning of these Tracks helps analyze performance issues.
In the SurfaceFlinger Process:
vsync-app
In this trace, the counter alternates between 0 and 1; its transitions help show the recorded scheduling cadence. It does not prove that any particular app received a callback.
vsync-sf
Displays SurfaceFlinger Vsync signal status. Equal configured phases do not guarantee matching edges when requests, target cycles, or dispatch differ.
vsync-appSf
appSf is separate client timing. The retained crop below only exposes VSYNC-app; it does not establish an appSf track, client, or cadence.
HW_VSYNC
The image has two tracks:HW_VSYNC_<displayId>toggles on hardware callbacks;HW_VSYNC_ON_<displayId>records callback enablement (1/0). Neither proves that an app has processed a frame. See Android 16 HWComposer.
In the Application Process:
FrameDisplayEventReceiver.onVsync Slice Track:
Displays the time point when the application receives Vsync signals. The connection is established through Binder, while events are delivered through BitTube/Looper channels, with scheduling-dependent delivery delay. This image labels it Choreographer#onVsync; missing slices may reflect instrumentation or capture coverage.

UI Thread Slice Track:
Contains Choreographer#doFrame and related Input, Animation, Traversal, etc. Slices. A doFrame processes due work but does not guarantee buffer submission or presentation.

RenderThread Slice Track:
Shows RenderThread work such as DrawFrames, syncFrameState, drawing, and queueBuffer. The initiating syncAndDrawFrame/postAndWait call is normally on UI, not this thread.

How Android App Frames Work Based on Vsync
The diagram follows one View rendering path. Stages can be absent or overlap across frames; match tokens and buffers rather than assuming adjacent slices belong together.

Process Overview (In Order)
- Register due work: View invalidation can schedule TRAVERSAL; animation, input, and FrameCallbacks also have their own paths
- Request Vsync:
Choreographerrequests next Vsync (app phase) throughDisplayEventReceiver.scheduleVsync() - Receive Vsync: After
DisplayEventReceiver.onVsync()receives Vsync, it posts an asynchronous message to the main thread message queue - Main Thread Frame Processing:
Choreographer.doFrame()executes five types of callbacks in order:INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT - Rendering Submission: UI calls syncAndDrawFrame; RenderThread processes DrawFrames and GPU submission, with buffers submitted through BufferQueue/BLAST transactions
- Composition Display: With pending updates,
SurfaceFlingerschedules composition (GPU or HWC) aroundvsync-sf, submits through HWC and obtains presentation-fence feedback - Frame Completion Measurement: Determines whether displayed on schedule through
FrameTimeline(PresentType/JankType) andacquire/present_fence
Below we’ll expand on each step’s key implementation and Perfetto observation points.
When Does App Request Vsync Signals
Applications don’t request Vsync signals all the time. The following are common reasons an app may request the next Vsync:
Scenarios Triggering Vsync Request:
- UI Update Needs: When View calls
invalidate() - Animation Execution: When ValueAnimator, ObjectAnimator, and other animations start
- User Interaction: Touch events, key events, etc. requiring UI response
- Data Changes: RecyclerView data updates, TextView text changes, etc.
Complete Process of App Requesting Vsync
When an application needs to update UI, it requests Vsync signals through the following process:
1 | // Android 16 excerpts from three different classes; surrounding state omitted. |
Due callbacks, visibility, and already-scheduled work affect requests. One invalidate is not necessarily one Vsync; any due callback type may trigger scheduling.
How Main Thread Listens for Vsync Signals
The application main thread listens for Vsync signals through DisplayEventReceiver. This process involves several key steps:
1. Establish Connection:
1 | // frameworks/base/core/java/android/view/Choreographer.java |
2. Receive Vsync Signal:
1 |
|
Several Remaining Questions
Q1: Why not execute doFrame directly in onVsync?

In this Choreographer path, both normally run on the bound Looper thread. Event reception posts an asynchronous Message for later frame processing. sendMessageAtTime establishes eligibility, not exact CPU execution time. Asynchronous messages bypass barriers without preempting the current callback or raising Linux priority.
Q2: Can a busy main thread lose Vsync?

One scheduleVsync request asks for one notification, not every subsequent hardware tick. When multiple events have accumulated, Android 16 DisplayEventDispatcher retains newer Vsync information for that dispatch. Event coalescing, dropped buffers, and late presentation are distinct. This long animation slice includes Running and waiting states, not continuous CPU execution.
Q3: Must CPU/GPU finish in one Vsync interval?
Multiple buffers allow stages of different frames to overlap. Deadlines depend on the target presentation and scheduling budget. App, BLAST, and SF do not each own a universal front/back/third-buffer set. Exceeding one nominal period need not miss the target deadline, but buffer counts alone do not prove an on-time frame.

The “about 14 ms but no dropped frame” annotation requires frame/presentation evidence. The visible thread and counter tracks alone do not establish it.
Q4: How do GPU and CPU coordinate?
CPU submission is asynchronous, though drivers and fences can still block. Acquire fences govern safe reads; unsignaled latching never permits reading unfinished content. Present fences provide presentation feedback, not app drawing completion.

GPU completion here is a CPU-side waiting/reporting thread, not a GPU execution track. Its wait duration is not GPU active time. Timely main-thread completion can still be followed by GPU, composition, or display delay.
Q5: What does Vsync phase affect?
Changing app/SF wakeup plans shifts their work windows and possible presentation opportunities; it cannot replace reducing persistently late work. VsyncWorkDuration-app records workDuration budget, not execution time, combined readyDuration, or a direct appOffset.

This screenshot came from the phone I was using at the time. I originally read the 13 ms 292 μs 500 ns selection as its configured app offset. No VsyncWorkDuration track or configuration dump is shown, and the selection spans different transitions. It does not establish a configured 13.3 ms app offset; correlate the same target frame, mode, and scheduling record.
Technical Implementation of Vsync Offset / WorkDuration
In Android 16, the entry point is the VsyncConfiguration abstraction, which returns a VsyncConfigSet organized by scenario. PhaseOffsets and WorkDuration derive from impl::VsyncConfiguration, which implements scheduler::VsyncConfiguration. The namespaces below distinguish those same-named classes:
1 | // Android 16 declarations: same class name in different namespaces. |
Key concepts: Android 16 VSyncDispatchTimerQueue uses wake = targetVsync - workDuration - readyDuration and ready = targetVsync - readyDuration, with further branches for rescheduling, existing targets, and past timing. Planned wakeup is not guaranteed CPU execution.
For app clients, readyDuration reserves downstream SF time; workDuration budgets client work. The counter is a budget, not measured duration. Compare signed app/SF phases for the same target cycle and mode; an absolute difference loses ordering and cross-cycle information.
Simplified Phase-Timing Example
This idealized example assumes a fixed 120 Hz mode with presentation opportunities every 8.333 ms. Both schedules give the app 3 ms and SF 2 ms, omit other waits, and use the same T0 reference.
Schedule A: app and SF wakeup planned together
- T0: App starts; SF does not yet have this new buffer.
- T0+3 ms: App buffer is ready.
- T0+8.333 ms: Assume SF handles it at the next scheduled opportunity.
- T0+10.333 ms: SF finishes, ready for T0+16.666 ms presentation.
Schedule B: staggered work windows
- T0+1 ms: App starts.
- T0+4 ms: Its same 3 ms of work finishes; buffer is ready.
- T0+4.5 ms: SF starts.
- T0+6.5 ms: Its same 2 ms of work finishes, ready for T0+8.333 ms presentation.
These assumptions allow an earlier display opportunity. No input-sampling or physical scanout/pixel timing is measured, so this does not establish a general halving of input latency. Real pipelines overlap and may span cycles with different deadlines.
For late frames, inspect app/GPU completion against deadlines, buffer ownership/fences, SF/display work, scheduling, and contention. Buffer exhaustion need not result from slow app execution, and SF lateness must be assessed per display/layer rather than attributed to all apps.
Complete Code Flow of Vsync Signals
The excerpts follow requests, predicted scheduling, and delivery. Hardware callbacks can calibrate timing; one is not required immediately before every software notification.
Android 16 Key Code Paths (Condensed Excerpts)
These excerpts use android-16.0.0_r1. Omitted fields, outer loops, and branches are marked. Java, JNI, client-native, and SF-side snippets belong to different classes and are not one compilable file.
1) Choreographer requests the next Vsync (Java)
1 | // android-16.0.0_r1, Choreographer.java |
2) Choreographer receives Vsync (Java)
1 |
|
3) JNI bridge: DisplayEventDispatcher (C++)
1 | // frameworks/base/core/jni/android_view_DisplayEventReceiver.cpp |
4) Native transport: DisplayEventReceiver + BitTube (C++)
1 | // android-16.0.0_r1, DisplayEventReceiver.cpp |
5) SurfaceFlinger scheduling and distribution (C++)
1 | // frameworks/native/services/surfaceflinger/Scheduler/VSyncDispatch.h |
1 | // android-16.0.0_r1, EventThread.cpp |
Key Timing Point Analysis
Through the above code flow, we can see the complete timing chain:
- HWC hardware callbacks or software-predicted timing → SurfaceFlinger Scheduler updates display cadence
- Scheduler computes wakeup window →
VSyncDispatch::schedule(...) - EventThread creates/distributes events →
DisplayEventReceiver::EventviaBitTube - App-side Native receives event →
DisplayEventDispatcher::dispatchVsync(...) - Java
FrameDisplayEventReceivercallback → async message enters Looper queue Choreographer#doFrame(...)executes → Input/Animation/Insets Animation/Traversal/Commit
Each link has different responsibilities and optimization points. Understanding the complete process helps analyze Vsync-related performance issues in Perfetto.
FrameTimeline
Android 12+ supports collection through android.surfaceflinger.frametimeline, subject to data-source and rendering-path coverage. This crop shows the app; correlate its SF DisplayFrame separately. Missing tracks are not proof of no jank.

- Tracks:
Expected Timeline,Actual Timeline - PresentType/JankType:
- PresentType indicates how this frame was presented (e.g., On-time, Late), JankType indicates jank type source
- Common JankType:
AppDeadlineMissed,BufferStuffing,SurfaceFlingerCpuDeadlineMissed,SurfaceFlingerGpuDeadlineMissed, etc.
- Operation Steps (Perfetto UI):
- Select target
Surface/Layerin application process or filter using FrameToken - Align Expected with Actual, view offset and color coding
- Drill up:
Choreographer#doFrame,RenderThread,queueBuffer,acquire/present_fence
- Select target
- Avoiding Misjudgment:
- Judging frame drops solely by
doFrameduration is unreliable; correlate PresentType/JankType/PredictionType and capture completeness; classification is not a code-level root cause - Multi-buffering may mask single frame timeout, need to look at consecutive frames and Buffer availability
- Judging frame drops solely by
App Actual Timeline ends at GPU/posting completion, not final presentation; SF Actual extends to presentation feedback. Continued updates with buffering do not eliminate input delay or backlog. See part seven and the official guide.
Impact of Refresh Rate/Display Mode/VRR on Vsync and Offset/Prediction
- Mode changes: Check active mode, app frame-rate overrides/requests, and scheduling budgets before interpreting intervals. Not every device emits a slice named display mode change.
- ARR versus mode switching: Supported Android 15+ ARR requires hardware/HAL support and can vary refresh cadence within a mode. TE/Vsync can differ from refresh rate; counters do not directly measure app FPS, and not every VRR implementation relies more heavily on present fences.
- Multiple displays: A physicalDisplayId parameter does not prove that every Choreographer can independently request arbitrary external-display timing. Check branch-specific pacesetter/display selection and correlate records that actually expose a display ID.
Perfetto Practice Checklist (Recommended Viewing Order)
- Vsync Signal and Cycle
- Whether intervals match actual modes and requests; separate software/hardware events from enablement state
- Dense/sparse records can reflect on-demand requests, mode changes, screen-off state, or capture loss, not necessarily prediction jitter

- Vsync Phase Difference Configuration
- Check VsyncWorkDuration against configuration when recorded; the pictured selection is not that configuration
- Correlate wakeup, readiness, and presentation for the same target frame; different frames can interleave

- FrameTimeline Interpretation
- First look at
PresentType, thenJankType; locate the late stage, then check prediction validity and thread evidence - Select target Surface/FrameToken to locate specific frame

- First look at
- Application Main Thread and Render Thread
Choreographer#doFrameeach stage duration (Input/Animation/Traversal)- Inspect UI syncAndDrawFrame/postAndWait separately from RT DrawFrames/syncFrameState; a red box does not establish frame identity

- BufferQueue and Fence
- Producer: After queueBuffer, BLAST’s queue/transaction path still needs to deliver the buffer. The image’s “can be consumed” annotation does not prove GPU completion or immediate SF access.
present fenceis mainly for confirming when the frame is actually presented. Unsignaled latch still respects the acquire fence. GPU completion is a CPU waiting thread, not GPU execution itself.
- Consumer SF and BufferTX: When there is a pending update, SF processes the layer under transaction, synchronization, deadline, and fence constraints rather than universally taking the newest buffer. BufferTX at 0 usually means no pending Buffer on that layer. This is normal for static content; investigate app starvation only if the layer was expected to update. It does not mean SF globally stopped compositing.

- Producer: After queueBuffer, BLAST’s queue/transaction path still needs to deliver the buffer. The image’s “can be consumed” annotation does not prove GPU completion or immediate SF access.
- Composition Strategy and Display
- Check composition against layer capabilities and HWC timing; a CPU wait on presentFence does not mean the whole display pipeline is stuck
- Whether there’s obvious prediction deviation during multi-display/mode switching/VRR

- Resources and Other Interference
- CPU competition (big core occupation), GPU busy, IO/memory jitter (GC/compaction)
- Correlate target Runnable waits, frequencies, and stacks; a busy CPU track alone does not identify who delayed whom

References
- Android Graphics Architecture
- VSYNC Implementation Guide
- Frame Pacing
- Perfetto Documentation
- Android Perfetto Series 5: Android App Rendering Flow Based on Choreographer
- Android Perfetto Series 6: Why 120Hz? Advantages and Challenges of High Refresh Rates
- Vsync offset Related Technical Analysis
- Android 13/14 High Version SurfaceFlinger VSYNC-app/VSYNC-appSf/VSYNC-sf Analysis
- AOSP - Choreographer.java (android-16.0.0_r1)
- AOSP - android_view_DisplayEventReceiver.cpp (android-16.0.0_r1)
- AOSP - DisplayEventDispatcher.h (android-16.0.0_r1)
- AOSP - DisplayEventReceiver.cpp (android-16.0.0_r1)
- AOSP - VSyncDispatch.h (android-16.0.0_r1)
- AOSP - EventThread.cpp (android-16.0.0_r1)
- Android Multi-display (official)
- AOSP - VsyncConfiguration.h (android-16.0.0_r1)
- AOSP - DisplayEventDispatcher.cpp (android-16.0.0_r1)
- Unsignaled buffer latch (official)
About Me && Blog
Below is a personal introduction and related links. I look forward to communicating with you all. When three people walk together, one of them can be my teacher!
- Blogger Personal Introduction: Contains personal WeChat and WeChat group links.
- Blog Content Navigation: A navigation of personal blog content.
- Excellent Blog Articles Collected and Organized by Individuals - Must-Know for Android Performance Optimization: Welcome everyone to recommend yourself and recommend (WeChat private chat is fine)
- Android Performance Optimization Knowledge Planet: Historical introduction; the community has closed.
One person can go faster, a group of people can go further
