blob: cbe3961e811ddd6ae2b2b2a4df0f303c2bcc8a6f [file] [log] [blame]
// Copyright (c) 2018, 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 com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppView;
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.DexString;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.VerticalClassMerger.IllegalAccessDetector;
import com.android.tools.r8.utils.FieldSignatureEquivalence;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.MethodJavaSignatureEquivalence;
import com.android.tools.r8.utils.MethodSignatureEquivalence;
import com.android.tools.r8.utils.SingletonEquivalence;
import com.google.common.base.Equivalence;
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Streams;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* This optimization merges all classes that only have static members and private virtual methods.
*
* <p>If a merge candidate does not access any package-private or protected members, then it is
* merged into a global representative. Otherwise it is merged into a representative for its
* package. If no such representatives exist, then the merge candidate is promoted to be the
* representative.
*
* <p>Note that, when merging a merge candidate X into Y, this optimization merely moves the members
* of X into Y -- it does not change all occurrences of X in the program into Y. This makes the
* optimization more applicable, because it would otherwise not be possible to merge two classes if
* they inherited from, say, X' and Y' (since multiple inheritance is not allowed).
*
* <p>When there is a main dex specification, merging will only happen within the three groups of
* classes found from main dex tracing ("main dex roots", "main dex dependencies" and
* "non main dex classes"), and not between them. This ensures that the size of the main dex
* will not grow out of proportions due to non main dex classes getting merged into main dex
* classes.
*/
public class StaticClassMerger {
enum MergeGroup {
MAIN_DEX_ROOTS,
MAIN_DEX_DEPENDENCIES,
NOT_MAIN_DEX,
DONT_MERGE;
private static final String GLOBAL = "<global>";
private static Key mainDexRootsGlobalKey = new Key(MergeGroup.MAIN_DEX_ROOTS, GLOBAL);
private static Key mainDexDependenciesGlobalKey =
new Key(MergeGroup.MAIN_DEX_DEPENDENCIES, GLOBAL);
private static Key notMainDexGlobalKey = new Key(MergeGroup.NOT_MAIN_DEX, GLOBAL);
private static class Key {
private final MergeGroup mergeGroup;
private final String packageOrGlobal;
public Key(MergeGroup mergeGroup, String packageOrGlobal) {
this.mergeGroup = mergeGroup;
this.packageOrGlobal = packageOrGlobal;
}
public MergeGroup getMergeGroup() {
return mergeGroup;
}
public String getPackageOrGlobal() {
return packageOrGlobal;
}
public boolean isGlobal() {
return packageOrGlobal.equals(GLOBAL);
}
@Override
public int hashCode() {
return mergeGroup.ordinal() * 13 + packageOrGlobal.hashCode();
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other == null || this.getClass() != other.getClass()) {
return false;
}
Key o = (Key) other;
return o.mergeGroup == mergeGroup && o.packageOrGlobal.equals(packageOrGlobal);
}
}
public Key globalKey() {
switch (this) {
case NOT_MAIN_DEX:
return notMainDexGlobalKey;
case MAIN_DEX_ROOTS:
return mainDexRootsGlobalKey;
case MAIN_DEX_DEPENDENCIES:
return mainDexDependenciesGlobalKey;
default:
throw new Unreachable("Unexpected MergeGroup value");
}
}
public Key key(String pkg) {
assert this != DONT_MERGE;
return new Key(this, pkg);
}
@Override
public String toString() {
switch (this) {
case NOT_MAIN_DEX:
return "outside main dex";
case MAIN_DEX_ROOTS:
return "main dex roots";
case MAIN_DEX_DEPENDENCIES:
return "main dex dependencies";
default:
assert this == DONT_MERGE;
return "don't merge";
}
}
}
// There are 52 characters in [a-zA-Z], so with a capacity just below 52 the minifier should be
// able to find single-character names for all members, but around 30 appears to work better in
// practice.
private static final int HEURISTIC_FOR_CAPACITY_OF_REPRESENTATIVES = 30;
private class Representative {
private final DexProgramClass clazz;
// Put all members of this class into buckets, where each bucket represent members that have a
// conflicting signature, and therefore must be given distinct names.
private final HashMultiset<Wrapper<DexField>> fieldBuckets = HashMultiset.create();
private final HashMultiset<Wrapper<DexMethod>> methodBuckets = HashMultiset.create();
public Representative(DexProgramClass clazz) {
this.clazz = clazz;
include(clazz);
}
// Includes the members of `clazz` in `fieldBuckets` and `methodBuckets`.
public void include(DexProgramClass clazz) {
for (DexEncodedField field : clazz.fields()) {
Wrapper<DexField> wrapper = fieldEquivalence.wrap(field.field);
fieldBuckets.add(wrapper);
}
for (DexEncodedMethod method : clazz.methods()) {
Wrapper<DexMethod> wrapper = methodEquivalence.wrap(method.method);
methodBuckets.add(wrapper);
}
}
// Returns true if this representative should no longer be used. The current heuristic is to
// stop using a representative when the number of members with the same signature (ignoring the
// name) exceeds a given threshold. This way it is unlikely that we will not be able to find a
// single-character name for all members.
public boolean isFull() {
int numberOfNamesNeeded = 1;
for (Entry<Wrapper<DexField>> entry : fieldBuckets.entrySet()) {
numberOfNamesNeeded = Math.max(entry.getCount(), numberOfNamesNeeded);
}
for (Entry<Wrapper<DexMethod>> entry : methodBuckets.entrySet()) {
numberOfNamesNeeded = Math.max(entry.getCount(), numberOfNamesNeeded);
}
return numberOfNamesNeeded > HEURISTIC_FOR_CAPACITY_OF_REPRESENTATIVES;
}
}
private final AppView<AppInfoWithLiveness> appView;
private final MainDexClasses mainDexClasses;
/** The equivalence that should be used for the member buckets in {@link Representative}. */
private final Equivalence<DexField> fieldEquivalence;
private final Equivalence<DexMethod> methodEquivalence;
private final Map<MergeGroup.Key, Representative> representatives = new HashMap<>();
private final BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
private final BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
private int numberOfMergedClasses = 0;
public StaticClassMerger(
AppView<AppInfoWithLiveness> appView,
InternalOptions options,
MainDexClasses mainDexClasses) {
this.appView = appView;
if (options.getProguardConfiguration().isOverloadAggressively()) {
fieldEquivalence = FieldSignatureEquivalence.getEquivalenceIgnoreName();
methodEquivalence = MethodSignatureEquivalence.getEquivalenceIgnoreName();
} else {
fieldEquivalence = new SingletonEquivalence<>();
methodEquivalence = MethodJavaSignatureEquivalence.getEquivalenceIgnoreName();
}
this.mainDexClasses = mainDexClasses;
}
public GraphLense run() {
for (DexProgramClass clazz : appView.appInfo().app().classesWithDeterministicOrder()) {
MergeGroup group = satisfiesMergeCriteria(clazz);
if (group != MergeGroup.DONT_MERGE) {
merge(clazz, group);
}
}
if (Log.ENABLED) {
Log.info(
getClass(),
"Merged %s classes with %s members.",
numberOfMergedClasses,
fieldMapping.size() + methodMapping.size());
}
return buildGraphLense();
}
private GraphLense buildGraphLense() {
if (!fieldMapping.isEmpty() || !methodMapping.isEmpty()) {
BiMap<DexField, DexField> originalFieldSignatures = fieldMapping.inverse();
BiMap<DexMethod, DexMethod> originalMethodSignatures = methodMapping.inverse();
return new NestedGraphLense(
ImmutableMap.of(),
methodMapping,
fieldMapping,
originalFieldSignatures,
originalMethodSignatures,
appView.graphLense(),
appView.dexItemFactory());
}
return appView.graphLense();
}
private MergeGroup satisfiesMergeCriteria(DexProgramClass clazz) {
if (appView.appInfo().neverMerge.contains(clazz.type)) {
return MergeGroup.DONT_MERGE;
}
if (clazz.staticFields().size() + clazz.directMethods().size() + clazz.virtualMethods().size()
== 0) {
return MergeGroup.DONT_MERGE;
}
if (clazz.instanceFields().size() > 0) {
return MergeGroup.DONT_MERGE;
}
if (clazz.staticFields().stream()
.anyMatch(field -> appView.appInfo().isPinned(field.field))) {
return MergeGroup.DONT_MERGE;
}
if (clazz.directMethods().stream().anyMatch(DexEncodedMethod::isInitializer)) {
return MergeGroup.DONT_MERGE;
}
if (!clazz.virtualMethods().stream().allMatch(DexEncodedMethod::isPrivateMethod)) {
return MergeGroup.DONT_MERGE;
}
if (Streams.stream(clazz.methods())
.anyMatch(
method ->
method.accessFlags.isNative()
|| appView.appInfo().isPinned(method.method)
// TODO(christofferqa): Remove the invariant that the graph lense should not
// modify any methods from the sets alwaysInline and noSideEffects.
|| appView.appInfo().alwaysInline.contains(method.method)
|| appView.appInfo().noSideEffects.keySet().contains(method.method))) {
return MergeGroup.DONT_MERGE;
}
if (clazz.classInitializationMayHaveSideEffects(appView.appInfo())) {
// This could have a negative impact on inlining.
//
// See {@link com.android.tools.r8.ir.optimize.DefaultInliningOracle#canInlineStaticInvoke}
// for further details.
//
// Note that this will be true for all classes that inherit from or implement a library class.
return MergeGroup.DONT_MERGE;
}
if (!mainDexClasses.isEmpty()) {
if (mainDexClasses.getRoots().contains(clazz.type)) {
return MergeGroup.MAIN_DEX_ROOTS;
}
if (mainDexClasses.getDependencies().contains(clazz.type)) {
return MergeGroup.MAIN_DEX_DEPENDENCIES;
}
}
return MergeGroup.NOT_MAIN_DEX;
}
private boolean isValidRepresentative(DexProgramClass clazz) {
// Disallow interfaces from being representatives, since interface methods require desugaring.
return !clazz.isInterface();
}
private boolean merge(DexProgramClass clazz, MergeGroup group) {
assert satisfiesMergeCriteria(clazz) == group;
assert group != MergeGroup.DONT_MERGE;
String pkg = clazz.type.getPackageDescriptor();
return mayMergeAcrossPackageBoundaries(clazz)
? mergeGlobally(clazz, pkg, group)
: mergeInsidePackage(clazz, pkg, group);
}
private boolean mergeGlobally(DexProgramClass clazz, String pkg, MergeGroup group) {
Representative globalRepresentative = representatives.get(group.globalKey());
if (globalRepresentative == null) {
if (isValidRepresentative(clazz)) {
// Make the current class the global representative.
setRepresentative(group.globalKey(), getOrCreateRepresentative(group.key(pkg), clazz));
} else {
clearRepresentative(group.globalKey());
}
// Do not attempt to merge this class inside its own package, because that could lead to
// an increase in the global representative, which is not desirable.
return false;
} else {
// Check if we can merge the current class into the current global representative.
globalRepresentative.include(clazz);
if (globalRepresentative.isFull()) {
if (isValidRepresentative(clazz)) {
// Make the current class the global representative instead.
setRepresentative(group.globalKey(), getOrCreateRepresentative(group.key(pkg), clazz));
} else {
clearRepresentative(group.globalKey());
}
// Do not attempt to merge this class inside its own package, because that could lead to
// an increase in the global representative, which is not desirable.
return false;
} else {
// Merge this class into the global representative.
moveMembersFromSourceToTarget(clazz, globalRepresentative.clazz);
return true;
}
}
}
private boolean mergeInsidePackage(DexProgramClass clazz, String pkg, MergeGroup group) {
MergeGroup.Key key = group.key(pkg);
Representative packageRepresentative = representatives.get(key);
if (packageRepresentative != null) {
if (isValidRepresentative(clazz)
&& clazz.accessFlags.isMoreVisibleThan(
packageRepresentative.clazz.accessFlags,
clazz.type.getPackageName(),
packageRepresentative.clazz.type.getPackageName())) {
// Use `clazz` as a representative for this package instead.
Representative newRepresentative = getOrCreateRepresentative(key, clazz);
newRepresentative.include(packageRepresentative.clazz);
if (!newRepresentative.isFull()) {
setRepresentative(group.key(pkg), newRepresentative);
moveMembersFromSourceToTarget(packageRepresentative.clazz, clazz);
return true;
}
// We are not allowed to merge members into a class that is less visible.
return false;
}
// Merge current class into the representative of this package if it has room.
packageRepresentative.include(clazz);
// If there is room, then merge, otherwise fall-through to update the representative of this
// package.
if (!packageRepresentative.isFull()) {
moveMembersFromSourceToTarget(clazz, packageRepresentative.clazz);
return true;
}
}
// We were unable to use the current representative for this package (if any).
if (isValidRepresentative(clazz)) {
setRepresentative(key, getOrCreateRepresentative(key, clazz));
}
return false;
}
private Representative getOrCreateRepresentative(MergeGroup.Key key, DexProgramClass clazz) {
Representative globalRepresentative = representatives.get(key.getMergeGroup().globalKey());
if (globalRepresentative != null && globalRepresentative.clazz == clazz) {
return globalRepresentative;
}
Representative packageRepresentative = representatives.get(key);
if (packageRepresentative != null && packageRepresentative.clazz == clazz) {
return packageRepresentative;
}
return new Representative(clazz);
}
private void setRepresentative(MergeGroup.Key key, Representative representative) {
assert isValidRepresentative(representative.clazz);
if (Log.ENABLED) {
if (key.isGlobal()) {
Log.info(
getClass(),
"Making %s the global representative in group %s",
representative.clazz.type.toSourceString(),
key.getMergeGroup().toString());
} else {
Log.info(
getClass(),
"Making %s the representative for package %s in group %s",
representative.clazz.type.toSourceString(),
key.getPackageOrGlobal(),
key.getMergeGroup().toString());
}
}
representatives.put(key, representative);
}
private void clearRepresentative(MergeGroup.Key key) {
if (Log.ENABLED) {
if (key.isGlobal()) {
Log.info(getClass(), "Removing the global representative");
} else {
Log.info(
getClass(), "Removing the representative for package %s", key.getPackageOrGlobal());
}
}
representatives.remove(key);
}
private boolean mayMergeAcrossPackageBoundaries(DexProgramClass clazz) {
// Check that the class is public. Otherwise, accesses to `clazz` from within its current
// package may become illegal.
if (!clazz.accessFlags.isPublic()) {
return false;
}
// Check that all of the members are private or public.
if (!clazz.directMethods().stream()
.allMatch(method -> method.accessFlags.isPrivate() || method.accessFlags.isPublic())) {
return false;
}
if (!clazz.staticFields().stream()
.allMatch(field -> field.accessFlags.isPrivate() || field.accessFlags.isPublic())) {
return false;
}
// Note that a class is only considered a candidate if it has no instance fields and all of its
// virtual methods are private. Therefore, we don't need to consider check if there are any
// package-private or protected instance fields or virtual methods here.
assert clazz.instanceFields().size() == 0;
assert clazz.virtualMethods().stream().allMatch(method -> method.accessFlags.isPrivate());
// Check that no methods access package-private or protected members.
IllegalAccessDetector registry = new IllegalAccessDetector(appView, clazz);
for (DexEncodedMethod method : clazz.methods()) {
registry.setContext(method);
method.registerCodeReferences(registry);
if (registry.foundIllegalAccess()) {
return false;
}
}
return true;
}
private void moveMembersFromSourceToTarget(
DexProgramClass sourceClass, DexProgramClass targetClass) {
if (Log.ENABLED) {
Log.info(
getClass(),
"Merging %s into %s",
sourceClass.type.toSourceString(),
targetClass.type.toSourceString());
}
// TODO(b/136457753) This check is a bit weird for protected, since it is moving access.
assert targetClass.accessFlags.isAtLeastAsVisibleAs(sourceClass.accessFlags);
assert sourceClass.instanceFields().size() == 0;
assert targetClass.instanceFields().size() == 0;
numberOfMergedClasses++;
// Move members from source to target.
targetClass.appendDirectMethods(
mergeMethods(sourceClass.directMethods(), targetClass.directMethods(), targetClass));
targetClass.appendVirtualMethods(
mergeMethods(sourceClass.virtualMethods(), targetClass.virtualMethods(), targetClass));
targetClass.setStaticFields(
mergeFields(sourceClass.staticFields(), targetClass.staticFields(), targetClass));
// Cleanup source.
sourceClass.setDirectMethods(DexEncodedMethod.EMPTY_ARRAY);
sourceClass.setVirtualMethods(DexEncodedMethod.EMPTY_ARRAY);
sourceClass.setStaticFields(DexEncodedField.EMPTY_ARRAY);
}
private List<DexEncodedMethod> mergeMethods(
List<DexEncodedMethod> sourceMethods,
List<DexEncodedMethod> targetMethods,
DexProgramClass targetClass) {
// Move source methods to result one by one, renaming them if needed.
MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
Set<Wrapper<DexMethod>> existingMethods =
targetMethods.stream()
.map(targetMethod -> equivalence.wrap(targetMethod.method))
.collect(Collectors.toSet());
Predicate<DexMethod> availableMethodSignatures =
method -> !existingMethods.contains(equivalence.wrap(method));
List<DexEncodedMethod> newMethods = new ArrayList<>(sourceMethods.size());
for (DexEncodedMethod sourceMethod : sourceMethods) {
DexEncodedMethod sourceMethodAfterMove =
renameMethodIfNeeded(sourceMethod, targetClass, availableMethodSignatures);
newMethods.add(sourceMethodAfterMove);
DexMethod originalMethod =
methodMapping.inverse().getOrDefault(sourceMethod.method, sourceMethod.method);
methodMapping.forcePut(originalMethod, sourceMethodAfterMove.method);
existingMethods.add(equivalence.wrap(sourceMethodAfterMove.method));
}
return newMethods;
}
private DexEncodedField[] mergeFields(
List<DexEncodedField> sourceFields,
List<DexEncodedField> targetFields,
DexProgramClass targetClass) {
DexEncodedField[] result = new DexEncodedField[sourceFields.size() + targetFields.size()];
// Move all target fields to result.
int index = 0;
for (DexEncodedField targetField : targetFields) {
result[index++] = targetField;
}
// Move source fields to result one by one, renaming them if needed.
FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
Set<Wrapper<DexField>> existingFields =
targetFields.stream()
.map(targetField -> equivalence.wrap(targetField.field))
.collect(Collectors.toSet());
Predicate<DexField> availableFieldSignatures =
field -> !existingFields.contains(equivalence.wrap(field));
for (DexEncodedField sourceField : sourceFields) {
DexEncodedField sourceFieldAfterMove =
renameFieldIfNeeded(sourceField, targetClass, availableFieldSignatures);
result[index++] = sourceFieldAfterMove;
DexField originalField =
fieldMapping.inverse().getOrDefault(sourceField.field, sourceField.field);
fieldMapping.forcePut(originalField, sourceFieldAfterMove.field);
existingFields.add(equivalence.wrap(sourceFieldAfterMove.field));
}
assert index == result.length;
return result;
}
private DexEncodedMethod renameMethodIfNeeded(
DexEncodedMethod method,
DexProgramClass targetClass,
Predicate<DexMethod> availableMethodSignatures) {
assert !method.accessFlags.isConstructor();
DexString oldName = method.method.name;
DexMethod newSignature =
appView.dexItemFactory().createMethod(targetClass.type, method.method.proto, oldName);
if (!availableMethodSignatures.test(newSignature)) {
int count = 1;
do {
DexString newName = appView.dexItemFactory().createString(oldName.toSourceString() + count);
newSignature =
appView.dexItemFactory().createMethod(targetClass.type, method.method.proto, newName);
count++;
} while (!availableMethodSignatures.test(newSignature));
}
return method.toTypeSubstitutedMethod(newSignature);
}
private DexEncodedField renameFieldIfNeeded(
DexEncodedField field,
DexProgramClass targetClass,
Predicate<DexField> availableFieldSignatures) {
DexString oldName = field.field.name;
DexField newSignature =
appView.dexItemFactory().createField(targetClass.type, field.field.type, oldName);
if (!availableFieldSignatures.test(newSignature)) {
int count = 1;
do {
DexString newName = appView.dexItemFactory().createString(oldName.toSourceString() + count);
newSignature =
appView.dexItemFactory().createField(targetClass.type, field.field.type, newName);
count++;
} while (!availableFieldSignatures.test(newSignature));
}
return field.toTypeSubstitutedField(newSignature);
}
}