Merge "Merge Android Pie into master"
diff --git a/Android.mk b/Android.mk
index 1427f4b..d68f654 100644
--- a/Android.mk
+++ b/Android.mk
@@ -19,7 +19,7 @@
 # Subprojects with separate makefiles
 #
 
-subdirs := benchmarks tzdata ojluni tools/upstream
+subdirs := benchmarks tzdata ojluni tools/upstream metrictests
 subdir_makefiles := $(call all-named-subdir-makefiles,$(subdirs))
 
 #
diff --git a/JavaLibrary.bp b/JavaLibrary.bp
index 517cc05..4024a43 100644
--- a/JavaLibrary.bp
+++ b/JavaLibrary.bp
@@ -109,6 +109,22 @@
     openjdk9: {
         javacflags: ["--patch-module=java.base=."],
     },
+    jacoco: {
+        exclude_filter: [
+            "java.lang.Class",
+            "java.lang.Long",
+            "java.lang.Number",
+            "java.lang.Object",
+            "java.lang.String",
+            "java.lang.invoke.MethodHandle",
+            "java.lang.ref.Reference",
+            "java.lang.reflect.Proxy",
+            "java.util.AbstractMap",
+            "java.util.HashMap",
+            "java.util.HashMap$Node",
+            "java.util.Map",
+        ],
+    },
 
     notice: "ojluni/NOTICE",
 
@@ -137,6 +153,12 @@
     openjdk9: {
         javacflags: ["--patch-module=java.base=."],
     },
+    jacoco: {
+        exclude_filter: [
+            "java.lang.DexCache",
+            "dalvik.system.ClassExt",
+        ],
+    },
 
     required: [
         "tzdata",
diff --git a/benchmarks/Android.mk b/benchmarks/Android.mk
index c48c224..b73f167 100644
--- a/benchmarks/Android.mk
+++ b/benchmarks/Android.mk
@@ -28,7 +28,7 @@
   core-oj \
   core-libart \
   conscrypt \
-  legacy-test \
+  android.test.base \
   bouncycastle \
   framework
 LOCAL_MODULE_TAGS := tests
diff --git a/dalvik/src/main/java/dalvik/system/DexFile.java b/dalvik/src/main/java/dalvik/system/DexFile.java
index bc737b8..afedba5 100644
--- a/dalvik/src/main/java/dalvik/system/DexFile.java
+++ b/dalvik/src/main/java/dalvik/system/DexFile.java
@@ -17,13 +17,11 @@
 package dalvik.system;
 
 import android.system.ErrnoException;
-import android.system.StructStat;
 import dalvik.annotation.optimization.ReachabilitySensitive;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.List;
@@ -527,6 +525,46 @@
         throws FileNotFoundException;
 
     /**
+     * Encapsulates information about the optimizations performed on a dex file.
+     *
+     * Note that the info is only meant for debugging and is not guaranteed to be
+     * stable across releases and/or devices.
+     *
+     * @hide
+     */
+    public static final class OptimizationInfo {
+        // The optimization status.
+        private final String status;
+        // The optimization reason. The reason might be "unknown" if the
+        // the compiler artifacts were not annotated during optimizations.
+        private final String reason;
+
+        private OptimizationInfo(String status, String reason) {
+            this.status = status;
+            this.reason = reason;
+        }
+
+        public String getStatus() {
+            return status;
+        }
+
+        public String getReason() {
+            return reason;
+        }
+    }
+
+    /**
+     * Retrieves the optimization info for a dex file.
+     *
+     * @hide
+     */
+    public static OptimizationInfo getDexFileOptimizationInfo(
+            String fileName, String instructionSet) throws FileNotFoundException {
+        String[] status = getDexFileOptimizationStatus(fileName, instructionSet);
+        return new OptimizationInfo(status[0], status[1]);
+    }
+
+    /**
      * Returns the optimization status of the dex file {@code fileName}. The returned
      * array will have 2 elements which specify:
      *   - index 0: the level of optimizations
@@ -538,7 +576,7 @@
      *
      * @hide
      */
-    public static native String[] getDexFileOptimizationStatus(
+    private static native String[] getDexFileOptimizationStatus(
             String fileName, String instructionSet) throws FileNotFoundException;
 
     /**
diff --git a/dalvik/src/main/java/dalvik/system/DexPathList.java b/dalvik/src/main/java/dalvik/system/DexPathList.java
index a32da4f..e68e365 100644
--- a/dalvik/src/main/java/dalvik/system/DexPathList.java
+++ b/dalvik/src/main/java/dalvik/system/DexPathList.java
@@ -30,7 +30,6 @@
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 import libcore.io.ClassPathURLStreamHandler;
 import libcore.io.IoUtils;
 import libcore.io.Libcore;
diff --git a/libart/src/main/java/dalvik/system/VMRuntime.java b/libart/src/main/java/dalvik/system/VMRuntime.java
index af6323c..78001fa 100644
--- a/libart/src/main/java/dalvik/system/VMRuntime.java
+++ b/libart/src/main/java/dalvik/system/VMRuntime.java
@@ -472,4 +472,9 @@
      * behaviour is to dedupe.
      */
     public static native void setDedupeHiddenApiWarnings(boolean dedupe);
+
+    /**
+     * Sets the package name of the app running in this process.
+     */
+    public static native void setProcessPackageName(String packageName);
 }
diff --git a/luni/src/main/java/libcore/util/Objects.java b/luni/src/main/java/libcore/util/Objects.java
index 573b973..3781fcf 100644
--- a/luni/src/main/java/libcore/util/Objects.java
+++ b/luni/src/main/java/libcore/util/Objects.java
@@ -24,17 +24,6 @@
     private Objects() {}
 
     /**
-     * Returns true if two possibly-null objects are equal.
-     */
-    public static boolean equal(Object a, Object b) {
-        return a == b || (a != null && a.equals(b));
-    }
-
-    public static int hashCode(Object o) {
-        return (o == null) ? 0 : o.hashCode();
-    }
-
-    /**
      * Returns a string reporting the value of each declared field, via reflection.
      * Static and transient fields are automatically skipped. Produces output like
      * "SimpleClassName[integer=1234,string="hello",character='c',intArray=[1,2,3]]".
diff --git a/luni/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java b/luni/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
index 289aef7..0749998 100644
--- a/luni/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
+++ b/luni/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
@@ -18,7 +18,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import libcore.util.Objects;
+import java.util.Objects;
 import org.w3c.dom.Attr;
 import org.w3c.dom.DOMException;
 import org.w3c.dom.Element;
@@ -59,7 +59,7 @@
     private int indexOfAttribute(String name) {
         for (int i = 0; i < attributes.size(); i++) {
             AttrImpl attr = attributes.get(i);
-            if (Objects.equal(name, attr.getNodeName())) {
+            if (Objects.equals(name, attr.getNodeName())) {
                 return i;
             }
         }
@@ -70,8 +70,8 @@
     private int indexOfAttributeNS(String namespaceURI, String localName) {
         for (int i = 0; i < attributes.size(); i++) {
             AttrImpl attr = attributes.get(i);
-            if (Objects.equal(namespaceURI, attr.getNamespaceURI())
-                    && Objects.equal(localName, attr.getLocalName())) {
+            if (Objects.equals(namespaceURI, attr.getNamespaceURI())
+                    && Objects.equals(localName, attr.getLocalName())) {
                 return i;
             }
         }
diff --git a/luni/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java b/luni/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java
index 4bea1b4..fea7b86 100644
--- a/luni/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java
+++ b/luni/src/main/java/org/apache/harmony/xml/dom/InnerNodeImpl.java
@@ -18,7 +18,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import libcore.util.Objects;
+import java.util.Objects;
 import org.w3c.dom.DOMException;
 import org.w3c.dom.DocumentFragment;
 import org.w3c.dom.Node;
@@ -262,6 +262,6 @@
      * may be {@code null}.
      */
     private static boolean matchesNameOrWildcard(String pattern, String s) {
-        return "*".equals(pattern) || Objects.equal(pattern, s);
+        return "*".equals(pattern) || Objects.equals(pattern, s);
     }
 }
diff --git a/luni/src/test/java/libcore/java/util/CollectionsTest.java b/luni/src/test/java/libcore/java/util/CollectionsTest.java
index d09cb83..8ca3122 100644
--- a/luni/src/test/java/libcore/java/util/CollectionsTest.java
+++ b/luni/src/test/java/libcore/java/util/CollectionsTest.java
@@ -36,6 +36,7 @@
 import java.util.NavigableMap;
 import java.util.NavigableSet;
 import java.util.NoSuchElementException;
+import java.util.Objects;
 import java.util.Queue;
 import java.util.Set;
 import java.util.SortedMap;
@@ -48,8 +49,6 @@
 import junit.framework.AssertionFailedError;
 import junit.framework.TestCase;
 
-import libcore.util.Objects;
-
 import dalvik.system.VMRuntime;
 
 import static java.util.Collections.checkedNavigableMap;
@@ -905,7 +904,7 @@
             assertNull(floor);
             assertNull(ceiling);
         } else {
-            assertFalse(Objects.equal(floor, ceiling));
+            assertFalse(Objects.equals(floor, ceiling));
             assertTrue(floor != null || ceiling != null);
             assertEquals(ceiling, floor == null ? map.firstKey() : map.higherKey(floor));
             assertEquals(floor, ceiling == null ? map.lastKey() : map.lowerKey(ceiling));
@@ -1139,7 +1138,7 @@
             assertNull(floor);
             assertNull(ceiling);
         } else {
-            assertFalse(Objects.equal(floor, ceiling));
+            assertFalse(Objects.equals(floor, ceiling));
             assertTrue(floor != null || ceiling != null);
         }
     }
diff --git a/metrictests/Android.mk b/metrictests/Android.mk
new file mode 100644
index 0000000..c362718
--- /dev/null
+++ b/metrictests/Android.mk
@@ -0,0 +1,17 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(call all-subdir-makefiles)
diff --git a/metrictests/memory/Android.mk b/metrictests/memory/Android.mk
new file mode 100644
index 0000000..9e5489c
--- /dev/null
+++ b/metrictests/memory/Android.mk
@@ -0,0 +1,17 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(call all-subdir-makefiles)
\ No newline at end of file
diff --git a/metrictests/memory/README b/metrictests/memory/README
new file mode 100644
index 0000000..77574ef
--- /dev/null
+++ b/metrictests/memory/README
@@ -0,0 +1,92 @@
+This directory contains android activities and host-side tests relating to libcore memory metrics.
+
+Directory structure
+===================
+
+apps
+  - Android activities. See instructions below for use.
+
+host
+  - Host-side test code (which installs & runs activites). See instructions below for use.
+
+Running LibcoreHeapMetricsTest
+==============================
+
+You can manually run this as follows:
+
+  make tradefed-all libcore-memory-metrics-tests LibcoreHeapDumper ahat
+  tradefed.sh run commandAndExit template/local_min --template:map test=libcore-memory-metrics-tests
+
+This installs and runs the LibcoreHeapDumpActivity on the device, pulls the heaps back to the host,
+analyses them, and derives metrics. You can see the metrics in the tradefed output.
+
+Manually running HeapDumpInstrumentation
+========================================
+
+This instrumentation dumps a heap, performs some configurable action (see the code for details), and
+then dumps another heap. You can run it manually as follows:
+
+  make LibcoreHeapDumper
+  adb install -g -r ${ANDROID_PRODUCT_OUT}/data/app/LibcoreHeapDumper/LibcoreHeapDumper.apk
+
+  DEVICE_EXTERNAL_STORAGE=$(adb shell 'echo -n ${EXTERNAL_STORAGE}')
+  # Pick a suitable name here:
+  RELATIVE_DIR=dumps
+  adb shell mkdir ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}
+  # It's okay if this does nothing:
+  adb shell rm -r ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}/*
+  # Pick the action you want here:
+  DUMPER_ACTION=NOOP
+  adb shell am instrument -w -e dumpdir ${RELATIVE_DIR} -e action ${DUMPER_ACTION} libcore.heapdumper/.HeapDumpInstrumentation
+  adb shell ls ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}
+  # That normally shows before.hprof and after.hprof files. If it shows an error file, adb shell cat
+  # it to see what happened. If it doesn't show anything, adb logcat to see what happened.
+
+  LOCAL_DIR=/tmp
+  mkdir -p ${LOCAL_DIR}/${RELATIVE_DIR}
+  # It's okay if this does nothing:
+  rm -r ${LOCAL_DIR}/${RELATIVE_DIR}/*
+  adb pull ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR} ${LOCAL_DIR}
+  ls ${LOCAL_DIR}/${RELATIVE_DIR}
+
+  make ahat
+  # To examine the first heap dump:
+  ahat ${LOCAL_DIR}/${RELATIVE_DIR}/before.hprof
+  # Visit the localhost URL shown; ctrl-C to exit
+  # To diff the second heap dump against the first:
+  ahat ${LOCAL_DIR}/${RELATIVE_DIR}/after.hprof --baseline ${LOCAL_DIR}/${RELATIVE_DIR}/before.hprof
+
+  # To clean up:
+  rm -r ${LOCAL_DIR}/${RELATIVE_DIR}
+  adb shell rm -r ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}
+  adb uninstall libcore.heapdumper
+
+Manually running PssInstrumentation
+===================================
+
+This instrumentation measures the PSS in kB, performs some configurable action (see the code for
+details), and then measures the PSS again. You can run it manually as follows:
+
+  make LibcoreHeapDumper
+  adb install -g -r ${ANDROID_PRODUCT_OUT}/data/app/LibcoreHeapDumper/LibcoreHeapDumper.apk
+
+  DEVICE_EXTERNAL_STORAGE=$(adb shell 'echo -n ${EXTERNAL_STORAGE}')
+  # Pick a suitable name here:
+  RELATIVE_DIR=pss
+  adb shell mkdir ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}
+  # It's okay if this does nothing:
+  adb shell rm -r ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}/*
+  # Pick the action you want here:
+  DUMPER_ACTION=NOOP
+  adb shell am instrument -w -e dumpdir ${RELATIVE_DIR} -e action ${DUMPER_ACTION} libcore.heapdumper/.PssInstrumentation
+  adb shell ls ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}
+  # That normally shows before.pss.txt and after.pss.txt files. If it shows an error file, adb shell
+  # cat it to see what happened. If it doesn't show anything, adb logcat to see what happened.
+
+  # To see the PSS measurements in kB:
+  adb shell cat ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}/before.pss.txt
+  adb shell cat ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}/after.pss.txt
+
+  # To clean up:
+  adb shell rm -r ${DEVICE_EXTERNAL_STORAGE}/${RELATIVE_DIR}
+  adb uninstall libcore.heapdumper
diff --git a/metrictests/memory/apps/Android.mk b/metrictests/memory/apps/Android.mk
new file mode 100644
index 0000000..98a8ca8
--- /dev/null
+++ b/metrictests/memory/apps/Android.mk
@@ -0,0 +1,30 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_PACKAGE_NAME := LibcoreHeapDumper
+LOCAL_SDK_VERSION := current
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_COMPATIBILITY_SUITE := general-tests
+
+include $(BUILD_PACKAGE)
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/metrictests/memory/apps/AndroidManifest.xml b/metrictests/memory/apps/AndroidManifest.xml
new file mode 100644
index 0000000..a8856aa
--- /dev/null
+++ b/metrictests/memory/apps/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="libcore.heapdumper">
+
+    <uses-sdk android:minSdkVersion="19" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <instrumentation
+            android:name="libcore.heapdumper.HeapDumpInstrumentation"
+            android:targetPackage="libcore.heapdumper" />
+
+    <instrumentation
+            android:name="libcore.heapdumper.PssInstrumentation"
+            android:targetPackage="libcore.heapdumper" />
+
+    <application
+            android:allowBackup="false"
+            android:label="@string/libcore_heap_dumper_title">
+    </application>
+</manifest>
diff --git a/metrictests/memory/apps/res/values/strings.xml b/metrictests/memory/apps/res/values/strings.xml
new file mode 100644
index 0000000..24c640f
--- /dev/null
+++ b/metrictests/memory/apps/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Title of the application. -->
+    <string name="libcore_heap_dumper_title">Libcore heap dumper</string>
+
+</resources>
diff --git a/metrictests/memory/apps/src/libcore/heapdumper/AbstractMetricInstrumentation.java b/metrictests/memory/apps/src/libcore/heapdumper/AbstractMetricInstrumentation.java
new file mode 100644
index 0000000..3f539a1
--- /dev/null
+++ b/metrictests/memory/apps/src/libcore/heapdumper/AbstractMetricInstrumentation.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapdumper;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An abstract base class for an {@link Instrumentation} that takes some measurement, performs some
+ * action, and then takes the measurement again, all without launching any activities.
+ *
+ * <p>The metric to be collected is defined by the concrete subclass's implementation of
+ * {@link #takeMeasurement}. The action to be performed is determined by an invocation argument.
+ *
+ * <p>The instrumentation should be invoked with two arguments:
+ * <ul>
+ *     <li>one called {@code dumpdir} which gives the name of a directory to put the dumps in,
+ *     relative to the public external storage directory;
+ *     <li>one called {@code action} which gives the name of an {@link Actions} value to run between
+ *     the two measurements.
+ * </ul>
+ *
+ * <p>If there is a problem, it will try to create a file called {@code error} in the output
+ * directory, containing a failure message.
+ */
+public abstract class AbstractMetricInstrumentation extends Instrumentation {
+
+    private static final String TAG = "AbstractMetricInstrumentation";
+
+    private File mOutputDirectory;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        mOutputDirectory = resolveOutputDirectory(icicle);
+        try {
+            Runnable mAction = loadAction(icicle);
+            takeMeasurement("before");
+            mAction.run();
+            takeMeasurement("after");
+        } catch (Exception e) {
+            recordException(e);
+        }
+        super.onCreate(icicle);
+        finish(Activity.RESULT_OK, new Bundle());
+    }
+
+    /**
+     * Takes a measurement, including the given label in the filename of the output.
+     */
+    protected abstract void takeMeasurement(String label) throws IOException;
+
+    /**
+     * Returns a {@link File} in the correct output directory with the given relative filename.
+     */
+    protected final File resolveRelativeOutputFilename(String relativeOutputFilename) {
+        return new File(mOutputDirectory, relativeOutputFilename);
+    }
+
+    /**
+     * Does its best to force as much garbage as possible to be collected.
+     */
+    protected final void tryRemoveGarbage() {
+        Runtime runtime = Runtime.getRuntime();
+        // Do a GC run.
+        runtime.gc();
+        // Run finalizers for any objects pending finalization.
+        runtime.runFinalization();
+        // Do another GC run, for objects made eligible for collection by the finalization process.
+        runtime.gc();
+    }
+
+    /**
+     * Resolves the directory to use for output, based on the arguments in the bundle.
+     */
+    private static File resolveOutputDirectory(Bundle icicle) {
+        String relativeDirectoryName = icicle.getString("dumpdir");
+        if (relativeDirectoryName == null) {
+            throw new IllegalArgumentException(
+                    "Instrumentation invocation missing dumpdir argument");
+        }
+        if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
+            throw new IllegalStateException("External storage unavailable");
+        }
+        File dir = Environment.getExternalStoragePublicDirectory(relativeDirectoryName);
+        if (!dir.isDirectory()) {
+            throw new IllegalArgumentException(
+                    "Instrumentation invocation's dumpdir argument is not a directory: "
+                            + dir.getAbsolutePath());
+        }
+        return dir;
+    }
+
+    /**
+     * Returns the {@link Runnable} to run between measurements, based on the arguments in the
+     * bundle.
+     */
+    private static Runnable loadAction(Bundle icicle) {
+        String name = icicle.getString("action");
+        if (name == null) {
+            throw new IllegalArgumentException(
+                    "Instrumentation invocation missing action argument");
+        }
+        return Actions.valueOf(name);
+    }
+
+    /**
+     * Write an {@code error} file into {@link #mOutputDirectory} containing the message of the
+     * exception.
+     */
+    private void recordException(Exception e) {
+        Log.e(TAG, "Exception while taking measurements", e);
+        String contents = e.getMessage();
+        File errorFile = new File(mOutputDirectory, "error");
+        try {
+            try (OutputStream errorStream = new FileOutputStream(errorFile)) {
+                errorStream.write(contents.getBytes("UTF-8"));
+            }
+        } catch (IOException e2) {
+            throw new RuntimeException("Exception writing error file!", e2);
+        }
+    }
+}
diff --git a/metrictests/memory/apps/src/libcore/heapdumper/Actions.java b/metrictests/memory/apps/src/libcore/heapdumper/Actions.java
new file mode 100644
index 0000000..e8b56f1
--- /dev/null
+++ b/metrictests/memory/apps/src/libcore/heapdumper/Actions.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapdumper;
+
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * An enumeration of actions for which we'd like to measure the effect on the post-GC heap.
+ */
+enum Actions implements Runnable {
+
+    /**
+     * Does nothing. Exists to measure the overhead inherent in the measurement system.
+     */
+    NOOP {
+        @Override
+        public void run() {
+            // noop!
+        }
+    },
+
+    /**
+     * Uses a collator for the root locale to trivially sort some strings.
+     */
+    COLLATOR_ROOT_LOCALE {
+        @Override
+        public void run() {
+            useCollatorForLocale(Locale.ROOT);
+        }
+    },
+
+    /**
+     * Uses a collator for the US English locale to trivially sort some strings.
+     */
+    COLLATOR_EN_US_LOCALE {
+        @Override
+        public void run() {
+            useCollatorForLocale(Locale.US);
+        }
+    },
+
+    /**
+     * Uses a collator for the Korean locale to trivially sort some strings.
+     */
+    COLLATOR_KOREAN_LOCALE {
+        @Override
+        public void run() {
+            useCollatorForLocale(Locale.KOREAN);
+        }
+    },
+
+    ;
+
+    private static void useCollatorForLocale(Locale locale) {
+        String[] strings = { "caff", "café", "cafe", "안녕", "잘 가" };
+        Collator collator = Collator.getInstance(locale);
+        Arrays.sort(strings, collator);
+    }
+}
diff --git a/metrictests/memory/apps/src/libcore/heapdumper/HeapDumpInstrumentation.java b/metrictests/memory/apps/src/libcore/heapdumper/HeapDumpInstrumentation.java
new file mode 100644
index 0000000..30cf6cf
--- /dev/null
+++ b/metrictests/memory/apps/src/libcore/heapdumper/HeapDumpInstrumentation.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapdumper;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Debug;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A specialization of {@link AbstractMetricInstrumentation} where the measurement is taken by
+ * dumping the process's heaps.
+ */
+public class HeapDumpInstrumentation extends AbstractMetricInstrumentation {
+
+    private static final String TAG = "HeapDumpInstrumentation";
+
+    @Override
+    protected void takeMeasurement(String label) throws IOException {
+        File dumpFile = resolveRelativeOutputFilename(label + ".hprof");
+        tryRemoveGarbage();
+        Debug.dumpHprofData(dumpFile.getCanonicalPath());
+        Log.i(TAG, "Wrote to heap dump to " + dumpFile.getCanonicalPath());
+    }
+}
diff --git a/metrictests/memory/apps/src/libcore/heapdumper/PssInstrumentation.java b/metrictests/memory/apps/src/libcore/heapdumper/PssInstrumentation.java
new file mode 100644
index 0000000..f6feb35
--- /dev/null
+++ b/metrictests/memory/apps/src/libcore/heapdumper/PssInstrumentation.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapdumper;
+
+import android.app.ActivityManager;
+import android.os.Debug;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A specialization of {@link AbstractMetricInstrumentation} where the measurement taken is the
+ * process's total PSS in kB.
+ */
+public class PssInstrumentation extends AbstractMetricInstrumentation {
+
+    private static final String TAG = "PssInstrumentation";
+
+    @Override
+    protected void takeMeasurement(String label) throws IOException {
+        ActivityManager activityManager = getContext().getSystemService(ActivityManager.class);
+        tryRemoveGarbage();
+        Debug.MemoryInfo memoryInfo =
+                activityManager.getProcessMemoryInfo(new int[] { Process.myPid() })[0];
+        File output = resolveRelativeOutputFilename(label + ".pss.txt");
+        Charset cs = StandardCharsets.UTF_8;
+        try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(output), cs)) {
+            writer.append(Integer.toString(memoryInfo.getTotalPss()));
+        }
+        Log.i(TAG, "Wrote to total PSS in kB to " + output.getCanonicalPath());
+    }
+}
diff --git a/metrictests/memory/host/Android.mk b/metrictests/memory/host/Android.mk
new file mode 100644
index 0000000..cce0288
--- /dev/null
+++ b/metrictests/memory/host/Android.mk
@@ -0,0 +1,32 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := libcore-memory-metrics-tests
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_JAVA_LIBRARIES := tradefed ahat
+
+LOCAL_COMPATIBILITY_SUITE := general-tests
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/metrictests/memory/host/AndroidTest.xml b/metrictests/memory/host/AndroidTest.xml
new file mode 100644
index 0000000..4cd77b1
--- /dev/null
+++ b/metrictests/memory/host/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<configuration description="Testing zygote+heap and core library memory">
+    <option name="test-suite-tag" value="libcore" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="LibcoreHeapDumper.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="libcore.heapmetrics.LibcoreHeapMetricsTest" />
+    </test>
+</configuration>
diff --git a/metrictests/memory/host/src/libcore/heapmetrics/HeapCategorization.java b/metrictests/memory/host/src/libcore/heapmetrics/HeapCategorization.java
new file mode 100644
index 0000000..099b79f
--- /dev/null
+++ b/metrictests/memory/host/src/libcore/heapmetrics/HeapCategorization.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapmetrics;
+
+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.RootType;
+import com.android.ahat.heapdump.Size;
+
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Representation of the break-down of a heap dump into categories.
+ */
+class HeapCategorization
+{
+
+    /**
+     * Enumeration of the categories used.
+     */
+    enum HeapCategory {
+
+        /**
+         * Interned strings that are mostly ASCII alphabetic characters, and have a bit of
+         * whitespace. These are probably human-readable text e.g. error messages.
+         */
+        INTERNED_STRING_TEXT_ISH("internedStringTextIsh"),
+
+        /**
+         * Interned strings that are mostly non-ASCII alphabetic characters. These are probably ICU
+         * data.
+         */
+        INTERNED_STRING_UNICODE_ALPHABET_ISH("internedStringUnicodeAlphabetIsh"),
+
+        /**
+         * Interned strings that are don't meet the criterea of {@link #INTERNED_STRING_TEXT_ISH} or
+         * {@link #INTERNED_STRING_UNICODE_ALPHABET_ISH}. These are probably code e.g. a regex.
+         */
+        INTERNED_STRING_CODE_ISH("internedStringCodeIsh"),
+
+        /** Objects in a {@code android.icu} package, or strongly reachable from such an object. */
+        PACKAGE_ANDROID_ICU("packageAndroidIcu"),
+
+        /** Objects in a {@code android.util} or {@code com.internal.android.util} package, or
+         * strongly reachable from such an object. */
+        PACKAGE_ANDROID_UTIL("packageAndroidUtil"),
+
+        /**
+         * Objects in a {@code android} package other than {@code android.icu} or
+         * {@code android.util}, or a {@code com.android.internal} package other than
+         * {@code com.android.internal.util}, or strongly reachable from such an object. Includes
+         * {@code app}, {@code widget}, {@code graphics}, {@code os}, and many more.
+         */
+        ANDROID_FRAMEWORK("androidFramework"),
+
+        /**
+         * Objects in a {@code java.security}, {@code sun.security},
+         * {@code com.android.org.conscrypt}, or {@code com.android.org.bouncycastle} package, or
+         * strongly reachable from such an object.
+         */
+        SECURITY("security"),
+
+        /**
+         * Objects in a {@code com.android.org.conscrypt} package, or strongly reachable from such
+         * an object.
+         */
+        SECURITY_CONSCRYPT("securityConscrypt"),
+
+        /**
+         * Objects in a {@code com.android.org.bouncycastle} package, or strongly reachable from
+         * such an object.
+         */
+        SECURITY_BOUNCYCASTLE("securityBouncycastle"),
+
+        /**
+         * Objects in a {@code java.security.keystore} package, or strongly reachable from such an
+         * object.
+         */
+        SECURITY_KEYSTORE("securityKeystore"),
+
+        /**
+         * Objects in a {@code java}, {@code javax}, {@code sun}, {@code com.sun}, or
+         * {@code libcore} package, and strongly reachable only from such objects (i.e. the entire
+         * reference graph is libcore). Excludes interned strings (which are in {@code java.lang}
+         * and have no references).
+         */
+        PURE_LIBCORE("pureLibcore"),
+
+        /**
+         * The subset of {@link #PURE_LIBCORE} which is static, rather than instance, state.
+         */
+        PURE_LIBCORE_STATIC("pureLibcoreStatic"),
+
+        /**
+         * Objects which don't fall into any of the above categories. (N.B. This ensures that every
+         * object is in at least one category, but objects may be in more than one of the above.)
+         */
+        NONE_OF_THE_ABOVE("noneOfTheAbove"),
+        ;
+
+        private final String metricSuffix;
+
+        HeapCategory(String metricSuffix) {
+            this.metricSuffix = metricSuffix;
+        }
+
+        /**
+         * Returns the name for a metric using the given prefix and a category-specific suffix.
+         */
+        String metricName(String metricPrefix) {
+            return metricPrefix + metricSuffix;
+        }
+    }
+
+    /**
+     * Returns the categorization of the given heap dump, counting the retained sizes on the given
+     * heaps.
+     */
+    static HeapCategorization of(AhatSnapshot snapshot, AhatHeap... heaps) {
+        HeapCategorization categorization = new HeapCategorization(snapshot, heaps);
+        categorization.initializeFromSnapshot();
+        return categorization;
+    }
+
+    private final Map<HeapCategory, Size> sizesByCategory = new HashMap<>();
+    private final AhatSnapshot snapshot;
+    private final AhatHeap[] heaps;
+
+    private HeapCategorization(AhatSnapshot snapshot, AhatHeap[] heaps) {
+        this.snapshot = snapshot;
+        this.heaps = heaps;
+    }
+
+    /**
+     * Returns an analysis of the configured heap dump, giving the retained sizes on the configured
+     * heaps broken down by category.
+     */
+    Map<HeapCategory, Size> sizesByCategory() {
+        return Collections.unmodifiableMap(sizesByCategory);
+    }
+
+    private void initializeFromSnapshot() {
+        for (AhatInstance rooted : snapshot.getRooted()) {
+            initializeFromRooted(rooted);
+        }
+    }
+
+    private void initializeFromRooted(AhatInstance rooted) {
+        int categories = 0;
+        if (isInternedString(rooted)) {
+            HeapCategory category = categorizeInternedString(rooted.asString());
+            incrementSize(rooted, category);
+            categories++;
+        }
+
+        if (isOwnedByClassMatching(rooted, str -> str.startsWith("android.icu."))) {
+            incrementSize(rooted, HeapCategory.PACKAGE_ANDROID_ICU);
+            categories++;
+        }
+        if (isOwnedByClassMatching(rooted, this::isAndroidUtilClass)) {
+            incrementSize(rooted, HeapCategory.PACKAGE_ANDROID_UTIL);
+            categories++;
+        }
+        if (isOwnedByClassMatching(rooted, this::isAndroidFrameworkClass)) {
+            incrementSize(rooted, HeapCategory.ANDROID_FRAMEWORK);
+            categories++;
+        }
+        if (isOwnedByClassMatching(rooted, this::isSecurityClass)) {
+            incrementSize(rooted, HeapCategory.SECURITY);
+            categories++;
+        }
+        if (isOwnedByClassMatching(rooted, str -> str.startsWith("com.android.org.conscrypt."))) {
+            incrementSize(rooted, HeapCategory.SECURITY_CONSCRYPT);
+            categories++;
+        }
+        if (isOwnedByClassMatching(rooted, str -> str.startsWith("com.android.org.bouncycastle."))) {
+            incrementSize(rooted, HeapCategory.SECURITY_BOUNCYCASTLE);
+            categories++;
+        }
+        if (isOwnedByClassMatching(rooted, str -> str.startsWith("android.security.keystore."))) {
+            incrementSize(rooted, HeapCategory.SECURITY_KEYSTORE);
+            categories++;
+        }
+
+        if (!isInternedString(rooted) && !isOwnedByClassMatching(rooted, c -> !isLibcoreClass(c))) {
+            incrementSize(rooted, HeapCategory.PURE_LIBCORE);
+            categories++;
+        }
+        if (rooted.isClassObj() && isLibcoreClass(rooted.asClassObj().getName())) {
+            incrementSize(rooted, HeapCategory.PURE_LIBCORE_STATIC);
+            categories++;
+        }
+
+        if (categories == 0) {
+            incrementSize(rooted, HeapCategory.NONE_OF_THE_ABOVE);
+        }
+    }
+
+    private static boolean isInternedString(AhatInstance instance) {
+        if (!instance.isRoot()) {
+            return false;
+        }
+        for (RootType rootType : instance.getRootTypes()) {
+            if (rootType.equals(RootType.INTERNED_STRING)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a category for an interned {@link String} with the given value. The categorization is
+     * done based on heuristics tuned through experimentation.
+     */
+    private static HeapCategory categorizeInternedString(String string) {
+        int nonAsciiChars = 0;
+        int alphabeticChars = 0;
+        int whitespaceChars = 0;
+        int totalChars = string.length();
+        for (int i = 0; i < totalChars; i++) {
+            char c = string.charAt(i);
+            if (c > '~') {
+                nonAsciiChars++;
+            }
+            if (Character.isAlphabetic(c)) {
+                alphabeticChars++;
+            }
+            if (Character.isWhitespace(c)) {
+                whitespaceChars++;
+            }
+        }
+        if (nonAsciiChars >= 0.5 * totalChars && alphabeticChars >= 0.5 * totalChars) {
+            // At least 50% non-ASCII and at least 50% alphabetic. There's a good chance that this
+            // is backing some kind of ICU property structure.
+            return HeapCategory.INTERNED_STRING_UNICODE_ALPHABET_ISH;
+        } else if (alphabeticChars >= 0.75 * totalChars && whitespaceChars >= 0.05 * totalChars) {
+            // At least 75% alphabetic and at least 5% whitespace and less than 50% non-ASCII.
+            // There's a good chance this is human-readable text e.g. an error message.
+            return HeapCategory.INTERNED_STRING_TEXT_ISH;
+        } else {
+            // Neither of the above. There's a good chance that this is something code-like e.g. a
+            // regex.
+            return HeapCategory.INTERNED_STRING_CODE_ISH;
+        }
+    }
+
+    private boolean isAndroidUtilClass(String className) {
+        return className.startsWith("android.util.")
+                || className.startsWith("com.android.internal.util.");
+    }
+
+    private boolean isAndroidFrameworkClass(String className) {
+        return (className.startsWith("android.")
+                        && !className.startsWith("android.icu.")
+                        && !className.startsWith("android.util."))
+                ||
+                (className.startsWith("com.android.internal.")
+                        && !className.startsWith("com.android.internal.util."));
+    }
+
+    private boolean isSecurityClass(String className) {
+        return className.startsWith("java.security.")
+                || className.startsWith("sun.security.")
+                || className.startsWith("com.android.org.bouncycastle.")
+                || className.startsWith("com.android.org.conscrypt.");
+    }
+
+    private boolean isOwnedByClassMatching(AhatInstance rooted, Predicate<String> predicate) {
+        // Do a BFS of the strong reference graph looking for matching classes.
+        Set<AhatInstance> visited = new HashSet<>();
+        Queue<AhatInstance> queue = new ArrayDeque<>();
+        visited.add(rooted);
+        queue.add(rooted);
+        while (!queue.isEmpty()) {
+            AhatInstance instance = queue.remove();
+            if (instance.isClassObj()) {
+                // This is the heap allocation for the static state of a class. Check the class.
+                // Don't continue up the reference tree, as every instance of this class has a
+                // reference to it.
+                return predicate.test(instance.asClassObj().getName());
+            } else if (instance.isPlaceHolder()) {
+                // Placeholders have no retained size and so can be ignored.
+                return false;
+            } else {
+                // This is the heap allocation for the instance state of an object. Check its class.
+                // If it's not a match, continue searching up the strong reference graph.
+                AhatClassObj classObj = instance.getClassObj();
+                if (predicate.test(classObj.getName())) {
+                    return true;
+                } else {
+                    for (AhatInstance reference : instance.getHardReverseReferences()) {
+                        if (!visited.contains(reference)) {
+                            visited.add(reference);
+                            queue.add(reference);
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isLibcoreClass(String name) {
+        return name.startsWith("java.")
+                || name.startsWith("javax.")
+                || name.startsWith("sun.")
+                || name.startsWith("com.sun.")
+                || name.startsWith("libcore.");
+    }
+
+    /**
+     * Increments the stored size for the given category by the retain size of the given rooted
+     * instance on the configured heaps.
+     */
+    private void incrementSize(AhatInstance rooted, HeapCategory category) {
+        Size size = Size.ZERO;
+        for (AhatHeap heap : heaps) {
+            size = size.plus(rooted.getRetainedSize(heap));
+        }
+        if (sizesByCategory.containsKey(category)) {
+            sizesByCategory.put(category, sizesByCategory.get(category).plus(size));
+        } else {
+            sizesByCategory.put(category, size);
+        }
+    }
+}
diff --git a/metrictests/memory/host/src/libcore/heapmetrics/LibcoreHeapMetricsTest.java b/metrictests/memory/host/src/libcore/heapmetrics/LibcoreHeapMetricsTest.java
new file mode 100644
index 0000000..5340d90
--- /dev/null
+++ b/metrictests/memory/host/src/libcore/heapmetrics/LibcoreHeapMetricsTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapmetrics;
+
+import com.android.ahat.heapdump.AhatHeap;
+import com.android.ahat.heapdump.AhatInstance;
+import com.android.ahat.heapdump.AhatSnapshot;
+import com.android.ahat.heapdump.Size;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
+import com.android.tradefed.testtype.IDeviceTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.EnumMap;
+import java.util.Map;
+import libcore.heapmetrics.HeapCategorization.HeapCategory;
+
+/**
+ * Tests that gather metrics about zygote+image heap and about the impact of core library calls on
+ * app heap.
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class LibcoreHeapMetricsTest implements IDeviceTest {
+
+    @Rule public TestMetrics metrics = new TestMetrics();
+    @Rule public TestLogData logs = new TestLogData();
+
+    private ITestDevice testDevice;
+    private MetricsRunner metricsRunner;
+
+    @Override
+    public void setDevice(ITestDevice device) {
+        testDevice = device;
+    }
+
+    @Override
+    public ITestDevice getDevice() {
+        return testDevice;
+    }
+
+    @Before
+    public void initializeHeapDumperRunner() throws DeviceNotAvailableException {
+        metricsRunner = MetricsRunner.create(testDevice, logs);
+    }
+
+    @Test
+    public void measureNoop() throws Exception {
+        MetricsRunner.Result result = metricsRunner.runAllInstrumentations("NOOP");
+        AhatSnapshot beforeDump = result.getBeforeDump();
+        AhatSnapshot afterDump = result.getAfterDump();
+        recordHeapMetrics(beforeDump, "zygoteSize", "zygote");
+        recordHeapMetrics(beforeDump, "imageSize", "image");
+        Map<HeapCategory, Size> zygoteAndImageSizesByCategory = HeapCategorization
+                .of(beforeDump, beforeDump.getHeap("zygote"), beforeDump.getHeap("image"))
+                .sizesByCategory();
+        for (Map.Entry<HeapCategory, Size> entry : zygoteAndImageSizesByCategory.entrySet()) {
+            recordSizeMetric(entry.getKey().metricName("zygoteAndImage_"), entry.getValue());
+        }
+        recordBeforeAndAfterAppHeapMetrics(beforeDump, afterDump);
+        recordBytesMetric("beforeTotalPss", result.getBeforeTotalPssKb() * 1024L);
+        recordBytesMetric(
+                "deltaTotalPss",
+                (result.getAfterTotalPssKb() - result.getBeforeTotalPssKb()) * 1024L);
+    }
+
+    @Test
+    public void measureCollatorRootLocale() throws Exception {
+        MetricsRunner.Result result = metricsRunner.runAllInstrumentations("COLLATOR_ROOT_LOCALE");
+        recordBeforeAndAfterAppHeapMetrics(result.getBeforeDump(), result.getAfterDump());
+    }
+
+    @Test
+    public void measureCollatorEnUsLocale() throws Exception {
+        MetricsRunner.Result result = metricsRunner.runAllInstrumentations("COLLATOR_EN_US_LOCALE");
+        recordBeforeAndAfterAppHeapMetrics(result.getBeforeDump(), result.getAfterDump());
+    }
+
+    @Test
+    public void measureCollatorKoreanLocale() throws Exception {
+        MetricsRunner.Result result =
+                metricsRunner.runAllInstrumentations("COLLATOR_KOREAN_LOCALE");
+        recordBeforeAndAfterAppHeapMetrics(result.getBeforeDump(), result.getAfterDump());
+    }
+
+    private void recordHeapMetrics(AhatSnapshot dump, String metricPrefix, String heapName) {
+        AhatHeap heap = dump.getHeap(heapName);
+        recordSizeMetric(metricPrefix, heap.getSize());
+        Map<Reachability, Size> sizesByReachability = sizesByReachability(dump, heap);
+        for (Reachability reachability : Reachability.values()) {
+            recordSizeMetric(
+                    reachability.metricName(metricPrefix), sizesByReachability.get(reachability));
+        }
+    }
+
+    private void recordBeforeAndAfterAppHeapMetrics(
+            AhatSnapshot beforeDump,
+            AhatSnapshot afterDump) {
+        AhatHeap beforeHeap = beforeDump.getHeap("app");
+        AhatHeap afterHeap = afterDump.getHeap("app");
+        recordSizeMetric("beforeAppSize", beforeHeap.getSize());
+        recordSizeDeltaMetric("deltaAppSize", beforeHeap.getSize(), afterHeap.getSize());
+        Map<Reachability, Size> beforeSizesByReachability =
+                sizesByReachability(beforeDump, beforeHeap);
+        Map<Reachability, Size> afterSizesByReachability = sizesByReachability(afterDump, afterHeap);
+        for (Reachability reachability : Reachability.values()) {
+            recordSizeMetric(
+                    reachability.metricName("beforeAppSize"),
+                    beforeSizesByReachability.get(reachability));
+            recordSizeDeltaMetric(
+                    reachability.metricName("deltaAppSize"),
+                    beforeSizesByReachability.get(reachability),
+                    afterSizesByReachability.get(reachability));
+        }
+    }
+
+    private void recordSizeMetric(String name, Size size) {
+        recordBytesMetric(name, size.getSize());
+    }
+
+    private void recordSizeDeltaMetric(String name, Size before, Size after) {
+        recordBytesMetric(name, after.getSize() - before.getSize());
+    }
+
+    private void recordBytesMetric(String name, long bytes) {
+        metrics.addTestMetric(name, Long.toString(bytes));
+    }
+
+    private static Map<Reachability, Size> sizesByReachability(AhatSnapshot dump, AhatHeap heap) {
+        EnumMap<Reachability, Size> map = new EnumMap<>(Reachability.class);
+        for (Reachability reachability : Reachability.values()) {
+            map.put(reachability, Size.ZERO);
+        }
+        for (AhatInstance instance : dump.getRooted()) {
+            Reachability reachability = Reachability.ofInstance(instance);
+            Size size = instance.getRetainedSize(heap);
+            map.put(reachability, map.get(reachability).plus(size));
+        }
+        return map;
+    }
+}
diff --git a/metrictests/memory/host/src/libcore/heapmetrics/MetricsRunner.java b/metrictests/memory/host/src/libcore/heapmetrics/MetricsRunner.java
new file mode 100644
index 0000000..c3f9e0d
--- /dev/null
+++ b/metrictests/memory/host/src/libcore/heapmetrics/MetricsRunner.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapmetrics;
+
+import com.android.ahat.heapdump.AhatSnapshot;
+import com.android.ahat.heapdump.Diff;
+import com.android.ahat.heapdump.HprofFormatException;
+import com.android.ahat.heapdump.Parser;
+import com.android.ahat.proguard.ProguardMap;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Helper class that runs the metric instrumentations on a test device.
+ */
+class MetricsRunner {
+
+    private final ITestDevice testDevice;
+    private final String deviceParentDirectory;
+    private final TestLogData logs;
+    private final String timestampedLabel;
+
+    /**
+     * Creates a helper using the given {@link ITestDevice}, uploading heap dumps to the given
+     * {@link TestLogData}.
+     */
+    static MetricsRunner create(ITestDevice testDevice, TestLogData logs)
+            throws DeviceNotAvailableException {
+        String deviceParentDirectory =
+                testDevice.executeShellCommand("echo -n ${EXTERNAL_STORAGE}");
+        return new MetricsRunner(testDevice, deviceParentDirectory, logs);
+    }
+
+    private MetricsRunner(
+            ITestDevice testDevice, String deviceParentDirectory, TestLogData logs) {
+        this.testDevice = testDevice;
+        this.deviceParentDirectory = deviceParentDirectory;
+        this.logs = logs;
+        this.timestampedLabel = "LibcoreHeapMetricsTest-" + getCurrentTimeIso8601();
+    }
+
+    /**
+     * Contains the results of running the instrumentation.
+     */
+    static class Result {
+
+        private final AhatSnapshot afterDump;
+        private final int beforeTotalPssKb;
+        private final int afterTotalPssKb;
+
+        private Result(
+                AhatSnapshot beforeDump, AhatSnapshot afterDump,
+                int beforeTotalPssKb, int afterTotalPssKb) {
+            Diff.snapshots(afterDump, beforeDump);
+            this.beforeTotalPssKb = beforeTotalPssKb;
+            this.afterTotalPssKb = afterTotalPssKb;
+            this.afterDump = afterDump;
+        }
+
+        /**
+         * Returns the parsed form of the heap dump captured when the instrumentation starts.
+         */
+        AhatSnapshot getBeforeDump() {
+            return afterDump.getBaseline();
+        }
+
+        /**
+         * Returns the parsed form of the heap dump captured after the instrumentation action has
+         * been executed. The first heap dump will be set as the baseline for this second one.
+         */
+        AhatSnapshot getAfterDump() {
+            return afterDump;
+        }
+
+        /**
+         * Returns the PSS measured when the instrumentation starts, in kB.
+         */
+        int getBeforeTotalPssKb() {
+            return beforeTotalPssKb;
+        }
+
+        /**
+         * Returns the PSS measured after the instrumentation action has been executed, in kB.
+         */
+        int getAfterTotalPssKb() {
+            return afterTotalPssKb;
+        }
+    }
+
+    /**
+     * Runs all the instrumentation and fetches the metrics.
+     *
+     * @param action The name of the action to run, to be sent as an argument to the instrumentation
+     * @return The combined results of the instrumentations.
+     */
+    Result runAllInstrumentations(String action)
+            throws DeviceNotAvailableException, IOException, HprofFormatException {
+        String relativeDirectoryName = String.format("%s-%s", timestampedLabel, action);
+        String deviceDirectoryName =
+                String.format("%s/%s", deviceParentDirectory, relativeDirectoryName);
+        testDevice.executeShellCommand(String.format("mkdir %s", deviceDirectoryName));
+        try {
+            runInstrumentation(
+                    action, relativeDirectoryName, deviceDirectoryName,
+                    "libcore.heapdumper/.HeapDumpInstrumentation");
+            runInstrumentation(
+                    action, relativeDirectoryName, deviceDirectoryName,
+                    "libcore.heapdumper/.PssInstrumentation");
+            AhatSnapshot beforeDump = fetchHeapDump(deviceDirectoryName, "before.hprof", action);
+            AhatSnapshot afterDump = fetchHeapDump(deviceDirectoryName, "after.hprof", action);
+            int beforeTotalPssKb = fetchTotalPssKb(deviceDirectoryName, "before.pss.txt");
+            int afterTotalPssKb = fetchTotalPssKb(deviceDirectoryName, "after.pss.txt");
+            return new Result(beforeDump, afterDump, beforeTotalPssKb, afterTotalPssKb);
+        } finally {
+            testDevice.executeShellCommand(String.format("rm -r %s", deviceDirectoryName));
+        }
+    }
+
+    /**
+     * Runs a given instrumentation.
+     *
+     * <p>After the instrumentation has been run, checks for any reported errors and throws a
+     * {@link ApplicationException} if any are found.
+     *
+     * @param action The name of the action to run, to be sent as an argument to the instrumentation
+     * @param relativeDirectoryName The relative directory name for files on the device, to be sent
+     *     as an argument to the instrumentation
+     * @param deviceDirectoryName The absolute directory name for files on the device
+     * @param apk The name of the APK, in the form {@code test_package/runner_class}
+     */
+    private void runInstrumentation(
+            String action, String relativeDirectoryName, String deviceDirectoryName, String apk)
+            throws DeviceNotAvailableException, IOException {
+        String command = String.format(
+                "am instrument -w -e dumpdir %s -e action %s  %s",
+                relativeDirectoryName, action, apk);
+        testDevice.executeShellCommand(command);
+        checkForErrorFile(deviceDirectoryName);
+    }
+
+    /**
+     * Looks for a file called {@code error} in the named device directory, and throws an
+     * {@link ApplicationException} using the first line of that file as the message if found.
+     */
+    private void checkForErrorFile(String deviceDirectoryName)
+            throws DeviceNotAvailableException, IOException {
+        String[] deviceDirectoryContents =
+                testDevice.executeShellCommand("ls " + deviceDirectoryName).split("\\s");
+        for (String deviceFileName : deviceDirectoryContents) {
+            if (deviceFileName.equals("error")) {
+                throw new ApplicationException(readErrorFile(deviceDirectoryName));
+            }
+        }
+    }
+
+    /**
+     * Returns the first line read from a file called {@code error} on the device in the named
+     * directory.
+     *
+     * <p>The file is pulled into a temporary location on the host, and deleted after reading.
+     */
+    private String readErrorFile(String deviceDirectoryName)
+            throws IOException, DeviceNotAvailableException {
+        File file = testDevice.pullFile(String.format("%s/error", deviceDirectoryName));
+        if (file == null) {
+            throw new RuntimeException(
+                    "Failed to pull error log from directory " + deviceDirectoryName);
+        }
+        try {
+            return FileUtil.readStringFromFile(file);
+        } finally {
+            file.delete();
+        }
+    }
+
+    /**
+     * Returns an {@link AhatSnapshot} parsed from an {@code hprof} file on the device at the
+     * given directory and relative filename.
+     *
+     * <p>The file is pulled into a temporary location on the host, and deleted after reading.
+     * It is also logged via {@link TestLogData} under a name formed from the action and the
+     * relative filename (e.g. {@code noop-before.hprof}).
+     */
+    private AhatSnapshot fetchHeapDump(
+            String deviceDirectoryName, String relativeDumpFilename, String action)
+            throws DeviceNotAvailableException, IOException, HprofFormatException {
+        String deviceFileName = String
+                .format("%s/%s", deviceDirectoryName, relativeDumpFilename);
+        File file = testDevice.pullFile(deviceFileName);
+        if (file == null) {
+            throw new RuntimeException("Failed to pull dump: " + deviceFileName);
+        }
+        try {
+            logHeapDump(file, String.format("%s-%s", action, relativeDumpFilename));
+            return Parser.parseHeapDump(file, new ProguardMap());
+        } finally {
+            file.delete();
+        }
+    }
+
+    /**
+     * Returns the total PSS in kB read from a stringified integer in a file on the device at the
+     * given directory and relative filename.
+     */
+    private int fetchTotalPssKb(
+            String deviceDirectoryName, String relativeFilename)
+            throws DeviceNotAvailableException, IOException, HprofFormatException {
+        String shellCommand = String.format("cat %s/%s", deviceDirectoryName, relativeFilename);
+        String totalPssKbStr = testDevice.executeShellCommand(shellCommand);
+        return Integer.parseInt(totalPssKbStr);
+    }
+
+    /**
+     * Logs the heap dump from the given file via {@link TestLogData} with the given log
+     * filename.
+     */
+    private void logHeapDump(File file, String logFilename) {
+        try (FileInputStreamSource dataStream = new FileInputStreamSource(file)) {
+            logs.addTestLog(logFilename, LogDataType.HPROF, dataStream);
+        }
+    }
+
+    private static String getCurrentTimeIso8601() {
+        SimpleDateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+        Date now = new Date();
+        return iso8601Format.format(now);
+    }
+
+    /**
+     * An exception indicating that the activity on the device encountered an error which it
+     * passed
+     * back to the host.
+     */
+    private static class ApplicationException extends RuntimeException {
+
+        private static final long serialVersionUID = 0;
+
+        ApplicationException(String applicationError) {
+            super("Error encountered running application on device: " + applicationError);
+        }
+    }
+}
diff --git a/metrictests/memory/host/src/libcore/heapmetrics/Reachability.java b/metrictests/memory/host/src/libcore/heapmetrics/Reachability.java
new file mode 100644
index 0000000..ac0bb78
--- /dev/null
+++ b/metrictests/memory/host/src/libcore/heapmetrics/Reachability.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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 libcore.heapmetrics;
+
+import com.android.ahat.heapdump.AhatInstance;
+
+/**
+ * An enumeration of the different ways that an object could be reachable.
+ */
+enum Reachability {
+
+    /** Objects that are rooted. */
+    ROOTED("Rooted"),
+
+    /** Objects that are strongly reachable but not rooted. */
+    NON_ROOTED_STRONG("NonRootedStrong"),
+
+    /** Objects that are weakly reachable (only through soft/weak/phantom/finalizer references). */
+    WEAK("Weak"),
+
+    /** Objects that are unreachable. */
+    UNREACHABLE("Unreachable");
+
+    private final String metricSuffix;
+
+    Reachability(String metricSuffix) {
+        this.metricSuffix = metricSuffix;
+    }
+
+    /** Returns a name for a metric combining the given prefix with a reachability suffix. */
+    String metricName(String metricPrefix) {
+        return metricPrefix + metricSuffix;
+    }
+
+    /** Returns the reachability of the given object. */
+    static final Reachability ofInstance(AhatInstance instance) {
+        if (instance.isRoot()) {
+            return ROOTED;
+        } else if (instance.isStronglyReachable()) {
+            return NON_ROOTED_STRONG;
+        } else if (instance.isWeaklyReachable()) {
+            return WEAK;
+        } else if (instance.isUnreachable()) {
+            return UNREACHABLE;
+        }
+        throw new AssertionError("Impossible reachability data for instance " + instance);
+    }
+}