summaryrefslogtreecommitdiff
path: root/startop/iorap
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2020-08-31 21:21:38 -0700
committerXin Li <delphij@google.com>2020-08-31 21:21:38 -0700
commit628590d7ec80e10a3fc24b1c18a1afb55cca10a8 (patch)
tree4b1c3f52d86d7fb53afbe9e9438468588fa489f8 /startop/iorap
parentb11b8ec3aec8bb42f2c07e1c5ac7942da293baa8 (diff)
parentd2d3a20624d968199353ccf6ddbae6f3ac39c9af (diff)
Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)
Bug: 166295507 Merged-In: I3d92a6de21a938f6b352ec26dc23420c0fe02b27 Change-Id: Ifdb80563ef042738778ebb8a7581a97c4e3d96e2
Diffstat (limited to 'startop/iorap')
-rw-r--r--startop/iorap/TEST_MAPPING (renamed from startop/iorap/DISABLED_TEST_MAPPING)0
-rw-r--r--startop/iorap/functional_tests/Android.bp42
-rw-r--r--startop/iorap/functional_tests/AndroidManifest.xml38
-rw-r--r--startop/iorap/functional_tests/AndroidTest.xml70
-rw-r--r--startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java416
-rw-r--r--startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java95
-rw-r--r--startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java114
-rw-r--r--startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java263
-rw-r--r--startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java414
-rw-r--r--startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java154
-rw-r--r--startop/iorap/src/com/google/android/startop/iorap/RequestId.java5
-rw-r--r--startop/iorap/stress/Android.bp33
-rw-r--r--startop/iorap/stress/main_memory.cc126
-rw-r--r--startop/iorap/tests/AndroidTest.xml22
-rw-r--r--startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt181
-rw-r--r--startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt17
16 files changed, 1949 insertions, 41 deletions
diff --git a/startop/iorap/DISABLED_TEST_MAPPING b/startop/iorap/TEST_MAPPING
index 8c9d4dfb0894..8c9d4dfb0894 100644
--- a/startop/iorap/DISABLED_TEST_MAPPING
+++ b/startop/iorap/TEST_MAPPING
diff --git a/startop/iorap/functional_tests/Android.bp b/startop/iorap/functional_tests/Android.bp
new file mode 100644
index 000000000000..8a5bd34af653
--- /dev/null
+++ b/startop/iorap/functional_tests/Android.bp
@@ -0,0 +1,42 @@
+// 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.
+
+android_test {
+ name: "iorap-functional-tests",
+ srcs: ["src/**/*.java"],
+ data: [":iorap-functional-test-apps"],
+ static_libs: [
+ // Non-test dependencies
+ // library under test
+ "services.startop.iorap",
+ // Test Dependencies
+ // test android dependencies
+ "platform-test-annotations",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "androidx.test.uiautomator_uiautomator",
+ // test framework dependencies
+ "truth-prebuilt",
+ ],
+ dxflags: ["--multi-dex"],
+ test_suites: ["device-tests"],
+ compile_multilib: "both",
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ certificate: "platform",
+ platform_apis: true,
+}
+
diff --git a/startop/iorap/functional_tests/AndroidManifest.xml b/startop/iorap/functional_tests/AndroidManifest.xml
new file mode 100644
index 000000000000..6bddc4a39577
--- /dev/null
+++ b/startop/iorap/functional_tests/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<!--suppress AndroidUnknownAttribute -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.startop.iorap.tests"
+ android:sharedUserId="com.google.android.startop.iorap.tests.functional"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <!--suppress AndroidDomInspection -->
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.google.android.startop.iorap.tests" />
+
+ <!--
+ 'debuggable=true' is required to properly load mockito jvmti dependencies,
+ otherwise it gives the following error at runtime:
+
+ Openjdkjvmti plugin was loaded on a non-debuggable Runtime.
+ Plugin was loaded too late to change runtime state to DEBUGGABLE. -->
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+</manifest>
diff --git a/startop/iorap/functional_tests/AndroidTest.xml b/startop/iorap/functional_tests/AndroidTest.xml
new file mode 100644
index 000000000000..31d4f6c47b11
--- /dev/null
+++ b/startop/iorap/functional_tests/AndroidTest.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Runs iorap-functional-tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="iorap-functional-tests.apk" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+ <target_preparer
+ class="com.android.tradefed.targetprep.DeviceSetup">
+
+ <!-- iorapd does not pick up the above changes until we restart it -->
+ <option name="run-command" value="stop iorapd" />
+
+ <!-- Clean up the existing iorap database. -->
+ <option name="run-command" value="rm -r /data/misc/iorapd/*" />
+ <option name="run-command" value="sleep 1" />
+
+ <!-- Set system properties to enable perfetto tracing, readahead and detailed logging. -->
+ <option name="run-command" value="setprop iorapd.perfetto.enable true" />
+ <option name="run-command" value="setprop iorapd.readahead.enable true" />
+ <option name="run-command" value="setprop iorapd.log.verbose true" />
+
+ <option name="run-command" value="start iorapd" />
+
+ <!-- give it some time to restart the service; otherwise the first unit test might fail -->
+ <option name="run-command" value="sleep 1" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="cleanup" value="true" />
+ <option name="abort-on-push-failure" value="true" />
+ <option name="push-file"
+ key="iorap_test_app_v1.apk"
+ value="/data/misc/iorapd/iorap_test_app_v1.apk" />
+ <option name="push-file"
+ key="iorap_test_app_v2.apk"
+ value="/data/misc/iorapd/iorap_test_app_v2.apk" />
+ <option name="push-file"
+ key="iorap_test_app_v3.apk"
+ value="/data/misc/iorapd/iorap_test_app_v3.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.google.android.startop.iorap.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- test-timeout unit is ms, value = 30 min -->
+ <option name="test-timeout" value="1800000" />
+ </test>
+
+</configuration>
+
diff --git a/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
new file mode 100644
index 000000000000..5352be6f283f
--- /dev/null
+++ b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java
@@ -0,0 +1,416 @@
+/*
+ * 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 com.google.android.startop.iorapd;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.Date;
+import java.util.function.BooleanSupplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.List;
+import java.text.SimpleDateFormat;
+
+/**
+ * Test for the work flow of iorap.
+ *
+ * <p> This test tests the function of iorap from:
+ * perfetto collection -> compilation -> prefetching -> version update -> perfetto collection.
+ */
+@RunWith(AndroidJUnit4.class)
+public class IorapWorkFlowTest {
+ private static final String TAG = "IorapWorkFlowTest";
+
+ private static final String TEST_APP_VERSION_ONE_PATH = "/data/misc/iorapd/iorap_test_app_v1.apk";
+ private static final String TEST_APP_VERSION_TWO_PATH = "/data/misc/iorapd/iorap_test_app_v2.apk";
+ private static final String TEST_APP_VERSION_THREE_PATH = "/data/misc/iorapd/iorap_test_app_v3.apk";
+
+ private static final String DB_PATH = "/data/misc/iorapd/sqlite.db";
+ private static final Duration TIMEOUT = Duration.ofSeconds(300L);
+
+ private UiDevice mDevice;
+
+ @Before
+ public void setUp() throws Exception {
+ // Initialize UiDevice instance
+ mDevice = UiDevice.getInstance(getInstrumentation());
+
+ // Start from the home screen
+ mDevice.pressHome();
+
+ // Wait for launcher
+ final String launcherPackage = mDevice.getLauncherPackageName();
+ assertThat(launcherPackage, notNullValue());
+ mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIMEOUT.getSeconds());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ String packageName = "com.example.ioraptestapp";
+ uninstallApk(packageName);
+ }
+
+ @Test (timeout = 300000)
+ public void testNormalWorkFlow() throws Exception {
+ assertThat(mDevice, notNullValue());
+
+ // Install test app version one
+ installApk(TEST_APP_VERSION_ONE_PATH);
+ String packageName = "com.example.ioraptestapp";
+ String activityName = "com.example.ioraptestapp.MainActivity";
+
+ // Perfetto trace collection phase.
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/1L));
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/1L));
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/1L));
+
+ // Trigger maintenance service for compilation.
+ TimeUnit.SECONDS.sleep(5L);
+ assertTrue(compile(packageName, activityName, /*version=*/1L));
+
+ // Run app with prefetching
+ assertTrue(startAppWithCompiledTrace(
+ packageName, activityName, /*version=*/1L));
+ }
+
+ @Test (timeout = 300000)
+ public void testUpdateApp() throws Exception {
+ assertThat(mDevice, notNullValue());
+
+ // Install test app version two,
+ String packageName = "com.example.ioraptestapp";
+ String activityName = "com.example.ioraptestapp.MainActivity";
+ installApk(TEST_APP_VERSION_TWO_PATH);
+
+ // Perfetto trace collection phase.
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/2L));
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/2L));
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/2L));
+
+ // Trigger maintenance service for compilation.
+ TimeUnit.SECONDS.sleep(5L);
+ assertTrue(compile(packageName, activityName, /*version=*/2L));
+
+ // Run app with prefetching
+ assertTrue(startAppWithCompiledTrace(
+ packageName, activityName, /*version=*/2L));
+
+ // Update test app to version 3
+ installApk(TEST_APP_VERSION_THREE_PATH);
+
+ // Rerun app, should do pefetto tracing.
+ assertTrue(startAppForPerfettoTrace(
+ packageName, activityName, /*version=*/3L));
+ }
+
+ private static void installApk(String apkPath) throws Exception {
+ // Disable the selinux to allow pm install apk in the dir.
+ executeShellCommand("setenforce 0");
+ executeShellCommand("pm install -r -d " + apkPath);
+ executeShellCommand("setenforce 1");
+
+ }
+
+ private static void uninstallApk(String apkPath) throws Exception {
+ executeShellCommand("pm uninstall " + apkPath);
+ }
+
+ /**
+ * Starts the testing app to collect the perfetto trace.
+ *
+ * @param expectPerfettoTraceCount is the expected count of perfetto traces.
+ */
+ private boolean startAppForPerfettoTrace(
+ String packageName, String activityName, long version)
+ throws Exception {
+ LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
+ return waitForPerfettoTraceSavedFromLogcat(
+ packageName, activityName, version, timestamp);
+ }
+
+ private boolean startAppWithCompiledTrace(
+ String packageName, String activityName, long version)
+ throws Exception {
+ LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
+ return waitForPrefetchingFromLogcat(
+ packageName, activityName, version, timestamp);
+ }
+
+ private LogcatTimestamp runAppOnce(String packageName, String activityName) throws Exception {
+ // Close the specified app if it's open
+ closeApp(packageName);
+ LogcatTimestamp timestamp = new LogcatTimestamp();
+ // Launch the specified app
+ startApp(packageName, activityName);
+ // Wait for the app to appear
+ mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), TIMEOUT.getSeconds());
+ return timestamp;
+ }
+
+ // Invokes the maintenance to compile the perfetto traces to compiled trace.
+ private boolean compile(
+ String packageName, String activityName, long version) throws Exception {
+ // The job id (283673059) is defined in class IorapForwardingService.
+ executeShellCommandViaTmpFile("cmd jobscheduler run -f android 283673059");
+ return waitForFileExistence(getCompiledTracePath(packageName, activityName, version));
+ }
+
+ private String getCompiledTracePath(
+ String packageName, String activityName, long version) {
+ return String.format(
+ "/data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
+ packageName, version, activityName);
+ }
+
+ /**
+ * Starts the testing app.
+ */
+ private void startApp(String packageName, String activityName) throws Exception {
+ executeShellCommandViaTmpFile(
+ String.format("am start %s/%s", packageName, activityName));
+ }
+
+ /**
+ * Closes the testing app.
+ * <p> Keep trying to kill the process of the app until no process of the app package
+ * appears.</p>
+ */
+ private void closeApp(String packageName) throws Exception {
+ while (true) {
+ String pid = executeShellCommand("pidof " + packageName);
+ if (pid.isEmpty()) {
+ Log.i(TAG, "Closed app " + packageName);
+ return;
+ }
+ executeShellCommand("kill -9 " + pid);
+ TimeUnit.SECONDS.sleep(1L);
+ }
+ }
+
+ /** Waits for a file to appear. */
+ private boolean waitForFileExistence(String fileName) throws Exception {
+ return retryWithTimeout(TIMEOUT, () -> {
+ try {
+ String fileExists = executeShellCommandViaTmpFile(
+ String.format("test -f %s; echo $?", fileName));
+ Log.i(TAG, fileName + " existence is " + fileExists);
+ return fileExists.trim().equals("0");
+ } catch (Exception e) {
+ Log.i(TAG, e.getMessage());
+ return false;
+ }
+ });
+ }
+
+ /** Waits for the perfetto trace saved message from logcat. */
+ private boolean waitForPerfettoTraceSavedFromLogcat(
+ String packageName, String activityName, long version, LogcatTimestamp timestamp)
+ throws Exception {
+ Pattern p = Pattern.compile(".*"
+ + getPerfettoTraceSavedIndicator(packageName, activityName, version)
+ + "(.*[.]perfetto_trace[.]pb)\n.*", Pattern.DOTALL);
+
+ return retryWithTimeout(TIMEOUT, () -> {
+ try {
+ String log = timestamp.getLogcatAfter();
+ Matcher m = p.matcher(log);
+ Log.d(TAG, "Tries to find perfetto trace...");
+ if (!m.matches()) {
+ Log.i(TAG, "Cannot find perfetto trace saved in log.");
+ return false;
+ }
+ String filePath = m.group(1);
+ Log.i(TAG, "Perfetto trace is saved to " + filePath);
+ return true;
+ } catch(Exception e) {
+ Log.e(TAG, e.getMessage());
+ return false;
+ }
+ });
+ }
+
+ private String getPerfettoTraceSavedIndicator(
+ String packageName, String activityName, long version) {
+ return String.format(
+ "Perfetto TraceBuffer saved to file: /data/misc/iorapd/%s/%d/%s/raw_traces/",
+ packageName, version, activityName);
+ }
+
+ /**
+ * Waits for the prefetching log in the logcat.
+ *
+ * <p> When prefetching works, the perfetto traces should not be collected. </p>
+ */
+ private boolean waitForPrefetchingFromLogcat(
+ String packageName, String activityName, long version, LogcatTimestamp timestamp)
+ throws Exception {
+ Pattern p = Pattern.compile(
+ ".*" + getReadaheadIndicator(packageName, activityName, version) +
+ ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+ + ".*Total Entries=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+ + ".*Total Bytes=(\\d+) \\(good: (\\d+[.]?\\d*)%\\).*",
+ Pattern.DOTALL);
+
+ return retryWithTimeout(TIMEOUT, () -> {
+ try {
+ String log = timestamp.getLogcatAfter();
+ Matcher m = p.matcher(log);
+ if (!m.matches()) {
+ Log.i(TAG, "Cannot find readahead log.");
+ return false;
+ }
+
+ int totalFilePath = Integer.parseInt(m.group(1));
+ float totalFilePathGoodRate = Float.parseFloat(m.group(2)) / 100;
+ int totalEntries = Integer.parseInt(m.group(3));
+ float totalEntriesGoodRate = Float.parseFloat(m.group(4)) / 100;
+ int totalBytes = Integer.parseInt(m.group(5));
+ float totalBytesGoodRate = Float.parseFloat(m.group(6)) / 100;
+
+ Log.i(TAG, String.format(
+ "totalFilePath: %d (good %.2f) totalEntries: %d (good %.2f) totalBytes: %d (good %.2f)",
+ totalFilePath, totalFilePathGoodRate, totalEntries, totalEntriesGoodRate, totalBytes,
+ totalBytesGoodRate));
+
+ return totalFilePath > 0 &&
+ totalEntries > 0 &&
+ totalBytes > 0 &&
+ totalFilePathGoodRate > 0.5 &&
+ totalEntriesGoodRate > 0.5 &&
+ totalBytesGoodRate > 0.5;
+ } catch(Exception e) {
+ return false;
+ }
+ });
+ }
+
+ private static String getReadaheadIndicator(
+ String packageName, String activityName, long version) {
+ return String.format(
+ "Description = /data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
+ packageName, version, activityName);
+ }
+
+ /** Retry until timeout. */
+ private boolean retryWithTimeout(Duration timeout, BooleanSupplier supplier) throws Exception {
+ long totalSleepTimeSeconds = 0L;
+ long sleepIntervalSeconds = 2L;
+ while (true) {
+ if (supplier.getAsBoolean()) {
+ return true;
+ }
+ TimeUnit.SECONDS.sleep(sleepIntervalSeconds);
+ totalSleepTimeSeconds += sleepIntervalSeconds;
+ if (totalSleepTimeSeconds > timeout.getSeconds()) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Executes command in adb shell via a tmp file.
+ *
+ * <p> This should be run as root.</p>
+ */
+ private static String executeShellCommandViaTmpFile(String cmd) throws Exception {
+ Log.i(TAG, "Execute via tmp file: " + cmd);
+ Path tmp = null;
+ try {
+ tmp = Files.createTempFile(/*prefix=*/null, /*suffix=*/".sh");
+ Files.write(tmp, cmd.getBytes(StandardCharsets.UTF_8));
+ tmp.toFile().setExecutable(true);
+ return UiDevice.getInstance(
+ InstrumentationRegistry.getInstrumentation()).
+ executeShellCommand(tmp.toString());
+ } finally {
+ if (tmp != null) {
+ Files.delete(tmp);
+ }
+ }
+ }
+
+ /**
+ * Executes command in adb shell.
+ *
+ * <p> This should be run as root.</p>
+ */
+ private static String executeShellCommand(String cmd) throws Exception {
+ Log.i(TAG, "Execute: " + cmd);
+ return UiDevice.getInstance(
+ InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
+ }
+
+ static class LogcatTimestamp {
+ private String epochTime;
+
+ public LogcatTimestamp() throws Exception{
+ long currentTimeMillis = System.currentTimeMillis();
+ epochTime = String.format(
+ "%d.%03d", currentTimeMillis/1000, currentTimeMillis%1000);
+ Log.i(TAG, "Current logcat timestamp is " + epochTime);
+ }
+
+ // For example, 1585264100.000
+ public String getEpochTime() {
+ return epochTime;
+ }
+
+ // Gets the logcat after this epoch time.
+ public String getLogcatAfter() throws Exception {
+ return executeShellCommandViaTmpFile(
+ "logcat -v epoch -t '" + epochTime + "'");
+ }
+ }
+}
+
diff --git a/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java b/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java
index acf994610182..8263e0af4422 100644
--- a/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java
+++ b/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java
@@ -33,6 +33,7 @@ import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
import java.util.Objects;
/**
@@ -86,10 +87,14 @@ public abstract class AppLaunchEvent implements Parcelable {
public static final class IntentStarted extends AppLaunchEvent {
@NonNull
public final Intent intent;
+ public final long timestampNs;
- public IntentStarted(@SequenceId long sequenceId, Intent intent) {
+ public IntentStarted(@SequenceId long sequenceId,
+ Intent intent,
+ long timestampNs) {
super(sequenceId);
this.intent = intent;
+ this.timestampNs = timestampNs;
Objects.requireNonNull(intent, "intent");
}
@@ -98,14 +103,16 @@ public abstract class AppLaunchEvent implements Parcelable {
public boolean equals(Object other) {
if (other instanceof IntentStarted) {
return intent.equals(((IntentStarted)other).intent) &&
- super.equals(other);
+ timestampNs == ((IntentStarted)other).timestampNs &&
+ super.equals(other);
}
return false;
}
@Override
protected String toStringBody() {
- return ", intent=" + intent.toString();
+ return ", intent=" + intent.toString() +
+ " , timestampNs=" + Long.toString(timestampNs);
}
@@ -113,11 +120,13 @@ public abstract class AppLaunchEvent implements Parcelable {
protected void writeToParcelImpl(Parcel p, int flags) {
super.writeToParcelImpl(p, flags);
IntentProtoParcelable.write(p, intent, flags);
+ p.writeLong(timestampNs);
}
IntentStarted(Parcel p) {
super(p);
intent = IntentProtoParcelable.create(p);
+ timestampNs = p.readLong();
}
}
@@ -154,8 +163,8 @@ public abstract class AppLaunchEvent implements Parcelable {
@Override
public boolean equals(Object other) {
if (other instanceof BaseWithActivityRecordData) {
- return activityRecordSnapshot.equals(
- ((BaseWithActivityRecordData)other).activityRecordSnapshot) &&
+ return (Arrays.equals(activityRecordSnapshot,
+ ((BaseWithActivityRecordData)other).activityRecordSnapshot)) &&
super.equals(other);
}
return false;
@@ -163,7 +172,7 @@ public abstract class AppLaunchEvent implements Parcelable {
@Override
protected String toStringBody() {
- return ", " + activityRecordSnapshot.toString();
+ return ", " + new String(activityRecordSnapshot);
}
@Override
@@ -200,7 +209,7 @@ public abstract class AppLaunchEvent implements Parcelable {
@Override
protected String toStringBody() {
- return ", temperature=" + Integer.toString(temperature);
+ return super.toStringBody() + ", temperature=" + Integer.toString(temperature);
}
@Override
@@ -216,18 +225,39 @@ public abstract class AppLaunchEvent implements Parcelable {
}
public static final class ActivityLaunchFinished extends BaseWithActivityRecordData {
+ public final long timestampNs;
+
public ActivityLaunchFinished(@SequenceId long sequenceId,
- @NonNull @ActivityRecordProto byte[] snapshot) {
+ @NonNull @ActivityRecordProto byte[] snapshot,
+ long timestampNs) {
super(sequenceId, snapshot);
+ this.timestampNs = timestampNs;
}
@Override
public boolean equals(Object other) {
- if (other instanceof ActivityLaunched) {
- return super.equals(other);
+ if (other instanceof ActivityLaunchFinished) {
+ return timestampNs == ((ActivityLaunchFinished)other).timestampNs &&
+ super.equals(other);
}
return false;
}
+
+ @Override
+ protected String toStringBody() {
+ return super.toStringBody() + ", timestampNs=" + Long.toString(timestampNs);
+ }
+
+ @Override
+ protected void writeToParcelImpl(Parcel p, int flags) {
+ super.writeToParcelImpl(p, flags);
+ p.writeLong(timestampNs);
+ }
+
+ ActivityLaunchFinished(Parcel p) {
+ super(p);
+ timestampNs = p.readLong();
+ }
}
public static class ActivityLaunchCancelled extends AppLaunchEvent {
@@ -242,8 +272,8 @@ public abstract class AppLaunchEvent implements Parcelable {
@Override
public boolean equals(Object other) {
if (other instanceof ActivityLaunchCancelled) {
- return Objects.equals(activityRecordSnapshot,
- ((ActivityLaunchCancelled)other).activityRecordSnapshot) &&
+ return Arrays.equals(activityRecordSnapshot,
+ ((ActivityLaunchCancelled)other).activityRecordSnapshot) &&
super.equals(other);
}
return false;
@@ -251,7 +281,7 @@ public abstract class AppLaunchEvent implements Parcelable {
@Override
protected String toStringBody() {
- return ", " + activityRecordSnapshot.toString();
+ return super.toStringBody() + ", " + new String(activityRecordSnapshot);
}
@Override
@@ -275,6 +305,42 @@ public abstract class AppLaunchEvent implements Parcelable {
}
}
+ public static final class ReportFullyDrawn extends BaseWithActivityRecordData {
+ public final long timestampNs;
+
+ public ReportFullyDrawn(@SequenceId long sequenceId,
+ @NonNull @ActivityRecordProto byte[] snapshot,
+ long timestampNs) {
+ super(sequenceId, snapshot);
+ this.timestampNs = timestampNs;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ReportFullyDrawn) {
+ return timestampNs == ((ReportFullyDrawn)other).timestampNs &&
+ super.equals(other);
+ }
+ return false;
+ }
+
+ @Override
+ protected String toStringBody() {
+ return super.toStringBody() + ", timestampNs=" + Long.toString(timestampNs);
+ }
+
+ @Override
+ protected void writeToParcelImpl(Parcel p, int flags) {
+ super.writeToParcelImpl(p, flags);
+ p.writeLong(timestampNs);
+ }
+
+ ReportFullyDrawn(Parcel p) {
+ super(p);
+ timestampNs = p.readLong();
+ }
+ }
+
@Override
public @ContentsFlags int describeContents() { return 0; }
@@ -348,6 +414,7 @@ public abstract class AppLaunchEvent implements Parcelable {
ActivityLaunched.class,
ActivityLaunchFinished.class,
ActivityLaunchCancelled.class,
+ ReportFullyDrawn.class,
};
public static class ActivityRecordProtoParcelable {
@@ -372,7 +439,7 @@ public abstract class AppLaunchEvent implements Parcelable {
final ProtoOutputStream protoOutputStream =
new ProtoOutputStream(INTENT_PROTO_CHUNK_SIZE);
// Write this data out as the top-most IntentProto (i.e. it is not a sub-object).
- intent.writeToProto(protoOutputStream);
+ intent.dumpDebug(protoOutputStream);
final byte[] bytes = protoOutputStream.getBytes();
p.writeByteArray(bytes);
diff --git a/startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java b/startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java
new file mode 100644
index 000000000000..72c5eaa84c96
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/DexOptEvent.java
@@ -0,0 +1,114 @@
+/*
+ * 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 com.google.android.startop.iorap;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Notifications for iorapd specifying when a package is updated by dexopt service.<br /><br />
+ *
+ * @hide
+ */
+public class DexOptEvent implements Parcelable {
+ public static final int TYPE_PACKAGE_UPDATE = 0;
+ private static final int TYPE_MAX = 0;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+ TYPE_PACKAGE_UPDATE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {}
+
+ @Type public final int type;
+ public final String packageName;
+
+ @NonNull
+ public static DexOptEvent createPackageUpdate(String packageName) {
+ return new DexOptEvent(TYPE_PACKAGE_UPDATE, packageName);
+ }
+
+ private DexOptEvent(@Type int type, String packageName) {
+ this.type = type;
+ this.packageName = packageName;
+
+ checkConstructorArguments();
+ }
+
+ private void checkConstructorArguments() {
+ CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+ Objects.requireNonNull(packageName, "packageName");
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{DexOptEvent: packageName: %s}", packageName);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (other instanceof DexOptEvent) {
+ return equals((DexOptEvent) other);
+ }
+ return false;
+ }
+
+ private boolean equals(DexOptEvent other) {
+ return packageName.equals(other.packageName);
+ }
+
+ //<editor-fold desc="Binder boilerplate">
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(type);
+ out.writeString(packageName);
+ }
+
+ private DexOptEvent(Parcel in) {
+ this.type = in.readInt();
+ this.packageName = in.readString();
+
+ checkConstructorArguments();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<DexOptEvent> CREATOR
+ = new Parcelable.Creator<DexOptEvent>() {
+ public DexOptEvent createFromParcel(Parcel in) {
+ return new DexOptEvent(in);
+ }
+
+ public DexOptEvent[] newArray(int size) {
+ return new DexOptEvent[size];
+ }
+ };
+ //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java b/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java
new file mode 100644
index 000000000000..67e1b440e28a
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2019 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 com.google.android.startop.iorap;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.util.Log;
+
+import com.android.server.wm.ActivityMetricsLaunchObserver;
+
+import java.io.StringWriter;
+import java.io.PrintWriter;
+
+/**
+ * A validator to check the correctness of event sequence during app startup.
+ *
+ * <p> A valid state transition of event sequence is shown as the following:
+ *
+ * <pre>
+ *
+ * +--------------------+
+ * | |
+ * | INIT |
+ * | |
+ * +--------------------+
+ * |
+ * |
+ * ↓
+ * +--------------------+
+ * | |
+ * +-------------------| INTENT_STARTED | ←--------------------------------+
+ * | | | |
+ * | +--------------------+ |
+ * | | |
+ * | | |
+ * ↓ ↓ |
+ * +--------------------+ +--------------------+ |
+ * | | | | |
+ * | INTENT_FAILED | | ACTIVITY_LAUNCHED |------------------+ |
+ * | | | | | |
+ * +--------------------+ +--------------------+ | |
+ * | | | |
+ * | ↓ ↓ |
+ * | +--------------------+ +--------------------+ |
+ * | | | | | |
+ * +------------------ | ACTIVITY_FINISHED | | ACTIVITY_CANCELLED | |
+ * | | | | | |
+ * | +--------------------+ +--------------------+ |
+ * | | | |
+ * | | | |
+ * | ↓ | |
+ * | +--------------------+ | |
+ * | | | | |
+ * | | REPORT_FULLY_DRAWN | | |
+ * | | | | |
+ * | +--------------------+ | |
+ * | | | |
+ * | | | |
+ * | ↓ | |
+ * | +--------------------+ | |
+ * | | | | |
+ * +-----------------→ | END |←-----------------+ |
+ * | | |
+ * +--------------------+ |
+ * | |
+ * | |
+ * | |
+ * +---------------------------------------------
+ *
+ * <p> END is not a real state in implementation. All states that points to END directly
+ * could transition to INTENT_STARTED.
+ *
+ * <p> If any bad transition happened, the state becomse UNKNOWN. The UNKNOWN state
+ * could be accumulated, because during the UNKNOWN state more IntentStarted may
+ * be triggered. To recover from UNKNOWN to INIT, all the accumualted IntentStarted
+ * should termniate.
+ *
+ * <p> During UNKNOWN state, each IntentStarted increases the accumulation, and any of
+ * IntentFailed, ActivityLaunchCancelled and ActivityFinished decreases the accumulation.
+ * ReportFullyDrawn doesn't impact the accumulation.
+ */
+public class EventSequenceValidator implements ActivityMetricsLaunchObserver {
+ static final String TAG = "EventSequenceValidator";
+
+ private State state = State.INIT;
+ private long accIntentStartedEvents = 0;
+
+ @Override
+ public void onIntentStarted(@NonNull Intent intent, long timestampNs) {
+ if (state == State.UNKNOWN) {
+ logWarningWithStackTrace("IntentStarted during UNKNOWN. " + intent);
+ incAccIntentStartedEvents();
+ return;
+ }
+
+ if (state != State.INIT &&
+ state != State.INTENT_FAILED &&
+ state != State.ACTIVITY_CANCELLED &&
+ state != State.ACTIVITY_FINISHED &&
+ state != State.REPORT_FULLY_DRAWN) {
+ logWarningWithStackTrace(
+ String.format("Cannot transition from %s to %s", state, State.INTENT_STARTED));
+ incAccIntentStartedEvents();
+ incAccIntentStartedEvents();
+ return;
+ }
+
+ Log.d(TAG, String.format("Transition from %s to %s", state, State.INTENT_STARTED));
+ state = State.INTENT_STARTED;
+ }
+
+ @Override
+ public void onIntentFailed() {
+ if (state == State.UNKNOWN) {
+ logWarningWithStackTrace("onIntentFailed during UNKNOWN.");
+ decAccIntentStartedEvents();
+ return;
+ }
+ if (state != State.INTENT_STARTED) {
+ logWarningWithStackTrace(
+ String.format("Cannot transition from %s to %s", state, State.INTENT_FAILED));
+ incAccIntentStartedEvents();
+ return;
+ }
+
+ Log.d(TAG, String.format("Transition from %s to %s", state, State.INTENT_FAILED));
+ state = State.INTENT_FAILED;
+ }
+
+ @Override
+ public void onActivityLaunched(@NonNull @ActivityRecordProto byte[] activity,
+ @Temperature int temperature) {
+ if (state == State.UNKNOWN) {
+ logWarningWithStackTrace("onActivityLaunched during UNKNOWN.");
+ return;
+ }
+ if (state != State.INTENT_STARTED) {
+ logWarningWithStackTrace(
+ String.format("Cannot transition from %s to %s", state, State.ACTIVITY_LAUNCHED));
+ incAccIntentStartedEvents();
+ return;
+ }
+
+ Log.d(TAG, String.format("Transition from %s to %s", state, State.ACTIVITY_LAUNCHED));
+ state = State.ACTIVITY_LAUNCHED;
+ }
+
+ @Override
+ public void onActivityLaunchCancelled(@Nullable @ActivityRecordProto byte[] activity) {
+ if (state == State.UNKNOWN) {
+ logWarningWithStackTrace("onActivityLaunchCancelled during UNKNOWN.");
+ decAccIntentStartedEvents();
+ return;
+ }
+ if (state != State.ACTIVITY_LAUNCHED) {
+ logWarningWithStackTrace(
+ String.format("Cannot transition from %s to %s", state, State.ACTIVITY_CANCELLED));
+ incAccIntentStartedEvents();
+ return;
+ }
+
+ Log.d(TAG, String.format("Transition from %s to %s", state, State.ACTIVITY_CANCELLED));
+ state = State.ACTIVITY_CANCELLED;
+ }
+
+ @Override
+ public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity,
+ long timestampNs) {
+ if (state == State.UNKNOWN) {
+ logWarningWithStackTrace("onActivityLaunchFinished during UNKNOWN.");
+ decAccIntentStartedEvents();
+ return;
+ }
+
+ if (state != State.ACTIVITY_LAUNCHED) {
+ logWarningWithStackTrace(
+ String.format("Cannot transition from %s to %s", state, State.ACTIVITY_FINISHED));
+ incAccIntentStartedEvents();
+ return;
+ }
+
+ Log.d(TAG, String.format("Transition from %s to %s", state, State.ACTIVITY_FINISHED));
+ state = State.ACTIVITY_FINISHED;
+ }
+
+ @Override
+ public void onReportFullyDrawn(@NonNull @ActivityRecordProto byte[] activity,
+ long timestampNs) {
+ if (state == State.UNKNOWN) {
+ logWarningWithStackTrace("onReportFullyDrawn during UNKNOWN.");
+ return;
+ }
+ if (state == State.INIT) {
+ return;
+ }
+
+ if (state != State.ACTIVITY_FINISHED) {
+ logWarningWithStackTrace(
+ String.format("Cannot transition from %s to %s", state, State.REPORT_FULLY_DRAWN));
+ return;
+ }
+
+ Log.d(TAG, String.format("Transition from %s to %s", state, State.REPORT_FULLY_DRAWN));
+ state = State.REPORT_FULLY_DRAWN;
+ }
+
+ enum State {
+ INIT,
+ INTENT_STARTED,
+ INTENT_FAILED,
+ ACTIVITY_LAUNCHED,
+ ACTIVITY_CANCELLED,
+ ACTIVITY_FINISHED,
+ REPORT_FULLY_DRAWN,
+ UNKNOWN,
+ }
+
+ private void incAccIntentStartedEvents() {
+ if (accIntentStartedEvents < 0) {
+ throw new AssertionError("The number of unknowns cannot be negative");
+ }
+ if (accIntentStartedEvents == 0) {
+ state = State.UNKNOWN;
+ }
+ ++accIntentStartedEvents;
+ Log.d(TAG,
+ String.format("inc AccIntentStartedEvents to %d", accIntentStartedEvents));
+ }
+
+ private void decAccIntentStartedEvents() {
+ if (accIntentStartedEvents <= 0) {
+ throw new AssertionError("The number of unknowns cannot be negative");
+ }
+ if(accIntentStartedEvents == 1) {
+ state = State.INIT;
+ }
+ --accIntentStartedEvents;
+ Log.d(TAG,
+ String.format("dec AccIntentStartedEvents to %d", accIntentStartedEvents));
+ }
+
+ private void logWarningWithStackTrace(String log) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ new Throwable("EventSequenceValidator#getStackTrace").printStackTrace(pw);
+ Log.d(TAG, String.format("%s\n%s", log, sw));
+ }
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java
index 12d3aba57b21..3104c7e7e0a1 100644
--- a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java
+++ b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java
@@ -19,6 +19,10 @@ package com.google.android.startop.iorap;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -30,25 +34,31 @@ import android.os.Parcel;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.IoThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.pm.BackgroundDexOptService;
import com.android.server.wm.ActivityMetricsLaunchObserver;
import com.android.server.wm.ActivityMetricsLaunchObserver.ActivityRecordProto;
import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature;
import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
import com.android.server.wm.ActivityTaskManagerInternal;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.HashMap;
+
/**
* System-server-local proxy into the {@code IIorap} native service.
*/
public class IorapForwardingService extends SystemService {
public static final String TAG = "IorapForwardingService";
- /** $> adb shell 'setprop log.tag.IorapdForwardingService VERBOSE' */
+ /** $> adb shell 'setprop log.tag.IorapForwardingService VERBOSE' */
public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
/** $> adb shell 'setprop ro.iorapd.enable true' */
private static boolean IS_ENABLED = SystemProperties.getBoolean("ro.iorapd.enable", true);
@@ -56,12 +66,20 @@ public class IorapForwardingService extends SystemService {
private static boolean WTF_CRASH = SystemProperties.getBoolean(
"iorapd.forwarding_service.wtf_crash", false);
+ // "Unique" job ID from the service name. Also equal to 283673059.
+ public static final int JOB_ID_IORAPD = encodeEnglishAlphabetStringIntoInt("iorapd");
+ // Run every 24 hours.
+ public static final long JOB_INTERVAL_MS = TimeUnit.HOURS.toMillis(24);
+
private IIorap mIorapRemote;
private final Object mLock = new Object();
/** Handle onBinderDeath by periodically trying to reconnect. */
private final Handler mHandler =
new BinderConnectionHandler(IoThread.getHandler().getLooper());
+ private volatile IorapdJobService mJobService; // Write-once (null -> non-null forever).
+ private volatile static IorapForwardingService sSelfService; // Write once (null -> non-null).
+
/**
* Initializes the system service.
* <p>
@@ -73,6 +91,15 @@ public class IorapForwardingService extends SystemService {
*/
public IorapForwardingService(Context context) {
super(context);
+
+ if (DEBUG) {
+ Log.v(TAG, "IorapForwardingService (Context=" + context.toString() + ")");
+ }
+
+ if (sSelfService != null) {
+ throw new AssertionError("only one service instance allowed");
+ }
+ sSelfService = this;
}
//<editor-fold desc="Providers">
@@ -117,6 +144,10 @@ public class IorapForwardingService extends SystemService {
public void binderDied() {
Log.w(TAG, "iorapd has died");
retryConnectToRemoteAndConfigure(/*attempts*/0);
+
+ if (mJobService != null) {
+ mJobService.onIorapdDisconnected();
+ }
}
};
}
@@ -139,6 +170,24 @@ public class IorapForwardingService extends SystemService {
retryConnectToRemoteAndConfigure(/*attempts*/0);
}
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == PHASE_BOOT_COMPLETED) {
+ if (DEBUG) {
+ Log.v(TAG, "onBootPhase(PHASE_BOOT_COMPLETED)");
+ }
+
+ if (isIorapEnabled()) {
+ // Set up a recurring background job. This has to be done in a later phase since it
+ // has a dependency the job scheduler.
+ //
+ // Doing this too early can result in a ServiceNotFoundException for 'jobservice'
+ // or a null reference for #getSystemService(JobScheduler.class)
+ mJobService = new IorapdJobService(getContext());
+ }
+ }
+ }
+
private class BinderConnectionHandler extends Handler {
public BinderConnectionHandler(android.os.Looper looper) {
super(looper);
@@ -229,13 +278,18 @@ public class IorapForwardingService extends SystemService {
Log.e(TAG, "connectToRemoteAndConfigure - null iorap remote. check for Log.wtf?");
return false;
}
- invokeRemote( () -> mIorapRemote.setTaskListener(new RemoteTaskListener()) );
+ invokeRemote(mIorapRemote,
+ (IIorap remote) -> remote.setTaskListener(new RemoteTaskListener()) );
registerInProcessListenersLocked();
+ Log.i(TAG, "Connected to iorapd native service.");
+
return true;
}
private final AppLaunchObserver mAppLaunchObserver = new AppLaunchObserver();
+ private final EventSequenceValidator mEventSequenceValidator = new EventSequenceValidator();
+ private final DexOptPackagesUpdated mDexOptPackagesUpdated = new DexOptPackagesUpdated();
private boolean mRegisteredListeners = false;
private void registerInProcessListenersLocked() {
@@ -256,10 +310,29 @@ public class IorapForwardingService extends SystemService {
ActivityMetricsLaunchObserverRegistry launchObserverRegistry =
provideLaunchObserverRegistry();
launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver);
+ launchObserverRegistry.registerLaunchObserver(mEventSequenceValidator);
+
+ BackgroundDexOptService.addPackagesUpdatedListener(mDexOptPackagesUpdated);
+
mRegisteredListeners = true;
}
+ private class DexOptPackagesUpdated implements BackgroundDexOptService.PackagesUpdatedListener {
+ @Override
+ public void onPackagesUpdated(ArraySet<String> updatedPackages) {
+ String[] updated = updatedPackages.toArray(new String[0]);
+ for (String packageName : updated) {
+ Log.d(TAG, "onPackagesUpdated: " + packageName);
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onDexOptEvent(RequestId.nextValueForSequence(),
+ DexOptEvent.createPackageUpdate(packageName))
+ );
+ }
+ }
+ }
+
private class AppLaunchObserver implements ActivityMetricsLaunchObserver {
// We add a synthetic sequence ID here to make it easier to differentiate new
// launch sequences on the native side.
@@ -268,18 +341,19 @@ public class IorapForwardingService extends SystemService {
// All callbacks occur on the same background thread. Don't synchronize explicitly.
@Override
- public void onIntentStarted(@NonNull Intent intent) {
+ public void onIntentStarted(@NonNull Intent intent, long timestampNs) {
// #onIntentStarted [is the only transition that] initiates a new launch sequence.
++mSequenceId;
if (DEBUG) {
- Log.v(TAG, String.format("AppLaunchObserver#onIntentStarted(%d, %s)",
- mSequenceId, intent));
+ Log.v(TAG, String.format("AppLaunchObserver#onIntentStarted(%d, %s, %d)",
+ mSequenceId, intent, timestampNs));
}
- invokeRemote(() ->
- mIorapRemote.onAppLaunchEvent(RequestId.nextValueForSequence(),
- new AppLaunchEvent.IntentStarted(mSequenceId, intent))
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+ new AppLaunchEvent.IntentStarted(mSequenceId, intent, timestampNs))
);
}
@@ -289,8 +363,9 @@ public class IorapForwardingService extends SystemService {
Log.v(TAG, String.format("AppLaunchObserver#onIntentFailed(%d)", mSequenceId));
}
- invokeRemote(() ->
- mIorapRemote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
new AppLaunchEvent.IntentFailed(mSequenceId))
);
}
@@ -303,8 +378,9 @@ public class IorapForwardingService extends SystemService {
mSequenceId, activity, temperature));
}
- invokeRemote(() ->
- mIorapRemote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
new AppLaunchEvent.ActivityLaunched(mSequenceId, activity, temperature))
);
}
@@ -316,26 +392,267 @@ public class IorapForwardingService extends SystemService {
mSequenceId, activity));
}
- invokeRemote(() ->
- mIorapRemote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
new AppLaunchEvent.ActivityLaunchCancelled(mSequenceId,
activity)));
}
@Override
- public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity) {
+ public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity,
+ long timestampNs) {
if (DEBUG) {
- Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchFinished(%d, %s)",
- mSequenceId, activity));
+ Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchFinished(%d, %s, %d)",
+ mSequenceId, activity, timestampNs));
+ }
+
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+ new AppLaunchEvent.ActivityLaunchFinished(mSequenceId,
+ activity,
+ timestampNs))
+ );
+ }
+
+ @Override
+ public void onReportFullyDrawn(@NonNull @ActivityRecordProto byte[] activity,
+ long timestampNs) {
+ if (DEBUG) {
+ Log.v(TAG, String.format("AppLaunchObserver#onReportFullyDrawn(%d, %s, %d)",
+ mSequenceId, activity, timestampNs));
}
- invokeRemote(() ->
- mIorapRemote.onAppLaunchEvent(RequestId.nextValueForSequence(),
- new AppLaunchEvent.ActivityLaunchFinished(mSequenceId, activity))
+ invokeRemote(mIorapRemote,
+ (IIorap remote) ->
+ remote.onAppLaunchEvent(RequestId.nextValueForSequence(),
+ new AppLaunchEvent.ReportFullyDrawn(mSequenceId, activity, timestampNs))
);
}
}
+ /**
+ * Debugging:
+ *
+ * $> adb shell dumpsys jobscheduler
+ *
+ * Search for 'IorapdJobServiceProxy'.
+ *
+ * JOB #1000/283673059: 6e54ed android/com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy
+ * ^ ^ ^
+ * (uid, job id) ComponentName(package/class)
+ *
+ * Forcing the job to be run, ignoring constraints:
+ *
+ * $> adb shell cmd jobscheduler run -f android 283673059
+ * ^ ^
+ * package job_id
+ *
+ * ------------------------------------------------------------
+ *
+ * This class is instantiated newly by the JobService every time
+ * it wants to run a new job.
+ *
+ * We need to forward invocations to the current running instance of
+ * IorapForwardingService#IorapdJobService.
+ *
+ * Visibility: Must be accessible from android.app.AppComponentFactory
+ */
+ public static class IorapdJobServiceProxy extends JobService {
+
+ public IorapdJobServiceProxy() {
+ getActualIorapdJobService().bindProxy(this);
+ }
+
+
+ @NonNull
+ private IorapdJobService getActualIorapdJobService() {
+ // Can't ever be null, because the guarantee is that the
+ // IorapForwardingService is always running.
+ // We are in the same process as Job Service.
+ return sSelfService.mJobService;
+ }
+
+ // Called by system to start the job.
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ return getActualIorapdJobService().onStartJob(params);
+ }
+
+ // Called by system to prematurely stop the job.
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return getActualIorapdJobService().onStopJob(params);
+ }
+ }
+
+ private class IorapdJobService extends JobService {
+ private final ComponentName IORAPD_COMPONENT_NAME;
+
+ private final Object mLock = new Object();
+ // Jobs currently running remotely on iorapd.
+ // They were started by the JobScheduler and need to be finished.
+ private final HashMap<RequestId, JobParameters> mRunningJobs = new HashMap<>();
+
+ private final JobInfo IORAPD_JOB_INFO;
+
+ private volatile IorapdJobServiceProxy mProxy;
+
+ public void bindProxy(IorapdJobServiceProxy proxy) {
+ mProxy = proxy;
+ }
+
+ // Create a new job service which immediately schedules a 24-hour idle maintenance mode
+ // background job to execute.
+ public IorapdJobService(Context context) {
+ if (DEBUG) {
+ Log.v(TAG, "IorapdJobService (Context=" + context.toString() + ")");
+ }
+
+ // Schedule the proxy class to be instantiated by the JobScheduler
+ // when it is time to invoke background jobs for IorapForwardingService.
+
+
+ // This also needs a BIND_JOB_SERVICE permission in
+ // frameworks/base/core/res/AndroidManifest.xml
+ IORAPD_COMPONENT_NAME = new ComponentName(context, IorapdJobServiceProxy.class);
+
+ JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_IORAPD, IORAPD_COMPONENT_NAME);
+ builder.setPeriodic(JOB_INTERVAL_MS);
+ builder.setPrefetch(true);
+
+ builder.setRequiresCharging(true);
+ builder.setRequiresDeviceIdle(true);
+
+ builder.setRequiresStorageNotLow(true);
+
+ IORAPD_JOB_INFO = builder.build();
+
+ JobScheduler js = context.getSystemService(JobScheduler.class);
+ js.schedule(IORAPD_JOB_INFO);
+ Log.d(TAG,
+ "BgJob Scheduled (jobId=" + JOB_ID_IORAPD
+ + ", interval: " + JOB_INTERVAL_MS + "ms)");
+ }
+
+ // Called by system to start the job.
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ // Tell iorapd to start a background job.
+ Log.d(TAG, "Starting background job: " + params.toString());
+
+ // We wait until that job's sequence ID returns to us with 'Completed',
+ RequestId request;
+ synchronized (mLock) {
+ // TODO: would be cleaner if we got the request from the 'invokeRemote' function.
+ // Better yet, consider a Pair<RequestId, Future<TaskResult>> or similar.
+ request = RequestId.nextValueForSequence();
+ mRunningJobs.put(request, params);
+ }
+
+ if (!invokeRemote(mIorapRemote, (IIorap remote) ->
+ remote.onJobScheduledEvent(request,
+ JobScheduledEvent.createIdleMaintenance(
+ JobScheduledEvent.TYPE_START_JOB,
+ params))
+ )) {
+ synchronized (mLock) {
+ mRunningJobs.remove(request); // Avoid memory leaks.
+ }
+
+ // Something went wrong on the remote side. Treat the job as being
+ // 'already finished' (i.e. immediately release wake lock).
+ return false;
+ }
+
+ // True -> keep the wakelock acquired until #jobFinished is called.
+ return true;
+ }
+
+ // Called by system to prematurely stop the job.
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ // As this is unexpected behavior, print a warning.
+ Log.w(TAG, "onStopJob(params=" + params.toString() + ")");
+
+ // No longer track this job (avoids a memory leak).
+ boolean wasTracking = false;
+ synchronized (mLock) {
+ for (HashMap.Entry<RequestId, JobParameters> entry : mRunningJobs.entrySet()) {
+ if (entry.getValue().getJobId() == params.getJobId()) {
+ mRunningJobs.remove(entry.getKey());
+ wasTracking = true;
+ }
+ }
+ }
+
+ // Notify iorapd to stop (abort) the job.
+ if (wasTracking) {
+ invokeRemote(mIorapRemote, (IIorap remote) ->
+ remote.onJobScheduledEvent(RequestId.nextValueForSequence(),
+ JobScheduledEvent.createIdleMaintenance(
+ JobScheduledEvent.TYPE_STOP_JOB,
+ params))
+ );
+ } else {
+ // Even weirder. This could only be considered "correct" if iorapd reported success
+ // concurrently to the JobService requesting an onStopJob.
+ Log.e(TAG, "Untracked onStopJob request"); // see above Log.w for the params.
+ }
+
+
+ // Yes, retry the job at a later time no matter what.
+ return true;
+ }
+
+ // Listen to *all* task completes for all requests.
+ // The majority of these might be unrelated to background jobs.
+ public void onIorapdTaskCompleted(RequestId requestId) {
+ JobParameters jobParameters;
+ synchronized (mLock) {
+ jobParameters = mRunningJobs.remove(requestId);
+ }
+
+ // Typical case: This was a task callback unrelated to our jobs.
+ if (jobParameters == null) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.v(TAG,
+ String.format("IorapdJobService#onIorapdTaskCompleted(%s), found params=%s",
+ requestId, jobParameters));
+ }
+
+ Log.d(TAG, "Finished background job: " + jobParameters.toString());
+
+ // Job is successful and periodic. Do not 'reschedule' according to the back-off
+ // criteria.
+ //
+ // This releases the wakelock that was acquired in #onStartJob.
+
+ IorapdJobServiceProxy proxy = mProxy;
+ if (proxy != null) {
+ proxy.jobFinished(jobParameters, /*reschedule*/false);
+ }
+ // Cannot call 'jobFinished' on 'this' because it was not constructed
+ // from the JobService, so it would get an NPE when calling mEngine.
+ }
+
+ public void onIorapdDisconnected() {
+ synchronized (mLock) {
+ mRunningJobs.clear();
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, String.format("IorapdJobService#onIorapdDisconnected"));
+ }
+
+ // TODO: should we try to resubmit all incomplete jobs after it's reconnected?
+ }
+ }
+
private class RemoteTaskListener extends ITaskListener.Stub {
@Override
public void onProgress(RequestId requestId, TaskResult result) throws RemoteException {
@@ -354,18 +671,29 @@ public class IorapForwardingService extends SystemService {
String.format("RemoteTaskListener#onComplete(%s, %s)", requestId, result));
}
+ if (mJobService != null) {
+ mJobService.onIorapdTaskCompleted(requestId);
+ }
+
// TODO: implement rest.
}
}
/** Allow passing lambdas to #invokeRemote */
private interface RemoteRunnable {
- void run() throws RemoteException;
+ // TODO: run(RequestId) ?
+ void run(IIorap iorap) throws RemoteException;
}
- private static void invokeRemote(RemoteRunnable r) {
+ // Always pass in the iorap directly here to avoid data race.
+ private static boolean invokeRemote(IIorap iorap, RemoteRunnable r) {
+ if (iorap == null) {
+ Log.w(TAG, "IIorap went to null in this thread, drop invokeRemote.");
+ return false;
+ }
try {
- r.run();
+ r.run(iorap);
+ return true;
} catch (RemoteException e) {
// This could be a logic error (remote side returning error), which we need to fix.
//
@@ -377,6 +705,7 @@ public class IorapForwardingService extends SystemService {
//
// DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath.
handleRemoteError(e);
+ return false;
}
}
@@ -389,4 +718,43 @@ public class IorapForwardingService extends SystemService {
Log.wtf(TAG, t);
}
}
+
+ // Encode A-Z bitstring into bits. Every character is bits.
+ // Characters outside of the range [a,z] are considered out of range.
+ //
+ // The least significant bits hold the last character.
+ // First 2 bits are left as 0.
+ private static int encodeEnglishAlphabetStringIntoInt(String name) {
+ int value = 0;
+
+ final int CHARS_PER_INT = 6;
+ final int BITS_PER_CHAR = 5;
+ // Note: 2 top bits are unused, this also means our values are non-negative.
+ final char CHAR_LOWER = 'a';
+ final char CHAR_UPPER = 'z';
+
+ if (name.length() > CHARS_PER_INT) {
+ throw new IllegalArgumentException(
+ "String too long. Cannot encode more than 6 chars: " + name);
+ }
+
+ for (int i = 0; i < name.length(); ++i) {
+ char c = name.charAt(i);
+
+ if (c < CHAR_LOWER || c > CHAR_UPPER) {
+ throw new IllegalArgumentException("String has out-of-range [a-z] chars: " + name);
+ }
+
+ // Avoid sign extension during promotion.
+ int cur_value = (c & 0xFFFF) - (CHAR_LOWER & 0xFFFF);
+ if (cur_value >= (1 << BITS_PER_CHAR)) {
+ throw new AssertionError("wtf? i=" + i + ", name=" + name);
+ }
+
+ value = value << BITS_PER_CHAR;
+ value = value | cur_value;
+ }
+
+ return value;
+ }
}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java
new file mode 100644
index 000000000000..2055b206dd7a
--- /dev/null
+++ b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2018 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 com.google.android.startop.iorap;
+
+import android.app.job.JobParameters;
+import android.annotation.NonNull;
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Forward JobService events to iorapd. <br /><br />
+ *
+ * iorapd sometimes need to use background jobs. Forwarding these events to iorapd
+ * notifies iorapd when it is an opportune time to execute these background jobs.
+ *
+ * @hide
+ */
+public class JobScheduledEvent implements Parcelable {
+
+ /** JobService#onJobStarted */
+ public static final int TYPE_START_JOB = 0;
+ /** JobService#onJobStopped */
+ public static final int TYPE_STOP_JOB = 1;
+ private static final int TYPE_MAX = 1;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+ TYPE_START_JOB,
+ TYPE_STOP_JOB,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {}
+
+ @Type public final int type;
+
+ /** @see JobParameters#getJobId() */
+ public final int jobId;
+
+ /** Device is 'idle' and it's charging (plugged in). */
+ public static final int SORT_IDLE_MAINTENANCE = 0;
+ private static final int SORT_MAX = 0;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SORT_" }, value = {
+ SORT_IDLE_MAINTENANCE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Sort {}
+
+ /**
+ * Roughly corresponds to the {@code extras} fields in a JobParameters.
+ */
+ @Sort public final int sort;
+
+ /**
+ * Creates a {@link #SORT_IDLE_MAINTENANCE} event from the type and job parameters.
+ *
+ * Only the job ID is retained from {@code jobParams}, all other param info is dropped.
+ */
+ @NonNull
+ public static JobScheduledEvent createIdleMaintenance(@Type int type, JobParameters jobParams) {
+ return new JobScheduledEvent(type, jobParams.getJobId(), SORT_IDLE_MAINTENANCE);
+ }
+
+ private JobScheduledEvent(@Type int type, int jobId, @Sort int sort) {
+ this.type = type;
+ this.jobId = jobId;
+ this.sort = sort;
+
+ checkConstructorArguments();
+ }
+
+ private void checkConstructorArguments() {
+ CheckHelpers.checkTypeInRange(type, TYPE_MAX);
+ // No check for 'jobId': any int is valid.
+ CheckHelpers.checkTypeInRange(sort, SORT_MAX);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (other instanceof JobScheduledEvent) {
+ return equals((JobScheduledEvent) other);
+ }
+ return false;
+ }
+
+ private boolean equals(JobScheduledEvent other) {
+ return type == other.type &&
+ jobId == other.jobId &&
+ sort == other.sort;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{type: %d, jobId: %d, sort: %d}", type, jobId, sort);
+ }
+
+ //<editor-fold desc="Binder boilerplate">
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(type);
+ out.writeInt(jobId);
+ out.writeInt(sort);
+
+ // We do not parcel the entire JobParameters here because there is no C++ equivalent
+ // of that class [which the iorapd side of the binder interface requires].
+ }
+
+ private JobScheduledEvent(Parcel in) {
+ this.type = in.readInt();
+ this.jobId = in.readInt();
+ this.sort = in.readInt();
+
+ checkConstructorArguments();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<JobScheduledEvent> CREATOR
+ = new Parcelable.Creator<JobScheduledEvent>() {
+ public JobScheduledEvent createFromParcel(Parcel in) {
+ return new JobScheduledEvent(in);
+ }
+
+ public JobScheduledEvent[] newArray(int size) {
+ return new JobScheduledEvent[size];
+ }
+ };
+ //</editor-fold>
+}
diff --git a/startop/iorap/src/com/google/android/startop/iorap/RequestId.java b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java
index adb3a910f7fe..503e1c633581 100644
--- a/startop/iorap/src/com/google/android/startop/iorap/RequestId.java
+++ b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java
@@ -75,6 +75,11 @@ public class RequestId implements Parcelable {
}
@Override
+ public int hashCode() {
+ return Long.hashCode(requestId);
+ }
+
+ @Override
public boolean equals(Object other) {
if (this == other) {
return true;
diff --git a/startop/iorap/stress/Android.bp b/startop/iorap/stress/Android.bp
new file mode 100644
index 000000000000..f9f251bdd889
--- /dev/null
+++ b/startop/iorap/stress/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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.
+//
+
+cc_binary {
+ name: "iorap.stress.memory",
+ srcs: ["main_memory.cc"],
+
+ cflags: [
+ "-Wall",
+ "-Wextra",
+ "-Werror",
+ "-Wno-unused-parameter"
+ ],
+
+ shared_libs: [
+ "libbase"
+ ],
+
+ host_supported: true,
+}
diff --git a/startop/iorap/stress/main_memory.cc b/startop/iorap/stress/main_memory.cc
new file mode 100644
index 000000000000..1f268619e4d9
--- /dev/null
+++ b/startop/iorap/stress/main_memory.cc
@@ -0,0 +1,126 @@
+//
+// 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.
+//
+
+#include <chrono>
+#include <fstream>
+#include <iostream>
+#include <random>
+#include <string>
+
+#include <string.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+
+#include <android-base/parseint.h>
+
+static constexpr size_t kBytesPerMb = 1048576;
+const size_t kMemoryAllocationSize = 2 * 1024 * kBytesPerMb;
+
+#define USE_MLOCKALL 0
+
+std::string GetProcessStatus(const char* key) {
+ // Build search pattern of key and separator.
+ std::string pattern(key);
+ pattern.push_back(':');
+
+ // Search for status lines starting with pattern.
+ std::ifstream fs("/proc/self/status");
+ std::string line;
+ while (std::getline(fs, line)) {
+ if (strncmp(pattern.c_str(), line.c_str(), pattern.size()) == 0) {
+ // Skip whitespace in matching line (if any).
+ size_t pos = line.find_first_not_of(" \t", pattern.size());
+ if (pos == std::string::npos) {
+ break;
+ }
+ return std::string(line, pos);
+ }
+ }
+ return "<unknown>";
+}
+
+int main(int argc, char** argv) {
+ size_t allocationSize = 0;
+ if (argc >= 2) {
+ if (!android::base::ParseUint(argv[1], /*out*/&allocationSize)) {
+ std::cerr << "Failed to parse the allocation size (must be 0,MAX_SIZE_T)" << std::endl;
+ return 1;
+ }
+ } else {
+ allocationSize = kMemoryAllocationSize;
+ }
+
+ void* mem = malloc(allocationSize);
+ if (mem == nullptr) {
+ std::cerr << "Malloc failed" << std::endl;
+ return 1;
+ }
+
+ volatile int* imem = static_cast<int *>(mem); // don't optimize out memory usage
+
+ size_t imemCount = allocationSize / sizeof(int);
+
+ std::cout << "Allocated " << allocationSize << " bytes" << std::endl;
+
+ auto seed = std::chrono::high_resolution_clock::now().time_since_epoch().count();
+ std::mt19937 mt_rand(seed);
+
+ size_t randPrintCount = 10;
+
+ // Write random numbers:
+ // * Ensures each page is resident
+ // * Avoids zeroed out pages (zRAM)
+ // * Avoids same-page merging
+ for (size_t i = 0; i < imemCount; ++i) {
+ imem[i] = mt_rand();
+
+ if (i < randPrintCount) {
+ std::cout << "Generated random value: " << imem[i] << std::endl;
+ }
+ }
+
+#if USE_MLOCKALL
+ /*
+ * Lock all pages from the address space of this process.
+ */
+ if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
+ std::cerr << "Mlockall failed" << std::endl;
+ return 1;
+ }
+#else
+ // Use mlock because of the predictable VmLck size.
+ // Using mlockall tends to bring in anywhere from 2-2.5GB depending on the device.
+ if (mlock(mem, allocationSize) != 0) {
+ std::cerr << "Mlock failed" << std::endl;
+ return 1;
+ }
+#endif
+
+ // Validate memory is actually resident and locked with:
+ // $> cat /proc/$(pidof iorap.stress.memory)/status | grep VmLck
+ std::cout << "Locked memory (VmLck) = " << GetProcessStatus("VmLck") << std::endl;
+
+ std::cout << "Press any key to terminate" << std::endl;
+ int any_input;
+ std::cin >> any_input;
+
+ std::cout << "Terminating..." << std::endl;
+
+ munlockall();
+ free(mem);
+
+ return 0;
+}
diff --git a/startop/iorap/tests/AndroidTest.xml b/startop/iorap/tests/AndroidTest.xml
index bcd11033bed3..6102c44e61bf 100644
--- a/startop/iorap/tests/AndroidTest.xml
+++ b/startop/iorap/tests/AndroidTest.xml
@@ -33,18 +33,34 @@
<target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer">
</target_preparer>
+ <!-- do not use DeviceSetup#set-property because it reboots the device b/136200738.
+ furthermore the changes in /data/local.prop don't actually seem to get picked up.
+ -->
<target_preparer
class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- we need this magic flag, otherwise it always reboots and breaks the selinux -->
+ <option name="force-skip-system-props" value="true" />
+
<!-- Crash instead of using Log.wtf within the system_server iorap code. -->
- <option name="set-property" key="iorapd.forwarding_service.wtf_crash" value="true" />
+ <option name="run-command" value="setprop iorapd.forwarding_service.wtf_crash true" />
<!-- IIorapd has fake behavior: it doesn't do anything but reply with 'DONE' status -->
- <option name="set-property" key="iorapd.binder.fake" value="true" />
- <option name="restore-properties" value="true" />
+ <option name="run-command" value="setprop iorapd.binder.fake true" />
+
+ <!-- iorapd does not pick up the above changes until we restart it -->
+ <option name="run-command" value="stop iorapd" />
+ <option name="run-command" value="start iorapd" />
+ <!-- give it some time to restart the service; otherwise the first unit test might fail -->
+ <option name="run-command" value="sleep 1" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.google.android.startop.iorap.tests" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
</test>
+
+ <!-- using DeviceSetup again does not work. we simply leave the device in a semi-bad
+ state. there is no way to clean this up as far as I know.
+ -->
+
</configuration>
diff --git a/startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt b/startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt
new file mode 100644
index 000000000000..51e407d4cbff
--- /dev/null
+++ b/startop/iorap/tests/src/com/google/android/startop/iorap/AppLaunchEventTest.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2019 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 com.google.android.startop.iorap
+
+import android.content.Intent;
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.test.filters.SmallTest
+import com.google.android.startop.iorap.AppLaunchEvent;
+import com.google.android.startop.iorap.AppLaunchEvent.ActivityLaunched
+import com.google.android.startop.iorap.AppLaunchEvent.ActivityLaunchCancelled
+import com.google.android.startop.iorap.AppLaunchEvent.ActivityLaunchFinished
+import com.google.android.startop.iorap.AppLaunchEvent.IntentStarted;
+import com.google.android.startop.iorap.AppLaunchEvent.IntentFailed;
+import com.google.android.startop.iorap.AppLaunchEvent.ReportFullyDrawn
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+
+/**
+ * Basic unit tests to test all of the [AppLaunchEvent]s in [com.google.android.startop.iorap].
+ */
+@SmallTest
+class AppLaunchEventTest {
+ /**
+ * Test for IntentStarted.
+ */
+ @Test
+ fun testIntentStarted() {
+ var intent = Intent()
+ val valid = IntentStarted(/* sequenceId= */2L, intent, /* timestampNs= */ 1L)
+ val copy = IntentStarted(/* sequenceId= */2L, intent, /* timestampNs= */ 1L)
+ val noneCopy1 = IntentStarted(/* sequenceId= */1L, intent, /* timestampNs= */ 1L)
+ val noneCopy2 = IntentStarted(/* sequenceId= */2L, intent, /* timestampNs= */ 2L)
+ val noneCopy3 = IntentStarted(/* sequenceId= */2L, Intent(), /* timestampNs= */ 1L)
+
+ // equals(Object other)
+ assertThat(valid).isEqualTo(copy)
+ assertThat(valid).isNotEqualTo(noneCopy1)
+ assertThat(valid).isNotEqualTo(noneCopy2)
+ assertThat(valid).isNotEqualTo(noneCopy3)
+
+ // test toString()
+ val result = valid.toString()
+ assertThat(result).isEqualTo("IntentStarted{sequenceId=2, intent=Intent { } , timestampNs=1}")
+ }
+
+ /**
+ * Test for IntentFailed.
+ */
+ @Test
+ fun testIntentFailed() {
+ val valid = IntentFailed(/* sequenceId= */2L)
+ val copy = IntentFailed(/* sequenceId= */2L)
+ val noneCopy = IntentFailed(/* sequenceId= */1L)
+
+ // equals(Object other)
+ assertThat(valid).isEqualTo(copy)
+ assertThat(valid).isNotEqualTo(noneCopy)
+
+ // test toString()
+ val result = valid.toString()
+ assertThat(result).isEqualTo("IntentFailed{sequenceId=2}")
+ }
+
+ /**
+ * Test for ActivityLaunched.
+ */
+ @Test
+ fun testActivityLaunched() {
+ //var activityRecord =
+ val valid = ActivityLaunched(/* sequenceId= */2L, "test".toByteArray(),
+ /* temperature= */ 0)
+ val copy = ActivityLaunched(/* sequenceId= */2L, "test".toByteArray(),
+ /* temperature= */ 0)
+ val noneCopy1 = ActivityLaunched(/* sequenceId= */1L, "test".toByteArray(),
+ /* temperature= */ 0)
+ val noneCopy2 = ActivityLaunched(/* sequenceId= */1L, "test".toByteArray(),
+ /* temperature= */ 1)
+ val noneCopy3 = ActivityLaunched(/* sequenceId= */1L, "test1".toByteArray(),
+ /* temperature= */ 0)
+
+ // equals(Object other)
+ assertThat(valid).isEqualTo(copy)
+ assertThat(valid).isNotEqualTo(noneCopy1)
+ assertThat(valid).isNotEqualTo(noneCopy2)
+ assertThat(valid).isNotEqualTo(noneCopy3)
+
+ // test toString()
+ val result = valid.toString()
+ assertThat(result).isEqualTo("ActivityLaunched{sequenceId=2, test, temperature=0}")
+ }
+
+
+ /**
+ * Test for ActivityLaunchFinished.
+ */
+ @Test
+ fun testActivityLaunchFinished() {
+ val valid = ActivityLaunchFinished(/* sequenceId= */2L, "test".toByteArray(),
+ /* timestampNs= */ 1L)
+ val copy = ActivityLaunchFinished(/* sequenceId= */2L, "test".toByteArray(),
+ /* timestampNs= */ 1L)
+ val noneCopy1 = ActivityLaunchFinished(/* sequenceId= */1L, "test".toByteArray(),
+ /* timestampNs= */ 1L)
+ val noneCopy2 = ActivityLaunchFinished(/* sequenceId= */1L, "test".toByteArray(),
+ /* timestampNs= */ 2L)
+ val noneCopy3 = ActivityLaunchFinished(/* sequenceId= */2L, "test1".toByteArray(),
+ /* timestampNs= */ 1L)
+
+ // equals(Object other)
+ assertThat(valid).isEqualTo(copy)
+ assertThat(valid).isNotEqualTo(noneCopy1)
+ assertThat(valid).isNotEqualTo(noneCopy2)
+ assertThat(valid).isNotEqualTo(noneCopy3)
+
+ // test toString()
+ val result = valid.toString()
+ assertThat(result).isEqualTo("ActivityLaunchFinished{sequenceId=2, test, timestampNs=1}")
+ }
+
+ /**
+ * Test for ActivityLaunchCancelled.
+ */
+ @Test
+ fun testActivityLaunchCancelled() {
+ val valid = ActivityLaunchCancelled(/* sequenceId= */2L, "test".toByteArray())
+ val copy = ActivityLaunchCancelled(/* sequenceId= */2L, "test".toByteArray())
+ val noneCopy1 = ActivityLaunchCancelled(/* sequenceId= */1L, "test".toByteArray())
+ val noneCopy2 = ActivityLaunchCancelled(/* sequenceId= */2L, "test1".toByteArray())
+
+ // equals(Object other)
+ assertThat(valid).isEqualTo(copy)
+ assertThat(valid).isNotEqualTo(noneCopy1)
+ assertThat(valid).isNotEqualTo(noneCopy2)
+
+ // test toString()
+ val result = valid.toString()
+ assertThat(result).isEqualTo("ActivityLaunchCancelled{sequenceId=2, test}")
+ }
+
+ /**
+ * Test for ReportFullyDrawn.
+ */
+ @Test
+ fun testReportFullyDrawn() {
+ val valid = ReportFullyDrawn(/* sequenceId= */2L, "test".toByteArray(), /* timestampNs= */ 1L)
+ val copy = ReportFullyDrawn(/* sequenceId= */2L, "test".toByteArray(), /* timestampNs= */ 1L)
+ val noneCopy1 = ReportFullyDrawn(/* sequenceId= */1L, "test".toByteArray(),
+ /* timestampNs= */ 1L)
+ val noneCopy2 = ReportFullyDrawn(/* sequenceId= */1L, "test".toByteArray(),
+ /* timestampNs= */ 1L)
+ val noneCopy3 = ReportFullyDrawn(/* sequenceId= */2L, "test1".toByteArray(),
+ /* timestampNs= */ 1L)
+
+ // equals(Object other)
+ assertThat(valid).isEqualTo(copy)
+ assertThat(valid).isNotEqualTo(noneCopy1)
+ assertThat(valid).isNotEqualTo(noneCopy2)
+ assertThat(valid).isNotEqualTo(noneCopy3)
+
+ // test toString()
+ val result = valid.toString()
+ assertThat(result).isEqualTo("ReportFullyDrawn{sequenceId=2, test, timestampNs=1}")
+ }
+}
diff --git a/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt b/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt
index b1e6194e0c92..18c249136d05 100644
--- a/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt
+++ b/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt
@@ -14,7 +14,9 @@
package com.google.android.startop.iorap
+import android.net.Uri
import android.os.ServiceManager
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import org.junit.Test
import org.mockito.Mockito.argThat
@@ -25,6 +27,7 @@ import org.mockito.Mockito.timeout
// @Ignore("Test is disabled until iorapd is added to init and there's selinux policies for it")
@MediumTest
+@FlakyTest(bugId = 149098310) // Failing on cuttlefish with SecurityException.
class IIorapIntegrationTest {
/**
* @throws ServiceManager.ServiceNotFoundException if iorapd service could not be found
@@ -54,6 +57,9 @@ class IIorapIntegrationTest {
private fun testAnyMethod(func: (RequestId) -> Unit) {
val taskListener = spy(DummyTaskListener())!!
+ // FIXME: b/149098310
+ return
+
try {
iorapService.setTaskListener(taskListener)
// Note: Binder guarantees total order for oneway messages sent to the same binder
@@ -85,6 +91,9 @@ class IIorapIntegrationTest {
@Test
fun testOnPackageEvent() {
+ // FIXME (b/137134253): implement PackageEvent parsing on the C++ side.
+ // This is currently (silently: b/137135024) failing because IIorap is 'oneway' and the
+ // C++ PackageEvent un-parceling fails since its not implemented fully.
/*
testAnyMethod { requestId : RequestId ->
iorapService.onPackageEvent(requestId,
@@ -92,7 +101,6 @@ class IIorapIntegrationTest {
Uri.parse("https://www.google.com"), "com.fake.package"))
}
*/
- // FIXME: Broken for some reason. C++ side never sees this call.
}
@Test
@@ -105,6 +113,13 @@ class IIorapIntegrationTest {
}
@Test
+ fun testOnAppLaunchEvent() {
+ testAnyMethod { requestId : RequestId ->
+ iorapService.onAppLaunchEvent(requestId, AppLaunchEvent.IntentFailed(/*sequenceId*/123))
+ }
+ }
+
+ @Test
fun testOnSystemServiceEvent() {
testAnyMethod { requestId: RequestId ->
iorapService.onSystemServiceEvent(requestId,