Initial structure horizontal class merging


* `Merger` performs the overall task of horizontal class merging
* `Merger.run()` initially sorts the classes by field spec and then executes all policies on the groups of classes.
   Uses `DexFieldSignature` to sort classes by signature.
* `Policy` is the abstract class for all horizontal class merging policies
* `SingleClassPolicy` and `MultiClassPolicy` abstract classes extending from Policy
* `PolicyExecutor` as abstract class for policy execution
* `SimplePolicyExecutor` as a reference implementation for a policy executor

Bug: 163311975
Change-Id: Idb27b1a9d4c6303f985b57212dff31bbe8fff420
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 39753be..4d1a076 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -39,6 +39,7 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.analysis.ClassInitializerAssertionEnablingAnalysis;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
@@ -541,6 +542,13 @@
           }
           timing.end();
         }
+        if (options.enableHorizontalClassMerging) {
+          timing.begin("HorizontalClassMerger");
+          HorizontalClassMerger merger =
+              new HorizontalClassMerger(appViewWithLiveness, mainDexClasses);
+          merger.run();
+          timing.end();
+        }
 
         // Only required for class merging, clear instance to save memory.
         classMergingEnqueuerExtension = null;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
new file mode 100644
index 0000000..37222de
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+
+public class FieldMultiset {
+
+  private final Multiset<DexType> fields = HashMultiset.create();
+
+  public FieldMultiset(DexProgramClass clazz) {
+    for (DexEncodedField field : clazz.instanceFields()) {
+      fields.add(field.type());
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return fields.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object object) {
+    if (object instanceof FieldMultiset) {
+      FieldMultiset other = (FieldMultiset) object;
+      return fields.equals(other.fields);
+    } else {
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
new file mode 100644
index 0000000..56da905
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexClasses;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HorizontalClassMerger {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final MainDexClasses mainDexClasses;
+
+  private final PolicyExecutor policyExecutor;
+
+  public HorizontalClassMerger(
+      AppView<AppInfoWithLiveness> appView, MainDexClasses mainDexClasses) {
+    this.appView = appView;
+    this.mainDexClasses = mainDexClasses;
+
+    Policy[] policies = {
+      // TODO: add policies
+    };
+    this.policyExecutor = new SimplePolicyExecutor(Arrays.asList(policies));
+  }
+
+  public Collection<Collection<DexProgramClass>> run() {
+    Map<FieldMultiset, Collection<DexProgramClass>> classes = new HashMap<>();
+
+    // Group classes by same field signature using the hash map.
+    for (DexProgramClass clazz : appView.appInfo().app().classesWithDeterministicOrder()) {
+      classes.computeIfAbsent(new FieldMultiset(clazz), ignore -> new ArrayList<>()).add(clazz);
+    }
+
+    // Run the policies on all collected classes to produce a final grouping.
+    Collection<Collection<DexProgramClass>> groups = policyExecutor.run(classes.values());
+
+    return groups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
new file mode 100644
index 0000000..261e8d2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import java.util.Collection;
+
+public abstract class MultiClassPolicy extends Policy {
+
+  /**
+   * Apply the multi class policy to a group of program classes.
+   *
+   * @param group This is a group of program classes which can currently still be merged.
+   * @return The same collection of program classes split into new groups of candidates which can be
+   *     merged. If the policy detects no issues then `group` will be returned unchanged. If classes
+   *     cannot be merged with any other classes they are returned as singleton lists.
+   */
+  public abstract Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
new file mode 100644
index 0000000..d8dea98
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+/**
+ * The super class of all horizontal class merging policies. Most classes will either implement
+ * {@link SingleClassPolicy} or {@link MultiClassPolicy}.
+ */
+public abstract class Policy {}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
new file mode 100644
index 0000000..7ffaa1b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import java.util.Collection;
+
+public abstract class PolicyExecutor {
+  protected final Collection<Policy> policies;
+
+  public PolicyExecutor(Collection<Policy> policies) {
+    this.policies = policies;
+  }
+
+  /**
+   * Given an initial collection of class groups which can potentially be merged, run all of the
+   * policies registered to this policy executor on the class groups yielding a new collection of
+   * class groups.
+   */
+  public abstract Collection<Collection<DexProgramClass>> run(
+      Collection<Collection<DexProgramClass>> classes);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
new file mode 100644
index 0000000..3e6254c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.stream.Collectors;
+
+/**
+ * This is a simple policy executor that ensures regular sequential execution of policies. It should
+ * primarily be readable and correct. The SimplePolicyExecutor should be a reference implementation,
+ * against which more efficient policy executors can be compared.
+ */
+public class SimplePolicyExecutor extends PolicyExecutor {
+  public SimplePolicyExecutor(Collection<Policy> policies) {
+    super(policies);
+  }
+
+  // TODO(b/165506334): if performing mutable operation ensure that linked lists are used
+  private Collection<Collection<DexProgramClass>> applySingleClassPolicy(
+      SingleClassPolicy policy, Collection<Collection<DexProgramClass>> groups) {
+    Iterator<Collection<DexProgramClass>> i = groups.iterator();
+    while (i.hasNext()) {
+      Collection<DexProgramClass> group = i.next();
+      Iterator<DexProgramClass> j = group.iterator();
+      while (j.hasNext()) {
+        DexProgramClass clazz = j.next();
+        if (!policy.canMerge(clazz)) {
+          j.remove();
+        }
+      }
+      if (group.isEmpty()) {
+        i.remove();
+      }
+    }
+    return groups;
+  }
+
+  private Collection<Collection<DexProgramClass>> applyMultiClassPolicy(
+      MultiClassPolicy policy, Collection<Collection<DexProgramClass>> groups) {
+    // For each group apply the multi class policy and add all the new groups together.
+    return groups.stream()
+        .flatMap(group -> policy.apply(group).stream())
+        .collect(Collectors.toList());
+  }
+
+  @Override
+  public Collection<Collection<DexProgramClass>> run(
+      Collection<Collection<DexProgramClass>> groups) {
+    for (Policy policy : policies) {
+      if (policy instanceof SingleClassPolicy) {
+        groups = applySingleClassPolicy((SingleClassPolicy) policy, groups);
+      } else if (policy instanceof MultiClassPolicy) {
+        groups = applyMultiClassPolicy((MultiClassPolicy) policy, groups);
+      }
+    }
+
+    return groups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
new file mode 100644
index 0000000..b0757c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2020, 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+
+public abstract class SingleClassPolicy extends Policy {
+  /**
+   * Determine if {@param program} can be merged with any other classes.
+   *
+   * @return {@code false} if the class should not be merged, otherwise {@code true}.
+   */
+  public abstract boolean canMerge(DexProgramClass program);
+}