DEV Community

Cover image for Android Vitals - Diving into cold start waters πŸ₯Ά
Py βš”
Py βš”

Posted on

Android Vitals - Diving into cold start waters πŸ₯Ά

Header image: A Song of Ice and Fire by Romain Guy.

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?

Over the next blog posts, I will explore how to monitor cold start. According to the App startup time documentation:

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.

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

Diagram created with WebSequenceDiagram.

Activity.startActivity()

Google Maps launcher icon

When a user taps a launcher icon, the launcher app process calls Activity.startActivity(), which delegates to Instrumentation.execStartActivity():

public class Instrumentation {

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

The launcher app process then makes an IPC call to ActivityTaskManagerService.startActivity() in the system_server process. The system_server process hosts most system services.

Diagram: startActivity IPC

Staring at the starting window πŸ‘€

Before creating a new app process, the system_server process creates a starting window via PhoneWindowManager.addSplashScreen():

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

The starting window content is loaded from the started activity's windowSplashscreenContent and windowBackground drawables. To learn more, check out Android App Launching Made Gorgeous.

Google Maps starting window

If the user brings back an activity from the Recents screen instead of tapping a launcher icon, the system_server process calls TaskSnapshotSurface.create() 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():

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() sends the starting arguments over a socket to the Zygote process.

Forking Zygote 🍴

According to the Android documentation on memory management:

Each app process is forked 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():

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() 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() which opens a socket and waits.

When a forking command is received on that socket, ZygoteConnection.processOneCommand() parses the arguments via ZygoteArguments.parseArgs() and calls Zygote.forkAndSpecialize():

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

Note: Android 10 added support for an optimization called Unspecialized App Process (USAP), 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 which gives much better results.

An App is Born ✨

Once forked, the child app process runs RuntimeInit.commonInit()
which installs the default UncaughtExceptionHandler. Then the app process runs ActivityThread.main():

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:

Diagram: ActivityManagerService.attachApplication

App Puppeteering

In the system_server process, ActivityManagerService.attachApplication() calls ActivityManagerService.attachApplicationLocked() which finishes setting up the application:

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:

Diagram: handleBindApplication()

Early initialization

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

  • The earliest hook is when the AppComponentFactory class is loaded.
    • Add the appComponentFactory attribute 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
    • 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().
  • A safe early hook for library developers is ContentProvider.onCreate(). This trick was popularized by Doug Stevenson in How does Firebase initialize on Android?
  • There's a new AndroidX App Startup library 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

Now we know exactly what happens:

Cold start sequence diagram

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!

Top comments (8)

Collapse
 
kuanyingchou profile image
Kuan-Ying Chou

Great article and thanks for the series! I'm wondering why Application subclass constructors are not one of the options for early hook. It's called earlier than Application.onCreate() or ContentProvider.onCreate(). Is it because it's not safe?

Collapse
 
pyricau profile image
Py βš”

Great question! In the Application subclass constructor the base context of the Application instance isn't set up. That means you can't call any of the context related methods, e.g. access the app file system, etc. Probably the same level of safety as doing work on classloading, etc.

Collapse
 
kuanyingchou profile image
Kuan-Ying Chou

Thanks for reply! Does that mean storing a timestamp in memory there is ok-ish?

Thread Thread
 
pyricau profile image
Py βš”

Yep! I'll cover that topic in more details in a follow up post ;)

Thread Thread
 
kuanyingchou profile image
Kuan-Ying Chou

Nice!

Collapse
 
maxxx profile image
maxdestroyer

Thanks for the article! Is there any chance to implement dynamic change of theme-based things? Like android:windowBackground . Looks like it's loaded from manifest and used for Preview window (starting and switches between activity stacks). So programmatical changes like setTheme() (or changing background of getWindow()) in Activity or Application doesn't affect it. It makes trouble when the app should use custom themes with different colors, but android:windowBackground is same for all of them.

Collapse
 
pyricau profile image
Py βš”

The theme of whichever activity is launched will be used, and that's the only way you can somewhat customize the launching window style, since it runs before any of your code runs.

Collapse
 
moayad1997 profile image
Moayad Ab

Great article, Thanks for your effort