Limit the size of merged constructors

Bug: 163311975
Change-Id: I445aa357c8ef6710cff0075c3e39ae2ca6e7c3f5
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index e08f5da..dbefe85 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.cf.code.CfPosition;
 import com.android.tools.r8.cf.code.CfReturnVoid;
 import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.code.Base5Format;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
@@ -198,6 +199,11 @@
     return countNonStackOperations(threshold) <= threshold;
   }
 
+  @Override
+  public int estimatedDexCodeSizeUpperBoundInBytes() {
+    return estimatedSizeForInlining() * Base5Format.SIZE;
+  }
+
   private int countNonStackOperations(int threshold) {
     int result = 0;
     for (CfInstruction instruction : instructions) {
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index 1f2a522..535c08f 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -71,6 +71,8 @@
     return estimatedSizeForInlining() <= threshold;
   }
 
+  public abstract int estimatedDexCodeSizeUpperBoundInBytes();
+
   public CfCode asCfCode() {
     throw new Unreachable(getClass().getCanonicalName() + ".asCfCode()");
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index eaf8d7f..7a9c724 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -101,6 +101,11 @@
   }
 
   @Override
+  public int estimatedDexCodeSizeUpperBoundInBytes() {
+    return codeSizeInBytes();
+  }
+
+  @Override
   public DexCode asDexCode() {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index e0aace9..cdafe4e 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -211,6 +211,11 @@
   }
 
   @Override
+  public int estimatedDexCodeSizeUpperBoundInBytes() {
+    return asCfCode().estimatedDexCodeSizeUpperBoundInBytes();
+  }
+
+  @Override
   public IRCode buildIR(ProgramMethod method, AppView<?> appView, Origin origin) {
     return asCfCode().buildIR(method, appView, origin);
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index a6003c4..f99999a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -174,6 +174,7 @@
   }
 
   public static class Builder {
+    private final AppView<AppInfoWithLiveness> appView;
     private final DexProgramClass target;
     private final List<DexProgramClass> toMergeGroup = new ArrayList<>();
     private final Map<DexProto, ConstructorMerger.Builder> constructorMergerBuilders =
@@ -181,7 +182,8 @@
     private final Map<Wrapper<DexMethod>, VirtualMethodMerger.Builder> virtualMethodMergerBuilders =
         new LinkedHashMap<>();
 
-    public Builder(DexProgramClass target) {
+    public Builder(AppView<AppInfoWithLiveness> appView, DexProgramClass target) {
+      this.appView = appView;
       this.target = target;
       setupForMethodMerging(target);
     }
@@ -215,7 +217,7 @@
       assert method.getDefinition().isInstanceInitializer();
       constructorMergerBuilders
           .computeIfAbsent(
-              method.getDefinition().getProto(), ignore -> new ConstructorMerger.Builder())
+              method.getDefinition().getProto(), ignore -> new ConstructorMerger.Builder(appView))
           .add(method.getDefinition());
     }
 
@@ -250,7 +252,7 @@
       List<ConstructorMerger> constructorMergers =
           new ArrayList<>(constructorMergerBuilders.size());
       for (ConstructorMerger.Builder builder : constructorMergerBuilders.values()) {
-        constructorMergers.add(builder.build(appView, target, classIdField));
+        constructorMergers.addAll(builder.build(appView, target, classIdField));
       }
 
       // Try and merge the functions with the most arguments first, to avoid using synthetic
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
index 4740da3..f6127d9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -20,7 +20,9 @@
 import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
+import com.android.tools.r8.utils.ListUtils;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -71,20 +73,41 @@
   }
 
   public static class Builder {
-    private final Collection<DexEncodedMethod> constructors;
+    private int estimatedDexCodeSize;
+    private final List<List<DexEncodedMethod>> constructorGroups = new ArrayList<>();
+    private AppView<AppInfoWithLiveness> appView;
 
-    public Builder() {
-      constructors = new ArrayList<>();
+    public Builder(AppView<AppInfoWithLiveness> appView) {
+      this.appView = appView;
+
+      createNewGroup();
+    }
+
+    private void createNewGroup() {
+      estimatedDexCodeSize = 0;
+      constructorGroups.add(new ArrayList<>());
     }
 
     public Builder add(DexEncodedMethod constructor) {
-      constructors.add(constructor);
+      int estimatedMaxSizeInBytes = constructor.getCode().estimatedDexCodeSizeUpperBoundInBytes();
+      // If the constructor gets too large, then the constructor should be merged into a new group.
+      if (estimatedDexCodeSize + estimatedMaxSizeInBytes
+              > appView.options().minimumVerificationSizeLimitInBytes() / 2
+          && estimatedDexCodeSize > 0) {
+        createNewGroup();
+      }
+
+      ListUtils.last(constructorGroups).add(constructor);
+      estimatedDexCodeSize += estimatedMaxSizeInBytes;
       return this;
     }
 
-    public ConstructorMerger build(
+    public List<ConstructorMerger> build(
         AppView<?> appView, DexProgramClass target, DexField classIdField) {
-      return new ConstructorMerger(appView, target, constructors, classIdField);
+      assert constructorGroups.stream().noneMatch(List::isEmpty);
+      return ListUtils.map(
+          constructorGroups,
+          constructors -> new ConstructorMerger(appView, target, constructors, classIdField));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index c8a1c16..5dc81b1 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -155,7 +155,7 @@
       group.remove(target);
 
       ClassMerger merger =
-          new ClassMerger.Builder(target)
+          new ClassMerger.Builder(appView, target)
               .addClassesToMerge(group)
               .build(appView, mergedClassesBuilder, lensBuilder, fieldAccessChangesBuilder);
       classMergers.add(merger);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index 9e16ab0..334238d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -1605,6 +1605,11 @@
     }
 
     @Override
+    public int estimatedDexCodeSizeUpperBoundInBytes() {
+      return Integer.MAX_VALUE;
+    }
+
+    @Override
     public OutlineCode asOutlineCode() {
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java
index da4daec..ad43a6c 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java
@@ -95,4 +95,9 @@
   public final String toString(DexEncodedMethod method, ClassNameMapper naming) {
     return this.getClass().getSimpleName();
   }
+
+  @Override
+  public int estimatedDexCodeSizeUpperBoundInBytes() {
+    return Integer.MAX_VALUE;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index ea902d5..3f5ceb6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -275,6 +275,13 @@
     return isGeneratingClassFiles() ? 65534 : 16383;
   }
 
+  public int minimumVerificationSizeLimitInBytes() {
+    if (testing.verificationSizeLimitInBytesOverride > -1) {
+      return testing.verificationSizeLimitInBytesOverride;
+    }
+    return 16383;
+  }
+
   // TODO(b/141719453): The inlining limit at least should be consistent with normal inlining.
   public int classInliningInstructionLimit = 10;
   public int classInliningInstructionAllowance = 50;
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
new file mode 100644
index 0000000..847db55
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
@@ -0,0 +1,83 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import org.junit.Test;
+
+public class LargeConstructorsMergingTest extends HorizontalClassMergingTestBase {
+  public LargeConstructorsMergingTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .addOptionsModification(options -> options.testing.verificationSizeLimitInBytesOverride = 4)
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector ->
+                inspector.assertMergedInto(B.class, A.class).assertMergedInto(C.class, A.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a: 1", "b: 2", "c: 3")
+        .inspect(
+            codeInspector -> {
+              ClassSubject aClassSubject = codeInspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(
+                  codeInspector.clazz(B.class), notIf(isPresent(), enableHorizontalClassMerging));
+              assertThat(
+                  codeInspector.clazz(C.class), notIf(isPresent(), enableHorizontalClassMerging));
+
+              if (enableHorizontalClassMerging) {
+                // There should be three constructors on class A after merging.
+                assertEquals(3, aClassSubject.allMethods().size());
+              }
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    public A(int v) {
+      System.out.println("a: " + v);
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public B(int v) {
+      System.out.println("b: " + v);
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+    public C(int v) {
+      System.out.println("c: " + v);
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A(1);
+      new B(2);
+      new C(3);
+    }
+  }
+}