Add progress bar to ahat.
It can take a while for ahat to process a heap dump. This change adds an
ascii progress bar to ahat as it processes a heap dump.
Other changes worth noting:
* Adds progress tracking functionality to the dominators computation.
* Updates Parser to use builder pattern to make it easier to specify
different parsing options.
* Stops pretending ahat runs with Java 1.7.
Sample output:
Preparing localhost/127.0.0.1:7100 ...
Processing 'foo.hprof' ...
[ 100% ] Reading hprof ...
[ 100% ] Resolving references ...
[ 100% ] Reversing references ...
[ 100% ] Initializing dominators ...
[ 100% ] Resolving dominators ...
Server started on localhost:7100
Bug: 68842538
Bug: 110129502
Test: m ahat-test
Test: Run on large heap dumps, manually inspect progress bar output.
Change-Id: I4903fef57371fa226f7802c50902319cb7506e68
diff --git a/tools/ahat/Android.mk b/tools/ahat/Android.mk
index 9f423ba..2741a92 100644
--- a/tools/ahat/Android.mk
+++ b/tools/ahat/Android.mk
@@ -28,9 +28,6 @@
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := ahat
-# Let users with Java 7 run ahat (b/28303627)
-LOCAL_JAVA_LANGUAGE_VERSION := 1.7
-
# Make this available on the classpath of the general-tests tradefed suite.
# It is used by libcore tests that run there.
LOCAL_COMPATIBILITY_SUITE := general-tests
diff --git a/tools/ahat/etc/ahat_api.txt b/tools/ahat/etc/ahat_api.txt
index 93fe46b..6fc62e7 100644
--- a/tools/ahat/etc/ahat_api.txt
+++ b/tools/ahat/etc/ahat_api.txt
@@ -10,6 +10,7 @@
public class DominatorsComputation {
method public static void computeDominators(com.android.ahat.dominators.DominatorsComputation.Node);
+ method public static void computeDominators(com.android.ahat.dominators.DominatorsComputation.Node, com.android.ahat.progress.Progress, long);
}
public static abstract interface DominatorsComputation.Node {
@@ -27,12 +28,10 @@
method public int getLength();
method public com.android.ahat.heapdump.Value getValue(int);
method public java.util.List<com.android.ahat.heapdump.Value> getValues();
- method public java.lang.String toString();
}
public class AhatClassInstance extends com.android.ahat.heapdump.AhatInstance {
method public java.lang.Iterable<com.android.ahat.heapdump.FieldValue> getInstanceFields();
- method public java.lang.String toString();
}
public class AhatClassObj extends com.android.ahat.heapdump.AhatInstance {
@@ -42,7 +41,6 @@
method public java.lang.String getName();
method public java.util.List<com.android.ahat.heapdump.FieldValue> getStaticFieldValues();
method public com.android.ahat.heapdump.AhatClassObj getSuperClassObj();
- method public java.lang.String toString();
}
public class AhatHeap implements com.android.ahat.heapdump.Diffable {
@@ -157,8 +155,13 @@
}
public class Parser {
+ ctor public Parser(java.nio.ByteBuffer);
+ ctor public Parser(java.io.File) throws java.io.IOException;
+ method public com.android.ahat.heapdump.Parser map(com.android.ahat.proguard.ProguardMap);
+ method public com.android.ahat.heapdump.AhatSnapshot parse() throws com.android.ahat.heapdump.HprofFormatException, java.io.IOException;
method public static com.android.ahat.heapdump.AhatSnapshot parseHeapDump(java.io.File, com.android.ahat.proguard.ProguardMap) throws com.android.ahat.heapdump.HprofFormatException, java.io.IOException;
method public static com.android.ahat.heapdump.AhatSnapshot parseHeapDump(java.nio.ByteBuffer, com.android.ahat.proguard.ProguardMap) throws com.android.ahat.heapdump.HprofFormatException, java.io.IOException;
+ method public com.android.ahat.heapdump.Parser progress(com.android.ahat.progress.Progress);
}
public class PathElement implements com.android.ahat.heapdump.Diffable {
@@ -284,6 +287,26 @@
}
+package com.android.ahat.progress {
+
+ public class NullProgress implements com.android.ahat.progress.Progress {
+ ctor public NullProgress();
+ method public void advance(long);
+ method public void done();
+ method public void start(java.lang.String, long);
+ method public void update(long);
+ }
+
+ public abstract interface Progress {
+ method public default void advance();
+ method public abstract void advance(long);
+ method public abstract void done();
+ method public abstract void start(java.lang.String, long);
+ method public abstract void update(long);
+ }
+
+}
+
package com.android.ahat.proguard {
public class ProguardMap {
diff --git a/tools/ahat/src/main/com/android/ahat/AsciiProgress.java b/tools/ahat/src/main/com/android/ahat/AsciiProgress.java
new file mode 100644
index 0000000..3ac98a4
--- /dev/null
+++ b/tools/ahat/src/main/com/android/ahat/AsciiProgress.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 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.progress.Progress;
+
+/**
+ * A progress bar that prints ascii to System.out.
+ * <p>
+ * For best results, have System.out positioned at a new line before using
+ * this progress indicator.
+ */
+class AsciiProgress implements Progress {
+ private String description;
+ private long duration;
+ private long progress;
+
+ private static void display(String description, long percent) {
+ System.out.print(String.format("\r[ %3d%% ] %s ...", percent, description));
+ System.out.flush();
+ }
+
+ @Override
+ public void start(String description, long duration) {
+ assert this.description == null;
+ this.description = description;
+ this.duration = duration;
+ this.progress = 0;
+ display(description, 0);
+ }
+
+ @Override
+ public void advance(long n) {
+ update(progress + n);
+ }
+
+ @Override
+ public void update(long current) {
+ assert description != null;
+ long oldPercent = progress * 100 / duration;
+ long newPercent = current * 100 / duration;
+ progress = current;
+
+ if (newPercent > oldPercent) {
+ display(description, newPercent);
+ }
+ }
+
+ @Override
+ public void done() {
+ update(duration);
+ System.out.println();
+ this.description = null;
+ }
+}
diff --git a/tools/ahat/src/main/com/android/ahat/Main.java b/tools/ahat/src/main/com/android/ahat/Main.java
index af197d4..d3cfcf9 100644
--- a/tools/ahat/src/main/com/android/ahat/Main.java
+++ b/tools/ahat/src/main/com/android/ahat/Main.java
@@ -20,6 +20,7 @@
import com.android.ahat.heapdump.Diff;
import com.android.ahat.heapdump.HprofFormatException;
import com.android.ahat.heapdump.Parser;
+import com.android.ahat.progress.Progress;
import com.android.ahat.proguard.ProguardMap;
import com.sun.net.httpserver.HttpServer;
import java.io.File;
@@ -58,10 +59,10 @@
* Prints an error message and exits the application on failure to load the
* heap dump.
*/
- private static AhatSnapshot loadHeapDump(File hprof, ProguardMap map) {
+ private static AhatSnapshot loadHeapDump(File hprof, ProguardMap map, Progress progress) {
System.out.println("Processing '" + hprof + "' ...");
try {
- return Parser.parseHeapDump(hprof, map);
+ return new Parser(hprof).map(map).progress(progress).parse();
} catch (IOException e) {
System.err.println("Unable to load '" + hprof + "':");
e.printStackTrace();
@@ -152,9 +153,9 @@
System.exit(1);
}
- AhatSnapshot ahat = loadHeapDump(hprof, map);
+ AhatSnapshot ahat = loadHeapDump(hprof, map, new AsciiProgress());
if (hprofbase != null) {
- AhatSnapshot base = loadHeapDump(hprofbase, mapbase);
+ AhatSnapshot base = loadHeapDump(hprofbase, mapbase, new AsciiProgress());
System.out.println("Diffing heap dumps ...");
Diff.snapshots(ahat, base);
diff --git a/tools/ahat/src/main/com/android/ahat/dominators/DominatorsComputation.java b/tools/ahat/src/main/com/android/ahat/dominators/DominatorsComputation.java
index 6185dee..903211e 100644
--- a/tools/ahat/src/main/com/android/ahat/dominators/DominatorsComputation.java
+++ b/tools/ahat/src/main/com/android/ahat/dominators/DominatorsComputation.java
@@ -16,6 +16,8 @@
package com.android.ahat.dominators;
+import com.android.ahat.progress.NullProgress;
+import com.android.ahat.progress.Progress;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
@@ -146,6 +148,10 @@
// If revisit != null, this node is on the global list of nodes to be
// revisited.
public NodeSet revisit = null;
+
+ // Distance from the root to this node. Used for purposes of tracking
+ // progress only.
+ public long depth;
}
// A collection of node ids.
@@ -245,6 +251,23 @@
* @see Node
*/
public static void computeDominators(Node root) {
+ computeDominators(root, new NullProgress(), 0);
+ }
+
+ /**
+ * Computes the immediate dominators of all nodes reachable from the <code>root</code> node.
+ * There must not be any incoming references to the <code>root</code> node.
+ * <p>
+ * The result of this function is to call the {@link Node#setDominator}
+ * function on every node reachable from the root node.
+ *
+ * @param root the root node of the dominators computation
+ * @param progress progress tracker.
+ * @param numNodes upper bound on the number of reachable nodes in the
+ * graph, for progress tracking purposes only.
+ * @see Node
+ */
+ public static void computeDominators(Node root, Progress progress, long numNodes) {
long id = 0;
// The set of nodes xS such that xS.revisit != null.
@@ -257,6 +280,7 @@
NodeS rootS = new NodeS();
rootS.node = root;
rootS.id = id++;
+ rootS.depth = 0;
root.setDominatorsComputationState(rootS);
Deque<Link> dfs = new ArrayDeque<Link>();
@@ -265,8 +289,14 @@
dfs.push(new Link(rootS, child));
}
+ // workBound is an upper bound on the amount of work required in the
+ // second phase of dominators computation, used solely for the purposes of
+ // tracking progress.
+ long workBound = 0;
+
// 1. Do a depth first search of the nodes, label them with ids and come
// up with initial candidate dominators for them.
+ progress.start("Initializing dominators", numNodes);
while (!dfs.isEmpty()) {
Link link = dfs.pop();
@@ -274,6 +304,7 @@
// This is the marker link indicating we have now visited all
// nodes reachable from link.srcS.
link.srcS.maxReachableId = id - 1;
+ progress.advance();
} else {
NodeS dstS = (NodeS)link.dst.getDominatorsComputationState();
if (dstS == null) {
@@ -288,6 +319,7 @@
dstS.domS = link.srcS;
dstS.domS.dominated.add(dstS);
dstS.oldDomS = link.srcS;
+ dstS.depth = link.srcS.depth + 1;
dfs.push(new Link(dstS));
for (Node child : link.dst.getReferencesForDominators()) {
@@ -296,6 +328,10 @@
} else {
// We have seen the destination node before. Update the state based
// on the new potential dominator.
+ if (dstS.inRefIds.size == 1) {
+ workBound += dstS.oldDomS.depth;
+ }
+
long seenid = dstS.inRefIds.last();
dstS.inRefIds.add(link.srcS.id);
@@ -330,9 +366,11 @@
}
}
}
+ progress.done();
// 2. Continue revisiting nodes until every node satisfies the requirement
// that domS.id == oldDomS.id.
+ progress.start("Resolving dominators", workBound);
while (!revisit.isEmpty()) {
NodeS oldDomS = revisit.poll();
assert oldDomS.revisit != null;
@@ -388,7 +426,10 @@
nodeS.oldDomS.revisit.add(nodeS);
}
}
+ progress.advance((oldDomS.depth - oldDomS.oldDomS.depth) * nodes.size);
}
+ progress.done();
+
// 3. We have figured out the correct dominator for each node. Notify the
// user of the results by doing one last traversal of the nodes.
diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java
index 67253bf..95553a2 100644
--- a/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java
+++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatInstance.java
@@ -17,6 +17,7 @@
package com.android.ahat.heapdump;
import com.android.ahat.dominators.DominatorsComputation;
+import com.android.ahat.progress.Progress;
import java.awt.image.BufferedImage;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -603,10 +604,16 @@
* mNextInstanceToGcRootField
* mHardReverseReferences
* mSoftReverseReferences
+ *
+ * @param progress used to track progress of the traversal.
+ * @param numInsts upper bound on the total number of instances reachable
+ * from the root, solely used for the purposes of tracking
+ * progress.
*/
- static void computeReverseReferences(SuperRoot root) {
+ static void computeReverseReferences(SuperRoot root, Progress progress, long numInsts) {
// Start by doing a breadth first search through strong references.
// Then continue the breadth first search through weak references.
+ progress.start("Reversing references", numInsts);
Queue<Reference> strong = new ArrayDeque<Reference>();
Queue<Reference> weak = new ArrayDeque<Reference>();
@@ -620,6 +627,7 @@
if (ref.ref.mNextInstanceToGcRoot == null) {
// This is the first time we have seen ref.ref.
+ progress.advance();
ref.ref.mNextInstanceToGcRoot = ref.src;
ref.ref.mNextInstanceToGcRootField = ref.field;
ref.ref.mHardReverseReferences = new ArrayList<AhatInstance>();
@@ -646,6 +654,7 @@
if (ref.ref.mNextInstanceToGcRoot == null) {
// This is the first time we have seen ref.ref.
+ progress.advance();
ref.ref.mNextInstanceToGcRoot = ref.src;
ref.ref.mNextInstanceToGcRootField = ref.field;
ref.ref.mHardReverseReferences = new ArrayList<AhatInstance>();
@@ -664,6 +673,8 @@
ref.ref.mSoftReverseReferences.add(ref.src);
}
}
+
+ progress.done();
}
/**
diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java b/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java
index 535db08..bc94047 100644
--- a/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java
+++ b/tools/ahat/src/main/com/android/ahat/heapdump/AhatSnapshot.java
@@ -17,6 +17,7 @@
package com.android.ahat.heapdump;
import com.android.ahat.dominators.DominatorsComputation;
+import com.android.ahat.progress.Progress;
import java.util.List;
/**
@@ -39,7 +40,8 @@
AhatSnapshot(SuperRoot root,
Instances<AhatInstance> instances,
List<AhatHeap> heaps,
- Site rootSite) {
+ Site rootSite,
+ Progress progress) {
mSuperRoot = root;
mInstances = instances;
mHeaps = heaps;
@@ -53,8 +55,8 @@
}
}
- AhatInstance.computeReverseReferences(mSuperRoot);
- DominatorsComputation.computeDominators(mSuperRoot);
+ AhatInstance.computeReverseReferences(mSuperRoot, progress, mInstances.size());
+ DominatorsComputation.computeDominators(mSuperRoot, progress, mInstances.size());
AhatInstance.computeRetainedSize(mSuperRoot, mHeaps.size());
for (AhatHeap heap : mHeaps) {
diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/Instances.java b/tools/ahat/src/main/com/android/ahat/heapdump/Instances.java
index 0851446..7bb19a2 100644
--- a/tools/ahat/src/main/com/android/ahat/heapdump/Instances.java
+++ b/tools/ahat/src/main/com/android/ahat/heapdump/Instances.java
@@ -67,6 +67,10 @@
return null;
}
+ public int size() {
+ return mInstances.size();
+ }
+
@Override
public Iterator<T> iterator() {
return mInstances.iterator();
diff --git a/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java b/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java
index 13be57d..597a260 100644
--- a/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java
+++ b/tools/ahat/src/main/com/android/ahat/heapdump/Parser.java
@@ -16,6 +16,8 @@
package com.android.ahat.heapdump;
+import com.android.ahat.progress.NullProgress;
+import com.android.ahat.progress.Progress;
import com.android.ahat.proguard.ProguardMap;
import java.io.File;
import java.io.IOException;
@@ -33,35 +35,95 @@
/**
* Provides methods for parsing heap dumps.
+ * <p>
+ * The heap dump should be a heap dump in the J2SE HPROF format optionally
+ * with Android extensions and satisfying the following additional
+ * constraints:
+ * <ul>
+ * <li>
+ * Class serial numbers, stack frames, and stack traces individually satisfy
+ * the following:
+ * <ul>
+ * <li> All elements are defined before they are referenced.
+ * <li> Ids are densely packed in some range [a, b] where a is not necessarily 0.
+ * <li> There are not more than 2^31 elements defined.
+ * </ul>
+ * <li> All classes are defined via a LOAD CLASS record before the first
+ * heap dump segment.
+ * <li> The ID size used in the heap dump is 4 bytes.
+ * </ul>
*/
public class Parser {
private static final int ID_SIZE = 4;
- private Parser() {
+ private HprofBuffer hprof = null;
+ private ProguardMap map = new ProguardMap();
+ private Progress progress = new NullProgress();
+
+ /**
+ * Creates an hprof Parser that parses a heap dump from a byte buffer.
+ *
+ * @param hprof byte buffer to parse the heap dump from.
+ */
+ public Parser(ByteBuffer hprof) {
+ this.hprof = new HprofBuffer(hprof);
}
/**
- * Parses a heap dump from a File.
- * <p>
- * The heap dump should be a heap dump in the J2SE HPROF format optionally
- * with Android extensions and satisfying the following additional
- * constraints:
- * <ul>
- * <li>
- * Class serial numbers, stack frames, and stack traces individually satisfy
- * the following:
- * <ul>
- * <li> All elements are defined before they are referenced.
- * <li> Ids are densely packed in some range [a, b] where a is not necessarily 0.
- * <li> There are not more than 2^31 elements defined.
- * </ul>
- * <li> All classes are defined via a LOAD CLASS record before the first
- * heap dump segment.
- * <li> The ID size used in the heap dump is 4 bytes.
- * </ul>
- * <p>
- * The given proguard map will be used to deobfuscate class names, field
- * names, and stack traces in the heap dump.
+ * Creates an hprof Parser that parses a heap dump from a file.
+ *
+ * @param hprof file to parse the heap dump from.
+ * @throws IOException if the file cannot be accessed.
+ */
+ public Parser(File hprof) throws IOException {
+ this.hprof = new HprofBuffer(hprof);
+ }
+
+ /**
+ * Sets the proguard map to use for deobfuscating the heap.
+ *
+ * @param map proguard map to use to deobfuscate the heap.
+ * @return this Parser instance.
+ */
+ public Parser map(ProguardMap map) {
+ if (map == null) {
+ throw new NullPointerException("map == null");
+ }
+ this.map = map;
+ return this;
+ }
+
+ /**
+ * Sets the progress indicator to use when parsing the heap.
+ *
+ * @param progress progress indicator to use when parsing the heap.
+ * @return this Parser instance.
+ */
+ public Parser progress(Progress progress) {
+ if (progress == null) {
+ throw new NullPointerException("progress == null");
+ }
+ this.progress = progress;
+ return this;
+ }
+
+ /**
+ * Parse the heap dump.
+ *
+ * @throws IOException if the heap dump could not be read
+ * @throws HprofFormatException if the heap dump is not properly formatted
+ * @return the parsed heap dump
+ */
+ public AhatSnapshot parse() throws IOException, HprofFormatException {
+ try {
+ return parseInternal();
+ } catch (BufferUnderflowException e) {
+ throw new HprofFormatException("Unexpected end of file", e);
+ }
+ }
+
+ /**
+ * Parses a heap dump from a File with given proguard map.
*
* @param hprof the hprof file to parse
* @param map the proguard map for deobfuscation
@@ -71,35 +133,11 @@
*/
public static AhatSnapshot parseHeapDump(File hprof, ProguardMap map)
throws IOException, HprofFormatException {
- try {
- return parseHeapDump(new HprofBuffer(hprof), map);
- } catch (BufferUnderflowException e) {
- throw new HprofFormatException("Unexpected end of file", e);
- }
+ return new Parser(hprof).map(map).parse();
}
/**
- * Parses a heap dump from a byte buffer.
- * <p>
- * The heap dump should be a heap dump in the J2SE HPROF format optionally
- * with Android extensions and satisfying the following additional
- * constraints:
- * <ul>
- * <li>
- * Class serial numbers, stack frames, and stack traces individually satisfy
- * the following:
- * <ul>
- * <li> All elements are defined before they are referenced.
- * <li> Ids are densely packed in some range [a, b] where a is not necessarily 0.
- * <li> There are not more than 2^31 elements defined.
- * </ul>
- * <li> All classes are defined via a LOAD CLASS record before the first
- * heap dump segment.
- * <li> The ID size used in the heap dump is 4 bytes.
- * </ul>
- * <p>
- * The given proguard map will be used to deobfuscate class names, field
- * names, and stack traces in the heap dump.
+ * Parses a heap dump from a byte buffer with given proguard map.
*
* @param hprof the bytes of the hprof file to parse
* @param map the proguard map for deobfuscation
@@ -109,15 +147,10 @@
*/
public static AhatSnapshot parseHeapDump(ByteBuffer hprof, ProguardMap map)
throws IOException, HprofFormatException {
- try {
- return parseHeapDump(new HprofBuffer(hprof), map);
- } catch (BufferUnderflowException e) {
- throw new HprofFormatException("Unexpected end of file", e);
- }
+ return new Parser(hprof).map(map).parse();
}
- private static AhatSnapshot parseHeapDump(HprofBuffer hprof, ProguardMap map)
- throws IOException, HprofFormatException, BufferUnderflowException {
+ private AhatSnapshot parseInternal() throws IOException, HprofFormatException {
// Read, and mostly ignore, the hprof header info.
{
StringBuilder format = new StringBuilder();
@@ -154,7 +187,9 @@
ArrayList<AhatClassObj> classes = new ArrayList<AhatClassObj>();
Instances<AhatClassObj> classById = null;
+ progress.start("Reading hprof", hprof.size());
while (hprof.hasRemaining()) {
+ progress.update(hprof.tell());
int tag = hprof.getU1();
int time = hprof.getU4();
int recordLength = hprof.getU4();
@@ -230,6 +265,7 @@
}
int subtag;
while (!isEndOfHeapDumpSegment(subtag = hprof.getU1())) {
+ progress.update(hprof.tell());
switch (subtag) {
case 0x01: { // ROOT JNI GLOBAL
long objectId = hprof.getId();
@@ -524,6 +560,7 @@
break;
}
}
+ progress.done();
instances.addAll(classes);
}
@@ -542,9 +579,11 @@
// that we couldn't previously resolve.
SuperRoot superRoot = new SuperRoot();
{
+ progress.start("Resolving references", mInstances.size());
Iterator<RootData> ri = roots.iterator();
RootData root = ri.next();
for (AhatInstance inst : mInstances) {
+ progress.advance();
long id = inst.getId();
// Skip past any roots that don't have associated instances.
@@ -613,11 +652,12 @@
((AhatArrayInstance)inst).initialize(array);
}
}
+ progress.done();
}
hprof = null;
roots = null;
- return new AhatSnapshot(superRoot, mInstances, heaps.heaps, rootSite);
+ return new AhatSnapshot(superRoot, mInstances, heaps.heaps, rootSite, progress);
}
private static boolean isEndOfHeapDumpSegment(int subtag) {
@@ -867,6 +907,13 @@
}
/**
+ * Returns the size of the file in bytes.
+ */
+ public int size() {
+ return mBuffer.capacity();
+ }
+
+ /**
* Return the current absolution position in the file.
*/
public int tell() {
diff --git a/tools/ahat/src/main/com/android/ahat/progress/NullProgress.java b/tools/ahat/src/main/com/android/ahat/progress/NullProgress.java
new file mode 100644
index 0000000..a0ca084
--- /dev/null
+++ b/tools/ahat/src/main/com/android/ahat/progress/NullProgress.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 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.progress;
+
+/**
+ * Null progress tracker that ignores all updates.
+ */
+public class NullProgress implements Progress {
+ @Override public void start(String description, long duration) { }
+ @Override public void advance() { }
+ @Override public void advance(long n) { }
+ @Override public void update(long current) { }
+ @Override public void done() { }
+}
diff --git a/tools/ahat/src/main/com/android/ahat/progress/Progress.java b/tools/ahat/src/main/com/android/ahat/progress/Progress.java
new file mode 100644
index 0000000..a10379d
--- /dev/null
+++ b/tools/ahat/src/main/com/android/ahat/progress/Progress.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 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.progress;
+
+/**
+ * Interface for notifying users of progress during long operations.
+ */
+public interface Progress {
+ /**
+ * Called to indicate the start of a new phase of work with the given
+ * duration. Behavior is undefined if there is a current phase in progress.
+ *
+ * @param description human readable description of the work to be done.
+ * @param duration the maximum duration of the phase, in arbitrary units
+ * appropriate for the work in question.
+ */
+ void start(String description, long duration);
+
+ /**
+ * Called to indicate the current phase has advanced a single unit of its
+ * overall duration towards completion. Behavior is undefined if there is no
+ * current phase in progress.
+ */
+ default void advance() {
+ advance(1);
+ }
+
+ /**
+ * Called to indicate the current phase has advanced <code>n</code> units of
+ * its overall duration towards completion. Behavior is undefined if there
+ * is no current phase in progress.
+ *
+ * @param n number of units of progress that have advanced.
+ */
+ void advance(long n);
+
+ /**
+ * Called to indicate the current phase has completed <code>current</code>
+ * absolute units of its overall duration. Behavior is undefined if there is
+ * no current phase in progress.
+ *
+ * @param current progress towards duration
+ */
+ void update(long current);
+
+ /**
+ * Called to indicates that the current phase has been completed. Behavior
+ * is undefined if there is no current phase in progress.
+ */
+ void done();
+}