Merge "Backport totalpss listener to qt-dev." into qt-dev
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/TotalPssHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/TotalPssHelper.java
new file mode 100644
index 0000000..4e20c74
--- /dev/null
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/TotalPssHelper.java
@@ -0,0 +1,218 @@
+/*
+ * 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 static com.android.helpers.MetricUtility.constructKey;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
+import android.content.Context;
+import android.os.Debug.MemoryInfo;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper to collect totalpss memory usage per process tracked by the ActivityManager
+ * memoryinfo.
+ */
+public class TotalPssHelper implements ICollectorHelper<Long> {
+
+    private static final String TAG = TotalPssHelper.class.getSimpleName();
+
+    private static final int DEFAULT_THRESHOLD = 1024;
+    private static final int DEFAULT_MIN_ITERATIONS = 6;
+    private static final int DEFAULT_MAX_ITERATIONS = 20;
+    private static final int DEFAULT_SLEEP_TIME = 1000;
+    private static final String PSS_METRIC_PREFIX = "AM_TOTAL_PSS";
+
+    private String[] mProcessNames;
+    // Minimum number of iterations needed before deciding on the memory usage.
+    private int mMinIterations;
+    // Maximum number of iterations needed waiting for memory usage to be stabilized.
+    private int mMaxIterations;
+    // Sleep time in between the iterations.
+    private int mSleepTime;
+    // Threshold in kb to use whether the data is stabilized.
+    private int mThreshold;
+    // Map to maintain the pss memory size.
+    private Map<String, Long> mPssFinalMap = new HashMap<>();
+
+    public void setUp(String... processNames) {
+        mProcessNames = processNames;
+        // Minimum iterations should be atleast 3 to check for the
+        // stabilization of the memory usage.
+        mMinIterations = DEFAULT_MIN_ITERATIONS;
+        mMaxIterations = DEFAULT_MAX_ITERATIONS;
+        mSleepTime = DEFAULT_SLEEP_TIME;
+        mThreshold = DEFAULT_THRESHOLD;
+    }
+
+    @Override
+    public boolean startCollecting() {
+        return true;
+    }
+
+    @Override
+    public Map<String, Long> getMetrics() {
+        if (mMinIterations < 3) {
+            Log.w(TAG, "Need atleast 3 iterations to check memory usage stabilization.");
+            return mPssFinalMap;
+        }
+        if (mProcessNames != null) {
+            for (String processName : mProcessNames) {
+                if (!processName.isEmpty()) {
+                    measureMemory(processName);
+                }
+            }
+        }
+        return mPssFinalMap;
+    }
+
+    @Override
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    /**
+     * Measure memory info of the given process name tracked by the activity manager
+     * MemoryInfo(i.e getTotalPss).
+     *
+     * @param processName to calculate the memory info.
+     */
+    private void measureMemory(String processName) {
+        Log.i(TAG, "Tracking memory usage of the process - " + processName);
+        List<Long> pssData = new ArrayList<Long>();
+        long pss = 0;
+        int iteration = 0;
+        while (iteration < mMaxIterations) {
+            sleep(mSleepTime);
+            pss = getPss(processName);
+            pssData.add(pss);
+            if (iteration >= mMinIterations && stabilized(pssData)) {
+                Log.i(TAG, "Memory usage stabilized at iteration count = " + iteration);
+                mPssFinalMap.put(constructKey(PSS_METRIC_PREFIX, processName), pss);
+                return;
+            }
+            iteration++;
+        }
+
+        Log.i(TAG, processName + " memory usage did not stabilize."
+                + " Returning the average of the pss data collected.");
+        mPssFinalMap.put(constructKey(PSS_METRIC_PREFIX, processName), average(pssData));
+    }
+
+    /**
+     * Time to sleep in between the iterations.
+     *
+     * @param time in ms to sleep.
+     */
+    private void sleep(int time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * Get the total pss memory of the given process name.
+     *
+     * @param processName of the process to measure the memory.
+     * @return the memory in KB.
+     */
+    private long getPss(String processName) {
+        ActivityManager am = (ActivityManager) InstrumentationRegistry.getInstrumentation()
+                .getContext().getSystemService(Context.ACTIVITY_SERVICE);
+        List<RunningAppProcessInfo> apps = am.getRunningAppProcesses();
+        for (RunningAppProcessInfo proc : apps) {
+            if (!proc.processName.equals(processName)) {
+                continue;
+            }
+            MemoryInfo meminfo = am.getProcessMemoryInfo(new int[] {
+                proc.pid
+            })[0];
+            Log.i(TAG,
+                    String.format("Memory usage of process - %s is %d", processName,
+                            meminfo.getTotalPss()));
+            return meminfo.getTotalPss();
+        }
+        Log.w(TAG, "Not able to find the process id for the process = " + processName);
+        return 0;
+    }
+
+    /**
+     * Checks whether the memory usage is stabilized by calculating the sum of the difference
+     * between the last 3 values and comparing that to the threshold.
+     *
+     * @param pssData list of pssData of the given process name.
+     * @return true if the memory is stabilized.
+     */
+    private boolean stabilized(List<Long> pssData) {
+        long diff1 = Math.abs(pssData.get(pssData.size() - 1) - pssData.get(pssData.size() - 2));
+        long diff2 = Math.abs(pssData.get(pssData.size() - 2) - pssData.get(pssData.size() - 3));
+        Log.i(TAG, "diff1=" + diff1 + " diff2=" + diff2);
+        return (diff1 + diff2) < mThreshold;
+    }
+
+    /**
+     * Returns the average of the pssData collected for the maxIterations.
+     *
+     * @param pssData list of pssData.
+     * @return
+     */
+    private long average(List<Long> pssData) {
+        long sum = 0;
+        for (long sample : pssData) {
+            sum += sample;
+        }
+        return sum / pssData.size();
+    }
+
+    /**
+     * @param minIterations before starting to check for memory is stabilized.
+     */
+    public void setMinIterations(int minIterations) {
+        mMinIterations = minIterations;
+    }
+
+    /**
+     * @param maxIterations to wait for memory to be stabilized.
+     */
+    public void setMaxIterations(int maxIterations) {
+        mMaxIterations = maxIterations;
+    }
+
+    /**
+     * @param sleepTime in between the iterations.
+     */
+    public void setSleepTime(int sleepTime) {
+        mSleepTime = sleepTime;
+    }
+
+    /**
+     * @param threshold for difference in memory usage between two successive iterations in kb
+     */
+    public void setThreshold(int threshold) {
+        mThreshold = threshold;
+    }
+}
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/TotalPssHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/TotalPssHelperTest.java
new file mode 100644
index 0000000..b3dd138
--- /dev/null
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/TotalPssHelperTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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 com.android.helpers.MetricUtility.constructKey;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.TotalPssHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android Unit tests for {@link TotalPssHelper}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.TotalPssHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class TotalPssHelperTest {
+
+    // 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 = "com.google.android.apps.nexuslauncher";
+    // Second process name used for testing
+    private static final String INVALID_PROCESS_NAME = "abc";
+    // Pss prefix in Key.
+    private static final String PSS_METRIC_PREFIX = "AM_TOTAL_PSS";
+
+    private TotalPssHelper mTotalPssHelper;
+
+    @Before
+    public void setUp() {
+         mTotalPssHelper = new TotalPssHelper();
+    }
+
+    /** Test no metrics are sampled if process name is empty. */
+    @Test
+    public void testEmptyProcessName() {
+        mTotalPssHelper.setUp("");
+        Map<String, Long> pssMetrics = mTotalPssHelper.getMetrics();
+        assertTrue(pssMetrics.isEmpty());
+    }
+
+    /** Test no metrics are sampled if process names is null */
+    @Test
+    public void testNullProcessName() {
+        mTotalPssHelper.setUp(null);
+        Map<String, Long> pssMetrics = mTotalPssHelper.getMetrics();
+        assertTrue(pssMetrics.isEmpty());
+    }
+
+    /** Test getting metrics for single process. */
+    @Test
+    public void testGetMetrics_OneProcess() {
+        mTotalPssHelper.setUp(TEST_PROCESS_NAME);
+        Map<String, Long> pssMetrics = mTotalPssHelper.getMetrics();
+        assertFalse(pssMetrics.isEmpty());
+        assertTrue(pssMetrics.containsKey(constructKey(PSS_METRIC_PREFIX, TEST_PROCESS_NAME)));
+        assertTrue(pssMetrics.get(constructKey(PSS_METRIC_PREFIX, TEST_PROCESS_NAME)) > 0);
+    }
+
+    /** Test getting metrics for multiple process. */
+    @Test
+    public void testGetMetrics_MultipleProcesses() {
+        mTotalPssHelper.setUp(TEST_PROCESS_NAME, TEST_PROCESS_NAME_2);
+        Map<String, Long> pssMetrics = mTotalPssHelper.getMetrics();
+        assertFalse(pssMetrics.isEmpty());
+        assertTrue(pssMetrics.containsKey(constructKey(PSS_METRIC_PREFIX, TEST_PROCESS_NAME)));
+        assertTrue(pssMetrics.containsKey(constructKey(PSS_METRIC_PREFIX, TEST_PROCESS_NAME_2)));
+        assertTrue(pssMetrics.get(constructKey(PSS_METRIC_PREFIX, TEST_PROCESS_NAME)) > 0);
+        assertTrue(pssMetrics.get(constructKey(PSS_METRIC_PREFIX, TEST_PROCESS_NAME_2)) > 0);
+    }
+
+    /** Test pss metric is 0 for invalid process name. */
+    @Test
+    public void testGetMetrics_InvalidProcess() {
+        mTotalPssHelper.setUp(INVALID_PROCESS_NAME);
+        Map<String, Long> pssMetrics = mTotalPssHelper.getMetrics();
+        assertTrue(pssMetrics.containsKey(constructKey(PSS_METRIC_PREFIX, INVALID_PROCESS_NAME)));
+        assertTrue(pssMetrics.get(constructKey(PSS_METRIC_PREFIX, INVALID_PROCESS_NAME)) == 0);
+    }
+}
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/TotalPssMetricListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/TotalPssMetricListener.java
new file mode 100644
index 0000000..bb5a8a9
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/TotalPssMetricListener.java
@@ -0,0 +1,93 @@
+/*
+ * 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 androidx.annotation.VisibleForTesting;
+
+import com.android.helpers.TotalPssHelper;
+
+/**
+ * A {@link TotalPssMetricListener} that measures process total pss tracked per
+ * process in activity manaager.
+ *
+ * Options:
+ * -e process-names [processName] : the process from the test case that we want to
+ * measure memory for.
+ */
+@OptionClass(alias = "totalpss-collector")
+public class TotalPssMetricListener extends BaseCollectionListener<Long> {
+
+    private static final String TAG = TotalPssMetricListener.class.getSimpleName();
+    @VisibleForTesting static final String PROCESS_SEPARATOR = ",";
+    @VisibleForTesting static final String PROCESS_NAMES_KEY = "process-names";
+    @VisibleForTesting static final String MIN_ITERATIONS_KEY = "min_iterations";
+    @VisibleForTesting static final String MAX_ITERATIONS_KEY = "max_iterations";
+    @VisibleForTesting static final String SLEEP_TIME_KEY = "sleep_time_ms";
+    @VisibleForTesting static final String THRESHOLD_KEY = "threshold_kb";
+    private TotalPssHelper mTotalPssHelper = new TotalPssHelper();
+
+    public TotalPssMetricListener() {
+        createHelperInstance(mTotalPssHelper);
+    }
+
+    /**
+     * Constructor to simulate receiving the instrumentation arguments. Should not be used except
+     * for testing.
+     */
+    @VisibleForTesting
+    public TotalPssMetricListener(Bundle args, TotalPssHelper helper) {
+        super(args, helper);
+        mTotalPssHelper = helper;
+        createHelperInstance(mTotalPssHelper);
+    }
+
+    /**
+     * Adds the options for total pss collector.
+     */
+    @Override
+    public void setupAdditionalArgs() {
+        Bundle args = getArgsBundle();
+        String procsString = args.getString(PROCESS_NAMES_KEY);
+        if (procsString == null) {
+            Log.e(TAG, "No processes provided to sample");
+            return;
+        }
+
+        String[] procs = procsString.split(PROCESS_SEPARATOR);
+        mTotalPssHelper.setUp(procs);
+
+        if (args.getString(MIN_ITERATIONS_KEY) != null) {
+            mTotalPssHelper.setMinIterations(Integer.parseInt(args.getString(MIN_ITERATIONS_KEY)));
+        }
+
+        if (args.getString(MAX_ITERATIONS_KEY) != null) {
+            mTotalPssHelper.setMaxIterations(Integer.parseInt(args.getString(MAX_ITERATIONS_KEY)));
+        }
+
+        if (args.getString(SLEEP_TIME_KEY) != null) {
+            mTotalPssHelper.setSleepTime(Integer.parseInt(args.getString(SLEEP_TIME_KEY)));
+        }
+
+        if (args.getString(THRESHOLD_KEY) != null) {
+            mTotalPssHelper.setThreshold(Integer.parseInt(args.getString(THRESHOLD_KEY)));
+        }
+    }
+}
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/TotalPssMetricListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/TotalPssMetricListenerTest.java
new file mode 100644
index 0000000..6653970
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/TotalPssMetricListenerTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 android.device.collectors.TotalPssMetricListener.PROCESS_NAMES_KEY;
+import static android.device.collectors.TotalPssMetricListener.PROCESS_SEPARATOR;
+import static android.device.collectors.TotalPssMetricListener.MIN_ITERATIONS_KEY;
+import static android.device.collectors.TotalPssMetricListener.MAX_ITERATIONS_KEY;
+import static android.device.collectors.TotalPssMetricListener.SLEEP_TIME_KEY;
+import static android.device.collectors.TotalPssMetricListener.THRESHOLD_KEY;
+
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.TotalPssHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Android Unit tests for {@link TotalPssMetricListener}.
+ *
+ * To run:
+ * atest CollectorDeviceLibTest:android.device.collectors.TotalPssMetricListenerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class TotalPssMetricListenerTest {
+
+    @Mock
+    private Instrumentation mInstrumentation;
+    @Mock
+    private TotalPssHelper mTotalPssMetricHelper;
+
+    private TotalPssMetricListener mListener;
+    private Description mRunDesc;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mRunDesc = Description.createSuiteDescription("run");
+    }
+
+    private TotalPssMetricListener initListener(Bundle b) {
+        TotalPssMetricListener listener = new TotalPssMetricListener(b, mTotalPssMetricHelper);
+        listener.setInstrumentation(mInstrumentation);
+        return listener;
+    }
+
+    @Test
+    public void testHelperReceivesProcessNames() throws Exception {
+        Bundle b = new Bundle();
+        b.putString(PROCESS_NAMES_KEY, "process1" + PROCESS_SEPARATOR + "process2");
+        mListener = initListener(b);
+
+        mListener.testRunStarted(mRunDesc);
+
+        verify(mTotalPssMetricHelper).setUp("process1", "process2");
+    }
+
+    @Test
+    public void testAdditionalPssOptions() throws Exception {
+        Bundle b = new Bundle();
+        b.putString(PROCESS_NAMES_KEY, "process1");
+        b.putString(MIN_ITERATIONS_KEY, "50");
+        b.putString(MAX_ITERATIONS_KEY, "102");
+        b.putString(SLEEP_TIME_KEY, "2000");
+        b.putString(THRESHOLD_KEY, "2048");
+        mListener = initListener(b);
+
+        mListener.testRunStarted(mRunDesc);
+
+        verify(mTotalPssMetricHelper).setUp("process1");
+        verify(mTotalPssMetricHelper).setMinIterations(50);
+        verify(mTotalPssMetricHelper).setMaxIterations(102);
+        verify(mTotalPssMetricHelper).setSleepTime(2000);
+        verify(mTotalPssMetricHelper).setThreshold(2048);
+    }
+}