Android Vitals - Rising to the first drawn surface 🤽‍♂️
Tags: android, vitals, performance, monitoring
Published: 2020-08-01 17:23:20

> Header image: *Flying in the Light* [by Romain Guy](https://www.flickr.com/photos/romainguy/10523896835/).

This blog series is focused on stability and performance monitoring of Android apps in production. Last week, I wrote about the [beginning of a cold start](https://dev.to/pyricau/android-vitals-diving-into-cold-start-waters-5hi6), from tapping a launcher icon to the creation of the app process.

The [App startup time documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold) outlines the next stages:

> As soon as the system creates the app process, the app process is responsible for the next stages:
> 1. Creating the app object.
> 2. Launching the main thread.
> 3. Creating the main activity.
> 4. Inflating views.
> 5. Laying out the screen.
> 6. Performing the initial draw.

> Once the app process has completed the first draw, the system process swaps out the currently displayed background window, replacing it with the main activity. At this point, the user can start using the app.

![Next stages](https://dev-to-uploads.s3.amazonaws.com/i/jnpiu0ag9nkzlqcs8bte.png)

<!--
participant AppProcess

state over AppProcess: Creating the app object
state over AppProcess: Launching the main thread
state over AppProcess: Creating the main activity
state over AppProcess: Inflating views
state over AppProcess: Laying out the screen
state over AppProcess: Performing the initial draw
-->

> Diagram created with [WebSequenceDiagram](https://www.websequencediagrams.com/).

This post is a continuation of our deep dive on cold start, where we'll rise from the launch of the first activity to the first draw on a surface.

# Main thread start

In the [previous blog post](https://dev.to/pyricau/android-vitals-diving-into-cold-start-waters-5hi6), we learnt that:

* When the app process starts, it calls [ActivityThread.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7350;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3) which makes  a **blocking IPC call** to [ActivityManagerService.attachApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5213;drc=69e700c7922c34583bddbab4b7ce78adadb90730) in the `system_server` process.
* The `system_server` process makes an IPC call to [ActivityThread.bindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=1005;drc=69e700c7922c34583bddbab4b7ce78adadb90730) in the app process, which enqueues a `BIND_APPLICATION` message to the main thread message queue.
* When the IPC call to [ActivityManagerService.attachApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5213;drc=69e700c7922c34583bddbab4b7ce78adadb90730) is done, [ActivityThread.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7350;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3) then calls [Looper.loop()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Looper.java;l=153;drc=bc3d8b9071d4f0b2903d6836770d974e70366290) which loops forever, processing messages posted to its [MessageQueue](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/MessageQueue.java;l=41;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
* The first message processed is `BIND_APPLICATION`, which calls [ActivityThread.handleBindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=6162;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which loads the APK and loads the app components.

![Main thread start](https://dev-to-uploads.s3.amazonaws.com/i/fg3caooukxzmcb7kza14.png)

<!--
participant App Main Thread
participant App MessageQueue


state over App Main Thread: Process created

App Main Thread->App Main Thread: ActivityThread.main()

App Main Thread->system_server: IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Waiting for IPC to return

system_server->App MessageQueue: IPC:\nActivityThread.bindApplication()

state over App MessageQueue: Enqueued Message:\nBIND_APPLICATION

state over system_server: more work

system_server->App Main Thread: return from IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Main Thread resumed

App Main Thread->App Main Thread: Looper.loop()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: BIND_APPLICATION

App Main Thread->App Main Thread: ActivityThread.handleBindApplication()
-->

An important take away here is that **nothing happens in the app process main thread until the IPC call to [ActivityManagerService.attachApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5213;drc=69e700c7922c34583bddbab4b7ce78adadb90730) returns**.

# Scheduling activity launch

Let's look at what happens in the `system_server` process after calling [ActivityThread.bindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=1005;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public class ActivityManagerService extends IActivityManager.Stub {

  private boolean attachApplicationLocked(
      IApplicationThread thread, int pid, int callingUid,
      long startSeq) {
    thread.bindApplication(...);
        
    // See if the top visible activity is waiting to run
    //  in this process...
    mAtmInternal.attachApplication(...);
    
    // Find any services that should be running in this process...
    mServices.attachApplicationLocked(app, processName);
    
    // Check if a next-broadcast receiver is in this process...
    if (isPendingBroadcastProcessLocked(pid)) {
        sendPendingBroadcastsLocked(app);
    }
    return true;
  }
}
```

The line relevant to activity launch is `mAtmInternal.attachApplication(...)`. It calls [ActivityTaskManagerService.attachApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=6924;drc=69e700c7922c34583bddbab4b7ce78adadb90730;bpv=1;bpt=1) which calls [RootActivityContainer.attachApplication](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/RootActivityContainer.java;drc=69e700c7922c34583bddbab4b7ce78adadb90730;bpv=1;bpt=1;l=769):

```java
class RootActivityContainer extends ConfigurationContainer {

  boolean attachApplication(WindowProcessController app) {
    for (ActivityDisplay display : mActivityDisplays) {
	  ActivityStack stack = display.getFocusedStack()
      ActivityRecord top = stack.topRunningActivityLocked();
      stack.getAllRunningVisibleActivitiesLocked(mTmpActivityList);
      for (ActivityRecord activity : mTmpActivityList) {
        if (activity.app == null
		    && app.mUid == activity.info.applicationInfo.uid
            && app.mName.equals(activity.processName)) {
          mStackSupervisor.realStartActivityLocked(
            activity,
          	app,
            top == activity /* andResume */,
          	true /* checkConfig */
          )
		}
      }
    }
    ...
  }
}
```

The above code:
- Iterates over each display.
- Retrieves the focused activity stack for that display.
- Iterates on each activity of the focused activity stack.
- Calls [ActivityStackSupervisor.realStartActivityLocked()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java;l=720;drc=69e700c7922c34583bddbab4b7ce78adadb90730) if that activity belongs to the process being started. Notice the `andResume` parameter passed as true if the activity is at the top of the stack.

Here's [ActivityStackSupervisor.realStartActivityLocked()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java;l=720;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public class ActivityStackSupervisor{

  boolean realStartActivityLocked(
    ActivityRecord r,
    WindowProcessController proc,
    boolean andResume,
    boolean checkConfig
  ) {
    ...
    ClientTransaction clientTransaction = ClientTransaction.obtain(
            proc.getThread(), r.appToken);
    
    clientTransaction.addCallback(LaunchActivityItem.obtain(...));
    
    // Set desired final state.
    final ActivityLifecycleItem lifecycleItem;
    if (andResume) {
        boolean forward = dc.isNextTransitionForward()
        lifecycleItem = ResumeActivityItem.obtain(forward);
    } else {
        lifecycleItem = PauseActivityItem.obtain();
    }
    clientTransaction.setLifecycleStateRequest(lifecycleItem);
    
    // Schedule transaction.
    mService.getLifecycleManager()
      .scheduleTransaction(clientTransaction);
    ...
  }
}
```

All the method calls we've looked at so far are happening in the `system_server` process. [ClientLifecycleManager.scheduleTransaction()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ClientLifecycleManager.java;l=45;drc=69e700c7922c34583bddbab4b7ce78adadb90730) makes an IPC call to [ActivityThread.scheduleTransaction()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=1679;drc=69e700c7922c34583bddbab4b7ce78adadb90730;bpv=1;bpt=1) in the **app process**, which calls  [ClientTransactionHandler.scheduleTransaction()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ClientTransactionHandler.java;l=45;drc=69e700c7922c34583bddbab4b7ce78adadb90730) to enqueue a `EXECUTE_TRANSACTION` message on the main thread message queue:

```java
public abstract class ClientTransactionHandler {

    /** Prepare and schedule transaction for execution. */
    void scheduleTransaction(ClientTransaction transaction) {
        transaction.preExecute(this);
        sendMessage(
          ActivityThread.H.EXECUTE_TRANSACTION,
          transaction
        );
    }
}
```

When `EXECUTE_TRANSACTION` is processed it calls [TransactionExecutor.execute()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java;l=69;drc=69e700c7922c34583bddbab4b7ce78adadb90730).

We can now update the initial sequence diagram:

![Main thread start - continued](https://dev-to-uploads.s3.amazonaws.com/i/byzacpqn1gz9h3fo6612.png)

<!--
participant App Main Thread
participant App MessageQueue


state over App Main Thread: Process created

App Main Thread->App Main Thread: ActivityThread.main()

App Main Thread->system_server: IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Waiting for IPC to return

system_server->App MessageQueue: IPC:\nActivityThread.bindApplication()

state over App MessageQueue: Enqueued Message:\nBIND_APPLICATION

system_server->App MessageQueue: IPC:\nActivityThread.scheduleTransaction()

state over App MessageQueue: Enqueued Message:\nEXECUTE_TRANSACTION

system_server->App Main Thread: return from IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Main Thread resumed

App Main Thread->App Main Thread: Looper.loop()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: BIND_APPLICATION

App Main Thread->App Main Thread: ActivityThread.handleBindApplication()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: EXECUTE_TRANSACTION

App Main Thread->App Main Thread: TransactionExecutor.execute()

-->

# Actual activity launch

[TransactionExecutor.execute()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java;l=69;drc=69e700c7922c34583bddbab4b7ce78adadb90730) calls [TransactionExecutor.performLifecycleSequence()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java;l=205;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which calls back into ActivityThread to [create](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=3400;drc=69e700c7922c34583bddbab4b7ce78adadb90730), [start](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=3297;drc=69e700c7922c34583bddbab4b7ce78adadb90730) and [resume](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=4269;drc=69e700c7922c34583bddbab4b7ce78adadb90730) the activity:

```java
public class TransactionExecutor {

  private void performLifecycleSequence(...) {
    for (int i = 0, state; i < path.size(); i++) {
      state = path.get(i);
      switch (state) {
        case ON_CREATE:
          mTransactionHandler.handleLaunchActivity(...);
          break;
        case ON_START:
          mTransactionHandler.handleStartActivity(...);
          break;
        case ON_RESUME:
          mTransactionHandler.handleResumeActivity(...);
          break;
        case ON_PAUSE:
          mTransactionHandler.handlePauseActivity(...);
          break;
        case ON_STOP:
          mTransactionHandler.handleStopActivity(...);
          break;
        case ON_DESTROY:
          mTransactionHandler.handleDestroyActivity(...);
          break;
        case ON_RESTART:
          mTransactionHandler.performRestartActivity(...);
          break;
      }
    }
  }
}
```

![Activity resume](https://dev-to-uploads.s3.amazonaws.com/i/qnl83bka6shtgd3mp3xu.png)

<!-- 
participant App Main Thread
participant App MessageQueue


state over App Main Thread: Main Thread resumed

App Main Thread->App Main Thread: Looper.loop()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: BIND_APPLICATION

App Main Thread->App Main Thread: ActivityThread.handleBindApplication()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: EXECUTE_TRANSACTION

App Main Thread->App Main Thread: TransactionExecutor.execute()

App Main Thread->App Main Thread: ActivityThread.handleLaunchActivity()

state over App Main Thread: Activity created

App Main Thread->App Main Thread: ActivityThread.handleStartActivity()

state over App Main Thread: Activity started

App Main Thread->App Main Thread: ActivityThread.handleResumeActivity()

state over App Main Thread: Activity resumed
-->

# First draw

Let's look at the sequence of calls that leads to the first draw from [ActivityThread.handleResumeActivity()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=4269;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

* [ActivityThread.handleResumeActivity()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=4269;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [WindowManagerImpl.addView()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/WindowManagerImpl.java;l=93;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [WindowManagerGlobal.addView()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/WindowManagerGlobal.java;l=309;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [ViewRootImpl.setView()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=784;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [ViewRootImpl.requestLayout()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=1442;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [ViewRootImpl.scheduleTraversals()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=1713;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [Choreographer.postCallback()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Choreographer.java;l=418;drc=69e700c7922c34583bddbab4b7ce78adadb90730)
* ➡️ [Choreographer.scheduleFrameLocked()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Choreographer.java;l=624;drc=69e700c7922c34583bddbab4b7ce78adadb90730)

[Choreographer.scheduleFrameLocked()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Choreographer.java;l=624;drc=69e700c7922c34583bddbab4b7ce78adadb90730) enqueues a `MSG_DO_FRAME` message on the main thread message queue:

![Scheduling MSG_DO_FRAME](https://dev-to-uploads.s3.amazonaws.com/i/cj22d0vhnpj8ededlf91.png)

<!--
participant App Main Thread
participant App MessageQueue


state over App Main Thread: Activity started

App Main Thread->App Main Thread: ActivityThread.handleResumeActivity()

App Main Thread->App MessageQueue: Choreographer.scheduleFrameLocked()

state over App MessageQueue: Enqueued Message:\nMSG_DO_FRAME

state over App Main Thread: Activity resumed
-->

When `MSG_DO_FRAME` is processed it calls [Choreographer.doFrame()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Choreographer.java;l=662;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which calls [ViewRootImpl.doTraversal()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=1736;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which performs a **measure pass**, a **layout pass**, and finally **the first draw pass** on the view hierarchy (see [How Android Draws Views](https://developer.android.com/guide/topics/ui/how-android-draws)):

![First draw](https://dev-to-uploads.s3.amazonaws.com/i/2ken9max4z9flkwlmdmf.png)

<!--
participant App Main Thread
participant App MessageQueue

state over App Main Thread: Activity started

App Main Thread->App Main Thread: ActivityThread.handleResumeActivity()

App Main Thread->App MessageQueue: Choreographer.scheduleFrameLocked()

state over App MessageQueue: Enqueued Message:\nMSG_DO_FRAME

state over App Main Thread: Activity resumed

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: MSG_DO_FRAME

App Main Thread->App Main Thread: ViewRootImpl.doTraversal()

state over App Main Thread: Activity first draw
-->

# Conclusion

We started with a high level understanding of what happens once the system creates the app process:

![Next stages](https://dev-to-uploads.s3.amazonaws.com/i/jnpiu0ag9nkzlqcs8bte.png)

Now we know exactly what happens:

![App start to first draw](https://dev-to-uploads.s3.amazonaws.com/i/00k7kn3bzc1u5rkqhqz5.png)

<!--
participant App Main Thread
participant App MessageQueue

state over App Main Thread: Process created

App Main Thread->App Main Thread: ActivityThread.main()

App Main Thread->system_server: IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Waiting for IPC to return

system_server->App MessageQueue: IPC:\nActivityThread.bindApplication()

state over App MessageQueue: Enqueued Message:\nBIND_APPLICATION

system_server->App MessageQueue: IPC:\nActivityThread.scheduleTransaction()

state over App MessageQueue: Enqueued Message:\nEXECUTE_TRANSACTION

system_server->App Main Thread: return from IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Main Thread resumed

App Main Thread->App Main Thread: Looper.loop()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: BIND_APPLICATION

App Main Thread->App Main Thread: ActivityThread.handleBindApplication()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: EXECUTE_TRANSACTION

App Main Thread->App Main Thread: TransactionExecutor.execute()

App Main Thread->App Main Thread: ActivityThread.handleLaunchActivity()

state over App Main Thread: Activity created

App Main Thread->App Main Thread: ActivityThread.handleStartActivity()

state over App Main Thread: Activity started

App Main Thread->App Main Thread: ActivityThread.handleResumeActivity()

App Main Thread->App MessageQueue: Choreographer.scheduleFrameLocked()

state over App MessageQueue: Enqueued Message:\nMSG_DO_FRAME

state over App Main Thread: Activity resumed

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: MSG_DO_FRAME

App Main Thread->App Main Thread: ViewRootImpl.doTraversal()

state over App Main Thread: Activity first draw
-->

Let's put this together with what we learnt from the prior blog post, and look at everything that happens from when **the user taps a launcher icon** to when the first activity is drawn and **the user can start using the app**. You might need to zoom in 🔎 😅:

![Full picture](https://dev-to-uploads.s3.amazonaws.com/i/7fqm484ga67h6x149ajn.png)

<!--
User->Launcher process: Tap launcher icon

Launcher process -> system_server: IPC:\nActivityTaskManagerService.startActivity()

system_server->User: Display starting window

state over User: Staring at starting window

system_server -> Zygote: socket:\nfork command

Zygote->App Main Thread: fork()

state over App Main Thread: Process created

App Main Thread->App Main Thread: ActivityThread.main()

App Main Thread->system_server: IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Waiting for IPC to return

system_server->App MessageQueue: IPC:\nActivityThread.bindApplication()

state over App MessageQueue: Enqueued Message:\nBIND_APPLICATION

system_server->App MessageQueue: IPC:\nActivityThread.scheduleTransaction()

state over App MessageQueue: Enqueued Message:\nEXECUTE_TRANSACTION

system_server->App Main Thread: return from IPC:\nActivityManagerService.attachApplication()

state over App Main Thread: Main Thread resumed

App Main Thread->App Main Thread: Looper.loop()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: BIND_APPLICATION

App Main Thread->App Main Thread: ActivityThread.handleBindApplication()

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: EXECUTE_TRANSACTION

App Main Thread->App Main Thread: TransactionExecutor.execute()

App Main Thread->App Main Thread: ActivityThread.handleLaunchActivity()

state over App Main Thread: Activity created

App Main Thread->App Main Thread: ActivityThread.handleStartActivity()

state over App Main Thread: Activity started

App Main Thread->App Main Thread: ActivityThread.handleResumeActivity()

App Main Thread->App MessageQueue: Choreographer.scheduleFrameLocked()

state over App MessageQueue: Enqueued Message:\nMSG_DO_FRAME

state over App Main Thread: Activity resumed

App Main Thread->App MessageQueue: MessageQueue.next()\n returns: MSG_DO_FRAME

App Main Thread->App Main Thread: ViewRootImpl.doTraversal()

state over App Main Thread: Activity first draw

state over User: Start using app
-->

Now that we have the full picture, we can start digging into how to properly monitor cold start. Stay tuned for more!