Loading startop/iorap/functional_tests/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ android_test { name: "iorap-functional-tests", srcs: ["src/**/*.java"], data: ["test_data/*"], static_libs: [ // Non-test dependencies // library under test Loading startop/iorap/functional_tests/AndroidTest.xml +14 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,20 @@ <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" /> Loading startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java +252 −213 Original line number Diff line number Diff line Loading @@ -37,45 +37,46 @@ 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. * </p> * <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_PACKAGE_NAME = "com.android.settings"; private static final String TEST_ACTIVITY_NAME = "com.android.settings.Settings"; 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 static final String READAHEAD_INDICATOR = "Description = /data/misc/iorapd/com.android.settings/-?\\d+/com.android.settings.Settings/compiled_traces/compiled_trace.pb"; private UiDevice mDevice; @Before public void startMainActivityFromHomeScreen() throws Exception { public void setUp() throws Exception { // Initialize UiDevice instance mDevice = UiDevice.getInstance(getInstrumentation()); Loading @@ -88,215 +89,136 @@ public class IorapWorkFlowTest { 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 testApp() throws Exception { 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(/*expectPerfettoTraceCount=*/1)); assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/2)); assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/3)); assertTrue(checkPerfettoTracesExistence(TIMEOUT, 3)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/1L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/1L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/1L)); // Trigger maintenance service for compilation. assertTrue(compile(TIMEOUT)); TimeUnit.SECONDS.sleep(5L); assertTrue(compile(packageName, activityName, /*version=*/1L)); // Check if prefetching works. assertTrue(waitForPrefetchingFromLogcat(/*expectPerfettoTraceCount=*/3)); // Run app with prefetching assertTrue(startAppWithCompiledTrace( packageName, activityName, /*version=*/1L)); } /** * 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()); @Test (timeout = 300000) public void testUpdateApp() throws Exception { assertThat(mDevice, notNullValue()); 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); } // Install test app version two, String packageName = "com.example.ioraptestapp"; String activityName = "com.example.ioraptestapp.MainActivity"; installApk(TEST_APP_VERSION_TWO_PATH); // 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; } // Perfetto trace collection phase. assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/2L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/2L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/2L)); 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; } }); } // Trigger maintenance service for compilation. TimeUnit.SECONDS.sleep(5L); assertTrue(compile(packageName, activityName, /*version=*/2L)); /** * 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; } }); } // Run app with prefetching assertTrue(startAppWithCompiledTrace( packageName, activityName, /*version=*/2L)); /** * 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 = ?"; // Update test app to version 3 installApk(TEST_APP_VERSION_THREE_PATH); 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; // Rerun app, should do pefetto tracing. assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/3L)); } 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}); 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); } /** * Checks the number of entries in the database table. * Starts the testing app to collect the perfetto trace. * * <p> Keep checking until the timeout. * @param expectPerfettoTraceCount is the expected count of perfetto traces. */ private boolean checkAndWaitEntriesNum(String sql, String[] selectionArgs, long count, Duration timeout) private boolean startAppForPerfettoTrace( String packageName, String activityName, long version) 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; } }); LogcatTimestamp timestamp = runAppOnce(packageName, activityName); return waitForPerfettoTraceSavedFromLogcat( packageName, activityName, version, timestamp); } /** * 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; } } private boolean startAppWithCompiledTrace( String packageName, String activityName, long version) throws Exception { LogcatTimestamp timestamp = runAppOnce(packageName, activityName); return waitForPrefetchingFromLogcat( packageName, activityName, version, timestamp); } /** * 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); } 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; } /** * 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; // 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)); } /** * 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; 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() 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); private void startApp(String packageName, String activityName) throws Exception { executeShellCommandViaTmpFile( String.format("am start %s/%s", packageName, activityName)); } /** Loading @@ -304,11 +226,11 @@ public class IorapWorkFlowTest { * <p> Keep trying to kill the process of the app until no process of the app package * appears.</p> */ private void closeApp() throws Exception { private void closeApp(String packageName) throws Exception { while (true) { String pid = executeShellCommand("pidof " + TEST_PACKAGE_NAME); String pid = executeShellCommand("pidof " + packageName); if (pid.isEmpty()) { Log.i(TAG, "Closed app " + TEST_PACKAGE_NAME); Log.i(TAG, "Closed app " + packageName); return; } executeShellCommand("kill -9 " + pid); Loading @@ -316,26 +238,74 @@ public class IorapWorkFlowTest { } } /** 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(long expectPerfettoTraceCount) throws Exception { if (!startAppForPerfettoTrace(expectPerfettoTraceCount)) { return false; } String log = executeShellCommand("logcat -d"); private boolean waitForPrefetchingFromLogcat( String packageName, String activityName, long version, LogcatTimestamp timestamp) throws Exception { Pattern p = Pattern.compile( ".*" + READAHEAD_INDICATOR + ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n" ".*" + 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); Matcher m = p.matcher(log); 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; Loading @@ -355,12 +325,60 @@ public class IorapWorkFlowTest { return totalFilePath > 0 && totalEntries > 0 && totalBytes > 100000 && 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. Loading @@ -372,6 +390,27 @@ public class IorapWorkFlowTest { 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.%d", 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 + "'"); } } } startop/iorap/functional_tests/test_data/iorap_test_app_v1.apk 0 → 120000 +1 −0 Original line number Diff line number Diff line ../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v1.apk No newline at end of file startop/iorap/functional_tests/test_data/iorap_test_app_v2.apk 0 → 120000 +1 −0 Original line number Diff line number Diff line ../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v2.apk No newline at end of file Loading
startop/iorap/functional_tests/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ android_test { name: "iorap-functional-tests", srcs: ["src/**/*.java"], data: ["test_data/*"], static_libs: [ // Non-test dependencies // library under test Loading
startop/iorap/functional_tests/AndroidTest.xml +14 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,20 @@ <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" /> Loading
startop/iorap/functional_tests/src/com/google/android/startop/iorap/IorapWorkFlowTest.java +252 −213 Original line number Diff line number Diff line Loading @@ -37,45 +37,46 @@ 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. * </p> * <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_PACKAGE_NAME = "com.android.settings"; private static final String TEST_ACTIVITY_NAME = "com.android.settings.Settings"; 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 static final String READAHEAD_INDICATOR = "Description = /data/misc/iorapd/com.android.settings/-?\\d+/com.android.settings.Settings/compiled_traces/compiled_trace.pb"; private UiDevice mDevice; @Before public void startMainActivityFromHomeScreen() throws Exception { public void setUp() throws Exception { // Initialize UiDevice instance mDevice = UiDevice.getInstance(getInstrumentation()); Loading @@ -88,215 +89,136 @@ public class IorapWorkFlowTest { 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 testApp() throws Exception { 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(/*expectPerfettoTraceCount=*/1)); assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/2)); assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/3)); assertTrue(checkPerfettoTracesExistence(TIMEOUT, 3)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/1L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/1L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/1L)); // Trigger maintenance service for compilation. assertTrue(compile(TIMEOUT)); TimeUnit.SECONDS.sleep(5L); assertTrue(compile(packageName, activityName, /*version=*/1L)); // Check if prefetching works. assertTrue(waitForPrefetchingFromLogcat(/*expectPerfettoTraceCount=*/3)); // Run app with prefetching assertTrue(startAppWithCompiledTrace( packageName, activityName, /*version=*/1L)); } /** * 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()); @Test (timeout = 300000) public void testUpdateApp() throws Exception { assertThat(mDevice, notNullValue()); 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); } // Install test app version two, String packageName = "com.example.ioraptestapp"; String activityName = "com.example.ioraptestapp.MainActivity"; installApk(TEST_APP_VERSION_TWO_PATH); // 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; } // Perfetto trace collection phase. assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/2L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/2L)); assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/2L)); 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; } }); } // Trigger maintenance service for compilation. TimeUnit.SECONDS.sleep(5L); assertTrue(compile(packageName, activityName, /*version=*/2L)); /** * 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; } }); } // Run app with prefetching assertTrue(startAppWithCompiledTrace( packageName, activityName, /*version=*/2L)); /** * 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 = ?"; // Update test app to version 3 installApk(TEST_APP_VERSION_THREE_PATH); 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; // Rerun app, should do pefetto tracing. assertTrue(startAppForPerfettoTrace( packageName, activityName, /*version=*/3L)); } 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}); 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); } /** * Checks the number of entries in the database table. * Starts the testing app to collect the perfetto trace. * * <p> Keep checking until the timeout. * @param expectPerfettoTraceCount is the expected count of perfetto traces. */ private boolean checkAndWaitEntriesNum(String sql, String[] selectionArgs, long count, Duration timeout) private boolean startAppForPerfettoTrace( String packageName, String activityName, long version) 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; } }); LogcatTimestamp timestamp = runAppOnce(packageName, activityName); return waitForPerfettoTraceSavedFromLogcat( packageName, activityName, version, timestamp); } /** * 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; } } private boolean startAppWithCompiledTrace( String packageName, String activityName, long version) throws Exception { LogcatTimestamp timestamp = runAppOnce(packageName, activityName); return waitForPrefetchingFromLogcat( packageName, activityName, version, timestamp); } /** * 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); } 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; } /** * 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; // 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)); } /** * 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; 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() 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); private void startApp(String packageName, String activityName) throws Exception { executeShellCommandViaTmpFile( String.format("am start %s/%s", packageName, activityName)); } /** Loading @@ -304,11 +226,11 @@ public class IorapWorkFlowTest { * <p> Keep trying to kill the process of the app until no process of the app package * appears.</p> */ private void closeApp() throws Exception { private void closeApp(String packageName) throws Exception { while (true) { String pid = executeShellCommand("pidof " + TEST_PACKAGE_NAME); String pid = executeShellCommand("pidof " + packageName); if (pid.isEmpty()) { Log.i(TAG, "Closed app " + TEST_PACKAGE_NAME); Log.i(TAG, "Closed app " + packageName); return; } executeShellCommand("kill -9 " + pid); Loading @@ -316,26 +238,74 @@ public class IorapWorkFlowTest { } } /** 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(long expectPerfettoTraceCount) throws Exception { if (!startAppForPerfettoTrace(expectPerfettoTraceCount)) { return false; } String log = executeShellCommand("logcat -d"); private boolean waitForPrefetchingFromLogcat( String packageName, String activityName, long version, LogcatTimestamp timestamp) throws Exception { Pattern p = Pattern.compile( ".*" + READAHEAD_INDICATOR + ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n" ".*" + 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); Matcher m = p.matcher(log); 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; Loading @@ -355,12 +325,60 @@ public class IorapWorkFlowTest { return totalFilePath > 0 && totalEntries > 0 && totalBytes > 100000 && 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. Loading @@ -372,6 +390,27 @@ public class IorapWorkFlowTest { 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.%d", 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 + "'"); } } }
startop/iorap/functional_tests/test_data/iorap_test_app_v1.apk 0 → 120000 +1 −0 Original line number Diff line number Diff line ../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v1.apk No newline at end of file
startop/iorap/functional_tests/test_data/iorap_test_app_v2.apk 0 → 120000 +1 −0 Original line number Diff line number Diff line ../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v2.apk No newline at end of file