1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.avf.test;
18 
19 import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
20 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
21 
22 import static com.google.common.truth.Truth.assertWithMessage;
23 import static com.google.common.truth.TruthJUnit.assume;
24 
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assume.assumeFalse;
27 import static org.junit.Assume.assumeTrue;
28 
29 import android.platform.test.annotations.RootPermissionTest;
30 
31 import com.android.microdroid.test.common.MetricsProcessor;
32 import com.android.microdroid.test.host.CommandRunner;
33 import com.android.microdroid.test.host.KvmHypTracer;
34 import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
35 import com.android.tradefed.device.DeviceNotAvailableException;
36 import com.android.tradefed.device.ITestDevice;
37 import com.android.tradefed.device.TestDevice;
38 import com.android.tradefed.log.LogUtil.CLog;
39 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
40 import com.android.tradefed.util.CommandResult;
41 import com.android.tradefed.util.SimpleStats;
42 
43 import org.junit.After;
44 import org.junit.Before;
45 import org.junit.Rule;
46 import org.junit.Test;
47 import org.junit.runner.RunWith;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.regex.Matcher;
53 import java.util.regex.Pattern;
54 
55 @RootPermissionTest
56 @RunWith(DeviceJUnit4ClassRunner.class)
57 public final class AVFHostTestCase extends MicrodroidHostTestCaseBase {
58 
59     private static final String COMPOSD_CMD_BIN = "/apex/com.android.compos/bin/composd_cmd";
60 
61     // Files that define the "test" instance of CompOS
62     private static final String COMPOS_TEST_ROOT = "/data/misc/apexdata/com.android.compos/test/";
63 
64     private static final String BOOTLOADER_TIME_PROP_NAME = "ro.boot.boottime";
65     private static final String BOOTLOADER_PREFIX = "bootloader-";
66     private static final String BOOTLOADER_TIME = "bootloader_time";
67     private static final String BOOTLOADER_PHASE_SW = "SW";
68 
69     /** Boot time test related variables */
70     private static final int REINSTALL_APEX_RETRY_INTERVAL_MS = 5 * 1000;
71     private static final int REINSTALL_APEX_TIMEOUT_SEC = 15;
72     private static final int COMPILE_STAGED_APEX_RETRY_INTERVAL_MS = 10 * 1000;
73     private static final int COMPILE_STAGED_APEX_TIMEOUT_SEC = 540;
74     private static final int BOOT_COMPLETE_TIMEOUT_MS = 10 * 60 * 1000;
75     private static final int ROUND_COUNT = 5;
76     private static final int ROUND_IGNORE_STARTUP_TIME = 3;
77     private static final String APK_NAME = "MicrodroidTestApp.apk";
78     private static final String PACKAGE_NAME = "com.android.microdroid.test";
79 
80     private MetricsProcessor mMetricsProcessor;
81     @Rule public TestMetrics mMetrics = new TestMetrics();
82 
83     private boolean mNeedTearDown = false;
84 
85     @Before
setUp()86     public void setUp() throws Exception {
87         mNeedTearDown = false;
88 
89         assumeDeviceIsCapable(getDevice());
90         mNeedTearDown = true;
91 
92         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
93 
94         mMetricsProcessor = new MetricsProcessor(getMetricPrefix() + "hostside/");
95     }
96 
97     @After
tearDown()98     public void tearDown() throws Exception {
99         if (!mNeedTearDown) {
100             // If we skipped setUp, we don't need to undo it, and that avoids potential exceptions
101             // incompatible hardware. (Note that tests can change what assumeDeviceIsCapable()
102             // sees, so we can't rely on that - b/268688303.)
103             return;
104         }
105 
106         CommandRunner android = new CommandRunner(getDevice());
107 
108         // Clear up any CompOS instance files we created.
109         android.tryRun("rm", "-rf", COMPOS_TEST_ROOT);
110     }
111 
112     @Test
testBootWithCompOS()113     public void testBootWithCompOS() throws Exception {
114         composTestHelper(true);
115     }
116 
117     @Test
testBootWithoutCompOS()118     public void testBootWithoutCompOS() throws Exception {
119         composTestHelper(false);
120     }
121 
122     @Test
testNoLongHypSections()123     public void testNoLongHypSections() throws Exception {
124         assumeTrue("Skip without hypervisor tracing", KvmHypTracer.isSupported(getDevice()));
125 
126         KvmHypTracer tracer = new KvmHypTracer(getDevice());
127         String result = tracer.run(COMPOSD_CMD_BIN + " test-compile");
128         assertWithMessage("Failed to test compilation VM.")
129                 .that(result).ignoringCase().contains("all ok");
130 
131         SimpleStats stats = tracer.getDurationStats();
132         reportMetric(stats.getData(), "hyp_sections", "s");
133         CLog.i("Hypervisor traces parsed successfully.");
134     }
135 
136     @Test
testCameraAppStartupTime()137     public void testCameraAppStartupTime() throws Exception {
138         String[] launchIntentPackages = {
139             "com.android.camera2",
140             "com.google.android.GoogleCamera/com.android.camera.CameraLauncher"
141         };
142         String launchIntentPackage = findSupportedPackage(launchIntentPackages);
143         assume().withMessage("No supported camera package").that(launchIntentPackage).isNotNull();
144         appStartupHelper(launchIntentPackage);
145     }
146 
147     @Test
testSettingsAppStartupTime()148     public void testSettingsAppStartupTime() throws Exception {
149         String[] launchIntentPackages = {
150             "com.android.settings"
151         };
152         String launchIntentPackage = findSupportedPackage(launchIntentPackages);
153         assume().withMessage("No supported settings package").that(launchIntentPackage).isNotNull();
154         appStartupHelper(launchIntentPackage);
155     }
156 
appStartupHelper(String launchIntentPackage)157     private void appStartupHelper(String launchIntentPackage) throws Exception {
158         assumeTrue(
159                 "Skip on non-protected VMs",
160                 ((TestDevice) getDevice()).supportsMicrodroid(/*protectedVm=*/ true));
161 
162         StartupTimeMetricCollection mCollection =
163                 new StartupTimeMetricCollection(getPackageName(launchIntentPackage), ROUND_COUNT);
164         getAppStartupTime(launchIntentPackage, mCollection);
165 
166         reportMetric(mCollection.mAppBeforeVmRunTotalTime,
167                 "app_startup/" + mCollection.getPkgName() + "/total_time/before_vm",
168                 "ms");
169         reportMetric(mCollection.mAppBeforeVmRunWaitTime,
170                 "app_startup/" + mCollection.getPkgName() + "/wait_time/before_vm",
171                 "ms");
172         reportMetric(mCollection.mAppDuringVmRunTotalTime,
173                 "app_startup/" + mCollection.getPkgName() + "/total_time/during_vm",
174                 "ms");
175         reportMetric(mCollection.mAppDuringVmRunWaitTime,
176                 "app_startup/" + mCollection.getPkgName() + "/wait_time/during_vm",
177                 "ms");
178         reportMetric(mCollection.mAppAfterVmRunTotalTime,
179                 "app_startup/" + mCollection.getPkgName() + "/total_time/after_vm",
180                 "ms");
181         reportMetric(mCollection.mAppAfterVmRunWaitTime,
182                 "app_startup/" + mCollection.getPkgName() + "/wait_time/after_vm",
183                 "ms");
184     }
185 
getPackageName(String launchIntentPackage)186     private String getPackageName(String launchIntentPackage) {
187         String appPkg = launchIntentPackage;
188 
189         // Does the appPkgName contain the intent ?
190         if (launchIntentPackage != null && launchIntentPackage.contains("/")) {
191             appPkg = launchIntentPackage.split("/")[0];
192         }
193         return appPkg;
194     }
195 
findSupportedPackage(String[] pkgNameList)196     private String findSupportedPackage(String[] pkgNameList) throws Exception {
197         CommandRunner android = new CommandRunner(getDevice());
198 
199         for (String pkgName : pkgNameList) {
200             String appPkg = getPackageName(pkgName);
201             String hasPackage = android.run("pm list package | grep -w " + appPkg + " 1> /dev/null"
202                     + "; echo $?");
203             assertNotNull(hasPackage);
204 
205             if (hasPackage.equals("0")) {
206                 return pkgName;
207             }
208         }
209         return null;
210     }
211 
getColdRunStartupTimes(CommandRunner android, String pkgName)212     private AmStartupTimeCmdParser getColdRunStartupTimes(CommandRunner android, String pkgName)
213             throws DeviceNotAvailableException, InterruptedException {
214         unlockScreen(android);
215         // Ensure we are killing the app to get the cold app startup time
216         android.run("am force-stop " + pkgName);
217         android.run("echo 3 > /proc/sys/vm/drop_caches");
218         String vmStartAppLog = android.run("am", "start -W -S " + pkgName);
219         assertNotNull(vmStartAppLog);
220         assumeFalse(vmStartAppLog.isEmpty());
221         return new AmStartupTimeCmdParser(vmStartAppLog);
222     }
223 
224     // Returns an array of two elements containing the delta between the initial app startup time
225     // and the time measured after running the VM.
getAppStartupTime(String pkgName, StartupTimeMetricCollection metricColector)226     private void getAppStartupTime(String pkgName, StartupTimeMetricCollection metricColector)
227             throws Exception {
228         TestDevice device = (TestDevice) getDevice();
229 
230         // 1. Reboot the device to run the test without stage2 fragmentation
231         getDevice().rebootUntilOnline();
232         waitForBootCompleted();
233 
234         // 2. Start the app and ignore first runs to warm up caches
235         CommandRunner android = new CommandRunner(getDevice());
236         for (int i = 0; i < ROUND_IGNORE_STARTUP_TIME; i++) {
237             getColdRunStartupTimes(android, pkgName);
238         }
239 
240         // 3. Run the app before the VM run and collect app startup time statistics
241         for (int i = 0; i < ROUND_COUNT; i++) {
242             AmStartupTimeCmdParser beforeVmStartApp = getColdRunStartupTimes(android, pkgName);
243             metricColector.addStartupTimeMetricBeforeVmRun(beforeVmStartApp);
244         }
245 
246         // Clear up any test dir
247         android.tryRun("rm", "-rf", MicrodroidHostTestCaseBase.TEST_ROOT);
248 
249         // Donate 80% of the available device memory to the VM
250         final String configPath = "assets/vm_config.json";
251         final int vm_mem_mb = getFreeMemoryInfoMb(android) * 80 / 100;
252         ITestDevice microdroidDevice =
253                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
254                         .debugLevel("full")
255                         .memoryMib(vm_mem_mb)
256                         .cpuTopology("match_host")
257                         .build(device);
258         try {
259             microdroidDevice.waitForBootComplete(30000);
260             microdroidDevice.enableAdbRoot();
261 
262             CommandRunner microdroid = new CommandRunner(microdroidDevice);
263 
264             microdroid.run("mkdir -p /mnt/ramdisk && chmod 777 /mnt/ramdisk");
265             microdroid.run("mount -t tmpfs -o size=32G tmpfs /mnt/ramdisk");
266 
267             // Allocate memory for the VM until it fails and make sure that we touch
268             // the allocated memory in the guest to be able to create stage2 fragmentation.
269             try {
270                 microdroid.tryRun(
271                         String.format(
272                                 "cd /mnt/ramdisk && truncate -s %dM sprayMemory"
273                                         + " && dd if=/dev/zero of=sprayMemory bs=1MB count=%d",
274                                 vm_mem_mb, vm_mem_mb));
275             } catch (Exception expected) {
276             }
277 
278             // Run the app during the VM run and collect cold startup time.
279             for (int i = 0; i < ROUND_COUNT; i++) {
280                 AmStartupTimeCmdParser duringVmStartApp = getColdRunStartupTimes(android, pkgName);
281                 metricColector.addStartupTimeMetricDuringVmRun(duringVmStartApp);
282             }
283         } finally {
284             device.shutdownMicrodroid(microdroidDevice);
285         }
286 
287         // Run the app after the VM run and collect cold startup time.
288         for (int i = 0; i < ROUND_COUNT; i++) {
289             AmStartupTimeCmdParser afterVmStartApp = getColdRunStartupTimes(android, pkgName);
290             metricColector.addStartupTimerMetricAfterVmRun(afterVmStartApp);
291         }
292     }
293 
294     static class AmStartupTimeCmdParser {
295         private int mTotalTime;
296         private int mWaitTime;
297 
AmStartupTimeCmdParser(String startAppLog)298         AmStartupTimeCmdParser(String startAppLog) {
299             String[] lines = startAppLog.split("[\r\n]+");
300             mTotalTime = mWaitTime = 0;
301 
302             for (String line : lines) {
303                 if (line.contains("TotalTime:")) {
304                     mTotalTime = Integer.parseInt(line.replaceAll("\\D+", ""));
305                 }
306                 if (line.contains("WaitTime:")) {
307                     mWaitTime = Integer.parseInt(line.replaceAll("\\D+", ""));
308                 }
309             }
310         }
311     }
312 
313     static class StartupTimeMetricCollection {
314         List<Double> mAppBeforeVmRunTotalTime;
315         List<Double> mAppBeforeVmRunWaitTime;
316 
317         List<Double> mAppDuringVmRunTotalTime;
318         List<Double> mAppDuringVmRunWaitTime;
319 
320         List<Double> mAppAfterVmRunTotalTime;
321         List<Double> mAppAfterVmRunWaitTime;
322 
323         private final String mPkgName;
324 
StartupTimeMetricCollection(String pkgName, int size)325         StartupTimeMetricCollection(String pkgName, int size) {
326             mAppBeforeVmRunTotalTime = new ArrayList<>(size);
327             mAppBeforeVmRunWaitTime = new ArrayList<>(size);
328 
329             mAppDuringVmRunTotalTime = new ArrayList<>(size);
330             mAppDuringVmRunWaitTime = new ArrayList<>(size);
331 
332             mAppAfterVmRunTotalTime = new ArrayList<>(size);
333             mAppAfterVmRunWaitTime = new ArrayList<>(size);
334             mPkgName = pkgName;
335         }
336 
addStartupTimeMetricBeforeVmRun(AmStartupTimeCmdParser m)337         public void addStartupTimeMetricBeforeVmRun(AmStartupTimeCmdParser m) {
338             mAppBeforeVmRunTotalTime.add((double) m.mTotalTime);
339             mAppBeforeVmRunWaitTime.add((double) m.mWaitTime);
340         }
341 
addStartupTimeMetricDuringVmRun(AmStartupTimeCmdParser m)342         public void addStartupTimeMetricDuringVmRun(AmStartupTimeCmdParser m) {
343             mAppDuringVmRunTotalTime.add((double) m.mTotalTime);
344             mAppDuringVmRunWaitTime.add((double) m.mWaitTime);
345         }
346 
addStartupTimerMetricAfterVmRun(AmStartupTimeCmdParser m)347         public void addStartupTimerMetricAfterVmRun(AmStartupTimeCmdParser m) {
348             mAppAfterVmRunTotalTime.add((double) m.mTotalTime);
349             mAppAfterVmRunWaitTime.add((double) m.mWaitTime);
350         }
351 
getPkgName()352         public String getPkgName() {
353             return this.mPkgName;
354         }
355     }
356 
getFreeMemoryInfoMb(CommandRunner android)357     private int getFreeMemoryInfoMb(CommandRunner android) throws DeviceNotAvailableException,
358             IllegalArgumentException {
359         int freeMemory = 0;
360         String content = android.runForResult("cat /proc/meminfo").getStdout().trim();
361         String[] lines = content.split("[\r\n]+");
362 
363         for (String line : lines) {
364             if (line.contains("MemFree:")) {
365                 freeMemory = Integer.parseInt(line.replaceAll("\\D+", "")) / 1024;
366                 return freeMemory;
367             }
368         }
369 
370         throw new IllegalArgumentException();
371     }
372 
unlockScreen(CommandRunner android)373     private void unlockScreen(CommandRunner android)
374             throws DeviceNotAvailableException, InterruptedException {
375         android.run("input keyevent", "KEYCODE_WAKEUP");
376         Thread.sleep(500);
377         final String ret = android.runForResult("dumpsys nfc | grep 'mScreenState='")
378                 .getStdout().trim();
379         if (ret != null && ret.contains("ON_LOCKED")) {
380             android.run("input keyevent", "KEYCODE_MENU");
381         }
382     }
383 
updateBootloaderTimeInfo(Map<String, List<Double>> bootloaderTime)384     private void updateBootloaderTimeInfo(Map<String, List<Double>> bootloaderTime)
385             throws Exception {
386 
387         String bootLoaderVal = getDevice().getProperty(BOOTLOADER_TIME_PROP_NAME);
388         // Sample Output : 1BLL:89,1BLE:590,2BLL:0,2BLE:1344,SW:6734,KL:1193
389         if (bootLoaderVal != null) {
390             String[] bootLoaderPhases = bootLoaderVal.split(",");
391             double bootLoaderTotalTime = 0d;
392             for (String bootLoaderPhase : bootLoaderPhases) {
393                 String[] bootKeyVal = bootLoaderPhase.split(":");
394                 String key = String.format("%s%s", BOOTLOADER_PREFIX, bootKeyVal[0]);
395 
396                 bootloaderTime.computeIfAbsent(key,
397                         k -> new ArrayList<>()).add(Double.parseDouble(bootKeyVal[1]));
398                 // SW is the time spent on the warning screen. So ignore it in
399                 // final boot time calculation.
400                 if (BOOTLOADER_PHASE_SW.equalsIgnoreCase(bootKeyVal[0])) {
401                     continue;
402                 }
403                 bootLoaderTotalTime += Double.parseDouble(bootKeyVal[1]);
404             }
405             bootloaderTime.computeIfAbsent(BOOTLOADER_TIME,
406                     k -> new ArrayList<>()).add(bootLoaderTotalTime);
407         }
408     }
409 
getDmesgBootTime()410     private Double getDmesgBootTime() throws Exception {
411 
412         CommandRunner android = new CommandRunner(getDevice());
413         String result = android.run("dmesg");
414         Pattern pattern = Pattern.compile("\\[(.*)].*sys.boot_completed=1.*");
415         for (String line : result.split("[\r\n]+")) {
416             Matcher matcher = pattern.matcher(line);
417             if (matcher.find()) {
418                 return Double.valueOf(matcher.group(1));
419             }
420         }
421         throw new IllegalArgumentException("Failed to get boot time info.");
422     }
423 
composTestHelper(boolean isWithCompos)424     private void composTestHelper(boolean isWithCompos) throws Exception {
425         assumeFalse("Skip on CF; too slow", isCuttlefish());
426 
427         List<Double> bootDmesgTime = new ArrayList<>(ROUND_COUNT);
428 
429         for (int round = 0; round < ROUND_COUNT; ++round) {
430             reInstallApex(REINSTALL_APEX_TIMEOUT_SEC);
431             try {
432                 if (isWithCompos) {
433                     compileStagedApex(COMPILE_STAGED_APEX_TIMEOUT_SEC);
434                 }
435             } finally {
436                 // If compilation fails, we still have a staged APEX, and we need to reboot to
437                 // clean that up for further tests.
438                 getDevice().nonBlockingReboot();
439                 waitForBootCompleted();
440             }
441 
442             double elapsedSec = getDmesgBootTime();
443             bootDmesgTime.add(elapsedSec);
444         }
445 
446         String suffix = "";
447         if (isWithCompos) {
448             suffix = "with_compos";
449         } else {
450             suffix = "without_compos";
451         }
452 
453         reportMetric(bootDmesgTime, "dmesg_boot_time_" + suffix, "s");
454     }
455 
reportMetric(List<Double> data, String name, String unit)456     private void reportMetric(List<Double> data, String name, String unit) {
457         CLog.d("Report metric " + name + "(" + unit + ") : " + data.toString());
458         Map<String, Double> stats = mMetricsProcessor.computeStats(data, name, unit);
459         for (Map.Entry<String, Double> entry : stats.entrySet()) {
460             CLog.d("Add test metrics " + entry.getKey() + " : " + entry.getValue().toString());
461             mMetrics.addTestMetric(entry.getKey(), entry.getValue().toString());
462         }
463     }
464 
waitForBootCompleted()465     private void waitForBootCompleted() throws Exception {
466         getDevice().waitForDeviceOnline(BOOT_COMPLETE_TIMEOUT_MS);
467         getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS);
468         getDevice().enableAdbRoot();
469     }
470 
compileStagedApex(int timeoutSec)471     private void compileStagedApex(int timeoutSec) throws Exception {
472 
473         long timeStart = System.currentTimeMillis();
474         long timeEnd = timeStart + timeoutSec * 1000L;
475 
476         while (true) {
477 
478             try {
479                 CommandRunner android = new CommandRunner(getDevice());
480 
481                 String result =
482                         android.runWithTimeout(
483                                 3 * 60 * 1000, COMPOSD_CMD_BIN + " staged-apex-compile");
484                 assertWithMessage("Failed to compile staged APEX. Reason: " + result)
485                     .that(result).ignoringCase().contains("all ok");
486 
487                 CLog.i("Success to compile staged APEX. Result: " + result);
488 
489                 break;
490             } catch (AssertionError e) {
491                 CLog.i("Gets AssertionError when compile staged APEX. Detail: " + e);
492             }
493 
494             if (System.currentTimeMillis() > timeEnd) {
495                 CLog.e("Try to compile staged APEX several times but all fail.");
496                 throw new AssertionError("Failed to compile staged APEX.");
497             }
498 
499             Thread.sleep(COMPILE_STAGED_APEX_RETRY_INTERVAL_MS);
500         }
501     }
502 
reInstallApex(int timeoutSec)503     private void reInstallApex(int timeoutSec) throws Exception {
504 
505         long timeStart = System.currentTimeMillis();
506         long timeEnd = timeStart + timeoutSec * 1000L;
507 
508         while (true) {
509 
510             try {
511                 CommandRunner android = new CommandRunner(getDevice());
512 
513                 String packagesOutput =
514                         android.run("pm list packages -f --apex-only");
515 
516                 Pattern p = Pattern.compile(
517                         "package:(.*)=(com(?:\\.google)?\\.android\\.art)$", Pattern.MULTILINE);
518                 Matcher m = p.matcher(packagesOutput);
519                 assertWithMessage("ART module not found. Packages are:\n" + packagesOutput)
520                     .that(m.find())
521                     .isTrue();
522 
523                 String artApexPath = m.group(1);
524 
525                 CommandResult result = android.runForResult(
526                         "pm install --apex " + artApexPath);
527                 assertWithMessage("Failed to install APEX. Reason: " + result)
528                     .that(result.getExitCode()).isEqualTo(0);
529 
530                 CLog.i("Success to install APEX. Result: " + result);
531 
532                 break;
533             } catch (AssertionError e) {
534                 CLog.i("Gets AssertionError when reinstall art APEX. Detail: " + e);
535             }
536 
537             if (System.currentTimeMillis() > timeEnd) {
538                 CLog.e("Try to reinstall art APEX several times but all fail.");
539                 throw new AssertionError("Failed to reinstall art APEX.");
540             }
541 
542             Thread.sleep(REINSTALL_APEX_RETRY_INTERVAL_MS);
543         }
544     }
545 }
546