Tracking Android App Launch in production

Tracking Android App Launch in production

ยท

8 min read

๐Ÿ‘‹ In this article, I summarize my findings on Android app launch after a few years of deep dives, with the hope that these guidelines help implementations track app launches correctly.

User-Centric App Launch analytics

In User-Centric Mobile Performance, I argued that mobile app teams should primarily track user-centric performance metrics that represent the experience of using the app, that responsiveness thresholds should be context specific as user expectations vary based on what they are doing and what they're used to.

I used the example of app launches and a few days later Hanson Ho published a blog post arguing, amongst other things, that you should think about the percentage makeup of cold vs warm launches rather than just focusing on improving cold launches. Read it, it's a great perspective.

Hanson made a really good point: users don't really care about the specifics of whether an app is a cold or warm launch.

So what is it that users care about? As I highlighted above, user expectations vary based on what they are doing and what they're used to. We should put cold / hot / warm launch aside. Users can launch an app in different ways:

  • Tapping on a launcher icon

    • Users expect this to take a few seconds.
  • Tapping on a notification

    • Recent notifications are expected to open fast, but it's fairly common for long-running notifications to take a while to open the app.
  • Launch from another app (integration / chooser intent)

    • This should be fairly fast, as users are in a flow trying to accomplish a task.
  • Bringing it back from Recents.

    • This is expected to be almost instant, as the OS gives the illusion of all these apps being available to go back to at any point in time.

The first 3 cases can be determined by looking at the intent of the launched activity, and the last case by looking at whether the activity is provided with a saved instance state.

While it's useful to track whether a launch was cold / warm / hot, your primary dashboards and metrics should instead focus on the different scenarios under which users are launching the app, and target different thresholds for each.

For example, if 80% of app launches are from Recents (i.e. users constantly swap in & out of the app), then you should focus on optimizing that. And if 50% of the launches from Recents are cold launches, bringing that metric down, then you should look into why the app keeps getting killed (e.g. crash or high memory usage).

App launch, app start or app startup?

These words tend to be used interchangeably. However, I prefer Launch because it reminds me of the app launcher. Start can be confusing because an app launch can involve a process start or not. Also, in a Hot Launch the activity isn't actually started as it goes from paused to resumed.

What is an App Launch?

An App Launch is when an app that had no visible activity now has visible activities.

In other words, an App Launch is when an app that had no activity in started or resumed state now has activities in started or resumed state.

  • I focus on activities because Android places each process into an importance hierarchy where a process can have a foreground or visible importance yet have no created activity (see Processes and app lifecycle).

  • I focus on visible rather than foreground because I don't want to count as app launch the case where the app had an activity visible in the background of an activity from another app and then came to the foreground.

App Launch Temperature

The App startup time documentation does a great job of describing the difference between cold, warm, and hot launches.

the various startup states

However, there are different flavors of cold and warm launches, so it's worth digging into the details.

Cold Launch

A Cold Launch is a launch where there was no existing app process and the system started the app process for the purpose of launching an activity. This is key: we need to check why the system decided to start the process. If the process was started for another reason and then sometimes during the init the system asked the app to launch an activity, then it's not a cold launch.

Some interesting attributes to track for cold launches:

  • Is it the first launch after an install?
    • First launch after install is critical for new users, yet it often triggers additional first time init work.
  • Is it the first launch after an upgrade?
    • Upgrades often trigger additional migration work on launch.
  • Is it the first launch after clear data?
    • This should be a similar launch to the first launch after install, but is interesting to track separately to detect when users clear data which likely indicates an issue with the app.
  • Did the launched activity have a saved instance state (process recreation for when bringing the activity back from Recents).

Hot Launch

A Hot Launch is a launch where there was an existing app process as well as an activity in Created state (i.e. stopped) that was then started.

Warm Launch

A Warm Launch is any launch that isn't a cold launch or a hot launch:

  • A launch where there was an existing app process but no activity. The activity might be created with a saved state to restore (brought back from Recents) or with no saved state.
  • A launch where the process was started for a reason not related to the launch, wasn't done with startup yet (i.e. Application.onCreate() wasn't finished running) then sometimes during the init the system asked the app to launch an activity. While this might look like a cold launch, users will actually not experience the full cold launch duration.

Pre Launch state

The launch temperature is determined by the state of the app before the launch. In square/papa, I defined the following list of pre-launch states:

enum class PreLaunchState(val launchType: AppLaunchType) {
  /**
   * This is typically referred to as a "cold start".
   * The process was started with a FOREGROUND importance and
   * the launched activity was created, started and resumed before our first post
   * ran.
   */
  NO_PROCESS(COLD),

  /**
   * Same as [NO_PROCESS] but this was the first launch ever,
   * which might trigger first launch additional work.
   */
  NO_PROCESS_FIRST_LAUNCH_AFTER_INSTALL(COLD),

  /**
   * Same as [NO_PROCESS] but this was the first launch after the app was upgraded, which might
   * trigger additional migration work. Note that if the upgrade if the first upgrade
   * that introduces this library, the value will be [NO_PROCESS_FIRST_LAUNCH_AFTER_CLEAR_DATA]
   * instead.
   */
  NO_PROCESS_FIRST_LAUNCH_AFTER_UPGRADE(COLD),

  /**
   * Same as [NO_PROCESS] but this was either the first launch after a clear data, or
   * this was the first launch after the upgrade that introduced this library.
   */
  NO_PROCESS_FIRST_LAUNCH_AFTER_CLEAR_DATA(COLD),

  /**
   * This is the coldest type of "warm start". The process was not started with
   * a FOREGROUND importance yet the launched activity was created, started and resumed
   * before our first post ran. This means that while the process while starting, the
   * system decided to launch the activity.
   */
  PROCESS_WAS_LAUNCHING_IN_BACKGROUND(WARM),

  /**
   * This is a "warm start" where the activity brought to the foreground had to be created,
   * started and resumed, and the task had no saved instance state bundle.
   */
  NO_ACTIVITY_NO_SAVED_STATE(WARM),

  /**
   * This is a "warm start" where the activity brought to the foreground had to be created,
   * started and resumed, and the task can benefit somewhat from the saved instance state bundle
   * passed into onCreate().
   */
  NO_ACTIVITY_BUT_SAVED_STATE(WARM),

  /**
   * This is a "hot start", the activity was already created and had been stopped when the app
   * went in background. Bringing it to the foreground means the activity was started and then
   * resumed. Note that there isn't a "ACTIVITY_WAS_PAUSED" entry here. We do not consider
   * going from PAUSE to RESUME to be a launch because the activity was still visible so there
   * is nothing to redraw on resume.
   */
  ACTIVITY_WAS_STOPPED(HOT);
}

Here's the code to compute the pre-launch state which checks for cold starts by looking at process importance and whether the launched activity was created before the first post ran (as highlighted in Why did my process start? ๐ŸŒ„).

Launch start timestamp

Cold Launch

In When did my app start? โฑ I concluded that you should use:

  • Up to API 24: Use the class load time of a content provider which high priority.
  • API 24 - API 28: Use Process.getStartUptimeMillis().
  • API 28 and beyond: Use Process.getStartUptimeMillis() but filter out weird values (e.g. more than 1 min to get to Application.onCreate()) and fallback to the time ContentProvider.onCreate().

Since then API 33 added Process.getStartRequestedUptimeMillis() which is super promising, though I haven't tried using yet.

  • Process.getStartRequestedUptimeMillis(): when the user started waiting for the app to launch
  • Process.getStartUptimeMillis(): when the app started having an influence on launch time (when the APK starts loading)

Warm & Hot Launch

As far as I know, there's no API that provides the launch intent timestamp. There should be!

So our best option here is to leverage ActivityLifecycleCallbacks to capture timestamps:

Launch end timestamp

The end of a launch is when the window of the launched activity has been rendered into a frame and submitted to the swap chain.

In First draw time ๐Ÿ‘ฉโ€๐ŸŽจ I showed how to access the decor view of the launched activity to add an OnDrawListener.

From the onDraw() callback, we need to figure out when that frame traversal is submitted to the swap chain with:

All together in square/papa

I'm hopeful that some day Google will release a new AndroidX library to track app launches (LaunchStats?). Until then, you can roll your own code following the guidelines I outlined, or leverage square/papa:

class ExampleApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    PapaEventListener.install { event ->
      when (event) {
        is AppLaunch -> {
          Analytics.logAppLaunch(
            preLaunchState = event.preLaunchState
            durationUptimeMillis = event.durationUptimeMillis
          )
        }
      }
    }
  }
}

I just realized that the AppLaunch event does not provide the intent and saved state details which are necessary to track User-Centric App Launch analytics as highlighted at the start of this article, so I filed square/papa#57 to fix that.

Header image generated by DALL-E.

ย