diff options
| -rw-r--r-- | tools/ahat/README.txt | 14 | ||||
| -rw-r--r-- | tools/ahat/src/AhatHandler.java | 46 | ||||
| -rw-r--r-- | tools/ahat/src/AhatHttpHandler.java | 64 | ||||
| -rw-r--r-- | tools/ahat/src/Doc.java | 22 | ||||
| -rw-r--r-- | tools/ahat/src/DominatedList.java | 105 | ||||
| -rw-r--r-- | tools/ahat/src/HeapTable.java | 62 | ||||
| -rw-r--r-- | tools/ahat/src/Main.java | 10 | ||||
| -rw-r--r-- | tools/ahat/src/ObjectHandler.java | 81 | ||||
| -rw-r--r-- | tools/ahat/src/ObjectsHandler.java | 12 | ||||
| -rw-r--r-- | tools/ahat/src/OverviewHandler.java | 14 | ||||
| -rw-r--r-- | tools/ahat/src/RootsHandler.java | 11 | ||||
| -rw-r--r-- | tools/ahat/src/SiteHandler.java | 19 | ||||
| -rw-r--r-- | tools/ahat/src/SitePrinter.java | 4 | ||||
| -rw-r--r-- | tools/ahat/src/SubsetSelector.java | 109 | ||||
| -rw-r--r-- | tools/ahat/test-dump/Main.java | 9 | ||||
| -rw-r--r-- | tools/ahat/test/PerformanceTest.java | 55 | ||||
| -rw-r--r-- | tools/ahat/test/Tests.java | 3 |
17 files changed, 420 insertions, 220 deletions
diff --git a/tools/ahat/README.txt b/tools/ahat/README.txt index d6f55aae16..aa548cca55 100644 --- a/tools/ahat/README.txt +++ b/tools/ahat/README.txt @@ -13,8 +13,6 @@ TODO: - 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. @@ -25,23 +23,15 @@ TODO: * Show root types. * 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 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? @@ -60,8 +50,6 @@ Things to Test: 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 @@ -72,8 +60,6 @@ Things to Test: 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. diff --git a/tools/ahat/src/AhatHandler.java b/tools/ahat/src/AhatHandler.java index 2da02f8671..d4b4d1b107 100644 --- a/tools/ahat/src/AhatHandler.java +++ b/tools/ahat/src/AhatHandler.java @@ -16,51 +16,17 @@ 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. + * Interface for an ahat page handler. */ -abstract class AhatHandler implements HttpHandler { +interface AhatHandler { - 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(); - } + /** + * Handle the given query, rendering the page to the given document. + */ + void handle(Doc doc, Query query) throws IOException; } diff --git a/tools/ahat/src/AhatHttpHandler.java b/tools/ahat/src/AhatHttpHandler.java new file mode 100644 index 0000000000..0553713702 --- /dev/null +++ b/tools/ahat/src/AhatHttpHandler.java @@ -0,0 +1,64 @@ +/* + * 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; + +/** + * AhatHttpHandler. + * + * HttpHandler for AhatHandlers. + */ +class AhatHttpHandler implements HttpHandler { + + private AhatHandler mAhatHandler; + + public AhatHttpHandler(AhatHandler handler) { + mAhatHandler = handler; + } + + @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); + mAhatHandler.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/Doc.java b/tools/ahat/src/Doc.java index 7fa70de915..5a70c4c74b 100644 --- a/tools/ahat/src/Doc.java +++ b/tools/ahat/src/Doc.java @@ -25,27 +25,27 @@ interface Doc extends AutoCloseable { /** * Output the title of the page. */ - public void title(String format, Object... args); + void title(String format, Object... args); /** * Print a line of text for a page menu. */ - public void menu(DocString string); + void menu(DocString string); /** * Start a new section with the given title. */ - public void section(String title); + void section(String title); /** * Print a line of text in a normal font. */ - public void println(DocString string); + 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); + void big(DocString string); /** * Start a table with the given columns. @@ -55,7 +55,7 @@ interface Doc extends AutoCloseable { * 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); + void table(Column... columns); /** * Start a table with the following heading structure: @@ -68,14 +68,14 @@ interface Doc extends AutoCloseable { * 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); + 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); + void row(DocString... values); /** * Start a new description list. @@ -83,15 +83,15 @@ interface Doc extends AutoCloseable { * This should be followed by calls to description() and finally a call to * end(). */ - public void descriptions(); + void descriptions(); /** * Add a description to the currently active description list. */ - public void description(DocString key, DocString value); + void description(DocString key, DocString value); /** * End the currently active table or description list. */ - public void end(); + void end(); } diff --git a/tools/ahat/src/DominatedList.java b/tools/ahat/src/DominatedList.java index 123d8be82b..34a5665b3e 100644 --- a/tools/ahat/src/DominatedList.java +++ b/tools/ahat/src/DominatedList.java @@ -21,71 +21,35 @@ 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. + * @param snapshot the snapshot where the instances reside + * @param doc the document to render the dominated list to + * @param query the current page query + * @param id a unique identifier to use for the dominated list in the current page + * @param instances the collection of instances to generate a list for */ - public static void render(final AhatSnapshot snapshot, Doc doc, - Collection<Instance> instances, Query query) { + public static void render(final AhatSnapshot snapshot, + Doc doc, Query query, String id, Collection<Instance> instances) { 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()); - } + HeapTable.render(doc, query, id, new TableConfig(snapshot), snapshot, insts); } 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) { + public TableConfig(AhatSnapshot snapshot) { 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 @@ -95,9 +59,6 @@ class DominatedList { @Override public long getSize(Instance element, Heap heap) { - if (element == null) { - return mHiddenSizes.get(heap); - } int index = mSnapshot.getHeapIndex(heap); return element.getRetainedSize(index); } @@ -110,56 +71,10 @@ class DominatedList { } public DocString render(Instance element) { - if (element == null) { - return DocString.text("..."); - } else { - return Value.render(element); - } + 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.appendFormat("(%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 index 37d58164a8..ed11d1724a 100644 --- a/tools/ahat/src/HeapTable.java +++ b/tools/ahat/src/HeapTable.java @@ -18,7 +18,9 @@ package com.android.ahat; import com.android.tools.perflib.heap.Heap; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Class for rendering a table that includes sizes of some kind for each heap. @@ -27,22 +29,27 @@ class HeapTable { /** * Configuration for a value column of a heap table. */ - public static interface ValueConfig<T> { - public String getDescription(); - public DocString render(T element); + public interface ValueConfig<T> { + String getDescription(); + 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 interface TableConfig<T> { + String getHeapsDescription(); + long getSize(T element, Heap heap); + List<ValueConfig<T>> getValueConfigs(); } - public static <T> void render(Doc doc, TableConfig<T> config, - AhatSnapshot snapshot, List<T> elements) { + /** + * Render the table to the given document. + * @param query - The page query. + * @param id - A unique identifier for the table on the page. + */ + public static <T> void render(Doc doc, Query query, String id, + 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()) { @@ -68,9 +75,10 @@ class HeapTable { } doc.table(DocString.text(config.getHeapsDescription()), subcols, cols); - // Print the entries. + // Print the entries up to the selected limit. + SubsetSelector<T> selector = new SubsetSelector(query, id, elements); ArrayList<DocString> vals = new ArrayList<DocString>(); - for (T elem : elements) { + for (T elem : selector.selected()) { vals.clear(); long total = 0; for (Heap heap : heaps) { @@ -87,7 +95,39 @@ class HeapTable { } doc.row(vals.toArray(new DocString[0])); } + + // Print a summary of the remaining entries if there are any. + List<T> remaining = selector.remaining(); + if (!remaining.isEmpty()) { + Map<Heap, Long> summary = new HashMap<Heap, Long>(); + for (Heap heap : heaps) { + summary.put(heap, 0L); + } + + for (T elem : remaining) { + for (Heap heap : heaps) { + summary.put(heap, summary.get(heap) + config.getSize(elem, heap)); + } + } + + vals.clear(); + long total = 0; + for (Heap heap : heaps) { + long size = summary.get(heap); + total += size; + vals.add(DocString.format("%,14d", size)); + } + if (showTotal) { + vals.add(DocString.format("%,14d", total)); + } + + for (ValueConfig<T> value : values) { + vals.add(DocString.text("...")); + } + doc.row(vals.toArray(new DocString[0])); + } doc.end(); + selector.render(doc); } // Returns true if the given heap has a non-zero size entry. diff --git a/tools/ahat/src/Main.java b/tools/ahat/src/Main.java index 1563aa0262..96fc53b6bd 100644 --- a/tools/ahat/src/Main.java +++ b/tools/ahat/src/Main.java @@ -73,11 +73,11 @@ public class Main { 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("/", new AhatHttpHandler(new OverviewHandler(ahat, hprof))); + server.createContext("/roots", new AhatHttpHandler(new RootsHandler(ahat))); + server.createContext("/object", new AhatHttpHandler(new ObjectHandler(ahat))); + server.createContext("/objects", new AhatHttpHandler(new ObjectsHandler(ahat))); + server.createContext("/site", new AhatHttpHandler(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")); diff --git a/tools/ahat/src/ObjectHandler.java b/tools/ahat/src/ObjectHandler.java index 5e321e267e..9e4ce563d5 100644 --- a/tools/ahat/src/ObjectHandler.java +++ b/tools/ahat/src/ObjectHandler.java @@ -25,13 +25,26 @@ 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.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -class ObjectHandler extends AhatHandler { +class ObjectHandler implements AhatHandler { + + private static final String ARRAY_ELEMENTS_ID = "elements"; + private static final String DOMINATOR_PATH_ID = "dompath"; + private static final String ALLOCATION_SITE_ID = "frames"; + private static final String DOMINATED_OBJECTS_ID = "dominated"; + private static final String INSTANCE_FIELDS_ID = "ifields"; + private static final String STATIC_FIELDS_ID = "sfields"; + private static final String HARD_REFS_ID = "refs"; + private static final String SOFT_REFS_ID = "srefs"; + + private AhatSnapshot mSnapshot; + public ObjectHandler(AhatSnapshot snapshot) { - super(snapshot); + mSnapshot = snapshot; } @Override @@ -46,8 +59,8 @@ class ObjectHandler extends AhatHandler { doc.title("Object %08x", inst.getUniqueId()); doc.big(Value.render(inst)); - printAllocationSite(doc, inst); - printDominatorPath(doc, inst); + printAllocationSite(doc, query, inst); + printDominatorPath(doc, query, inst); doc.section("Object Info"); ClassObj cls = inst.getClassObj(); @@ -62,39 +75,46 @@ class ObjectHandler extends AhatHandler { printBitmap(doc, inst); if (inst instanceof ClassInstance) { - printClassInstanceFields(doc, (ClassInstance)inst); + printClassInstanceFields(doc, query, (ClassInstance)inst); } else if (inst instanceof ArrayInstance) { - printArrayElements(doc, (ArrayInstance)inst); + printArrayElements(doc, query, (ArrayInstance)inst); } else if (inst instanceof ClassObj) { - printClassInfo(doc, (ClassObj)inst); + printClassInfo(doc, query, (ClassObj)inst); } - printReferences(doc, inst); + printReferences(doc, query, inst); printDominatedObjects(doc, query, inst); } - private static void printClassInstanceFields(Doc doc, ClassInstance inst) { + private static void printClassInstanceFields(Doc doc, Query query, ClassInstance inst) { doc.section("Fields"); doc.table(new Column("Type"), new Column("Name"), new Column("Value")); - for (ClassInstance.FieldValue field : inst.getValues()) { + SubsetSelector<ClassInstance.FieldValue> selector + = new SubsetSelector(query, INSTANCE_FIELDS_ID, inst.getValues()); + for (ClassInstance.FieldValue field : selector.selected()) { doc.row( DocString.text(field.getField().getType().toString()), DocString.text(field.getField().getName()), Value.render(field.getValue())); } doc.end(); + selector.render(doc); } - private static void printArrayElements(Doc doc, ArrayInstance array) { + private static void printArrayElements(Doc doc, Query query, 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.format("%d", i), Value.render(elements[i])); + List<Object> elements = Arrays.asList(array.getValues()); + SubsetSelector<Object> selector = new SubsetSelector(query, ARRAY_ELEMENTS_ID, elements); + int i = 0; + for (Object elem : selector.selected()) { + doc.row(DocString.format("%d", i), Value.render(elem)); + i++; } doc.end(); + selector.render(doc); } - private static void printClassInfo(Doc doc, ClassObj clsobj) { + private static void printClassInfo(Doc doc, Query query, ClassObj clsobj) { doc.section("Class Info"); doc.descriptions(); doc.description(DocString.text("Super Class"), Value.render(clsobj.getSuperClassObj())); @@ -103,41 +123,52 @@ class ObjectHandler extends AhatHandler { doc.section("Static Fields"); doc.table(new Column("Type"), new Column("Name"), new Column("Value")); - for (Map.Entry<Field, Object> field : clsobj.getStaticFieldValues().entrySet()) { + List<Map.Entry<Field, Object>> fields + = new ArrayList<Map.Entry<Field, Object>>(clsobj.getStaticFieldValues().entrySet()); + SubsetSelector<Map.Entry<Field, Object>> selector + = new SubsetSelector(query, STATIC_FIELDS_ID, fields); + for (Map.Entry<Field, Object> field : selector.selected()) { doc.row( DocString.text(field.getKey().getType().toString()), DocString.text(field.getKey().getName()), Value.render(field.getValue())); } doc.end(); + selector.render(doc); } - private static void printReferences(Doc doc, Instance inst) { + private static void printReferences(Doc doc, Query query, 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()) { + List<Instance> references = inst.getHardReferences(); + SubsetSelector<Instance> selector = new SubsetSelector(query, HARD_REFS_ID, references); + for (Instance ref : selector.selected()) { doc.row(Value.render(ref)); } doc.end(); + selector.render(doc); } 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)); + List<Instance> references = inst.getSoftReferences(); + SubsetSelector<Instance> selector = new SubsetSelector(query, SOFT_REFS_ID, references); + for (Instance ref : selector.selected()) { + doc.row(Value.render(ref)); } doc.end(); + selector.render(doc); } } - private void printAllocationSite(Doc doc, Instance inst) { + private void printAllocationSite(Doc doc, Query query, Instance inst) { doc.section("Allocation Site"); Site site = mSnapshot.getSiteForInstance(inst); - SitePrinter.printSite(doc, mSnapshot, site); + SitePrinter.printSite(mSnapshot, doc, query, ALLOCATION_SITE_ID, site); } // Draw the bitmap corresponding to this instance if there is one. @@ -150,7 +181,7 @@ class ObjectHandler extends AhatHandler { } } - private void printDominatorPath(Doc doc, Instance inst) { + private void printDominatorPath(Doc doc, Query query, Instance inst) { doc.section("Dominator Path from Root"); List<Instance> path = new ArrayList<Instance>(); for (Instance parent = inst; @@ -193,14 +224,14 @@ class ObjectHandler extends AhatHandler { return Collections.singletonList(value); } }; - HeapTable.render(doc, table, mSnapshot, path); + HeapTable.render(doc, query, DOMINATOR_PATH_ID, 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); + DominatedList.render(mSnapshot, doc, query, DOMINATED_OBJECTS_ID, instances); } else { doc.println(DocString.text("(none)")); } diff --git a/tools/ahat/src/ObjectsHandler.java b/tools/ahat/src/ObjectsHandler.java index 4e9c42e566..8ad3f481da 100644 --- a/tools/ahat/src/ObjectsHandler.java +++ b/tools/ahat/src/ObjectsHandler.java @@ -22,9 +22,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -class ObjectsHandler extends AhatHandler { +class ObjectsHandler implements AhatHandler { + private static final String OBJECTS_ID = "objects"; + + private AhatSnapshot mSnapshot; + public ObjectsHandler(AhatSnapshot snapshot) { - super(snapshot); + mSnapshot = snapshot; } @Override @@ -51,13 +55,15 @@ class ObjectsHandler extends AhatHandler { new Column("Size", Column.Align.RIGHT), new Column("Heap"), new Column("Object")); - for (Instance inst : insts) { + SubsetSelector<Instance> selector = new SubsetSelector(query, OBJECTS_ID, insts); + for (Instance inst : selector.selected()) { doc.row( DocString.format("%,d", inst.getSize()), DocString.text(inst.getHeap().getName()), Value.render(inst)); } doc.end(); + selector.render(doc); } } diff --git a/tools/ahat/src/OverviewHandler.java b/tools/ahat/src/OverviewHandler.java index f49c009b30..e86679f378 100644 --- a/tools/ahat/src/OverviewHandler.java +++ b/tools/ahat/src/OverviewHandler.java @@ -22,11 +22,15 @@ import java.io.File; import java.util.Collections; import java.util.List; -class OverviewHandler extends AhatHandler { +class OverviewHandler implements AhatHandler { + + private static final String OVERVIEW_ID = "overview"; + + private AhatSnapshot mSnapshot; private File mHprof; public OverviewHandler(AhatSnapshot snapshot, File hprof) { - super(snapshot); + mSnapshot = snapshot; mHprof = hprof; } @@ -43,7 +47,7 @@ class OverviewHandler extends AhatHandler { doc.end(); doc.section("Heap Sizes"); - printHeapSizes(doc); + printHeapSizes(doc, query); DocString menu = new DocString(); menu.appendLink(DocString.uri("roots"), DocString.text("Roots")); @@ -54,7 +58,7 @@ class OverviewHandler extends AhatHandler { doc.big(menu); } - private void printHeapSizes(Doc doc) { + private void printHeapSizes(Doc doc, Query query) { List<Object> dummy = Collections.singletonList(null); HeapTable.TableConfig<Object> table = new HeapTable.TableConfig<Object>() { @@ -70,7 +74,7 @@ class OverviewHandler extends AhatHandler { return Collections.emptyList(); } }; - HeapTable.render(doc, table, mSnapshot, dummy); + HeapTable.render(doc, query, OVERVIEW_ID, table, mSnapshot, dummy); } } diff --git a/tools/ahat/src/RootsHandler.java b/tools/ahat/src/RootsHandler.java index 185b9bf24f..2a92c90c4e 100644 --- a/tools/ahat/src/RootsHandler.java +++ b/tools/ahat/src/RootsHandler.java @@ -24,9 +24,14 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -class RootsHandler extends AhatHandler { +class RootsHandler implements AhatHandler { + + private static final String ROOTS_ID = "roots"; + + private AhatSnapshot mSnapshot; + public RootsHandler(AhatSnapshot snapshot) { - super(snapshot); + mSnapshot = snapshot; } @Override @@ -45,7 +50,7 @@ class RootsHandler extends AhatHandler { for (Instance inst : rootset) { roots.add(inst); } - DominatedList.render(mSnapshot, doc, roots, query); + DominatedList.render(mSnapshot, doc, query, ROOTS_ID, roots); } } diff --git a/tools/ahat/src/SiteHandler.java b/tools/ahat/src/SiteHandler.java index 0a9381ef24..0425a5a825 100644 --- a/tools/ahat/src/SiteHandler.java +++ b/tools/ahat/src/SiteHandler.java @@ -22,9 +22,15 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -class SiteHandler extends AhatHandler { +class SiteHandler implements AhatHandler { + private static final String ALLOCATION_SITE_ID = "frames"; + private static final String SITES_CALLED_ID = "called"; + private static final String OBJECTS_ALLOCATED_ID = "objects"; + + private AhatSnapshot mSnapshot; + public SiteHandler(AhatSnapshot snapshot) { - super(snapshot); + mSnapshot = snapshot; } @Override @@ -35,7 +41,7 @@ class SiteHandler extends AhatHandler { doc.title("Site %s", site.getName()); doc.section("Allocation Site"); - SitePrinter.printSite(doc, mSnapshot, site); + SitePrinter.printSite(mSnapshot, doc, query, ALLOCATION_SITE_ID, site); doc.section("Sites Called from Here"); List<Site> children = site.getChildren(); @@ -69,7 +75,7 @@ class SiteHandler extends AhatHandler { return Collections.singletonList(value); } }; - HeapTable.render(doc, table, mSnapshot, children); + HeapTable.render(doc, query, SITES_CALLED_ID, table, mSnapshot, children); } doc.section("Objects Allocated"); @@ -84,7 +90,9 @@ class SiteHandler extends AhatHandler { new Sort.ObjectsInfoBySize(), new Sort.ObjectsInfoByClassName()); Collections.sort(infos, compare); - for (Site.ObjectsInfo info : infos) { + SubsetSelector<Site.ObjectsInfo> selector + = new SubsetSelector(query, OBJECTS_ALLOCATED_ID, infos); + for (Site.ObjectsInfo info : selector.selected()) { String className = AhatSnapshot.getClassName(info.classObj); doc.row( DocString.format("%,14d", info.numBytes), @@ -96,6 +104,7 @@ class SiteHandler extends AhatHandler { Value.render(info.classObj)); } doc.end(); + selector.render(doc); } } diff --git a/tools/ahat/src/SitePrinter.java b/tools/ahat/src/SitePrinter.java index be87032a2c..2c06b47a29 100644 --- a/tools/ahat/src/SitePrinter.java +++ b/tools/ahat/src/SitePrinter.java @@ -22,7 +22,7 @@ import java.util.Collections; import java.util.List; class SitePrinter { - public static void printSite(Doc doc, AhatSnapshot snapshot, Site site) { + public static void printSite(AhatSnapshot snapshot, Doc doc, Query query, String id, Site site) { List<Site> path = new ArrayList<Site>(); for (Site parent = site; parent != null; parent = parent.getParent()) { path.add(parent); @@ -60,6 +60,6 @@ class SitePrinter { return Collections.singletonList(value); } }; - HeapTable.render(doc, table, snapshot, path); + HeapTable.render(doc, query, id, table, snapshot, path); } } diff --git a/tools/ahat/src/SubsetSelector.java b/tools/ahat/src/SubsetSelector.java new file mode 100644 index 0000000000..79399c178b --- /dev/null +++ b/tools/ahat/src/SubsetSelector.java @@ -0,0 +1,109 @@ +/* + * 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; + +/** + * The SubsetSelector is that can be added to a page that lets the + * user select a limited number of elements to show. + * This is used to limit the number of elements shown on a page by default, + * requiring the user to explicitly request more, so users not interested in + * more don't have to wait for everything to render. + */ +class SubsetSelector<T> { + private static final int kIncrAmount = 1000; + private static final int kDefaultShown = 1000; + + private Query mQuery; + private String mId; + private int mLimit; + private List<T> mElements; + + /** + * @param id - the name of the query parameter key that should hold + * the limit selectors selected value. + * @param query - The query for the current page. This is required so the + * LimitSelector can add a link to the same page with modified limit + * selection. + * @param elements - the elements to select from. The collection of elements + * should not be modified during the lifetime of the SubsetSelector object. + */ + public SubsetSelector(Query query, String id, List<T> elements) { + mQuery = query; + mId = id; + mLimit = getSelectedLimit(query, id, elements.size()); + mElements = elements; + } + + // Return the list of elements included in the selected subset. + public List<T> selected() { + return mElements.subList(0, mLimit); + } + + // Return the list of remaining elements not included in the selected subset. + public List<T> remaining() { + return mElements.subList(mLimit, mElements.size()); + } + + /** + * Returns the currently selected limit. + * @param query the current page query + * @param size the total number of elements to select from + * @return the number of selected elements + */ + private static int getSelectedLimit(Query query, String id, int size) { + String value = query.get(id, null); + try { + int ivalue = Math.min(size, Integer.parseInt(value)); + return Math.max(0, ivalue); + } catch (NumberFormatException e) { + // We can't parse the value as a number. Ignore it. + } + return Math.min(kDefaultShown, size); + } + + // Render the limit selector to the given doc. + // It has the form: + // (showing X of Y - show none - show less - show more - show all) + public void render(Doc doc) { + int all = mElements.size(); + if (all > kDefaultShown) { + DocString menu = new DocString(); + menu.appendFormat("(%d of %d elements shown - ", mLimit, all); + if (mLimit > 0) { + int less = Math.max(0, mLimit - kIncrAmount); + menu.appendLink(mQuery.with(mId, 0), DocString.text("show none")); + menu.append(" - "); + menu.appendLink(mQuery.with(mId, less), DocString.text("show less")); + menu.append(" - "); + } else { + menu.append("show none - show less - "); + } + if (mLimit < all) { + int more = Math.min(mLimit + kIncrAmount, all); + menu.appendLink(mQuery.with(mId, more), DocString.text("show more")); + menu.append(" - "); + menu.appendLink(mQuery.with(mId, all), DocString.text("show all")); + menu.append(")"); + } else { + menu.append("show more - show all)"); + } + doc.println(menu); + } + } +} diff --git a/tools/ahat/test-dump/Main.java b/tools/ahat/test-dump/Main.java index 7b8774a34d..90cd7af2cc 100644 --- a/tools/ahat/test-dump/Main.java +++ b/tools/ahat/test-dump/Main.java @@ -39,6 +39,15 @@ public class Main { public ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>(); public PhantomReference aPhantomReference = new PhantomReference(anObject, referenceQueue); public WeakReference aWeakReference = new WeakReference(anObject, referenceQueue); + public byte[] bigArray; + + DumpedStuff() { + int N = 1000000; + bigArray = new byte[N]; + for (int i = 0; i < N; i++) { + bigArray[i] = (byte)((i*i) & 0xFF); + } + } } public static void main(String[] args) throws IOException { diff --git a/tools/ahat/test/PerformanceTest.java b/tools/ahat/test/PerformanceTest.java new file mode 100644 index 0000000000..6e46800603 --- /dev/null +++ b/tools/ahat/test/PerformanceTest.java @@ -0,0 +1,55 @@ +/* + * 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.io.OutputStream; +import java.io.PrintStream; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class PerformanceTest { + private static class NullOutputStream extends OutputStream { + public void write(int b) throws IOException { + } + } + + @Test + public void bigArray() throws IOException { + // It should not take more than 1 second to load the default object view + // for any object, including big arrays. + TestDump dump = TestDump.getTestDump(); + + Instance bigArray = (Instance)dump.getDumpedThing("bigArray"); + assertNotNull(bigArray); + + AhatSnapshot snapshot = dump.getAhatSnapshot(); + AhatHandler handler = new ObjectHandler(snapshot); + + PrintStream ps = new PrintStream(new NullOutputStream()); + HtmlDoc doc = new HtmlDoc(ps, DocString.text("bigArray test"), DocString.uri("style.css")); + String uri = "http://localhost:7100/object?id=" + bigArray.getId(); + Query query = new Query(DocString.uri(uri)); + + long start = System.currentTimeMillis(); + handler.handle(doc, query); + long time = System.currentTimeMillis() - start; + assertTrue("bigArray took too long: " + time + "ms", time < 1000); + } +} diff --git a/tools/ahat/test/Tests.java b/tools/ahat/test/Tests.java index bab712199c..e8894e2603 100644 --- a/tools/ahat/test/Tests.java +++ b/tools/ahat/test/Tests.java @@ -23,8 +23,9 @@ public class Tests { if (args.length == 0) { args = new String[]{ "com.android.ahat.InstanceUtilsTest", + "com.android.ahat.PerformanceTest", "com.android.ahat.QueryTest", - "com.android.ahat.SortTest" + "com.android.ahat.SortTest", }; } JUnitCore.main(args); |