Refactor vertical class merger to use horizontal policy executor
Change-Id: I69665c5784245d21ba873b8b75126cb73adc2199
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 57ff66a..3bc0655 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -127,7 +127,8 @@
List<Policy> policies =
PolicyScheduler.getPolicies(appView, codeProvider, mode, runtimeTypeCheckInfo);
Collection<MergeGroup> groups =
- new PolicyExecutor().run(getInitialGroups(), policies, executorService, timing);
+ new HorizontalClassMergerPolicyExecutor()
+ .run(getInitialGroups(), policies, executorService, timing);
// If there are no groups, then end horizontal class merging.
if (groups.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java
new file mode 100644
index 0000000..f207cbc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerPolicyExecutor.java
@@ -0,0 +1,90 @@
+// 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.horizontalclassmerging;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class HorizontalClassMergerPolicyExecutor extends PolicyExecutor<MergeGroup> {
+
+ @Override
+ protected LinkedList<MergeGroup> apply(
+ Policy policy, LinkedList<MergeGroup> linkedGroups, ExecutorService executorService)
+ throws ExecutionException {
+ if (policy.isSingleClassPolicy()) {
+ applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
+ } else {
+ if (policy.isMultiClassPolicy()) {
+ linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
+ } else {
+ assert policy.isMultiClassPolicyWithPreprocessing();
+ linkedGroups =
+ applyMultiClassPolicyWithPreprocessing(
+ policy.asMultiClassPolicyWithPreprocessing(), linkedGroups, executorService);
+ }
+ }
+ return linkedGroups;
+ }
+
+ void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
+ Iterator<MergeGroup> i = groups.iterator();
+ while (i.hasNext()) {
+ MergeGroup group = i.next();
+ boolean isInterfaceGroup = group.isInterfaceGroup();
+ int previousGroupSize = group.size();
+ group.removeIf(clazz -> !policy.canMerge(clazz));
+ assert policy.recordRemovedClassesForDebugging(
+ isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
+ if (group.isTrivial()) {
+ i.remove();
+ }
+ }
+ }
+
+ // TODO(b/270398965): Replace LinkedList.
+ @SuppressWarnings("JdkObsolete")
+ private LinkedList<MergeGroup> applyMultiClassPolicy(
+ MultiClassPolicy policy, LinkedList<MergeGroup> groups) {
+ // For each group apply the multi class policy and add all the new groups together.
+ LinkedList<MergeGroup> newGroups = new LinkedList<>();
+ groups.forEach(
+ group -> {
+ boolean isInterfaceGroup = group.isInterfaceGroup();
+ int previousGroupSize = group.size();
+ Collection<MergeGroup> policyGroups = policy.apply(group);
+ policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+ assert policy.recordRemovedClassesForDebugging(
+ isInterfaceGroup, previousGroupSize, policyGroups);
+ newGroups.addAll(policyGroups);
+ });
+ return newGroups;
+ }
+
+ // TODO(b/270398965): Replace LinkedList.
+ @SuppressWarnings("JdkObsolete")
+ private <T> LinkedList<MergeGroup> applyMultiClassPolicyWithPreprocessing(
+ MultiClassPolicyWithPreprocessing<T> policy,
+ LinkedList<MergeGroup> groups,
+ ExecutorService executorService)
+ throws ExecutionException {
+ // For each group apply the multi class policy and add all the new groups together.
+ T data = policy.preprocess(groups, executorService);
+ LinkedList<MergeGroup> newGroups = new LinkedList<>();
+ groups.forEach(
+ group -> {
+ boolean isInterfaceGroup = group.isInterfaceGroup();
+ int previousGroupSize = group.size();
+ Collection<MergeGroup> policyGroups = policy.apply(group, data);
+ policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+ assert policy.recordRemovedClassesForDebugging(
+ isInterfaceGroup, previousGroupSize, policyGroups);
+ newGroups.addAll(policyGroups);
+ });
+ return newGroups;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index 8062a16..06b3ab6 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -26,7 +26,7 @@
import java.util.function.Consumer;
import java.util.function.Predicate;
-public class MergeGroup implements Collection<DexProgramClass> {
+public class MergeGroup extends MergeGroupBase implements Collection<DexProgramClass> {
public static class Metadata {}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroupBase.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroupBase.java
new file mode 100644
index 0000000..8d96410
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroupBase.java
@@ -0,0 +1,9 @@
+// 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.horizontalclassmerging;
+
+public abstract class MergeGroupBase {
+
+ public abstract int size();
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
index 5bff09e..530a642 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.horizontalclassmerging;
+import com.android.tools.r8.verticalclassmerging.VerticalClassMergerPolicy;
import java.util.ArrayList;
import java.util.Collection;
@@ -50,6 +51,14 @@
return null;
}
+ public boolean isVerticalClassMergerPolicy() {
+ return false;
+ }
+
+ public VerticalClassMergerPolicy asVerticalClassMergerPolicy() {
+ return null;
+ }
+
public boolean shouldSkipPolicy() {
return false;
}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
index 28ec89d..0495ce2 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -5,9 +5,7 @@
package com.android.tools.r8.horizontalclassmerging;
import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableList;
import java.util.Collection;
-import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -17,64 +15,7 @@
* primarily be readable and correct. The SimplePolicyExecutor should be a reference implementation,
* against which more efficient policy executors can be compared.
*/
-public class PolicyExecutor {
-
- private void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
- Iterator<MergeGroup> i = groups.iterator();
- while (i.hasNext()) {
- MergeGroup group = i.next();
- boolean isInterfaceGroup = group.isInterfaceGroup();
- int previousGroupSize = group.size();
- group.removeIf(clazz -> !policy.canMerge(clazz));
- assert policy.recordRemovedClassesForDebugging(
- isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
- if (group.isTrivial()) {
- i.remove();
- }
- }
- }
-
- // TODO(b/270398965): Replace LinkedList.
- @SuppressWarnings("JdkObsolete")
- private LinkedList<MergeGroup> applyMultiClassPolicy(
- MultiClassPolicy policy, LinkedList<MergeGroup> groups) {
- // For each group apply the multi class policy and add all the new groups together.
- LinkedList<MergeGroup> newGroups = new LinkedList<>();
- groups.forEach(
- group -> {
- boolean isInterfaceGroup = group.isInterfaceGroup();
- int previousGroupSize = group.size();
- Collection<MergeGroup> policyGroups = policy.apply(group);
- policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
- assert policy.recordRemovedClassesForDebugging(
- isInterfaceGroup, previousGroupSize, policyGroups);
- newGroups.addAll(policyGroups);
- });
- return newGroups;
- }
-
- // TODO(b/270398965): Replace LinkedList.
- @SuppressWarnings("JdkObsolete")
- private <T> LinkedList<MergeGroup> applyMultiClassPolicyWithPreprocessing(
- MultiClassPolicyWithPreprocessing<T> policy,
- LinkedList<MergeGroup> groups,
- ExecutorService executorService)
- throws ExecutionException {
- // For each group apply the multi class policy and add all the new groups together.
- T data = policy.preprocess(groups, executorService);
- LinkedList<MergeGroup> newGroups = new LinkedList<>();
- groups.forEach(
- group -> {
- boolean isInterfaceGroup = group.isInterfaceGroup();
- int previousGroupSize = group.size();
- Collection<MergeGroup> policyGroups = policy.apply(group, data);
- policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
- assert policy.recordRemovedClassesForDebugging(
- isInterfaceGroup, previousGroupSize, policyGroups);
- newGroups.addAll(policyGroups);
- });
- return newGroups;
- }
+public abstract class PolicyExecutor<MG extends MergeGroupBase> {
/**
* Given an initial collection of class groups which can potentially be merged, run all of the
@@ -83,16 +24,16 @@
*/
// TODO(b/270398965): Replace LinkedList.
@SuppressWarnings("JdkObsolete")
- public Collection<MergeGroup> run(
- Collection<MergeGroup> inputGroups,
+ public Collection<MG> run(
+ Collection<MG> inputGroups,
Collection<Policy> policies,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
- LinkedList<MergeGroup> linkedGroups;
+ LinkedList<MG> linkedGroups;
if (inputGroups instanceof LinkedList) {
- linkedGroups = (LinkedList<MergeGroup>) inputGroups;
+ linkedGroups = (LinkedList<MG>) inputGroups;
} else {
linkedGroups = new LinkedList<>(inputGroups);
}
@@ -103,16 +44,7 @@
}
timing.begin(policy.getName());
- if (policy.isSingleClassPolicy()) {
- applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
- } else if (policy.isMultiClassPolicy()) {
- linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
- } else {
- assert policy.isMultiClassPolicyWithPreprocessing();
- linkedGroups =
- applyMultiClassPolicyWithPreprocessing(
- policy.asMultiClassPolicyWithPreprocessing(), linkedGroups, executorService);
- }
+ linkedGroups = apply(policy, linkedGroups, executorService);
timing.end();
policy.clear();
@@ -127,4 +59,8 @@
return linkedGroups;
}
+
+ protected abstract LinkedList<MG> apply(
+ Policy policy, LinkedList<MG> linkedGroups, ExecutorService executorService)
+ throws ExecutionException;
}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
index 22316f3c..cb785f7 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/ConnectedComponentVerticalClassMerger.java
@@ -5,19 +5,18 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.ListUtils;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Comparator;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.ExecutionException;
public class ConnectedComponentVerticalClassMerger {
private final AppView<AppInfoWithLiveness> appView;
- private final Set<DexProgramClass> classesToMerge;
+ private final Collection<VerticalMergeGroup> classesToMerge;
// The resulting graph lens that should be used after class merging.
private final VerticalClassMergerGraphLens.Builder lensBuilder;
@@ -29,7 +28,7 @@
VerticallyMergedClasses.builder();
ConnectedComponentVerticalClassMerger(
- AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> classesToMerge) {
+ AppView<AppInfoWithLiveness> appView, Collection<VerticalMergeGroup> classesToMerge) {
this.appView = appView;
this.classesToMerge = classesToMerge;
this.lensBuilder = new VerticalClassMergerGraphLens.Builder();
@@ -39,23 +38,19 @@
return classesToMerge.isEmpty();
}
- public VerticalClassMergerResult.Builder run(ImmediateProgramSubtypingInfo immediateSubtypingInfo)
- throws ExecutionException {
- List<DexProgramClass> classesToMergeSorted =
- ListUtils.sort(classesToMerge, Comparator.comparing(DexProgramClass::getType));
- for (DexProgramClass clazz : classesToMergeSorted) {
- mergeClassIfPossible(clazz, immediateSubtypingInfo);
+ public VerticalClassMergerResult.Builder run() throws ExecutionException {
+ List<VerticalMergeGroup> classesToMergeSorted =
+ ListUtils.sort(classesToMerge, Comparator.comparing(group -> group.getSource().getType()));
+ for (VerticalMergeGroup group : classesToMergeSorted) {
+ mergeClassIfPossible(group);
}
return VerticalClassMergerResult.builder(
lensBuilder, synthesizedBridges, verticallyMergedClassesBuilder);
}
- private void mergeClassIfPossible(
- DexProgramClass sourceClass, ImmediateProgramSubtypingInfo immediateSubtypingInfo)
- throws ExecutionException {
- List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
- assert subclasses.size() == 1;
- DexProgramClass targetClass = ListUtils.first(subclasses);
+ private void mergeClassIfPossible(VerticalMergeGroup group) throws ExecutionException {
+ DexProgramClass sourceClass = group.getSource();
+ DexProgramClass targetClass = group.getTarget();
if (verticallyMergedClassesBuilder.isMergeSource(targetClass)
|| verticallyMergedClassesBuilder.isMergeTarget(sourceClass)) {
return;
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 cb482b9..85a48f5 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
@@ -224,8 +224,7 @@
Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
getConnectedComponentMergers(
connectedComponents, immediateSubtypingInfo, executorService, timing);
- return applyConnectedComponentMergers(
- connectedComponentMergers, immediateSubtypingInfo, executorService, timing);
+ return applyConnectedComponentMergers(connectedComponentMergers, executorService, timing);
}
private Collection<ConnectedComponentVerticalClassMerger> getConnectedComponentMergers(
@@ -244,8 +243,9 @@
connectedComponent -> {
Timing threadTiming = Timing.create("Compute classes to merge in component", options);
ConnectedComponentVerticalClassMerger connectedComponentMerger =
- new VerticalClassMergerPolicyExecutor(appView, pinnedClasses)
- .run(connectedComponent, immediateSubtypingInfo);
+ new VerticalClassMergerPolicyExecutor(
+ appView, immediateSubtypingInfo, pinnedClasses)
+ .run(connectedComponent, executorService, threadTiming);
if (!connectedComponentMerger.isEmpty()) {
synchronized (connectedComponentMergers) {
connectedComponentMergers.add(connectedComponentMerger);
@@ -263,7 +263,6 @@
private VerticalClassMergerResult applyConnectedComponentMergers(
Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers,
- ImmediateProgramSubtypingInfo immediateSubtypingInfo,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
@@ -276,7 +275,7 @@
connectedComponentMerger -> {
Timing threadTiming = Timing.create("Merge classes in component", options);
VerticalClassMergerResult.Builder verticalClassMergerComponentResult =
- connectedComponentMerger.run(immediateSubtypingInfo);
+ connectedComponentMerger.run();
verticalClassMergerResult.merge(verticalClassMergerComponentResult);
threadTiming.end();
return threadTiming;
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicy.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicy.java
new file mode 100644
index 0000000..9635b3c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicy.java
@@ -0,0 +1,441 @@
+// 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.verticalclassmerging;
+
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
+
+import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
+import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
+import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.Policy;
+import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
+import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class VerticalClassMergerPolicy extends Policy {
+
+ private final AppView<AppInfoWithLiveness> appView;
+ private final MainDexInfo mainDexInfo;
+ private final InternalOptions options;
+ private final Set<DexProgramClass> pinnedClasses;
+
+ VerticalClassMergerPolicy(
+ AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> pinnedClasses) {
+ this.appView = appView;
+ this.options = appView.options();
+ this.mainDexInfo = appView.appInfo().getMainDexInfo();
+ this.pinnedClasses = pinnedClasses;
+ }
+
+ @Override
+ public boolean isVerticalClassMergerPolicy() {
+ return true;
+ }
+
+ @Override
+ public VerticalClassMergerPolicy asVerticalClassMergerPolicy() {
+ return this;
+ }
+
+ public boolean canMerge(VerticalMergeGroup group) {
+ DexProgramClass sourceClass = group.getSource();
+ DexProgramClass targetClass = group.getTarget();
+ if (!isMergeCandidate(sourceClass, targetClass)) {
+ return false;
+ }
+ if (!isStillMergeCandidate(sourceClass, targetClass)) {
+ return false;
+ }
+ if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)
+ || mergeMayLeadToNoSuchMethodError(sourceClass, targetClass)) {
+ return false;
+ }
+ return true;
+ }
+
+ // Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
+ // method do not change in response to any class merges.
+ private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+ assert targetClass != null;
+ ObjectAllocationInfoCollection allocationInfo =
+ appView.appInfo().getObjectAllocationInfoCollection();
+ if (allocationInfo.isInstantiatedDirectly(sourceClass)
+ || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
+ || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
+ || !appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
+ || pinnedClasses.contains(sourceClass)) {
+ return false;
+ }
+
+ assert sourceClass
+ .traverseProgramMembers(
+ member -> {
+ assert !appView.getKeepInfo(member).isPinned(options);
+ return TraversalContinuation.doContinue();
+ })
+ .shouldContinue();
+
+ if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+ sourceClass, targetClass, appView)) {
+ return false;
+ }
+ if (!StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
+ sourceClass, targetClass, appView)) {
+ return false;
+ }
+ if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
+ && appView.getKeepInfo(targetClass).isPinned(options)) {
+ return false;
+ }
+ if (sourceClass.isAnnotation()) {
+ return false;
+ }
+ if (!sourceClass.isInterface()
+ && targetClass.isSerializable(appView)
+ && !appView.appInfo().isSerializable(sourceClass.getType())) {
+ // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
+ // 1.10 The Serializable Interface
+ // ...
+ // A Serializable class must do the following:
+ // ...
+ // * Have access to the no-arg constructor of its first non-serializable superclass
+ return false;
+ }
+
+ // If there is a constructor in the target, make sure that all source constructors can be
+ // inlined.
+ if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
+ TraversalContinuation<?, ?> result =
+ sourceClass.traverseProgramInstanceInitializers(
+ method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
+ if (result.shouldBreak()) {
+ return false;
+ }
+ }
+ if (sourceClass.hasEnclosingMethodAttribute() || !sourceClass.getInnerClasses().isEmpty()) {
+ return false;
+ }
+ // We abort class merging when merging across nests or from a nest to non-nest.
+ // Without nest this checks null == null.
+ if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
+ return false;
+ }
+
+ // If there is an invoke-special to a default interface method and we are not merging into an
+ // interface, then abort, since invoke-special to a virtual class method requires desugaring.
+ if (sourceClass.isInterface() && !targetClass.isInterface()) {
+ TraversalContinuation<?, ?> result =
+ sourceClass.traverseProgramMethods(
+ method -> {
+ boolean foundInvokeSpecialToDefaultLibraryMethod =
+ method.registerCodeReferencesWithResult(
+ new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
+ return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
+ });
+ if (result.shouldBreak()) {
+ return false;
+ }
+ }
+
+ // Check with main dex classes to see if we are allowed to merge.
+ if (!mainDexInfo.canMerge(sourceClass, targetClass, appView.getSyntheticItems())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if {@param sourceClass} is a merge candidate. Note that the result of the checks
+ * in this method may change in response to class merges. Therefore, this method should always be
+ * called before merging {@param sourceClass} into {@param targetClass}.
+ */
+ private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
+ // For interface types, this is more complicated, see:
+ // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
+ // We basically can't move the clinit, since it is not called when implementing classes have
+ // their clinit called - except when the interface has a default method.
+ if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
+ || targetClass.classInitializationMayHaveSideEffects(
+ appView, type -> type.isIdenticalTo(sourceClass.getType()))
+ || (sourceClass.isInterface()
+ && sourceClass.classInitializationMayHaveSideEffects(appView))) {
+ return false;
+ }
+ boolean sourceCanBeSynchronizedOn =
+ appView.appInfo().isLockCandidate(sourceClass)
+ || sourceClass.hasStaticSynchronizedMethods();
+ boolean targetCanBeSynchronizedOn =
+ appView.appInfo().isLockCandidate(targetClass)
+ || targetClass.hasStaticSynchronizedMethods();
+ if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
+ return false;
+ }
+ if (targetClass.hasEnclosingMethodAttribute() || !targetClass.getInnerClasses().isEmpty()) {
+ return false;
+ }
+ if (methodResolutionMayChange(sourceClass, targetClass)) {
+ return false;
+ }
+ // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
+ // to the super class.
+ if (fieldResolutionMayChange(sourceClass, targetClass)) {
+ return false;
+ }
+ // Only merge if api reference level of source class is equal to target class. The check is
+ // somewhat expensive.
+ if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
+ AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
+ ComputedApiLevel sourceApiLevel =
+ getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
+ ComputedApiLevel targetApiLevel =
+ getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
+ if (!sourceApiLevel.equals(targetApiLevel)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
+ if (!appView.options().inlinerOptions().enableInlining) {
+ return true;
+ }
+ Code code = method.getDefinition().getCode();
+ if (code.isCfCode()) {
+ CfCode cfCode = code.asCfCode();
+ ConstraintWithTarget constraint =
+ cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
+ if (constraint.isNever()) {
+ return true;
+ }
+ // Constructors can have references beyond the root main dex classes. This can increase the
+ // size of the main dex dependent classes and we should bail out.
+ if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
+ return true;
+ }
+ return false;
+ }
+ if (code.isDefaultInstanceInitializerCode()) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
+ if (source.getType().isIdenticalTo(target.getSuperType())) {
+ // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
+ // Target implements an interface that declares a static final field f, this should yield an
+ // IncompatibleClassChangeError.
+ // TODO(christofferqa): In the following we only check if a static field from an interface
+ // shadows an instance field from [source]. We could actually check if there is an iget/iput
+ // instruction whose resolution would be affected by the merge. The situation where a static
+ // field shadows an instance field is probably not widespread in practice, though.
+ FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+ Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
+ for (DexType interfaceType : target.getInterfaces()) {
+ DexClass clazz = appView.definitionFor(interfaceType);
+ for (DexEncodedField staticField : clazz.staticFields()) {
+ staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
+ }
+ }
+ for (DexEncodedField instanceField : source.instanceFields()) {
+ if (staticFieldsInInterfacesOfTarget.contains(
+ equivalence.wrap(instanceField.getReference()))) {
+ // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
+ // interface would now hit an instance field from [source], so that an IncompatibleClass-
+ // ChangeError would no longer be thrown. Abort merge.
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
+ if (source.isSamePackage(target)) {
+ // When merging two classes from the same package, we only need to make sure that [source]
+ // does not get less visible, since that could make a valid access to [source] from another
+ // package illegal after [source] has been merged into [target].
+ assert source.getAccessFlags().isPackagePrivateOrPublic();
+ assert target.getAccessFlags().isPackagePrivateOrPublic();
+ // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
+ return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
+ }
+
+ // Check that all accesses to [source] and its members from inside the current package of
+ // [source] will continue to work. This is guaranteed if [target] is public and all members of
+ // [source] are either private or public.
+ //
+ // (Deliberately not checking all accesses to [source] since that would be expensive.)
+ if (!target.isPublic()) {
+ return true;
+ }
+ for (DexType sourceInterface : source.getInterfaces()) {
+ DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
+ if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
+ return true;
+ }
+ }
+ for (DexEncodedField field : source.fields()) {
+ if (!(field.isPublic() || field.isPrivate())) {
+ return true;
+ }
+ }
+ for (DexEncodedMethod method : source.methods()) {
+ if (!(method.isPublic() || method.isPrivate())) {
+ return true;
+ }
+ // Check if the target is overriding and narrowing the access.
+ if (method.isPublic()) {
+ DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
+ if (targetOverride != null && !targetOverride.isPublic()) {
+ return true;
+ }
+ }
+ }
+ // Check that all accesses from [source] to classes or members from the current package of
+ // [source] will continue to work. This is guaranteed if the methods of [source] do not access
+ // any private or protected classes or members from the current package of [source].
+ TraversalContinuation<?, ?> result =
+ source.traverseProgramMethods(
+ method -> {
+ boolean foundIllegalAccess =
+ method.registerCodeReferencesWithResult(
+ new IllegalAccessDetector(appView, method));
+ if (foundIllegalAccess) {
+ return TraversalContinuation.doBreak();
+ }
+ return TraversalContinuation.doContinue();
+ },
+ DexEncodedMethod::hasCode);
+ return result.shouldBreak();
+ }
+
+ // TODO: maybe skip this check if target does not implement any interfaces (directly or
+ // indirectly)?
+ private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
+ // This only returns true when an invoke-super instruction is found that targets a default
+ // interface method.
+ if (!options.canUseDefaultAndStaticInterfaceMethods()) {
+ return false;
+ }
+ // This problem may only arise when merging (non-interface) classes into classes.
+ if (source.isInterface() || target.isInterface()) {
+ return false;
+ }
+ return target
+ .traverseProgramMethods(
+ method -> {
+ MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
+ new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
+ method.registerCodeReferencesWithResult(registry);
+ return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
+ },
+ DexEncodedMethod::hasCode)
+ .shouldBreak();
+ }
+
+ private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
+ for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
+ DexEncodedMethod directTargetMethod =
+ target.lookupDirectMethod(virtualSourceMethod.getReference());
+ if (directTargetMethod != null) {
+ // A private method shadows a virtual method. This situation is rare, since it is not
+ // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
+ // possible to rename the private method in the subclass, and then move the virtual method
+ // to the subclass without changing its name.)
+ return true;
+ }
+ }
+
+ // When merging an interface into a class, all instructions on the form "invoke-interface
+ // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
+ // transformation could hide IncompatibleClassChangeErrors.
+ if (source.isInterface() && !target.isInterface()) {
+ List<DexEncodedMethod> defaultMethods = new ArrayList<>();
+ for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+ if (!virtualMethod.accessFlags.isAbstract()) {
+ defaultMethods.add(virtualMethod);
+ }
+ }
+
+ // For each of the default methods, the subclass [target] could inherit another default method
+ // with the same signature from another interface (i.e., there is a conflict). In such cases,
+ // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
+ // ClassChangeError.
+ //
+ // Example:
+ // interface I1 { default void m() {} }
+ // interface I2 { default void m() {} }
+ // class C implements I1, I2 {
+ // ... invoke-interface I1.m ... <- IncompatibleClassChangeError
+ // }
+ for (DexEncodedMethod method : defaultMethods) {
+ // Conservatively find all possible targets for this method.
+ LookupResultSuccess lookupResult =
+ appView
+ .appInfo()
+ .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
+ .lookupVirtualDispatchTargets(target, appView)
+ .asLookupResultSuccess();
+ assert lookupResult != null;
+ if (lookupResult == null) {
+ return true;
+ }
+ if (lookupResult.contains(method)) {
+ Box<Boolean> found = new Box<>(false);
+ lookupResult.forEach(
+ interfaceTarget -> {
+ if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
+ return;
+ }
+ DexClass enclosingClass = interfaceTarget.getHolder();
+ if (enclosingClass != null && enclosingClass.isInterface()) {
+ // Found a default method that is different from the one in [source], aborting.
+ found.set(true);
+ }
+ },
+ lambdaTarget -> {
+ // The merger should already have excluded lambda implemented interfaces.
+ assert false;
+ });
+ if (found.get()) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return "VerticalClassMergerPolicy";
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
index e2bfb17..e55dacb 100644
--- a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMergerPolicyExecutor.java
@@ -3,436 +3,70 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.verticalclassmerging;
-import static com.android.tools.r8.utils.AndroidApiLevelUtils.getApiReferenceLevelForMerging;
-
-import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
-import com.android.tools.r8.androidapi.ComputedApiLevel;
-import com.android.tools.r8.features.FeatureSplitBoundaryOptimizationUtils;
import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.Code;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
-import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
-import com.android.tools.r8.profile.startup.optimization.StartupBoundaryOptimizationUtils;
+import com.android.tools.r8.horizontalclassmerging.Policy;
+import com.android.tools.r8.horizontalclassmerging.PolicyExecutor;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.MainDexInfo;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.FieldSignatureEquivalence;
-import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.ObjectUtils;
-import com.android.tools.r8.utils.TraversalContinuation;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
-import java.util.HashSet;
+import com.android.tools.r8.utils.Timing;
+import java.util.Collection;
+import java.util.LinkedList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
// TODO(b/315252934): Parallelize policy execution over connected program components.
-public class VerticalClassMergerPolicyExecutor {
+public class VerticalClassMergerPolicyExecutor extends PolicyExecutor<VerticalMergeGroup> {
private final AppView<AppInfoWithLiveness> appView;
- private final InternalOptions options;
- private final MainDexInfo mainDexInfo;
+ private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
private final Set<DexProgramClass> pinnedClasses;
VerticalClassMergerPolicyExecutor(
- AppView<AppInfoWithLiveness> appView, Set<DexProgramClass> pinnedClasses) {
+ AppView<AppInfoWithLiveness> appView,
+ ImmediateProgramSubtypingInfo immediateSubtypingInfo,
+ Set<DexProgramClass> pinnedClasses) {
this.appView = appView;
- this.options = appView.options();
- this.mainDexInfo = appView.appInfo().getMainDexInfo();
+ this.immediateSubtypingInfo = immediateSubtypingInfo;
this.pinnedClasses = pinnedClasses;
}
ConnectedComponentVerticalClassMerger run(
- Set<DexProgramClass> connectedComponent,
- ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
- Set<DexProgramClass> mergeCandidates = Sets.newIdentityHashSet();
- for (DexProgramClass sourceClass : connectedComponent) {
- List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(sourceClass);
- if (subclasses.size() != 1) {
- continue;
- }
- DexProgramClass targetClass = ListUtils.first(subclasses);
- if (!isMergeCandidate(sourceClass, targetClass)) {
- continue;
- }
- if (!isStillMergeCandidate(sourceClass, targetClass)) {
- continue;
- }
- if (mergeMayLeadToIllegalAccesses(sourceClass, targetClass)
- || mergeMayLeadToNoSuchMethodError(sourceClass, targetClass)) {
- continue;
- }
- mergeCandidates.add(sourceClass);
- }
- return new ConnectedComponentVerticalClassMerger(appView, mergeCandidates);
+ Set<DexProgramClass> connectedComponent, ExecutorService executorService, Timing timing)
+ throws ExecutionException {
+ Collection<VerticalMergeGroup> groups = createInitialMergeGroups(connectedComponent);
+ Collection<Policy> policies = List.of(new VerticalClassMergerPolicy(appView, pinnedClasses));
+ groups = run(groups, policies, executorService, timing);
+ return new ConnectedComponentVerticalClassMerger(appView, groups);
}
- // Returns true if [clazz] is a merge candidate. Note that the result of the checks in this
- // method do not change in response to any class merges.
- private boolean isMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
- assert targetClass != null;
- ObjectAllocationInfoCollection allocationInfo =
- appView.appInfo().getObjectAllocationInfoCollection();
- if (allocationInfo.isInstantiatedDirectly(sourceClass)
- || allocationInfo.isInterfaceWithUnknownSubtypeHierarchy(sourceClass)
- || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
- || !appView.getKeepInfo(sourceClass).isVerticalClassMergingAllowed(options)
- || pinnedClasses.contains(sourceClass)) {
- return false;
- }
-
- assert sourceClass
- .traverseProgramMembers(
- member -> {
- assert !appView.getKeepInfo(member).isPinned(options);
- return TraversalContinuation.doContinue();
- })
- .shouldContinue();
-
- if (!FeatureSplitBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
- sourceClass, targetClass, appView)) {
- return false;
- }
- if (!StartupBoundaryOptimizationUtils.isSafeForVerticalClassMerging(
- sourceClass, targetClass, appView)) {
- return false;
- }
- if (appView.appServices().allServiceTypes().contains(sourceClass.getType())
- && appView.getKeepInfo(targetClass).isPinned(options)) {
- return false;
- }
- if (sourceClass.isAnnotation()) {
- return false;
- }
- if (!sourceClass.isInterface()
- && targetClass.isSerializable(appView)
- && !appView.appInfo().isSerializable(sourceClass.getType())) {
- // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html
- // 1.10 The Serializable Interface
- // ...
- // A Serializable class must do the following:
- // ...
- // * Have access to the no-arg constructor of its first non-serializable superclass
- return false;
- }
-
- // If there is a constructor in the target, make sure that all source constructors can be
- // inlined.
- if (!Iterables.isEmpty(targetClass.programInstanceInitializers())) {
- TraversalContinuation<?, ?> result =
- sourceClass.traverseProgramInstanceInitializers(
- method -> TraversalContinuation.breakIf(disallowInlining(method, targetClass)));
- if (result.shouldBreak()) {
- return false;
+ @SuppressWarnings("JdkObsolete")
+ private LinkedList<VerticalMergeGroup> createInitialMergeGroups(
+ Set<DexProgramClass> connectedComponent) {
+ LinkedList<VerticalMergeGroup> groups = new LinkedList<>();
+ for (DexProgramClass mergeCandidate : connectedComponent) {
+ List<DexProgramClass> subclasses = immediateSubtypingInfo.getSubclasses(mergeCandidate);
+ if (subclasses.size() == 1) {
+ groups.add(new VerticalMergeGroup(mergeCandidate, ListUtils.first(subclasses)));
}
}
- if (sourceClass.hasEnclosingMethodAttribute() || !sourceClass.getInnerClasses().isEmpty()) {
- return false;
- }
- // We abort class merging when merging across nests or from a nest to non-nest.
- // Without nest this checks null == null.
- if (ObjectUtils.notIdentical(targetClass.getNestHost(), sourceClass.getNestHost())) {
- return false;
- }
-
- // If there is an invoke-special to a default interface method and we are not merging into an
- // interface, then abort, since invoke-special to a virtual class method requires desugaring.
- if (sourceClass.isInterface() && !targetClass.isInterface()) {
- TraversalContinuation<?, ?> result =
- sourceClass.traverseProgramMethods(
- method -> {
- boolean foundInvokeSpecialToDefaultLibraryMethod =
- method.registerCodeReferencesWithResult(
- new InvokeSpecialToDefaultLibraryMethodUseRegistry(appView, method));
- return TraversalContinuation.breakIf(foundInvokeSpecialToDefaultLibraryMethod);
- });
- if (result.shouldBreak()) {
- return false;
- }
- }
-
- // Check with main dex classes to see if we are allowed to merge.
- if (!mainDexInfo.canMerge(sourceClass, targetClass, appView.getSyntheticItems())) {
- return false;
- }
-
- return true;
+ return groups;
}
- /**
- * Returns true if {@param sourceClass} is a merge candidate. Note that the result of the checks
- * in this method may change in response to class merges. Therefore, this method should always be
- * called before merging {@param sourceClass} into {@param targetClass}.
- */
- boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
- // For interface types, this is more complicated, see:
- // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5
- // We basically can't move the clinit, since it is not called when implementing classes have
- // their clinit called - except when the interface has a default method.
- if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
- || targetClass.classInitializationMayHaveSideEffects(
- appView, type -> type.isIdenticalTo(sourceClass.getType()))
- || (sourceClass.isInterface()
- && sourceClass.classInitializationMayHaveSideEffects(appView))) {
- return false;
- }
- boolean sourceCanBeSynchronizedOn =
- appView.appInfo().isLockCandidate(sourceClass)
- || sourceClass.hasStaticSynchronizedMethods();
- boolean targetCanBeSynchronizedOn =
- appView.appInfo().isLockCandidate(targetClass)
- || targetClass.hasStaticSynchronizedMethods();
- if (sourceCanBeSynchronizedOn && targetCanBeSynchronizedOn) {
- return false;
- }
- if (targetClass.hasEnclosingMethodAttribute() || !targetClass.getInnerClasses().isEmpty()) {
- return false;
- }
- if (methodResolutionMayChange(sourceClass, targetClass)) {
- return false;
- }
- // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
- // to the super class.
- if (fieldResolutionMayChange(sourceClass, targetClass)) {
- return false;
- }
- // Only merge if api reference level of source class is equal to target class. The check is
- // somewhat expensive.
- if (appView.options().apiModelingOptions().isApiCallerIdentificationEnabled()) {
- AndroidApiLevelCompute apiLevelCompute = appView.apiLevelCompute();
- ComputedApiLevel sourceApiLevel =
- getApiReferenceLevelForMerging(apiLevelCompute, sourceClass);
- ComputedApiLevel targetApiLevel =
- getApiReferenceLevelForMerging(apiLevelCompute, targetClass);
- if (!sourceApiLevel.equals(targetApiLevel)) {
- return false;
- }
- }
- return true;
+ @Override
+ protected LinkedList<VerticalMergeGroup> apply(
+ Policy policy, LinkedList<VerticalMergeGroup> linkedGroups, ExecutorService executorService)
+ throws ExecutionException {
+ assert policy.isVerticalClassMergerPolicy();
+ return apply(policy.asVerticalClassMergerPolicy(), linkedGroups);
}
- private boolean disallowInlining(ProgramMethod method, DexProgramClass context) {
- if (!appView.options().inlinerOptions().enableInlining) {
- return true;
- }
- Code code = method.getDefinition().getCode();
- if (code.isCfCode()) {
- CfCode cfCode = code.asCfCode();
- ConstraintWithTarget constraint =
- cfCode.computeInliningConstraint(appView, appView.graphLens(), method);
- if (constraint.isNever()) {
- return true;
- }
- // Constructors can have references beyond the root main dex classes. This can increase the
- // size of the main dex dependent classes and we should bail out.
- if (mainDexInfo.disallowInliningIntoContext(appView, context, method)) {
- return true;
- }
- return false;
- }
- if (code.isDefaultInstanceInitializerCode()) {
- return false;
- }
- return true;
- }
-
- private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
- if (source.getType().isIdenticalTo(target.getSuperType())) {
- // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
- // Target implements an interface that declares a static final field f, this should yield an
- // IncompatibleClassChangeError.
- // TODO(christofferqa): In the following we only check if a static field from an interface
- // shadows an instance field from [source]. We could actually check if there is an iget/iput
- // instruction whose resolution would be affected by the merge. The situation where a static
- // field shadows an instance field is probably not widespread in practice, though.
- FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
- Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
- for (DexType interfaceType : target.getInterfaces()) {
- DexClass clazz = appView.definitionFor(interfaceType);
- for (DexEncodedField staticField : clazz.staticFields()) {
- staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.getReference()));
- }
- }
- for (DexEncodedField instanceField : source.instanceFields()) {
- if (staticFieldsInInterfacesOfTarget.contains(
- equivalence.wrap(instanceField.getReference()))) {
- // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
- // interface would now hit an instance field from [source], so that an IncompatibleClass-
- // ChangeError would no longer be thrown. Abort merge.
- return true;
- }
- }
- }
- return false;
- }
-
- private boolean mergeMayLeadToIllegalAccesses(DexProgramClass source, DexProgramClass target) {
- if (source.isSamePackage(target)) {
- // When merging two classes from the same package, we only need to make sure that [source]
- // does not get less visible, since that could make a valid access to [source] from another
- // package illegal after [source] has been merged into [target].
- assert source.getAccessFlags().isPackagePrivateOrPublic();
- assert target.getAccessFlags().isPackagePrivateOrPublic();
- // TODO(b/287891322): Allow merging if `source` is only accessed from inside its own package.
- return source.getAccessFlags().isPublic() && target.getAccessFlags().isPackagePrivate();
- }
-
- // Check that all accesses to [source] and its members from inside the current package of
- // [source] will continue to work. This is guaranteed if [target] is public and all members of
- // [source] are either private or public.
- //
- // (Deliberately not checking all accesses to [source] since that would be expensive.)
- if (!target.isPublic()) {
- return true;
- }
- for (DexType sourceInterface : source.getInterfaces()) {
- DexClass sourceInterfaceClass = appView.definitionFor(sourceInterface);
- if (sourceInterfaceClass != null && !sourceInterfaceClass.isPublic()) {
- return true;
- }
- }
- for (DexEncodedField field : source.fields()) {
- if (!(field.isPublic() || field.isPrivate())) {
- return true;
- }
- }
- for (DexEncodedMethod method : source.methods()) {
- if (!(method.isPublic() || method.isPrivate())) {
- return true;
- }
- // Check if the target is overriding and narrowing the access.
- if (method.isPublic()) {
- DexEncodedMethod targetOverride = target.lookupVirtualMethod(method.getReference());
- if (targetOverride != null && !targetOverride.isPublic()) {
- return true;
- }
- }
- }
- // Check that all accesses from [source] to classes or members from the current package of
- // [source] will continue to work. This is guaranteed if the methods of [source] do not access
- // any private or protected classes or members from the current package of [source].
- TraversalContinuation<?, ?> result =
- source.traverseProgramMethods(
- method -> {
- boolean foundIllegalAccess =
- method.registerCodeReferencesWithResult(
- new IllegalAccessDetector(appView, method));
- if (foundIllegalAccess) {
- return TraversalContinuation.doBreak();
- }
- return TraversalContinuation.doContinue();
- },
- DexEncodedMethod::hasCode);
- return result.shouldBreak();
- }
-
- // TODO: maybe skip this check if target does not implement any interfaces (directly or
- // indirectly)?
- private boolean mergeMayLeadToNoSuchMethodError(DexProgramClass source, DexProgramClass target) {
- // This only returns true when an invoke-super instruction is found that targets a default
- // interface method.
- if (!options.canUseDefaultAndStaticInterfaceMethods()) {
- return false;
- }
- // This problem may only arise when merging (non-interface) classes into classes.
- if (source.isInterface() || target.isInterface()) {
- return false;
- }
- return target
- .traverseProgramMethods(
- method -> {
- MergeMayLeadToNoSuchMethodErrorUseRegistry registry =
- new MergeMayLeadToNoSuchMethodErrorUseRegistry(appView, method, source);
- method.registerCodeReferencesWithResult(registry);
- return TraversalContinuation.breakIf(registry.mayLeadToNoSuchMethodError());
- },
- DexEncodedMethod::hasCode)
- .shouldBreak();
- }
-
- private boolean methodResolutionMayChange(DexProgramClass source, DexProgramClass target) {
- for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
- DexEncodedMethod directTargetMethod =
- target.lookupDirectMethod(virtualSourceMethod.getReference());
- if (directTargetMethod != null) {
- // A private method shadows a virtual method. This situation is rare, since it is not
- // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
- // possible to rename the private method in the subclass, and then move the virtual method
- // to the subclass without changing its name.)
- return true;
- }
- }
-
- // When merging an interface into a class, all instructions on the form "invoke-interface
- // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
- // transformation could hide IncompatibleClassChangeErrors.
- if (source.isInterface() && !target.isInterface()) {
- List<DexEncodedMethod> defaultMethods = new ArrayList<>();
- for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
- if (!virtualMethod.accessFlags.isAbstract()) {
- defaultMethods.add(virtualMethod);
- }
- }
-
- // For each of the default methods, the subclass [target] could inherit another default method
- // with the same signature from another interface (i.e., there is a conflict). In such cases,
- // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
- // ClassChangeError.
- //
- // Example:
- // interface I1 { default void m() {} }
- // interface I2 { default void m() {} }
- // class C implements I1, I2 {
- // ... invoke-interface I1.m ... <- IncompatibleClassChangeError
- // }
- for (DexEncodedMethod method : defaultMethods) {
- // Conservatively find all possible targets for this method.
- LookupResultSuccess lookupResult =
- appView
- .appInfo()
- .resolveMethodOnInterfaceLegacy(method.getHolderType(), method.getReference())
- .lookupVirtualDispatchTargets(target, appView)
- .asLookupResultSuccess();
- assert lookupResult != null;
- if (lookupResult == null) {
- return true;
- }
- if (lookupResult.contains(method)) {
- Box<Boolean> found = new Box<>(false);
- lookupResult.forEach(
- interfaceTarget -> {
- if (ObjectUtils.identical(interfaceTarget.getDefinition(), method)) {
- return;
- }
- DexClass enclosingClass = interfaceTarget.getHolder();
- if (enclosingClass != null && enclosingClass.isInterface()) {
- // Found a default method that is different from the one in [source], aborting.
- found.set(true);
- }
- },
- lambdaTarget -> {
- // The merger should already have excluded lambda implemented interfaces.
- assert false;
- });
- if (found.get()) {
- return true;
- }
- }
- }
- }
- return false;
+ private LinkedList<VerticalMergeGroup> apply(
+ VerticalClassMergerPolicy policy, LinkedList<VerticalMergeGroup> linkedGroups) {
+ linkedGroups.removeIf(group -> !policy.canMerge(group));
+ return linkedGroups;
}
}
diff --git a/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java
new file mode 100644
index 0000000..28b2a82
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/verticalclassmerging/VerticalMergeGroup.java
@@ -0,0 +1,31 @@
+// 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.verticalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroupBase;
+
+public class VerticalMergeGroup extends MergeGroupBase {
+
+ private final DexProgramClass source;
+ private final DexProgramClass target;
+
+ VerticalMergeGroup(DexProgramClass source, DexProgramClass target) {
+ this.source = source;
+ this.target = target;
+ }
+
+ public DexProgramClass getSource() {
+ return source;
+ }
+
+ public DexProgramClass getTarget() {
+ return target;
+ }
+
+ @Override
+ public int size() {
+ return 2;
+ }
+}