diff options
author | 2022-01-31 19:00:37 +0000 | |
---|---|---|
committer | 2022-01-31 19:00:37 +0000 | |
commit | 2f393cdbce9878ede68b7d4c32ddc404727c3532 (patch) | |
tree | be85e373f23b19fca73bf504be6ded62b79d3adf | |
parent | 59a56500c0c8171427852a0ff95de9622807745c (diff) |
Revert "Remove iorap framework codes"
Revert "Remove iorap daemon codes"
Revert "Remove configs relevant to iorap"
Revert submission 16528474-remove-iorap
Reason for revert: build break
Reverted Changes:
I464c9e9c4:Remove scripts related to iorap
I0b8b1b064:Remove iorap daemon codes
I848f65908:Remove iorap framework codes
I294f37265:Remove configs relevant to iorap
Change-Id: Idbd2f34e952325d9fee00ce3b269293b848cf545
32 files changed, 4349 insertions, 0 deletions
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 92f7f2921bee..eff7ff152d47 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6803,6 +6803,10 @@ android:resource="@xml/autofill_compat_accessibility_service" /> </service> + <service android:name="com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy" + android:permission="android.permission.BIND_JOB_SERVICE" > + </service> + <service android:name="com.android.server.blob.BlobStoreIdleJobService" android:permission="android.permission.BIND_JOB_SERVICE"> </service> diff --git a/services/Android.bp b/services/Android.bp index 7e009868fa1a..af70692a88e5 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -95,6 +95,7 @@ filegroup { ":services.selectiontoolbar-sources", ":services.smartspace-sources", ":services.speech-sources", + ":services.startop.iorap-sources", ":services.systemcaptions-sources", ":services.translation-sources", ":services.texttospeech-sources", @@ -150,6 +151,7 @@ java_library { "services.selectiontoolbar", "services.smartspace", "services.speech", + "services.startop", "services.systemcaptions", "services.translation", "services.texttospeech", @@ -205,6 +207,7 @@ stubs_defaults { " --hide-annotation android.annotation.Hide" + " --hide InternalClasses" + // com.android.* classes are okay in this interface // TODO: remove the --hide options below + " --hide-package com.google.android.startop.iorap" + " --hide DeprecationMismatch" + " --hide HiddenTypedefConstant", visibility: ["//frameworks/base:__subpackages__"], diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 3fd5cd167d95..aad1bf8ab7c3 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -213,6 +213,8 @@ import com.android.server.wm.WindowManagerService; import dalvik.system.VMRuntime; +import com.google.android.startop.iorap.IorapForwardingService; + import java.io.File; import java.io.FileDescriptor; import java.io.IOException; @@ -1615,6 +1617,10 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(PinnerService.class); t.traceEnd(); + t.traceBegin("IorapForwardingService"); + mSystemServiceManager.startService(IorapForwardingService.class); + t.traceEnd(); + if (Build.IS_DEBUGGABLE && ProfcollectForwardingService.enabled()) { t.traceBegin("ProfcollectForwardingService"); mSystemServiceManager.startService(ProfcollectForwardingService.class); diff --git a/services/startop/Android.bp b/services/startop/Android.bp new file mode 100644 index 000000000000..c56c463d0168 --- /dev/null +++ b/services/startop/Android.bp @@ -0,0 +1,36 @@ +/* + * 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + // SPDX-license-identifier-MIT + // SPDX-license-identifier-Unicode-DFS + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_static { + name: "services.startop", + defaults: ["platform_service_defaults"], + + static_libs: [ + // frameworks/base/startop/iorap + "services.startop.iorap", + ], +} diff --git a/startop/iorap/Android.bp b/startop/iorap/Android.bp new file mode 100644 index 000000000000..4fdf34cc1814 --- /dev/null +++ b/startop/iorap/Android.bp @@ -0,0 +1,44 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "services.startop.iorap-javasources", + srcs: ["src/**/*.java"], + path: "src", + visibility: ["//visibility:private"], +} + +filegroup { + name: "services.startop.iorap-sources", + srcs: [ + ":services.startop.iorap-javasources", + ":iorap-aidl", + ], + visibility: ["//frameworks/base/services:__subpackages__"], +} + +java_library_static { + name: "services.startop.iorap", + srcs: [":services.startop.iorap-sources"], + libs: ["services.core"], +} diff --git a/startop/iorap/TEST_MAPPING b/startop/iorap/TEST_MAPPING new file mode 100644 index 000000000000..8c9d4dfb0894 --- /dev/null +++ b/startop/iorap/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "libiorap-java-tests" + } + ], + "imports": [ + { + "path": "system/iorap" + } + ] +} diff --git a/startop/iorap/functional_tests/Android.bp b/startop/iorap/functional_tests/Android.bp new file mode 100644 index 000000000000..43c61551cdec --- /dev/null +++ b/startop/iorap/functional_tests/Android.bp @@ -0,0 +1,50 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_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/ActivityHintEvent.java b/startop/iorap/src/com/google/android/startop/iorap/ActivityHintEvent.java new file mode 100644 index 000000000000..1d38f4c1e23d --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/ActivityHintEvent.java @@ -0,0 +1,139 @@ +/* + * 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.os.Parcelable; +import android.os.Parcel; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Provide a hint to iorapd that an activity has transitioned state.<br /><br /> + * + * Knowledge of when an activity starts/stops can be used by iorapd to increase system + * performance (e.g. by launching perfetto tracing to record an io profile, or by + * playing back an ioprofile via readahead) over the long run.<br /><br /> + * + * /@see com.google.android.startop.iorap.IIorap#onActivityHintEvent<br /><br /> + * + * Once an activity hint is in {@link #TYPE_STARTED} it must transition to another type. + * All other states could be terminal, see below: <br /><br /> + * + * <pre> + * + * ┌──────────────────────────────────────┐ + * │ ▼ + * ┌─────────┐ ╔════════════════╗ ╔═══════════╗ + * ──▶ │ STARTED │ ──▶ ║ COMPLETED ║ ──▶ ║ CANCELLED ║ + * └─────────┘ ╚════════════════╝ ╚═══════════╝ + * │ + * │ + * ▼ + * ╔════════════════╗ + * ║ POST_COMPLETED ║ + * ╚════════════════╝ + * + * </pre> <!-- system/iorap/docs/binder/ActivityHint.dot --> + * + * @hide + */ +public class ActivityHintEvent implements Parcelable { + + public static final int TYPE_STARTED = 0; + public static final int TYPE_CANCELLED = 1; + public static final int TYPE_COMPLETED = 2; + public static final int TYPE_POST_COMPLETED = 3; + private static final int TYPE_MAX = TYPE_POST_COMPLETED; + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_STARTED, + TYPE_CANCELLED, + TYPE_COMPLETED, + TYPE_POST_COMPLETED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + @Type public final int type; + public final ActivityInfo activityInfo; + + public ActivityHintEvent(@Type int type, ActivityInfo activityInfo) { + this.type = type; + this.activityInfo = activityInfo; + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + CheckHelpers.checkTypeInRange(type, TYPE_MAX); + Objects.requireNonNull(activityInfo, "activityInfo"); + } + + @Override + public String toString() { + return String.format("{type: %d, activityInfo: %s}", type, activityInfo); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof ActivityHintEvent) { + return equals((ActivityHintEvent) other); + } + return false; + } + + private boolean equals(ActivityHintEvent other) { + return type == other.type && + Objects.equals(activityInfo, other.activityInfo); + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + activityInfo.writeToParcel(out, flags); + } + + private ActivityHintEvent(Parcel in) { + this.type = in.readInt(); + this.activityInfo = ActivityInfo.CREATOR.createFromParcel(in); + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<ActivityHintEvent> CREATOR + = new Parcelable.Creator<ActivityHintEvent>() { + public ActivityHintEvent createFromParcel(Parcel in) { + return new ActivityHintEvent(in); + } + + public ActivityHintEvent[] newArray(int size) { + return new ActivityHintEvent[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/ActivityInfo.java b/startop/iorap/src/com/google/android/startop/iorap/ActivityInfo.java new file mode 100644 index 000000000000..f47a42cffdd8 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/ActivityInfo.java @@ -0,0 +1,104 @@ +/* + * 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 java.util.Objects; + +import android.os.Parcelable; +import android.os.Parcel; + +/** + * Provide minimal information for launched activities to iorap.<br /><br /> + * + * This uniquely identifies a system-wide activity by providing the {@link #packageName} and + * {@link #activityName}. + * + * @see ActivityHintEvent + * @see AppIntentEvent + * + * @hide + */ +public class ActivityInfo implements Parcelable { + + /** The name of the package, for example {@code com.android.calculator}. */ + public final String packageName; + /** The name of the activity, for example {@code .activities.activity.MainActivity} */ + public final String activityName; + + public ActivityInfo(String packageName, String activityName) { + this.packageName = packageName; + this.activityName = activityName; + + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + Objects.requireNonNull(packageName, "packageName"); + Objects.requireNonNull(activityName, "activityName"); + } + + @Override + public String toString() { + return String.format("{packageName: %s, activityName: %s}", packageName, activityName); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof ActivityInfo) { + return equals((ActivityInfo) other); + } + return false; + } + + private boolean equals(ActivityInfo other) { + return Objects.equals(packageName, other.packageName) && + Objects.equals(activityName, other.activityName); + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(packageName); + out.writeString(activityName); + } + + private ActivityInfo(Parcel in) { + packageName = in.readString(); + activityName = in.readString(); + + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<ActivityInfo> CREATOR + = new Parcelable.Creator<ActivityInfo>() { + public ActivityInfo createFromParcel(Parcel in) { + return new ActivityInfo(in); + } + + public ActivityInfo[] newArray(int size) { + return new ActivityInfo[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/AppIntentEvent.java b/startop/iorap/src/com/google/android/startop/iorap/AppIntentEvent.java new file mode 100644 index 000000000000..1cd37b5546b9 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/AppIntentEvent.java @@ -0,0 +1,138 @@ +/* + * 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.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 system-wide intent defaults change.<br /><br /> + * + * Intent defaults provide a mechanism for an app to register itself as an automatic handler. + * For example the camera app might be registered as the default handler for + * {@link android.provider.MediaStore#INTENT_ACTION_STILL_IMAGE_CAMERA} intent. Subsequently, + * if an arbitrary other app requests for a still image camera photo to be taken, the system + * will launch the respective default camera app to be launched to handle that request.<br /><br /> + * + * In some cases iorapd might need to know default intents, e.g. for boot-time pinning of + * applications that resolve from the default intent. If the application would now be resolved + * differently, iorapd would unpin the old application and pin the new application.<br /><br /> + * + * @hide + */ +public class AppIntentEvent implements Parcelable { + + /** @see android.content.Intent#CATEGORY_DEFAULT */ + public static final int TYPE_DEFAULT_INTENT_CHANGED = 0; + private static final int TYPE_MAX = 0; + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_DEFAULT_INTENT_CHANGED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + @Type public final int type; + + public final ActivityInfo oldActivityInfo; + public final ActivityInfo newActivityInfo; + + // TODO: Probably need the corresponding action here as well. + + public static AppIntentEvent createDefaultIntentChanged(ActivityInfo oldActivityInfo, + ActivityInfo newActivityInfo) { + return new AppIntentEvent(TYPE_DEFAULT_INTENT_CHANGED, oldActivityInfo, + newActivityInfo); + } + + private AppIntentEvent(@Type int type, ActivityInfo oldActivityInfo, + ActivityInfo newActivityInfo) { + this.type = type; + this.oldActivityInfo = oldActivityInfo; + this.newActivityInfo = newActivityInfo; + + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + CheckHelpers.checkTypeInRange(type, TYPE_MAX); + Objects.requireNonNull(oldActivityInfo, "oldActivityInfo"); + Objects.requireNonNull(oldActivityInfo, "newActivityInfo"); + } + + @Override + public String toString() { + return String.format("{oldActivityInfo: %s, newActivityInfo: %s}", oldActivityInfo, + newActivityInfo); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof AppIntentEvent) { + return equals((AppIntentEvent) other); + } + return false; + } + + private boolean equals(AppIntentEvent other) { + return type == other.type && + Objects.equals(oldActivityInfo, other.oldActivityInfo) && + Objects.equals(newActivityInfo, other.newActivityInfo); + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + oldActivityInfo.writeToParcel(out, flags); + newActivityInfo.writeToParcel(out, flags); + } + + private AppIntentEvent(Parcel in) { + this.type = in.readInt(); + this.oldActivityInfo = ActivityInfo.CREATOR.createFromParcel(in); + this.newActivityInfo = ActivityInfo.CREATOR.createFromParcel(in); + + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<AppIntentEvent> CREATOR + = new Parcelable.Creator<AppIntentEvent>() { + public AppIntentEvent createFromParcel(Parcel in) { + return new AppIntentEvent(in); + } + + public AppIntentEvent[] newArray(int size) { + return new AppIntentEvent[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java b/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java new file mode 100644 index 000000000000..8263e0af4422 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/AppLaunchEvent.java @@ -0,0 +1,459 @@ +/* + * 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.annotation.LongDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; + +import com.android.server.wm.ActivityMetricsLaunchObserver; +import com.android.server.wm.ActivityMetricsLaunchObserver.ActivityRecordProto; +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; + +/** + * Provide a hint to iorapd that an app launch sequence has transitioned state.<br /><br /> + * + * Knowledge of when an activity starts/stops can be used by iorapd to increase system + * performance (e.g. by launching perfetto tracing to record an io profile, or by + * playing back an ioprofile via readahead) over the long run.<br /><br /> + * + * /@see com.google.android.startop.iorap.IIorap#onAppLaunchEvent <br /><br /> + * @see com.android.server.wm.ActivityMetricsLaunchObserver + * ActivityMetricsLaunchObserver for the possible event states. + * @hide + */ +public abstract class AppLaunchEvent implements Parcelable { + @LongDef + @Retention(RetentionPolicy.SOURCE) + public @interface SequenceId {} + + public final @SequenceId + long sequenceId; + + protected AppLaunchEvent(@SequenceId long sequenceId) { + this.sequenceId = sequenceId; + } + + @Override + public boolean equals(Object other) { + if (other instanceof AppLaunchEvent) { + return equals((AppLaunchEvent) other); + } + return false; + } + + protected boolean equals(AppLaunchEvent other) { + return sequenceId == other.sequenceId; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + + "{" + "sequenceId=" + Long.toString(sequenceId) + + toStringBody() + "}"; + } + + protected String toStringBody() { return ""; }; + + // List of possible variants: + + public static final class IntentStarted extends AppLaunchEvent { + @NonNull + public final Intent intent; + public final long timestampNs; + + public IntentStarted(@SequenceId long sequenceId, + Intent intent, + long timestampNs) { + super(sequenceId); + this.intent = intent; + this.timestampNs = timestampNs; + + Objects.requireNonNull(intent, "intent"); + } + + @Override + public boolean equals(Object other) { + if (other instanceof IntentStarted) { + return intent.equals(((IntentStarted)other).intent) && + timestampNs == ((IntentStarted)other).timestampNs && + super.equals(other); + } + return false; + } + + @Override + protected String toStringBody() { + return ", intent=" + intent.toString() + + " , timestampNs=" + Long.toString(timestampNs); + } + + + @Override + 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(); + } + } + + public static final class IntentFailed extends AppLaunchEvent { + public IntentFailed(@SequenceId long sequenceId) { + super(sequenceId); + } + + @Override + public boolean equals(Object other) { + if (other instanceof IntentFailed) { + return super.equals(other); + } + return false; + } + + IntentFailed(Parcel p) { + super(p); + } + } + + public static abstract class BaseWithActivityRecordData extends AppLaunchEvent { + public final @NonNull + @ActivityRecordProto byte[] activityRecordSnapshot; + + protected BaseWithActivityRecordData(@SequenceId long sequenceId, + @NonNull @ActivityRecordProto byte[] snapshot) { + super(sequenceId); + activityRecordSnapshot = snapshot; + + Objects.requireNonNull(snapshot, "snapshot"); + } + + @Override + public boolean equals(Object other) { + if (other instanceof BaseWithActivityRecordData) { + return (Arrays.equals(activityRecordSnapshot, + ((BaseWithActivityRecordData)other).activityRecordSnapshot)) && + super.equals(other); + } + return false; + } + + @Override + protected String toStringBody() { + return ", " + new String(activityRecordSnapshot); + } + + @Override + protected void writeToParcelImpl(Parcel p, int flags) { + super.writeToParcelImpl(p, flags); + ActivityRecordProtoParcelable.write(p, activityRecordSnapshot, flags); + } + + BaseWithActivityRecordData(Parcel p) { + super(p); + activityRecordSnapshot = ActivityRecordProtoParcelable.create(p); + } + } + + public static final class ActivityLaunched extends BaseWithActivityRecordData { + public final @ActivityMetricsLaunchObserver.Temperature + int temperature; + + public ActivityLaunched(@SequenceId long sequenceId, + @NonNull @ActivityRecordProto byte[] snapshot, + @ActivityMetricsLaunchObserver.Temperature int temperature) { + super(sequenceId, snapshot); + this.temperature = temperature; + } + + @Override + public boolean equals(Object other) { + if (other instanceof ActivityLaunched) { + return temperature == ((ActivityLaunched)other).temperature && + super.equals(other); + } + return false; + } + + @Override + protected String toStringBody() { + return super.toStringBody() + ", temperature=" + Integer.toString(temperature); + } + + @Override + protected void writeToParcelImpl(Parcel p, int flags) { + super.writeToParcelImpl(p, flags); + p.writeInt(temperature); + } + + ActivityLaunched(Parcel p) { + super(p); + temperature = p.readInt(); + } + } + + public static final class ActivityLaunchFinished extends BaseWithActivityRecordData { + public final long timestampNs; + + public ActivityLaunchFinished(@SequenceId long sequenceId, + @NonNull @ActivityRecordProto byte[] snapshot, + long timestampNs) { + super(sequenceId, snapshot); + this.timestampNs = timestampNs; + } + + @Override + public boolean equals(Object 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 { + public final @Nullable @ActivityRecordProto byte[] activityRecordSnapshot; + + public ActivityLaunchCancelled(@SequenceId long sequenceId, + @Nullable @ActivityRecordProto byte[] snapshot) { + super(sequenceId); + activityRecordSnapshot = snapshot; + } + + @Override + public boolean equals(Object other) { + if (other instanceof ActivityLaunchCancelled) { + return Arrays.equals(activityRecordSnapshot, + ((ActivityLaunchCancelled)other).activityRecordSnapshot) && + super.equals(other); + } + return false; + } + + @Override + protected String toStringBody() { + return super.toStringBody() + ", " + new String(activityRecordSnapshot); + } + + @Override + protected void writeToParcelImpl(Parcel p, int flags) { + super.writeToParcelImpl(p, flags); + if (activityRecordSnapshot != null) { + p.writeBoolean(true); + ActivityRecordProtoParcelable.write(p, activityRecordSnapshot, flags); + } else { + p.writeBoolean(false); + } + } + + ActivityLaunchCancelled(Parcel p) { + super(p); + if (p.readBoolean()) { + activityRecordSnapshot = ActivityRecordProtoParcelable.create(p); + } else { + activityRecordSnapshot = null; + } + } + } + + 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; } + + @Override + public void writeToParcel(Parcel p, @WriteFlags int flags) { + p.writeInt(getTypeIndex()); + + writeToParcelImpl(p, flags); + } + + + public static Creator<AppLaunchEvent> CREATOR = + new Creator<AppLaunchEvent>() { + @Override + public AppLaunchEvent createFromParcel(Parcel source) { + int typeIndex = source.readInt(); + + Class<?> kls = getClassFromTypeIndex(typeIndex); + if (kls == null) { + throw new IllegalArgumentException("Invalid type index: " + typeIndex); + } + + try { + return (AppLaunchEvent) kls.getConstructor(Parcel.class).newInstance(source); + } catch (InstantiationException e) { + throw new AssertionError(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new AssertionError(e); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + @Override + public AppLaunchEvent[] newArray(int size) { + return new AppLaunchEvent[0]; + } + }; + + protected void writeToParcelImpl(Parcel p, int flags) { + p.writeLong(sequenceId); + } + + protected AppLaunchEvent(Parcel p) { + sequenceId = p.readLong(); + } + + private int getTypeIndex() { + for (int i = 0; i < sTypes.length; ++i) { + if (sTypes[i].equals(this.getClass())) { + return i; + } + } + throw new AssertionError("sTypes did not include this type: " + this.getClass()); + } + + private static @Nullable Class<?> getClassFromTypeIndex(int typeIndex) { + if (typeIndex >= 0 && typeIndex < sTypes.length) { + return sTypes[typeIndex]; + } + return null; + } + + // Index position matters: It is used to encode the specific type in parceling. + // Keep up-to-date with C++ side. + private static Class<?>[] sTypes = new Class[] { + IntentStarted.class, + IntentFailed.class, + ActivityLaunched.class, + ActivityLaunchFinished.class, + ActivityLaunchCancelled.class, + ReportFullyDrawn.class, + }; + + public static class ActivityRecordProtoParcelable { + public static void write(Parcel p, @ActivityRecordProto byte[] activityRecordSnapshot, + int flags) { + p.writeByteArray(activityRecordSnapshot); + } + + public static @ActivityRecordProto byte[] create(Parcel p) { + byte[] data = p.createByteArray(); + + return data; + } + } + + public static class IntentProtoParcelable { + private static final int INTENT_PROTO_CHUNK_SIZE = 1024; + + public static void write(Parcel p, @NonNull Intent intent, int flags) { + // There does not appear to be a way to 'reset' a ProtoOutputBuffer stream, + // so create a new one every time. + 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.dumpDebug(protoOutputStream); + final byte[] bytes = protoOutputStream.getBytes(); + + p.writeByteArray(bytes); + } + + // TODO: Should be mockable for testing? + // We cannot deserialize in the platform because we don't have a 'readFromProto' + // code. + public static @NonNull Intent create(Parcel p) { + // This will "read" the correct amount of data, but then we discard it. + byte[] data = p.createByteArray(); + + // Never called by real code in a platform, this binder API is implemented only in C++. + return new Intent("<cannot deserialize IntentProto>"); + } + } +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/CheckHelpers.java b/startop/iorap/src/com/google/android/startop/iorap/CheckHelpers.java new file mode 100644 index 000000000000..34aedd7685d8 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/CheckHelpers.java @@ -0,0 +1,46 @@ +/* + * 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; + +/** + * Convenience short-hand to throw {@link IllegalAccessException} when the arguments + * are out-of-range. + */ +public class CheckHelpers { + /** @throws IllegalAccessException if {@param type} is not in {@code [0..maxValue]} */ + public static void checkTypeInRange(int type, int maxValue) { + if (type < 0) { + throw new IllegalArgumentException( + String.format("type must be non-negative (value=%d)", type)); + } + if (type > maxValue) { + throw new IllegalArgumentException( + String.format("type out of range (value=%d, max=%d)", type, maxValue)); + } + } + + /** @throws IllegalAccessException if {@param state} is not in {@code [0..maxValue]} */ + public static void checkStateInRange(int state, int maxValue) { + if (state < 0) { + throw new IllegalArgumentException( + String.format("state must be non-negative (value=%d)", state)); + } + if (state > maxValue) { + throw new IllegalArgumentException( + String.format("state out of range (value=%d, max=%d)", state, maxValue)); + } + } +} 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..dcaff26b79c6 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/EventSequenceValidator.java @@ -0,0 +1,267 @@ +/* + * 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"; + /** $> adb shell 'setprop log.tag.EventSequenceValidator VERBOSE' */ + public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + 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.i(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.i(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.i(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.i(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.i(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.i(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.i(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.i(TAG, + String.format("dec AccIntentStartedEvents to %d", accIntentStartedEvents)); + } + + private void logWarningWithStackTrace(String log) { + if (DEBUG) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + new Throwable("EventSequenceValidator#getStackTrace").printStackTrace(pw); + Log.wtf(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 new file mode 100644 index 000000000000..77046f2276e9 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java @@ -0,0 +1,806 @@ +/* + * 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; +// TODO: rename to com.android.server.startop.iorap + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder.DeathRecipient; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.provider.DeviceConfig; +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.pm.PackageManagerService; +import com.android.server.wm.ActivityMetricsLaunchObserver; +import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; +import com.android.server.wm.ActivityTaskManagerInternal; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +/** + * 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.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); + /** $> adb shell 'setprop iorapd.forwarding_service.wtf_crash true' */ + private static boolean WTF_CRASH = SystemProperties.getBoolean( + "iorapd.forwarding_service.wtf_crash", false); + private static final Duration TIMEOUT = Duration.ofSeconds(600L); + + // "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). + + + /** + * Atomics set to true if the JobScheduler requests an abort. + */ + private final AtomicBoolean mAbortIdleCompilation = new AtomicBoolean(false); + + /** + * Initializes the system service. + * <p> + * Subclasses must define a single argument constructor that accepts the context + * and passes it to super. + * </p> + * + * @param context The system server context. + */ + 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"> + /* + * Providers for external dependencies: + * - These are marked as protected to allow tests to inject different values via mocks. + */ + + @VisibleForTesting + protected ActivityMetricsLaunchObserverRegistry provideLaunchObserverRegistry() { + ActivityTaskManagerInternal amtInternal = + LocalServices.getService(ActivityTaskManagerInternal.class); + ActivityMetricsLaunchObserverRegistry launchObserverRegistry = + amtInternal.getLaunchObserverRegistry(); + return launchObserverRegistry; + } + + @VisibleForTesting + protected IIorap provideIorapRemote() { + IIorap iorap; + try { + iorap = IIorap.Stub.asInterface(ServiceManager.getServiceOrThrow("iorapd")); + } catch (ServiceManager.ServiceNotFoundException e) { + Log.w(TAG, e.getMessage()); + return null; + } + + try { + iorap.asBinder().linkToDeath(provideDeathRecipient(), /*flags*/0); + } catch (RemoteException e) { + handleRemoteError(e); + return null; + } + + return iorap; + } + + @VisibleForTesting + protected DeathRecipient provideDeathRecipient() { + return new DeathRecipient() { + @Override + public void binderDied() { + Log.w(TAG, "iorapd has died"); + retryConnectToRemoteAndConfigure(/*attempts*/0); + + if (mJobService != null) { + mJobService.onIorapdDisconnected(); + } + } + }; + } + + @VisibleForTesting + protected boolean isIorapEnabled() { + // These two mendel flags should match those in iorapd native process + // system/iorapd/src/common/property.h + boolean isTracingEnabled = + getMendelFlag("iorap_perfetto_enable", "iorapd.perfetto.enable", false); + boolean isReadAheadEnabled = + getMendelFlag("iorap_readahead_enable", "iorapd.readahead.enable", false); + // Same as the property in iorapd.rc -- disabling this will mean the 'iorapd' binder process + // never comes up, so all binder connections will fail indefinitely. + return IS_ENABLED && (isTracingEnabled || isReadAheadEnabled); + } + + private boolean getMendelFlag(String mendelFlag, String sysProperty, boolean defaultValue) { + // TODO(yawanng) use DeviceConfig to get mendel property. + // DeviceConfig doesn't work and the reason is not clear. + // Provider service is already up before IORapForwardService. + String mendelProperty = "persist.device_config." + + DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT + + "." + + mendelFlag; + return SystemProperties.getBoolean(mendelProperty, + SystemProperties.getBoolean(sysProperty, defaultValue)); + } + + //</editor-fold> + + @Override + public void onStart() { + if (DEBUG) { + Log.v(TAG, "onStart"); + } + + 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); + } + + public static final int MESSAGE_BINDER_CONNECT = 0; + + private int mAttempts = 0; + + @Override + public void handleMessage(android.os.Message message) { + switch (message.what) { + case MESSAGE_BINDER_CONNECT: + if (!retryConnectToRemoteAndConfigure(mAttempts)) { + mAttempts++; + } else { + mAttempts = 0; + } + break; + default: + throw new AssertionError("Unknown message: " + message.toString()); + } + } + } + + /** + * Handle iorapd shutdowns and crashes, by attempting to reconnect + * until the service is reached again. + * + * <p>The first connection attempt is synchronous, + * subsequent attempts are done by posting delayed tasks to the IoThread.</p> + * + * @return true if connection succeeded now, or false if it failed now [and needs to requeue]. + */ + private boolean retryConnectToRemoteAndConfigure(int attempts) { + final int sleepTime = 1000; // ms + + if (DEBUG) { + Log.v(TAG, "retryConnectToRemoteAndConfigure - attempt #" + attempts); + } + + if (connectToRemoteAndConfigure()) { + return true; + } + + // Either 'iorapd' is stuck in a crash loop (ouch!!) or we manually + // called 'adb shell stop iorapd' , which means this would loop until it comes back + // up. + // + // TODO: it would be good to get nodified of 'adb shell stop iorapd' to avoid + // printing this warning. + if (DEBUG) { + Log.v(TAG, "Failed to connect to iorapd, is it down? Delay for " + sleepTime); + } + + // Use a handler instead of Thread#sleep to avoid backing up the binder thread + // when this is called from the death recipient callback. + mHandler.sendMessageDelayed( + mHandler.obtainMessage(BinderConnectionHandler.MESSAGE_BINDER_CONNECT), + sleepTime); + + return false; + + // Log.e(TAG, "Can't connect to iorapd - giving up after " + attempts + " attempts"); + } + + private boolean connectToRemoteAndConfigure() { + synchronized (mLock) { + // Synchronize against any concurrent calls to this via the DeathRecipient. + return connectToRemoteAndConfigureLocked(); + } + } + + private boolean connectToRemoteAndConfigureLocked() { + if (!isIorapEnabled()) { + if (DEBUG) { + Log.v(TAG, "connectToRemoteAndConfigure - iorapd is disabled, skip rest of work"); + } + // When we see that iorapd is disabled (when system server comes up), + // it stays disabled permanently until the next system server reset. + + // TODO: consider listening to property changes as a callback, then we can + // be more dynamic about handling enable/disable. + return true; + } + + // Connect to the native binder service. + mIorapRemote = provideIorapRemote(); + if (mIorapRemote == null) { + if (DEBUG) { + Log.e(TAG, "connectToRemoteAndConfigure - null iorap remote. check for Log.wtf?"); + } + return false; + } + 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() { + if (mRegisteredListeners) { + // Listeners are registered only once (idempotent operation). + // + // Today listeners are tolerant of the remote side going away + // by handling remote errors. + // + // We could try to 'unregister' the listener when we get a binder disconnect, + // but we'd still have to handle the case of encountering synchronous errors so + // it really wouldn't be a win (other than having less log spew). + return; + } + + // Listen to App Launch Sequence events from ActivityTaskManager, + // and forward them to the native binder service. + ActivityMetricsLaunchObserverRegistry launchObserverRegistry = + provideLaunchObserverRegistry(); + launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver); + launchObserverRegistry.registerLaunchObserver(mEventSequenceValidator); + + BackgroundDexOptService.getService().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. + private @AppLaunchEvent.SequenceId long mSequenceId = -1; + + // All callbacks occur on the same background thread. Don't synchronize explicitly. + + @Override + 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, %d)", + mSequenceId, intent, timestampNs)); + } + + invokeRemote(mIorapRemote, + (IIorap remote) -> + remote.onAppLaunchEvent(RequestId.nextValueForSequence(), + new AppLaunchEvent.IntentStarted(mSequenceId, intent, timestampNs)) + ); + } + + @Override + public void onIntentFailed() { + if (DEBUG) { + Log.v(TAG, String.format("AppLaunchObserver#onIntentFailed(%d)", mSequenceId)); + } + + invokeRemote(mIorapRemote, + (IIorap remote) -> + remote.onAppLaunchEvent(RequestId.nextValueForSequence(), + new AppLaunchEvent.IntentFailed(mSequenceId)) + ); + } + + @Override + public void onActivityLaunched(@NonNull @ActivityRecordProto byte[] activity, + @Temperature int temperature) { + if (DEBUG) { + Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunched(%d, %s, %d)", + mSequenceId, activity, temperature)); + } + + invokeRemote(mIorapRemote, + (IIorap remote) -> + remote.onAppLaunchEvent(RequestId.nextValueForSequence(), + new AppLaunchEvent.ActivityLaunched(mSequenceId, activity, temperature)) + ); + } + + @Override + public void onActivityLaunchCancelled(@Nullable @ActivityRecordProto byte[] activity) { + if (DEBUG) { + Log.v(TAG, String.format("AppLaunchObserver#onActivityLaunchCancelled(%d, %s)", + mSequenceId, activity)); + } + + invokeRemote(mIorapRemote, + (IIorap remote) -> + remote.onAppLaunchEvent(RequestId.nextValueForSequence(), + new AppLaunchEvent.ActivityLaunchCancelled(mSequenceId, + activity))); + } + + @Override + public void onActivityLaunchFinished(@NonNull @ActivityRecordProto byte[] activity, + long timestampNs) { + if (DEBUG) { + 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, + (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.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()); + + mAbortIdleCompilation.set(false); + // PackageManagerService starts before IORap service. + PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package"); + List<String> pkgs = pm.getAllPackages(); + runIdleCompilationAsync(params, pkgs); + return true; + } + + private void runIdleCompilationAsync(final JobParameters params, final List<String> pkgs) { + new Thread("IORap_IdleCompilation") { + @Override + public void run() { + for (int i = 0; i < pkgs.size(); i++) { + String pkg = pkgs.get(i); + if (mAbortIdleCompilation.get()) { + Log.i(TAG, "The idle compilation is aborted"); + return; + } + + // 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); + } + + Log.i(TAG, String.format("IORap compile package: %s, [%d/%d]", + pkg, i + 1, pkgs.size())); + boolean shouldUpdateVersions = (i == 0); + if (!invokeRemote(mIorapRemote, (IIorap remote) -> + remote.onJobScheduledEvent(request, + JobScheduledEvent.createIdleMaintenance( + JobScheduledEvent.TYPE_START_JOB, + params, + pkg, + shouldUpdateVersions)))) { + synchronized (mLock) { + mRunningJobs.remove(request); // Avoid memory leaks. + } + } + + // Wait until the job is complete and removed from the running jobs. + retryWithTimeout(TIMEOUT, () -> { + synchronized (mLock) { + return !mRunningJobs.containsKey(request); + } + }); + } + + // Finish the job after all packages are compiled. + if (mProxy != null) { + mProxy.jobFinished(params, /*reschedule*/false); + } + } + }.start(); + } + + /** Retry until timeout. */ + private boolean retryWithTimeout(final Duration timeout, BooleanSupplier supplier) { + long totalSleepTimeMs = 0L; + long sleepIntervalMs = 10L; + while (true) { + if (supplier.getAsBoolean()) { + return true; + } + try { + TimeUnit.MILLISECONDS.sleep(sleepIntervalMs); + } catch (InterruptedException e) { + Log.e(TAG, e.getMessage()); + return false; + } + + totalSleepTimeMs += sleepIntervalMs; + if (totalSleepTimeMs > timeout.toMillis()) { + return false; + } + } + } + + // 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() + ")"); + mAbortIdleCompilation.set(true); + + // 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()); + } + + 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 { + if (DEBUG) { + Log.v(TAG, + String.format("RemoteTaskListener#onProgress(%s, %s)", requestId, result)); + } + + // TODO: implement rest. + } + + @Override + public void onComplete(RequestId requestId, TaskResult result) throws RemoteException { + if (DEBUG) { + Log.v(TAG, + 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 { + // TODO: run(RequestId) ? + void run(IIorap iorap) throws RemoteException; + } + + // 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(iorap); + return true; + } catch (RemoteException e) { + // This could be a logic error (remote side returning error), which we need to fix. + // + // This could also be a DeadObjectException in which case its probably just iorapd + // being manually restarted. + // + // Don't make any assumption, since DeadObjectException could also mean iorapd crashed + // unexpectedly. + // + // DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath. + handleRemoteError(e); + return false; + } + } + + private static void handleRemoteError(Throwable t) { + if (WTF_CRASH) { + // In development modes, we just want to crash. + throw new AssertionError("unexpected remote error", t); + } else { + // Log to wtf which gets sent to dropbox, and in system_server this does not crash. + 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..b91dd71fd28c --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java @@ -0,0 +1,174 @@ +/* + * 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; + + public final String packageName; + + public final boolean shouldUpdateVersions; + + /** 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, String packageName, boolean shouldUpdateVersions) { + return new JobScheduledEvent( + type, jobParams.getJobId(), SORT_IDLE_MAINTENANCE, packageName, shouldUpdateVersions); + } + + private JobScheduledEvent(@Type int type, + int jobId, + @Sort int sort, + String packageName, + boolean shouldUpdateVersions) { + this.type = type; + this.jobId = jobId; + this.sort = sort; + this.packageName = packageName; + this.shouldUpdateVersions = shouldUpdateVersions; + + 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 && + packageName.equals(other.packageName) && + shouldUpdateVersions == other.shouldUpdateVersions; + } + + @Override + public String toString() { + return String.format( + "{type: %d, jobId: %d, sort: %d, packageName: %s, shouldUpdateVersions %b}", + type, jobId, sort, packageName, shouldUpdateVersions); + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + out.writeInt(jobId); + out.writeInt(sort); + out.writeString(packageName); + out.writeBoolean(shouldUpdateVersions); + + // 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(); + this.packageName = in.readString(); + this.shouldUpdateVersions = in.readBoolean(); + + 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/PackageEvent.java b/startop/iorap/src/com/google/android/startop/iorap/PackageEvent.java new file mode 100644 index 000000000000..aa4eea716363 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/PackageEvent.java @@ -0,0 +1,131 @@ +/* + * 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.annotation.NonNull; +import android.os.Parcelable; +import android.os.Parcel; +import android.net.Uri; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Forward package manager events to iorapd. <br /><br /> + * + * Knowing when packages are modified by the system are a useful tidbit to help with performance: + * for example when a package is replaced, it could be a hint used to invalidate any collected + * io profiles used for prefetching or pinning. + * + * @hide + */ +public class PackageEvent implements Parcelable { + + /** @see android.content.Intent#ACTION_PACKAGE_REPLACED */ + public static final int TYPE_REPLACED = 0; + private static final int TYPE_MAX = 0; + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_REPLACED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + @Type public final int type; + + /** The path that a package is installed in, for example {@code /data/app/.../base.apk}. */ + public final Uri packageUri; + /** The name of the package, for example {@code com.android.calculator}. */ + public final String packageName; + + @NonNull + public static PackageEvent createReplaced(Uri packageUri, String packageName) { + return new PackageEvent(TYPE_REPLACED, packageUri, packageName); + } + + private PackageEvent(@Type int type, Uri packageUri, String packageName) { + this.type = type; + this.packageUri = packageUri; + this.packageName = packageName; + + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + CheckHelpers.checkTypeInRange(type, TYPE_MAX); + Objects.requireNonNull(packageUri, "packageUri"); + Objects.requireNonNull(packageName, "packageName"); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof PackageEvent) { + return equals((PackageEvent) other); + } + return false; + } + + private boolean equals(PackageEvent other) { + return type == other.type && + Objects.equals(packageUri, other.packageUri) && + Objects.equals(packageName, other.packageName); + } + + @Override + public String toString() { + return String.format("{packageUri: %s, packageName: %s}", packageUri, packageName); + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + packageUri.writeToParcel(out, flags); + out.writeString(packageName); + } + + private PackageEvent(Parcel in) { + this.type = in.readInt(); + this.packageUri = Uri.CREATOR.createFromParcel(in); + this.packageName = in.readString(); + + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<PackageEvent> CREATOR + = new Parcelable.Creator<PackageEvent>() { + public PackageEvent createFromParcel(Parcel in) { + return new PackageEvent(in); + } + + public PackageEvent[] newArray(int size) { + return new PackageEvent[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 new file mode 100644 index 000000000000..503e1c633581 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java @@ -0,0 +1,125 @@ +/* + * 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.os.Parcelable; +import android.os.Parcel; + +import android.annotation.NonNull; + +/** + * Uniquely identify an {@link com.google.android.startop.iorap.IIorap} method invocation, + * used for asynchronous callbacks by the server. <br /><br /> + * + * As all system server binder calls must be {@code oneway}, this means all invocations + * into {@link com.google.android.startop.iorap.IIorap} are non-blocking. The request ID + * exists to associate all calls with their respective callbacks in + * {@link com.google.android.startop.iorap.ITaskListener}. + * + * @see com.google.android.startop.iorap.IIorap + * + * @hide + */ +public class RequestId implements Parcelable { + + public final long requestId; + + private static Object mLock = new Object(); + private static long mNextRequestId = 0; + + /** + * Create a monotonically increasing request ID.<br /><br /> + * + * It is invalid to re-use the same request ID for multiple method calls on + * {@link com.google.android.startop.iorap.IIorap}; a new request ID must be created + * each time. + */ + @NonNull public static RequestId nextValueForSequence() { + long currentRequestId; + synchronized (mLock) { + currentRequestId = mNextRequestId; + ++mNextRequestId; + } + return new RequestId(currentRequestId); + } + + private RequestId(long requestId) { + this.requestId = requestId; + + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + if (requestId < 0) { + throw new IllegalArgumentException("request id must be non-negative"); + } + } + + @Override + public String toString() { + return String.format("{requestId: %d}", requestId); + } + + @Override + public int hashCode() { + return Long.hashCode(requestId); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof RequestId) { + return equals((RequestId) other); + } + return false; + } + + private boolean equals(RequestId other) { + return requestId == other.requestId; + } + + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(requestId); + } + + private RequestId(Parcel in) { + requestId = in.readLong(); + + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<RequestId> CREATOR + = new Parcelable.Creator<RequestId>() { + public RequestId createFromParcel(Parcel in) { + return new RequestId(in); + } + + public RequestId[] newArray(int size) { + return new RequestId[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/SystemServiceEvent.java b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceEvent.java new file mode 100644 index 000000000000..75d47f9e3d17 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceEvent.java @@ -0,0 +1,109 @@ +/* + * 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.os.Parcelable; +import android.os.Parcel; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Forward system service events to iorapd. + * + * @see com.android.server.SystemService + * + * @hide + */ +public class SystemServiceEvent implements Parcelable { + + /** @see com.android.server.SystemService#onBootPhase */ + public static final int TYPE_BOOT_PHASE = 0; + /** @see com.android.server.SystemService#onStart */ + public static final int TYPE_START = 1; + private static final int TYPE_MAX = TYPE_START; + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_BOOT_PHASE, + TYPE_START, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + @Type public final int type; + + // TODO: do we want to pass the exact build phase enum? + + public SystemServiceEvent(@Type int type) { + this.type = type; + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + CheckHelpers.checkTypeInRange(type, TYPE_MAX); + } + + @Override + public String toString() { + return String.format("{type: %d}", type); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof SystemServiceEvent) { + return equals((SystemServiceEvent) other); + } + return false; + } + + private boolean equals(SystemServiceEvent other) { + return type == other.type; + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + } + + private SystemServiceEvent(Parcel in) { + this.type = in.readInt(); + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<SystemServiceEvent> CREATOR + = new Parcelable.Creator<SystemServiceEvent>() { + public SystemServiceEvent createFromParcel(Parcel in) { + return new SystemServiceEvent(in); + } + + public SystemServiceEvent[] newArray(int size) { + return new SystemServiceEvent[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/SystemServiceUserEvent.java b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceUserEvent.java new file mode 100644 index 000000000000..2e7bafe7bdbf --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/SystemServiceUserEvent.java @@ -0,0 +1,127 @@ +/* + * 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.os.Parcelable; +import android.os.Parcel; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Forward user events to iorapd.<br /><br /> + * + * Knowledge of the logged-in user is reserved to be used to set-up appropriate policies + * by iorapd (e.g. to handle user default pinned applications changing). + * + * @see com.android.server.SystemService + * + * @hide + */ +public class SystemServiceUserEvent implements Parcelable { + + /** @see com.android.server.SystemService#onUserStarting */ + public static final int TYPE_START_USER = 0; + /** @see com.android.server.SystemService#onUserUnlocking */ + public static final int TYPE_UNLOCK_USER = 1; + /** @see com.android.server.SystemService#onUserSwitching*/ + public static final int TYPE_SWITCH_USER = 2; + /** @see com.android.server.SystemService#onUserStopping */ + public static final int TYPE_STOP_USER = 3; + /** @see com.android.server.SystemService#onUserStopped */ + public static final int TYPE_CLEANUP_USER = 4; + private static final int TYPE_MAX = TYPE_CLEANUP_USER; + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_START_USER, + TYPE_UNLOCK_USER, + TYPE_SWITCH_USER, + TYPE_STOP_USER, + TYPE_CLEANUP_USER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + @Type public final int type; + public final int userHandle; + + public SystemServiceUserEvent(@Type int type, int userHandle) { + this.type = type; + this.userHandle = userHandle; + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + CheckHelpers.checkTypeInRange(type, TYPE_MAX); + if (userHandle < 0) { + throw new IllegalArgumentException("userHandle must be non-negative"); + } + } + + @Override + public String toString() { + return String.format("{type: %d, userHandle: %d}", type, userHandle); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof SystemServiceUserEvent) { + return equals((SystemServiceUserEvent) other); + } + return false; + } + + private boolean equals(SystemServiceUserEvent other) { + return type == other.type && + userHandle == other.userHandle; + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + out.writeInt(userHandle); + } + + private SystemServiceUserEvent(Parcel in) { + this.type = in.readInt(); + this.userHandle = in.readInt(); + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<SystemServiceUserEvent> CREATOR + = new Parcelable.Creator<SystemServiceUserEvent>() { + public SystemServiceUserEvent createFromParcel(Parcel in) { + return new SystemServiceUserEvent(in); + } + + public SystemServiceUserEvent[] newArray(int size) { + return new SystemServiceUserEvent[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/TaskResult.java b/startop/iorap/src/com/google/android/startop/iorap/TaskResult.java new file mode 100644 index 000000000000..b5fd6d8d1c45 --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/TaskResult.java @@ -0,0 +1,130 @@ +/* + * 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.os.Parcelable; +import android.os.Parcel; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Result data accompanying a request for {@link com.google.android.startop.iorap.ITaskListener} + * callbacks.<br /><br /> + * + * Following {@link com.google.android.startop.iorap.IIorap} method invocation, + * iorapd will issue in-order callbacks for that corresponding {@link RequestId}.<br /><br /> + * + * State transitions are as follows: <br /><br /> + * + * <pre> + * ┌─────────────────────────────┐ + * │ ▼ + * ┌───────┐ ┌─────────┐ ╔═══════════╗ + * ──▶ │ BEGAN │ ──▶ │ ONGOING │ ──▶ ║ COMPLETED ║ + * └───────┘ └─────────┘ ╚═══════════╝ + * │ │ + * │ │ + * ▼ │ + * ╔═══════╗ │ + * ──▶ ║ ERROR ║ ◀─────┘ + * ╚═══════╝ + * + * </pre> <!-- system/iorap/docs/binder/TaskResult.dot --> + * + * @hide + */ +public class TaskResult implements Parcelable { + + public static final int STATE_BEGAN = 0; + public static final int STATE_ONGOING = 1; + public static final int STATE_COMPLETED = 2; + public static final int STATE_ERROR = 3; + private static final int STATE_MAX = STATE_ERROR; + + /** @hide */ + @IntDef(flag = true, prefix = { "STATE_" }, value = { + STATE_BEGAN, + STATE_ONGOING, + STATE_COMPLETED, + STATE_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface State {} + + @State public final int state; + + @Override + public String toString() { + return String.format("{state: %d}", state); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof TaskResult) { + return equals((TaskResult) other); + } + return false; + } + + private boolean equals(TaskResult other) { + return state == other.state; + } + + public TaskResult(@State int state) { + this.state = state; + + checkConstructorArguments(); + } + + private void checkConstructorArguments() { + CheckHelpers.checkStateInRange(state, STATE_MAX); + } + + //<editor-fold desc="Binder boilerplate"> + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(state); + } + + private TaskResult(Parcel in) { + state = in.readInt(); + + checkConstructorArguments(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<TaskResult> CREATOR + = new Parcelable.Creator<TaskResult>() { + public TaskResult createFromParcel(Parcel in) { + return new TaskResult(in); + } + + public TaskResult[] newArray(int size) { + return new TaskResult[size]; + } + }; + //</editor-fold> +} diff --git a/startop/iorap/stress/Android.bp b/startop/iorap/stress/Android.bp new file mode 100644 index 000000000000..6e8725d091fb --- /dev/null +++ b/startop/iorap/stress/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. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_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/Android.bp b/startop/iorap/tests/Android.bp new file mode 100644 index 000000000000..ad3d001ad7d4 --- /dev/null +++ b/startop/iorap/tests/Android.bp @@ -0,0 +1,72 @@ +// 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. + +// TODO: once b/80095087 is fixed, rewrite this back to android_test +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "libiorap-java-test-lib", + srcs: ["src/**/*.kt"], + static_libs: [ + // Non-test dependencies + // library under test + "services.startop.iorap", + // need the system_server code to be on the classpath, + "services.core", + // Test Dependencies + // test android dependencies + "platform-test-annotations", + "androidx.test.rules", + // test framework dependencies + "mockito-target-inline-minus-junit4", + // "mockito-target-minus-junit4", + // Mockito also requires JNI (see Android.mk) + // and android:debuggable=true (see AndroidManifest.xml) + "truth-prebuilt", + ], + // sdk_version: "current", + // certificate: "platform", + libs: [ + "android.test.base", + "android.test.runner", + ], + // test_suites: ["device-tests"], +} + +android_test { + name: "libiorap-java-tests", + dxflags: ["--multi-dex"], + test_suites: ["device-tests"], + static_libs: ["libiorap-java-test-lib"], + compile_multilib: "both", + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + // Use private APIs + certificate: "platform", + platform_apis: true, +} diff --git a/startop/iorap/tests/AndroidManifest.xml b/startop/iorap/tests/AndroidManifest.xml new file mode 100644 index 000000000000..b967e7207a3f --- /dev/null +++ b/startop/iorap/tests/AndroidManifest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<!--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" + android:versionCode="1" + android:versionName="1.0" > + + <!--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/tests/AndroidTest.xml b/startop/iorap/tests/AndroidTest.xml new file mode 100644 index 000000000000..6102c44e61bf --- /dev/null +++ b/startop/iorap/tests/AndroidTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<configuration description="Runs libiorap-java-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="libiorap-java-tests.apk" /> + </target_preparer> + + <!-- + Our IIorapIntegrationTest.kt requires setlinux to be disabled: + it connects to the iorapd binder service but this requires selinux permissions: + + avc: denied { find } for service=iorapd pid=2738 uid=10050 + scontext=u:r:platform_app:s0:c512,c768 tcontext=u:object_r:iorapd_service:s0 + tclass=service_manager permissive=0 + --> + <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="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="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 new file mode 100644 index 000000000000..18c249136d05 --- /dev/null +++ b/startop/iorap/tests/src/com/google/android/startop/iorap/IIorapIntegrationTest.kt @@ -0,0 +1,137 @@ +/* + * 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.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 +import org.mockito.Mockito.eq +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.spy +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 + */ + private val iorapService: IIorap by lazy { + // TODO: connect to 'iorapd.stub' which doesn't actually do any work other than reply. + IIorap.Stub.asInterface(ServiceManager.getServiceOrThrow("iorapd")) + + // Use 'adb shell setenforce 0' otherwise this whole test fails, + // because the servicemanager is not allowed to hand out the binder token for iorapd. + + // TODO: implement the selinux policies for iorapd. + } + + // A dummy binder stub implementation is required to use with mockito#spy. + // Mockito overrides the methods at runtime and tracks how methods were invoked. + open class DummyTaskListener : ITaskListener.Stub() { + // Note: make parameters nullable to avoid the kotlin IllegalStateExceptions + // from using the mockito matchers (eq, argThat, etc). + override fun onProgress(requestId: RequestId?, result: TaskResult?) { + } + + override fun onComplete(requestId: RequestId?, result: TaskResult?) { + } + } + + 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 + // interface, so we don't need any additional blocking here before sending later calls. + + // Every new method call should have a unique request id. + val requestId = RequestId.nextValueForSequence()!! + + // Apply the specific function under test. + func(requestId) + + // Typical mockito behavior is to allow any-order callbacks, but we want to test order. + val inOrder = inOrder(taskListener) + + // The "stub" behavior of iorapd is that every request immediately gets a response of + // BEGAN,ONGOING,COMPLETED + inOrder.verify(taskListener, timeout(100)) + .onProgress(eq(requestId), argThat { it!!.state == TaskResult.STATE_BEGAN }) + inOrder.verify(taskListener, timeout(100)) + .onProgress(eq(requestId), argThat { it!!.state == TaskResult.STATE_ONGOING }) + inOrder.verify(taskListener, timeout(100)) + .onComplete(eq(requestId), argThat { it!!.state == TaskResult.STATE_COMPLETED }) + inOrder.verifyNoMoreInteractions() + } finally { + // iorapService.setTaskListener(null) + // FIXME: null is broken, C++ side sees a non-null object. + } + } + + @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, + PackageEvent.createReplaced( + Uri.parse("https://www.google.com"), "com.fake.package")) + } + */ + } + + @Test + fun testOnAppIntentEvent() { + testAnyMethod { requestId: RequestId -> + iorapService.onAppIntentEvent(requestId, AppIntentEvent.createDefaultIntentChanged( + ActivityInfo("dont care", "dont care"), + ActivityInfo("dont care 2", "dont care 2"))) + } + } + + @Test + fun testOnAppLaunchEvent() { + testAnyMethod { requestId : RequestId -> + iorapService.onAppLaunchEvent(requestId, AppLaunchEvent.IntentFailed(/*sequenceId*/123)) + } + } + + @Test + fun testOnSystemServiceEvent() { + testAnyMethod { requestId: RequestId -> + iorapService.onSystemServiceEvent(requestId, + SystemServiceEvent(SystemServiceEvent.TYPE_START)) + } + } + + @Test + fun testOnSystemServiceUserEvent() { + testAnyMethod { requestId: RequestId -> + iorapService.onSystemServiceUserEvent(requestId, + SystemServiceUserEvent(SystemServiceUserEvent.TYPE_START_USER, 0)) + } + } +} diff --git a/startop/iorap/tests/src/com/google/android/startop/iorap/ParcelablesTest.kt b/startop/iorap/tests/src/com/google/android/startop/iorap/ParcelablesTest.kt new file mode 100644 index 000000000000..150577a21f5a --- /dev/null +++ b/startop/iorap/tests/src/com/google/android/startop/iorap/ParcelablesTest.kt @@ -0,0 +1,140 @@ +/* + * 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.net.Uri +import android.os.Parcel +import android.os.Parcelable +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith +import com.google.common.truth.Truth.assertThat +import org.junit.runners.Parameterized + +/** + * Basic unit tests to ensure that all of the [Parcelable]s in [com.google.android.startop.iorap] + * have a valid-conforming interface implementation. + */ +@SmallTest +@RunWith(Parameterized::class) +class ParcelablesTest<T : Parcelable>(private val inputData: InputData<T>) { + companion object { + private val initialRequestId = RequestId.nextValueForSequence()!! + + @JvmStatic + @Parameterized.Parameters + fun data() = listOf( + InputData( + newActivityInfo(), + newActivityInfo(), + ActivityInfo("some package", "some other activity")), + InputData( + ActivityHintEvent(ActivityHintEvent.TYPE_COMPLETED, newActivityInfo()), + ActivityHintEvent(ActivityHintEvent.TYPE_COMPLETED, newActivityInfo()), + ActivityHintEvent(ActivityHintEvent.TYPE_POST_COMPLETED, + newActivityInfo())), + InputData( + AppIntentEvent.createDefaultIntentChanged(newActivityInfo(), + newActivityInfoOther()), + AppIntentEvent.createDefaultIntentChanged(newActivityInfo(), + newActivityInfoOther()), + AppIntentEvent.createDefaultIntentChanged(newActivityInfoOther(), + newActivityInfo())), + InputData( + PackageEvent.createReplaced(newUri(), "some package"), + PackageEvent.createReplaced(newUri(), "some package"), + PackageEvent.createReplaced(newUri(), "some other package") + ), + InputData(initialRequestId, cloneRequestId(initialRequestId), + RequestId.nextValueForSequence()), + InputData( + SystemServiceEvent(SystemServiceEvent.TYPE_BOOT_PHASE), + SystemServiceEvent(SystemServiceEvent.TYPE_BOOT_PHASE), + SystemServiceEvent(SystemServiceEvent.TYPE_START)), + InputData( + SystemServiceUserEvent(SystemServiceUserEvent.TYPE_START_USER, 12345), + SystemServiceUserEvent(SystemServiceUserEvent.TYPE_START_USER, 12345), + SystemServiceUserEvent(SystemServiceUserEvent.TYPE_CLEANUP_USER, 12345)), + InputData( + TaskResult(TaskResult.STATE_COMPLETED), + TaskResult(TaskResult.STATE_COMPLETED), + TaskResult(TaskResult.STATE_ONGOING)) + ) + + private fun newActivityInfo(): ActivityInfo { + return ActivityInfo("some package", "some activity") + } + + private fun newActivityInfoOther(): ActivityInfo { + return ActivityInfo("some package 2", "some activity 2") + } + + private fun newUri(): Uri { + return Uri.parse("https://www.google.com") + } + + private fun cloneRequestId(requestId: RequestId): RequestId { + val constructor = requestId::class.java.declaredConstructors[0] + constructor.isAccessible = true + return constructor.newInstance(requestId.requestId) as RequestId + } + } + + /** + * Test for [Object.equals] implementation. + */ + @Test + fun testEquality() { + assertThat(inputData.valid).isEqualTo(inputData.valid) + assertThat(inputData.valid).isEqualTo(inputData.validCopy) + assertThat(inputData.valid).isNotEqualTo(inputData.validOther) + } + + /** + * Test for [Parcelable] implementation. + */ + @Test + fun testParcelRoundTrip() { + // calling writeToParcel and then T::CREATOR.createFromParcel would return the same data. + val assertParcels = { it: T, data: InputData<T> -> + val parcel = Parcel.obtain() + it.writeToParcel(parcel, 0) + parcel.setDataPosition(0) // future reads will see all previous writes. + assertThat(it).isEqualTo(data.createFromParcel(parcel)) + parcel.recycle() + } + + assertParcels(inputData.valid, inputData) + assertParcels(inputData.validCopy, inputData) + assertParcels(inputData.validOther, inputData) + } + + data class InputData<T : Parcelable>(val valid: T, val validCopy: T, val validOther: T) { + val kls = valid.javaClass + init { + assertThat(valid).isNotSameInstanceAs(validCopy) + // Don't use isInstanceOf because of phantom warnings in intellij about Class! + assertThat(validCopy.javaClass).isEqualTo(valid.javaClass) + assertThat(validOther.javaClass).isEqualTo(valid.javaClass) + } + + fun createFromParcel(parcel: Parcel): T { + val field = kls.getDeclaredField("CREATOR") + val creator = field.get(null) as Parcelable.Creator<T> + + return creator.createFromParcel(parcel) + } + } +} |