Chasing frame drops on Android with GPU rendering inspection

I recently started looking into the Android vitals of my app on the store. I noticed a high percentage of “slow rendering”, therefore, I decided to have a look at the GPU rendering speed profiler.

Slow rendering is pretty bad, especially for a timer app. Dropping frames in your app means your users are experiencing lags and janks. Probably unnecessary work is done, more battery is consumed (critical if your app runs sometimes 30 minutes on the foreground).

It’s easy to think your app run smoothly because you’re using a high-end device while debugging but this is not the case in production where users will use a wide variety of devices including old and slow devices.

To make your app runs smoothly regardless of the device, there is a magic number to keep in mind: 16ms. It’s the maximum time that one frame can take to be drawn on the screen without causing rendering delays.

The GPU rendering speed profiler

From the documentation:

Android includes some on-device developer options that help you visualize where your app might be running into issues rendering its UI, such as performing more rendering work than necessary, or executing long thread and GPU operations.

It’s a developer option that you can enable and will draw some sort of colored histograms on your screen while you execute apps.

The GPU rendering profiler draw histograms on screen

Don’t get scared by the fuzzy display, it’s not really complex to understand.

Each bar height represents the time spent to draw the frame (the green line at the top is the 16ms threshold), the colors show the different stages of the rendering pipeline.

Problems

So after enabling the profiler I started to play around with my app and looked for specific peaks and where the frames were dropped. Turned out I had much more dropped frames than I expected.

onMeasure()

This one clearly showed a huge portion of the rendering was colored in light green, which corresponds to Measure/Layout according to the documentation. I started to investigate the onMeasure() methods of my custom views.

After adding a few logs I noticed something strange, the onMeasure() of one of my custom view (round progress bar) was called way too often. In fact, it was called every time I was updating the “current round” value. This couldn’t be a coincidence.

My layout looked like this:

But why my progress bar onMeasure() gets called when I update the “current round” TextView text?

After thinking about it my guess was that since I was setting a new text in my TextView , it needed to compute its new width to place the TextView accordingly and would probably result in a requestLayout() that would call the onMeasure() of my progress bar.

I changed the TextView layout_width from wrap_content to a fixed width and it solved the issue of excessive onMeasure() calls.

Before/after setting a fixed with on the “crurent round” TextView

This one was curious at first, but such an easy win.

onDraw()

On this screenshot we can notice that this time the most time spent in the GPU rendering is in dark blue color which is the Draw phase. I assumed I had to look around some heavy computation in the onDraw() of my custom views.

I first tried to look at the logic itself on the onDraw() call, seeing if anything could be simplified/avoided, removing any new objects creation and caching things. This didn’t have any effect, I still had this blue bar on the GPU rendering.

After further investigations, I noticed it was when I was drawing the foreground of my progress bar (the part that is a solid white) that the spikes would be persistent, but not the background (which uses the same Path ). This was weird, I didn’t understand at first, but then the only difference between the background circle and the foreground one was the Paint object used.

After further digging, I found what was my issue:

progressPaint.setShadowLayer(12F, 0F, 0F, ColorUtils.setAlphaComponent(progressPaint.color, 90))

setLayerType(LAYER_TYPE_SOFTWARE, progressPaint)

Simply adding the small shadow on my progress bar would make everything super slow (the progress bar is animated, it needs to be redrawn very often). I didn’t really find a way around that, drawing shadows on Android is very costly.

I just removed those two lines and rendering time went straight down.

Before/After removing the shadow layer on the circle Paint

You can see on the left, the progress bar has a small “glow” which is the shadow that is drawn around the circle. On the right it is removed.

Conclusion

After a few hours of investigations, I managed to bring the Slow rendering of the most used screen of my app down.

This was interesting as it really showed that there is no magic and if your app experience janks there is probably a rational reason behind it.

I encourage you to give the profiler a try and start optimizing your apps rendering!