blob: d9547d932bfab08d365925bd2290129e8a4fe7b5 [file] [log] [blame]
// Copyright (c) 2023, 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.build.shrinker.r8integration.R8ResourceShrinkerState;
import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
import com.android.tools.r8.AndroidResourceInput;
import com.android.tools.r8.AndroidResourceInput.Kind;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
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.DexType;
import com.android.tools.r8.graph.DexValue;
import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
import com.android.tools.r8.graph.ProgramField;
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.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.NewArrayEmpty;
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.shaking.Enqueuer;
import com.android.tools.r8.shaking.EnqueuerWorklist;
import com.android.tools.r8.shaking.KeepFieldInfo;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.Timing;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
public class ResourceAccessAnalysis implements EnqueuerFieldAccessAnalysis {
private final R8ResourceShrinkerState resourceShrinkerState;
private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final Enqueuer enqueuer;
private ResourceAccessAnalysis(
AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
this.appView = appView;
this.enqueuer = enqueuer;
this.resourceShrinkerState = new R8ResourceShrinkerState();
try {
for (AndroidResourceInput androidResource :
appView.options().androidResourceProvider.getAndroidResources()) {
if (androidResource.getKind() == Kind.RESOURCE_TABLE) {
resourceShrinkerState.setResourceTableInput(androidResource.getByteStream());
break;
}
}
} catch (ResourceException e) {
throw appView.reporter().fatalError("Failed initializing resource table");
}
}
public static void register(
AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
if (enabled(appView)) {
enqueuer.registerFieldAccessAnalysis(new ResourceAccessAnalysis(appView, enqueuer));
}
}
@Override
public void done(Enqueuer enqueuer) {
appView.setResourceAnalysisResult(
new ResourceAnalysisResult(resourceShrinkerState, fieldToValueMapping));
EnqueuerFieldAccessAnalysis.super.done(enqueuer);
}
private static boolean enabled(AppView<? extends AppInfoWithClassHierarchy> appView) {
return appView.options().androidResourceProvider != null
&& appView.options().resourceShrinkerConfiguration.isOptimizedShrinking();
}
@Override
public void traceStaticFieldRead(
DexField field,
SingleFieldResolutionResult<?> resolutionResult,
ProgramMethod context,
EnqueuerWorklist worklist) {
ProgramField resolvedField = resolutionResult.getProgramField();
if (resolvedField == null) {
return;
}
if (getMaybeCachedIsRClass(resolvedField.getHolder())) {
DexType holderType = resolvedField.getHolderType();
if (!fieldToValueMapping.containsKey(holderType)) {
populateRClassValues(resolvedField);
}
assert fieldToValueMapping.containsKey(holderType);
RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
IntList integers = rClassFieldToValueStore.valueMapping.get(field);
enqueuer.applyMinimumKeepInfoWhenLive(
resolvedField, KeepFieldInfo.newEmptyJoiner().disallowOptimization());
for (Integer integer : integers) {
resourceShrinkerState.trace(integer);
}
}
}
@SuppressWarnings("ReferenceEquality")
private void populateRClassValues(ProgramField field) {
// 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(field, rClassValueBuilder);
ProgramMethod programClassInitializer = field.getHolder().getProgramClassInitializer();
if (programClassInitializer != null) {
analyzeClassInitializer(rClassValueBuilder, programClassInitializer);
}
fieldToValueMapping.put(field.getHolderType(), rClassValueBuilder.build());
}
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()) {
continue;
}
IntList values;
Instruction definition = staticPut.value().definition;
if (definition.isConstNumber()) {
values = new IntArrayList(1);
values.add(definition.asConstNumber().getIntValue());
} else if (definition.isNewArrayEmpty()) {
NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
values = new IntArrayList();
for (Instruction uniqueUser : newArrayEmpty.outValue().uniqueUsers()) {
if (uniqueUser.isArrayPut()) {
Value constValue = uniqueUser.asArrayPut().value();
if (constValue.isConstNumber()) {
values.add(constValue.getDefinition().asConstNumber().getIntValue());
}
} else {
assert uniqueUser == staticPut;
}
}
} else if (definition.isNewArrayFilled()) {
values = new IntArrayList();
for (Value inValue : definition.asNewArrayFilled().inValues()) {
if (value.isPhi()) {
continue;
}
Instruction valueDefinition = inValue.definition;
if (valueDefinition.isConstNumber()) {
values.add(valueDefinition.asConstNumber().getIntValue());
}
}
} else {
continue;
}
rClassValueBuilder.addMapping(staticPut.getField(), values);
}
}
private void analyzeStaticFields(
ProgramField field, RClassFieldToValueStore.Builder rClassValueBuilder) {
for (DexEncodedField staticField :
field.getHolder().staticFields(DexEncodedField::hasExplicitStaticValue)) {
DexValue staticValue = staticField.getStaticValue();
if (staticValue.isDexValueInt()) {
assert !enqueuer.getMode().isInitialTreeShaking();
IntList values = new IntArrayList(1);
values.add(staticValue.asDexValueInt().getValue());
rClassValueBuilder.addMapping(staticField.getReference(), values);
}
}
}
private final Map<DexProgramClass, Boolean> cachedClassLookups = new IdentityHashMap<>();
private boolean getMaybeCachedIsRClass(DexProgramClass holder) {
Boolean result = cachedClassLookups.get(holder);
if (result != null) {
return result;
}
String simpleClassName =
DescriptorUtils.getSimpleClassNameFromDescriptor(holder.getType().toDescriptorString());
List<String> split = StringUtils.split(simpleClassName, '$');
if (split.size() < 2) {
cachedClassLookups.put(holder, false);
return false;
}
String type = split.get(split.size() - 1);
String rClass = split.get(split.size() - 2);
// We match on R if:
// - The name of the Class is R$type - we allow R to be an inner class.
// - The inner type should be with lower case
boolean isRClass = Character.isLowerCase(type.charAt(0)) && rClass.equals("R");
cachedClassLookups.put(holder, isRClass);
return isRClass;
}
public static class ResourceAnalysisResult {
private final R8ResourceShrinkerState resourceShrinkerState;
private Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap;
private ResourceAnalysisResult(
R8ResourceShrinkerState resourceShrinkerState,
Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap) {
this.resourceShrinkerState = resourceShrinkerState;
this.rClassFieldToValueStoreMap = rClassFieldToValueStoreMap;
}
public R8ResourceShrinkerModel getModel() {
return resourceShrinkerState.getR8ResourceShrinkerModel();
}
public void rewrittenWithLens(GraphLens lens, GraphLens appliedLens, Timing timing) {
Map<DexType, DexType> changed = new IdentityHashMap<>();
for (DexType dexType : rClassFieldToValueStoreMap.keySet()) {
DexType rewritten = lens.lookupClassType(dexType, appliedLens);
if (rewritten.isNotIdenticalTo(dexType)) {
changed.put(dexType, rewritten);
}
}
if (!changed.isEmpty()) {
Map<DexType, RClassFieldToValueStore> rewrittenMap = new IdentityHashMap<>();
rClassFieldToValueStoreMap.forEach(
(type, map) -> {
rewrittenMap.put(changed.getOrDefault(type, type), map);
map.rewrittenWithLens(lens);
});
rClassFieldToValueStoreMap = rewrittenMap;
}
}
public void withoutPrunedItems(PrunedItems prunedItems, Timing timing) {
rClassFieldToValueStoreMap.keySet().removeIf(prunedItems::isRemoved);
rClassFieldToValueStoreMap.values().forEach(store -> store.pruneItems(prunedItems));
}
public String getSingleStringValueForField(ProgramField programField) {
RClassFieldToValueStore rClassFieldToValueStore =
rClassFieldToValueStoreMap.get(programField.getHolderType());
if (rClassFieldToValueStore == null) {
return null;
}
if (!rClassFieldToValueStore.hasField(programField.getReference())) {
return null;
}
return getModel()
.getStringResourcesWithSingleValue()
.get(rClassFieldToValueStore.getResourceId(programField.getReference()));
}
}
private static class RClassFieldToValueStore {
private Map<DexField, IntList> valueMapping;
private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
this.valueMapping = valueMapping;
}
boolean hasField(DexField field) {
return valueMapping.containsKey(field);
}
void pruneItems(PrunedItems prunedItems) {
valueMapping.keySet().removeIf(prunedItems::isRemoved);
}
int getResourceId(DexField field) {
IntList integers = valueMapping.get(field);
assert integers.size() == 1;
return integers.get(0);
}
@SuppressWarnings("ReferenceEquality")
public void rewrittenWithLens(GraphLens lens) {
Map<DexField, DexField> changed = new IdentityHashMap<>();
valueMapping
.keySet()
.forEach(
dexField -> {
DexField rewritten = lens.lookupField(dexField);
if (rewritten != dexField) {
changed.put(dexField, rewritten);
}
});
if (changed.size() > 0) {
Map<DexField, IntList> rewrittenMapping = new IdentityHashMap<>();
valueMapping.forEach(
(key, value) -> rewrittenMapping.put(changed.getOrDefault(key, key), value));
valueMapping = rewrittenMapping;
}
}
public static class Builder {
private final Map<DexField, IntList> valueMapping = new IdentityHashMap<>();
public void addMapping(DexField field, IntList values) {
assert !valueMapping.containsKey(field);
valueMapping.put(field, values);
}
public RClassFieldToValueStore build() {
return new RClassFieldToValueStore(valueMapping);
}
}
}
}