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 com.android.microdroid.benchmark; 18 19 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST; 20 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU; 21 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL; 22 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE; 23 24 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 25 26 import static com.google.common.truth.Truth.assertThat; 27 import static com.google.common.truth.Truth.assertWithMessage; 28 import static com.google.common.truth.TruthJUnit.assume; 29 30 import android.app.Application; 31 import android.app.Instrumentation; 32 import android.content.ComponentCallbacks2; 33 import android.content.Context; 34 import android.os.Bundle; 35 import android.os.ParcelFileDescriptor; 36 import android.os.ParcelFileDescriptor.AutoCloseInputStream; 37 import android.os.ParcelFileDescriptor.AutoCloseOutputStream; 38 import android.os.RemoteException; 39 import android.system.Os; 40 import android.system.virtualmachine.VirtualMachine; 41 import android.system.virtualmachine.VirtualMachineConfig; 42 import android.system.virtualmachine.VirtualMachineException; 43 import android.system.virtualmachine.VirtualMachineManager; 44 import android.util.Log; 45 46 import com.android.microdroid.test.common.MetricsProcessor; 47 import com.android.microdroid.test.common.ProcessUtil; 48 import com.android.microdroid.test.device.MicrodroidDeviceTestBase; 49 import com.android.microdroid.testservice.IBenchmarkService; 50 import com.android.microdroid.testservice.ITestService; 51 52 import org.junit.After; 53 import org.junit.Before; 54 import org.junit.Rule; 55 import org.junit.Test; 56 import org.junit.rules.Timeout; 57 import org.junit.runner.RunWith; 58 import org.junit.runners.Parameterized; 59 60 import java.io.BufferedReader; 61 import java.io.File; 62 import java.io.FileReader; 63 import java.io.IOException; 64 import java.io.InputStream; 65 import java.io.InputStreamReader; 66 import java.io.OutputStream; 67 import java.io.OutputStreamWriter; 68 import java.io.Writer; 69 import java.nio.file.Files; 70 import java.util.ArrayList; 71 import java.util.Collection; 72 import java.util.Collections; 73 import java.util.HashMap; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.OptionalLong; 77 import java.util.concurrent.atomic.AtomicReference; 78 import java.util.function.Function; 79 80 @RunWith(Parameterized.class) 81 public class MicrodroidBenchmarks extends MicrodroidDeviceTestBase { 82 private static final String TAG = "MicrodroidBenchmarks"; 83 private static final String METRIC_NAME_PREFIX = getMetricPrefix() + "microdroid/"; 84 private static final int IO_TEST_TRIAL_COUNT = 5; 85 private static final int TEST_TRIAL_COUNT = 5; 86 private static final long ONE_MEBI = 1024 * 1024; 87 88 @Rule public Timeout globalTimeout = Timeout.seconds(300); 89 90 private static final String APEX_ETC_FS = "/apex/com.android.virt/etc/fs/"; 91 private static final double SIZE_MB = 1024.0 * 1024.0; 92 private static final double NANO_TO_MILLI = 1_000_000.0; 93 private static final double NANO_TO_MICRO = 1_000.0; 94 private static final String MICRODROID_IMG_PREFIX = "microdroid_"; 95 private static final String MICRODROID_IMG_SUFFIX = ".img"; 96 97 @Parameterized.Parameters(name = "protectedVm={0},os={1}") params()98 public static Collection<Object[]> params() { 99 List<Object[]> ret = new ArrayList<>(); 100 for (String os : SUPPORTED_OSES) { 101 ret.add(new Object[] {true /* protectedVm */, os}); 102 ret.add(new Object[] {false /* protectedVm */, os}); 103 } 104 return ret; 105 } 106 107 @Parameterized.Parameter(0) 108 public boolean mProtectedVm; 109 110 @Parameterized.Parameter(1) 111 public String mOs; 112 113 private final MetricsProcessor mMetricsProcessor = new MetricsProcessor(METRIC_NAME_PREFIX); 114 115 private Instrumentation mInstrumentation; 116 117 private boolean mTeardownDebugfs; 118 setupDebugfs()119 private void setupDebugfs() throws IOException { 120 BufferedReader reader = new BufferedReader(new FileReader("/proc/mounts")); 121 122 mTeardownDebugfs = 123 !reader.lines().filter(line -> line.startsWith("debugfs ")).findAny().isPresent(); 124 125 if (mTeardownDebugfs) { 126 executeCommand("mount -t debugfs none /sys/kernel/debug"); 127 } 128 } 129 MicrodroidBenchmarks()130 public MicrodroidBenchmarks() throws IOException { 131 // See b/325745564#comment28. Calling this method here ensures that threads spawned for 132 // @Test methods are with the desired task profile. If this is called in @Before, the task 133 // profile may not be set to the test threads because they may be spanwed prior to the 134 // execution of the @Before method (though the test methods will be executed after the 135 // @Before method). With this, children of this benchmark process (virtmgr and crosvm) also 136 // run in the desired task profile. 137 setMaxPerformanceTaskProfile(); 138 } 139 140 @Before setup()141 public void setup() throws IOException { 142 grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION); 143 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 144 prepareTestSetup(mProtectedVm, mOs); 145 mInstrumentation = getInstrumentation(); 146 } 147 148 @After tearDown()149 public void tearDown() throws IOException { 150 if (mTeardownDebugfs) { 151 executeCommand("umount /sys/kernel/debug"); 152 } 153 } 154 canBootMicrodroidWithMemory(int mem)155 private boolean canBootMicrodroidWithMemory(int mem) 156 throws VirtualMachineException, InterruptedException, IOException { 157 VirtualMachineConfig normalConfig = 158 newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so") 159 .setDebugLevel(DEBUG_LEVEL_NONE) 160 .setMemoryBytes(mem * ONE_MEBI) 161 .setShouldUseHugepages(true) 162 .build(); 163 164 // returns true if succeeded at least once. 165 final int trialCount = 5; 166 for (int i = 0; i < trialCount; i++) { 167 forceCreateNewVirtualMachine("test_vm_minimum_memory", normalConfig); 168 169 if (tryBootVm(TAG, "test_vm_minimum_memory").payloadStarted) return true; 170 } 171 172 return false; 173 } 174 175 @Test testMinimumRequiredRAM()176 public void testMinimumRequiredRAM() 177 throws VirtualMachineException, InterruptedException, IOException { 178 assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse(); 179 180 int lo = 16, hi = 512, minimum = 0; 181 boolean found = false; 182 183 while (lo <= hi) { 184 int mid = (lo + hi) / 2; 185 if (canBootMicrodroidWithMemory(mid)) { 186 found = true; 187 minimum = mid; 188 hi = mid - 1; 189 } else { 190 lo = mid + 1; 191 } 192 } 193 194 assertThat(found).isTrue(); 195 196 Bundle bundle = new Bundle(); 197 bundle.putInt(METRIC_NAME_PREFIX + "minimum_required_memory", minimum); 198 mInstrumentation.sendStatus(0, bundle); 199 } 200 201 private static class BootTimeStats { 202 private final Map<BootTimeMetric, List<Double>> mData = new HashMap<>(); 203 BootTimeStats(int trialCount)204 public BootTimeStats(int trialCount) { 205 for (BootTimeMetric metric : BootTimeMetric.values()) { 206 mData.put(metric, new ArrayList<>(trialCount)); 207 } 208 } 209 collect(BootResult result)210 public void collect(BootResult result) { 211 for (BootTimeMetric metric : BootTimeMetric.values()) { 212 OptionalLong value = result.getBootTimeMetricNanoTime(metric); 213 if (value.isPresent()) { 214 mData.get(metric).add(value.getAsLong() / NANO_TO_MILLI); 215 } 216 } 217 } 218 get(BootTimeMetric metric)219 public List<Double> get(BootTimeMetric metric) { 220 return Collections.unmodifiableList(mData.get(metric)); 221 } 222 } 223 runBootTimeTest( String name, boolean fullDebug, Function<VirtualMachineConfig.Builder, VirtualMachineConfig.Builder> fnConfig)224 private void runBootTimeTest( 225 String name, 226 boolean fullDebug, 227 Function<VirtualMachineConfig.Builder, VirtualMachineConfig.Builder> fnConfig) 228 throws VirtualMachineException, InterruptedException, IOException { 229 assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse(); 230 231 final int trialCount = 10; 232 233 BootTimeStats stats = new BootTimeStats(trialCount); 234 for (int i = 0; i < trialCount; i++) { 235 VirtualMachineConfig.Builder builder = 236 newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so") 237 .setShouldBoostUclamp(true) 238 .setShouldUseHugepages(true) 239 .setMemoryBytes(256 * ONE_MEBI) 240 .setDebugLevel(DEBUG_LEVEL_NONE); 241 if (fullDebug) { 242 builder = builder.setDebugLevel(DEBUG_LEVEL_FULL).setVmOutputCaptured(true); 243 } 244 VirtualMachineConfig config = fnConfig.apply(builder).build(); 245 forceCreateNewVirtualMachine(name, config); 246 247 BootResult result = tryBootVm(TAG, name); 248 assertThat(result.payloadStarted).isTrue(); 249 stats.collect(result); 250 } 251 252 reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms"); 253 if (fullDebug) { 254 reportMetrics(stats.get(BootTimeMetric.VM_START), "vm_starting_time", "ms"); 255 reportMetrics(stats.get(BootTimeMetric.BOOTLOADER), "bootloader_time", "ms"); 256 reportMetrics(stats.get(BootTimeMetric.KERNEL), "kernel_boot_time", "ms"); 257 reportMetrics(stats.get(BootTimeMetric.USERSPACE), "userspace_boot_time", "ms"); 258 } 259 } 260 261 @Test testMicrodroidBootTime()262 public void testMicrodroidBootTime() 263 throws VirtualMachineException, InterruptedException, IOException { 264 runBootTimeTest( 265 "test_vm_boot_time", 266 /* fullDebug */ false, 267 (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU)); 268 } 269 270 @Test testMicrodroidHostCpuTopologyBootTime()271 public void testMicrodroidHostCpuTopologyBootTime() 272 throws VirtualMachineException, InterruptedException, IOException { 273 runBootTimeTest( 274 "test_vm_boot_time_host_topology", 275 /* fullDebug */ false, 276 (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)); 277 } 278 279 @Test testMicrodroidDebugBootTime()280 public void testMicrodroidDebugBootTime() 281 throws VirtualMachineException, InterruptedException, IOException { 282 runBootTimeTest("test_vm_boot_time_debug", /* fullDebug */ true, (builder) -> builder); 283 } 284 testMicrodroidDebugBootTime_withVendorBase(File vendorDiskImage)285 private void testMicrodroidDebugBootTime_withVendorBase(File vendorDiskImage) throws Exception { 286 // TODO(b/325094712): Boot fails with vendor partition in Cuttlefish. 287 assume().withMessage("Cuttlefish doesn't support device tree under /proc/device-tree") 288 .that(isCuttlefish()) 289 .isFalse(); 290 // TODO(b/317567210): Boots fails with vendor partition in HWASAN enabled microdroid 291 // after introducing verification based on DT and fstab in microdroid vendor partition. 292 assume().withMessage("Boot with vendor partition is failing in HWASAN enabled Microdroid.") 293 .that(isHwasan()) 294 .isFalse(); 295 assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES); 296 runBootTimeTest( 297 "test_vm_boot_time_debug_with_vendor_partition", 298 /* fullDebug */ true, 299 (builder) -> builder.setVendorDiskImage(vendorDiskImage)); 300 } 301 302 @Test testMicrodroidDebugBootTime_withVendorPartition()303 public void testMicrodroidDebugBootTime_withVendorPartition() throws Exception { 304 File vendorDiskImage = new File("/vendor/etc/avf/microdroid/microdroid_vendor.img"); 305 assume().withMessage("Microdroid vendor image doesn't exist, skip") 306 .that(vendorDiskImage.exists()) 307 .isTrue(); 308 testMicrodroidDebugBootTime_withVendorBase(vendorDiskImage); 309 } 310 311 @Test testMicrodroidDebugBootTime_withCustomVendorPartition()312 public void testMicrodroidDebugBootTime_withCustomVendorPartition() throws Exception { 313 assume().withMessage( 314 "Skip test for protected VM, pvmfw config data doesn't contain any" 315 + " information of test images, such as root digest.") 316 .that(mProtectedVm) 317 .isFalse(); 318 File vendorDiskImage = 319 new File("/data/local/tmp/microdroid-bench/microdroid_vendor_image.img"); 320 testMicrodroidDebugBootTime_withVendorBase(vendorDiskImage); 321 } 322 323 @Test testMicrodroidImageSize()324 public void testMicrodroidImageSize() throws IOException { 325 Bundle bundle = new Bundle(); 326 for (File file : new File(APEX_ETC_FS).listFiles()) { 327 String name = file.getName(); 328 329 if (!name.startsWith(MICRODROID_IMG_PREFIX) || !name.endsWith(MICRODROID_IMG_SUFFIX)) { 330 continue; 331 } 332 333 String base = 334 name.substring( 335 MICRODROID_IMG_PREFIX.length(), 336 name.length() - MICRODROID_IMG_SUFFIX.length()); 337 String metric = METRIC_NAME_PREFIX + "img_size_" + base + "_MB"; 338 double size = Files.size(file.toPath()) / SIZE_MB; 339 bundle.putDouble(metric, size); 340 } 341 mInstrumentation.sendStatus(0, bundle); 342 } 343 344 @Test testVsockTransferFromHostToVM()345 public void testVsockTransferFromHostToVM() throws Exception { 346 VirtualMachineConfig config = 347 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json") 348 .setDebugLevel(DEBUG_LEVEL_NONE) 349 .setShouldBoostUclamp(true) 350 .setShouldUseHugepages(true) 351 .build(); 352 List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT); 353 354 for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) { 355 int port = (mProtectedVm ? 5666 : 6666) + i; 356 String vmName = "test_vm_io_" + i; 357 VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config); 358 BenchmarkVmListener.create(new VsockListener(transferRates, port)).runToFinish(TAG, vm); 359 } 360 reportMetrics(transferRates, "vsock/transfer_host_to_vm", "mb_per_sec"); 361 } 362 363 @Test testVirtioBlkSeqReadRate()364 public void testVirtioBlkSeqReadRate() throws Exception { 365 testVirtioBlkReadRate(/* isRand= */ false); 366 } 367 368 @Test testVirtioBlkRandReadRate()369 public void testVirtioBlkRandReadRate() throws Exception { 370 testVirtioBlkReadRate(/* isRand= */ true); 371 } 372 testVirtioBlkReadRate(boolean isRand)373 private void testVirtioBlkReadRate(boolean isRand) throws Exception { 374 VirtualMachineConfig config = 375 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json") 376 .setDebugLevel(DEBUG_LEVEL_NONE) 377 .setShouldUseHugepages(true) 378 .build(); 379 List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT); 380 381 for (int i = 0; i < IO_TEST_TRIAL_COUNT + 1; ++i) { 382 if (i == 1) { 383 // Clear the first result because when the file was loaded the first time, 384 // the data also needs to be loaded from hard drive to host. This is 385 // not part of the virtio-blk IO throughput. 386 readRates.clear(); 387 } 388 String vmName = "test_vm_io_" + i; 389 VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config); 390 BenchmarkVmListener.create(new VirtioBlkListener(readRates, isRand)) 391 .runToFinish(TAG, vm); 392 } 393 reportMetrics( 394 readRates, isRand ? "virtio-blk/rand_read" : "virtio-blk/seq_read", "mb_per_sec"); 395 } 396 reportMetrics(List<Double> metrics, String name, String unit)397 private void reportMetrics(List<Double> metrics, String name, String unit) { 398 Map<String, Double> stats = mMetricsProcessor.computeStats(metrics, name, unit); 399 Bundle bundle = new Bundle(); 400 for (Map.Entry<String, Double> entry : stats.entrySet()) { 401 bundle.putDouble(entry.getKey(), entry.getValue()); 402 } 403 mInstrumentation.sendStatus(0, bundle); 404 } 405 406 private static class VirtioBlkListener implements BenchmarkVmListener.InnerListener { 407 private static final String FILENAME = APEX_ETC_FS + "microdroid_super.img"; 408 409 private final List<Double> mReadRates; 410 private final boolean mIsRand; 411 VirtioBlkListener(List<Double> readRates, boolean isRand)412 VirtioBlkListener(List<Double> readRates, boolean isRand) { 413 mReadRates = readRates; 414 mIsRand = isRand; 415 } 416 417 @Override onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)418 public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService) 419 throws RemoteException { 420 double readRate = benchmarkService.measureReadRate(FILENAME, mIsRand); 421 mReadRates.add(readRate); 422 } 423 } 424 executeCommand(String command)425 private String executeCommand(String command) { 426 return runInShell(TAG, mInstrumentation.getUiAutomation(), command); 427 } 428 429 private static class CrosvmStats { 430 public final long mHostRss; 431 public final long mHostPss; 432 public final long mGuestRss; 433 public final long mGuestPss; 434 CrosvmStats(int vmPid, Function<String, String> shellExecutor)435 CrosvmStats(int vmPid, Function<String, String> shellExecutor) { 436 try { 437 long hostRss = 0; 438 long hostPss = 0; 439 long guestRss = 0; 440 long guestPss = 0; 441 boolean hasGuestMaps = false; 442 List<ProcessUtil.SMapEntry> smaps = 443 ProcessUtil.getProcessSmaps(vmPid, shellExecutor); 444 for (ProcessUtil.SMapEntry entry : smaps) { 445 long rss = entry.metrics.get("Rss"); 446 long pss = entry.metrics.get("Pss"); 447 if (entry.name.contains("crosvm_guest")) { 448 guestRss += rss; 449 guestPss += pss; 450 hasGuestMaps = true; 451 } else { 452 hostRss += rss; 453 hostPss += pss; 454 } 455 } 456 if (!hasGuestMaps) { 457 StringBuilder sb = new StringBuilder(); 458 for (ProcessUtil.SMapEntry entry : smaps) { 459 sb.append(entry.toString()); 460 } 461 throw new IllegalStateException( 462 "found no crosvm_guest smap entry in crosvm process: " + sb); 463 } 464 mHostRss = hostRss; 465 mHostPss = hostPss; 466 mGuestRss = guestRss; 467 mGuestPss = guestPss; 468 } catch (Exception e) { 469 Log.e(TAG, "Error inside onPayloadReady():" + e); 470 throw new RuntimeException(e); 471 } 472 } 473 } 474 475 private static class KvmVmStats { 476 public final long mProtectedHyp; 477 public final long mProtectedShared; 478 private final Function<String, String> mShellExecutor; 479 private static final String KVM_STATS_FS = "/sys/kernel/debug/kvm"; 480 createIfSupported( int vmPid, Function<String, String> shellExecutor)481 public static KvmVmStats createIfSupported( 482 int vmPid, Function<String, String> shellExecutor) { 483 484 if (!new File(KVM_STATS_FS + "/protected_hyp_mem").exists()) { 485 return null; 486 } 487 488 return new KvmVmStats(vmPid, shellExecutor); 489 } 490 KvmVmStats(int vmPid, Function<String, String> shellExecutor)491 KvmVmStats(int vmPid, Function<String, String> shellExecutor) { 492 mShellExecutor = shellExecutor; 493 494 try { 495 String dir = getKvmVmStatDir(vmPid); 496 497 mProtectedHyp = getKvmVmStat(dir, "protected_hyp_mem"); 498 mProtectedShared = getKvmVmStat(dir, "protected_shared_mem"); 499 500 } catch (Exception e) { 501 Log.e(TAG, "Error inside onPayloadReady():" + e); 502 throw new RuntimeException(e); 503 } 504 } 505 getKvmVmStatDir(int vmPid)506 private String getKvmVmStatDir(int vmPid) { 507 String output = mShellExecutor.apply("find " + KVM_STATS_FS + " -type d"); 508 509 for (String line : output.split("\n")) { 510 if (line.startsWith(KVM_STATS_FS + "/" + Integer.toString(vmPid) + "-")) { 511 return line; 512 } 513 } 514 515 throw new IllegalStateException("KVM stat folder for PID " + vmPid + " not found"); 516 } 517 getKvmVmStat(String dir, String name)518 private int getKvmVmStat(String dir, String name) throws IOException { 519 return Integer.parseInt(mShellExecutor.apply("cat " + dir + "/" + name).trim()); 520 } 521 } 522 523 @Test testMemoryUsage()524 public void testMemoryUsage() throws Exception { 525 final String vmName = "test_vm_mem_usage"; 526 VirtualMachineConfig config = 527 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json") 528 .setDebugLevel(DEBUG_LEVEL_NONE) 529 .setShouldUseHugepages(true) 530 .setMemoryBytes(256 * ONE_MEBI) 531 .build(); 532 VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config); 533 MemoryUsageListener listener = new MemoryUsageListener(this::executeCommand); 534 535 setupDebugfs(); 536 537 BenchmarkVmListener.create(listener).runToFinish(TAG, vm); 538 539 assertWithMessage("VM failed to start").that(listener.mCrosvm).isNotNull(); 540 541 double mem_overall = 256.0; 542 double mem_total = (double) listener.mMemTotal / 1024.0; 543 double mem_free = (double) listener.mMemFree / 1024.0; 544 double mem_avail = (double) listener.mMemAvailable / 1024.0; 545 double mem_buffers = (double) listener.mBuffers / 1024.0; 546 double mem_cached = (double) listener.mCached / 1024.0; 547 double mem_slab = (double) listener.mSlab / 1024.0; 548 double mem_crosvm_host_rss = (double) listener.mCrosvm.mHostRss / 1024.0; 549 double mem_crosvm_host_pss = (double) listener.mCrosvm.mHostPss / 1024.0; 550 double mem_crosvm_guest_rss = (double) listener.mCrosvm.mGuestRss / 1024.0; 551 double mem_crosvm_guest_pss = (double) listener.mCrosvm.mGuestPss / 1024.0; 552 553 double mem_kernel = mem_overall - mem_total; 554 double mem_used = mem_total - mem_free - mem_buffers - mem_cached - mem_slab; 555 double mem_unreclaimable = mem_total - mem_avail; 556 557 Bundle bundle = new Bundle(); 558 bundle.putDouble(METRIC_NAME_PREFIX + "mem_kernel_MB", mem_kernel); 559 bundle.putDouble(METRIC_NAME_PREFIX + "mem_used_MB", mem_used); 560 bundle.putDouble(METRIC_NAME_PREFIX + "mem_buffers_MB", mem_buffers); 561 bundle.putDouble(METRIC_NAME_PREFIX + "mem_cached_MB", mem_cached); 562 bundle.putDouble(METRIC_NAME_PREFIX + "mem_slab_MB", mem_slab); 563 bundle.putDouble(METRIC_NAME_PREFIX + "mem_unreclaimable_MB", mem_unreclaimable); 564 bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_host_rss_MB", mem_crosvm_host_rss); 565 bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_host_pss_MB", mem_crosvm_host_pss); 566 bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_guest_rss_MB", mem_crosvm_guest_rss); 567 bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_guest_pss_MB", mem_crosvm_guest_pss); 568 if (listener.mKvm != null) { 569 double mem_protected_shared = (double) listener.mKvm.mProtectedShared / 1048576.0; 570 double mem_protected_hyp = (double) listener.mKvm.mProtectedHyp / 1048576.0; 571 bundle.putDouble(METRIC_NAME_PREFIX + "mem_protected_shared_MB", mem_protected_shared); 572 bundle.putDouble(METRIC_NAME_PREFIX + "mem_protected_hyp_MB", mem_protected_hyp); 573 } 574 mInstrumentation.sendStatus(0, bundle); 575 } 576 577 private static class MemoryUsageListener implements BenchmarkVmListener.InnerListener { MemoryUsageListener(Function<String, String> shellExecutor)578 MemoryUsageListener(Function<String, String> shellExecutor) { 579 mShellExecutor = shellExecutor; 580 } 581 582 public final Function<String, String> mShellExecutor; 583 584 public long mMemTotal; 585 public long mMemFree; 586 public long mMemAvailable; 587 public long mBuffers; 588 public long mCached; 589 public long mSlab; 590 591 public CrosvmStats mCrosvm; 592 public KvmVmStats mKvm; 593 594 @Override onPayloadReady(VirtualMachine vm, IBenchmarkService service)595 public void onPayloadReady(VirtualMachine vm, IBenchmarkService service) 596 throws RemoteException { 597 int vmPid = ProcessUtil.getCrosvmPid(Os.getpid(), "test_vm_mem_usage", mShellExecutor); 598 599 mMemTotal = service.getMemInfoEntry("MemTotal"); 600 mMemFree = service.getMemInfoEntry("MemFree"); 601 mMemAvailable = service.getMemInfoEntry("MemAvailable"); 602 mBuffers = service.getMemInfoEntry("Buffers"); 603 mCached = service.getMemInfoEntry("Cached"); 604 mSlab = service.getMemInfoEntry("Slab"); 605 mCrosvm = new CrosvmStats(vmPid, mShellExecutor); 606 mKvm = KvmVmStats.createIfSupported(vmPid, mShellExecutor); 607 } 608 } 609 610 @Test testMemoryReclaim()611 public void testMemoryReclaim() throws Exception { 612 final String vmName = "test_vm_mem_reclaim"; 613 VirtualMachineConfig config = 614 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json") 615 .setDebugLevel(DEBUG_LEVEL_NONE) 616 .setShouldUseHugepages(true) 617 .setMemoryBytes(256 * ONE_MEBI) 618 .build(); 619 VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config); 620 MemoryReclaimListener listener = 621 new MemoryReclaimListener(this::executeCommand, getContext()); 622 BenchmarkVmListener.create(listener).runToFinish(TAG, vm); 623 assertWithMessage("VM failed to start").that(listener.mPreCrosvm).isNotNull(); 624 assertWithMessage("Post trim stats not available").that(listener.mPostCrosvm).isNotNull(); 625 626 double mem_pre_crosvm_host_rss = (double) listener.mPreCrosvm.mHostRss / 1024.0; 627 double mem_pre_crosvm_host_pss = (double) listener.mPreCrosvm.mHostPss / 1024.0; 628 double mem_pre_crosvm_guest_rss = (double) listener.mPreCrosvm.mGuestRss / 1024.0; 629 double mem_pre_crosvm_guest_pss = (double) listener.mPreCrosvm.mGuestPss / 1024.0; 630 double mem_post_crosvm_host_rss = (double) listener.mPostCrosvm.mHostRss / 1024.0; 631 double mem_post_crosvm_host_pss = (double) listener.mPostCrosvm.mHostPss / 1024.0; 632 double mem_post_crosvm_guest_rss = (double) listener.mPostCrosvm.mGuestRss / 1024.0; 633 double mem_post_crosvm_guest_pss = (double) listener.mPostCrosvm.mGuestPss / 1024.0; 634 635 Bundle bundle = new Bundle(); 636 bundle.putDouble( 637 METRIC_NAME_PREFIX + "mem_pre_crosvm_host_rss_MB", mem_pre_crosvm_host_rss); 638 bundle.putDouble( 639 METRIC_NAME_PREFIX + "mem_pre_crosvm_host_pss_MB", mem_pre_crosvm_host_pss); 640 bundle.putDouble( 641 METRIC_NAME_PREFIX + "mem_pre_crosvm_guest_rss_MB", mem_pre_crosvm_guest_rss); 642 bundle.putDouble( 643 METRIC_NAME_PREFIX + "mem_pre_crosvm_guest_pss_MB", mem_pre_crosvm_guest_pss); 644 bundle.putDouble( 645 METRIC_NAME_PREFIX + "mem_post_crosvm_host_rss_MB", mem_post_crosvm_host_rss); 646 bundle.putDouble( 647 METRIC_NAME_PREFIX + "mem_post_crosvm_host_pss_MB", mem_post_crosvm_host_pss); 648 bundle.putDouble( 649 METRIC_NAME_PREFIX + "mem_post_crosvm_guest_rss_MB", mem_post_crosvm_guest_rss); 650 bundle.putDouble( 651 METRIC_NAME_PREFIX + "mem_post_crosvm_guest_pss_MB", mem_post_crosvm_guest_pss); 652 mInstrumentation.sendStatus(0, bundle); 653 } 654 655 private static class MemoryReclaimListener implements BenchmarkVmListener.InnerListener { MemoryReclaimListener(Function<String, String> shellExecutor, Context applicationCtx)656 MemoryReclaimListener(Function<String, String> shellExecutor, Context applicationCtx) { 657 mShellExecutor = shellExecutor; 658 mApplication = (Application) applicationCtx; 659 } 660 661 public final Function<String, String> mShellExecutor; 662 private final Application mApplication; 663 664 public CrosvmStats mPreCrosvm; 665 public CrosvmStats mPostCrosvm; 666 667 @Override 668 @SuppressWarnings("ReturnValueIgnored") onPayloadReady(VirtualMachine vm, IBenchmarkService service)669 public void onPayloadReady(VirtualMachine vm, IBenchmarkService service) 670 throws RemoteException { 671 int vmPid = 672 ProcessUtil.getCrosvmPid(Os.getpid(), "test_vm_mem_reclaim", mShellExecutor); 673 674 // Allocate 256MB of anonymous memory. This will fill all guest 675 // memory and cause swapping to start. 676 service.allocAnonMemory(256); 677 mPreCrosvm = new CrosvmStats(vmPid, mShellExecutor); 678 // Send a memory trim hint to cause memory reclaim. 679 mApplication.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL); 680 // Give time for the memory reclaim to do its work. 681 try { 682 Thread.sleep(isCuttlefish() ? 10000 : 5000); 683 } catch (InterruptedException e) { 684 Log.e(TAG, "Interrupted sleep:" + e); 685 Thread.currentThread().interrupt(); 686 } 687 mPostCrosvm = new CrosvmStats(vmPid, mShellExecutor); 688 } 689 } 690 691 private static class VsockListener implements BenchmarkVmListener.InnerListener { 692 private static final int NUM_BYTES_TO_TRANSFER = 48 * 1024 * 1024; 693 694 private final List<Double> mReadRates; 695 private final int mPort; 696 VsockListener(List<Double> readRates, int port)697 VsockListener(List<Double> readRates, int port) { 698 mReadRates = readRates; 699 mPort = port; 700 } 701 702 @Override onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)703 public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService) 704 throws RemoteException { 705 AtomicReference<Double> sendRate = new AtomicReference(); 706 707 int serverFd = benchmarkService.initVsockServer(mPort); 708 new Thread(() -> sendRate.set(runVsockClientAndSendData(vm))).start(); 709 benchmarkService.runVsockServerAndReceiveData(serverFd, NUM_BYTES_TO_TRANSFER); 710 711 Double rate = sendRate.get(); 712 if (rate == null) { 713 throw new IllegalStateException("runVsockClientAndSendData() failed"); 714 } 715 mReadRates.add(rate); 716 } 717 runVsockClientAndSendData(VirtualMachine vm)718 private double runVsockClientAndSendData(VirtualMachine vm) { 719 try { 720 ParcelFileDescriptor fd = vm.connectVsock(mPort); 721 double sendRate = 722 IoVsockHostNative.measureSendRate(fd.getFd(), NUM_BYTES_TO_TRANSFER); 723 fd.closeWithError("Cannot close socket file descriptor"); 724 return sendRate; 725 } catch (Exception e) { 726 Log.e(TAG, "Error inside runVsockClientAndSendData():" + e); 727 throw new RuntimeException(e); 728 } 729 } 730 } 731 732 @Test testRpcBinderLatency()733 public void testRpcBinderLatency() throws Exception { 734 final int NUM_WARMUPS = 10; 735 final int NUM_REQUESTS = 10_000; 736 737 VirtualMachineConfig config = 738 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so") 739 .setDebugLevel(DEBUG_LEVEL_NONE) 740 .setShouldUseHugepages(true) 741 .setShouldBoostUclamp(true) 742 .build(); 743 744 List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS); 745 for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) { 746 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_latency" + i, config); 747 TestResults testResults = 748 runVmTestService( 749 TAG, 750 vm, 751 (ts, tr) -> { 752 // Correctness check 753 tr.mAddInteger = ts.addInteger(123, 456); 754 755 // Warmup 756 for (int j = 0; j < NUM_WARMUPS; j++) { 757 ts.addInteger(j, j + 1); 758 } 759 760 // Count Fibonacci numbers, measure latency. 761 int a = 0; 762 int b = 1; 763 int c; 764 tr.mTimings = new long[NUM_REQUESTS]; 765 for (int j = 0; j < NUM_REQUESTS; j++) { 766 long start = System.nanoTime(); 767 c = ts.addInteger(a, b); 768 tr.mTimings[j] = System.nanoTime() - start; 769 a = b; 770 b = c; 771 } 772 }); 773 testResults.assertNoException(); 774 assertThat(testResults.mAddInteger).isEqualTo(579); 775 for (long duration : testResults.mTimings) { 776 requestLatencies.add((double) duration / NANO_TO_MICRO); 777 } 778 } 779 reportMetrics(requestLatencies, "latency/rpcbinder", "us"); 780 } 781 782 @Test testVsockLatency()783 public void testVsockLatency() throws Exception { 784 final int NUM_WARMUPS = 10; 785 final int NUM_REQUESTS = 10_000; 786 787 VirtualMachineConfig config = 788 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so") 789 .setDebugLevel(DEBUG_LEVEL_NONE) 790 .setShouldBoostUclamp(true) 791 .setShouldUseHugepages(true) 792 .build(); 793 794 List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS); 795 for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) { 796 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_latency" + i, config); 797 TestResults testResults = 798 runVmTestService( 799 TAG, 800 vm, 801 (ts, tr) -> { 802 ts.runEchoReverseServer(); 803 ParcelFileDescriptor pfd = 804 vm.connectVsock(ITestService.ECHO_REVERSE_PORT); 805 try (InputStream input = new AutoCloseInputStream(pfd); 806 OutputStream output = new AutoCloseOutputStream(pfd)) { 807 BufferedReader reader = 808 new BufferedReader(new InputStreamReader(input)); 809 Writer writer = new OutputStreamWriter(output); 810 811 // Correctness check. 812 writer.write("hello\n"); 813 writer.flush(); 814 tr.mFileContent = reader.readLine().trim(); 815 816 // Warmup. 817 for (int j = 0; j < NUM_WARMUPS; ++j) { 818 String text = "test" + j + "\n"; 819 writer.write(text); 820 writer.flush(); 821 reader.readLine(); 822 } 823 824 // Measured requests. 825 tr.mTimings = new long[NUM_REQUESTS]; 826 for (int j = 0; j < NUM_REQUESTS; j++) { 827 String text = "test" + j + "\n"; 828 long start = System.nanoTime(); 829 writer.write(text); 830 writer.flush(); 831 reader.readLine(); 832 tr.mTimings[j] = System.nanoTime() - start; 833 } 834 } 835 }); 836 testResults.assertNoException(); 837 assertThat(testResults.mFileContent).isEqualTo("olleh"); 838 for (long duration : testResults.mTimings) { 839 requestLatencies.add((double) duration / NANO_TO_MICRO); 840 } 841 } 842 reportMetrics(requestLatencies, "latency/vsock", "us"); 843 } 844 845 @Test testVmKillTime()846 public void testVmKillTime() throws Exception { 847 VirtualMachineConfig config = 848 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json") 849 .setDebugLevel(DEBUG_LEVEL_NONE) 850 .setShouldUseHugepages(true) 851 .build(); 852 List<Double> vmKillTime = new ArrayList<>(TEST_TRIAL_COUNT); 853 854 for (int i = 0; i < TEST_TRIAL_COUNT; ++i) { 855 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_kill_time" + i, config); 856 VmEventListener listener = 857 new VmEventListener() { 858 @Override 859 public void onPayloadReady(VirtualMachine vm) { 860 long start = System.nanoTime(); 861 try { 862 vm.stop(); 863 } catch (Exception e) { 864 Log.e(TAG, "Error in vm.stop():" + e); 865 throw new RuntimeException(e); 866 } 867 vmKillTime.add((double) (System.nanoTime() - start) / NANO_TO_MICRO); 868 super.onPayloadReady(vm); 869 } 870 }; 871 listener.runToFinish(TAG, vm); 872 } 873 reportMetrics(vmKillTime, "vm_kill_time", "microsecond"); 874 } 875 } 876