Refactor optimized resource shrinker to be an Enqueuer Extension
Simplify the interaction between Enqueuer and R8ResourceShrinkerState by
introducing ResourceShrinkerEnqueuerExtension.
Change-Id: Ic51492fd3ec3153245d52e6d135f46d1e51a4a4f
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
index b7bbe6f..1b8428c 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/EnqueuerAnalysisCollection.java
@@ -58,7 +58,8 @@
private final NewlyTargetedMethodEnqueuerAnalysis[] newlyTargetedMethodAnalyses;
private final MarkFieldAsKeptEnqueuerAnalysis[] markFieldAsKeptEnqueuerAnalyses;
- // Tear down events.
+ // Start & Tear down events.
+ private final StartEnqueuerAnalysis[] startAnalyses;
private final FinishedEnqueuerAnalysis[] finishedAnalyses;
private final FixpointEnqueuerAnalysis[] fixpointAnalyses;
@@ -86,9 +87,11 @@
NewlyReferencedFieldEnqueuerAnalysis[] newlyReferencedFieldAnalyses,
NewlyTargetedMethodEnqueuerAnalysis[] newlyTargetedMethodAnalyses,
MarkFieldAsKeptEnqueuerAnalysis[] markFieldAsKeptEnqueuerAnalyses,
- // Tear down events.
+ // Start & Tear down events.
+ StartEnqueuerAnalysis[] startAnalyses,
FinishedEnqueuerAnalysis[] finishedAnalyses,
FixpointEnqueuerAnalysis[] fixpointAnalyses) {
+ this.startAnalyses = startAnalyses;
// Trace events.
this.checkCastAnalyses = checkCastAnalyses;
this.constClassAnalyses = constClassAnalyses;
@@ -367,6 +370,12 @@
}
}
+ public void onStarted(Enqueuer enqueuer) {
+ for (StartEnqueuerAnalysis analysis : startAnalyses) {
+ analysis.onStarted(enqueuer);
+ }
+ }
+
// Tear down events.
public void done(Enqueuer enqueuer, ExecutorService executorService) throws ExecutionException {
@@ -431,6 +440,9 @@
new ArrayList<>();
private final List<MarkFieldAsKeptEnqueuerAnalysis> markFieldAsKeptAnalyses = new ArrayList<>();
+ // Start events.
+ private final List<StartEnqueuerAnalysis> startAnalyses = new ArrayList<>();
+
// Tear down events.
private final List<FinishedEnqueuerAnalysis> finishedAnalyses = new ArrayList<>();
private final List<FixpointEnqueuerAnalysis> fixpointAnalyses = new ArrayList<>();
@@ -546,6 +558,11 @@
return this;
}
+ public Builder addStartAnalysis(StartEnqueuerAnalysis analysis) {
+ startAnalyses.add(analysis);
+ return this;
+ }
+
// Tear down events.
public Builder addFinishedAnalysis(FinishedEnqueuerAnalysis analysis) {
@@ -585,6 +602,7 @@
newlyTargetedMethodAnalyses.toArray(NewlyTargetedMethodEnqueuerAnalysis[]::new),
markFieldAsKeptAnalyses.toArray(MarkFieldAsKeptEnqueuerAnalysis[]::new),
// Tear down events.
+ startAnalyses.toArray(StartEnqueuerAnalysis[]::new),
finishedAnalyses.toArray(FinishedEnqueuerAnalysis[]::new),
fixpointAnalyses.toArray(FixpointEnqueuerAnalysis[]::new));
}
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/StartEnqueuerAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/StartEnqueuerAnalysis.java
new file mode 100644
index 0000000..8284fc4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/analysis/StartEnqueuerAnalysis.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2026, 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.graph.analysis;
+
+import com.android.tools.r8.shaking.Enqueuer;
+
+public interface StartEnqueuerAnalysis {
+
+ /** Called when the Enqueuer starts tracing. */
+ void onStarted(Enqueuer enqueuer);
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 61ed436..c4867fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -17,7 +17,6 @@
import static java.util.Collections.emptySet;
import com.android.tools.r8.Diagnostic;
-import com.android.tools.r8.FeatureSplit;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfInvoke;
import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
@@ -124,9 +123,7 @@
import com.android.tools.r8.naming.IdentifierNameStringCollection;
import com.android.tools.r8.optimize.interfaces.analysis.CfOpenClosedInterfacesAnalysis;
import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
-import com.android.tools.r8.resourceshrinker.ResourceShrinkerState;
import com.android.tools.r8.shaking.AnnotationMatchResult.MatchedAnnotation;
import com.android.tools.r8.shaking.EnqueuerEvent.ClassEnqueuerEvent;
import com.android.tools.r8.shaking.EnqueuerEvent.InstantiatedClassEnqueuerEvent;
@@ -175,7 +172,6 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
-import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -304,6 +300,8 @@
*/
private final Map<DexProgramClass, ProgramMethod> synthesizingContexts = new IdentityHashMap<>();
+ private ResourceShrinkerEnqueuerExtension resourceShrinkerExtension = null;
+
/**
* Set of types that are mentioned in the program. We at least need an empty abstract class item
* for these.
@@ -530,13 +528,6 @@
this.initialPrunedTypes = initialPrunedTypes;
this.prunedClasspathTypes = prunedClasspathTypes;
- if (options.isOptimizedResourceShrinking()) {
- ResourceShrinkerState<FeatureSplit> resourceShrinkerState =
- appView.getResourceShrinkerState();
- resourceShrinkerState.setEnqueuerCallback(this::recordReferenceFromResources);
- resourceShrinkerState.setEnqueuerMethodCallback(this::recordMethodReferenceFromResources);
- }
-
EnqueuerAnalysisCollection.Builder analysesBuilder = EnqueuerAnalysisCollection.builder();
if (mode.isTreeShaking()) {
EnqueuerDefinitionSupplier enqueuerDefinitionSupplier = new EnqueuerDefinitionSupplier(this);
@@ -554,7 +545,8 @@
KotlinMetadataEnqueuerExtension.register(
appView, enqueuerDefinitionSupplier, initialPrunedTypes, analysesBuilder);
ProtoEnqueuerExtension.register(appView, this, analysesBuilder);
- ResourceShrinkerEnqueuerExtension.register(appView, this, analysesBuilder);
+ this.resourceShrinkerExtension =
+ ResourceShrinkerEnqueuerExtension.register(appView, this, analysesBuilder);
RuntimeTypeCheckInfo.register(runtimeTypeCheckInfoBuilder, analysesBuilder);
EnqueuerMockitoAnalysis.register(appView, this, analysesBuilder);
// Enum reflection tracing is best-effort, but since it is more common for non-Android uses to
@@ -697,15 +689,7 @@
recordTypeReference(type, context, this::recordNonProgramClass, this::reportMissingClass);
}
- private final Map<DexString, Origin> onClickMethodReferences = new HashMap<>();
-
- private void recordMethodReferenceFromResources(String method, String xmlFilePath) {
- Origin origin = new PathOrigin(Paths.get(xmlFilePath));
- onClickMethodReferences.put(appView.dexItemFactory().createString(method), origin);
- }
-
- private boolean recordReferenceFromResources(
- String possibleClass, String xmlFilePath, boolean markAsLive) {
+ boolean recordReferenceFromResources(String possibleClass, Origin origin, boolean markAsLive) {
if (!DescriptorUtils.isValidJavaType(possibleClass)) {
return false;
}
@@ -722,7 +706,6 @@
return false;
}
- Origin origin = new PathOrigin(Paths.get(xmlFilePath));
ReflectiveUseFromXml reason = KeepReason.reflectiveUseFromXml(origin);
ensureClassKeptForResourceLookup(clazz, reason, markAsLive);
@@ -1269,7 +1252,9 @@
if (mode.isInitialTreeShaking()) {
return;
}
- appView.getResourceShrinkerState().trace(value, "from dex");
+ if (resourceShrinkerExtension != null) {
+ resourceShrinkerExtension.traceResourceValue(value);
+ }
}
public void traceReflectiveFieldWrite(ProgramField field, ProgramMethod context) {
@@ -2410,39 +2395,6 @@
analyses.processNewlyLiveClass(clazz, worklist);
}
- private void processOnClickMethods(Timing timing) {
- if (onClickMethodReferences.isEmpty()) {
- return;
- }
- timing.begin("Process onclick methods");
- for (DexProgramClass item : liveTypes.getItems()) {
- for (ProgramMethod method :
- item.virtualProgramMethods(
- p ->
- p.getParameters().size() == 1
- && p.getParameter(0)
- .isIdenticalTo(appInfo.dexItemFactory().androidViewViewType)
- && onClickMethodReferences.containsKey(p.getName()))) {
- KeepMethodInfo methodInfo = getKeepInfo().getMethodInfo(method);
- if (!methodInfo.isOptimizationAllowed(options)
- && !methodInfo.isShrinkingAllowed(options)
- && !methodInfo.isMinificationAllowed(options)) {
- continue;
- }
- ReflectiveUseFromXml reason =
- KeepReason.reflectiveUseFromXml(onClickMethodReferences.get(method.getName()));
- Joiner minimumKeepInfo =
- KeepMethodInfo.newEmptyJoiner()
- .disallowOptimization()
- .disallowShrinking()
- .disallowMinification()
- .addReason(reason);
- applyMinimumKeepInfo(method, minimumKeepInfo);
- }
- }
- timing.end();
- }
-
private void ensureMethodsContinueToWidenAccess(ClassDefinition clazz) {
assert !clazz.isProgramClass();
ScopedDexMethodSet seen =
@@ -3958,6 +3910,7 @@
assert analyses.isEmpty();
assert mode.isMainDexTracing();
this.rootSet = appView.getMainDexRootSet();
+ analyses.onStarted(this);
// Translate the result of root-set computation into enqueuer actions.
includeMinimumKeepInfo(rootSet);
trace(executorService, timing);
@@ -3979,6 +3932,7 @@
public EnqueuerResult traceApplication(
RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException {
this.rootSet = rootSet;
+ analyses.onStarted(this);
rootSet.pendingMethodMoveInverse.forEach(pendingMethodMoveInverse::put);
// Transfer the minimum keep info from the root set into the Enqueuer state.
@@ -4008,7 +3962,6 @@
enqueueAllIfNotShrinking();
timing.end();
timing.begin("Trace");
- traceManifestsAndRoots(timing);
trace(executorService, timing);
timing.end();
options.reporter.failIfPendingErrors();
@@ -4018,9 +3971,6 @@
timing.begin("Finish analysis");
taskCollection.awaitEnqueuerIndependentTasks();
analyses.done(this, executorService);
- if (appView.options().isOptimizedResourceShrinking()) {
- appView.getResourceShrinkerState().enqueuerDone(this.mode.isFinalTreeShaking());
- }
timing.end();
assert verifyKeptGraph();
timing.begin("Finish compat building");
@@ -4043,17 +3993,6 @@
return result;
}
- private void traceManifestsAndRoots(Timing timing) {
- if (options.isOptimizedResourceShrinking()) {
- timing.begin("Trace AndroidManifest.xml files");
- appView.getResourceShrinkerState().traceKeepXmlAndManifest();
- for (int rootResourceId : appView.rootSet().resourceIds) {
- appView.getResourceShrinkerState().trace(rootResourceId, "Non shrunken dex code");
- }
- timing.end();
- }
- }
-
private void includeMinimumKeepInfo(RootSetBase rootSet) {
rootSet
.getDependentMinimumKeepInfo()
@@ -5027,12 +4966,6 @@
}
}
- processOnClickMethods(timing);
- if (worklist.hasNext()) {
- timing.end();
- continue;
- }
-
// Continue fix-point processing while there are additional work items to ensure items that
// are passed to Java reflections are traced.
reflectiveIdentification.processWorklist(timing);
diff --git a/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java b/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java
index 4642cb3..bc50d85 100644
--- a/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/shaking/ResourceShrinkerEnqueuerExtension.java
@@ -11,16 +11,20 @@
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexValue;
-import com.android.tools.r8.graph.DexValue.DexValueResourceNumber;
import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
import com.android.tools.r8.graph.ProgramDefinition;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.analysis.EnqueuerAnalysisCollection;
+import com.android.tools.r8.graph.analysis.FinishedEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.FixpointEnqueuerAnalysis;
import com.android.tools.r8.graph.analysis.MarkFieldAsKeptEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.NewlyLiveClassEnqueuerAnalysis;
import com.android.tools.r8.graph.analysis.NewlyLiveFieldEnqueuerAnalysis;
+import com.android.tools.r8.graph.analysis.StartEnqueuerAnalysis;
import com.android.tools.r8.graph.analysis.TraceFieldAccessEnqueuerAnalysis;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
@@ -28,74 +32,104 @@
import com.android.tools.r8.ir.code.StaticPut;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.resourceshrinker.ResourceShrinkerState;
+import com.android.tools.r8.resourceshrinker.ResourceShrinkerState.ResourceShrinkerCallback;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
import com.android.tools.r8.utils.internal.exceptions.Unreachable;
+import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
+import java.nio.file.Paths;
import java.util.IdentityHashMap;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
public class ResourceShrinkerEnqueuerExtension
- implements TraceFieldAccessEnqueuerAnalysis, MarkFieldAsKeptEnqueuerAnalysis {
+ implements TraceFieldAccessEnqueuerAnalysis,
+ MarkFieldAsKeptEnqueuerAnalysis,
+ NewlyLiveClassEnqueuerAnalysis,
+ NewlyLiveFieldEnqueuerAnalysis,
+ StartEnqueuerAnalysis,
+ FixpointEnqueuerAnalysis,
+ FinishedEnqueuerAnalysis,
+ ResourceShrinkerCallback {
- private final ResourceShrinkerState<FeatureSplit> resourceShrinkerState;
- private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final Enqueuer enqueuer;
+ private final ResourceShrinkerState<FeatureSplit> resourceShrinkerState;
+ private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
+
+ // Deferred state
+ private final Map<DexString, Origin> onClickMethodReferences = new IdentityHashMap<>();
+ private final ProgramMethodSet processedOnClickMethods = ProgramMethodSet.create();
+
+ // Pending incremental state
+ private final Set<DexProgramClass> pendingLiveClasses = Sets.newIdentityHashSet();
+ private final Map<DexString, Origin> pendingOnClickMethodReferences = new IdentityHashMap<>();
private ResourceShrinkerEnqueuerExtension(
AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
this.appView = appView;
- this.resourceShrinkerState = appView.getResourceShrinkerState();
this.enqueuer = enqueuer;
+ this.resourceShrinkerState = appView.getResourceShrinkerState();
}
- public static void register(
+ public static ResourceShrinkerEnqueuerExtension register(
AppView<? extends AppInfoWithClassHierarchy> appView,
Enqueuer enqueuer,
EnqueuerAnalysisCollection.Builder builder) {
- if (fieldAccessAnalysisEnabled(appView, enqueuer)) {
- ResourceShrinkerEnqueuerExtension analysis =
+ if (appView.options().isOptimizedResourceShrinking()) {
+ ResourceShrinkerEnqueuerExtension extension =
new ResourceShrinkerEnqueuerExtension(appView, enqueuer);
- builder.addTraceFieldAccessAnalysis(analysis);
- builder.addMarkFieldAsKeptAnalysis(analysis);
+
+ // Always register for started, fixpoint, finished, and newly live class events
+ builder.addStartAnalysis(extension);
+ builder.addFixpointAnalysis(extension);
+ builder.addFinishedAnalysis(extension);
+ builder.addNewlyLiveClassAnalysis(extension);
+
+ if (fieldAccessAnalysisEnabled(enqueuer)) {
+ builder.addTraceFieldAccessAnalysis(extension);
+ builder.addMarkFieldAsKeptAnalysis(extension);
+ }
+ if (liveFieldAnalysisEnabled(appView, enqueuer)) {
+ builder.addNewlyLiveFieldAnalysis(extension);
+ }
+ return extension;
}
- if (liveFieldAnalysisEnabled(appView, enqueuer)) {
- builder.addNewlyLiveFieldAnalysis(
- new NewlyLiveFieldEnqueuerAnalysis() {
- @Override
- public void processNewlyLiveField(
- ProgramField field, ProgramDefinition context, EnqueuerWorklist worklist) {
- DexEncodedField definition = field.getDefinition();
- if (field.getAccessFlags().isStatic()
- && definition.hasExplicitStaticValue()
- && definition.getStaticValue().isDexValueResourceNumber()) {
- appView
- .getResourceShrinkerState()
- .trace(
- definition.getStaticValue().asDexValueResourceNumber().getValue(),
- // TODO(b/378625969): Consider wrapping this in a reachability structure
- // to avoid decoding.
- field.toString());
- }
- }
- });
- }
+ return null;
}
private static boolean liveFieldAnalysisEnabled(
AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
return appView.options().androidResourceProvider != null
- && appView.options().isOptimizedResourceShrinking()
&& enqueuer.getMode().isFinalTreeShaking();
}
- private static boolean fieldAccessAnalysisEnabled(
- AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
- return appView.options().isOptimizedResourceShrinking()
- // Only run this in the first round, we explicitly trace the resource values
- // with ResourceConstNumber in the optimizing pipeline.
- && enqueuer.getMode().isInitialTreeShaking();
+ private static boolean fieldAccessAnalysisEnabled(Enqueuer enqueuer) {
+ return enqueuer.getMode().isInitialTreeShaking();
+ }
+
+ // ClassReferenceCallback (called from ResourceShrinkerState during trace)
+ @Override
+ public boolean tryClass(String possibleClass, String xmlFilePath, boolean markAsLive) {
+ Origin xmlFileOrigin = new PathOrigin(Paths.get(xmlFilePath));
+ return enqueuer.recordReferenceFromResources(possibleClass, xmlFileOrigin, markAsLive);
+ }
+
+ @Override
+ public void tryMethod(String methodName, String xmlFilePath) {
+ Origin xmlFileOrigin = new PathOrigin(Paths.get(xmlFilePath));
+ pendingOnClickMethodReferences.put(
+ appView.dexItemFactory().createString(methodName), xmlFileOrigin);
+ }
+
+ public void traceResourceValue(int value) {
+ resourceShrinkerState.trace(value, "from dex", this);
}
@Override
@@ -122,57 +156,137 @@
if (!fieldToValueMapping.containsKey(holderType)) {
populateRClassValues(resolvedField.getHolder());
}
- assert fieldToValueMapping.containsKey(holderType);
RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
IntList integers = rClassFieldToValueStore.valueMapping.get(resolvedField.getReference());
// The R class can have fields injected, e.g., by jacoco, we don't have resource values for
// these.
if (integers != null) {
- for (Integer integer : integers) {
- resourceShrinkerState.trace(integer, resolvedField.getReference().toString());
+ for (int id : integers) {
+ resourceShrinkerState.trace(id, resolvedField.getReference().toString(), this);
}
}
}
}
+ @Override
+ public void processNewlyLiveClass(DexProgramClass clazz, EnqueuerWorklist worklist) {
+ pendingLiveClasses.add(clazz);
+ // Warn on final ID fields if needed
+ if (enqueuer.isRClass(clazz)) {
+ for (DexEncodedField field : clazz.staticFields()) {
+ if (field.isFinal() && field.hasExplicitStaticValue() && field.getType().isIntType()) {
+ appView
+ .reporter()
+ .warning(
+ new FinalRClassEntriesWithOptimizedShrinkingDiagnostic(
+ clazz.origin, field.getReference()));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void processNewlyLiveField(
+ ProgramField field, ProgramDefinition context, EnqueuerWorklist worklist) {
+ DexEncodedField definition = field.getDefinition();
+ if (field.getAccessFlags().isStatic()
+ && definition.hasExplicitStaticValue()
+ && definition.getStaticValue().isDexValueResourceNumber()) {
+ resourceShrinkerState.trace(
+ definition.getStaticValue().asDexValueResourceNumber().getValue(),
+ field.getReference().toString(),
+ this);
+ }
+ }
+
+ @Override
+ public void onStarted(Enqueuer enqueuer) {
+ if (!enqueuer.getMode().isTreeShaking()) {
+ return;
+ }
+ resourceShrinkerState.traceKeepXmlAndManifest(this);
+ for (int rootResourceId : appView.rootSet().resourceIds) {
+ resourceShrinkerState.trace(rootResourceId, "Non shrunken dex code", this);
+ }
+ }
+
+ @Override
+ public void notifyFixpoint(
+ Enqueuer enqueuer,
+ EnqueuerWorklist worklist,
+ ExecutorService executorService,
+ com.android.tools.r8.utils.timing.Timing timing)
+ throws ExecutionException {
+ if (pendingLiveClasses.isEmpty() && pendingOnClickMethodReferences.isEmpty()) {
+ return;
+ }
+
+ // 1. Match pendingLiveClasses against committed onClickMethodReferences
+ if (!onClickMethodReferences.isEmpty()) {
+ for (DexProgramClass clazz : pendingLiveClasses) {
+ matchOnClickMethods(clazz, onClickMethodReferences);
+ }
+ }
+
+ // 2. Match pendingOnClickMethodReferences against ALL liveClasses
+ if (!pendingOnClickMethodReferences.isEmpty()) {
+ enqueuer.forAllLiveClasses(
+ clazz -> matchOnClickMethods(clazz, pendingOnClickMethodReferences));
+ }
+
+ // 3. Commit pending states
+ pendingLiveClasses.clear();
+ onClickMethodReferences.putAll(pendingOnClickMethodReferences);
+ pendingOnClickMethodReferences.clear();
+ }
+
+ private void matchOnClickMethods(
+ DexProgramClass clazz, Map<DexString, Origin> onClickReferences) {
+ for (ProgramMethod method :
+ clazz.virtualProgramMethods(
+ p ->
+ p.getParameters().size() == 1
+ && p.getParameter(0).isIdenticalTo(appView.dexItemFactory().androidViewViewType)
+ && onClickReferences.containsKey(p.getName()))) {
+ if (processedOnClickMethods.add(method)) {
+ KeepMethodInfo methodInfo = enqueuer.getKeepInfo().getMethodInfo(method);
+ if (!methodInfo.isOptimizationAllowed(appView.options())
+ && !methodInfo.isShrinkingAllowed(appView.options())
+ && !methodInfo.isMinificationAllowed(appView.options())) {
+ continue;
+ }
+ KeepReason reason =
+ KeepReason.reflectiveUseFromXml(onClickReferences.get(method.getName()));
+ KeepMethodInfo.Joiner minimumKeepInfo =
+ KeepMethodInfo.newEmptyJoiner()
+ .disallowOptimization()
+ .disallowShrinking()
+ .disallowMinification()
+ .addReason(reason);
+ enqueuer.applyMinimumKeepInfo(method, minimumKeepInfo);
+ }
+ }
+ }
+
+ @Override
+ public void done(Enqueuer enqueuer, ExecutorService executorService) {
+ resourceShrinkerState.enqueuerDone(enqueuer.getMode().isFinalTreeShaking());
+ }
+
private void populateRClassValues(DexProgramClass programClass) {
- // TODO(287398085): Pending discussions with the AAPT2 team, we might need to harden this
- // to not fail if we wrongly classify an unrelated class as R class in our heuristic..
RClassFieldToValueStore.Builder rClassValueBuilder = new RClassFieldToValueStore.Builder();
analyzeStaticFields(programClass, rClassValueBuilder);
ProgramMethod programClassInitializer = programClass.getProgramClassInitializer();
if (programClassInitializer != null) {
analyzeClassInitializer(rClassValueBuilder, programClassInitializer);
}
- warnOnFinalIdFields(programClass);
fieldToValueMapping.put(programClass.getType(), rClassValueBuilder.build());
}
- private void warnOnFinalIdFields(DexProgramClass holder) {
- if (!appView.options().isOptimizedResourceShrinking()) {
- return;
- }
- for (DexEncodedField field : holder.fields()) {
- if (field.isStatic()
- && field.isFinal()
- && field.hasExplicitStaticValue()
- && field.getType().isIntType()) {
- appView
- .reporter()
- .warning(
- new FinalRClassEntriesWithOptimizedShrinkingDiagnostic(
- holder.origin, field.getReference()));
- }
- }
- }
-
private void analyzeClassInitializer(
RClassFieldToValueStore.Builder rClassValueBuilder, ProgramMethod programClassInitializer) {
IRCode code = programClassInitializer.buildIR(appView, MethodConversionOptions.nonConverting());
- // We handle two cases:
- // - Simple integer field assigments.
- // - Assigments of integer arrays to fields.
for (StaticPut staticPut : code.<StaticPut>instructions(Instruction::isStaticPut)) {
Value value = staticPut.value();
if (value.isPhi()) {
@@ -204,7 +318,7 @@
} else if (definition.isNewArrayFilled()) {
values = new IntArrayList();
for (Value inValue : definition.asNewArrayFilled().inValues()) {
- if (value.isPhi()) {
+ if (inValue.isPhi()) {
continue;
}
Instruction valueDefinition = inValue.definition;
@@ -231,14 +345,14 @@
IntList values = new IntArrayList(1);
values.add(staticValue.asDexValueInt().getValue());
staticField.setStaticValue(
- DexValueResourceNumber.create(staticValue.asDexValueInt().value));
+ DexValue.DexValueResourceNumber.create(staticValue.asDexValueInt().value));
rClassValueBuilder.addMapping(staticField.getReference(), values);
}
}
}
private static class RClassFieldToValueStore {
- private Map<DexField, IntList> valueMapping;
+ private final Map<DexField, IntList> valueMapping;
private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
this.valueMapping = valueMapping;
diff --git a/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java
index a14a3b8..80afae1 100644
--- a/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/tools/r8/resourceshrinker/ResourceShrinkerState.java
@@ -73,8 +73,7 @@
private final ShrinkerDebugReporter shrinkerDebugReporter;
private final boolean enableXmlInlining;
private final boolean enableManifestPruning;
- private ClassReferenceCallback enqueuerCallback;
- private MethodReferenceCallback methodCallback;
+
private Map<Integer, Set<String>> resourceIdToXmlFiles;
private Set<String> packageNames;
private final Set<String> seenNoneClassValues = new HashSet<>();
@@ -92,13 +91,9 @@
private static final Set<String> SPECIAL_APPLICATION_ATTRIBUTES =
ImmutableSet.of("backupAgent", "appComponentFactory", "zygotePreloadName");
- @FunctionalInterface
- public interface ClassReferenceCallback {
+ public interface ResourceShrinkerCallback {
boolean tryClass(String possibleClass, String xmlFilePath, boolean markAsLive);
- }
- @FunctionalInterface
- public interface MethodReferenceCallback {
void tryMethod(String methodName, String xmlFilePath);
}
@@ -114,7 +109,7 @@
this.enableManifestPruning = enableManifestPruning;
}
- public void trace(int id, String reachableFrom) {
+ public void trace(int id, String reachableFrom, ResourceShrinkerCallback callback) {
if (!seenResourceIds.add(id)) {
return;
}
@@ -128,11 +123,11 @@
reachabilityMap.compute(
resource, (r, v) -> v == null || v.compareTo(reachableFrom) > 0 ? reachableFrom : v);
ResourceUsageModel.markReachable(resource);
- traceXmlForResourceId(id);
+ traceXmlForResourceId(id, callback);
if (resource.references != null) {
for (Resource reference : resource.references) {
if (!reference.isReachable()) {
- trace(reference.value, resource.toString());
+ trace(reference.value, resource.toString(), callback);
}
}
}
@@ -142,7 +137,7 @@
return getR8ResourceShrinkerModel().getResourceStore().getResource(resourceId) != null;
}
- public void traceKeepXmlAndManifest() {
+ public void traceKeepXmlAndManifest(ResourceShrinkerCallback callback) {
// We start by building the root set of all keep/discard rules to find those pinned resources
// before marking additional resources in the trace.
// We then explicitly trace those resources to transitively get the full set of reachable
@@ -156,22 +151,12 @@
r8ResourceShrinkerModel
.getResourceStore()
.processToolsAttributes()
- .forEach(resource -> trace(resource.value, "keep xml file"));
+ .forEach(resource -> trace(resource.value, "keep xml file", callback));
for (Supplier<InputStream> manifestProvider : manifestProviders) {
- traceXml("AndroidManifest.xml", manifestProvider.get(), ids -> {});
+ traceXml("AndroidManifest.xml", manifestProvider.get(), ids -> {}, callback);
}
}
- public void setEnqueuerCallback(ClassReferenceCallback enqueuerCallback) {
- assert this.enqueuerCallback == null;
- this.enqueuerCallback = enqueuerCallback;
- }
-
- public void setEnqueuerMethodCallback(MethodReferenceCallback methodCallback) {
- assert this.methodCallback == null;
- this.methodCallback = methodCallback;
- }
-
private synchronized Set<String> getPackageNames() {
// TODO(b/325888516): Consider only doing this for the package corresponding to the current
// feature.
@@ -312,15 +297,15 @@
return resEntriesToKeep.build();
}
- private void traceXmlForResourceId(int id) {
+ private void traceXmlForResourceId(int id, ResourceShrinkerCallback callback) {
Set<String> xmlFiles = getResourceIdToXmlFiles().get(id);
if (xmlFiles != null) {
for (String xmlFile : xmlFiles) {
InputStream inputStream = xmlFileProviders.get(xmlFile).get();
Resource resource = r8ResourceShrinkerModel.getResourceStore().getResource(id);
- traceXml(xmlFile, inputStream, inlinedIds -> pruneModel(resource, inlinedIds));
+ traceXml(xmlFile, inputStream, inlinedIds -> pruneModel(resource, inlinedIds), callback);
if (duplicatedResFolderEntries.contains(xmlFile)) {
- traceDuplicatedXmlFileIds(id, xmlFile);
+ traceDuplicatedXmlFileIds(id, xmlFile, callback);
}
}
}
@@ -333,18 +318,22 @@
}
}
- private void traceDuplicatedXmlFileIds(int currentId, String xmlFile) {
+ private void traceDuplicatedXmlFileIds(
+ int currentId, String xmlFile, ResourceShrinkerCallback callback) {
for (Map.Entry<Integer, Set<String>> entry : getResourceIdToXmlFiles().entrySet()) {
if (entry.getValue().contains(xmlFile)) {
if (entry.getKey() != currentId) {
- trace(entry.getKey(), "Duplicated xmlfile " + xmlFile);
+ trace(entry.getKey(), "Duplicated xmlfile " + xmlFile, callback);
}
}
}
}
private void traceXml(
- String xmlFile, InputStream inputStream, Consumer<IntSet> inlinedIdsConsumer) {
+ String xmlFile,
+ InputStream inputStream,
+ Consumer<IntSet> inlinedIdsConsumer,
+ ResourceShrinkerCallback callback) {
try {
XmlNode xmlNode;
if (changedXmlFiles.containsKey(xmlFile)) {
@@ -358,12 +347,12 @@
changedXmlFiles.put(xmlFile, xmlNode.toByteArray());
}
}
- visitNode(xmlNode, xmlFile, null);
+ visitNode(xmlNode, xmlFile, null, callback);
// Ensure that we trace the transitive reachable ids, without us having to iterate all
// resources for the reachable marker.
ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(xmlNode, r8ResourceShrinkerModel)
.iterator()
- .forEachRemaining(resource -> trace(resource.value, xmlFile));
+ .forEachRemaining(resource -> trace(resource.value, xmlFile, callback));
} catch (IOException e) {
errorHandler.apply(e);
}
@@ -410,21 +399,26 @@
return changedChildren;
}
- private void tryEnqueuerOnString(String possibleClass, String xmlName) {
+ private void tryEnqueuerOnString(
+ String possibleClass, String xmlName, ResourceShrinkerCallback callback) {
// There are a lot of xml tags and attributes that are evaluated over and over, if it is
// not a class, ignore it.
if (seenNoneClassValues.contains(possibleClass)) {
return;
}
- if (!enqueuerCallback.tryClass(possibleClass, xmlName, true)) {
+ if (!callback.tryClass(possibleClass, xmlName, true)) {
seenNoneClassValues.add(possibleClass);
}
}
- private void visitNode(XmlNode xmlNode, String xmlName, String manifestPackageName) {
+ private void visitNode(
+ XmlNode xmlNode,
+ String xmlName,
+ String manifestPackageName,
+ ResourceShrinkerCallback callback) {
XmlElement element = xmlNode.getElement();
String xmlElementName = element.getName();
- tryEnqueuerOnString(xmlElementName, xmlName);
+ tryEnqueuerOnString(xmlElementName, xmlName, callback);
for (XmlAttribute xmlAttribute : element.getAttributeList()) {
if (xmlAttribute.getName().equals("package") && xmlElementName.equals("manifest")) {
@@ -443,44 +437,48 @@
.anyMatch(child -> child.getElement().getName().equals("intent-filter"));
if (isNotExported && !hasFilters) {
String fullyQualifiedName = getFullyQualifiedName(manifestPackageName, xmlAttribute);
- enqueuerCallback.tryClass(fullyQualifiedName, xmlName, false);
+ callback.tryClass(fullyQualifiedName, xmlName, false);
continue;
}
}
String value = xmlAttribute.getValue();
- tryEnqueuerOnString(value, xmlName);
+ tryEnqueuerOnString(value, xmlName, callback);
if (value.startsWith(".")) {
// package specific names, e.g. context
- getPackageNames().forEach(s -> tryEnqueuerOnString(s + value, xmlName));
+ getPackageNames().forEach(s -> tryEnqueuerOnString(s + value, xmlName, callback));
}
if (manifestPackageName != null) {
// Manifest case
- traceManifestSpecificValues(xmlName, manifestPackageName, xmlAttribute, element);
+ traceManifestSpecificValues(xmlName, manifestPackageName, xmlAttribute, element, callback);
}
if (xmlAttribute.getName().equals("onClick")
&& xmlAttribute.getNamespaceUri().equals("http://schemas.android.com/apk/res/android")) {
- methodCallback.tryMethod(xmlAttribute.getValue(), xmlName);
+ callback.tryMethod(xmlAttribute.getValue(), xmlName);
}
}
for (XmlNode node : element.getChildList()) {
- visitNode(node, xmlName, manifestPackageName);
+ visitNode(node, xmlName, manifestPackageName, callback);
}
}
private void traceManifestSpecificValues(
- String xmlName, String packageName, XmlAttribute xmlAttribute, XmlElement element) {
+ String xmlName,
+ String packageName,
+ XmlAttribute xmlAttribute,
+ XmlElement element,
+ ResourceShrinkerCallback callback) {
if (!SPECIAL_MANIFEST_ELEMENTS.contains(element.getName())) {
return;
}
// All elements can have package specific name attributes pointing at classes.
if (xmlAttribute.getName().equals("name")) {
- tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName);
+ tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName, callback);
}
// Application elements have multiple special case attributes, where the value is potentially
// a class name (unqualified).
if (element.getName().equals("application")) {
if (SPECIAL_APPLICATION_ATTRIBUTES.contains(xmlAttribute.getName())) {
- tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName);
+ tryEnqueuerOnString(getFullyQualifiedName(packageName, xmlAttribute), xmlName, callback);
}
}
}
@@ -552,8 +550,6 @@
}
public void enqueuerDone(boolean isFinalTreeshaking) {
- enqueuerCallback = null;
- methodCallback = null;
seenResourceIds.clear();
if (!isFinalTreeshaking) {
// After final tree shaking we will need the reachability bits to decide what to write out