Merge "Update change annotations in Object(In|Out)putStream." am: 58b1654b67 am: 669a908df7
am: b9c861416c
Change-Id: I4c353a82ec3f0f93275e3a13e28718738c228977
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 d96b63e..378aca2 100644
--- a/JavaLibrary.bp
+++ b/JavaLibrary.bp
@@ -107,6 +107,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",
@@ -134,6 +150,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/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);
+ }
+}