Android Vitals - Diving into cold start waters 🥶
Tags: android, vitals, performance, monitoring
Published: 2020-07-20 14:32:24

> Header image: *A Song of Ice and Fire* [by Romain Guy](https://www.flickr.com/photos/romainguy/6899855819).

This blog series is focused on stability and performance monitoring of Android apps in production. Last week, I wrote about measuring time in [Android Vitals - What time is it?](https://dev.to/pyricau/android-vitals-what-time-is-it-2oih)

Over the next blog posts, I will explore how to monitor **cold start**. According to the [App startup time documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold):

> A cold start refers to an app's starting from scratch: the system's process has not, until this start, created the app's process. Cold starts happen in cases such as your app's being launched for the first time since the device booted, or since the system killed the app.
>
> At the beginning of a cold start, the system has 3 tasks:
> 1. Loading and launching the app.
> 2. Displaying a starting window.
> 3. Creating the app [process](https://developer.android.com/guide/components/processes-and-threads#Processes).

This post is a deep dive on the **beginning of a cold start**, from tapping a launcher icon to the creation of the app process. 

![Diagram: cold start from documentation](https://dev-to-uploads.s3.amazonaws.com/i/i3vrdsk7b61dlu75y364.png)

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

<!--
User->The System: Tap launcher icon

state over The System: Loading and launching the app

The System->User: Displaying a starting window

state over The System: Creating the app process
-->

# Activity.startActivity()

![Google Maps launcher icon](https://dev-to-uploads.s3.amazonaws.com/i/3w4ujwcwwh1zd6262k59.png)

When a user taps a launcher icon, the launcher app process calls [Activity.startActivity()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Activity.java;l=5533;drc=69e700c7922c34583bddbab4b7ce78adadb90730), which delegates to [Instrumentation.execStartActivity()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Instrumentation.java;l=1711-1715;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public class Instrumentation {

  public ActivityResult execStartActivity(...) {
    ...
    ActivityTaskManager.getService()
        .startActivity(...);
  }
}
```

The launcher app process then makes an [IPC](https://source.android.com/devices/architecture) call to [ActivityTaskManagerService.startActivity()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=1009-1015?q=IActivityTaskManager&ss=android%2Fplatform%2Fsuperproject) in the `system_server` process. The `system_server` process hosts most system services.

![Diagram: startActivity IPC](https://dev-to-uploads.s3.amazonaws.com/i/qyii4r1b994dm50tvc4u.png)

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

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

# Staring at the starting window 👀

Before creating a new app process, the `system_server` process creates a **starting window** via [PhoneWindowManager.addSplashScreen()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java;l=2342;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public class PhoneWindowManager implements WindowManagerPolicy {

  public StartingSurface addSplashScreen(...) {
    ...
    PhoneWindow win = new PhoneWindow(context);
    win.setIsStartingWindow(true);
    win.setType(TYPE_APPLICATION_STARTING);
    win.setTitle(label);
    win.setDefaultIcon(icon);
    win.setDefaultLogo(logo);
    win.setLayout(MATCH_PARENT, MATCH_PARENT);
    
    addSplashscreenContent(win, context);
    
    WindowManager wm = (WindowManager) context.getSystemService(
      WINDOW_SERVICE
    );
    View view = win.getDecorView();
    wm.addView(view, params);
    ...
  }
  
  private void addSplashscreenContent(PhoneWindow win,
      Context ctx) {
    TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
    int resId = a.getResourceId(
      R.styleable.Window_windowSplashscreenContent,
      0
    );
    a.recycle();
    Drawable drawable = ctx.getDrawable(resId);
    View v = new View(ctx);
    v.setBackground(drawable);
    win.setContentView(v);
  }
}
```

The **starting window** is what a user sees while the app process starts, until it creates its activity and draws its first frame, i.e. **until cold start is done**. The user could be staring at the starting window for a long time, so **make sure it looks good** 😎.

![Diagram: display starting window](https://dev-to-uploads.s3.amazonaws.com/i/noysdmq6p1rpssxj7dwg.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
-->


The starting window content is loaded from the started activity's [windowSplashscreenContent](https://developer.android.com/reference/android/R.attr#windowSplashscreenContent) and [windowBackground](https://developer.android.com/reference/android/R.attr#windowBackground) drawables. To learn more, check out [Android App Launching Made Gorgeous](https://cyrilmottier.com/2013/01/23/android-app-launching-made-gorgeous/).

![Google Maps starting window](https://dev-to-uploads.s3.amazonaws.com/i/g1dqkirifr11fbdqll34.png)

If the user brings back an activity from the [Recents screen](https://developer.android.com/guide/components/activities/recents) instead of tapping a launcher icon, the `system_server` process calls [TaskSnapshotSurface.create()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskSnapshotSurface.java;l=137;drc=69e700c7922c34583bddbab4b7ce78adadb90730) to create a starting window that draws a saved snapshot of the activity.

Once the starting window is displayed, the `system_server` process is ready to start the app process and calls [ZygoteProcess.startViaZygote()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/ZygoteProcess.java;l=605;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public class ZygoteProcess {
  private Process.ProcessStartResult startViaZygote(...) {
    ArrayList<String> argsForZygote = new ArrayList<>();
    argsForZygote.add("--runtime-args");
    argsForZygote.add("--setuid=" + uid);
    argsForZygote.add("--setgid=" + gid);
    argsForZygote.add("--runtime-flags=" + runtimeFlags);
    ...
    return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
                                          zygotePolicyFlags,
                                          argsForZygote);
  }
}
```

[ZygoteProcess.zygoteSendArgsAndGetResult()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/ZygoteProcess.java;l=405;drc=69e700c7922c34583bddbab4b7ce78adadb90730) sends the **starting arguments over a socket** to the **Zygote process**.

# Forking Zygote 🍴

According to the Android documentation on [memory management](https://developer.android.com/topic/performance/memory-overview#SharingRAM):

> Each app process is [forked](https://en.wikipedia.org/wiki/Fork_(system_call)) from an existing process called Zygote. The Zygote process starts when the system boots and loads common framework code and resources (such as activity themes). To start a new app process, the system forks the Zygote process then loads and runs the app's code in the new process. This approach allows most of the RAM pages allocated for framework code and resources to be shared across all app processes.

When the system boots, the Zygote process starts and invokes [ZygoteInit.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java;l=915;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public class ZygoteInit {

  public static void main(String argv[]) {
    ...
    if (!enableLazyPreload) {
        preload(bootTimingsTraceLog);
    }
    // The select loop returns early in the child process after
    // a fork and loops forever in the zygote.
    caller = zygoteServer.runSelectLoop(abiList);
    // We're in the child process and have exited the
    // select loop. Proceed to execute the command.
    if (caller != null) {
      caller.run();
    }
  }

  static void preload(TimingsTraceLog bootTimingsTraceLog) {
    preloadClasses();
    cacheNonBootClasspathClassLoaders();
    preloadResources();
    nativePreloadAppProcessHALs();
    maybePreloadGraphicsDriver();
    preloadSharedLibraries();
    preloadTextResources();
    WebViewFactory.prepareWebViewInZygote();
    warmUpJcaProviders();
  }
}
```

As you can see, [ZygoteInit.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java;l=915;drc=69e700c7922c34583bddbab4b7ce78adadb90730) does 2 important things:
* It preloads the Android framework classes & resources, shared libraries, graphic drivers, etc. This preloading doesn't just save memory, it also improves startup time.
* Then it calls [ZygoteServer.runSelectLoop()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/ZygoteServer.java;l=424;drc=69e700c7922c34583bddbab4b7ce78adadb90730) which opens a socket and waits.

When a forking command is received on that socket, [ZygoteConnection.processOneCommand()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java;l=121;drc=69e700c7922c34583bddbab4b7ce78adadb90730) parses the arguments via [ZygoteArguments.parseArgs()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/ZygoteArguments.java;drc=69e700c7922c34583bddbab4b7ce78adadb90730;l=239) and calls [Zygote.forkAndSpecialize()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/Zygote.java;l=285;drc=69e700c7922c34583bddbab4b7ce78adadb90730):

```java
public final class Zygote {

  public static int forkAndSpecialize(...) {
    ZygoteHooks.preFork();

    int pid = nativeForkAndSpecialize(...);

    // Set the Java Language thread priority to the default value.
    Thread.currentThread().setPriority(Thread.NORM_PRIORITY);

    ZygoteHooks.postForkCommon();
    return pid;
  }
}
```

![Diagram: zygote fork](https://dev-to-uploads.s3.amazonaws.com/i/8xqcaxu0wdpypf5w3748.png)

<!--
system_server -> Zygote: socket:\nfork command

Zygote->App process: fork()

state over App process: Process created
-->


Note: Android 10 added support for an optimization called Unspecialized App Process ([USAP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/ZygoteProcess.java;l=87;drc=69e700c7922c34583bddbab4b7ce78adadb90730)), a pool of forked Zygotes waiting to be specialized. Slightly faster startup at the cost of extra memory (turned off by default). Android 11 shipped with [IORap](https://medium.com/androiddevelopers/improving-app-startup-with-i-o-prefetching-62fbdb9c9020) which gives much better results.

# An App is Born ✨

Once forked, the child app process runs [RuntimeInit.commonInit()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java;l=251;drc=69e700c7922c34583bddbab4b7ce78adadb90730) 
 which installs the [default UncaughtExceptionHandler](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java;l=260;drc=69e700c7922c34583bddbab4b7ce78adadb90730). Then the app process runs [ActivityThread.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7350;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3):

```java
public final class ActivityThread {

  public static void main(String[] args) {
    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    Looper.loop();
  }

  final ApplicationThread mAppThread = new ApplicationThread();

  private void attach(boolean system, long startSeq) {
    if (!system) {
      IActivityManager mgr = ActivityManager.getService();
      mgr.attachApplication(mAppThread, startSeq);
    }
  }
}
```

There are two interesting parts here:

* [ActivityThread.main()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7350;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3) 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, waiting for new messages to be posted to its [MessageQueue](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/MessageQueue.java;l=41;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
* [ActivityThread.attach()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7111;drc=69e700c7922c34583bddbab4b7ce78adadb90730) makes an 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 to let it know that the app's **main** thread is ready to go 🚀.

![Diagram: ActivityManagerService.attachApplication](https://dev-to-uploads.s3.amazonaws.com/i/57kdxf83tv93dc3xxdog.png)

<!--
participant system_server

state over App process: Process created

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

# App Puppeteering

In the `system_server` process, [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) calls [ActivityManagerService.attachApplicationLocked()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=4791;drc=69e700c7922c34583bddbab4b7ce78adadb90730;bpv=0;bpt=1) which finishes setting up the application:

```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;
  }
}
```

A few key takeaways:

* 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 schedules a call to [ActivityThread.handleBindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=6162;drc=69e700c7922c34583bddbab4b7ce78adadb90730) on the application main thread.
* Immediately after, the `system_server` process schedules the start of any pending activity, service, and broadcast receiver.
* [ActivityThread.handleBindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=6162;drc=69e700c7922c34583bddbab4b7ce78adadb90730) loads the APK and loads the app components in the following order:
  * Load the app [AppComponentFactory](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/AppComponentFactory.java;l=35;drc=69e700c7922c34583bddbab4b7ce78adadb90730) subclass and create an instance.
  * Call [AppComponentFactory.instantiateClassLoader()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/AppComponentFactory.java;l=56;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
  * Call [AppComponentFactory.instantiateApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/AppComponentFactory.java;l=73;drc=69e700c7922c34583bddbab4b7ce78adadb90730) to load the app [Application](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Application.java;drc=69e700c7922c34583bddbab4b7ce78adadb90730;l=52) subclass and create an instance.
  * For each declared [ContentProvider](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/ContentProvider.java;drc=69e700c7922c34583bddbab4b7ce78adadb90730;l=108), in [priority order](https://developer.android.com/guide/topics/manifest/provider-element.html#init), call [AppComponentFactory.instantiateProvider()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/AppComponentFactory.java;l=144;drc=69e700c7922c34583bddbab4b7ce78adadb90730) to load its class and create an instance, then call [ContentProvider.onCreate()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/ContentProvider.java;l=1048;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
  * Call [Application.onCreate()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Application.java;l=247;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
* App developers have little influence on the time spent before [ActivityThread.handleBindApplication()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=6162;drc=69e700c7922c34583bddbab4b7ce78adadb90730), so that's where **app cold start monitoring should start**.

![Diagram: handleBindApplication()](https://dev-to-uploads.s3.amazonaws.com/i/7c1ivkjz9b4it2imzcf2.png)

<!--
participant system_server

state over App process: Process created

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

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

  Loads APK & app components. Monitor cold start here.

App process->App process: handleBindApplication()
-->


# Early initialization

If you need to run code as early as possible, you have several options:

* The earliest hook is when the [AppComponentFactory](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/AppComponentFactory.java;drc=69e700c7922c34583bddbab4b7ce78adadb90730;bpv=1;bpt=1;l=35) class is loaded.
  * Add the [appComponentFactory attribute](https://developer.android.com/reference/android/R.styleable#AndroidManifestApplication_appComponentFactory) to the application tag in `AndroidManifest.xml`.
  * If you use AndroidX, you need to add `tools:replace="android:appComponentFactory"` and delegate calls to the [AndroidX AppComponentFactory](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:core/core/src/main/java/androidx/core/app/AppComponentFactory.java;l=40;drc=229b5b258af6aa8071ef5aeb805669a376223730)
  * You can add a static initializer there and do things like storing a timestamp.
  * Downsides: this is only available in Android P+, and you won't have access to a context.
* A safe early hook for app developers is [Application.onCreate()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Application.java;l=247;drc=69e700c7922c34583bddbab4b7ce78adadb90730).
* A safe early hook for library developers is [ContentProvider.onCreate()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/ContentProvider.java;l=1048;drc=69e700c7922c34583bddbab4b7ce78adadb90730). This trick was popularized by **Doug Stevenson** in [How does Firebase initialize on Android?](https://firebase.googleblog.com/2016/12/how-does-firebase-initialize-on-android.html)
* There's a new AndroidX [App Startup library](https://developer.android.com/topic/libraries/app-startup) which relies on the same provider trick. The goal is to have just one provider declared instead of many, because each declared provider slows the app start by a few milliseconds and increases the size of the ApplicationInfo object from package manager.

# Conclusion

We started with a high level understanding of how cold start begins:

![Diagram: cold start from documentation](https://dev-to-uploads.s3.amazonaws.com/i/i3vrdsk7b61dlu75y364.png)

Now we know exactly what happens:

![Cold start sequence diagram](https://dev-to-uploads.s3.amazonaws.com/i/puh3wi86r25lhktnnyxv.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 process: fork()

state over App process: Process created

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

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

  Loads APK & app components. Monitor cold start here.

App process->App process: handleBindApplication()
-->

The user experience of launching an activity starts when the user touches the screen, however app developers have little influence on the time spent before  `ActivityThread.handleBindApplication()`, so that's where app cold start monitoring should start.

That was a long post, and we're far from being done with cold start. Stay tuned for more!