Report amount of used memory in a VM

Add a benchmark which boots Microdroid in non-debuggable mode and
reports the amount of used memory from /proc/meminfo. This is different
from an existing host-side test which also reports stats from
/proc/meminfo but runs in full-debug mode and with root privileges. This
new test is therefore more representative of a production environment.

Test: atest MicrodroidBenchmarks
Change-Id: I03a528bc09a4d4a08767653601f14bf93018c907
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
index afcf989..9fdf190 100644
--- a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -22,4 +22,7 @@
 
     /** Reads a file and returns the elapsed seconds for the reading. */
     double readFile(String filename, long fileSizeBytes, boolean isRand);
+
+    /** Returns an entry from /proc/meminfo. */
+    long getMemInfoEntry(String name);
 }
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 908da61..f236e47 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -272,4 +272,65 @@
             forceStop(vm);
         }
     }
+
+    @Test
+    public void testMemoryUsage() throws Exception {
+        final String vmName = "test_vm_mem_usage";
+        VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder(
+                "assets/vm_config_io.json");
+        VirtualMachineConfig config = builder.debugLevel(DebugLevel.NONE).memoryMib(256).build();
+        mInner.forceCreateNewVirtualMachine(vmName, config);
+        VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
+        MemoryUsageListener listener = new MemoryUsageListener();
+        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_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);
+        mInstrumentation.sendStatus(0, bundle);
+    }
+
+    private static class MemoryUsageListener extends VmEventListener {
+        public long mMemTotal;
+        public long mMemFree;
+        public long mMemAvailable;
+        public long mBuffers;
+        public long mCached;
+        public long mSlab;
+
+        @Override
+        public void onPayloadReady(VirtualMachine vm) {
+            try {
+                IBenchmarkService service =
+                        IBenchmarkService.Stub.asInterface(
+                                vm.connectToVsockServer(IBenchmarkService.SERVICE_PORT).get());
+
+                mMemTotal = service.getMemInfoEntry("MemTotal");
+                mMemFree = service.getMemInfoEntry("MemFree");
+                mMemAvailable = service.getMemInfoEntry("MemAvailable");
+                mBuffers = service.getMemInfoEntry("Buffers");
+                mCached = service.getMemInfoEntry("Cached");
+                mSlab = service.getMemInfoEntry("Slab");
+            } 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 5523579..2558a7d 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -25,10 +25,13 @@
 
 #include <binder_rpc_unstable.hpp>
 #include <chrono>
+#include <fstream>
 #include <random>
 #include <string>
 
 #include "android-base/logging.h"
+#include "android-base/parseint.h"
+#include "android-base/strings.h"
 
 using aidl::android::system::virtualmachineservice::IVirtualMachineService;
 using android::base::ErrnoError;
@@ -39,18 +42,35 @@
 namespace {
 constexpr uint64_t kBlockSizeBytes = 4096;
 
+template <typename T>
+static ndk::ScopedAStatus resultStatus(const T& result) {
+    if (!result.ok()) {
+        std::stringstream error;
+        error << result.error();
+        return ndk::ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_ARGUMENT,
+                                                                error.str().c_str());
+    }
+    return ndk::ScopedAStatus::ok();
+}
+
 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()) {
+        auto res = read_file(filename, fileSizeBytes, isRand);
+        if (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 resultStatus(res);
+    }
+
+    ndk::ScopedAStatus getMemInfoEntry(const std::string& name, int64_t* out) override {
+        auto value = read_meminfo_entry(name);
+        if (!value.ok()) {
+            return resultStatus(value);
+        }
+
+        *out = (int64_t)value.value();
         return ndk::ScopedAStatus::ok();
     }
 
@@ -87,6 +107,32 @@
         }
         return {((double)clock() - start) / CLOCKS_PER_SEC};
     }
+
+    Result<size_t> read_meminfo_entry(const std::string& stat) {
+        std::ifstream fs("/proc/meminfo");
+        if (!fs.is_open()) {
+            return Error() << "could not open /proc/meminfo";
+        }
+
+        std::string line;
+        while (std::getline(fs, line)) {
+            auto elems = android::base::Split(line, ":");
+            if (elems[0] != stat) continue;
+
+            std::string str = android::base::Trim(elems[1]);
+            if (android::base::EndsWith(str, " kB")) {
+                str = str.substr(0, str.length() - 3);
+            }
+
+            size_t value;
+            if (!android::base::ParseUint(str, &value)) {
+                return ErrnoError() << "failed to parse \"" << str << "\" as size_t";
+            }
+            return {value};
+        }
+
+        return Error() << "entry \"" << stat << "\" not found";
+    }
 };
 
 Result<void> run_io_benchmark_tests() {