CN

Android Perfetto Series 8: Understanding Vsync Mechanism and Performance Anal...

Word count: 4.1kReading time: 25 min
2025/08/05
loading

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

  1. Android Perfetto Series Catalog
  2. Android Perfetto Series 1: Introduction to Perfetto
  3. Android Perfetto Series 2: Capturing Perfetto Traces
  4. Android Perfetto Series 3: Familiarizing with Perfetto View
  5. Android Perfetto Series 4: Opening Large Traces via Command Line
  6. Android Perfetto Series 5: Android App Rendering Flow Based on Choreographer
  7. Android Perfetto Series 6: Why 120Hz? Advantages and Challenges of High Refresh Rates
  8. Android Perfetto Series 7: MainThread and RenderThread Deep Dive
  9. Android Perfetto Series 8: Understanding Vsync Mechanism and Performance Analysis
  10. Android Perfetto Series 9: CPU Information Interpretation
  11. Android Perfetto Series 10: Binder Scheduling and Lock Contention
  12. Android Perfetto Series 11: PerfettoSQL, Trace Processor and Regression Detection
  13. Android Perfetto Series 12: Trace Dataflow and Data Loss
  14. Android Perfetto Series 13: Perfetto SDK, Track Event and App Field Traces
  15. Android Perfetto Series 14: heapprofd and Memory Profiling
  16. Android Perfetto Series 15: Boot Traces and Long-running Field Tracing
  17. Android Perfetto Series 16: GPU, Power Counters and Hardware Bottlenecks
  18. Android Perfetto Series 17: Scenario Automation and Platform Tracing
  19. Android Perfetto Series 18: Input Response Latency
  20. Video (Bilibili) - Android Perfetto Basics and Case Sharing
  21. 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:

  1. Systrace Series Catalog: A systematic introduction to Systrace, Perfetto’s predecessor, and learning about Android performance optimization and Android system operation basics through Systrace.
  2. 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:

  1. Hardware timing can inform predictions of later display opportunities.
  2. Choreographer processes due callbacks for requested frames; GPU work may continue asynchronously.
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Android 16 Vsync path, timer fallback/logging omitted.
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
// Non-Vsync timer fallback omitted here; shown later.
}
}

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:

  1. 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.
    image-20250811221826847

  2. vsync-sf
    Displays SurfaceFlinger Vsync signal status. Equal configured phases do not guarantee matching edges when requests, target cycles, or dispatch differ.
    image-20250811221902646

  3. 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.
    image-20250811222036489

  4. 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.
    image-20250811222159253

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.

image-20250918220632473

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.

image-20250918220709655

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.

image-20250918220730872

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.

image-20250918221821265

Process Overview (In Order)

  1. Register due work: View invalidation can schedule TRAVERSAL; animation, input, and FrameCallbacks also have their own paths
  2. Request Vsync: Choreographer requests next Vsync (app phase) through DisplayEventReceiver.scheduleVsync()
  3. Receive Vsync: After DisplayEventReceiver.onVsync() receives Vsync, it posts an asynchronous message to the main thread message queue
  4. Main Thread Frame Processing: Choreographer.doFrame() executes five types of callbacks in order: INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT
  5. Rendering Submission: UI calls syncAndDrawFrame; RenderThread processes DrawFrames and GPU submission, with buffers submitted through BufferQueue/BLAST transactions
  6. Composition Display: With pending updates, SurfaceFlinger schedules composition (GPU or HWC) around vsync-sf, submits through HWC and obtains presentation-fence feedback
  7. Frame Completion Measurement: Determines whether displayed on schedule through FrameTimeline (PresentType/JankType) and acquire/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:

  1. UI Update Needs: When View calls invalidate()
  2. Animation Execution: When ValueAnimator, ObjectAnimator, and other animations start
  3. User Interaction: Touch events, key events, etc. requiring UI response
  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Android 16 excerpts from three different classes; surrounding state omitted.
// View.java
public void invalidate() {
invalidate(true);
}

public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

// invalidateInternal propagates damage under further conditions.
// ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

// Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}

synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

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
2
3
4
5
6
7
8
9
// frameworks/base/core/java/android/view/Choreographer.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {

public FrameDisplayEventReceiver(Looper looper, int vsyncSource, long layerHandle) {
super(looper, vsyncSource, /* eventRegistration */ 0, layerHandle);
// Establish connection with SurfaceFlinger during construction
}
}

2. Receive Vsync Signal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
// Timestamp checks, pending-state updates and tracing omitted.
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData.copyFrom(vsyncEventData);
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}

Several Remaining Questions

Q1: Why not execute doFrame directly in onVsync?

image-20250918221936675

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?

image-20250918222045731

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.

image-20250918222258536

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.

image-20250918222626100

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.

image-20250918222707300

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Android 16 declarations: same class name in different namespaces.
namespace android::scheduler {
class VsyncConfiguration {
public:
virtual ~VsyncConfiguration() = default;
virtual VsyncConfigSet getCurrentConfigs() const = 0;
virtual VsyncConfigSet getConfigsForRefreshRate(Fps fps) const = 0;
virtual void reset() = 0;
virtual void setRefreshRateFps(Fps fps) = 0;
virtual void dump(std::string& result) const = 0;
};
namespace impl {
class VsyncConfiguration : public scheduler::VsyncConfiguration {
// Shared implementation, cache and virtual overrides omitted.
};
class WorkDuration : public VsyncConfiguration {
public:
explicit WorkDuration(Fps currentRefreshRate);
// Other constructors and constructOffsets implementation omitted.
};
} // namespace impl
} // namespace android::scheduler

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// android-16.0.0_r1, Choreographer.java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}

// If running on the Looper thread, then schedule the vsync immediately,
// otherwise post a message to schedule the vsync from the UI thread
// as soon as possible.
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

private void scheduleVsyncLocked() {
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#scheduleVsyncLocked");
mDisplayEventReceiver.scheduleVsync();
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

2) Choreographer receives Vsync (Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
// Timestamp checks, pending-state updates and tracing omitted.
mTimestampNanos = timestampNanos;
mFrame = frame;
mLastVsyncEventData.copyFrom(vsyncEventData);
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}

3) JNI bridge: DisplayEventDispatcher (C++)

1
2
3
4
5
6
7
8
9
10
11
12
// frameworks/base/core/jni/android_view_DisplayEventReceiver.cpp
class NativeDisplayEventReceiver : public DisplayEventDispatcher {
public:
NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, jobject vsyncEventDataWeak,
const sp<MessageQueue>& messageQueue, jint vsyncSource,
jint eventRegistration, jlong layerHandle);

private:
void dispatchVsync(nsecs_t timestamp, PhysicalDisplayId displayId, uint32_t count,
VsyncEventData vsyncEventData) override;
void dispatchHotplug(nsecs_t timestamp, PhysicalDisplayId displayId, bool connected) override;
};

4) Native transport: DisplayEventReceiver + BitTube (C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// android-16.0.0_r1, DisplayEventReceiver.cpp
DisplayEventReceiver::DisplayEventReceiver(gui::ISurfaceComposer::VsyncSource vsyncSource,
EventRegistrationFlags eventRegistration,
const sp<IBinder>& layerHandle) {
sp<gui::ISurfaceComposer> sf(ComposerServiceAIDL::getComposerService());
if (sf != nullptr) {
mEventConnection = nullptr;
binder::Status status =
sf->createDisplayEventConnection(vsyncSource,
static_cast<
gui::ISurfaceComposer::EventRegistration>(
eventRegistration.get()),
layerHandle, &mEventConnection);
if (status.isOk() && mEventConnection != nullptr) {
mDataChannel = std::make_unique<gui::BitTube>();
status = mEventConnection->stealReceiveChannel(mDataChannel.get());
if (!status.isOk()) {
ALOGE("stealReceiveChannel failed: %s", status.toString8().c_str());
mInitError = std::make_optional<status_t>(status.transactionError());
mDataChannel.reset();
mEventConnection.clear();
}
} else {
ALOGE("DisplayEventConnection creation failed: status=%s", status.toString8().c_str());
}
}
}

status_t DisplayEventReceiver::requestNextVsync() {
if (mEventConnection != nullptr) {
mEventConnection->requestNextVsync();
return NO_ERROR;
}
return mInitError.has_value() ? mInitError.value() : NO_INIT;
}

ssize_t DisplayEventReceiver::getEvents(DisplayEventReceiver::Event* events,
size_t count) {
return DisplayEventReceiver::getEvents(mDataChannel.get(), events, count);
}

ssize_t DisplayEventReceiver::getEvents(gui::BitTube* dataChannel,
Event* events, size_t count)
{
return gui::BitTube::recvObjects(dataChannel, events, count);
}

5) SurfaceFlinger scheduling and distribution (C++)

1
2
3
// frameworks/native/services/surfaceflinger/Scheduler/VSyncDispatch.h
virtual std::optional<ScheduleResult> schedule(
CallbackToken token, ScheduleTiming scheduleTiming) = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// android-16.0.0_r1, EventThread.cpp
void EventThread::onVsync(nsecs_t vsyncTime, nsecs_t wakeupTime, nsecs_t readyTime) {
std::lock_guard<std::mutex> lock(mMutex);
mLastVsyncCallbackTime = TimePoint::fromNs(vsyncTime);

LOG_FATAL_IF(!mVSyncState);
mVsyncTracer = (mVsyncTracer + 1) % 2;
mPendingEvents.push_back(makeVSync(mVsyncSchedule->getPhysicalDisplayId(), wakeupTime,
++mVSyncState->count, vsyncTime, readyTime));
mCondition.notify_all();
}

// Selected scheduling branch; surrounding loop and state updates omitted.
void EventThread::threadMain(std::unique_lock<std::mutex>& lock) {
// Consumer requests and state are evaluated before this branch.
if (mState == State::VSync) {
const auto scheduleResult = mVsyncRegistration.schedule(
{.workDuration = mWorkDuration.get().count(),
.readyDuration = mReadyDuration.count(),
.lastVsync = mLastVsyncCallbackTime.ns(),
.committedVsyncOpt = mLastCommittedVsyncTime.ns()});
LOG_ALWAYS_FATAL_IF(!scheduleResult, "Error scheduling callback");
} else {
mVsyncRegistration.cancel();
}
// Event delivery, waits and loop continuation omitted.
}

Key Timing Point Analysis

Through the above code flow, we can see the complete timing chain:

  1. HWC hardware callbacks or software-predicted timing → SurfaceFlinger Scheduler updates display cadence
  2. Scheduler computes wakeup window → VSyncDispatch::schedule(...)
  3. EventThread creates/distributes events → DisplayEventReceiver::Event via BitTube
  4. App-side Native receives event → DisplayEventDispatcher::dispatchVsync(...)
  5. Java FrameDisplayEventReceiver callback → async message enters Looper queue
  6. 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.

image-20250918223035361

  • 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):
    1. Select target Surface/Layer in application process or filter using FrameToken
    2. Align Expected with Actual, view offset and color coding
    3. Drill up: Choreographer#doFrame, RenderThread, queueBuffer, acquire/present_fence
  • Avoiding Misjudgment:
    • Judging frame drops solely by doFrame duration 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

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.
  1. 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
      image-20250918223148748
  2. 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
      image-20250918222707300
  3. FrameTimeline Interpretation
    • First look at PresentType, then JankType; locate the late stage, then check prediction validity and thread evidence
    • Select target Surface/FrameToken to locate specific frame
      image-20250918223220718
  4. Application Main Thread and Render Thread
    • Choreographer#doFrame each stage duration (Input/Animation/Traversal)
    • Inspect UI syncAndDrawFrame/postAndWait separately from RT DrawFrames/syncFrameState; a red box does not establish frame identity
      image-20250918223340940
  5. 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 fence is 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.
      image-20250918224205093
    • 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.
      image-20250918223441117
  6. 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
      image-20250918223517315
  7. 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
      image-20250918223532482

References

  1. Android Graphics Architecture
  2. VSYNC Implementation Guide
  3. Frame Pacing
  4. Perfetto Documentation
  5. Android Perfetto Series 5: Android App Rendering Flow Based on Choreographer
  6. Android Perfetto Series 6: Why 120Hz? Advantages and Challenges of High Refresh Rates
  7. Vsync offset Related Technical Analysis
  8. Android 13/14 High Version SurfaceFlinger VSYNC-app/VSYNC-appSf/VSYNC-sf Analysis
  9. AOSP - Choreographer.java (android-16.0.0_r1)
  10. AOSP - android_view_DisplayEventReceiver.cpp (android-16.0.0_r1)
  11. AOSP - DisplayEventDispatcher.h (android-16.0.0_r1)
  12. AOSP - DisplayEventReceiver.cpp (android-16.0.0_r1)
  13. AOSP - VSyncDispatch.h (android-16.0.0_r1)
  14. AOSP - EventThread.cpp (android-16.0.0_r1)
  15. Android Multi-display (official)
  16. AOSP - VsyncConfiguration.h (android-16.0.0_r1)
  17. AOSP - DisplayEventDispatcher.cpp (android-16.0.0_r1)
  18. 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!

  1. Blogger Personal Introduction: Contains personal WeChat and WeChat group links.
  2. Blog Content Navigation: A navigation of personal blog content.
  3. 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)
  4. Android Performance Optimization Knowledge Planet: Historical introduction; the community has closed.

One person can go faster, a group of people can go further

WeChat QR Code

CATALOG
  1. 1. Table of Contents
  • Series Catalog
    1. 1. What is Vsync
      1. 1.1. What Problems Does Vsync Solve?
    2. 2. Basic Working Principles of Vsync in Android
      1. 2.1. Layered Architecture of Vsync Signals
    3. 3. Observing Vsync in Perfetto
    4. 4. How Android App Frames Work Based on Vsync
      1. 4.1. Process Overview (In Order)
      2. 4.2. When Does App Request Vsync Signals
      3. 4.3. Complete Process of App Requesting Vsync
      4. 4.4. How Main Thread Listens for Vsync Signals
      5. 4.5. Several Remaining Questions
      6. 4.6. Technical Implementation of Vsync Offset / WorkDuration
      7. 4.7. Simplified Phase-Timing Example
    5. 5. Complete Code Flow of Vsync Signals
      1. 5.1. Android 16 Key Code Paths (Condensed Excerpts)
      2. 5.2. Key Timing Point Analysis
      3. 5.3. FrameTimeline
      4. 5.4. Impact of Refresh Rate/Display Mode/VRR on Vsync and Offset/Prediction
      5. 5.5. Perfetto Practice Checklist (Recommended Viewing Order)
    6. 6. References
  • About Me && Blog