Merge "iorap_functional_test: Add iorap function test." into rvc-dev am: d3e541eb03 am: 77bf81a749

Change-Id: Idd4625b1e1fe1a8d9e11d84fe42e40d9805d38e6
This commit is contained in:
TreeHugger Robot
2020-04-04 06:20:17 +00:00
committed by Automerger Merge Worker
6 changed files with 273 additions and 216 deletions

View File

@@ -15,6 +15,7 @@
android_test { android_test {
name: "iorap-functional-tests", name: "iorap-functional-tests",
srcs: ["src/**/*.java"], srcs: ["src/**/*.java"],
data: ["test_data/*"],
static_libs: [ static_libs: [
// Non-test dependencies // Non-test dependencies
// library under test // library under test

View File

@@ -45,6 +45,20 @@
<option name="run-command" value="sleep 1" /> <option name="run-command" value="sleep 1" />
</target_preparer> </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" > <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.google.android.startop.iorap.tests" /> <option name="package" value="com.google.android.startop.iorap.tests" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />

View File

@@ -37,45 +37,46 @@ import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until; import androidx.test.uiautomator.Until;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.File; 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.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.Date;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.List; import java.util.List;
import java.text.SimpleDateFormat;
/** /**
* Test for the work flow of iorap. * Test for the work flow of iorap.
* *
* <p> This test tests the function of iorap from perfetto collection -> compilation -> * <p> This test tests the function of iorap from:
* prefetching. * perfetto collection -> compilation -> prefetching -> version update -> perfetto collection.
* </p>
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class IorapWorkFlowTest { public class IorapWorkFlowTest {
private static final String TAG = "IorapWorkFlowTest"; private static final String TAG = "IorapWorkFlowTest";
private static final String TEST_PACKAGE_NAME = "com.android.settings"; private static final String TEST_APP_VERSION_ONE_PATH = "/data/misc/iorapd/iorap_test_app_v1.apk";
private static final String TEST_ACTIVITY_NAME = "com.android.settings.Settings"; 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 String DB_PATH = "/data/misc/iorapd/sqlite.db";
private static final Duration TIMEOUT = Duration.ofSeconds(300L); 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; private UiDevice mDevice;
@Before @Before
public void startMainActivityFromHomeScreen() throws Exception { public void setUp() throws Exception {
// Initialize UiDevice instance // Initialize UiDevice instance
mDevice = UiDevice.getInstance(getInstrumentation()); mDevice = UiDevice.getInstance(getInstrumentation());
@@ -88,21 +89,81 @@ public class IorapWorkFlowTest {
mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIMEOUT.getSeconds()); 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) @Test (timeout = 300000)
public void testApp() throws Exception { public void testNormalWorkFlow() throws Exception {
assertThat(mDevice, notNullValue()); 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. // Perfetto trace collection phase.
assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/1)); assertTrue(startAppForPerfettoTrace(
assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/2)); packageName, activityName, /*version=*/1L));
assertTrue(startAppForPerfettoTrace(/*expectPerfettoTraceCount=*/3)); assertTrue(startAppForPerfettoTrace(
assertTrue(checkPerfettoTracesExistence(TIMEOUT, 3)); packageName, activityName, /*version=*/1L));
assertTrue(startAppForPerfettoTrace(
packageName, activityName, /*version=*/1L));
// Trigger maintenance service for compilation. // Trigger maintenance service for compilation.
assertTrue(compile(TIMEOUT)); TimeUnit.SECONDS.sleep(5L);
assertTrue(compile(packageName, activityName, /*version=*/1L));
// Check if prefetching works. // Run app with prefetching
assertTrue(waitForPrefetchingFromLogcat(/*expectPerfettoTraceCount=*/3)); 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);
} }
/** /**
@@ -110,193 +171,54 @@ public class IorapWorkFlowTest {
* *
* @param expectPerfettoTraceCount is the expected count of perfetto traces. * @param expectPerfettoTraceCount is the expected count of perfetto traces.
*/ */
private boolean startAppForPerfettoTrace(long expectPerfettoTraceCount) private boolean startAppForPerfettoTrace(
String packageName, String activityName, long version)
throws Exception { throws Exception {
// Close the specified app if it's open LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
closeApp(); return waitForPerfettoTraceSavedFromLogcat(
// Launch the specified app packageName, activityName, version, timestamp);
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 " private boolean startAppWithCompiledTrace(
+ "JOIN app_launch_histories ON activities.id = app_launch_histories.activity_id " String packageName, String activityName, long version)
+ "JOIN raw_traces ON raw_traces.history_id = app_launch_histories.id " throws Exception {
+ "WHERE activities.name = ?"; LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
return checkAndWaitEntriesNum(sql, new String[]{TEST_ACTIVITY_NAME}, expectPerfettoTraceCount, return waitForPrefetchingFromLogcat(
TIMEOUT); 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. // Invokes the maintenance to compile the perfetto traces to compiled trace.
private boolean compile(Duration timeout) throws Exception { private boolean compile(
String packageName, String activityName, long version) throws Exception {
// The job id (283673059) is defined in class IorapForwardingService. // The job id (283673059) is defined in class IorapForwardingService.
executeShellCommand("cmd jobscheduler run -f android 283673059"); executeShellCommandViaTmpFile("cmd jobscheduler run -f android 283673059");
return waitForFileExistence(getCompiledTracePath(packageName, activityName, version));
// 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, () -> { private String getCompiledTracePath(
try { String packageName, String activityName, long version) {
String compiledTrace = getCompiledTraceFilePath(); return String.format(
File compiledTraceLocal = copyFileToLocal(compiledTrace, "compiled_trace.tmp"); "/data/misc/iorapd/%s/%d/%s/compiled_traces/compiled_trace.pb",
return compiledTraceLocal.exists(); packageName, version, activityName);
} 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(sleepIntervalSeconds);
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. * Starts the testing app.
*/ */
private void startApp() throws Exception { private void startApp(String packageName, String activityName) throws Exception {
Context context = getApplicationContext(); executeShellCommandViaTmpFile(
final Intent intent = context.getPackageManager() String.format("am start %s/%s", packageName, activityName));
.getLaunchIntentForPackage(TEST_PACKAGE_NAME);
context.startActivity(intent);
Log.i(TAG, "Started app " + TEST_PACKAGE_NAME);
} }
/** /**
@@ -304,11 +226,11 @@ public class IorapWorkFlowTest {
* <p> Keep trying to kill the process of the app until no process of the app package * <p> Keep trying to kill the process of the app until no process of the app package
* appears.</p> * appears.</p>
*/ */
private void closeApp() throws Exception { private void closeApp(String packageName) throws Exception {
while (true) { while (true) {
String pid = executeShellCommand("pidof " + TEST_PACKAGE_NAME); String pid = executeShellCommand("pidof " + packageName);
if (pid.isEmpty()) { if (pid.isEmpty()) {
Log.i(TAG, "Closed app " + TEST_PACKAGE_NAME); Log.i(TAG, "Closed app " + packageName);
return; return;
} }
executeShellCommand("kill -9 " + pid); executeShellCommand("kill -9 " + pid);
@@ -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. * Waits for the prefetching log in the logcat.
* *
* <p> When prefetching works, the perfetto traces should not be collected. </p> * <p> When prefetching works, the perfetto traces should not be collected. </p>
*/ */
private boolean waitForPrefetchingFromLogcat(long expectPerfettoTraceCount) throws Exception { private boolean waitForPrefetchingFromLogcat(
if (!startAppForPerfettoTrace(expectPerfettoTraceCount)) { String packageName, String activityName, long version, LogcatTimestamp timestamp)
return false; throws Exception {
}
String log = executeShellCommand("logcat -d");
Pattern p = Pattern.compile( Pattern p = Pattern.compile(
".*" + READAHEAD_INDICATOR ".*" + getReadaheadIndicator(packageName, activityName, version) +
+ ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n" ".*Total File Paths=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+ ".*Total Entries=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n" + ".*Total Entries=(\\d+) \\(good: (\\d+[.]?\\d*)%\\)\n"
+ ".*Total Bytes=(\\d+) \\(good: (\\d+[.]?\\d*)%\\).*", + ".*Total Bytes=(\\d+) \\(good: (\\d+[.]?\\d*)%\\).*",
Pattern.DOTALL); Pattern.DOTALL);
Matcher m = p.matcher(log);
return retryWithTimeout(TIMEOUT, () -> {
try {
String log = timestamp.getLogcatAfter();
Matcher m = p.matcher(log);
if (!m.matches()) { if (!m.matches()) {
Log.i(TAG, "Cannot find readahead log."); Log.i(TAG, "Cannot find readahead log.");
return false; return false;
@@ -355,12 +325,60 @@ public class IorapWorkFlowTest {
return totalFilePath > 0 && return totalFilePath > 0 &&
totalEntries > 0 && totalEntries > 0 &&
totalBytes > 100000 && totalBytes > 0 &&
totalFilePathGoodRate > 0.5 && totalFilePathGoodRate > 0.5 &&
totalEntriesGoodRate > 0.5 && totalEntriesGoodRate > 0.5 &&
totalBytesGoodRate > 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. * Executes command in adb shell.
@@ -372,6 +390,27 @@ public class IorapWorkFlowTest {
return UiDevice.getInstance( return UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd); 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 + "'");
}
}
}

View File

@@ -0,0 +1 @@
../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v1.apk

View File

@@ -0,0 +1 @@
../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v2.apk

View File

@@ -0,0 +1 @@
../../../../../../packages/modules/ArtPrebuilt/iorap/test/iorap_test_app_v3.apk