Report breakdown of Microdroid userspace memory usage

Dump the list of running processes in Microdroid using 'ps -A' and their
memory usage statistics from '/proc/<pid>/smaps_rollup', and report them
as metrics. Do the same for '/proc/meminfo'.

This requires root, so it is done from the host-side Microdroid tests.
Not ideal approach as the reported stats include adbd and its children,
but it is a useful set of data nonetheless.

Test: atest MicrodroidHostTestCases
Change-Id: I24368f7e647a083488ff0d9f485e618876d27f3a
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
index fb92467..76bfafb 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
@@ -36,6 +36,7 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.FileUtil;
@@ -58,6 +59,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -80,6 +82,7 @@
 
     @Rule public TestLogData mTestLogs = new TestLogData();
     @Rule public TestName mTestName = new TestName();
+    @Rule public TestMetrics mMetrics = new TestMetrics();
 
     private int minMemorySize() throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(getDevice());
@@ -545,6 +548,92 @@
         shutdownMicrodroid(getDevice(), cid);
     }
 
+    private static class ProcessInfo {
+        public final String mName;
+        public final int mPid;
+
+        ProcessInfo(String name, int pid) {
+            mName = name;
+            mPid = pid;
+        }
+    }
+
+    private Map<String, Long> parseMemInfo(String file) {
+        Map<String, Long> stats = new HashMap<>();
+        file.lines().forEach(line -> {
+            if (line.endsWith(" kB")) line = line.substring(0, line.length() - 3);
+
+            String[] elems = line.split(":");
+            assertThat(elems.length).isEqualTo(2);
+            stats.put(elems[0].trim(), Long.parseLong(elems[1].trim()));
+        });
+        return stats;
+    }
+
+    private String skipFirstLine(String str) {
+        int index = str.indexOf("\n");
+        return (index < 0) ? "" : str.substring(index + 1);
+    }
+
+    private List<ProcessInfo> getRunningProcessesList() {
+        List<ProcessInfo> list = new ArrayList<ProcessInfo>();
+        skipFirstLine(runOnMicrodroid("ps", "-Ao", "PID,NAME")).lines().forEach(ps -> {
+            // Each line is '  <pid> <name>'.
+            ps = ps.trim();
+            int space = ps.indexOf(" ");
+            list.add(new ProcessInfo(
+                    ps.substring(space + 1),
+                    Integer.parseInt(ps.substring(0, space))));
+        });
+
+        return list;
+    }
+
+    private Map<String, Long> getProcMemInfo() {
+        return parseMemInfo(runOnMicrodroid("cat", "/proc/meminfo"));
+    }
+
+    private Map<String, Long> getProcSmapsRollup(int pid) {
+        String path = "/proc/" + pid + "/smaps_rollup";
+        return  parseMemInfo(skipFirstLine(runOnMicrodroid("cat", path, "||", "true")));
+    }
+
+    @Test
+    public void testMicrodroidRamUsage() throws Exception {
+        final String configPath = "assets/vm_config.json";
+        final String cid =
+                startMicrodroid(
+                        getDevice(),
+                        getBuild(),
+                        APK_NAME,
+                        PACKAGE_NAME,
+                        configPath,
+                        /* debug */ true,
+                        minMemorySize(),
+                        Optional.of(NUM_VCPUS),
+                        Optional.of(CPU_AFFINITY));
+        adbConnectToMicrodroid(getDevice(), cid);
+        waitForBootComplete();
+        rootMicrodroid();
+
+        for (Map.Entry<String, Long> stat : getProcMemInfo().entrySet()) {
+            mMetrics.addTestMetric(
+                    "avf_perf/microdroid/meminfo/" + stat.getKey().toLowerCase(),
+                    stat.getValue().toString());
+        }
+
+        for (ProcessInfo proc : getRunningProcessesList()) {
+            for (Map.Entry<String, Long> stat : getProcSmapsRollup(proc.mPid).entrySet()) {
+                String name = stat.getKey().toLowerCase();
+                mMetrics.addTestMetric(
+                        "avf_perf/microdroid/smaps/" + name + "/" + proc.mName,
+                        stat.getValue().toString());
+            }
+        }
+
+        shutdownMicrodroid(getDevice(), cid);
+    }
+
     @Test
     public void testCustomVirtualMachinePermission()
             throws DeviceNotAvailableException, IOException, JSONException {