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);
+    }
+}