blob: 080daa49a42c9d5217cc37d5625039ac8904b000 [file] [log] [blame]
// Copyright (c) 2017, 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.graph.DexClassAndMethod.asProgramMethodOrNull;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import com.android.tools.r8.classmerging.ClassMergerMode;
import com.android.tools.r8.classmerging.SyntheticArgumentClass;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.PrunedItems;
import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
import com.android.tools.r8.profile.art.ArtProfile;
import com.android.tools.r8.profile.art.ArtProfileCompletenessChecker;
import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.KeepInfoCollection;
import com.android.tools.r8.utils.ConsumerUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
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.Iterables;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
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;
import java.util.concurrent.ExecutorService;
/**
* Merges Supertypes with a single implementation into their single subtype.
*
* <p>A common use-case for this is to merge an interface into its single implementation.
*
* <p>The class merger only fixes the structure of the graph but leaves the actual instructions
* untouched. Fixup of instructions is deferred via a {@link GraphLens} to the IR building phase.
*/
public class VerticalClassMerger {
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory dexItemFactory;
private final ClassMergerMode mode;
private final InternalOptions options;
public VerticalClassMerger(AppView<AppInfoWithLiveness> appView, ClassMergerMode mode) {
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.mode = mode;
this.options = appView.options();
}
public static VerticalClassMerger createForInitialClassMerging(
AppView<AppInfoWithLiveness> appView) {
return new VerticalClassMerger(appView, ClassMergerMode.INITIAL);
}
public static VerticalClassMerger createForFinalClassMerging(
AppView<AppInfoWithLiveness> appView) {
return new VerticalClassMerger(appView, ClassMergerMode.FINAL);
}
// Returns a set of types that must not be merged into other types.
private Set<DexProgramClass> getPinnedClasses() {
Set<DexProgramClass> pinnedClasses = Sets.newIdentityHashSet();
// For all pinned fields, also pin the type of the field (because changing the type of the field
// implicitly changes the signature of the field). Similarly, for all pinned methods, also pin
// the return type and the parameter types of the method.
// TODO(b/156715504): Compute referenced-by-pinned in the keep info objects.
List<DexReference> pinnedReferences = new ArrayList<>();
KeepInfoCollection keepInfo = appView.getKeepInfo();
keepInfo.forEachPinnedType(pinnedReferences::add, options);
keepInfo.forEachPinnedMethod(pinnedReferences::add, options);
keepInfo.forEachPinnedField(pinnedReferences::add, options);
extractPinnedClasses(pinnedReferences, pinnedClasses);
for (DexProgramClass clazz : appView.appInfo().classes()) {
if (Iterables.any(clazz.methods(), method -> method.getAccessFlags().isNative())) {
markClassAsPinned(clazz, pinnedClasses);
}
}
// It is valid to have an invoke-direct instruction in a default interface method that targets
// another default method in the same interface (see InterfaceMethodDesugaringTests.testInvoke-
// SpecialToDefaultMethod). However, in a class, that would lead to a verification error.
// Therefore, we disallow merging such interfaces into their subtypes.
for (DexMethod signature : appView.appInfo().getVirtualMethodsTargetedByInvokeDirect()) {
markTypeAsPinned(signature.getHolderType(), pinnedClasses);
}
// The set of targets that must remain for proper resolution error cases should not be merged.
// TODO(b/192821424): Can be removed if handled.
extractPinnedClasses(appView.appInfo().getFailedMethodResolutionTargets(), pinnedClasses);
// The ART profiles may contain method rules that do not exist in the app. These method may
// refer to classes that will be vertically merged into their unique subtype, but the vertical
// class merger lens will not contain any mappings for the missing methods in the ART profiles.
// Therefore, trying to perform a lens lookup on these methods will fail.
for (ArtProfile artProfile : appView.getArtProfileCollection()) {
artProfile.forEachRule(
ConsumerUtils.emptyThrowingConsumer(),
methodRule -> {
DexMethod method = methodRule.getMethod();
if (method.getHolderType().isArrayType()) {
return;
}
DexClass holder =
appView.appInfo().definitionForWithoutExistenceAssert(method.getHolderType());
if (method.lookupOnClass(holder) == null) {
extractPinnedClasses(methodRule.getMethod(), pinnedClasses);
}
});
}
return pinnedClasses;
}
private <T extends DexReference> void extractPinnedClasses(
Iterable<T> items, Set<DexProgramClass> pinnedClasses) {
for (DexReference item : items) {
extractPinnedClasses(item, pinnedClasses);
}
}
private void extractPinnedClasses(DexReference reference, Set<DexProgramClass> pinnedClasses) {
markTypeAsPinned(reference.getContextType(), pinnedClasses);
reference.accept(
ConsumerUtils.emptyConsumer(),
field -> {
// Pin the type of the field.
markTypeAsPinned(field.getType(), pinnedClasses);
},
method -> {
// Pin the return type and the parameter types of the method. If we were to merge any of
// these types into their sub classes, then we would implicitly change the signature of
// this method.
for (DexType type : method.getReferencedTypes()) {
markTypeAsPinned(type, pinnedClasses);
}
});
}
private void markTypeAsPinned(DexType type, Set<DexProgramClass> pinnedClasses) {
DexType baseType = type.toBaseType(dexItemFactory);
if (!baseType.isClassType()) {
return;
}
DexProgramClass clazz =
asProgramClassOrNull(appView.appInfo().definitionForWithoutExistenceAssert(baseType));
if (clazz != null && !appView.getKeepInfo(clazz).isPinned(options)) {
// We check for the case where the type is pinned according to its keep info, so we only need
// to add it here if it is not the case.
markClassAsPinned(clazz, pinnedClasses);
}
}
private void markClassAsPinned(DexProgramClass clazz, Set<DexProgramClass> pinnedClasses) {
pinnedClasses.add(clazz);
}
public void runIfNecessary(ExecutorService executorService, Timing timing)
throws ExecutionException {
timing.begin("VerticalClassMerger");
if (shouldRun()) {
run(executorService, timing);
} else {
appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty());
}
assert appView.hasVerticallyMergedClasses();
assert ArtProfileCompletenessChecker.verify(appView);
timing.end();
}
private boolean shouldRun() {
return options.getVerticalClassMergerOptions().isEnabled(mode)
&& !appView.hasCfByteCodePassThroughMethods();
}
private void run(ExecutorService executorService, Timing timing) throws ExecutionException {
appView.appInfo().getMethodAccessInfoCollection().verifyNoNonResolving(appView);
timing.begin("Setup");
ImmediateProgramSubtypingInfo immediateSubtypingInfo =
ImmediateProgramSubtypingInfo.create(appView);
// Compute the disjoint class hierarchies for parallel processing.
List<Set<DexProgramClass>> connectedComponents =
new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
.computeStronglyConnectedComponents();
// Remove singleton class hierarchies as they are not subject to vertical class merging.
connectedComponents.removeIf(connectedComponent -> connectedComponent.size() == 1);
timing.end();
// Apply class merging concurrently in disjoint class hierarchies.
VerticalClassMergerResult verticalClassMergerResult =
mergeClassesInConnectedComponents(
connectedComponents, immediateSubtypingInfo, executorService, timing);
appView.setVerticallyMergedClasses(verticalClassMergerResult.getVerticallyMergedClasses());
if (verticalClassMergerResult.isEmpty()) {
return;
}
ProfileCollectionAdditions profileCollectionAdditions =
ProfileCollectionAdditions.create(appView);
VerticalClassMergerGraphLens lens =
runFixup(profileCollectionAdditions, verticalClassMergerResult, executorService, timing);
updateKeepInfoForMergedClasses(verticalClassMergerResult);
assert verifyGraphLens(lens, verticalClassMergerResult);
updateArtProfiles(profileCollectionAdditions, lens, verticalClassMergerResult);
appView.rewriteWithLens(lens, executorService, timing);
updateKeepInfoForSynthesizedBridges(verticalClassMergerResult);
appView.notifyOptimizationFinishedForTesting();
}
private VerticalClassMergerResult mergeClassesInConnectedComponents(
List<Set<DexProgramClass>> connectedComponents,
ImmediateProgramSubtypingInfo immediateSubtypingInfo,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
getConnectedComponentMergers(
connectedComponents, immediateSubtypingInfo, executorService, timing);
return applyConnectedComponentMergers(connectedComponentMergers, executorService, timing);
}
private Collection<ConnectedComponentVerticalClassMerger> getConnectedComponentMergers(
List<Set<DexProgramClass>> connectedComponents,
ImmediateProgramSubtypingInfo immediateSubtypingInfo,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
TimingMerger merger = timing.beginMerger("Compute classes to merge", executorService);
List<ConnectedComponentVerticalClassMerger> connectedComponentMergers =
new ArrayList<>(connectedComponents.size());
Set<DexProgramClass> pinnedClasses = getPinnedClasses();
Collection<Timing> timings =
ThreadUtils.processItemsWithResults(
connectedComponents,
connectedComponent -> {
Timing threadTiming = Timing.create("Compute classes to merge in component", options);
ConnectedComponentVerticalClassMerger connectedComponentMerger =
new VerticalClassMergerPolicyExecutor(
appView, immediateSubtypingInfo, pinnedClasses)
.run(connectedComponent, executorService, threadTiming);
if (!connectedComponentMerger.isEmpty()) {
synchronized (connectedComponentMergers) {
connectedComponentMergers.add(connectedComponentMerger);
}
}
threadTiming.end();
return threadTiming;
},
appView.options().getThreadingModule(),
executorService);
merger.add(timings);
merger.end();
return connectedComponentMergers;
}
private VerticalClassMergerResult applyConnectedComponentMergers(
Collection<ConnectedComponentVerticalClassMerger> connectedComponentMergers,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
TimingMerger merger = timing.beginMerger("Merge classes", executorService);
VerticalClassMergerResult.Builder verticalClassMergerResult =
VerticalClassMergerResult.builder(appView);
Collection<Timing> timings =
ThreadUtils.processItemsWithResults(
connectedComponentMergers,
connectedComponentMerger -> {
Timing threadTiming = Timing.create("Merge classes in component", options);
VerticalClassMergerResult.Builder verticalClassMergerComponentResult =
connectedComponentMerger.run();
verticalClassMergerResult.merge(verticalClassMergerComponentResult);
threadTiming.end();
return threadTiming;
},
appView.options().getThreadingModule(),
executorService);
merger.add(timings);
merger.end();
return verticalClassMergerResult.build();
}
private VerticalClassMergerGraphLens runFixup(
ProfileCollectionAdditions profileCollectionAdditions,
VerticalClassMergerResult verticalClassMergerResult,
ExecutorService executorService,
Timing timing)
throws ExecutionException {
DexProgramClass deterministicContext =
appView
.definitionFor(
ListUtils.first(
ListUtils.sort(
verticalClassMergerResult.getVerticallyMergedClasses().getTargets(),
Comparator.naturalOrder())))
.asProgramClass();
SyntheticArgumentClass syntheticArgumentClass =
new SyntheticArgumentClass.Builder(appView).build(deterministicContext);
VerticalClassMergerGraphLens lens =
new VerticalClassMergerTreeFixer(
appView,
profileCollectionAdditions,
syntheticArgumentClass,
verticalClassMergerResult)
.run(executorService, timing);
return lens;
}
private void updateArtProfiles(
ProfileCollectionAdditions profileCollectionAdditions,
VerticalClassMergerGraphLens verticalClassMergerLens,
VerticalClassMergerResult verticalClassMergerResult) {
// Include bridges in art profiles.
if (!profileCollectionAdditions.isNop()) {
List<SynthesizedBridgeCode> synthesizedBridges =
verticalClassMergerResult.getSynthesizedBridges();
for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
profileCollectionAdditions.applyIfContextIsInProfile(
verticalClassMergerLens.getPreviousMethodSignature(synthesizedBridge.getMethod()),
additionsBuilder -> additionsBuilder.addRule(synthesizedBridge.getMethod()));
}
}
profileCollectionAdditions.commit(appView);
}
private void updateKeepInfoForMergedClasses(VerticalClassMergerResult verticalClassMergerResult) {
KeepInfoCollection keepInfo = appView.getKeepInfo();
keepInfo.mutate(
mutator -> {
VerticallyMergedClasses verticallyMergedClasses =
verticalClassMergerResult.getVerticallyMergedClasses();
mutator.removeKeepInfoForMergedClasses(
PrunedItems.builder()
.setRemovedClasses(verticallyMergedClasses.getSources())
.build());
});
}
private void updateKeepInfoForSynthesizedBridges(
VerticalClassMergerResult verticalClassMergerResult) {
// Copy keep info to newly synthesized methods.
KeepInfoCollection keepInfo = appView.getKeepInfo();
keepInfo.mutate(
mutator -> {
List<SynthesizedBridgeCode> synthesizedBridges =
verticalClassMergerResult.getSynthesizedBridges();
for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
ProgramMethod bridge =
asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getMethod()));
ProgramMethod target =
asProgramMethodOrNull(appView.definitionFor(synthesizedBridge.getTarget()));
if (bridge != null && target != null) {
mutator.joinMethod(bridge, info -> info.merge(appView.getKeepInfo(target).joiner()));
continue;
}
assert false;
}
});
}
private boolean verifyGraphLens(
VerticalClassMergerGraphLens graphLens, VerticalClassMergerResult verticalClassMergerResult) {
// Note that the method assertReferencesNotModified() relies on getRenamedFieldSignature() and
// getRenamedMethodSignature() instead of lookupField() and lookupMethod(). This is important
// for this check to succeed, since it is not guaranteed that calling lookupMethod() with a
// pinned method will return the method itself.
//
// Consider the following example.
//
// class A {
// public void method() {}
// }
// class B extends A {
// @Override
// public void method() {}
// }
// class C extends B {
// @Override
// public void method() {}
// }
//
// If A.method() is pinned, then A cannot be merged into B, but B can still be merged into C.
// Now, if there is an invoke-super instruction in C that hits B.method(), then this needs to
// be rewritten into an invoke-direct instruction. In particular, there could be an instruction
// `invoke-super A.method` in C. This would hit B.method(). Therefore, the graph lens records
// that `invoke-super A.method` instructions, which are in one of the methods from C, needs to
// be rewritten to `invoke-direct C.method$B`. This is valid even though A.method() is actually
// pinned, because this rewriting does not affect A.method() in any way.
assert graphLens.assertPinnedNotModified(appView);
VerticallyMergedClasses mergedClasses = verticalClassMergerResult.getVerticallyMergedClasses();
for (DexProgramClass clazz : appView.appInfo().classes()) {
for (DexEncodedMethod encodedMethod : clazz.methods()) {
DexMethod method = encodedMethod.getReference();
DexMethod originalMethod = graphLens.getOriginalMethodSignature(method);
DexMethod renamedMethod = graphLens.getRenamedMethodSignature(originalMethod);
// Must be able to map back and forth.
if (encodedMethod.hasCode() && encodedMethod.getCode() instanceof SynthesizedBridgeCode) {
// For virtual methods, the vertical class merger creates two methods in the sub class
// in order to deal with invoke-super instructions (one that is private and one that is
// virtual). Therefore, it is not possible to go back and forth. Instead, we check that
// the two methods map back to the same original method, and that the original method
// can be mapped to the implementation method.
DexMethod implementationMethod =
((SynthesizedBridgeCode) encodedMethod.getCode()).getTarget();
DexMethod originalImplementationMethod =
graphLens.getOriginalMethodSignature(implementationMethod);
assert originalMethod.isIdenticalTo(originalImplementationMethod);
assert implementationMethod.isIdenticalTo(renamedMethod);
} else {
assert method.isIdenticalTo(renamedMethod);
}
// Verify that all types are up-to-date. After vertical class merging, there should be no
// more references to types that have been merged into another type.
assert Streams.stream(method.getReferencedBaseTypes(dexItemFactory))
.noneMatch(mergedClasses::hasBeenMergedIntoSubtype);
}
}
return true;
}
}