| // 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 static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull; |
| |
| import com.android.tools.r8.graph.AppInfoWithClassHierarchy; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexClassAndField; |
| 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.DexMethod; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.FieldResolutionResult; |
| 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.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.analysis.type.TypeElement; |
| import com.android.tools.r8.ir.code.BasicBlock; |
| import com.android.tools.r8.ir.code.IRCode; |
| import com.android.tools.r8.ir.code.IRCodeUtils; |
| import com.android.tools.r8.ir.code.Instruction; |
| import com.android.tools.r8.ir.code.InvokeMethod; |
| import com.android.tools.r8.ir.code.Phi; |
| import com.android.tools.r8.ir.code.StaticGet; |
| import com.android.tools.r8.ir.code.StaticPut; |
| import com.android.tools.r8.ir.code.Value; |
| 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.InstantiationReason; |
| import com.android.tools.r8.shaking.KeepReason; |
| import com.android.tools.r8.utils.BitUtils; |
| import com.android.tools.r8.utils.OptionalBool; |
| import com.android.tools.r8.utils.Timing; |
| import com.android.tools.r8.utils.collections.ProgramMethodSet; |
| import com.google.common.collect.ImmutableSet; |
| 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.Consumer; |
| 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 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<? extends AppInfoWithClassHierarchy> 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(); |
| |
| // The findLiteExtensionByNumber() methods that have become live since the last fixpoint. |
| private final ProgramMethodSet findLiteExtensionByNumberMethods = ProgramMethodSet.create(); |
| |
| // Mapping from extension container types to the extensions for that type. |
| private final Map<DexType, Set<DexType>> extensionGraph = new IdentityHashMap<>(); |
| |
| public ProtoEnqueuerExtension(AppView<? extends AppInfoWithClassHierarchy> appView) { |
| ProtoShrinker protoShrinker = appView.protoShrinker(); |
| this.appView = appView; |
| this.decoder = protoShrinker.decoder; |
| this.factory = protoShrinker.factory; |
| this.references = protoShrinker.references; |
| } |
| |
| @Override |
| public void processNewlyLiveClass(DexProgramClass clazz, EnqueuerWorklist worklist) { |
| assert appView.appInfo().hasClassHierarchy(); |
| AppInfoWithClassHierarchy appInfo = appView.appInfo().withClassHierarchy(); |
| if (appInfo.isStrictSubtypeOf(clazz.type, references.generatedMessageLiteType)) { |
| markGeneratedMessageLiteSubtypeAsInstantiated(clazz, worklist); |
| } |
| } |
| |
| private void markGeneratedMessageLiteSubtypeAsInstantiated( |
| DexProgramClass clazz, EnqueuerWorklist worklist) { |
| if (clazz.isAbstract()) { |
| assert clazz.type == references.extendableMessageType; |
| return; |
| } |
| ProgramMethod dynamicMethod = clazz.lookupProgramMethod(references.dynamicMethod); |
| if (dynamicMethod != null) { |
| worklist.enqueueMarkInstantiatedAction( |
| clazz, |
| dynamicMethod, |
| InstantiationReason.REFLECTION, |
| KeepReason.reflectiveUseIn(dynamicMethod)); |
| } else { |
| assert false |
| : "Expected class `" + clazz.type.toSourceString() + "` to declare a dynamicMethod()"; |
| } |
| } |
| |
| /** |
| * 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(ProgramMethod method, ProgramDefinition context) { |
| if (references.isFindLiteExtensionByNumberMethod(method.getReference())) { |
| findLiteExtensionByNumberMethods.add(method); |
| return; |
| } |
| |
| if (!references.isDynamicMethod(method)) { |
| return; |
| } |
| |
| DexType holderType = method.getHolderType(); |
| if (seenButNotLiveProtos.containsKey(holderType)) { |
| // The proto is now live instead of dead. |
| liveProtos.put(holderType, seenButNotLiveProtos.remove(holderType)); |
| 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(holderType); |
| createProtoMessageInfoFromDynamicMethod(method, liveProtos); |
| } |
| |
| private void createProtoMessageInfoFromDynamicMethod( |
| ProgramMethod dynamicMethod, Map<DexType, ProtoMessageInfo> protos) { |
| DexType holder = dynamicMethod.getHolderType(); |
| assert !protos.containsKey(holder); |
| |
| IRCode code = dynamicMethod.buildIR(appView); |
| InvokeMethod newMessageInfoInvoke = |
| GeneratedMessageLiteShrinker.getNewMessageInfoInvoke(code, references); |
| ProtoMessageInfo protoMessageInfo = |
| newMessageInfoInvoke != null ? decoder.run(dynamicMethod, newMessageInfoInvoke) : null; |
| protos.put(holder, protoMessageInfo); |
| } |
| |
| @Override |
| public void notifyFixpoint(Enqueuer enqueuer, EnqueuerWorklist worklist, Timing timing) { |
| timing.begin("[Proto] Extend fixpoint"); |
| populateExtensionGraph(enqueuer); |
| |
| 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); |
| } |
| } |
| timing.end(); |
| } |
| |
| /** |
| * For each extension field referenced from any of the methods in {@link |
| * #findLiteExtensionByNumberMethods}, this method finds the definition of the field, and then |
| * adds an edge to the proto extension graph {@link #extensionGraph} based on the definition of |
| * the field. |
| * |
| * <p>Example: If the field is defined as below, then an edge is added from the container type |
| * MyProtoMessage (argument #0) to the extension type MyProtoMessage$Ext (argument #2). |
| * |
| * <pre> |
| * GeneratedMessageLite.newSingularGeneratedExtension( |
| * MyProtoMessage.getDefaultInstance(), |
| * MyProtoMessage.Ext.getDefaultInstance(), |
| * MyProtoMessage.Ext.getDefaultInstance(), |
| * null, |
| * 10, |
| * WireFormat.FieldType.MESSAGE, |
| * MyProtoMessage.Ext.class); |
| * </pre> |
| * |
| * In addition to {@code newSingularGeneratedExtension()} we also model {@code |
| * newRepeatedGeneratedExtension()}. Both of these methods forward to {@code |
| * GeneratedExtension.<init>()}, hence we also model this constructor for robustness against |
| * inlining. |
| */ |
| private void populateExtensionGraph(Enqueuer enqueuer) { |
| collectExtensionFields() |
| .forEach( |
| (clazz, extensionFields) -> { |
| ProgramMethod clinit = clazz.getProgramClassInitializer(); |
| if (clinit == null) { |
| assert false; // Should generally not happen. |
| return; |
| } |
| |
| IRCode code = clinit.buildIR(appView); |
| Map<DexEncodedField, StaticPut> uniqueStaticPuts = |
| IRCodeUtils.findUniqueStaticPuts(appView, code, extensionFields); |
| for (DexEncodedField extensionField : extensionFields) { |
| StaticPut staticPut = uniqueStaticPuts.get(extensionField); |
| if (staticPut == null) { |
| // Could happen after we have optimized the code. |
| assert enqueuer.getMode().isFinalTreeShaking(); |
| continue; |
| } |
| populateExtensionGraphWithExtensionFieldDefinition(staticPut); |
| } |
| }); |
| |
| // Clear the set of methods such that we don't re-analyze these methods upon the next fixpoint. |
| findLiteExtensionByNumberMethods.clear(); |
| } |
| |
| /** |
| * Finds the extension fields referenced in the methods in {@link |
| * #findLiteExtensionByNumberMethods}. |
| */ |
| private Map<DexProgramClass, Set<DexEncodedField>> collectExtensionFields() { |
| Map<DexProgramClass, Set<DexEncodedField>> extensionFieldsByClass = new IdentityHashMap<>(); |
| for (ProgramMethod findLiteExtensionByNumberMethod : findLiteExtensionByNumberMethods) { |
| IRCode code = findLiteExtensionByNumberMethod.buildIR(appView); |
| Set<Phi> seenPhis = Sets.newIdentityHashSet(); |
| for (BasicBlock block : code.blocks(BasicBlock::isReturnBlock)) { |
| Value returnValue = block.exit().asReturn().returnValue(); |
| collectExtensionFieldsFromValue( |
| returnValue, |
| seenPhis, |
| field -> |
| extensionFieldsByClass |
| .computeIfAbsent(field.getHolder(), ignore -> Sets.newIdentityHashSet()) |
| .add(field.getDefinition())); |
| } |
| } |
| return extensionFieldsByClass; |
| } |
| |
| private void collectExtensionFieldsFromValue( |
| Value returnValue, Set<Phi> seenPhis, Consumer<ProgramField> consumer) { |
| Value root = returnValue.getAliasedValue(); |
| if (root.isPhi()) { |
| Phi phi = root.asPhi(); |
| if (seenPhis.add(phi)) { |
| for (Value operand : phi.getOperands()) { |
| collectExtensionFieldsFromValue(operand, seenPhis, consumer); |
| } |
| } |
| return; |
| } |
| |
| if (root.isZero()) { |
| return; |
| } |
| |
| Instruction definition = root.definition; |
| if (definition.isStaticGet()) { |
| StaticGet staticGet = definition.asStaticGet(); |
| DexClassAndField field = |
| appView.appInfo().resolveField(staticGet.getField()).getResolutionPair(); |
| if (field == null || !field.isProgramField()) { |
| assert false; |
| return; |
| } |
| consumer.accept(field.asProgramField()); |
| return; |
| } |
| |
| assert definition.isInvokeMethodWithReceiver() |
| && references.isFindLiteExtensionByNumberMethod( |
| definition.asInvokeMethodWithReceiver().getInvokedMethod()); |
| } |
| |
| /** |
| * Updates {@link #extensionGraph} based on the definition of {@param staticPut}. |
| * |
| * <p>See also {@link #populateExtensionGraph}. |
| */ |
| private void populateExtensionGraphWithExtensionFieldDefinition(StaticPut staticPut) { |
| Value value = staticPut.value().getAliasedValue(); |
| if (value.isPhi()) { |
| return; |
| } |
| |
| Instruction extensionFactory = value.definition; |
| if (extensionFactory.isNewInstance()) { |
| extensionFactory = |
| extensionFactory.asNewInstance().getUniqueConstructorInvoke(appView.dexItemFactory()); |
| if (extensionFactory == null) { |
| assert false; |
| return; |
| } |
| } |
| |
| if (extensionFactory.isInvokeDirect() || extensionFactory.isInvokeStatic()) { |
| InvokeMethod invoke = extensionFactory.asInvokeMethod(); |
| DexMethod invokedMethod = invoke.getInvokedMethod(); |
| |
| TypeElement containerType, extensionType; |
| if (invokedMethod == references.generatedMessageLiteMethods.newRepeatedGeneratedExtension) { |
| containerType = invoke.arguments().get(0).getType(); |
| extensionType = invoke.arguments().get(1).getType(); |
| } else if (invokedMethod |
| == references.generatedMessageLiteMethods.newSingularGeneratedExtension) { |
| containerType = invoke.arguments().get(0).getType(); |
| extensionType = invoke.arguments().get(2).getType(); |
| } else if (references.generatedExtensionMethods.isConstructor(invokedMethod)) { |
| containerType = invoke.arguments().get(1).getType(); |
| extensionType = invoke.arguments().get(3).getType(); |
| } else { |
| return; |
| } |
| |
| if (extensionType.isNullType()) { |
| return; // Extension is a primitive type. |
| } |
| |
| if (!containerType.isClassType() || !extensionType.isClassType()) { |
| assert false; // Should generally not happen. |
| return; |
| } |
| |
| extensionGraph |
| .computeIfAbsent( |
| containerType.asClassType().getClassType(), ignore -> Sets.newIdentityHashSet()) |
| .add(extensionType.asClassType().getClassType()); |
| } |
| } |
| |
| 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; |
| } |
| |
| ProgramMethod dynamicMethod = protoMessageInfo.getDynamicMethod(); |
| for (ProtoFieldInfo protoFieldInfo : protoMessageInfo.getFields()) { |
| ProgramField 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.registerReflectiveFieldAccess(valueStorage.getReference(), 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.registerReflectiveFieldAccess(valueStorage.getReference(), dynamicMethod); |
| worklist.enqueueMarkFieldAsReachableAction( |
| valueStorage, dynamicMethod, KeepReason.reflectiveUseIn(dynamicMethod)); |
| valueStorageIsLive = true; |
| } else { |
| valueStorageIsLive = false; |
| } |
| |
| ProgramField 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); |
| enqueuer.registerReflectiveFieldAccess(valueStorage.getReference(), dynamicMethod); |
| } |
| } 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()) { |
| ProgramField oneOfCaseField = |
| protoFieldInfo.getOneOfCaseField(appView, protoMessageInfo); |
| if (oneOfCaseField != null && enqueuer.isFieldLive(oneOfCaseField)) { |
| newlyLiveField = valueStorage; |
| } |
| } else if (protoFieldInfo.hasHazzerBitField(protoMessageInfo)) { |
| ProgramField hazzerBitField = |
| protoFieldInfo.getHazzerBitField(appView, protoMessageInfo); |
| if (hazzerBitField == null || !enqueuer.isFieldLive(hazzerBitField)) { |
| continue; |
| } |
| |
| if (appView.options().enableFieldBitAccessAnalysis && appView.isAllCodeProcessed()) { |
| FieldOptimizationInfo optimizationInfo = |
| hazzerBitField.getDefinition().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. |
| ProgramMethod defaultInitializer = |
| dynamicMethod.getHolder().getProgramDefaultInitializer(); |
| assert defaultInitializer != null; |
| Predicate<ProgramMethod> neitherDefaultConstructorNorDynamicMethod = |
| writer -> |
| !writer.isStructurallyEqualTo(defaultInitializer) |
| && !writer.isStructurallyEqualTo(dynamicMethod); |
| if (enqueuer.isFieldWrittenInMethodSatisfying( |
| newlyLiveField, neitherDefaultConstructorNorDynamicMethod)) { |
| enqueuer.registerReflectiveFieldRead(newlyLiveField.getReference(), dynamicMethod); |
| } |
| |
| // Unconditionally register the hazzer and one-of proto fields as written from |
| // dynamicMethod(). |
| if (enqueuer.registerReflectiveFieldWrite(newlyLiveField.getReference(), dynamicMethod)) { |
| worklist.enqueueMarkFieldAsReachableAction( |
| newlyLiveField, dynamicMethod, 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; |
| } |
| |
| ProgramMethod dynamicMethod = protoMessageInfo.getDynamicMethod(); |
| if (!dynamicMethodsWithTracedProtoObjects.add(dynamicMethod.getDefinition())) { |
| 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. |
| ProgramField 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 oneOfCaseFieldReference = oneOfCaseObject.asLiveProtoFieldObject().getField(); |
| FieldResolutionResult oneOfCaseFieldResolutionResult = |
| appView.appInfo().resolveField(oneOfCaseFieldReference); |
| if (oneOfCaseFieldResolutionResult.isFailedOrUnknownResolution()) { |
| assert false; |
| return; |
| } |
| |
| ProgramField oneOfCaseField = |
| oneOfCaseFieldResolutionResult |
| .asSuccessfulResolution() |
| .getResolutionPair() |
| .asProgramField(); |
| if (oneOfCaseField == null) { |
| assert false; |
| return; |
| } |
| |
| ProgramMethod dynamicMethod = |
| oneOfCaseField.getHolder().lookupProgramMethod(references.dynamicMethod); |
| if (dynamicMethod == null) { |
| assert false; |
| return; |
| } |
| |
| if (!enqueuer.isFieldLive(oneOfCaseField)) { |
| return; |
| } |
| |
| ProtoFieldObject oneOfObject = oneOfObjectPair.getOneOfObject(); |
| if (!oneOfObject.isLiveProtoFieldObject()) { |
| assert false; |
| return; |
| } |
| |
| DexField oneOfFieldReference = oneOfObject.asLiveProtoFieldObject().getField(); |
| FieldResolutionResult oneOfFieldResolutionResult = |
| appView.appInfo().resolveField(oneOfFieldReference); |
| if (oneOfFieldResolutionResult.isFailedOrUnknownResolution()) { |
| assert false; |
| return; |
| } |
| |
| ProgramField oneOfField = |
| oneOfFieldResolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField(); |
| if (oneOfField == null || oneOfField.getHolder() != oneOfCaseField.getHolder()) { |
| assert false; |
| return; |
| } |
| |
| if (enqueuer.registerReflectiveFieldWrite(oneOfField.getReference(), dynamicMethod)) { |
| worklist.enqueueMarkFieldAsReachableAction( |
| oneOfField, dynamicMethod, 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); |
| if (protoMessageInfo != null) { |
| return reachesMapOrRequiredField(protoMessageInfo); |
| } |
| assert false : "Unable to find proto message info for `" + baseMessageType + "`"; |
| } |
| 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() && !extensionGraph.containsKey(protoMessageInfo.getType())) { |
| 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. |
| if (protoMessageInfo.hasFields()) { |
| for (ProtoFieldInfo protoFieldInfo : protoMessageInfo.getFields()) { |
| if (reachesMapOrRequiredField(protoFieldInfo)) { |
| reachesMapOrRequiredFieldFromMessageCache.put(protoMessageInfo, OptionalBool.of(true)); |
| return true; |
| } |
| } |
| } |
| |
| Iterable<DexType> extensionTypes = |
| extensionGraph.getOrDefault(protoMessageInfo.getType(), ImmutableSet.of()); |
| for (DexType extensionType : extensionTypes) { |
| ProtoMessageInfo protoExtensionMessageInfo = getOrCreateProtoMessageInfo(extensionType); |
| assert protoExtensionMessageInfo != null; |
| if (reachesMapOrRequiredField(protoExtensionMessageInfo)) { |
| 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); |
| } |
| |
| DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type)); |
| if (clazz == null) { |
| seenButNotLiveProtos.put(type, null); |
| return null; |
| } |
| |
| ProgramMethod dynamicMethod = clazz.lookupProgramMethod(references.dynamicMethod); |
| if (dynamicMethod == null) { |
| seenButNotLiveProtos.put(type, null); |
| return null; |
| } |
| |
| createProtoMessageInfoFromDynamicMethod(dynamicMethod, seenButNotLiveProtos); |
| |
| return seenButNotLiveProtos.get(type); |
| } |
| } |