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()));
 }