diff options
author | 2017-09-22 10:56:22 +0000 | |
---|---|---|
committer | 2017-09-22 10:56:22 +0000 | |
commit | 290d692e518f44fbc1fa9ef05a8f468d37510462 (patch) | |
tree | 36294dac425bf662a9db88ab796d790d842728b5 | |
parent | 79bbbc1c9c40478ccf752214da9574dd22cd8b02 (diff) | |
parent | 26a982ad022a254ac57f84e996c31b4e271de028 (diff) |
Merge changes I9a71ea46,Ib14c294a,Id91c2be4,I3fa77e2e
* changes:
Remove last remaining guava dependencies.
Use a custom parser implementation instead of perflib.
Remove perflib-based native allocation registry identification.
ahat: Expand test coverage using static heap dumps.
39 files changed, 2402 insertions, 764 deletions
diff --git a/tools/ahat/Android.mk b/tools/ahat/Android.mk index 2ce61cf048..f628fe5e11 100644 --- a/tools/ahat/Android.mk +++ b/tools/ahat/Android.mk @@ -22,10 +22,7 @@ include art/build/Android.common_path.mk include $(CLEAR_VARS) LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_JAR_MANIFEST := src/manifest.txt -LOCAL_JAVA_RESOURCE_FILES := \ - $(LOCAL_PATH)/src/style.css - -LOCAL_STATIC_JAVA_LIBRARIES := perflib-prebuilt guavalib trove-prebuilt +LOCAL_JAVA_RESOURCE_FILES := $(LOCAL_PATH)/src/style.css LOCAL_IS_HOST_MODULE := true LOCAL_MODULE_TAGS := optional LOCAL_MODULE := ahat @@ -43,17 +40,6 @@ LOCAL_MODULE := ahat LOCAL_SRC_FILES := ahat include $(BUILD_PREBUILT) -# --- ahat-tests.jar -------------- -include $(CLEAR_VARS) -LOCAL_SRC_FILES := $(call all-java-files-under, test) -LOCAL_JAR_MANIFEST := test/manifest.txt -LOCAL_STATIC_JAVA_LIBRARIES := ahat junit-host -LOCAL_IS_HOST_MODULE := true -LOCAL_MODULE_TAGS := tests -LOCAL_MODULE := ahat-tests -include $(BUILD_HOST_JAVA_LIBRARY) -AHAT_TEST_JAR := $(LOCAL_BUILT_MODULE) - # --- ahat-test-dump.jar -------------- include $(CLEAR_VARS) LOCAL_MODULE := ahat-test-dump @@ -69,7 +55,13 @@ include $(BUILD_JAVA_LIBRARY) AHAT_TEST_DUMP_JAR := $(LOCAL_BUILT_MODULE) AHAT_TEST_DUMP_HPROF := $(intermediates.COMMON)/test-dump.hprof AHAT_TEST_DUMP_BASE_HPROF := $(intermediates.COMMON)/test-dump-base.hprof -AHAT_TEST_DUMP_PROGUARD_MAP := $(proguard_dictionary) +AHAT_TEST_DUMP_PROGUARD_MAP := $(intermediates.COMMON)/test-dump.map + +# Generate the proguard map in the desired location by copying it from +# wherever the build system generates it by default. +$(AHAT_TEST_DUMP_PROGUARD_MAP): PRIVATE_AHAT_SOURCE_PROGUARD_MAP := $(proguard_dictionary) +$(AHAT_TEST_DUMP_PROGUARD_MAP): $(proguard_dictionary) + cp $(PRIVATE_AHAT_SOURCE_PROGUARD_MAP) $@ # Run ahat-test-dump.jar to generate test-dump.hprof and test-dump-base.hprof AHAT_TEST_DUMP_DEPENDENCIES := \ @@ -80,23 +72,36 @@ AHAT_TEST_DUMP_DEPENDENCIES := \ $(AHAT_TEST_DUMP_HPROF): PRIVATE_AHAT_TEST_ART := $(HOST_OUT_EXECUTABLES)/art $(AHAT_TEST_DUMP_HPROF): PRIVATE_AHAT_TEST_DUMP_JAR := $(AHAT_TEST_DUMP_JAR) -$(AHAT_TEST_DUMP_HPROF): PRIVATE_AHAT_TEST_DUMP_DEPENDENCIES := $(AHAT_TEST_DUMP_DEPENDENCIES) $(AHAT_TEST_DUMP_HPROF): $(AHAT_TEST_DUMP_JAR) $(AHAT_TEST_DUMP_DEPENDENCIES) $(PRIVATE_AHAT_TEST_ART) -cp $(PRIVATE_AHAT_TEST_DUMP_JAR) Main $@ $(AHAT_TEST_DUMP_BASE_HPROF): PRIVATE_AHAT_TEST_ART := $(HOST_OUT_EXECUTABLES)/art $(AHAT_TEST_DUMP_BASE_HPROF): PRIVATE_AHAT_TEST_DUMP_JAR := $(AHAT_TEST_DUMP_JAR) -$(AHAT_TEST_DUMP_BASE_HPROF): PRIVATE_AHAT_TEST_DUMP_DEPENDENCIES := $(AHAT_TEST_DUMP_DEPENDENCIES) $(AHAT_TEST_DUMP_BASE_HPROF): $(AHAT_TEST_DUMP_JAR) $(AHAT_TEST_DUMP_DEPENDENCIES) $(PRIVATE_AHAT_TEST_ART) -cp $(PRIVATE_AHAT_TEST_DUMP_JAR) Main $@ --base +# --- ahat-tests.jar -------------- +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(call all-java-files-under, test) +LOCAL_JAR_MANIFEST := test/manifest.txt +LOCAL_JAVA_RESOURCE_FILES := \ + $(AHAT_TEST_DUMP_HPROF) \ + $(AHAT_TEST_DUMP_BASE_HPROF) \ + $(AHAT_TEST_DUMP_PROGUARD_MAP) \ + $(LOCAL_PATH)/test-dump/L.hprof \ + $(LOCAL_PATH)/test-dump/O.hprof \ + $(LOCAL_PATH)/test-dump/RI.hprof +LOCAL_STATIC_JAVA_LIBRARIES := ahat junit-host +LOCAL_IS_HOST_MODULE := true +LOCAL_MODULE_TAGS := tests +LOCAL_MODULE := ahat-tests +include $(BUILD_HOST_JAVA_LIBRARY) +AHAT_TEST_JAR := $(LOCAL_BUILT_MODULE) + .PHONY: ahat-test -ahat-test: PRIVATE_AHAT_TEST_DUMP_HPROF := $(AHAT_TEST_DUMP_HPROF) -ahat-test: PRIVATE_AHAT_TEST_DUMP_BASE_HPROF := $(AHAT_TEST_DUMP_BASE_HPROF) ahat-test: PRIVATE_AHAT_TEST_JAR := $(AHAT_TEST_JAR) -ahat-test: PRIVATE_AHAT_PROGUARD_MAP := $(AHAT_TEST_DUMP_PROGUARD_MAP) -ahat-test: $(AHAT_TEST_JAR) $(AHAT_TEST_DUMP_HPROF) $(AHAT_TEST_DUMP_BASE_HPROF) - java -enableassertions -Dahat.test.dump.hprof=$(PRIVATE_AHAT_TEST_DUMP_HPROF) -Dahat.test.dump.base.hprof=$(PRIVATE_AHAT_TEST_DUMP_BASE_HPROF) -Dahat.test.dump.map=$(PRIVATE_AHAT_PROGUARD_MAP) -jar $(PRIVATE_AHAT_TEST_JAR) +ahat-test: $(AHAT_TEST_JAR) + java -enableassertions -jar $(PRIVATE_AHAT_TEST_JAR) # Clean up local variables. AHAT_TEST_JAR := diff --git a/tools/ahat/README.txt b/tools/ahat/README.txt index 4471c0a7ec..ed40cb7030 100644 --- a/tools/ahat/README.txt +++ b/tools/ahat/README.txt @@ -55,25 +55,6 @@ Things to Test: Reported Issues: * Request to be able to sort tables by size. -Perflib Requests: - * Class objects should have java.lang.Class as their class object, not null. - * ArrayInstance should have asString() to get the string, without requiring a - length function. - * Document that getHeapIndex returns -1 for no such heap. - * Look up totalRetainedSize for a heap by Heap object, not by a separate heap - index. - * What's the difference between getId and getUniqueId? - * I see objects with duplicate references. - * A way to get overall retained size by heap. - * A method Instance.isReachable() - -Things to move to perflib: - * Extracting the string from a String Instance. - * Extracting bitmap data from bitmap instances. - * Adding up allocations by stack frame. - * Computing, for each instance, the other instances it dominates. - * Instance.isRoot and Instance.getRootTypes. - Release History: 1.4 Pending diff --git a/tools/ahat/src/DocString.java b/tools/ahat/src/DocString.java index 7970bf8de4..76e9e80d46 100644 --- a/tools/ahat/src/DocString.java +++ b/tools/ahat/src/DocString.java @@ -16,7 +16,6 @@ package com.android.ahat; -import com.google.common.html.HtmlEscapers; import java.net.URI; import java.net.URISyntaxException; @@ -67,7 +66,7 @@ class DocString { * Returns this object. */ public DocString append(String text) { - mStringBuilder.append(HtmlEscapers.htmlEscaper().escape(text)); + mStringBuilder.append(HtmlEscaper.escape(text)); return this; } @@ -185,7 +184,7 @@ class DocString { public DocString appendImage(URI uri, String alt) { mStringBuilder.append("<img alt=\""); - mStringBuilder.append(HtmlEscapers.htmlEscaper().escape(alt)); + mStringBuilder.append(HtmlEscaper.escape(alt)); mStringBuilder.append("\" src=\""); mStringBuilder.append(uri.toASCIIString()); mStringBuilder.append("\" />"); @@ -194,7 +193,7 @@ class DocString { public DocString appendThumbnail(URI uri, String alt) { mStringBuilder.append("<img height=\"16\" alt=\""); - mStringBuilder.append(HtmlEscapers.htmlEscaper().escape(alt)); + mStringBuilder.append(HtmlEscaper.escape(alt)); mStringBuilder.append("\" src=\""); mStringBuilder.append(uri.toASCIIString()); mStringBuilder.append("\" />"); diff --git a/tools/ahat/src/HtmlEscaper.java b/tools/ahat/src/HtmlEscaper.java new file mode 100644 index 0000000000..75a68277d3 --- /dev/null +++ b/tools/ahat/src/HtmlEscaper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat; + +public class HtmlEscaper { + /** + * Escape html characters in the input string. + */ + public static String escape(String text) { + String specials = "&<>\'\""; + String[] replacements = new String[]{"&", "<", ">", "'", """}; + StringBuilder sb = null; + int low = 0; + for (int i = 0; i < text.length(); ++i) { + int s = specials.indexOf(text.charAt(i)); + if (s != -1) { + if (sb == null) { + sb = new StringBuilder(); + } + sb.append(text.substring(low, i)); + sb.append(replacements[s]); + low = i + 1; + } + } + if (sb == null) { + return text; + } + + sb.append(text.substring(low)); + return sb.toString(); + } +} + + diff --git a/tools/ahat/src/Main.java b/tools/ahat/src/Main.java index 7cda035576..623a865785 100644 --- a/tools/ahat/src/Main.java +++ b/tools/ahat/src/Main.java @@ -18,7 +18,8 @@ package com.android.ahat; import com.android.ahat.heapdump.AhatSnapshot; import com.android.ahat.heapdump.Diff; -import com.android.tools.perflib.heap.ProguardMap; +import com.android.ahat.heapdump.Parser; +import com.android.ahat.proguard.ProguardMap; import com.sun.net.httpserver.HttpServer; import java.io.File; import java.io.IOException; @@ -46,7 +47,7 @@ public class Main { out.println(""); } - public static void main(String[] args) throws IOException { + public static void main(String[] args) throws Exception { int port = 7100; for (String arg : args) { if (arg.equals("--help")) { @@ -110,11 +111,11 @@ public class Main { HttpServer server = HttpServer.create(addr, 0); System.out.println("Processing hprof file..."); - AhatSnapshot ahat = AhatSnapshot.fromHprof(hprof, map); + AhatSnapshot ahat = Parser.parseHeapDump(hprof, map); if (hprofbase != null) { System.out.println("Processing baseline hprof file..."); - AhatSnapshot base = AhatSnapshot.fromHprof(hprofbase, mapbase); + AhatSnapshot base = Parser.parseHeapDump(hprofbase, mapbase); System.out.println("Diffing hprof files..."); Diff.snapshots(ahat, base); diff --git a/tools/ahat/src/ObjectHandler.java b/tools/ahat/src/ObjectHandler.java index f4926aa83b..79f8b76c92 100644 --- a/tools/ahat/src/ObjectHandler.java +++ b/tools/ahat/src/ObjectHandler.java @@ -25,6 +25,7 @@ import com.android.ahat.heapdump.DiffFields; import com.android.ahat.heapdump.DiffedFieldValue; import com.android.ahat.heapdump.FieldValue; import com.android.ahat.heapdump.PathElement; +import com.android.ahat.heapdump.RootType; import com.android.ahat.heapdump.Site; import com.android.ahat.heapdump.Value; import java.io.IOException; @@ -74,13 +75,13 @@ class ObjectHandler implements AhatHandler { doc.description(DocString.text("Heap"), DocString.text(inst.getHeap().getName())); - Collection<String> rootTypes = inst.getRootTypes(); + Collection<RootType> rootTypes = inst.getRootTypes(); if (rootTypes != null) { DocString types = new DocString(); String comma = ""; - for (String type : rootTypes) { + for (RootType type : rootTypes) { types.append(comma); - types.append(type); + types.append(type.toString()); comma = ", "; } doc.description(DocString.text("Root Types"), types); @@ -175,21 +176,21 @@ class ObjectHandler implements AhatHandler { was.append(Summarizer.summarize(previous)); switch (field.status) { case ADDED: - doc.row(DocString.text(field.type), + doc.row(DocString.text(field.type.name), DocString.text(field.name), Summarizer.summarize(field.current), DocString.added("new")); break; case MATCHED: - doc.row(DocString.text(field.type), + doc.row(DocString.text(field.type.name), DocString.text(field.name), Summarizer.summarize(field.current), Objects.equals(field.current, previous) ? new DocString() : was); break; case DELETED: - doc.row(DocString.text(field.type), + doc.row(DocString.text(field.type.name), DocString.text(field.name), DocString.removed("del"), was); diff --git a/tools/ahat/src/StaticHandler.java b/tools/ahat/src/StaticHandler.java index b2805d624d..4a68f1c12f 100644 --- a/tools/ahat/src/StaticHandler.java +++ b/tools/ahat/src/StaticHandler.java @@ -16,7 +16,6 @@ package com.android.ahat; -import com.google.common.io.ByteStreams; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import java.io.IOException; @@ -49,7 +48,12 @@ class StaticHandler implements HttpHandler { exchange.getResponseHeaders().add("Content-Type", mContentType); exchange.sendResponseHeaders(200, 0); OutputStream os = exchange.getResponseBody(); - ByteStreams.copy(is, os); + int read; + byte[] buf = new byte[4096]; + while ((read = is.read(buf)) >= 0) { + os.write(buf, 0, read); + } + is.close(); os.close(); } } diff --git a/tools/ahat/src/heapdump/AhatArrayInstance.java b/tools/ahat/src/heapdump/AhatArrayInstance.java index 8d23276fde..50a4805bed 100644 --- a/tools/ahat/src/heapdump/AhatArrayInstance.java +++ b/tools/ahat/src/heapdump/AhatArrayInstance.java @@ -16,20 +16,20 @@ package com.android.ahat.heapdump; -import com.android.tools.perflib.heap.ArrayInstance; -import com.android.tools.perflib.heap.Instance; import java.nio.charset.StandardCharsets; import java.util.AbstractList; import java.util.Collections; import java.util.List; public class AhatArrayInstance extends AhatInstance { - // To save space, we store byte, character, and object arrays directly as - // byte, character, and AhatInstance arrays respectively. This is especially - // important for large byte arrays, such as bitmaps. All other array types - // are stored as an array of objects, though we could potentially save space - // by specializing those too. mValues is a list view of the underlying - // array. + // To save space, we store arrays as primitive arrays or AhatInstance arrays + // and provide a wrapper over the arrays to expose a list of Values. + // This is especially important for large byte arrays, such as bitmaps. + // We keep a separate pointer to the underlying array in the case of byte or + // char arrays because they are sometimes useful to have. + // TODO: Have different subtypes of AhatArrayInstance to avoid the overhead + // of these extra pointers and cost in getReferences when the array type is + // not relevant? private List<Value> mValues; private byte[] mByteArray; // null if not a byte array. private char[] mCharArray; // null if not a char array. @@ -38,72 +38,151 @@ public class AhatArrayInstance extends AhatInstance { super(id); } - @Override void initialize(AhatSnapshot snapshot, Instance inst, Site site) { - super.initialize(snapshot, inst, site); + /** + * Initialize the array elements for a primitive boolean array. + */ + void initialize(final boolean[] bools) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return bools.length; + } - ArrayInstance array = (ArrayInstance)inst; - switch (array.getArrayType()) { - case OBJECT: - Object[] objects = array.getValues(); - final AhatInstance[] insts = new AhatInstance[objects.length]; - for (int i = 0; i < objects.length; i++) { - if (objects[i] != null) { - Instance ref = (Instance)objects[i]; - insts[i] = snapshot.findInstance(ref.getId()); - } - } - mValues = new AbstractList<Value>() { - @Override public int size() { - return insts.length; - } + @Override public Value get(int index) { + return Value.pack(bools[index]); + } + }; + } - @Override public Value get(int index) { - return Value.pack(insts[index]); - } - }; - break; - - case CHAR: - final char[] chars = array.asCharArray(0, array.getLength()); - mCharArray = chars; - mValues = new AbstractList<Value>() { - @Override public int size() { - return chars.length; - } + /** + * Initialize the array elements for a primitive char array. + */ + void initialize(final char[] chars) { + mCharArray = chars; + mValues = new AbstractList<Value>() { + @Override public int size() { + return chars.length; + } - @Override public Value get(int index) { - return Value.pack(chars[index]); - } - }; - break; - - case BYTE: - final byte[] bytes = array.asRawByteArray(0, array.getLength()); - mByteArray = bytes; - mValues = new AbstractList<Value>() { - @Override public int size() { - return bytes.length; - } + @Override public Value get(int index) { + return Value.pack(chars[index]); + } + }; + } - @Override public Value get(int index) { - return Value.pack(bytes[index]); - } - }; - break; + /** + * Initialize the array elements for a primitive float array. + */ + void initialize(final float[] floats) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return floats.length; + } - default: - final Object[] values = array.getValues(); - mValues = new AbstractList<Value>() { - @Override public int size() { - return values.length; - } + @Override public Value get(int index) { + return Value.pack(floats[index]); + } + }; + } - @Override public Value get(int index) { - return Value.pack(values[index]); - } - }; - break; + /** + * Initialize the array elements for a primitive double array. + */ + void initialize(final double[] doubles) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return doubles.length; + } + + @Override public Value get(int index) { + return Value.pack(doubles[index]); + } + }; + } + + /** + * Initialize the array elements for a primitive byte array. + */ + void initialize(final byte[] bytes) { + mByteArray = bytes; + mValues = new AbstractList<Value>() { + @Override public int size() { + return bytes.length; + } + + @Override public Value get(int index) { + return Value.pack(bytes[index]); + } + }; + } + + /** + * Initialize the array elements for a primitive short array. + */ + void initialize(final short[] shorts) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return shorts.length; + } + + @Override public Value get(int index) { + return Value.pack(shorts[index]); + } + }; + } + + /** + * Initialize the array elements for a primitive int array. + */ + void initialize(final int[] ints) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return ints.length; + } + + @Override public Value get(int index) { + return Value.pack(ints[index]); + } + }; + } + + /** + * Initialize the array elements for a primitive long array. + */ + void initialize(final long[] longs) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return longs.length; + } + + @Override public Value get(int index) { + return Value.pack(longs[index]); + } + }; + } + + /** + * Initialize the array elements for an instance array. + */ + void initialize(final AhatInstance[] insts) { + mValues = new AbstractList<Value>() { + @Override public int size() { + return insts.length; + } + + @Override public Value get(int index) { + return Value.pack(insts[index]); + } + }; + } + + @Override + protected long getExtraJavaSize() { + int length = getLength(); + if (length == 0) { + return 0; } + + return Value.getType(mValues.get(0)).size * getLength(); } /** diff --git a/tools/ahat/src/heapdump/AhatClassInstance.java b/tools/ahat/src/heapdump/AhatClassInstance.java index f7d8431a7b..94efa5049f 100644 --- a/tools/ahat/src/heapdump/AhatClassInstance.java +++ b/tools/ahat/src/heapdump/AhatClassInstance.java @@ -16,11 +16,8 @@ package com.android.ahat.heapdump; -import com.android.tools.perflib.heap.ClassInstance; -import com.android.tools.perflib.heap.Instance; import java.awt.image.BufferedImage; import java.util.Iterator; -import java.util.List; import java.util.NoSuchElementException; public class AhatClassInstance extends AhatInstance { @@ -34,15 +31,13 @@ public class AhatClassInstance extends AhatInstance { super(id); } - @Override void initialize(AhatSnapshot snapshot, Instance inst, Site site) { - super.initialize(snapshot, inst, site); + void initialize(Value[] fields) { + mFields = fields; + } - ClassInstance classInst = (ClassInstance)inst; - List<ClassInstance.FieldValue> fieldValues = classInst.getValues(); - mFields = new Value[fieldValues.size()]; - for (int i = 0; i < mFields.length; i++) { - mFields[i] = snapshot.getValue(fieldValues.get(i).getValue()); - } + @Override + protected long getExtraJavaSize() { + return 0; } @Override public Value getField(String fieldName) { @@ -123,7 +118,7 @@ public class AhatClassInstance extends AhatInstance { } Value value = getField("value"); - if (!value.isAhatInstance()) { + if (value == null || !value.isAhatInstance()) { return null; } @@ -248,6 +243,49 @@ public class AhatClassInstance extends AhatInstance { return bitmap; } + @Override + public RegisteredNativeAllocation asRegisteredNativeAllocation() { + if (!isInstanceOfClass("sun.misc.Cleaner")) { + return null; + } + + Value vthunk = getField("thunk"); + if (vthunk == null || !vthunk.isAhatInstance()) { + return null; + } + + AhatClassInstance thunk = vthunk.asAhatInstance().asClassInstance(); + if (thunk == null + || !thunk.isInstanceOfClass("libcore.util.NativeAllocationRegistry$CleanerThunk")) { + return null; + } + + Value vregistry = thunk.getField("this$0"); + if (vregistry == null || !vregistry.isAhatInstance()) { + return null; + } + + AhatClassInstance registry = vregistry.asAhatInstance().asClassInstance(); + if (registry == null || !registry.isInstanceOfClass("libcore.util.NativeAllocationRegistry")) { + return null; + } + + Value size = registry.getField("size"); + if (!size.isLong()) { + return null; + } + + Value referent = getField("referent"); + if (referent == null || !referent.isAhatInstance()) { + return null; + } + + RegisteredNativeAllocation rna = new RegisteredNativeAllocation(); + rna.referent = referent.asAhatInstance(); + rna.size = size.asLong(); + return rna; + } + private static class InstanceFieldIterator implements Iterable<FieldValue>, Iterator<FieldValue> { // The complete list of instance field values to iterate over, including diff --git a/tools/ahat/src/heapdump/AhatClassObj.java b/tools/ahat/src/heapdump/AhatClassObj.java index 08c70974c6..be0f71306e 100644 --- a/tools/ahat/src/heapdump/AhatClassObj.java +++ b/tools/ahat/src/heapdump/AhatClassObj.java @@ -16,13 +16,9 @@ package com.android.ahat.heapdump; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Instance; import java.util.AbstractList; import java.util.Arrays; -import java.util.Collection; import java.util.List; -import java.util.Map; public class AhatClassObj extends AhatInstance { private String mClassName; @@ -30,43 +26,32 @@ public class AhatClassObj extends AhatInstance { private AhatInstance mClassLoader; private FieldValue[] mStaticFieldValues; private Field[] mInstanceFields; + private long mStaticFieldsSize; + private long mInstanceSize; - public AhatClassObj(long id) { + public AhatClassObj(long id, String className) { super(id); + mClassName = className; } - @Override void initialize(AhatSnapshot snapshot, Instance inst, Site site) { - super.initialize(snapshot, inst, site); - - ClassObj classObj = (ClassObj)inst; - mClassName = classObj.getClassName(); - - ClassObj superClassObj = classObj.getSuperClassObj(); - if (superClassObj != null) { - mSuperClassObj = snapshot.findClassObj(superClassObj.getId()); - } - - Instance loader = classObj.getClassLoader(); - if (loader != null) { - mClassLoader = snapshot.findInstance(loader.getId()); - } - - Collection<Map.Entry<com.android.tools.perflib.heap.Field, Object>> fieldValues - = classObj.getStaticFieldValues().entrySet(); - mStaticFieldValues = new FieldValue[fieldValues.size()]; - int index = 0; - for (Map.Entry<com.android.tools.perflib.heap.Field, Object> field : fieldValues) { - String name = field.getKey().getName(); - String type = field.getKey().getType().toString(); - Value value = snapshot.getValue(field.getValue()); - mStaticFieldValues[index++] = new FieldValue(name, type, value); - } - - com.android.tools.perflib.heap.Field[] fields = classObj.getFields(); - mInstanceFields = new Field[fields.length]; - for (int i = 0; i < fields.length; i++) { - mInstanceFields[i] = new Field(fields[i].getName(), fields[i].getType().toString()); - } + void initialize(AhatClassObj superClass, + long instanceSize, + Field[] instanceFields, + long staticFieldsSize) { + mSuperClassObj = superClass; + mInstanceSize = instanceSize; + mInstanceFields = instanceFields; + mStaticFieldsSize = staticFieldsSize; + } + + void initialize(AhatInstance classLoader, FieldValue[] staticFields) { + mClassLoader = classLoader; + mStaticFieldValues = staticFields; + } + + @Override + protected long getExtraJavaSize() { + return mStaticFieldsSize; } /** @@ -91,6 +76,14 @@ public class AhatClassObj extends AhatInstance { } /** + * Returns the size of instances of this object, as reported in the heap + * dump. + */ + public long getInstanceSize() { + return mInstanceSize; + } + + /** * Returns the static field values for this class object. */ public List<FieldValue> getStaticFieldValues() { diff --git a/tools/ahat/src/heapdump/AhatInstance.java b/tools/ahat/src/heapdump/AhatInstance.java index 0e7855801d..c04448728f 100644 --- a/tools/ahat/src/heapdump/AhatInstance.java +++ b/tools/ahat/src/heapdump/AhatInstance.java @@ -17,8 +17,6 @@ package com.android.ahat.heapdump; import com.android.ahat.dominators.DominatorsComputation; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Instance; import java.awt.image.BufferedImage; import java.util.ArrayDeque; import java.util.ArrayList; @@ -34,14 +32,15 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, private final long mId; // Fields initialized in initialize(). - private Size mSize; private AhatHeap mHeap; private AhatClassObj mClassObj; private Site mSite; - // If this instance is a root, mRootTypes contains a set of the root types. - // If this instance is not a root, mRootTypes is null. - private List<String> mRootTypes; + // Bit vector of the root types of this object. + private int mRootTypes; + + // Field initialized via addRegisterednativeSize. + private long mRegisteredNativeSize = 0; // Fields initialized in computeReverseReferences(). private AhatInstance mNextInstanceToGcRoot; @@ -55,33 +54,29 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, private AhatInstance mImmediateDominator; private List<AhatInstance> mDominated = new ArrayList<AhatInstance>(); private Size[] mRetainedSizes; - private Object mDominatorsComputationState; // The baseline instance for purposes of diff. private AhatInstance mBaseline; + // temporary user data associated with this instance. This is used for a + // couple different purposes: + // 1. During parsing of instances, to store temporary field data. + // 2. During dominators computation, to store the dominators computation state. + private Object mTemporaryUserData; + public AhatInstance(long id) { mId = id; mBaseline = this; } /** - * Initializes this AhatInstance based on the given perflib instance. - * The AhatSnapshot should be used to look up AhatInstances and AhatHeaps. - * There is no guarantee that the AhatInstances returned by - * snapshot.findInstance have been initialized yet. + * Initialize this AhatInstance based on the the given info. */ - void initialize(AhatSnapshot snapshot, Instance inst, Site site) { - site.addInstance(this); - mSize = new Size(inst.getSize(), 0); - mHeap = snapshot.getHeap(inst.getHeap().getName()); - - ClassObj clsObj = inst.getClassObj(); - if (clsObj != null) { - mClassObj = snapshot.findClassObj(clsObj.getId()); - } - + void initialize(AhatHeap heap, Site site, AhatClassObj classObj) { + mHeap = heap; mSite = site; + site.addInstance(this); + mClassObj = classObj; } /** @@ -95,10 +90,20 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, * Returns the shallow number of bytes this object takes up. */ public Size getSize() { - return mSize; + return new Size(mClassObj.getInstanceSize() + getExtraJavaSize(), mRegisteredNativeSize); } /** + * Returns the number of bytes taken up by this object on the Java heap + * beyond the standard instance size as recorded by the class of this + * instance. + * + * For example, class objects will have extra size for static fields and + * array objects will have extra size for the array elements. + */ + protected abstract long getExtraJavaSize(); + + /** * Returns the number of bytes belonging to the given heap that this instance * retains. */ @@ -127,7 +132,7 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, * Increment the number of registered native bytes tied to this object. */ void addRegisteredNativeSize(long size) { - mSize = mSize.plusRegisteredNativeSize(size); + mRegisteredNativeSize += size; } /** @@ -154,27 +159,32 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, * Returns true if this instance is marked as a root instance. */ public boolean isRoot() { - return mRootTypes != null; + return mRootTypes != 0; } /** * Marks this instance as being a root of the given type. */ - void addRootType(String type) { - if (mRootTypes == null) { - mRootTypes = new ArrayList<String>(); - mRootTypes.add(type); - } else if (!mRootTypes.contains(type)) { - mRootTypes.add(type); - } + void addRootType(RootType type) { + mRootTypes |= type.mask; } /** - * Returns a list of string descriptions of the root types of this object. + * Returns a list of the root types of this object. * Returns null if this object is not a root. */ - public Collection<String> getRootTypes() { - return mRootTypes; + public Collection<RootType> getRootTypes() { + if (!isRoot()) { + return null; + } + + List<RootType> types = new ArrayList<RootType>(); + for (RootType type : RootType.values()) { + if ((mRootTypes & type.mask) != 0) { + types.add(type); + } + } + return types; } /** @@ -363,6 +373,19 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, return null; } + public static class RegisteredNativeAllocation { + public AhatInstance referent; + public long size; + }; + + /** + * Return the registered native allocation that this instance represents, if + * any. This is relevant for instances of sun.misc.Cleaner. + */ + public RegisteredNativeAllocation asRegisteredNativeAllocation() { + return null; + } + /** * Returns a sample path from a GC root to this instance. * This instance is included as the last element of the path with an empty @@ -433,6 +456,14 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, return new AhatPlaceHolderInstance(this); } + public void setTemporaryUserData(Object state) { + mTemporaryUserData = state; + } + + public Object getTemporaryUserData() { + return mTemporaryUserData; + } + /** * Initialize the reverse reference fields of this instance and all other * instances reachable from it. Initializes the following fields: @@ -498,7 +529,7 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, } if (!(inst instanceof SuperRoot)) { inst.mRetainedSizes[inst.mHeap.getIndex()] = - inst.mRetainedSizes[inst.mHeap.getIndex()].plus(inst.mSize); + inst.mRetainedSizes[inst.mHeap.getIndex()].plus(inst.getSize()); } deque.push(inst); for (AhatInstance dominated : inst.mDominated) { @@ -516,12 +547,12 @@ public abstract class AhatInstance implements Diffable<AhatInstance>, @Override public void setDominatorsComputationState(Object state) { - mDominatorsComputationState = state; + setTemporaryUserData(state); } @Override public Object getDominatorsComputationState() { - return mDominatorsComputationState; + return getTemporaryUserData(); } @Override diff --git a/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java b/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java index 8b4c6796aa..07f5b50012 100644 --- a/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java +++ b/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java @@ -24,11 +24,15 @@ package com.android.ahat.heapdump; */ public class AhatPlaceHolderClassObj extends AhatClassObj { AhatPlaceHolderClassObj(AhatClassObj baseline) { - super(-1); + super(-1, baseline.getClassName()); setBaseline(baseline); baseline.setBaseline(this); } + @Override public Size getSize() { + return Size.ZERO; + } + @Override public Size getRetainedSize(AhatHeap heap) { return Size.ZERO; } diff --git a/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java b/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java index 9abc952072..884940370d 100644 --- a/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java +++ b/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java @@ -36,6 +36,10 @@ public class AhatPlaceHolderInstance extends AhatInstance { return Size.ZERO; } + @Override protected long getExtraJavaSize() { + return 0; + } + @Override public Size getRetainedSize(AhatHeap heap) { return Size.ZERO; } diff --git a/tools/ahat/src/heapdump/AhatSnapshot.java b/tools/ahat/src/heapdump/AhatSnapshot.java index 1b2cf3c59f..945966cec7 100644 --- a/tools/ahat/src/heapdump/AhatSnapshot.java +++ b/tools/ahat/src/heapdump/AhatSnapshot.java @@ -17,158 +17,43 @@ package com.android.ahat.heapdump; import com.android.ahat.dominators.DominatorsComputation; -import com.android.tools.perflib.captures.DataBuffer; -import com.android.tools.perflib.captures.MemoryMappedFileBuffer; -import com.android.tools.perflib.heap.ArrayInstance; -import com.android.tools.perflib.heap.ClassInstance; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Heap; -import com.android.tools.perflib.heap.Instance; -import com.android.tools.perflib.heap.ProguardMap; -import com.android.tools.perflib.heap.RootObj; -import com.android.tools.perflib.heap.Snapshot; -import com.android.tools.perflib.heap.StackFrame; -import com.android.tools.perflib.heap.StackTrace; -import gnu.trove.TObjectProcedure; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; import java.util.List; -import java.util.Map; public class AhatSnapshot implements Diffable<AhatSnapshot> { - private final Site mRootSite = new Site("ROOT"); + private final Site mRootSite; - // Collection of objects whose immediate dominator is the SENTINEL_ROOT. - private final List<AhatInstance> mRooted; + private final SuperRoot mSuperRoot; - // List of all ahat instances stored in increasing order by id. - private final List<AhatInstance> mInstances = new ArrayList<AhatInstance>(); + // List of all ahat instances. + private final Instances<AhatInstance> mInstances; - private final List<AhatHeap> mHeaps = new ArrayList<AhatHeap>(); + private List<AhatHeap> mHeaps; private AhatSnapshot mBaseline = this; - /** - * Create an AhatSnapshot from an hprof file. - */ - public static AhatSnapshot fromHprof(File hprof, ProguardMap map) throws IOException { - return fromDataBuffer(new MemoryMappedFileBuffer(hprof), map); - } - - /** - * Create an AhatSnapshot from an in-memory data buffer. - */ - public static AhatSnapshot fromDataBuffer(DataBuffer buffer, ProguardMap map) throws IOException { - AhatSnapshot snapshot = new AhatSnapshot(buffer, map); - - // Request a GC now to clean up memory used by perflib. This helps to - // avoid a noticable pause when visiting the first interesting page in - // ahat. - System.gc(); - - return snapshot; - } - - /** - * Constructs an AhatSnapshot for the given hprof binary data. - */ - private AhatSnapshot(DataBuffer buffer, ProguardMap map) throws IOException { - Snapshot snapshot = Snapshot.createSnapshot(buffer, map); - - // Properly label the class of class objects in the perflib snapshot. - final ClassObj javaLangClass = snapshot.findClass("java.lang.Class"); - if (javaLangClass != null) { - for (Heap heap : snapshot.getHeaps()) { - Collection<ClassObj> classes = heap.getClasses(); - for (ClassObj clsObj : classes) { - if (clsObj.getClassObj() == null) { - clsObj.setClassId(javaLangClass.getId()); - } - } - } - } - - // Create mappings from id to ahat instance and heaps. - Collection<Heap> heaps = snapshot.getHeaps(); - for (Heap heap : heaps) { - // Note: mHeaps will not be in index order if snapshot.getHeaps does not - // return heaps in index order. That's fine, because we don't rely on - // mHeaps being in index order. - mHeaps.add(new AhatHeap(heap.getName(), snapshot.getHeapIndex(heap))); - TObjectProcedure<Instance> doCreate = new TObjectProcedure<Instance>() { - @Override - public boolean execute(Instance inst) { - long id = inst.getId(); - if (inst instanceof ClassInstance) { - mInstances.add(new AhatClassInstance(id)); - } else if (inst instanceof ArrayInstance) { - mInstances.add(new AhatArrayInstance(id)); - } else if (inst instanceof ClassObj) { - AhatClassObj classObj = new AhatClassObj(id); - mInstances.add(classObj); - } - return true; - } - }; - for (Instance instance : heap.getClasses()) { - doCreate.execute(instance); - } - heap.forEachInstance(doCreate); - } - - // Sort the instances by id so we can use binary search to lookup - // instances by id. - mInstances.sort(new Comparator<AhatInstance>() { - @Override - public int compare(AhatInstance a, AhatInstance b) { - return Long.compare(a.getId(), b.getId()); - } - }); - - Map<Instance, Long> registeredNative = Perflib.getRegisteredNativeAllocations(snapshot); - - // Initialize ahat snapshot and instances based on the perflib snapshot - // and instances. - for (AhatInstance ahat : mInstances) { - Instance inst = snapshot.findInstance(ahat.getId()); - - StackFrame[] frames = null; - StackTrace stack = inst.getStack(); - if (stack != null) { - frames = stack.getFrames(); - } - ahat.initialize(this, inst, mRootSite.getSite(frames)); - - Long registeredNativeSize = registeredNative.get(inst); - if (registeredNativeSize != null) { - ahat.addRegisteredNativeSize(registeredNativeSize); + AhatSnapshot(SuperRoot root, + Instances<AhatInstance> instances, + List<AhatHeap> heaps, + Site rootSite) { + mSuperRoot = root; + mInstances = instances; + mHeaps = heaps; + mRootSite = rootSite; + + // Update registered native allocation size. + for (AhatInstance cleaner : mInstances) { + AhatInstance.RegisteredNativeAllocation nra = cleaner.asRegisteredNativeAllocation(); + if (nra != null) { + nra.referent.addRegisteredNativeSize(nra.size); } } - // Record the roots and their types. - SuperRoot superRoot = new SuperRoot(); - for (RootObj root : snapshot.getGCRoots()) { - Instance inst = root.getReferredInstance(); - if (inst != null) { - AhatInstance ahat = findInstance(inst.getId()); - if (!ahat.isRoot()) { - superRoot.addRoot(ahat); - } - ahat.addRootType(root.getRootType().toString()); - } - } - snapshot.dispose(); - - AhatInstance.computeReverseReferences(superRoot); - DominatorsComputation.computeDominators(superRoot); - AhatInstance.computeRetainedSize(superRoot, mHeaps.size()); + AhatInstance.computeReverseReferences(mSuperRoot); + DominatorsComputation.computeDominators(mSuperRoot); + AhatInstance.computeRetainedSize(mSuperRoot, mHeaps.size()); - mRooted = superRoot.getDominated(); for (AhatHeap heap : mHeaps) { - heap.addToSize(superRoot.getRetainedSize(heap)); + heap.addToSize(mSuperRoot.getRetainedSize(heap)); } mRootSite.prepareForUse(0, mHeaps.size()); @@ -179,22 +64,7 @@ public class AhatSnapshot implements Diffable<AhatSnapshot> { * Returns null if no instance with the given id is found. */ public AhatInstance findInstance(long id) { - // Binary search over the sorted instances. - int start = 0; - int end = mInstances.size(); - while (start < end) { - int mid = start + ((end - start) / 2); - AhatInstance midInst = mInstances.get(mid); - long midId = midInst.getId(); - if (id == midId) { - return midInst; - } else if (id < midId) { - end = mid; - } else { - start = mid + 1; - } - } - return null; + return mInstances.get(id); } /** @@ -235,7 +105,7 @@ public class AhatSnapshot implements Diffable<AhatSnapshot> { * SENTINEL_ROOT. */ public List<AhatInstance> getRooted() { - return mRooted; + return mSuperRoot.getDominated(); } /** @@ -252,14 +122,6 @@ public class AhatSnapshot implements Diffable<AhatSnapshot> { return site == null ? mRootSite : site; } - // Return the Value for the given perflib value object. - Value getValue(Object value) { - if (value instanceof Instance) { - value = findInstance(((Instance)value).getId()); - } - return Value.pack(value); - } - public void setBaseline(AhatSnapshot baseline) { mBaseline = baseline; } diff --git a/tools/ahat/src/heapdump/DiffedFieldValue.java b/tools/ahat/src/heapdump/DiffedFieldValue.java index e2dcf3e8f0..3cd273ed98 100644 --- a/tools/ahat/src/heapdump/DiffedFieldValue.java +++ b/tools/ahat/src/heapdump/DiffedFieldValue.java @@ -23,7 +23,7 @@ import java.util.Objects; */ public class DiffedFieldValue { public final String name; - public final String type; + public final Type type; public final Value current; public final Value baseline; @@ -60,7 +60,7 @@ public class DiffedFieldValue { return new DiffedFieldValue(baseline.name, baseline.type, null, baseline.value, Status.DELETED); } - private DiffedFieldValue(String name, String type, Value current, Value baseline, Status status) { + private DiffedFieldValue(String name, Type type, Value current, Value baseline, Status status) { this.name = name; this.type = type; this.current = current; diff --git a/tools/ahat/src/heapdump/Field.java b/tools/ahat/src/heapdump/Field.java index 01f87c726e..dff401796a 100644 --- a/tools/ahat/src/heapdump/Field.java +++ b/tools/ahat/src/heapdump/Field.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ package com.android.ahat.heapdump; public class Field { public final String name; - public final String type; + public final Type type; - public Field(String name, String type) { + public Field(String name, Type type) { this.name = name; this.type = type; } diff --git a/tools/ahat/src/heapdump/FieldValue.java b/tools/ahat/src/heapdump/FieldValue.java index 6d725953b3..20e6da7271 100644 --- a/tools/ahat/src/heapdump/FieldValue.java +++ b/tools/ahat/src/heapdump/FieldValue.java @@ -18,10 +18,10 @@ package com.android.ahat.heapdump; public class FieldValue { public final String name; - public final String type; + public final Type type; public final Value value; - public FieldValue(String name, String type, Value value) { + public FieldValue(String name, Type type, Value value) { this.name = name; this.type = type; this.value = value; diff --git a/tools/ahat/src/heapdump/HprofFormatException.java b/tools/ahat/src/heapdump/HprofFormatException.java new file mode 100644 index 0000000000..55e8958c43 --- /dev/null +++ b/tools/ahat/src/heapdump/HprofFormatException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat.heapdump; + +public class HprofFormatException extends Exception { + public HprofFormatException(String msg) { + super(msg); + } +} diff --git a/tools/ahat/src/heapdump/Instances.java b/tools/ahat/src/heapdump/Instances.java new file mode 100644 index 0000000000..085144650f --- /dev/null +++ b/tools/ahat/src/heapdump/Instances.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat.heapdump; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +/** + * A collection of instances that can be searched for by id. + */ +class Instances<T extends AhatInstance> implements Iterable<T> { + + private final List<T> mInstances; + + /** + * Create a collection of instances that can be looked up by id. + * Note: this takes ownership of the given list of instances. + */ + public Instances(List<T> instances) { + mInstances = instances; + + // Sort the instances by id so we can use binary search to lookup + // instances by id. + instances.sort(new Comparator<AhatInstance>() { + @Override + public int compare(AhatInstance a, AhatInstance b) { + return Long.compare(a.getId(), b.getId()); + } + }); + } + + /** + * Look up an instance by id. + * Returns null if no instance with the given id is found. + */ + public T get(long id) { + // Binary search over the sorted instances. + int start = 0; + int end = mInstances.size(); + while (start < end) { + int mid = start + ((end - start) / 2); + T midInst = mInstances.get(mid); + long midId = midInst.getId(); + if (id == midId) { + return midInst; + } else if (id < midId) { + end = mid; + } else { + start = mid + 1; + } + } + return null; + } + + @Override + public Iterator<T> iterator() { + return mInstances.iterator(); + } +} + diff --git a/tools/ahat/src/heapdump/Parser.java b/tools/ahat/src/heapdump/Parser.java new file mode 100644 index 0000000000..3d5f95f6ae --- /dev/null +++ b/tools/ahat/src/heapdump/Parser.java @@ -0,0 +1,942 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat.heapdump; + +import com.android.ahat.proguard.ProguardMap; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class Parser { + private static final int ID_SIZE = 4; + + /** + * Parse the given heap dump using the given proguard map for deobfuscation. + * We make the following assumptions about valid heap dumps: + * Class serial numbers, stack frames, and stack traces + * individually satisfy the following: + * - all elements are defined before they are referenced. + * - ids are densely packed in some range [a, b] where a is not + * necessarily 0. + * - there are not more than 2^31 elements defined. + * All classes are defined via a LOAD CLASS record before the first heap + * dump segment. + * The ID size used in the heap dump is 4 bytes. + */ + public static AhatSnapshot parseHeapDump(File hprof, ProguardMap map) + throws IOException, HprofFormatException { + return parseHeapDump(new HprofBuffer(hprof), map); + } + + /** + * Parse a heap dump from a byte buffer. + */ + public static AhatSnapshot parseHeapDump(ByteBuffer hprof, ProguardMap map) + throws IOException, HprofFormatException { + return parseHeapDump(new HprofBuffer(hprof), map); + } + + private static AhatSnapshot parseHeapDump(HprofBuffer hprof, ProguardMap map) + throws IOException, HprofFormatException { + // Read, and mostly ignore, the hprof header info. + { + StringBuilder format = new StringBuilder(); + int b; + while ((b = hprof.getU1()) != 0) { + format.append((char)b); + } + + int idSize = hprof.getU4(); + if (idSize != ID_SIZE) { + throw new HprofFormatException("Id size " + idSize + " not supported."); + } + int hightime = hprof.getU4(); + int lowtime = hprof.getU4(); + } + + // First pass: Read through all the heap dump records. Construct the + // AhatInstances, initialize them as much as possible and save any + // additional temporary data we need to complete their initialization in + // the fixup pass. + Site rootSite = new Site("ROOT"); + List<AhatInstance> instances = new ArrayList<AhatInstance>(); + List<RootData> roots = new ArrayList<RootData>(); + HeapList heaps = new HeapList(); + { + // Note: Strings do not satisfy the DenseMap requirements on heap dumps + // from Android K. + UnDenseMap<String> strings = new UnDenseMap<String>("String"); + DenseMap<ProguardMap.Frame> frames = new DenseMap<ProguardMap.Frame>("Stack Frame"); + DenseMap<Site> sites = new DenseMap<Site>("Stack Trace"); + DenseMap<String> classNamesBySerial = new DenseMap<String>("Class Serial Number"); + AhatClassObj javaLangClass = null; + AhatClassObj[] primArrayClasses = new AhatClassObj[Type.values().length]; + ArrayList<AhatClassObj> classes = new ArrayList<AhatClassObj>(); + Instances<AhatClassObj> classById = null; + + while (hprof.hasRemaining()) { + int tag = hprof.getU1(); + int time = hprof.getU4(); + int recordLength = hprof.getU4(); + switch (tag) { + case 0x01: { // STRING + long id = hprof.getId(); + byte[] bytes = new byte[recordLength - ID_SIZE]; + hprof.getBytes(bytes); + String str = new String(bytes, StandardCharsets.UTF_8); + strings.put(id, str); + break; + } + + case 0x02: { // LOAD CLASS + int classSerialNumber = hprof.getU4(); + long objectId = hprof.getId(); + int stackSerialNumber = hprof.getU4(); + long classNameStringId = hprof.getId(); + String obfClassName = strings.get(classNameStringId); + String clrClassName = map.getClassName(obfClassName); + AhatClassObj classObj = new AhatClassObj(objectId, clrClassName); + classNamesBySerial.put(classSerialNumber, clrClassName); + classes.add(classObj); + + // Check whether this class is one of the special classes we are + // interested in, and if so, save it for later use. + if ("java.lang.Class".equals(clrClassName)) { + javaLangClass = classObj; + } + + for (Type type : Type.values()) { + if (clrClassName.equals(type.name + "[]")) { + primArrayClasses[type.ordinal()] = classObj; + } + } + break; + } + + case 0x04: { // STACK FRAME + long frameId = hprof.getId(); + long methodNameStringId = hprof.getId(); + long methodSignatureStringId = hprof.getId(); + long methodFileNameStringId = hprof.getId(); + int classSerialNumber = hprof.getU4(); + int lineNumber = hprof.getU4(); + + ProguardMap.Frame frame = map.getFrame( + classNamesBySerial.get(classSerialNumber), + strings.get(methodNameStringId), + strings.get(methodSignatureStringId), + strings.get(methodFileNameStringId), + lineNumber); + frames.put(frameId, frame); + break; + } + + case 0x05: { // STACK TRACE + int stackSerialNumber = hprof.getU4(); + int threadSerialNumber = hprof.getU4(); + int numFrames = hprof.getU4(); + ProguardMap.Frame[] trace = new ProguardMap.Frame[numFrames]; + for (int i = 0; i < numFrames; i++) { + long frameId = hprof.getId(); + trace[i] = frames.get(frameId); + } + sites.put(stackSerialNumber, rootSite.getSite(trace)); + break; + } + + case 0x1C: { // HEAP DUMP SEGMENT + if (classById == null) { + classById = new Instances<AhatClassObj>(classes); + } + int subtag; + while (!isEndOfHeapDumpSegment(subtag = hprof.getU1())) { + switch (subtag) { + case 0x01: { // ROOT JNI GLOBAL + long objectId = hprof.getId(); + long refId = hprof.getId(); + roots.add(new RootData(objectId, RootType.JNI_GLOBAL)); + break; + } + + case 0x02: { // ROOT JNI LOCAL + long objectId = hprof.getId(); + int threadSerialNumber = hprof.getU4(); + int frameNumber = hprof.getU4(); + roots.add(new RootData(objectId, RootType.JNI_LOCAL)); + break; + } + + case 0x03: { // ROOT JAVA FRAME + long objectId = hprof.getId(); + int threadSerialNumber = hprof.getU4(); + int frameNumber = hprof.getU4(); + roots.add(new RootData(objectId, RootType.JAVA_FRAME)); + break; + } + + case 0x04: { // ROOT NATIVE STACK + long objectId = hprof.getId(); + int threadSerialNumber = hprof.getU4(); + roots.add(new RootData(objectId, RootType.NATIVE_STACK)); + break; + } + + case 0x05: { // ROOT STICKY CLASS + long objectId = hprof.getId(); + roots.add(new RootData(objectId, RootType.STICKY_CLASS)); + break; + } + + case 0x06: { // ROOT THREAD BLOCK + long objectId = hprof.getId(); + int threadSerialNumber = hprof.getU4(); + roots.add(new RootData(objectId, RootType.THREAD_BLOCK)); + break; + } + + case 0x07: { // ROOT MONITOR USED + long objectId = hprof.getId(); + roots.add(new RootData(objectId, RootType.MONITOR)); + break; + } + + case 0x08: { // ROOT THREAD OBJECT + long objectId = hprof.getId(); + int threadSerialNumber = hprof.getU4(); + int stackSerialNumber = hprof.getU4(); + roots.add(new RootData(objectId, RootType.THREAD)); + break; + } + + case 0x20: { // CLASS DUMP + ClassObjData data = new ClassObjData(); + long objectId = hprof.getId(); + int stackSerialNumber = hprof.getU4(); + long superClassId = hprof.getId(); + data.classLoaderId = hprof.getId(); + long signersId = hprof.getId(); + long protectionId = hprof.getId(); + long reserved1 = hprof.getId(); + long reserved2 = hprof.getId(); + int instanceSize = hprof.getU4(); + int constantPoolSize = hprof.getU2(); + for (int i = 0; i < constantPoolSize; ++i) { + int index = hprof.getU2(); + Type type = hprof.getType(); + hprof.skip(type.size); + } + int numStaticFields = hprof.getU2(); + data.staticFields = new FieldValue[numStaticFields]; + AhatClassObj obj = classById.get(objectId); + String clrClassName = obj.getName(); + long staticFieldsSize = 0; + for (int i = 0; i < numStaticFields; ++i) { + String obfName = strings.get(hprof.getId()); + String clrName = map.getFieldName(clrClassName, obfName); + Type type = hprof.getType(); + Value value = hprof.getDeferredValue(type); + staticFieldsSize += type.size; + data.staticFields[i] = new FieldValue(clrName, type, value); + } + AhatClassObj superClass = classById.get(superClassId); + int numInstanceFields = hprof.getU2(); + Field[] ifields = new Field[numInstanceFields]; + for (int i = 0; i < numInstanceFields; ++i) { + String name = map.getFieldName(obj.getName(), strings.get(hprof.getId())); + ifields[i] = new Field(name, hprof.getType()); + } + Site site = sites.get(stackSerialNumber); + + if (javaLangClass == null) { + throw new HprofFormatException("No class definition found for java.lang.Class"); + } + obj.initialize(heaps.getCurrentHeap(), site, javaLangClass); + obj.initialize(superClass, instanceSize, ifields, staticFieldsSize); + obj.setTemporaryUserData(data); + break; + } + + case 0x21: { // INSTANCE DUMP + long objectId = hprof.getId(); + int stackSerialNumber = hprof.getU4(); + long classId = hprof.getId(); + int numBytes = hprof.getU4(); + ClassInstData data = new ClassInstData(hprof.tell()); + hprof.skip(numBytes); + + Site site = sites.get(stackSerialNumber); + AhatClassObj classObj = classById.get(classId); + AhatClassInstance obj = new AhatClassInstance(objectId); + obj.initialize(heaps.getCurrentHeap(), site, classObj); + obj.setTemporaryUserData(data); + instances.add(obj); + break; + } + + case 0x22: { // OBJECT ARRAY DUMP + long objectId = hprof.getId(); + int stackSerialNumber = hprof.getU4(); + int length = hprof.getU4(); + long classId = hprof.getId(); + ObjArrayData data = new ObjArrayData(length, hprof.tell()); + hprof.skip(length * ID_SIZE); + + Site site = sites.get(stackSerialNumber); + AhatClassObj classObj = classById.get(classId); + AhatArrayInstance obj = new AhatArrayInstance(objectId); + obj.initialize(heaps.getCurrentHeap(), site, classObj); + obj.setTemporaryUserData(data); + instances.add(obj); + break; + } + + case 0x23: { // PRIMITIVE ARRAY DUMP + long objectId = hprof.getId(); + int stackSerialNumber = hprof.getU4(); + int length = hprof.getU4(); + Type type = hprof.getPrimitiveType(); + Site site = sites.get(stackSerialNumber); + + AhatClassObj classObj = primArrayClasses[type.ordinal()]; + if (classObj == null) { + throw new HprofFormatException( + "No class definition found for " + type.name + "[]"); + } + + AhatArrayInstance obj = new AhatArrayInstance(objectId); + obj.initialize(heaps.getCurrentHeap(), site, classObj); + instances.add(obj); + switch (type) { + case BOOLEAN: { + boolean[] data = new boolean[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getBool(); + } + obj.initialize(data); + break; + } + + case CHAR: { + char[] data = new char[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getChar(); + } + obj.initialize(data); + break; + } + + case FLOAT: { + float[] data = new float[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getFloat(); + } + obj.initialize(data); + break; + } + + case DOUBLE: { + double[] data = new double[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getDouble(); + } + obj.initialize(data); + break; + } + + case BYTE: { + byte[] data = new byte[length]; + hprof.getBytes(data); + obj.initialize(data); + break; + } + + case SHORT: { + short[] data = new short[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getShort(); + } + obj.initialize(data); + break; + } + + case INT: { + int[] data = new int[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getInt(); + } + obj.initialize(data); + break; + } + + case LONG: { + long[] data = new long[length]; + for (int i = 0; i < length; ++i) { + data[i] = hprof.getLong(); + } + obj.initialize(data); + break; + } + } + break; + } + + case 0x89: { // ROOT INTERNED STRING (ANDROID) + long objectId = hprof.getId(); + roots.add(new RootData(objectId, RootType.INTERNED_STRING)); + break; + } + + case 0x8b: { // ROOT DEBUGGER (ANDROID) + long objectId = hprof.getId(); + roots.add(new RootData(objectId, RootType.DEBUGGER)); + break; + } + + case 0x8d: { // ROOT VM INTERNAL (ANDROID) + long objectId = hprof.getId(); + roots.add(new RootData(objectId, RootType.VM_INTERNAL)); + break; + } + + case 0x8e: { // ROOT JNI MONITOR (ANDROID) + long objectId = hprof.getId(); + int threadSerialNumber = hprof.getU4(); + int frameNumber = hprof.getU4(); + roots.add(new RootData(objectId, RootType.JNI_MONITOR)); + break; + } + + case 0xfe: { // HEAP DUMP INFO (ANDROID) + int type = hprof.getU4(); + long stringId = hprof.getId(); + heaps.setCurrentHeap(strings.get(stringId)); + break; + } + + case 0xff: { // ROOT UNKNOWN + long objectId = hprof.getId(); + roots.add(new RootData(objectId, RootType.UNKNOWN)); + break; + } + + default: + throw new HprofFormatException( + String.format("Unsupported heap dump sub tag 0x%02x", subtag)); + } + } + + // Reset the file pointer back because we read the first byte into + // the next record. + hprof.skip(-1); + break; + } + + default: + // Ignore any other tags that we either don't know about or don't + // care about. + hprof.skip(recordLength); + break; + } + } + + instances.addAll(classes); + } + + // Sort roots and instances by id in preparation for the fixup pass. + Instances<AhatInstance> mInstances = new Instances<AhatInstance>(instances); + roots.sort(new Comparator<RootData>() { + @Override + public int compare(RootData a, RootData b) { + return Long.compare(a.id, b.id); + } + }); + roots.add(null); + + // Fixup pass: Label the root instances and fix up references to instances + // that we couldn't previously resolve. + SuperRoot superRoot = new SuperRoot(); + { + Iterator<RootData> ri = roots.iterator(); + RootData root = ri.next(); + for (AhatInstance inst : mInstances) { + long id = inst.getId(); + + // Skip past any roots that don't have associated instances. + // It's not clear why there would be a root without an associated + // instance dump, but it does happen in practice, for example when + // taking heap dumps using the RI. + while (root != null && root.id < id) { + root = ri.next(); + } + + // Check if this instance is a root, and if so, update its root types. + if (root != null && root.id == id) { + superRoot.addRoot(inst); + while (root != null && root.id == id) { + inst.addRootType(root.type); + root = ri.next(); + } + } + + // Fixup the instance based on its type using the temporary data we + // saved during the first pass over the heap dump. + if (inst instanceof AhatClassInstance) { + ClassInstData data = (ClassInstData)inst.getTemporaryUserData(); + inst.setTemporaryUserData(null); + + // Compute the size of the fields array in advance to avoid + // extra allocations and copies that would come from using an array + // list to collect the field values. + int numFields = 0; + for (AhatClassObj cls = inst.getClassObj(); cls != null; cls = cls.getSuperClassObj()) { + numFields += cls.getInstanceFields().length; + } + + Value[] fields = new Value[numFields]; + int i = 0; + hprof.seek(data.position); + for (AhatClassObj cls = inst.getClassObj(); cls != null; cls = cls.getSuperClassObj()) { + for (Field field : cls.getInstanceFields()) { + fields[i++] = hprof.getValue(field.type, mInstances); + } + } + ((AhatClassInstance)inst).initialize(fields); + } else if (inst instanceof AhatClassObj) { + ClassObjData data = (ClassObjData)inst.getTemporaryUserData(); + inst.setTemporaryUserData(null); + AhatInstance loader = mInstances.get(data.classLoaderId); + for (int i = 0; i < data.staticFields.length; ++i) { + FieldValue field = data.staticFields[i]; + if (field.value instanceof DeferredInstanceValue) { + DeferredInstanceValue deferred = (DeferredInstanceValue)field.value; + data.staticFields[i] = new FieldValue( + field.name, field.type, Value.pack(mInstances.get(deferred.getId()))); + } + } + ((AhatClassObj)inst).initialize(loader, data.staticFields); + } else if (inst instanceof AhatArrayInstance && inst.getTemporaryUserData() != null) { + // TODO: Have specialized object array instance and check for that + // rather than checking for the presence of user data? + ObjArrayData data = (ObjArrayData)inst.getTemporaryUserData(); + inst.setTemporaryUserData(null); + AhatInstance[] array = new AhatInstance[data.length]; + hprof.seek(data.position); + for (int i = 0; i < data.length; i++) { + array[i] = mInstances.get(hprof.getId()); + } + ((AhatArrayInstance)inst).initialize(array); + } + } + } + + hprof = null; + roots = null; + return new AhatSnapshot(superRoot, mInstances, heaps.heaps, rootSite); + } + + private static boolean isEndOfHeapDumpSegment(int subtag) { + return subtag == 0x1C || subtag == 0x2C; + } + + private static class RootData { + public long id; + public RootType type; + + public RootData(long id, RootType type) { + this.id = id; + this.type = type; + } + } + + private static class ClassInstData { + // The byte position in the hprof file where instance field data starts. + public int position; + + public ClassInstData(int position) { + this.position = position; + } + } + + private static class ObjArrayData { + public int length; // Number of array elements. + public int position; // Position in hprof file containing element data. + + public ObjArrayData(int length, int position) { + this.length = length; + this.position = position; + } + } + + private static class ClassObjData { + public long classLoaderId; + public FieldValue[] staticFields; // Contains DeferredInstanceValues. + } + + /** + * Dummy value representing a reference to an instance that has not yet been + * resolved. + * When first initializing class static fields, we don't yet know what kinds + * of objects Object references refer to. We use DeferredInstanceValue as + * a dummy kind of value to store the id of an object. In the fixup pass we + * resolve all the DeferredInstanceValues into their proper InstanceValues. + */ + private static class DeferredInstanceValue extends Value { + private long mId; + + public DeferredInstanceValue(long id) { + mId = id; + } + + public long getId() { + return mId; + } + + @Override + protected Type getType() { + return Type.OBJECT; + } + + @Override + public String toString() { + return String.format("0x%08x", mId); + } + + @Override public boolean equals(Object other) { + if (other instanceof DeferredInstanceValue) { + DeferredInstanceValue value = (DeferredInstanceValue)other; + return mId == value.mId; + } + return false; + } + } + + /** + * A convenient abstraction for lazily building up the list of heaps seen in + * the heap dump. + */ + private static class HeapList { + public List<AhatHeap> heaps = new ArrayList<AhatHeap>(); + private AhatHeap current; + + public AhatHeap getCurrentHeap() { + if (current == null) { + setCurrentHeap("default"); + } + return current; + } + + public void setCurrentHeap(String name) { + for (AhatHeap heap : heaps) { + if (name.equals(heap.getName())) { + current = heap; + return; + } + } + + current = new AhatHeap(name, heaps.size()); + heaps.add(current); + } + } + + /** + * A mapping from id to elements, where certain conditions are + * satisfied. The conditions are: + * - all elements are defined before they are referenced. + * - ids are densely packed in some range [a, b] where a is not + * necessarily 0. + * - there are not more than 2^31 elements defined. + */ + private static class DenseMap<T> { + private String mElementType; + + // mValues behaves like a circular buffer. + // mKeyAt0 is the key corresponding to index 0 of mValues. Values with + // smaller keys will wrap around to the end of the mValues buffer. The + // buffer is expanded when it is no longer big enough to hold all the keys + // from mMinKey to mMaxKey. + private Object[] mValues; + private long mKeyAt0; + private long mMaxKey; + private long mMinKey; + + /** + * Constructs a DenseMap. + * @param elementType Human readable name describing the type of + * elements for error message if the required + * conditions are found not to hold. + */ + public DenseMap(String elementType) { + mElementType = elementType; + } + + public void put(long key, T value) { + if (mValues == null) { + mValues = new Object[8]; + mValues[0] = value; + mKeyAt0 = key; + mMaxKey = key; + mMinKey = key; + return; + } + + long max = Math.max(mMaxKey, key); + long min = Math.min(mMinKey, key); + int count = (int)(max + 1 - min); + if (count > mValues.length) { + Object[] values = new Object[2 * count]; + + // Copy over the values into the newly allocated larger buffer. It is + // convenient to move the value with mMinKey to index 0 when we make + // the copy. + for (int i = 0; i < mValues.length; ++i) { + values[i] = mValues[indexOf(i + mMinKey)]; + } + mValues = values; + mKeyAt0 = mMinKey; + } + mMinKey = min; + mMaxKey = max; + mValues[indexOf(key)] = value; + } + + /** + * Returns the value for the given key. + * @throws HprofFormatException if there is no value with the key in the + * given map. + */ + public T get(long key) throws HprofFormatException { + T value = null; + if (mValues != null && key >= mMinKey && key <= mMaxKey) { + value = (T)mValues[indexOf(key)]; + } + + if (value == null) { + throw new HprofFormatException(String.format( + "%s with id 0x%x referenced before definition", mElementType, key)); + } + return value; + } + + private int indexOf(long key) { + return ((int)(key - mKeyAt0) + mValues.length) % mValues.length; + } + } + + /** + * A mapping from id to elements, where we don't have nice conditions to + * work with. + */ + private static class UnDenseMap<T> { + private String mElementType; + private Map<Long, T> mValues = new HashMap<Long, T>(); + + /** + * Constructs an UnDenseMap. + * @param elementType Human readable name describing the type of + * elements for error message if the required + * conditions are found not to hold. + */ + public UnDenseMap(String elementType) { + mElementType = elementType; + } + + public void put(long key, T value) { + mValues.put(key, value); + } + + /** + * Returns the value for the given key. + * @throws HprofFormatException if there is no value with the key in the + * given map. + */ + public T get(long key) throws HprofFormatException { + T value = mValues.get(key); + if (value == null) { + throw new HprofFormatException(String.format( + "%s with id 0x%x referenced before definition", mElementType, key)); + } + return value; + } + } + + /** + * Wrapper around a ByteBuffer that presents a uniform interface for + * accessing data from an hprof file. + */ + private static class HprofBuffer { + private ByteBuffer mBuffer; + + public HprofBuffer(File path) throws IOException { + FileChannel channel = FileChannel.open(path.toPath(), StandardOpenOption.READ); + mBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); + channel.close(); + } + + public HprofBuffer(ByteBuffer buffer) { + mBuffer = buffer; + } + + public boolean hasRemaining() { + return mBuffer.hasRemaining(); + } + + /** + * Return the current absolution position in the file. + */ + public int tell() { + return mBuffer.position(); + } + + /** + * Seek to the given absolution position in the file. + */ + public void seek(int position) { + mBuffer.position(position); + } + + /** + * Skip ahead in the file by the given delta bytes. Delta may be negative + * to skip backwards in the file. + */ + public void skip(int delta) { + seek(tell() + delta); + } + + public int getU1() { + return mBuffer.get() & 0xFF; + } + + public int getU2() { + return mBuffer.getShort() & 0xFFFF; + } + + public int getU4() { + return mBuffer.getInt(); + } + + public long getId() { + return mBuffer.getInt(); + } + + public boolean getBool() { + return mBuffer.get() != 0; + } + + public char getChar() { + return mBuffer.getChar(); + } + + public float getFloat() { + return mBuffer.getFloat(); + } + + public double getDouble() { + return mBuffer.getDouble(); + } + + public byte getByte() { + return mBuffer.get(); + } + + public void getBytes(byte[] bytes) { + mBuffer.get(bytes); + } + + public short getShort() { + return mBuffer.getShort(); + } + + public int getInt() { + return mBuffer.getInt(); + } + + public long getLong() { + return mBuffer.getLong(); + } + + private static Type[] TYPES = new Type[] { + null, null, Type.OBJECT, null, + Type.BOOLEAN, Type.CHAR, Type.FLOAT, Type.DOUBLE, + Type.BYTE, Type.SHORT, Type.INT, Type.LONG + }; + + public Type getType() throws HprofFormatException { + int id = getU1(); + Type type = id < TYPES.length ? TYPES[id] : null; + if (type == null) { + throw new HprofFormatException("Invalid basic type id: " + id); + } + return type; + } + + public Type getPrimitiveType() throws HprofFormatException { + Type type = getType(); + if (type == Type.OBJECT) { + throw new HprofFormatException("Expected primitive type, but found type 'Object'"); + } + return type; + } + + /** + * Get a value from the hprof file, using the given instances map to + * convert instance ids to their corresponding AhatInstance objects. + */ + public Value getValue(Type type, Instances instances) { + switch (type) { + case OBJECT: return Value.pack(instances.get(getId())); + case BOOLEAN: return Value.pack(getBool()); + case CHAR: return Value.pack(getChar()); + case FLOAT: return Value.pack(getFloat()); + case DOUBLE: return Value.pack(getDouble()); + case BYTE: return Value.pack(getByte()); + case SHORT: return Value.pack(getShort()); + case INT: return Value.pack(getInt()); + case LONG: return Value.pack(getLong()); + default: throw new AssertionError("unsupported enum member"); + } + } + + /** + * Get a value from the hprof file. AhatInstance values are returned as + * DefferredInstanceValues rather than their corresponding AhatInstance + * objects. + */ + public Value getDeferredValue(Type type) { + switch (type) { + case OBJECT: return new DeferredInstanceValue(getId()); + case BOOLEAN: return Value.pack(getBool()); + case CHAR: return Value.pack(getChar()); + case FLOAT: return Value.pack(getFloat()); + case DOUBLE: return Value.pack(getDouble()); + case BYTE: return Value.pack(getByte()); + case SHORT: return Value.pack(getShort()); + case INT: return Value.pack(getInt()); + case LONG: return Value.pack(getLong()); + default: throw new AssertionError("unsupported enum member"); + } + } + } +} diff --git a/tools/ahat/src/heapdump/Perflib.java b/tools/ahat/src/heapdump/Perflib.java deleted file mode 100644 index d0264a3b39..0000000000 --- a/tools/ahat/src/heapdump/Perflib.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * 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.android.ahat.heapdump; - -import com.android.tools.perflib.heap.ClassInstance; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Instance; -import com.android.tools.perflib.heap.Snapshot; -import java.util.HashMap; -import java.util.Map; - -/** - * Collection of utilities that may be suitable to have in perflib instead of - * ahat. - */ -public class Perflib { - /** - * Return a collection of instances in the given snapshot that are tied to - * registered native allocations and their corresponding registered native - * sizes. - */ - public static Map<Instance, Long> getRegisteredNativeAllocations(Snapshot snapshot) { - Map<Instance, Long> allocs = new HashMap<Instance, Long>(); - ClassObj cleanerClass = snapshot.findClass("sun.misc.Cleaner"); - if (cleanerClass != null) { - for (Instance cleanerInst : cleanerClass.getInstancesList()) { - ClassInstance cleaner = (ClassInstance)cleanerInst; - Object referent = getField(cleaner, "referent"); - if (referent instanceof Instance) { - Instance inst = (Instance)referent; - Object thunkValue = getField(cleaner, "thunk"); - if (thunkValue instanceof ClassInstance) { - ClassInstance thunk = (ClassInstance)thunkValue; - ClassObj thunkClass = thunk.getClassObj(); - String cleanerThunkClassName = "libcore.util.NativeAllocationRegistry$CleanerThunk"; - if (thunkClass != null && cleanerThunkClassName.equals(thunkClass.getClassName())) { - for (ClassInstance.FieldValue thunkField : thunk.getValues()) { - if (thunkField.getValue() instanceof ClassInstance) { - ClassInstance registry = (ClassInstance)thunkField.getValue(); - ClassObj registryClass = registry.getClassObj(); - String registryClassName = "libcore.util.NativeAllocationRegistry"; - if (registryClass != null - && registryClassName.equals(registryClass.getClassName())) { - Object sizeValue = getField(registry, "size"); - if (sizeValue instanceof Long) { - long size = (Long)sizeValue; - if (size > 0) { - Long old = allocs.get(inst); - allocs.put(inst, old == null ? size : old + size); - } - } - break; - } - } - } - } - } - } - } - } - return allocs; - } - - /** - * Helper function to read a single field from a perflib class instance. - * Returns null if field not found. Note there is no way to distinguish - * between field not found an a field value of null. - */ - private static Object getField(ClassInstance cls, String name) { - for (ClassInstance.FieldValue field : cls.getValues()) { - if (name.equals(field.getField().getName())) { - return field.getValue(); - } - } - return null; - } -} diff --git a/tools/ahat/src/heapdump/RootType.java b/tools/ahat/src/heapdump/RootType.java new file mode 100644 index 0000000000..7165b83722 --- /dev/null +++ b/tools/ahat/src/heapdump/RootType.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat.heapdump; + +public enum RootType { + JNI_GLOBAL (1 << 0), + JNI_LOCAL (1 << 1), + JAVA_FRAME (1 << 2), + NATIVE_STACK (1 << 3), + STICKY_CLASS (1 << 4), + THREAD_BLOCK (1 << 5), + MONITOR (1 << 6), + THREAD (1 << 7), + INTERNED_STRING (1 << 8), + DEBUGGER (1 << 9), + VM_INTERNAL (1 << 10), + UNKNOWN (1 << 11), + JNI_MONITOR (1 << 12); + + public final int mask; + + RootType(int mask) { + this.mask = mask; + } +} diff --git a/tools/ahat/src/heapdump/Site.java b/tools/ahat/src/heapdump/Site.java index 82931f0056..821493f1be 100644 --- a/tools/ahat/src/heapdump/Site.java +++ b/tools/ahat/src/heapdump/Site.java @@ -16,7 +16,7 @@ package com.android.ahat.heapdump; -import com.android.tools.perflib.heap.StackFrame; +import com.android.ahat.proguard.ProguardMap; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -127,27 +127,27 @@ public class Site implements Diffable<Site> { * inner-most frame. May be null, in which case this site is * returned. */ - Site getSite(StackFrame frames[]) { + Site getSite(ProguardMap.Frame[] frames) { return frames == null ? this : getSite(this, frames); } - private static Site getSite(Site site, StackFrame frames[]) { + private static Site getSite(Site site, ProguardMap.Frame[] frames) { for (int s = frames.length - 1; s >= 0; --s) { - StackFrame frame = frames[s]; + ProguardMap.Frame frame = frames[s]; Site child = null; for (int i = 0; i < site.mChildren.size(); i++) { Site curr = site.mChildren.get(i); - if (curr.mLineNumber == frame.getLineNumber() - && curr.mMethodName.equals(frame.getMethodName()) - && curr.mSignature.equals(frame.getSignature()) - && curr.mFilename.equals(frame.getFilename())) { + if (curr.mLineNumber == frame.line + && curr.mMethodName.equals(frame.method) + && curr.mSignature.equals(frame.signature) + && curr.mFilename.equals(frame.filename)) { child = curr; break; } } if (child == null) { - child = new Site(site, frame.getMethodName(), frame.getSignature(), - frame.getFilename(), frame.getLineNumber()); + child = new Site(site, frame.method, frame.signature, + frame.filename, frame.line); site.mChildren.add(child); } site = child; diff --git a/tools/ahat/src/heapdump/SuperRoot.java b/tools/ahat/src/heapdump/SuperRoot.java index d377113862..a2adbd2808 100644 --- a/tools/ahat/src/heapdump/SuperRoot.java +++ b/tools/ahat/src/heapdump/SuperRoot.java @@ -34,6 +34,11 @@ public class SuperRoot extends AhatInstance implements DominatorsComputation.Nod } @Override + protected long getExtraJavaSize() { + return 0; + } + + @Override public String toString() { return "SUPER_ROOT"; } diff --git a/tools/ahat/src/heapdump/Type.java b/tools/ahat/src/heapdump/Type.java new file mode 100644 index 0000000000..726bc47cf2 --- /dev/null +++ b/tools/ahat/src/heapdump/Type.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat.heapdump; + +public enum Type { + OBJECT("Object", 4), + BOOLEAN("boolean", 1), + CHAR("char", 2), + FLOAT("float", 4), + DOUBLE("double", 8), + BYTE("byte", 1), + SHORT("short", 2), + INT("int", 4), + LONG("long", 8); + + public final String name; + public final int size; + + Type(String name, int size) { + this.name = name; + this.size = size; + } + + @Override + public String toString() { + return name; + } +} diff --git a/tools/ahat/src/heapdump/Value.java b/tools/ahat/src/heapdump/Value.java index 7f86c01efb..01fd25057d 100644 --- a/tools/ahat/src/heapdump/Value.java +++ b/tools/ahat/src/heapdump/Value.java @@ -25,37 +25,6 @@ public abstract class Value { return value == null ? null : new InstanceValue(value); } - /** - * Constructs a value from a generic Java Object. - * The Object must either be a boxed Java primitive type or a subclass of - * AhatInstance. The object must not be null. - */ - public static Value pack(Object object) { - if (object == null) { - return null; - } else if (object instanceof AhatInstance) { - return Value.pack((AhatInstance)object); - } else if (object instanceof Boolean) { - return Value.pack(((Boolean)object).booleanValue()); - } else if (object instanceof Character) { - return Value.pack(((Character)object).charValue()); - } else if (object instanceof Float) { - return Value.pack(((Float)object).floatValue()); - } else if (object instanceof Double) { - return Value.pack(((Double)object).doubleValue()); - } else if (object instanceof Byte) { - return Value.pack(((Byte)object).byteValue()); - } else if (object instanceof Short) { - return Value.pack(((Short)object).shortValue()); - } else if (object instanceof Integer) { - return Value.pack(((Integer)object).intValue()); - } else if (object instanceof Long) { - return Value.pack(((Long)object).longValue()); - } - throw new IllegalArgumentException( - "AhatInstance or primitive type required, but got: " + object.toString()); - } - public static Value pack(boolean value) { return new BooleanValue(value); } @@ -89,6 +58,18 @@ public abstract class Value { } /** + * Return the type of the given value. + */ + public static Type getType(Value value) { + return value == null ? Type.OBJECT : value.getType(); + } + + /** + * Return the type of the given value. + */ + protected abstract Type getType(); + + /** * Returns true if the Value is an AhatInstance, as opposed to a Java * primitive value. */ @@ -172,6 +153,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.BOOLEAN; + } + + @Override public String toString() { return Boolean.toString(mBool); } @@ -198,6 +184,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.BYTE; + } + + @Override public String toString() { return Byte.toString(mByte); } @@ -224,6 +215,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.CHAR; + } + + @Override public String toString() { return Character.toString(mChar); } @@ -245,6 +241,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.DOUBLE; + } + + @Override public String toString() { return Double.toString(mDouble); } @@ -266,6 +267,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.FLOAT; + } + + @Override public String toString() { return Float.toString(mFloat); } @@ -298,6 +304,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.OBJECT; + } + + @Override public String toString() { return mInstance.toString(); } @@ -334,6 +345,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.INT; + } + + @Override public String toString() { return Integer.toString(mInt); } @@ -365,6 +381,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.LONG; + } + + @Override public String toString() { return Long.toString(mLong); } @@ -386,6 +407,11 @@ public abstract class Value { } @Override + protected Type getType() { + return Type.SHORT; + } + + @Override public String toString() { return Short.toString(mShort); } diff --git a/tools/ahat/src/proguard/ProguardMap.java b/tools/ahat/src/proguard/ProguardMap.java new file mode 100644 index 0000000000..50c110aad4 --- /dev/null +++ b/tools/ahat/src/proguard/ProguardMap.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2016 Google 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.android.ahat.proguard; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Map; + +// Class used to deobfuscate classes, fields, and stack frames. +public class ProguardMap { + + private static final String ARRAY_SYMBOL = "[]"; + + private static class FrameData { + public FrameData(String clearMethodName, int lineDelta) { + this.clearMethodName = clearMethodName; + this.lineDelta = lineDelta; + } + + public final String clearMethodName; + public final int lineDelta; // lineDelta = obfuscatedLine - clearLine + } + + private static class ClassData { + private final String mClearName; + + // Mapping from obfuscated field name to clear field name. + private final Map<String, String> mFields = new HashMap<String, String>(); + + // obfuscatedMethodName + clearSignature -> FrameData + private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>(); + + // Constructs a ClassData object for a class with the given clear name. + public ClassData(String clearName) { + mClearName = clearName; + } + + // Returns the clear name of the class. + public String getClearName() { + return mClearName; + } + + public void addField(String obfuscatedName, String clearName) { + mFields.put(obfuscatedName, clearName); + } + + // Get the clear name for the field in this class with the given + // obfuscated name. Returns the original obfuscated name if a clear + // name for the field could not be determined. + // TODO: Do we need to take into account the type of the field to + // propery determine the clear name? + public String getField(String obfuscatedName) { + String clearField = mFields.get(obfuscatedName); + return clearField == null ? obfuscatedName : clearField; + } + + // TODO: Does this properly interpret the meaning of line numbers? Is + // it possible to have multiple frame entries for the same method + // name and signature that differ only by line ranges? + public void addFrame(String obfuscatedMethodName, String clearMethodName, + String clearSignature, int obfuscatedLine, int clearLine) { + String key = obfuscatedMethodName + clearSignature; + mFrames.put(key, new FrameData(clearMethodName, obfuscatedLine - clearLine)); + } + + public Frame getFrame(String clearClassName, String obfuscatedMethodName, + String clearSignature, String obfuscatedFilename, int obfuscatedLine) { + String key = obfuscatedMethodName + clearSignature; + FrameData frame = mFrames.get(key); + if (frame == null) { + return new Frame(obfuscatedMethodName, clearSignature, + obfuscatedFilename, obfuscatedLine); + } + return new Frame(frame.clearMethodName, clearSignature, + getFileName(clearClassName, frame.clearMethodName), + obfuscatedLine - frame.lineDelta); + } + } + + private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>(); + private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>(); + + public static class Frame { + public Frame(String method, String signature, String filename, int line) { + this.method = method; + this.signature = signature; + this.filename = filename; + this.line = line; + } + + public final String method; + public final String signature; + public final String filename; + public final int line; + } + + private static void parseException(String msg) throws ParseException { + throw new ParseException(msg, 0); + } + + // Read in proguard mapping information from the given file. + public void readFromFile(File mapFile) + throws FileNotFoundException, IOException, ParseException { + readFromReader(new FileReader(mapFile)); + } + + // Read in proguard mapping information from the given Reader. + public void readFromReader(Reader mapReader) throws IOException, ParseException { + BufferedReader reader = new BufferedReader(mapReader); + String line = reader.readLine(); + while (line != null) { + // Class lines are of the form: + // 'clear.class.name -> obfuscated_class_name:' + int sep = line.indexOf(" -> "); + if (sep == -1 || sep + 5 >= line.length()) { + parseException("Error parsing class line: '" + line + "'"); + } + String clearClassName = line.substring(0, sep); + String obfuscatedClassName = line.substring(sep + 4, line.length() - 1); + + ClassData classData = new ClassData(clearClassName); + mClassesFromClearName.put(clearClassName, classData); + mClassesFromObfuscatedName.put(obfuscatedClassName, classData); + + // After the class line comes zero or more field/method lines of the form: + // ' type clearName -> obfuscatedName' + line = reader.readLine(); + while (line != null && line.startsWith(" ")) { + String trimmed = line.trim(); + int ws = trimmed.indexOf(' '); + sep = trimmed.indexOf(" -> "); + if (ws == -1 || sep == -1) { + parseException("Error parse field/method line: '" + line + "'"); + } + + String type = trimmed.substring(0, ws); + String clearName = trimmed.substring(ws + 1, sep); + String obfuscatedName = trimmed.substring(sep + 4, trimmed.length()); + + // If the clearName contains '(', then this is for a method instead of a + // field. + if (clearName.indexOf('(') == -1) { + classData.addField(obfuscatedName, clearName); + } else { + // For methods, the type is of the form: [#:[#:]]<returnType> + int obfuscatedLine = 0; + int colon = type.indexOf(':'); + if (colon != -1) { + obfuscatedLine = Integer.parseInt(type.substring(0, colon)); + type = type.substring(colon + 1); + } + colon = type.indexOf(':'); + if (colon != -1) { + type = type.substring(colon + 1); + } + + // For methods, the clearName is of the form: <clearName><sig>[:#[:#]] + int op = clearName.indexOf('('); + int cp = clearName.indexOf(')'); + if (op == -1 || cp == -1) { + parseException("Error parse method line: '" + line + "'"); + } + + String sig = clearName.substring(op, cp + 1); + + int clearLine = obfuscatedLine; + colon = clearName.lastIndexOf(':'); + if (colon != -1) { + clearLine = Integer.parseInt(clearName.substring(colon + 1)); + clearName = clearName.substring(0, colon); + } + + colon = clearName.lastIndexOf(':'); + if (colon != -1) { + clearLine = Integer.parseInt(clearName.substring(colon + 1)); + clearName = clearName.substring(0, colon); + } + + clearName = clearName.substring(0, op); + + String clearSig = fromProguardSignature(sig + type); + classData.addFrame(obfuscatedName, clearName, clearSig, + obfuscatedLine, clearLine); + } + + line = reader.readLine(); + } + } + reader.close(); + } + + // Returns the deobfuscated version of the given class name. If no + // deobfuscated version is known, the original string is returned. + public String getClassName(String obfuscatedClassName) { + // Class names for arrays may have trailing [] that need to be + // stripped before doing the lookup. + String baseName = obfuscatedClassName; + String arraySuffix = ""; + while (baseName.endsWith(ARRAY_SYMBOL)) { + arraySuffix += ARRAY_SYMBOL; + baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length()); + } + + ClassData classData = mClassesFromObfuscatedName.get(baseName); + String clearBaseName = classData == null ? baseName : classData.getClearName(); + return clearBaseName + arraySuffix; + } + + // Returns the deobfuscated version of the given field name for the given + // (clear) class name. If no deobfuscated version is known, the original + // string is returned. + public String getFieldName(String clearClass, String obfuscatedField) { + ClassData classData = mClassesFromClearName.get(clearClass); + if (classData == null) { + return obfuscatedField; + } + return classData.getField(obfuscatedField); + } + + // Returns the deobfuscated frame for the given obfuscated frame and (clear) + // class name. As much of the frame is deobfuscated as can be. + public Frame getFrame(String clearClassName, String obfuscatedMethodName, + String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) { + String clearSignature = getSignature(obfuscatedSignature); + ClassData classData = mClassesFromClearName.get(clearClassName); + if (classData == null) { + return new Frame(obfuscatedMethodName, clearSignature, + obfuscatedFilename, obfuscatedLine); + } + return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature, + obfuscatedFilename, obfuscatedLine); + } + + // Converts a proguard-formatted method signature into a Java formatted + // method signature. + private static String fromProguardSignature(String sig) throws ParseException { + if (sig.startsWith("(")) { + int end = sig.indexOf(')'); + if (end == -1) { + parseException("Error parsing signature: " + sig); + } + + StringBuilder converted = new StringBuilder(); + converted.append('('); + if (end > 1) { + for (String arg : sig.substring(1, end).split(",")) { + converted.append(fromProguardSignature(arg)); + } + } + converted.append(')'); + converted.append(fromProguardSignature(sig.substring(end + 1))); + return converted.toString(); + } else if (sig.endsWith(ARRAY_SYMBOL)) { + return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2)); + } else if (sig.equals("boolean")) { + return "Z"; + } else if (sig.equals("byte")) { + return "B"; + } else if (sig.equals("char")) { + return "C"; + } else if (sig.equals("short")) { + return "S"; + } else if (sig.equals("int")) { + return "I"; + } else if (sig.equals("long")) { + return "J"; + } else if (sig.equals("float")) { + return "F"; + } else if (sig.equals("double")) { + return "D"; + } else if (sig.equals("void")) { + return "V"; + } else { + return "L" + sig.replace('.', '/') + ";"; + } + } + + // Return a clear signature for the given obfuscated signature. + private String getSignature(String obfuscatedSig) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < obfuscatedSig.length(); i++) { + if (obfuscatedSig.charAt(i) == 'L') { + int e = obfuscatedSig.indexOf(';', i); + builder.append('L'); + String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.'); + builder.append(getClassName(cls).replace('.', '/')); + builder.append(';'); + i = e; + } else { + builder.append(obfuscatedSig.charAt(i)); + } + } + return builder.toString(); + } + + // Return a file name for the given clear class name and method. + private static String getFileName(String clearClass, String method) { + int dot = method.lastIndexOf('.'); + if (dot != -1) { + clearClass = method.substring(0, dot); + } + + String filename = clearClass; + dot = filename.lastIndexOf('.'); + if (dot != -1) { + filename = filename.substring(dot + 1); + } + + int dollar = filename.indexOf('$'); + if (dollar != -1) { + filename = filename.substring(0, dollar); + } + return filename + ".java"; + } +} diff --git a/tools/ahat/test-dump/L.hprof b/tools/ahat/test-dump/L.hprof Binary files differnew file mode 100644 index 0000000000..cf82557d6d --- /dev/null +++ b/tools/ahat/test-dump/L.hprof diff --git a/tools/ahat/test-dump/O.hprof b/tools/ahat/test-dump/O.hprof Binary files differnew file mode 100644 index 0000000000..d474c6c6b4 --- /dev/null +++ b/tools/ahat/test-dump/O.hprof diff --git a/tools/ahat/test-dump/README.txt b/tools/ahat/test-dump/README.txt new file mode 100644 index 0000000000..344271c2f4 --- /dev/null +++ b/tools/ahat/test-dump/README.txt @@ -0,0 +1,5 @@ + +Main.java - A program used to generate a heap dump used for tests. +L.hprof - A version of the test dump generated on Android L. +O.hprof - A version of the test dump generated on Android O. +RI.hprof - A version of the test dump generated on the reference implementation. diff --git a/tools/ahat/test-dump/RI.hprof b/tools/ahat/test-dump/RI.hprof Binary files differnew file mode 100644 index 0000000000..9482542a7f --- /dev/null +++ b/tools/ahat/test-dump/RI.hprof diff --git a/tools/ahat/test/DiffFieldsTest.java b/tools/ahat/test/DiffFieldsTest.java index 7dc519d60b..19399757a6 100644 --- a/tools/ahat/test/DiffFieldsTest.java +++ b/tools/ahat/test/DiffFieldsTest.java @@ -19,6 +19,7 @@ package com.android.ahat; import com.android.ahat.heapdump.DiffFields; import com.android.ahat.heapdump.DiffedFieldValue; import com.android.ahat.heapdump.FieldValue; +import com.android.ahat.heapdump.Type; import com.android.ahat.heapdump.Value; import java.util.ArrayList; import java.util.List; @@ -28,14 +29,25 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; public class DiffFieldsTest { + // Give more convenient abstract names for different types. + private static final Type t0 = Type.OBJECT; + private static final Type t1 = Type.BOOLEAN; + private static final Type t2 = Type.CHAR; + private static final Type t3 = Type.FLOAT; + private static final Type t4 = Type.DOUBLE; + private static final Type t5 = Type.BYTE; + private static final Type t6 = Type.SHORT; + private static final Type t7 = Type.INT; + private static final Type t8 = Type.LONG; + @Test public void normalMatchedDiffedFieldValues() { - FieldValue normal1 = new FieldValue("name", "type", Value.pack(1)); - FieldValue normal2 = new FieldValue("name", "type", Value.pack(2)); + FieldValue normal1 = new FieldValue("name", t0, Value.pack(1)); + FieldValue normal2 = new FieldValue("name", t0, Value.pack(2)); DiffedFieldValue x = DiffedFieldValue.matched(normal1, normal2); assertEquals("name", x.name); - assertEquals("type", x.type); + assertEquals(t0, x.type); assertEquals(Value.pack(1), x.current); assertEquals(Value.pack(2), x.baseline); assertEquals(DiffedFieldValue.Status.MATCHED, x.status); @@ -43,19 +55,19 @@ public class DiffFieldsTest { @Test public void nulledMatchedDiffedFieldValues() { - FieldValue normal = new FieldValue("name", "type", Value.pack(1)); - FieldValue nulled = new FieldValue("name", "type", null); + FieldValue normal = new FieldValue("name", t0, Value.pack(1)); + FieldValue nulled = new FieldValue("name", t0, null); DiffedFieldValue x = DiffedFieldValue.matched(normal, nulled); assertEquals("name", x.name); - assertEquals("type", x.type); + assertEquals(t0, x.type); assertEquals(Value.pack(1), x.current); assertNull(x.baseline); assertEquals(DiffedFieldValue.Status.MATCHED, x.status); DiffedFieldValue y = DiffedFieldValue.matched(nulled, normal); assertEquals("name", y.name); - assertEquals("type", y.type); + assertEquals(t0, y.type); assertNull(y.current); assertEquals(Value.pack(1), y.baseline); assertEquals(DiffedFieldValue.Status.MATCHED, y.status); @@ -63,44 +75,44 @@ public class DiffFieldsTest { @Test public void normalAddedDiffedFieldValues() { - FieldValue normal = new FieldValue("name", "type", Value.pack(1)); + FieldValue normal = new FieldValue("name", t0, Value.pack(1)); DiffedFieldValue x = DiffedFieldValue.added(normal); assertEquals("name", x.name); - assertEquals("type", x.type); + assertEquals(t0, x.type); assertEquals(Value.pack(1), x.current); assertEquals(DiffedFieldValue.Status.ADDED, x.status); } @Test public void nulledAddedDiffedFieldValues() { - FieldValue nulled = new FieldValue("name", "type", null); + FieldValue nulled = new FieldValue("name", t0, null); DiffedFieldValue x = DiffedFieldValue.added(nulled); assertEquals("name", x.name); - assertEquals("type", x.type); + assertEquals(t0, x.type); assertNull(x.current); assertEquals(DiffedFieldValue.Status.ADDED, x.status); } @Test public void normalDeletedDiffedFieldValues() { - FieldValue normal = new FieldValue("name", "type", Value.pack(1)); + FieldValue normal = new FieldValue("name", t0, Value.pack(1)); DiffedFieldValue x = DiffedFieldValue.deleted(normal); assertEquals("name", x.name); - assertEquals("type", x.type); + assertEquals(t0, x.type); assertEquals(Value.pack(1), x.baseline); assertEquals(DiffedFieldValue.Status.DELETED, x.status); } @Test public void nulledDeletedDiffedFieldValues() { - FieldValue nulled = new FieldValue("name", "type", null); + FieldValue nulled = new FieldValue("name", t0, null); DiffedFieldValue x = DiffedFieldValue.deleted(nulled); assertEquals("name", x.name); - assertEquals("type", x.type); + assertEquals(t0, x.type); assertNull(x.baseline); assertEquals(DiffedFieldValue.Status.DELETED, x.status); } @@ -108,21 +120,21 @@ public class DiffFieldsTest { @Test public void basicDiff() { List<FieldValue> a = new ArrayList<FieldValue>(); - a.add(new FieldValue("n0", "t0", null)); - a.add(new FieldValue("n2", "t2", null)); - a.add(new FieldValue("n3", "t3", null)); - a.add(new FieldValue("n4", "t4", null)); - a.add(new FieldValue("n5", "t5", null)); - a.add(new FieldValue("n6", "t6", null)); + a.add(new FieldValue("n0", t0, null)); + a.add(new FieldValue("n2", t2, null)); + a.add(new FieldValue("n3", t3, null)); + a.add(new FieldValue("n4", t4, null)); + a.add(new FieldValue("n5", t5, null)); + a.add(new FieldValue("n6", t6, null)); List<FieldValue> b = new ArrayList<FieldValue>(); - b.add(new FieldValue("n0", "t0", null)); - b.add(new FieldValue("n1", "t1", null)); - b.add(new FieldValue("n2", "t2", null)); - b.add(new FieldValue("n3", "t3", null)); - b.add(new FieldValue("n5", "t5", null)); - b.add(new FieldValue("n6", "t6", null)); - b.add(new FieldValue("n7", "t7", null)); + b.add(new FieldValue("n0", t0, null)); + b.add(new FieldValue("n1", t1, null)); + b.add(new FieldValue("n2", t2, null)); + b.add(new FieldValue("n3", t3, null)); + b.add(new FieldValue("n5", t5, null)); + b.add(new FieldValue("n6", t6, null)); + b.add(new FieldValue("n7", t7, null)); // Note: The expected result makes assumptions about the implementation of // field diff to match the order of the returned fields. If the @@ -145,22 +157,22 @@ public class DiffFieldsTest { @Test public void reorderedDiff() { List<FieldValue> a = new ArrayList<FieldValue>(); - a.add(new FieldValue("n0", "t0", null)); - a.add(new FieldValue("n1", "t1", null)); - a.add(new FieldValue("n2", "t2", null)); - a.add(new FieldValue("n3", "t3", null)); - a.add(new FieldValue("n4", "t4", null)); - a.add(new FieldValue("n5", "t5", null)); - a.add(new FieldValue("n6", "t6", null)); + a.add(new FieldValue("n0", t0, null)); + a.add(new FieldValue("n1", t1, null)); + a.add(new FieldValue("n2", t2, null)); + a.add(new FieldValue("n3", t3, null)); + a.add(new FieldValue("n4", t4, null)); + a.add(new FieldValue("n5", t5, null)); + a.add(new FieldValue("n6", t6, null)); List<FieldValue> b = new ArrayList<FieldValue>(); - b.add(new FieldValue("n4", "t4", null)); - b.add(new FieldValue("n1", "t1", null)); - b.add(new FieldValue("n3", "t3", null)); - b.add(new FieldValue("n0", "t0", null)); - b.add(new FieldValue("n5", "t5", null)); - b.add(new FieldValue("n2", "t2", null)); - b.add(new FieldValue("n6", "t6", null)); + b.add(new FieldValue("n4", t4, null)); + b.add(new FieldValue("n1", t1, null)); + b.add(new FieldValue("n3", t3, null)); + b.add(new FieldValue("n0", t0, null)); + b.add(new FieldValue("n5", t5, null)); + b.add(new FieldValue("n2", t2, null)); + b.add(new FieldValue("n6", t6, null)); // Note: The expected result makes assumptions about the implementation of // field diff to match the order of the returned fields. If the diff --git a/tools/ahat/test/DiffTest.java b/tools/ahat/test/DiffTest.java index d0349fd178..585f29ae61 100644 --- a/tools/ahat/test/DiffTest.java +++ b/tools/ahat/test/DiffTest.java @@ -18,26 +18,7 @@ package com.android.ahat; import com.android.ahat.heapdump.AhatHeap; import com.android.ahat.heapdump.AhatInstance; -import com.android.ahat.heapdump.AhatSnapshot; -import com.android.ahat.heapdump.Diff; -import com.android.tools.perflib.heap.hprof.HprofClassDump; -import com.android.tools.perflib.heap.hprof.HprofConstant; -import com.android.tools.perflib.heap.hprof.HprofDumpRecord; -import com.android.tools.perflib.heap.hprof.HprofHeapDump; -import com.android.tools.perflib.heap.hprof.HprofInstanceDump; -import com.android.tools.perflib.heap.hprof.HprofInstanceField; -import com.android.tools.perflib.heap.hprof.HprofLoadClass; -import com.android.tools.perflib.heap.hprof.HprofPrimitiveArrayDump; -import com.android.tools.perflib.heap.hprof.HprofRecord; -import com.android.tools.perflib.heap.hprof.HprofRootDebugger; -import com.android.tools.perflib.heap.hprof.HprofStaticField; -import com.android.tools.perflib.heap.hprof.HprofStringBuilder; -import com.android.tools.perflib.heap.hprof.HprofType; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -93,39 +74,9 @@ public class DiffTest { } @Test - public void nullClassObj() throws IOException { - // Set up a heap dump that has a null classObj. - // The heap dump is derived from the InstanceTest.asStringEmbedded test. - HprofStringBuilder strings = new HprofStringBuilder(0); - List<HprofRecord> records = new ArrayList<HprofRecord>(); - List<HprofDumpRecord> dump = new ArrayList<HprofDumpRecord>(); - - final int stringClassObjectId = 1; - records.add(new HprofLoadClass(0, 0, stringClassObjectId, 0, strings.get("java.lang.String"))); - dump.add(new HprofClassDump(stringClassObjectId, 0, 0, 0, 0, 0, 0, 0, 0, - new HprofConstant[0], new HprofStaticField[0], - new HprofInstanceField[]{ - new HprofInstanceField(strings.get("count"), HprofType.TYPE_INT), - new HprofInstanceField(strings.get("hashCode"), HprofType.TYPE_INT), - new HprofInstanceField(strings.get("offset"), HprofType.TYPE_INT), - new HprofInstanceField(strings.get("value"), HprofType.TYPE_OBJECT)})); - - dump.add(new HprofPrimitiveArrayDump(0x41, 0, HprofType.TYPE_CHAR, - new long[]{'n', 'o', 't', ' ', 'h', 'e', 'l', 'l', 'o', 'o', 'p'})); - - ByteArrayDataOutput values = ByteStreams.newDataOutput(); - values.writeInt(5); // count - values.writeInt(0); // hashCode - values.writeInt(4); // offset - values.writeInt(0x41); // value - dump.add(new HprofInstanceDump(0x42, 0, stringClassObjectId, values.toByteArray())); - dump.add(new HprofRootDebugger(stringClassObjectId)); - dump.add(new HprofRootDebugger(0x42)); - - records.add(new HprofHeapDump(0, dump.toArray(new HprofDumpRecord[0]))); - AhatSnapshot snapshot = SnapshotBuilder.makeSnapshot(strings, records); - - // Diffing should not crash. - Diff.snapshots(snapshot, snapshot); + public void diffClassRemoved() throws IOException { + TestDump dump = TestDump.getTestDump("O.hprof", "L.hprof", null); + AhatHandler handler = new ObjectsHandler(dump.getAhatSnapshot()); + TestHandler.testNoCrash(handler, "http://localhost:7100/objects?class=java.lang.Class"); } } diff --git a/tools/ahat/test/HtmlEscaperTest.java b/tools/ahat/test/HtmlEscaperTest.java new file mode 100644 index 0000000000..a36db356f5 --- /dev/null +++ b/tools/ahat/test/HtmlEscaperTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.ahat; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class HtmlEscaperTest { + @Test + public void tests() { + assertEquals("nothing to escape", HtmlEscaper.escape("nothing to escape")); + assertEquals("a<b> & "c'd"e", HtmlEscaper.escape("a<b> & \"c\'d\"e")); + assertEquals("adjacent <<>> x", HtmlEscaper.escape("adjacent <<>> x")); + assertEquals("< initial", HtmlEscaper.escape("< initial")); + assertEquals("ending >", HtmlEscaper.escape("ending >")); + } +} diff --git a/tools/ahat/test/InstanceTest.java b/tools/ahat/test/InstanceTest.java index 63055db93d..49a21e2d70 100644 --- a/tools/ahat/test/InstanceTest.java +++ b/tools/ahat/test/InstanceTest.java @@ -23,23 +23,7 @@ import com.android.ahat.heapdump.AhatSnapshot; import com.android.ahat.heapdump.PathElement; import com.android.ahat.heapdump.Size; import com.android.ahat.heapdump.Value; -import com.android.tools.perflib.heap.hprof.HprofClassDump; -import com.android.tools.perflib.heap.hprof.HprofConstant; -import com.android.tools.perflib.heap.hprof.HprofDumpRecord; -import com.android.tools.perflib.heap.hprof.HprofHeapDump; -import com.android.tools.perflib.heap.hprof.HprofInstanceDump; -import com.android.tools.perflib.heap.hprof.HprofInstanceField; -import com.android.tools.perflib.heap.hprof.HprofLoadClass; -import com.android.tools.perflib.heap.hprof.HprofPrimitiveArrayDump; -import com.android.tools.perflib.heap.hprof.HprofRecord; -import com.android.tools.perflib.heap.hprof.HprofRootDebugger; -import com.android.tools.perflib.heap.hprof.HprofStaticField; -import com.android.tools.perflib.heap.hprof.HprofStringBuilder; -import com.android.tools.perflib.heap.hprof.HprofType; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import org.junit.Test; @@ -395,44 +379,63 @@ public class InstanceTest { @Test public void asStringEmbedded() throws IOException { - // Set up a heap dump with an instance of java.lang.String of - // "hello" with instance id 0x42 that is backed by a char array that is - // bigger. This is how ART used to represent strings, and we should still - // support it in case the heap dump is from a previous platform version. - HprofStringBuilder strings = new HprofStringBuilder(0); - List<HprofRecord> records = new ArrayList<HprofRecord>(); - List<HprofDumpRecord> dump = new ArrayList<HprofDumpRecord>(); - - final int stringClassObjectId = 1; - records.add(new HprofLoadClass(0, 0, stringClassObjectId, 0, strings.get("java.lang.String"))); - dump.add(new HprofClassDump(stringClassObjectId, 0, 0, 0, 0, 0, 0, 0, 0, - new HprofConstant[0], new HprofStaticField[0], - new HprofInstanceField[]{ - new HprofInstanceField(strings.get("count"), HprofType.TYPE_INT), - new HprofInstanceField(strings.get("hashCode"), HprofType.TYPE_INT), - new HprofInstanceField(strings.get("offset"), HprofType.TYPE_INT), - new HprofInstanceField(strings.get("value"), HprofType.TYPE_OBJECT)})); - - dump.add(new HprofPrimitiveArrayDump(0x41, 0, HprofType.TYPE_CHAR, - new long[]{'n', 'o', 't', ' ', 'h', 'e', 'l', 'l', 'o', 'o', 'p'})); - - ByteArrayDataOutput values = ByteStreams.newDataOutput(); - values.writeInt(5); // count - values.writeInt(0); // hashCode - values.writeInt(4); // offset - values.writeInt(0x41); // value - dump.add(new HprofInstanceDump(0x42, 0, stringClassObjectId, values.toByteArray())); - dump.add(new HprofRootDebugger(stringClassObjectId)); - dump.add(new HprofRootDebugger(0x42)); - - records.add(new HprofHeapDump(0, dump.toArray(new HprofDumpRecord[0]))); - AhatSnapshot snapshot = SnapshotBuilder.makeSnapshot(strings, records); - AhatInstance chars = snapshot.findInstance(0x41); - assertNotNull(chars); - assertEquals("not helloop", chars.asString()); - - AhatInstance stringInstance = snapshot.findInstance(0x42); - assertNotNull(stringInstance); - assertEquals("hello", stringInstance.asString()); + // On Android L, image strings were backed by a single big char array. + // Verify we show just the relative part of the string, not the entire + // char array. + TestDump dump = TestDump.getTestDump("L.hprof", null, null); + AhatSnapshot snapshot = dump.getAhatSnapshot(); + + // java.lang.String@0x6fe17050 is an image string "char" backed by a + // shared char array. + AhatInstance str = snapshot.findInstance(0x6fe17050); + assertEquals("char", str.asString()); + } + + @Test + public void nonDefaultHeapRoot() throws IOException { + TestDump dump = TestDump.getTestDump("O.hprof", null, null); + AhatSnapshot snapshot = dump.getAhatSnapshot(); + + // java.util.HashMap@6004fdb8 is marked as a VM INTERNAL root. + // Previously we had a bug where roots not on the default heap were not + // properly treated as roots (b/65356532). + AhatInstance map = snapshot.findInstance(0x6004fdb8); + assertEquals("java.util.HashMap", map.getClassName()); + assertTrue(map.isRoot()); + } + + @Test + public void threadRoot() throws IOException { + TestDump dump = TestDump.getTestDump("O.hprof", null, null); + AhatSnapshot snapshot = dump.getAhatSnapshot(); + + // java.lang.Thread@12c03470 is marked as a thread root. + // Previously we had a bug where thread roots were not properly treated as + // roots (b/65356532). + AhatInstance thread = snapshot.findInstance(0x12c03470); + assertEquals("java.lang.Thread", thread.getClassName()); + assertTrue(thread.isRoot()); + } + + @Test + public void classOfClass() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("anObject"); + AhatClassObj cls = obj.getClassObj(); + AhatClassObj clscls = cls.getClassObj(); + assertNotNull(clscls); + assertEquals("java.lang.Class", clscls.getName()); + } + + @Test + public void nullValueString() throws IOException { + TestDump dump = TestDump.getTestDump("RI.hprof", null, null); + AhatSnapshot snapshot = dump.getAhatSnapshot(); + + // java.lang.String@500001a8 has a null 'value' field, which should not + // cause ahat to crash. + AhatInstance str = snapshot.findInstance(0x500001a8); + assertEquals("java.lang.String", str.getClassName()); + assertNull(str.asString()); } } diff --git a/tools/ahat/test/ProguardMapTest.java b/tools/ahat/test/ProguardMapTest.java new file mode 100644 index 0000000000..ad40f45665 --- /dev/null +++ b/tools/ahat/test/ProguardMapTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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.android.ahat; + +import com.android.ahat.proguard.ProguardMap; +import java.io.IOException; +import java.io.StringReader; +import java.text.ParseException; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class ProguardMapTest { + private static final String TEST_MAP = + "class.that.is.Empty -> a:\n" + + "class.that.is.Empty$subclass -> b:\n" + + "class.with.only.Fields -> c:\n" + + " int prim_type_field -> a\n" + + " int[] prim_array_type_field -> b\n" + + " class.that.is.Empty class_type_field -> c\n" + + " class.that.is.Empty[] array_type_field -> d\n" + + " int longObfuscatedNameField -> abc\n" + + "class.with.Methods -> d:\n" + + " int some_field -> a\n" + + " 12:23:void <clinit>() -> <clinit>\n" + + " 42:43:void boringMethod() -> m\n" + + " 45:48:void methodWithPrimArgs(int,float) -> m\n" + + " 49:50:void methodWithPrimArrArgs(int[],float) -> m\n" + + " 52:55:void methodWithClearObjArg(class.not.in.Map) -> m\n" + + " 57:58:void methodWithClearObjArrArg(class.not.in.Map[]) -> m\n" + + " 59:61:void methodWithObfObjArg(class.with.only.Fields) -> m\n" + + " 64:66:class.with.only.Fields methodWithObfRes() -> n\n" + + " 80:80:void lineObfuscatedMethod():8:8 -> o\n" + + " 90:90:void lineObfuscatedMethod2():9 -> p\n" + + " 120:121:void method.from.a.Superclass.supermethod() -> q\n" + ; + + @Test + public void proguardMap() throws IOException, ParseException { + ProguardMap map = new ProguardMap(); + + // An empty proguard map should not deobfuscate anything. + assertEquals("foo.bar.Sludge", map.getClassName("foo.bar.Sludge")); + assertEquals("fooBarSludge", map.getClassName("fooBarSludge")); + assertEquals("myfield", map.getFieldName("foo.bar.Sludge", "myfield")); + assertEquals("myfield", map.getFieldName("fooBarSludge", "myfield")); + ProguardMap.Frame frame = map.getFrame( + "foo.bar.Sludge", "mymethod", "(Lfoo/bar/Sludge;)V", "SourceFile.java", 123); + assertEquals("mymethod", frame.method); + assertEquals("(Lfoo/bar/Sludge;)V", frame.signature); + assertEquals("SourceFile.java", frame.filename); + assertEquals(123, frame.line); + + // Read in the proguard map. + map.readFromReader(new StringReader(TEST_MAP)); + + // It should still not deobfuscate things that aren't in the map + assertEquals("foo.bar.Sludge", map.getClassName("foo.bar.Sludge")); + assertEquals("fooBarSludge", map.getClassName("fooBarSludge")); + assertEquals("myfield", map.getFieldName("foo.bar.Sludge", "myfield")); + assertEquals("myfield", map.getFieldName("fooBarSludge", "myfield")); + frame = map.getFrame("foo.bar.Sludge", "mymethod", "(Lfoo/bar/Sludge;)V", + "SourceFile.java", 123); + assertEquals("mymethod", frame.method); + assertEquals("(Lfoo/bar/Sludge;)V", frame.signature); + assertEquals("SourceFile.java", frame.filename); + assertEquals(123, frame.line); + + // Test deobfuscation of class names + assertEquals("class.that.is.Empty", map.getClassName("a")); + assertEquals("class.that.is.Empty$subclass", map.getClassName("b")); + assertEquals("class.with.only.Fields", map.getClassName("c")); + assertEquals("class.with.Methods", map.getClassName("d")); + + // Test deobfuscation of array classes. + assertEquals("class.with.Methods[]", map.getClassName("d[]")); + assertEquals("class.with.Methods[][]", map.getClassName("d[][]")); + + // Test deobfuscation of methods + assertEquals("prim_type_field", map.getFieldName("class.with.only.Fields", "a")); + assertEquals("prim_array_type_field", map.getFieldName("class.with.only.Fields", "b")); + assertEquals("class_type_field", map.getFieldName("class.with.only.Fields", "c")); + assertEquals("array_type_field", map.getFieldName("class.with.only.Fields", "d")); + assertEquals("longObfuscatedNameField", map.getFieldName("class.with.only.Fields", "abc")); + assertEquals("some_field", map.getFieldName("class.with.Methods", "a")); + + // Test deobfuscation of frames + frame = map.getFrame("class.with.Methods", "<clinit>", "()V", "SourceFile.java", 13); + assertEquals("<clinit>", frame.method); + assertEquals("()V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(13, frame.line); + + frame = map.getFrame("class.with.Methods", "m", "()V", "SourceFile.java", 42); + assertEquals("boringMethod", frame.method); + assertEquals("()V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(42, frame.line); + + frame = map.getFrame("class.with.Methods", "m", "(IF)V", "SourceFile.java", 45); + assertEquals("methodWithPrimArgs", frame.method); + assertEquals("(IF)V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(45, frame.line); + + frame = map.getFrame("class.with.Methods", "m", "([IF)V", "SourceFile.java", 49); + assertEquals("methodWithPrimArrArgs", frame.method); + assertEquals("([IF)V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(49, frame.line); + + frame = map.getFrame("class.with.Methods", "m", "(Lclass/not/in/Map;)V", + "SourceFile.java", 52); + assertEquals("methodWithClearObjArg", frame.method); + assertEquals("(Lclass/not/in/Map;)V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(52, frame.line); + + frame = map.getFrame("class.with.Methods", "m", "([Lclass/not/in/Map;)V", + "SourceFile.java", 57); + assertEquals("methodWithClearObjArrArg", frame.method); + assertEquals("([Lclass/not/in/Map;)V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(57, frame.line); + + frame = map.getFrame("class.with.Methods", "m", "(Lc;)V", "SourceFile.java", 59); + assertEquals("methodWithObfObjArg", frame.method); + assertEquals("(Lclass/with/only/Fields;)V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(59, frame.line); + + frame = map.getFrame("class.with.Methods", "n", "()Lc;", "SourceFile.java", 64); + assertEquals("methodWithObfRes", frame.method); + assertEquals("()Lclass/with/only/Fields;", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(64, frame.line); + + frame = map.getFrame("class.with.Methods", "o", "()V", "SourceFile.java", 80); + assertEquals("lineObfuscatedMethod", frame.method); + assertEquals("()V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(8, frame.line); + + frame = map.getFrame("class.with.Methods", "p", "()V", "SourceFile.java", 94); + assertEquals("lineObfuscatedMethod2", frame.method); + assertEquals("()V", frame.signature); + assertEquals("Methods.java", frame.filename); + assertEquals(13, frame.line); + + frame = map.getFrame("class.with.Methods", "q", "()V", "SourceFile.java", 120); + // TODO: Should this be "supermethod", instead of + // "method.from.a.Superclass.supermethod"? + assertEquals("method.from.a.Superclass.supermethod", frame.method); + assertEquals("()V", frame.signature); + assertEquals("Superclass.java", frame.filename); + assertEquals(120, frame.line); + } +} diff --git a/tools/ahat/test/SnapshotBuilder.java b/tools/ahat/test/SnapshotBuilder.java deleted file mode 100644 index 0eea6357fd..0000000000 --- a/tools/ahat/test/SnapshotBuilder.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * 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.android.ahat; - -import com.android.ahat.heapdump.AhatSnapshot; -import com.android.tools.perflib.heap.ProguardMap; -import com.android.tools.perflib.heap.hprof.Hprof; -import com.android.tools.perflib.heap.hprof.HprofRecord; -import com.android.tools.perflib.heap.hprof.HprofStringBuilder; -import com.android.tools.perflib.heap.io.InMemoryBuffer; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * Class with utilities to help constructing snapshots for tests. - */ -public class SnapshotBuilder { - - // Helper function to make a snapshot with id size 4 given an - // HprofStringBuilder and list of HprofRecords - public static AhatSnapshot makeSnapshot(HprofStringBuilder strings, List<HprofRecord> records) - throws IOException { - // TODO: When perflib can handle the case where strings are referred to - // before they are defined, just add the string records to the records - // list. - List<HprofRecord> actualRecords = new ArrayList<HprofRecord>(); - actualRecords.addAll(strings.getStringRecords()); - actualRecords.addAll(records); - - Hprof hprof = new Hprof("JAVA PROFILE 1.0.3", 4, new Date(), actualRecords); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - hprof.write(os); - InMemoryBuffer buffer = new InMemoryBuffer(os.toByteArray()); - return AhatSnapshot.fromDataBuffer(buffer, new ProguardMap()); - } -} diff --git a/tools/ahat/test/TestDump.java b/tools/ahat/test/TestDump.java index db9b25646a..a0d1021ef1 100644 --- a/tools/ahat/test/TestDump.java +++ b/tools/ahat/test/TestDump.java @@ -21,75 +21,124 @@ import com.android.ahat.heapdump.AhatInstance; import com.android.ahat.heapdump.AhatSnapshot; import com.android.ahat.heapdump.Diff; import com.android.ahat.heapdump.FieldValue; +import com.android.ahat.heapdump.HprofFormatException; +import com.android.ahat.heapdump.Parser; import com.android.ahat.heapdump.Site; import com.android.ahat.heapdump.Value; -import com.android.tools.perflib.heap.ProguardMap; -import java.io.File; +import com.android.ahat.proguard.ProguardMap; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Objects; /** - * The TestDump class is used to get an AhatSnapshot for the test-dump - * program. + * The TestDump class is used to get the current and baseline AhatSnapshots + * for heap dumps generated by the test-dump program that are stored as + * resources in this jar file. */ public class TestDump { - // It can take on the order of a second to parse and process the test-dump - // hprof. To avoid repeating this overhead for each test case, we cache the - // loaded instance of TestDump and reuse it when possible. In theory the - // test cases should not be able to modify the cached snapshot in a way that - // is visible to other test cases. - private static TestDump mCachedTestDump = null; + // It can take on the order of a second to parse and process test dumps. + // To avoid repeating this overhead for each test case, we provide a way to + // cache loaded instance of TestDump and reuse it when possible. In theory + // the test cases should not be able to modify the cached snapshot in a way + // that is visible to other test cases. + private static List<TestDump> mCachedTestDumps = new ArrayList<TestDump>(); + + // The name of the resources this test dump is loaded from. + private String mHprofResource; + private String mHprofBaseResource; + private String mMapResource; // If the test dump fails to load the first time, it will likely fail every // other test we try. Rather than having to wait a potentially very long // time for test dump loading to fail over and over again, record when it // fails and don't try to load it again. - private static boolean mTestDumpFailed = false; + private boolean mTestDumpFailed = true; + // The loaded heap dumps. private AhatSnapshot mSnapshot; private AhatSnapshot mBaseline; + + // Cached reference to the 'Main' class object in the snapshot and baseline + // heap dumps. private AhatClassObj mMain; private AhatClassObj mBaselineMain; /** - * Load the test-dump.hprof and test-dump-base.hprof files. - * The location of the files are read from the system properties - * "ahat.test.dump.hprof" and "ahat.test.dump.base.hprof", which is expected - * to be set on the command line. - * The location of the proguard map for both hprof files is read from the - * system property "ahat.test.dump.map". For example: - * java -Dahat.test.dump.hprof=test-dump.hprof \ - * -Dahat.test.dump.base.hprof=test-dump-base.hprof \ - * -Dahat.test.dump.map=proguard.map \ - * -jar ahat-tests.jar + * Read the named resource into a ByteBuffer. + */ + private static ByteBuffer dataBufferFromResource(String name) throws IOException { + ClassLoader loader = TestDump.class.getClassLoader(); + InputStream is = loader.getResourceAsStream(name); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = is.read(buf)) != -1) { + baos.write(buf, 0, read); + } + return ByteBuffer.wrap(baos.toByteArray()); + } + + /** + * Create a TestDump instance. + * The load() method should be called to load and process the heap dumps. + * The files are specified as names of resources compiled into the jar file. + * The baseline resouce may be null to indicate that no diffing should be + * performed. + * The map resource may be null to indicate no proguard map will be used. * + */ + private TestDump(String hprofResource, String hprofBaseResource, String mapResource) { + mHprofResource = hprofResource; + mHprofBaseResource = hprofBaseResource; + mMapResource = mapResource; + } + + /** + * Load the heap dumps for this TestDump. * An IOException is thrown if there is a failure reading the hprof files or * the proguard map. */ - private TestDump() throws IOException { - // TODO: Make use of the baseline hprof for tests. - String hprof = System.getProperty("ahat.test.dump.hprof"); - String hprofBase = System.getProperty("ahat.test.dump.base.hprof"); - - String mapfile = System.getProperty("ahat.test.dump.map"); + private void load() throws IOException { ProguardMap map = new ProguardMap(); - try { - map.readFromFile(new File(mapfile)); - } catch (ParseException e) { - throw new IOException("Unable to load proguard map", e); + if (mMapResource != null) { + try { + ClassLoader loader = TestDump.class.getClassLoader(); + InputStream is = loader.getResourceAsStream(mMapResource); + map.readFromReader(new InputStreamReader(is)); + } catch (ParseException e) { + throw new IOException("Unable to load proguard map", e); + } } - mSnapshot = AhatSnapshot.fromHprof(new File(hprof), map); - mBaseline = AhatSnapshot.fromHprof(new File(hprofBase), map); - Diff.snapshots(mSnapshot, mBaseline); + try { + ByteBuffer hprof = dataBufferFromResource(mHprofResource); + mSnapshot = Parser.parseHeapDump(hprof, map); + mMain = findClass(mSnapshot, "Main"); + assert(mMain != null); + } catch (HprofFormatException e) { + throw new IOException("Unable to parse heap dump", e); + } - mMain = findClass(mSnapshot, "Main"); - assert(mMain != null); + if (mHprofBaseResource != null) { + try { + ByteBuffer hprofBase = dataBufferFromResource(mHprofBaseResource); + mBaseline = Parser.parseHeapDump(hprofBase, map); + mBaselineMain = findClass(mBaseline, "Main"); + assert(mBaselineMain != null); + } catch (HprofFormatException e) { + throw new IOException("Unable to parse base heap dump", e); + } + Diff.snapshots(mSnapshot, mBaseline); + } - mBaselineMain = findClass(mBaseline, "Main"); - assert(mBaselineMain != null); + mTestDumpFailed = false; } /** @@ -182,22 +231,42 @@ public class TestDump { } /** - * Get the test dump. + * Get the default (cached) test dump. * An IOException is thrown if there is an error reading the test dump hprof * file. * To improve performance, this returns a cached instance of the TestDump * when possible. */ public static synchronized TestDump getTestDump() throws IOException { - if (mTestDumpFailed) { - throw new RuntimeException("Test dump failed before, assuming it will again"); - } + return getTestDump("test-dump.hprof", "test-dump-base.hprof", "test-dump.map"); + } - if (mCachedTestDump == null) { - mTestDumpFailed = true; - mCachedTestDump = new TestDump(); - mTestDumpFailed = false; + /** + * Get a (cached) test dump. + * @param hprof - The string resouce name of the hprof file. + * @param base - The string resouce name of the baseline hprof, may be null. + * @param map - The string resouce name of the proguard map, may be null. + * An IOException is thrown if there is an error reading the test dump hprof + * file. + * To improve performance, this returns a cached instance of the TestDump + * when possible. + */ + public static synchronized TestDump getTestDump(String hprof, String base, String map) + throws IOException { + for (TestDump loaded : mCachedTestDumps) { + if (Objects.equals(loaded.mHprofResource, hprof) + && Objects.equals(loaded.mHprofBaseResource, base) + && Objects.equals(loaded.mMapResource, map)) { + if (loaded.mTestDumpFailed) { + throw new IOException("Test dump failed before, assuming it will again"); + } + return loaded; + } } - return mCachedTestDump; + + TestDump dump = new TestDump(hprof, base, map); + mCachedTestDumps.add(dump); + dump.load(); + return dump; } } diff --git a/tools/ahat/test/Tests.java b/tools/ahat/test/Tests.java index cd33a9059b..0e7043291d 100644 --- a/tools/ahat/test/Tests.java +++ b/tools/ahat/test/Tests.java @@ -25,11 +25,13 @@ public class Tests { "com.android.ahat.DiffFieldsTest", "com.android.ahat.DiffTest", "com.android.ahat.DominatorsTest", + "com.android.ahat.HtmlEscaperTest", "com.android.ahat.InstanceTest", "com.android.ahat.NativeAllocationTest", "com.android.ahat.ObjectHandlerTest", "com.android.ahat.OverviewHandlerTest", "com.android.ahat.PerformanceTest", + "com.android.ahat.ProguardMapTest", "com.android.ahat.RootedHandlerTest", "com.android.ahat.QueryTest", "com.android.ahat.SiteHandlerTest", |