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

Commit 188e6373 authored by Yan Wang's avatar Yan Wang
Browse files

startop: Add a function test for iorapd.

Bug: 144181684
Test: atest iorap-functional-tests
Change-Id: Ida3f524003fe6bd386ac22aaa2298f2b6f7e5aa7
parent 747bbfaf
Loading
Loading
Loading
Loading
+41 −0
Original line number Diff line number Diff line
// 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,
}
+38 −0
Original line number Diff line number Diff line
<?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>
+53 −0
Original line number Diff line number Diff line
<?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>
+377 −0
Original line number Diff line number Diff line
/*
 * 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);
  }
}