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