diff options
author | 2015-07-15 16:01:58 -0700 | |
---|---|---|
committer | 2015-08-25 09:34:23 -0700 | |
commit | b730b78dac047c6d8ead93ad77605bcb7414f5ce (patch) | |
tree | feda8437b927954bcc849a6c5df6c11d46104355 | |
parent | 24011e738d77dedb28c1b4d6ff34445cc2acc4a7 (diff) |
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
33 files changed, 3037 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk index 49b61bb221..ab3eca4ea2 100644 --- a/Android.mk +++ b/Android.mk @@ -87,6 +87,7 @@ include $(art_path)/imgdiag/Android.mk 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 0000000000..3c1522c3d8 --- /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 0000000000..a8e3884077 --- /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 0000000000..77c1d6e430 --- /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 0000000000..2da02f8671 --- /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 0000000000..2437d0388c --- /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 0000000000..0f567e3200 --- /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 0000000000..b7f2829c58 --- /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 0000000000..7fa70de915 --- /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 0000000000..1d997dc969 --- /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 0000000000..53d1073ed1 --- /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 0000000000..60bb38780a --- /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 0000000000..5ccbacb2d6 --- /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 0000000000..7ee3ff24ed --- /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 0000000000..2e2ddd299f --- /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 0000000000..eecd7d18fb --- /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 0000000000..066c9d59cd --- /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 0000000000..6e6c3235ce --- /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 0000000000..f910608771 --- /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 0000000000..185b9bf24f --- /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 0000000000..d504096314 --- /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 0000000000..8fbc17667f --- /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 0000000000..9c0c2e0efd --- /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 0000000000..3b7916661d --- /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 0000000000..fb7049dcd4 --- /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 0000000000..22c3b8fd4f --- /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 0000000000..b48d79173f --- /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 0000000000..7efb1a770f --- /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 0000000000..ca074a526c --- /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 0000000000..40e3322472 --- /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 0000000000..02ff7db068 --- /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 0000000000..fb53d90801 --- /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 0000000000..af17fadded --- /dev/null +++ b/tools/ahat/test/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.ahat.Tests |