DEV Community

Manzurur Khan
Manzurur Khan

Posted on

Profiling Android apps with Perfetto

Have you ever tried to analyze the performance of your Android apps, or detect any performance bottlenecks? If so, you need to collect performance data.

If you're an Android developer, you may have used the Android Studio Profiler or systrace in the past. However, for complex scenarios, I've found that the Android Studio Profiler can be inflexible, and systrace can be difficult to work with.

I've found Perfetto to be a useful middle ground. It has some advanced features, but it also has easy-to-use visualization and command-line interfaces.

What is Perfetto?

Perfetto is a general purpose trace analysis tool. It can be used with web and linux native apps, along with Android apps. This section focuses on collecting traces from Android applications.

How to collect traces with Perfetto?

To collect a trace with Perfetto, you may need

  • An Android device running Android 9 (P) or later
  • A profileable APK To enable tracing services on Android 9 (P) and Android 10 (Q), you can run the following command:
# not needed, if you are using Android 11+
adb shell setprop persist.traced.enable 1
Enter fullscreen mode Exit fullscreen mode

To create a profileable build of your app, add the following line to your AndroidManifest.xml:

<profileable android:shell="true" tools:targetApi="q" />
Enter fullscreen mode Exit fullscreen mode

You may not need to make your app profilable, if you are profiling a debug app. But if you are trying to debug performance issues, you probably should use a profilable release build.
Once you have your APK installed and connected to your development machine, you can use the Perfetto UI to collect a trace. To do this, go to https://ui.perfetto.dev/ and click "Record new trace".

Web interface to record a Perfetto trace

In the Perfetto UI, you can configure the metrics you want to track and the recording mode. At a minimum, I recommend enabling the following metrics:

  • Android apps & svcs > Atrace userspace annotations

Perfetto UI allows collecting trace directly from your device, through WebUSB. I have had issues in the past, using it from Firefox. But you can find the command to collect manually, from the tab Recording command. The command will look like the following

adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/trace \
<<EOF
...
Enter fullscreen mode Exit fullscreen mode

The trace file will be saved in the /data/misc/perfetto-traces/trace directory.

adb pull /data/misc/perfetto-traces/trace /local/path/to/your/trace
Enter fullscreen mode Exit fullscreen mode

Now, you can open your trace in Perfetto UI, from Open trace file.

How do I read this trace?

For this example, I was profiling the nowinandroid app as the sample app. Your trace might look like the following:

A sample perfetto trace

You can find the trace here, if you want to play around with it

  • Traces are grouped by process. The process name may include the package name, if you don't limit tracing to your app only.
  • The process name is generally followed by a number. In this example, the process name is Process 9305. I believe this is the main thread, but I have not been able to confirm that.
  • Your trace may include "Actual Timeline" and "Expected Timeline". Use these timelines to identify any frame drops. In this case, you can see that there were 5 slow renderings of frames (154407810, 15440847, 154408112, 154408141, 154408368).
  • If you check the row for Thread 9305, you can see why rendering was slow. The row shows that the thread was waiting for a response from the GPU. This is likely the cause of the frame drops.

Can I add my own sections?

The sample app uses Jetpack Compose. Compose components add these additional tracing sections. You can add your own sections as well, which can give you more insight into performance bottlenecks.

To do this, use Jetpack Tracing. You can add the dependency as follows:

dependencies {
  ...
  implementation("androidx.tracing:tracing-ktx:1.1.0")
  ...
}
Enter fullscreen mode Exit fullscreen mode

This will give you access to the following two methods:

  • trace()
  • traceAsync()

You can wrap your statements in a trace() block. Each trace will show up in the row for their corresponding thread. However, if the trace you are measuring is not bound to any one thread, you can use traceAsync(). In the example, the Sync section is an asynchronous trace.

If you can't use the utility methods (for example, if your trace doesn't begin and end in the same method), you can use the androidx.tracing.Trace class and a combination of the beginSection()/endSection() or beginAsyncSection()/endAsyncSection() methods.

Querying the Traces with SQL

The flame graphs generated with Perfetto are a great way to visualize performance data, but for certain cases, you might want to filter the data differently. For example, you might want to:

  • Find all network calls that happened during the trace.
  • Identify if certain types of item binding in a RecyclerView are slower than others.
  • etc.

You can add your own traces and run queries like the following in the Query (SQL) tab:

SELECT ts, dur, name FROM slice WHERE name LIKE '%NETWORK:%'
Enter fullscreen mode Exit fullscreen mode

Which generate this list for the trace I was using.

A table of network requests made during the trace

I modified the OkHttpClient builder to trace them

    fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder()
        .addInterceptor(
            HttpLoggingInterceptor()
                .apply {
                    if (BuildConfig.DEBUG) {
                        setLevel(HttpLoggingInterceptor.Level.BODY)
                    }
                },
        )
        .addInterceptor { chain ->
            val request = chain.request()
            Log.e("NETWORK", "NETWORK: ${request.url}")
            trace("NETWORK: ${request.url}".take(126)) {
                chain.proceed(request)
            }
        }
        .build()
Enter fullscreen mode Exit fullscreen mode

This will trace all network requests and allow you to query them using SQL. You can find the full list of data sources that you can query from in the Perfetto documentation.

Happy profiling!

You can find my code I used here: https://github.com/sidky/nowinandroid/

Top comments (0)