Allow using startup profile only for guiding optimizations

Bug: b/284929119
Change-Id: I0ce07973f570f7e60d9de27593e14b9e057d1111
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 07b3ef5..d2647bb 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -137,6 +137,7 @@
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableExperimentalKeepAnnotations =
         System.getProperty("com.android.tools.r8.enableKeepAnnotations") != null;
+    public boolean enableStartupLayoutOptimization = true;
     private SemanticVersion fakeCompilerVersion = null;
     private AndroidResourceProvider androidResourceProvider = null;
     private AndroidResourceConsumer androidResourceConsumer = null;
@@ -290,7 +291,7 @@
       return super.setProguardMapOutputPath(proguardMapOutput);
     }
 
-    /** Set input proguard map used for distribution of classes in multi-dex. */
+    /** Set input proguard map used for distribution of classes in multi-DEX. */
     public Builder setProguardMapInputFile(Path proguardInputMap) {
       getAppBuilder().setProguardMapInputData(proguardInputMap);
       return self();
@@ -507,11 +508,11 @@
 
     /**
      * Add a collection of startup profile providers that should be used for distributing the
-     * program classes in dex. The given startup profiles are also used to disallow optimizations
+     * program classes in DEX. The given startup profiles are also used to disallow optimizations
      * across the startup and post-startup boundary.
      *
      * <p>NOTE: Startup profiles are ignored when compiling to class files or the min-API level does
-     * not support native multidex (API<=20).
+     * not support native multi-DEX (API<=20).
      */
     @Override
     public Builder addStartupProfileProviders(StartupProfileProvider... startupProfileProviders) {
@@ -520,11 +521,12 @@
 
     /**
      * Add a collection of startup profile providers that should be used for distributing the
-     * program classes in dex. The given startup profiles are also used to disallow optimizations
-     * across the startup and post-startup boundary.
+     * program classes in DEX, unless turned off using {@link #setEnableStartupLayoutOptimization}.
+     * The given startup profiles are also used to disallow optimizations across the startup and
+     * post-startup boundary.
      *
      * <p>NOTE: Startup profiles are ignored when compiling to class files or the min-API level does
-     * not support native multidex (API<=20).
+     * not support native multi-DEX (API<=20).
      */
     @Override
     public Builder addStartupProfileProviders(
@@ -533,6 +535,18 @@
     }
 
     /**
+     * API for specifying whether R8 should use the provided startup profiles to layout the DEX.
+     * When this is set to {@code false}, the given startup profiles are then only used to disallow
+     * optimizations across the startup and post-startup boundary.
+     *
+     * <p>Defaults to true.
+     */
+    public Builder setEnableStartupLayoutOptimization(boolean enable) {
+      enableStartupLayoutOptimization = enable;
+      return this;
+    }
+
+    /**
      * Exprimental API for supporting android resource shrinking.
      *
      * <p>Add an android resource provider, providing the resource table, manifest and res table
@@ -717,6 +731,7 @@
               getMapIdProvider(),
               getSourceFileProvider(),
               enableMissingLibraryApiModeling,
+              enableStartupLayoutOptimization,
               getAndroidPlatformBuild(),
               getArtProfilesForRewriting(),
               getStartupProfileProviders(),
@@ -912,6 +927,7 @@
   private final FeatureSplitConfiguration featureSplitConfiguration;
   private final String synthesizedClassPrefix;
   private final boolean enableMissingLibraryApiModeling;
+  private final boolean enableStartupLayoutOptimization;
   private final AndroidResourceProvider androidResourceProvider;
   private final AndroidResourceConsumer androidResourceConsumer;
   private final ResourceShrinkerConfiguration resourceShrinkerConfiguration;
@@ -1004,6 +1020,7 @@
       MapIdProvider mapIdProvider,
       SourceFileProvider sourceFileProvider,
       boolean enableMissingLibraryApiModeling,
+      boolean enableStartupLayoutOptimization,
       boolean isAndroidPlatformBuild,
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
@@ -1055,6 +1072,7 @@
     this.featureSplitConfiguration = featureSplitConfiguration;
     this.synthesizedClassPrefix = synthesizedClassPrefix;
     this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
+    this.enableStartupLayoutOptimization = enableStartupLayoutOptimization;
     this.androidResourceProvider = androidResourceProvider;
     this.androidResourceConsumer = androidResourceConsumer;
     this.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
@@ -1081,6 +1099,7 @@
     featureSplitConfiguration = null;
     synthesizedClassPrefix = null;
     enableMissingLibraryApiModeling = false;
+    enableStartupLayoutOptimization = true;
     androidResourceProvider = null;
     androidResourceConsumer = null;
     resourceShrinkerConfiguration = null;
@@ -1199,7 +1218,7 @@
       internal.apiModelingOptions().disableOutliningAndStubbing();
     }
 
-    // Default is to remove all javac generated assertion code when generating dex.
+    // Default is to remove all javac generated assertion code when generating DEX.
     assert internal.assertionsConfiguration == null;
     AssertionsConfiguration.Builder builder = AssertionsConfiguration.builder(getReporter());
     internal.assertionsConfiguration =
@@ -1244,7 +1263,10 @@
 
     internal.getArtProfileOptions().setArtProfilesForRewriting(getArtProfilesForRewriting());
     if (!getStartupProfileProviders().isEmpty()) {
-      internal.getStartupOptions().setStartupProfileProviders(getStartupProfileProviders());
+      internal
+          .getStartupOptions()
+          .setStartupProfileProviders(getStartupProfileProviders())
+          .setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
     }
 
     internal.programClassConflictResolver =
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 105ea1c..37a55ce 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -241,10 +241,15 @@
       // Retrieve the startup order for writing the app. In R8, the startup order is created
       // up-front to guide optimizations through-out the compilation. In D8, the startup
       // order is only used for writing the app, so we create it here for the first time.
-      StartupProfile startupProfile =
-          appView.appInfo().hasClassHierarchy()
-              ? appView.getStartupProfile()
-              : StartupProfile.createInitialStartupProfileForD8(appView);
+      StartupProfile startupProfile;
+      if (options.getStartupOptions().isStartupLayoutOptimizationEnabled()) {
+        startupProfile =
+            appView.appInfo().hasClassHierarchy()
+                ? appView.getStartupProfile()
+                : StartupProfile.createInitialStartupProfileForD8(appView);
+      } else {
+        startupProfile = StartupProfile.empty();
+      }
       distributor =
           new VirtualFile.FillFilesDistributor(
               this, classes, options, executorService, startupProfile);
diff --git a/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java b/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java
index 158b912..1e5e242 100644
--- a/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java
+++ b/src/main/java/com/android/tools/r8/dex/MixedSectionLayoutStrategy.java
@@ -28,7 +28,7 @@
     } else {
       assert virtualFile.getId() == 0;
       startupProfileForWriting =
-          appView.options().getStartupOptions().isStartupLayoutOptimizationsEnabled()
+          appView.options().getStartupOptions().isStartupMixedSectionLayoutOptimizationsEnabled()
               ? virtualFile.getStartupProfile().toStartupProfileForWriting(appView)
               : StartupProfile.empty();
     }
diff --git a/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java b/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java
index 632fd74..e6406bc 100644
--- a/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java
+++ b/src/main/java/com/android/tools/r8/profile/startup/StartupOptions.java
@@ -42,11 +42,14 @@
   private boolean enableStartupCompletenessCheckForTesting =
       parseSystemPropertyOrDefault("com.android.tools.r8.startup.completenesscheck", false);
 
+  /** When enabled, the startup profile is used to layout the DEX. */
+  private boolean enableStartupLayoutOptimization = true;
+
   /**
-   * When enabled, the layout of the primary dex file will be generated using the startup list,
-   * using {@link com.android.tools.r8.dex.StartupMixedSectionLayoutStrategy}.
+   * When enabled, the mixed section layout of the primary dex file will be generated using the
+   * startup list, using {@link com.android.tools.r8.dex.StartupMixedSectionLayoutStrategy}.
    */
-  private boolean enableStartupLayoutOptimizations =
+  private boolean enableStartupMixedSectionLayoutOptimizations =
       parseSystemPropertyOrDefault("com.android.tools.r8.startup.layout", true);
 
   private String multiStartupDexDistributionStrategyName =
@@ -84,14 +87,18 @@
     return this;
   }
 
-  public boolean isStartupLayoutOptimizationsEnabled() {
-    return enableStartupLayoutOptimizations;
+  public boolean isStartupMixedSectionLayoutOptimizationsEnabled() {
+    return enableStartupMixedSectionLayoutOptimizations;
   }
 
   public boolean isStartupCompletenessCheckForTestingEnabled() {
     return enableStartupCompletenessCheckForTesting;
   }
 
+  public boolean isStartupLayoutOptimizationEnabled() {
+    return enableStartupLayoutOptimization;
+  }
+
   public StartupOptions setEnableStartupCompletenessCheckForTesting() {
     return setEnableStartupCompletenessCheckForTesting(true);
   }
@@ -102,6 +109,12 @@
     return this;
   }
 
+  public StartupOptions setEnableStartupLayoutOptimization(
+      boolean enableStartupLayoutOptimization) {
+    this.enableStartupLayoutOptimization = enableStartupLayoutOptimization;
+    return this;
+  }
+
   public String getMultiStartupDexDistributionStrategyName() {
     return multiStartupDexDistributionStrategyName;
   }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 2183979..abafdc8 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -76,6 +76,7 @@
   private AllowedDiagnosticMessages allowedDiagnosticMessages = AllowedDiagnosticMessages.NONE;
   private boolean allowUnusedProguardConfigurationRules = false;
   private boolean enableMissingLibraryApiModeling = true;
+  private boolean enableStartupLayoutOptimization = true;
   private CollectingGraphConsumer graphConsumer = null;
   private final List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
   private final List<String> keepRules = new ArrayList<>();
@@ -152,6 +153,7 @@
         builder, rules -> box.syntheticProguardRules = rules);
     libraryDesugaringTestConfiguration.configure(builder);
     builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
+    builder.setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
     StringBuilder pgConfOutput = wrapProguardConfigConsumer(builder);
     ToolHelper.runAndBenchmarkR8WithoutResult(builder, optionsConsumer, benchmarkResults);
     R8TestCompileResult compileResult =
@@ -880,6 +882,11 @@
     return self();
   }
 
+  public T enableStartupLayoutOptimization(boolean enableStartupLayoutOptimization) {
+    this.enableStartupLayoutOptimization = enableStartupLayoutOptimization;
+    return self();
+  }
+
   public T setFakeCompilerVersion(SemanticVersion version) {
     getBuilder().setFakeCompilerVersion(version);
     return self();
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 25001ac..8237f9b 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.List;
@@ -136,8 +137,12 @@
   @SafeVarargs
   @Override
   public final <E extends Throwable> R8TestCompileResult inspectMultiDex(
-      ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
-    return inspectMultiDex(writeProguardMap(), consumers);
+      ThrowingConsumer<CodeInspector, E>... consumers) throws E {
+    try {
+      return inspectMultiDex(writeProguardMap(), consumers);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
   }
 
   public final <E extends Throwable> R8TestCompileResult inspectGraph(
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index dfdfa9d..4c2d6a8 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -39,6 +39,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -456,21 +457,25 @@
 
   @SuppressWarnings("unchecked")
   public <E extends Throwable> CR inspectMultiDex(ThrowingConsumer<CodeInspector, E>... consumers)
-      throws IOException, E {
+      throws E {
     return inspectMultiDex(null, consumers);
   }
 
   @SafeVarargs
   public final <E extends Throwable> CR inspectMultiDex(
-      Path mappingFile, ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
-    Path out = state.getNewTempFolder();
-    getApp().writeToDirectory(out, OutputMode.DexIndexed);
-    consumers[0].accept(new CodeInspector(out.resolve("classes.dex"), mappingFile));
-    for (int i = 1; i < consumers.length; i++) {
-      Path dex = out.resolve("classes" + (i + 1) + ".dex");
-      CodeInspector inspector =
-          dex.toFile().exists() ? new CodeInspector(dex, mappingFile) : CodeInspector.empty();
-      consumers[i].accept(inspector);
+      Path mappingFile, ThrowingConsumer<CodeInspector, E>... consumers) throws E {
+    try {
+      Path out = state.getNewTempFolder();
+      getApp().writeToDirectory(out, OutputMode.DexIndexed);
+      consumers[0].accept(new CodeInspector(out.resolve("classes.dex"), mappingFile));
+      for (int i = 1; i < consumers.length; i++) {
+        Path dex = out.resolve("classes" + (i + 1) + ".dex");
+        CodeInspector inspector =
+            dex.toFile().exists() ? new CodeInspector(dex, mappingFile) : CodeInspector.empty();
+        consumers[i].accept(inspector);
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
     }
     return self();
   }
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 892c68f..0515b8d 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -90,6 +90,10 @@
     return isDexRuntime() && getDexRuntimeVersion().isNewerThanOrEqual(DexVm.Version.V8_1_0);
   }
 
+  public boolean canUseNativeMultidex() {
+    return isDexRuntime() && getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L);
+  }
+
   public boolean canUseNestBasedAccesses() {
     assert isCfRuntime() || isDexRuntime();
     return isCfRuntime() && getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11);
diff --git a/src/test/java/com/android/tools/r8/startup/optimization/NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest.java b/src/test/java/com/android/tools/r8/startup/optimization/NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest.java
new file mode 100644
index 0000000..4ca9640
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/startup/optimization/NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest.java
@@ -0,0 +1,169 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.startup.optimization;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsentIf;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.errors.StartupClassesNonStartupFractionDiagnostic;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.startup.profile.ExternalStartupClass;
+import com.android.tools.r8.startup.profile.ExternalStartupItem;
+import com.android.tools.r8.startup.profile.ExternalStartupMethod;
+import com.android.tools.r8.startup.utils.StartupTestingUtils;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests the option to provide a startup profile for guiding optimizations without impacting the DEX
+ * layout.
+ */
+@RunWith(Parameterized.class)
+public class NoStartupBoundaryOptimizationsWithoutStartupLayoutOptimizationTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableStartupProfile;
+
+  @Parameter(1)
+  public boolean enableStartupLayoutOptimization;
+
+  @Parameter(2)
+  public TestParameters parameters;
+
+  @Parameters(name = "{2}, profile: {0}, layout: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        getTestParameters().withDexRuntimesAndAllApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    // Startup profile requires native multi dex.
+    assumeTrue(!enableStartupProfile || parameters.canUseNativeMultidex());
+    // Startup layout optimization is meaningless without startup profile.
+    assumeTrue(enableStartupProfile || !enableStartupLayoutOptimization);
+    List<ExternalStartupItem> startupProfile =
+        ImmutableList.of(
+            ExternalStartupClass.builder()
+                .setClassReference(Reference.classFromClass(Main.class))
+                .build(),
+            ExternalStartupMethod.builder()
+                .setMethodReference(MethodReferenceUtils.mainMethod(Main.class))
+                .build());
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .allowDiagnosticMessages()
+        .applyIf(
+            enableStartupProfile,
+            testBuilder ->
+                testBuilder
+                    .apply(StartupTestingUtils.addStartupProfile(startupProfile))
+                    .enableStartupLayoutOptimization(enableStartupLayoutOptimization))
+        .setMinApi(parameters)
+        .compileWithExpectedDiagnostics(this::inspectDiagnostics)
+        .applyIf(
+            !enableStartupProfile || !enableStartupLayoutOptimization,
+            compileResult ->
+                compileResult.inspectMultiDex(
+                    primaryDexInspector ->
+                        inspectPrimaryDex(primaryDexInspector, compileResult.inspector())),
+            compileResult ->
+                compileResult.inspectMultiDex(
+                    primaryDexInspector ->
+                        inspectPrimaryDex(primaryDexInspector, compileResult.inspector()),
+                    this::inspectSecondaryDex))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Click!");
+  }
+
+  private void inspectDiagnostics(TestDiagnosticMessages diagnostics) {
+    if (enableStartupProfile && enableStartupLayoutOptimization) {
+      diagnostics.assertInfosMatch(
+          diagnosticType(StartupClassesNonStartupFractionDiagnostic.class));
+    } else {
+      diagnostics.assertNoMessages();
+    }
+  }
+
+  private void inspectPrimaryDex(CodeInspector primaryDexInspector, CodeInspector appInspector) {
+    if (!enableStartupProfile) {
+      // Everything should be inlined into main().
+      assertEquals(1, primaryDexInspector.allClasses().size());
+      assertEquals(1, primaryDexInspector.clazz(Main.class).allMethods().size());
+      return;
+    }
+
+    // Main.onClick() should be inlined into Main.main(), but OnClickHandler.handle() should
+    // remain.
+    ClassSubject onClickHandlerClassSubject = appInspector.clazz(OnClickHandler.class);
+    assertThat(onClickHandlerClassSubject, isPresent());
+
+    MethodSubject handleMethodSubject =
+        onClickHandlerClassSubject.uniqueMethodWithOriginalName("handle");
+    assertThat(handleMethodSubject, isPresent());
+
+    ClassSubject mainClassSubject = primaryDexInspector.clazz(Main.class);
+    assertThat(mainClassSubject, isPresent());
+    assertEquals(1, mainClassSubject.allMethods().size());
+    assertThat(mainClassSubject.mainMethod(), invokesMethod(handleMethodSubject));
+
+    // OnClickHandler should not be in the primary DEX when startup layout is enabled.
+    assertThat(
+        primaryDexInspector.clazz(OnClickHandler.class),
+        isAbsentIf(enableStartupLayoutOptimization));
+  }
+
+  private void inspectSecondaryDex(CodeInspector secondaryDexInspector) {
+    assertTrue(enableStartupProfile);
+    assertTrue(enableStartupLayoutOptimization);
+    assertEquals(1, secondaryDexInspector.allClasses().size());
+
+    ClassSubject onClickHandlerClassSubject = secondaryDexInspector.clazz(OnClickHandler.class);
+    assertThat(onClickHandlerClassSubject, isPresent());
+
+    MethodSubject handleMethodSubject =
+        onClickHandlerClassSubject.uniqueMethodWithOriginalName("handle");
+    assertThat(handleMethodSubject, isPresent());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      onClick();
+    }
+
+    static void onClick() {
+      OnClickHandler.handle();
+    }
+  }
+
+  static class OnClickHandler {
+
+    static void handle() {
+      System.out.println("Click!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
index 8805931..f51ab4b 100644
--- a/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/startup/utils/StartupTestingUtils.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8TestBuilder;
 import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -192,6 +193,11 @@
     }
   }
 
+  public static <B extends R8TestBuilder<B>> ThrowableConsumer<B> addStartupProfile(
+      Collection<ExternalStartupItem> startupItems) {
+    return testBuilder -> addStartupProfile(testBuilder, startupItems);
+  }
+
   private static byte[] getTransformedAndroidUtilLog() throws IOException {
     return transformer(Log.class).setClassDescriptor("Landroid/util/Log;").transform();
   }