diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2020-01-29 23:44:37 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-01-29 23:44:37 +0000 |
commit | 9c01b1398ba0ac9fee5566b10514222821f0f700 (patch) | |
tree | cb8454222752210d23bc802034f2a64f0393ef70 /startop | |
parent | 53b77b57f4676a1ccd4060ebf007f99b6fdf09fa (diff) | |
parent | 188e6373453b75728d8482aac580e34b4d09bffd (diff) | |
download | base-9c01b1398ba0ac9fee5566b10514222821f0f700.tar.gz |
Merge "startop: Add a function test for iorapd."
Diffstat (limited to 'startop')
4 files changed, 509 insertions, 0 deletions
diff --git a/startop/iorap/functional_tests/Android.bp b/startop/iorap/functional_tests/Android.bp new file mode 100644 index 000000000000..ce9dc325c76d --- /dev/null +++ b/startop/iorap/functional_tests/Android.bp @@ -0,0 +1,41 @@ +// 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"], + 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..41109b43ab82 --- /dev/null +++ b/startop/iorap/functional_tests/AndroidTest.xml @@ -0,0 +1,53 @@ +<?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" /> + + <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/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..bd8a45c2ca00 --- /dev/null +++ b/startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java @@ -0,0 +1,377 @@ +/* + * 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.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.List; + + +/** + * Test for the work flow of iorap. + * + * <p> This test tests the function of iorap from perfetto collection -> compilation -> + * prefetching. + * </p> + */ +@RunWith(AndroidJUnit4.class) +public class IorapWorkFlowTest { + + private static final String TAG = "IorapWorkFlowTest"; + + private static final String TEST_PACKAGE_NAME = "com.android.settings"; + private static final String TEST_ACTIVITY_NAME = "com.android.settings.Settings"; + + private static final String DB_PATH = "/data/misc/iorapd/sqlite.db"; + private static final Duration TIMEOUT = Duration.ofSeconds(20L); + + private static final String READAHEAD_INDICATOR = + "Description = /data/misc/iorapd/com.android.settings/none/com.android.settings.Settings/compiled_traces/compiled_trace.pb"; + + private UiDevice mDevice; + + @Before + public void startMainActivityFromHomeScreen() 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()); + } + + @Test + public void testApp() throws Exception { + assertThat(mDevice, notNullValue()); + + // Perfetto trace collection phase. + assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/1)); + assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/2)); + assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/3)); + assertTrue(checkPerfettoTracesExistence(TIMEOUT, 3)); + + // Trigger maintenance service for compilation. + assertTrue(compile(TIMEOUT)); + + // Check if prefetching works. + assertTrue(waitForPrefetchingFromLogcat(/*expectPerfettoTraceCount=*/3)); + } + + /** + * Starts the testing app to collect the perfetto trace. + * + * @param expectPerfettoTraceCount is the expected count of perfetto traces. + */ + private boolean startAppForPerfettoTrace(long expectPerfettoTraceCount) + throws Exception { + // Close the specified app if it's open + closeApp(); + // Launch the specified app + startApp(); + // Wait for the app to appear + mDevice.wait(Until.hasObject(By.pkg(TEST_PACKAGE_NAME).depth(0)), TIMEOUT.getSeconds()); + + String sql = "SELECT COUNT(*) FROM activities " + + "JOIN app_launch_histories ON activities.id = app_launch_histories.activity_id " + + "JOIN raw_traces ON raw_traces.history_id = app_launch_histories.id " + + "WHERE activities.name = ?"; + return checkAndWaitEntriesNum(sql, new String[]{TEST_ACTIVITY_NAME}, expectPerfettoTraceCount, + TIMEOUT); + } + + // Invokes the maintenance to compile the perfetto traces to compiled trace. + private boolean compile(Duration timeout) throws Exception { + // The job id (283673059) is defined in class IorapForwardingService. + executeShellCommand("cmd jobscheduler run -f android 283673059"); + + // Wait for the compilation. + String sql = "SELECT COUNT(*) FROM activities JOIN prefetch_files ON " + + "activities.id = prefetch_files.activity_id " + + "WHERE activities.name = ?"; + boolean result = checkAndWaitEntriesNum(sql, new String[]{TEST_ACTIVITY_NAME}, /*count=*/1, + timeout); + if (!result) { + return false; + } + + return retryWithTimeout(timeout, () -> { + try { + String compiledTrace = getCompiledTraceFilePath(); + File compiledTraceLocal = copyFileToLocal(compiledTrace, "compiled_trace.tmp"); + return compiledTraceLocal.exists(); + } catch (Exception e) { + Log.i(TAG, e.getMessage()); + return false; + } + }); + } + + /** + * Check if all the perfetto traces in the db exist. + */ + private boolean checkPerfettoTracesExistence(Duration timeout, int expectPerfettoTraceCount) + throws Exception { + return retryWithTimeout(timeout, () -> { + try { + File dbFile = getIorapDb(); + List<String> traces = getPerfettoTracePaths(dbFile); + assertEquals(traces.size(), expectPerfettoTraceCount); + + int count = 0; + for (String trace : traces) { + File tmp = copyFileToLocal(trace, "perfetto_trace.tmp" + count); + ++count; + Log.i(TAG, "Check perfetto trace: " + trace); + if (!tmp.exists()) { + Log.i(TAG, "Perfetto trace does not exist: " + trace); + return false; + } + } + return true; + } catch (Exception e) { + Log.i(TAG, e.getMessage()); + return false; + } + }); + } + + /** + * Gets the perfetto traces file path from the db. + */ + private List<String> getPerfettoTracePaths(File dbFile) throws Exception { + String sql = "SELECT raw_traces.file_path FROM activities " + + "JOIN app_launch_histories ON activities.id = app_launch_histories.activity_id " + + "JOIN raw_traces ON raw_traces.history_id = app_launch_histories.id " + + "WHERE activities.name = ?"; + + List<String> perfettoTraces = new ArrayList<>(); + try (SQLiteDatabase db = SQLiteDatabase + .openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY)) { + Cursor cursor = db.rawQuery(sql, new String[]{TEST_ACTIVITY_NAME}); + while (cursor.moveToNext()) { + perfettoTraces.add(cursor.getString(0)); + } + } + return perfettoTraces; + } + + private String getCompiledTraceFilePath() throws Exception { + File dbFile = getIorapDb(); + try (SQLiteDatabase db = SQLiteDatabase + .openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY)) { + String sql = "SELECT prefetch_files.file_path FROM activities JOIN prefetch_files ON " + + "activities.id = prefetch_files.activity_id " + + "WHERE activities.name = ?"; + return DatabaseUtils.stringForQuery(db, sql, new String[]{TEST_ACTIVITY_NAME}); + } + } + + /** + * Checks the number of entries in the database table. + * + * <p> Keep checking until the timeout. + */ + private boolean checkAndWaitEntriesNum(String sql, String[] selectionArgs, long count, + Duration timeout) + throws Exception { + return retryWithTimeout(timeout, () -> { + try { + File db = getIorapDb(); + long curCount = getEntriesNum(db, selectionArgs, sql); + Log.i(TAG, String + .format("For %s, current count is %d, expected count is :%d.", sql, curCount, + count)); + return curCount == count; + } catch (Exception e) { + Log.i(TAG, e.getMessage()); + return false; + } + }); + } + + /** + * 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(totalSleepTimeSeconds); + totalSleepTimeSeconds += sleepIntervalSeconds; + if (totalSleepTimeSeconds > timeout.getSeconds()) { + return false; + } + } + } + + /** + * Gets the number of entries in the query of sql. + */ + private long getEntriesNum(File dbFile, String[] selectionArgs, String sql) throws Exception { + try (SQLiteDatabase db = SQLiteDatabase + .openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY)) { + return DatabaseUtils.longForQuery(db, sql, selectionArgs); + } + } + + /** + * Gets the iorapd sqlite db file. + * + * <p> The test cannot access the db file directly under "/data/misc/iorapd". + * Copy it to the local directory and change the mode. + */ + private File getIorapDb() throws Exception { + File tmpDb = copyFileToLocal("/data/misc/iorapd/sqlite.db", "tmp.db"); + // Change the mode of the file to allow the access from test. + executeShellCommand("chmod 777 " + tmpDb.getPath()); + return tmpDb; + } + + /** + * Copys a file to local directory. + */ + private File copyFileToLocal(String src, String tgtFileName) throws Exception { + File localDir = getApplicationContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); + File localFile = new File(localDir, tgtFileName); + executeShellCommand(String.format("cp %s %s", src, localFile.getPath())); + return localFile; + } + + /** + * Starts the testing app. + */ + private void startApp() throws Exception { + Context context = getApplicationContext(); + final Intent intent = context.getPackageManager() + .getLaunchIntentForPackage(TEST_PACKAGE_NAME); + context.startActivity(intent); + Log.i(TAG, "Started app " + TEST_PACKAGE_NAME); + } + + /** + * 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() throws Exception { + while (true) { + String pid = executeShellCommand("pidof " + TEST_PACKAGE_NAME); + if (pid.isEmpty()) { + Log.i(TAG, "Closed app " + TEST_PACKAGE_NAME); + return; + } + executeShellCommand("kill -9 " + pid); + TimeUnit.SECONDS.sleep(1L); + } + } + + /** + * Waits for the prefetching log in the logcat. + * + * <p> When prefetching works, the perfetto traces should not be collected. </p> + */ + private boolean waitForPrefetchingFromLogcat(long expectPerfettoTraceCount) throws Exception { + if (!startAppForPerfettoTrace(expectPerfettoTraceCount)) { + return false; + } + + String log = executeShellCommand("logcat -s iorapd -d"); + + Pattern p = Pattern.compile( + ".*" + READAHEAD_INDICATOR + + ".*Total File Paths=(\\d+) \\(good: (\\d+)%\\)\n" + + ".*Total Entries=(\\d+) \\(good: (\\d+)%\\)\n" + + ".*Total Bytes=(\\d+) \\(good: (\\d+)%\\).*", + Pattern.DOTALL); + 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 > 100000 && + totalFilePathGoodRate > 0.5 && + totalEntriesGoodRate > 0.5 && + totalBytesGoodRate > 0.5; + } + + + /** + * Executes command in adb shell. + * + * <p> This should be run as root.</p> + */ + private String executeShellCommand(String cmd) throws Exception { + Log.i(TAG, "Execute: " + cmd); + return UiDevice.getInstance( + InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd); + } +} + + |