ahat - An android heap dump viewer. Initial checkin.

ahat is an android-aware heap dump viewer based on perflib with a
simple html interface.

Change-Id: I7c18a7603dbbe735f778a95cd047f4f9ec1705ef
diff --git a/Android.mk b/Android.mk
index 49b61bb..ab3eca4 100644
--- a/Android.mk
+++ b/Android.mk
@@ -87,6 +87,7 @@
 include $(art_path)/patchoat/Android.mk
 include $(art_path)/dalvikvm/Android.mk
 include $(art_path)/tools/Android.mk
+include $(art_path)/tools/ahat/Android.mk
 include $(art_path)/tools/dexfuzz/Android.mk
 include $(art_path)/sigchainlib/Android.mk
 
diff --git a/tools/ahat/Android.mk b/tools/ahat/Android.mk
new file mode 100644
index 0000000..3c1522c
--- /dev/null
+++ b/tools/ahat/Android.mk
@@ -0,0 +1,58 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+# --- ahat.jar ----------------
+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/help.html \
+  $(LOCAL_PATH)/src/style.css
+
+LOCAL_STATIC_JAVA_LIBRARIES := perflib-prebuilt guavalib trove-prebuilt
+LOCAL_IS_HOST_MODULE := true
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE := ahat
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# --- ahat script ----------------
+include $(CLEAR_VARS)
+LOCAL_IS_HOST_MODULE := true
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE := ahat
+include $(BUILD_SYSTEM)/base_rules.mk
+$(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/ahat $(ACP)
+	@echo "Copy: $(PRIVATE_MODULE) ($@)"
+	$(copy-file-to-new-target)
+	$(hide) chmod 755 $@
+
+ahat: $(LOCAL_BUILT_MODULE)
+
+# --- ahat-test.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
+LOCAL_IS_HOST_MODULE := true
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE := ahat-tests
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+ahat-test: $(LOCAL_BUILT_MODULE)
+	java -jar $<
diff --git a/tools/ahat/README.txt b/tools/ahat/README.txt
new file mode 100644
index 0000000..a8e3884
--- /dev/null
+++ b/tools/ahat/README.txt
@@ -0,0 +1,110 @@
+AHAT - Android Heap Analysis Tool
+
+Usage:
+  java -jar ahat.jar [-p port] FILE
+    Launch an http server for viewing the given Android heap-dump FILE.
+
+  Options:
+    -p <port>
+       Serve pages on the given port. Defaults to 7100.
+
+TODO:
+ * Add more tips to the help page.
+   - Note that only 'app' heap matters, not 'zygote' or 'image'.
+   - Say what a dex cache is.
+   - Recommend how to start looking at a heap dump.
+   - Say how to enable allocation sites.
+   - Where to submit feedback, questions, and bug reports.
+ * Submit perflib fix for getting stack traces, then uncomment that code in
+   AhatSnapshot to use that.
+ * Dim 'image' and 'zygote' heap sizes slightly? Why do we even show these?
+ * Filter out RootObjs in mSnapshot.getGCRoots, not RootsHandler.
+ * Let user re-sort sites objects info by clicking column headers.
+ * Let user re-sort "Objects" list.
+ * Show site context and heap and class filter in "Objects" view?
+ * Have a menu at the top of an object view with links to the sections?
+ * Include ahat version and hprof file in the menu at the top of the page?
+ * Heaped Table
+   - Make sortable by clicking on headers.
+   - Use consistent order for heap columns.
+      Sometimes I see "app" first, sometimes last (from one heap dump to
+      another) How about, always sort by name?
+ * For long strings, limit the string length shown in the summary view to
+   something reasonable.  Say 50 chars, then add a "..." at the end.
+ * For string summaries, if the string is an offset into a bigger byte array,
+   make sure to show just the part that's in the bigger byte array, not the
+   entire byte array.
+ * For HeapTable with single heap shown, the heap name isn't centered?
+ * Consistently document functions.
+ * Should help be part of an AhatHandler, that automatically gets the menu and
+   stylesheet link rather than duplicating that?
+ * Show version number with --version.
+ * Show somewhere where to send bugs.
+ * /objects query takes a long time to load without parameters.
+ * Include a link to /objects in the overview and menu?
+ * Turn on LOCAL_JAVACFLAGS := -Xlint:unchecked -Werror
+ * Use hex for object ids in URLs?
+ * In general, all tables and descriptions should show a limited amount to
+   start, and only show more when requested by the user.
+ * Don't have handlers inherit from HttpHandler
+   - because they should be independent from http.
+
+ * [low priority] by site allocations won't line up if the stack has been
+   truncated. Is there any way to manually line them up in that case?
+
+ * [low priority] Have a switch to choose whether unreachable objects are
+   ignored or not?  Is there any interest in what's unreachable, or is it only
+   reachable objects that people care about?
+
+ * [low priority] Have a way to diff two heap dumps by site.
+   This should be pretty easy to do, actually. The interface is the real
+   question. Maybe: augment each byte count field on every page with the diff
+   if a baseline has been provided, and allow the user to sort by the diff.
+
+Things to Test:
+ * That we can open a hprof without an 'app' heap and show a tabulation of
+   objects normally sorted by 'app' heap by default.
+ * Visit /objects without parameters and verify it doesn't throw an exception.
+ * Visit /objects with an invalid site, verify it doesn't throw an exception.
+ * That we can view an array with 3 million elements in a reasonably short
+   amount of time (not more than 1 second?)
+ * That we can view the list of all objects in a reasonably short amount of
+   time.
+ * That we don't show the 'extra' column in the DominatedList if we are
+   showing all the instances.
+
+Reported Issues:
+ * Request to be able to sort tables by size.
+ * Hangs on showing large arrays, where hat does not hang.
+   - Solution is probably to not show all the array elements by default.
+
+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.
+ * Don't store stack trace by heap (CL 157252)
+ * 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.
+
+Release History:
+ 0.1ss Aug 04, 2015
+   Enable stack allocations code (using custom modified perflib).
+   Sort objects in 'objects/' with default sort.
+
+ 0.1-stacks Aug 03, 2015
+   Enable stack allocations code (using custom modified perflib).
+
+ 0.1 July 30, 2015
+   Initial Release
+
diff --git a/tools/ahat/ahat b/tools/ahat/ahat
new file mode 100755
index 0000000..77c1d6e
--- /dev/null
+++ b/tools/ahat/ahat
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# 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.
+#
+
+#
+# Wrapper script for calling ahat
+java -jar ${ANDROID_HOST_OUT}/framework/ahat.jar "$@"
diff --git a/tools/ahat/src/AhatHandler.java b/tools/ahat/src/AhatHandler.java
new file mode 100644
index 0000000..2da02f8
--- /dev/null
+++ b/tools/ahat/src/AhatHandler.java
@@ -0,0 +1,66 @@
+/*
+ * 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.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import java.io.IOException;
+import java.io.PrintStream;
+
+/**
+ * AhatHandler.
+ *
+ * Common base class of all the ahat HttpHandlers.
+ */
+abstract class AhatHandler implements HttpHandler {
+
+  protected AhatSnapshot mSnapshot;
+
+  public AhatHandler(AhatSnapshot snapshot) {
+    mSnapshot = snapshot;
+  }
+
+  public abstract void handle(Doc doc, Query query) throws IOException;
+
+  @Override
+  public void handle(HttpExchange exchange) throws IOException {
+    exchange.getResponseHeaders().add("Content-Type", "text/html;charset=utf-8");
+    exchange.sendResponseHeaders(200, 0);
+    PrintStream ps = new PrintStream(exchange.getResponseBody());
+    try {
+      HtmlDoc doc = new HtmlDoc(ps, DocString.text("ahat"), DocString.uri("style.css"));
+      DocString menu = new DocString();
+      menu.appendLink(DocString.uri("/"), DocString.text("overview"));
+      menu.append(" - ");
+      menu.appendLink(DocString.uri("roots"), DocString.text("roots"));
+      menu.append(" - ");
+      menu.appendLink(DocString.uri("sites"), DocString.text("allocations"));
+      menu.append(" - ");
+      menu.appendLink(DocString.uri("help"), DocString.text("help"));
+      doc.menu(menu);
+      handle(doc, new Query(exchange.getRequestURI()));
+      doc.close();
+    } catch (RuntimeException e) {
+      // Print runtime exceptions to standard error for debugging purposes,
+      // because otherwise they are swallowed and not reported.
+      System.err.println("Exception when handling " + exchange.getRequestURI() + ": ");
+      e.printStackTrace();
+      throw e;
+    }
+    ps.close();
+  }
+}
diff --git a/tools/ahat/src/AhatSnapshot.java b/tools/ahat/src/AhatSnapshot.java
new file mode 100644
index 0000000..2437d03
--- /dev/null
+++ b/tools/ahat/src/AhatSnapshot.java
@@ -0,0 +1,197 @@
+/*
+ * 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.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Heap;
+import com.android.tools.perflib.heap.Instance;
+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 com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A wrapper over the perflib snapshot that provides the behavior we use in
+ * ahat.
+ */
+class AhatSnapshot {
+  private Snapshot mSnapshot;
+  private List<Heap> mHeaps;
+
+  // Map from Instance to the list of Instances it immediately dominates.
+  private Map<Instance, List<Instance>> mDominated;
+
+  private Site mRootSite;
+  private Map<Heap, Long> mHeapSizes;
+
+  public AhatSnapshot(Snapshot snapshot) {
+    mSnapshot = snapshot;
+    mHeaps = new ArrayList<Heap>(mSnapshot.getHeaps());
+    mDominated = new HashMap<Instance, List<Instance>>();
+    mRootSite = new Site("ROOT");
+    mHeapSizes = new HashMap<Heap, Long>();
+
+    ClassObj javaLangClass = mSnapshot.findClass("java.lang.Class");
+    for (Heap heap : mHeaps) {
+      long total = 0;
+      for (Instance inst : Iterables.concat(heap.getClasses(), heap.getInstances())) {
+        Instance dominator = inst.getImmediateDominator();
+        if (dominator != null) {
+          total += inst.getSize();
+
+          // Properly label the class of a class object.
+          if (inst instanceof ClassObj && javaLangClass != null && inst.getClassObj() == null) {
+              inst.setClassId(javaLangClass.getId());
+          }
+
+          // Update dominated instances.
+          List<Instance> instances = mDominated.get(dominator);
+          if (instances == null) {
+            instances = new ArrayList<Instance>();
+            mDominated.put(dominator, instances);
+          }
+          instances.add(inst);
+
+          // Update sites.
+          List<StackFrame> path = Collections.emptyList();
+          StackTrace stack = getStack(inst);
+          int stackId = getStackTraceSerialNumber(stack);
+          if (stack != null) {
+            StackFrame[] frames = getStackFrames(stack);
+            if (frames != null && frames.length > 0) {
+              path = Lists.reverse(Arrays.asList(frames));
+            }
+          }
+          mRootSite.add(stackId, 0, path.iterator(), inst);
+        }
+      }
+      mHeapSizes.put(heap, total);
+    }
+  }
+
+  public Instance findInstance(long id) {
+    return mSnapshot.findInstance(id);
+  }
+
+  public int getHeapIndex(Heap heap) {
+    return mSnapshot.getHeapIndex(heap);
+  }
+
+  public Heap getHeap(String name) {
+    return mSnapshot.getHeap(name);
+  }
+
+  public Collection<RootObj> getGCRoots() {
+    return mSnapshot.getGCRoots();
+  }
+
+  public List<Heap> getHeaps() {
+    return mHeaps;
+  }
+
+  public Site getRootSite() {
+    return mRootSite;
+  }
+
+  /**
+   * Look up the site at which the given object was allocated.
+   */
+  public Site getSiteForInstance(Instance inst) {
+    Site site = mRootSite;
+    StackTrace stack = getStack(inst);
+    if (stack != null) {
+      StackFrame[] frames = getStackFrames(stack);
+      if (frames != null) {
+        List<StackFrame> path = Lists.reverse(Arrays.asList(frames));
+        site = mRootSite.getChild(path.iterator());
+      }
+    }
+    return site;
+  }
+
+  /**
+   * Return a list of those objects immediately dominated by the given
+   * instance.
+   */
+  public List<Instance> getDominated(Instance inst) {
+    return mDominated.get(inst);
+  }
+
+  /**
+   * Return the total size of reachable objects allocated on the given heap.
+   */
+  public long getHeapSize(Heap heap) {
+    return mHeapSizes.get(heap);
+  }
+
+  /**
+   * Return the class name for the given class object.
+   * classObj may be null, in which case "(class unknown)" is returned.
+   */
+  public static String getClassName(ClassObj classObj) {
+    if (classObj == null) {
+      return "(class unknown)";
+    }
+    return classObj.getClassName();
+  }
+
+  // Return the stack where the given instance was allocated.
+  private static StackTrace getStack(Instance inst) {
+    // TODO: return inst.getStack() once perflib is fixed.
+    return null;
+  }
+
+  // Return the list of stack frames for a stack trace.
+  private static StackFrame[] getStackFrames(StackTrace stack) {
+    // TODO: Use stack.getFrames() once perflib is fixed.
+    return null;
+  }
+
+  // Return the serial number of the given stack trace.
+  private static int getStackTraceSerialNumber(StackTrace stack) {
+    // TODO: Use stack.getSerialNumber() once perflib is fixed.
+    return 0;
+  }
+
+  // Get the site associated with the given stack id and depth.
+  // Returns the root site if no such site found.
+  // depth of -1 means the full stack.
+  public Site getSite(int stackId, int depth) {
+    Site site = mRootSite;
+    StackTrace stack = mSnapshot.getStackTrace(stackId);
+    if (stack != null) {
+      StackFrame[] frames = getStackFrames(stack);
+      if (frames != null) {
+        List<StackFrame> path = Lists.reverse(Arrays.asList(frames));
+        if (depth >= 0) {
+          path = path.subList(0, depth);
+        }
+        site = mRootSite.getChild(path.iterator());
+      }
+    }
+    return site;
+  }
+}
diff --git a/tools/ahat/src/BitmapHandler.java b/tools/ahat/src/BitmapHandler.java
new file mode 100644
index 0000000..0f567e3
--- /dev/null
+++ b/tools/ahat/src/BitmapHandler.java
@@ -0,0 +1,68 @@
+/*
+ * 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.tools.perflib.heap.Instance;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import javax.imageio.ImageIO;
+
+class BitmapHandler implements HttpHandler {
+  private AhatSnapshot mSnapshot;
+
+  public BitmapHandler(AhatSnapshot snapshot) {
+    mSnapshot = snapshot;
+  }
+
+  @Override
+  public void handle(HttpExchange exchange) throws IOException {
+    try {
+      Query query = new Query(exchange.getRequestURI());
+      long id = query.getLong("id", 0);
+      BufferedImage bitmap = null;
+      Instance inst = mSnapshot.findInstance(id);
+      if (inst != null) {
+        bitmap = InstanceUtils.asBitmap(inst);
+      }
+
+      if (bitmap != null) {
+        exchange.getResponseHeaders().add("Content-Type", "image/png");
+        exchange.sendResponseHeaders(200, 0);
+        OutputStream os = exchange.getResponseBody();
+        ImageIO.write(bitmap, "png", os);
+        os.close();
+      } else {
+        exchange.getResponseHeaders().add("Content-Type", "text/html");
+        exchange.sendResponseHeaders(404, 0);
+        PrintStream ps = new PrintStream(exchange.getResponseBody());
+        HtmlDoc doc = new HtmlDoc(ps, DocString.text("ahat"), DocString.uri("style.css"));
+        doc.big(DocString.text("No bitmap found for the given request."));
+        doc.close();
+      }
+    } catch (RuntimeException e) {
+      // Print runtime exceptions to standard error for debugging purposes,
+      // because otherwise they are swallowed and not reported.
+      System.err.println("Exception when handling " + exchange.getRequestURI() + ": ");
+      e.printStackTrace();
+      throw e;
+    }
+  }
+}
diff --git a/tools/ahat/src/Column.java b/tools/ahat/src/Column.java
new file mode 100644
index 0000000..b7f2829
--- /dev/null
+++ b/tools/ahat/src/Column.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Configuration of a Doc table column.
+ */
+class Column {
+  public DocString heading;
+  public Align align;
+
+  public static enum Align {
+    LEFT, RIGHT
+  };
+
+  public Column(DocString heading, Align align) {
+    this.heading = heading;
+    this.align = align;
+  }
+
+  /**
+   * Construct a left-aligned column with a simple heading.
+   */
+  public Column(String heading) {
+    this(DocString.text(heading), Align.LEFT);
+  }
+
+  /**
+   * Construct a column with a simple heading.
+   */
+  public Column(String heading, Align align) {
+    this(DocString.text(heading), align);
+  }
+}
diff --git a/tools/ahat/src/Doc.java b/tools/ahat/src/Doc.java
new file mode 100644
index 0000000..7fa70de
--- /dev/null
+++ b/tools/ahat/src/Doc.java
@@ -0,0 +1,97 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * An interface for rendering a page of content to the user.
+ */
+interface Doc extends AutoCloseable {
+  /**
+   * Output the title of the page.
+   */
+  public void title(String format, Object... args);
+
+  /**
+   * Print a line of text for a page menu.
+   */
+  public void menu(DocString string);
+
+  /**
+   * Start a new section with the given title.
+   */
+  public void section(String title);
+
+  /**
+   * Print a line of text in a normal font.
+   */
+  public void println(DocString string);
+
+  /**
+   * Print a line of text in a large font that is easy to see and click on.
+   */
+  public void big(DocString string);
+
+  /**
+   * Start a table with the given columns.
+   *
+   * An IllegalArgumentException is thrown if no columns are provided.
+   *
+   * This should be followed by calls to the 'row' method to fill in the table
+   * contents and the 'end' method to end the table.
+   */
+  public void table(Column... columns);
+
+  /**
+   * Start a table with the following heading structure:
+   *   |  description  |  c2  | c3 | ... |
+   *   | h1 | h2 | ... |      |    |     |
+   *
+   * Where subcols describes h1, h2, ...
+   * and cols describes c2, c3, ...
+   *
+   * This should be followed by calls to the 'row' method to fill in the table
+   * contents and the 'end' method to end the table.
+   */
+  public void table(DocString description, List<Column> subcols, List<Column> cols);
+
+  /**
+   * Add a row to the currently active table.
+   * The number of values must match the number of columns provided for the
+   * currently active table.
+   */
+  public void row(DocString... values);
+
+  /**
+   * Start a new description list.
+   *
+   * This should be followed by calls to description() and finally a call to
+   * end().
+   */
+  public void descriptions();
+
+  /**
+   * Add a description to the currently active description list.
+   */
+  public void description(DocString key, DocString value);
+
+  /**
+   * End the currently active table or description list.
+   */
+  public void end();
+}
diff --git a/tools/ahat/src/DocString.java b/tools/ahat/src/DocString.java
new file mode 100644
index 0000000..1d997dc
--- /dev/null
+++ b/tools/ahat/src/DocString.java
@@ -0,0 +1,121 @@
+/*
+ * 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.google.common.html.HtmlEscapers;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * A class representing a small string of document content consisting of text,
+ * links, images, etc.
+ */
+class DocString {
+  private StringBuilder mStringBuilder;
+
+  public DocString() {
+    mStringBuilder = new StringBuilder();
+  }
+
+  /**
+   * Construct a new DocString, initialized with the given text.
+   * Format arguments are supported.
+   */
+  public static DocString text(String format, Object... args) {
+    DocString doc = new DocString();
+    return doc.append(format, args);
+  }
+
+  /**
+   * Construct a new DocString, initialized with the given link.
+   */
+  public static DocString link(URI uri, DocString content) {
+    DocString doc = new DocString();
+    return doc.appendLink(uri, content);
+
+  }
+
+  /**
+   * Construct a new DocString initialized with the given image.
+   */
+  public static DocString image(URI uri, String alt) {
+    return (new DocString()).appendImage(uri, alt);
+  }
+
+  /**
+   * Append literal text to the given doc string.
+   * Format arguments are supported.
+   * Returns this object.
+   */
+  public DocString append(String format, Object... args) {
+    String text = String.format(format, args);
+    mStringBuilder.append(HtmlEscapers.htmlEscaper().escape(text));
+    return this;
+  }
+
+  public DocString append(DocString str) {
+    mStringBuilder.append(str.html());
+    return this;
+  }
+
+  public DocString appendLink(URI uri, DocString content) {
+    mStringBuilder.append("<a href=\"");
+    mStringBuilder.append(uri.toASCIIString());
+    mStringBuilder.append("\">");
+    mStringBuilder.append(content.html());
+    mStringBuilder.append("</a>");
+    return this;
+  }
+
+  public DocString appendImage(URI uri, String alt) {
+    mStringBuilder.append("<img alt=\"");
+    mStringBuilder.append(HtmlEscapers.htmlEscaper().escape(alt));
+    mStringBuilder.append("\" src=\"");
+    mStringBuilder.append(uri.toASCIIString());
+    mStringBuilder.append("\" />");
+    return this;
+  }
+
+  public DocString appendThumbnail(URI uri, String alt) {
+    mStringBuilder.append("<img height=\"16\" alt=\"");
+    mStringBuilder.append(HtmlEscapers.htmlEscaper().escape(alt));
+    mStringBuilder.append("\" src=\"");
+    mStringBuilder.append(uri.toASCIIString());
+    mStringBuilder.append("\" />");
+    return this;
+  }
+
+  /**
+   * Convenience function for constructing a URI from a string with a uri
+   * known to be valid. Format arguments are supported.
+   */
+  public static URI uri(String format, Object... args) {
+    String uriString = String.format(format, args);
+    try {
+      return new URI(uriString);
+    } catch (URISyntaxException e) {
+      throw new IllegalStateException("Known good uri has syntax error: " + uriString, e);
+    }
+  }
+
+  /**
+   * Render the DocString as html.
+   */
+  public String html() {
+    return mStringBuilder.toString();
+  }
+}
diff --git a/tools/ahat/src/DominatedList.java b/tools/ahat/src/DominatedList.java
new file mode 100644
index 0000000..53d1073
--- /dev/null
+++ b/tools/ahat/src/DominatedList.java
@@ -0,0 +1,165 @@
+/*
+ * 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.tools.perflib.heap.Heap;
+import com.android.tools.perflib.heap.Instance;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class for rendering a list of instances dominated by a single instance in a
+ * pretty way.
+ */
+class DominatedList {
+  private static final int kIncrAmount = 100;
+  private static final int kDefaultShown = 100;
+
+  /**
+   * Render a table to the given HtmlWriter showing a pretty list of
+   * instances.
+   *
+   * Rather than show all of the instances (which may be very many), we use
+   * the query parameter "dominated" to specify a limited number of
+   * instances to show. The 'uri' parameter should be the current page URI, so
+   * that we can add links to "show more" and "show less" objects that go to
+   * the same page with only the number of objects adjusted.
+   */
+  public static void render(final AhatSnapshot snapshot, Doc doc,
+      Collection<Instance> instances, Query query) {
+    List<Instance> insts = new ArrayList<Instance>(instances);
+    Collections.sort(insts, Sort.defaultInstanceCompare(snapshot));
+
+    int numInstancesToShow = getNumInstancesToShow(query, insts.size());
+    List<Instance> shown = new ArrayList<Instance>(insts.subList(0, numInstancesToShow));
+    List<Instance> hidden = insts.subList(numInstancesToShow, insts.size());
+
+    // Add 'null' as a marker for "all the rest of the objects".
+    if (!hidden.isEmpty()) {
+      shown.add(null);
+    }
+    HeapTable.render(doc, new TableConfig(snapshot, hidden), snapshot, shown);
+
+    if (insts.size() > kDefaultShown) {
+      printMenu(doc, query, numInstancesToShow, insts.size());
+    }
+  }
+
+  private static class TableConfig implements HeapTable.TableConfig<Instance> {
+    AhatSnapshot mSnapshot;
+
+    // Map from heap name to the total size of the instances not shown in the
+    // table.
+    Map<Heap, Long> mHiddenSizes;
+
+    public TableConfig(AhatSnapshot snapshot, List<Instance> hidden) {
+      mSnapshot = snapshot;
+      mHiddenSizes = new HashMap<Heap, Long>();
+      for (Heap heap : snapshot.getHeaps()) {
+        mHiddenSizes.put(heap, 0L);
+      }
+
+      if (!hidden.isEmpty()) {
+        for (Instance inst : hidden) {
+          for (Heap heap : snapshot.getHeaps()) {
+            int index = snapshot.getHeapIndex(heap);
+            long size = inst.getRetainedSize(index);
+            mHiddenSizes.put(heap, mHiddenSizes.get(heap) + size);
+          }
+        }
+      }
+    }
+
+    @Override
+    public String getHeapsDescription() {
+      return "Bytes Retained by Heap";
+    }
+
+    @Override
+    public long getSize(Instance element, Heap heap) {
+      if (element == null) {
+        return mHiddenSizes.get(heap);
+      }
+      int index = mSnapshot.getHeapIndex(heap);
+      return element.getRetainedSize(index);
+    }
+
+    @Override
+    public List<HeapTable.ValueConfig<Instance>> getValueConfigs() {
+      HeapTable.ValueConfig<Instance> value = new HeapTable.ValueConfig<Instance>() {
+        public String getDescription() {
+          return "Object";
+        }
+
+        public DocString render(Instance element) {
+          if (element == null) {
+            return DocString.text("...");
+          } else {
+            return Value.render(element);
+          }
+        }
+      };
+      return Collections.singletonList(value);
+    }
+  }
+
+  // Figure out how many objects to show based on the query parameter.
+  // The resulting value is guaranteed to be at least zero, and no greater
+  // than the number of total objects.
+  private static int getNumInstancesToShow(Query query, int totalNumInstances) {
+    String value = query.get("dominated", null);
+    try {
+      int count = Math.min(totalNumInstances, Integer.parseInt(value));
+      return Math.max(0, count);
+    } catch (NumberFormatException e) {
+      // We can't parse the value as a number. Ignore it.
+    }
+    return Math.min(kDefaultShown, totalNumInstances);
+  }
+
+  // Print a menu line after the table to control how many objects are shown.
+  // It has the form:
+  //  (showing X of Y objects - show none - show less - show more - show all)
+  private static void printMenu(Doc doc, Query query, int shown, int all) {
+    DocString menu = new DocString();
+    menu.append("(%d of %d objects shown - ", shown, all);
+    if (shown > 0) {
+      int less = Math.max(0, shown - kIncrAmount);
+      menu.appendLink(query.with("dominated", 0), DocString.text("show none"));
+      menu.append(" - ");
+      menu.appendLink(query.with("dominated", less), DocString.text("show less"));
+      menu.append(" - ");
+    } else {
+      menu.append("show none - show less - ");
+    }
+    if (shown < all) {
+      int more = Math.min(shown + kIncrAmount, all);
+      menu.appendLink(query.with("dominated", more), DocString.text("show more"));
+      menu.append(" - ");
+      menu.appendLink(query.with("dominated", all), DocString.text("show all"));
+      menu.append(")");
+    } else {
+      menu.append("show more - show all)");
+    }
+    doc.println(menu);
+  }
+}
+
diff --git a/tools/ahat/src/HeapTable.java b/tools/ahat/src/HeapTable.java
new file mode 100644
index 0000000..60bb387
--- /dev/null
+++ b/tools/ahat/src/HeapTable.java
@@ -0,0 +1,106 @@
+/*
+ * 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.tools.perflib.heap.Heap;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for rendering a table that includes sizes of some kind for each heap.
+ */
+class HeapTable {
+  /**
+   * Configuration for a value column of a heap table.
+   */
+  public static interface ValueConfig<T> {
+    public String getDescription();
+    public DocString render(T element);
+  }
+
+  /**
+   * Configuration for the HeapTable.
+   */
+  public static interface TableConfig<T> {
+    public String getHeapsDescription();
+    public long getSize(T element, Heap heap);
+    public List<ValueConfig<T>> getValueConfigs();
+  }
+
+  public static <T> void render(Doc doc, TableConfig<T> config,
+      AhatSnapshot snapshot, List<T> elements) {
+    // Only show the heaps that have non-zero entries.
+    List<Heap> heaps = new ArrayList<Heap>();
+    for (Heap heap : snapshot.getHeaps()) {
+      if (hasNonZeroEntry(snapshot, heap, config, elements)) {
+        heaps.add(heap);
+      }
+    }
+
+    List<ValueConfig<T>> values = config.getValueConfigs();
+
+    // Print the heap and values descriptions.
+    boolean showTotal = heaps.size() > 1;
+    List<Column> subcols = new ArrayList<Column>();
+    for (Heap heap : heaps) {
+      subcols.add(new Column(heap.getName(), Column.Align.RIGHT));
+    }
+    if (showTotal) {
+      subcols.add(new Column("Total", Column.Align.RIGHT));
+    }
+    List<Column> cols = new ArrayList<Column>();
+    for (ValueConfig value : values) {
+      cols.add(new Column(value.getDescription()));
+    }
+    doc.table(DocString.text(config.getHeapsDescription()), subcols, cols);
+
+    // Print the entries.
+    ArrayList<DocString> vals = new ArrayList<DocString>();
+    for (T elem : elements) {
+      vals.clear();
+      long total = 0;
+      for (Heap heap : heaps) {
+        long size = config.getSize(elem, heap);
+        total += size;
+        vals.add(DocString.text("%,14d", size));
+      }
+      if (showTotal) {
+        vals.add(DocString.text("%,14d", total));
+      }
+
+      for (ValueConfig<T> value : values) {
+        vals.add(value.render(elem));
+      }
+      doc.row(vals.toArray(new DocString[0]));
+    }
+    doc.end();
+  }
+
+  // Returns true if the given heap has a non-zero size entry.
+  public static <T> boolean hasNonZeroEntry(AhatSnapshot snapshot, Heap heap,
+      TableConfig<T> config, List<T> elements) {
+    if (snapshot.getHeapSize(heap) > 0) {
+      for (T element : elements) {
+        if (config.getSize(element, heap) > 0) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+}
+
diff --git a/tools/ahat/src/HtmlDoc.java b/tools/ahat/src/HtmlDoc.java
new file mode 100644
index 0000000..5ccbacb
--- /dev/null
+++ b/tools/ahat/src/HtmlDoc.java
@@ -0,0 +1,175 @@
+/*
+ * 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 java.io.PrintStream;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * An Html implementation of Doc.
+ */
+public class HtmlDoc implements Doc {
+  private PrintStream ps;
+  private Column[] mCurrentTableColumns;
+
+  /**
+   * Create an HtmlDoc that writes to the given print stream.
+   * @param title - The main page title.
+   * @param style - A URI link to a stylesheet to link to.
+   */
+  public HtmlDoc(PrintStream ps, DocString title, URI style) {
+    this.ps = ps;
+
+    ps.println("<!DOCTYPE html>");
+    ps.println("<html>");
+    ps.println("<head>");
+    ps.format("<title>%s</title>\n", title.html());
+    ps.format("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">\n",
+        style.toASCIIString());
+    ps.println("</head>");
+    ps.println("<body>");
+  }
+
+  @Override
+  public void title(String format, Object... args) {
+    ps.print("<h1>");
+    ps.print(DocString.text(String.format(format, args)).html());
+    ps.println("</h1>");
+  }
+
+  @Override
+  public void menu(DocString string) {
+    ps.format("<div class=\"menu\">%s</div>", string.html());
+  }
+
+  @Override
+  public void section(String title) {
+    ps.print("<h2>");
+    ps.print(DocString.text(title).html());
+    ps.println(":</h2>");
+  }
+
+  @Override
+  public void println(DocString string) {
+    ps.print(string.html());
+    ps.println("<br />");
+  }
+
+  @Override
+  public void big(DocString str) {
+    ps.print("<h2>");
+    ps.print(str.html());
+    ps.println("</h2>");
+  }
+
+  @Override
+  public void table(Column... columns) {
+    if (columns.length == 0) {
+      throw new IllegalArgumentException("No columns specified");
+    }
+
+    mCurrentTableColumns = columns;
+    ps.println("<table>");
+    for (int i = 0; i < columns.length - 1; i++) {
+      ps.format("<th>%s</th>", columns[i].heading.html());
+    }
+
+    // Align the last header to the left so it's easier to see if the last
+    // column is very wide.
+    ps.format("<th align=\"left\">%s</th>", columns[columns.length - 1].heading.html());
+  }
+
+  @Override
+  public void table(DocString description, List<Column> subcols, List<Column> cols) {
+    mCurrentTableColumns = new Column[subcols.size() + cols.size()];
+    int j = 0;
+    for (Column col : subcols) {
+      mCurrentTableColumns[j] = col;
+      j++;
+    }
+    for (Column col : cols) {
+      mCurrentTableColumns[j] = col;
+      j++;
+    }
+
+    ps.println("<table>");
+    ps.format("<tr><th colspan=\"%d\">%s</th>", subcols.size(), description.html());
+    for (int i = 0; i < cols.size() - 1; i++) {
+      ps.format("<th rowspan=\"2\">%s</th>", cols.get(i).heading.html());
+    }
+    if (!cols.isEmpty()) {
+      // Align the last column header to the left so it can still be seen if
+      // the last column is very wide.
+      ps.format("<th align=\"left\" rowspan=\"2\">%s</th>",
+          cols.get(cols.size() - 1).heading.html());
+    }
+    ps.println("</tr>");
+
+    ps.print("<tr>");
+    for (Column subcol : subcols) {
+      ps.format("<th>%s</th>", subcol.heading.html());
+    }
+    ps.println("</tr>");
+  }
+
+  @Override
+  public void row(DocString... values) {
+    if (mCurrentTableColumns == null) {
+      throw new IllegalStateException("table method must be called before row");
+    }
+
+    if (mCurrentTableColumns.length != values.length) {
+      throw new IllegalArgumentException(String.format(
+          "Wrong number of row values. Expected %d, but got %d",
+          mCurrentTableColumns.length, values.length));
+    }
+
+    ps.print("<tr>");
+    for (int i = 0; i < values.length; i++) {
+      ps.print("<td");
+      if (mCurrentTableColumns[i].align == Column.Align.RIGHT) {
+        ps.print(" align=\"right\"");
+      }
+      ps.format(">%s</td>", values[i].html());
+    }
+    ps.println("</tr>");
+  }
+
+  @Override
+  public void descriptions() {
+    ps.println("<table>");
+  }
+
+  @Override
+  public void description(DocString key, DocString value) {
+    ps.format("<tr><th align=\"left\">%s:</th><td>%s</td></tr>", key.html(), value.html());
+  }
+
+  @Override
+  public void end() {
+    ps.println("</table>");
+    mCurrentTableColumns = null;
+  }
+
+  @Override
+  public void close() {
+    ps.println("</body>");
+    ps.println("</html>");
+    ps.close();
+  }
+}
diff --git a/tools/ahat/src/InstanceUtils.java b/tools/ahat/src/InstanceUtils.java
new file mode 100644
index 0000000..7ee3ff2
--- /dev/null
+++ b/tools/ahat/src/InstanceUtils.java
@@ -0,0 +1,238 @@
+/*
+ * 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.tools.perflib.heap.ArrayInstance;
+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.Type;
+import java.awt.image.BufferedImage;
+
+/**
+ * Utilities for extracting information from hprof instances.
+ */
+class InstanceUtils {
+  /**
+   * Returns true if the given instance is an instance of a class with the
+   * given name.
+   */
+  public static boolean isInstanceOfClass(Instance inst, String className) {
+    ClassObj cls = inst.getClassObj();
+    return (cls != null && className.equals(cls.getClassName()));
+  }
+
+  /**
+   * Read the char[] value from an hprof Instance.
+   * Returns null if the object can't be interpreted as a char[].
+   */
+  private static char[] asCharArray(Instance inst) {
+    if (! (inst instanceof ArrayInstance)) {
+      return null;
+    }
+
+    ArrayInstance array = (ArrayInstance) inst;
+    if (array.getArrayType() != Type.CHAR) {
+      return null;
+    }
+    return array.asCharArray(0, array.getValues().length);
+  }
+
+  /**
+   * Read the byte[] value from an hprof Instance.
+   * Returns null if the instance is not a byte array.
+   */
+  private static byte[] asByteArray(Instance inst) {
+    if (! (inst instanceof ArrayInstance)) {
+      return null;
+    }
+
+    ArrayInstance array = (ArrayInstance)inst;
+    if (array.getArrayType() != Type.BYTE) {
+      return null;
+    }
+
+    Object[] objs = array.getValues();
+    byte[] bytes = new byte[objs.length];
+    for (int i = 0; i < objs.length; i++) {
+      Byte b = (Byte)objs[i];
+      bytes[i] = b.byteValue();
+    }
+    return bytes;
+  }
+
+
+  // Read the string value from an hprof Instance.
+  // Returns null if the object can't be interpreted as a string.
+  public static String asString(Instance inst) {
+    if (!isInstanceOfClass(inst, "java.lang.String")) {
+      return null;
+    }
+    char[] value = getCharArrayField(inst, "value");
+    return (value == null) ? null : new String(value);
+  }
+
+  /**
+   * Read the bitmap data for the given android.graphics.Bitmap object.
+   * Returns null if the object isn't for android.graphics.Bitmap or the
+   * bitmap data couldn't be read.
+   */
+  public static BufferedImage asBitmap(Instance inst) {
+    if (!isInstanceOfClass(inst, "android.graphics.Bitmap")) {
+      return null;
+    }
+
+    Integer width = getIntField(inst, "mWidth");
+    if (width == null) {
+      return null;
+    }
+
+    Integer height = getIntField(inst, "mHeight");
+    if (height == null) {
+      return null;
+    }
+
+    byte[] buffer = getByteArrayField(inst, "mBuffer");
+    if (buffer == null) {
+      return null;
+    }
+
+    // Convert the raw data to an image
+    // Convert BGRA to ABGR
+    int[] abgr = new int[height * width];
+    for (int i = 0; i < abgr.length; i++) {
+      abgr[i] = (
+          (((int)buffer[i * 4 + 3] & 0xFF) << 24) +
+          (((int)buffer[i * 4 + 0] & 0xFF) << 16) +
+          (((int)buffer[i * 4 + 1] & 0xFF) << 8) +
+          ((int)buffer[i * 4 + 2] & 0xFF));
+    }
+
+    BufferedImage bitmap = new BufferedImage(
+        width, height, BufferedImage.TYPE_4BYTE_ABGR);
+    bitmap.setRGB(0, 0, width, height, abgr, 0, width);
+    return bitmap;
+  }
+
+  /**
+   * Read a field of an instance.
+   * Returns null if the field value is null or if the field couldn't be read.
+   */
+  private static Object getField(Instance inst, String fieldName) {
+    if (!(inst instanceof ClassInstance)) {
+      return null;
+    }
+
+    ClassInstance clsinst = (ClassInstance) inst;
+    Object value = null;
+    int count = 0;
+    for (ClassInstance.FieldValue field : clsinst.getValues()) {
+      if (fieldName.equals(field.getField().getName())) {
+        value = field.getValue();
+        count++;
+      }
+    }
+    return count == 1 ? value : null;
+  }
+
+  /**
+   * Read a reference field of an instance.
+   * Returns null if the field value is null, or if the field couldn't be read.
+   */
+  private static Instance getRefField(Instance inst, String fieldName) {
+    Object value = getField(inst, fieldName);
+    if (!(value instanceof Instance)) {
+      return null;
+    }
+    return (Instance)value;
+  }
+
+  /**
+   * Read an int field of an instance.
+   * The field is assumed to be an int type.
+   * Returns null if the field value is not an int or could not be read.
+   */
+  private static Integer getIntField(Instance inst, String fieldName) {
+    Object value = getField(inst, fieldName);
+    if (!(value instanceof Integer)) {
+      return null;
+    }
+    return (Integer)value;
+  }
+
+  /**
+   * Read the given field from the given instance.
+   * The field is assumed to be a byte[] field.
+   * Returns null if the field value is null, not a byte[] or could not be read.
+   */
+  private static byte[] getByteArrayField(Instance inst, String fieldName) {
+    Object value = getField(inst, fieldName);
+    if (!(value instanceof Instance)) {
+      return null;
+    }
+    return asByteArray((Instance)value);
+  }
+
+  private static char[] getCharArrayField(Instance inst, String fieldName) {
+    Object value = getField(inst, fieldName);
+    if (!(value instanceof Instance)) {
+      return null;
+    }
+    return asCharArray((Instance)value);
+  }
+
+  // Return the bitmap instance associated with this object, or null if there
+  // is none. This works for android.graphics.Bitmap instances and their
+  // underlying Byte[] instances.
+  public static Instance getAssociatedBitmapInstance(Instance inst) {
+    ClassObj cls = inst.getClassObj();
+    if (cls == null) {
+      return null;
+    }
+
+    if ("android.graphics.Bitmap".equals(cls.getClassName())) {
+      return inst;
+    }
+
+    if (inst instanceof ArrayInstance) {
+      ArrayInstance array = (ArrayInstance)inst;
+      if (array.getArrayType() == Type.BYTE && inst.getHardReferences().size() == 1) {
+        Instance ref = inst.getHardReferences().get(0);
+        ClassObj clsref = ref.getClassObj();
+        if (clsref != null && "android.graphics.Bitmap".equals(clsref.getClassName())) {
+          return ref;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Assuming inst represents a DexCache object, return the dex location for
+   * that dex cache. Returns null if the given instance doesn't represent a
+   * DexCache object or the location could not be found.
+   */
+  public static String getDexCacheLocation(Instance inst) {
+    if (isInstanceOfClass(inst, "java.lang.DexCache")) {
+      Instance location = getRefField(inst, "location");
+      if (location != null) {
+        return asString(location);
+      }
+    }
+    return null;
+  }
+}
diff --git a/tools/ahat/src/Main.java b/tools/ahat/src/Main.java
new file mode 100644
index 0000000..2e2ddd2
--- /dev/null
+++ b/tools/ahat/src/Main.java
@@ -0,0 +1,100 @@
+/*
+ * 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.tools.perflib.heap.HprofParser;
+import com.android.tools.perflib.heap.Snapshot;
+import com.android.tools.perflib.heap.io.HprofBuffer;
+import com.android.tools.perflib.heap.io.MemoryMappedFileBuffer;
+import com.sun.net.httpserver.HttpServer;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executors;
+
+public class Main {
+
+  public static void help(PrintStream out) {
+    out.println("java -jar ahat.jar [-p port] FILE");
+    out.println("  Launch an http server for viewing "
+        + "the given Android heap-dump FILE.");
+    out.println("");
+    out.println("Options:");
+    out.println("  -p <port>");
+    out.println("     Serve pages on the given port. Defaults to 7100.");
+    out.println("");
+  }
+
+  public static void main(String[] args) throws IOException {
+    int port = 7100;
+    for (String arg : args) {
+      if (arg.equals("--help")) {
+        help(System.out);
+        return;
+      }
+    }
+
+    File hprof = null;
+    for (int i = 0; i < args.length; i++) {
+      if ("-p".equals(args[i]) && i + 1 < args.length) {
+        i++;
+        port = Integer.parseInt(args[i]);
+      } else {
+        if (hprof != null) {
+          System.err.println("multiple input files.");
+          help(System.err);
+          return;
+        }
+        hprof = new File(args[i]);
+      }
+    }
+
+    if (hprof == null) {
+      System.err.println("no input file.");
+      help(System.err);
+      return;
+    }
+
+    System.out.println("Reading hprof file...");
+    HprofBuffer buffer = new MemoryMappedFileBuffer(hprof);
+    Snapshot snapshot = (new HprofParser(buffer)).parse();
+
+    System.out.println("Computing Dominators...");
+    snapshot.computeDominators();
+
+    System.out.println("Processing snapshot for ahat...");
+    AhatSnapshot ahat = new AhatSnapshot(snapshot);
+
+    InetAddress loopback = InetAddress.getLoopbackAddress();
+    InetSocketAddress addr = new InetSocketAddress(loopback, port);
+    HttpServer server = HttpServer.create(addr, 0);
+    server.createContext("/", new OverviewHandler(ahat, hprof));
+    server.createContext("/roots", new RootsHandler(ahat));
+    server.createContext("/object", new ObjectHandler(ahat));
+    server.createContext("/objects", new ObjectsHandler(ahat));
+    server.createContext("/site", new SiteHandler(ahat));
+    server.createContext("/bitmap", new BitmapHandler(ahat));
+    server.createContext("/help", new StaticHandler("help.html", "text/html"));
+    server.createContext("/style.css", new StaticHandler("style.css", "text/css"));
+    server.setExecutor(Executors.newFixedThreadPool(1));
+    System.out.println("Server started on localhost:" + port);
+    server.start();
+  }
+}
+
diff --git a/tools/ahat/src/ObjectHandler.java b/tools/ahat/src/ObjectHandler.java
new file mode 100644
index 0000000..eecd7d1
--- /dev/null
+++ b/tools/ahat/src/ObjectHandler.java
@@ -0,0 +1,209 @@
+/*
+ * 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.tools.perflib.heap.ArrayInstance;
+import com.android.tools.perflib.heap.ClassInstance;
+import com.android.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Field;
+import com.android.tools.perflib.heap.Heap;
+import com.android.tools.perflib.heap.Instance;
+import com.android.tools.perflib.heap.RootObj;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+class ObjectHandler extends AhatHandler {
+  public ObjectHandler(AhatSnapshot snapshot) {
+    super(snapshot);
+  }
+
+  @Override
+  public void handle(Doc doc, Query query) throws IOException {
+    long id = query.getLong("id", 0);
+    Instance inst = mSnapshot.findInstance(id);
+    if (inst == null) {
+      doc.println(DocString.text("No object with id %08xl", id));
+      return;
+    }
+
+    doc.title("Object %08x", inst.getUniqueId());
+    doc.big(Value.render(inst));
+
+    printAllocationSite(doc, inst);
+    printDominatorPath(doc, inst);
+
+    doc.section("Object Info");
+    ClassObj cls = inst.getClassObj();
+    doc.descriptions();
+    doc.description(DocString.text("Class"), Value.render(cls));
+    doc.description(DocString.text("Size"), DocString.text("%d", inst.getSize()));
+    doc.description(
+        DocString.text("Retained Size"),
+        DocString.text("%d", inst.getTotalRetainedSize()));
+    doc.description(DocString.text("Heap"), DocString.text(inst.getHeap().getName()));
+    doc.end();
+
+    printBitmap(doc, inst);
+    if (inst instanceof ClassInstance) {
+      printClassInstanceFields(doc, (ClassInstance)inst);
+    } else if (inst instanceof ArrayInstance) {
+      printArrayElements(doc, (ArrayInstance)inst);
+    } else if (inst instanceof ClassObj) {
+      printClassInfo(doc, (ClassObj)inst);
+    }
+    printReferences(doc, inst);
+    printDominatedObjects(doc, query, inst);
+  }
+
+  private static void printClassInstanceFields(Doc doc, ClassInstance inst) {
+    doc.section("Fields");
+    doc.table(new Column("Type"), new Column("Name"), new Column("Value"));
+    for (ClassInstance.FieldValue field : inst.getValues()) {
+      doc.row(
+          DocString.text(field.getField().getType().toString()),
+          DocString.text(field.getField().getName()),
+          Value.render(field.getValue()));
+    }
+    doc.end();
+  }
+
+  private static void printArrayElements(Doc doc, ArrayInstance array) {
+    doc.section("Array Elements");
+    doc.table(new Column("Index", Column.Align.RIGHT), new Column("Value"));
+    Object[] elements = array.getValues();
+    for (int i = 0; i < elements.length; i++) {
+      doc.row(DocString.text("%d", i), Value.render(elements[i]));
+    }
+    doc.end();
+  }
+
+  private static void printClassInfo(Doc doc, ClassObj clsobj) {
+    doc.section("Class Info");
+    doc.descriptions();
+    doc.description(DocString.text("Super Class"), Value.render(clsobj.getSuperClassObj()));
+    doc.description(DocString.text("Class Loader"), Value.render(clsobj.getClassLoader()));
+    doc.end();
+
+    doc.section("Static Fields");
+    doc.table(new Column("Type"), new Column("Name"), new Column("Value"));
+    for (Map.Entry<Field, Object> field : clsobj.getStaticFieldValues().entrySet()) {
+      doc.row(
+          DocString.text(field.getKey().getType().toString()),
+          DocString.text(field.getKey().getName()),
+          Value.render(field.getValue()));
+    }
+    doc.end();
+  }
+
+  private static void printReferences(Doc doc, Instance inst) {
+    doc.section("Objects with References to this Object");
+    if (inst.getHardReferences().isEmpty()) {
+      doc.println(DocString.text("(none)"));
+    } else {
+      doc.table(new Column("Object"));
+      for (Instance ref : inst.getHardReferences()) {
+        doc.row(Value.render(ref));
+      }
+      doc.end();
+    }
+
+    if (inst.getSoftReferences() != null) {
+      doc.section("Objects with Soft References to this Object");
+      doc.table(new Column("Object"));
+      for (Instance ref : inst.getSoftReferences()) {
+        doc.row(Value.render(inst));
+      }
+      doc.end();
+    }
+  }
+
+  private void printAllocationSite(Doc doc, Instance inst) {
+    doc.section("Allocation Site");
+    Site site = mSnapshot.getSiteForInstance(inst);
+    SitePrinter.printSite(doc, mSnapshot, site);
+  }
+
+  // Draw the bitmap corresponding to this instance if there is one.
+  private static void printBitmap(Doc doc, Instance inst) {
+    Instance bitmap = InstanceUtils.getAssociatedBitmapInstance(inst);
+    if (bitmap != null) {
+      doc.section("Bitmap Image");
+      doc.println(DocString.image(
+            DocString.uri("bitmap?id=%d", bitmap.getId()), "bitmap image"));
+    }
+  }
+
+  private void printDominatorPath(Doc doc, Instance inst) {
+    doc.section("Dominator Path from Root");
+    List<Instance> path = new ArrayList<Instance>();
+    for (Instance parent = inst;
+        parent != null && !(parent instanceof RootObj);
+        parent = parent.getImmediateDominator()) {
+      path.add(parent);
+    }
+
+    // Add 'null' as a marker for the root.
+    path.add(null);
+    Collections.reverse(path);
+
+    HeapTable.TableConfig<Instance> table = new HeapTable.TableConfig<Instance>() {
+      public String getHeapsDescription() {
+        return "Bytes Retained by Heap";
+      }
+
+      public long getSize(Instance element, Heap heap) {
+        if (element == null) {
+          return mSnapshot.getHeapSize(heap);
+        }
+        int index = mSnapshot.getHeapIndex(heap);
+        return element.getRetainedSize(index);
+      }
+
+      public List<HeapTable.ValueConfig<Instance>> getValueConfigs() {
+        HeapTable.ValueConfig<Instance> value = new HeapTable.ValueConfig<Instance>() {
+          public String getDescription() {
+            return "Object";
+          }
+
+          public DocString render(Instance element) {
+            if (element == null) {
+              return DocString.link(DocString.uri("roots"), DocString.text("ROOT"));
+            } else {
+              return DocString.text("→ ").append(Value.render(element));
+            }
+          }
+        };
+        return Collections.singletonList(value);
+      }
+    };
+    HeapTable.render(doc, table, mSnapshot, path);
+  }
+
+  public void printDominatedObjects(Doc doc, Query query, Instance inst) {
+    doc.section("Immediately Dominated Objects");
+    List<Instance> instances = mSnapshot.getDominated(inst);
+    if (instances != null) {
+      DominatedList.render(mSnapshot, doc, instances, query);
+    } else {
+      doc.println(DocString.text("(none)"));
+    }
+  }
+}
+
diff --git a/tools/ahat/src/ObjectsHandler.java b/tools/ahat/src/ObjectsHandler.java
new file mode 100644
index 0000000..066c9d5
--- /dev/null
+++ b/tools/ahat/src/ObjectsHandler.java
@@ -0,0 +1,63 @@
+/*
+ * 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.tools.perflib.heap.Instance;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class ObjectsHandler extends AhatHandler {
+  public ObjectsHandler(AhatSnapshot snapshot) {
+    super(snapshot);
+  }
+
+  @Override
+  public void handle(Doc doc, Query query) throws IOException {
+    int stackId = query.getInt("stack", 0);
+    int depth = query.getInt("depth", 0);
+    String className = query.get("class", null);
+    String heapName = query.get("heap", null);
+    Site site = mSnapshot.getSite(stackId, depth);
+
+    List<Instance> insts = new ArrayList<Instance>();
+    for (Instance inst : site.getObjects()) {
+      if ((heapName == null || inst.getHeap().getName().equals(heapName))
+          && (className == null
+            || AhatSnapshot.getClassName(inst.getClassObj()).equals(className))) {
+        insts.add(inst);
+      }
+    }
+
+    Collections.sort(insts, Sort.defaultInstanceCompare(mSnapshot));
+
+    doc.title("Objects");
+    doc.table(
+        new Column("Size", Column.Align.RIGHT),
+        new Column("Heap"),
+        new Column("Object"));
+    for (Instance inst : insts) {
+      doc.row(
+          DocString.text("%,d", inst.getSize()),
+          DocString.text(inst.getHeap().getName()),
+          Value.render(inst));
+    }
+    doc.end();
+  }
+}
+
diff --git a/tools/ahat/src/OverviewHandler.java b/tools/ahat/src/OverviewHandler.java
new file mode 100644
index 0000000..6e6c323
--- /dev/null
+++ b/tools/ahat/src/OverviewHandler.java
@@ -0,0 +1,76 @@
+/*
+ * 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.tools.perflib.heap.Heap;
+import java.io.IOException;
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+class OverviewHandler extends AhatHandler {
+  private File mHprof;
+
+  public OverviewHandler(AhatSnapshot snapshot, File hprof) {
+    super(snapshot);
+    mHprof = hprof;
+  }
+
+  @Override
+  public void handle(Doc doc, Query query) throws IOException {
+    doc.title("Overview");
+
+    doc.section("General Information");
+    doc.descriptions();
+    doc.description(
+        DocString.text("ahat version"),
+        DocString.text("ahat-%s", OverviewHandler.class.getPackage().getImplementationVersion()));
+    doc.description(DocString.text("hprof file"), DocString.text(mHprof.toString()));
+    doc.end();
+
+    doc.section("Heap Sizes");
+    printHeapSizes(doc);
+
+    DocString menu = new DocString();
+    menu.appendLink(DocString.uri("roots"), DocString.text("Roots"));
+    menu.append(" - ");
+    menu.appendLink(DocString.uri("site"), DocString.text("Allocations"));
+    menu.append(" - ");
+    menu.appendLink(DocString.uri("help"), DocString.text("Help"));
+    doc.big(menu);
+  }
+
+  private void printHeapSizes(Doc doc) {
+    List<Object> dummy = Collections.singletonList(null);
+
+    HeapTable.TableConfig<Object> table = new HeapTable.TableConfig<Object>() {
+      public String getHeapsDescription() {
+        return "Bytes Retained by Heap";
+      }
+
+      public long getSize(Object element, Heap heap) {
+        return mSnapshot.getHeapSize(heap);
+      }
+
+      public List<HeapTable.ValueConfig<Object>> getValueConfigs() {
+        return Collections.emptyList();
+      }
+    };
+    HeapTable.render(doc, table, mSnapshot, dummy);
+  }
+}
+
diff --git a/tools/ahat/src/Query.java b/tools/ahat/src/Query.java
new file mode 100644
index 0000000..f910608
--- /dev/null
+++ b/tools/ahat/src/Query.java
@@ -0,0 +1,111 @@
+/*
+ * 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 java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A class for getting and modifying query parameters.
+ */
+class Query {
+  private URI mUri;
+
+  // Map from parameter name to value. If the same parameter appears multiple
+  // times, only the last value is used.
+  private Map<String, String> mParams;
+
+  public Query(URI uri) {
+    mUri = uri;
+    mParams = new HashMap<String, String>();
+
+    String query = uri.getQuery();
+    if (query != null) {
+      for (String param : query.split("&")) {
+        int i = param.indexOf('=');
+        if (i < 0) {
+          mParams.put(param, "");
+        } else {
+          mParams.put(param.substring(0, i), param.substring(i + 1));
+        }
+      }
+    }
+  }
+
+  /**
+   * Return the value of a query parameter with the given name.
+   * If there is no query parameter with that name, returns the default value.
+   * If there are multiple query parameters with that name, the value of the
+   * last query parameter is returned.
+   * If the parameter is defined with an empty value, "" is returned.
+   */
+  public String get(String name, String defaultValue) {
+    String value = mParams.get(name);
+    return (value == null) ? defaultValue : value;
+  }
+
+  /**
+   * Return the long value of a query parameter with the given name.
+   */
+  public long getLong(String name, long defaultValue) {
+    String value = get(name, null);
+    return value == null ? defaultValue : Long.parseLong(value);
+  }
+
+  /**
+   * Return the int value of a query parameter with the given name.
+   */
+  public int getInt(String name, int defaultValue) {
+    String value = get(name, null);
+    return value == null ? defaultValue : Integer.parseInt(value);
+  }
+
+  /**
+   * Return a uri suitable for an href target that links to the current
+   * page, except with the named query parameter set to the new value.
+   *
+   * The generated parameters will be sorted alphabetically so it is easier to
+   * test.
+   */
+  public URI with(String name, String value) {
+    StringBuilder newQuery = new StringBuilder();
+    newQuery.append(mUri.getRawPath());
+    newQuery.append('?');
+
+    Map<String, String> params = new TreeMap<String, String>(mParams);
+    params.put(name, value);
+    String and = "";
+    for (Map.Entry<String, String> entry : params.entrySet()) {
+      newQuery.append(and);
+      newQuery.append(entry.getKey());
+      newQuery.append('=');
+      newQuery.append(entry.getValue());
+      and = "&";
+    }
+    return DocString.uri(newQuery.toString());
+  }
+
+  /**
+   * Return a uri suitable for an href target that links to the current
+   * page, except with the named query parameter set to the new long value.
+   */
+  public URI with(String name, long value) {
+    return with(name, String.valueOf(value));
+  }
+}
diff --git a/tools/ahat/src/RootsHandler.java b/tools/ahat/src/RootsHandler.java
new file mode 100644
index 0000000..185b9bf
--- /dev/null
+++ b/tools/ahat/src/RootsHandler.java
@@ -0,0 +1,51 @@
+/*
+ * 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.tools.perflib.heap.Instance;
+import com.android.tools.perflib.heap.RootObj;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+class RootsHandler extends AhatHandler {
+  public RootsHandler(AhatSnapshot snapshot) {
+    super(snapshot);
+  }
+
+  @Override
+  public void handle(Doc doc, Query query) throws IOException {
+    doc.title("Roots");
+
+    Set<Instance> rootset = new HashSet<Instance>();
+    for (RootObj root : mSnapshot.getGCRoots()) {
+      Instance inst = root.getReferredInstance();
+      if (inst != null) {
+        rootset.add(inst);
+      }
+    }
+
+    List<Instance> roots = new ArrayList<Instance>();
+    for (Instance inst : rootset) {
+      roots.add(inst);
+    }
+    DominatedList.render(mSnapshot, doc, roots, query);
+  }
+}
+
diff --git a/tools/ahat/src/Site.java b/tools/ahat/src/Site.java
new file mode 100644
index 0000000..d504096
--- /dev/null
+++ b/tools/ahat/src/Site.java
@@ -0,0 +1,198 @@
+/*
+ * 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.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Heap;
+import com.android.tools.perflib.heap.Instance;
+import com.android.tools.perflib.heap.StackFrame;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+class Site {
+  // The site that this site was directly called from.
+  // mParent is null for the root site.
+  private Site mParent;
+
+  // A description of the Site. Currently this is used to uniquely identify a
+  // site within its parent.
+  private String mName;
+
+  // To identify this site, we pick one stack trace where we have seen the
+  // site. mStackId is the id for that stack trace, and mStackDepth is the
+  // depth of this site in that stack trace.
+  // For the root site, mStackId is 0 and mStackDepth is 0.
+  private int mStackId;
+  private int mStackDepth;
+
+  // Mapping from heap name to the total size of objects allocated in this
+  // site (including child sites) on the given heap.
+  private Map<String, Long> mSizesByHeap;
+
+  // Mapping from child site name to child site.
+  private Map<String, Site> mChildren;
+
+  // List of all objects allocated in this site (including child sites).
+  private List<Instance> mObjects;
+  private List<ObjectsInfo> mObjectsInfos;
+  private Map<Heap, Map<ClassObj, ObjectsInfo>> mObjectsInfoMap;
+
+  public static class ObjectsInfo {
+    public Heap heap;
+    public ClassObj classObj;
+    public long numInstances;
+    public long numBytes;
+
+    public ObjectsInfo(Heap heap, ClassObj classObj, long numInstances, long numBytes) {
+      this.heap = heap;
+      this.classObj = classObj;
+      this.numInstances = numInstances;
+      this.numBytes = numBytes;
+    }
+  }
+
+  /**
+   * Construct a root site.
+   */
+  public Site(String name) {
+    this(null, name, 0, 0);
+  }
+
+  public Site(Site parent, String name, int stackId, int stackDepth) {
+    mParent = parent;
+    mName = name;
+    mStackId = stackId;
+    mStackDepth = stackDepth;
+    mSizesByHeap = new HashMap<String, Long>();
+    mChildren = new HashMap<String, Site>();
+    mObjects = new ArrayList<Instance>();
+    mObjectsInfos = new ArrayList<ObjectsInfo>();
+    mObjectsInfoMap = new HashMap<Heap, Map<ClassObj, ObjectsInfo>>();
+  }
+
+  /**
+   * Add an instance to this site.
+   * Returns the site at which the instance was allocated.
+   */
+  public Site add(int stackId, int stackDepth, Iterator<StackFrame> path, Instance inst) {
+    mObjects.add(inst);
+
+    String heap = inst.getHeap().getName();
+    mSizesByHeap.put(heap, getSize(heap) + inst.getSize());
+
+    Map<ClassObj, ObjectsInfo> classToObjectsInfo = mObjectsInfoMap.get(inst.getHeap());
+    if (classToObjectsInfo == null) {
+      classToObjectsInfo = new HashMap<ClassObj, ObjectsInfo>();
+      mObjectsInfoMap.put(inst.getHeap(), classToObjectsInfo);
+    }
+
+    ObjectsInfo info = classToObjectsInfo.get(inst.getClassObj());
+    if (info == null) {
+      info = new ObjectsInfo(inst.getHeap(), inst.getClassObj(), 0, 0);
+      mObjectsInfos.add(info);
+      classToObjectsInfo.put(inst.getClassObj(), info);
+    }
+
+    info.numInstances++;
+    info.numBytes += inst.getSize();
+
+    if (path.hasNext()) {
+      String next = path.next().toString();
+      Site child = mChildren.get(next);
+      if (child == null) {
+        child = new Site(this, next, stackId, stackDepth + 1);
+        mChildren.put(next, child);
+      }
+      return child.add(stackId, stackDepth + 1, path, inst);
+    } else {
+      return this;
+    }
+  }
+
+  // Get the size of a site for a specific heap.
+  public long getSize(String heap) {
+    Long val = mSizesByHeap.get(heap);
+    if (val == null) {
+      return 0;
+    }
+    return val;
+  }
+
+  /**
+   * Get the list of objects allocated under this site. Includes objects
+   * allocated in children sites.
+   */
+  public Collection<Instance> getObjects() {
+    return mObjects;
+  }
+
+  public List<ObjectsInfo> getObjectsInfos() {
+    return mObjectsInfos;
+  }
+
+  // Get the combined size of the site for all heaps.
+  public long getTotalSize() {
+    long size = 0;
+    for (Long val : mSizesByHeap.values()) {
+      size += val;
+    }
+    return size;
+  }
+
+  /**
+   * Return the site this site was called from.
+   * Returns null for the root site.
+   */
+  public Site getParent() {
+    return mParent;
+  }
+
+  public String getName() {
+    return mName;
+  }
+
+  // Returns the hprof id of a stack this site appears on.
+  public int getStackId() {
+    return mStackId;
+  }
+
+  // Returns the stack depth of this site in the stack whose id is returned
+  // by getStackId().
+  public int getStackDepth() {
+    return mStackDepth;
+  }
+
+  List<Site> getChildren() {
+    return new ArrayList<Site>(mChildren.values());
+  }
+
+  // Get the child at the given path relative to this site.
+  // Returns null if no such child found.
+  Site getChild(Iterator<StackFrame> path) {
+    if (path.hasNext()) {
+      String next = path.next().toString();
+      Site child = mChildren.get(next);
+      return (child == null) ? null : child.getChild(path);
+    } else {
+      return this;
+    }
+  }
+}
diff --git a/tools/ahat/src/SiteHandler.java b/tools/ahat/src/SiteHandler.java
new file mode 100644
index 0000000..8fbc176
--- /dev/null
+++ b/tools/ahat/src/SiteHandler.java
@@ -0,0 +1,101 @@
+/*
+ * 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.tools.perflib.heap.Heap;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class SiteHandler extends AhatHandler {
+  public SiteHandler(AhatSnapshot snapshot) {
+    super(snapshot);
+  }
+
+  @Override
+  public void handle(Doc doc, Query query) throws IOException {
+    int stackId = query.getInt("stack", 0);
+    int depth = query.getInt("depth", -1);
+    Site site = mSnapshot.getSite(stackId, depth);
+
+    doc.title("Site %s", site.getName());
+    doc.section("Allocation Site");
+    SitePrinter.printSite(doc, mSnapshot, site);
+
+    doc.section("Sites Called from Here");
+    List<Site> children = site.getChildren();
+    if (children.isEmpty()) {
+      doc.println(DocString.text("(none)"));
+    } else {
+      Collections.sort(children, new Sort.SiteBySize("app"));
+
+      HeapTable.TableConfig<Site> table = new HeapTable.TableConfig<Site>() {
+        public String getHeapsDescription() {
+          return "Reachable Bytes Allocated on Heap";
+        }
+
+        public long getSize(Site element, Heap heap) {
+          return element.getSize(heap.getName());
+        }
+
+        public List<HeapTable.ValueConfig<Site>> getValueConfigs() {
+          HeapTable.ValueConfig<Site> value = new HeapTable.ValueConfig<Site>() {
+            public String getDescription() {
+              return "Child Site";
+            }
+
+            public DocString render(Site element) {
+              return DocString.link(
+                  DocString.uri("site?stack=%d&depth=%d",
+                    element.getStackId(), element.getStackDepth()),
+                  DocString.text(element.getName()));
+            }
+          };
+          return Collections.singletonList(value);
+        }
+      };
+      HeapTable.render(doc, table, mSnapshot, children);
+    }
+
+    doc.section("Objects Allocated");
+    doc.table(
+        new Column("Reachable Bytes Allocated", Column.Align.RIGHT),
+        new Column("Instances", Column.Align.RIGHT),
+        new Column("Heap"),
+        new Column("Class"));
+    List<Site.ObjectsInfo> infos = site.getObjectsInfos();
+    Comparator<Site.ObjectsInfo> compare = new Sort.WithPriority<Site.ObjectsInfo>(
+        new Sort.ObjectsInfoByHeapName(),
+        new Sort.ObjectsInfoBySize(),
+        new Sort.ObjectsInfoByClassName());
+    Collections.sort(infos, compare);
+    for (Site.ObjectsInfo info : infos) {
+      String className = AhatSnapshot.getClassName(info.classObj);
+      doc.row(
+          DocString.text("%,14d", info.numBytes),
+          DocString.link(
+            DocString.uri("objects?stack=%d&depth=%d&heap=%s&class=%s",
+                site.getStackId(), site.getStackDepth(), info.heap.getName(), className),
+            DocString.text("%,14d", info.numInstances)),
+          DocString.text(info.heap.getName()),
+          Value.render(info.classObj));
+    }
+    doc.end();
+  }
+}
+
diff --git a/tools/ahat/src/SitePrinter.java b/tools/ahat/src/SitePrinter.java
new file mode 100644
index 0000000..9c0c2e0
--- /dev/null
+++ b/tools/ahat/src/SitePrinter.java
@@ -0,0 +1,65 @@
+/*
+ * 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.tools.perflib.heap.Heap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class SitePrinter {
+  public static void printSite(Doc doc, AhatSnapshot snapshot, Site site) {
+    List<Site> path = new ArrayList<Site>();
+    for (Site parent = site; parent != null; parent = parent.getParent()) {
+      path.add(parent);
+    }
+    Collections.reverse(path);
+
+
+    HeapTable.TableConfig<Site> table = new HeapTable.TableConfig<Site>() {
+      public String getHeapsDescription() {
+        return "Reachable Bytes Allocated on Heap";
+      }
+
+      public long getSize(Site element, Heap heap) {
+        return element.getSize(heap.getName());
+      }
+
+      public List<HeapTable.ValueConfig<Site>> getValueConfigs() {
+        HeapTable.ValueConfig<Site> value = new HeapTable.ValueConfig<Site>() {
+          public String getDescription() {
+            return "Stack Frame";
+          }
+
+          public DocString render(Site element) {
+            DocString str = new DocString();
+            if (element.getParent() != null) {
+              str.append("→ ");
+            }
+            str.appendLink(
+                DocString.uri("site?stack=%d&depth=%d",
+                    element.getStackId(), element.getStackDepth()),
+                DocString.text(element.getName()));
+            return str;
+          }
+        };
+        return Collections.singletonList(value);
+      }
+    };
+    HeapTable.render(doc, table, snapshot, path);
+  }
+}
diff --git a/tools/ahat/src/Sort.java b/tools/ahat/src/Sort.java
new file mode 100644
index 0000000..3b79166
--- /dev/null
+++ b/tools/ahat/src/Sort.java
@@ -0,0 +1,181 @@
+/*
+ * 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.tools.perflib.heap.Instance;
+import com.android.tools.perflib.heap.Heap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Iterator;
+
+/**
+ * Provides Comparators and helper functions for sorting Instances, Sites, and
+ * other things.
+ *
+ * Note: The Comparators defined here impose orderings that are inconsistent
+ * with equals. They should not be used for element lookup or search. They
+ * should only be used for showing elements to the user in different orders.
+ */
+class Sort {
+  /**
+   * Compare instances by their instance id.
+   * This sorts instances from smaller id to larger id.
+   */
+  public static class InstanceById implements Comparator<Instance> {
+    @Override
+    public int compare(Instance a, Instance b) {
+      return Long.compare(a.getId(), b.getId());
+    }
+  }
+
+  /**
+   * Compare instances by their total retained size.
+   * Different instances with the same total retained size are considered
+   * equal for the purposes of comparison.
+   * This sorts instances from larger retained size to smaller retained size.
+   */
+  public static class InstanceByTotalRetainedSize implements Comparator<Instance> {
+    @Override
+    public int compare(Instance a, Instance b) {
+      return Long.compare(b.getTotalRetainedSize(), a.getTotalRetainedSize());
+    }
+  }
+
+  /**
+   * Compare instances by their retained size for a given heap index.
+   * Different instances with the same total retained size are considered
+   * equal for the purposes of comparison.
+   * This sorts instances from larger retained size to smaller retained size.
+   */
+  public static class InstanceByHeapRetainedSize implements Comparator<Instance> {
+    private int mIndex;
+
+    public InstanceByHeapRetainedSize(AhatSnapshot snapshot, Heap heap) {
+      mIndex = snapshot.getHeapIndex(heap);
+    }
+
+    public InstanceByHeapRetainedSize(int heapIndex) {
+      mIndex = heapIndex;
+    }
+
+    @Override
+    public int compare(Instance a, Instance b) {
+      return Long.compare(b.getRetainedSize(mIndex), a.getRetainedSize(mIndex));
+    }
+  }
+
+  /**
+   * Compare objects based on a list of comparators, giving priority to the
+   * earlier comparators in the list.
+   */
+  public static class WithPriority<T> implements Comparator<T> {
+    private List<Comparator<T>> mComparators;
+
+    public WithPriority(Comparator<T>... comparators) {
+      mComparators = Arrays.asList(comparators);
+    }
+
+    public WithPriority(List<Comparator<T>> comparators) {
+      mComparators = comparators;
+    }
+
+    @Override
+    public int compare(T a, T b) {
+      int res = 0;
+      Iterator<Comparator<T>> iter = mComparators.iterator();
+      while (res == 0 && iter.hasNext()) {
+        res = iter.next().compare(a, b);
+      }
+      return res;
+    }
+  }
+
+  public static Comparator<Instance> defaultInstanceCompare(AhatSnapshot snapshot) {
+    List<Comparator<Instance>> comparators = new ArrayList<Comparator<Instance>>();
+
+    // Priority goes to the app heap, if we can find one.
+    Heap appHeap = snapshot.getHeap("app");
+    if (appHeap != null) {
+      comparators.add(new InstanceByHeapRetainedSize(snapshot, appHeap));
+    }
+
+    // Next is by total retained size.
+    comparators.add(new InstanceByTotalRetainedSize());
+    return new WithPriority<Instance>(comparators);
+  }
+
+  /**
+   * Compare Sites by the size of objects allocated on a given heap.
+   * Different object infos with the same size on the given heap are
+   * considered equal for the purposes of comparison.
+   * This sorts sites from larger size to smaller size.
+   */
+  public static class SiteBySize implements Comparator<Site> {
+    String mHeap;
+
+    public SiteBySize(String heap) {
+      mHeap = heap;
+    }
+
+    @Override
+    public int compare(Site a, Site b) {
+      return Long.compare(b.getSize(mHeap), a.getSize(mHeap));
+    }
+  }
+
+  /**
+   * Compare Site.ObjectsInfo by their size.
+   * Different object infos with the same total retained size are considered
+   * equal for the purposes of comparison.
+   * This sorts object infos from larger retained size to smaller size.
+   */
+  public static class ObjectsInfoBySize implements Comparator<Site.ObjectsInfo> {
+    @Override
+    public int compare(Site.ObjectsInfo a, Site.ObjectsInfo b) {
+      return Long.compare(b.numBytes, a.numBytes);
+    }
+  }
+
+  /**
+   * Compare Site.ObjectsInfo by heap name.
+   * Different object infos with the same heap name are considered equal for
+   * the purposes of comparison.
+   */
+  public static class ObjectsInfoByHeapName implements Comparator<Site.ObjectsInfo> {
+    @Override
+    public int compare(Site.ObjectsInfo a, Site.ObjectsInfo b) {
+      return a.heap.getName().compareTo(b.heap.getName());
+    }
+  }
+
+  /**
+   * Compare Site.ObjectsInfo by class name.
+   * Different object infos with the same class name are considered equal for
+   * the purposes of comparison.
+   */
+  public static class ObjectsInfoByClassName implements Comparator<Site.ObjectsInfo> {
+    @Override
+    public int compare(Site.ObjectsInfo a, Site.ObjectsInfo b) {
+      String aName = AhatSnapshot.getClassName(a.classObj);
+      String bName = AhatSnapshot.getClassName(b.classObj);
+      return aName.compareTo(bName);
+    }
+  }
+}
+
diff --git a/tools/ahat/src/StaticHandler.java b/tools/ahat/src/StaticHandler.java
new file mode 100644
index 0000000..fb7049d
--- /dev/null
+++ b/tools/ahat/src/StaticHandler.java
@@ -0,0 +1,56 @@
+/*
+ * 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.google.common.io.ByteStreams;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+// Handler that returns a static file included in ahat.jar.
+class StaticHandler implements HttpHandler {
+  private String mResourceName;
+  private String mContentType;
+
+  public StaticHandler(String resourceName, String contentType) {
+    mResourceName = resourceName;
+    mContentType = contentType;
+  }
+
+  @Override
+  public void handle(HttpExchange exchange) throws IOException {
+    ClassLoader loader = StaticHandler.class.getClassLoader();
+    InputStream is = loader.getResourceAsStream(mResourceName);
+    if (is == null) {
+      exchange.getResponseHeaders().add("Content-Type", "text/html");
+      exchange.sendResponseHeaders(404, 0);
+      PrintStream ps = new PrintStream(exchange.getResponseBody());
+      HtmlDoc doc = new HtmlDoc(ps, DocString.text("ahat"), DocString.uri("style.css"));
+      doc.big(DocString.text("Resource not found."));
+      doc.close();
+    } else {
+      exchange.getResponseHeaders().add("Content-Type", mContentType);
+      exchange.sendResponseHeaders(200, 0);
+      OutputStream os = exchange.getResponseBody();
+      ByteStreams.copy(is, os);
+      os.close();
+    }
+  }
+}
diff --git a/tools/ahat/src/Value.java b/tools/ahat/src/Value.java
new file mode 100644
index 0000000..22c3b8f
--- /dev/null
+++ b/tools/ahat/src/Value.java
@@ -0,0 +1,80 @@
+/*
+ * 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.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Instance;
+import java.net.URI;
+
+/**
+ * Class to render an hprof value to a DocString.
+ */
+class Value {
+
+  /**
+   * Create a DocString representing a summary of the given instance.
+   */
+  private static DocString renderInstance(Instance inst) {
+    DocString link = new DocString();
+    if (inst == null) {
+      link.append("(null)");
+      return link;
+    }
+
+    // Annotate classes as classes.
+    if (inst instanceof ClassObj) {
+      link.append("class ");
+    }
+
+    link.append(inst.toString());
+
+    // Annotate Strings with their values.
+    String stringValue = InstanceUtils.asString(inst);
+    if (stringValue != null) {
+      link.append("\"%s\"", stringValue);
+    }
+
+    // Annotate DexCache with its location.
+    String dexCacheLocation = InstanceUtils.getDexCacheLocation(inst);
+    if (dexCacheLocation != null) {
+      link.append(" for " + dexCacheLocation);
+    }
+
+    URI objTarget = DocString.uri("object?id=%d", inst.getId());
+    DocString formatted = DocString.link(objTarget, link);
+
+    // Annotate bitmaps with a thumbnail.
+    Instance bitmap = InstanceUtils.getAssociatedBitmapInstance(inst);
+    String thumbnail = "";
+    if (bitmap != null) {
+      URI uri = DocString.uri("bitmap?id=%d", bitmap.getId());
+      formatted.appendThumbnail(uri, "bitmap image");
+    }
+    return formatted;
+  }
+
+  /**
+   * Create a DocString summarizing the given value.
+   */
+  public static DocString render(Object val) {
+    if (val instanceof Instance) {
+      return renderInstance((Instance)val);
+    } else {
+      return DocString.text("%s", val);
+    }
+  }
+}
diff --git a/tools/ahat/src/help.html b/tools/ahat/src/help.html
new file mode 100644
index 0000000..b48d791
--- /dev/null
+++ b/tools/ahat/src/help.html
@@ -0,0 +1,56 @@
+<!--
+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.
+-->
+
+<head>
+<link rel="stylesheet" type="text/css" href="style.css">
+</head>
+
+<div class="menu">
+  <a href="/">overview</a> -
+  <a href="roots">roots</a> -
+  <a href="sites">allocations</a> -
+  <a href="help">help</a>
+</div>
+
+<h1>Help</h1>
+<h2>Information shown by ahat:</h2>
+<ul>
+  <li><a href="/">The total bytes retained by heap.</a></li>
+  <li><a href="/roots">A list of root objects and their retained sizes for each heap.</a></li>
+  <li>Information about each allocated object:
+    <ul>
+      <li>The allocation site (stack trace) of the object (if available).</li>
+      <li>The dominator path from a root to the object.</li>
+      <li>The class, (shallow) size, retained size, and heap of the object.</li>
+      <li>The bitmap image for the object if the object represents a bitmap.</li>
+      <li>The instance fields or array elements of the object.</li>
+      <li>The super class, class loader, and static fields of class objects.</li>
+      <li>Other objects with references to the object.</li>
+      <li>Other objects immediately dominated by the object.</li>
+    </ul>
+  </li>
+  <li>A list of objects, optionally filtered by class, allocation site, and/or
+    heap.</li>
+  <li><a href="site">Information about each allocation site:</a>
+    <ul>
+      <li>The stack trace for the allocation site.</li>
+      <li>The number of bytes allocated at the allocation site.</li>
+      <li>Child sites called from the allocation site.</li>
+      <li>The size and count of objects allocated at the site, organized by
+        heap and object type.</li>
+    </ul>
+  </li>
+</ul>
diff --git a/tools/ahat/src/manifest.txt b/tools/ahat/src/manifest.txt
new file mode 100644
index 0000000..7efb1a7
--- /dev/null
+++ b/tools/ahat/src/manifest.txt
@@ -0,0 +1,4 @@
+Name: ahat/
+Implementation-Title: ahat
+Implementation-Version: 0.2
+Main-Class: com.android.ahat.Main
diff --git a/tools/ahat/src/style.css b/tools/ahat/src/style.css
new file mode 100644
index 0000000..ca074a5
--- /dev/null
+++ b/tools/ahat/src/style.css
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+div.menu {
+  background-color: #eeffff;
+}
+
+/*
+ * Most of the columns show numbers of bytes. Numbers should be right aligned.
+ */
+table td {
+  background-color: #eeeeee;
+  padding-left: 4px;
+  padding-right: 4px;
+}
+
+table th {
+  padding-left: 8px;
+  padding-right: 8px;
+}
diff --git a/tools/ahat/test/QueryTest.java b/tools/ahat/test/QueryTest.java
new file mode 100644
index 0000000..40e3322
--- /dev/null
+++ b/tools/ahat/test/QueryTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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 java.net.URI;
+import java.net.URISyntaxException;
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+public class QueryTest {
+  @Test
+  public void simple() throws URISyntaxException {
+    String uri = "http://localhost:7100/object?foo=bar&answer=42";
+    Query query = new Query(new URI(uri));
+    assertEquals("bar", query.get("foo", "not found"));
+    assertEquals("42", query.get("answer", "not found"));
+    assertEquals(42, query.getLong("answer", 0));
+    assertEquals(42, query.getInt("answer", 0));
+    assertEquals("not found", query.get("bar", "not found"));
+    assertEquals("really not found", query.get("bar", "really not found"));
+    assertEquals(0, query.getLong("bar", 0));
+    assertEquals(0, query.getInt("bar", 0));
+    assertEquals(42, query.getLong("bar", 42));
+    assertEquals(42, query.getInt("bar", 42));
+    assertEquals("/object?answer=42&foo=sludge", query.with("foo", "sludge").toString());
+    assertEquals("/object?answer=43&foo=bar", query.with("answer", "43").toString());
+    assertEquals("/object?answer=43&foo=bar", query.with("answer", 43).toString());
+    assertEquals("/object?answer=42&bar=finally&foo=bar", query.with("bar", "finally").toString());
+  }
+
+  @Test
+  public void multiValue() throws URISyntaxException {
+    String uri = "http://localhost:7100/object?foo=bar&answer=42&foo=sludge";
+    Query query = new Query(new URI(uri));
+    assertEquals("sludge", query.get("foo", "not found"));
+    assertEquals(42, query.getLong("answer", 0));
+    assertEquals(42, query.getInt("answer", 0));
+    assertEquals("not found", query.get("bar", "not found"));
+    assertEquals("/object?answer=42&foo=tar", query.with("foo", "tar").toString());
+    assertEquals("/object?answer=43&foo=sludge", query.with("answer", "43").toString());
+    assertEquals("/object?answer=42&bar=finally&foo=sludge",
+        query.with("bar", "finally").toString());
+  }
+
+  @Test
+  public void empty() throws URISyntaxException {
+    String uri = "http://localhost:7100/object";
+    Query query = new Query(new URI(uri));
+    assertEquals("not found", query.get("foo", "not found"));
+    assertEquals(2, query.getLong("foo", 2));
+    assertEquals(2, query.getInt("foo", 2));
+    assertEquals("/object?foo=sludge", query.with("foo", "sludge").toString());
+    assertEquals("/object?answer=43", query.with("answer", "43").toString());
+  }
+}
diff --git a/tools/ahat/test/SortTest.java b/tools/ahat/test/SortTest.java
new file mode 100644
index 0000000..02ff7db
--- /dev/null
+++ b/tools/ahat/test/SortTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Heap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+public class SortTest {
+  @Test
+  public void objectsInfo() {
+    Heap heapA = new Heap(0xA, "A");
+    Heap heapB = new Heap(0xB, "B");
+    ClassObj classA = new ClassObj(0x1A, null, "classA", 0);
+    ClassObj classB = new ClassObj(0x1B, null, "classB", 0);
+    ClassObj classC = new ClassObj(0x1C, null, "classC", 0);
+    Site.ObjectsInfo infoA = new Site.ObjectsInfo(heapA, classA, 4, 14);
+    Site.ObjectsInfo infoB = new Site.ObjectsInfo(heapB, classB, 2, 15);
+    Site.ObjectsInfo infoC = new Site.ObjectsInfo(heapA, classC, 3, 13);
+    Site.ObjectsInfo infoD = new Site.ObjectsInfo(heapB, classA, 5, 12);
+    Site.ObjectsInfo infoE = new Site.ObjectsInfo(heapA, classB, 1, 11);
+    List<Site.ObjectsInfo> list = new ArrayList<Site.ObjectsInfo>();
+    list.add(infoA);
+    list.add(infoB);
+    list.add(infoC);
+    list.add(infoD);
+    list.add(infoE);
+
+    // Sort by size.
+    Collections.sort(list, new Sort.ObjectsInfoBySize());
+    assertEquals(infoB, list.get(0));
+    assertEquals(infoA, list.get(1));
+    assertEquals(infoC, list.get(2));
+    assertEquals(infoD, list.get(3));
+    assertEquals(infoE, list.get(4));
+
+    // Sort by class name.
+    Collections.sort(list, new Sort.ObjectsInfoByClassName());
+    assertEquals(classA, list.get(0).classObj);
+    assertEquals(classA, list.get(1).classObj);
+    assertEquals(classB, list.get(2).classObj);
+    assertEquals(classB, list.get(3).classObj);
+    assertEquals(classC, list.get(4).classObj);
+
+    // Sort by heap name.
+    Collections.sort(list, new Sort.ObjectsInfoByHeapName());
+    assertEquals(heapA, list.get(0).heap);
+    assertEquals(heapA, list.get(1).heap);
+    assertEquals(heapA, list.get(2).heap);
+    assertEquals(heapB, list.get(3).heap);
+    assertEquals(heapB, list.get(4).heap);
+
+    // Sort first by class name, then by size.
+    Collections.sort(list, new Sort.WithPriority<Site.ObjectsInfo>(
+          new Sort.ObjectsInfoByClassName(),
+          new Sort.ObjectsInfoBySize()));
+    assertEquals(infoA, list.get(0));
+    assertEquals(infoD, list.get(1));
+    assertEquals(infoB, list.get(2));
+    assertEquals(infoE, list.get(3));
+    assertEquals(infoC, list.get(4));
+  }
+}
diff --git a/tools/ahat/test/Tests.java b/tools/ahat/test/Tests.java
new file mode 100644
index 0000000..fb53d90
--- /dev/null
+++ b/tools/ahat/test/Tests.java
@@ -0,0 +1,32 @@
+/*
+ * 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 org.junit.runner.JUnitCore;
+
+public class Tests {
+  public static void main(String[] args) {
+    if (args.length == 0) {
+      args = new String[]{
+        "com.android.ahat.QueryTest",
+        "com.android.ahat.SortTest"
+      };
+    }
+    JUnitCore.main(args);
+  }
+}
+
diff --git a/tools/ahat/test/manifest.txt b/tools/ahat/test/manifest.txt
new file mode 100644
index 0000000..af17fad
--- /dev/null
+++ b/tools/ahat/test/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.ahat.Tests