blob: 40023878af193f6e7ed159c78a582e4d8732d732 [file] [log] [blame]
/*
* 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(300L);
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 (timeout = 300000)
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(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.
*/
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 static String executeShellCommand(String cmd) throws Exception {
Log.i(TAG, "Execute: " + cmd);
return UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
}
}