| /* |
| * Copyright 2013-present Facebook, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may |
| * not use this file except in compliance with the License. You may obtain |
| * a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package com.facebook.buck.dalvik; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.ClassVisitor; |
| import org.objectweb.asm.FieldVisitor; |
| import org.objectweb.asm.MethodVisitor; |
| import org.objectweb.asm.Opcodes; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Tool to get stats about dalvik classes. |
| */ |
| public class DalvikStatsTool { |
| |
| // Reasonable defaults based on dreiss's observations. |
| private static final ImmutableMap<Pattern, Integer> PENALTIES = ImmutableMap.of( |
| Pattern.compile("Layout$"), 1500, |
| Pattern.compile("View$"), 1500, |
| Pattern.compile("ViewGroup$"), 1800, |
| Pattern.compile("Activity$"), 1100 |
| ); |
| |
| public static class MethodReference { |
| |
| public final String className; |
| public final String methodName; |
| public final String methodDesc; |
| |
| |
| public MethodReference(String className, String methodName, String methodDesc) { |
| this.className = className; |
| this.methodName = methodName; |
| this.methodDesc = methodDesc; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| MethodReference that = (MethodReference) o; |
| |
| if (className != null ? !className.equals(that.className) : that.className != null) |
| return false; |
| if (methodDesc != null ? !methodDesc.equals(that.methodDesc) : that.methodDesc != null) |
| return false; |
| if (methodName != null ? !methodName.equals(that.methodName) : that.methodName != null) |
| return false; |
| |
| return true; |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = className != null ? className.hashCode() : 0; |
| result = 31 * result + (methodName != null ? methodName.hashCode() : 0); |
| result = 31 * result + (methodDesc != null ? methodDesc.hashCode() : 0); |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| return className + "." + methodName + ":" + methodDesc; |
| } |
| } |
| |
| /** |
| * Stats about a java class. |
| */ |
| public static class Stats { |
| |
| public static final Stats ZERO = new Stats(0, ImmutableSet.<MethodReference>of()); |
| |
| /** Estimated bytes the class will contribute to Dalvik linear alloc. */ |
| public final int estimatedLinearAllocSize; |
| |
| /** Methods referenced by the class. */ |
| public final ImmutableSet<MethodReference> methodReferences; |
| |
| public Stats(int estimatedLinearAllocSize, Set<MethodReference> methodReferences) { |
| this.estimatedLinearAllocSize = estimatedLinearAllocSize; |
| this.methodReferences = ImmutableSet.copyOf(methodReferences); |
| } |
| } |
| |
| /** |
| * CLI wrapper to run against every class in a set of JARs. |
| */ |
| public static void main(String[] args) throws IOException { |
| for (String fname : args) { |
| try (ZipFile inJar = new ZipFile(fname)) { |
| for (ZipEntry entry : Collections.list(inJar.entries())) { |
| if (!entry.getName().endsWith(".class")) { |
| continue; |
| } |
| InputStream rawClass = inJar.getInputStream(entry); |
| int footprint = getEstimate(rawClass, PENALTIES).estimatedLinearAllocSize; |
| System.out.println(footprint + "\t" + entry.getName().replace(".class", "")); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Estimates the footprint that a given class will have in the LinearAlloc buffer |
| * of Android's Dalvik VM. |
| * |
| * @param rawClass Raw bytes of the Java class to analyze. |
| * @return the estimate |
| */ |
| public static Stats getEstimate(InputStream rawClass) throws IOException { |
| return getEstimate(rawClass, PENALTIES); |
| } |
| |
| /** |
| * Estimates the footprint that a given class will have in the LinearAlloc buffer |
| * of Android's Dalvik VM. |
| * |
| * @param rawClass Raw bytes of the Java class to analyze. |
| * @param penalties Map from regex patterns to run against the internal name of the class and |
| * its parent to a "penalty" to apply to the footprint, representing the size |
| * of the vtable of the parent class. |
| * @return the estimate |
| */ |
| private static Stats getEstimate( |
| InputStream rawClass, |
| ImmutableMap<Pattern, Integer> penalties) throws IOException { |
| // SKIP_FRAMES was required to avoid an exception in ClassReader when running on proguard |
| // output. We don't need to visit frames so this isn't an issue. |
| StatsClassVisitor statsVisitor = new StatsClassVisitor(penalties); |
| new ClassReader(rawClass).accept(statsVisitor, ClassReader.SKIP_FRAMES); |
| return new Stats( |
| statsVisitor.footprint, |
| statsVisitor.methodReferenceBuilder.build()); |
| } |
| |
| private static class StatsClassVisitor extends ClassVisitor { |
| |
| private final ImmutableMap<Pattern, Integer> penalties; |
| private final MethodVisitor methodVisitor = new StatsMethodVisitor(); |
| private int footprint; |
| private boolean isInterface; |
| private ImmutableSet.Builder<MethodReference> methodReferenceBuilder; |
| |
| private String className; |
| |
| private StatsClassVisitor(Map<Pattern, Integer> penalties) { |
| super(Opcodes.ASM4); |
| this.penalties = ImmutableMap.copyOf(penalties); |
| this.methodReferenceBuilder = ImmutableSet.builder(); |
| } |
| |
| @Override |
| public void visit( |
| int version, |
| int access, |
| String name, |
| String signature, |
| String superName, |
| String[] interfaces) { |
| |
| this.className = name; |
| if ((access & (Opcodes.ACC_INTERFACE)) != 0) { |
| // Interfaces don't have vtables. |
| // This might undercount annotations, but they are mostly small. |
| isInterface = true; |
| } else { |
| // Some parent classes have big vtable footprints. We try to estimate the parent vtable |
| // size based on the name of the class and parent class. This seems to work reasonably |
| // well in practice because the heaviest vtables are View and Activity, and many of those |
| // classes have clear names and cannot be obfuscated. |
| // Non-interfaces inherit the java.lang.Object vtable, which is 48 bytes. |
| int vtablePenalty = 48; |
| |
| String[] names = new String[]{name, superName}; |
| for (Map.Entry<Pattern, Integer> entry : penalties.entrySet()) { |
| for (String cls : names) { |
| if (entry.getKey().matcher(cls).find()) { |
| vtablePenalty = Math.max(vtablePenalty, entry.getValue()); |
| } |
| } |
| } |
| footprint += vtablePenalty; |
| } |
| } |
| |
| @Override |
| @Nullable |
| public FieldVisitor visitField( |
| int access, String name, String desc, String signature, Object value) { |
| // For non-static fields, Field objects are 16 bytes. |
| if ((access & Opcodes.ACC_STATIC) == 0) { |
| footprint += 16; |
| } |
| |
| return null; |
| } |
| |
| @Override |
| @Nullable |
| public MethodVisitor visitMethod( |
| int access, String name, String desc, String signature, String[] exceptions) { |
| // Method objects are 52 bytes. |
| footprint += 52; |
| |
| // For non-interfaces, each virtual method adds another 4 bytes to the vtable. |
| if (!isInterface) { |
| boolean isDirect = |
| ((access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0) || |
| name.equals("<init>"); |
| if (!isDirect) { |
| footprint += 4; |
| } |
| } |
| methodReferenceBuilder.add(new MethodReference(className, name, desc)); |
| return methodVisitor; |
| } |
| |
| @Override |
| public void visitOuterClass(String owner, String name, String desc) { |
| super.visitOuterClass(owner, name, desc); |
| if (name != null) { |
| methodReferenceBuilder.add(new MethodReference(className, name, desc)); |
| } |
| } |
| |
| private class StatsMethodVisitor extends MethodVisitor { |
| |
| public StatsMethodVisitor() { |
| super(Opcodes.ASM4); |
| } |
| |
| @Override |
| public void visitMethodInsn(int opcode, String owner, String name, String desc) { |
| super.visitMethodInsn(opcode, owner, name, desc); |
| methodReferenceBuilder.add(new MethodReference(owner, name, desc)); |
| } |
| } |
| } |
| } |