During a scrolling slowdown, the main thread may be doing layout or may already have handed drawing work to RenderThread. This article follows their hand-off, buffer submission, and SurfaceFlinger activity in Perfetto screenshots to locate waits.
The focus is the hardware-accelerated View/HWUI path. Other engines and software drawing can use different threads. FrameTimeline and presentation-side evidence are needed to determine what reached the screen.
Table of Contents
- Series Catalog
- Rendering Flow Analysis Based on Perfetto
- Evolution of Dual-Thread Rendering Architecture
- Main Thread Creation Process
- ActivityThread Functionality
- RenderThread Creation and Development
- Performance
- Perfetto’s Unique FrameTimeline Feature
- Vsync Signals in Perfetto
- References
- Attachments
- 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.
I uploaded the trace used in this article to GitHub: https://github.com/Gracker/SystraceForBlog/blob/master/Android_Perfetto/demo_app_aosp_scroll.perfetto-trace
Feel free to download it.
Version note (reviewed October 2026): Java/HWUI excerpts are checked against
android-16.0.0_r1, with BLAST counters explained usingandroid-15.0.0_r1. Android 13 unsignaled latching and Android 17 MessageQueue changes are labeled separately. The attached trace’s SystemInfo reports OPPO PKH110, Android 15/API 35, and Perfetto v46.0; aosp in its filename does not mean stock AOSP firmware. Most newer images show the demo app, but a com.tencent.mm detail image and an older Systrace example come from other captures.
Rendering Flow Analysis Based on Perfetto
Using a scrolling list as an example, we’ll inspect main-thread and RenderThread activity near one frame in Perfetto. The phases vary by frame and rendering path, so check frame tokens before treating nearby slices as one frame.
Frame Concept and Basic Parameters
Before analyzing Perfetto traces, distinguish the display’s nominal interval in a fixed mode from the number of distinct app frames actually presented:
- 60Hz mode: Nominal refresh interval about 16.67ms
- 90Hz mode: Nominal refresh interval about 11.11ms
- 120Hz mode: Nominal refresh interval about 8.33ms
The display can repeat the preceding frame; none of these modes proves that the app produced that many new frames per second.
When analyzing rendering performance in Perfetto, focus on these two threads:
- UI Thread: The application main thread, handling user input, business logic, and layout calculations
- RenderThread: The rendering thread, preparing and submitting GPU commands and interacting with the buffer submission path; the GPU executes those commands
Main Thread and RenderThread Workflow

The screenshot shows overlapping work for frames such as 82272289 and 82272296. Match doFrame/DrawFrames tokens before treating neighboring slices as the same frame. The arrows are a guide to dependencies, not a complete function trace.
Important Note: Not every frame executes all steps. Input, Animation, and Insets Animation are all on-demand. Traversal (measure, layout, draw) is also on-demand: requestLayout, invalidate, and window or visibility changes are common reasons for scheduleTraversals() to post CALLBACK_TRAVERSAL. In continuous scrolling/animation scenarios, it often looks like it runs every frame.
Follow the recorded stages below:
1. Main Thread Waits for Vsync Signal
- Perfetto trace: The main thread may be Sleeping between two periods of work
- Process description: With a pending frame, Choreographer requests a Vsync callback. Sleeping alone does not prove the thread is waiting specifically for Vsync.
2. Vsync-app Signal Delivery Process
SurfaceFlinger schedules notifications using display timing and software prediction. A VSYNC-app transition is not necessarily an immediate forwarding of a hardware interrupt. The app/EventThread service can have multiple connections, so the global counter cannot identify one app by itself.
Choreographer requests one-shot notifications as needed. Other subscriptions and rendering paths can differ; no request for this Choreographer cycle does not prove there are no other frames or buffer submissions.
3. SurfaceFlinger Wakes Up App Main Thread
DisplayEventReceiver delivers events through native FD handling and JNI to Java onVsync, which posts an asynchronous Message. Looper later dispatches the Receiver Runnable into doFrame. Delivery and frame processing are distinct times; visibility of their slices depends on instrumentation and capture settings. See part five.
4. Processing Input Events (Input)
- Perfetto trace:
Inputblock - Process description: Runs when the frame has a registered Input callback; not every system input event is handled in this callback
- Trigger conditions:
- May have an Input callback: Batched input work was registered for this frame
- May have no Input callback: No corresponding batched work is due, including some inertial or static periods
- Note: Check whether the callback was registered for this frame; finger state alone does not determine whether the slice appears
5. Processing Animations (Animation)
The animation phase executes due CALLBACK_ANIMATION work, including property animations, View.postOnAnimation, and app FrameCallbacks. Its presence does not prove visible animation, and item creation/input alone does not guarantee it. Callbacks may be posted earlier or during preceding phases of the current frame, not only the previous frame.
6. Processing Insets Animations
The insetsAnimation phase handles due window-insets animation callbacks, such as supported IME/system-bar transitions. Insets describe areas occupied by system UI, not newly inserted windows. A static inset change or keyboard appearance does not necessarily produce this slice.
7. Traversal (Measure, Layout, Draw Preparation)
- Perfetto trace:
performTraversals,measure,layout,draw - Process description: These are the three core UI rendering stages, but they do not run as a complete set on every Vsync. Execution depends on whether the current frame has layout/draw requests.
7.1 Measure Phase
- Purpose: Determine the size of each View
- Process: Parents measure children required by their layout policy; cached or skipped branches mean the entire tree need not be remeasured
- Key concepts:
MeasureSpec: Encapsulates parent container’s size requirements for child Views (EXACTLY, AT_MOST, UNSPECIFIED)onMeasure(): Custom Views can override this method for their measurement logic
- Perfetto representation:
measureevent, duration depends on View hierarchy complexity
7.2 Layout Phase
- Purpose: Determine each View’s position coordinates in the parent container
- Process: Based on Measure phase results, assign actual display positions to each View
- Key concepts:
layout(left, top, right, bottom): Set View’s four boundary coordinatesonLayout(): ViewGroup overrides this method to determine child View positions
- Perfetto representation:
layoutevent; cost depends on layout code and hierarchy, with no universal ranking against measure
7.3 Draw Phase
- Purpose: Draw View content onto canvas
- Hardware-accelerated View path: Records or updates display lists; software Canvas drawing can still rasterize pixels
- Key process:
draw(Canvas): Draw View’s own contentonDraw(Canvas): Subclass overrides to implement specific drawing logicdispatchDraw(Canvas): ViewGroup uses this to draw child Views
- Perfetto representation:
drawevent, mainly builds DisplayList under hardware acceleration
ViewRootImpl.performTraversals Core Code
1 | // frameworks/base/core/java/android/view/ViewRootImpl.java |
Android 16 relationship sketch, with surrounding locals and branches omitted; not a compilable method. Invisible windows, canceled draws, and rescheduling are distinct branches, and performDraw can return without drawing.
Execution conditions for the three phases:
- Measure: Runs on first draw and when layout/configuration/insets/window changes require it, not fixed every frame
- Layout: Runs when
layoutRequestedis true and the app is in a drawable state - Draw: Runs only when draw conditions are satisfied; canceled/pre-draw-blocked cases reschedule traversal
8. Sync DisplayList to Render Thread
- Perfetto trace: syncAndDrawFrame, visible “sync” or “syncAndDrawFrame” event (usually shown as data transfer point from main thread to render thread)
- Process description: Main thread syncs current-frame RenderNode/DisplayList state to RenderThread through
syncAndDrawFrame. This is not pure fire-and-forget async: UI thread waits for a key sync point (DrawFrameTask::postAndWait) and is then unblocked as early as possible; it does not wait until the frame is actually presented.
9. Render Thread Acquires Buffer
- Perfetto trace: Inspect recorded dequeueBuffer/queueBuffer slices when available. DequeueBufferDuration and QueueBufferDuration are HWUI frame-stat fields, not guaranteed slice names.
- Process description: During draw submission, RenderThread goes through the
ANativeWindow/RenderPipelinepath for buffer acquire/swap. Whether it waits, and how long it waits, directly affects deadline miss risk.
10. Process Rendering Instructions and Flush to GPU
- Perfetto trace:
drawingrelated blocks - Process description: RenderThread (CPU side) processes the RenderNode tree synced from UI thread via
HardwareRenderer/CanvasContext, builds GPU commands, and submits them. GPU work executes asynchronously; the associated fence signals completion for downstream synchronization.
11. Submit Buffer (Possibly Unsignaled)
queueBuffer can submit an acquire fence that has not signaled yet in an asynchronous GPU pipeline. Submission is distinct from SurfaceFlinger’s policy for latching an unsignaled buffer. The queueBuffer slice alone does not establish fence state; correlate relevant fence/graphics data.
12. Trigger Transaction to SurfaceFlinger
- Perfetto trace: TransactionQueue or BLAST transaction events, generally after queueBuffer, some traces don’t have this tag
- Process description: App side associates buffer and layer-property updates via BLAST/SurfaceControl transactions and submits them to SurfaceFlinger. SurfaceFlinger then decides latch timing based on
LatchUnsignaledConfigand related policies, composes, and presents.
Identifying different rendering modes in Perfetto:
- During finger sliding: Look for
InputandTraversal, then confirm whether RenderThread submitted a frame - During inertial scrolling:
AnimationandTraversalare common; whether Input also appears depends on the captured callbacks - In static state: The app may submit no new frame; background animation or another update can still trigger work
Software Drawing vs Hardware Acceleration
Although hardware-accelerated rendering is now basically standard, understanding the differences between the two rendering modes still helps understand Perfetto traces:
| Aspect | Software Drawing | Hardware Acceleration |
|---|---|---|
| Drawing Thread | Usually main thread for ordinary Views; other Canvas uses can differ | Main records, RenderThread submits, GPU executes |
| Drawing Engine | Skia (CPU) | Skia with an OpenGL/Vulkan GPU backend |
| Perfetto Characteristics | Main-thread draw may include rasterization/waits | Both main thread and RenderThread may do work or wait |
| Performance Impact | May occupy the main thread | Some work moves to RenderThread, but synchronization can still block the main thread |
The above introduces the basic rendering process. For more detailed Choreographer principles, refer to Android Perfetto Series 5: Android App Rendering Flow Based on Choreographer.
Next, we’ll focus on in-depth content about the main thread and render thread:
- Main thread development
- Main thread creation
- Render thread creation
- Division of labor between main thread and render thread
Evolution of Dual-Thread Rendering Architecture
Android’s rendering system has undergone an important evolution from single-thread to dual-thread.
Earlier View Rendering (Before Android 5.0)
Hardware acceleration was supported from Android 3.0 (API 11), before the HWUI RenderThread introduced in Android 5.0. Pre-5.0 does not mean CPU-only rendering; this section concerns earlier ordinary View paths.
In early software-drawn View paths, the main thread handled more UI drawing work:
- Processing user input events
- Executing measure, layout, draw
- Submitting software-drawn buffers; other graphics paths could use other threads
Problems with this design:
- Poor responsiveness: Main thread overloaded, prone to ANR
- Performance bottleneck: Drawing work competes with input and business messages on the main thread
- Unstable frame rate: Complex interfaces easily cause frame drops
Dual-Thread Era (Starting from Android 5.0 Lollipop)
Android 5.0 introduced RenderThread, implementing separation of rendering work:
Main Thread Responsibilities:
- Processing user input and business logic
- Executing View’s measure, layout, draw
- Building DisplayList (drawing instruction list)
- Synchronizing data with render thread
Render Thread Responsibilities:
- Receiving and processing DisplayList
- Generating and submitting OpenGL/Vulkan rendering commands for the GPU to execute
- Managing textures and rendering resources
- Interacting with SurfaceFlinger
Advantages of this architecture:
- Parallel opportunity: After the key sync point, the main thread can process other messages while RenderThread continues
- Sync boundary: The main thread can still block at
syncAndDrawFrame; the two threads are not fully independent - Performance depends on the workload: Scheduling, GPU work, buffers, and resource synchronization determine the actual benefit
Main Thread Creation Process
Ordinary Android app processes are typically forked and specialized from Zygote, with preparation differing for paths such as USAP. fork creates a child process retaining the calling thread; a process and its main thread are distinct.
The central operation in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp is:
1 | pid_t pid = fork(); |
ActivityThread is a framework object running on the app’s main thread, not a Thread subclass or the creator of that Linux thread. Its entry prepares the main Looper, instance, and system connection. H dispatches lifecycle work; app Handlers, Choreographer’s Handler, and native FD events do not all pass through H.
ActivityThread Creation
The following is an Android 16 ordinary Zygote child path, omitting surrounding setup and special branches:ZygoteConnection.handleChildProc → ZygoteInit.zygoteInit → RuntimeInit.applicationInit → ActivityThread.main
Android 16 ZygoteConnection.java
1 | private Runnable handleChildProc(ZygoteArguments parsedArgs, FileDescriptor pipeFd, |
For regular app processes, this goes through zygoteInit and eventually reaches ActivityThread.main. childZygoteInit is for child-zygote flow, not the normal app path.
Android 16 ActivityThread.java
1 | public static void main(String[] args) { |
The comments are very clear, so I won’t elaborate here. Once Looper.loop() starts, the main thread begins dispatching messages; ActivityThread.main() does not normally return while the app is running.
ActivityThread Functionality
ActivityThread’s Handler dispatches many component lifecycle messages on the main thread. Subsequent work in a Service or Receiver can move to other threads, so this does not mean every operation of all four component types runs on the main thread.
1 | class H extends Handler { // Illustrative message types; check values in the target branch |
H does not create the process. Activity lifecycle work also uses EXECUTE_TRANSACTION; Service/Receiver lifecycle callbacks commonly run on main, while remote Binder methods and Provider query/insert requests can execute in a pool. Not every component method passes through H. On Android 17, apps targeting API 37 or higher use a new lock-free MessageQueue implementation. Looper and Handler usage remains the same, but queue-internal lock analysis must account for targetSdkVersion, and tools reflecting on private MessageQueue internals may break.
RenderThread Creation and Development
HWUI RenderThread has carried part of the UI rendering work since Android 5.0. It is a CPU thread; GPU-accelerated View rendering existed earlier, so its introduction was not Android’s first use of the GPU.
Software Drawing
Hardware acceleration is enabled by default for target API 14+, with Application/Activity-level configuration. The following attribute belongs on an existing application tag; activity overrides and other graphics components may still use hardware paths:
1 | android:hardwareAccelerated="false" |
It affects ordinary View drawing, not necessarily every graphics component or RenderThread in the process, and differs from a software layer on one View. The old image visibly contains RenderThread, DrawFrame, and dequeueBuffer. It is retained as a historical rendering-wait example, not proof of purely software drawing:

This Systrace image and the opening Perfetto capture are not a controlled comparison. Slices include waits, so their lengths do not establish an acceleration factor or pure CPU raster cost.
Hardware-Accelerated Drawing
The ordinary hardware-accelerated View path records or updates display lists in RenderNodes on the main thread before syncAndDrawFrame. The UI may be unblocked before subsequent drawing, or only after later draw/flush work. The wait is not guaranteed to be brief.
Render Thread Initialization
In Android 16, RenderProxy construction calls RenderThread::getInstance, which creates/starts the process’s HWUI thread on demand. initializeIfNeeded primarily handles renderer/Surface state and is not necessarily the thread-creation point. BLAST updates occur elsewhere in ViewRootImpl; the two excerpts below are not a consecutive call sequence.
android/view/ViewRootImpl.java
1 | // ViewRootImpl: renderer/Surface initialization call (surrounding conditions omitted) |
The BlastBufferQueue created here will play a key role in subsequent rendering processes:
- Manages client-side BufferQueue endpoints and SurfaceControl transactions
- Can associate buffer and property updates; reduced calls or waits require measurement
- QueuedBuffer metric changes can be observed in Perfetto
When drawing is required, ThreadedRenderer.draw is used. The following method is from Android 16:
Android 16 ThreadedRenderer.java
1 | void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) { |
The draw above updates DisplayList first, then calls syncAndDrawFrame for the key UI Thread to RenderThread sync stage.
UI Thread and RenderThread DisplayList Synchronization Mechanism
In the syncAndDrawFrame key function, the following important synchronization operations occur:
1 | // android-16.0.0_r1, RenderProxy.cpp |
syncAndDrawFrame is not fully non-blocking. In Android 16, DrawFrameTask::drawFrame() calls postAndWait(): it posts work to RenderThread queue and then waits on a condition variable; RenderThread unblocks UI at an appropriate sync point. So this is a conditional early-unblock design, not “UI never waits”.
1 | // frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp |
In run(), canUnblockUiThread selects release before later drawing or after the draw/flush/wait path. Queued work, resource synchronization, and fences can extend the UI wait. See Android 16 DrawFrameTask.
The specific synchronization process includes:
- RenderNode tree transfer: The RenderNode tree (containing DisplayList) built by the main thread during the draw process is passed to RenderThread
- Property synchronization: View’s transformation matrices, transparency, clipping regions, and other properties are synchronized together
- Resource sharing: HWUI manages staging/render state and resource lifetime; this does not make arbitrary concurrent mutation of app Paths/Paints safe
- Rendering state transfer: Rendering state information needed for the current frame is passed to RenderThread
This synchronization process is the core of Android hardware-accelerated rendering. It implements a division of labor where UI Thread focuses on logic processing and RenderThread focuses on rendering.
The core implementation of the render thread is in the libhwui library, with code located at frameworks/base/libs/hwui
RenderThread and BlastBufferQueue Interaction Flow
After RenderThread receives the synced DisplayList, it begins actual rendering work. During this process, it interacts closely with BlastBufferQueue:
1 | // frameworks/base/libs/hwui/renderthread/CanvasContext.cpp |
Note: The old intuitive flow (
dequeueBuffer/queueBuffer/flushTransaction) is still useful as a mental model, but in the Android 16 implementation checked here these details are handled throughRenderPipeline/ANativeWindowpath, andCanvasContext::draw()is no longer in the old shape.
Key Features of BlastBufferQueue:
- App-side management: In the window paths compared here, the client creates and manages BLASTBufferQueue
- Buffer acquisition: Producer and consumer are local to the app, but
dequeueBuffercan still wait when no buffer is available - Buffer rotation: Measure any waiting-time or high-refresh-rate benefit on the target device trace
- Asynchronous submission: Submits buffers and acquire fences through transactions; GPU work may still be pending
- Supports unsignaled buffer: Cooperating with SurfaceFlinger unsignaled-latch policies can reduce end-to-end latency in specific scenarios
In-depth Discussion on Latching Unsignaled Buffers
Modern Android systems can sometimes latch a Buffer before its acquire fence signals. This policy is called “Latching Unsignaled Buffers”. The present fence records a later presentation stage and must not be confused with the acquire fence.
Traditional mode: SurfaceFlinger checks the Buffer’s acquire fence before reading content that the GPU may still be writing.
Latch Unsignaled mode: Under the supported policy, SurfaceFlinger can latch a Buffer whose acquire fence has not yet signaled. It must still wait before actually reading unfinished content; whether this lowers latency depends on the device and scene.
Control Switch and Policy (Android 13+):
The Android 13 policy documentation describes boot-time LatchUnsignaledConfig selection and debug.sf.auto_latch_unsignaled. It is not a runtime switch guaranteed across all releases. AutoSingleLayer means:
- In Android 13’s default
AutoSingleLayerpolicy, early latch requires a single updated layer with a Buffer-only update and no synchronizing transaction or geometry change. - Multiple updated layers or other excluded transaction conditions do not take that early-latch path.
SurfaceFlinger therefore follows an acquire-fence and layer-update policy; the present fence is used later to assess when presentation completed.
Division of Labor Between Main Thread and Render Thread
For ordinary Views, main handles input, business logic, layout, and command recording; RenderThread organizes later GPU work and submission. Main can also update layer properties and transactions through ViewRootImpl/SurfaceControl, so SF interaction is not exclusive to RenderThread.
RenderNode combines drawing records with properties such as transforms and alpha. DisplayList is the recorded commands, not a synonym for RenderNode. Reuse and property-only updates can avoid another onDraw, while layout/content changes may require recording. Batching must preserve order, clipping, and blending; it cannot arbitrarily combine all text into one draw.
BlastBufferQueue Working Principle
This compares an older window BufferQueue consumed by SurfaceFlinger with client-side BLAST. BufferQueues also serve cameras/codecs and are not universally created by SF.
In the common Android 15 window path, ViewRootImpl manages BLASTBufferQueue. Its local consumer acquires a buffer and associates it with a SurfaceControl transaction. Synchronization/merging affect submission, while buffer/fence back-pressure can still block. The counters below use Android 15 BLASTBufferQueue, not a performance guarantee from BLAST alone.
App-side QueuedBuffer Metric
- Perfetto display:
QueuedBuffervalue track - AOSP definition (BLASTBufferQueue):
QueuedBuffer = mNumFrameAvailable + mNumAcquired - mPendingRelease.size() - How to read it: This is a composite state metric, not a fixed-offset formula
- Practical use: Focus on trend and duration, not one instantaneous value

QueuedBuffer Value Change Timing
The rising example shows queueBuffer/onFrameAvailable near a counter update. Android 15 emits the combined state after updating available frames; synchronization, merging, and release paths can affect the result. Do not translate every function invocation into exactly +1.

The falling example shows an app Binder-thread release callback near the decrease. This does not prove the release fence has completed or the buffer is immediately safe to rewrite. BLAST can retain pending releases, which the counter subtracts; it is not a count of free buffers.

SurfaceFlinger-side BufferTX Metric
Android 15 Layer uses mPendingBufferTransactions, traced as BufferTX - plus the layer name. Valid buffer transactions increment it; latch/drop paths decrement it. Property-only transactions are not necessarily +1, and a decrease is not completed presentation.
The annotated 1 and 2 are observed values. The track’s vertical scale is not a universal capacity or fixed maximum of three.

Collaboration Flow Between App Side and SF Side
- BLAST receives/acquires app buffers according to queue and synchronization state.
- SurfaceControl transactions associate buffers, frameNumber, and layer properties; frameNumber is not a doFrame token without explicit correlation.
- SF updates pending counts; latch/drop and later composition/presentation are separate stages.
- Release callbacks/fences and BLAST pending-release management govern return. A counter decrease proves neither presentation nor immediate reuse.
Key Performance Observation Points
Counter trends suggest investigation, not a unique cause. High occupancy can involve intended pipelining, synchronization, GPU/fence waits, or delayed consumption; low occupancy can mean unchanged content. Correlate frameNumber/token/layer, doFrame/DrawFrames, scheduling, fences, and presentation before attributing a bottleneck to app, GPU, or SF.
Performance
Synchronous database access, locks, and other waits occupy main-thread dispatch time, delaying input and frame callbacks. Diagnose jank and ANRs separately:
- Jank: Use the frame deadline and presentation. UI, RenderThread, and GPU stages overlap; adding wall durations and comparing with a fixed 8.33 ms is misleading.
- ANR: Specific input/component timeout rules apply. Not every pause of a few seconds produces a dialog; foreground/background state, configuration, and failure type affect handling. No dialog does not imply a crash. See the ANR series.
Dumps may arrive after the failure, so a stack does not guarantee an easy diagnosis. Use Perfetto/System Trace for scheduling and waits, then Android Studio method sampling/instrumentation when needed, accounting for overhead. Jank and an unresponsive app are both frustrating for users. That is one reason I wrote this series: understand how the two threads hand work to each other, then find where progress stops.
Perfetto’s Unique FrameTimeline Feature
FrameTimeline supplies expected/actual app and SurfaceFlinger records for investigating lateness. Compared with the older Systrace workflow, it provides correlated frame records that Trace Processor can also query.
Android 12+ support and actual collection are required. Missing tracks can reflect coverage or configuration, not an absence of jank.
Core Concepts of FrameTimeline
According to Perfetto official documentation, classification uses expected/actual timing and can include prediction errors, not a uniform perceptual result for every mismatch. Supported captured frames have two timeline types:

1. Expected Timeline
- Purpose: Shows the rendering time window allocated by the system to the application
- Start time: Predicted callback start, not the time it actually obtained CPU
- Meaning: To avoid system jank, the application needs to complete work within this time range
2. Actual Timeline
- Purpose: Shows the actual time the application completed the frame (including GPU work)
- Start time: When
Choreographer#doFrameorAChoreographer_vsyncCallbackstarts running - End time:
max(GPU time, Post time), where Post time is when the frame was submitted to SurfaceFlinger
The App Actual duration includes app/GPU completion and posting, not final presentation or pure CPU time. Follow the linked SurfaceFlinger frame and inspect present/prediction fields.

Color Coding System
FrameTimeline uses intuitive colors to identify different frame states:
| Color | Meaning | Description |
|---|---|---|
| Green | Normal frame | No jank observed, ideal state |
| Light green | High latency state | Stable frame rate but delayed frame presentation, causing increased input latency |
| Red | Jank frame | Jank caused by current process |
| Yellow | App-blameless jank | Frame has jank but app is not the cause, jank caused by SurfaceFlinger |
| Blue | Dropped frame | On the SF side a newer frame was chosen; on the app side, a UI state update may have missed RenderThread before it drew |
Clicking different colored ActualTimeline slices shows classifications such as jank_type; inspect threads and fences to identify the underlying cause:
These are common UI classifications, not code-level root causes. The detail image above is com.tencent.mm, token 54160848, rather than the demo app’s 8227… tokens. App Deadline Missed and Buffer Stuffing appear together; labels can combine.
Jank Type Analysis
FrameTimeline can identify multiple jank types:
App-side jank:
- AppDeadlineMissed: App runtime exceeded expectations
- BufferStuffing: Sustained queued work adds presentation latency; merely preparing the next frame before the previous one presents is normal pipelining and is insufficient evidence
SurfaceFlinger jank:
- SurfaceFlingerCpuDeadlineMissed: SF’s composition path misses its deadline, not an ANR timeout or pure Running time
- SurfaceFlingerGpuDeadlineMissed: CPU work met its deadline, but GPU composition did not
- DisplayHAL: Presentation was late after timely SF hand-off; inspect available HAL budget and work
- PredictionError: Scheduler prediction deviation
Configuring FrameTimeline
Add this textproto data-source fragment to a complete TraceConfig. It is not YAML or a standalone configuration with buffers/duration. See the capture guide:
1 | data_sources { |
Vsync Signals in Perfetto
In this trace, VSYNC-app is displayed as a Counter, which differs from many people’s intuitive understanding:
- 0 → 1 change: Represents a Vsync signal
- 1 → 0 change: Also represents a Vsync signal
- Incorrect understanding: Many people mistakenly think only becoming 1 is a Vsync signal
Correct Vsync Signal Identification
In the diagram below, time points 1, 2, 3, 4 are all Vsync signal arrivals

Key points:
- Each value change in this screenshot corresponds to a recorded Vsync marker: Whether 0→1 or 1→0; this is not a universal encoding for all Vsync tracks
- Signal frequency: Measure adjacent recorded transitions in this capture; 120 Hz support alone does not imply constant 8.33 ms delivery, especially across mode changes or ARR
- Multi-app scenario: Counter may remain active due to other apps’ requests
Analysis Techniques
The SF counter alone does not prove delivery to the target app. Correlate app-side onVsync instrumentation, PID, timing, and doFrame tokens when available. An absent FrameDisplayEventReceiver.onVsync slice can simply mean missing instrumentation or capture coverage. Distinguish notification, dispatch, callbacks, and presentation.
References
- https://juejin.im/post/5a9e01c3f265da239d48ce32
- http://www.cocoachina.com/articles/35302
- https://juejin.im/post/5b7767fef265da43803bdc65
- http://gityuan.com/2019/06/15/flutter_ui_draw/
- https://developer.android.google.cn/guide/components/processes-and-threads
Attachments
The Perfetto trace files involved in this article have also been uploaded. You can download and open them in Perfetto UI (https://ui.perfetto.dev/) for analysis
Click this link to download the Perfetto trace files involved in this article
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
