Refactor ahat's perflib api.

This change substantially refactors how ahat accesses heap dump data.
Rather than use the perflib API directly with some additional
information accessed on the side via AhatSnapshot, we introduce an
entirely new API for accessing all the information we need from a heap
dump. Perflib is used when processing the heap dump to populate the
information initially, but afterwards all views and handlers go
through the new com.android.ahat.heapdump API.

The primary motivation for this change is to facilitate adding support
for diffing two heap dumps to ahat. The new API provides flexibility
that will make it easier to form links between objects in different
snapshots and introduce place holder objects to show when there is an
object in another snapshot that has no corresponding object in this
snapshot.

A large number of test cases were added to cover missing cases
discovered in the process of refactoring ahat's perflib API.

The external user-facing UI may have minor cosmetic changes, but
otherwise is unchanged.

Test: m ahat-test, with many new tests added.
Bug: 33770653

Change-Id: I1a6b05ea469ebbbac67d99129dd9faa457b4d17e
diff --git a/tools/ahat/test/InstanceTest.java b/tools/ahat/test/InstanceTest.java
new file mode 100644
index 0000000..7173b11
--- /dev/null
+++ b/tools/ahat/test/InstanceTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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 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 reverseReferencesAreNotUnreachable() throws IOException {
+    TestDump dump = TestDump.getTestDump();
+    AhatInstance obj = dump.getDumpedAhatInstance("basicString");
+    assertEquals(2, obj.getHardReverseReferences().size());
+    assertEquals(0, obj.getSoftReverseReferences().size());
+  }
+
+  @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());
+  }
+}