blob: f8faefdba134e8ed1b9023a846267c4dce292aff [file] [log] [blame]
// Copyright (c) 2021, 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.policies;
import static com.android.tools.r8.graph.DexClassAndMethod.asProgramMethodOrNull;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.utils.MapUtils.ignoreKey;
import com.android.tools.r8.code.CfOrDexInstruction;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexCallSite;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.MethodResolutionResult;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.UseRegistry;
import com.android.tools.r8.horizontalclassmerging.MergeGroup;
import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
import com.android.tools.r8.ir.desugar.LambdaDescriptor;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
import com.android.tools.r8.utils.SetUtils;
import com.android.tools.r8.utils.collections.ProgramMethodSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* Disallows merging of classes when the merging could introduce class initialization deadlocks.
*
* <p>Example: In the below example, if thread t0 triggers the class initialization of A, and thread
* t1 triggers the class initialization of C, then the program will never deadlock. However, if
* classes B and C are merged, then the program may all of a sudden deadlock, since thread t0 may
* hold the lock for A and wait for BC's lock, meanwhile thread t1 holds the lock for BC while
* waiting for A's lock.
*
* <pre>
* class A {
* static {
* new B();
* }
* }
* class B extends A {}
* class C extends A {}
* </pre>
*
* <p>To identify the above situation, we perform a tracing from {@code A.<clinit>} to check if
* there is an execution path that triggers the class initialization of B or C. In that case, the
* reached subclass is ineligible for class merging.
*
* <p>Example: In the below example, if thread t0 triggers the class initialization of A, and thread
* t1 triggers the class initialization of B, then the program will never deadlock. However, if
* classes B and C are merged, then the program may all of a sudden deadlock, since thread 0 may
* hold the lock for A and wait for BC's lock, meanwhile thread t1 holds the lock for BC while
* waiting for A's lock.
*
* <pre>
* class A {
* static {
* new C();
* }
* }
* class B {
* static {
* new A();
* }
* }
* class C {}
* </pre>
*
* <p>To identify the above situation, we perform a tracing for each {@code <clinit>} in the merge
* group. If we find an execution path from the class initializer of one class in the merge group to
* the class initializer of another class in the merge group, then after merging there is a cycle in
* the class initialization that could lead to a deadlock.
*/
public class NoClassInitializerCycles extends MultiClassPolicyWithPreprocessing<Void> {
final AppView<AppInfoWithLiveness> appView;
// Mapping from each merge candidate to its merge group.
final Map<DexProgramClass, MergeGroup> allGroups = new IdentityHashMap<>();
public NoClassInitializerCycles(AppView<AppInfoWithLiveness> appView) {
this.appView = appView;
}
@Override
public Collection<MergeGroup> apply(MergeGroup group, Void nothing) {
Tracer tracer = new Tracer(group);
removeClassesWithPossibleClassInitializerDeadlock(group, tracer);
List<MergeGroup> newGroups = new LinkedList<>();
for (DexProgramClass clazz : group) {
MergeGroup newGroup = getOrCreateGroupFor(clazz, newGroups, tracer);
if (newGroup != null) {
newGroup.add(clazz);
} else {
// Ineligible for merging.
}
}
return removeTrivialGroups(newGroups);
}
private MergeGroup getOrCreateGroupFor(
DexProgramClass clazz, List<MergeGroup> groups, Tracer tracer) {
assert !tracer.hasPossibleClassInitializerDeadlock(clazz);
ProgramMethod classInitializer = clazz.getProgramClassInitializer();
if (classInitializer != null) {
assert tracer.verifySeenSetIsEmpty();
assert tracer.verifyWorklistIsEmpty();
tracer.setTracingRoot(clazz);
tracer.enqueueMethod(classInitializer);
tracer.trace();
if (tracer.hasPossibleClassInitializerDeadlock(clazz)) {
// Ineligible for merging.
return null;
}
}
for (MergeGroup group : groups) {
if (canMerge(clazz, group, tracer)) {
return group;
}
}
MergeGroup newGroup = new MergeGroup();
groups.add(newGroup);
return newGroup;
}
private boolean canMerge(DexProgramClass clazz, MergeGroup group, Tracer tracer) {
for (DexProgramClass member : group) {
// Check that the class initialization of the given class cannot reach the class initializer
// of the current group member.
if (tracer.isClassInitializedByClassInitializationOf(member, clazz)) {
return false;
}
// Check that the class initialization of the current group member cannot reach the class
// initializer of the given class.
if (tracer.isClassInitializedByClassInitializationOf(clazz, member)) {
return false;
}
}
return true;
}
/**
* Runs the tracer from the parent class initializers, using the entire group as tracing context.
* If the class initializer of one of the classes in the merge group is reached, then that class
* is not eligible for merging.
*/
private void removeClassesWithPossibleClassInitializerDeadlock(MergeGroup group, Tracer tracer) {
tracer.setTracingRoots(group);
tracer.enqueueParentClassInitializers(group);
tracer.trace();
group.removeIf(tracer::hasPossibleClassInitializerDeadlock);
}
@Override
public void clear() {
allGroups.clear();
}
@Override
public String getName() {
return "NoClassInitializerCycles";
}
@Override
public Void preprocess(Collection<MergeGroup> groups) {
for (MergeGroup group : groups) {
for (DexProgramClass clazz : group) {
allGroups.put(clazz, group);
}
}
return null;
}
@Override
public boolean shouldSkipPolicy() {
HorizontalClassMergerOptions options = appView.options().horizontalClassMergerOptions();
return !options.isClassInitializerDeadlockDetectionEnabled();
}
private class Tracer {
final Set<DexProgramClass> group;
private final Set<DexProgramClass> seenClassInitializers = Sets.newIdentityHashSet();
private final ProgramMethodSet seenMethods = ProgramMethodSet.create();
private final Deque<ProgramMethod> worklist = new ArrayDeque<>();
// Mapping from each merge grop member to the set of merge group members whose class
// initializers may trigger the class initialization of the group member.
private final Map<DexProgramClass, Set<DexProgramClass>> classInitializerReachableFromClasses =
new IdentityHashMap<>();
// The current tracing roots (either the entire merge group or one of the classes in the merge
// group).
private Collection<DexProgramClass> tracingRoots;
Tracer(MergeGroup group) {
this.group = SetUtils.newIdentityHashSet(group);
}
void clearSeen() {
seenClassInitializers.clear();
seenMethods.clear();
}
boolean markClassInitializerAsSeen(DexProgramClass clazz) {
return seenClassInitializers.add(clazz);
}
boolean enqueueMethod(ProgramMethod method) {
if (seenMethods.add(method)) {
worklist.add(method);
return true;
}
return false;
}
void enqueueParentClassInitializers(MergeGroup group) {
DexProgramClass member = group.iterator().next();
enqueueParentClassInitializers(member);
}
void enqueueParentClassInitializers(DexProgramClass clazz) {
DexProgramClass superClass =
asProgramClassOrNull(appView.definitionFor(clazz.getSuperType()));
if (superClass == null) {
return;
}
ProgramMethod classInitializer = superClass.getProgramClassInitializer();
if (classInitializer != null) {
enqueueMethod(classInitializer);
}
enqueueParentClassInitializers(superClass);
}
void recordClassInitializerReachableFromTracingRoots(DexProgramClass clazz) {
assert group.contains(clazz);
classInitializerReachableFromClasses
.computeIfAbsent(clazz, ignoreKey(Sets::newIdentityHashSet))
.addAll(tracingRoots);
}
void recordTracingRootsIneligibleForClassMerging() {
for (DexProgramClass tracingRoot : tracingRoots) {
classInitializerReachableFromClasses
.computeIfAbsent(tracingRoot, ignoreKey(Sets::newIdentityHashSet))
.add(tracingRoot);
}
}
boolean hasSingleTracingRoot(DexProgramClass clazz) {
return tracingRoots.size() == 1 && tracingRoots.contains(clazz);
}
boolean hasPossibleClassInitializerDeadlock(DexProgramClass clazz) {
return classInitializerReachableFromClasses
.getOrDefault(clazz, Collections.emptySet())
.contains(clazz);
}
boolean isClassInitializedByClassInitializationOf(
DexProgramClass classToBeInitialized, DexProgramClass classBeingInitialized) {
return classInitializerReachableFromClasses
.getOrDefault(classToBeInitialized, Collections.emptySet())
.contains(classBeingInitialized);
}
void setTracingRoot(DexProgramClass tracingRoot) {
setTracingRoots(ImmutableList.of(tracingRoot));
}
void setTracingRoots(Collection<DexProgramClass> tracingRoots) {
this.tracingRoots = tracingRoots;
}
void trace() {
// TODO(b/205611444): Avoid redundant tracing of the same methods.
while (!worklist.isEmpty()) {
ProgramMethod method = worklist.removeLast();
method.registerCodeReferences(new TracerUseRegistry(method));
}
clearSeen();
}
boolean verifySeenSetIsEmpty() {
assert seenClassInitializers.isEmpty();
assert seenMethods.isEmpty();
return true;
}
boolean verifyWorklistIsEmpty() {
assert worklist.isEmpty();
return true;
}
class TracerUseRegistry extends UseRegistry<ProgramMethod> {
TracerUseRegistry(ProgramMethod context) {
super(appView, context);
}
private void fail() {
// Ensures that hasPossibleClassInitializerDeadlock() returns true for each tracing root.
recordTracingRootsIneligibleForClassMerging();
doBreak();
}
private void triggerClassInitializerIfNotAlreadyTriggeredInContext(DexType type) {
DexProgramClass clazz = type.asProgramClass(appView);
if (clazz != null) {
triggerClassInitializerIfNotAlreadyTriggeredInContext(clazz);
}
}
private void triggerClassInitializerIfNotAlreadyTriggeredInContext(DexProgramClass clazz) {
if (!isClassAlreadyInitializedInCurrentContext(clazz)) {
triggerClassInitializer(clazz);
}
}
private boolean isClassAlreadyInitializedInCurrentContext(DexProgramClass clazz) {
// TODO(b/205611444): There is only a risk of a deadlock if the execution path comes from
// outside the merge group. We could address this by updating this check.
return appView.appInfo().isSubtype(getContext().getHolder(), clazz);
}
private void triggerClassInitializer(DexType type) {
DexProgramClass clazz = type.asProgramClass(appView);
if (clazz != null) {
triggerClassInitializer(clazz);
}
}
// TODO(b/205611444): This needs to account for pending merging. If the given class is in a
// merge group, then this should trigger the class initializers of all of the classes in the
// merge group.
private void triggerClassInitializer(DexProgramClass clazz) {
if (!markClassInitializerAsSeen(clazz)) {
return;
}
if (group.contains(clazz)) {
if (hasSingleTracingRoot(clazz)) {
// We found an execution path from the class initializer of the given class back to its
// own class initializer. Therefore this class is not eligible for merging.
fail();
} else {
// Record that this class initializer is reachable from the tracing roots.
recordClassInitializerReachableFromTracingRoots(clazz);
}
}
ProgramMethod classInitializer = clazz.getProgramClassInitializer();
if (classInitializer != null) {
if (!enqueueMethod(classInitializer)) {
// This class initializer is already seen in the current context, thus all of the parent
// class initializers are also seen in the current context.
return;
}
}
triggerClassInitializer(clazz.getSuperType());
}
@Override
public void registerInitClass(DexType type) {
triggerClassInitializerIfNotAlreadyTriggeredInContext(type);
}
@Override
public void registerInvokeDirect(DexMethod method) {
DexMethod rewrittenMethod =
appView.graphLens().lookupInvokeDirect(method, getContext()).getReference();
MethodResolutionResult resolutionResult =
appView.appInfo().resolveMethodOnClass(rewrittenMethod);
if (resolutionResult.isSingleResolution()
&& resolutionResult.getResolvedHolder().isProgramClass()) {
enqueueMethod(resolutionResult.getResolvedProgramMethod());
}
}
@Override
public void registerInvokeInterface(DexMethod method) {
fail();
}
@Override
public void registerInvokeStatic(DexMethod method) {
DexMethod rewrittenMethod =
appView.graphLens().lookupInvokeStatic(method, getContext()).getReference();
ProgramMethod resolvedMethod =
appView
.appInfo()
.unsafeResolveMethodDueToDexFormat(rewrittenMethod)
.getResolvedProgramMethod();
if (resolvedMethod != null) {
triggerClassInitializerIfNotAlreadyTriggeredInContext(resolvedMethod.getHolder());
enqueueMethod(resolvedMethod);
}
}
@Override
public void registerInvokeSuper(DexMethod method) {
DexMethod rewrittenMethod =
appView.graphLens().lookupInvokeSuper(method, getContext()).getReference();
ProgramMethod superTarget =
asProgramMethodOrNull(
appView.appInfo().lookupSuperTarget(rewrittenMethod, getContext()));
if (superTarget != null) {
enqueueMethod(superTarget);
}
}
@Override
public void registerInvokeVirtual(DexMethod method) {
fail();
}
@Override
public void registerNewInstance(DexType type) {
DexType rewrittenType = appView.graphLens().lookupType(type);
triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenType);
}
@Override
public void registerStaticFieldRead(DexField field) {
DexField rewrittenField = appView.graphLens().lookupField(field);
triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
}
@Override
public void registerStaticFieldWrite(DexField field) {
DexField rewrittenField = appView.graphLens().lookupField(field);
triggerClassInitializerIfNotAlreadyTriggeredInContext(rewrittenField.getHolderType());
}
@Override
public void registerTypeReference(DexType type) {
// Intentionally empty, new-array etc. does not trigger any class initialization.
}
@Override
public void registerCallSite(DexCallSite callSite) {
LambdaDescriptor descriptor =
LambdaDescriptor.tryInfer(callSite, appView.appInfo(), getContext());
if (descriptor != null) {
// Use of lambda metafactory does not trigger any class initialization.
} else {
fail();
}
}
@Override
public void registerCheckCast(DexType type, boolean ignoreCompatRules) {
// Intentionally empty, does not trigger any class initialization.
}
@Override
public void registerConstClass(
DexType type,
ListIterator<? extends CfOrDexInstruction> iterator,
boolean ignoreCompatRules) {
// Intentionally empty, does not trigger any class initialization.
}
@Override
public void registerInstanceFieldRead(DexField field) {
// Intentionally empty, does not trigger any class initialization.
}
@Override
public void registerInstanceFieldWrite(DexField field) {
// Intentionally empty, does not trigger any class initialization.
}
@Override
public void registerInstanceOf(DexType type) {
// Intentionally empty, does not trigger any class initialization.
}
@Override
public void registerExceptionGuard(DexType guard) {
// Intentionally empty, does not trigger any class initialization.
}
}
}
}