blob: 92954e4b7649dc87dacb8ad65f8c51750007abb9 [file] [log] [blame]
// Copyright (c) 2019, 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.ir.analysis.proto.schema;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
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.analysis.EnqueuerAnalysis;
import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteShrinker;
import com.android.tools.r8.ir.analysis.proto.ProtoEnqueuerUseRegistry;
import com.android.tools.r8.ir.analysis.proto.ProtoReferences;
import com.android.tools.r8.ir.analysis.proto.ProtoShrinker;
import com.android.tools.r8.ir.analysis.proto.RawMessageInfoDecoder;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.EnqueuerWorklist;
import com.android.tools.r8.shaking.KeepReason;
import com.android.tools.r8.utils.BitUtils;
import com.android.tools.r8.utils.OptionalBool;
import com.google.common.collect.Sets;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
// TODO(b/112437944): Handle cycles in the graph + add a test that fails with the current
// implementation. The current caching mechanism is unsafe, because we may mark a message as not
// containing a map/required field in presence of cycles, although it does.
// TODO(b/112437944): Handle extensions in the map/required field detection + add a test that fails
// with the current implementation. If there is a field whose type is an extension, then we should
// look if any of the applicable extensions could contain a map/required field.
// TODO(b/112437944): Handle incomplete information about extensions + add a test that fails with
// the current implementation. If there are some extensions that cannot be resolved, then we should
// keep fields that could reach extensions to be conservative.
public class ProtoEnqueuerExtension extends EnqueuerAnalysis {
private final AppView<?> appView;
private final RawMessageInfoDecoder decoder;
private final ProtoFieldTypeFactory factory;
private final ProtoReferences references;
// Mapping for the set of proto message that have already become live.
private final Map<DexType, ProtoMessageInfo> liveProtos = new IdentityHashMap<>();
// Mapping for additional proto messages that have not yet become live. If there is a proto that
// has become live, then its schema may refer to another proto message that has not yet become
// live. In that case, we still need to decode the schema of the not-yet-live proto message,
// because we need to check if it has a required/map field.
private final Map<DexType, ProtoMessageInfo> seenButNotLiveProtos = new IdentityHashMap<>();
// To cache whether a proto message contains a map/required field directly or indirectly.
private final Map<ProtoMessageInfo, OptionalBool> reachesMapOrRequiredFieldFromMessageCache =
new IdentityHashMap<>();
// Keeps track of the set of dynamicMethod() methods for which we have traced const-class and
// static-get instructions.
private final Set<DexEncodedMethod> dynamicMethodsWithTracedProtoObjects =
Sets.newIdentityHashSet();
public ProtoEnqueuerExtension(AppView<?> appView) {
ProtoShrinker protoShrinker = appView.protoShrinker();
this.appView = appView;
this.decoder = protoShrinker.decoder;
this.factory = protoShrinker.factory;
this.references = protoShrinker.references;
}
/**
* When a dynamicMethod() of a proto message becomes live, then build the corresponding {@link
* ProtoMessageInfo} object, and create a mapping from the holder to it.
*/
@Override
public void processNewlyLiveMethod(DexEncodedMethod encodedMethod) {
if (!references.isDynamicMethod(encodedMethod)) {
return;
}
DexType holder = encodedMethod.method.holder;
if (seenButNotLiveProtos.containsKey(holder)) {
// The proto is now live instead of dead.
liveProtos.put(holder, seenButNotLiveProtos.remove(holder));
return;
}
// Since this dynamicMethod() only becomes live once, and it has just become live, it must be
// the case that the proto is not already live.
assert !liveProtos.containsKey(holder);
createProtoMessageInfoFromDynamicMethod(encodedMethod, liveProtos);
}
private void createProtoMessageInfoFromDynamicMethod(
DexEncodedMethod dynamicMethod, Map<DexType, ProtoMessageInfo> protos) {
DexType holder = dynamicMethod.method.holder;
assert !protos.containsKey(holder);
DexClass context = appView.definitionFor(holder);
if (context == null || !context.isProgramClass()) {
// TODO(b/112437944): What if a proto message references a proto message on the classpath or
// library path? We should treat them as having a map/required field to be conservative.
assert false; // Should generally not happen.
return;
}
IRCode code = dynamicMethod.buildIR(appView, context.origin);
InvokeMethod newMessageInfoInvoke =
GeneratedMessageLiteShrinker.getNewMessageInfoInvoke(code, references);
ProtoMessageInfo protoMessageInfo =
newMessageInfoInvoke != null
? decoder.run(dynamicMethod, context, newMessageInfoInvoke)
: null;
protos.put(holder, protoMessageInfo);
}
@Override
public void notifyFixpoint(Enqueuer enqueuer, EnqueuerWorklist worklist) {
markMapOrRequiredFieldsAsReachable(enqueuer, worklist);
// The ProtoEnqueuerUseRegistry does not trace the const-class instructions in dynamicMethod().
// Therefore, we manually trace the const-class instructions in each dynamicMethod() here,
// ***but only those that will remain after the proto schema has been optimized***.
if (enqueuer.getUseRegistryFactory() == ProtoEnqueuerUseRegistry.getFactory()) {
// We only use the ProtoEnqueuerUseRegistry in the second round of tree shaking. This means
// that the initial round of tree shaking will be less precise, but likely faster.
assert enqueuer.getMode().isFinalTreeShaking();
if (worklist.isEmpty()) {
tracePendingInstructionsInDynamicMethods(enqueuer, worklist);
}
}
}
private void markMapOrRequiredFieldsAsReachable(Enqueuer enqueuer, EnqueuerWorklist worklist) {
// TODO(b/112437944): We only need to check if a given field can reach a map/required field
// once. Maybe maintain a map `newlyLiveProtos` that store the set of proto messages that have
// become live since the last intermediate fixpoint.
// TODO(b/112437944): We only need to visit the subset of protos in `liveProtos` that has at
// least one field that is not yet live. Maybe split `liveProtos` into `partiallyLiveProtos`
// and `fullyLiveProtos`.
for (ProtoMessageInfo protoMessageInfo : liveProtos.values()) {
if (protoMessageInfo == null || !protoMessageInfo.hasFields()) {
continue;
}
DexEncodedMethod dynamicMethod = protoMessageInfo.getDynamicMethod();
DexProgramClass clazz = appView.definitionFor(dynamicMethod.method.holder).asProgramClass();
for (ProtoFieldInfo protoFieldInfo : protoMessageInfo.getFields()) {
DexEncodedField valueStorage = protoFieldInfo.getValueStorage(appView, protoMessageInfo);
if (valueStorage == null) {
continue;
}
boolean valueStorageIsLive;
if (enqueuer.isFieldLive(valueStorage)) {
if (enqueuer.isFieldRead(valueStorage)
|| enqueuer.isFieldWrittenOutsideDefaultConstructor(valueStorage)
|| reachesMapOrRequiredField(protoFieldInfo)) {
// Mark that the field is both read and written by reflection such that we do not
// (i) optimize field reads into loading the default value of the field or (ii) remove
// field writes to proto fields that could be read using reflection by the proto
// library.
enqueuer.registerFieldAccess(valueStorage.field, dynamicMethod);
}
valueStorageIsLive = true;
} else if (reachesMapOrRequiredField(protoFieldInfo)) {
// Map/required fields cannot be removed. Therefore, we mark such fields as both read and
// written such that we cannot optimize any field reads or writes.
enqueuer.registerFieldAccess(valueStorage.field, dynamicMethod);
worklist.enqueueMarkReachableFieldAction(
clazz, valueStorage, KeepReason.reflectiveUseIn(dynamicMethod));
valueStorageIsLive = true;
} else {
valueStorageIsLive = false;
}
DexEncodedField newlyLiveField = null;
if (valueStorageIsLive) {
// For one-of fields, mark the corresponding one-of-case field as live, and for proto2
// singular fields, mark the corresponding hazzer-bit field as live.
if (protoFieldInfo.getType().isOneOf()) {
newlyLiveField = protoFieldInfo.getOneOfCaseField(appView, protoMessageInfo);
} else if (protoFieldInfo.hasHazzerBitField(protoMessageInfo)) {
newlyLiveField = protoFieldInfo.getHazzerBitField(appView, protoMessageInfo);
}
} else {
// For one-of fields, mark the one-of field as live if the one-of-case field is live, and
// for proto2 singular fields, mark the field as live if the corresponding hazzer-bit
// field is live.
if (protoFieldInfo.getType().isOneOf()) {
DexEncodedField oneOfCaseField =
protoFieldInfo.getOneOfCaseField(appView, protoMessageInfo);
if (oneOfCaseField != null && enqueuer.isFieldLive(oneOfCaseField)) {
newlyLiveField = valueStorage;
}
} else if (protoFieldInfo.hasHazzerBitField(protoMessageInfo)) {
DexEncodedField hazzerBitField =
protoFieldInfo.getHazzerBitField(appView, protoMessageInfo);
if (hazzerBitField == null || !enqueuer.isFieldLive(hazzerBitField)) {
continue;
}
if (appView.options().enableFieldBitAccessAnalysis && appView.isAllCodeProcessed()) {
FieldOptimizationInfo optimizationInfo = hazzerBitField.getOptimizationInfo();
int hazzerBitIndex = protoFieldInfo.getHazzerBitFieldIndex(protoMessageInfo);
if (!BitUtils.isBitSet(optimizationInfo.getReadBits(), hazzerBitIndex)) {
continue;
}
}
newlyLiveField = valueStorage;
}
}
if (newlyLiveField != null) {
// Mark hazzer and one-of proto fields as read from dynamicMethod() if they are written in
// the app. This is needed to ensure that field writes are not removed from the app.
DexEncodedMethod defaultInitializer = clazz.getDefaultInitializer();
assert defaultInitializer != null;
Predicate<DexEncodedMethod> neitherDefaultConstructorNorDynamicMethod =
writer -> writer != defaultInitializer && writer != dynamicMethod;
if (enqueuer.isFieldWrittenInMethodSatisfying(
newlyLiveField, neitherDefaultConstructorNorDynamicMethod)) {
enqueuer.registerFieldRead(newlyLiveField.field, dynamicMethod);
}
// Unconditionally register the hazzer and one-of proto fields as written from
// dynamicMethod().
if (enqueuer.registerFieldWrite(newlyLiveField.field, dynamicMethod)) {
worklist.enqueueMarkReachableFieldAction(
clazz, newlyLiveField, KeepReason.reflectiveUseIn(dynamicMethod));
}
}
}
registerWriteToOneOfObjectsWithLiveOneOfCaseObject(protoMessageInfo, enqueuer, worklist);
}
}
private void tracePendingInstructionsInDynamicMethods(
Enqueuer enqueuer, EnqueuerWorklist worklist) {
for (ProtoMessageInfo protoMessageInfo : liveProtos.values()) {
if (protoMessageInfo == null || !protoMessageInfo.hasFields()) {
continue;
}
DexEncodedMethod dynamicMethod = protoMessageInfo.getDynamicMethod();
if (!dynamicMethodsWithTracedProtoObjects.add(dynamicMethod)) {
continue;
}
for (ProtoFieldInfo protoFieldInfo : protoMessageInfo.getFields()) {
List<ProtoObject> objects = protoFieldInfo.getObjects();
if (objects == null || objects.isEmpty()) {
// Nothing to trace.
continue;
}
// NOTE: If `valueStorage` is not a live field, then code for it will not be emitted in the
// schema, and therefore we do need to trace the const-class instructions that will be
// emitted for it.
DexEncodedField valueStorage = protoFieldInfo.getValueStorage(appView, protoMessageInfo);
if (valueStorage != null && enqueuer.isFieldLive(valueStorage)) {
for (ProtoObject object : objects) {
if (object.isProtoObjectFromStaticGet()) {
worklist.enqueueTraceStaticFieldRead(
object.asProtoObjectFromStaticGet().getField(), dynamicMethod);
} else if (object.isProtoTypeObject()) {
worklist.enqueueTraceConstClassAction(
object.asProtoTypeObject().getType(), dynamicMethod);
}
}
}
}
}
}
/** Marks each oneof field whose corresponding oneof-case field is live as being written. */
private void registerWriteToOneOfObjectsWithLiveOneOfCaseObject(
ProtoMessageInfo protoMessageInfo, Enqueuer enqueuer, EnqueuerWorklist worklist) {
if (protoMessageInfo.numberOfOneOfObjects() == 0) {
return;
}
for (ProtoOneOfObjectPair oneOfObjectPair : protoMessageInfo.getOneOfObjects()) {
registerWriteToOneOfObjectIfOneOfCaseObjectIsLive(oneOfObjectPair, enqueuer, worklist);
}
}
/** Marks the given oneof field as being written if the corresponding oneof-case field is live. */
private void registerWriteToOneOfObjectIfOneOfCaseObjectIsLive(
ProtoOneOfObjectPair oneOfObjectPair, Enqueuer enqueuer, EnqueuerWorklist worklist) {
ProtoFieldObject oneOfCaseObject = oneOfObjectPair.getOneOfCaseObject();
if (!oneOfCaseObject.isLiveProtoFieldObject()) {
assert false;
return;
}
DexField oneOfCaseField = oneOfCaseObject.asLiveProtoFieldObject().getField();
DexEncodedField encodedOneOfCaseField = appView.appInfo().resolveField(oneOfCaseField);
if (encodedOneOfCaseField == null) {
assert false;
return;
}
DexClass clazz = appView.definitionFor(encodedOneOfCaseField.field.holder);
if (clazz == null || !clazz.isProgramClass()) {
assert false;
return;
}
DexEncodedMethod dynamicMethod = clazz.lookupVirtualMethod(references::isDynamicMethod);
if (dynamicMethod == null) {
assert false;
return;
}
if (!enqueuer.isFieldLive(encodedOneOfCaseField)) {
return;
}
ProtoFieldObject oneOfObject = oneOfObjectPair.getOneOfObject();
if (!oneOfObject.isLiveProtoFieldObject()) {
assert false;
return;
}
DexField oneOfField = oneOfObject.asLiveProtoFieldObject().getField();
DexEncodedField encodedOneOfField = appView.appInfo().resolveField(oneOfField);
if (encodedOneOfField == null) {
assert false;
return;
}
if (encodedOneOfField.field.holder != encodedOneOfCaseField.field.holder) {
assert false;
return;
}
if (enqueuer.registerFieldWrite(encodedOneOfField.field, dynamicMethod)) {
worklist.enqueueMarkReachableFieldAction(
clazz.asProgramClass(), encodedOneOfField, KeepReason.reflectiveUseIn(dynamicMethod));
}
}
/**
* Traverses the proto schema graph.
*
* @return true if this proto field contains a map/required field directly or indirectly.
*/
private boolean reachesMapOrRequiredField(ProtoFieldInfo protoFieldInfo) {
ProtoFieldType protoFieldType = protoFieldInfo.getType();
// If it is a map/required field, return true.
if (protoFieldType.isMap() || protoFieldType.isRequired()) {
return true;
}
if (!appView.options().protoShrinking().traverseOneOfAndRepeatedProtoFields) {
if (protoFieldType.isOneOf() || protoFieldType.isRepeated()) {
return false;
}
}
// Otherwise, check if the type of the field may contain a map/required field.
DexType baseMessageType = protoFieldInfo.getBaseMessageType(factory);
if (baseMessageType != null) {
ProtoMessageInfo protoMessageInfo = getOrCreateProtoMessageInfo(baseMessageType);
assert protoMessageInfo != null;
return reachesMapOrRequiredField(protoMessageInfo);
}
return false;
}
/**
* Traverses the proto schema graph.
*
* @return true if this proto message contains a map/required field directly or indirectly.
*/
private boolean reachesMapOrRequiredField(ProtoMessageInfo protoMessageInfo) {
if (!protoMessageInfo.hasFields()) {
return false;
}
OptionalBool cache =
reachesMapOrRequiredFieldFromMessageCache.getOrDefault(
protoMessageInfo, OptionalBool.unknown());
if (!cache.isUnknown()) {
return cache.isTrue();
}
// To guard against infinite recursion, we set the cache for this message to false, although
// we may later find out that this message actually contains a map/required field.
reachesMapOrRequiredFieldFromMessageCache.put(protoMessageInfo, OptionalBool.of(false));
// Check if any of the fields contains a map/required field.
for (ProtoFieldInfo protoFieldInfo : protoMessageInfo.getFields()) {
if (reachesMapOrRequiredField(protoFieldInfo)) {
reachesMapOrRequiredFieldFromMessageCache.put(protoMessageInfo, OptionalBool.of(true));
return true;
}
}
return false;
}
private ProtoMessageInfo getOrCreateProtoMessageInfo(DexType type) {
if (liveProtos.containsKey(type)) {
return liveProtos.get(type);
}
if (seenButNotLiveProtos.containsKey(type)) {
return seenButNotLiveProtos.get(type);
}
DexClass clazz = appView.definitionFor(type);
if (clazz == null || !clazz.isProgramClass()) {
seenButNotLiveProtos.put(type, null);
return null;
}
DexEncodedMethod dynamicMethod = clazz.lookupVirtualMethod(references::isDynamicMethod);
if (dynamicMethod == null) {
seenButNotLiveProtos.put(type, null);
return null;
}
createProtoMessageInfoFromDynamicMethod(dynamicMethod, seenButNotLiveProtos);
return seenButNotLiveProtos.get(type);
}
}