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