Use Systrace to debug your Android app performance

React Native

Android

Performance

One of the key metrics to measure the performance of your app is the frame rate. You want your app to run at 60fps (frames per second) to give an impression of smoothness.

This video from Google explains it well:

  • 60 is high enough for good perceived smoothness
  • there are less benefits in higher numbers
  • your frame rate should be constant and not drop

 

Is this also valid for a React Native app?

Yes! But you also have to take into account the JS thread.
In a React Native app, the UI is driven by the JS. The UI thread should still run at 60fps, but you also want the JS thread to ideally not go under 60. JavaScript is driving the UI, so if it's not performing well, especially if it goes down to 0, your app will start to feel unresponsive.

If you're having performance issues in your app, one way to figure out if it's coming from the UI thread (so likely an issue on the native side) or the JS thread, you can simply use the Performance monitor, available out of the box in the development menu.

If you see the JS thread going down a lot, checkout this great article by Louis to debug JS performance issues.

If you see the UI thread going down a lot on Android, you're in the right place! I'll show you how I solved some issues in our app, using Systrace.

If you'd rather watch than read, you can also checkout the lightning talk I gave on the subject at React Summit 2021:

 

 

Measuring the UI thread performance in production

The good news is the play store reports it out of the box! You can check out the Excessive slow frames stats in the Vitals section.

You should something like this (it used to be called Slow UI rendering): Sloe UI rendering stats on play store vitals

In our case, the numbers mean we had 22% of sessions for which more than 50% frames took longer than 16ms to render. Essentially, this means 22% of sessions in our app are laggy. This is horrendous! 😱

Checking the top apps on the play store, we can actually see that most apps have this number below 4%, much better.

Slow UI rendering vs top apps on play store vitals

Which means we definitely have some issues in our app!

 

Systrace dive in

It's time to investigate what the problem is. And for a native Android performance issue, a nice tool to use is Systrace.

Systrace is short for "System trace". It's a python script located in the platform-tools folder of your Android SDK (for me, with an Android Studio install on macOS, it's ~/Library/Android/sdk/platform-tools/systrace/systrace.py).

It's simple to use:

  • Connect an Android device or an emulator to your computer with your app installed (to test performance, it's usually best to take a low end device to aggravate any performance issues)
  • Then run python /path/to/systrace.py -o trace.html sched gfx view -a com.yourappid (replace /path/to by the proper path for your computer and com.yourappid by your app bundle id)
  • Use your app
  • Then press "enter" to stop the trace in the terminal
  • You should now have a trace.html file created
  • In Google Chrome, open chrome://tracing/ and click "Load" on the top left to load your trace file. You should see something something like this:

First look at a trace file

Beautiful right? A bit daunting, but very colorful ☀️.

Note that Google is actively working on Perfetto, a better version of Systrace. But at this time, the features we need to investigate frame drops are not yet implemented.

All right, let's dive in. The first thing you want to do is find your app on the left side panel based on your bundle id:

Find your app in the left panel

Next, since we're talking about frame rate issues, what's really interesting for us is the frame line: Take a look at the "F"s line

Let's zoom in on this line. In the colourful center panel, you can navigate with the wasd keys, try it out! After zooming in with the w key you should see something like this:

We see a lot of red Fs on this line

On that line, there are a lot of "F"s, some are red, some are orange, some are green. You might guess what that means. "F" is of course for frame. Red "F" = bad ❌, green "F" = good ✅. Indeed a green "F" corresponds to a frame that was able to be calculated under 16ms, which means 60fps can be achieved. Orange and red "F"s are failure to do so.

So if you want analyze performance issues in your app, you want to take a look at sources of red "F"s.

In my case, I had only opened the app and waited for a few seconds on the home screen, and the trace file was already giving me a lot of red "F"s, as you can see above. There were 2 major parts of the trace file concerned, here's how I found where those issues were coming from, thanks to Systrace:

 

1st method: Investigate the threads

On the left panel, you should see threads under your app.

For a React Native app, you would typically see:

  • mqt_js: this is the JS thread, so that can be useful to check when the JS thread is busy
  • mqt_native_modules: this is the thread used by bridges
  • some OkHttp threads, this is the native library used by React Native to make API calls
  • some Fresco threads, this is the native library used by React Native to display images

But what interests us here is the UI thread: this is the thread that should be able to calculate rendering in 16ms or less to achieve 60fps. The performance monitor for React Native actually reports its frame rate.

First, it's a good idea to click "View Options" on the top right, and check "Highlight VSync". This will basically highlight all the 16ms frames in which calculation must be done.

Now if you zoom in (with the W key) on a single frame where you have a red "F", you should something like this for the UI thread:

Zoom in on the UI thread in a single frame

For the UI thread, you see a stack of calls. The top one (which calls the one below it) is Choreographer#doFrame, this is the one that should take less than 16ms. We can see that here, this is definitely not the case:

The draw function is doing too much work on the UI thread

We see here that it's calling traversal, then draw. This is very low-level. Essentially this is Android going through the hierarchy of components and checking what needs to be redrawn on the screen. We can see here that the draw function is taking more than 16ms, so there must be something very expensive redrawn on every frame here.

You might be more lucky and have more information in your trace, but in my case, I had to look further to find the issue.

We can scroll down a bit and find the Render thread. The rendering thread is running on the GPU. It runs heavy calculation with high performance (since it does it on the GPU) and forwards the results to the UI thread. For instance, if you use a native animation (using native driver on React Native) such as a translation, the calculation would run on the Render Thread, that's why the performance would be much better.

Here we're luckier! We can see there's function taking a lot of time to complete related to a Lottie animation:

Clear that a Lottie animation needs optimization

In fact, we had a Lottie animation running at the start that needed optimization. Lottie has a neat guide on how to do it if you're curious.

With our Lottie animation optimized, that was the first issue solved! Let's take a look at the second one.

 

2nd method: Check what keeps the CPU busy

At the end of our trace file, we had still a lot of red "F"s. But we were not as lucky as before to find the root cause investigating what goes on in the threads.

Harder to see what's wrong in our second batch of red Fs

Now's a good time to bring up the Kernel panel.

Take a look at the Kernel panel

This panel, usually at the top of the trace file, tells us what each of the device CPUs is doing at any given time (my device, a Wiko Fever, has 8 CPUs).

For instance, we can see below that CPU 4 is running the JS thread for a few ms. We can also see CPU 2 doing something Fresco related for a short time.

See what each CPU is doing at any given time

A very useful feature is this: you can draw a rectangle with your mouse over a section:

Trace a rectangle in the Kernel panel

and you will see in the panel below, if you click on Wall duration what's taking the most calculation time in that time frame.

See what takes the most CPU time

In our case, we see that the Render thread is indeed quite busy. But we knew that already with our previous investigation. We don't know what's causing it to be so busy though.

The second one, however is more interesting:

In our case, a task called Chrome_InProcRe takes a lot of time

It's called Chrome_InProcRe and a quick search reveals that this is related to a WebView. So it would seem that there is a WebView taking most of rendering time, and thus being the cause of performance issues in our app.

In fact, on the home screen, we have ads running inside a WebView and they (severely) needed to be optimized!

A happy conclusion

And with both our issues solved, let's admire the beautiful new trace full of green "F"s now:

With both problems fixed, green Fs everywhere

And our slow frame stats on the play store are much better! 🥳

 

Systrace is a powerful tool to have in your toolbox and I hope it will help you as well! Let me know how you get on with it 😉