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