Header image: Follow the Light by Romain Guy.
This blog series is focused on stability and performance monitoring of Android apps in production. In the last 2 posts, I wrote about what happens from when the user taps a launcher icon to when the first activity is drawn.
A cold start is an activity launch where the app process starts from scratch in response to an intent to start an activity. According to the App startup time documentation:
This type of start presents the greatest challenge in terms of minimizing startup time, because the system and app have more work to do than in the other launch states.
We recommend that you always optimize based on an assumption of a cold start. Doing so can improve the performance of warm and hot starts, as well.
To optimize cold start, we need to measure it, which means we need to monitor cold start times in production.
Unfortunately, there is no Activity.isThisAColdStart()
API on Android. This is by design: the Activity lifecycle APIs indicate when to save and restore state and abstract away the death and rebirth of processes. The engineers who designed the Android APIs didn't want us to write overly complex code with special cases for all the various ways an activity can be started. So there's no API.
How are we supposed to monitor cold start if we can't tell a cold start from any other process start?
This post leverages what we learnt from our previous deep dives on cold start to start building out our own version of the missing Activity.isThisAColdStart()
API.
Traditional approach
Most apps and libraries report a cold start if the first activity was created within a minute of the app start. It looks something like this:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
val appCreateMs = SystemClock.uptimeMillis()
var firstActivityCreated = false
registerActivityLifecycleCallbacks(object :
ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
if (firstActivityCreated) {
return
}
firstActivityCreated = true
val activityCreateMs = SystemClock.uptimeMillis()
if (activityCreateMs - appCreateMs < 60_000) {
// TODO Report cold start
}
}
})
}
}
Unfortunately, this approach also includes cases where the app process was started to respond to a broadcast receiver, a content provider query or to start a service, and then an activity was launched sometimes later within the first minute. We should exclude these cases from our cold start monitoring to avoid skewing our results.
Leveraging our research
In the previous blog post, we learnt that:
- When the app process starts, it calls ActivityThread.main() which makes a blocking IPC call to ActivityManagerService.attachApplication() in the
system_server
process. - The
system_server
process makes an IPC call to ActivityThread.bindApplication() in the app process which enqueues aBIND_APPLICATION
message to the main thread message queue. - Then, for each activity that needs to launch, the
system_server
process makes an IPC call to ActivityThread.scheduleTransaction() in the app process which enqueues anEXECUTE_TRANSACTION
message to the main thread message queue. - When the IPC call to ActivityManagerService.attachApplication() is done, ActivityThread.main() then calls Looper.loop() which loops forever, processing messages posted to its MessageQueue.
- The first message processed is
BIND_APPLICATION
, which calls ActivityThread.handleBindApplication() which calls Application.onCreate(). - The next message processed is
EXECUTE_TRANSACTION
which calls TransactionExecutor.execute() which launches the activity.
This means that in Application.onCreate()
, the main thread message queue already has the EXECUTE_TRANSACTION
message enqueued. If we post a new message from Application.onCreate()
, it will execute after EXECUTE_TRANSACTION
and therefore after the activity has been created. If we post a message and no activity was created when it executes, then we know this isn't a cold start, even if an activity is eventually launched 20 seconds later.
Here's how we can detect a cold start:
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
}
}
}
}
Lukewarm start
According to the App startup time documentation:
There are many potential states that could be considered warm starts. For instance:
- The user backs out of your app, but then re-launches it. The process may have continued to run, but the app must recreate the activity from scratch via a call to onCreate().
- The system evicts your app from memory, and then the user re-launches it. The process and the activity need to be restarted, but the task can benefit somewhat from the saved instance state bundle passed into onCreate().
So if the activity is created with a saved instance state bundle, then that shouldn't be considered a cold start. However, since the process needs to be restarted, there's still a lot more work to do than just creating an activity. Let's call this a lukewarm start.
We can update our code to take this into account:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
var firstActivityCreated = false
var hasSavedState = false
registerActivityLifecycleCallbacks(object :
ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
if (firstActivityCreated) {
return
}
firstActivityCreated = true
hasSavedState = savedInstanceState != null
}
})
Handler().post {
if (firstActivityCreated) {
if (hasSavedState) {
// TODO Report lukewarm start
} else {
// TODO Report cold start
}
}
}
}
}
Conclusion
The Android framework team doesn't want us to think too hard about process start and activity launch, yet the Android app performance team insists that we optimize cold start. Challenge accepted! We're starting to shape our own version of the missing Activity.isThisAColdStart()
API, but we're far from done. Stay tuned for more!
Top comments (4)
Hi! Great series, have read them all. I have a question: why do we need to "Report cold start" inside Handler.post()? Why can't we do it from within the onActivityCreated callback? From what I see, the difference is only that we're waiting Activity.onCreate() to finish which is irrelevant. Is it just to demonstrate that there are messages already in the message queue?
Did you get an answer for this?
Cold Start - "in cases such as your appβs being launched for the first time since the device booted, or since the system killed the app"
Warm Start - "The system evicts your app from memory, and then the user re-launches it.
Really confused with this. While our app is in background, system kills it because of low memory and restores when we launch it again, this is Cold or Warm start ? Is System killing the app different from system evicting your app from memory !!
That's a great question, and you're right, the documentation is confusing.
The main difference seem to be about whether the activity is created with a restored state bundle. If the system starts an activity from scratch with no saved state, it's a cold start. If it starts the process then relaunches an activity with a restored state then they assume there's going to be less work to do so they call it a warm start. I'm not quite convinced that a restored state yields much less work..