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[]{"&amp;", "&lt;", "&gt;", "&apos;", "&quot;"};
+    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&lt;b&gt; &amp; &quot;c&apos;d&quot;e", HtmlEscaper.escape("a<b> & \"c\'d\"e"));
+    assertEquals("adjacent &lt;&lt;&gt;&gt; x", HtmlEscaper.escape("adjacent <<>> x"));
+    assertEquals("&lt; initial", HtmlEscaper.escape("< initial"));
+    assertEquals("ending &gt;", 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",