diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl new file mode 100644 index 00000000..afcf9893 --- /dev/null +++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl @@ -0,0 +1,25 @@ +/* + * Copyright 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.testservice; + +/** {@hide} */ +interface IBenchmarkService { + const int SERVICE_PORT = 5677; + + /** Reads a file and returns the elapsed seconds for the reading. */ + double readFile(String filename, long fileSizeBytes, boolean isRand); +} diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp index e6d5b833..2111620a 100644 --- a/tests/benchmark/Android.bp +++ b/tests/benchmark/Android.bp @@ -12,6 +12,7 @@ android_test { "MicroroidDeviceTestHelper", "androidx.test.runner", "androidx.test.ext.junit", + "com.android.microdroid.testservice-java", "truth-prebuilt", ], libs: ["android.system.virtualmachine"], @@ -24,4 +25,12 @@ android_test { cc_library_shared { name: "MicrodroidBenchmarkNativeLib", srcs: ["src/native/benchmarkbinary.cpp"], + shared_libs: [ + "android.system.virtualmachineservice-ndk", + "com.android.microdroid.testservice-ndk", + "libbase", + "libbinder_ndk", + "libbinder_rpc_unstable", + "liblog", + ], } diff --git a/tests/benchmark/assets/vm_config.json b/tests/benchmark/assets/vm_config.json index 67e3d216..e8f43e03 100644 --- a/tests/benchmark/assets/vm_config.json +++ b/tests/benchmark/assets/vm_config.json @@ -4,7 +4,10 @@ }, "task": { "type": "microdroid_launcher", - "command": "MicrodroidBenchmarkNativeLib.so" + "command": "MicrodroidBenchmarkNativeLib.so", + "args": [ + "no_io" + ] }, "export_tombstones": true } diff --git a/tests/benchmark/assets/vm_config_io.json b/tests/benchmark/assets/vm_config_io.json new file mode 100644 index 00000000..1a5a9e54 --- /dev/null +++ b/tests/benchmark/assets/vm_config_io.json @@ -0,0 +1,18 @@ +{ + "os": { + "name": "microdroid" + }, + "task": { + "type": "microdroid_launcher", + "command": "MicrodroidBenchmarkNativeLib.so", + "args": [ + "io" + ] + }, + "apexes": [ + { + "name": "com.android.virt" + } + ], + "export_tombstones": true +} diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java index 3731e130..7ee2d394 100644 --- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java +++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.microdroid.benchmark; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -22,11 +23,13 @@ import static com.google.common.truth.TruthJUnit.assume; import android.app.Instrumentation; import android.os.Bundle; +import android.system.virtualmachine.VirtualMachine; import android.system.virtualmachine.VirtualMachineConfig; import android.system.virtualmachine.VirtualMachineConfig.DebugLevel; import android.system.virtualmachine.VirtualMachineException; import com.android.microdroid.test.MicrodroidDeviceTestBase; +import com.android.microdroid.testservice.IBenchmarkService; import org.junit.Before; import org.junit.Rule; @@ -38,10 +41,13 @@ 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; @RunWith(Parameterized.class) public class MicrodroidBenchmarks extends MicrodroidDeviceTestBase { private static final String TAG = "MicrodroidBenchmarks"; + private static final int VIRTIO_BLK_TRIAL_COUNT = 5; @Rule public Timeout globalTimeout = Timeout.seconds(300); @@ -159,12 +165,100 @@ public class MicrodroidBenchmarks extends MicrodroidDeviceTestBase { continue; } - String base = name.substring(MICRODROID_IMG_PREFIX.length(), - name.length() - MICRODROID_IMG_SUFFIX.length()); - String metric = "avf_perf/microdroid/img_size_" + base + "_MB"; + String base = + name.substring( + MICRODROID_IMG_PREFIX.length(), + name.length() - MICRODROID_IMG_SUFFIX.length()); + String metric = "avf_perf/microdroid/img_size_" + base + "_MB" + "+" + name; double size = Files.size(file.toPath()) / SIZE_MB; bundle.putDouble(metric, size); } mInstrumentation.sendStatus(0, bundle); } + + @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.Builder builder = + mInner.newVmConfigBuilder("assets/vm_config_io.json"); + VirtualMachineConfig config = builder.debugLevel(DebugLevel.FULL).build(); + List readRates = new ArrayList<>(); + + for (int i = 0; i < VIRTIO_BLK_TRIAL_COUNT; ++i) { + String vmName = "test_vm_io_" + i; + mInner.forceCreateNewVirtualMachine(vmName, config); + VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName); + VirtioBlkVmEventListener listener = new VirtioBlkVmEventListener(readRates, isRand); + listener.runToFinish(TAG, vm); + } + reportMetrics(readRates, isRand); + } + + private void reportMetrics(List readRates, boolean isRand) { + double sum = 0; + for (double rate : readRates) { + sum += rate; + } + double mean = sum / readRates.size(); + double sqSum = 0; + for (double rate : readRates) { + sqSum += (rate - mean) * (rate - mean); + } + double stdDev = Math.sqrt(sqSum / (readRates.size() - 1)); + + Bundle bundle = new Bundle(); + String metricNamePrefix = + "avf_perf/virtio-blk/" + + (mProtectedVm ? "protected-vm/" : "unprotected-vm/") + + (isRand ? "rand_read_" : "seq_read_"); + String unit = "_mb_per_sec"; + + bundle.putDouble(metricNamePrefix + "mean" + unit, mean); + bundle.putDouble(metricNamePrefix + "std" + unit, stdDev); + mInstrumentation.sendStatus(0, bundle); + } + + private static class VirtioBlkVmEventListener extends VmEventListener { + private static final String FILENAME = APEX_ETC_FS + "microdroid_super.img"; + + private final long mFileSizeBytes; + private final List mReadRates; + private final boolean mIsRand; + + VirtioBlkVmEventListener(List readRates, boolean isRand) { + File file = new File(FILENAME); + try { + mFileSizeBytes = Files.size(file.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + assertThat(mFileSizeBytes).isGreaterThan((long) SIZE_MB); + mReadRates = readRates; + mIsRand = isRand; + } + + @Override + public void onPayloadReady(VirtualMachine vm) { + try { + IBenchmarkService benchmarkService = + IBenchmarkService.Stub.asInterface( + vm.connectToVsockServer(IBenchmarkService.SERVICE_PORT).get()); + double elapsedSeconds = + benchmarkService.readFile(FILENAME, mFileSizeBytes, mIsRand); + double fileSizeMb = mFileSizeBytes / SIZE_MB; + mReadRates.add(fileSizeMb / elapsedSeconds); + } catch (Exception e) { + throw new RuntimeException(e); + } + forceStop(vm); + } + } } diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp index b5ec49cc..6a5b7641 100644 --- a/tests/benchmark/src/native/benchmarkbinary.cpp +++ b/tests/benchmark/src/native/benchmarkbinary.cpp @@ -13,11 +13,123 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +#include +#include +#include +#include +#include +#include +#include #include -extern "C" int android_native_main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { - // do nothing for now; just leave it alive. good night. - for (;;) { - sleep(1000); +#include +#include +#include +#include + +#include "android-base/logging.h" + +using aidl::android::system::virtualmachineservice::IVirtualMachineService; +using android::base::ErrnoError; +using android::base::Error; +using android::base::Result; +using android::base::unique_fd; + +namespace { +constexpr uint64_t kBlockSizeBytes = 4096; + +class IOBenchmarkService : public aidl::com::android::microdroid::testservice::BnBenchmarkService { +public: + ndk::ScopedAStatus readFile(const std::string& filename, int64_t fileSizeBytes, bool isRand, + double* out) override { + if (auto res = read_file(filename, fileSizeBytes, isRand); res.ok()) { + *out = res.value(); + } else { + std::stringstream error; + error << "Failed reading file: " << res.error(); + return ndk::ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_ARGUMENT, + error.str().c_str()); + } + return ndk::ScopedAStatus::ok(); } + +private: + /** Returns the elapsed seconds for reading the file. */ + Result read_file(const std::string& filename, int64_t fileSizeBytes, bool is_rand) { + const int64_t block_count = fileSizeBytes / kBlockSizeBytes; + std::vector offsets; + if (is_rand) { + std::mt19937 rd{std::random_device{}()}; + offsets.reserve(block_count); + for (auto i = 0; i < block_count; ++i) offsets.push_back(i * kBlockSizeBytes); + std::shuffle(offsets.begin(), offsets.end(), rd); + } + char buf[kBlockSizeBytes]; + + clock_t start = clock(); + unique_fd fd(open(filename.c_str(), O_RDONLY)); + if (fd.get() == -1) { + return ErrnoError() << "Read: opening " << filename << " failed"; + } + for (auto i = 0; i < block_count; ++i) { + if (is_rand) { + if (lseek(fd.get(), offsets[i], SEEK_SET) == -1) { + return ErrnoError() << "failed to lseek"; + } + } + auto bytes = read(fd.get(), buf, kBlockSizeBytes); + if (bytes == 0) { + return Error() << "unexpected end of file"; + } else if (bytes == -1) { + return ErrnoError() << "failed to read"; + } + } + return {((double)clock() - start) / CLOCKS_PER_SEC}; + } +}; + +Result run_io_benchmark_tests() { + auto test_service = ndk::SharedRefBase::make(); + auto callback = []([[maybe_unused]] void* param) { + // Tell microdroid_manager that we're ready. + // If we can't, abort in order to fail fast - the host won't proceed without + // receiving the onReady signal. + ndk::SpAIBinder binder( + RpcClient(VMADDR_CID_HOST, IVirtualMachineService::VM_BINDER_SERVICE_PORT)); + auto vm_service = IVirtualMachineService::fromBinder(binder); + if (vm_service == nullptr) { + LOG(ERROR) << "failed to connect VirtualMachineService\n"; + abort(); + } + if (auto status = vm_service->notifyPayloadReady(); !status.isOk()) { + LOG(ERROR) << "failed to notify payload ready to virtualizationservice: " + << status.getDescription(); + abort(); + } + }; + + if (!RunRpcServerCallback(test_service->asBinder().get(), test_service->SERVICE_PORT, callback, + nullptr)) { + return Error() << "RPC Server failed to run"; + } + return {}; +} +} // Anonymous namespace + +extern "C" int android_native_main([[maybe_unused]] int argc, char* argv[]) { + if (strcmp(argv[1], "no_io") == 0) { + // do nothing for now; just leave it alive. good night. + for (;;) { + sleep(1000); + } + } else if (strcmp(argv[1], "io") == 0) { + if (auto res = run_io_benchmark_tests(); res.ok()) { + return 0; + } else { + LOG(ERROR) << "IO benchmark test failed: " << res.error() << "\n"; + return 1; + } + } + return 0; } diff --git a/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java index a2c43d72..1f576345 100644 --- a/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java +++ b/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java @@ -139,7 +139,7 @@ public abstract class MicrodroidDeviceTestBase { protected abstract static class VmEventListener implements VirtualMachineCallback { private ExecutorService mExecutorService = Executors.newSingleThreadExecutor(); - void runToFinish(String logTag, VirtualMachine vm) + public void runToFinish(String logTag, VirtualMachine vm) throws VirtualMachineException, InterruptedException { vm.setCallback(mExecutorService, this); vm.run(); @@ -148,7 +148,7 @@ public abstract class MicrodroidDeviceTestBase { mExecutorService.awaitTermination(300, TimeUnit.SECONDS); } - void forceStop(VirtualMachine vm) { + protected void forceStop(VirtualMachine vm) { try { vm.clearCallback(); vm.stop();