Two years ago, I published an articled titled Android Performance Case Study to help Android developers understand what tools and technique can be used to identify, track down, and fix performance issues.
This article focused on Falcon Pro, a Twitter client designed and developed by Joaquim Vergès. Joaquim was kind enough to let me use his application in my article and quickly addressed all the issues I found. All was well until Joaquim started working on Falcon Pro 3, written from scratch. Shortly before releasing his new application, Joaquim contacted me because he needed help figuring out a performance problem that was affecting scrolling (and once again, I did not have access to the source code).
Joaquim used all the right tools and was able to quickly determine what was not causing the issue. For instance, he found that overdraw was not an issue. He was however able to narrow down the problem to the use of a ViewPager. He sent me the following screenshots:
Joaquim used the system’s on-screen GPU profiling tool to detect framerate drops. The screenshot on the left shows the performance of scrolling a timeline without a
ViewPager and the screenshot on the right shows performance with a
ViewPager (he used a 2014 Moto X to capture this data). The root cause seems pretty obvious.
My first idea was to see whether the
ViewPager was somehow misusing hardware layers. The performance issue we observed could have been caused by a hardware layer updated on every frame by the list’s scroll. The system’s hardware layers updates debugging tool did not reveal anything. I double checked with HierarchyViewer and I was satisfied that the
ViewPager was behaving correctly (the contrary was unlikely anyway and would have been troublesome).
I then turned to another powerful, seldom used, tool called Tracer for OpenGL. My previous article explains how the tool works in more details. All you need to know is that this tool collects all the drawing commands sent by the UI toolkit to the GPU.
Android 4.3 and up: Tracer has unfortunately become a little more difficult to use since Android 4.3 when we introduced reordering and merging of drawing commands. It’s an amazingly useful optimization but it prevents Tracer from grouping drawing commands by view. You can restore the old behavior by disabling display lists optimization using the following command (before you start your application):adb shell setprop debug.hwui.disable_draw_reorder true
Reading OpenGL traces: Commands shown in blue are GL operations that draw pixels on screen. All other commands are used to transfer data or set state and can easily be ignored. Every time you click on one of the blue commands, Tracer will update the Details tab and show you the content of the current render target right after the command you clicked is executed. You can thus reconstruct a frame by clicking on each blue command one after another. It’s pretty much how I analyze performance issues with Tracer. Seeing how a frame is rendered gives a lot of insight on what the application is doing.
While perusing the traces collected during a scroll in Falcon Pro I was surprised to see a series of
ComposeLayer blocks of commands (click the picture to enlarge):
These blocks indicate the creation and composition of a temporary hardware layer. These temporary layers are created by the different variants of Canvas.saveLayer(). The UI toolkit uses
Canvas.saveLayer() to draw Views with an alpha < 1 (see View.setAlpha()) when specific conditions are met:
Chet and I explained in several presentations why you should use alpha with care. Every time the UI toolkit has to use a temporary layer, drawing commands are sent to a different render target, and switching render target is an expensive operation for the GPU. GPUs using a tiling/deferred architecture (ImaginationTech’s SGX, Qualcomm’s Adreno, etc.) are particularly hurt by this behavior. Direct rendering architectures such as Nvidia’s fare better. Since the Moto X 2014 devices Joaquim and I were working with use a Qualcomm Adreno GPU, the use of multiple temporary hardware layers was most likely the root cause of our performance problem.
The big question thus become: what is creating all these temporary layers? Tracer gave us the answer. If you look at the screenshot of Tracer you can see that the only drawing command in the
SaveLayer group of OpenGL operations renders what appears to be a circle in a small render target (the tool magnifies the result). Now let’s look at a screenshot of the application:
Do you see these little circles at the top? That’s a
ViewPager indicator, used to show the user her position. Joaquim was using a third party library (I don’t remember which one) to draw these indicators. What’s interesting is how that library draws the indicator: the current page is indicated by a white circle, the other pages with what appears to be a gray circle. I say “what appears to be a gray” because the circles are actually translucent white circles. The library uses a View for each circle (which is in itself wasteful) and calls
setAlpha() to change their color.
There are several solutions to fix this problem:
hasOverlappingRendering()and the framework will set the proper alpha on the
onSetAlpha()and set an alpha on the
Paintused to draw the “gray” circles
The easiest solution is the second one but it is only available from API level 16. If you must support older versions of Android, use one of the other two solutions. I believe Joaquim simply ditched the third party library and used his own indicator.
I hope this article makes it clear that performance issues can arise from what appears to be innocent and harmless operations. So remember: don’t make assumptions, measure!