loading...
Cover image for Android Vitals - Why did my process start? ๐ŸŒ„

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

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

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

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()
    }
  }
}

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

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.

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

I hope you're enjoying these investigations!

Posted on by:

pyricau profile

Py โš”

@pyricau

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

Discussion

markdown guide
 

I have a question about this:
Sometime in the past, Google added a weird new function to start a foreground service ("startForegroundService") , but it seems it should work only if it's not in the foreground, while the previous one should work fine when it's in the foreground ("startService").

I was told that if I want to always choose the correct one, no matter where I start the service, I can just check whether it's in the foreground or not:

val appProcessInfo = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(appProcessInfo)
(appProcessInfo.importance == IMPORTANCE_FOREGROUND || appProcessInfo.importance == IMPORTANCE_VISIBLE)

This worked in almost all cases, but from time to time, I got crash reports (via Crashlytics) showing that it failed:
"IllegalStateException: Not allowed to start service Intent...app is in background"

How could it be?
For now, I tried using try-catch in this case, and use the other function when this occurs. I hope this will help.

 

I'm not sure, but I do know we've stuck to only IMPORTANCE_FOREGROUND and didn't include IMPORTANCE_VISIBLE when starting a normal service.

This could also be an unfortunate race condition, ie right after you check the importance that importance changes... and yes there'd likely be nothing to do about it.

 

I can't find where I got this code. I'm sure it was from Google though, somewhere in the issue tracker.
Why did Google even make this new function?