blob: 8596642bdf78e5f66fbbc546f70af5e4bddfcc5b [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.shaking;
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.DexClass;
import com.android.tools.r8.graph.DexClassAndField;
import com.android.tools.r8.graph.DexClassAndMethod;
import com.android.tools.r8.graph.DexDefinition;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.SubtypingInfo;
import com.android.tools.r8.shaking.InlineRule.Type;
import com.android.tools.r8.shaking.RootSetUtils.ConsequentRootSet;
import com.android.tools.r8.shaking.RootSetUtils.ConsequentRootSetBuilder;
import com.android.tools.r8.shaking.RootSetUtils.RootSetBuilder;
import com.android.tools.r8.utils.InternalOptions.TestingOptions.ProguardIfRuleEvaluationData;
import com.android.tools.r8.utils.ThreadUtils;
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
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.concurrent.Future;
import java.util.stream.Collectors;
public class IfRuleEvaluator {
private final AppView<? extends AppInfoWithClassHierarchy> appView;
private final SubtypingInfo subtypingInfo;
private final Enqueuer enqueuer;
private final ExecutorService executorService;
private final List<Future<?>> futures = new ArrayList<>();
private final Map<Wrapper<ProguardIfRule>, Set<ProguardIfRule>> ifRules;
private final ConsequentRootSetBuilder rootSetBuilder;
IfRuleEvaluator(
AppView<? extends AppInfoWithClassHierarchy> appView,
SubtypingInfo subtypingInfo,
Enqueuer enqueuer,
ExecutorService executorService,
Map<Wrapper<ProguardIfRule>, Set<ProguardIfRule>> ifRules,
ConsequentRootSetBuilder rootSetBuilder) {
this.appView = appView;
this.subtypingInfo = subtypingInfo;
this.enqueuer = enqueuer;
this.executorService = executorService;
this.ifRules = ifRules;
this.rootSetBuilder = rootSetBuilder;
}
public ConsequentRootSet run() throws ExecutionException {
appView.appInfo().app().timing.begin("Find consequent items for -if rules...");
try {
if (ifRules != null && !ifRules.isEmpty()) {
Iterator<Map.Entry<Wrapper<ProguardIfRule>, Set<ProguardIfRule>>> it =
ifRules.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Wrapper<ProguardIfRule>, Set<ProguardIfRule>> ifRuleEntry = it.next();
ProguardIfRule ifRuleKey = ifRuleEntry.getKey().get();
Set<ProguardIfRule> ifRulesInEquivalence = ifRuleEntry.getValue();
ProguardIfRuleEvaluationData ifRuleEvaluationData =
appView.options().testing.proguardIfRuleEvaluationData;
List<ProguardIfRule> toRemove = new ArrayList<>();
// Depending on which types that trigger the -if rule, the application of the subsequent
// -keep rule may vary (due to back references). So, we need to try all pairs of -if
// rule and live types.
for (DexProgramClass clazz :
ifRuleKey.relevantCandidatesForRule(
appView, subtypingInfo, appView.appInfo().classes())) {
if (!isEffectivelyLive(clazz)) {
continue;
}
// Check if the class matches the if-rule.
if (appView.options().testing.measureProguardIfRuleEvaluations) {
ifRuleEvaluationData.numberOfProguardIfRuleClassEvaluations++;
}
if (evaluateClassForIfRule(ifRuleKey, clazz)) {
// When matching an if rule against a type, the if-rule are filled with the current
// capture of wildcards. Propagate this down to member rules with same class part
// equivalence.
ifRulesInEquivalence.forEach(
ifRule -> {
registerClassCapture(ifRule, clazz, clazz);
if (appView.options().testing.measureProguardIfRuleEvaluations) {
ifRuleEvaluationData.numberOfProguardIfRuleMemberEvaluations++;
}
boolean matched = evaluateIfRuleMembersAndMaterialize(ifRule, clazz, clazz);
if (matched && canRemoveSubsequentKeepRule(ifRule)) {
toRemove.add(ifRule);
}
});
}
// Check if one of the types that have been merged into `clazz` satisfies the if-rule.
if (appView.verticallyMergedClasses() != null) {
Iterable<DexType> sources =
appView.verticallyMergedClasses().getSourcesFor(clazz.type);
for (DexType sourceType : sources) {
// Note that, although `sourceType` has been merged into `type`, the dex class for
// `sourceType` is still available until the second round of tree shaking. This
// way we can still retrieve the access flags of `sourceType`.
DexProgramClass sourceClass =
asProgramClassOrNull(appView.definitionFor(sourceType));
if (sourceClass == null) {
assert false;
continue;
}
if (appView.options().testing.measureProguardIfRuleEvaluations) {
ifRuleEvaluationData.numberOfProguardIfRuleClassEvaluations++;
}
if (evaluateClassForIfRule(ifRuleKey, sourceClass)) {
ifRulesInEquivalence.forEach(
ifRule -> {
registerClassCapture(ifRule, sourceClass, clazz);
if (appView.options().testing.measureProguardIfRuleEvaluations) {
ifRuleEvaluationData.numberOfProguardIfRuleMemberEvaluations++;
}
if (evaluateIfRuleMembersAndMaterialize(ifRule, sourceClass, clazz)
&& canRemoveSubsequentKeepRule(ifRule)) {
toRemove.add(ifRule);
}
});
}
}
}
}
if (ifRulesInEquivalence.size() == toRemove.size()) {
it.remove();
} else if (!toRemove.isEmpty()) {
ifRulesInEquivalence.removeAll(toRemove);
}
}
ThreadUtils.awaitFutures(futures);
}
} finally {
appView.appInfo().app().timing.end();
}
return rootSetBuilder.buildConsequentRootSet();
}
private boolean canRemoveSubsequentKeepRule(ProguardIfRule rule) {
return Iterables.isEmpty(rule.subsequentRule.getWildcards());
}
/**
* To ensure the matching work correctly going forward, when a class has matched, we have to run
* the capturing again on the member rule to ensure the wild cards are correctly populated.
*
* @param memberRule The member rule to populate with wildcards.
* @param source The source class.
* @param target The target class that can be different when we have vertically merged classes.
*/
private void registerClassCapture(ProguardIfRule memberRule, DexClass source, DexClass target) {
boolean classNameResult = memberRule.getClassNames().matches(source.type);
assert classNameResult;
if (memberRule.hasInheritanceClassName()) {
boolean inheritanceResult = rootSetBuilder.satisfyInheritanceRule(target, memberRule);
assert inheritanceResult;
}
}
private boolean isEffectivelyLive(DexProgramClass clazz) {
// A type is effectively live if (1) it is truly live, (2) the value of one of its fields has
// been inlined by the member value propagation, or (3) the return value of one of its methods
// has been forwarded by the member value propagation.
if (enqueuer.isTypeLive(clazz)) {
return true;
}
for (DexEncodedField field : clazz.fields()) {
if (field.getOptimizationInfo().valueHasBeenPropagated()) {
return true;
}
}
for (DexEncodedMethod method : clazz.methods()) {
if (method.getOptimizationInfo().returnValueHasBeenPropagated()) {
return true;
}
}
return false;
}
/** Determines if {@param clazz} satisfies the given if-rule class specification. */
private boolean evaluateClassForIfRule(ProguardIfRule rule, DexProgramClass clazz) {
if (!RootSetBuilder.satisfyClassType(rule, clazz)) {
return false;
}
if (!RootSetBuilder.satisfyAccessFlag(rule, clazz)) {
return false;
}
AnnotationMatchResult annotationMatchResult = RootSetBuilder.satisfyAnnotation(rule, clazz);
if (annotationMatchResult == null) {
return false;
}
rootSetBuilder.handleMatchedAnnotation(annotationMatchResult);
if (!rule.getClassNames().matches(clazz.type)) {
return false;
}
if (rule.hasInheritanceClassName()) {
// Try another live type since the current one doesn't satisfy the inheritance rule.
return rootSetBuilder.satisfyInheritanceRule(clazz, rule);
}
return true;
}
private boolean evaluateIfRuleMembersAndMaterialize(
ProguardIfRule rule, DexClass sourceClass, DexClass targetClass) {
Collection<ProguardMemberRule> memberKeepRules = rule.getMemberRules();
if (memberKeepRules.isEmpty()) {
materializeIfRule(rule, ImmutableSet.of(sourceClass.getReference()));
return true;
}
List<DexClassAndField> fieldsInlinedByJavaC = new ArrayList<>();
Set<DexDefinition> filteredMembers = Sets.newIdentityHashSet();
Iterables.addAll(
filteredMembers,
targetClass.fields(
f -> {
// Fields that are javac inlined are unsound as predicates for conditional rules.
// Ignore any such field members and record it for possible reporting later.
if (isFieldInlinedByJavaC(f)) {
fieldsInlinedByJavaC.add(DexClassAndField.create(targetClass, f));
return false;
}
// Fields referenced only by -keep may not be referenced, we therefore have to
// filter on both live and referenced.
return (enqueuer.isFieldLive(f)
|| enqueuer.isFieldReferenced(f)
|| f.getOptimizationInfo().valueHasBeenPropagated())
&& (appView.graphLens().getOriginalFieldSignature(f.getReference()).holder
== sourceClass.type);
}));
Iterables.addAll(
filteredMembers,
targetClass.methods(
m ->
(enqueuer.isMethodLive(m)
|| enqueuer.isMethodTargeted(m)
|| m.getOptimizationInfo().returnValueHasBeenPropagated())
&& appView.graphLens().getOriginalMethodSignature(m.getReference()).holder
== sourceClass.type));
// Check if the rule could hypothetically have matched a javac inlined field.
// If so mark the rule. Reporting happens only if the rule is otherwise unused.
if (!fieldsInlinedByJavaC.isEmpty()) {
for (ProguardMemberRule memberRule : memberKeepRules) {
if (!memberRule.getRuleType().includesFields()) {
continue;
}
for (DexClassAndField field : fieldsInlinedByJavaC) {
if (rootSetBuilder.sideEffectFreeIsRuleSatisfiedByField(memberRule, field)) {
rule.addInlinableFieldMatchingPrecondition(field.getReference());
}
}
}
}
// If the number of member rules to hold is more than live members, we can't make it.
if (filteredMembers.size() < memberKeepRules.size()) {
return false;
}
// Depending on which members trigger the -if rule, the application of the subsequent
// -keep rule may vary (due to back references). So, we need to try literally all
// combinations of live members. But, we can at least limit the number of elements per
// combination as the size of member rules to satisfy.
// TODO(b/206086945): Consider ways of reducing the size of this set computation.
for (Set<DexDefinition> combination :
Sets.combinations(filteredMembers, memberKeepRules.size())) {
Collection<DexClassAndField> fieldsInCombination =
DexDefinition.filterDexEncodedField(
combination.stream(), field -> DexClassAndField.create(targetClass, field))
.collect(Collectors.toList());
Collection<DexClassAndMethod> methodsInCombination =
DexDefinition.filterDexEncodedMethod(
combination.stream(), method -> DexClassAndMethod.create(targetClass, method))
.collect(Collectors.toList());
// Member rules are combined as AND logic: if found unsatisfied member rule, this
// combination of live members is not a good fit.
boolean satisfied =
memberKeepRules.stream()
.allMatch(
memberRule ->
rootSetBuilder.ruleSatisfiedByFields(memberRule, fieldsInCombination)
|| rootSetBuilder.ruleSatisfiedByMethods(
memberRule, methodsInCombination));
if (satisfied) {
materializeIfRule(rule, ImmutableSet.of(sourceClass.getReference()));
if (canRemoveSubsequentKeepRule(rule)) {
return true;
}
}
}
return false;
}
private boolean isFieldInlinedByJavaC(DexEncodedField field) {
if (enqueuer.getMode().isFinalTreeShaking()) {
// Ignore any field value in the final tree shaking pass so it remains consistent with the
// initial pass.
return field.getIsInlinableByJavaC();
}
return field.getOrComputeIsInlinableByJavaC(appView.dexItemFactory());
}
private void materializeIfRule(ProguardIfRule rule, Set<DexReference> preconditions) {
DexItemFactory dexItemFactory = appView.dexItemFactory();
ProguardIfRule materializedRule = rule.materialize(dexItemFactory, preconditions);
if (enqueuer.getMode().isInitialTreeShaking() && !rule.isUsed()) {
// We need to abort class inlining of classes that could be matched by the condition of this
// -if rule.
ClassInlineRule neverClassInlineRuleForCondition =
materializedRule.neverClassInlineRuleForCondition(dexItemFactory);
if (neverClassInlineRuleForCondition != null) {
rootSetBuilder.runPerRule(executorService, futures, neverClassInlineRuleForCondition, null);
}
InlineRule neverInlineForClassInliningRuleForCondition =
materializedRule.neverInlineRuleForCondition(dexItemFactory, Type.NEVER_CLASS_INLINE);
if (neverInlineForClassInliningRuleForCondition != null) {
rootSetBuilder.runPerRule(
executorService, futures, neverInlineForClassInliningRuleForCondition, null);
}
// If the condition of the -if rule has any members, then we need to keep these members to
// ensure that the subsequent rule will be applied again in the second round of tree
// shaking.
InlineRule neverInlineRuleForCondition =
materializedRule.neverInlineRuleForCondition(dexItemFactory, Type.NEVER);
if (neverInlineRuleForCondition != null) {
rootSetBuilder.runPerRule(executorService, futures, neverInlineRuleForCondition, null);
}
// Prevent horizontal class merging of any -if rule members.
NoHorizontalClassMergingRule noHorizontalClassMergingRule =
materializedRule.noHorizontalClassMergingRuleForCondition(dexItemFactory);
if (noHorizontalClassMergingRule != null) {
rootSetBuilder.runPerRule(executorService, futures, noHorizontalClassMergingRule, null);
}
}
// Keep whatever is required by the -if rule.
rootSetBuilder.runPerRule(
executorService, futures, materializedRule.subsequentRule, materializedRule);
rule.markAsUsed();
}
}