Profiling Android apps with Perfetto
Tags: android, profiling, perfetto
Published: 2023-07-31 21:59:18

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](https://perfetto.dev/docs/) 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:
```bash
# not needed, if you are using Android 11+
adb shell setprop persist.traced.enable 1
```

To create a profileable build of your app, add the following line to your `AndroidManifest.xml`:
```xml
<profileable android:shell="true" tools:targetApi="q" />
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/339sj4wanbaqnzluqqd6.png)

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
```bash
adb shell perfetto \
  -c - --txt \
  -o /data/misc/perfetto-traces/trace \
<<EOF
...
```
The trace file will be saved in the /data/misc/perfetto-traces/trace directory.

```bash
adb pull /data/misc/perfetto-traces/trace /local/path/to/your/trace
```

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](https://github.com/android/nowinandroid) app as the sample app. Your trace might look like the following:


![A sample perfetto trace](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbdbud3egsrevcu6srw6.png)

*You can find the trace [here](https://github.com/sidky/nowinandroid/blob/main/trace.perfetto-trace), 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](https://developer.android.com/jetpack/androidx/releases/tracing). You can add the dependency as follows:
```gradle
dependencies {
  ...
  implementation("androidx.tracing:tracing-ktx:1.1.0")
  ...
}
```
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](https://developer.android.com/reference/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:

```sql
SELECT ts, dur, name FROM slice WHERE name LIKE '%NETWORK:%'
```

Which generate this list for the trace I was using.

![A table of network requests made during the trace](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ehz53tzqca23vngk6bmv.png)

I modified the OkHttpClient builder to trace them
```kotlin
    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()
```

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] (https://perfetto.dev/docs/data-sources/memory-counters).

Happy profiling!

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