Android Vitals - Why did my process start? ๐
Py โ
Posted on August 15, 2020
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:
This pie chart includes only app startups where an activity was created before the first post.
- 97.4% had an importance of 100, i.e. IMPORTANCE_FOREGROUND.
- 2.4% had an importance of 400, i.e. IMPORTANCE_CACHED (previously named IMPORTANCE_BACKGROUND).
- 0.2% map to other importances.
Let's look at that data from another angle: given a specific importance, how often was an activity created before the first post?
- 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
}
}
I hope you're enjoying these investigations!
Py โ@piwaiAndroid Vitals, a thread!
I'm writing a blog series focused on stability and performance monitoring of Android apps in production.
I'll update this thread with links to new articles as I publish them.
Check in every week for new content!
๐งตโฌ๏ธโฌ๏ธโฌ๏ธโฌ๏ธโฌ๏ธโฌ๏ธ๐งต21:40 PM - 05 Aug 2020
Posted on August 15, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.