| // 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); |
| } |
| } |