Limit summary string lengths to 200 characters.

Strings longer than 200 characters are now truncated and are shown
with a trailing "..." instead of an end quote. The number 200 was
chosen arbitrarily.

Bug: 23223379
Change-Id: I96d7c9d563026233ff5f4962245b4c276d776a58
diff --git a/tools/ahat/README.txt b/tools/ahat/README.txt
index 5615f8f..d6f55aa 100644
--- a/tools/ahat/README.txt
+++ b/tools/ahat/README.txt
@@ -28,8 +28,6 @@
    - Use consistent order for heap columns.
       Sometimes I see "app" first, sometimes last (from one heap dump to
       another) How about, always sort by name?
- * For long strings, limit the string length shown in the summary view to
-   something reasonable.  Say 50 chars, then add a "..." at the end.
  * For HeapTable with single heap shown, the heap name isn't centered?
  * Consistently document functions.
  * Should help be part of an AhatHandler, that automatically gets the menu and
@@ -70,6 +68,7 @@
    showing all the instances.
  * That InstanceUtils.asString properly takes into account "offset" and
    "count" fields, if they are present.
+ * InstanceUtils.getDexCacheLocation
 
 Reported Issues:
  * Request to be able to sort tables by size.
diff --git a/tools/ahat/src/InstanceUtils.java b/tools/ahat/src/InstanceUtils.java
index eb9e363..c2d75c4 100644
--- a/tools/ahat/src/InstanceUtils.java
+++ b/tools/ahat/src/InstanceUtils.java
@@ -60,9 +60,21 @@
   }
 
 
-  // Read the string value from an hprof Instance.
-  // Returns null if the object can't be interpreted as a string.
+  /**
+   * 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) {
     if (!isInstanceOfClass(inst, "java.lang.String")) {
       return null;
     }
@@ -81,13 +93,15 @@
     // array, we should use that here.
     int numChars = chars.getValues().length;
     int count = getIntField(inst, "count", numChars);
-    int offset = getIntField(inst, "offset", 0);
-    int end = offset + count - 1;
-
     if (count == 0) {
       return "";
     }
+    if (0 <= maxChars && maxChars < count) {
+      count = maxChars;
+    }
 
+    int offset = getIntField(inst, "offset", 0);
+    int end = offset + count - 1;
     if (offset >= 0 && offset < numChars && end >= 0 && end < numChars) {
       return new String(chars.asCharArray(offset, count));
     }
@@ -234,12 +248,14 @@
    * 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) {
+  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);
+        return asString(location, maxChars);
       }
     }
     return null;
diff --git a/tools/ahat/src/Value.java b/tools/ahat/src/Value.java
index 9b483fa..4eb27b1 100644
--- a/tools/ahat/src/Value.java
+++ b/tools/ahat/src/Value.java
@@ -25,6 +25,10 @@
  */
 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.
    */
@@ -43,15 +47,19 @@
     link.append(inst.toString());
 
     // Annotate Strings with their values.
-    String stringValue = InstanceUtils.asString(inst);
+    String stringValue = InstanceUtils.asString(inst, kMaxChars);
     if (stringValue != null) {
-      link.appendFormat("\"%s\"", stringValue);
+      link.appendFormat("\"%s", stringValue);
+      link.append(kMaxChars == stringValue.length() ? "..." : "\"");
     }
 
     // Annotate DexCache with its location.
-    String dexCacheLocation = InstanceUtils.getDexCacheLocation(inst);
+    String dexCacheLocation = InstanceUtils.getDexCacheLocation(inst, kMaxChars);
     if (dexCacheLocation != null) {
-      link.append(" for " + dexCacheLocation);
+      link.appendFormat(" for %s", dexCacheLocation);
+      if (kMaxChars == dexCacheLocation.length()) {
+        link.append("...");
+      }
     }
 
     URI objTarget = DocString.formattedUri("object?id=%d", inst.getId());
diff --git a/tools/ahat/src/manifest.txt b/tools/ahat/src/manifest.txt
index 7efb1a7..421de17 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.2
+Implementation-Version: 0.3
 Main-Class: com.android.ahat.Main
diff --git a/tools/ahat/test/InstanceUtilsTest.java b/tools/ahat/test/InstanceUtilsTest.java
index 7613df4..11f82a2 100644
--- a/tools/ahat/test/InstanceUtilsTest.java
+++ b/tools/ahat/test/InstanceUtilsTest.java
@@ -25,21 +25,49 @@
 
 public class InstanceUtilsTest {
   @Test
-  public void basicString() throws IOException {
+  public void asStringBasic() throws IOException {
     TestDump dump = TestDump.getTestDump();
     Instance str = (Instance)dump.getDumpedThing("basicString");
     assertEquals("hello, world", InstanceUtils.asString(str));
   }
 
   @Test
-  public void nullString() throws IOException {
+  public void asStringTruncated() throws IOException {
+    TestDump dump = TestDump.getTestDump();
+    Instance str = (Instance)dump.getDumpedThing("basicString");
+    assertEquals("hello", 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 asStringNotTruncated() throws IOException {
+    TestDump dump = TestDump.getTestDump();
+    Instance str = (Instance)dump.getDumpedThing("basicString");
+    assertEquals("hello, world", 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 asStringNull() throws IOException {
     TestDump dump = TestDump.getTestDump();
     Instance obj = (Instance)dump.getDumpedThing("nullString");
     assertNull(InstanceUtils.asString(obj));
   }
 
   @Test
-  public void notString() throws IOException {
+  public void asStringNotString() throws IOException {
     TestDump dump = TestDump.getTestDump();
     Instance obj = (Instance)dump.getDumpedThing("anObject");
     assertNotNull(obj);