Android Vitals - Is this a cold start? 🦋
Tags: android, vitals, performance, monitoring
Published: 2020-08-05 22:00:07

> Header image: *Follow the Light* [by Romain Guy](https://www.flickr.com/photos/romainguy/46217553745/).

This blog series is focused on stability and performance monitoring of Android apps in production. In the last 2 posts, I wrote about what happens from when [the user taps a launcher icon](https://dev.to/pyricau/android-vitals-diving-into-cold-start-waters-5hi6) to when the [first activity is drawn](https://dev.to/pyricau/android-vitals-rising-to-the-first-drawn-surface-1j9e).

A **cold start** is an **activity launch** where the app process **starts from scratch** in response to an intent to start an activity. According to the [App startup time documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold):

> This type of start presents the **greatest challenge** in terms of minimizing startup time, because the system and app have more work to do than in the other launch states.
> We recommend that you always optimize based on an assumption of a cold start. Doing so can improve the performance of warm and hot starts, as well.

To **optimize** cold start, we need to **measure** it, which means we need to **monitor** cold start times in production.

Unfortunately, there is no `Activity.isThisAColdStart()` API on Android. This is by design: the [Activity lifecycle APIs](https://developer.android.com/guide/components/activities/activity-lifecycle) indicate when to save and restore state and abstract away the death and rebirth of processes. The engineers who designed the Android APIs didn't want us to write overly complex code with special cases for all the various ways an activity can be started. So there's no API.

![meme](https://dev-to-uploads.s3.amazonaws.com/i/48xeo1kaz207ib35lwhd.jpg)

### How are we supposed to monitor cold start if we can't tell a cold start from any other process start?

This post leverages what we learnt from our previous deep dives on cold start to start building out our own version of the missing `Activity.isThisAColdStart()` API. 

# Traditional approach

Most apps and libraries report a cold start if the first activity was created within a minute of the app start. It looks something like this:

```kotlin
class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    val appCreateMs = SystemClock.uptimeMillis()

    var firstActivityCreated = false

    registerActivityLifecycleCallbacks(object :
        ActivityLifecycleCallbacks {
      override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
      ) {
        if (firstActivityCreated) {
          return
        }
        firstActivityCreated = true
        val activityCreateMs = SystemClock.uptimeMillis()
        if (activityCreateMs - appCreateMs < 60_000) {
          // TODO Report cold start
        }
      }
    })
  }
}
```

Unfortunately, this approach also includes cases where the app process was started to respond to a broadcast receiver, a content provider query or to start a service, and then an activity was launched sometimes later within the first minute. We should exclude these cases from our cold start monitoring to avoid skewing our results.

# Leveraging our research

In the [previous blog post](https://dev.to/pyricau/android-vitals-rising-to-the-first-drawn-surface-1j9e), we learnt that:

* When the app process starts, it calls [ActivityThread.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7350;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3) which makes  a **blocking IPC call** to [ActivityManagerService.attachApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5213;drc=69e700c7922c34583bddbab4b7ce78adadb90730) in the `system_server` process.
* The `system_server` process makes an IPC call to [ActivityThread.bindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=1005;drc=69e700c7922c34583bddbab4b7ce78adadb90730) in the app process which enqueues a `BIND_APPLICATION` message to the main thread message queue.
* Then, for each activity that needs to launch, the `system_server` process makes an IPC call to [ActivityThread.scheduleTransaction()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=1679;drc=69e700c7922c34583bddbab4b7ce78adadb90730;bpv=1;bpt=1) in the app process which enqueues an `EXECUTE_TRANSACTION` message to the main thread message queue.
* When the IPC call to [ActivityManagerService.attachApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5213;drc=69e700c7922c34583bddbab4b7ce78adadb90730) is done, [ActivityThread.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7350;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3) then calls [Looper.loop()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Looper.java;l=153;drc=bc3d8b9071d4f0b2903d6836770d974e70366290) which loops forever, processing messages posted to its [MessageQueue](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/MessageQueue.java;l=41;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
* The first message processed is `BIND_APPLICATION`, which calls [ActivityThread.handleBindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=6162;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which [calls Application.onCreate()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=6500;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3).
* The next message processed is `EXECUTE_TRANSACTION` which calls [TransactionExecutor.execute()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java;l=69;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which launches the activity.

![Main thread start - continued](https://dev-to-uploads.s3.amazonaws.com/i/byzacpqn1gz9h3fo6612.png)

This means that in `Application.onCreate()`, the main thread message queue already has the `EXECUTE_TRANSACTION` message enqueued. If we post a new message from `Application.onCreate()`, it will execute after `EXECUTE_TRANSACTION` and therefore after the activity has been created. If we post a message and no activity was created when it executes, then we know this isn't a cold start, even if an activity is eventually launched 20 seconds later.

Here's how we can detect a cold start:

```kotlin
class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstActivityCreated = false

    registerActivityLifecycleCallbacks(object :
        ActivityLifecycleCallbacks {

      override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
      ) {
        if (firstActivityCreated) {
          return
        }
        firstActivityCreated = true
      }
    })
    Handler().post {
      if (firstActivityCreated) {
        // TODO Report cold start
      }
    }
  }
}
```

# Lukewarm start

According to the [App startup time documentation](https://developer.android.com/topic/performance/vitals/launch-time#warm):

> There are many potential states that could be considered warm starts. For instance:
> * The user backs out of your app, but then re-launches it. The process may have continued to run, but the app must recreate the activity from scratch via a call to onCreate().
> * The system evicts your app from memory, and then the user re-launches it. The process and the activity need to be restarted, but the task can benefit somewhat from the saved instance state bundle passed into onCreate().

So if the activity is created with a saved instance state bundle, then that shouldn't be considered a cold start. However, since the process needs to be restarted, there's still a lot more work to do than just creating an activity. Let's call this a **lukewarm start**.

We can update our code to take this into account:

```kotlin
class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstActivityCreated = false
    var hasSavedState = false

    registerActivityLifecycleCallbacks(object :
        ActivityLifecycleCallbacks {

      override fun onActivityCreated(
          activity: Activity,
          savedInstanceState: Bundle?
      ) {
        if (firstActivityCreated) {
          return
        }
        firstActivityCreated = true
        hasSavedState = savedInstanceState != null
      }
    })
    Handler().post {
      if (firstActivityCreated) {
        if (hasSavedState) {
          // TODO Report lukewarm start
        } else {
          // TODO Report cold start
        }
      }
    }
  }
}
```

# Conclusion

The Android framework team doesn't want us to think too hard about process start and activity launch, yet the Android app performance team insists that we optimize cold start. Challenge accepted! We're starting to shape our own version of the missing `Activity.isThisAColdStart()` API, but we're far from done. Stay tuned for more!

{% twitter 1291126907426099200 %}