Introduction
This is an annotated edition of Android Performance Case Study Follow-up by Romain Guy. The first-person case narrative below belongs to the original author.
Technical review — 2026-10-13: The 2015 Falcon Pro 3 / Moto X 2014 case is retained. Android Device Monitor, which hosted Tracer for OpenGL ES and Hierarchy Viewer, was deprecated in Android Studio 3.1 and removed in 3.2. The old workflow is historical; an Android 12+ Perfetto workflow is added below. The original website could not be reopened during this review; the technical corrections use the linked official documentation and versioned source.
Case study
Two years ago, I published Android Performance Case Study to help Android developers understand the tools and techniques needed to identify, track, and optimize performance bottlenecks.
That article used the Twitter client Falcon Pro as a case study. Its developer, Joaquim Vergès, was kind enough to let me use his app as an example and quickly addressed the issues I found. Fast forward to recently: Joaquim was building Falcon Pro 3 from scratch. Before its release, he contacted me about a scrolling performance issue. Once again, I had to analyze it without access to the source code.
Joaquim had already exhausted most tools. He confirmed that Overdraw wasn’t the culprit and suspected that his use of ViewPager (the old Support Library version; see the current AndroidX reference) was the cause. He sent over these screenshots:

Joaquim used the built-in GPU profiling tool to identify dropped frames. The left screenshot shows the timeline scrolling without a ViewPager; the right shows it with one (captured on a 2014 Moto X). The problem was stark.
My first thought was to check if the ViewPager was abusing hardware layers, as it looked like the system was performing a layer update every frame during scrolling. However, the hardware layers updates debugging tool didn’t show much. I inspected the layout with Hierarchy Viewer, and to my surprise (or rather, disappointment for a quick fix), the ViewPager was behaving correctly.
Then I turned to another powerful but less-used tool, Tracer for OpenGL ES. It recorded application GLES calls and, with framebuffer readback enabled, could show the progressive frame images. This describes the old GLES rendering path, not a GPU timer covering every modern rendering backend. See the historical tool documentation.
Historical workflow — Android 4.3 onward: Reordering and merging drawing commands made grouping them by View harder. This switch is confirmed in AOSP android-4.4.4_r2 Properties.h; it disables the old HWUI draw deferral before the app starts:
1 | adb shell getprop debug.hwui.disable_draw_defer |
Record the original value, then restore it and restart the app after inspection. If it was unset, false restores the normal path. Disabling an optimization changes the workload: do not use those timings as a performance baseline or assume the property applies to current Android releases.
Reading old GL traces: Click a blue draw call and the Details panel shows the framebuffer after that call. Clicking through them in order lets me watch the frame being built; that is usually how I analyze it. Uploads, state changes, and waits can also be expensive, so other calls cannot simply be ignored when investigating cost. The displayed Wall Time / Thread Time is not pure GPU execution time.
After scrolling through Falcon Pro for a bit, I examined the GL trace. I was surprised to find many SaveLayer/ComposeLayer groups. These describe drawing into an offscreen layer and compositing it back, not thread-blocking instructions named SaveLayer.

These groups indicate an offscreen drawing target. Canvas.saveLayer() redirects drawing there until a balancing restore() composites it back. That is different from a retained View hardware layer configured through setLayerType(LAYER_TYPE_HARDWARE, ...). The saveFlags overloads were deprecated in API 26; the overloads without that argument have been available since API 21. See the Canvas reference.
For the old hardware-accelerated path discussed here, a typical combination was:
- A translucent View alpha:
0 < alpha < 1. onSetAlpha(int)returningfalse, leaving alpha handling to the framework.- No configured View layer:
getLayerType()returningLAYER_TYPE_NONE. hasOverlappingRendering()returningtrue.
The Android 4.4.4 View implementation provides the historical context. These conditions explain the example; they do not imply an identical temporary-layer path on every Android release.
Chet and I have discussed why to use alpha with care. Offscreen rendering can add render-target changes, storage, and compositing work. On the Moto X 2014 in this case, that made the layers worth investigating. Vendor names such as Adreno, SGX, or Nvidia alone do not predict the cost on a current GPU; the hardware, driver, rendering backend, and content all matter.
So, what was creating these layers? Tracer provided the answer. In the image above, the GL commands in the SaveLayer group were rendering a small circle. Let’s look at the app:

See the circles at the top? Those are ViewPager indicators. Joaquim was using a third-party library to draw them. Interestingly, the current page was indicated by a solid white circle, while others were a “grayish” white. The library was achieving this “gray” by drawing white circles and then calling setAlpha() on the View to make them semi-transparent.
Solutions
There are several ways to address this while preserving the intended appearance.
1. Draw the inactive color directly
Keep the View’s alpha at 1 and draw the dot with the appropriate Paint color or alpha. An opaque gray can replace translucent white only when the background and intended color make them equivalent. They are not interchangeable over a changing background.
2. Report non-overlapping drawing accurately
hasOverlappingRendering() is available from API 16. A View that draws only one dot, with no overlapping background or foreground, can override it as follows:
1 |
|
This lets the framework apply alpha to individual drawing operations instead of compositing the content as a group. It describes overlap within the View’s own drawing, not whether the View intersects a neighboring View. Returning false when content actually overlaps can change the color or opacity at intersections. See the API contract.
3. Handle alpha inside the custom View
The argument of onSetAlpha(int alpha) is already an integer from 0 to 255. This method fragment belongs in a custom dot View; indicatorPaint is the initialized Paint also used to draw its only dot in onDraw():
1 |
|
Returning true makes the View responsible for applying opacity to all its drawing. Do not omit other content or overwrite the alpha with a later setColor(). The Chinese edition’s paint.setAlpha((int) alpha * 255) was incorrect: a 0–1 float such as getAlpha() needs multiplication before conversion, (int) (alpha * 255). The integer received by onSetAlpha() needs no multiplication.
For this single-dot View, the second option is straightforward; pre-API-16 versions require another color or drawing approach. I suspect Joaquim may already have replaced the library with his own indicator.
I hope this post illustrates how seemingly “innocent” or “safe” operations can hide major performance pitfalls. Never assume—always verify, measure, and trace.
2026 update: investigating the same issue today
The useful lesson is tracing an abnormal frame back to the content being drawn. A current investigation does not require restoring the old SDK tools:
- Find the affected frames. On Android 12 / API 31 and later, capture Perfetto with FrameTimeline. Relate expected and actual frame times to the main thread, RenderThread, and SurfaceFlinger. A saveLayer marker alone does not establish the cause of jank.
- Inspect the content. From Android Studio 3.1, Layout Inspector replaces Hierarchy Viewer for hierarchy/property inspection, without reproducing its layout-timing results. For individual GPU commands, AGI requires a supported Android 11+ physical device and driver. Its GLES frame capture runs through ANGLE to Vulkan, so it is not an unchanged replay of the native GLES path. See the Device Monitor retirement notice.
- Change the confirmed issue and compare again. Keep the device, scenario, and build conditions comparable. Verify the appearance over different backgrounds, selection states, and alpha animations as well as the measured frame behavior.
The renderer has also changed. In android-16.0.0_r1 RenderNodeDrawable.cpp, effective layer type and overlap properties select different paths. The source even retains an alpha caused saveLayer trace label to flag risk on older releases. That label alone does not prove a fresh offscreen allocation, and the 2015 timing conclusions cannot be carried forward unchanged.
Appendix
Historical further reading: Android Tips: Best Practices for Using Alpha. It could not be reopened during this review; use the official references and versioned source above for the current contracts.
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
- Blog Content Navigation: A guide for my blog content.
- Curated Excellent Blog Articles - Android Performance Optimization Must-Knows
- 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.
