Only split virtual methods called by invoke-super in class merger
Bug: b/323022702
Change-Id: I0f9e0e104af6a500dd7a975dff0078cab06af8a0
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
index f4e1b95..ce747d5 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ClassMerger.java
@@ -6,6 +6,7 @@
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.ir.code.InvokeType.STATIC;
import static com.android.tools.r8.ir.code.InvokeType.VIRTUAL;
+import static com.google.common.base.Predicates.alwaysTrue;
import static java.util.function.Predicate.not;
import com.android.tools.r8.cf.CfVersion;
@@ -73,6 +74,7 @@
private final ClassMergerSharedData sharedData;
private final List<IncompleteVerticalClassMergerBridgeCode> synthesizedBridges;
+ private Predicate<DexEncodedMethod> virtualMethodsTargetedByInvokeSuperInImmediateSubclass;
ClassMerger(
AppView<AppInfoWithLiveness> appView,
@@ -93,6 +95,7 @@
}
public void setup() {
+ setupVirtualMethodsTargetedByInvokeSuperInImmediateSubclass();
setupInvokeSuperMapping();
}
@@ -101,6 +104,11 @@
}
private void redirectSuperCallsToMethod(ProgramMethod sourceMethod) {
+ if (!virtualMethodsTargetedByInvokeSuperInImmediateSubclass.test(
+ sourceMethod.getDefinition())) {
+ // Not targeted by invoke-super.
+ return;
+ }
if (sourceMethod.getAccessFlags().belongsToDirectPool()) {
redirectSuperCallsToDirectMethod(sourceMethod);
} else {
@@ -214,6 +222,22 @@
for (DexEncodedMethod virtualMethod :
source.virtualMethods(not(DexEncodedMethod::isAbstract))) {
DexEncodedMethod shadowedBy = findMethodInTarget(virtualMethod);
+ if (!virtualMethodsTargetedByInvokeSuperInImmediateSubclass.test(virtualMethod)) {
+ if (shadowedBy != null) {
+ // Similarly to abstract methods that are overridden, we remove overridden non-abstract
+ // methods that are never targeted by invoke-super. These are guaranteed to be dead as we
+ // currently only allow merging uninstantiated classes into instantiated classes.
+ lensBuilder.recordSplit(virtualMethod, shadowedBy, null, null);
+ } else {
+ DexEncodedMethod movedMethod =
+ virtualMethod.toTypeSubstitutedMethodAsInlining(
+ virtualMethod.getReference().withHolder(target, dexItemFactory), dexItemFactory);
+ virtualMethods.put(movedMethod, movedMethod);
+ lensBuilder.recordMove(virtualMethod, movedMethod);
+ }
+ continue;
+ }
+
DexEncodedMethod resultingMethod;
if (source.isInterface()) {
// Moving a default interface method into its subtype. This method could be hit directly
@@ -367,6 +391,25 @@
verticallyMergedClassesBuilder.add(source, target);
}
+ private void setupVirtualMethodsTargetedByInvokeSuperInImmediateSubclass() {
+ if (!appView.options().getVerticalClassMergerOptions().isBridgeAnalysisEnabled()) {
+ virtualMethodsTargetedByInvokeSuperInImmediateSubclass = alwaysTrue();
+ return;
+ }
+ DexMethodSignatureSet methodsOfInterest = DexMethodSignatureSet.create();
+ source.forEachProgramMethodMatching(
+ method -> method.hasCode() && method.isVirtualMethod(), methodsOfInterest::add);
+ DexMethodSignatureSet result = DexMethodSignatureSet.create();
+ target.forEachProgramMethodMatching(
+ DexEncodedMethod::hasCode,
+ method -> {
+ InvokeSuperExtractor invokeSuperExtractor =
+ new InvokeSuperExtractor(appView, method, methodsOfInterest, result, source);
+ method.registerCodeReferences(invokeSuperExtractor);
+ });
+ virtualMethodsTargetedByInvokeSuperInImmediateSubclass = result::contains;
+ }
+
private void fixupAccessFlags() {
if (source.getAccessFlags().isEnum()) {
target.getAccessFlags().setEnum();
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSuperExtractor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSuperExtractor.java
new file mode 100644
index 0000000..b0770ee
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/InvokeSuperExtractor.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2024, 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.verticalclassmerging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DefaultUseRegistry;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
+
+public class InvokeSuperExtractor extends DefaultUseRegistry<ProgramMethod> {
+
+ private final AppView<AppInfoWithLiveness> appViewWithLiveness;
+ private final GraphLens graphLens;
+ private final GraphLens codeLens;
+ private final DexMethodSignatureSet methodsOfInterest;
+ private final DexMethodSignatureSet result;
+ private final DexProgramClass source;
+
+ public InvokeSuperExtractor(
+ AppView<AppInfoWithLiveness> appView,
+ ProgramMethod context,
+ DexMethodSignatureSet methodsOfInterest,
+ DexMethodSignatureSet result,
+ DexProgramClass source) {
+ super(appView, context);
+ this.appViewWithLiveness = appView;
+ this.graphLens = appView.graphLens();
+ this.codeLens = context.getDefinition().getCode().getCodeLens(appView);
+ this.methodsOfInterest = methodsOfInterest;
+ this.result = result;
+ this.source = source;
+ }
+
+ // TODO(b/323022702): This should not need to be overridden, but we sometimes (correctly) map an
+ // invoke-special to invoke-direct, but then later decides to map the same invoke-special to
+ // invoke-super.
+ @Override
+ public void registerInvokeSpecial(DexMethod method) {
+ handleInvokeSpecial(method);
+ }
+
+ @Override
+ public void registerInvokeSuper(DexMethod method) {
+ handleInvokeSpecial(method);
+ }
+
+ private void handleInvokeSpecial(DexMethod method) {
+ MethodLookupResult lookupResult = graphLens.lookupInvokeSuper(method, getContext(), codeLens);
+ DexMethod rewrittenMethod = lookupResult.getReference();
+ if (!methodsOfInterest.contains(rewrittenMethod) || result.contains(rewrittenMethod)) {
+ return;
+ }
+ MethodResolutionResult currentResolutionResult =
+ appViewWithLiveness.appInfo().unsafeResolveMethodDueToDexFormat(rewrittenMethod);
+ DexClassAndMethod superTarget =
+ currentResolutionResult.lookupInvokeSuperTarget(getContext(), appViewWithLiveness);
+ if (superTarget != null && superTarget.getHolder() == source) {
+ result.add(superTarget);
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
index 8ef8743..64d6556 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -28,7 +28,6 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.Timing.TimingMerger;
-import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import java.util.ArrayList;
import java.util.Collection;
@@ -130,8 +129,7 @@
// can't build IR.
rewriteCodeWithLens(executorService, timing);
- // Remove force inlined constructors.
- removeFullyInlinedInstanceInitializers(executorService, timing);
+ // Remove merged classes from app now that the code is fully rewritten.
removeMergedClasses(verticalClassMergerResult.getVerticallyMergedClasses(), timing);
// Convert the (incomplete) synthesized bridges to CF or LIR.
@@ -278,38 +276,6 @@
timing.end();
}
- private void removeFullyInlinedInstanceInitializers(
- ExecutorService executorService, Timing timing) throws ExecutionException {
- if (mode.isInitial()) {
- return;
- }
- timing.begin("Remove fully inlined instance initializers");
- PrunedItems.Builder prunedItemsBuilder =
- PrunedItems.concurrentBuilder().setPrunedApp(appView.app());
- ThreadUtils.<DexProgramClass, Exception>processItems(
- consumer -> {
- for (DexProgramClass clazz : appView.appInfo().classes()) {
- if (!clazz.isInterface()) {
- consumer.accept(clazz);
- }
- }
- },
- clazz -> {
- Set<DexEncodedMethod> methodsToRemove = Sets.newIdentityHashSet();
- // TODO(b/321171043): Inline and remove instance initializers that are only called from
- // other instance initializers in the same class.
- clazz.getMethodCollection().removeMethods(methodsToRemove);
- methodsToRemove.forEach(
- removedMethod -> prunedItemsBuilder.addRemovedMethod(removedMethod.getReference()));
- },
- options.getThreadingModule(),
- executorService);
- PrunedItems prunedItems = prunedItemsBuilder.build();
- appView.pruneItems(prunedItems, executorService, Timing.empty());
- appView.appInfo().getMethodAccessInfoCollection().withoutPrunedItems(prunedItems);
- timing.end();
- }
-
private void removeMergedClasses(VerticallyMergedClasses verticallyMergedClasses, Timing timing) {
if (mode.isInitial()) {
return;
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
index ead346d..783b3d2 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerGraphLens.java
@@ -513,7 +513,6 @@
}
if (implementation == null) {
- assert from.isAbstract();
return;
}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
index dbe28ca..55b3b41 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerOptions.java
@@ -12,6 +12,7 @@
private boolean enabled = true;
private boolean enableInitial = true;
+ private boolean enableBridgeAnalysis = true;
public VerticalClassMergerOptions(InternalOptions options) {
this.options = options;
@@ -35,7 +36,15 @@
return true;
}
+ public boolean isBridgeAnalysisEnabled() {
+ return enableBridgeAnalysis;
+ }
+
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
+
+ public void setEnableBridgeAnalysis(boolean enableBridgeAnalysis) {
+ this.enableBridgeAnalysis = enableBridgeAnalysis;
+ }
}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 50f9638..c5c75a8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -270,7 +270,10 @@
testForR8(parameters.getBackend())
.addKeepRules(getProguardConfig(EXAMPLE_KEEP))
.addOptionsModification(this::configure)
- .addOptionsModification(InlinerOptions::setOnlyForceInlining)
+ .addOptionsModification(
+ options ->
+ options.getVerticalClassMergerOptions().setEnableBridgeAnalysis(false))
+ .addOptionsModification(InlinerOptions::disableInlining)
.allowUnusedProguardConfigurationRules(),
main,
readProgramFiles(programFiles),
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
index de57198..d79b35a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithMissingTypeArgsSubstitutionTest.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.classmerging.vertical;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@@ -13,10 +14,11 @@
import com.android.tools.r8.NoMethodStaticizing;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import java.lang.reflect.TypeVariable;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -27,11 +29,15 @@
public class VerticalClassMergingWithMissingTypeArgsSubstitutionTest extends TestBase {
@Parameter(0)
+ public boolean enableBridgeAnalysis;
+
+ @Parameter(1)
public TestParameters parameters;
- @Parameters(name = "{0}")
- public static TestParametersCollection data() {
- return getTestParameters().withAllRuntimesAndApiLevels().build();
+ @Parameters(name = "{1}, bridge analysis: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
}
@Test
@@ -41,8 +47,13 @@
.addKeepMainRule(Main.class)
.addKeepClassRules(A.class)
.addKeepAttributeSignature()
+ .addOptionsModification(
+ options ->
+ options
+ .getVerticalClassMergerOptions()
+ .setEnableBridgeAnalysis(enableBridgeAnalysis))
.addVerticallyMergedClassesInspector(
- inspector -> inspector.assertMergedIntoSubtype(B.class))
+ inspector -> inspector.assertMergedIntoSubtype(B.class).assertNoOtherClassesMerged())
.enableInliningAnnotations()
.enableNoMethodStaticizingAnnotations()
.enableConstantArgumentAnnotations()
@@ -56,9 +67,11 @@
assertEquals(
"<T:Ljava/lang/Object;>L" + binaryName(A.class) + "<Ljava/lang/Object;>;",
classSubject.getFinalSignatureAttribute());
+
MethodSubject bar = classSubject.uniqueMethodWithOriginalName("bar");
assertThat(bar, isPresentAndRenamed());
assertEquals("(TT;)V", bar.getFinalSignatureAttribute());
+
// The NeverInline is transferred to the private vertically merged method but also
// copied to the virtual bridge.
MethodSubject fooMovedFromB =
@@ -72,6 +85,7 @@
assertEquals(
"(Ljava/lang/Object;)Ljava/lang/Object;",
fooMovedFromB.getFinalSignatureAttribute());
+
MethodSubject fooBridge =
classSubject.uniqueMethodThatMatches(
method ->
@@ -79,9 +93,14 @@
&& method.isVirtual()
&& method.isSynthetic()
&& method.getOriginalName(false).equals("foo"));
- assertThat(fooBridge, isPresentAndRenamed());
- assertEquals(
- "(Ljava/lang/Object;)Ljava/lang/Object;", fooBridge.getFinalSignatureAttribute());
+ if (enableBridgeAnalysis) {
+ assertThat(fooBridge, isAbsent());
+ } else {
+ assertThat(fooBridge, isPresentAndRenamed());
+ assertEquals(
+ "(Ljava/lang/Object;)Ljava/lang/Object;",
+ fooBridge.getFinalSignatureAttribute());
+ }
});
}
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
index 6271b08..52a8deb 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingAfterVerticalMergingMethodTest.java
@@ -113,14 +113,14 @@
assertThat(inspector.clazz(LibrarySubclass.class), isPresent());
List<FoundMethodSubject> methods =
inspector.clazz(LibrarySubclass.class).allMethods();
- assertEquals(backend.isCf() ? 5 : 4, methods.size());
+ assertEquals(backend.isCf() ? 4 : 3, methods.size());
assertEquals(
backend.isCf() ? 2 : 1,
methods.stream().filter(FoundMethodSubject::isInstanceInitializer).count());
assertEquals(
1, methods.stream().filter(m -> m.getFinalName().contains("main")).count());
assertEquals(
- 2, methods.stream().filter(m -> m.getOriginalName().contains("foo")).count());
+ 1, methods.stream().filter(m -> m.getOriginalName().contains("foo")).count());
});
}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
index 2fd2c50..cb94b5d 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
@@ -27,22 +27,34 @@
@RunWith(Parameterized.class)
public class VerticalClassMergingRetraceTest extends RetraceTestBase {
- @Parameters(name = "{0}, mode: {1}, compat: {2}")
+ @Parameters(name = "{0}, mode: {1}, compat: {2}, bridge analysis: {3}")
public static Collection<Object[]> data() {
return buildParameters(
getTestParameters().withAllRuntimesAndApiLevels().build(),
CompilationMode.values(),
+ BooleanUtils.values(),
BooleanUtils.values());
}
+ private final boolean enableBridgeAnalysis;
+
public VerticalClassMergingRetraceTest(
- TestParameters parameters, CompilationMode mode, boolean compat) {
+ TestParameters parameters,
+ CompilationMode mode,
+ boolean compat,
+ boolean enableBridgeAnalysis) {
super(parameters, mode, compat);
+ this.enableBridgeAnalysis = enableBridgeAnalysis;
}
@Override
- public void configure(R8TestBuilder builder) {
+ public void configure(R8TestBuilder<?> builder) {
builder
+ .addOptionsModification(
+ options ->
+ options
+ .getVerticalClassMergerOptions()
+ .setEnableBridgeAnalysis(enableBridgeAnalysis))
.enableInliningAnnotations()
.enableKeepUnusedReturnValueAnnotations()
.enableNeverClassInliningAnnotations();
@@ -59,8 +71,9 @@
}
private int expectedActualStackTraceHeight() {
- // In RELEASE mode, a synthetic bridge will be added by vertical class merger.
- return mode == CompilationMode.RELEASE ? 3 : 2;
+ // In RELEASE mode a synthetic bridge is added by the vertical class merger if the method is
+ // targeted by the invoke-super (which is modeled by setting enableBridgeAnalysis to false).
+ return mode == CompilationMode.DEBUG || enableBridgeAnalysis ? 2 : 3;
}
private boolean filterSynthesizedMethodWhenLineNumberAvailable(
diff --git a/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java b/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
index 56ea0b9..4493de9 100644
--- a/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/profile/art/completeness/VerticalClassMergingBridgeProfileRewritingTest.java
@@ -41,9 +41,12 @@
.addInnerClasses(getClass())
.addKeepMainRule(Main.class)
.addArtProfileForRewriting(getArtProfile())
- .addOptionsModification(InlinerOptions::setOnlyForceInlining)
+ .addOptionsModification(InlinerOptions::disableInlining)
.addOptionsModification(
- options -> options.callSiteOptimizationOptions().disableOptimization())
+ options -> {
+ options.callSiteOptimizationOptions().disableOptimization();
+ options.getVerticalClassMergerOptions().setEnableBridgeAnalysis(false);
+ })
.addVerticallyMergedClassesInspector(
inspector -> inspector.assertMergedIntoSubtype(A.class))
.enableNeverClassInliningAnnotations()