DEV Community

Cover image for Android Vitals - Is this a cold start? ๐Ÿฆ‹
Py โš”
Py โš”

Posted on

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

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!

Oldest comments (3)

Collapse
 
vishugupta86 profile image
vishugupta86 • Edited

Cold Start - "in cases such as your appโ€™s being launched for the first time since the device booted, or since the system killed the app"
Warm Start - "The system evicts your app from memory, and then the user re-launches it.

Really confused with this. While our app is in background, system kills it because of low memory and restores when we launch it again, this is Cold or Warm start ? Is System killing the app different from system evicting your app from memory !!

Collapse
 
pyricau profile image
Py โš”

That's a great question, and you're right, the documentation is confusing.

The main difference seem to be about whether the activity is created with a restored state bundle. If the system starts an activity from scratch with no saved state, it's a cold start. If it starts the process then relaunches an activity with a restored state then they assume there's going to be less work to do so they call it a warm start. I'm not quite convinced that a restored state yields much less work..

Collapse
 
ildar2 profile image
ildar2 • Edited

Hi! Great series, have read them all. I have a question: why do we need to "Report cold start" inside Handler.post()? Why can't we do it from within the onActivityCreated callback? From what I see, the difference is only that we're waiting Activity.onCreate() to finish which is irrelevant. Is it just to demonstrate that there are messages already in the message queue?