| // 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.Collection; |
| 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, Collection<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); |
| } |
| } |