chromium/chrome/android/java/src/org/chromium/chrome/browser/base/ColdStartTracker.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.base;

import android.app.Activity;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Process;
import android.os.SystemClock;

import androidx.annotation.RequiresApi;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.build.BuildConfig;
import org.chromium.chrome.browser.base.SplitCompatAppComponentFactory.ProcessCreationReason;
import org.chromium.chrome.browser.metrics.SimpleStartupForegroundSessionDetector;

/**
 * Utility class to guess whether the first Activity was created during application cold start.
 *
 * <p>
 * Cold startup is most important in cases when an activity is created and needs to become
 * interactive as fast as possible, while the browser is still initializing. Android OS can
 * create the app process in many ways without an Activity and keep it "cached" for a long time
 * before the first Activity gets created. This quite common course of events runs faster and is
 * therefore considered to be a "warm" start.
 *
 * <p>
 * For this class, the "cold start" is a time interval closely related to the lifecycle of the first
 * Activity created in the app process lifetime.
 *
 * <p>
 * The cold start interval is _empty_ if the process creation was not _caused_ by the creation of an
 * Activity. In other words, when the process is instantiated due to receiving a broadcast,
 * instantiating a service or a content provider, Chrome gets initialized during that time and can
 * remain idle (frozen) for a long time before the first Activity is created. Otherwise, the cold
 * start interval starts at process creation and ends when the first activity returns from
 * onCreate().
 *
 * <p>
 * It is possible to confirm that it was cold start when the first Activity started. Detecting that
 * no other Activities intervened since then is more difficult and is not always possible using the
 * ApplicationStatus because activity state changes arrive when the activity calls corresponding
 * methods of the superclass (e.g. performs super.onPause()).
 *
 * <p>
 * Note: This class is not ideal for use when recording UMA metrics filtered by cold startup because
 * it applies a time-based heuristic on Android releases prior to P. On these systems the app may
 * sometimes appear as cold even if it has been fully initialized a little bit in advance. UMA
 * histograms based on this heuristic may feature an unwanted population-biased hump.
 */
public class ColdStartTracker implements ActivityStateListener {
    private static ColdStartTracker sColdStartTracker;

    private Boolean mStartedAsCold;

    private ColdStartTracker() {
        assert ApplicationStatus.isInitialized();
        ApplicationStatus.registerStateListenerForAllActivities(this);
    }

    /** Must be called after {@link ApplicationStatus} is initialized. */
    public static void initialize() {
        assert sColdStartTracker == null;
        sColdStartTracker = new ColdStartTracker();
    }

    /**
     * Robolectric JUnit tests create a new application between each test without resetting static
     * state. This method allows to reset the state and then {@link #initialize()} again.
     */
    public static void resetInstanceForTesting() {
        sColdStartTracker = null;
    }

    public static void setStartedAsColdForTesting() {
        if (sColdStartTracker == null) initialize();
        sColdStartTracker.mStartedAsCold = true;
    }

    @Override
    public void onActivityStateChange(Activity activity, @ActivityState int newState) {
        if (newState == ActivityState.CREATED) {
            detectStartedAsCold();
            ApplicationStatus.unregisterActivityStateListener(this);
        }
    }

    // Must be called during onCreate() (or earlier) of the first activity created in the process.
    private void detectStartedAsCold() {
        if (mStartedAsCold != null) return;
        if (VERSION.SDK_INT >= VERSION_CODES.P) {
            mStartedAsCold = isColdStartupOnP();
            return;
        }

        // Fallback: treat recently started process as cold.
        mStartedAsCold = (SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() < 500);
    }

    @RequiresApi(Build.VERSION_CODES.P)
    private boolean isColdStartupOnP() {
        @ProcessCreationReason
        int creationReason = SplitCompatAppComponentFactory.getProcessCreationReason();
        if (creationReason <= ProcessCreationReason.PENDING) {
            assert BuildConfig.IS_FOR_TEST;
            // The process creation hooks have not been run, therefore this is a cold start.
            // This condition was observed in robolectric tests. Unlikely to happen on real devices.
            return true;
        }

        // Service connections, content providers and broadcast receivers could have created the
        // process long before the activity was created. These other process creation reasons likely
        // make it a warm start.
        return creationReason == ProcessCreationReason.ACTIVITY;
    }

    /**
     * Tells whether it was cold start when the first Activity was created. If called before any
     * Activity is fully created, considers that the call is made from the first Activity.onCreate()
     * and tells whether it is starting as cold.
     *
     * <p>
     * It is highly recommended to combine this heuristic with
     * {@link SimpleStartupForegroundSessionDetector#runningCleanForegroundSession()} to filter out
     * cases when another Activity was created.
     *
     * <p>
     * This method must *not* be called from code running before the first Activity (such as in
     * Application).
     */
    public static boolean wasColdOnFirstActivityCreationOrNow() {
        if (BuildConfig.IS_FOR_TEST && sColdStartTracker == null) return false;
        return sColdStartTracker.firstActivityWasColdOrDidNotGetCreatedYet();
    }

    private boolean firstActivityWasColdOrDidNotGetCreatedYet() {
        detectStartedAsCold();
        return mStartedAsCold;
    }
}