loading...
Cover image for Android Vitals - Is this a cold start? ๐Ÿฆ‹

Android Vitals - Is this a cold start? ๐Ÿฆ‹

pyricau profile image Py โš” ใƒป5 min read

Header image: Follow the Light by Romain Guy.

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 to when the first activity is drawn.

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:

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 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

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:

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, we learnt that:

Main thread start - continued

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:

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:

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:

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!

Posted on by:

pyricau profile

Py โš”

@pyricau

Author of LeakCanary ๐Ÿท๐Ÿฅ–โ›ท๐Ÿ‡ซ๐Ÿ‡ท

Discussion

markdown guide