CN

Android Systrace Basics - Triple Buffer Explained

Word count: 2.9kReading time: 18 min
2019/12/15
loading

This is the eleventh article in the Systrace series, providing a brief introduction to Triple Buffer within Systrace. It covers how to identify jank in Systrace, perform preliminary localization and analysis, and explains the impact of Triple Buffer on performance.

The purpose of this series is to view the overall operation of the Android system from a different perspective using Systrace, while also providing an alternative angle for learning the Framework. Perhaps you’ve read many articles about the Framework but can never remember the code, or you’re unclear about the execution flow. Maybe from Systrace’s graphical perspective, you can gain a deeper understanding.

Version note (reviewed 2026-10-13): This article retains its 2019 Systrace screenshots and 60 Hz timing diagrams. Buffering can still reduce pipeline waits, but the old topology and scheduling do not describe every newer system. Historical switches were checked against AOSP android-9.0.0_r1 / android-10.0.0_r1, and newer paths against android-16.0.0_r1. Historical properties describe those implementations rather than a universal runtime switch.

Table of Contents

Series Article Index

  1. Introduction to Systrace
  2. Systrace Basics - Prerequisites for Systrace
  3. Systrace Basics - Why 60 fps?
  4. Android Systrace Basics - SystemServer Explained
  5. Systrace Basics - SurfaceFlinger Explained
  6. Systrace Basics - Input Explained
  7. Systrace Basics - Vsync Explained
  8. Systrace Basics - Vsync-App: Detailed Explanation of Choreographer-Based Rendering Mechanism
  9. Systrace Basics - MainThread and RenderThread Explained
  10. Systrace Basics - Binder and Lock Contention Explained
  11. Systrace Basics - Triple Buffer Explained
  12. Systrace Basics - CPU Info Explained
  13. Systrace Smoothness in Action 1: Understanding Jank Principles
  14. Systrace Smoothness in Action 2: Case Analysis - MIUI Launcher Scroll Jank Analysis
  15. Systrace Smoothness in Action 3: FAQs During Jank Analysis
  16. Systrace Responsiveness in Action 1: Understanding Responsiveness Principles
  17. Systrace Responsiveness in Action 2: Responsiveness Analysis - Using App Startup as an Example
  18. Systrace Responsiveness in Action 3: Extended Knowledge on Responsiveness
  19. Systrace Thread CPU State Analysis Tips - Runnable
  20. Systrace Thread CPU State Analysis Tips - Running
  21. Systrace Thread CPU State Analysis Tips - Sleep and Uninterruptible Sleep

How to Define Dropped Frames (Jank)?

Read this alongside Perfetto 8: Vsync and performance analysis. Distinguish a missed app deadline, a buffer that was never presented, uneven visual updates, and increased input-to-display latency. They do not always describe the same event.

Display periods are about 16.67 ms at 60 Hz, 11.11 ms at 90 Hz, and 8.33 ms at 120 Hz. Those numbers alone do not define the budget for a main-thread slice: target frame rate, scheduled deadlines, CPU/GPU pipelining, and target presentation time also matter. FrameTimeline, available from Android 12, provides related evidence when captured. Start with Expected/Actual Timeline, jank type, and presentation status, then follow app, RenderThread, SurfaceFlinger, fence, and scheduling tracks. Older traces without this data support narrower conclusions.

Judging Jank on the App Side

At first glance, you might think: the main thread took more than 16.6 ms, so surely that is a dropped frame? Let’s look a little further. The selected interval below is 30.891 ms, with a long main-thread Choreographer#doFrame. This indicates delayed production, but does not by itself prove that a particular display refresh repeated old content. Previously produced, not-yet-presented buffers might temporarily sustain output, or there might be none.

Correlate old app-side method timings with downstream presentation evidence. This does not mean that all app-side frame metrics are incapable of identifying jank.

Judging Jank on the SurfaceFlinger Side

In this old example, the layer has no newly queued item in the relevant interval, and SurfaceFlinger has no corresponding new work. If the scene was expected to update every period, this is consistent with retaining old content for an extra period. No new buffer does not mean that the app’s layer disappears or is omitted from composition. Its latched content can remain, including when other layers update. Reusing content for a static scene or a lower-rate source is not inherently jank.

Here is the SurfaceFlinger side of the first example, where previously produced items remain available as latch candidates:

SurfaceFlinger

The layer counter measures queued items, not the total allocation count or GPU completion. A positive value and visible SurfaceFlinger work do not independently establish on-time presentation or unchanged input latency. Follow frame numbers/tokens, acquire fences, and actual presentation. Values 0/1/2 are the range seen in this sample, not a universal limit.

Logical Jank

Content can also advance incorrectly despite timely output. Here, “logical jank” describes cases such as a flinging list unexpectedly moving 20, 10, then 30 pixels when its animation curve calls for a different progression. Users can see discontinuity even when rendering and presentation meet their deadlines.

Generic frame tracks may not reveal the cause. Add animation-progress, position, or input-timestamp instrumentation and compare with video. Acceleration, deceleration, and spring motion can be intentional; uneven steps alone are not proof of a bug.

Android 10’s AnimationUtils contains these related methods (signatures only):

1
2
3
public static void lockAnimationClock(long vsyncMillis)
public static void unlockAnimationClock()
public static long currentAnimationTimeMillis()

In the Android 10 source, the first two are hidden test/Framework interfaces. Choreographer freezes a thread-local animation clock while processing a frame so animations use a consistent time. currentAnimationTimeMillis() uses the uptime time domain, not calendar wall time. Android 16 also has an internal overload carrying expected presentation time.

Apps normally use animation frameworks or Choreographer frame timestamps instead of calling those hidden methods. Actual callback execution can jitter with scheduling. A consistent frame-time basis helps coordinate animation updates, but cannot recover missed display updates or guarantee that system animations never have logic errors.

For other causes of jank, see:

  1. Overview of Jank Causes in Android - Methodology
  2. Overview of Jank Causes in Android - System Side
  3. Overview of Jank Causes in Android - App Side

BufferQueue and Triple Buffer

BufferQueue

BufferQueue connects a producer and a consumer using graphical-buffer handles and synchronization information; the queue does not copy the entire pixel image. The endpoints may share a process or cross a process boundary. The AOSP BufferQueue documentation introduces the model:

  1. The producer calls dequeueBuffer() to request a free slot, with dimensions, pixel format, and usage parameters. Before overwriting old content, it must respect the returned release fence. This requests an available buffer, not the release of an arbitrary one.
  2. After submitting CPU/GPU drawing work, the producer uses queueBuffer() to submit the buffer and fence. GPU work can still be pending; queued does not mean rendering is complete.
  3. The consumer uses acquireBuffer() to obtain an item and satisfies its acquire-fence dependency before reading the pixels. Obtaining the handle and accessing the content need not happen at the same time.
  4. The consumer returns the slot with releaseBuffer() and a release fence. Reuse must still wait for the relevant accesses to finish.

In ordinary, non-shared mode, slots commonly follow FREE → DEQUEUED → QUEUED → ACQUIRED → FREE. Ownership state does not replace fence completion. Cancellation, dropping, and other paths also change state; see Android 16 BufferSlot.h.

Vsync schedules rendering and presentation; ownership and fences synchronize access. The four queue operations are not all directly triggered by Vsync. Related articles:

  1. Systrace Basics - Vsync Explained
  2. Android Rendering Based on Choreographer

Producer and consumer are still rather abstract. Let’s make that concrete with the app and SurfaceFlinger: in this old layer-BufferQueue path, the app produces buffers and SurfaceFlinger consumes them. The app dequeues, draws, and queues; SurfaceFlinger acquires during composition and releases old content when downstream use allows it. Returning from a SurfaceFlinger CPU composition function does not necessarily make the storage safe to overwrite: GPU/HWC dependencies can remain.

Newer systems have a different process boundary. Android 16 BLASTBufferQueue creates a queue and consumer in the client, then submits buffers to SurfaceFlinger through SurfaceControl transactions. Limits depend on producer/consumer constraints and modes, with refresh rate and work duration also involved. “Triple buffering” does not mean every window always allocates or occupies exactly three buffers.

So how many buffers does the queue need to keep things moving? The diagrams below use one, two, and three image buffers to illustrate overlap. Front/Back are roles, not fixed slots; A/B/C are reused storage, not permanent frame numbers. For hardware-accelerated rendering, the CPU work includes recording and submitting GPU commands. CPU, GPU, and SurfaceFlinger do not each permanently own a separate pixel buffer. The screen row denotes displayed content, which may pass through a composition target or HWC rather than directly scanning the app’s memory.

Single Buffer

With one image buffer, producer writes and consumer reads need coordination; they cannot freely overlap as in an ordinary multi-buffer pipeline.

Single Buffer

This idealized diagram schedules CPU, GPU, composition, and display in sequence. A Vsync offset changes planned timing, but neither guarantees that work finishes within budget nor replaces synchronization:

Single Buffer

Writing the same storage while it is being scanned can expose a mixture of old and new content, causing tearing. Merely missing a rendering deadline does not imply tearing: a correctly synchronized multi-buffer system generally delays presentation or retains old content.

Single Buffer

Single/front-buffer rendering is not obsolete in every context. The low-latency front-buffer path, available from Android 10/API 29, supports small updates such as handwriting while managing tearing risk. It is not a general replacement for full-screen View rendering.

Double Buffer

Two buffers let the consumer use one while the producer works on another reusable allocation:

Double Buffer

Here is the ideal A/B alternation. The fourth note at the bottom of the original diagram incorrectly says that the app produces A; its blue interval means produce B, consume A.

DoubleBufferPipline_NoJank

In the next example, production and release timing prevent timely preparation, and display periods two and four repeat old content. This is one particular schedule, not a universal outcome of two long app frames. The final clause of the fifth note should say reuse the released A for production.

Double Buffer

Triple Buffer

An extra allocation can overlap some CPU submission work with earlier GPU/consumer work, reducing idle time spent waiting for a buffer’s return. If one buffer is being consumed and another is still being drawn by the GPU, the third may be available. There are not always two free buffers, and the extra allocation does not make unfinished GPU results ready.

Triple Buffer

In this example, C can be prepared during period two and displayed in period four. Period two still repeats B. The fourth note under the original diagram was copied from the double-buffer case: it should describe displaying C, not another missed update. The SurfaceFlinger row shows both B and A in period five; this simplified drawing does not establish that both are presented in one display refresh.

TripleBufferPipline_NoJank

Together, the diagrams show two repeated-content periods becoming one in this model. More available storage can help under these conditions; it does not eliminate every source of jank.

TripleBuffer_VS_DoubleBuffer

The Role of Triple Buffer

Mitigating Jank

Extra buffer capacity can absorb short production or consumption fluctuations. The benefit depends on which stage is waiting, when buffers are released, and whether useful not-yet-presented content already exists. A delay temporarily hidden by buffering is not necessarily a GPU problem.

Mitigating Jank

This is the earlier sample again. Queue activity helps correlate production and consumption, but counters and composition slices alone do not establish on-time presentation. Sustained overload eventually exhausts the available capacity.

Reducing Wait Times

With two buffers, a producer may wait for an earlier allocation to be released before continuing. In the diagram, B becomes reusable only after downstream use and dependencies permit it. App and SurfaceFlinger Vsync phases can differ; simultaneous callbacks are not a prerequisite for this wait.

Reducing Wait Times

In the old HWUI path, a long RenderThread dequeueBuffer can also delay a later main-thread synchronization. The image illustrates that relationship; identify the actual blocking point with thread states and fence/Binder evidence:

Reducing Wait Times

A third buffer can shorten some waits. It can still block when all buffers are in use, consumption falls behind, or fences remain unsignaled. When HWUI releases the main thread also depends on its execution path; the entire DrawFrame duration is not necessarily a main-thread wait.

Lowering GPU and SurfaceFlinger Bottlenecks

The potential reduction is in additional waiting caused by buffer shortages, not in the computation required from the GPU or SurfaceFlinger. Submitted commands and dependencies drive GPU work; it does not simply start when SurfaceFlinger decides it wants a buffer. More overlap cannot remove sustained GPU overload, long composition, or scheduling delays.

These old images show SurfaceFlinger Binder-thread waits and long app RenderThread dequeueBuffer calls. They are clues for investigating consumer backpressure. The images and attachment filenames alone do not establish a controlled experiment proving that switching from double to triple buffering made the problem disappear.

Memory and Latency Costs

Extra buffers consume graphics memory; the size depends on stride, format, compression, and allocation. Producing too far ahead can queue not-yet-presented content and increase input-to-display latency, potentially appearing as Buffer Stuffing in FrameTimeline. Steady output can therefore still feel less responsive. Whether queued content is retained, replaced, or dropped depends on queue mode and presentation policy; not every produced buffer must reach the screen.

Evaluate frame intervals, target presentation, input latency, blocking points, and memory together, rather than relying only on FPS or allocation counts.

Debugging Triple Buffer

Dumpsys SurfaceFlinger

Start with read-only inspection of the build, property, and service state. These commands were not run on a device during this review; fields and permissions vary by system:

1
2
3
4
adb shell getprop ro.build.version.release
adb shell getprop ro.build.fingerprint
adb shell getprop ro.sf.disable_triple_buffer
adb shell dumpsys SurfaceFlinger

An empty getprop result means the property is unset or unavailable, not that its effective value is zero. Check both the source default and the branch that consumes it.

This is an old Buffering stats format. Android 9 SurfaceFlinger accumulates time over activity segments accepted by OccupancyTracker:

  • Active time: included activity time, in seconds, rather than the app’s full lifetime.
  • Two buffer: time share of segments with no recorded queue occupancy above 1.
  • Double buffered / Triple buffered: time shares of segments whose average queue occupancy is below 1, or at least 1 but below 2, respectively.

These are not instantaneous allocation counts or three mutually exclusive usage percentages. Two buffer can overlap the other classifications. A zero Triple buffered ratio does not prove that a third graphics allocation never existed. OccupancyTracker also filters segments, so an absent row must not be converted to zero use.

Disabling Triple Buffer

The following preserves the historical implementation, not a universal device switch. Follow the property’s use rather than inferring behavior from its disable name or log message.

Android Version <= Android P

Only Android 9, android-9.0.0_r1, is verified here; do not extrapolate to all earlier versions or vendor ROMs. SurfaceFlinger reads a default value of 1:

1
2
3
property_get("ro.sf.disable_triple_buffer", value, "1");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

However, BufferLayer::onFirstRef uses a positive condition:

1
2
3
if (mFlinger->isLayerTripleBufferingDisabled()) {
mProducer->setMaxDequeuedBufferCount(2);
}

In this path, value 1 actually raises the producer’s maximum dequeued-buffer count from its default of 1 to 2; value 0 skips that expansion. The property’s name and logging do conflict with its use. It changes this queue’s parameter, not a guaranteed device-wide allocation count of exactly two or three.

Android Version > Android P

In Android 10, android-10.0.0_r1, the read default becomes 0:

1
2
3
property_get("ro.sf.disable_triple_buffer", value, "0");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

BufferQueueLayer::onFirstRef also negates the condition:

1
2
3
if (!mFlinger->isLayerTripleBufferingDisabled()) {
mProducer->setMaxDequeuedBufferCount(2);
}

Thus value 0 allows the expansion and value 1 skips it in this tag: both the default and the consuming condition changed. This does not describe every release after Android P. Android 16’s BLAST path obtains a maximum acquired-buffer count from SurfaceFlinger and combines it with other queue constraints. SurfaceFlinger’s calculation includes refresh rate and app/SF work duration.

The former adb root → setprop ro.sf.disable_triple_buffer → stop/start recipe is no longer provided as a general method. ro.* properties are write-once: ordinary setprop cannot overwrite an existing value even with root. First-setting an unset property is also permission-dependent; see property_service. System-level comparisons need a controlled test build, a verified configuration path and restart scope, and matched device, version, and workload evidence.

References

  1. AOSP BufferQueue and Gralloc
  2. Perfetto FrameTimeline and Buffer Stuffing

Attachments

The Systrace_Demo.zip archive contains five old HTML traces. This review checked the archive and embedded trace text without executing its scripts. Compatibility with current Chrome was not verified. Double/Triple filenames do not independently establish the active configuration; comparisons also need build details, actual queue parameters, and matching workloads.
Download Systrace attachments for this article

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!”

  1. Blogger Intro: Includes personal WeChat and WeChat group links.
  2. Blog Content Navigation: A guide for my blog content.
  3. Curated Excellent Blog Articles - Android Performance Optimization Must-Knows: Welcome to recommend projects/articles.
  4. 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.

Scan WeChat QR Code

CATALOG
  1. 1. Table of Contents
  • Series Article Index
  • How to Define Dropped Frames (Jank)?
    1. 1. Judging Jank on the App Side
    2. 2. Judging Jank on the SurfaceFlinger Side
    3. 3. Logical Jank
  • BufferQueue and Triple Buffer
    1. 1. BufferQueue
    2. 2. Single Buffer
    3. 3. Double Buffer
    4. 4. Triple Buffer
  • The Role of Triple Buffer
    1. 1. Mitigating Jank
    2. 2. Reducing Wait Times
    3. 3. Lowering GPU and SurfaceFlinger Bottlenecks
    4. 4. Memory and Latency Costs
  • Debugging Triple Buffer
    1. 1. Dumpsys SurfaceFlinger
    2. 2. Disabling Triple Buffer
      1. 2.1. Android Version <= Android P
      2. 2.2. Android Version > Android P
  • References
  • Attachments
  • About Me && Blog