loading...
Cover image for Android Vitals - Rising to the first drawn surface 🤽‍♂️

Android Vitals - Rising to the first drawn surface 🤽‍♂️

pyricau profile image Py ⚔ ・10 min read

Header image: Flying in the Light by Romain Guy.

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, from tapping a launcher icon to the creation of the app process.

The App startup time documentation 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

Diagram created with WebSequenceDiagram.

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, we learnt that:

Main thread start

An important take away here is that nothing happens in the app process main thread until the IPC call to ActivityManagerService.attachApplication() returns.

Scheduling activity launch

Let's look at what happens in the system_server process after calling ActivityThread.bindApplication():

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() which calls RootActivityContainer.attachApplication:

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

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() makes an IPC call to ActivityThread.scheduleTransaction() in the app process, which calls ClientTransactionHandler.scheduleTransaction() to enqueue a EXECUTE_TRANSACTION message on the main thread message queue:

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

We can now update the initial sequence diagram:

Main thread start - continued

Actual activity launch

TransactionExecutor.execute() calls TransactionExecutor.performLifecycleSequence() which calls back into ActivityThread to create, start and resume the activity:

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

First draw

Let's look at the sequence of calls that leads to the first draw from ActivityThread.handleResumeActivity():

Choreographer.scheduleFrameLocked() enqueues a MSG_DO_FRAME message on the main thread message queue:

Scheduling MSG_DO_FRAME

When MSG_DO_FRAME is processed it calls Choreographer.doFrame() which calls ViewRootImpl.doTraversal() which performs a measure pass, a layout pass, and finally the first draw pass on the view hierarchy (see How Android Draws Views):

First draw

Conclusion

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

Next stages

Now we know exactly what happens:

App start to 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

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

Posted on by:

pyricau profile

Py ⚔

@pyricau

Author of LeakCanary 🍷🥖⛷🇫🇷

Discussion

markdown guide
 

Isn't "creating the main thread" before "creating the app object" ?
That's how some dependencies start before the app, no?

 

Absolutely, the main thread is the first thread created so that happens before the Application instance is created. I'm not certain what the Android documentation is trying to convey here, it's likely wrong or maybe to very clear.

 

So, the diagram doesn't seem right, and that's the first thing I read...

I was trying to show what the doc says vs what's actually happening. I guess I could have added a "please read the words above and below this diagram" header 😉

Can you please show the original of those mistakes?
Maybe you should report it to Google:
issuetracker.google.com/issues

I think I know why I missed it. I use an addon to make all websites in dark, and thus the link text looks exactly like normal one, so I didn't notice there is a link....