rules: Add IorapCompilationRule to toggle iorap on/off
Adds iorap-based app startup optimization for crystalball-based tests.
Uses the command line flag 'iorapd-enabled true' or 'iorapd-enabled false'.
Does nothing if 'iorapd-enabled' is left unspecified.
Also adds a unit test to validate functionality.
Bug: 150880186
Test: am instrument -w -r --no-isolated-storage -e iorapd-enabled -e listener \
android.device.collectors.AppStartupListener -e iterations 10 -e \
skip_test_failure_metrics true -e class \
'android.platform.test.scenario.calculator.OpenAppMicrobenchmark' -e \
newRunListenerMode true -e timeout_msec 300000 -e include-ui-xml true -e \
compilation-filter speed-profile \
android.platform.test.scenario/androidx.test.runner.AndroidJUnitRunner
Change-Id: I61fe914529945912fad97a6d37245adbc8e42cee
Merged-In: I61fe914529945912fad97a6d37245adbc8e42cee
diff --git a/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java b/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java
new file mode 100644
index 0000000..ecdf8af
--- /dev/null
+++ b/libraries/health/rules/src/android/platform/test/rule/IorapCompilationRule.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 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 android.platform.test.rule;
+
+import android.os.SystemClock;
+import android.platform.test.rule.DropCachesRule;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.InitializationError;
+
+/** This rule toggles iorap compilation for an app, or skips if unspecified. */
+public class IorapCompilationRule extends TestWatcher {
+ //
+ private static final String TAG = IorapCompilationRule.class.getSimpleName();
+ // constants
+ @VisibleForTesting static final String ARGUMENT_IORAPD_ENABLED = "iorapd-enabled";
+
+ @VisibleForTesting
+ static final String IORAP_COMPILE_CMD = "cmd jobscheduler run -f android 283673059";
+ @VisibleForTesting
+ static final String IORAP_MAINTENANCE_CMD =
+ "iorap.cmd.maintenance --purge-package %s /data/misc/iorapd/sqlite.db";
+ @VisibleForTesting
+ static final String IORAP_DUMPSYS_CMD = "dumpsys iorapd";
+
+ private static final int IORAP_COMPILE_CMD_TIMEOUT_SEC = 600; // in seconds: 10 minutes
+ private static final int IORAP_TRACE_DURATION_TIMEOUT = 7000; // Allow 7s for trace to complete.
+ private static final int IORAP_TRIAL_LAUNCH_ITERATIONS = 3; // min 3 launches to merge traces.
+
+ private static final String DROP_CACHE_SCRIPT = "/data/local/tmp/dropCache.sh";
+
+ // Global static counter. Each junit instrument command must launch 1 package with
+ // 1 compiler filter.
+ private static int sIterationCounter = 0;
+ private String mApplication;
+
+ private enum IorapStatus {
+ UNDEFINED,
+ ENABLED,
+ DISABLED
+ }
+ private static IorapStatus sIorapStatus = IorapStatus.UNDEFINED;
+
+ private enum IorapCompilationStatus {
+ INCOMPLETE,
+ COMPLETE,
+ INSUFFICIENT_TRACES,
+ }
+
+ @VisibleForTesting
+ protected static void resetState() {
+ sIorapStatus = IorapStatus.UNDEFINED;
+ sIterationCounter = 0;
+ Log.v(TAG, "resetState");
+ }
+
+ public IorapCompilationRule() throws InitializationError {
+ throw new InitializationError("Must supply an application to enable iorapd for.");
+ }
+
+ public IorapCompilationRule(String application) {
+ mApplication = application;
+ }
+
+ protected void sleep(int ms) {
+ SystemClock.sleep(ms);
+ }
+
+ // [[ $(adb shell whoami) == "root" ]]
+ protected boolean checkIfRoot() {
+ String result = executeShellCommand("whoami");
+ return result.contains("root");
+ }
+
+ // Delete all db rows and files associated with a package in iorapd.
+ // Effectively deletes any raw or compiled trace files, unoptimizing the package in iorap.
+ private void purgeIorapPackage(String packageName) {
+ if (!checkIfRoot()) {
+ throw new IllegalStateException("Must be root to toggle iorapd; try adb root?");
+ }
+
+ executeShellCommand("stop iorapd");
+ sleep(100); // give iorapd enough time to stop.
+ executeShellCommand(String.format(IORAP_MAINTENANCE_CMD, packageName));
+ Log.v(TAG, "Executed: " + String.format(IORAP_MAINTENANCE_CMD, packageName));
+ executeShellCommand("start iorapd");
+ sleep(2000); // give iorapd enough time to start up.
+ }
+
+ /**
+ * Toggle iorapd-based readahead and trace-collection.
+ * If iorapd is already enabled and enable is true, does nothing.
+ * If iorapd is already disabled and enable is false, does nothing.
+ */
+ private void toggleIorapStatus(boolean enable) {
+ boolean currentlyEnabled = false;
+ Log.v(TAG, "toggleIorapStatus " + Boolean.toString(enable));
+
+ // Do nothing if we are already enabled or disabled.
+ if (sIorapStatus == IorapStatus.ENABLED && enable) {
+ return;
+ } else if (sIorapStatus == IorapStatus.DISABLED && !enable) {
+ return;
+ }
+
+ if (!checkIfRoot()) {
+ throw new IllegalStateException("Must be root to toggle iorapd; try adb root?");
+ }
+
+ executeShellCommand("stop iorapd");
+ executeShellCommand(String.format("setprop iorapd.perfetto.enable %b", enable));
+ executeShellCommand(String.format("setprop iorapd.readahead.enable %b", enable));
+ executeShellCommand("start iorapd");
+ sleep(2000); // give enough time for iorapd to start back up.
+
+ if (enable) {
+ sIorapStatus = IorapStatus.ENABLED;
+ } else {
+ sIorapStatus = IorapStatus.DISABLED;
+ }
+ }
+
+ /**
+ * Compile the app package using iorap.cmd.maintenance and return false
+ * if the compilation failed for some reason.
+ */
+ private boolean compileAppForIorap(String appPkgName) {
+ executeShellCommand(IORAP_COMPILE_CMD);
+
+ for (int i = 0; i < IORAP_COMPILE_CMD_TIMEOUT_SEC; ++i) {
+ IorapCompilationStatus status = waitForIorapCompiled(appPkgName);
+ if (status == IorapCompilationStatus.COMPLETE) {
+ Log.v(TAG, "compileAppForIorap: complete");
+ return true;
+ } else if (status == IorapCompilationStatus.INSUFFICIENT_TRACES) {
+ Log.v(TAG, "compileAppForIorap: insufficient traces");
+ return false;
+ } // else INCOMPLETE. keep asking iorapd if it's done yet.
+ sleep(1000);
+ }
+
+ Log.v(TAG, "compileAppForIorap: timed out");
+ return false;
+ }
+
+ private IorapCompilationStatus waitForIorapCompiled(String appPkgName) {
+ String output = executeShellCommand(IORAP_DUMPSYS_CMD);
+
+ String prevLine = "";
+ for (String line : output.split("\n")) {
+ // Match the indented VersionedComponentName string.
+ // " com.google.android.deskclock/com.android.deskclock.DeskClock@62000712"
+ // Note: spaces are meaningful here.
+ if (prevLine.contains(" " + appPkgName) && prevLine.contains("@")) {
+ // pre-requisite:
+ // Compiled Status: Raw traces pending compilation (3)
+ if (line.contains("Compiled Status: Usable compiled trace")) {
+ return IorapCompilationStatus.COMPLETE;
+ } else if (line.contains("Compiled Status: ") &&
+ line.contains("more traces for compilation")) {
+ // Compiled Status: Need 1 more traces for compilation
+ // No amount of waiting will help here because there were
+ // insufficient traces made.
+ return IorapCompilationStatus.INSUFFICIENT_TRACES;
+ }
+ }
+ prevLine = line;
+ }
+ return IorapCompilationStatus.INCOMPLETE;
+ }
+
+ /**
+ * The first {@code IORAP_TRIAL_LAUNCH_ITERATIONS} are used for collecting an iorap trace file.
+ */
+ private boolean isIorapTraceBeingCollected() {
+ return sIterationCounter < IORAP_TRIAL_LAUNCH_ITERATIONS;
+ }
+
+ private boolean isLastIorapTraceCollection() {
+ return sIterationCounter == IORAP_TRIAL_LAUNCH_ITERATIONS - 1;
+ }
+
+ private boolean isFirstIorapTraceCollection() {
+ return sIterationCounter == 0;
+ }
+
+ /**
+ * Returns null if iorapd-enabled is unset, otherwise returns
+ * the true/false value of iorapd-enabled.
+ */
+ private Boolean isIorapdEnabled() {
+ String value = getArguments().getString(ARGUMENT_IORAPD_ENABLED);
+ if (value == null) {
+ return null;
+ }
+ return Boolean.parseBoolean(value);
+ }
+
+ @Override
+ protected void starting(Description description) {
+ // Don't do anything if iorapd-enabled was not set.
+ Boolean enabled = isIorapdEnabled();
+ if (enabled == null) {
+ Log.d(TAG, "Skipping iorapd toggling because 'iorapd-enabled' option is unset.");
+ return;
+ }
+ logStatus("starting");
+ // Compile each application in sequence.
+ String app = mApplication;
+
+ toggleIorapStatus(enabled);
+
+ if (!enabled) {
+ return;
+ }
+
+ if (isIorapTraceBeingCollected()) {
+ // Purge all iorap traces prior to first run of an application.
+ if (isFirstIorapTraceCollection()) {
+ purgeIorapPackage(mApplication);
+ }
+
+ // We must always drop caches to simulate a cold start if the app
+ // launch is going to be used for an iorap-trace collection.
+ DropCachesRule.executeDropCaches();
+ }
+ }
+
+ @Override
+ protected void finished(Description description) {
+ logStatus("finishing");
+
+ Boolean enabled = isIorapdEnabled();
+
+ if (Boolean.TRUE.equals(enabled) && isIorapTraceBeingCollected()) {
+ // wait for slightly more than 5s (iorapd.perfetto.trace_duration_ms) for the
+ // trace buffers to complete.
+ sleep(IORAP_TRACE_DURATION_TIMEOUT);
+
+ if (isLastIorapTraceCollection()) {
+ compileAppForIorap(mApplication);
+ }
+ }
+
+ logStatus("finished");
+ sIterationCounter++;
+ }
+
+ private void logStatus(String status) {
+ Log.v(TAG, String.format("%s iteration %s for app %s", status, sIterationCounter, mApplication));
+ }
+}
+
diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java
new file mode 100644
index 0000000..090609a
--- /dev/null
+++ b/libraries/health/rules/tests/src/android/platform/test/rule/IorapCompilationRuleTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2020 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 android.platform.test.rule;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit test the logic for {@link IorapCompilationRule}
+ */
+@RunWith(JUnit4.class)
+public class IorapCompilationRuleTest {
+ /**
+ * Tests that this rule will fail to register if no apps are supplied.
+ */
+ @Test
+ public void testNoAppToKillFails() {
+ try {
+ IorapCompilationRule rule = new IorapCompilationRule();
+ fail("An initialization error should have been thrown, but wasn't.");
+ } catch (InitializationError e) {
+ return;
+ }
+ }
+
+ /**
+ * Tests that this rule does nothing when 'iorapd-enabled' is unset.
+ */
+ @Test
+ public void testDoingNothingWhenParamsUnset() throws Throwable {
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(new Bundle(), "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ assertThat(rule.getOperations()).containsExactly(
+ "test")
+ .inOrder();
+ }
+
+ /**
+ * Tests that this rule will disable iorapd when 'iorapd-enabled' is false.
+ */
+ @Test
+ public void testDisablingIorapdWhenParamsAreSet() throws Throwable {
+ Bundle bundle = new Bundle();
+ bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "false");
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ assertThat(rule.getOperations()).containsExactly(
+ "stop iorapd",
+ "setprop iorapd.perfetto.enable false",
+ "setprop iorapd.readahead.enable false",
+ "start iorapd",
+ "test")
+ .inOrder();
+ }
+
+ /**
+ * Tests that this rule will enable iorapd when 'iorapd-enabled' is true.
+ */
+ @Test
+ public void testEnablingIorapdWhenParamsAreSet() throws Throwable {
+ Bundle bundle = new Bundle();
+ bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "true");
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ assertThat(rule.getOperations()).containsExactly(
+ "stop iorapd",
+ "setprop iorapd.perfetto.enable true",
+ "setprop iorapd.readahead.enable true",
+ "start iorapd",
+ "stop iorapd",
+ String.format(IorapCompilationRule.IORAP_MAINTENANCE_CMD, "example.package.name"),
+ "start iorapd",
+ "test")
+ .inOrder();
+ }
+
+ /**
+ * Tests that this rule will enable iorapd when 'iorapd-enabled' is true.
+ */
+ @Test
+ public void testCompilingIorapdWhenParamsAreSet() throws Throwable {
+ Bundle bundle = new Bundle();
+ bundle.putString(IorapCompilationRule.ARGUMENT_IORAPD_ENABLED, "true");
+ TestableIorapCompilationRule rule =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd"))
+ .evaluate();
+ // The first iteration turns on iorapd and will trace the app.
+ assertThat(rule.getOperations()).containsExactly(
+ "stop iorapd",
+ "setprop iorapd.perfetto.enable true",
+ "setprop iorapd.readahead.enable true",
+ "start iorapd",
+ "stop iorapd",
+ String.format(IorapCompilationRule.IORAP_MAINTENANCE_CMD, "example.package.name"),
+ "start iorapd",
+ "test")
+ .inOrder();
+
+ // We do nothing special for the second iteration, iorapd will be tracing.
+ TestableIorapCompilationRule rule2 =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule2.apply(rule2.getTestStatement(), Description.createTestDescription("clzz", "mthd2"))
+ .evaluate();
+ assertThat(rule2.getOperations()).containsExactly(
+ "test")
+ .inOrder();
+
+ // On the 3rd iteration, we iorap compile the package after the test method finishes.
+ TestableIorapCompilationRule rule3 =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule3.apply(rule3.getTestStatement(), Description.createTestDescription("clzz", "mthd3"))
+ .evaluate();
+ assertThat(rule3.getOperations()).containsExactly(
+ "test",
+ IorapCompilationRule.IORAP_COMPILE_CMD,
+ IorapCompilationRule.IORAP_DUMPSYS_CMD)
+ .inOrder();
+
+ // On the 4th and later iteration, we do nothing.
+ TestableIorapCompilationRule rule4 =
+ new TestableIorapCompilationRule(bundle, "example.package.name");
+ rule4.apply(rule4.getTestStatement(), Description.createTestDescription("clzz", "mthd4"))
+ .evaluate();
+ assertThat(rule4.getOperations()).containsExactly(
+ "test")
+ .inOrder();
+ }
+
+ @Before
+ public void resetRuleState() {
+ TestableIorapCompilationRule.resetState();
+ }
+
+ private static class TestableIorapCompilationRule extends IorapCompilationRule {
+ private List<String> mOperations = new ArrayList<>();
+ private Bundle mBundle;
+
+ public TestableIorapCompilationRule(Bundle bundle, String app) {
+ super(app);
+ mBundle = bundle;
+ }
+
+ @Override
+ protected String executeShellCommand(String cmd) {
+ mOperations.add(cmd);
+
+ if (cmd.equals(IorapCompilationRule.IORAP_DUMPSYS_CMD)) {
+ return " example.package.name/com.android.example.Activity@62000712" +
+ "\n Compiled Status: Usable compiled trace";
+ }
+
+ return "";
+ }
+
+ @Override
+ protected Bundle getArguments() {
+ return mBundle;
+ }
+
+ public List<String> getOperations() {
+ return mOperations;
+ }
+
+ public Statement getTestStatement() {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ mOperations.add("test");
+ }
+ };
+ }
+
+ public static void resetState() {
+ IorapCompilationRule.resetState();
+ }
+
+ @Override
+ protected boolean checkIfRoot() {
+ return true;
+ }
+
+ @Override
+ protected void sleep(int ms) {
+ // Intentionally left empty. The tests don't need to sleep.
+ }
+ }
+}
+
diff --git a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
index 4b15e88..03b9da6 100644
--- a/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
+++ b/tests/health/scenarios/tests/src/android/platform/test/scenario/generic/OpenAppMicrobenchmark.java
@@ -18,6 +18,7 @@
import android.platform.test.microbenchmark.Microbenchmark;
import android.platform.test.rule.CompilationFilterRule;
import android.platform.test.rule.DropCachesRule;
+import android.platform.test.rule.IorapCompilationRule;
import android.platform.test.rule.KillAppsRule;
import android.platform.test.rule.PressHomeRule;
@@ -33,5 +34,6 @@
RuleChain.outerRule(new KillAppsRule(sPkgOption.get()))
.around(new DropCachesRule())
.around(new CompilationFilterRule(sPkgOption.get()))
- .around(new PressHomeRule());
+ .around(new PressHomeRule())
+ .around(new IorapCompilationRule(sPkgOption.get()));
}