blob: 0ca8fac0a10f4dc720eb82f82d482eeaca1d036b [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.OptionalBool;
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.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.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 java.util.IdentityHashMap;
import java.util.Map;
// 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<>();
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(context, newMessageInfoInvoke) : null;
protos.put(holder, protoMessageInfo);
}
@Override
public void notifyFixpoint(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;
}
for (ProtoFieldInfo protoFieldInfo : protoMessageInfo.getFields()) {
DexField valueStorage = protoFieldInfo.getValueStorage(protoMessageInfo);
DexEncodedField encodedValueStorage = appView.appInfo().resolveField(valueStorage);
if (encodedValueStorage == null) {
continue;
}
DexClass clazz = appView.definitionFor(encodedValueStorage.field.holder);
if (clazz == null || !clazz.isProgramClass()) {
assert false; // Should generally not happen.
continue;
}
DexEncodedMethod dynamicMethod = clazz.lookupVirtualMethod(references::isDynamicMethod);
if (dynamicMethod == null) {
assert false; // Should generally not happen.
continue;
}
boolean encodedValueStorageIsLive;
if (enqueuer.isFieldLive(encodedValueStorage)) {
if (enqueuer.isFieldRead(encodedValueStorage)
|| enqueuer.isFieldWrittenOutsideDefaultConstructor(encodedValueStorage)) {
// 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(encodedValueStorage.field, dynamicMethod);
}
encodedValueStorageIsLive = 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(encodedValueStorage.field, dynamicMethod);
worklist.enqueueMarkReachableFieldAction(
clazz.asProgramClass(),
encodedValueStorage,
KeepReason.reflectiveUseIn(dynamicMethod));
encodedValueStorageIsLive = true;
} else {
encodedValueStorageIsLive = false;
}
DexEncodedField newlyLiveField = null;
if (encodedValueStorageIsLive) {
// 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 =
appView.appInfo().resolveField(protoFieldInfo.getOneOfCaseField(protoMessageInfo));
} else if (protoFieldInfo.hasHazzerBitField(protoMessageInfo)) {
newlyLiveField =
appView.appInfo().resolveField(protoFieldInfo.getHazzerBitField(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()) {
DexField oneOfCaseField = protoFieldInfo.getOneOfCaseField(protoMessageInfo);
DexEncodedField encodedOneOfCaseField = appView.appInfo().resolveField(oneOfCaseField);
if (encodedOneOfCaseField != null && enqueuer.isFieldLive(encodedOneOfCaseField)) {
newlyLiveField = encodedValueStorage;
}
} else if (protoFieldInfo.hasHazzerBitField(protoMessageInfo)) {
DexField hazzerBitField = protoFieldInfo.getHazzerBitField(protoMessageInfo);
DexEncodedField encodedHazzerBitField = appView.appInfo().resolveField(hazzerBitField);
if (encodedHazzerBitField == null || !enqueuer.isFieldLive(encodedHazzerBitField)) {
continue;
}
if (appView.options().enableFieldBitAccessAnalysis && appView.isAllCodeProcessed()) {
FieldOptimizationInfo optimizationInfo = encodedHazzerBitField.getOptimizationInfo();
int hazzerBitIndex = protoFieldInfo.getHazzerBitFieldIndex(protoMessageInfo);
if (!BitUtils.isBitSet(optimizationInfo.getReadBits(), hazzerBitIndex)) {
continue;
}
}
newlyLiveField = encodedValueStorage;
}
}
if (newlyLiveField != null) {
if (enqueuer.registerFieldWrite(newlyLiveField.field, dynamicMethod)) {
worklist.enqueueMarkReachableFieldAction(
clazz.asProgramClass(), newlyLiveField, KeepReason.reflectiveUseIn(dynamicMethod));
}
}
}
registerWriteToOneOfObjectsWithLiveOneOfCaseObject(protoMessageInfo, enqueuer, worklist);
}
}
/** 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;
}
// 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);
}
}