blob: cba29a66865ac779e158bdbfef34749ca84814b0 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.microdroid.benchmark;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import android.app.Instrumentation;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineException;
import android.util.Log;
import com.android.microdroid.test.common.MetricsProcessor;
import com.android.microdroid.test.common.ProcessUtil;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
import com.android.microdroid.testservice.IBenchmarkService;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@RunWith(Parameterized.class)
public class MicrodroidBenchmarks extends MicrodroidDeviceTestBase {
private static final String TAG = "MicrodroidBenchmarks";
private static final String METRIC_NAME_PREFIX = getMetricPrefix() + "microdroid/";
private static final int IO_TEST_TRIAL_COUNT = 5;
@Rule public Timeout globalTimeout = Timeout.seconds(300);
private static final String APEX_ETC_FS = "/apex/com.android.virt/etc/fs/";
private static final double SIZE_MB = 1024.0 * 1024.0;
private static final double NANO_TO_MILLI = 1000000.0;
private static final String MICRODROID_IMG_PREFIX = "microdroid_";
private static final String MICRODROID_IMG_SUFFIX = ".img";
@Parameterized.Parameters(name = "protectedVm={0}")
public static Object[] protectedVmConfigs() {
return new Object[] {false, true};
}
@Parameterized.Parameter public boolean mProtectedVm;
private final MetricsProcessor mMetricsProcessor = new MetricsProcessor(METRIC_NAME_PREFIX);
private Instrumentation mInstrumentation;
@Before
public void setup() {
grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
prepareTestSetup(mProtectedVm);
mInstrumentation = getInstrumentation();
}
private boolean canBootMicrodroidWithMemory(int mem)
throws VirtualMachineException, InterruptedException, IOException {
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
.setMemoryMib(mem)
.build();
// returns true if succeeded at least once.
final int trialCount = 5;
for (int i = 0; i < trialCount; i++) {
forceCreateNewVirtualMachine("test_vm_minimum_memory", normalConfig);
if (tryBootVm(TAG, "test_vm_minimum_memory").payloadStarted) return true;
}
return false;
}
@Test
public void testMinimumRequiredRAM()
throws VirtualMachineException, InterruptedException, IOException {
assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
int lo = 16, hi = 512, minimum = 0;
boolean found = false;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (canBootMicrodroidWithMemory(mid)) {
found = true;
minimum = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
assertThat(found).isTrue();
Bundle bundle = new Bundle();
bundle.putInt(METRIC_NAME_PREFIX + "minimum_required_memory", minimum);
mInstrumentation.sendStatus(0, bundle);
}
@Test
public void testMicrodroidBootTime()
throws VirtualMachineException, InterruptedException, IOException {
assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
final int trialCount = 10;
List<Double> bootTimeMetrics = new ArrayList<>();
for (int i = 0; i < trialCount; i++) {
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
.setMemoryMib(256)
.build();
forceCreateNewVirtualMachine("test_vm_boot_time", normalConfig);
BootResult result = tryBootVm(TAG, "test_vm_boot_time");
assertThat(result.payloadStarted).isTrue();
bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
}
reportMetrics(bootTimeMetrics, "boot_time", "ms");
}
@Test
public void testMicrodroidDebugBootTime()
throws VirtualMachineException, InterruptedException, IOException {
assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
final int trialCount = 10;
List<Double> vmStartingTimeMetrics = new ArrayList<>();
List<Double> bootTimeMetrics = new ArrayList<>();
List<Double> bootloaderTimeMetrics = new ArrayList<>();
List<Double> kernelBootTimeMetrics = new ArrayList<>();
List<Double> userspaceBootTimeMetrics = new ArrayList<>();
for (int i = 0; i < trialCount; i++) {
// To grab boot events from log, set debug mode to FULL
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setMemoryMib(256)
.build();
forceCreateNewVirtualMachine("test_vm_boot_time_debug", normalConfig);
BootResult result = tryBootVm(TAG, "test_vm_boot_time_debug");
assertThat(result.payloadStarted).isTrue();
vmStartingTimeMetrics.add(result.getVMStartingElapsedNanoTime() / NANO_TO_MILLI);
bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
bootloaderTimeMetrics.add(result.getBootloaderElapsedNanoTime() / NANO_TO_MILLI);
kernelBootTimeMetrics.add(result.getKernelElapsedNanoTime() / NANO_TO_MILLI);
userspaceBootTimeMetrics.add(result.getUserspaceElapsedNanoTime() / NANO_TO_MILLI);
}
reportMetrics(vmStartingTimeMetrics, "vm_starting_time", "ms");
reportMetrics(bootTimeMetrics, "boot_time", "ms");
reportMetrics(bootloaderTimeMetrics, "bootloader_time", "ms");
reportMetrics(kernelBootTimeMetrics, "kernel_boot_time", "ms");
reportMetrics(userspaceBootTimeMetrics, "userspace_boot_time", "ms");
}
@Test
public void testMicrodroidImageSize() throws IOException {
Bundle bundle = new Bundle();
for (File file : new File(APEX_ETC_FS).listFiles()) {
String name = file.getName();
if (!name.startsWith(MICRODROID_IMG_PREFIX) || !name.endsWith(MICRODROID_IMG_SUFFIX)) {
continue;
}
String base =
name.substring(
MICRODROID_IMG_PREFIX.length(),
name.length() - MICRODROID_IMG_SUFFIX.length());
String metric = METRIC_NAME_PREFIX + "img_size_" + base + "_MB";
double size = Files.size(file.toPath()) / SIZE_MB;
bundle.putDouble(metric, size);
}
mInstrumentation.sendStatus(0, bundle);
}
@Test
public void testVsockTransferFromHostToVM() throws Exception {
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
int port = (mProtectedVm ? 5666 : 6666) + i;
String vmName = "test_vm_io_" + i;
VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
BenchmarkVmListener.create(new VsockListener(transferRates, port)).runToFinish(TAG, vm);
}
reportMetrics(transferRates, "vsock/transfer_host_to_vm", "mb_per_sec");
}
@Test
public void testVirtioBlkSeqReadRate() throws Exception {
testVirtioBlkReadRate(/*isRand=*/ false);
}
@Test
public void testVirtioBlkRandReadRate() throws Exception {
testVirtioBlkReadRate(/*isRand=*/ true);
}
private void testVirtioBlkReadRate(boolean isRand) throws Exception {
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
for (int i = 0; i < IO_TEST_TRIAL_COUNT + 1; ++i) {
if (i == 1) {
// Clear the first result because when the file was loaded the first time,
// the data also needs to be loaded from hard drive to host. This is
// not part of the virtio-blk IO throughput.
readRates.clear();
}
String vmName = "test_vm_io_" + i;
VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
BenchmarkVmListener.create(new VirtioBlkListener(readRates, isRand))
.runToFinish(TAG, vm);
}
reportMetrics(
readRates, isRand ? "virtio-blk/rand_read" : "virtio-blk/seq_read", "mb_per_sec");
}
private void reportMetrics(List<Double> metrics, String name, String unit) {
Map<String, Double> stats = mMetricsProcessor.computeStats(metrics, name, unit);
Bundle bundle = new Bundle();
for (Map.Entry<String, Double> entry : stats.entrySet()) {
bundle.putDouble(entry.getKey(), entry.getValue());
}
mInstrumentation.sendStatus(0, bundle);
}
private static class VirtioBlkListener implements BenchmarkVmListener.InnerListener {
private static final String FILENAME = APEX_ETC_FS + "microdroid_super.img";
private final List<Double> mReadRates;
private final boolean mIsRand;
VirtioBlkListener(List<Double> readRates, boolean isRand) {
mReadRates = readRates;
mIsRand = isRand;
}
@Override
public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)
throws RemoteException {
double readRate = benchmarkService.measureReadRate(FILENAME, mIsRand);
mReadRates.add(readRate);
}
}
private String executeCommand(String command) {
return runInShell(TAG, mInstrumentation.getUiAutomation(), command);
}
private static class CrosvmStats {
public final long mHostRss;
public final long mHostPss;
public final long mGuestRss;
public final long mGuestPss;
CrosvmStats(Function<String, String> shellExecutor) {
try {
List<Integer> crosvmPids =
ProcessUtil.getProcessMap(shellExecutor).entrySet().stream()
.filter(e -> e.getValue().contains("crosvm"))
.map(e -> e.getKey())
.collect(java.util.stream.Collectors.toList());
if (crosvmPids.size() != 1) {
throw new IllegalStateException(
"expected to find exactly one crosvm processes, found "
+ crosvmPids.size());
}
long hostRss = 0;
long hostPss = 0;
long guestRss = 0;
long guestPss = 0;
boolean hasGuestMaps = false;
for (ProcessUtil.SMapEntry entry :
ProcessUtil.getProcessSmaps(crosvmPids.get(0), shellExecutor)) {
long rss = entry.metrics.get("Rss");
long pss = entry.metrics.get("Pss");
if (entry.name.contains("crosvm_guest")) {
guestRss += rss;
guestPss += pss;
hasGuestMaps = true;
} else {
hostRss += rss;
hostPss += pss;
}
}
if (!hasGuestMaps) {
throw new IllegalStateException(
"found no crosvm_guest smap entry in crosvm process");
}
mHostRss = hostRss;
mHostPss = hostPss;
mGuestRss = guestRss;
mGuestPss = guestPss;
} catch (Exception e) {
Log.e(TAG, "Error inside onPayloadReady():" + e);
throw new RuntimeException(e);
}
}
}
@Test
public void testMemoryUsage() throws Exception {
final String vmName = "test_vm_mem_usage";
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
.setMemoryMib(256)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
MemoryUsageListener listener = new MemoryUsageListener(this::executeCommand);
BenchmarkVmListener.create(listener).runToFinish(TAG, vm);
double mem_overall = 256.0;
double mem_total = (double) listener.mMemTotal / 1024.0;
double mem_free = (double) listener.mMemFree / 1024.0;
double mem_avail = (double) listener.mMemAvailable / 1024.0;
double mem_buffers = (double) listener.mBuffers / 1024.0;
double mem_cached = (double) listener.mCached / 1024.0;
double mem_slab = (double) listener.mSlab / 1024.0;
double mem_crosvm_host_rss = (double) listener.mCrosvm.mHostRss / 1024.0;
double mem_crosvm_host_pss = (double) listener.mCrosvm.mHostPss / 1024.0;
double mem_crosvm_guest_rss = (double) listener.mCrosvm.mGuestRss / 1024.0;
double mem_crosvm_guest_pss = (double) listener.mCrosvm.mGuestPss / 1024.0;
double mem_kernel = mem_overall - mem_total;
double mem_used = mem_total - mem_free - mem_buffers - mem_cached - mem_slab;
double mem_unreclaimable = mem_total - mem_avail;
Bundle bundle = new Bundle();
bundle.putDouble(METRIC_NAME_PREFIX + "mem_kernel_MB", mem_kernel);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_used_MB", mem_used);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_buffers_MB", mem_buffers);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_cached_MB", mem_cached);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_slab_MB", mem_slab);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_unreclaimable_MB", mem_unreclaimable);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_host_rss_MB", mem_crosvm_host_rss);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_host_pss_MB", mem_crosvm_host_pss);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_guest_rss_MB", mem_crosvm_guest_rss);
bundle.putDouble(METRIC_NAME_PREFIX + "mem_crosvm_guest_pss_MB", mem_crosvm_guest_pss);
mInstrumentation.sendStatus(0, bundle);
}
private static class MemoryUsageListener implements BenchmarkVmListener.InnerListener {
MemoryUsageListener(Function<String, String> shellExecutor) {
mShellExecutor = shellExecutor;
}
public final Function<String, String> mShellExecutor;
public long mMemTotal;
public long mMemFree;
public long mMemAvailable;
public long mBuffers;
public long mCached;
public long mSlab;
public CrosvmStats mCrosvm;
@Override
public void onPayloadReady(VirtualMachine vm, IBenchmarkService service)
throws RemoteException {
mMemTotal = service.getMemInfoEntry("MemTotal");
mMemFree = service.getMemInfoEntry("MemFree");
mMemAvailable = service.getMemInfoEntry("MemAvailable");
mBuffers = service.getMemInfoEntry("Buffers");
mCached = service.getMemInfoEntry("Cached");
mSlab = service.getMemInfoEntry("Slab");
mCrosvm = new CrosvmStats(mShellExecutor);
}
}
@Test
public void testMemoryReclaim() throws Exception {
final String vmName = "test_vm_mem_reclaim";
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
.setMemoryMib(256)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
MemoryReclaimListener listener = new MemoryReclaimListener(this::executeCommand);
BenchmarkVmListener.create(listener).runToFinish(TAG, vm);
double mem_pre_crosvm_host_rss = (double) listener.mPreCrosvm.mHostRss / 1024.0;
double mem_pre_crosvm_host_pss = (double) listener.mPreCrosvm.mHostPss / 1024.0;
double mem_pre_crosvm_guest_rss = (double) listener.mPreCrosvm.mGuestRss / 1024.0;
double mem_pre_crosvm_guest_pss = (double) listener.mPreCrosvm.mGuestPss / 1024.0;
double mem_post_crosvm_host_rss = (double) listener.mPostCrosvm.mHostRss / 1024.0;
double mem_post_crosvm_host_pss = (double) listener.mPostCrosvm.mHostPss / 1024.0;
double mem_post_crosvm_guest_rss = (double) listener.mPostCrosvm.mGuestRss / 1024.0;
double mem_post_crosvm_guest_pss = (double) listener.mPostCrosvm.mGuestPss / 1024.0;
Bundle bundle = new Bundle();
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_pre_crosvm_host_rss_MB", mem_pre_crosvm_host_rss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_pre_crosvm_host_pss_MB", mem_pre_crosvm_host_pss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_pre_crosvm_guest_rss_MB", mem_pre_crosvm_guest_rss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_pre_crosvm_guest_pss_MB", mem_pre_crosvm_guest_pss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_post_crosvm_host_rss_MB", mem_post_crosvm_host_rss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_post_crosvm_host_pss_MB", mem_post_crosvm_host_pss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_post_crosvm_guest_rss_MB", mem_post_crosvm_guest_rss);
bundle.putDouble(
METRIC_NAME_PREFIX + "mem_post_crosvm_guest_pss_MB", mem_post_crosvm_guest_pss);
mInstrumentation.sendStatus(0, bundle);
}
private static class MemoryReclaimListener implements BenchmarkVmListener.InnerListener {
MemoryReclaimListener(Function<String, String> shellExecutor) {
mShellExecutor = shellExecutor;
}
public final Function<String, String> mShellExecutor;
public CrosvmStats mPreCrosvm;
public CrosvmStats mPostCrosvm;
@Override
@SuppressWarnings("ReturnValueIgnored")
public void onPayloadReady(VirtualMachine vm, IBenchmarkService service)
throws RemoteException {
// Allocate 256MB of anonymous memory. This will fill all guest
// memory and cause swapping to start.
service.allocAnonMemory(256);
mPreCrosvm = new CrosvmStats(mShellExecutor);
// Send a memory trim hint to cause memory reclaim.
mShellExecutor.apply("am send-trim-memory " + Process.myPid() + " RUNNING_CRITICAL");
// Give time for the memory reclaim to do its work.
try {
Thread.sleep(isCuttlefish() ? 10000 : 5000);
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted sleep:" + e);
Thread.currentThread().interrupt();
}
mPostCrosvm = new CrosvmStats(mShellExecutor);
}
}
private static class VsockListener implements BenchmarkVmListener.InnerListener {
private static final int NUM_BYTES_TO_TRANSFER = 48 * 1024 * 1024;
private final List<Double> mReadRates;
private final int mPort;
VsockListener(List<Double> readRates, int port) {
mReadRates = readRates;
mPort = port;
}
@Override
public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)
throws RemoteException {
AtomicReference<Double> sendRate = new AtomicReference();
int serverFd = benchmarkService.initVsockServer(mPort);
new Thread(() -> sendRate.set(runVsockClientAndSendData(vm))).start();
benchmarkService.runVsockServerAndReceiveData(serverFd, NUM_BYTES_TO_TRANSFER);
Double rate = sendRate.get();
if (rate == null) {
throw new IllegalStateException("runVsockClientAndSendData() failed");
}
mReadRates.add(rate);
}
private double runVsockClientAndSendData(VirtualMachine vm) {
try {
ParcelFileDescriptor fd = vm.connectVsock(mPort);
double sendRate =
IoVsockHostNative.measureSendRate(fd.getFd(), NUM_BYTES_TO_TRANSFER);
fd.closeWithError("Cannot close socket file descriptor");
return sendRate;
} catch (Exception e) {
Log.e(TAG, "Error inside runVsockClientAndSendData():" + e);
throw new RuntimeException(e);
}
}
}
}