Android Vitals - Profiling App Startup 🔬
Tags: android, performance, profiling, trace
Published: 2020-11-23 23:03:19

> Header image: *The In-Between* [by Romain Guy](https://www.flickr.com/photos/romainguy/26364010648/).

My previous articles focused on measuring Android app start in production. Once we've established a metric and scenarios that trigger a slow app start, the next step is to improve performance.

To understand what makes the app start slow, we need to profile it. Android Studio provides several types of [profiler recording configurations](https://developer.android.com/games/optimize/cpu-profiler?hl=en#configurations):

* **Trace System Calls** (aka systrace, perfetto): Low impact on runtime, great for understanding how the app interacts with the system and CPUs, but not the Java method calls that happen inside the app VM.
* **Sample C/C++ Functions** (aka Simpleperf): Not interesting to me, the apps I deal with run much more bytecode than native code. On Q+ this is supposed to now also sample Java stacks in a low overhead way, but I haven't managed to get that working.
* **Trace Java Methods**: This captures all VM method calls which introduces so much overhead that the results don't mean much.
* **Sample Java Methods**: Less overhead than tracing but shows the Java method calls that happen inside the VM. **This is my preferred option when profiling app startup.**

![trace from sampled java methods](https://dev-to-uploads.s3.amazonaws.com/i/lz5k7pg38ysn3zcpe0ij.png)

# Start recording on app startup

The Android Studio profiler has UI to start a trace by connecting to an already running process, but no obvious way to start recording on app startup.

![recording a trace](https://dev-to-uploads.s3.amazonaws.com/i/zp1x44mern9im1zicag9.png)
 
The option exist but is hidden away in the **run configuration** for your app: check *Start this recording on startup* in the *profiling* tab.

![run configuration](https://dev-to-uploads.s3.amazonaws.com/i/56trfz9ru6x902mfxs4n.png)

Then deploy the app via *Run* > *Profile app*.
 
![profile app](https://dev-to-uploads.s3.amazonaws.com/i/k7jg9l5di7zpnr3l30zg.png)
 
# Profiling release builds

Android developers typically use a **debug** build type for their everyday work, and debug builds often include a debug drawer, extra libraries such as LeakCanary, etc. **Developers should profile release builds rather than debug builds** to make sure they're fixing the actual issues that their customers are facing.

Unfortunately, release builds are non debuggable so the Android profiler can't record traces on release builds.

Here are a few options to work around that issue.

## 1. Create a debuggable release build

We could temporarily make our release build debuggable, or create a new release build type just for profiling.

```gradle
android {
  buildTypes {
    release {
      debuggable true
      // ...
    }
  }
}
```

Libraries and Android Framework code often have a different behavior if the APK is debuggable. ART disables a lot of optimizations to enable connecting a debugger, which affects performance significantly and unpredictably (see [this talk](https://youtu.be/ZffMCJdA5Qc?t=631). So this solution is not ideal.

## 2. Profile on a rooted device

Rooted devices allow the Android Studio profiler to record traces on non debuggable builds.

Profiling on an emulator is generally not recommended - the performance of every system component will be different (cpu speed, cache sizes, disk perf), so an 'optimization' can actually make things slower by shifting the work to something that's slower on a phone. If you don't have a rooted physical device available, you can create an emulator **without Play Services** and then run `adb root`.

## 3. Use simpleperf on Android Q

There's a tool called [simpleperf](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md#prepare-an-android-application) which supposedly enables profiling release builds on non rooted Q+ devices, if they have a special manifest flag. The doc calls it `profileableFromShell`, the XML example has a `profileable` tag with an `android:shell` attribute, and the official manifest documentation shows nothing.

```xml
<manifest ...>
    <application ...>
      <profileable android:shell="true" />
    </application>
</manifest>
```

I looked at the manifest parsing code on [cs.android.com](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/PackageParser.java;l=3878-3885;drc=master):

```java
if (tagName.equals("profileable")) {
  sa = res.obtainAttributes(
    parser,
    R.styleable.AndroidManifestProfileable
  );
  if (sa.getBoolean(
    R.styleable.AndroidManifestProfileable_shell,
    false
  )) {
    ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL;
  }
}
```

It looks like you can trigger profiling from command line if the manifest has `<profileable android:shell="true" />` (I haven't tried). As far as I understand the Android Studio team is still working on integrating with this new capability.

# Profiling a downloaded APK

At Square our releases are built in CI. As we saw earlier, profiling app startup from Android Studio requires checking an option in a run configuration. How can we do this with a downloaded APK?

Turns out, it's possible but hidden under *File* > *Profile or Debug APK*. This opens up a new window with the unzipped APK, and from that you can set up the run configuration and start profiling.

![profile APK](https://dev-to-uploads.s3.amazonaws.com/i/42zjjsi7t4sszdpwyqfj.png)

# Android Studio profiler slows everything down

Unfortunately, when I tested these methods on a production app, profiling from Android Studio slowed down app startup a lot (~10x slower), even on recent Android versions. I'm not sure why, maybe it's the "advanced profiling", which doesn't seem like it can be disabled. We need to find another way!

# Profiling from code

Instead of profiling from Android Studio, we can start the trace directly from code:

```kotlin
val tracesDirPath = TODO("path for trace directory")
val fileNameFormat = SimpleDateFormat(
  "yyyy-MM-dd_HH-mm-ss_SSS'.trace'",
  Locale.US
)
val fileName = fileNameFormat.format(Date())
val traceFilePath = tracesDirPath + fileName
// Save up to 50Mb data.
val maxBufferSize = 50 * 1000 * 1000
// Sample every 1000 microsecond (1ms)
val samplingIntervalUs = 1000
Debug.startMethodTracingSampling(
  traceFilePath, 
  maxBufferSize,
  samplingIntervalUs
)

// ...

Debug.stopMethodTracing()
```

We can then pull the trace file from the device and load it in Android Studio.

## When to start recording

We should start recording the trace as early as possible in the app lifecycle. As I explained in [Android Vitals - Diving into cold start waters 🥶](https://dev.to/pyricau/android-vitals-diving-into-cold-start-waters-5hi6#early-initialization), the earliest code that can run on app startup before Android P is a `ContentProvider` and on Android P+ it's the `AppComponentFactory`.

### Before Android P / API < 28

```kotlin
class AppStartListener : ContentProvider() {
  override fun onCreate(): Boolean {
    Debug.startMethodTracingSampling(...)
    return false
  }
  // ...
}
```

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application>
    <provider
        android:name=".AppStartListener"
        android:authorities="com.example.appstartlistener"
        android:exported="false" />
  </application>

</manifest>
```

~~Unfortunately we cannot control the order in which `ContentProvider` instances are created, so we may be missing some of the early startup code.~~

> *Edit:* @anjalsaneen pointed out [in the comments](https://dev.to/anjalsaneen/comment/1ccod) that when defining a provider we can set a [initOrder](https://developer.android.com/guide/topics/manifest/provider-element#init) tag, and the highest number gets initialized first.

### Android P+ / API 28+

```kotlin
@RequiresApi(28)
class MyAppComponentFactory() :
  androidx.core.app.AppComponentFactory() {

  @RequiresApi(29)
  override fun instantiateClassLoader(
    cl: ClassLoader,
    aInfo: ApplicationInfo
  ): ClassLoader {
    if  (Build.VERSION.SDK_INT >= 29) {
      Debug.startMethodTracingSampling(...)
    }
    return super.instantiateClassLoader(cl, aInfo)
  }

  override fun instantiateApplicationCompat(
    cl: ClassLoader,
    className: String
  ): Application {
    if  (Build.VERSION.SDK_INT < 29) {
      Debug.startMethodTracingSampling(...)
    }
    return super.instantiateApplicationCompat(cl, className)
  }
}
```

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application
    android:appComponentFactory=".MyAppComponentFactory"
    tools:replace="android:appComponentFactory"
    tools:targetApi="p">
  </application>

</manifest>
```

### Where to store traces

```kotlin
val tracesDirPath = TODO("path for trace directory")
```

* **API < 28**: The broadcast receiver has access to a context on which we can call [Context.getDataDir()](https://developer.android.com/reference/android/content/Context#getDataDir()) to store the trace in the app directory.
* **API 28**: [AppComponentFactory.instantiateApplication()](https://developer.android.com/reference/android/app/AppComponentFactory#instantiateApplication(java.lang.ClassLoader,%20java.lang.String)) is in charge of creating a new application instance so there's no context available yet. We can hard code the path to `/sdcard/` directly, though that requires the `WRITE_EXTERNAL_STORAGE` permission.
* **API 29+**: When targeting API 29, hardcoding `/sdcard/` stops working. We can add the [requestLegacyExternalStorage](https://developer.android.com/guide/topics/manifest/application-element#requestLegacyExternalStorage) flag but it's not supported on API 30 anyway. **Edit:** [Yacine Rezgui suggested trying out MANAGE_EXTERNAL_STORAGE](https://twitter.com/yrezgui/status/1330960375865544706) on API 30+. Either way, [AppComponentFactory.instantiateClassLoader()](https://developer.android.com/reference/android/app/AppComponentFactory#instantiateClassLoader(java.lang.ClassLoader,%20android.content.pm.ApplicationInfo)) passes an `ApplicationInfo` so we can use [ApplicationInfo.dataDir](https://developer.android.com/reference/android/content/pm/ApplicationInfo#dataDir) to store the trace in the app directory. 

## When to stop recording

In [Android Vitals - First draw time 👩‍🎨](https://dev.to/pyricau/android-vitals-first-draw-time-m1d), we learnt that cold start ends when the app's first frame completely loads. We can leverage the code from that article to know when to stop method tracing:

```kotlin
class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstDraw = false
    val handler = Handler()

    registerActivityLifecycleCallbacks(
      object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (firstDraw) return
        val window = activity.window
        window.onDecorViewReady {
          window.decorView.onNextDraw {
            if (firstDraw) return
            firstDraw = true
            handler.postAtFrontOfQueue {
              Debug.stopMethodTracing()
            }
          }
        }
      }
    })
  }
}
```

We could also record for a fixed amount of time greater than the app start, e.g. 5 seconds:

```kotlin
Handler(Looper.getMainLooper()).postDelayed({
  Debug.stopMethodTracing()
}, 5000)
```

# Profiling with Nanoscope

Another option for profiling app startup is [uber/nanoscope](https://github.com/uber/nanoscope). It's an Android image with built-in low overhead tracing. It's great but has a few limitations:

- It only traces the main thread.
- A large app will overfill the in-memory trace buffer.

{% twitter 1330929229341442048 %}

# App Startup steps

Once we have a startup trace, we can start investigating what's taking time. You should expect 3 main sections:

1. `ActivityThread.handlingBindApplication()` contains the startup work before activity creation. If that's slow then we probably need to optimize `Application.onCreate()`.
2. `TransactionExecutor.execute()` is in charge of creating and resuming the activity, which includes inflating the view hierarchy.
3. `ViewRootImpl.performTraversals()` is where the framework performs the first measure, layout and draw. If this is slow then it could be the view hierarchy being too complex, or views with custom drawing that need to be optimized.

![startup steps](https://dev-to-uploads.s3.amazonaws.com/i/q92b4ejsjf9t295t5twr.png)

If you notice that a service is being started before the first view traversal, it might be worth delaying the start of that service so that it happens after the view traversal.

# Conclusion

A few take aways:

* Profile **release builds** to focus on the actual issues.
* The state of profiling app start on Android is far from ideal. There's basically no good out of the box solution, but the Jetpack Benchmark team [is working on this](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt;l=38;drc=ea99c3ac7be5b54a9493ba9fe00edee9e91baf6e).
* Start the recording from code to prevent Android Studio from slowing everything down.

Many thanks to the many folks who helped me out on Slack and [Twitter](https://twitter.com/Piwai/status/1330914849019203584): Kurt Nelson, Justin Wong, Leland Takamine, Yacine Rezgui, Raditya Gumay, Chris Craik, Mike Nakhimovich, Artem Chubaryan, Rahul Ravikumar, Yacine Rezgui, Eugen Pechanec, Louis CAD, Max Kovalev.