Towards different distribution strategies for multi startup dex files

Bug: b/298617875
Change-Id: I54b929cbe1b9d08037bd06194b03009a48a3c6b0
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index fc0c691..65c0f2e 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -29,6 +29,7 @@
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.profile.startup.distribution.MultiStartupDexDistributor;
 import com.android.tools.r8.profile.startup.profile.StartupProfile;
 import com.android.tools.r8.shaking.MainDexInfo;
 import com.android.tools.r8.synthesis.SyntheticNaming;
@@ -249,7 +250,7 @@
             timing);
   }
 
-  void addClass(DexProgramClass clazz) {
+  public void addClass(DexProgramClass clazz) {
     transaction.addClassAndDependencies(clazz);
   }
 
@@ -258,7 +259,7 @@
         || (transaction.getNumberOfFields() > maxEntries);
   }
 
-  boolean isFull() {
+  public boolean isFull() {
     return isFull(MAX_ENTRIES);
   }
 
@@ -1172,15 +1173,15 @@
   /**
    * Helper class to cycle through the set of virtual files.
    *
-   * Iteration starts at the first file and iterates through all files.
+   * <p>Iteration starts at the first file and iterates through all files.
    *
-   * When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at the
-   * current file.
+   * <p>When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at
+   * the current file.
    *
-   * If the fill strategy indicate that the main dex file should be minimal, then the main dex file
-   * will not be part of the iteration.
+   * <p>If the fill strategy indicate that the main dex file should be minimal, then the main dex
+   * file will not be part of the iteration.
    */
-  static class VirtualFileCycler {
+  public static class VirtualFileCycler {
 
     private final List<VirtualFile> files;
     private final List<VirtualFile> filesForDistribution;
@@ -1262,7 +1263,7 @@
       activeFiles = Iterators.limit(allFilesCyclic, filesForDistribution.size());
     }
 
-    VirtualFile addFile() {
+    public VirtualFile addFile() {
       VirtualFile newFile = internalAddFile();
       reset();
       return newFile;
@@ -1466,19 +1467,9 @@
       } else {
         virtualFile.abortTransaction();
 
-        // If the above failed, then add the startup classes one by one.
-        for (DexProgramClass startupClass : classPartioning.getStartupClasses()) {
-          virtualFile.addClass(startupClass);
-          if (hasSpaceForTransaction(virtualFile, options)) {
-            virtualFile.commitTransaction();
-          } else {
-            virtualFile.abortTransaction();
-            virtualFile = cycler.addFile();
-            virtualFile.addClass(startupClass);
-            assert hasSpaceForTransaction(virtualFile, options);
-            virtualFile.commitTransaction();
-          }
-        }
+        // If the above failed, then apply the selected multi startup dex distribution strategy.
+        MultiStartupDexDistributor distributor = MultiStartupDexDistributor.get(options);
+        distributor.distribute(classPartioning.getStartupClasses(), cycler, virtualFile);
 
         options.reporter.warning(
             createStartupClassesOverflowDiagnostic(cycler.filesForDistribution.size()));
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 4180266..632fd74 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
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.profile.startup;
 
-import static com.android.tools.r8.utils.SystemPropertyUtils.parseSystemPropertyForDevelopmentOrDefault;
+import static com.android.tools.r8.utils.SystemPropertyUtils.getSystemPropertyOrDefault;
+import static com.android.tools.r8.utils.SystemPropertyUtils.parseSystemPropertyOrDefault;
 
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.utils.SystemPropertyUtils;
@@ -20,8 +21,7 @@
    * (non-startup) classes will be placed in classes2.dex, ..., classesN.dex.
    */
   private boolean enableMinimalStartupDex =
-      parseSystemPropertyForDevelopmentOrDefault(
-          "com.android.tools.r8.startup.minimalstartupdex", true);
+      parseSystemPropertyOrDefault("com.android.tools.r8.startup.minimalstartupdex", true);
 
   /**
    * When enabled, optimizations crossing the startup/non-startup boundary will be allowed.
@@ -30,8 +30,7 @@
    * result of optimizations such as inlining and class merging.
    */
   private boolean enableStartupBoundaryOptimizations =
-      parseSystemPropertyForDevelopmentOrDefault(
-          "com.android.tools.r8.startup.boundaryoptimizations", false);
+      parseSystemPropertyOrDefault("com.android.tools.r8.startup.boundaryoptimizations", false);
 
   /**
    * When enabled, each method that is not classified as a startup method at the end of compilation
@@ -41,15 +40,17 @@
    * rewrites the startup list in presence of optimizations).
    */
   private boolean enableStartupCompletenessCheckForTesting =
-      parseSystemPropertyForDevelopmentOrDefault(
-          "com.android.tools.r8.startup.completenesscheck", false);
+      parseSystemPropertyOrDefault("com.android.tools.r8.startup.completenesscheck", false);
 
   /**
    * When enabled, the layout of the primary dex file will be generated using the startup list,
    * using {@link com.android.tools.r8.dex.StartupMixedSectionLayoutStrategy}.
    */
   private boolean enableStartupLayoutOptimizations =
-      parseSystemPropertyForDevelopmentOrDefault("com.android.tools.r8.startup.layout", true);
+      parseSystemPropertyOrDefault("com.android.tools.r8.startup.layout", true);
+
+  private String multiStartupDexDistributionStrategyName =
+      getSystemPropertyOrDefault("com.android.tools.r8.startup.multistartupdexdistribution", null);
 
   private Collection<StartupProfileProvider> startupProfileProviders;
 
@@ -101,6 +102,10 @@
     return this;
   }
 
+  public String getMultiStartupDexDistributionStrategyName() {
+    return multiStartupDexDistributionStrategyName;
+  }
+
   public boolean hasStartupProfileProviders() {
     return startupProfileProviders != null && !startupProfileProviders.isEmpty();
   }
diff --git a/src/main/java/com/android/tools/r8/profile/startup/distribution/MultiStartupDexDistributor.java b/src/main/java/com/android/tools/r8/profile/startup/distribution/MultiStartupDexDistributor.java
new file mode 100644
index 0000000..471ea08
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/profile/startup/distribution/MultiStartupDexDistributor.java
@@ -0,0 +1,69 @@
+// 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.profile.startup.distribution;
+
+import com.android.tools.r8.dex.VirtualFile;
+import com.android.tools.r8.dex.VirtualFile.VirtualFileCycler;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.List;
+
+public abstract class MultiStartupDexDistributor {
+
+  public abstract void distribute(
+      List<DexProgramClass> classes, VirtualFileCycler cycler, VirtualFile virtualFile);
+
+  boolean hasSpaceForTransaction(VirtualFile virtualFile) {
+    return !virtualFile.isFull();
+  }
+
+  public static MultiStartupDexDistributor getDefault() {
+    return new ClassByNameMultiStartupDexDistributor();
+  }
+
+  public static MultiStartupDexDistributor get(InternalOptions options) {
+    String strategyName = options.getStartupOptions().getMultiStartupDexDistributionStrategyName();
+    if (strategyName == null) {
+      return getDefault();
+    }
+    switch (strategyName) {
+      case "classByName":
+        return getDefault();
+      case "classByNumberOfStartupMethods":
+        throw new Unimplemented();
+      case "classByNumberOfStartupMethodsMinusNumberOfNonStartupMethods":
+        throw new Unimplemented();
+      case "packageByName":
+        throw new Unimplemented();
+      case "packageByNumberOfStartupMethods":
+        throw new Unimplemented();
+      default:
+        throw new IllegalArgumentException(
+            "Unexpected multi startup dex distribution strategy: " + strategyName);
+    }
+  }
+
+  private static class ClassByNameMultiStartupDexDistributor extends MultiStartupDexDistributor {
+
+    @Override
+    public void distribute(
+        List<DexProgramClass> classes, VirtualFileCycler cycler, VirtualFile virtualFile) {
+      // Add the (already sorted) startup classes one by one.
+      for (DexProgramClass startupClass : classes) {
+        virtualFile.addClass(startupClass);
+        if (hasSpaceForTransaction(virtualFile)) {
+          virtualFile.commitTransaction();
+        } else {
+          virtualFile.abortTransaction();
+          virtualFile = cycler.addFile();
+          virtualFile.addClass(startupClass);
+          assert hasSpaceForTransaction(virtualFile);
+          virtualFile.commitTransaction();
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java b/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java
index a984560..57cb56d 100644
--- a/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java
@@ -22,6 +22,10 @@
     }
   }
 
+  public static String getSystemPropertyOrDefault(String propertyName, String defaultValue) {
+    return System.getProperty(propertyName, defaultValue);
+  }
+
   public static String getSystemPropertyForDevelopment(String propertyName) {
     return Version.isDevelopmentVersion() ? System.getProperty(propertyName) : null;
   }