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