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