Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bdde9f53 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "iorap_functional_test: Add iorap function test." into rvc-dev am: d3e541eb am: 77bf81a7

Change-Id: Iac8f9423bc2f99fca94d650a22ece3acb20525db
parents c757d679 77bf81a7
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@
android_test {
    name: "iorap-functional-tests",
    srcs: ["src/**/*.java"],
    data: ["test_data/*"],
    static_libs: [
        // Non-test dependencies
        // library under test
+14 −0
Original line number Diff line number Diff line
@@ -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" />
+252 −213
Original line number Diff line number Diff line
@@ -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());

@@ -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));
  }

  /**
@@ -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);
@@ -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;
@@ -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.
@@ -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 + "'");
    }
  }
}
+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
+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