Added dumpsys meminfo collector and unit test

Bug: 132612793
Test: atest CollectorsHelperTest:com.android.helpers.tests.DumpsysMeminfoHelperTest
atest CollectorDeviceLibTest:android.device.collectors.DumpsysMeminfoListenerTest
Change-Id: Iba128af3cb207c0150bb04088b40bd10829199b5
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/DumpsysMeminfoHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/DumpsysMeminfoHelper.java
new file mode 100644
index 0000000..168b3a2
--- /dev/null
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/DumpsysMeminfoHelper.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2019 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.helpers;
+
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This is a collector helper to use adb "dumpsys meminfo -a" command to get important memory
+ * metrics like PSS, Shared Dirty, Private Dirty, e.t.c. for the specified packages
+ */
+public class DumpsysMeminfoHelper implements ICollectorHelper<Long> {
+
+    private static final String TAG = DumpsysMeminfoHelper.class.getSimpleName();
+
+    private static final String SEPARATOR = "\\s+";
+    private static final String LINE_SEPARATOR = "\\n";
+
+    private static final String DUMPSYS_MEMINFO_CMD = "dumpsys meminfo -a %s";
+    private static final String PIDOF_CMD = "pidof %s";
+
+    private static final String METRIC_SOURCE = "dumpsys";
+    private static final String METRIC_UNIT = "kb";
+
+    // Prefixes of the lines in the output of "dumpsys meminfo -a" command that will be parsed
+    private static final String NATIVE_HEAP_PREFIX = "Native Heap";
+    private static final String DALVIK_HEAP_PREFIX = "Dalvik Heap";
+    private static final String TOTAL_PREFIX = "TOTAL";
+
+    // The metric names corresponding to the columns in the output of "dumpsys meminfo -a" command
+    private static final String PSS_TOTAL = "pss_total";
+    private static final String SHARED_DIRTY = "shared_dirty";
+    private static final String PRIVATE_DIRTY = "private_dirty";
+    private static final String HEAP_SIZE = "heap_size";
+    private static final String HEAP_ALLOC = "heap_alloc";
+    private static final String PSS = "pss";
+
+    // Mapping from prefixes of lines to metric category names
+    private static final Map<String, String> CATEGORIES =
+            Stream.of(
+                    new String[][] {
+                            {NATIVE_HEAP_PREFIX, "native"},
+                            {DALVIK_HEAP_PREFIX, "dalvik"},
+                            {TOTAL_PREFIX, "total"},
+                    }).collect(Collectors.toMap(data -> data[0], data -> data[1]));
+
+    // Mapping from metric keys to its column index (exclude prefix) in dumpsys meminfo output.
+    // The index might change across different Android releases
+    private static final Map<String, Integer> METRIC_POSITIONS =
+            Stream.of(
+                    new Object[][] {
+                            {PSS_TOTAL, 0},
+                            {SHARED_DIRTY, 2},
+                            {PRIVATE_DIRTY, 3},
+                            {HEAP_SIZE, 7},
+                            {HEAP_ALLOC, 8},
+                    })
+            .collect(Collectors.toMap(data -> (String) data[0], data -> (Integer) data[1]));
+
+    private String[] mProcessNames = {};
+    private UiDevice mUiDevice;
+
+    public void setUp(String... processNames) {
+        if (processNames == null) {
+            return;
+        }
+        mProcessNames = processNames;
+    }
+
+    @Override
+    public boolean startCollecting() {
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        return true;
+    }
+
+    @Override
+    public Map<String, Long> getMetrics() {
+        Map<String, Long> metrics = new HashMap<>();
+        for (String processName : mProcessNames) {
+            String rawOutput = getRawDumpsysMeminfo(processName);
+            metrics.putAll(parseMetrics(processName, rawOutput));
+        }
+        return metrics;
+    }
+
+    @Override
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    private String getRawDumpsysMeminfo(String processName) {
+        if (isEmpty(processName)) {
+            return "";
+        }
+        try {
+            String pidStr = mUiDevice.executeShellCommand(String.format(PIDOF_CMD, processName));
+            if (isEmpty(pidStr)) {
+                return "";
+            }
+            return mUiDevice.executeShellCommand(String.format(DUMPSYS_MEMINFO_CMD, pidStr));
+        } catch (IOException e) {
+            Log.e(TAG, String.format("Failed to execute command. %s", e));
+            return "";
+        }
+    }
+
+    private Map<String, Long> parseMetrics(String processName, String rawOutput) {
+        String[] lines = rawOutput.split(LINE_SEPARATOR);
+        Map<String, Long> metrics = new HashMap<>();
+        for (String line : lines) {
+            String[] tokens = line.trim().split(SEPARATOR);
+            if (tokens.length < 2) {
+                continue;
+            }
+            String firstToken = tokens[0];
+            String firstTwoTokens = String.join(" ", tokens[0], tokens[1]);
+            if (firstTwoTokens.equals(NATIVE_HEAP_PREFIX)
+                    || firstTwoTokens.equals(DALVIK_HEAP_PREFIX)) {
+                if (tokens.length < 11) {
+                    continue;
+                }
+                int offset = 2;
+                for (Map.Entry<String, Integer> metric : METRIC_POSITIONS.entrySet()) {
+                    metrics.put(
+                            MetricUtility.constructKey(
+                                    METRIC_SOURCE,
+                                    CATEGORIES.get(firstTwoTokens),
+                                    metric.getKey(),
+                                    METRIC_UNIT,
+                                    processName),
+                            Long.parseLong(tokens[offset + metric.getValue()]));
+                }
+            } else if (firstToken.equals(TOTAL_PREFIX)) {
+                int offset = 1;
+                metrics.put(
+                        MetricUtility.constructKey(
+                                METRIC_SOURCE,
+                                CATEGORIES.get(firstToken),
+                                PSS_TOTAL,
+                                METRIC_UNIT,
+                                processName),
+                        Long.parseLong(tokens[offset + METRIC_POSITIONS.get(PSS_TOTAL)]));
+            }
+        }
+        return metrics;
+    }
+
+    private boolean isEmpty(String input) {
+        return input == null || input.isEmpty();
+    }
+}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/DumpsysMeminfoHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/DumpsysMeminfoHelperTest.java
new file mode 100644
index 0000000..1c337e4
--- /dev/null
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/DumpsysMeminfoHelperTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2019 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.helpers.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.DumpsysMeminfoHelper;
+import com.android.helpers.MetricUtility;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android unit test for {@link DumpsysMeminfoHelper}
+ *
+ * <p>To run: atest CollectorsHelperTest:com.android.helpers.tests.DumpsysMeminfoHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class DumpsysMeminfoHelperTest {
+
+    // Process name used for testing
+    private static final String TEST_PROCESS_NAME = "com.android.systemui";
+    // Second process name used for testing
+    private static final String TEST_PROCESS_NAME_2 = "system_server";
+    // A process that does not exists
+    private static final String PROCESS_NOT_FOUND = "process_not_found";
+
+    private static final String[] CATEGORIES = {"native", "dalvik"};
+
+    private static final String[] METRICS = {
+        "pss_total", "shared_dirty", "private_dirty", "heap_size", "heap_alloc"
+    };
+
+    private static final String UNIT = "kb";
+
+    private static final String METRIC_SOURCE = "dumpsys";
+
+    private DumpsysMeminfoHelper mDumpsysMeminfoHelper;
+
+    @Before
+    public void setUp() {
+        mDumpsysMeminfoHelper = new DumpsysMeminfoHelper();
+    }
+
+    @Test
+    public void testCollectMeminfo_noProcess() {
+        mDumpsysMeminfoHelper.startCollecting();
+        Map<String, Long> results = mDumpsysMeminfoHelper.getMetrics();
+        mDumpsysMeminfoHelper.stopCollecting();
+        assertTrue(results.isEmpty());
+    }
+
+    @Test
+    public void testCollectMeminfo_nullProcess() {
+        mDumpsysMeminfoHelper.setUp(null);
+        mDumpsysMeminfoHelper.startCollecting();
+        Map<String, Long> results = mDumpsysMeminfoHelper.getMetrics();
+        assertTrue(results.isEmpty());
+    }
+
+    @Test
+    public void testCollectMeminfo_wrongProcesses() {
+        mDumpsysMeminfoHelper.setUp(PROCESS_NOT_FOUND, null, "");
+        mDumpsysMeminfoHelper.startCollecting();
+        Map<String, Long> results = mDumpsysMeminfoHelper.getMetrics();
+        assertTrue(results.isEmpty());
+    }
+
+    @Test
+    public void testCollectMeminfo_oneProcess() {
+        mDumpsysMeminfoHelper.setUp(TEST_PROCESS_NAME);
+        mDumpsysMeminfoHelper.startCollecting();
+        Map<String, Long> results = mDumpsysMeminfoHelper.getMetrics();
+        mDumpsysMeminfoHelper.stopCollecting();
+        assertFalse(results.isEmpty());
+        verifyKeysForProcess(results, TEST_PROCESS_NAME);
+    }
+
+    @Test
+    public void testCollectMeminfo_multipleProcesses() {
+        mDumpsysMeminfoHelper.setUp(TEST_PROCESS_NAME, TEST_PROCESS_NAME_2);
+        mDumpsysMeminfoHelper.startCollecting();
+        Map<String, Long> results = mDumpsysMeminfoHelper.getMetrics();
+        mDumpsysMeminfoHelper.stopCollecting();
+        assertFalse(results.isEmpty());
+        verifyKeysForProcess(results, TEST_PROCESS_NAME);
+        verifyKeysForProcess(results, TEST_PROCESS_NAME_2);
+    }
+
+    private void verifyKeysForProcess(Map<String, Long> results, String processName) {
+        for (String category : CATEGORIES) {
+            for (String metric : METRICS) {
+                assertTrue(
+                        results.containsKey(
+                                MetricUtility.constructKey(
+                                        METRIC_SOURCE, category, metric, UNIT, processName)));
+            }
+        }
+        assertTrue(
+                results.containsKey(
+                        MetricUtility.constructKey(
+                                METRIC_SOURCE, "total", "pss_total", UNIT, processName)));
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java
new file mode 100644
index 0000000..fae1619
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/DumpsysMeminfoListener.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.helpers.DumpsysMeminfoHelper;
+
+import com.google.common.annotations.VisibleForTesting;
+
+@OptionClass(alias = "dumpsys-meminfo-listener")
+public class DumpsysMeminfoListener extends BaseCollectionListener<Long> {
+
+    private static final String TAG = DumpsysMeminfoHelper.class.getSimpleName();
+    @VisibleForTesting static final String PROCESS_SEPARATOR = ",";
+    @VisibleForTesting static final String PROCESS_NAMES_KEY = "process-names";
+
+    public DumpsysMeminfoListener() {
+        createHelperInstance(new DumpsysMeminfoHelper());
+    }
+
+    @VisibleForTesting
+    public DumpsysMeminfoListener(Bundle args, DumpsysMeminfoHelper helper) {
+        super(args, helper);
+    }
+
+    @Override
+    public void setupAdditionalArgs() {
+        Bundle args = getArgsBundle();
+        String processesString = args.getString(PROCESS_NAMES_KEY);
+        if (processesString == null) {
+            Log.w(TAG, "No process name provided. Nothing will be collected");
+            return;
+        }
+        ((DumpsysMeminfoHelper) mHelper).setUp(processesString.split(PROCESS_SEPARATOR));
+    }
+}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/DumpsysMeminfoListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/DumpsysMeminfoListenerTest.java
new file mode 100644
index 0000000..41e57d4
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/DumpsysMeminfoListenerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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 android.device.collectors;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.DumpsysMeminfoHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Android Unit tests for {@link DumpsysMeminfoListener}.
+ *
+ * <p>To run: atest CollectorDeviceLibTest:android.device.collectors.DumpsysMeminfoListenerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class DumpsysMeminfoListenerTest {
+
+    @Mock private DumpsysMeminfoHelper mDumpsysMeminfoHelper;
+    @Mock private Instrumentation mInstrumentation;
+
+    private Description mRunDesc;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+        mRunDesc = Description.createSuiteDescription("run");
+    }
+
+    @Test
+    public void testListener_noProcessNames() throws Exception {
+        DumpsysMeminfoListener listener = initListener(new Bundle(), mDumpsysMeminfoHelper);
+        listener.testRunStarted(mRunDesc);
+        verify(mDumpsysMeminfoHelper, never()).setUp(any());
+    }
+
+    @Test
+    public void testListener_withProcessNames() throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putString(
+                DumpsysMeminfoListener.PROCESS_NAMES_KEY,
+                String.join(DumpsysMeminfoListener.PROCESS_SEPARATOR, "process1", "process2"));
+        DumpsysMeminfoListener listener = initListener(bundle, mDumpsysMeminfoHelper);
+        listener.testRunStarted(mRunDesc);
+        verify(mDumpsysMeminfoHelper).setUp("process1", "process2");
+    }
+
+    private DumpsysMeminfoListener initListener(Bundle bundle, DumpsysMeminfoHelper helper) {
+        DumpsysMeminfoListener listener = new DumpsysMeminfoListener(bundle, helper);
+        listener.setInstrumentation(mInstrumentation);
+        return listener;
+    }
+}