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.
diff --git a/tools/ahat/Android.mk b/tools/ahat/Android.mk
index 2ce61cf..f628fe5 100644
--- a/tools/ahat/Android.mk
+++ b/tools/ahat/Android.mk
@@ -22,10 +22,7 @@
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_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 @@
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_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 4471c0a..ed40cb7 100644
--- a/tools/ahat/README.txt
+++ b/tools/ahat/README.txt
@@ -55,25 +55,6 @@
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 7970bf8..76e9e80 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 @@
* 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 @@
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 @@
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 0000000..75a6827
--- /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 7cda035..623a865 100644
--- a/tools/ahat/src/Main.java
+++ b/tools/ahat/src/Main.java
@@ -18,7 +18,8 @@
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 @@
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 @@
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 f4926aa..79f8b76 100644
--- a/tools/ahat/src/ObjectHandler.java
+++ b/tools/ahat/src/ObjectHandler.java
@@ -25,6 +25,7 @@
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 @@
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 @@
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 b2805d6..4a68f1c 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 @@
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 8d23276..50a4805 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 @@
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;
+ /**
+ * 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;
+ }
- case CHAR:
- final char[] chars = array.asCharArray(0, array.getLength());
- mCharArray = chars;
- mValues = new AbstractList<Value>() {
- @Override public int size() {
- return chars.length;
- }
+ @Override public Value get(int index) {
+ return Value.pack(chars[index]);
+ }
+ };
+ }
- @Override public Value get(int index) {
- return Value.pack(chars[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;
+ }
- 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(floats[index]);
+ }
+ };
+ }
- @Override public Value get(int index) {
- return Value.pack(bytes[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;
+ }
- 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(doubles[index]);
+ }
+ };
+ }
- @Override public Value get(int index) {
- return Value.pack(values[index]);
- }
- };
- break;
+ /**
+ * 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 f7d8431..94efa50 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 @@
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 @@
}
Value value = getField("value");
- if (!value.isAhatInstance()) {
+ if (value == null || !value.isAhatInstance()) {
return null;
}
@@ -248,6 +243,49 @@
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 08c7097..be0f713 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 @@
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);
+ void initialize(AhatClassObj superClass,
+ long instanceSize,
+ Field[] instanceFields,
+ long staticFieldsSize) {
+ mSuperClassObj = superClass;
+ mInstanceSize = instanceSize;
+ mInstanceFields = instanceFields;
+ mStaticFieldsSize = staticFieldsSize;
+ }
- ClassObj classObj = (ClassObj)inst;
- mClassName = classObj.getClassName();
+ void initialize(AhatInstance classLoader, FieldValue[] staticFields) {
+ mClassLoader = classLoader;
+ mStaticFieldValues = staticFields;
+ }
- 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());
- }
+ @Override
+ protected long getExtraJavaSize() {
+ return mStaticFieldsSize;
}
/**
@@ -91,6 +76,14 @@
}
/**
+ * 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 0e78558..c044487 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 @@
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 @@
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 @@
* 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 @@
* 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 @@
* 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 @@
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 @@
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 @@
}
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 @@
@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 8b4c679..07f5b50 100644
--- a/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java
+++ b/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java
@@ -24,11 +24,15 @@
*/
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 9abc952..8849403 100644
--- a/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java
+++ b/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java
@@ -36,6 +36,10 @@
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 1b2cf3c..945966c 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);
- }
+ AhatSnapshot(SuperRoot root,
+ Instances<AhatInstance> instances,
+ List<AhatHeap> heaps,
+ Site rootSite) {
+ mSuperRoot = root;
+ mInstances = instances;
+ mHeaps = heaps;
+ mRootSite = rootSite;
- /**
- * 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());
- }
- }
+ // Update registered native allocation size.
+ for (AhatInstance cleaner : mInstances) {
+ AhatInstance.RegisteredNativeAllocation nra = cleaner.asRegisteredNativeAllocation();
+ if (nra != null) {
+ nra.referent.addRegisteredNativeSize(nra.size);
}
}
- // 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);
- }
+ AhatInstance.computeReverseReferences(mSuperRoot);
+ DominatorsComputation.computeDominators(mSuperRoot);
+ AhatInstance.computeRetainedSize(mSuperRoot, mHeaps.size());
- // 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);
- }
- }
-
- // 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());
-
- 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 @@
* 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 @@
* SENTINEL_ROOT.
*/
public List<AhatInstance> getRooted() {
- return mRooted;
+ return mSuperRoot.getDominated();
}
/**
@@ -252,14 +122,6 @@
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 e2dcf3e..3cd273e 100644
--- a/tools/ahat/src/heapdump/DiffedFieldValue.java
+++ b/tools/ahat/src/heapdump/DiffedFieldValue.java
@@ -23,7 +23,7 @@
*/
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 @@
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 01f87c7..dff4017 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 @@
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 6d72595..20e6da7 100644
--- a/tools/ahat/src/heapdump/FieldValue.java
+++ b/tools/ahat/src/heapdump/FieldValue.java
@@ -18,10 +18,10 @@
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 0000000..55e8958
--- /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 0000000..0851446
--- /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 0000000..3d5f95f
--- /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 d0264a3..0000000
--- 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 0000000..7165b83
--- /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 82931f00..821493f 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 @@
* 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 d377113..a2adbd2 100644
--- a/tools/ahat/src/heapdump/SuperRoot.java
+++ b/tools/ahat/src/heapdump/SuperRoot.java
@@ -34,6 +34,11 @@
}
@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 0000000..726bc47
--- /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 7f86c01e..01fd250 100644
--- a/tools/ahat/src/heapdump/Value.java
+++ b/tools/ahat/src/heapdump/Value.java
@@ -25,37 +25,6 @@
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 @@
}
/**
+ * 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 @@
}
@Override
+ protected Type getType() {
+ return Type.BOOLEAN;
+ }
+
+ @Override
public String toString() {
return Boolean.toString(mBool);
}
@@ -198,6 +184,11 @@
}
@Override
+ protected Type getType() {
+ return Type.BYTE;
+ }
+
+ @Override
public String toString() {
return Byte.toString(mByte);
}
@@ -224,6 +215,11 @@
}
@Override
+ protected Type getType() {
+ return Type.CHAR;
+ }
+
+ @Override
public String toString() {
return Character.toString(mChar);
}
@@ -245,6 +241,11 @@
}
@Override
+ protected Type getType() {
+ return Type.DOUBLE;
+ }
+
+ @Override
public String toString() {
return Double.toString(mDouble);
}
@@ -266,6 +267,11 @@
}
@Override
+ protected Type getType() {
+ return Type.FLOAT;
+ }
+
+ @Override
public String toString() {
return Float.toString(mFloat);
}
@@ -298,6 +304,11 @@
}
@Override
+ protected Type getType() {
+ return Type.OBJECT;
+ }
+
+ @Override
public String toString() {
return mInstance.toString();
}
@@ -334,6 +345,11 @@
}
@Override
+ protected Type getType() {
+ return Type.INT;
+ }
+
+ @Override
public String toString() {
return Integer.toString(mInt);
}
@@ -365,6 +381,11 @@
}
@Override
+ protected Type getType() {
+ return Type.LONG;
+ }
+
+ @Override
public String toString() {
return Long.toString(mLong);
}
@@ -386,6 +407,11 @@
}
@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 0000000..50c110a
--- /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
new file mode 100644
index 0000000..cf82557
--- /dev/null
+++ b/tools/ahat/test-dump/L.hprof
Binary files differ
diff --git a/tools/ahat/test-dump/O.hprof b/tools/ahat/test-dump/O.hprof
new file mode 100644
index 0000000..d474c6c
--- /dev/null
+++ b/tools/ahat/test-dump/O.hprof
Binary files differ
diff --git a/tools/ahat/test-dump/README.txt b/tools/ahat/test-dump/README.txt
new file mode 100644
index 0000000..344271c
--- /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
new file mode 100644
index 0000000..9482542
--- /dev/null
+++ b/tools/ahat/test-dump/RI.hprof
Binary files differ
diff --git a/tools/ahat/test/DiffFieldsTest.java b/tools/ahat/test/DiffFieldsTest.java
index 7dc519d..1939975 100644
--- a/tools/ahat/test/DiffFieldsTest.java
+++ b/tools/ahat/test/DiffFieldsTest.java
@@ -19,6 +19,7 @@
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.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 @@
@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 @@
@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 @@
@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 @@
@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 d0349fd..585f29a 100644
--- a/tools/ahat/test/DiffTest.java
+++ b/tools/ahat/test/DiffTest.java
@@ -18,26 +18,7 @@
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 @@
}
@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 0000000..a36db35
--- /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 63055db..49a21e2 100644
--- a/tools/ahat/test/InstanceTest.java
+++ b/tools/ahat/test/InstanceTest.java
@@ -23,23 +23,7 @@
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 @@
@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>();
+ // 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();
- 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)}));
+ // java.lang.String@0x6fe17050 is an image string "char" backed by a
+ // shared char array.
+ AhatInstance str = snapshot.findInstance(0x6fe17050);
+ assertEquals("char", str.asString());
+ }
- dump.add(new HprofPrimitiveArrayDump(0x41, 0, HprofType.TYPE_CHAR,
- new long[]{'n', 'o', 't', ' ', 'h', 'e', 'l', 'l', 'o', 'o', 'p'}));
+ @Test
+ public void nonDefaultHeapRoot() throws IOException {
+ TestDump dump = TestDump.getTestDump("O.hprof", null, null);
+ AhatSnapshot snapshot = dump.getAhatSnapshot();
- 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));
+ // 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());
+ }
- 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());
+ @Test
+ public void threadRoot() throws IOException {
+ TestDump dump = TestDump.getTestDump("O.hprof", null, null);
+ AhatSnapshot snapshot = dump.getAhatSnapshot();
- AhatInstance stringInstance = snapshot.findInstance(0x42);
- assertNotNull(stringInstance);
- assertEquals("hello", stringInstance.asString());
+ // 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 0000000..ad40f456
--- /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 0eea635..0000000
--- 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 db9b256..a0d1021 100644
--- a/tools/ahat/test/TestDump.java
+++ b/tools/ahat/test/TestDump.java
@@ -21,75 +21,124 @@
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 @@
}
/**
- * 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");
+ }
+
+ /**
+ * 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;
+ }
}
- if (mCachedTestDump == null) {
- mTestDumpFailed = true;
- mCachedTestDump = new TestDump();
- mTestDumpFailed = false;
- }
- 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 cd33a90..0e70432 100644
--- a/tools/ahat/test/Tests.java
+++ b/tools/ahat/test/Tests.java
@@ -25,11 +25,13 @@
"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",