blob: e520d1bfcb7ab755f1921e6d48dcf6b0d78aa4ab [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;
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.Code;
import com.android.tools.r8.graph.DexApplication;
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.DexProgramClass;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNamingForNameMapper;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.AndroidAppConsumers;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.Timing;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.BiConsumer;
/**
* JarSizeCompare outputs the class, method, field sizes of the given JAR files. For each input, a
* ProGuard map can be passed that is used to resolve minified names.
*
* <p>By default, only shows methods where R8's DEX output is 5 or more instructions larger than
* ProGuard+D8's output. Pass {@code --threshold 0} to display all methods.
*/
public class JarSizeCompare {
private static final String USAGE =
"Arguments:\n"
+ " [--threshold <threshold>]\n"
+ " [--lib <lib.jar>]\n"
+ " --input <name1> <input1.jar> [<map1.txt>]\n"
+ " --input <name2> <input2.jar> [<map2.txt>] ...\n"
+ "\n"
+ "JarSizeCompare outputs the class, method, field sizes of the given JAR files.\n"
+ "For each input, a ProGuard map can be passed that is used to resolve minified"
+ " names.\n";
private static final ImmutableMap<String, String> R8_RELOCATIONS =
ImmutableMap.<String, String>builder()
.put("com.google.common", "com.android.tools.r8.com.google.common")
.put("com.google.gson", "com.android.tools.r8.com.google.gson")
.put("com.google.thirdparty", "com.android.tools.r8.com.google.thirdparty")
.put("joptsimple", "com.android.tools.r8.joptsimple")
.put("org.apache.commons", "com.android.tools.r8.org.apache.commons")
.put("org.objectweb.asm", "com.android.tools.r8.org.objectweb.asm")
.put("it.unimi.dsi.fastutil", "com.android.tools.r8.it.unimi.dsi.fastutil")
.build();
private final List<Path> libraries;
private final List<InputParameter> inputParameters;
private final int threshold;
private final InternalOptions options;
private int pgIndex;
private int r8Index;
private JarSizeCompare(
List<Path> libraries, List<InputParameter> inputParameters, int threshold) {
this.libraries = libraries;
this.inputParameters = inputParameters;
this.threshold = threshold;
options = new InternalOptions();
}
public void run() throws Exception {
List<String> names = new ArrayList<>();
List<InputApplication> inputApplicationList = new ArrayList<>();
Timing timing = new Timing("JarSizeCompare");
for (InputParameter inputParameter : inputParameters) {
AndroidApp inputApp = inputParameter.getInputApp(libraries);
DexApplication input = inputParameter.getReader(options, inputApp, timing);
AndroidAppConsumers appConsumer = new AndroidAppConsumers();
D8.run(
D8Command.builder(inputApp)
.setMinApiLevel(AndroidApiLevel.P.getLevel())
.setProgramConsumer(appConsumer.wrapDexIndexedConsumer(null))
.build());
DexApplication d8Input = inputParameter.getReader(options, appConsumer.build(), timing);
InputApplication inputApplication =
new InputApplication(input, translateClassNames(input, input.classes()));
InputApplication d8Classes =
new InputApplication(input, translateClassNames(input, d8Input.classes()));
names.add(inputParameter.name + "-input");
inputApplicationList.add(inputApplication);
names.add(inputParameter.name + "-d8");
inputApplicationList.add(d8Classes);
}
if (threshold != 0) {
pgIndex = names.indexOf("pg-d8");
r8Index = names.indexOf("r8-d8");
}
Map<String, InputClass[]> inputClasses = new HashMap<>();
for (int i = 0; i < names.size(); i++) {
InputApplication classes = inputApplicationList.get(i);
for (String className : classes.getClasses()) {
inputClasses.computeIfAbsent(className, k -> new InputClass[names.size()])[i] =
classes.getInputClass(className);
}
}
for (Entry<String, Map<String, InputClass[]>> library : byLibrary(inputClasses)) {
System.out.println("");
System.out.println(Strings.repeat("=", 100));
String commonPrefix = getCommonPrefix(library.getValue().keySet());
if (library.getKey().isEmpty()) {
System.out.println("PROGRAM (" + commonPrefix + ")");
} else {
System.out.println("LIBRARY: " + library.getKey() + " (" + commonPrefix + ")");
}
printLibrary(library.getValue(), commonPrefix);
}
}
private Map<String, DexProgramClass> translateClassNames(
DexApplication input, List<DexProgramClass> classes) {
Map<String, DexProgramClass> result = new HashMap<>();
ClassNameMapper classNameMapper = input.getProguardMap();
for (DexProgramClass programClass : classes) {
ClassNamingForNameMapper classNaming;
if (classNameMapper == null) {
classNaming = null;
} else {
classNaming = classNameMapper.getClassNaming(programClass.type);
}
String type =
classNaming == null ? programClass.type.toSourceString() : classNaming.originalName;
result.put(type, programClass);
}
return result;
}
private String getCommonPrefix(Set<String> classes) {
if (classes.size() <= 1) {
return "";
}
String commonPrefix = null;
for (String clazz : classes) {
if (clazz.equals("r8.GeneratedOutlineSupport")) {
continue;
}
if (commonPrefix == null) {
commonPrefix = clazz;
} else {
int i = 0;
while (i < clazz.length()
&& i < commonPrefix.length()
&& clazz.charAt(i) == commonPrefix.charAt(i)) {
i++;
}
commonPrefix = commonPrefix.substring(0, i);
}
}
return commonPrefix;
}
private void printLibrary(Map<String, InputClass[]> classMap, String commonPrefix) {
List<Entry<String, InputClass[]>> classes = new ArrayList<>(classMap.entrySet());
classes.sort(Comparator.comparing(Entry::getKey));
for (Entry<String, InputClass[]> clazz : classes) {
printClass(
clazz.getKey().substring(commonPrefix.length()), new ClassCompare(clazz.getValue()));
}
}
private void printClass(String name, ClassCompare inputClasses) {
List<MethodSignature> methods = getMethods(inputClasses);
List<FieldSignature> fields = getFields(inputClasses);
if (methods.isEmpty() && fields.isEmpty()) {
return;
}
System.out.println(name);
for (MethodSignature sig : methods) {
printSignature(getMethodString(sig), inputClasses.sizes(sig));
}
for (FieldSignature sig : fields) {
printSignature(getFieldString(sig), inputClasses.sizes(sig));
}
}
private String getMethodString(MethodSignature sig) {
StringBuilder builder = new StringBuilder().append('(');
for (int i = 0; i < sig.parameters.length; i++) {
builder.append(DescriptorUtils.javaTypeToShorty(sig.parameters[i]));
}
builder.append(')').append(DescriptorUtils.javaTypeToShorty(sig.type)).append(' ');
return builder.append(sig.name).toString();
}
private String getFieldString(FieldSignature sig) {
return DescriptorUtils.javaTypeToShorty(sig.type) + ' ' + sig.name;
}
private void printSignature(String key, int[] sizes) {
System.out.print(padItem(key));
for (int size : sizes) {
System.out.print(padValue(size));
}
System.out.print('\n');
}
private List<MethodSignature> getMethods(ClassCompare inputClasses) {
List<MethodSignature> methods = new ArrayList<>();
for (MethodSignature methodSignature : inputClasses.getMethods()) {
if (threshold == 0 || methodExceedsThreshold(inputClasses, methodSignature)) {
methods.add(methodSignature);
}
}
return methods;
}
private boolean methodExceedsThreshold(
ClassCompare inputClasses, MethodSignature methodSignature) {
assert threshold > 0;
assert pgIndex != r8Index;
int pgSize = inputClasses.size(methodSignature, pgIndex);
int r8Size = inputClasses.size(methodSignature, r8Index);
return pgSize != -1 && r8Size != -1 && pgSize + threshold <= r8Size;
}
private List<FieldSignature> getFields(ClassCompare inputClasses) {
return threshold == 0 ? inputClasses.getFields() : Collections.emptyList();
}
private String padItem(String s) {
return String.format("%-52s", s);
}
private String padValue(int v) {
return String.format("%8s", v == -1 ? "---" : v);
}
private List<Map.Entry<String, Map<String, InputClass[]>>> byLibrary(
Map<String, InputClass[]> inputClasses) {
Map<String, Map<String, InputClass[]>> byLibrary = new HashMap<>();
for (Entry<String, InputClass[]> entry : inputClasses.entrySet()) {
Map<String, InputClass[]> library =
byLibrary.computeIfAbsent(getLibraryName(entry.getKey()), k -> new HashMap<>());
library.put(entry.getKey(), entry.getValue());
}
List<Entry<String, Map<String, InputClass[]>>> list = new ArrayList<>(byLibrary.entrySet());
list.sort(Comparator.comparing(Entry::getKey));
return list;
}
private String getLibraryName(String className) {
for (Entry<String, String> relocation : R8_RELOCATIONS.entrySet()) {
if (className.startsWith(relocation.getValue())) {
return relocation.getKey();
}
}
return "";
}
static class InputParameter {
private final String name;
private final Path jar;
private final Path map;
InputParameter(String name, Path jar, Path map) {
this.name = name;
this.jar = jar;
this.map = map;
}
DexApplication getReader(InternalOptions options, AndroidApp inputApp, Timing timing)
throws Exception {
ApplicationReader applicationReader = new ApplicationReader(inputApp, options, timing);
return applicationReader.read(map == null ? null : StringResource.fromFile(map)).toDirect();
}
AndroidApp getInputApp(List<Path> libraries) throws Exception {
return AndroidApp.builder().addLibraryFiles(libraries).addProgramFiles(jar).build();
}
}
static class InputApplication {
private final DexApplication dexApplication;
private final Map<String, DexProgramClass> classMap;
private InputApplication(DexApplication dexApplication, Map<String, DexProgramClass> classMap) {
this.dexApplication = dexApplication;
this.classMap = classMap;
}
public Set<String> getClasses() {
return classMap.keySet();
}
private InputClass getInputClass(String type) {
DexProgramClass inputClass = classMap.get(type);
ClassNameMapper proguardMap = dexApplication.getProguardMap();
return new InputClass(inputClass, proguardMap);
}
}
static class InputClass {
private final DexProgramClass programClass;
private final ClassNameMapper proguardMap;
InputClass(DexClass dexClass, ClassNameMapper proguardMap) {
this.programClass = dexClass == null ? null : dexClass.asProgramClass();
this.proguardMap = proguardMap;
}
void forEachMethod(BiConsumer<MethodSignature, DexEncodedMethod> consumer) {
if (programClass == null) {
return;
}
programClass.forEachMethod(
dexEncodedMethod -> {
MethodSignature originalSignature =
proguardMap == null
? null
: proguardMap.originalSignatureOf(dexEncodedMethod.getReference());
MethodSignature signature =
MethodSignature.fromDexMethod(dexEncodedMethod.getReference());
consumer.accept(
originalSignature == null ? signature : originalSignature, dexEncodedMethod);
});
}
void forEachField(BiConsumer<FieldSignature, DexEncodedField> consumer) {
if (programClass == null) {
return;
}
programClass.forEachField(
dexEncodedField -> {
FieldSignature originalSignature =
proguardMap == null
? null
: proguardMap.originalSignatureOf(dexEncodedField.getReference());
FieldSignature signature = FieldSignature.fromDexField(dexEncodedField.getReference());
consumer.accept(
originalSignature == null ? signature : originalSignature, dexEncodedField);
});
}
}
private static class ClassCompare {
final Map<MethodSignature, DexEncodedMethod[]> methods = new HashMap<>();
final Map<FieldSignature, DexEncodedField[]> fields = new HashMap<>();
final int classes;
ClassCompare(InputClass[] inputs) {
for (int i = 0; i < inputs.length; i++) {
InputClass inputClass = inputs[i];
int finalI = i;
if (inputClass == null) {
continue;
}
inputClass.forEachMethod(
(sig, m) ->
methods.computeIfAbsent(sig, o -> new DexEncodedMethod[inputs.length])[finalI] = m);
inputClass.forEachField(
(sig, f) ->
fields.computeIfAbsent(sig, o -> new DexEncodedField[inputs.length])[finalI] = f);
}
classes = inputs.length;
}
List<MethodSignature> getMethods() {
List<MethodSignature> methods = new ArrayList<>(this.methods.keySet());
methods.sort(Comparator.comparing(MethodSignature::toString));
return methods;
}
List<FieldSignature> getFields() {
List<FieldSignature> fields = new ArrayList<>(this.fields.keySet());
fields.sort(Comparator.comparing(FieldSignature::toString));
return fields;
}
int size(MethodSignature method, int classIndex) {
DexEncodedMethod dexEncodedMethod = methods.get(method)[classIndex];
if (dexEncodedMethod == null) {
return -1;
}
Code code = dexEncodedMethod.getCode();
if (code == null) {
return 0;
}
if (code.isCfCode()) {
return code.asCfCode().getInstructions().size();
}
if (code.isDexCode()) {
return code.asDexCode().instructions.length;
}
throw new Unreachable();
}
int[] sizes(MethodSignature method) {
int[] result = new int[classes];
for (int i = 0; i < classes; i++) {
result[i] = size(method, i);
}
return result;
}
int size(FieldSignature field, int classIndex) {
return fields.get(field)[classIndex] == null ? -1 : 1;
}
int[] sizes(FieldSignature field) {
int[] result = new int[classes];
for (int i = 0; i < classes; i++) {
result[i] = size(field, i);
}
return result;
}
}
public static void main(String[] args) throws Exception {
JarSizeCompare program = JarSizeCompare.parse(args);
if (program == null) {
System.out.println(USAGE);
} else {
program.run();
}
}
public static JarSizeCompare parse(String[] args) {
int i = 0;
int threshold = 0;
List<Path> libraries = new ArrayList<>();
List<InputParameter> inputs = new ArrayList<>();
Set<String> names = new HashSet<>();
while (i < args.length) {
if (args[i].equals("--threshold") && i + 1 < args.length) {
threshold = Integer.parseInt(args[i + 1]);
i += 2;
} else if (args[i].equals("--lib") && i + 1 < args.length) {
libraries.add(Paths.get(args[i + 1]));
i += 2;
} else if (args[i].equals("--input") && i + 2 < args.length) {
String name = args[i + 1];
Path jar = Paths.get(args[i + 2]);
Path map = null;
if (i + 3 < args.length && !args[i + 3].startsWith("-")) {
map = Paths.get(args[i + 3]);
i += 4;
} else {
i += 3;
}
inputs.add(new InputParameter(name, jar, map));
if (!names.add(name)) {
System.out.println("Duplicate name: " + name);
return null;
}
} else {
return null;
}
}
if (inputs.size() < 2) {
return null;
}
if (threshold != 0 && (!names.contains("r8") || !names.contains("pg"))) {
System.out.println(
"You must either specify names \"pg\" and \"r8\" for input files "
+ "or use \"--threshold 0\".");
return null;
}
return new JarSizeCompare(libraries, inputs, threshold);
}
}