[Clank Startup] Adding a Clank Startup Latency Ablation Experiment

This experiment will add a fixed-amount of latency to Clank startup
during Main Intent launches to determine downstream effects.

The ablation is injected in performPostInflationStartup at an Activity level via busy-waiting.

Bug: 334143643
Change-Id: Ifc7cbe1f55cd6427b947a7a282329686f15acd61
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5818012
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Reviewed-by: Sean Maher <spvw@chromium.org>
Commit-Queue: Nafis Abedin <nafisabedin@google.com>
Reviewed-by: Michael Thiessen <mthiesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1352091}
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index 8c5f4893..d9280d1 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -747,6 +747,7 @@
   "java/src/org/chromium/chrome/browser/instantapps/InstantAppsHandler.java",
   "java/src/org/chromium/chrome/browser/invalidation/ResumableDelayedTaskRunner.java",
   "java/src/org/chromium/chrome/browser/invalidation/SessionsInvalidationManager.java",
+  "java/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjector.java",
   "java/src/org/chromium/chrome/browser/lens/LensDebugBridge.java",
   "java/src/org/chromium/chrome/browser/lens/LensPolicyUtils.java",
   "java/src/org/chromium/chrome/browser/login/ChromeHttpAuthHandler.java",
diff --git a/chrome/android/chrome_test_java_sources.gni b/chrome/android/chrome_test_java_sources.gni
index 0e9820d..00d7278 100644
--- a/chrome/android/chrome_test_java_sources.gni
+++ b/chrome/android/chrome_test_java_sources.gni
@@ -250,6 +250,7 @@
   "javatests/src/org/chromium/chrome/browser/javascript/CloseWatcherTest.java",
   "javatests/src/org/chromium/chrome/browser/jsdialog/JavascriptAppModalDialogTest.java",
   "javatests/src/org/chromium/chrome/browser/jsdialog/JavascriptTabModalDialogTest.java",
+  "javatests/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjectorTest.java",
   "javatests/src/org/chromium/chrome/browser/locale/LocaleManagerReferralTest.java",
   "javatests/src/org/chromium/chrome/browser/locale/LocaleManagerTest.java",
   "javatests/src/org/chromium/chrome/browser/login/ChromeHttpAuthHandlerTest.java",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
index cbba0322..706f3e9c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
@@ -129,6 +129,7 @@
 import org.chromium.chrome.browser.incognito.IncognitoTabbedSnapshotController;
 import org.chromium.chrome.browser.incognito.IncognitoUtils;
 import org.chromium.chrome.browser.init.ActivityProfileProvider;
+import org.chromium.chrome.browser.latency_injection.StartupLatencyInjector;
 import org.chromium.chrome.browser.layouts.LayoutStateProvider;
 import org.chromium.chrome.browser.layouts.LayoutType;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
@@ -1055,6 +1056,30 @@
         }
     }
 
+    private boolean isMainIntentLaunch() {
+        assert !mFromResumption : "Method is correct only when it's a new Activity launch.";
+
+        Intent launchIntent = getIntent();
+        if (launchIntent == null) return false;
+
+        // Also ignore if launched from recents.
+        if (0 != (launchIntent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)) {
+            return false;
+        }
+
+        if (IntentUtils.isMainIntentFromLauncher(launchIntent)) {
+            return true;
+        }
+
+        if (IntentUtils.safeGetBooleanExtra(
+                        launchIntent, IntentHandler.EXTRA_INVOKED_FROM_SHORTCUT, false)
+                && IntentHandler.wasIntentSenderChrome(launchIntent)) {
+            return true;
+        }
+
+        return false;
+    }
+
     @Override
     protected OneshotSupplier<ProfileProvider> createProfileProvider() {
         return new ActivityProfileProvider(getLifecycleDispatcher());
@@ -1990,6 +2015,11 @@
     public void performPreInflationStartup() {
         super.performPreInflationStartup();
 
+        if (isMainIntentLaunch()) {
+            StartupLatencyInjector startupLatencyInjector = new StartupLatencyInjector();
+            startupLatencyInjector.maybeInjectLatency();
+        }
+
         // Android FrameMetrics allow tracking of java views and their deadline misses (frame
         // drops/janks).
         if (ChromeFeatureList.sCollectAndroidFrameTimelineMetrics.isEnabled()) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java b/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java
index 05f115e..d20dbd2 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java
@@ -27,6 +27,7 @@
 import org.chromium.chrome.browser.firstrun.FirstRunUtils;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.hub.HubFieldTrial;
+import org.chromium.chrome.browser.latency_injection.StartupLatencyInjector;
 import org.chromium.chrome.browser.logo.LogoUtils;
 import org.chromium.chrome.browser.magic_stack.HomeModulesMetricsUtils;
 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
@@ -130,6 +131,7 @@
                         SuggestionsNavigationDelegate.MOST_VISITED_TILES_RESELECT_LAX_QUERY,
                         SuggestionsNavigationDelegate.MOST_VISITED_TILES_RESELECT_LAX_REF,
                         SuggestionsNavigationDelegate.MOST_VISITED_TILES_RESELECT_LAX_SCHEME_HOST,
+                        StartupLatencyInjector.CLANK_STARTUP_LATENCY_PARAM_MS,
                         TabManagementFieldTrial.DELAY_TEMP_STRIP_TIMEOUT_MS,
                         HomeModulesMetricsUtils.HOME_MODULES_SHOW_ALL_MODULES,
                         HomeModulesMetricsUtils.TAB_RESUMPTION_COMBINE_TABS,
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/latency_injection/OWNERS b/chrome/android/java/src/org/chromium/chrome/browser/latency_injection/OWNERS
new file mode 100644
index 0000000..c19374d6
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/latency_injection/OWNERS
@@ -0,0 +1 @@
+mthiesse@chromium.org
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjector.java b/chrome/android/java/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjector.java
new file mode 100644
index 0000000..00d03d57
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjector.java
@@ -0,0 +1,56 @@
+// Copyright 2024 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.latency_injection;
+
+import org.chromium.base.TimeUtils;
+import org.chromium.base.TimeUtils.UptimeMillisTimer;
+import org.chromium.base.cached_flags.IntCachedFieldTrialParameter;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
+
+public final class StartupLatencyInjector {
+    private static final String LATENCY_INJECTION_PARAM = "latency_injection_amount_millis";
+
+    private static final int LATENCY_INJECTION_DEFAULT_MILLIS = 0;
+
+    private static final String HISTOGRAM_TOTAL_WAIT_TIME =
+            "Startup.Android.MainIconLaunchTotalWaitTime";
+
+    private final Long mBusyWaitDurationMillis;
+
+    /**
+     * A cached parameter representing the amount of latency to inject during Clank startup based on
+     * experiment configuration.
+     */
+    public static final IntCachedFieldTrialParameter CLANK_STARTUP_LATENCY_PARAM_MS =
+            ChromeFeatureList.newIntCachedFieldTrialParameter(
+                    ChromeFeatureList.CLANK_STARTUP_LATENCY_INJECTION,
+                    LATENCY_INJECTION_PARAM,
+                    LATENCY_INJECTION_DEFAULT_MILLIS);
+
+    public StartupLatencyInjector() {
+        mBusyWaitDurationMillis = Long.valueOf(CLANK_STARTUP_LATENCY_PARAM_MS.getValue());
+    }
+
+    private boolean isEnabled() {
+        return ChromeFeatureList.sClankStartupLatencyInjection.isEnabled();
+    }
+
+    public void maybeInjectLatency() {
+        if (!isEnabled() || mBusyWaitDurationMillis <= 0) {
+            return;
+        }
+
+        long startTime = TimeUtils.uptimeMillis();
+        busyWait();
+        long totalWaitTime = TimeUtils.uptimeMillis() - startTime;
+        RecordHistogram.recordMediumTimesHistogram(HISTOGRAM_TOTAL_WAIT_TIME, totalWaitTime);
+    }
+
+    private void busyWait() {
+        UptimeMillisTimer timer = new UptimeMillisTimer();
+        while (mBusyWaitDurationMillis.compareTo(timer.getElapsedMillis()) >= 0);
+    }
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/latency_injection/OWNERS b/chrome/android/javatests/src/org/chromium/chrome/browser/latency_injection/OWNERS
new file mode 100644
index 0000000..a35fd0d
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/latency_injection/OWNERS
@@ -0,0 +1 @@
+file://chrome/android/java/src/org/chromium/chrome/browser/latency_injection/OWNERS
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjectorTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjectorTest.java
new file mode 100644
index 0000000..4e5d5fbe
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/latency_injection/StartupLatencyInjectorTest.java
@@ -0,0 +1,43 @@
+// Copyright 2024 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.latency_injection;
+
+import androidx.test.filters.LargeTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.DoNotBatch;
+import org.chromium.base.test.util.Features.EnableFeatures;
+import org.chromium.base.test.util.HistogramWatcher;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
+
+@RunWith(ChromeJUnit4ClassRunner.class)
+@EnableFeatures(ChromeFeatureList.CLANK_STARTUP_LATENCY_INJECTION)
+@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
+@DoNotBatch(reason = "Tests require cold browser start.")
+public class StartupLatencyInjectorTest {
+    @Rule
+    public ChromeTabbedActivityTestRule mTabbedActivityTestRule =
+            new ChromeTabbedActivityTestRule();
+
+    private static final String HISTOGRAM_TOTAL_WAIT_TIME =
+            "Startup.Android.MainIconLaunchTotalWaitTime";
+
+    @Test
+    @LargeTest
+    public void checkLatencyInjectedForMainIntentLaunch() throws Exception {
+        HistogramWatcher watcher =
+                HistogramWatcher.newSingleRecordWatcher(HISTOGRAM_TOTAL_WAIT_TIME);
+        StartupLatencyInjector.CLANK_STARTUP_LATENCY_PARAM_MS.setForTesting(100);
+        mTabbedActivityTestRule.startMainActivityFromLauncher();
+        watcher.assertExpected();
+    }
+}
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index 0e0c25ea..26b31c0 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -229,6 +229,7 @@
     &kCacheDeprecatedSystemLocationSetting,
     &kChromeSharePageInfo,
     &kChromeSurveyNextAndroid,
+    &kClankStartupLatencyInjection,
     &kCommandLineOnNonRooted,
     &kContextMenuTranslateWithGoogleLens,
     &kContextMenuSysUiMatchesActivity,
@@ -641,6 +642,10 @@
              "ChromeSurveyNextAndroid",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kClankStartupLatencyInjection,
+             "ClankStartupLatencyInjection",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 BASE_FEATURE(kCommandLineOnNonRooted,
              "CommandLineOnNonRooted",
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
index 0ddb20a..86569c8 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -78,6 +78,7 @@
 BASE_DECLARE_FEATURE(kChromeShareScreenshot);
 BASE_DECLARE_FEATURE(kChromeSharingHubLaunchAdjacent);
 BASE_DECLARE_FEATURE(kChromeSurveyNextAndroid);
+BASE_DECLARE_FEATURE(kClankStartupLatencyInjection);
 BASE_DECLARE_FEATURE(kCommandLineOnNonRooted);
 BASE_DECLARE_FEATURE(kContextMenuSysUiMatchesActivity);
 BASE_DECLARE_FEATURE(kContextMenuTranslateWithGoogleLens);
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index ff04119..ee0446cf 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -268,6 +268,7 @@
     public static final String CCT_TAB_MODAL_DIALOG = "CCTTabModalDialog";
     public static final String CHROME_SURVEY_NEXT_ANDROID = "ChromeSurveyNextAndroid";
     public static final String CHROME_SHARE_PAGE_INFO = "ChromeSharePageInfo";
+    public static final String CLANK_STARTUP_LATENCY_INJECTION = "ClankStartupLatencyInjection";
     public static final String COLLECT_ANDROID_FRAME_TIMELINE_METRICS =
             "CollectAndroidFrameTimelineMetrics";
     public static final String COMMAND_LINE_ON_NON_ROOTED = "CommandLineOnNonRooted";
@@ -600,6 +601,8 @@
     public static final CachedFlag sCctNestedSecurityIcon =
             newCachedFlag(CCT_NESTED_SECURITY_ICON, false);
     public static final CachedFlag sCctTabModalDialog = newCachedFlag(CCT_TAB_MODAL_DIALOG, true);
+    public static final CachedFlag sClankStartupLatencyInjection =
+            newCachedFlag(CLANK_STARTUP_LATENCY_INJECTION, false);
     public static final CachedFlag sCollectAndroidFrameTimelineMetrics =
             newCachedFlag(COLLECT_ANDROID_FRAME_TIMELINE_METRICS, false);
     public static final CachedFlag sCommandLineOnNonRooted =
@@ -755,6 +758,7 @@
                     sCctRevampedBranding,
                     sCctNestedSecurityIcon,
                     sCctTabModalDialog,
+                    sClankStartupLatencyInjection,
                     sCollectAndroidFrameTimelineMetrics,
                     sCommandLineOnNonRooted,
                     sCrossDeviceTabPaneAndroid,
diff --git a/tools/metrics/histograms/metadata/startup/histograms.xml b/tools/metrics/histograms/metadata/startup/histograms.xml
index 12999057..20f567e8 100644
--- a/tools/metrics/histograms/metadata/startup/histograms.xml
+++ b/tools/metrics/histograms/metadata/startup/histograms.xml
@@ -564,6 +564,22 @@
   </summary>
 </histogram>
 
+<histogram name="Startup.Android.MainIconLaunchTotalWaitTime" units="ms"
+    expires_after="2025-03-03">
+  <owner>nafisabedin@google.com</owner>
+  <owner>yfriedman@chromium.org</owner>
+  <summary>
+    The total time a user waits for the Clank startup ablation study. The
+    enabled arms of the study prescribes an amount of time to delay the launch
+    of Clank, and this histogram records the amount of time waited to verify
+    ablation and any additional delay.
+
+    Recorded only when the main Chrome launcher icon is used to start the app
+    (i.e. LaunchCauseType is MAIN_LAUNCHER_ICON or MAIN_LAUNCHER_ICON_SHORTCUT)
+    and after the ablation occurs.
+  </summary>
+</histogram>
+
 <histogram name="Startup.Android.MainIntentIsColdStart" enum="Boolean"
     expires_after="2025-02-08">
   <owner>nafisabedin@google.com</owner>