Android Vitals - Why did my process start? ๐ŸŒ„

pyricau

Py โš”

Posted on August 15, 2020

Android Vitals - Why did my process start? ๐ŸŒ„

Header image: Windmill Sunrise by Romain Guy.

This blog series is focused on stability and performance monitoring of Android apps in production. Last week, I wrote about how to determine if an app start is a cold start:

If we post a message and no activity was created when that message runs, then we know this isn't a cold start, even if an activity is eventually launched 20 seconds later.

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With this approach, we must wait for an activity to be launched or that message to run before we know if an app start is a cold start. Sometimes it would be useful to know that from within Application.onCreate(). For example, we might want to preload resources asynchronously to optimize cold start:

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    if (isColdStart()) {
      preloadDataForUiAsync()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Process importance

While there is no Android API to know why a process was started, there is one to know why a process is still running: RunningAppProcessInfo.importance, which we can read from ActivityManager.getMyMemoryState(). According to the Processes and Application Lifecycle documentation:

To determine which processes should be killed when low on memory, Android places each process into an "importance hierarchy" based on the components running in them and the state of those components. [...] When deciding how to classify a process, the system will base its decision on the most important level found among all the components currently active in the process.

Right when the process starts, we could check its importance. If the importance is IMPORTANCE_FOREGROUND, it must be a cold start:

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    if (isForegroundProcess()) {
      preloadDataForUiAsync()
    }
  }

  private fun isForegroundProcess(): Boolean {
    val processInfo = ActivityManager.RunningAppProcessInfo()
    ActivityManager.getMyMemoryState(processInfo)
    return processInfo.importance == IMPORTANCE_FOREGROUND
  }
}
Enter fullscreen mode Exit fullscreen mode

Confirming with data

I implemented cold start detection with both approaches in a sample app and got identical results either way. I then added the detection code to a production app with enough installs to provide meaningful results. Here are the (anonymized) results:

importance at startup

This pie chart includes only app startups where an activity was created before the first post.

Let's look at that data from another angle: given a specific importance, how often was an activity created before the first post?

importance at startup 2

  • When startup importance is 100 there's always an activity created before first post. And when an activity is created before first post, 97.4% of the time the importance is 100.
  • When startup importance is 400 there's almost never an activity created before first post. Almost never is not never, there are still enough cases that when an activity is created before first post, 2.4% of the time the importance is 400.

The most likely explanation for the 2.4% with importance 400 is that those were not cold starts, however the system received a query to start an activity almost immediately after the process started, right when running Application.onCreate() but before we had a chance to add our first post.

Edit: I logged the importance after first post and compared that to the importance on app start. My data showed that 74% of app starts where an activity was created before first post and the start process importance was not 100 had that process importance changed to 100 after first post. This seem to confirm the theory that the system decided to start the activity after the app had already began to start.

Conclusion

We can combine our findings from the previous post to determine cold start accurately. A cold start:

  • Has a process importance of IMPORTANCE_FOREGROUND on app startup.
  • Has the first activity created before the first post is dequeued.
  • Has the first activity created with a null bundle.

Here's the updated code:

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    if (isForegroundProcess()) {
      var firstPostEnqueued = true
      Handler().post {
        firstPostEnqueued = false
      }
      registerActivityLifecycleCallbacks(object :
          ActivityLifecycleCallbacks {

        override fun onActivityCreated(
            activity: Activity,
            savedInstanceState: Bundle?
        ) {
          unregisterActivityLifecycleCallbacks(this)
          if (firstPostEnqueued && savedInstanceState == null) {
            // TODO Report cold start
          }
        }
      })
    }
  }

  private fun isForegroundProcess(): Boolean {
    val processInfo = ActivityManager.RunningAppProcessInfo()
    ActivityManager.getMyMemoryState(processInfo)
    return processInfo.importance == IMPORTANCE_FOREGROUND
  }
}
Enter fullscreen mode Exit fullscreen mode

I hope you're enjoying these investigations!

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
pyricau
Py โš”

Posted on August 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About