Fix hprof/ahat for string compression.

Test: m test-art-host
Test: m ahat-test
Test: m test-art-host with string compression enabled
Test: m ahat-test with string compression enabled
Bug: 31040547
Change-Id: I660e39c586d23f4a95686d484ca108466e52d249
diff --git a/runtime/hprof/hprof.cc b/runtime/hprof/hprof.cc
index 3d3ad59..133502e 100644
--- a/runtime/hprof/hprof.cc
+++ b/runtime/hprof/hprof.cc
@@ -224,12 +224,6 @@
     HandleU1List(values, count);
     length_ += count;
   }
-  void AddU1AsU2List(const uint8_t* values, size_t count) {
-    HandleU1AsU2List(values, count);
-    // Array of char from compressed String (8-bit) is added as 16-bit blocks
-    int ceil_count_to_even = count + ((count & 1) ? 1 : 0);
-    length_ += ceil_count_to_even * sizeof(uint8_t);
-  }
   void AddU2List(const uint16_t* values, size_t count) {
     HandleU2List(values, count);
     length_ += count * sizeof(uint16_t);
@@ -1277,7 +1271,7 @@
     HprofBasicType t = SignatureToBasicTypeAndSize(f->GetTypeDescriptor(), nullptr);
     __ AddU1(t);
   }
-  // Add native value character array for strings.
+  // Add native value character array for strings / byte array for compressed strings.
   if (klass->IsStringClass()) {
     __ AddStringId(LookupStringId("value"));
     __ AddU1(hprof_basic_object);
@@ -1359,8 +1353,16 @@
       case hprof_basic_short:
         __ AddU2(f->GetShort(obj));
         break;
-      case hprof_basic_float:
       case hprof_basic_int:
+        if (mirror::kUseStringCompression &&
+            klass->IsStringClass() &&
+            f->GetOffset().SizeValue() == mirror::String::CountOffset().SizeValue()) {
+          // Store the string length instead of the raw count field with compression flag.
+          __ AddU4(obj->AsString()->GetLength());
+          break;
+        }
+        FALLTHROUGH_INTENDED;
+      case hprof_basic_float:
       case hprof_basic_object:
         __ AddU4(f->Get32(obj));
         break;
@@ -1397,16 +1399,15 @@
   CHECK_EQ(obj->IsString(), string_value != nullptr);
   if (string_value != nullptr) {
     mirror::String* s = obj->AsString();
-    // Compressed string's (8-bit) length is ceil(length/2) in 16-bit blocks
-    int length_in_16_bit = (s->IsCompressed()) ? ((s->GetLength() + 1) / 2) : s->GetLength();
     __ AddU1(HPROF_PRIMITIVE_ARRAY_DUMP);
     __ AddObjectId(string_value);
     __ AddStackTraceSerialNumber(LookupStackTraceSerialNumber(obj));
-    __ AddU4(length_in_16_bit);
-    __ AddU1(hprof_basic_char);
+    __ AddU4(s->GetLength());
     if (s->IsCompressed()) {
-      __ AddU1AsU2List(s->GetValueCompressed(), s->GetLength());
+      __ AddU1(hprof_basic_byte);
+      __ AddU1List(s->GetValueCompressed(), s->GetLength());
     } else {
+      __ AddU1(hprof_basic_char);
       __ AddU2List(s->GetValue(), s->GetLength());
     }
   }
diff --git a/runtime/mirror/string.h b/runtime/mirror/string.h
index 95b6c3e..409c6c2 100644
--- a/runtime/mirror/string.h
+++ b/runtime/mirror/string.h
@@ -241,8 +241,9 @@
       REQUIRES_SHARED(Locks::mutator_lock_) REQUIRES(!Roles::uninterruptible_);
 
   // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
-  // First bit (uppermost/leftmost) is taken out for Compressed/Uncompressed flag
-  // [0] Uncompressed: string uses 16-bit memory | [1] Compressed: 8-bit memory
+
+  // If string compression is enabled, count_ holds the StringCompressionFlag in the
+  // least significant bit and the length in the remaining bits, length = count_ >> 1.
   int32_t count_;
 
   uint32_t hash_code_;
diff --git a/tools/ahat/src/InstanceUtils.java b/tools/ahat/src/InstanceUtils.java
index 94934a2..a062afd 100644
--- a/tools/ahat/src/InstanceUtils.java
+++ b/tools/ahat/src/InstanceUtils.java
@@ -26,6 +26,7 @@
 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;
@@ -87,22 +88,27 @@
     // 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.
-    Object value = inst;
-    if (isInstanceOfClass(inst, "java.lang.String")) {
-      value = getField(inst, "value");
-    }
+    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;
     }
-
-    int numChars = chars.getLength();
-    int count = getIntField(inst, "count", numChars);
     if (count == 0) {
       return "";
     }
@@ -110,7 +116,6 @@
       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));
diff --git a/tools/ahat/test-dump/Main.java b/tools/ahat/test-dump/Main.java
index e08df67..587d9de 100644
--- a/tools/ahat/test-dump/Main.java
+++ b/tools/ahat/test-dump/Main.java
@@ -45,6 +45,8 @@
   // 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 embeddedZero = "embedded\0...";  // Non-ASCII for string compression purposes.
     public char[] charArray = "char thing".toCharArray();
     public String nullString = null;
     public Object anObject = new Object();
diff --git a/tools/ahat/test/InstanceUtilsTest.java b/tools/ahat/test/InstanceUtilsTest.java
index ec77e70..fe2706d 100644
--- a/tools/ahat/test/InstanceUtilsTest.java
+++ b/tools/ahat/test/InstanceUtilsTest.java
@@ -37,6 +37,20 @@
   }
 
   @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");
@@ -51,6 +65,20 @@
   }
 
   @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");
@@ -65,6 +93,20 @@
   }
 
   @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");
@@ -79,6 +121,20 @@
   }
 
   @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");
@@ -93,6 +149,20 @@
   }
 
   @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");