blob: b0a03a208e42f9b99cc5a306ed6977e74b3432bb [file] [log] [blame]
// Copyright (c) 2022, 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.shaking;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.shaking.ObjectAllocationInfoCollectionUtils.mayHaveFinalizeMethodDirectlyOrIndirectly;
import static com.android.tools.r8.utils.MapUtils.ignoreKey;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.Code;
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.FieldAccessInfo;
import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
import com.android.tools.r8.graph.FieldResolutionResult;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.conversion.IRFinalizer;
import com.android.tools.r8.ir.conversion.IRToCfFinalizer;
import com.android.tools.r8.ir.conversion.IRToDexFinalizer;
import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfo;
import com.android.tools.r8.ir.optimize.membervaluepropagation.assume.AssumeInfoLookup;
import com.android.tools.r8.shaking.Enqueuer.FieldAccessKind;
import com.android.tools.r8.shaking.Enqueuer.FieldAccessMetadata;
import com.android.tools.r8.shaking.Enqueuer.Mode;
import com.android.tools.r8.shaking.EnqueuerWorklist.EnqueuerAction;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.collections.ProgramFieldMap;
import com.android.tools.r8.utils.collections.ProgramFieldSet;
import com.android.tools.r8.utils.collections.ProgramMethodSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
public class EnqueuerDeferredTracing {
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final Enqueuer enqueuer;
private final Mode mode;
private final InternalOptions options;
// Helper for rewriting code instances at the end of tree shaking.
private final EnqueuerDeferredTracingRewriter rewriter;
// Maps each field to the tracing actions that have been deferred for that field. This allows
// enqueuing previously deferred tracing actions into the worklist if a given field cannot be
// optimized after all.
private final ProgramFieldMap<Set<EnqueuerAction>> deferredEnqueuerActions =
ProgramFieldMap.create();
// A set of fields that are never eligible for pruning.
private final ProgramFieldSet ineligibleForPruning = ProgramFieldSet.create();
EnqueuerDeferredTracing(
AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer, Mode mode) {
this.appView = appView;
this.enqueuer = enqueuer;
this.mode = mode;
this.options = appView.options();
this.rewriter = new EnqueuerDeferredTracingRewriter(appView);
}
/**
* Returns true if the {@link Enqueuer} should not trace the given field reference.
*
* <p>If for some reason the field reference should be traced after all, a worklist item can be
* enqueued upon reaching a (preliminary) fixpoint in {@link
* #enqueueWorklistActions(EnqueuerWorklist)}, which will cause tracing to continue.
*/
public boolean deferTracingOfFieldAccess(
DexField fieldReference,
FieldResolutionResult resolutionResult,
ProgramMethod context,
FieldAccessKind accessKind,
FieldAccessMetadata metadata) {
if (!enqueuer.getMode().isFinalTreeShaking()) {
return false;
}
ProgramField field = resolutionResult.getSingleProgramField();
if (field == null) {
return false;
}
// Check if field access is consistent with the field access flags.
if (field.getAccessFlags().isStatic() != accessKind.isStatic()) {
return enqueueDeferredEnqueuerActions(field);
}
// If the access is from a reachability sensitive method, then bail out.
if (context.getHolder().getOrComputeReachabilitySensitive(appView)) {
return enqueueDeferredEnqueuerActions(field);
}
if (accessKind.isRead()) {
// If the value of the field is not guaranteed to be the default value, even if it is never
// assigned, then give up.
// TODO(b/205810841): Allow this by handling this in the corresponding IR rewriter.
AssumeInfo assumeInfo = AssumeInfoLookup.lookupAssumeInfo(appView, field);
if (assumeInfo != null && assumeInfo.hasReturnInfo()) {
return enqueueDeferredEnqueuerActions(field);
}
if (field.getAccessFlags().isStatic() && field.getDefinition().hasExplicitStaticValue()) {
return enqueueDeferredEnqueuerActions(field);
}
}
if (!isEligibleForPruning(field)) {
return enqueueDeferredEnqueuerActions(field);
}
// Field can be removed unless some other field access that has not yet been seen prohibits it.
// Record an EnqueuerAction that must be traced if that should happen.
EnqueuerAction deferredEnqueuerAction =
accessKind.toEnqueuerAction(fieldReference, context, metadata.toDeferred());
deferredEnqueuerActions
.computeIfAbsent(field, ignoreKey(LinkedHashSet::new))
.add(deferredEnqueuerAction);
// If the field is static, then the field access will trigger the class initializer of the
// field's holder. Therefore, we unconditionally trace the class initializer in this case.
// The corresponding IR rewriter will rewrite the field access into an init-class instruction.
if (accessKind.isStatic()) {
KeepReason reason =
enqueuer.getGraphReporter().reportClassReferencedFrom(field.getHolder(), context);
enqueuer.getWorklist().enqueueTraceTypeReferenceAction(field.getHolder(), reason);
enqueuer.getWorklist().enqueueTraceDirectAndIndirectClassInitializers(field.getHolder());
}
return true;
}
public void notifyReflectiveFieldAccess(ProgramField field, ProgramMethod context) {
enqueueDeferredEnqueuerActions(field);
}
private boolean isEligibleForPruning(ProgramField field) {
FieldAccessInfo info = enqueuer.getFieldAccessInfoCollection().get(field.getReference());
if (info.hasReflectiveAccess()
|| info.isAccessedFromMethodHandle()
|| info.isReadFromAnnotation()
|| info.isReadFromRecordInvokeDynamic()
|| enqueuer.getKeepInfo(field).isPinned(options)) {
return false;
}
if (info.isWritten()) {
// If the assigned value may have an override of Object#finalize() then give up.
// Note that this check depends on the set of instantiated types, and must therefore be rerun
// when the enqueuer's fixpoint is reached.
if (field.getType().isReferenceType()) {
DexType fieldBaseType = field.getType().toBaseType(appView.dexItemFactory());
if (fieldBaseType.isClassType()
&& mayHaveFinalizeMethodDirectlyOrIndirectly(
appView, fieldBaseType, enqueuer.getObjectAllocationInfoCollection())) {
return false;
}
}
}
// We always have precise knowledge of field accesses during tracing.
assert info.hasKnownReadContexts();
assert info.hasKnownWriteContexts();
DexType fieldType = field.getType();
// If the field is now both read and written, then we cannot optimize the field unless the field
// type is an uninstantiated class type.
if (info.getReadsWithContexts().hasAccesses() && info.getWritesWithContexts().hasAccesses()) {
if (!fieldType.isClassType()) {
return false;
}
DexProgramClass fieldTypeDefinition = asProgramClassOrNull(appView.definitionFor(fieldType));
if (fieldTypeDefinition == null
|| enqueuer
.getObjectAllocationInfoCollection()
.isInstantiatedDirectlyOrHasInstantiatedSubtype(fieldTypeDefinition)) {
return false;
}
}
return !ineligibleForPruning.contains(field);
}
private boolean enqueueDeferredEnqueuerActions(ProgramField field) {
Set<EnqueuerAction> actions = deferredEnqueuerActions.remove(field);
if (actions != null) {
enqueuer.getWorklist().enqueueAll(actions);
}
ineligibleForPruning.add(field);
return false;
}
/**
* Called when the {@link EnqueuerWorklist} is empty, to allow additional tracing before ending
* tree shaking.
*/
public boolean enqueueWorklistActions(EnqueuerWorklist worklist) {
return deferredEnqueuerActions.removeIf(
(field, worklistActions) -> {
if (isEligibleForPruning(field)) {
return false;
}
worklist.enqueueAll(worklistActions);
return true;
});
}
/**
* Called when tree shaking has ended, to allow rewriting the application according to the tracing
* that has not been performed (e.g., rewriting of dead field instructions).
*/
public void rewriteApplication(ExecutorService executorService) throws ExecutionException {
FieldAccessInfoCollectionImpl fieldAccessInfoCollection =
enqueuer.getFieldAccessInfoCollection();
ProgramMethodSet methodsToProcess = ProgramMethodSet.create();
Map<DexField, ProgramField> prunedFields = new IdentityHashMap<>();
deferredEnqueuerActions.forEach(
(field, ignore) -> {
FieldAccessInfo accessInfo = fieldAccessInfoCollection.get(field.getReference());
prunedFields.put(field.getReference(), field);
accessInfo.forEachAccessContext(methodsToProcess::add);
accessInfo.forEachIndirectAccess(reference -> prunedFields.put(reference, field));
});
deferredEnqueuerActions.clear();
// Rewrite application.
Map<DexProgramClass, ProgramMethodSet> initializedClassesWithContexts =
new ConcurrentHashMap<>();
ThreadUtils.processItems(
methodsToProcess,
method -> rewriteMethod(method, initializedClassesWithContexts, prunedFields),
executorService);
// Register new InitClass instructions.
initializedClassesWithContexts.forEach(
(clazz, contexts) ->
contexts.forEach(context -> enqueuer.traceInitClass(clazz.getType(), context)));
assert enqueuer.getWorklist().isEmpty();
// Prune field access info collection.
prunedFields.values().forEach(field -> fieldAccessInfoCollection.remove(field.getReference()));
}
private void rewriteMethod(
ProgramMethod method,
Map<DexProgramClass, ProgramMethodSet> initializedClassesWithContexts,
Map<DexField, ProgramField> prunedFields) {
// Build IR.
MutableMethodConversionOptions conversionOptions =
mode.isInitialTreeShaking()
? new MutableMethodConversionOptions(options).setIsGeneratingClassFiles(true)
: new MutableMethodConversionOptions(options);
conversionOptions.disableStringSwitchConversion();
IRCode ir = method.buildIR(appView, conversionOptions);
// Rewrite the IR according to the tracing that has been deferred.
rewriter.rewriteCode(ir, initializedClassesWithContexts, prunedFields);
// Run dead code elimination.
rewriter.getCodeRewriter().optimizeAlwaysThrowingInstructions(ir);
rewriter.getDeadCodeRemover().run(ir, Timing.empty());
// Finalize to class files or dex.
IRFinalizer<?> finalizer =
conversionOptions.isGeneratingClassFiles()
? new IRToCfFinalizer(appView, rewriter.getDeadCodeRemover())
: new IRToDexFinalizer(appView, rewriter.getDeadCodeRemover());
Code newCode = finalizer.finalizeCode(ir, BytecodeMetadataProvider.empty(), Timing.empty());
method.setCode(newCode, appView);
}
}