DEV Community

Cover image for Android Vitals - Why did my process start? ๐ŸŒ„
Py โš”
Py โš”

Posted on • Updated on

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!

Top comments (3)

Collapse
 
androiddeveloperlb profile image
AndroidDeveloperLB

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.

Collapse
 
pyricau profile image
Py โš” • Edited

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.

Collapse
 
androiddeveloperlb profile image
AndroidDeveloperLB

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?