Minor cleanup of PackageSplitPopulator prior to emitting startup classes

Change-Id: Id50bc906bd010c9d37afbf7f1c47b7443a3b4018
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 827f43bd..d58916c 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -39,20 +39,20 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Function;
@@ -62,12 +62,6 @@
 
   public static final int MAX_ENTRIES = Constants.U16BIT_MAX + 1;
 
-  /**
-   * When distributing classes across files we aim to leave some space. The amount of space left is
-   * driven by this constant.
-   */
-  private static final int MAX_PREFILL_ENTRIES = MAX_ENTRIES - 5000;
-
   private final int id;
   private final VirtualFileIndexedItemCollection indexedItems;
   private final IndexedItemTransaction transaction;
@@ -187,22 +181,6 @@
     return originalNames;
   }
 
-  private static String extractPrefixToken(int prefixLength, String className, boolean addStar) {
-    int index = 0;
-    int lastIndex = 0;
-    int segmentCount = 0;
-    while (lastIndex != -1 && segmentCount++ < prefixLength) {
-      index = lastIndex;
-      lastIndex = className.indexOf('.', index + 1);
-    }
-    String prefix = className.substring(0, index);
-    if (addStar && segmentCount >= prefixLength) {
-      // Full match, add a * to also match sub-packages.
-      prefix += ".*";
-    }
-    return prefix;
-  }
-
   private ObjectToOffsetMapping objectMapping = null;
 
   public ObjectToOffsetMapping getObjectMapping() {
@@ -427,38 +405,6 @@
       mainDexFile.throwIfFull(true, options.reporter);
     }
 
-    TreeSet<DexProgramClass> sortClassesByPackage(Set<DexProgramClass> classes,
-        Map<DexProgramClass, String> originalNames) {
-      TreeSet<DexProgramClass> sortedClasses = new TreeSet<>(
-          (DexProgramClass a, DexProgramClass b) -> {
-            String originalA = originalNames.get(a);
-            String originalB = originalNames.get(b);
-            int indexA = originalA.lastIndexOf('.');
-            int indexB = originalB.lastIndexOf('.');
-            if (indexA == -1 && indexB == -1) {
-              // Empty package, compare the class names.
-              return originalA.compareTo(originalB);
-            }
-            if (indexA == -1) {
-              // Empty package name comes first.
-              return -1;
-            }
-            if (indexB == -1) {
-              // Empty package name comes first.
-              return 1;
-            }
-            String prefixA = originalA.substring(0, indexA);
-            String prefixB = originalB.substring(0, indexB);
-            int result = prefixA.compareTo(prefixB);
-            if (result != 0) {
-              return result;
-            }
-            return originalA.compareTo(originalB);
-          });
-      sortedClasses.addAll(classes);
-      return sortedClasses;
-    }
-
     protected Map<FeatureSplit, Set<DexProgramClass>> removeFeatureSplitClassesGetMapping() {
       assert appView.appInfo().hasClassHierarchy() == appView.enableWholeProgramOptimizations();
       if (!appView.appInfo().hasClassHierarchy()) {
@@ -482,8 +428,8 @@
       return featureSplitClasses;
     }
 
-    protected void addFeatureSplitFiles(Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses)
-        throws IOException {
+    protected void addFeatureSplitFiles(
+        Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses) {
       if (featureSplitClasses.isEmpty()) {
         return;
       }
@@ -499,19 +445,15 @@
                 featureSplitSetEntry.getKey());
         virtualFiles.add(featureFile);
         addMarkers(featureFile);
-        Set<DexProgramClass> featureClasses =
-            sortClassesByPackage(featureSplitSetEntry.getValue(), originalNames);
         filesForDistribution = virtualFiles.subList(virtualFiles.size() - 1, virtualFiles.size());
-
         new PackageSplitPopulator(
                 filesForDistribution,
                 appView,
-                featureClasses,
+                featureSplitSetEntry.getValue(),
                 originalNames,
                 0,
-                writer.namingLens,
-                options)
-            .call();
+                writer.namingLens)
+            .run();
       }
     }
   }
@@ -564,18 +506,14 @@
                 executorService)
             .distribute();
       } else {
-        // Sort the remaining classes based on the original names.
-        // This with make classes from the same package be adjacent.
-        classes = sortClassesByPackage(classes, originalNames);
         new PackageSplitPopulator(
                 filesForDistribution,
                 appView,
                 classes,
                 originalNames,
                 fileIndexOffset,
-                writer.namingLens,
-                options)
-            .call();
+                writer.namingLens)
+            .run();
       }
       addFeatureSplitFiles(featureSplitClasses);
 
@@ -974,7 +912,63 @@
    * <p>The populator cycles through the files until all classes have been successfully placed and
    * adds new files to the passed in map if it can't fit in the existing files.
    */
-  private static class PackageSplitPopulator implements Callable<Map<String, Integer>> {
+  private static class PackageSplitPopulator {
+
+    static class PackageSplitClassPartioning {
+
+      // The set of classes that must be written, sorted by original names so that classes in the
+      // same package are adjacent.
+      private final List<DexProgramClass> nonStartupClasses;
+
+      private PackageSplitClassPartioning(List<DexProgramClass> nonStartupClasses) {
+        this.nonStartupClasses = nonStartupClasses;
+      }
+
+      public static PackageSplitClassPartioning create(
+          Collection<DexProgramClass> classes, Map<DexProgramClass, String> originalNames) {
+        return create(classes, getClassesByPackageComparator(originalNames));
+      }
+
+      private static PackageSplitClassPartioning create(
+          Collection<DexProgramClass> classes, Comparator<DexProgramClass> comparator) {
+        List<DexProgramClass> nonStartupClasses = new ArrayList<>(classes);
+        nonStartupClasses.sort(comparator);
+        return new PackageSplitClassPartioning(nonStartupClasses);
+      }
+
+      private static Comparator<DexProgramClass> getClassesByPackageComparator(
+          Map<DexProgramClass, String> originalNames) {
+        return (a, b) -> {
+          String originalA = originalNames.get(a);
+          String originalB = originalNames.get(b);
+          int indexA = originalA.lastIndexOf('.');
+          int indexB = originalB.lastIndexOf('.');
+          if (indexA == -1 && indexB == -1) {
+            // Empty package, compare the class names.
+            return originalA.compareTo(originalB);
+          }
+          if (indexA == -1) {
+            // Empty package name comes first.
+            return -1;
+          }
+          if (indexB == -1) {
+            // Empty package name comes first.
+            return 1;
+          }
+          String prefixA = originalA.substring(0, indexA);
+          String prefixB = originalB.substring(0, indexB);
+          int result = prefixA.compareTo(prefixB);
+          if (result != 0) {
+            return result;
+          }
+          return originalA.compareTo(originalB);
+        };
+      }
+
+      public List<DexProgramClass> getNonStartupClasses() {
+        return nonStartupClasses;
+      }
+    }
 
     /**
      * Android suggests com.company.product for package names, so the components will be at level 4
@@ -988,7 +982,7 @@
      */
     private static final int MIN_FILL_FACTOR = 5;
 
-    private final List<DexProgramClass> classes;
+    private final PackageSplitClassPartioning classPartioning;
     private final Map<DexProgramClass, String> originalNames;
     private final DexItemFactory dexItemFactory;
     private final InternalOptions options;
@@ -997,15 +991,14 @@
     PackageSplitPopulator(
         List<VirtualFile> files,
         AppView<?> appView,
-        Set<DexProgramClass> classes,
+        Collection<DexProgramClass> classes,
         Map<DexProgramClass, String> originalNames,
         int fileIndexOffset,
-        NamingLens namingLens,
-        InternalOptions options) {
-      this.classes = new ArrayList<>(classes);
+        NamingLens namingLens) {
+      this.classPartioning = PackageSplitClassPartioning.create(classes, originalNames);
       this.originalNames = originalNames;
       this.dexItemFactory = appView.dexItemFactory();
-      this.options = options;
+      this.options = appView.options();
       this.cycler = new VirtualFileCycler(files, appView, namingLens, fileIndexOffset);
     }
 
@@ -1022,17 +1015,21 @@
     }
 
     private String getOriginalName(DexProgramClass clazz) {
-      return originalNames != null ? originalNames.get(clazz) : clazz.toString();
+      return originalNames.get(clazz);
     }
 
-    @Override
-    public Map<String, Integer> call() throws IOException {
+    public void run() {
+      List<DexProgramClass> nonPackageClasses = addNonStartupClasses();
+      addNonPackageClasses(cycler, nonPackageClasses);
+    }
+
+    private List<DexProgramClass> addNonStartupClasses() {
       int prefixLength = MINIMUM_PREFIX_LENGTH;
       int transactionStartIndex = 0;
-      int fileStartIndex = 0;
       String currentPrefix = null;
-      Map<String, Integer> newPackageAssignments = new LinkedHashMap<>();
+      Object2IntMap<String> packageAssignments = new Object2IntOpenHashMap<>();
       VirtualFile current = cycler.next();
+      List<DexProgramClass> classes = classPartioning.getNonStartupClasses();
       List<DexProgramClass> nonPackageClasses = new ArrayList<>();
       for (int classIndex = 0; classIndex < classes.size(); classIndex++) {
         DexProgramClass clazz = classes.get(classIndex);
@@ -1040,10 +1037,9 @@
         if (!coveredByPrefix(originalName, currentPrefix)) {
           if (currentPrefix != null) {
             current.commitTransaction();
+            assert verifyPackageToVirtualFileAssignment(packageAssignments, currentPrefix, current);
             // Reset the cycler to again iterate over all files, starting with the current one.
             cycler.restart();
-            assert !newPackageAssignments.containsKey(currentPrefix);
-            newPackageAssignments.put(currentPrefix, current.id);
             // Try to reduce the prefix length if possible. Only do this on a successful commit.
             prefixLength = MINIMUM_PREFIX_LENGTH - 1;
           }
@@ -1067,58 +1063,93 @@
           }
           transactionStartIndex = classIndex;
         }
-        if (currentPrefix != null) {
-          assert clazz.superType != null || clazz.type == dexItemFactory.objectType;
-          current.addClass(clazz);
-        } else {
+
+        if (currentPrefix == null) {
           assert clazz.superType != null;
           // We don't have a package, add this to a list of classes that we will add last.
-          assert current.transaction.classes.isEmpty();
+          assert current.transaction.isEmpty();
           nonPackageClasses.add(clazz);
           continue;
         }
-        if (isFullEnough(current, options)) {
-          current.abortTransaction();
-          // We allow for a final rollback that has at most 20% of classes in it.
-          // This is a somewhat random number that was empirically chosen.
-          if (classIndex - transactionStartIndex > (classIndex - fileStartIndex) / MIN_FILL_FACTOR
-              && prefixLength < MAXIMUM_PREFIX_LENGTH) {
-            prefixLength++;
-          } else {
-            // Reset the state to after the last commit and cycle through files.
-            // The idea is that we do not increase the number of files, so it has to fit
-            // somewhere.
-            fileStartIndex = transactionStartIndex;
-            if (!cycler.hasNext()) {
-              // Special case where we simply will never be able to fit the current package into
-              // one dex file. This is currently the case for Strings in jumbo tests, see:
-              // b/33227518
-              if (current.transaction.getNumberOfClasses() == 0) {
-                for (int j = transactionStartIndex; j <= classIndex; j++) {
-                  nonPackageClasses.add(classes.get(j));
-                }
-                transactionStartIndex = classIndex + 1;
-              }
-              // All files are filled up to the 20% mark.
-              cycler.addFile();
-            }
-            current = cycler.next();
-          }
-          currentPrefix = null;
+
+        assert clazz.superType != null || clazz.type == dexItemFactory.objectType;
+        current.addClass(clazz);
+
+        if (hasSpaceForTransaction(current, options)) {
+          continue;
+        }
+
+        int numberOfClassesInTransaction = classIndex - transactionStartIndex + 1;
+        int numberOfClassesInVirtualFileWithTransaction = current.getNumberOfClasses();
+
+        current.abortTransaction();
+
+        // We allow for a final rollback that has at most 20% of classes in it.
+        // This is a somewhat random number that was empirically chosen.
+        if (numberOfClassesInTransaction
+                > numberOfClassesInVirtualFileWithTransaction / MIN_FILL_FACTOR
+            && prefixLength < MAXIMUM_PREFIX_LENGTH) {
           // Go back to previous start index.
           classIndex = transactionStartIndex - 1;
-          assert current != null;
+          currentPrefix = null;
+          prefixLength++;
+          continue;
         }
+
+        // Reset the state to after the last commit and cycle through files.
+        // The idea is that we do not increase the number of files, so it has to fit somewhere.
+        if (!cycler.hasNext()) {
+          // Special case where we simply will never be able to fit the current package into
+          // one dex file. This is currently the case for Strings in jumbo tests, see b/33227518.
+          if (current.isEmpty()) {
+            for (int j = transactionStartIndex; j <= classIndex; j++) {
+              nonPackageClasses.add(classes.get(j));
+            }
+            transactionStartIndex = classIndex + 1;
+          }
+          // All files are filled up to the 20% mark.
+          cycler.addFile();
+        }
+
+        // Go back to previous start index.
+        classIndex = transactionStartIndex - 1;
+        current = cycler.next();
+        currentPrefix = null;
+        prefixLength = MINIMUM_PREFIX_LENGTH;
       }
+
       current.commitTransaction();
-      assert !newPackageAssignments.containsKey(currentPrefix);
-      if (currentPrefix != null) {
-        newPackageAssignments.put(currentPrefix, current.id);
+      assert currentPrefix == null
+          || verifyPackageToVirtualFileAssignment(packageAssignments, currentPrefix, current);
+
+      return nonPackageClasses;
+    }
+
+    private static String extractPrefixToken(int prefixLength, String className, boolean addStar) {
+      int index = 0;
+      int lastIndex = 0;
+      int segmentCount = 0;
+      while (lastIndex != -1 && segmentCount++ < prefixLength) {
+        index = lastIndex;
+        lastIndex = className.indexOf('.', index + 1);
       }
-      if (nonPackageClasses.size() > 0) {
-        addNonPackageClasses(cycler, nonPackageClasses);
+      String prefix = className.substring(0, index);
+      if (addStar && segmentCount >= prefixLength) {
+        // Full match, add a * to also match sub-packages.
+        prefix += ".*";
       }
-      return newPackageAssignments;
+      return prefix;
+    }
+
+    private boolean verifyPackageToVirtualFileAssignment(
+        Object2IntMap<String> packageAssignments, String packageName, VirtualFile virtualFile) {
+      assert !packageAssignments.containsKey(packageName);
+      packageAssignments.put(packageName, virtualFile.getId());
+      return true;
+    }
+
+    private boolean hasSpaceForTransaction(VirtualFile current, InternalOptions options) {
+      return !isFullEnough(current, options);
     }
 
     private boolean isFullEnough(VirtualFile current, InternalOptions options) {
@@ -1131,6 +1162,9 @@
 
     private void addNonPackageClasses(
         VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
+      if (nonPackageClasses.isEmpty()) {
+        return;
+      }
       cycler.restart();
       VirtualFile current;
       current = cycler.next();