blob: 5352be6f283f26c8a3c6956db4dafc47cb5d07c3 [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.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 -> version update -> perfetto collection.
*/
@RunWith(AndroidJUnit4.class)
public class IorapWorkFlowTest {
private static final String TAG = "IorapWorkFlowTest";
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 UiDevice mDevice;
@Before
public void setUp() 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());
}
@After
public void tearDown() throws Exception {
String packageName = "com.example.ioraptestapp";
uninstallApk(packageName);
}
@Test (timeout = 300000)
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(
packageName, activityName, /*version=*/1L));
assertTrue(startAppForPerfettoTrace(
packageName, activityName, /*version=*/1L));
assertTrue(startAppForPerfettoTrace(
packageName, activityName, /*version=*/1L));
// Trigger maintenance service for compilation.
TimeUnit.SECONDS.sleep(5L);
assertTrue(compile(packageName, activityName, /*version=*/1L));
// Run app with prefetching
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);
}
/**
* Starts the testing app to collect the perfetto trace.
*
* @param expectPerfettoTraceCount is the expected count of perfetto traces.
*/
private boolean startAppForPerfettoTrace(
String packageName, String activityName, long version)
throws Exception {
LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
return waitForPerfettoTraceSavedFromLogcat(
packageName, activityName, version, timestamp);
}
private boolean startAppWithCompiledTrace(
String packageName, String activityName, long version)
throws Exception {
LogcatTimestamp timestamp = runAppOnce(packageName, activityName);
return waitForPrefetchingFromLogcat(
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.
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));
}
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(String packageName, String activityName) throws Exception {
executeShellCommandViaTmpFile(
String.format("am start %s/%s", packageName, activityName));
}
/**
* 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(String packageName) throws Exception {
while (true) {
String pid = executeShellCommand("pidof " + packageName);
if (pid.isEmpty()) {
Log.i(TAG, "Closed app " + packageName);
return;
}
executeShellCommand("kill -9 " + pid);
TimeUnit.SECONDS.sleep(1L);
}
}
/** 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(
String packageName, String activityName, long version, LogcatTimestamp timestamp)
throws Exception {
Pattern p = Pattern.compile(
".*" + 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);
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;
}
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 > 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.
*
* <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);
}
static class LogcatTimestamp {
private String epochTime;
public LogcatTimestamp() throws Exception{
long currentTimeMillis = System.currentTimeMillis();
epochTime = String.format(
"%d.%03d", 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 + "'");
}
}
}