Different people have different understandings of smoothness (jank/dropped frames) and different perceptions of jitter thresholds. Therefore, before starting this series, it is necessary to clarify the content to avoid misunderstandings. Here are some basic explanations:
- For mobile users, jank encompasses many scenarios: dropped frames when scrolling lists, excessive white screen during app startup, slow screen wake-up when pressing the power button, unresponsive interface followed by a crash, no response when clicking an icon, incoherent window animations, lagging touch response, or stuttering when entering the desktop after a reboot. These scenarios differ slightly from what developers understand as “jank.” Developers categorize these more finely, which is a cognitive gap between developers and users that must be noted when handling feedback from users or testers.
- Developers can initially classify feedback as Smoothness (uneven visual progress), Responsiveness (long waits after an action), or Stability (crashes and ANRs). These can overlap. An icon that appears unresponsive is not necessarily a stability failure; input, application state, and logs determine the diagnosis.
- These are not one Message-timeout mechanism with different thresholds. Long main-thread tasks can delay both input and rendering, but GPU work, SurfaceFlinger, the display stack, scheduling, or remote services can also cause delays. ANRs depend on specific input or component timeout rules; crashes have separate causes such as exceptions or signals. A long Message alone establishes neither.
- This series primarily analyzes smoothness-related issues. Responsiveness and stability will be covered in dedicated articles. Understanding smoothness first will make analyzing responsiveness and stability much easier.
- This series focuses on using Systrace (Perfetto) for analysis. Systrace is our entry point because many factors affect smoothness—some within the app itself and others within the system. Systrace (Perfetto) provides a holistic view of the system’s operation during the problem, helping us initially pinpoint the root cause.
Historical case note (reviewed 2026-10-14): This case concerns a Mi 10 Pro, MIUI 12.5.3, and the listed launcher version, not current MIUI/HyperOS. The screenshots and original experience remain, while technical conclusions are narrowed using the attached trace and supplemented with event-level checks. The introductory Bilibili images are a separate example. Obsolete Chrome/Traceview instructions are updated; new captures on Android 12 (API 31) or later can include FrameTimeline, which these legacy attachments do not contain.
Table of Contents
- The Routine for Analyzing Jank with Systrace
- Case Study: Analyzing Jank with Systrace
- Case Description
- What Role Does Triple Buffer Play in This Scenario?
- Series Articles
- Attachments
- About Me && Blog
Systrace Series Articles:
- Introduction to Systrace
- Systrace Basics - Prerequisites for Systrace
- Systrace Basics - Why 60 fps?
- Android Systrace Basics - SystemServer Explained
- Systrace Basics - SurfaceFlinger Explained
- Systrace Basics - Input Explained
- Systrace Basics - Vsync Explained
- Systrace Basics - Vsync-App: Detailed Explanation of Choreographer-Based Rendering Mechanism
- Systrace Basics - MainThread and RenderThread Explained
- Systrace Basics - Binder and Lock Contention Explained
- Systrace Basics - Triple Buffer Explained
- Systrace Basics - CPU Info Explained
- Systrace Smoothness in Action 1: Understanding Jank Principles
- Systrace Smoothness in Action 2: Case Analysis - MIUI Launcher Scroll Jank Analysis
- Systrace Smoothness in Action 3: FAQs During Jank Analysis
- Systrace Responsiveness in Action 1: Understanding Responsiveness Principles
- Systrace Responsiveness in Action 2: Responsiveness Analysis - Using App Startup as an Example
- Systrace Responsiveness in Action 3: Extended Knowledge on Responsiveness
- Systrace Thread CPU State Analysis Tips - Runnable
- Systrace Thread CPU State Analysis Tips - Running
- Systrace Thread CPU State Analysis Tips - Sleep and Uninterruptible Sleep
If you are not familiar with the basic use of Systrace (Perfetto), please catch up on the Systrace Basics Series first. This article assumes you are already familiar with using Systrace (Perfetto).
The Routine for Analyzing Jank with Systrace
Use the following sequence to collect evidence:
- Reproduce with recorded conditions: Capture the steps, frequency of reproduction, system/app versions, display mode, thermal state, and battery state. Keep content, input, and cache conditions comparable across devices.
- Open with a compatible tool: First try Open trace file in Perfetto UI. Embedded legacy HTML viewers,
chrome://tracing, and “Open With Legacy UI” are not stable instructions across browser versions. If import fails, preserve the original and use a viewer supporting that format. Compatibility of the embedded UI was not retested here. - Establish context: Locate the interaction, process, and window/layer. Establish whether continuous updates were expected; static content or intentional frame-rate limits are not automatically jank.
- Check system evidence: Correlate scheduling, CPU frequency/limits, thermal state, memory pressure, and I/O. Low frequency alone does not prove thermal throttling; dense tracks do not prove sustained high utilization.
lmkd,HeapTaskDaemon, or Block I/O activity alone does not prove low memory. Keep missing evidence unknown. - Locate app work: Inspect the UI thread, RenderThread, and available GPU data, separating wall time into Running, Runnable, and blocked intervals. The Bilibili selection below is about 110.05 ms, with repeated inflation and drawing. It warrants investigation but does not identify a particular View to fix, and is separate from the MIUI case.

The next selection is 23.526 ms. Function-slice colors are not jank grades; F markers in the legacy Frames track are viewer-generated clues. Green/yellow/red cannot be mapped directly to actual presentation outcomes.

Target intervals for 60/90/120 fps are about 16.67/11.11/8.33 ms. Actual deadlines also depend on scheduling, pipelining, and display mode.
- Connect composition and presentation: The next images show SurfaceFlinger work gaps and a
BufferTXcounter. Investigate the target window: no composition can mean no update, while composition can update another layer. Check the counter’s implementation; blank space or zero alone does not establish a missed presentation.


- Follow dependencies and validate: Long Runnable intervals mean waiting for CPU. Follow long Binder calls into the remote execution/wait chain rather than labeling everything slow app execution. On Android 12+ with the relevant data, use FrameTimeline to relate expected and actual presentation, then inspect CPU/GPU work and dependencies. Validate changes with repeated comparable captures, checking deadlines, latency, and side effects.
App frame drops have many causes. Refer to these articles; different causes have distinct Systrace signatures:
- Overview of Jank Causes in Android - Methodology
- Overview of Jank Causes in Android - System Side
- Overview of Jank Causes in Android - App Side
- Overview of Jank Causes in Android - Low Memory Side
Case Study: Analyzing Jank with Systrace
Systrace / Perfetto exposes timing across processes, threads, and the system, helping form hypotheses. App and system effects can overlap rather than fit a binary classification. Missing source-level stacks or hardware data remain limitations.
For deeper analysis, combine source code with Android Studio CPU Profiler or sampling/custom tracing supported by the target version. Traceview is deprecated; official guidance recommends CPU Profiler from Android Studio 3.2 onward. Method instrumentation changes overhead, so timings from different recording modes are not directly comparable optimization results.
This article focuses on discovery and analysis. Solutions (like optimizing logic, scheduling, or layout) must be tailored to findings.
Case Description
On my Mi 10 Pro, I often felt a stutter during the common gesture of swiping the desktop. With the Mi 10 Pro’s 90Hz mode and a 90 fps target (not a guarantee of actual throughput), I noticed the stutter quite clearly—I’m fairly sensitive to this. After upgrading to MIUI 12.5, the issue remained, so I decided to investigate.
After capturing and studying the Systrace, I thought it would make a good practical example, so I decided to write it up.
I recommend downloading the attached Systrace and following along with the article.
- Because many factors affect jank, I’ll clarify the hardware and software versions first. Findings apply to the attached Systrace even if the scenario is optimized later.
- Hardware: Mi 10 Pro
- Software: MIUI 12.5.3 Stable
- Launcher Version: RELEASE-4.21.11.2922-03151646
These are the conditions I recorded at the time. The attachment lacks a full build fingerprint or APK identity, so it cannot independently verify the exact firmware/launcher version; the MIUI number alone does not establish the underlying Android version. Xiaomi’s FAQ confirms 90 Hz and Snapdragon 865 support. Current-device behavior needs fresh testing.
Starting with Input events
In the captured Systrace, I performed only one swipe, making it easy to locate. The input sequence consists of one Input Down, several Input Moves, and one Input Up.
In Systrace, we see InputDispatcher and InputReader threads in SystemServer, but we primarily focus on the App UI thread.

Down/Move/Up are my annotations for this gesture. deliverInputEvent marks an app input-processing interval; it does not by itself identify the action or represent pure CPU time. Correlate event details. In this case motion continues after release, called Fling below; an Up event does not guarantee fling in every gesture. Background:
Analyzing the UI Thread
Since this jank mostly occurred after releasing the finger, we focus on the segment after “Input Up.”

The legacy Frames track has green and yellow F markers. Does yellow necessarily mean jank? Does red? These reflect viewer rules and available data, not sufficient evidence of actual presentation. Do not confuse them with function-slice colors or modern FrameTimeline colors. The three marked points are starting points, not an exhaustive classification of anomalies.
Identifying three suspicious points, we move to the RenderThread.
Analyzing the Render Thread
The screenshot’s ruler spans about 19.103 ms; RenderThread’s DrawFrame takes about 16.519 ms. The ruler is not a measured presentation duration, and UI/RenderThread slices overlap rather than simply add.
Rechecking xiaomi_launcher.html using PID 3553, RenderThread TID 3690, B/E markers, and sched_switch gives:
| Item | Result for this interval |
|---|---|
| DrawFrame start/end (trace seconds) | 28306.209230 → 28306.225749 |
| Wall time | 16.519 ms |
| Running time | 15.517 ms |
| Running on CPU 2 / CPU 0 | 14.794 ms / 0.723 ms |
| Off CPU | 1.002 ms |
“All green, always Running” is too strong. Execution dominates, but there are off-CPU intervals. Workload, core/frequency, cache/memory behavior, and driver work remain possible contributors.

Next correlate the target layer’s submissions and consumption with SurfaceFlinger work. Exceeding 11.11 ms alone does not determine the final presentation outcome.
Analyzing SurfaceFlinger
For basics on SF trace reading, see SurfaceFlinger Explained.
Inspect the target layer’s queue events and SurfaceFlinger work, then available GPU fences and presentation evidence. This counter is neither the app’s total buffer allocation nor a guarantee that every queued image is GPU-ready. The previous image can remain in use when the queue is empty.
The following three images offer clues, not universal jank tests:
Three cases to investigate:
No new composition and an empty target queue: With expected ongoing updates, this suggests insufficient new content. Check target timing, window state, and presentation of retained content. Not every SurfaceFlinger gap is jank.
(Matches the first suspicious point).
Composition with an empty target queue: A separate
com.miui.miwallpaper.snowmountainwallpaper layer is visible and may trigger composition. SurfaceFlinger activity does not prove Launcher updated; counters alone do not identify the sole trigger.
Composition with queued target content: This is a useful intermediate state, but verify which buffer was selected, its fence, and presentation timing. “Normal” here describes the shown production/consumption pattern, not acceptance of the entire display path.
(Provided for comparison).
Near the first point, Launcher’s queue counter reaches zero at 28306.212867 s and rises to one at 28306.224481 s; SurfaceFlinger refresh work also has a gap. This supports investigating production delay and stalled content, but does not independently establish an exact on-screen dropped-frame count or touch-to-display latency.
Back to the Render Thread
This DrawFrame lasts 16.519 ms versus 5.974 ms for the preceding one, about 2.77 times as long. A visually simple gesture or finger release does not prove identical work per frame. A live wallpaper is present, and Launcher/driver work is not fully resolved. Was this frame doing more work, or was the same work taking longer? Within the long slice, prepareTree is about 4.165 ms and flush commands about 4.656 ms of wall time; do not add these to their enclosing parent intervals. Stacks, content/command counts, cache state, and controlled comparisons are needed to separate increased work from slower execution.
Check the RenderThread CPU info:

Execution dominates this interval, so next inspect the actual CPU residency and switches in the Kernel area.
Analyzing CPU Info
See CPU Info Explained and Prerequisites for Systrace.
The trace confirms execution on CPUs 2 and 0. Device documentation describes four small, three big, and one prime core in Snapdragon 865. Mapping Linux CPU IDs to those types still requires device topology/capacity data; low IDs do not universally mean small cores. The discussion below retains my CPU 0–3 mapping from the time as an explicit premise.

The recorded value is 1,804,800 kHz = 1.8048 GHz. It is also the last CPU 0/2 frequency event before the target slice, with no new frequency event recorded during it. This is recorded frequency state, not a direct measurement of instantaneous hardware speed or utilization. Establishing a maximum requires the device’s frequency table and limits.

Clock Frequency Limits shows constraint changes. The selected minimum of 1,075,200 kHz and maximum of 1,804,800 kHz do not prove that every CPU boost mechanism has ended. Power HAL, cpuset, utilization constraints, or vendor policies may be unrecorded; “Boost ended” remains the original image’s inference.

My hypothesis at the time was that small-core capacity was insufficient for these long frames. The attachment supports an association between long slices and CPU 0/2 residency, not a controlled demonstration that the scheduler caused the problem.
The immediately following DrawFrame lasts 9.170 ms, running 3.890 ms on CPU 0 and 4.554 ms on CPU 5; it did not instantly run entirely on a big core. Other short DrawFrames last 3.843 ms and 5.809 ms, with their Running intervals entirely on CPU 0. “Short” describes RenderThread duration, not proven on-time presentation. These observations contradict a blanket “normal frames never use small cores” claim, while a fast small-core slice also cannot disprove every capacity problem.
Compare equivalent work, frequency, core residency, runnable delay, and thermal conditions, then change a supported policy under controlled conditions and measure deadlines and latency. See Linux CPUFreq and Capacity Aware Scheduling for the distinction between frequency and scheduling capacity; they do not document MIUI’s full kernel implementation.
What we can establish here is: Long RenderThread slices on cores I identified as small at the time are an evidence-backed investigation target. Establishing a particular scheduling policy as the root cause still requires controlled validation.
Vendors with source access, permissions, and regression coverage can investigate migration, frequency constraints, and power policies, measuring latency, temperature, battery cost, and fairness to other tasks. App developers should first investigate their own rendering work and supported performance interfaces; changing migration thresholds is not a general app-level fix. Competitor traces suggest hypotheses, not proof of private scheduling algorithms.
What Role Does Triple Buffer Play in This Scenario?
Triple Buffer Explained describes extra production/consumption overlap and tolerance of some short variations. More buffers do not shorten GPU work or remove sustained bottlenecks; they cost memory and may add queueing latency. Keeping every older frame for the next refresh is not necessarily desirable.
The following image comes from a Matrix sample (SurfaceFlinger PID 510), not this MIUI attachment (PID 990). Its “already composited but not consumed” annotation should mean submitted content awaiting selection, and the counter does not represent all fully rendered buffers.

While looking through this MIUI trace, I also noticed the target queue count sometimes going from 2→0. The step alone cannot identify a discarded image, but nested queue/acquire events provide more evidence:

| Trace time (seconds) | Launcher queue event |
|---|---|
| 28306.212859 | Acquire label 4:617, then count 0 |
| 28306.224466 | Queue label 3:618, then count 1 |
| 28306.234435 | Queue label 0:619, then count 2 |
| 28306.235906 | Acquire label 0:619, then count 0 |
These are vendor trace labels. Interpreted as slots/sequential frame numbers, they support 618 being skipped and 619 being acquired. The full vendor source is unavailable, so these markers alone do not identify the exact code branch or each image’s final presentation.
As a mechanism reference, AOSP android-11.0.0_r1 BufferQueueConsumer::acquireBuffer can skip older entries according to expected presentation time, buffer timestamps, and frame-number conditions, acquire a later entry, then record the remaining queue length. A 2→0 step can therefore reflect skipping one and acquiring another in a single operation; it does not establish a bug.
A shallower queue can expose later production variation, but this does not prove that retaining the old image would prevent the next jank. Its intended presentation time, position, or input freshness may already be unsuitable, and retaining it can increase latency. Compare pacing and responsiveness under controlled conditions.
The next image retains the original annotations. It shows another long RenderThread slice and queue/SF gaps; “keeping the earlier buffer would prevent this drop” is an untested counterfactual.

Fling positions are not necessarily precomputed fixed increments for every future frame. Common animations evaluate position from frame/elapsed time, so the next image can reflect a later point after a missed presentation. Retaining an old position can reduce one visible displacement while increasing latency. Assess position, timing, and presentation together rather than declaring triple buffering harmful from queue counts alone.

For an illustration with fixed display intervals, let offset be an element’s horizontal position. These values are not measurements from the attachment:
- Regular progress:
2→4→6→8→10→12. - Repeat old content, then show older positions in sequence:
2→4→6→6→8→10→12. The sequence is longer and may accumulate latency; it is not guaranteed smoother. - Repeat, then catch up to a newer position:
2→4→6→6→10→12. The adjacent displacement is larger, but the state is fresher.
The appropriate strategy depends on animation timing, intended presentation times, and latency requirements. This illustrates a possible stall-then-jump sensation; it does not establish the positions actually displayed in this MIUI trace or justify disabling buffer-discard policies.
Series Articles
- Systrace Smoothness in Action 1: Understanding Jank Principles
- Systrace Smoothness in Action 2: Case Analysis - MIUI Launcher Scroll Jank Analysis
- Systrace Smoothness in Action 3: FAQs During Jank Analysis
Attachments
Attachments are on GitHub: https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action
xiaomi_launcher.zip: Main analysis trace.xiaomi_launcher_scroll_all_the_time.zip: Continuous scrolling trace.oppo_launcher_scroll.zip: Comparison trace.
On 2026-10-14, all three ZIPs were downloadable, passed CRC checks, and contained readable ftrace data. New numbers above come from local event analysis of the first attachment, not a device reproduction or a complete frame-rate test. Device topology, full vendor scheduling source, complete fence/presentation correlation, and equivalent-work A/B tests are unavailable. The Bilibili example’s trace is not one of these three attachments.
About Me && Blog
Below is my personal intro and related links. I look forward to exchanging ideas with fellow professionals. “When three walk together, one can always be my teacher!”
- Blogger Intro: Includes personal WeChat and WeChat group links.
- Blog Content Navigation: A guide for my blog content.
- Curated Excellent Blog Articles - Android Performance Optimization Must-Knows: Welcome to recommend projects/articles.
- Android Performance Optimization Knowledge Planet: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.
One walks faster alone, but a group walks further together.
