Add microdroid minimum ram test
testMinimumRequiredRAM will find minimum required RAM to boot Microdroid
by bisecting. The result will be collected as a metric.
Bug: 231105297
Test: atest MicrodroidBenchmarks
Change-Id: Icb34a2cc2f9d906ebde661be86ac9b802288de5b
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
new file mode 100644
index 0000000..f333d03
--- /dev/null
+++ b/tests/benchmark/Android.bp
@@ -0,0 +1,26 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "MicrodroidBenchmarkApp",
+ test_suites: [
+ "general-tests",
+ ],
+ srcs: ["src/java/**/*.java"],
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+ "truth-prebuilt",
+ ],
+ libs: ["android.system.virtualmachine"],
+ jni_libs: ["MicrodroidBenchmarkNativeLib"],
+ platform_apis: true,
+ use_embedded_native_libs: true,
+ compile_multilib: "64",
+}
+
+cc_library_shared {
+ name: "MicrodroidBenchmarkNativeLib",
+ srcs: ["src/native/benchmarkbinary.cpp"],
+}
diff --git a/tests/benchmark/AndroidManifest.xml b/tests/benchmark/AndroidManifest.xml
new file mode 100644
index 0000000..ff18130
--- /dev/null
+++ b/tests/benchmark/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.microdroid.benchmark">
+ <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+ <application>
+ <uses-library android:name="android.system.virtualmachine" android:required="false" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.microdroid.benchmark"
+ android:label="Microdroid Benchmark" />
+</manifest>
diff --git a/tests/benchmark/AndroidTest.xml b/tests/benchmark/AndroidTest.xml
new file mode 100644
index 0000000..e908077
--- /dev/null
+++ b/tests/benchmark/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs sample instrumentation test.">
+ <option name="config-descriptor:metadata" key="component" value="security" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="MicrodroidBenchmarkApp.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option
+ name="run-command"
+ value="pm grant com.android.microdroid.benchmark android.permission.MANAGE_VIRTUAL_MACHINE" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.microdroid.benchmark" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="shell-timeout" value="300000" />
+ <option name="test-timeout" value="300000" />
+ </test>
+</configuration>
diff --git a/tests/benchmark/assets/vm_config.json b/tests/benchmark/assets/vm_config.json
new file mode 100644
index 0000000..67e3d21
--- /dev/null
+++ b/tests/benchmark/assets/vm_config.json
@@ -0,0 +1,10 @@
+{
+ "os": {
+ "name": "microdroid"
+ },
+ "task": {
+ "type": "microdroid_launcher",
+ "command": "MicrodroidBenchmarkNativeLib.so"
+ },
+ "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
new file mode 100644
index 0000000..bc99e6e
--- /dev/null
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -0,0 +1,280 @@
+/*
+ * 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import static org.junit.Assume.assumeNoException;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemProperties;
+import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.DeathReason;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import androidx.annotation.CallSuper;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.After;
+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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(Parameterized.class)
+public class MicrodroidBenchmarks {
+ private static final String TAG = "MicrodroidBenchmarks";
+
+ @Rule public Timeout globalTimeout = Timeout.seconds(300);
+
+ private static final String KERNEL_VERSION = SystemProperties.get("ro.kernel.version");
+
+ /** Copy output from the VM to logcat. This is helpful when things go wrong. */
+ private static void logVmOutput(InputStream vmOutputStream, String name) {
+ new Thread(
+ () -> {
+ try {
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(vmOutputStream));
+ String line;
+ while ((line = reader.readLine()) != null
+ && !Thread.interrupted()) {
+ Log.i(TAG, name + ": " + line);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, name, e);
+ }
+ }).start();
+ }
+
+ private static class Inner {
+ public boolean mProtectedVm;
+ public Context mContext;
+ public VirtualMachineManager mVmm;
+ public VirtualMachine mVm;
+
+ Inner(boolean protectedVm) {
+ mProtectedVm = protectedVm;
+ }
+
+ /** Create a new VirtualMachineConfig.Builder with the parameterized protection mode. */
+ public VirtualMachineConfig.Builder newVmConfigBuilder(String payloadConfigPath) {
+ return new VirtualMachineConfig.Builder(mContext, payloadConfigPath)
+ .protectedVm(mProtectedVm);
+ }
+ }
+
+ @Parameterized.Parameters(name = "protectedVm={0}")
+ public static Object[] protectedVmConfigs() {
+ return new Object[] {false, true};
+ }
+
+ @Parameterized.Parameter public boolean mProtectedVm;
+
+ private boolean mPkvmSupported = false;
+ private Inner mInner;
+
+ private Instrumentation mInstrumentation;
+
+ @Before
+ public void setup() {
+ // In case when the virt APEX doesn't exist on the device, classes in the
+ // android.system.virtualmachine package can't be loaded. Therefore, before using the
+ // classes, check the existence of a class in the package and skip this test if not exist.
+ try {
+ Class.forName("android.system.virtualmachine.VirtualMachineManager");
+ mPkvmSupported = true;
+ } catch (ClassNotFoundException e) {
+ assumeNoException(e);
+ return;
+ }
+ if (mProtectedVm) {
+ assume().withMessage("Skip where protected VMs aren't support")
+ .that(HypervisorProperties.hypervisor_protected_vm_supported().orElse(false))
+ .isTrue();
+ } else {
+ assume().withMessage("Skip where VMs aren't support")
+ .that(HypervisorProperties.hypervisor_vm_supported().orElse(false))
+ .isTrue();
+ }
+ mInner = new Inner(mProtectedVm);
+ mInner.mContext = ApplicationProvider.getApplicationContext();
+ mInner.mVmm = VirtualMachineManager.getInstance(mInner.mContext);
+ mInstrumentation = getInstrumentation();
+ }
+
+ @After
+ public void cleanup() throws VirtualMachineException {
+ if (!mPkvmSupported) {
+ return;
+ }
+ if (mInner == null) {
+ return;
+ }
+ if (mInner.mVm == null) {
+ return;
+ }
+ mInner.mVm.stop();
+ mInner.mVm.delete();
+ }
+
+ private abstract static class VmEventListener implements VirtualMachineCallback {
+ private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+
+ void runToFinish(VirtualMachine vm) throws VirtualMachineException, InterruptedException {
+ vm.setCallback(mExecutorService, this);
+ vm.run();
+ logVmOutput(vm.getConsoleOutputStream(), "Console");
+ logVmOutput(vm.getLogOutputStream(), "Log");
+ mExecutorService.awaitTermination(300, TimeUnit.SECONDS);
+ }
+
+ void forceStop(VirtualMachine vm) {
+ try {
+ vm.clearCallback();
+ vm.stop();
+ mExecutorService.shutdown();
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {}
+
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {}
+
+ @Override
+ public void onPayloadFinished(VirtualMachine vm, int exitCode) {}
+
+ @Override
+ public void onError(VirtualMachine vm, int errorCode, String message) {}
+
+ @Override
+ @CallSuper
+ public void onDied(VirtualMachine vm, @DeathReason int reason) {
+ mExecutorService.shutdown();
+ }
+ }
+
+ private static class BootResult {
+ public final boolean payloadStarted;
+ public final int deathReason;
+
+ BootResult(boolean payloadStarted, int deathReason) {
+ this.payloadStarted = payloadStarted;
+ this.deathReason = deathReason;
+ }
+ }
+
+ private BootResult tryBootVm(String vmName)
+ throws VirtualMachineException, InterruptedException {
+ mInner.mVm = mInner.mVmm.get(vmName); // re-load the vm before running tests
+ final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
+ final CompletableFuture<Integer> deathReason = new CompletableFuture<>();
+ VmEventListener listener =
+ new VmEventListener() {
+ @Override
+ public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+ payloadStarted.complete(true);
+ forceStop(vm);
+ }
+
+ @Override
+ public void onDied(VirtualMachine vm, int reason) {
+ deathReason.complete(reason);
+ super.onDied(vm, reason);
+ }
+ };
+ listener.runToFinish(mInner.mVm);
+ return new BootResult(
+ payloadStarted.getNow(false), deathReason.getNow(DeathReason.INFRASTRUCTURE_ERROR));
+ }
+
+ private boolean canBootMicrodroidWithMemory(int mem)
+ throws VirtualMachineException, InterruptedException, IOException {
+ final int trialCount = 5;
+
+ // returns true if succeeded at least once.
+ for (int i = 0; i < trialCount; i++) {
+ VirtualMachine existingVm = mInner.mVmm.get("test_vm_minimum_memory");
+ if (existingVm != null) {
+ existingVm.delete();
+ }
+
+ VirtualMachineConfig.Builder builder =
+ mInner.newVmConfigBuilder("assets/vm_config.json");
+ VirtualMachineConfig normalConfig =
+ builder.debugLevel(DebugLevel.FULL).memoryMib(mem).build();
+ mInner.mVmm.create("test_vm_minimum_memory", normalConfig);
+
+ if (tryBootVm("test_vm_minimum_memory").payloadStarted) return true;
+ }
+
+ return false;
+ }
+
+ @Test
+ public void testMinimumRequiredRAM()
+ throws VirtualMachineException, InterruptedException, IOException {
+ int lo = 16, hi = 512, minimum = 0;
+ boolean found = false;
+
+ // TODO(b/236672526): giving inefficient memory to pVM sometimes causes host crash.
+ assume().withMessage("Skip on pVM. b/236672526").that(mProtectedVm).isFalse();
+
+ 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("microdroid_minimum_required_memory", minimum);
+ mInstrumentation.sendStatus(0, bundle);
+ }
+}
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
new file mode 100644
index 0000000..b5ec49c
--- /dev/null
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+#include <unistd.h>
+
+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);
+ }
+}