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();
+}