Android Performance

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

Word count: 4.6kReading time: 28 min
2025/08/05
loading

This is the eighth article in the Perfetto series, providing an in-depth introduction to the Vsync mechanism in Android and its representation in Perfetto. The article will analyze how the Android system performs frame rendering and composition based on Vsync signals from Perfetto’s perspective, covering core concepts such as Vsync, Vsync-app, Vsync-sf, and VsyncWorkDuration.

With the popularization of high refresh rate screens, understanding the Vsync mechanism has become increasingly important. This article uses 120Hz refresh rate as the main narrative thread to help developers understand the working principles of Vsync in modern Android devices, and how to observe and analyze Vsync-related performance issues in Perfetto.

Note: This article is based on Android 16’s latest architecture and implementation

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. Video (Bilibili) - Android Perfetto Basics and Case Sharing

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.

Welcome to join the WeChat group or community 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.

Before the Vsync mechanism existed, the common problem was Screen Tearing. When the display reads the framebuffer while the GPU writes the next frame, the same refresh will show inconsistent top and bottom parts of the image.

What Problems Does Vsync Solve?

The core idea of the Vsync mechanism is very simple: make all rendering work proceed according to the display’s refresh beat. Specifically:

  1. Synchronization Signal: The display emits a Vsync signal every time it starts a new refresh cycle.
  2. Frame Beat and Production: On the application side, when Vsync arrives, Choreographer drives the production of a frame (Input/Animation/Traversal); after the CPU submits rendering commands, the GPU executes asynchronously in a pipeline. On the SurfaceFlinger side, when Vsync arrives, Buffer composition operations are performed.
  3. Buffering Mechanism: Using double buffering or triple buffering technology ensures the display always reads complete frame data.

This way, frame production and display are aligned with Vsync as the beat. Taking 120Hz as an example, there’s a display opportunity every 8.333ms; the application needs to submit a compositable Buffer to SurfaceFlinger before this window. The key constraint is the timing of queueBuffer/acquire_fence/present_fence; if it doesn’t catch up with this cycle, it will be delayed to the next cycle for display.

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

In the Android system, there isn’t just one simple Vsync signal. In fact, the system maintains multiple Vsync signals for different purposes:

Hardware Vsync (HW Vsync):
This is the lowest-level Vsync signal, generated by the display hardware (HWC, Hardware Composer). Its frequency strictly corresponds to the display’s refresh rate, for example, a 60Hz display generates HW Vsync every 16.67ms, and a 120Hz display generates one every 8.333ms. (Hardware Vsync callbacks are managed by HWC/SurfaceFlinger, see frameworks/native/services/surfaceflinger related implementation)

However, HW Vsync is not always on. Since frequent hardware interrupts consume considerable power, the Android system adopts an intelligent strategy: only enabling HW Vsync when precise synchronization is needed, and using software prediction to generate Vsync signals most of the time.

Vsync-app (Application Vsync):
This is a Vsync signal specifically used to drive application layer rendering. When an application needs to perform UI updates (such as user touch, animation running, interface scrolling, etc.), the application will request to receive Vsync-app signals from the system.

1
2
3
4
5
6
7
8
9
10
// frameworks/base/core/java/android/view/Choreographer.java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
// Request next Vsync signal from system
mDisplayEventReceiver.scheduleVsync();
}
}
}

Vsync-app is requested on-demand. If the application interface is static with no animations or user interaction, the application won’t request Vsync-app signals, and the system won’t generate Vsync events for this application.

Vsync-sf (SurfaceFlinger Vsync):
This is a Vsync signal specifically used to drive SurfaceFlinger for layer composition. SurfaceFlinger is the service in the Android system responsible for compositing all application layers into the final image.

Vsync-appSf (Application-SurfaceFlinger Vsync):
A new signal type introduced in Android 13. To eliminate the timing ambiguity brought by the old design where the sf EventThread both woke up SurfaceFlinger and served some Choreographer clients, the system separated the two responsibilities: vsync-sf focuses on waking up SurfaceFlinger, while vsync-appSf faces clients that need to synchronize with SurfaceFlinger.

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
    Displays application Vsync signal status, with values changing between 0 and 1. Each value change represents a Vsync signal.
    image-20250811221826847

  2. vsync-sf
    Displays SurfaceFlinger Vsync signal status. When there’s no Vsync Offset, it changes synchronously with vsync-app.
    image-20250811221902646

  3. vsync-appSf
    Added in Android 13+, serves special Choreographer clients that need to synchronize with SurfaceFlinger.
    image-20250811222036489

  4. HW_VSYNC
    Displays hardware Vsync enabled status. Value of 1 means enabled, value of 0 means disabled. To save power, hardware Vsync is only enabled when precise synchronization is needed.
    image-20250811222159253

In the Application Process:

FrameDisplayEventReceiver.onVsync Slice Track:
Displays the time point when the application receives Vsync signals. Since signals are passed through Binder, the time may be slightly later than vsync-app in SurfaceFlinger.

image-20250918220632473

UI Thread Slice Track:
Contains Choreographer#doFrame and related Input, Animation, Traversal, etc. Slices. Each doFrame corresponds to one frame’s processing work.

image-20250918220709655

RenderThread Slice Track:
Contains DrawFrame, syncAndDrawFrame, queueBuffer, etc. Slices, corresponding to render thread work.

image-20250918220730872

How Android App Frames Work Based on Vsync

Each frame of an Android application completes the entire process from rendering to display based on the Vsync mechanism, involving multiple key steps.

image-20250918221821265

Process Overview (In Order)

  1. Trigger Redraw/Input: View.invalidate(), animation, data change, or input event triggers → ViewRootImpl.scheduleTraversals()Choreographer.postCallback(TRAVERSAL)
  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: RenderThread executes syncAndDrawFrame/DrawFrame, CPU records GPU commands, queueBuffer submits to BufferQueue
  6. Composition Display: SurfaceFlinger composites (GPU/or HWC) when vsync-sf arrives, generates present_fence, outputs to display
  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. Vsync signals are requested on-demand, only in the following situations will applications request the next Vsync from the system:

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
// 1. UI component requests redraw
// frameworks/base/core/java/android/view/View.java
public void invalidate() {
// Mark as needing redraw, but don't execute immediately
mPrivateFlags |= PFLAG_DIRTY;

if (mParent != null && mAttachInfo != null) {
// Request redraw from parent container
mParent.invalidateChild(this, null);
}
}

// 2. ViewRootImpl schedules traversal
// frameworks/base/core/java/android/view/ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// Key: Register callback with Choreographer, wait for next Vsync
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

// 3. Choreographer requests Vsync
// frameworks/base/core/java/android/view/Choreographer.java
public void postCallback(int callbackType, Runnable action, Object token) {
// Add callback to queue
mCallbackQueues[callbackType].addCallbackLocked(action, token);

if (callbackType == CALLBACK_TRAVERSAL) {
// If it's UI traversal callback, immediately request Vsync
scheduleFrameLocked(now);
}
}

private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
// Key: Request Vsync signal from system
mDisplayEventReceiver.scheduleVsync();
}
}

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) {
super(looper, vsyncSource);
// 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) {
// Received Vsync signal, but note: doFrame is not executed directly here
mTimestampNanos = timestampNanos;
mFrame = frame;

// Key: Post work to main thread's MessageQueue
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true); // Set as asynchronous message, priority processing
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
// This is where the work for one frame actually begins
doFrame(mTimestampNanos, mFrame);
}

Several Remaining Questions

Q1: Why not execute doFrame() directly in onVsync()?
image-20250918221936675

  • Thread boundary: onVsync() may not be on the main thread, UI must execute on the main thread
  • Scheduling control: Precisely align execution moment through sendMessageAtTime()
  • Queue semantics: Enter main thread MessageQueue, ensure coordination with other high-priority tasks

Q2: If Vsync message arrives but main thread is busy, will it be lost?
image-20250918222045731

  • No. Vsync message waits for execution after queuing

Q3: Must CPU/GPU complete within a single Vsync cycle? If any link exceeds 1 vsync, will it cause frame drops?

  • Modern Android systems use multi-buffering (usually triple buffering) mechanism:

    • Application side: Front Buffer (displaying) + Back Buffer (rendering) + possible third Buffer

    • SurfaceFlinger side: Also has similar buffering mechanism

    • This means even if an application’s某frame exceeds the Vsync cycle, it won’t necessarily drop frames immediately.

  • GPU asynchronous pipeline; the key is whether queueBuffer catches up with SF composition window. Multi-buffering can mask single frame delays but may introduce additional latency. As shown in the figure below, both App-side BufferQueue and SurfaceFlinger-side Buffers are sufficient with redundancy, so no frames are dropped.

  • However, if the App hasn’t accumulated Buffers before, frame drops will still occur.

image-20250918222258536

Q5: How do GPU and CPU coordinate?:

  • GPU rendering is asynchronous, bringing additional complexity:

    • CPU works normally, GPU becomes bottleneck: Even if application main thread completes work within Vsync cycle, excessive GPU rendering time will still cause frame drops
    • GPU Fence mechanism: presentFence is the key synchronization mechanism connecting GPU rendering and SurfaceFlinger composition. According to the system’s Latch Unsignaled Buffers strategy, SurfaceFlinger doesn’t always wait for GPU completion, but can “jump the gun” to start composition early, only waiting for fence signal when finally needing to use Buffer content, thus hiding latency.

    image-20250918222626100

Q6: What is the real role of Vsync Phase (phase difference)?:

  • Improve touch responsiveness: By adjusting sf vsync’s phase difference, the time from when an application starts drawing to displaying on screen can be shortened from 3 Vsync cycles to 2 Vsync cycles. This is very important for interaction scenarios like touch response.
  • Solve application drawing timeout issues: When application drawing times out, a reasonable sf phase difference can争取更多的处理时间 for the application, avoiding frame drops due to improper timing.
  • Understanding the system’s Vsync Offset configuration can be achieved by observing the VsyncWorkDuration metric in Perfetto.
  • The time period shown in the figure below is the app offset (13.3ms) configured on my phone

image-20250918222707300

Technical Implementation of Vsync Offset

In the Android system, Vsync Offset is implemented through VsyncConfiguration:

1
2
3
4
5
6
7
8
9
10
11
// frameworks/native/services/surfaceflinger/Scheduler/VsyncConfiguration.cpp
struct VsyncConfiguration {
// Application Vsync offset relative to hardware Vsync
nsecs_t appOffset;
// SurfaceFlinger Vsync offset relative to hardware Vsync
nsecs_t sfOffset;
// Early application Vsync offset (for special cases)
nsecs_t appOffsetEarly;
// Early SurfaceFlinger Vsync offset
nsecs_t sfOffsetEarly;
};

Key Concepts:

  • appOffset: Application Vsync time offset relative to hardware Vsync
  • sfOffset: SurfaceFlinger Vsync time offset relative to hardware Vsync
  • Vsync Offset = sfOffset - appOffset: Time difference between when App and SurfaceFlinger receive signals

Actual Optimization Effects

Taking a 120Hz device as an example, the effect of configuring 3ms Offset:

Without Offset (Traditional Method):

  • T0: Application and SurfaceFlinger receive Vsync simultaneously
  • T0+3ms: Application completes rendering
  • T0+8.333ms: Next Vsync, SurfaceFlinger starts composition
  • T0+16.666ms: User sees the image (total latency 16.666ms)

With Offset (Optimized Method):

  • T0+1ms: Application receives Vsync-app, starts rendering
  • T0+3ms: Application completes rendering, submits Buffer
  • T0+4ms: SurfaceFlinger receives Vsync-sf, immediately starts composition
  • T0+6ms: SurfaceFlinger completes composition
  • T0+8.333ms: User sees the image (total latency 8.333ms)

Through reasonable Offset configuration, latency can be reduced from 16.666ms to 8.333ms, doubling response performance.

Actual Time Budget Allocation:

Taking a 120Hz device as an example (8.333ms cycle):

  • Ideal case: Application 4ms + SurfaceFlinger 2ms + buffer 2.333ms
  • But actually acceptable: Application 6ms + SurfaceFlinger 3ms (if there’s enough Buffer buffering)
  • GPU limitation: On low-end devices, GPU rendering may need 10-15ms, becoming the real bottleneck

Real Causes of Frame Drops:

  1. Application timeout + Buffer exhaustion: Continuous multiple frame timeouts lead to no available Buffers in BufferQueue
  2. GPU rendering timeout: Even if CPU work is normal, GPU rendering timeout will still cause frame drops
  3. SurfaceFlinger timeout: System-level composition timeout affects all applications
  4. System resource competition: CPU/GPU/memory and other resources occupied by other processes

Complete Code Flow of Vsync Signals

The complete chain of Vsync signals from hardware to application layer is as follows.

Native Layer: Vsync Signal Generation and Management

Hardware Vsync Generation

Vsync signals are initially generated by display hardware. At the HWC (Hardware Composer) level, hardware periodically generates Vsync interrupts:

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
// hardware/interfaces/graphics/composer/2.1/utils/hwc2on1adapter/HWC2On1Adapter.cpp
// Note: This is a compatibility adapter from HWC 1.x to 2.x, modern devices typically use native HWC 2.x+
// Based on Android 16 AOSP latest code
void HWC2On1Adapter::vsyncThread() {
while (!mVsyncEnded) {
if (mVsyncEnabled) {
const auto now = std::chrono::steady_clock::now();
const auto vsyncPeriod = std::chrono::nanoseconds(mVsyncPeriod);
const auto nextVsync = mLastVsync + vsyncPeriod;

if (now >= nextVsync) {
// Generate hardware Vsync timestamp
auto timestamp = std::chrono::duration_cast<std::chrono::nanoseconds>(
nextVsync.time_since_epoch()).count();

// Callback to SurfaceFlinger
if (mVsyncCallback) {
mVsyncCallback->onVsync(timestamp, mDisplayId);
}
mLastVsync = nextVsync;
}
}
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}

VsyncController: Core Controller

VsyncController is the brain of the entire Vsync system, receiving hardware Vsync and managing software prediction:

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
// frameworks/native/services/surfaceflinger/Scheduler/VsyncController.h
// Actual implementation usually in VSyncReactor.cpp
// Based on Android 16 AOSP latest code
void VsyncController::onVsync(nsecs_t timestamp, int32_t hwcDisplayId) {
std::lock_guard<std::mutex> lock(mMutex);

// Record hardware Vsync timestamp to tracker
mVsyncTracker->addVsyncTimestamp(timestamp);

// Check if more hardware Vsync samples needed to calibrate prediction model
if (mVsyncTracker->needsMoreSamples()) {
// Model still needs more samples, continue enabling hardware Vsync
ALOGV("VsyncController: Need more samples, keeping HW Vsync enabled");
} else {
// Model is stable, can disable hardware Vsync to save power
ALOGV("VsyncController: Model stable, disabling HW Vsync");
enableHardwareVsync(false);
}

// Notify all waiting clients
for (auto& callback : mCallbacks) {
callback->onVsync(timestamp);
}
}

void VsyncController::enableHardwareVsync(bool enable) {
if (mHwVsyncEnabled != enable) {
mHwVsyncEnabled = enable;
// Control hardware Vsync through HWC interface
mHwc.setVsyncEnabled(mDisplayId, enable);

// This state change can be seen in Perfetto trace
ATRACE_INT("HW_VSYNC", enable ? 1 : 0);
}
}

VsyncDispatch: Signal Distribution Mechanism

VsyncDispatch is responsible for precisely distributing Vsync signals to different consumers, each consumer can have different trigger times:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// frameworks/native/services/surfaceflinger/Scheduler/VSyncDispatch.h
// Actual implementation in VSyncDispatchTimerQueue.cpp
// Based on Android 16 AOSP latest code
VsyncDispatch::CallbackToken VsyncDispatch::registerCallback(
Callback callback, std::string callbackName) {
std::lock_guard<decltype(mMutex)> lock(mMutex);

// Generate unique token for each callback
auto token = CallbackToken(mCallbackMap.size());
mCallbackMap[token] = std::make_unique<CallbackEntry>(
std::move(callback), std::move(callbackName));

return token;
}

nsecs_t VsyncDispatch::schedule(CallbackToken token,
nsecs_t workDuration,
nsecs_t earliestVsync) {
std::lock_guard<decltype(mMutex)> lock(mMutex);

auto it = mCallbackMap.find(token);
if (it == mCallbackMap.end()) {
return kInvalidTime;
}

// Calculate target Vsync time
auto targetVsync = mVsyncTracker->nextVsyncTime(earliestVsync);

// Calculate wakeup time = target Vsync - work duration
auto wakeupTime = targetVsync - workDuration;

// If wakeup time has passed, push to next Vsync
auto now = systemTime(SYSTEM_TIME_MONOTONIC);
if (wakeupTime < now) {
targetVsync = mVsyncTracker->nextVsyncTime(targetVsync + 1);
wakeupTime = targetVsync - workDuration;
}

// Set callback trigger time
it->second->wakeupTime = wakeupTime;
it->second->targetVsync = targetVsync;
it->second->workDuration = workDuration;

// Reschedule timer
reschedule();

return targetVsync;
}

void VsyncDispatch::reschedule() {
// Find earliest callback that needs to trigger
nsecs_t nextWakeup = std::numeric_limits<nsecs_t>::max();
for (const auto& [token, entry] : mCallbackMap) {
if (entry->wakeupTime > 0 && entry->wakeupTime < nextWakeup) {
nextWakeup = entry->wakeupTime;
}
}

if (nextWakeup != std::numeric_limits<nsecs_t>::max()) {
// Set timer to trigger at nextWakeup time
mTimeKeeper->alarmAt(std::bind(&VsyncDispatch::timerCallback, this), nextWakeup);
}
}

void VsyncDispatch::timerCallback() {
std::vector<std::pair<CallbackToken, CallbackEntry*>> firedCallbacks;

{
std::lock_guard<decltype(mMutex)> lock(mMutex);
auto now = systemTime(SYSTEM_TIME_MONOTONIC);

// Find all callbacks that should trigger
for (auto& [token, entry] : mCallbackMap) {
if (entry->wakeupTime > 0 && now >= entry->wakeupTime) {
firedCallbacks.push_back({token, entry.get()});
entry->wakeupTime = 0; // Reset
}
}
}

// Execute callbacks outside lock to avoid deadlock
for (auto& [token, entry] : firedCallbacks) {
entry->callback(entry->targetVsync, entry->wakeupTime);
}
}

EventThread: Connecting Native and Framework

EventThread is the bridge connecting Native layer Vsync system and Framework layer:

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
48
49
50
51
52
53
54
55
// frameworks/native/services/surfaceflinger/Scheduler/EventThread.cpp
EventThread::EventThread(std::unique_ptr<VSyncSource> vsyncSource,
std::unique_ptr<InterceptVSyncCallback> interceptVSyncCallback,
const char* threadName)
: mVSyncSource(std::move(vsyncSource))
, mInterceptVSyncCallback(std::move(interceptVSyncCallback))
, mThreadName(threadName) {

// Register callback with VsyncDispatch
mVSyncSource->setCallback(this);

// Start EventThread thread
mThread = std::thread([this] { threadMain(); });
}

void EventThread::threadMain() {
std::unique_lock<std::mutex> lock(mMutex);

while (mKeepRunning) {
// Wait for Vsync events or connection changes
mCondition.wait(lock, [this] {
return !mPendingEvents.empty() || !mKeepRunning;
});

// Process all pending events
auto pendingEvents = std::move(mPendingEvents);
lock.unlock();

for (const auto& event : pendingEvents) {
// Distribute to all connected clients
for (const auto& connection : mConnections) {
if (connection->vsyncRequest != VSyncRequest::None) {
connection->postEvent(event);
}
}
}

lock.lock();
}
}

void EventThread::onVSyncEvent(nsecs_t timestamp, int32_t displayId) {
std::lock_guard<std::mutex> lock(mMutex);

// Create Vsync event
DisplayEventReceiver::Event event;
event.header.type = DisplayEventReceiver::DISPLAY_EVENT_VSYNC;
event.header.id = displayId;
event.header.timestamp = timestamp;
event.vsync.count = ++mVSyncCount;

// Add to pending events queue
mPendingEvents.push_back(event);
mCondition.notify_all();
}

Framework Layer: Vsync Signal Reception and Processing

DisplayEventReceiver: Bridge Between Java and Native

DisplayEventReceiver is the key component for Framework layer to receive Vsync signals:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// frameworks/base/core/jni/android_view_DisplayEventReceiver.cpp
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
jobject messageQueueObj, jint vsyncSource, jint configChanged) {

// Get Java layer's MessageQueue
sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);

// Create Native layer's DisplayEventReceiver
sp<NativeDisplayEventReceiver> receiver = new NativeDisplayEventReceiver(
env, receiverWeak, messageQueue, vsyncSource, configChanged);

// Register to EventThread
status_t status = receiver->initialize();
if (status) {
ALOGE("Failed to initialize display event receiver, status=%d", status);
return 0;
}

receiver->incStrong(gDisplayEventReceiverClassInfo.clazz);
return reinterpret_cast<jlong>(receiver.get());
}

class NativeDisplayEventReceiver : public DisplayEventReceiver, public MessageHandler {
public:
NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
const sp<MessageQueue>& messageQueue,
jint vsyncSource, jint configChanged)
: DisplayEventReceiver(static_cast<ISurfaceComposer::VsyncSource>(vsyncSource),
static_cast<ISurfaceComposer::ConfigChanged>(configChanged))
, mReceiverWeakGlobal(env->NewGlobalWeakRef(receiverWeak))
, mMessageQueue(messageQueue) {
}

// Callback when Vsync event arrives
virtual void onVsync(nsecs_t timestamp, nsecs_t physicalDisplayId, uint32_t count) override {
ALOGV("NativeDisplayEventReceiver: onVsync timestamp=%lld", (long long)timestamp);

// Save event and post to MessageQueue
mTimestamp = timestamp;
mDisplayId = physicalDisplayId;
mCount = count;

// Send message to Java layer's MessageQueue
mMessageQueue->getLooper()->sendMessage(this, Message(MSG_VSYNC));
}

// MessageHandler implementation, executes in Java layer Looper
virtual void handleMessage(const Message& message) override {
switch (message.what) {
case MSG_VSYNC:
// Call Java layer's onVsync method
dispatchVsync(mTimestamp, mDisplayId, mCount);
break;
}
}

private:
void dispatchVsync(nsecs_t timestamp, nsecs_t displayId, uint32_t count) {
JNIEnv* env = AndroidRuntime::getJNIEnv();

jobject receiverObj = env->NewLocalRef(mReceiverWeakGlobal);
if (receiverObj) {
// Call Java layer DisplayEventReceiver.onVsync()
env->CallVoidMethod(receiverObj, gDisplayEventReceiverClassInfo.onVsync,
ns2ms(timestamp), displayId, count);
env->DeleteLocalRef(receiverObj);
}
}
};

Vsync Processing in Choreographer

In the Java layer, Choreographer’s FrameDisplayEventReceiver is responsible for handling Vsync signals:

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
48
49
50
51
52
53
// frameworks/base/core/java/android/view/Choreographer.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {

private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

// Native layer calls back here
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
// Check for duplicate Vsync (defensive programming)
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be one"
+ " at a time.");
} else {
mHavePendingVsync = true;
}

// Save Vsync information
mTimestampNanos = timestampNanos;
mFrame = frame;

// Key: Post processing work to main thread's MessageQueue
// Note using timestampNanos as execution time here
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true); // Asynchronous message, higher priority
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

// Runnable interface implementation, executes on main thread
@Override
public void run() {
mHavePendingVsync = false;
// Start processing one frame's work
doFrame(mTimestampNanos, mFrame);
}
}

// Choreographer's core logic for processing one frame (simplified excerpt, see AOSP source code for details)
void doFrame(long frameTimeNanos, int frame) {
// ... omit pre-checks and time correction ...
// Execute five types of callbacks in order
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}

Key Timing Point Analysis

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

  1. HWC generates hardware interruptVsyncController::onVsync()
  2. VsyncController processingVsyncDispatch::schedule()
  3. VsyncDispatch distributionEventThread::onVSyncEvent()
  4. EventThread notificationNativeDisplayEventReceiver::onVsync()
  5. Native passes to JavaFrameDisplayEventReceiver::onVsync()
  6. Main thread processingChoreographer::doFrame()

Each link has different responsibilities and optimization points. Understanding the complete process helps analyze Vsync-related performance issues in Perfetto.

FrameTimeline

Both App and SurfaceFlinger have FrameTimeline

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, SfCpuDeadlineMissed, SfGpuDeadlineMissed, 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; use FrameTimeline’s PresentType/JankType as standard
    • Multi-buffering may mask single frame timeout, need to look at consecutive frames and Buffer availability

Impact of Refresh Rate/Display Mode/VRR on Vsync and Offset/Prediction

  • Mode Switching: Refresh rate changes will reconfigure VsyncConfiguration, affecting app/sf Offset and prediction model;
    • Perfetto: Check display mode change events and subsequent vsync interval changes
  • VRR (Variable Refresh Rate): Target cycle is not constant, software prediction relies more on present_fence feedback calibration;
    • Perfetto: Observe vsync interval distribution and present_fence deviation
  • Multi-display/External Display: Different physicalDisplayId vsync sources are independent, note app/sf/appSf selection and alignment;
    • Perfetto: Filter related Counter/Slice by display ID
  1. Vsync Signal and Cycle
    • Whether vsync-app / vsync-sf / vsync-appSf intervals are stable (60/90/120Hz corresponding cycles)
    • Whether there are abnormally dense/sparse Vsyncs (prediction jitter)
      image-20250918223148748
  2. Vsync Phase Difference Configuration
    • Whether VsyncWorkDuration matches device’s expected app/sf Offset
    • Whether app and sf sequence matches “draw first then composite” strategy
      image-20250918222707300
  3. FrameTimeline Interpretation
    • First look at PresentType, then JankType; confirm whether it’s app or SF/GPU side issue
    • 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)
    • Whether RenderThread‘s syncAndDrawFrame/DrawFrame duration is abnormal
      image-20250918223340940
  5. BufferQueue and Fence
    • Producer: After RenderThread queueBuffer, this Buffer is marked as consumable, and can be consumed by sf after one Transaction. Just after queueBuffer, the Buffer still needs GPU to actually render, here Fence is used to track GPU execution time. SF also needs to check this Fence to decide whether this Buffer is Ready (new version SF has a property value configuration, can also not wait for this Fence, wait until actual composition time to check).
      image-20250918224205093
    • Consumer SF and BufferTX: SF as Buffer consumer, will take a Buffer for composition when vsync arrives. Here App’s Buffer exists with the name BufferTX-xxxx. The figure below shows that each time SF takes a Buffer, BufferTX count decreases by 1, which is normal. If BufferTX is 0, it means App didn’t send Buffer up in time, SF also stops composition, which will cause jank (of course if there are multiple output sources simultaneously, SF will take other Buffers, but for the App it’s still jank)
      image-20250918223441117
  6. Composition Strategy and Display
    • Whether SF frequently goes ClientComposition; whether HWC validate/present is abnormal
    • 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)
    • Whether other foreground apps/system services occupy key resources
      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

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: Welcome to join, thanks for support~

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
      7. 4.7. Actual Optimization Effects
    5. 5. Complete Code Flow of Vsync Signals
      1. 5.1. Native Layer: Vsync Signal Generation and Management
      2. 5.2. Framework Layer: Vsync Signal Reception and Processing
      3. 5.3. Key Timing Point Analysis
      4. 5.4. FrameTimeline
      5. 5.5. Impact of Refresh Rate/Display Mode/VRR on Vsync and Offset/Prediction
      6. 5.6. Perfetto Practice Checklist (Recommended Viewing Order)
    6. 6. References
  • About Me && Blog