blob: c2cd2aa3e59289bb0c365968ac792ce8244d2ccc [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;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
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.ProgramMethod;
import com.android.tools.r8.graph.SubtypingInfo;
import com.android.tools.r8.graph.analysis.EnqueuerAnalysis;
import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
import com.android.tools.r8.ir.code.CheckCast;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.CallGraph.Node;
import com.android.tools.r8.ir.conversion.IRConverter;
import com.android.tools.r8.ir.conversion.MethodProcessor;
import com.android.tools.r8.ir.optimize.Inliner;
import com.android.tools.r8.ir.optimize.Inliner.Reason;
import com.android.tools.r8.ir.optimize.enums.EnumValueOptimizer;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
import com.android.tools.r8.ir.optimize.inliner.FixedInliningReasonStrategy;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.EnqueuerWorklist;
import com.android.tools.r8.utils.ObjectUtils;
import com.android.tools.r8.utils.PredicateSet;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.BooleanSupplier;
public class GeneratedMessageLiteBuilderShrinker {
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final ProtoReferences references;
private final boolean enableAggressiveBuilderOptimization;
private final Map<DexProgramClass, ProgramMethod> builders = new IdentityHashMap<>();
GeneratedMessageLiteBuilderShrinker(
AppView<? extends AppInfoWithClassHierarchy> appView, ProtoReferences references) {
this.appView = appView;
this.references = references;
this.enableAggressiveBuilderOptimization = computeEnableAggressiveBuilderOptimization();
// If this fails it is likely an unsupported version of the protobuf library.
assert enableAggressiveBuilderOptimization;
}
private boolean computeEnableAggressiveBuilderOptimization() {
boolean unexpectedGeneratedMessageLiteBuilder =
ObjectUtils.getBooleanOrElse(
appView
.appInfo()
.definitionForWithoutExistenceAssert(references.generatedMessageLiteBuilderType),
clazz -> clazz.getMethodCollection().hasMethods(DexEncodedMethod::isAbstract),
true);
boolean unexpectedGeneratedMessageLiteExtendableBuilder =
ObjectUtils.getBooleanOrElse(
appView
.appInfo()
.definitionForWithoutExistenceAssert(
references.generatedMessageLiteExtendableBuilderType),
clazz -> clazz.getMethodCollection().hasMethods(DexEncodedMethod::isAbstract),
true);
if (unexpectedGeneratedMessageLiteBuilder) {
appView
.options()
.reporter
.warning(
"Unexpected implementation of `"
+ references.generatedMessageLiteBuilderType.toSourceString()
+ "`: disabling aggressive protobuf builder optimization.");
return false;
}
if (unexpectedGeneratedMessageLiteExtendableBuilder) {
appView
.options()
.reporter
.warning(
"Unexpected implementation of `"
+ references.generatedMessageLiteExtendableBuilderType.toSourceString()
+ "`: disabling aggressive protobuf builder optimization.");
return false;
}
return true;
}
public EnqueuerAnalysis createEnqueuerAnalysis() {
Set<DexProgramClass> seen = Sets.newIdentityHashSet();
return new EnqueuerAnalysis() {
@Override
public void notifyFixpoint(Enqueuer enqueuer, EnqueuerWorklist worklist, Timing timing) {
builders.forEach(
(builder, dynamicMethod) -> {
if (seen.add(builder)) {
// This builder class is never used in the program except from dynamicMethod(),
// which creates an instance of the builder. Instead of creating an instance of the
// builder class, we just instantiate the parent builder class. For this to work,
// we make the parent builder non-abstract.
DexProgramClass superClass =
asProgramClassOrNull(appView.definitionFor(builder.superType));
assert superClass != null;
superClass.accessFlags.demoteFromAbstract();
if (superClass.type == references.generatedMessageLiteBuilderType) {
// Manually trace `new GeneratedMessageLite.Builder(DEFAULT_INSTANCE)` since we
// haven't rewritten the code yet.
worklist.enqueueTraceNewInstanceAction(
references.generatedMessageLiteBuilderType, dynamicMethod);
worklist.enqueueTraceInvokeDirectAction(
references.generatedMessageLiteBuilderMethods.constructorMethod,
dynamicMethod);
} else {
assert superClass.type == references.generatedMessageLiteExtendableBuilderType;
// Manually trace `new GeneratedMessageLite.ExtendableBuilder(DEFAULT_INSTANCE)`
// since we haven't rewritten the code yet.
worklist.enqueueTraceNewInstanceAction(
references.generatedMessageLiteExtendableBuilderType, dynamicMethod);
worklist.enqueueTraceInvokeDirectAction(
references.generatedMessageLiteExtendableBuilderMethods.constructorMethod,
dynamicMethod);
}
worklist.enqueueTraceStaticFieldRead(
references.getDefaultInstanceField(dynamicMethod.getHolder()), dynamicMethod);
}
});
}
};
}
/** Returns true if an action was deferred. */
public boolean deferDeadProtoBuilders(
DexProgramClass clazz, ProgramMethod method, BooleanSupplier register) {
if (!enableAggressiveBuilderOptimization) {
return false;
}
DexEncodedMethod definition = method.getDefinition();
if (references.isDynamicMethod(definition) && references.isGeneratedMessageLiteBuilder(clazz)) {
if (register.getAsBoolean()) {
assert !builders.containsKey(clazz) || builders.get(clazz).getDefinition() == definition;
builders.put(clazz, method);
return true;
}
}
return false;
}
/**
* Reprocesses each dynamicMethod() that references a dead builder to rewrite the dead builder
* references.
*/
public void rewriteDeadBuilderReferencesFromDynamicMethods(
AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
throws ExecutionException {
if (builders.isEmpty()) {
return;
}
timing.begin("Remove dead builder references");
AppInfoWithLiveness appInfo = appView.appInfo();
IRConverter converter = new IRConverter(appView, Timing.empty());
ThreadUtils.processMap(
builders,
(builder, dynamicMethod) -> {
if (!appInfo.isLiveProgramClass(builder)) {
rewriteDeadBuilderReferencesFromDynamicMethod(
appView, builder, dynamicMethod, converter);
}
},
executorService);
builders.clear();
timing.end(); // Remove dead builder references
}
private void rewriteDeadBuilderReferencesFromDynamicMethod(
AppView<AppInfoWithLiveness> appView,
DexProgramClass builder,
ProgramMethod dynamicMethod,
IRConverter converter) {
IRCode code = dynamicMethod.buildIR(appView);
InstructionListIterator instructionIterator = code.instructionListIterator();
assert builder.superType == references.generatedMessageLiteBuilderType
|| builder.superType == references.generatedMessageLiteExtendableBuilderType;
DexField defaultInstanceField = references.getDefaultInstanceField(dynamicMethod.getHolder());
Value builderValue =
code.createValue(ClassTypeElement.create(builder.superType, definitelyNotNull(), appView));
Value defaultInstanceValue =
code.createValue(ClassTypeElement.create(defaultInstanceField.type, maybeNull(), appView));
// Replace `new Message.Builder()` by `new GeneratedMessageLite.Builder()`
// (or `new GeneratedMessageLite.ExtendableBuilder()`).
NewInstance newInstance =
instructionIterator.nextUntil(
instruction ->
instruction.isNewInstance() && instruction.asNewInstance().clazz == builder.type);
assert newInstance != null;
instructionIterator.replaceCurrentInstruction(new NewInstance(builder.superType, builderValue));
// Replace `builder.<init>()` by `builder.<init>(Message.DEFAULT_INSTANCE)`.
//
// We may also see an accessibility bridge constructor, because the Builder constructor is
// private. The accessibility bridge takes null as an argument.
InvokeDirect constructorInvoke =
instructionIterator.nextUntil(
instruction -> {
assert instruction.isInvokeDirect() || instruction.isConstNumber();
return instruction.isInvokeDirect();
});
assert constructorInvoke != null;
instructionIterator.replaceCurrentInstruction(
new StaticGet(defaultInstanceValue, defaultInstanceField));
instructionIterator.setInsertionPosition(constructorInvoke.getPosition());
instructionIterator.add(
new InvokeDirect(
builder.superType == references.generatedMessageLiteBuilderType
? references.generatedMessageLiteBuilderMethods.constructorMethod
: references.generatedMessageLiteExtendableBuilderMethods.constructorMethod,
null,
ImmutableList.of(builderValue, defaultInstanceValue)));
converter.removeDeadCodeAndFinalizeIR(
dynamicMethod, code, OptimizationFeedbackSimple.getInstance(), Timing.empty());
}
public static void addInliningHeuristicsForBuilderInlining(
AppView<? extends AppInfoWithClassHierarchy> appView,
SubtypingInfo subtypingInfo,
PredicateSet<DexType> alwaysClassInline,
Set<DexType> neverMerge,
Set<DexMethod> alwaysInline,
Set<DexMethod> bypassClinitforInlining) {
new RootSetExtension(
appView, alwaysClassInline, neverMerge, alwaysInline, bypassClinitforInlining)
.extend(subtypingInfo);
}
public void preprocessCallGraphBeforeCycleElimination(Map<DexMethod, Node> nodes) {
Node node = nodes.get(references.generatedMessageLiteBuilderMethods.constructorMethod);
if (node != null) {
List<Node> calleesToBeRemoved = new ArrayList<>();
for (Node callee : node.getCalleesWithDeterministicOrder()) {
if (references.isDynamicMethodBridge(callee.getMethod())) {
calleesToBeRemoved.add(callee);
}
}
for (Node callee : calleesToBeRemoved) {
callee.removeCaller(node);
}
}
}
public void inlineCallsToDynamicMethod(
ProgramMethod method,
IRCode code,
EnumValueOptimizer enumValueOptimizer,
OptimizationFeedback feedback,
MethodProcessor methodProcessor,
Inliner inliner) {
strengthenCheckCastInstructions(code);
ProtoInliningReasonStrategy inliningReasonStrategy =
new ProtoInliningReasonStrategy(appView, new FixedInliningReasonStrategy(Reason.NEVER));
inliner.performInlining(
method, code, feedback, methodProcessor, Timing.empty(), inliningReasonStrategy);
// Run the enum optimization to optimize all Enum.ordinal() invocations. This is required to
// get rid of the enum switch in dynamicMethod().
if (enumValueOptimizer != null) {
enumValueOptimizer.rewriteConstantEnumMethodCalls(code);
}
}
/**
* This method tries to strengthen the type of check-cast instructions that cast a value to
* GeneratedMessageLite.
*
* <p>New proto messages are created by calling dynamicMethod(MethodToInvoke.NEW_MUTABLE_INSTANCE)
* and casting the result to GeneratedMessageLite.
*
* <p>If we encounter the following pattern, then we cannot inline the second call to
* dynamicMethod, because we don't have a precise receiver type.
*
* <pre>
* GeneratedMessageLite msg =
* (GeneratedMessageLite)
* Message.DEFAULT_INSTANCE.dynamicMethod(MethodToInvoke.NEW_MUTABLE_INSTANCE);
* GeneratedMessageLite msg2 =
* (GeneratedMessageLite) msg.dynamicMethod(MethodToInvoke.NEW_MUTABLE_INSTANCE);
* </pre>
*
* <p>This method therefore optimizes the code above into:
*
* <pre>
* Message msg =
* (Message) Message.DEFAULT_INSTANCE.dynamicMethod(MethodToInvoke.NEW_MUTABLE_INSTANCE);
* Message msg2 = (Message) msg.dynamicMethod(MethodToInvoke.NEW_MUTABLE_INSTANCE);
* </pre>
*
* <p>This is assuming that calling dynamicMethod() on a proto message with
* MethodToInvoke.NEW_MUTABLE_INSTANCE will create an instance of the enclosing class.
*/
private void strengthenCheckCastInstructions(IRCode code) {
Set<Value> affectedValues = Sets.newIdentityHashSet();
InstructionListIterator instructionIterator = code.instructionListIterator();
CheckCast checkCast;
while ((checkCast = instructionIterator.nextUntil(Instruction::isCheckCast)) != null) {
if (checkCast.getType() != references.generatedMessageLiteType) {
continue;
}
Value root = checkCast.object().getAliasedValue();
if (root.isPhi() || !root.definition.isInvokeVirtual()) {
continue;
}
InvokeVirtual invoke = root.definition.asInvokeVirtual();
DexMethod invokedMethod = invoke.getInvokedMethod();
if (!references.isDynamicMethod(invokedMethod)
&& !references.isDynamicMethodBridge(invokedMethod)) {
continue;
}
assert invokedMethod.proto.parameters.values[0] == references.methodToInvokeType;
Value methodToInvokeValue = invoke.arguments().get(1);
if (!references.methodToInvokeMembers.isNewMutableInstanceEnum(methodToInvokeValue)) {
continue;
}
ClassTypeElement receiverType =
invoke.getReceiver().getDynamicUpperBoundType(appView).asClassType();
if (receiverType != null) {
AppInfoWithClassHierarchy appInfo = appView.appInfo();
DexType rawReceiverType = receiverType.getClassType();
if (appInfo.isStrictSubtypeOf(rawReceiverType, references.generatedMessageLiteType)) {
Value dest = code.createValue(receiverType.asMaybeNull(), checkCast.getLocalInfo());
CheckCast replacement = new CheckCast(dest, checkCast.object(), rawReceiverType);
instructionIterator.replaceCurrentInstruction(replacement, affectedValues);
}
}
}
if (!affectedValues.isEmpty()) {
new TypeAnalysis(appView).narrowing(affectedValues);
}
}
private static class RootSetExtension {
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final ProtoReferences references;
private final PredicateSet<DexType> alwaysClassInline;
private final Set<DexType> neverMerge;
private final Set<DexMethod> alwaysInline;
private final Set<DexMethod> bypassClinitforInlining;
RootSetExtension(
AppView<? extends AppInfoWithClassHierarchy> appView,
PredicateSet<DexType> alwaysClassInline,
Set<DexType> neverMerge,
Set<DexMethod> alwaysInline,
Set<DexMethod> bypassClinitforInlining) {
this.appView = appView;
this.references = appView.protoShrinker().references;
this.alwaysClassInline = alwaysClassInline;
this.neverMerge = neverMerge;
this.alwaysInline = alwaysInline;
this.bypassClinitforInlining = bypassClinitforInlining;
}
void extend(SubtypingInfo subtypingInfo) {
alwaysClassInlineGeneratedMessageLiteBuilders();
// GeneratedMessageLite heuristics.
alwaysInlineCreateBuilderFromGeneratedMessageLite();
// * extends GeneratedMessageLite heuristics.
bypassClinitforInliningNewBuilderMethods(subtypingInfo);
// GeneratedMessageLite$Builder heuristics.
alwaysInlineBuildPartialFromGeneratedMessageLiteExtendableBuilder();
neverMergeGeneratedMessageLiteBuilder();
}
private void alwaysClassInlineGeneratedMessageLiteBuilders() {
alwaysClassInline.addPredicate(
type ->
appView
.appInfo()
.isStrictSubtypeOf(type, references.generatedMessageLiteBuilderType));
}
private void bypassClinitforInliningNewBuilderMethods(SubtypingInfo subtypingInfo) {
for (DexType type : subtypingInfo.subtypes(references.generatedMessageLiteType)) {
DexProgramClass clazz = appView.definitionFor(type).asProgramClass();
if (clazz != null) {
DexEncodedMethod newBuilderMethod =
clazz.lookupDirectMethod(
method -> method.method.name == references.newBuilderMethodName);
if (newBuilderMethod != null) {
bypassClinitforInlining.add(newBuilderMethod.method);
}
}
}
}
private void alwaysInlineBuildPartialFromGeneratedMessageLiteExtendableBuilder() {
alwaysInline.add(references.generatedMessageLiteExtendableBuilderMethods.buildPartialMethod);
}
private void alwaysInlineCreateBuilderFromGeneratedMessageLite() {
alwaysInline.add(references.generatedMessageLiteMethods.createBuilderMethod);
}
private void neverMergeGeneratedMessageLiteBuilder() {
// For consistency, never merge the GeneratedMessageLite builders. These will only have a
// unique subtype if the application has a single proto message, which mostly happens during
// testing.
neverMerge.add(references.generatedMessageLiteBuilderType);
neverMerge.add(references.generatedMessageLiteExtendableBuilderType);
}
}
}