diff options
author | 2017-02-21 10:54:51 +0000 | |
---|---|---|
committer | 2017-02-21 10:54:52 +0000 | |
commit | 89bed6d6fcd687cfedd10c14927c104eddf99c7f (patch) | |
tree | c044cdb91c15a4827cb1a15a6b35cd6b7ef9e37b | |
parent | 8ca86eae1f6030782b2646b5b5b0976e06227233 (diff) | |
parent | d640e29f9dad93f51e74026327dd53bb5a30eb33 (diff) |
Merge changes Ic39b6d55,Id9a392ac,I1a6b05ea
* changes:
Show unreachable objects in ahat.
ahat: add support for diffing two heap dumps.
Refactor ahat's perflib api.
59 files changed, 4225 insertions, 2015 deletions
diff --git a/tools/ahat/Android.mk b/tools/ahat/Android.mk index 493eafb3c9..f79377d518 100644 --- a/tools/ahat/Android.mk +++ b/tools/ahat/Android.mk @@ -23,7 +23,6 @@ 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 @@ -79,8 +78,9 @@ include $(BUILD_HOST_DALVIK_JAVA_LIBRARY) # BUILD_HOST_DALVIK_JAVA_LIBRARY above. AHAT_TEST_DUMP_JAR := $(LOCAL_BUILT_MODULE) AHAT_TEST_DUMP_HPROF := $(intermediates.COMMON)/test-dump.hprof +AHAT_TEST_DUMP_BASE_HPROF := $(intermediates.COMMON)/test-dump-base.hprof -# Run ahat-test-dump.jar to generate test-dump.hprof +# Run ahat-test-dump.jar to generate test-dump.hprof and test-dump-base.hprof AHAT_TEST_DUMP_DEPENDENCIES := \ $(ART_HOST_EXECUTABLES) \ $(ART_HOST_SHARED_LIBRARY_DEPENDENCIES) \ @@ -93,12 +93,19 @@ $(AHAT_TEST_DUMP_HPROF): PRIVATE_AHAT_TEST_DUMP_DEPENDENCIES := $(AHAT_TEST_DUMP $(AHAT_TEST_DUMP_HPROF): $(AHAT_TEST_DUMP_JAR) $(AHAT_TEST_DUMP_DEPENDENCIES) $(PRIVATE_AHAT_TEST_ART) -cp $(PRIVATE_AHAT_TEST_DUMP_JAR) Main $@ +$(AHAT_TEST_DUMP_BASE_HPROF): PRIVATE_AHAT_TEST_ART := $(HOST_OUT_EXECUTABLES)/art +$(AHAT_TEST_DUMP_BASE_HPROF): PRIVATE_AHAT_TEST_DUMP_JAR := $(AHAT_TEST_DUMP_JAR) +$(AHAT_TEST_DUMP_BASE_HPROF): PRIVATE_AHAT_TEST_DUMP_DEPENDENCIES := $(AHAT_TEST_DUMP_DEPENDENCIES) +$(AHAT_TEST_DUMP_BASE_HPROF): $(AHAT_TEST_DUMP_JAR) $(AHAT_TEST_DUMP_DEPENDENCIES) + $(PRIVATE_AHAT_TEST_ART) -cp $(PRIVATE_AHAT_TEST_DUMP_JAR) Main $@ --base + .PHONY: ahat-test ahat-test: PRIVATE_AHAT_TEST_DUMP_HPROF := $(AHAT_TEST_DUMP_HPROF) +ahat-test: PRIVATE_AHAT_TEST_DUMP_BASE_HPROF := $(AHAT_TEST_DUMP_BASE_HPROF) ahat-test: PRIVATE_AHAT_TEST_JAR := $(AHAT_TEST_JAR) ahat-test: PRIVATE_AHAT_PROGUARD_MAP := $(AHAT_TEST_DUMP_PROGUARD_MAP) -ahat-test: $(AHAT_TEST_JAR) $(AHAT_TEST_DUMP_HPROF) - java -Dahat.test.dump.hprof=$(PRIVATE_AHAT_TEST_DUMP_HPROF) -Dahat.test.dump.map=$(PRIVATE_AHAT_PROGUARD_MAP) -jar $(PRIVATE_AHAT_TEST_JAR) +ahat-test: $(AHAT_TEST_JAR) $(AHAT_TEST_DUMP_HPROF) $(AHAT_TEST_DUMP_BASE_HPROF) + java -enableassertions -Dahat.test.dump.hprof=$(PRIVATE_AHAT_TEST_DUMP_HPROF) -Dahat.test.dump.base.hprof=$(PRIVATE_AHAT_TEST_DUMP_BASE_HPROF) -Dahat.test.dump.map=$(PRIVATE_AHAT_PROGUARD_MAP) -jar $(PRIVATE_AHAT_TEST_JAR) # Clean up local variables. AHAT_TEST_DUMP_DEPENDENCIES := diff --git a/tools/ahat/README.txt b/tools/ahat/README.txt index 8dfb4abe5b..08d41f0feb 100644 --- a/tools/ahat/README.txt +++ b/tools/ahat/README.txt @@ -1,22 +1,21 @@ AHAT - Android Heap Analysis Tool Usage: - java -jar ahat.jar [-p port] [--proguard-map FILE] FILE - Launch an http server for viewing the given Android heap-dump FILE. + java -jar ahat.jar [OPTIONS] FILE + Launch an http server for viewing the given Android heap dump FILE. - Options: + OPTIONS: -p <port> Serve pages on the given port. Defaults to 7100. --proguard-map FILE Use the proguard map FILE to deobfuscate the heap dump. + --baseline FILE + Diff the heap dump against the given baseline heap dump FILE. + --baseline-proguard-map FILE + Use the proguard map FILE to deobfuscate the baseline heap dump. TODO: - * Have a way to diff two heap dumps. - - * Add more tips to the help page. - - Recommend how to start looking at a heap dump. - - Say how to enable allocation sites. - - Where to submit feedback, questions, and bug reports. + * Add a user guide. * Dim 'image' and 'zygote' heap sizes slightly? Why do we even show these? * Let user re-sort sites objects info by clicking column headers. * Let user re-sort "Objects" list. @@ -49,9 +48,9 @@ Things to Test: time. * That we don't show the 'extra' column in the DominatedList if we are showing all the instances. - * That InstanceUtils.asString properly takes into account "offset" and + * That Instance.asString properly takes into account "offset" and "count" fields, if they are present. - * InstanceUtils.getDexCacheLocation + * Instance.getDexCacheLocation Reported Issues: * Request to be able to sort tables by size. @@ -76,7 +75,11 @@ Things to move to perflib: * Instance.isRoot and Instance.getRootTypes. Release History: - 0.9 Pending + 1.0 Dec 20, 2016 + Add support for diffing two heap dumps. + Remove native allocations view. + Remove outdated help page. + Significant refactoring of ahat internals. 0.8 Oct 18, 2016 Show sample path from GC root with field names in place of dominator path. diff --git a/tools/ahat/src/AhatSnapshot.java b/tools/ahat/src/AhatSnapshot.java deleted file mode 100644 index ba8243f744..0000000000 --- a/tools/ahat/src/AhatSnapshot.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.tools.perflib.captures.MemoryMappedFileBuffer; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Heap; -import com.android.tools.perflib.heap.Instance; -import com.android.tools.perflib.heap.ProguardMap; -import com.android.tools.perflib.heap.RootObj; -import com.android.tools.perflib.heap.RootType; -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.Lists; - -import gnu.trove.TObjectProcedure; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -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 final Snapshot mSnapshot; - private final List<Heap> mHeaps; - - // Map from Instance to the list of Instances it immediately dominates. - private final Map<Instance, List<Instance>> mDominated - = new HashMap<Instance, List<Instance>>(); - - // Collection of objects whose immediate dominator is the SENTINEL_ROOT. - private final List<Instance> mRooted = new ArrayList<Instance>(); - - // Map from roots to their types. - // Instances are only included if they are roots, and the collection of root - // types is guaranteed to be non-empty. - private final Map<Instance, Collection<RootType>> mRoots - = new HashMap<Instance, Collection<RootType>>(); - - private final Site mRootSite = new Site("ROOT"); - private final Map<Heap, Long> mHeapSizes = new HashMap<Heap, Long>(); - - private final List<InstanceUtils.NativeAllocation> mNativeAllocations - = new ArrayList<InstanceUtils.NativeAllocation>(); - - /** - * Create an AhatSnapshot from an hprof file. - */ - public static AhatSnapshot fromHprof(File hprof, ProguardMap map) throws IOException { - Snapshot snapshot = Snapshot.createSnapshot(new MemoryMappedFileBuffer(hprof), map); - snapshot.computeDominators(); - return new AhatSnapshot(snapshot); - } - - /** - * Construct an AhatSnapshot for the given perflib snapshot. - * Ther user is responsible for calling snapshot.computeDominators before - * calling this AhatSnapshot constructor. - */ - private AhatSnapshot(Snapshot snapshot) { - mSnapshot = snapshot; - mHeaps = new ArrayList<Heap>(mSnapshot.getHeaps()); - - final ClassObj javaLangClass = mSnapshot.findClass("java.lang.Class"); - for (Heap heap : mHeaps) { - // Use a single element array for the total to act as a reference to a - // long. - final long[] total = new long[]{0}; - TObjectProcedure<Instance> processInstance = new TObjectProcedure<Instance>() { - @Override - public boolean execute(Instance inst) { - Instance dominator = inst.getImmediateDominator(); - if (dominator != null) { - total[0] += inst.getSize(); - - if (dominator == Snapshot.SENTINEL_ROOT) { - mRooted.add(inst); - } - - // 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); - - // Update native allocations. - InstanceUtils.NativeAllocation alloc = InstanceUtils.getNativeAllocation(inst); - if (alloc != null) { - mNativeAllocations.add(alloc); - } - } - return true; - } - }; - for (Instance instance : heap.getClasses()) { - processInstance.execute(instance); - } - heap.forEachInstance(processInstance); - mHeapSizes.put(heap, total[0]); - } - - // Record the roots and their types. - for (RootObj root : snapshot.getGCRoots()) { - Instance inst = root.getReferredInstance(); - Collection<RootType> types = mRoots.get(inst); - if (types == null) { - types = new HashSet<RootType>(); - mRoots.put(inst, types); - } - types.add(root.getRootType()); - } - } - - // Note: This method is exposed for testing purposes. - public ClassObj findClass(String name) { - return mSnapshot.findClass(name); - } - - 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); - } - - /** - * Returns a collection of instances whose immediate dominator is the - * SENTINEL_ROOT. - */ - public List<Instance> getRooted() { - return mRooted; - } - - /** - * Returns true if the given instance is a root. - */ - public boolean isRoot(Instance inst) { - return mRoots.containsKey(inst); - } - - /** - * Returns the list of root types for the given instance, or null if the - * instance is not a root. - */ - public Collection<RootType> getRootTypes(Instance inst) { - return mRoots.get(inst); - } - - 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) { - return inst.getStack(); - } - - // Return the list of stack frames for a stack trace. - private static StackFrame[] getStackFrames(StackTrace stack) { - return stack.getFrames(); - } - - // Return the serial number of the given stack trace. - private static int getStackTraceSerialNumber(StackTrace stack) { - return stack.getSerialNumber(); - } - - // 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; - } - - // Return a list of known native allocations in the snapshot. - public List<InstanceUtils.NativeAllocation> getNativeAllocations() { - return mNativeAllocations; - } -} diff --git a/tools/ahat/src/BitmapHandler.java b/tools/ahat/src/BitmapHandler.java index 0f567e3200..836aef67b8 100644 --- a/tools/ahat/src/BitmapHandler.java +++ b/tools/ahat/src/BitmapHandler.java @@ -16,7 +16,8 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Instance; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import java.awt.image.BufferedImage; @@ -38,9 +39,9 @@ class BitmapHandler implements HttpHandler { Query query = new Query(exchange.getRequestURI()); long id = query.getLong("id", 0); BufferedImage bitmap = null; - Instance inst = mSnapshot.findInstance(id); + AhatInstance inst = mSnapshot.findInstance(id); if (inst != null) { - bitmap = InstanceUtils.asBitmap(inst); + bitmap = inst.asBitmap(); } if (bitmap != null) { diff --git a/tools/ahat/src/Column.java b/tools/ahat/src/Column.java index b7f2829c58..819e586ef9 100644 --- a/tools/ahat/src/Column.java +++ b/tools/ahat/src/Column.java @@ -22,14 +22,24 @@ package com.android.ahat; class Column { public DocString heading; public Align align; + public boolean visible; public static enum Align { LEFT, RIGHT }; - public Column(DocString heading, Align align) { + public Column(DocString heading, Align align, boolean visible) { this.heading = heading; this.align = align; + this.visible = visible; + } + + public Column(String heading, Align align, boolean visible) { + this(DocString.text(heading), align, visible); + } + + public Column(DocString heading, Align align) { + this(heading, align, true); } /** diff --git a/tools/ahat/src/DocString.java b/tools/ahat/src/DocString.java index 19666dea8c..c6303c8c35 100644 --- a/tools/ahat/src/DocString.java +++ b/tools/ahat/src/DocString.java @@ -53,7 +53,6 @@ class DocString { public static DocString link(URI uri, DocString content) { DocString doc = new DocString(); return doc.appendLink(uri, content); - } /** @@ -86,6 +85,78 @@ class DocString { return this; } + /** + * Adorn the given string to indicate it represents something added relative + * to a baseline. + */ + public static DocString added(DocString str) { + DocString string = new DocString(); + string.mStringBuilder.append("<span class=\"added\">"); + string.mStringBuilder.append(str.html()); + string.mStringBuilder.append("</span>"); + return string; + } + + /** + * Adorn the given string to indicate it represents something added relative + * to a baseline. + */ + public static DocString added(String str) { + return added(text(str)); + } + + /** + * Adorn the given string to indicate it represents something removed relative + * to a baseline. + */ + public static DocString removed(DocString str) { + DocString string = new DocString(); + string.mStringBuilder.append("<span class=\"removed\">"); + string.mStringBuilder.append(str.html()); + string.mStringBuilder.append("</span>"); + return string; + } + + /** + * Adorn the given string to indicate it represents something removed relative + * to a baseline. + */ + public static DocString removed(String str) { + return removed(text(str)); + } + + /** + * Standard formatted DocString for describing a change in size relative to + * a baseline. + * @param noCurrent - whether no current object exists. + * @param noBaseline - whether no basline object exists. + * @param current - the size of the current object. + * @param baseline - the size of the baseline object. + */ + public static DocString delta(boolean noCurrent, boolean noBaseline, + long current, long baseline) { + DocString doc = new DocString(); + return doc.appendDelta(noCurrent, noBaseline, current, baseline); + } + + /** + * Standard formatted DocString for describing a change in size relative to + * a baseline. + */ + public DocString appendDelta(boolean noCurrent, boolean noBaseline, + long current, long baseline) { + if (noCurrent) { + append(removed(format("%+,14d", 0 - baseline))); + } else if (noBaseline) { + append(added("new")); + } else if (current > baseline) { + append(added(format("%+,14d", current - baseline))); + } else if (current < baseline) { + append(removed(format("%+,14d", current - baseline))); + } + return this; + } + public DocString appendLink(URI uri, DocString content) { mStringBuilder.append("<a href=\""); mStringBuilder.append(uri.toASCIIString()); diff --git a/tools/ahat/src/DominatedList.java b/tools/ahat/src/DominatedList.java index 7a673f556e..f73e3ca027 100644 --- a/tools/ahat/src/DominatedList.java +++ b/tools/ahat/src/DominatedList.java @@ -16,8 +16,10 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Heap; -import com.android.tools.perflib.heap.Instance; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Sort; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -39,39 +41,32 @@ class DominatedList { * @param instances the collection of instances to generate a list for */ public static void render(final AhatSnapshot snapshot, - Doc doc, Query query, String id, Collection<Instance> instances) { - List<Instance> insts = new ArrayList<Instance>(instances); + Doc doc, Query query, String id, Collection<AhatInstance> instances) { + List<AhatInstance> insts = new ArrayList<AhatInstance>(instances); Collections.sort(insts, Sort.defaultInstanceCompare(snapshot)); - HeapTable.render(doc, query, id, new TableConfig(snapshot), snapshot, insts); + HeapTable.render(doc, query, id, new TableConfig(), snapshot, insts); } - private static class TableConfig implements HeapTable.TableConfig<Instance> { - AhatSnapshot mSnapshot; - - public TableConfig(AhatSnapshot snapshot) { - mSnapshot = snapshot; - } - + private static class TableConfig implements HeapTable.TableConfig<AhatInstance> { @Override public String getHeapsDescription() { return "Bytes Retained by Heap"; } @Override - public long getSize(Instance element, Heap heap) { - int index = mSnapshot.getHeapIndex(heap); - return element.getRetainedSize(index); + public long getSize(AhatInstance element, AhatHeap heap) { + return element.getRetainedSize(heap); } @Override - public List<HeapTable.ValueConfig<Instance>> getValueConfigs() { - HeapTable.ValueConfig<Instance> value = new HeapTable.ValueConfig<Instance>() { + public List<HeapTable.ValueConfig<AhatInstance>> getValueConfigs() { + HeapTable.ValueConfig<AhatInstance> value = new HeapTable.ValueConfig<AhatInstance>() { public String getDescription() { return "Object"; } - public DocString render(Instance element) { - return Value.render(mSnapshot, element); + public DocString render(AhatInstance element) { + return Summarizer.summarize(element); } }; return Collections.singletonList(value); diff --git a/tools/ahat/src/HeapTable.java b/tools/ahat/src/HeapTable.java index 5b840489d2..9abbe4a4ed 100644 --- a/tools/ahat/src/HeapTable.java +++ b/tools/ahat/src/HeapTable.java @@ -16,7 +16,9 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Heap; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Diffable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -39,21 +41,31 @@ class HeapTable { */ public interface TableConfig<T> { String getHeapsDescription(); - long getSize(T element, Heap heap); + long getSize(T element, AhatHeap heap); List<ValueConfig<T>> getValueConfigs(); } + private static DocString sizeString(long size, boolean isPlaceHolder) { + DocString string = new DocString(); + if (isPlaceHolder) { + string.append(DocString.removed("del")); + } else if (size != 0) { + string.appendFormat("%,14d", size); + } + return string; + } + /** * 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, + public static <T extends Diffable<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()) { - if (hasNonZeroEntry(snapshot, heap, config, elements)) { + List<AhatHeap> heaps = new ArrayList<AhatHeap>(); + for (AhatHeap heap : snapshot.getHeaps()) { + if (hasNonZeroEntry(heap, config, elements)) { heaps.add(heap); } } @@ -61,14 +73,14 @@ class HeapTable { 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) { + for (AhatHeap heap : heaps) { subcols.add(new Column(heap.getName(), Column.Align.RIGHT)); + subcols.add(new Column("Δ", Column.Align.RIGHT, snapshot.isDiffed())); } - if (showTotal) { - subcols.add(new Column("Total", Column.Align.RIGHT)); - } + boolean showTotal = heaps.size() > 1; + subcols.add(new Column("Total", Column.Align.RIGHT, showTotal)); + subcols.add(new Column("Δ", Column.Align.RIGHT, showTotal && snapshot.isDiffed())); List<Column> cols = new ArrayList<Column>(); for (ValueConfig value : values) { cols.add(new Column(value.getDescription())); @@ -79,16 +91,20 @@ class HeapTable { SubsetSelector<T> selector = new SubsetSelector(query, id, elements); ArrayList<DocString> vals = new ArrayList<DocString>(); for (T elem : selector.selected()) { + T base = elem.getBaseline(); vals.clear(); long total = 0; - for (Heap heap : heaps) { + long basetotal = 0; + for (AhatHeap heap : heaps) { long size = config.getSize(elem, heap); + long basesize = config.getSize(base, heap.getBaseline()); total += size; - vals.add(size == 0 ? DocString.text("") : DocString.format("%,14d", size)); - } - if (showTotal) { - vals.add(total == 0 ? DocString.text("") : DocString.format("%,14d", total)); + basetotal += basesize; + vals.add(sizeString(size, elem.isPlaceHolder())); + vals.add(DocString.delta(elem.isPlaceHolder(), base.isPlaceHolder(), size, basesize)); } + vals.add(sizeString(total, elem.isPlaceHolder())); + vals.add(DocString.delta(elem.isPlaceHolder(), base.isPlaceHolder(), total, basetotal)); for (ValueConfig<T> value : values) { vals.add(value.render(elem)); @@ -99,27 +115,36 @@ class HeapTable { // 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) { + Map<AhatHeap, Long> summary = new HashMap<AhatHeap, Long>(); + Map<AhatHeap, Long> basesummary = new HashMap<AhatHeap, Long>(); + for (AhatHeap heap : heaps) { summary.put(heap, 0L); + basesummary.put(heap, 0L); } for (T elem : remaining) { - for (Heap heap : heaps) { - summary.put(heap, summary.get(heap) + config.getSize(elem, heap)); + for (AhatHeap heap : heaps) { + long size = config.getSize(elem, heap); + summary.put(heap, summary.get(heap) + size); + + long basesize = config.getSize(elem.getBaseline(), heap.getBaseline()); + basesummary.put(heap, basesummary.get(heap) + basesize); } } vals.clear(); long total = 0; - for (Heap heap : heaps) { + long basetotal = 0; + for (AhatHeap heap : heaps) { long size = summary.get(heap); + long basesize = basesummary.get(heap); total += size; - vals.add(DocString.format("%,14d", size)); - } - if (showTotal) { - vals.add(DocString.format("%,14d", total)); + basetotal += basesize; + vals.add(sizeString(size, false)); + vals.add(DocString.delta(false, false, size, basesize)); } + vals.add(sizeString(total, false)); + vals.add(DocString.delta(false, false, total, basetotal)); for (ValueConfig<T> value : values) { vals.add(DocString.text("...")); @@ -131,11 +156,13 @@ class HeapTable { } // Returns true if the given heap has a non-zero size entry. - public static <T> boolean hasNonZeroEntry(AhatSnapshot snapshot, Heap heap, + public static <T extends Diffable<T>> boolean hasNonZeroEntry(AhatHeap heap, TableConfig<T> config, List<T> elements) { - if (snapshot.getHeapSize(heap) > 0) { + AhatHeap baseheap = heap.getBaseline(); + if (heap.getSize() > 0 || baseheap.getSize() > 0) { for (T element : elements) { - if (config.getSize(element, heap) > 0) { + if (config.getSize(element, heap) > 0 || + config.getSize(element.getBaseline(), baseheap) > 0) { return true; } } diff --git a/tools/ahat/src/HelpHandler.java b/tools/ahat/src/HelpHandler.java deleted file mode 100644 index 8de3c85f5c..0000000000 --- a/tools/ahat/src/HelpHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.google.common.io.ByteStreams; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; - -/** - * HelpHandler. - * - * HttpHandler to show the help page. - */ -class HelpHandler implements HttpHandler { - - @Override - public void handle(HttpExchange exchange) throws IOException { - ClassLoader loader = HelpHandler.class.getClassLoader(); - exchange.getResponseHeaders().add("Content-Type", "text/html;charset=utf-8"); - exchange.sendResponseHeaders(200, 0); - PrintStream ps = new PrintStream(exchange.getResponseBody()); - HtmlDoc doc = new HtmlDoc(ps, DocString.text("ahat"), DocString.uri("style.css")); - doc.menu(Menu.getMenu()); - - InputStream is = loader.getResourceAsStream("help.html"); - if (is == null) { - ps.println("No help available."); - } else { - ByteStreams.copy(is, ps); - } - - doc.close(); - ps.close(); - } -} diff --git a/tools/ahat/src/HtmlDoc.java b/tools/ahat/src/HtmlDoc.java index 5ccbacb2d6..5a22fc75fe 100644 --- a/tools/ahat/src/HtmlDoc.java +++ b/tools/ahat/src/HtmlDoc.java @@ -86,19 +86,27 @@ public class HtmlDoc implements Doc { mCurrentTableColumns = columns; ps.println("<table>"); for (int i = 0; i < columns.length - 1; i++) { - ps.format("<th>%s</th>", columns[i].heading.html()); + if (columns[i].visible) { + 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()); + if (columns[columns.length - 1].visible) { + 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; + int visibleSubCols = 0; for (Column col : subcols) { + if (col.visible) { + visibleSubCols++; + } mCurrentTableColumns[j] = col; j++; } @@ -108,21 +116,27 @@ public class HtmlDoc implements Doc { } ps.println("<table>"); - ps.format("<tr><th colspan=\"%d\">%s</th>", subcols.size(), description.html()); + ps.format("<tr><th colspan=\"%d\">%s</th>", visibleSubCols, 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.get(i).visible) { + 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()); + Column col = cols.get(cols.size() - 1); + if (col.visible) { + ps.format("<th align=\"left\" rowspan=\"2\">%s</th>", col.heading.html()); + } } ps.println("</tr>"); ps.print("<tr>"); for (Column subcol : subcols) { - ps.format("<th>%s</th>", subcol.heading.html()); + if (subcol.visible) { + ps.format("<th>%s</th>", subcol.heading.html()); + } } ps.println("</tr>"); } @@ -141,11 +155,13 @@ public class HtmlDoc implements Doc { ps.print("<tr>"); for (int i = 0; i < values.length; i++) { + if (mCurrentTableColumns[i].visible) { ps.print("<td"); - if (mCurrentTableColumns[i].align == Column.Align.RIGHT) { - ps.print(" align=\"right\""); + if (mCurrentTableColumns[i].align == Column.Align.RIGHT) { + ps.print(" align=\"right\""); + } + ps.format(">%s</td>", values[i].html()); } - ps.format(">%s</td>", values[i].html()); } ps.println("</tr>"); } diff --git a/tools/ahat/src/InstanceUtils.java b/tools/ahat/src/InstanceUtils.java deleted file mode 100644 index a062afdaef..0000000000 --- a/tools/ahat/src/InstanceUtils.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.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 com.android.tools.perflib.heap.Type; - -import java.awt.image.BufferedImage; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * 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. - */ - private static boolean isInstanceOfClass(Instance inst, String className) { - ClassObj cls = (inst == null) ? null : inst.getClassObj(); - return (cls != null && className.equals(cls.getClassName())); - } - - /** - * 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) { - return asString(inst, -1); - } - - /** - * Read the string value from an hprof Instance. - * Returns null if the object can't be interpreted as a string. - * The returned string is truncated to maxChars characters. - * If maxChars is negative, the returned string is not truncated. - */ - public static String asString(Instance inst, int maxChars) { - // The inst object could either be a java.lang.String or a char[]. If it - // is a char[], use that directly as the value, otherwise use the value - // field of the string object. The field accesses for count and offset - // later on will work okay regardless of what type the inst object is. - boolean isString = isInstanceOfClass(inst, "java.lang.String"); - Object value = isString ? getField(inst, "value") : inst; - - if (!(value instanceof ArrayInstance)) { - return null; - } - - ArrayInstance chars = (ArrayInstance) value; - int numChars = chars.getLength(); - int offset = getIntField(inst, "offset", 0); - int count = getIntField(inst, "count", numChars); - - // With string compression enabled, the array type can be BYTE but in that case - // offset must be 0 and count must match numChars. - if (isString && (chars.getArrayType() == Type.BYTE) && (offset == 0) && (count == numChars)) { - int length = (0 <= maxChars && maxChars < numChars) ? maxChars : numChars; - return new String(chars.asRawByteArray(/* offset */ 0, length), StandardCharsets.US_ASCII); - } - if (chars.getArrayType() != Type.CHAR) { - return null; - } - if (count == 0) { - return ""; - } - if (0 <= maxChars && maxChars < count) { - count = maxChars; - } - - int end = offset + count - 1; - if (offset >= 0 && offset < numChars && end >= 0 && end < numChars) { - return new String(chars.asCharArray(offset, count)); - } - return null; - } - - /** - * 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", null); - if (width == null) { - return null; - } - - Integer height = getIntField(inst, "mHeight", null); - 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. - */ - public 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. - */ - public 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 <code>def</code> if the field value is not an int or could not be - * read. - */ - private static Integer getIntField(Instance inst, String fieldName, Integer def) { - Object value = getField(inst, fieldName); - if (!(value instanceof Integer)) { - return def; - } - return (Integer) value; - } - - /** - * Read a long field of an instance. - * The field is assumed to be a long type. - * Returns <code>def</code> if the field value is not an long or could not - * be read. - */ - private static Long getLongField(Instance inst, String fieldName, Long def) { - Object value = getField(inst, fieldName); - if (!(value instanceof Long)) { - return def; - } - return (Long) 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); - } - - // 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.getHardReverseReferences().size() == 1) { - Instance ref = inst.getHardReverseReferences().get(0); - ClassObj clsref = ref.getClassObj(); - if (clsref != null && "android.graphics.Bitmap".equals(clsref.getClassName())) { - return ref; - } - } - } - return null; - } - - private static boolean isJavaLangRefReference(Instance inst) { - ClassObj cls = (inst == null) ? null : inst.getClassObj(); - while (cls != null) { - if ("java.lang.ref.Reference".equals(cls.getClassName())) { - return true; - } - cls = cls.getSuperClassObj(); - } - return false; - } - - public static Instance getReferent(Instance inst) { - if (isJavaLangRefReference(inst)) { - return getRefField(inst, "referent"); - } - 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. - * If maxChars is non-negative, the returned location is truncated to - * maxChars in length. - */ - public static String getDexCacheLocation(Instance inst, int maxChars) { - if (isInstanceOfClass(inst, "java.lang.DexCache")) { - Instance location = getRefField(inst, "location"); - if (location != null) { - return asString(location, maxChars); - } - } - return null; - } - - public static class NativeAllocation { - public long size; - public Heap heap; - public long pointer; - public Instance referent; - - public NativeAllocation(long size, Heap heap, long pointer, Instance referent) { - this.size = size; - this.heap = heap; - this.pointer = pointer; - this.referent = referent; - } - } - - /** - * Assuming inst represents a NativeAllocation, return information about the - * native allocation. Returns null if the given instance doesn't represent a - * native allocation. - */ - public static NativeAllocation getNativeAllocation(Instance inst) { - if (!isInstanceOfClass(inst, "libcore.util.NativeAllocationRegistry$CleanerThunk")) { - return null; - } - - Long pointer = InstanceUtils.getLongField(inst, "nativePtr", null); - if (pointer == null) { - return null; - } - - // Search for the registry field of inst. - // Note: We know inst as an instance of ClassInstance because we already - // read the nativePtr field from it. - Instance registry = null; - for (ClassInstance.FieldValue field : ((ClassInstance) inst).getValues()) { - Object fieldValue = field.getValue(); - if (fieldValue instanceof Instance) { - Instance fieldInst = (Instance) fieldValue; - if (isInstanceOfClass(fieldInst, "libcore.util.NativeAllocationRegistry")) { - registry = fieldInst; - break; - } - } - } - - if (registry == null) { - return null; - } - - Long size = InstanceUtils.getLongField(registry, "size", null); - if (size == null) { - return null; - } - - Instance referent = null; - for (Instance ref : inst.getHardReverseReferences()) { - if (isInstanceOfClass(ref, "sun.misc.Cleaner")) { - referent = InstanceUtils.getReferent(ref); - if (referent != null) { - break; - } - } - } - - if (referent == null) { - return null; - } - return new NativeAllocation(size, inst.getHeap(), pointer, referent); - } - - public static class PathElement { - public final Instance instance; - public final String field; - public boolean isDominator; - - public PathElement(Instance instance, String field) { - this.instance = instance; - this.field = field; - this.isDominator = false; - } - } - - /** - * Returns a sample path from a GC root to this instance. - * The given instance is included as the last element of the path with an - * empty field description. - */ - public static List<PathElement> getPathFromGcRoot(Instance inst) { - List<PathElement> path = new ArrayList<PathElement>(); - - Instance dom = inst; - for (PathElement elem = new PathElement(inst, ""); elem != null; - elem = getNextPathElementToGcRoot(elem.instance)) { - if (elem.instance == dom) { - elem.isDominator = true; - dom = dom.getImmediateDominator(); - } - path.add(elem); - } - Collections.reverse(path); - return path; - } - - /** - * Returns the next instance to GC root from this object and a string - * description of which field of that object refers to the given instance. - * Returns null if the given instance has no next instance to the gc root. - */ - private static PathElement getNextPathElementToGcRoot(Instance inst) { - Instance parent = inst.getNextInstanceToGcRoot(); - if (parent == null || parent instanceof RootObj) { - return null; - } - - // Search the parent for the reference to the child. - // TODO: This seems terribly inefficient. Can we use data structures to - // help us here? - String description = ".???"; - if (parent instanceof ArrayInstance) { - ArrayInstance array = (ArrayInstance)parent; - Object[] values = array.getValues(); - for (int i = 0; i < values.length; i++) { - if (values[i] instanceof Instance) { - Instance ref = (Instance)values[i]; - if (ref.getId() == inst.getId()) { - description = String.format("[%d]", i); - break; - } - } - } - } else if (parent instanceof ClassObj) { - ClassObj cls = (ClassObj)parent; - for (Map.Entry<Field, Object> entries : cls.getStaticFieldValues().entrySet()) { - if (entries.getValue() instanceof Instance) { - Instance ref = (Instance)entries.getValue(); - if (ref.getId() == inst.getId()) { - description = "." + entries.getKey().getName(); - break; - } - } - } - } else if (parent instanceof ClassInstance) { - ClassInstance obj = (ClassInstance)parent; - for (ClassInstance.FieldValue fields : obj.getValues()) { - if (fields.getValue() instanceof Instance) { - Instance ref = (Instance)fields.getValue(); - if (ref.getId() == inst.getId()) { - description = "." + fields.getField().getName(); - break; - } - } - } - } - return new PathElement(parent, description); - } -} diff --git a/tools/ahat/src/Main.java b/tools/ahat/src/Main.java index c79b57863e..b8552fe1ce 100644 --- a/tools/ahat/src/Main.java +++ b/tools/ahat/src/Main.java @@ -16,6 +16,8 @@ package com.android.ahat; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Diff; import com.android.tools.perflib.heap.ProguardMap; import com.sun.net.httpserver.HttpServer; import java.io.File; @@ -29,15 +31,18 @@ import java.util.concurrent.Executors; public class Main { public static void help(PrintStream out) { - out.println("java -jar ahat.jar [-p port] [--proguard-map FILE] FILE"); - out.println(" Launch an http server for viewing " - + "the given Android heap-dump FILE."); + out.println("java -jar ahat.jar [OPTIONS] FILE"); + out.println(" Launch an http server for viewing the given Android heap dump FILE."); out.println(""); - out.println("Options:"); + out.println("OPTIONS:"); out.println(" -p <port>"); out.println(" Serve pages on the given port. Defaults to 7100."); out.println(" --proguard-map FILE"); out.println(" Use the proguard map FILE to deobfuscate the heap dump."); + out.println(" --baseline FILE"); + out.println(" Diff the heap dump against the given baseline heap dump FILE."); + out.println(" --baseline-proguard-map FILE"); + out.println(" Use the proguard map FILE to deobfuscate the baseline heap dump."); out.println(""); } @@ -51,7 +56,9 @@ public class Main { } File hprof = null; + File hprofbase = null; ProguardMap map = new ProguardMap(); + ProguardMap mapbase = new ProguardMap(); for (int i = 0; i < args.length; i++) { if ("-p".equals(args[i]) && i + 1 < args.length) { i++; @@ -64,6 +71,22 @@ public class Main { System.out.println("Unable to read proguard map: " + ex); System.out.println("The proguard map will not be used."); } + } else if ("--baseline-proguard-map".equals(args[i]) && i + 1 < args.length) { + i++; + try { + mapbase.readFromFile(new File(args[i])); + } catch (IOException|ParseException ex) { + System.out.println("Unable to read baselline proguard map: " + ex); + System.out.println("The proguard map will not be used."); + } + } else if ("--baseline".equals(args[i]) && i + 1 < args.length) { + i++; + if (hprofbase != null) { + System.err.println("multiple baseline heap dumps."); + help(System.err); + return; + } + hprofbase = new File(args[i]); } else { if (hprof != null) { System.err.println("multiple input files."); @@ -88,17 +111,25 @@ public class Main { System.out.println("Processing hprof file..."); AhatSnapshot ahat = AhatSnapshot.fromHprof(hprof, map); - server.createContext("/", new AhatHttpHandler(new OverviewHandler(ahat, hprof))); + + if (hprofbase != null) { + System.out.println("Processing baseline hprof file..."); + AhatSnapshot base = AhatSnapshot.fromHprof(hprofbase, mapbase); + + System.out.println("Diffing hprof files..."); + Diff.snapshots(ahat, base); + } + + server.createContext("/", new AhatHttpHandler(new OverviewHandler(ahat, hprof, hprofbase))); server.createContext("/rooted", new AhatHttpHandler(new RootedHandler(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("/native", new AhatHttpHandler(new NativeAllocationsHandler(ahat))); server.createContext("/bitmap", new BitmapHandler(ahat)); - server.createContext("/help", new HelpHandler()); 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/Menu.java b/tools/ahat/src/Menu.java index 232b849c28..6d38dc5731 100644 --- a/tools/ahat/src/Menu.java +++ b/tools/ahat/src/Menu.java @@ -25,11 +25,7 @@ class Menu { .append(" - ") .appendLink(DocString.uri("rooted"), DocString.text("rooted")) .append(" - ") - .appendLink(DocString.uri("sites"), DocString.text("allocations")) - .append(" - ") - .appendLink(DocString.uri("native"), DocString.text("native")) - .append(" - ") - .appendLink(DocString.uri("help"), DocString.text("help")); + .appendLink(DocString.uri("sites"), DocString.text("allocations")); /** * Returns the menu as a DocString. diff --git a/tools/ahat/src/NativeAllocationsHandler.java b/tools/ahat/src/NativeAllocationsHandler.java deleted file mode 100644 index 17407e1ae1..0000000000 --- a/tools/ahat/src/NativeAllocationsHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import java.io.IOException; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -class NativeAllocationsHandler implements AhatHandler { - private static final String ALLOCATIONS_ID = "allocations"; - - private AhatSnapshot mSnapshot; - - public NativeAllocationsHandler(AhatSnapshot snapshot) { - mSnapshot = snapshot; - } - - @Override - public void handle(Doc doc, Query query) throws IOException { - List<InstanceUtils.NativeAllocation> allocs = mSnapshot.getNativeAllocations(); - - doc.title("Registered Native Allocations"); - - doc.section("Overview"); - long totalSize = 0; - for (InstanceUtils.NativeAllocation alloc : allocs) { - totalSize += alloc.size; - } - doc.descriptions(); - doc.description(DocString.text("Number of Registered Native Allocations"), - DocString.format("%,14d", allocs.size())); - doc.description(DocString.text("Total Size of Registered Native Allocations"), - DocString.format("%,14d", totalSize)); - doc.end(); - - doc.section("List of Allocations"); - if (allocs.isEmpty()) { - doc.println(DocString.text("(none)")); - } else { - doc.table( - new Column("Size", Column.Align.RIGHT), - new Column("Heap"), - new Column("Native Pointer"), - new Column("Referent")); - Comparator<InstanceUtils.NativeAllocation> compare - = new Sort.WithPriority<InstanceUtils.NativeAllocation>( - new Sort.NativeAllocationByHeapName(), - new Sort.NativeAllocationBySize()); - Collections.sort(allocs, compare); - SubsetSelector<InstanceUtils.NativeAllocation> selector - = new SubsetSelector(query, ALLOCATIONS_ID, allocs); - for (InstanceUtils.NativeAllocation alloc : selector.selected()) { - doc.row( - DocString.format("%,14d", alloc.size), - DocString.text(alloc.heap.getName()), - DocString.format("0x%x", alloc.pointer), - Value.render(mSnapshot, alloc.referent)); - } - - // Print a summary of the remaining entries if there are any. - List<InstanceUtils.NativeAllocation> remaining = selector.remaining(); - if (!remaining.isEmpty()) { - long total = 0; - for (InstanceUtils.NativeAllocation alloc : remaining) { - total += alloc.size; - } - - doc.row( - DocString.format("%,14d", total), - DocString.text("..."), - DocString.text("..."), - DocString.text("...")); - } - - doc.end(); - selector.render(doc); - } - } -} - diff --git a/tools/ahat/src/ObjectHandler.java b/tools/ahat/src/ObjectHandler.java index 78aac178e9..2e0ae6ed2d 100644 --- a/tools/ahat/src/ObjectHandler.java +++ b/tools/ahat/src/ObjectHandler.java @@ -16,22 +16,23 @@ 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.RootType; +import com.android.ahat.heapdump.AhatArrayInstance; +import com.android.ahat.heapdump.AhatClassInstance; +import com.android.ahat.heapdump.AhatClassObj; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Diff; +import com.android.ahat.heapdump.FieldValue; +import com.android.ahat.heapdump.PathElement; +import com.android.ahat.heapdump.Site; +import com.android.ahat.heapdump.Value; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.Objects; -import static com.android.ahat.InstanceUtils.PathElement; class ObjectHandler implements AhatHandler { @@ -53,35 +54,43 @@ class ObjectHandler implements AhatHandler { @Override public void handle(Doc doc, Query query) throws IOException { long id = query.getLong("id", 0); - Instance inst = mSnapshot.findInstance(id); + AhatInstance inst = mSnapshot.findInstance(id); if (inst == null) { doc.println(DocString.format("No object with id %08xl", id)); return; } + AhatInstance base = inst.getBaseline(); - doc.title("Object %08x", inst.getUniqueId()); - doc.big(Value.render(mSnapshot, inst)); + doc.title("Object %08x", inst.getId()); + doc.big(Summarizer.summarize(inst)); printAllocationSite(doc, query, inst); printGcRootPath(doc, query, inst); doc.section("Object Info"); - ClassObj cls = inst.getClassObj(); + AhatClassObj cls = inst.getClassObj(); doc.descriptions(); - doc.description(DocString.text("Class"), Value.render(mSnapshot, cls)); - doc.description(DocString.text("Size"), DocString.format("%d", inst.getSize())); - doc.description( - DocString.text("Retained Size"), - DocString.format("%d", inst.getTotalRetainedSize())); + doc.description(DocString.text("Class"), Summarizer.summarize(cls)); + + DocString sizeDescription = DocString.format("%,14d ", inst.getSize()); + sizeDescription.appendDelta(false, base.isPlaceHolder(), + inst.getSize(), base.getSize()); + doc.description(DocString.text("Size"), sizeDescription); + + DocString rsizeDescription = DocString.format("%,14d ", inst.getTotalRetainedSize()); + rsizeDescription.appendDelta(false, base.isPlaceHolder(), + inst.getTotalRetainedSize(), base.getTotalRetainedSize()); + doc.description(DocString.text("Retained Size"), rsizeDescription); + doc.description(DocString.text("Heap"), DocString.text(inst.getHeap().getName())); - Collection<RootType> rootTypes = mSnapshot.getRootTypes(inst); + Collection<String> rootTypes = inst.getRootTypes(); if (rootTypes != null) { DocString types = new DocString(); String comma = ""; - for (RootType type : rootTypes) { + for (String type : rootTypes) { types.append(comma); - types.append(type.getName()); + types.append(type); comma = ", "; } doc.description(DocString.text("Root Types"), types); @@ -90,112 +99,146 @@ class ObjectHandler implements AhatHandler { doc.end(); printBitmap(doc, inst); - if (inst instanceof ClassInstance) { - printClassInstanceFields(doc, query, mSnapshot, (ClassInstance)inst); - } else if (inst instanceof ArrayInstance) { - printArrayElements(doc, query, mSnapshot, (ArrayInstance)inst); - } else if (inst instanceof ClassObj) { - printClassInfo(doc, query, mSnapshot, (ClassObj)inst); + if (inst.isClassInstance()) { + printClassInstanceFields(doc, query, inst.asClassInstance()); + } else if (inst.isArrayInstance()) { + printArrayElements(doc, query, inst.asArrayInstance()); + } else if (inst.isClassObj()) { + printClassInfo(doc, query, inst.asClassObj()); } - printReferences(doc, query, mSnapshot, inst); + printReferences(doc, query, inst); printDominatedObjects(doc, query, inst); } - private static void printClassInstanceFields( - Doc doc, Query query, AhatSnapshot snapshot, ClassInstance inst) { + private static void printClassInstanceFields(Doc doc, Query query, AhatClassInstance inst) { doc.section("Fields"); - doc.table(new Column("Type"), new Column("Name"), new Column("Value")); - 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(snapshot, field.getValue())); + AhatInstance base = inst.getBaseline(); + List<FieldValue> fields = inst.getInstanceFields(); + if (!base.isPlaceHolder()) { + Diff.fields(fields, base.asClassInstance().getInstanceFields()); } - doc.end(); + SubsetSelector<FieldValue> selector = new SubsetSelector(query, INSTANCE_FIELDS_ID, fields); + printFields(doc, inst != base && !base.isPlaceHolder(), selector.selected()); selector.render(doc); } - private static void printArrayElements( - Doc doc, Query query, AhatSnapshot snapshot, ArrayInstance array) { + private static void printArrayElements(Doc doc, Query query, AhatArrayInstance array) { doc.section("Array Elements"); - doc.table(new Column("Index", Column.Align.RIGHT), new Column("Value")); - List<Object> elements = Arrays.asList(array.getValues()); - SubsetSelector<Object> selector = new SubsetSelector(query, ARRAY_ELEMENTS_ID, elements); + AhatInstance base = array.getBaseline(); + boolean diff = array.getBaseline() != array && !base.isPlaceHolder(); + doc.table( + new Column("Index", Column.Align.RIGHT), + new Column("Value"), + new Column("Δ", Column.Align.LEFT, diff)); + + List<Value> elements = array.getValues(); + SubsetSelector<Value> selector = new SubsetSelector(query, ARRAY_ELEMENTS_ID, elements); int i = 0; - for (Object elem : selector.selected()) { - doc.row(DocString.format("%d", i), Value.render(snapshot, elem)); + for (Value current : selector.selected()) { + DocString delta = new DocString(); + if (diff) { + Value previous = Value.getBaseline(base.asArrayInstance().getValue(i)); + if (!Objects.equals(current, previous)) { + delta.append("was "); + delta.append(Summarizer.summarize(previous)); + } + } + doc.row(DocString.format("%d", i), Summarizer.summarize(current), delta); i++; } doc.end(); selector.render(doc); } - private static void printClassInfo( - Doc doc, Query query, AhatSnapshot snapshot, ClassObj clsobj) { + private static void printFields(Doc doc, boolean diff, List<FieldValue> fields) { + doc.table( + new Column("Type"), + new Column("Name"), + new Column("Value"), + new Column("Δ", Column.Align.LEFT, diff)); + + for (FieldValue field : fields) { + Value current = field.getValue(); + DocString value; + if (field.isPlaceHolder()) { + value = DocString.removed("del"); + } else { + value = Summarizer.summarize(current); + } + + DocString delta = new DocString(); + FieldValue basefield = field.getBaseline(); + if (basefield.isPlaceHolder()) { + delta.append(DocString.added("new")); + } else { + Value previous = Value.getBaseline(basefield.getValue()); + if (!Objects.equals(current, previous)) { + delta.append("was "); + delta.append(Summarizer.summarize(previous)); + } + } + doc.row(DocString.text(field.getType()), DocString.text(field.getName()), value, delta); + } + doc.end(); + } + + private static void printClassInfo(Doc doc, Query query, AhatClassObj clsobj) { doc.section("Class Info"); doc.descriptions(); doc.description(DocString.text("Super Class"), - Value.render(snapshot, clsobj.getSuperClassObj())); + Summarizer.summarize(clsobj.getSuperClassObj())); doc.description(DocString.text("Class Loader"), - Value.render(snapshot, clsobj.getClassLoader())); + Summarizer.summarize(clsobj.getClassLoader())); doc.end(); doc.section("Static Fields"); - doc.table(new Column("Type"), new Column("Name"), new Column("Value")); - 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(snapshot, field.getValue())); + AhatInstance base = clsobj.getBaseline(); + List<FieldValue> fields = clsobj.getStaticFieldValues(); + if (!base.isPlaceHolder()) { + Diff.fields(fields, base.asClassObj().getStaticFieldValues()); } - doc.end(); + SubsetSelector<FieldValue> selector = new SubsetSelector(query, STATIC_FIELDS_ID, fields); + printFields(doc, clsobj != base && !base.isPlaceHolder(), selector.selected()); selector.render(doc); } - private static void printReferences( - Doc doc, Query query, AhatSnapshot snapshot, Instance inst) { + private static void printReferences(Doc doc, Query query, AhatInstance inst) { doc.section("Objects with References to this Object"); if (inst.getHardReverseReferences().isEmpty()) { doc.println(DocString.text("(none)")); } else { doc.table(new Column("Object")); - List<Instance> references = inst.getHardReverseReferences(); - SubsetSelector<Instance> selector = new SubsetSelector(query, HARD_REFS_ID, references); - for (Instance ref : selector.selected()) { - doc.row(Value.render(snapshot, ref)); + List<AhatInstance> references = inst.getHardReverseReferences(); + SubsetSelector<AhatInstance> selector = new SubsetSelector(query, HARD_REFS_ID, references); + for (AhatInstance ref : selector.selected()) { + doc.row(Summarizer.summarize(ref)); } doc.end(); selector.render(doc); } - if (inst.getSoftReverseReferences() != null) { + if (!inst.getSoftReverseReferences().isEmpty()) { doc.section("Objects with Soft References to this Object"); doc.table(new Column("Object")); - List<Instance> references = inst.getSoftReverseReferences(); - SubsetSelector<Instance> selector = new SubsetSelector(query, SOFT_REFS_ID, references); - for (Instance ref : selector.selected()) { - doc.row(Value.render(snapshot, ref)); + List<AhatInstance> references = inst.getSoftReverseReferences(); + SubsetSelector<AhatInstance> selector = new SubsetSelector(query, SOFT_REFS_ID, references); + for (AhatInstance ref : selector.selected()) { + doc.row(Summarizer.summarize(ref)); } doc.end(); selector.render(doc); } } - private void printAllocationSite(Doc doc, Query query, Instance inst) { + private void printAllocationSite(Doc doc, Query query, AhatInstance inst) { doc.section("Allocation Site"); - Site site = mSnapshot.getSiteForInstance(inst); + Site site = inst.getSite(); SitePrinter.printSite(mSnapshot, doc, query, ALLOCATION_SITE_ID, 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); + private static void printBitmap(Doc doc, AhatInstance inst) { + AhatInstance bitmap = inst.getAssociatedBitmapInstance(); if (bitmap != null) { doc.section("Bitmap Image"); doc.println(DocString.image( @@ -203,25 +246,25 @@ class ObjectHandler implements AhatHandler { } } - private void printGcRootPath(Doc doc, Query query, Instance inst) { + private void printGcRootPath(Doc doc, Query query, AhatInstance inst) { doc.section("Sample Path from GC Root"); - List<PathElement> path = InstanceUtils.getPathFromGcRoot(inst); + List<PathElement> path = inst.getPathFromGcRoot(); - // Add 'null' as a marker for the root. - path.add(0, null); + // Add a dummy PathElement as a marker for the root. + final PathElement root = new PathElement(null, null); + path.add(0, root); HeapTable.TableConfig<PathElement> table = new HeapTable.TableConfig<PathElement>() { public String getHeapsDescription() { return "Bytes Retained by Heap (Dominators Only)"; } - public long getSize(PathElement element, Heap heap) { - if (element == null) { - return mSnapshot.getHeapSize(heap); + public long getSize(PathElement element, AhatHeap heap) { + if (element == root) { + return heap.getSize(); } if (element.isDominator) { - int index = mSnapshot.getHeapIndex(heap); - return element.instance.getRetainedSize(index); + return element.instance.getRetainedSize(heap); } return 0; } @@ -233,11 +276,11 @@ class ObjectHandler implements AhatHandler { } public DocString render(PathElement element) { - if (element == null) { + if (element == root) { return DocString.link(DocString.uri("rooted"), DocString.text("ROOT")); } else { - DocString label = DocString.text(" → "); - label.append(Value.render(mSnapshot, element.instance)); + DocString label = DocString.text("→ "); + label.append(Summarizer.summarize(element.instance)); label.append(element.field); return label; } @@ -249,9 +292,9 @@ class ObjectHandler implements AhatHandler { HeapTable.render(doc, query, DOMINATOR_PATH_ID, table, mSnapshot, path); } - public void printDominatedObjects(Doc doc, Query query, Instance inst) { + public void printDominatedObjects(Doc doc, Query query, AhatInstance inst) { doc.section("Immediately Dominated Objects"); - List<Instance> instances = mSnapshot.getDominated(inst); + List<AhatInstance> instances = inst.getDominated(); if (instances != null) { DominatedList.render(mSnapshot, doc, query, DOMINATED_OBJECTS_ID, instances); } else { diff --git a/tools/ahat/src/ObjectsHandler.java b/tools/ahat/src/ObjectsHandler.java index 4cfb0a55cf..3062d23b53 100644 --- a/tools/ahat/src/ObjectsHandler.java +++ b/tools/ahat/src/ObjectsHandler.java @@ -16,7 +16,10 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Instance; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Site; +import com.android.ahat.heapdump.Sort; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -33,17 +36,16 @@ class ObjectsHandler implements AhatHandler { @Override public void handle(Doc doc, Query query) throws IOException { - int stackId = query.getInt("stack", 0); + int id = query.getInt("id", 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); + Site site = mSnapshot.getSite(id, depth); - List<Instance> insts = new ArrayList<Instance>(); - for (Instance inst : site.getObjects()) { + List<AhatInstance> insts = new ArrayList<AhatInstance>(); + for (AhatInstance inst : site.getObjects()) { if ((heapName == null || inst.getHeap().getName().equals(heapName)) - && (className == null - || AhatSnapshot.getClassName(inst.getClassObj()).equals(className))) { + && (className == null || inst.getClassName().equals(className))) { insts.add(inst); } } @@ -51,16 +53,22 @@ class ObjectsHandler implements AhatHandler { Collections.sort(insts, Sort.defaultInstanceCompare(mSnapshot)); doc.title("Objects"); + doc.table( new Column("Size", Column.Align.RIGHT), + new Column("Δ", Column.Align.RIGHT, mSnapshot.isDiffed()), new Column("Heap"), new Column("Object")); - SubsetSelector<Instance> selector = new SubsetSelector(query, OBJECTS_ID, insts); - for (Instance inst : selector.selected()) { + + SubsetSelector<AhatInstance> selector = new SubsetSelector(query, OBJECTS_ID, insts); + for (AhatInstance inst : selector.selected()) { + AhatInstance base = inst.getBaseline(); doc.row( - DocString.format("%,d", inst.getSize()), + DocString.format("%,14d", inst.getSize()), + DocString.delta(inst.isPlaceHolder(), base.isPlaceHolder(), + inst.getSize(), base.getSize()), DocString.text(inst.getHeap().getName()), - Value.render(mSnapshot, inst)); + Summarizer.summarize(inst)); } doc.end(); selector.render(doc); diff --git a/tools/ahat/src/OverviewHandler.java b/tools/ahat/src/OverviewHandler.java index 0dbad7e00c..ea305c4e94 100644 --- a/tools/ahat/src/OverviewHandler.java +++ b/tools/ahat/src/OverviewHandler.java @@ -16,9 +16,11 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Heap; -import java.io.IOException; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Diffable; import java.io.File; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -28,10 +30,12 @@ class OverviewHandler implements AhatHandler { private AhatSnapshot mSnapshot; private File mHprof; + private File mBaseHprof; - public OverviewHandler(AhatSnapshot snapshot, File hprof) { + public OverviewHandler(AhatSnapshot snapshot, File hprof, File basehprof) { mSnapshot = snapshot; mHprof = hprof; + mBaseHprof = basehprof; } @Override @@ -44,42 +48,40 @@ class OverviewHandler implements AhatHandler { DocString.text("ahat version"), DocString.format("ahat-%s", OverviewHandler.class.getPackage().getImplementationVersion())); doc.description(DocString.text("hprof file"), DocString.text(mHprof.toString())); + if (mBaseHprof != null) { + doc.description(DocString.text("baseline hprof file"), DocString.text(mBaseHprof.toString())); + } doc.end(); doc.section("Heap Sizes"); printHeapSizes(doc, query); - List<InstanceUtils.NativeAllocation> allocs = mSnapshot.getNativeAllocations(); - if (!allocs.isEmpty()) { - doc.section("Registered Native Allocations"); - long totalSize = 0; - for (InstanceUtils.NativeAllocation alloc : allocs) { - totalSize += alloc.size; - } - doc.descriptions(); - doc.description(DocString.text("Number of Registered Native Allocations"), - DocString.format("%,14d", allocs.size())); - doc.description(DocString.text("Total Size of Registered Native Allocations"), - DocString.format("%,14d", totalSize)); - doc.end(); + doc.big(Menu.getMenu()); + } + + private static class TableElem implements Diffable<TableElem> { + @Override public TableElem getBaseline() { + return this; } - doc.big(Menu.getMenu()); + @Override public boolean isPlaceHolder() { + return false; + } } private void printHeapSizes(Doc doc, Query query) { - List<Object> dummy = Collections.singletonList(null); + List<TableElem> dummy = Collections.singletonList(new TableElem()); - HeapTable.TableConfig<Object> table = new HeapTable.TableConfig<Object>() { + HeapTable.TableConfig<TableElem> table = new HeapTable.TableConfig<TableElem>() { public String getHeapsDescription() { return "Bytes Retained by Heap"; } - public long getSize(Object element, Heap heap) { - return mSnapshot.getHeapSize(heap); + public long getSize(TableElem element, AhatHeap heap) { + return heap.getSize(); } - public List<HeapTable.ValueConfig<Object>> getValueConfigs() { + public List<HeapTable.ValueConfig<TableElem>> getValueConfigs() { return Collections.emptyList(); } }; diff --git a/tools/ahat/src/RootedHandler.java b/tools/ahat/src/RootedHandler.java index ec3272fa92..26451a3963 100644 --- a/tools/ahat/src/RootedHandler.java +++ b/tools/ahat/src/RootedHandler.java @@ -16,6 +16,7 @@ package com.android.ahat; +import com.android.ahat.heapdump.AhatSnapshot; import java.io.IOException; class RootedHandler implements AhatHandler { diff --git a/tools/ahat/src/Site.java b/tools/ahat/src/Site.java deleted file mode 100644 index dbb84f600e..0000000000 --- a/tools/ahat/src/Site.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.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 index 839e220ca4..febf1713fb 100644 --- a/tools/ahat/src/SiteHandler.java +++ b/tools/ahat/src/SiteHandler.java @@ -16,7 +16,10 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Heap; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Site; +import com.android.ahat.heapdump.Sort; import java.io.IOException; import java.util.Collections; import java.util.Comparator; @@ -35,11 +38,13 @@ class SiteHandler implements AhatHandler { @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); + int id = query.getInt("id", 0); + int depth = query.getInt("depth", 0); + Site site = mSnapshot.getSite(id, depth); + + doc.title("Site"); + doc.big(Summarizer.summarize(site)); - doc.title("Site %s", site.getName()); doc.section("Allocation Site"); SitePrinter.printSite(mSnapshot, doc, query, ALLOCATION_SITE_ID, site); @@ -48,15 +53,14 @@ class SiteHandler implements AhatHandler { if (children.isEmpty()) { doc.println(DocString.text("(none)")); } else { - Collections.sort(children, new Sort.SiteBySize("app")); - + Collections.sort(children, Sort.defaultSiteCompare(mSnapshot)); 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 long getSize(Site element, AhatHeap heap) { + return element.getSize(heap); } public List<HeapTable.ValueConfig<Site>> getValueConfigs() { @@ -66,10 +70,7 @@ class SiteHandler implements AhatHandler { } public DocString render(Site element) { - return DocString.link( - DocString.formattedUri("site?stack=%d&depth=%d", - element.getStackId(), element.getStackDepth()), - DocString.text(element.getName())); + return Summarizer.summarize(element); } }; return Collections.singletonList(value); @@ -79,29 +80,36 @@ class SiteHandler implements AhatHandler { } doc.section("Objects Allocated"); + doc.table( new Column("Reachable Bytes Allocated", Column.Align.RIGHT), + new Column("Δ", Column.Align.RIGHT, mSnapshot.isDiffed()), new Column("Instances", Column.Align.RIGHT), + new Column("Δ", Column.Align.RIGHT, mSnapshot.isDiffed()), 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()); + Sort.OBJECTS_INFO_BY_HEAP_NAME, + Sort.OBJECTS_INFO_BY_SIZE, + Sort.OBJECTS_INFO_BY_CLASS_NAME); Collections.sort(infos, compare); SubsetSelector<Site.ObjectsInfo> selector = new SubsetSelector(query, OBJECTS_ALLOCATED_ID, infos); for (Site.ObjectsInfo info : selector.selected()) { - String className = AhatSnapshot.getClassName(info.classObj); + Site.ObjectsInfo baseinfo = info.getBaseline(); + String className = info.getClassName(); doc.row( DocString.format("%,14d", info.numBytes), + DocString.delta(false, false, info.numBytes, baseinfo.numBytes), DocString.link( - DocString.formattedUri("objects?stack=%d&depth=%d&heap=%s&class=%s", - site.getStackId(), site.getStackDepth(), info.heap.getName(), className), + DocString.formattedUri("objects?id=%d&depth=%d&heap=%s&class=%s", + site.getId(), site.getDepth(), info.heap.getName(), className), DocString.format("%,14d", info.numInstances)), + DocString.delta(false, false, info.numInstances, baseinfo.numInstances), DocString.text(info.heap.getName()), - Value.render(mSnapshot, info.classObj)); + Summarizer.summarize(info.classObj)); } doc.end(); selector.render(doc); diff --git a/tools/ahat/src/SitePrinter.java b/tools/ahat/src/SitePrinter.java index 2c06b47a29..21ca2deda4 100644 --- a/tools/ahat/src/SitePrinter.java +++ b/tools/ahat/src/SitePrinter.java @@ -16,7 +16,9 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Heap; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Site; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,8 +37,8 @@ class SitePrinter { return "Reachable Bytes Allocated on Heap"; } - public long getSize(Site element, Heap heap) { - return element.getSize(heap.getName()); + public long getSize(Site element, AhatHeap heap) { + return element.getSize(heap); } public List<HeapTable.ValueConfig<Site>> getValueConfigs() { @@ -50,11 +52,7 @@ class SitePrinter { if (element.getParent() != null) { str.append("→ "); } - str.appendLink( - DocString.formattedUri("site?stack=%d&depth=%d", - element.getStackId(), element.getStackDepth()), - DocString.text(element.getName())); - return str; + return str.append(Summarizer.summarize(element)); } }; return Collections.singletonList(value); diff --git a/tools/ahat/src/StaticHandler.java b/tools/ahat/src/StaticHandler.java index fb7049dcd4..b2805d624d 100644 --- a/tools/ahat/src/StaticHandler.java +++ b/tools/ahat/src/StaticHandler.java @@ -17,10 +17,10 @@ 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 com.sun.net.httpserver.HttpHandler; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; diff --git a/tools/ahat/src/Summarizer.java b/tools/ahat/src/Summarizer.java new file mode 100644 index 0000000000..016eab44f2 --- /dev/null +++ b/tools/ahat/src/Summarizer.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.Site; +import com.android.ahat.heapdump.Value; +import java.net.URI; + +/** + * Class for generating a DocString summary of an instance or value. + */ +class Summarizer { + + // For string literals, we limit the number of characters we show to + // kMaxChars in case the string is really long. + private static int kMaxChars = 200; + + /** + * Creates a DocString representing a summary of the given instance. + */ + public static DocString summarize(AhatInstance inst) { + DocString formatted = new DocString(); + if (inst == null) { + formatted.append("null"); + return formatted; + } + + // Annotate new objects as new. + if (inst.getBaseline().isPlaceHolder()) { + formatted.append(DocString.added("new ")); + } + + // Annotate deleted objects as deleted. + if (inst.isPlaceHolder()) { + formatted.append(DocString.removed("del ")); + } + + // Annotate unreachable objects as such. + if (!inst.isReachable()) { + formatted.append("unreachable "); + } + + // Annotate roots as roots. + if (inst.isRoot()) { + formatted.append("root "); + } + + // Annotate classes as classes. + DocString linkText = new DocString(); + if (inst.isClassObj()) { + linkText.append("class "); + } + + linkText.append(inst.toString()); + + if (inst.isPlaceHolder()) { + // Don't make links to placeholder objects. + formatted.append(linkText); + } else { + URI objTarget = DocString.formattedUri("object?id=%d", inst.getId()); + formatted.appendLink(objTarget, linkText); + } + + // Annotate Strings with their values. + String stringValue = inst.asString(kMaxChars); + if (stringValue != null) { + formatted.appendFormat(" \"%s", stringValue); + formatted.append(kMaxChars == stringValue.length() ? "..." : "\""); + } + + // Annotate Reference with its referent + AhatInstance referent = inst.getReferent(); + if (referent != null) { + formatted.append(" for "); + + // It should not be possible for a referent to refer back to the + // reference object, even indirectly, so there shouldn't be any issues + // with infinite recursion here. + formatted.append(summarize(referent)); + } + + // Annotate DexCache with its location. + String dexCacheLocation = inst.getDexCacheLocation(kMaxChars); + if (dexCacheLocation != null) { + formatted.appendFormat(" for %s", dexCacheLocation); + if (kMaxChars == dexCacheLocation.length()) { + formatted.append("..."); + } + } + + // Annotate bitmaps with a thumbnail. + AhatInstance bitmap = inst.getAssociatedBitmapInstance(); + String thumbnail = ""; + if (bitmap != null) { + URI uri = DocString.formattedUri("bitmap?id=%d", bitmap.getId()); + formatted.appendThumbnail(uri, "bitmap image"); + } + return formatted; + } + + /** + * Creates a DocString summarizing the given value. + */ + public static DocString summarize(Value value) { + if (value == null) { + return DocString.text("null"); + } + if (value.isAhatInstance()) { + return summarize(value.asAhatInstance()); + } + return DocString.text(value.toString()); + } + + /** + * Creates a DocString summarizing the given site. + */ + public static DocString summarize(Site site) { + DocString text = DocString.text(site.getMethodName()); + text.append(site.getSignature()); + text.append(" - "); + text.append(site.getFilename()); + if (site.getLineNumber() > 0) { + text.append(":").append(Integer.toString(site.getLineNumber())); + } + URI uri = DocString.formattedUri("site?id=%d&depth=%d", site.getId(), site.getDepth()); + return DocString.link(uri, text); + } +} diff --git a/tools/ahat/src/Value.java b/tools/ahat/src/Value.java deleted file mode 100644 index 847692bd10..0000000000 --- a/tools/ahat/src/Value.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.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 { - - // For string literals, we limit the number of characters we show to - // kMaxChars in case the string is really long. - private static int kMaxChars = 200; - - /** - * Create a DocString representing a summary of the given instance. - */ - private static DocString renderInstance(AhatSnapshot snapshot, Instance inst) { - DocString formatted = new DocString(); - if (inst == null) { - formatted.append("(null)"); - return formatted; - } - - // Annotate roots as roots. - if (snapshot.isRoot(inst)) { - formatted.append("(root) "); - } - - - // Annotate classes as classes. - DocString link = new DocString(); - if (inst instanceof ClassObj) { - link.append("class "); - } - - link.append(inst.toString()); - - URI objTarget = DocString.formattedUri("object?id=%d", inst.getId()); - formatted.appendLink(objTarget, link); - - // Annotate Strings with their values. - String stringValue = InstanceUtils.asString(inst, kMaxChars); - if (stringValue != null) { - formatted.appendFormat(" \"%s", stringValue); - formatted.append(kMaxChars == stringValue.length() ? "..." : "\""); - } - - // Annotate Reference with its referent - Instance referent = InstanceUtils.getReferent(inst); - if (referent != null) { - formatted.append(" for "); - - // It should not be possible for a referent to refer back to the - // reference object, even indirectly, so there shouldn't be any issues - // with infinite recursion here. - formatted.append(renderInstance(snapshot, referent)); - } - - // Annotate DexCache with its location. - String dexCacheLocation = InstanceUtils.getDexCacheLocation(inst, kMaxChars); - if (dexCacheLocation != null) { - formatted.appendFormat(" for %s", dexCacheLocation); - if (kMaxChars == dexCacheLocation.length()) { - formatted.append("..."); - } - } - - - // Annotate bitmaps with a thumbnail. - Instance bitmap = InstanceUtils.getAssociatedBitmapInstance(inst); - String thumbnail = ""; - if (bitmap != null) { - URI uri = DocString.formattedUri("bitmap?id=%d", bitmap.getId()); - formatted.appendThumbnail(uri, "bitmap image"); - } - return formatted; - } - - /** - * Create a DocString summarizing the given value. - */ - public static DocString render(AhatSnapshot snapshot, Object val) { - if (val instanceof Instance) { - return renderInstance(snapshot, (Instance)val); - } else { - return DocString.format("%s", val); - } - } -} diff --git a/tools/ahat/src/heapdump/AhatArrayInstance.java b/tools/ahat/src/heapdump/AhatArrayInstance.java new file mode 100644 index 0000000000..d88cf94075 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatArrayInstance.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import com.android.tools.perflib.heap.ArrayInstance; +import com.android.tools.perflib.heap.Instance; +import java.nio.charset.StandardCharsets; +import java.util.AbstractList; +import java.util.List; + +public class AhatArrayInstance extends AhatInstance { + // To save space, we store byte, character, and object arrays directly as + // byte, character, and AhatInstance arrays respectively. This is especially + // important for large byte arrays, such as bitmaps. All other array types + // are stored as an array of objects, though we could potentially save space + // by specializing those too. mValues is a list view of the underlying + // array. + private List<Value> mValues; + private byte[] mByteArray; // null if not a byte array. + private char[] mCharArray; // null if not a char array. + + public AhatArrayInstance(long id) { + super(id); + } + + @Override void initialize(AhatSnapshot snapshot, Instance inst) { + super.initialize(snapshot, inst); + + ArrayInstance array = (ArrayInstance)inst; + switch (array.getArrayType()) { + case OBJECT: + Object[] objects = array.getValues(); + final AhatInstance[] insts = new AhatInstance[objects.length]; + for (int i = 0; i < objects.length; i++) { + if (objects[i] != null) { + Instance ref = (Instance)objects[i]; + insts[i] = snapshot.findInstance(ref.getId()); + if (ref.getNextInstanceToGcRoot() == inst) { + String field = "[" + Integer.toString(i) + "]"; + insts[i].setNextInstanceToGcRoot(this, field); + } + } + } + mValues = new AbstractList<Value>() { + @Override public int size() { + return insts.length; + } + + @Override public Value get(int index) { + AhatInstance obj = insts[index]; + return obj == null ? null : new Value(insts[index]); + } + }; + break; + + case CHAR: + final char[] chars = array.asCharArray(0, array.getLength()); + mCharArray = chars; + mValues = new AbstractList<Value>() { + @Override public int size() { + return chars.length; + } + + @Override public Value get(int index) { + return new Value(chars[index]); + } + }; + break; + + case BYTE: + final byte[] bytes = array.asRawByteArray(0, array.getLength()); + mByteArray = bytes; + mValues = new AbstractList<Value>() { + @Override public int size() { + return bytes.length; + } + + @Override public Value get(int index) { + return new Value(bytes[index]); + } + }; + break; + + default: + final Object[] values = array.getValues(); + mValues = new AbstractList<Value>() { + @Override public int size() { + return values.length; + } + + @Override public Value get(int index) { + Object obj = values[index]; + return obj == null ? null : new Value(obj); + } + }; + break; + } + } + + /** + * Returns the length of the array. + */ + public int getLength() { + return mValues.size(); + } + + /** + * Returns the array's values. + */ + public List<Value> getValues() { + return mValues; + } + + /** + * Returns the object at the given index of this array. + */ + public Value getValue(int index) { + return mValues.get(index); + } + + @Override public boolean isArrayInstance() { + return true; + } + + @Override public AhatArrayInstance asArrayInstance() { + return this; + } + + @Override public String asString(int maxChars) { + return asString(0, getLength(), maxChars); + } + + /** + * Returns the String value associated with this array. + * Only char arrays are considered as having an associated String value. + */ + String asString(int offset, int count, int maxChars) { + if (mCharArray == null) { + return null; + } + + if (count == 0) { + return ""; + } + int numChars = mCharArray.length; + if (0 <= maxChars && maxChars < count) { + count = maxChars; + } + + int end = offset + count - 1; + if (offset >= 0 && offset < numChars && end >= 0 && end < numChars) { + return new String(mCharArray, offset, count); + } + return null; + } + + /** + * Returns the ascii String value associated with this array. + * Only byte arrays are considered as having an associated ascii String value. + */ + String asAsciiString(int offset, int count, int maxChars) { + if (mByteArray == null) { + return null; + } + + if (count == 0) { + return ""; + } + int numChars = mByteArray.length; + if (0 <= maxChars && maxChars < count) { + count = maxChars; + } + + int end = offset + count - 1; + if (offset >= 0 && offset < numChars && end >= 0 && end < numChars) { + return new String(mByteArray, offset, count, StandardCharsets.US_ASCII); + } + return null; + } + + /** + * Returns the String value associated with this array. Byte arrays are + * considered as ascii encoded strings. + */ + String asMaybeCompressedString(int offset, int count, int maxChars) { + String str = asString(offset, count, maxChars); + if (str == null) { + str = asAsciiString(offset, count, maxChars); + } + return str; + } + + @Override public AhatInstance getAssociatedBitmapInstance() { + if (mByteArray != null) { + List<AhatInstance> refs = getHardReverseReferences(); + if (refs.size() == 1) { + AhatInstance ref = refs.get(0); + return ref.getAssociatedBitmapInstance(); + } + } + return null; + } + + @Override public String toString() { + String className = getClassName(); + if (className.endsWith("[]")) { + className = className.substring(0, className.length() - 2); + } + return String.format("%s[%d]@%08x", className, mValues.size(), getId()); + } + + byte[] asByteArray() { + return mByteArray; + } +} diff --git a/tools/ahat/src/heapdump/AhatClassInstance.java b/tools/ahat/src/heapdump/AhatClassInstance.java new file mode 100644 index 0000000000..273530af64 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatClassInstance.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import com.android.tools.perflib.heap.ClassInstance; +import com.android.tools.perflib.heap.Instance; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.List; + +public class AhatClassInstance extends AhatInstance { + private FieldValue[] mFieldValues; + + public AhatClassInstance(long id) { + super(id); + } + + @Override void initialize(AhatSnapshot snapshot, Instance inst) { + super.initialize(snapshot, inst); + + ClassInstance classInst = (ClassInstance)inst; + List<ClassInstance.FieldValue> fieldValues = classInst.getValues(); + mFieldValues = new FieldValue[fieldValues.size()]; + for (int i = 0; i < mFieldValues.length; i++) { + ClassInstance.FieldValue field = fieldValues.get(i); + String name = field.getField().getName(); + String type = field.getField().getType().toString(); + Value value = snapshot.getValue(field.getValue()); + + mFieldValues[i] = new FieldValue(name, type, value); + + if (field.getValue() instanceof Instance) { + Instance ref = (Instance)field.getValue(); + if (ref.getNextInstanceToGcRoot() == inst) { + value.asAhatInstance().setNextInstanceToGcRoot(this, "." + name); + } + } + } + } + + @Override public Value getField(String fieldName) { + for (FieldValue field : mFieldValues) { + if (fieldName.equals(field.getName())) { + return field.getValue(); + } + } + return null; + } + + @Override public AhatInstance getRefField(String fieldName) { + Value value = getField(fieldName); + return value == null ? null : value.asAhatInstance(); + } + + /** + * Read an int field of an instance. + * The field is assumed to be an int type. + * Returns <code>def</code> if the field value is not an int or could not be + * read. + */ + private Integer getIntField(String fieldName, Integer def) { + Value value = getField(fieldName); + if (value == null || !value.isInteger()) { + return def; + } + return value.asInteger(); + } + + /** + * Read a long field of this instance. + * The field is assumed to be a long type. + * Returns <code>def</code> if the field value is not an long or could not + * be read. + */ + private Long getLongField(String fieldName, Long def) { + Value value = getField(fieldName); + if (value == null || !value.isLong()) { + return def; + } + return value.asLong(); + } + + /** + * Returns the list of class instance fields for this instance. + */ + public List<FieldValue> getInstanceFields() { + return Arrays.asList(mFieldValues); + } + + /** + * Returns true if this is an instance of a class with the given name. + */ + private boolean isInstanceOfClass(String className) { + AhatClassObj cls = getClassObj(); + while (cls != null) { + if (className.equals(cls.getName())) { + return true; + } + cls = cls.getSuperClassObj(); + } + return false; + } + + @Override public String asString(int maxChars) { + if (!isInstanceOfClass("java.lang.String")) { + return null; + } + + Value value = getField("value"); + if (!value.isAhatInstance()) { + return null; + } + + AhatInstance inst = value.asAhatInstance(); + if (inst.isArrayInstance()) { + AhatArrayInstance chars = inst.asArrayInstance(); + int numChars = chars.getLength(); + int count = getIntField("count", numChars); + int offset = getIntField("offset", 0); + return chars.asMaybeCompressedString(offset, count, maxChars); + } + return null; + } + + @Override public AhatInstance getReferent() { + if (isInstanceOfClass("java.lang.ref.Reference")) { + return getRefField("referent"); + } + return null; + } + + @Override public String getDexCacheLocation(int maxChars) { + if (isInstanceOfClass("java.lang.DexCache")) { + AhatInstance location = getRefField("location"); + if (location != null) { + return location.asString(maxChars); + } + } + return null; + } + + @Override public AhatInstance getAssociatedBitmapInstance() { + if (isInstanceOfClass("android.graphics.Bitmap")) { + return this; + } + return null; + } + + @Override public boolean isClassInstance() { + return true; + } + + @Override public AhatClassInstance asClassInstance() { + return this; + } + + @Override public String toString() { + return String.format("%s@%08x", getClassName(), getId()); + } + + /** + * 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 byte[] getByteArrayField(String fieldName) { + Value value = getField(fieldName); + if (!value.isAhatInstance()) { + return null; + } + return value.asAhatInstance().asByteArray(); + } + + public BufferedImage asBitmap() { + if (!isInstanceOfClass("android.graphics.Bitmap")) { + return null; + } + + Integer width = getIntField("mWidth", null); + if (width == null) { + return null; + } + + Integer height = getIntField("mHeight", null); + if (height == null) { + return null; + } + + byte[] buffer = getByteArrayField("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; + } +} diff --git a/tools/ahat/src/heapdump/AhatClassObj.java b/tools/ahat/src/heapdump/AhatClassObj.java new file mode 100644 index 0000000000..c5ade1d405 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatClassObj.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import com.android.tools.perflib.heap.ClassObj; +import com.android.tools.perflib.heap.Field; +import com.android.tools.perflib.heap.Instance; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class AhatClassObj extends AhatInstance { + private String mClassName; + private AhatClassObj mSuperClassObj; + private AhatInstance mClassLoader; + private FieldValue[] mStaticFieldValues; + + public AhatClassObj(long id) { + super(id); + } + + @Override void initialize(AhatSnapshot snapshot, Instance inst) { + super.initialize(snapshot, inst); + + ClassObj classObj = (ClassObj)inst; + mClassName = classObj.getClassName(); + + ClassObj superClassObj = classObj.getSuperClassObj(); + if (superClassObj != null) { + mSuperClassObj = snapshot.findClassObj(superClassObj.getId()); + } + + Instance loader = classObj.getClassLoader(); + if (loader != null) { + mClassLoader = snapshot.findInstance(loader.getId()); + } + + Collection<Map.Entry<Field, Object>> fieldValues = classObj.getStaticFieldValues().entrySet(); + mStaticFieldValues = new FieldValue[fieldValues.size()]; + int index = 0; + for (Map.Entry<Field, Object> field : fieldValues) { + String name = field.getKey().getName(); + String type = field.getKey().getType().toString(); + Value value = snapshot.getValue(field.getValue()); + mStaticFieldValues[index++] = new FieldValue(name, type, value); + + if (field.getValue() instanceof Instance) { + Instance ref = (Instance)field.getValue(); + if (ref.getNextInstanceToGcRoot() == inst) { + value.asAhatInstance().setNextInstanceToGcRoot(this, "." + name); + } + } + } + } + + /** + * Returns the name of the class this is a class object for. + */ + public String getName() { + return mClassName; + } + + /** + * Returns the superclass of this class object. + */ + public AhatClassObj getSuperClassObj() { + return mSuperClassObj; + } + + /** + * Returns the class loader of this class object. + */ + public AhatInstance getClassLoader() { + return mClassLoader; + } + + /** + * Returns the static field values for this class object. + */ + public List<FieldValue> getStaticFieldValues() { + return Arrays.asList(mStaticFieldValues); + } + + @Override public boolean isClassObj() { + return true; + } + + @Override public AhatClassObj asClassObj() { + return this; + } + + @Override public String toString() { + return mClassName; + } + + @Override AhatInstance newPlaceHolderInstance() { + return new AhatPlaceHolderClassObj(this); + } +} + diff --git a/tools/ahat/src/heapdump/AhatField.java b/tools/ahat/src/heapdump/AhatField.java new file mode 100644 index 0000000000..a25ee2869d --- /dev/null +++ b/tools/ahat/src/heapdump/AhatField.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +public class AhatField { + private final String mName; + private final String mType; + + public AhatField(String name, String type) { + mName = name; + mType = type; + } + + /** + * Returns the name of the field. + */ + public String getName() { + return mName; + } + + /** + * Returns a description of the type of the field. + */ + public String getType() { + return mType; + } +} + diff --git a/tools/ahat/src/heapdump/AhatHeap.java b/tools/ahat/src/heapdump/AhatHeap.java new file mode 100644 index 0000000000..c39adc4b41 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatHeap.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +public class AhatHeap implements Diffable<AhatHeap> { + private String mName; + private long mSize = 0; + private int mIndex; + private AhatHeap mBaseline; + private boolean mIsPlaceHolder = false; + + AhatHeap(String name, int index) { + mName = name; + mIndex = index; + mBaseline = this; + } + + /** + * Construct a place holder heap. + */ + private AhatHeap(String name, AhatHeap baseline) { + mName = name; + mIndex = -1; + mBaseline = baseline; + baseline.setBaseline(this); + mIsPlaceHolder = true; + } + + /** + * Construct a new place holder heap that has the given baseline heap. + */ + static AhatHeap newPlaceHolderHeap(String name, AhatHeap baseline) { + return new AhatHeap(name, baseline); + } + + void addToSize(long increment) { + mSize += increment; + } + + /** + * Returns a unique instance for this heap between 0 and the total number of + * heaps in this snapshot, or -1 if this is a placeholder heap. + */ + int getIndex() { + return mIndex; + } + + /** + * Returns the name of this heap. + */ + public String getName() { + return mName; + } + + /** + * Returns the total number of bytes allocated on this heap. + */ + public long getSize() { + return mSize; + } + + void setBaseline(AhatHeap baseline) { + mBaseline = baseline; + } + + @Override + public AhatHeap getBaseline() { + return mBaseline; + } + + @Override + public boolean isPlaceHolder() { + return mIsPlaceHolder; + } +} diff --git a/tools/ahat/src/heapdump/AhatInstance.java b/tools/ahat/src/heapdump/AhatInstance.java new file mode 100644 index 0000000000..e6b9c00384 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatInstance.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import com.android.tools.perflib.heap.ClassObj; +import com.android.tools.perflib.heap.Instance; +import com.android.tools.perflib.heap.RootObj; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public abstract class AhatInstance implements Diffable<AhatInstance> { + private long mId; + private long mSize; + private long mTotalRetainedSize; + private long mRetainedSizes[]; // Retained size indexed by heap index + private boolean mIsReachable; + private AhatHeap mHeap; + private AhatInstance mImmediateDominator; + private AhatInstance mNextInstanceToGcRoot; + private String mNextInstanceToGcRootField = "???"; + private AhatClassObj mClassObj; + private AhatInstance[] mHardReverseReferences; + private AhatInstance[] mSoftReverseReferences; + private Site mSite; + + // If this instance is a root, mRootTypes contains a set of the root types. + // If this instance is not a root, mRootTypes is null. + private List<String> mRootTypes; + + // List of instances this instance immediately dominates. + private List<AhatInstance> mDominated = new ArrayList<AhatInstance>(); + + private AhatInstance mBaseline; + + public AhatInstance(long id) { + mId = id; + mBaseline = this; + } + + /** + * Initializes this AhatInstance based on the given perflib instance. + * The AhatSnapshot should be used to look up AhatInstances and AhatHeaps. + * There is no guarantee that the AhatInstances returned by + * snapshot.findInstance have been initialized yet. + */ + void initialize(AhatSnapshot snapshot, Instance inst) { + mId = inst.getId(); + mSize = inst.getSize(); + mTotalRetainedSize = inst.getTotalRetainedSize(); + mIsReachable = inst.isReachable(); + + List<AhatHeap> heaps = snapshot.getHeaps(); + mRetainedSizes = new long[heaps.size()]; + for (AhatHeap heap : heaps) { + mRetainedSizes[heap.getIndex()] = inst.getRetainedSize(heap.getIndex()); + } + + mHeap = snapshot.getHeap(inst.getHeap().getName()); + + Instance dom = inst.getImmediateDominator(); + if (dom == null || dom instanceof RootObj) { + mImmediateDominator = null; + } else { + mImmediateDominator = snapshot.findInstance(dom.getId()); + mImmediateDominator.mDominated.add(this); + } + + ClassObj clsObj = inst.getClassObj(); + if (clsObj != null) { + mClassObj = snapshot.findClassObj(clsObj.getId()); + } + + // A couple notes about reverse references: + // * perflib sometimes returns unreachable reverse references. If + // snapshot.findInstance returns null, it means the reverse reference is + // not reachable, so we filter it out. + // * We store the references as AhatInstance[] instead of + // ArrayList<AhatInstance> because it saves a lot of space and helps + // with performance when there are a lot of AhatInstances. + ArrayList<AhatInstance> ahatRefs = new ArrayList<AhatInstance>(); + ahatRefs = new ArrayList<AhatInstance>(); + for (Instance ref : inst.getHardReverseReferences()) { + AhatInstance ahat = snapshot.findInstance(ref.getId()); + if (ahat != null) { + ahatRefs.add(ahat); + } + } + mHardReverseReferences = new AhatInstance[ahatRefs.size()]; + ahatRefs.toArray(mHardReverseReferences); + + List<Instance> refs = inst.getSoftReverseReferences(); + ahatRefs.clear(); + if (refs != null) { + for (Instance ref : refs) { + AhatInstance ahat = snapshot.findInstance(ref.getId()); + if (ahat != null) { + ahatRefs.add(ahat); + } + } + } + mSoftReverseReferences = new AhatInstance[ahatRefs.size()]; + ahatRefs.toArray(mSoftReverseReferences); + } + + /** + * Returns a unique identifier for the instance. + */ + public long getId() { + return mId; + } + + /** + * Returns the shallow number of bytes this object takes up. + */ + public long getSize() { + return mSize; + } + + /** + * Returns the number of bytes belonging to the given heap that this instance + * retains. + */ + public long getRetainedSize(AhatHeap heap) { + int index = heap.getIndex(); + return 0 <= index && index < mRetainedSizes.length ? mRetainedSizes[heap.getIndex()] : 0; + } + + /** + * Returns the total number of bytes this instance retains. + */ + public long getTotalRetainedSize() { + return mTotalRetainedSize; + } + + /** + * Returns whether this object is strongly-reachable. + */ + public boolean isReachable() { + return mIsReachable; + } + + /** + * Returns the heap that this instance is allocated on. + */ + public AhatHeap getHeap() { + return mHeap; + } + + /** + * Returns true if this instance is marked as a root instance. + */ + public boolean isRoot() { + return mRootTypes != null; + } + + /** + * Marks this instance as being a root of the given type. + */ + void addRootType(String type) { + if (mRootTypes == null) { + mRootTypes = new ArrayList<String>(); + mRootTypes.add(type); + } else if (!mRootTypes.contains(type)) { + mRootTypes.add(type); + } + } + + /** + * Returns a list of string descriptions of the root types of this object. + * Returns null if this object is not a root. + */ + public Collection<String> getRootTypes() { + return mRootTypes; + } + + /** + * Returns the immediate dominator of this instance. + * Returns null if this is a root instance. + */ + public AhatInstance getImmediateDominator() { + return mImmediateDominator; + } + + /** + * Returns a list of those objects immediately dominated by the given + * instance. + */ + public List<AhatInstance> getDominated() { + return mDominated; + } + + /** + * Returns the site where this instance was allocated. + */ + public Site getSite() { + return mSite; + } + + /** + * Sets the allocation site of this instance. + */ + void setSite(Site site) { + mSite = site; + } + + /** + * Returns true if the given instance is a class object + */ + public boolean isClassObj() { + // Overridden by AhatClassObj. + return false; + } + + /** + * Returns this as an AhatClassObj if this is an AhatClassObj. + * Returns null if this is not an AhatClassObj. + */ + public AhatClassObj asClassObj() { + // Overridden by AhatClassObj. + return null; + } + + /** + * Returns the class object instance for the class of this object. + */ + public AhatClassObj getClassObj() { + return mClassObj; + } + + /** + * Returns the name of the class this object belongs to. + */ + public String getClassName() { + AhatClassObj classObj = getClassObj(); + return classObj == null ? "???" : classObj.getName(); + } + + /** + * Returns true if the given instance is an array instance + */ + public boolean isArrayInstance() { + // Overridden by AhatArrayInstance. + return false; + } + + /** + * Returns this as an AhatArrayInstance if this is an AhatArrayInstance. + * Returns null if this is not an AhatArrayInstance. + */ + public AhatArrayInstance asArrayInstance() { + // Overridden by AhatArrayInstance. + return null; + } + + /** + * Returns true if the given instance is a class instance + */ + public boolean isClassInstance() { + return false; + } + + /** + * Returns this as an AhatClassInstance if this is an AhatClassInstance. + * Returns null if this is not an AhatClassInstance. + */ + public AhatClassInstance asClassInstance() { + return null; + } + + /** + * Return the referent associated with this instance. + * This is relevent for instances of java.lang.ref.Reference. + * Returns null if the instance has no referent associated with it. + */ + public AhatInstance getReferent() { + // Overridden by AhatClassInstance. + return null; + } + + /** + * Returns a list of objects with hard references to this object. + */ + public List<AhatInstance> getHardReverseReferences() { + return Arrays.asList(mHardReverseReferences); + } + + /** + * Returns a list of objects with soft references to this object. + */ + public List<AhatInstance> getSoftReverseReferences() { + return Arrays.asList(mSoftReverseReferences); + } + + /** + * Returns the value of a field of an instance. + * Returns null if the field value is null, the field couldn't be read, or + * there are multiple fields with the same name. + */ + public Value getField(String fieldName) { + // Overridden by AhatClassInstance. + return null; + } + + /** + * Reads a reference field of this instance. + * Returns null if the field value is null, or if the field couldn't be read. + */ + public AhatInstance getRefField(String fieldName) { + // Overridden by AhatClassInstance. + 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. + * If maxChars is non-negative, the returned location is truncated to + * maxChars in length. + */ + public String getDexCacheLocation(int maxChars) { + return null; + } + + /** + * 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 AhatInstance getAssociatedBitmapInstance() { + return null; + } + + /** + * Read the string value from this instance. + * Returns null if this object can't be interpreted as a string. + * The returned string is truncated to maxChars characters. + * If maxChars is negative, the returned string is not truncated. + */ + public String asString(int maxChars) { + // By default instances can't be interpreted as a string. This method is + // overridden by AhatClassInstance and AhatArrayInstance for those cases + // when an instance can be interpreted as a string. + return null; + } + + /** + * Reads the string value from an hprof Instance. + * Returns null if the object can't be interpreted as a string. + */ + public String asString() { + return asString(-1); + } + + /** + * Return the bitmap associated with the given instance, if any. + * This is relevant for instances of android.graphics.Bitmap and byte[]. + * Returns null if there is no bitmap associated with the given instance. + */ + public BufferedImage asBitmap() { + return null; + } + + /** + * Returns a sample path from a GC root to this instance. + * This instance is included as the last element of the path with an empty + * field description. + */ + public List<PathElement> getPathFromGcRoot() { + List<PathElement> path = new ArrayList<PathElement>(); + + AhatInstance dom = this; + for (PathElement elem = new PathElement(this, ""); elem != null; + elem = getNextPathElementToGcRoot(elem.instance)) { + if (elem.instance.equals(dom)) { + elem.isDominator = true; + dom = dom.getImmediateDominator(); + } + path.add(elem); + } + Collections.reverse(path); + return path; + } + + /** + * Returns the next instance to GC root from this object and a string + * description of which field of that object refers to the given instance. + * Returns null if the given instance has no next instance to the gc root. + */ + private static PathElement getNextPathElementToGcRoot(AhatInstance inst) { + AhatInstance parent = inst.mNextInstanceToGcRoot; + if (parent == null) { + return null; + } + return new PathElement(inst.mNextInstanceToGcRoot, inst.mNextInstanceToGcRootField); + } + + void setNextInstanceToGcRoot(AhatInstance inst, String field) { + mNextInstanceToGcRoot = inst; + mNextInstanceToGcRootField = field; + } + + /** Returns a human-readable identifier for this object. + * For class objects, the string is the class name. + * For class instances, the string is the class name followed by '@' and the + * hex id of the instance. + * For array instances, the string is the array type followed by the size in + * square brackets, followed by '@' and the hex id of the instance. + */ + @Override public abstract String toString(); + + /** + * Read the byte[] value from an hprof Instance. + * Returns null if the instance is not a byte array. + */ + byte[] asByteArray() { + return null; + } + + public void setBaseline(AhatInstance baseline) { + mBaseline = baseline; + } + + @Override public AhatInstance getBaseline() { + return mBaseline; + } + + @Override public boolean isPlaceHolder() { + return false; + } + + /** + * Returns a new place holder instance corresponding to this instance. + */ + AhatInstance newPlaceHolderInstance() { + return new AhatPlaceHolderInstance(this); + } +} diff --git a/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java b/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java new file mode 100644 index 0000000000..c6ad87fda5 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatPlaceHolderClassObj.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +/** + * PlaceHolder instance to take the place of a real AhatClassObj for + * the purposes of displaying diffs. + * + * This should be created through a call to newPlaceHolder(); + */ +public class AhatPlaceHolderClassObj extends AhatClassObj { + AhatPlaceHolderClassObj(AhatClassObj baseline) { + super(-1); + setBaseline(baseline); + baseline.setBaseline(this); + } + + @Override public long getSize() { + return 0; + } + + @Override public long getRetainedSize(AhatHeap heap) { + return 0; + } + + @Override public long getTotalRetainedSize() { + return 0; + } + + @Override public AhatHeap getHeap() { + return getBaseline().getHeap().getBaseline(); + } + + @Override public String getClassName() { + return getBaseline().getClassName(); + } + + @Override public String toString() { + return getBaseline().toString(); + } + + @Override public boolean isPlaceHolder() { + return true; + } + + @Override public String getName() { + return getBaseline().asClassObj().getName(); + } + + @Override public AhatClassObj getSuperClassObj() { + return getBaseline().asClassObj().getSuperClassObj().getBaseline().asClassObj(); + } + + @Override public AhatInstance getClassLoader() { + return getBaseline().asClassObj().getClassLoader().getBaseline(); + } +} diff --git a/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java b/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java new file mode 100644 index 0000000000..9412eae9a1 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatPlaceHolderInstance.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +/** + * Generic PlaceHolder instance to take the place of a real AhatInstance for + * the purposes of displaying diffs. + * + * This should be created through a call to AhatInstance.newPlaceHolder(); + */ +public class AhatPlaceHolderInstance extends AhatInstance { + AhatPlaceHolderInstance(AhatInstance baseline) { + super(-1); + setBaseline(baseline); + baseline.setBaseline(this); + } + + @Override public long getSize() { + return 0; + } + + @Override public long getRetainedSize(AhatHeap heap) { + return 0; + } + + @Override public long getTotalRetainedSize() { + return 0; + } + + @Override public AhatHeap getHeap() { + return getBaseline().getHeap().getBaseline(); + } + + @Override public String getClassName() { + return getBaseline().getClassName(); + } + + @Override public String asString(int maxChars) { + return getBaseline().asString(maxChars); + } + + @Override public String toString() { + return getBaseline().toString(); + } + + @Override public boolean isPlaceHolder() { + return true; + } +} diff --git a/tools/ahat/src/heapdump/AhatSnapshot.java b/tools/ahat/src/heapdump/AhatSnapshot.java new file mode 100644 index 0000000000..20b85da763 --- /dev/null +++ b/tools/ahat/src/heapdump/AhatSnapshot.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import com.android.tools.perflib.captures.DataBuffer; +import com.android.tools.perflib.captures.MemoryMappedFileBuffer; +import com.android.tools.perflib.heap.ArrayInstance; +import com.android.tools.perflib.heap.ClassInstance; +import com.android.tools.perflib.heap.ClassObj; +import com.android.tools.perflib.heap.Heap; +import com.android.tools.perflib.heap.Instance; +import com.android.tools.perflib.heap.ProguardMap; +import com.android.tools.perflib.heap.RootObj; +import com.android.tools.perflib.heap.Snapshot; +import com.android.tools.perflib.heap.StackFrame; +import com.android.tools.perflib.heap.StackTrace; +import gnu.trove.TObjectProcedure; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AhatSnapshot implements Diffable<AhatSnapshot> { + private final Site mRootSite = new Site("ROOT"); + + // Collection of objects whose immediate dominator is the SENTINEL_ROOT. + private final List<AhatInstance> mRooted = new ArrayList<AhatInstance>(); + + // List of all ahat instances stored in increasing order by id. + private final List<AhatInstance> mInstances = new ArrayList<AhatInstance>(); + + // Map from class name to class object. + private final Map<String, AhatClassObj> mClasses = new HashMap<String, AhatClassObj>(); + + private final List<AhatHeap> mHeaps = new ArrayList<AhatHeap>(); + + private AhatSnapshot mBaseline = this; + + /** + * Create an AhatSnapshot from an hprof file. + */ + public static AhatSnapshot fromHprof(File hprof, ProguardMap map) throws IOException { + return fromDataBuffer(new MemoryMappedFileBuffer(hprof), map); + } + + /** + * Create an AhatSnapshot from an in-memory data buffer. + */ + public static AhatSnapshot fromDataBuffer(DataBuffer buffer, ProguardMap map) throws IOException { + AhatSnapshot snapshot = new AhatSnapshot(buffer, map); + + // Request a GC now to clean up memory used by perflib. This helps to + // avoid a noticable pause when visiting the first interesting page in + // ahat. + System.gc(); + + return snapshot; + } + + /** + * Constructs an AhatSnapshot for the given hprof binary data. + */ + private AhatSnapshot(DataBuffer buffer, ProguardMap map) throws IOException { + Snapshot snapshot = Snapshot.createSnapshot(buffer, map); + snapshot.computeDominators(); + + // Properly label the class of class objects in the perflib snapshot, and + // count the total number of instances. + final ClassObj javaLangClass = snapshot.findClass("java.lang.Class"); + if (javaLangClass != null) { + for (Heap heap : snapshot.getHeaps()) { + Collection<ClassObj> classes = heap.getClasses(); + for (ClassObj clsObj : classes) { + if (clsObj.getClassObj() == null) { + clsObj.setClassId(javaLangClass.getId()); + } + } + } + } + + // Create mappings from id to ahat instance and heaps. + Collection<Heap> heaps = snapshot.getHeaps(); + for (Heap heap : heaps) { + // Note: mHeaps will not be in index order if snapshot.getHeaps does not + // return heaps in index order. That's fine, because we don't rely on + // mHeaps being in index order. + mHeaps.add(new AhatHeap(heap.getName(), snapshot.getHeapIndex(heap))); + TObjectProcedure<Instance> doCreate = new TObjectProcedure<Instance>() { + @Override + public boolean execute(Instance inst) { + long id = inst.getId(); + if (inst instanceof ClassInstance) { + mInstances.add(new AhatClassInstance(id)); + } else if (inst instanceof ArrayInstance) { + mInstances.add(new AhatArrayInstance(id)); + } else if (inst instanceof ClassObj) { + AhatClassObj classObj = new AhatClassObj(id); + mInstances.add(classObj); + mClasses.put(((ClassObj)inst).getClassName(), classObj); + } + return true; + } + }; + for (Instance instance : heap.getClasses()) { + doCreate.execute(instance); + } + heap.forEachInstance(doCreate); + } + + // Sort the instances by id so we can use binary search to lookup + // instances by id. + mInstances.sort(new Comparator<AhatInstance>() { + @Override + public int compare(AhatInstance a, AhatInstance b) { + return Long.compare(a.getId(), b.getId()); + } + }); + + // Initialize ahat snapshot and instances based on the perflib snapshot + // and instances. + for (AhatInstance ahat : mInstances) { + Instance inst = snapshot.findInstance(ahat.getId()); + ahat.initialize(this, inst); + + if (inst.getImmediateDominator() == Snapshot.SENTINEL_ROOT) { + mRooted.add(ahat); + } + + if (inst.isReachable()) { + ahat.getHeap().addToSize(ahat.getSize()); + } + + // Update sites. + StackFrame[] frames = null; + StackTrace stack = inst.getStack(); + if (stack != null) { + frames = stack.getFrames(); + } + Site site = mRootSite.add(frames, frames == null ? 0 : frames.length, ahat); + ahat.setSite(site); + } + + // Record the roots and their types. + for (RootObj root : snapshot.getGCRoots()) { + Instance inst = root.getReferredInstance(); + if (inst != null) { + findInstance(inst.getId()).addRootType(root.getRootType().toString()); + } + } + snapshot.dispose(); + } + + /** + * Returns the instance with given id in this snapshot. + * Returns null if no instance with the given id is found. + */ + public AhatInstance findInstance(long id) { + // Binary search over the sorted instances. + int start = 0; + int end = mInstances.size(); + while (start < end) { + int mid = start + ((end - start) / 2); + AhatInstance midInst = mInstances.get(mid); + long midId = midInst.getId(); + if (id == midId) { + return midInst; + } else if (id < midId) { + end = mid; + } else { + start = mid + 1; + } + } + return null; + } + + /** + * Returns the AhatClassObj with given id in this snapshot. + * Returns null if no class object with the given id is found. + */ + public AhatClassObj findClassObj(long id) { + AhatInstance inst = findInstance(id); + return inst == null ? null : inst.asClassObj(); + } + + /** + * Returns the class object for the class with given name. + * Returns null if there is no class object for the given name. + * Note: This method is exposed for testing purposes. + */ + public AhatClassObj findClass(String name) { + return mClasses.get(name); + } + + /** + * Returns the heap with the given name, if any. + * Returns null if no heap with the given name could be found. + */ + public AhatHeap getHeap(String name) { + // We expect a small number of heaps (maybe 3 or 4 total), so a linear + // search should be acceptable here performance wise. + for (AhatHeap heap : getHeaps()) { + if (heap.getName().equals(name)) { + return heap; + } + } + return null; + } + + /** + * Returns a list of heaps in the snapshot in canonical order. + * Modifications to the returned list are visible to this AhatSnapshot, + * which is used by diff to insert place holder heaps. + */ + public List<AhatHeap> getHeaps() { + return mHeaps; + } + + /** + * Returns a collection of instances whose immediate dominator is the + * SENTINEL_ROOT. + */ + public List<AhatInstance> getRooted() { + return mRooted; + } + + /** + * Returns the root site for this snapshot. + */ + public Site getRootSite() { + return mRootSite; + } + + // Get the site associated with the given id and depth. + // Returns the root site if no such site found. + public Site getSite(int id, int depth) { + AhatInstance obj = findInstance(id); + if (obj == null) { + return mRootSite; + } + + Site site = obj.getSite(); + for (int i = 0; i < depth && site.getParent() != null; i++) { + site = site.getParent(); + } + return site; + } + + // Return the Value for the given perflib value object. + Value getValue(Object value) { + if (value instanceof Instance) { + value = findInstance(((Instance)value).getId()); + } + return value == null ? null : new Value(value); + } + + public void setBaseline(AhatSnapshot baseline) { + mBaseline = baseline; + } + + /** + * Returns true if this snapshot has been diffed against another, different + * snapshot. + */ + public boolean isDiffed() { + return mBaseline != this; + } + + @Override public AhatSnapshot getBaseline() { + return mBaseline; + } + + @Override public boolean isPlaceHolder() { + return false; + } +} diff --git a/tools/ahat/src/heapdump/Diff.java b/tools/ahat/src/heapdump/Diff.java new file mode 100644 index 0000000000..943e6e63f5 --- /dev/null +++ b/tools/ahat/src/heapdump/Diff.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class Diff { + /** + * Perform a diff between two heap lists. + * + * Heaps are diffed based on heap name. PlaceHolder heaps will be added to + * the given lists as necessary so that every heap in A has a corresponding + * heap in B and vice-versa. + */ + private static void heaps(List<AhatHeap> a, List<AhatHeap> b) { + int asize = a.size(); + int bsize = b.size(); + for (int i = 0; i < bsize; i++) { + // Set the B heap's baseline as null to mark that we have not yet + // matched it with an A heap. + b.get(i).setBaseline(null); + } + + for (int i = 0; i < asize; i++) { + AhatHeap aheap = a.get(i); + aheap.setBaseline(null); + for (int j = 0; j < bsize; j++) { + AhatHeap bheap = b.get(j); + if (bheap.getBaseline() == null && aheap.getName().equals(bheap.getName())) { + // We found a match between aheap and bheap. + aheap.setBaseline(bheap); + bheap.setBaseline(aheap); + break; + } + } + + if (aheap.getBaseline() == null) { + // We did not find any match for aheap in snapshot B. + // Create a placeholder heap in snapshot B to use as the baseline. + b.add(AhatHeap.newPlaceHolderHeap(aheap.getName(), aheap)); + } + } + + // Make placeholder heaps in snapshot A for any unmatched heaps in + // snapshot B. + for (int i = 0; i < bsize; i++) { + AhatHeap bheap = b.get(i); + if (bheap.getBaseline() == null) { + a.add(AhatHeap.newPlaceHolderHeap(bheap.getName(), bheap)); + } + } + } + + /** + * Key represents an equivalence class of AhatInstances that are allowed to + * be considered for correspondence between two different snapshots. + */ + private static class Key { + // Corresponding objects must belong to classes of the same name. + private final String mClass; + + // Corresponding objects must belong to heaps of the same name. + private final String mHeapName; + + // Corresponding string objects must have the same value. + // mStringValue is set to the empty string for non-string objects. + private final String mStringValue; + + // Corresponding class objects must have the same class name. + // mClassName is set to the empty string for non-class objects. + private final String mClassName; + + // Corresponding array objects must have the same length. + // mArrayLength is set to 0 for non-array objects. + private final int mArrayLength; + + + private Key(AhatInstance inst) { + mClass = inst.getClassName(); + mHeapName = inst.getHeap().getName(); + mClassName = inst.isClassObj() ? inst.asClassObj().getName() : ""; + String string = inst.asString(); + mStringValue = string == null ? "" : string; + AhatArrayInstance array = inst.asArrayInstance(); + mArrayLength = array == null ? 0 : array.getLength(); + } + + /** + * Return the key for the given instance. + */ + public static Key keyFor(AhatInstance inst) { + return new Key(inst); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Key)) { + return false; + } + Key o = (Key)other; + return mClass.equals(o.mClass) + && mHeapName.equals(o.mHeapName) + && mStringValue.equals(o.mStringValue) + && mClassName.equals(o.mClassName) + && mArrayLength == o.mArrayLength; + } + + @Override + public int hashCode() { + return Objects.hash(mClass, mHeapName, mStringValue, mClassName, mArrayLength); + } + } + + private static class InstanceListPair { + public final List<AhatInstance> a; + public final List<AhatInstance> b; + + public InstanceListPair() { + this.a = new ArrayList<AhatInstance>(); + this.b = new ArrayList<AhatInstance>(); + } + + public InstanceListPair(List<AhatInstance> a, List<AhatInstance> b) { + this.a = a; + this.b = b; + } + } + + /** + * Recursively create place holder instances for the given instance and + * every instance dominated by that instance. + * Returns the place holder instance created for the given instance. + * Adds all allocated placeholders to the given placeholders list. + */ + private static AhatInstance createPlaceHolders(AhatInstance inst, + List<AhatInstance> placeholders) { + // Don't actually use recursion, because we could easily smash the stack. + // Instead we iterate. + AhatInstance result = inst.newPlaceHolderInstance(); + placeholders.add(result); + Deque<AhatInstance> deque = new ArrayDeque<AhatInstance>(); + deque.push(inst); + while (!deque.isEmpty()) { + inst = deque.pop(); + + for (AhatInstance child : inst.getDominated()) { + placeholders.add(child.newPlaceHolderInstance()); + deque.push(child); + } + } + return result; + } + + /** + * Recursively diff two dominator trees of instances. + * PlaceHolder objects are appended to the lists as needed to ensure every + * object has a corresponding baseline in the other list. All PlaceHolder + * objects are also appended to the given placeholders list, so their Site + * info can be updated later on. + */ + private static void instances(List<AhatInstance> a, List<AhatInstance> b, + List<AhatInstance> placeholders) { + // Don't actually use recursion, because we could easily smash the stack. + // Instead we iterate. + Deque<InstanceListPair> deque = new ArrayDeque<InstanceListPair>(); + deque.push(new InstanceListPair(a, b)); + while (!deque.isEmpty()) { + InstanceListPair p = deque.pop(); + + // Group instances of the same equivalence class together. + Map<Key, InstanceListPair> byKey = new HashMap<Key, InstanceListPair>(); + for (AhatInstance inst : p.a) { + Key key = Key.keyFor(inst); + InstanceListPair pair = byKey.get(key); + if (pair == null) { + pair = new InstanceListPair(); + byKey.put(key, pair); + } + pair.a.add(inst); + } + for (AhatInstance inst : p.b) { + Key key = Key.keyFor(inst); + InstanceListPair pair = byKey.get(key); + if (pair == null) { + pair = new InstanceListPair(); + byKey.put(key, pair); + } + pair.b.add(inst); + } + + // diff objects from the same key class. + for (InstanceListPair pair : byKey.values()) { + // Sort by retained size and assume the elements at the top of the lists + // correspond to each other in that order. This could probably be + // improved if desired, but it gives good enough results for now. + Collections.sort(pair.a, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE); + Collections.sort(pair.b, Sort.INSTANCE_BY_TOTAL_RETAINED_SIZE); + + int common = Math.min(pair.a.size(), pair.b.size()); + for (int i = 0; i < common; i++) { + AhatInstance ainst = pair.a.get(i); + AhatInstance binst = pair.b.get(i); + ainst.setBaseline(binst); + binst.setBaseline(ainst); + deque.push(new InstanceListPair(ainst.getDominated(), binst.getDominated())); + } + + // Add placeholder objects for anything leftover. + for (int i = common; i < pair.a.size(); i++) { + p.b.add(createPlaceHolders(pair.a.get(i), placeholders)); + } + + for (int i = common; i < pair.b.size(); i++) { + p.a.add(createPlaceHolders(pair.b.get(i), placeholders)); + } + } + } + } + + /** + * Sets the baseline for root and all its descendants to baseline. + */ + private static void setSitesBaseline(Site root, Site baseline) { + root.setBaseline(baseline); + for (Site child : root.getChildren()) { + setSitesBaseline(child, baseline); + } + } + + /** + * Recursively diff the two sites, setting them and their descendants as + * baselines for each other as appropriate. + * + * This requires that instances have already been diffed. In particular, we + * require all AhatClassObjs in one snapshot have corresponding (possibly + * place-holder) AhatClassObjs in the other snapshot. + */ + private static void sites(Site a, Site b) { + // Set the sites as baselines of each other. + a.setBaseline(b); + b.setBaseline(a); + + // Set the site's ObjectsInfos as baselines of each other. This implicitly + // adds new empty ObjectsInfo as needed. + for (Site.ObjectsInfo ainfo : a.getObjectsInfos()) { + AhatClassObj baseClassObj = null; + if (ainfo.classObj != null) { + baseClassObj = (AhatClassObj) ainfo.classObj.getBaseline(); + } + ainfo.setBaseline(b.getObjectsInfo(ainfo.heap.getBaseline(), baseClassObj)); + } + for (Site.ObjectsInfo binfo : b.getObjectsInfos()) { + AhatClassObj baseClassObj = null; + if (binfo.classObj != null) { + baseClassObj = (AhatClassObj) binfo.classObj.getBaseline(); + } + binfo.setBaseline(a.getObjectsInfo(binfo.heap.getBaseline(), baseClassObj)); + } + + // Set B children's baselines as null to mark that we have not yet matched + // them with A children. + for (Site bchild : b.getChildren()) { + bchild.setBaseline(null); + } + + for (Site achild : a.getChildren()) { + achild.setBaseline(null); + for (Site bchild : b.getChildren()) { + if (achild.getLineNumber() == bchild.getLineNumber() + && achild.getMethodName().equals(bchild.getMethodName()) + && achild.getSignature().equals(bchild.getSignature()) + && achild.getFilename().equals(bchild.getFilename())) { + // We found a match between achild and bchild. + sites(achild, bchild); + break; + } + } + + if (achild.getBaseline() == null) { + // We did not find any match for achild in site B. + // Use B for the baseline of achild and its descendants. + setSitesBaseline(achild, b); + } + } + + for (Site bchild : b.getChildren()) { + if (bchild.getBaseline() == null) { + setSitesBaseline(bchild, a); + } + } + } + + /** + * Perform a diff of the two snapshots, setting each as the baseline for the + * other. + */ + public static void snapshots(AhatSnapshot a, AhatSnapshot b) { + a.setBaseline(b); + b.setBaseline(a); + + // Diff the heaps of each snapshot. + heaps(a.getHeaps(), b.getHeaps()); + + // Diff the instances of each snapshot. + List<AhatInstance> placeholders = new ArrayList<AhatInstance>(); + instances(a.getRooted(), b.getRooted(), placeholders); + + // Diff the sites of each snapshot. + // This requires the instances have already been diffed. + sites(a.getRootSite(), b.getRootSite()); + + // Add placeholders to their corresponding sites. + // This requires the sites have already been diffed. + for (AhatInstance placeholder : placeholders) { + placeholder.getBaseline().getSite().getBaseline().addPlaceHolderInstance(placeholder); + } + } + + /** + * Diff two lists of field values. + * PlaceHolder objects are added to the given lists as needed to ensure + * every FieldValue in A ends up with a corresponding FieldValue in B. + */ + public static void fields(List<FieldValue> a, List<FieldValue> b) { + // Fields with the same name and type are considered matching fields. + // For simplicity, we assume the matching fields are in the same order in + // both A and B, though some fields may be added or removed in either + // list. If our assumption is wrong, in the worst case the quality of the + // field diff is poor. + + for (int i = 0; i < a.size(); i++) { + FieldValue afield = a.get(i); + afield.setBaseline(null); + + // Find the matching field in B, if any. + for (int j = i; j < b.size(); j++) { + FieldValue bfield = b.get(j); + if (afield.getName().equals(bfield.getName()) + && afield.getType().equals(bfield.getType())) { + // We found the matching field in B. + // Assume fields i, ..., j-1 in B have no match in A. + for ( ; i < j; i++) { + a.add(i, FieldValue.newPlaceHolderFieldValue(b.get(i))); + } + + afield.setBaseline(bfield); + bfield.setBaseline(afield); + break; + } + } + + if (afield.getBaseline() == null) { + b.add(i, FieldValue.newPlaceHolderFieldValue(afield)); + } + } + + // All remaining fields in B are unmatched by any in A. + for (int i = a.size(); i < b.size(); i++) { + a.add(i, FieldValue.newPlaceHolderFieldValue(b.get(i))); + } + } +} diff --git a/tools/ahat/src/heapdump/Diffable.java b/tools/ahat/src/heapdump/Diffable.java new file mode 100644 index 0000000000..53442c857e --- /dev/null +++ b/tools/ahat/src/heapdump/Diffable.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +/** + * An interface for objects that have corresponding objects in a baseline heap + * dump. + */ +public interface Diffable<T> { + /** + * Return the baseline object that corresponds to this one. + */ + T getBaseline(); + + /** + * Returns true if this is a placeholder object. + * A placeholder object is used to indicate there is some object in the + * baseline heap dump that is not in this heap dump. In that case, we create + * a dummy place holder object in this heap dump as an indicator of the + * object removed from the baseline heap dump. + */ + boolean isPlaceHolder(); +} + diff --git a/tools/ahat/src/heapdump/FieldValue.java b/tools/ahat/src/heapdump/FieldValue.java new file mode 100644 index 0000000000..3f65cd3030 --- /dev/null +++ b/tools/ahat/src/heapdump/FieldValue.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +public class FieldValue implements Diffable<FieldValue> { + private final String mName; + private final String mType; + private final Value mValue; + private FieldValue mBaseline; + private final boolean mIsPlaceHolder; + + public FieldValue(String name, String type, Value value) { + mName = name; + mType = type; + mValue = value; + mBaseline = this; + mIsPlaceHolder = false; + } + + /** + * Construct a place holder FieldValue + */ + private FieldValue(FieldValue baseline) { + mName = baseline.mName; + mType = baseline.mType; + mValue = Value.getBaseline(baseline.mValue); + mBaseline = baseline; + mIsPlaceHolder = true; + } + + static FieldValue newPlaceHolderFieldValue(FieldValue baseline) { + FieldValue field = new FieldValue(baseline); + baseline.setBaseline(field); + return field; + } + + /** + * Returns the name of the field. + */ + public String getName() { + return mName; + } + + /** + * Returns a description of the type of the field. + */ + public String getType() { + return mType; + } + + /** + * Returns the value of this field. + */ + public Value getValue() { + return mValue; + } + + public void setBaseline(FieldValue baseline) { + mBaseline = baseline; + } + + @Override public FieldValue getBaseline() { + return mBaseline; + } + + @Override public boolean isPlaceHolder() { + return mIsPlaceHolder; + } +} diff --git a/tools/ahat/src/heapdump/PathElement.java b/tools/ahat/src/heapdump/PathElement.java new file mode 100644 index 0000000000..196a24628c --- /dev/null +++ b/tools/ahat/src/heapdump/PathElement.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +public class PathElement implements Diffable<PathElement> { + public final AhatInstance instance; + public final String field; + public boolean isDominator; + + public PathElement(AhatInstance instance, String field) { + this.instance = instance; + this.field = field; + this.isDominator = false; + } + + @Override public PathElement getBaseline() { + return this; + } + + @Override public boolean isPlaceHolder() { + return false; + } +} diff --git a/tools/ahat/src/heapdump/Site.java b/tools/ahat/src/heapdump/Site.java new file mode 100644 index 0000000000..738eaf0687 --- /dev/null +++ b/tools/ahat/src/heapdump/Site.java @@ -0,0 +1,284 @@ +/* + * 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.heapdump; + +import com.android.tools.perflib.heap.StackFrame; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Site implements Diffable<Site> { + // The site that this site was directly called from. + // mParent is null for the root site. + private Site mParent; + + private String mMethodName; + private String mSignature; + private String mFilename; + private int mLineNumber; + + // To identify this site, we pick a stack trace that includes the site. + // mId is the id of an object allocated at that stack trace, and mDepth + // is the number of calls between this site and the innermost site of + // allocation of the object with mId. + // For the root site, mId is 0 and mDepth is 0. + private long mId; + private int mDepth; + + // The total size of objects allocated in this site (including child sites), + // organized by heap index. Heap indices outside the range of mSizesByHeap + // implicitly have size 0. + private long[] mSizesByHeap; + + // List of child sites. + private List<Site> mChildren; + + // List of all objects allocated in this site (including child sites). + private List<AhatInstance> mObjects; + private List<ObjectsInfo> mObjectsInfos; + private Map<AhatHeap, Map<AhatClassObj, ObjectsInfo>> mObjectsInfoMap; + + private Site mBaseline; + + public static class ObjectsInfo implements Diffable<ObjectsInfo> { + public AhatHeap heap; + public AhatClassObj classObj; // May be null. + public long numInstances; + public long numBytes; + private ObjectsInfo baseline; + + public ObjectsInfo(AhatHeap heap, AhatClassObj classObj, long numInstances, long numBytes) { + this.heap = heap; + this.classObj = classObj; + this.numInstances = numInstances; + this.numBytes = numBytes; + this.baseline = this; + } + + /** + * Returns the name of the class this ObjectsInfo is associated with. + */ + public String getClassName() { + return classObj == null ? "???" : classObj.getName(); + } + + public void setBaseline(ObjectsInfo baseline) { + this.baseline = baseline; + } + + @Override public ObjectsInfo getBaseline() { + return baseline; + } + + @Override public boolean isPlaceHolder() { + return false; + } + } + + /** + * Construct a root site. + */ + public Site(String name) { + this(null, name, "", "", 0, 0, 0); + } + + public Site(Site parent, String method, String signature, String file, + int line, long id, int depth) { + mParent = parent; + mMethodName = method; + mSignature = signature; + mFilename = file; + mLineNumber = line; + mId = id; + mDepth = depth; + mSizesByHeap = new long[1]; + mChildren = new ArrayList<Site>(); + mObjects = new ArrayList<AhatInstance>(); + mObjectsInfos = new ArrayList<ObjectsInfo>(); + mObjectsInfoMap = new HashMap<AhatHeap, Map<AhatClassObj, ObjectsInfo>>(); + mBaseline = this; + } + + /** + * Add an instance to this site. + * Returns the site at which the instance was allocated. + * @param frames - The list of frames in the stack trace, starting with the inner-most frame. + * @param depth - The number of frames remaining before the inner-most frame is reached. + */ + Site add(StackFrame[] frames, int depth, AhatInstance inst) { + return add(this, frames, depth, inst); + } + + private static Site add(Site site, StackFrame[] frames, int depth, AhatInstance inst) { + while (true) { + site.mObjects.add(inst); + + ObjectsInfo info = site.getObjectsInfo(inst.getHeap(), inst.getClassObj()); + if (inst.isReachable()) { + AhatHeap heap = inst.getHeap(); + if (heap.getIndex() >= site.mSizesByHeap.length) { + long[] newSizes = new long[heap.getIndex() + 1]; + for (int i = 0; i < site.mSizesByHeap.length; i++) { + newSizes[i] = site.mSizesByHeap[i]; + } + site.mSizesByHeap = newSizes; + } + site.mSizesByHeap[heap.getIndex()] += inst.getSize(); + + info.numInstances++; + info.numBytes += inst.getSize(); + } + + if (depth > 0) { + StackFrame next = frames[depth - 1]; + Site child = null; + for (int i = 0; i < site.mChildren.size(); i++) { + Site curr = site.mChildren.get(i); + if (curr.mLineNumber == next.getLineNumber() + && curr.mMethodName.equals(next.getMethodName()) + && curr.mSignature.equals(next.getSignature()) + && curr.mFilename.equals(next.getFilename())) { + child = curr; + break; + } + } + if (child == null) { + child = new Site(site, next.getMethodName(), next.getSignature(), + next.getFilename(), next.getLineNumber(), inst.getId(), depth - 1); + site.mChildren.add(child); + } + depth = depth - 1; + site = child; + } else { + return site; + } + } + } + + // Get the size of a site for a specific heap. + public long getSize(AhatHeap heap) { + int index = heap.getIndex(); + return index >= 0 && index < mSizesByHeap.length ? mSizesByHeap[index] : 0; + } + + /** + * Get the list of objects allocated under this site. Includes objects + * allocated in children sites. + */ + public Collection<AhatInstance> getObjects() { + return mObjects; + } + + /** + * Returns the ObjectsInfo at this site for the given heap and class + * objects. Creates a new empty ObjectsInfo if none existed before. + */ + ObjectsInfo getObjectsInfo(AhatHeap heap, AhatClassObj classObj) { + Map<AhatClassObj, ObjectsInfo> classToObjectsInfo = mObjectsInfoMap.get(heap); + if (classToObjectsInfo == null) { + classToObjectsInfo = new HashMap<AhatClassObj, ObjectsInfo>(); + mObjectsInfoMap.put(heap, classToObjectsInfo); + } + + ObjectsInfo info = classToObjectsInfo.get(classObj); + if (info == null) { + info = new ObjectsInfo(heap, classObj, 0, 0); + mObjectsInfos.add(info); + classToObjectsInfo.put(classObj, info); + } + return info; + } + + public List<ObjectsInfo> getObjectsInfos() { + return mObjectsInfos; + } + + // Get the combined size of the site for all heaps. + public long getTotalSize() { + long total = 0; + for (int i = 0; i < mSizesByHeap.length; i++) { + total += mSizesByHeap[i]; + } + return total; + } + + /** + * Return the site this site was called from. + * Returns null for the root site. + */ + public Site getParent() { + return mParent; + } + + public String getMethodName() { + return mMethodName; + } + + public String getSignature() { + return mSignature; + } + + public String getFilename() { + return mFilename; + } + + public int getLineNumber() { + return mLineNumber; + } + + /** + * Returns the id of some object allocated in this site. + */ + public long getId() { + return mId; + } + + /** + * Returns the number of frames between this site and the site where the + * object with id getId() was allocated. + */ + public int getDepth() { + return mDepth; + } + + public List<Site> getChildren() { + return mChildren; + } + + void setBaseline(Site baseline) { + mBaseline = baseline; + } + + @Override public Site getBaseline() { + return mBaseline; + } + + @Override public boolean isPlaceHolder() { + return false; + } + + /** + * Adds a place holder instance to this site and all parent sites. + */ + void addPlaceHolderInstance(AhatInstance placeholder) { + for (Site site = this; site != null; site = site.mParent) { + site.mObjects.add(placeholder); + } + } +} diff --git a/tools/ahat/src/Sort.java b/tools/ahat/src/heapdump/Sort.java index 8a3d9f2053..93d147a49e 100644 --- a/tools/ahat/src/Sort.java +++ b/tools/ahat/src/heapdump/Sort.java @@ -14,10 +14,7 @@ * limitations under the License. */ -package com.android.ahat; - -import com.android.tools.perflib.heap.Heap; -import com.android.tools.perflib.heap.Instance; +package com.android.ahat.heapdump; import java.util.ArrayList; import java.util.Arrays; @@ -33,30 +30,20 @@ import java.util.List; * 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()); - } - } - +public class Sort { /** * 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> { + public static final Comparator<AhatInstance> INSTANCE_BY_TOTAL_RETAINED_SIZE + = new Comparator<AhatInstance>() { @Override - public int compare(Instance a, Instance b) { + public int compare(AhatInstance a, AhatInstance b) { return Long.compare(b.getTotalRetainedSize(), a.getTotalRetainedSize()); } - } + }; /** * Compare instances by their retained size for a given heap index. @@ -64,20 +51,16 @@ class Sort { * 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 static class InstanceByHeapRetainedSize implements Comparator<AhatInstance> { + private AhatHeap mHeap; - public InstanceByHeapRetainedSize(AhatSnapshot snapshot, Heap heap) { - mIndex = snapshot.getHeapIndex(heap); - } - - public InstanceByHeapRetainedSize(int heapIndex) { - mIndex = heapIndex; + public InstanceByHeapRetainedSize(AhatHeap heap) { + mHeap = heap; } @Override - public int compare(Instance a, Instance b) { - return Long.compare(b.getRetainedSize(mIndex), a.getRetainedSize(mIndex)); + public int compare(AhatInstance a, AhatInstance b) { + return Long.compare(b.getRetainedSize(mHeap), a.getRetainedSize(mHeap)); } } @@ -107,18 +90,18 @@ class Sort { } } - public static Comparator<Instance> defaultInstanceCompare(AhatSnapshot snapshot) { - List<Comparator<Instance>> comparators = new ArrayList<Comparator<Instance>>(); + public static Comparator<AhatInstance> defaultInstanceCompare(AhatSnapshot snapshot) { + List<Comparator<AhatInstance>> comparators = new ArrayList<Comparator<AhatInstance>>(); // Priority goes to the app heap, if we can find one. - Heap appHeap = snapshot.getHeap("app"); + AhatHeap appHeap = snapshot.getHeap("app"); if (appHeap != null) { - comparators.add(new InstanceByHeapRetainedSize(snapshot, appHeap)); + comparators.add(new InstanceByHeapRetainedSize(appHeap)); } // Next is by total retained size. - comparators.add(new InstanceByTotalRetainedSize()); - return new WithPriority<Instance>(comparators); + comparators.add(INSTANCE_BY_TOTAL_RETAINED_SIZE); + return new WithPriority<AhatInstance>(comparators); } /** @@ -127,10 +110,10 @@ class Sort { * 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 static class SiteByHeapSize implements Comparator<Site> { + AhatHeap mHeap; - public SiteBySize(String heap) { + public SiteByHeapSize(AhatHeap heap) { mHeap = heap; } @@ -141,68 +124,70 @@ class Sort { } /** + * Compare Sites by the total size of objects allocated. + * This sorts sites from larger size to smaller size. + */ + public static final Comparator<Site> SITE_BY_TOTAL_SIZE = new Comparator<Site>() { + @Override + public int compare(Site a, Site b) { + return Long.compare(b.getTotalSize(), a.getTotalSize()); + } + }; + + public static Comparator<Site> defaultSiteCompare(AhatSnapshot snapshot) { + List<Comparator<Site>> comparators = new ArrayList<Comparator<Site>>(); + + // Priority goes to the app heap, if we can find one. + AhatHeap appHeap = snapshot.getHeap("app"); + if (appHeap != null) { + comparators.add(new SiteByHeapSize(appHeap)); + } + + // Next is by total size. + comparators.add(SITE_BY_TOTAL_SIZE); + return new WithPriority<Site>(comparators); + } + + /** * 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> { + public static final Comparator<Site.ObjectsInfo> OBJECTS_INFO_BY_SIZE + = new 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> { + public static final Comparator<Site.ObjectsInfo> OBJECTS_INFO_BY_HEAP_NAME + = new 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> { + public static final Comparator<Site.ObjectsInfo> OBJECTS_INFO_BY_CLASS_NAME + = new 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); + String aName = a.getClassName(); + String bName = b.getClassName(); return aName.compareTo(bName); } - } - - /** - * Compare AhatSnapshot.NativeAllocation by heap name. - * Different allocations with the same heap name are considered equal for - * the purposes of comparison. - */ - public static class NativeAllocationByHeapName - implements Comparator<InstanceUtils.NativeAllocation> { - @Override - public int compare(InstanceUtils.NativeAllocation a, InstanceUtils.NativeAllocation b) { - return a.heap.getName().compareTo(b.heap.getName()); - } - } - - /** - * Compare InstanceUtils.NativeAllocation by their size. - * Different allocations with the same size are considered equal for the - * purposes of comparison. - * This sorts allocations from larger size to smaller size. - */ - public static class NativeAllocationBySize implements Comparator<InstanceUtils.NativeAllocation> { - @Override - public int compare(InstanceUtils.NativeAllocation a, InstanceUtils.NativeAllocation b) { - return Long.compare(b.size, a.size); - } - } + }; } diff --git a/tools/ahat/src/heapdump/Value.java b/tools/ahat/src/heapdump/Value.java new file mode 100644 index 0000000000..6b2d38f7b1 --- /dev/null +++ b/tools/ahat/src/heapdump/Value.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat.heapdump; + +/** + * Value represents a field value in a heap dump. The field value is either a + * subclass of AhatInstance or a primitive Java type. + */ +public class Value { + private Object mObject; + + /** + * Constructs a value from a generic Java Object. + * The Object must either be a boxed Java primitive type or a subclass of + * AhatInstance. The object must not be null. + */ + Value(Object object) { + // TODO: Check that the Object is either an AhatSnapshot or boxed Java + // primitive type? + assert object != null; + mObject = object; + } + + /** + * Returns true if the Value is an AhatInstance, as opposed to a Java + * primitive value. + */ + public boolean isAhatInstance() { + return mObject instanceof AhatInstance; + } + + /** + * Return the Value as an AhatInstance if it is one. + * Returns null if the Value represents a Java primitive value. + */ + public AhatInstance asAhatInstance() { + if (isAhatInstance()) { + return (AhatInstance)mObject; + } + return null; + } + + /** + * Returns true if the Value is an Integer. + */ + public boolean isInteger() { + return mObject instanceof Integer; + } + + /** + * Return the Value as an Integer if it is one. + * Returns null if the Value does not represent an Integer. + */ + public Integer asInteger() { + if (isInteger()) { + return (Integer)mObject; + } + return null; + } + + /** + * Returns true if the Value is an Long. + */ + public boolean isLong() { + return mObject instanceof Long; + } + + /** + * Return the Value as an Long if it is one. + * Returns null if the Value does not represent an Long. + */ + public Long asLong() { + if (isLong()) { + return (Long)mObject; + } + return null; + } + + /** + * Return the Value as a Byte if it is one. + * Returns null if the Value does not represent a Byte. + */ + public Byte asByte() { + if (mObject instanceof Byte) { + return (Byte)mObject; + } + return null; + } + + /** + * Return the Value as a Char if it is one. + * Returns null if the Value does not represent a Char. + */ + public Character asChar() { + if (mObject instanceof Character) { + return (Character)mObject; + } + return null; + } + + public String toString() { + return mObject.toString(); + } + + public static Value getBaseline(Value value) { + if (value == null || !value.isAhatInstance()) { + return value; + } + return new Value(value.asAhatInstance().getBaseline()); + } + + @Override public boolean equals(Object other) { + if (other instanceof Value) { + Value value = (Value)other; + return mObject.equals(value.mObject); + } + return false; + } +} diff --git a/tools/ahat/src/help.html b/tools/ahat/src/help.html deleted file mode 100644 index ff04ad2840..0000000000 --- a/tools/ahat/src/help.html +++ /dev/null @@ -1,80 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<h1>Help</h1> -<h2>Information shown by ahat:</h2> -<ul> - <li><a href="/">The total bytes retained by heap.</a></li> - <li><a href="/rooted">A list of rooted 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> - -<h2>Tips:</h2> -<h3>Heaps</h3> -<p> -Android heap dumps contain information for multiple heaps. The <b>app</b> heap -is the memory used by your application. The <b>zygote</b> and <b>image</b> -heaps are used by the system. You should ignore everything in the zygote and -image heap and look only at the app heap. This is because changes in your -application will not effect the zygote or image heaps, and because the zygote -and image heaps are shared, they don't contribute significantly to your -applications PSS. -</p> - -<h3>Bitmaps</h3> -<p> -Bitmaps store their data using byte[] arrays. Whenever you see a large -byte[], check if it is a bitmap by looking to see if there is a single -android.graphics.Bitmap object referring to it. The byte[] will be marked as a -root, but it is really being retained by the android.graphics.Bitmap object. -</p> - -<h3>DexCaches</h3> -<p> -For each DexFile you load, there will be a corresponding DexCache whose size -is proportional to the number of strings, fields, methods, and classes in your -dex file. The DexCache entries may or may not be visible depending on the -version of the Android platform the heap dump is from. -</p> - -<h3>FinalizerReferences</h3> -<p> -A FinalizerReference is allocated for every object on the heap that has a -non-trivial finalizer. These are stored in a linked list reachable from the -FinalizerReference class object. -</p> diff --git a/tools/ahat/src/manifest.txt b/tools/ahat/src/manifest.txt index 1993910513..87a82b9f99 100644 --- a/tools/ahat/src/manifest.txt +++ b/tools/ahat/src/manifest.txt @@ -1,4 +1,4 @@ Name: ahat/ Implementation-Title: ahat -Implementation-Version: 0.8 +Implementation-Version: 1.0 Main-Class: com.android.ahat.Main diff --git a/tools/ahat/src/style.css b/tools/ahat/src/style.css index ca074a526c..47fae1d551 100644 --- a/tools/ahat/src/style.css +++ b/tools/ahat/src/style.css @@ -18,6 +18,14 @@ div.menu { background-color: #eeffff; } +span.added { + color: #770000; +} + +span.removed { + color: #007700; +} + /* * Most of the columns show numbers of bytes. Numbers should be right aligned. */ diff --git a/tools/ahat/test-dump/Main.java b/tools/ahat/test-dump/Main.java index 587d9defef..7a05b1cb89 100644 --- a/tools/ahat/test-dump/Main.java +++ b/tools/ahat/test-dump/Main.java @@ -18,8 +18,9 @@ import dalvik.system.VMDebug; import java.io.IOException; import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; -import libcore.util.NativeAllocationRegistry; +import org.apache.harmony.dalvik.ddmc.DdmVmInternal; /** * Program used to create a heap dump for test purposes. @@ -39,13 +40,32 @@ public class Main { } } + public static class AddedObject { + } + + public static class RemovedObject { + } + + public static class UnchangedObject { + } + + public static class ModifiedObject { + public int value; + public String modifiedRefField; + public String unmodifiedRefField; + } + + public static class StackSmasher { + public StackSmasher child; + } + // We will take a heap dump that includes a single instance of this // DumpedStuff class. Objects stored as fields in this class can be easily // found in the hprof dump by searching for the instance of the DumpedStuff // class and reading the desired field. public static class DumpedStuff { public String basicString = "hello, world"; - public String nonAscii = "Sigma (\u01a9) is not ASCII"; + public String nonAscii = "Sigma (Ʃ) is not ASCII"; public String embeddedZero = "embedded\0..."; // Non-ASCII for string compression purposes. public char[] charArray = "char thing".toCharArray(); public String nullString = null; @@ -53,23 +73,53 @@ 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 WeakReference aNullReferentReference = new WeakReference(null, referenceQueue); + public SoftReference aSoftReference = new SoftReference(new Object()); public byte[] bigArray; public ObjectTree[] gcPathArray = new ObjectTree[]{null, null, new ObjectTree( new ObjectTree(null, new ObjectTree(null, null)), new ObjectTree(null, null)), null}; + public Object[] basicStringRef; + public AddedObject addedObject; + public UnchangedObject unchangedObject = new UnchangedObject(); + public RemovedObject removedObject; + public ModifiedObject modifiedObject; + public StackSmasher stackSmasher; + public StackSmasher stackSmasherAdded; + public static String modifiedStaticField; + public int[] modifiedArray; - DumpedStuff() { - int N = 1000000; + DumpedStuff(boolean baseline) { + int N = baseline ? 400000 : 1000000; bigArray = new byte[N]; for (int i = 0; i < N; i++) { bigArray[i] = (byte)((i*i) & 0xFF); } - NativeAllocationRegistry registry = new NativeAllocationRegistry( - Main.class.getClassLoader(), 0x12345, 42); - registry.registerNativeAllocation(anObject, 0xABCDABCD); + addedObject = baseline ? null : new AddedObject(); + removedObject = baseline ? new RemovedObject() : null; + modifiedObject = new ModifiedObject(); + modifiedObject.value = baseline ? 5 : 8; + modifiedObject.modifiedRefField = baseline ? "A1" : "A2"; + modifiedObject.unmodifiedRefField = "B"; + modifiedStaticField = baseline ? "C1" : "C2"; + modifiedArray = baseline ? new int[]{0,1,2,3} : new int[]{3,1,2,0}; + + // Deep matching dominator trees shouldn't smash the stack when we try + // to diff them. Make some deep dominator trees to help test it. + for (int i = 0; i < 10000; i++) { + StackSmasher smasher = new StackSmasher(); + smasher.child = stackSmasher; + stackSmasher = smasher; + + if (!baseline) { + smasher = new StackSmasher(); + smasher.child = stackSmasherAdded; + stackSmasherAdded = smasher; + } + } gcPathArray[2].right.left = gcPathArray[2].left.right; } @@ -82,8 +132,21 @@ public class Main { } String file = args[0]; + // If a --base argument is provided, it means we should generate a + // baseline hprof file suitable for using in testing diff. + boolean baseline = args.length > 1 && args[1].equals("--base"); + + // Enable allocation tracking so we get stack traces in the heap dump. + DdmVmInternal.enableRecentAllocations(true); + // Allocate the instance of DumpedStuff. - stuff = new DumpedStuff(); + stuff = new DumpedStuff(baseline); + + // Create a bunch of unreachable objects pointing to basicString for the + // reverseReferencesAreNotUnreachable test + for (int i = 0; i < 100; i++) { + stuff.basicStringRef = new Object[]{stuff.basicString}; + } // Take a heap dump that will include that instance of DumpedStuff. System.err.println("Dumping hprof data to " + file); diff --git a/tools/ahat/test/DiffTest.java b/tools/ahat/test/DiffTest.java new file mode 100644 index 0000000000..52b6b7b3ae --- /dev/null +++ b/tools/ahat/test/DiffTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Diff; +import com.android.ahat.heapdump.FieldValue; +import com.android.tools.perflib.heap.hprof.HprofClassDump; +import com.android.tools.perflib.heap.hprof.HprofConstant; +import com.android.tools.perflib.heap.hprof.HprofDumpRecord; +import com.android.tools.perflib.heap.hprof.HprofHeapDump; +import com.android.tools.perflib.heap.hprof.HprofInstanceDump; +import com.android.tools.perflib.heap.hprof.HprofInstanceField; +import com.android.tools.perflib.heap.hprof.HprofLoadClass; +import com.android.tools.perflib.heap.hprof.HprofPrimitiveArrayDump; +import com.android.tools.perflib.heap.hprof.HprofRecord; +import com.android.tools.perflib.heap.hprof.HprofRootDebugger; +import com.android.tools.perflib.heap.hprof.HprofStaticField; +import com.android.tools.perflib.heap.hprof.HprofStringBuilder; +import com.android.tools.perflib.heap.hprof.HprofType; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class DiffTest { + @Test + public void diffMatchedHeap() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatHeap a = dump.getAhatSnapshot().getHeap("app"); + assertNotNull(a); + AhatHeap b = dump.getBaselineAhatSnapshot().getHeap("app"); + assertNotNull(b); + assertEquals(a.getBaseline(), b); + assertEquals(b.getBaseline(), a); + } + + @Test + public void diffUnchanged() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatInstance a = dump.getDumpedAhatInstance("unchangedObject"); + assertNotNull(a); + + AhatInstance b = dump.getBaselineDumpedAhatInstance("unchangedObject"); + assertNotNull(b); + assertEquals(a, b.getBaseline()); + assertEquals(b, a.getBaseline()); + assertEquals(a.getSite(), b.getSite().getBaseline()); + assertEquals(b.getSite(), a.getSite().getBaseline()); + } + + @Test + public void diffAdded() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatInstance a = dump.getDumpedAhatInstance("addedObject"); + assertNotNull(a); + assertNull(dump.getBaselineDumpedAhatInstance("addedObject")); + assertTrue(a.getBaseline().isPlaceHolder()); + } + + @Test + public void diffRemoved() throws IOException { + TestDump dump = TestDump.getTestDump(); + + assertNull(dump.getDumpedAhatInstance("removedObject")); + AhatInstance b = dump.getBaselineDumpedAhatInstance("removedObject"); + assertNotNull(b); + assertTrue(b.getBaseline().isPlaceHolder()); + } + + @Test + public void nullClassObj() throws IOException { + // Set up a heap dump that has a null classObj. + // The heap dump is derived from the InstanceTest.asStringEmbedded test. + HprofStringBuilder strings = new HprofStringBuilder(0); + List<HprofRecord> records = new ArrayList<HprofRecord>(); + List<HprofDumpRecord> dump = new ArrayList<HprofDumpRecord>(); + + final int stringClassObjectId = 1; + records.add(new HprofLoadClass(0, 0, stringClassObjectId, 0, strings.get("java.lang.String"))); + dump.add(new HprofClassDump(stringClassObjectId, 0, 0, 0, 0, 0, 0, 0, 0, + new HprofConstant[0], new HprofStaticField[0], + new HprofInstanceField[]{ + new HprofInstanceField(strings.get("count"), HprofType.TYPE_INT), + new HprofInstanceField(strings.get("hashCode"), HprofType.TYPE_INT), + new HprofInstanceField(strings.get("offset"), HprofType.TYPE_INT), + new HprofInstanceField(strings.get("value"), HprofType.TYPE_OBJECT)})); + + dump.add(new HprofPrimitiveArrayDump(0x41, 0, HprofType.TYPE_CHAR, + new long[]{'n', 'o', 't', ' ', 'h', 'e', 'l', 'l', 'o', 'o', 'p'})); + + ByteArrayDataOutput values = ByteStreams.newDataOutput(); + values.writeInt(5); // count + values.writeInt(0); // hashCode + values.writeInt(4); // offset + values.writeInt(0x41); // value + dump.add(new HprofInstanceDump(0x42, 0, stringClassObjectId, values.toByteArray())); + dump.add(new HprofRootDebugger(stringClassObjectId)); + dump.add(new HprofRootDebugger(0x42)); + + records.add(new HprofHeapDump(0, dump.toArray(new HprofDumpRecord[0]))); + AhatSnapshot snapshot = SnapshotBuilder.makeSnapshot(strings, records); + + // Diffing should not crash. + Diff.snapshots(snapshot, snapshot); + } + + @Test + public void diffFields() { + List<FieldValue> a = new ArrayList<FieldValue>(); + a.add(new FieldValue("n0", "t0", null)); + a.add(new FieldValue("n2", "t2", null)); + a.add(new FieldValue("n3", "t3", null)); + a.add(new FieldValue("n4", "t4", null)); + a.add(new FieldValue("n5", "t5", null)); + a.add(new FieldValue("n6", "t6", null)); + + List<FieldValue> b = new ArrayList<FieldValue>(); + b.add(new FieldValue("n0", "t0", null)); + b.add(new FieldValue("n1", "t1", null)); + b.add(new FieldValue("n2", "t2", null)); + b.add(new FieldValue("n3", "t3", null)); + b.add(new FieldValue("n5", "t5", null)); + b.add(new FieldValue("n6", "t6", null)); + b.add(new FieldValue("n7", "t7", null)); + + Diff.fields(a, b); + assertEquals(8, a.size()); + assertEquals(8, b.size()); + for (int i = 0; i < 8; i++) { + assertEquals(a.get(i), b.get(i).getBaseline()); + assertEquals(b.get(i), a.get(i).getBaseline()); + } + assertTrue(a.get(1).isPlaceHolder()); + assertTrue(a.get(7).isPlaceHolder()); + assertTrue(b.get(4).isPlaceHolder()); + } +} diff --git a/tools/ahat/test/InstanceTest.java b/tools/ahat/test/InstanceTest.java new file mode 100644 index 0000000000..3a50150c0e --- /dev/null +++ b/tools/ahat/test/InstanceTest.java @@ -0,0 +1,413 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatClassObj; +import com.android.ahat.heapdump.AhatHeap; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.PathElement; +import com.android.ahat.heapdump.Value; +import com.android.tools.perflib.heap.hprof.HprofClassDump; +import com.android.tools.perflib.heap.hprof.HprofConstant; +import com.android.tools.perflib.heap.hprof.HprofDumpRecord; +import com.android.tools.perflib.heap.hprof.HprofHeapDump; +import com.android.tools.perflib.heap.hprof.HprofInstanceDump; +import com.android.tools.perflib.heap.hprof.HprofInstanceField; +import com.android.tools.perflib.heap.hprof.HprofLoadClass; +import com.android.tools.perflib.heap.hprof.HprofPrimitiveArrayDump; +import com.android.tools.perflib.heap.hprof.HprofRecord; +import com.android.tools.perflib.heap.hprof.HprofRootDebugger; +import com.android.tools.perflib.heap.hprof.HprofStaticField; +import com.android.tools.perflib.heap.hprof.HprofStringBuilder; +import com.android.tools.perflib.heap.hprof.HprofType; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class InstanceTest { + @Test + public void asStringBasic() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("basicString"); + assertEquals("hello, world", str.asString()); + } + + @Test + public void asStringNonAscii() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("nonAscii"); + assertEquals("Sigma (Ʃ) is not ASCII", str.asString()); + } + + @Test + public void asStringEmbeddedZero() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("embeddedZero"); + assertEquals("embedded\0...", str.asString()); + } + + @Test + public void asStringCharArray() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("charArray"); + assertEquals("char thing", str.asString()); + } + + @Test + public void asStringTruncated() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("basicString"); + assertEquals("hello", str.asString(5)); + } + + @Test + public void asStringTruncatedNonAscii() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("nonAscii"); + assertEquals("Sigma (Ʃ)", str.asString(9)); + } + + @Test + public void asStringTruncatedEmbeddedZero() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("embeddedZero"); + assertEquals("embed", str.asString(5)); + } + + @Test + public void asStringCharArrayTruncated() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("charArray"); + assertEquals("char ", str.asString(5)); + } + + @Test + public void asStringExactMax() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("basicString"); + assertEquals("hello, world", str.asString(12)); + } + + @Test + public void asStringExactMaxNonAscii() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("nonAscii"); + assertEquals("Sigma (Ʃ) is not ASCII", str.asString(22)); + } + + @Test + public void asStringExactMaxEmbeddedZero() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("embeddedZero"); + assertEquals("embedded\0...", str.asString(12)); + } + + @Test + public void asStringCharArrayExactMax() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("charArray"); + assertEquals("char thing", str.asString(10)); + } + + @Test + public void asStringNotTruncated() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("basicString"); + assertEquals("hello, world", str.asString(50)); + } + + @Test + public void asStringNotTruncatedNonAscii() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("nonAscii"); + assertEquals("Sigma (Ʃ) is not ASCII", str.asString(50)); + } + + @Test + public void asStringNotTruncatedEmbeddedZero() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("embeddedZero"); + assertEquals("embedded\0...", str.asString(50)); + } + + @Test + public void asStringCharArrayNotTruncated() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("charArray"); + assertEquals("char thing", str.asString(50)); + } + + @Test + public void asStringNegativeMax() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("basicString"); + assertEquals("hello, world", str.asString(-3)); + } + + @Test + public void asStringNegativeMaxNonAscii() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("nonAscii"); + assertEquals("Sigma (Ʃ) is not ASCII", str.asString(-3)); + } + + @Test + public void asStringNegativeMaxEmbeddedZero() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("embeddedZero"); + assertEquals("embedded\0...", str.asString(-3)); + } + + @Test + public void asStringCharArrayNegativeMax() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance str = dump.getDumpedAhatInstance("charArray"); + assertEquals("char thing", str.asString(-3)); + } + + @Test + public void asStringNull() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("nullString"); + assertNull(obj); + } + + @Test + public void asStringNotString() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("anObject"); + assertNotNull(obj); + assertNull(obj.asString()); + } + + @Test + public void basicReference() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatInstance pref = dump.getDumpedAhatInstance("aPhantomReference"); + AhatInstance wref = dump.getDumpedAhatInstance("aWeakReference"); + AhatInstance nref = dump.getDumpedAhatInstance("aNullReferentReference"); + AhatInstance referent = dump.getDumpedAhatInstance("anObject"); + assertNotNull(pref); + assertNotNull(wref); + assertNotNull(nref); + assertNotNull(referent); + assertEquals(referent, pref.getReferent()); + assertEquals(referent, wref.getReferent()); + assertNull(nref.getReferent()); + assertNull(referent.getReferent()); + } + + @Test + public void unreachableReferent() throws IOException { + // The test dump program should never be under enough GC pressure for the + // soft reference to be cleared. Ensure that ahat will show the soft + // reference as having a non-null referent. + TestDump dump = TestDump.getTestDump(); + AhatInstance ref = dump.getDumpedAhatInstance("aSoftReference"); + assertNotNull(ref.getReferent()); + } + + @Test + public void gcRootPath() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatClassObj main = dump.getAhatSnapshot().findClass("Main"); + AhatInstance gcPathArray = dump.getDumpedAhatInstance("gcPathArray"); + Value value = gcPathArray.asArrayInstance().getValue(2); + AhatInstance base = value.asAhatInstance(); + AhatInstance left = base.getRefField("left"); + AhatInstance right = base.getRefField("right"); + AhatInstance target = left.getRefField("right"); + + List<PathElement> path = target.getPathFromGcRoot(); + assertEquals(6, path.size()); + + assertEquals(main, path.get(0).instance); + assertEquals(".stuff", path.get(0).field); + assertTrue(path.get(0).isDominator); + + assertEquals(".gcPathArray", path.get(1).field); + assertTrue(path.get(1).isDominator); + + assertEquals(gcPathArray, path.get(2).instance); + assertEquals("[2]", path.get(2).field); + assertTrue(path.get(2).isDominator); + + assertEquals(base, path.get(3).instance); + assertTrue(path.get(3).isDominator); + + // There are two possible paths. Either it can go through the 'left' node, + // or the 'right' node. + if (path.get(3).field.equals(".left")) { + assertEquals(".left", path.get(3).field); + + assertEquals(left, path.get(4).instance); + assertEquals(".right", path.get(4).field); + assertFalse(path.get(4).isDominator); + + } else { + assertEquals(".right", path.get(3).field); + + assertEquals(right, path.get(4).instance); + assertEquals(".left", path.get(4).field); + assertFalse(path.get(4).isDominator); + } + + assertEquals(target, path.get(5).instance); + assertEquals("", path.get(5).field); + assertTrue(path.get(5).isDominator); + } + + @Test + public void retainedSize() throws IOException { + TestDump dump = TestDump.getTestDump(); + + // anObject should not be an immediate dominator of any other object. This + // means its retained size should be equal to its size for the heap it was + // allocated on, and should be 0 for all other heaps. + AhatInstance anObject = dump.getDumpedAhatInstance("anObject"); + AhatSnapshot snapshot = dump.getAhatSnapshot(); + long size = anObject.getSize(); + assertEquals(size, anObject.getTotalRetainedSize()); + assertEquals(size, anObject.getRetainedSize(anObject.getHeap())); + for (AhatHeap heap : snapshot.getHeaps()) { + if (!heap.equals(anObject.getHeap())) { + assertEquals(String.format("For heap '%s'", heap.getName()), + 0, anObject.getRetainedSize(heap)); + } + } + } + + @Test + public void objectNotABitmap() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("anObject"); + assertNull(obj.asBitmap()); + } + + @Test + public void arrayNotABitmap() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("gcPathArray"); + assertNull(obj.asBitmap()); + } + + @Test + public void classObjNotABitmap() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getAhatSnapshot().findClass("Main"); + assertNull(obj.asBitmap()); + } + + @Test + public void classInstanceToString() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("aPhantomReference"); + long id = obj.getId(); + assertEquals(String.format("java.lang.ref.PhantomReference@%08x", id), obj.toString()); + } + + @Test + public void classObjToString() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getAhatSnapshot().findClass("Main"); + assertEquals("Main", obj.toString()); + } + + @Test + public void arrayInstanceToString() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("gcPathArray"); + long id = obj.getId(); + + // There's a bug in perfib's proguard deobfuscation for arrays. + // To work around that bug for the time being, only test the suffix of + // the toString result. Ideally we test for string equality against + // "Main$ObjectTree[4]@%08x", id. + assertTrue(obj.toString().endsWith(String.format("[4]@%08x", id))); + } + + @Test + public void primArrayInstanceToString() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("bigArray"); + long id = obj.getId(); + assertEquals(String.format("byte[1000000]@%08x", id), obj.toString()); + } + + @Test + public void isNotRoot() throws IOException { + TestDump dump = TestDump.getTestDump(); + AhatInstance obj = dump.getDumpedAhatInstance("anObject"); + assertFalse(obj.isRoot()); + assertNull(obj.getRootTypes()); + } + + @Test + public void asStringEmbedded() throws IOException { + // Set up a heap dump with an instance of java.lang.String of + // "hello" with instance id 0x42 that is backed by a char array that is + // bigger. This is how ART used to represent strings, and we should still + // support it in case the heap dump is from a previous platform version. + HprofStringBuilder strings = new HprofStringBuilder(0); + List<HprofRecord> records = new ArrayList<HprofRecord>(); + List<HprofDumpRecord> dump = new ArrayList<HprofDumpRecord>(); + + final int stringClassObjectId = 1; + records.add(new HprofLoadClass(0, 0, stringClassObjectId, 0, strings.get("java.lang.String"))); + dump.add(new HprofClassDump(stringClassObjectId, 0, 0, 0, 0, 0, 0, 0, 0, + new HprofConstant[0], new HprofStaticField[0], + new HprofInstanceField[]{ + new HprofInstanceField(strings.get("count"), HprofType.TYPE_INT), + new HprofInstanceField(strings.get("hashCode"), HprofType.TYPE_INT), + new HprofInstanceField(strings.get("offset"), HprofType.TYPE_INT), + new HprofInstanceField(strings.get("value"), HprofType.TYPE_OBJECT)})); + + dump.add(new HprofPrimitiveArrayDump(0x41, 0, HprofType.TYPE_CHAR, + new long[]{'n', 'o', 't', ' ', 'h', 'e', 'l', 'l', 'o', 'o', 'p'})); + + ByteArrayDataOutput values = ByteStreams.newDataOutput(); + values.writeInt(5); // count + values.writeInt(0); // hashCode + values.writeInt(4); // offset + values.writeInt(0x41); // value + dump.add(new HprofInstanceDump(0x42, 0, stringClassObjectId, values.toByteArray())); + dump.add(new HprofRootDebugger(stringClassObjectId)); + dump.add(new HprofRootDebugger(0x42)); + + records.add(new HprofHeapDump(0, dump.toArray(new HprofDumpRecord[0]))); + AhatSnapshot snapshot = SnapshotBuilder.makeSnapshot(strings, records); + AhatInstance chars = snapshot.findInstance(0x41); + assertNotNull(chars); + assertEquals("not helloop", chars.asString()); + + AhatInstance stringInstance = snapshot.findInstance(0x42); + assertNotNull(stringInstance); + assertEquals("hello", stringInstance.asString()); + } +} diff --git a/tools/ahat/test/InstanceUtilsTest.java b/tools/ahat/test/InstanceUtilsTest.java deleted file mode 100644 index fe2706d7d4..0000000000 --- a/tools/ahat/test/InstanceUtilsTest.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.tools.perflib.heap.ArrayInstance; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Instance; -import java.io.IOException; -import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import org.junit.Test; - -public class InstanceUtilsTest { - @Test - public void asStringBasic() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("basicString"); - assertEquals("hello, world", InstanceUtils.asString(str)); - } - - @Test - public void asStringNonAscii() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("nonAscii"); - assertEquals("Sigma (\u01a9) is not ASCII", InstanceUtils.asString(str)); - } - - @Test - public void asStringEmbeddedZero() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("embeddedZero"); - assertEquals("embedded\0...", InstanceUtils.asString(str)); - } - - @Test - public void asStringCharArray() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("charArray"); - assertEquals("char thing", InstanceUtils.asString(str)); - } - - @Test - public void asStringTruncated() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("basicString"); - assertEquals("hello", InstanceUtils.asString(str, 5)); - } - - @Test - public void asStringTruncatedNonAscii() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("nonAscii"); - assertEquals("Sigma (\u01a9)", InstanceUtils.asString(str, 9)); - } - - @Test - public void asStringTruncatedEmbeddedZero() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("embeddedZero"); - assertEquals("embed", InstanceUtils.asString(str, 5)); - } - - @Test - public void asStringCharArrayTruncated() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("charArray"); - assertEquals("char ", InstanceUtils.asString(str, 5)); - } - - @Test - public void asStringExactMax() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("basicString"); - assertEquals("hello, world", InstanceUtils.asString(str, 12)); - } - - @Test - public void asStringExactMaxNonAscii() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("nonAscii"); - assertEquals("Sigma (\u01a9) is not ASCII", InstanceUtils.asString(str, 22)); - } - - @Test - public void asStringExactMaxEmbeddedZero() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("embeddedZero"); - assertEquals("embedded\0...", InstanceUtils.asString(str, 12)); - } - - @Test - public void asStringCharArrayExactMax() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("charArray"); - assertEquals("char thing", InstanceUtils.asString(str, 10)); - } - - @Test - public void asStringNotTruncated() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("basicString"); - assertEquals("hello, world", InstanceUtils.asString(str, 50)); - } - - @Test - public void asStringNotTruncatedNonAscii() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("nonAscii"); - assertEquals("Sigma (\u01a9) is not ASCII", InstanceUtils.asString(str, 50)); - } - - @Test - public void asStringNotTruncatedEmbeddedZero() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("embeddedZero"); - assertEquals("embedded\0...", InstanceUtils.asString(str, 50)); - } - - @Test - public void asStringCharArrayNotTruncated() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("charArray"); - assertEquals("char thing", InstanceUtils.asString(str, 50)); - } - - @Test - public void asStringNegativeMax() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("basicString"); - assertEquals("hello, world", InstanceUtils.asString(str, -3)); - } - - @Test - public void asStringNegativeMaxNonAscii() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("nonAscii"); - assertEquals("Sigma (\u01a9) is not ASCII", InstanceUtils.asString(str, -3)); - } - - @Test - public void asStringNegativeMaxEmbeddedZero() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("embeddedZero"); - assertEquals("embedded\0...", InstanceUtils.asString(str, -3)); - } - - @Test - public void asStringCharArrayNegativeMax() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance str = (Instance)dump.getDumpedThing("charArray"); - assertEquals("char thing", InstanceUtils.asString(str, -3)); - } - - @Test - public void asStringNull() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance obj = (Instance)dump.getDumpedThing("nullString"); - assertNull(InstanceUtils.asString(obj)); - } - - @Test - public void asStringNotString() throws IOException { - TestDump dump = TestDump.getTestDump(); - Instance obj = (Instance)dump.getDumpedThing("anObject"); - assertNotNull(obj); - assertNull(InstanceUtils.asString(obj)); - } - - @Test - public void basicReference() throws IOException { - TestDump dump = TestDump.getTestDump(); - - Instance pref = (Instance)dump.getDumpedThing("aPhantomReference"); - Instance wref = (Instance)dump.getDumpedThing("aWeakReference"); - Instance referent = (Instance)dump.getDumpedThing("anObject"); - assertNotNull(pref); - assertNotNull(wref); - assertNotNull(referent); - assertEquals(referent, InstanceUtils.getReferent(pref)); - assertEquals(referent, InstanceUtils.getReferent(wref)); - assertNull(InstanceUtils.getReferent(referent)); - } - - @Test - public void gcRootPath() throws IOException { - TestDump dump = TestDump.getTestDump(); - - ClassObj main = dump.getAhatSnapshot().findClass("Main"); - ArrayInstance gcPathArray = (ArrayInstance)dump.getDumpedThing("gcPathArray"); - Object[] values = gcPathArray.getValues(); - Instance base = (Instance)values[2]; - Instance left = InstanceUtils.getRefField(base, "left"); - Instance right = InstanceUtils.getRefField(base, "right"); - Instance target = InstanceUtils.getRefField(left, "right"); - - List<InstanceUtils.PathElement> path = InstanceUtils.getPathFromGcRoot(target); - assertEquals(6, path.size()); - - assertEquals(main, path.get(0).instance); - assertEquals(".stuff", path.get(0).field); - assertTrue(path.get(0).isDominator); - - assertEquals(".gcPathArray", path.get(1).field); - assertTrue(path.get(1).isDominator); - - assertEquals(gcPathArray, path.get(2).instance); - assertEquals("[2]", path.get(2).field); - assertTrue(path.get(2).isDominator); - - assertEquals(base, path.get(3).instance); - assertTrue(path.get(3).isDominator); - - // There are two possible paths. Either it can go through the 'left' node, - // or the 'right' node. - if (path.get(3).field.equals(".left")) { - assertEquals(".left", path.get(3).field); - - assertEquals(left, path.get(4).instance); - assertEquals(".right", path.get(4).field); - assertFalse(path.get(4).isDominator); - - } else { - assertEquals(".right", path.get(3).field); - - assertEquals(right, path.get(4).instance); - assertEquals(".left", path.get(4).field); - assertFalse(path.get(4).isDominator); - } - - assertEquals(target, path.get(5).instance); - assertEquals("", path.get(5).field); - assertTrue(path.get(5).isDominator); - } -} diff --git a/tools/ahat/test/NativeAllocationTest.java b/tools/ahat/test/NativeAllocationTest.java deleted file mode 100644 index 7ad4c1d431..0000000000 --- a/tools/ahat/test/NativeAllocationTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.tools.perflib.heap.Instance; -import java.io.IOException; -import static org.junit.Assert.fail; -import static org.junit.Assert.assertEquals; -import org.junit.Test; - -public class NativeAllocationTest { - - @Test - public void nativeAllocation() throws IOException { - TestDump dump = TestDump.getTestDump(); - - AhatSnapshot snapshot = dump.getAhatSnapshot(); - Instance referent = (Instance)dump.getDumpedThing("anObject"); - for (InstanceUtils.NativeAllocation alloc : snapshot.getNativeAllocations()) { - if (alloc.referent == referent) { - assertEquals(42 , alloc.size); - assertEquals(referent.getHeap(), alloc.heap); - assertEquals(0xABCDABCD , alloc.pointer); - return; - } - } - fail("No native allocation found with anObject as the referent"); - } -} - diff --git a/tools/ahat/test/ObjectHandlerTest.java b/tools/ahat/test/ObjectHandlerTest.java new file mode 100644 index 0000000000..cd0ba23013 --- /dev/null +++ b/tools/ahat/test/ObjectHandlerTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import java.io.IOException; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +public class ObjectHandlerTest { + @Test + public void noCrashClassInstance() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatInstance object = dump.getDumpedAhatInstance("aPhantomReference"); + assertNotNull(object); + + AhatHandler handler = new ObjectHandler(dump.getAhatSnapshot()); + TestHandler.testNoCrash(handler, "http://localhost:7100/object?id=" + object.getId()); + } + + @Test + public void noCrashClassObj() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatSnapshot snapshot = dump.getAhatSnapshot(); + AhatHandler handler = new ObjectHandler(snapshot); + + AhatInstance object = snapshot.findClass("Main"); + assertNotNull(object); + + TestHandler.testNoCrash(handler, "http://localhost:7100/object?id=" + object.getId()); + } + + @Test + public void noCrashSystemClassObj() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatSnapshot snapshot = dump.getAhatSnapshot(); + AhatHandler handler = new ObjectHandler(snapshot); + + AhatInstance object = snapshot.findClass("java.lang.String"); + assertNotNull(object); + + TestHandler.testNoCrash(handler, "http://localhost:7100/object?id=" + object.getId()); + } + + @Test + public void noCrashArrayInstance() throws IOException { + TestDump dump = TestDump.getTestDump(); + + AhatInstance object = dump.getDumpedAhatInstance("gcPathArray"); + assertNotNull(object); + + AhatHandler handler = new ObjectHandler(dump.getAhatSnapshot()); + TestHandler.testNoCrash(handler, "http://localhost:7100/object?id=" + object.getId()); + } +} diff --git a/tools/ahat/test/OverviewHandlerTest.java b/tools/ahat/test/OverviewHandlerTest.java new file mode 100644 index 0000000000..c2f773b64b --- /dev/null +++ b/tools/ahat/test/OverviewHandlerTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatSnapshot; +import java.io.File; +import java.io.IOException; +import org.junit.Test; + +public class OverviewHandlerTest { + + @Test + public void noCrash() throws IOException { + AhatSnapshot snapshot = TestDump.getTestDump().getAhatSnapshot(); + AhatHandler handler = new OverviewHandler(snapshot, + new File("my.hprof.file"), + new File("my.base.hprof.file")); + TestHandler.testNoCrash(handler, "http://localhost:7100"); + } +} diff --git a/tools/ahat/test/PerformanceTest.java b/tools/ahat/test/PerformanceTest.java index 6e46800603..e13974bb6f 100644 --- a/tools/ahat/test/PerformanceTest.java +++ b/tools/ahat/test/PerformanceTest.java @@ -16,13 +16,15 @@ package com.android.ahat; -import com.android.tools.perflib.heap.Instance; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; +import org.junit.Test; + 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 { @@ -36,7 +38,7 @@ public class PerformanceTest { // for any object, including big arrays. TestDump dump = TestDump.getTestDump(); - Instance bigArray = (Instance)dump.getDumpedThing("bigArray"); + AhatInstance bigArray = dump.getDumpedAhatInstance("bigArray"); assertNotNull(bigArray); AhatSnapshot snapshot = dump.getAhatSnapshot(); diff --git a/tools/ahat/test/QueryTest.java b/tools/ahat/test/QueryTest.java index 40e3322472..5bcf8eafc3 100644 --- a/tools/ahat/test/QueryTest.java +++ b/tools/ahat/test/QueryTest.java @@ -18,9 +18,10 @@ package com.android.ahat; import java.net.URI; import java.net.URISyntaxException; -import static org.junit.Assert.assertEquals; import org.junit.Test; +import static org.junit.Assert.assertEquals; + public class QueryTest { @Test public void simple() throws URISyntaxException { diff --git a/tools/ahat/test/RootedHandlerTest.java b/tools/ahat/test/RootedHandlerTest.java new file mode 100644 index 0000000000..f325b8e9a7 --- /dev/null +++ b/tools/ahat/test/RootedHandlerTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatSnapshot; +import java.io.IOException; +import org.junit.Test; + +public class RootedHandlerTest { + @Test + public void noCrash() throws IOException { + AhatSnapshot snapshot = TestDump.getTestDump().getAhatSnapshot(); + AhatHandler handler = new RootedHandler(snapshot); + TestHandler.testNoCrash(handler, "http://localhost:7100/rooted"); + } +} diff --git a/tools/ahat/test/SiteHandlerTest.java b/tools/ahat/test/SiteHandlerTest.java new file mode 100644 index 0000000000..37596be8bb --- /dev/null +++ b/tools/ahat/test/SiteHandlerTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatSnapshot; +import java.io.IOException; +import org.junit.Test; + +public class SiteHandlerTest { + @Test + public void noCrash() throws IOException { + AhatSnapshot snapshot = TestDump.getTestDump().getAhatSnapshot(); + AhatHandler handler = new SiteHandler(snapshot); + TestHandler.testNoCrash(handler, "http://localhost:7100/sites"); + } +} diff --git a/tools/ahat/test/SnapshotBuilder.java b/tools/ahat/test/SnapshotBuilder.java new file mode 100644 index 0000000000..0eea6357fd --- /dev/null +++ b/tools/ahat/test/SnapshotBuilder.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.tools.perflib.heap.ProguardMap; +import com.android.tools.perflib.heap.hprof.Hprof; +import com.android.tools.perflib.heap.hprof.HprofRecord; +import com.android.tools.perflib.heap.hprof.HprofStringBuilder; +import com.android.tools.perflib.heap.io.InMemoryBuffer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Class with utilities to help constructing snapshots for tests. + */ +public class SnapshotBuilder { + + // Helper function to make a snapshot with id size 4 given an + // HprofStringBuilder and list of HprofRecords + public static AhatSnapshot makeSnapshot(HprofStringBuilder strings, List<HprofRecord> records) + throws IOException { + // TODO: When perflib can handle the case where strings are referred to + // before they are defined, just add the string records to the records + // list. + List<HprofRecord> actualRecords = new ArrayList<HprofRecord>(); + actualRecords.addAll(strings.getStringRecords()); + actualRecords.addAll(records); + + Hprof hprof = new Hprof("JAVA PROFILE 1.0.3", 4, new Date(), actualRecords); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + hprof.write(os); + InMemoryBuffer buffer = new InMemoryBuffer(os.toByteArray()); + return AhatSnapshot.fromDataBuffer(buffer, new ProguardMap()); + } +} diff --git a/tools/ahat/test/SortTest.java b/tools/ahat/test/SortTest.java deleted file mode 100644 index 02ff7db068..0000000000 --- a/tools/ahat/test/SortTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ahat; - -import com.android.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/TestDump.java b/tools/ahat/test/TestDump.java index ebce61c2e8..ceb7346bc4 100644 --- a/tools/ahat/test/TestDump.java +++ b/tools/ahat/test/TestDump.java @@ -16,14 +16,16 @@ package com.android.ahat; -import com.android.tools.perflib.heap.ClassObj; -import com.android.tools.perflib.heap.Field; -import com.android.tools.perflib.heap.Instance; +import com.android.ahat.heapdump.AhatClassObj; +import com.android.ahat.heapdump.AhatInstance; +import com.android.ahat.heapdump.AhatSnapshot; +import com.android.ahat.heapdump.Diff; +import com.android.ahat.heapdump.FieldValue; +import com.android.ahat.heapdump.Value; import com.android.tools.perflib.heap.ProguardMap; import java.io.File; import java.io.IOException; import java.text.ParseException; -import java.util.Map; /** * The TestDump class is used to get an AhatSnapshot for the test-dump @@ -37,30 +39,46 @@ public class TestDump { // is visible to other test cases. private static TestDump mCachedTestDump = null; + // If the test dump fails to load the first time, it will likely fail every + // other test we try. Rather than having to wait a potentially very long + // time for test dump loading to fail over and over again, record when it + // fails and don't try to load it again. + private static boolean mTestDumpFailed = false; + private AhatSnapshot mSnapshot = null; + private AhatSnapshot mBaseline = null; /** - * Load the test-dump.hprof file. - * The location of the file is read from the system property - * "ahat.test.dump.hprof", which is expected to be set on the command line. - * For example: - * java -Dahat.test.dump.hprof=test-dump.hprof -jar ahat-tests.jar + * Load the test-dump.hprof and test-dump-base.hprof files. + * The location of the files are read from the system properties + * "ahat.test.dump.hprof" and "ahat.test.dump.base.hprof", which is expected + * to be set on the command line. + * The location of the proguard map for both hprof files is read from the + * system property "ahat.test.dump.map". For example: + * java -Dahat.test.dump.hprof=test-dump.hprof \ + * -Dahat.test.dump.base.hprof=test-dump-base.hprof \ + * -Dahat.test.dump.map=proguard.map \ + * -jar ahat-tests.jar * - * An IOException is thrown if there is a failure reading the hprof file or + * An IOException is thrown if there is a failure reading the hprof files or * the proguard map. */ private TestDump() throws IOException { - String hprof = System.getProperty("ahat.test.dump.hprof"); - - String mapfile = System.getProperty("ahat.test.dump.map"); - ProguardMap map = new ProguardMap(); - try { - map.readFromFile(new File(mapfile)); - } catch (ParseException e) { - throw new IOException("Unable to load proguard map", e); - } + // TODO: Make use of the baseline hprof for tests. + String hprof = System.getProperty("ahat.test.dump.hprof"); + String hprofBase = System.getProperty("ahat.test.dump.base.hprof"); + + String mapfile = System.getProperty("ahat.test.dump.map"); + ProguardMap map = new ProguardMap(); + try { + map.readFromFile(new File(mapfile)); + } catch (ParseException e) { + throw new IOException("Unable to load proguard map", e); + } - mSnapshot = AhatSnapshot.fromHprof(new File(hprof), map); + mSnapshot = AhatSnapshot.fromHprof(new File(hprof), map); + mBaseline = AhatSnapshot.fromHprof(new File(hprofBase), map); + Diff.snapshots(mSnapshot, mBaseline); } /** @@ -71,18 +89,59 @@ public class TestDump { } /** - * Return the value of a field in the DumpedStuff instance in the + * Get the baseline AhatSnapshot for the test dump program. + */ + public AhatSnapshot getBaselineAhatSnapshot() { + return mBaseline; + } + + /** + * Returns the value of a field in the DumpedStuff instance in the * snapshot for the test-dump program. */ - public Object getDumpedThing(String name) { - ClassObj main = mSnapshot.findClass("Main"); - Instance stuff = null; - for (Map.Entry<Field, Object> fields : main.getStaticFieldValues().entrySet()) { - if ("stuff".equals(fields.getKey().getName())) { - stuff = (Instance) fields.getValue(); + public Value getDumpedValue(String name) { + return getDumpedValue(name, mSnapshot); + } + + /** + * Returns the value of a field in the DumpedStuff instance in the + * baseline snapshot for the test-dump program. + */ + public Value getBaselineDumpedValue(String name) { + return getDumpedValue(name, mBaseline); + } + + /** + * Returns the value of a field in the DumpedStuff instance in the + * given snapshot for the test-dump program. + */ + private Value getDumpedValue(String name, AhatSnapshot snapshot) { + AhatClassObj main = snapshot.findClass("Main"); + AhatInstance stuff = null; + for (FieldValue fields : main.getStaticFieldValues()) { + if ("stuff".equals(fields.getName())) { + stuff = fields.getValue().asAhatInstance(); } } - return InstanceUtils.getField(stuff, name); + return stuff.getField(name); + } + + /** + * Returns the value of a non-primitive field in the DumpedStuff instance in + * the snapshot for the test-dump program. + */ + public AhatInstance getDumpedAhatInstance(String name) { + Value value = getDumpedValue(name); + return value == null ? null : value.asAhatInstance(); + } + + /** + * Returns the value of a non-primitive field in the DumpedStuff instance in + * the baseline snapshot for the test-dump program. + */ + public AhatInstance getBaselineDumpedAhatInstance(String name) { + Value value = getBaselineDumpedValue(name); + return value == null ? null : value.asAhatInstance(); } /** @@ -93,8 +152,14 @@ public class TestDump { * when possible. */ public static synchronized TestDump getTestDump() throws IOException { + if (mTestDumpFailed) { + throw new RuntimeException("Test dump failed before, assuming it will again"); + } + if (mCachedTestDump == null) { + mTestDumpFailed = true; mCachedTestDump = new TestDump(); + mTestDumpFailed = false; } return mCachedTestDump; } diff --git a/tools/ahat/test/TestHandler.java b/tools/ahat/test/TestHandler.java new file mode 100644 index 0000000000..859e39a688 --- /dev/null +++ b/tools/ahat/test/TestHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ahat; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +/** + * Provide common utilities for basic handler tests. + */ +public class TestHandler { + private static class NullOutputStream extends OutputStream { + public void write(int b) throws IOException { + } + } + + /** + * Test that the given handler doesn't crash on the given query. + */ + public static void testNoCrash(AhatHandler handler, String uri) throws IOException { + PrintStream ps = new PrintStream(new NullOutputStream()); + HtmlDoc doc = new HtmlDoc(ps, DocString.text("noCrash test"), DocString.uri("style.css")); + Query query = new Query(DocString.uri(uri)); + handler.handle(doc, query); + } +} diff --git a/tools/ahat/test/Tests.java b/tools/ahat/test/Tests.java index 3291470df9..2fd3286172 100644 --- a/tools/ahat/test/Tests.java +++ b/tools/ahat/test/Tests.java @@ -22,11 +22,14 @@ public class Tests { public static void main(String[] args) { if (args.length == 0) { args = new String[]{ - "com.android.ahat.InstanceUtilsTest", - "com.android.ahat.NativeAllocationTest", + "com.android.ahat.DiffTest", + "com.android.ahat.InstanceTest", + "com.android.ahat.ObjectHandlerTest", + "com.android.ahat.OverviewHandlerTest", "com.android.ahat.PerformanceTest", + "com.android.ahat.RootedHandlerTest", "com.android.ahat.QueryTest", - "com.android.ahat.SortTest", + "com.android.ahat.SiteHandlerTest", }; } JUnitCore.main(args); |