Merge "Added a RunnerBuilder to enable cmdline swapping to longevity runner." into qt-dev
diff --git a/libraries/collectors-helper/statsd/Android.bp b/libraries/collectors-helper/statsd/Android.bp
index e1060bb..ea586a3 100644
--- a/libraries/collectors-helper/statsd/Android.bp
+++ b/libraries/collectors-helper/statsd/Android.bp
@@ -27,6 +27,7 @@
         "statsdprotolite",
         "androidx.test.runner",
         "guava",
+        "ub-uiautomator",
     ],
 
     platform_apis: true,
diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java
index b764959..567dd14 100644
--- a/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java
+++ b/libraries/collectors-helper/statsd/src/com/android/helpers/ThermalHelper.java
@@ -17,13 +17,17 @@
 package com.android.helpers;
 
 import android.os.TemperatureTypeEnum;
+import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
 
 import com.android.os.AtomsProto.Atom;
-import com.android.os.AtomsProto.ThermalThrottlingSeverityStateChanged;
 import com.android.os.StatsLog.EventMetricData;
 
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -36,15 +40,45 @@
 public class ThermalHelper implements ICollectorHelper<StringBuilder> {
     private static final String LOG_TAG = ThermalHelper.class.getSimpleName();
 
+    private static final int UNDEFINED_SEVERITY = -1;
+    private static final Pattern SEVERITY_DUMPSYS_PATTERN =
+            Pattern.compile("Thermal Status: (\\d+)");
+
     private StatsdHelper mStatsdHelper;
+    private UiDevice mDevice;
+    private int mInitialSeverity;
 
     /** Set up the statsd config to track thermal events. */
     @Override
     public boolean startCollecting() {
-        Log.i(LOG_TAG, "Registering thermal config to statsd.");
+        // Add an initial value because this only detects changes.
+        mInitialSeverity = UNDEFINED_SEVERITY;
+        try {
+            String[] output = getDevice().executeShellCommand("dumpsys thermalservice").split("\n");
+            for (String line : output) {
+                Matcher severityMatcher = SEVERITY_DUMPSYS_PATTERN.matcher(line);
+                if (severityMatcher.matches()) {
+                    mInitialSeverity = Integer.parseInt(severityMatcher.group(1));
+                    Log.v(LOG_TAG, String.format("Initial severity: %s.", mInitialSeverity));
+                }
+            }
+        } catch (NumberFormatException nfe) {
+            Log.w(LOG_TAG, String.format("Couldn't identify severity. Error parsing: %s", nfe));
+            return false;
+        } catch (IOException ioe) {
+            Log.w(LOG_TAG, String.format("Failed to query thermalservice. Error: %s", ioe));
+            return false;
+        }
+
+        // Skip this monitor if severity isn't identified.
+        if (mInitialSeverity == UNDEFINED_SEVERITY) {
+            Log.w(LOG_TAG, "Did not find an initial severity. Quitting.");
+            return false;
+        }
+
+        // Register the thermal event config to statsd.
         List<Integer> atomIdList = new ArrayList<>();
         atomIdList.add(Atom.THERMAL_THROTTLING_SEVERITY_STATE_CHANGED_FIELD_NUMBER);
-        // TODO(b/137793331): Add an initial value because this only detects changes.
         return getStatsdHelper().addEventConfig(atomIdList);
     }
 
@@ -53,6 +87,10 @@
     public Map<String, StringBuilder> getMetrics() {
         Map<String, StringBuilder> results = new HashMap<>();
 
+        // Add the initial severity value every time metrics are collected.
+        String severityKey = MetricUtility.constructKey("thermal", "throttling", "severity");
+        MetricUtility.addMetric(severityKey, mInitialSeverity, results);
+
         List<EventMetricData> eventMetricData = getStatsdHelper().getEventMetrics();
         Log.i(LOG_TAG, String.format("%d thermal data points found.", eventMetricData.size()));
         // Collect all thermal throttling severity state change events.
@@ -60,14 +98,15 @@
             if (dataItem.getAtom().hasThermalThrottlingSeverityStateChanged()) {
                 // TODO(b/137878503): Add elapsed_timestamp_nanos for timpestamp data.
                 // Get thermal throttling severity state change data point.
-                ThermalThrottlingSeverityStateChanged stateChange =
-                        dataItem.getAtom().getThermalThrottlingSeverityStateChanged();
-                String sensorType = getShorthandSensorType(stateChange.getSensorType());
-                String sensorName = stateChange.getSensorName();
-                int severity = stateChange.getSeverity().getNumber();
-                // Store the severity state change by sensor type and name.
-                String metricKey = MetricUtility.constructKey("thermal", sensorType, sensorName);
-                MetricUtility.addMetric(metricKey, severity, results);
+                int severity =
+                        dataItem.getAtom()
+                                .getThermalThrottlingSeverityStateChanged()
+                                .getSeverity()
+                                .getNumber();
+                // Store the severity state change ignoring where the measurement came from.
+                MetricUtility.addMetric(severityKey, severity, results);
+                // Set the initial severity to the last value, in case #getMetrics is called again.
+                mInitialSeverity = severity;
             }
         }
 
@@ -119,4 +158,16 @@
     void setStatsdHelper(StatsdHelper helper) {
         mStatsdHelper = helper;
     }
+
+    private UiDevice getDevice() {
+        if (mDevice == null) {
+            mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        }
+        return mDevice;
+    }
+
+    @VisibleForTesting
+    void setUiDevice(UiDevice device) {
+        mDevice = device;
+    }
 }
diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java
index 7620c04..bd32b29 100644
--- a/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java
+++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/ThermalHelperTest.java
@@ -19,6 +19,7 @@
 
 import android.os.TemperatureTypeEnum;
 import android.os.ThrottlingSeverityEnum;
+import android.support.test.uiautomator.UiDevice;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.os.AtomsProto.Atom;
@@ -34,6 +35,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
 
@@ -46,15 +48,24 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class ThermalHelperTest {
+    private static final String THROTTLING_KEY =
+            MetricUtility.constructKey("thermal", "throttling", "severity");
+    private static final String FAKE_SERVICE_DUMP = "F\nA\nK\nE\nThermal Status: 2\nO\nK";
 
     private ThermalHelper mThermalHelper;
     private StatsdHelper mStatsdHelper;
+    private UiDevice mDevice;
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         mThermalHelper = new ThermalHelper();
+        // Set up the statsd helper to mock statsd calls.
         mStatsdHelper = Mockito.spy(new StatsdHelper());
         mThermalHelper.setStatsdHelper(mStatsdHelper);
+        // Set up the fake device for mocking shell commands.
+        mDevice = Mockito.mock(UiDevice.class);
+        mThermalHelper.setUiDevice(mDevice);
+        when(mDevice.executeShellCommand("dumpsys thermalservice")).thenReturn(FAKE_SERVICE_DUMP);
     }
 
     /** Test registering and unregistering the thermal config. */
@@ -64,16 +75,25 @@
         assertTrue(mThermalHelper.stopCollecting());
     }
 
-    /** Test that no metrics show up when there are no events. */
+    /** Test registering the thermal config fails when no initial throttling value is found. */
     @Test
-    public void testNoMetricsWithoutEvents() throws Exception {
+    public void testThermalConfigRegistration_noInitialValue() throws Exception {
+        when(mDevice.executeShellCommand("dumpsys thermalservice")).thenReturn("FAKE RESPONSE");
+        assertFalse(mThermalHelper.startCollecting());
+    }
+
+    /** Test that only the initial value shows up when there are no events. */
+    @Test
+    public void testInitialMetricsWithoutEvents() throws Exception {
         when(mStatsdHelper.getEventMetrics()).thenReturn(new ArrayList<EventMetricData>());
         assertTrue(mThermalHelper.startCollecting());
-        assertTrue(mThermalHelper.getMetrics().isEmpty());
+        assertEquals(
+                mThermalHelper.getMetrics().get(THROTTLING_KEY).toString(),
+                String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()));
         assertTrue(mThermalHelper.stopCollecting());
     }
 
-    /** Test that a single event shows up as a single metric event. */
+    /** Test that the initial value and a single event show up from a single metric event. */
     @Test
     public void testSingleEvent() throws Exception {
         when(mStatsdHelper.getEventMetrics())
@@ -82,52 +102,19 @@
                                 getThermalThrottlingSeverityStateChangedEvent(
                                         TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN,
                                         "sensor_name",
-                                        ThrottlingSeverityEnum.NONE)));
-
+                                        ThrottlingSeverityEnum.LIGHT)));
         assertTrue(mThermalHelper.startCollecting());
         Map<String, StringBuilder> metrics = mThermalHelper.getMetrics();
-        String key = getMetricKey(TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN, "sensor_name");
-        assertTrue(metrics.containsKey(key));
         assertEquals(
-                metrics.get(key).toString(),
-                String.valueOf(ThrottlingSeverityEnum.NONE.getNumber()));
-        assertTrue(mThermalHelper.stopCollecting());
-    }
-
-    /** Test that multiple, similar events shows up as a single metric with multiple values. */
-    @Test
-    public void testMultipleSimilarEvents() throws Exception {
-        when(mStatsdHelper.getEventMetrics())
-                .thenReturn(
-                        getFakeEventMetrics(
-                                getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN,
-                                        "sensor_name",
-                                        ThrottlingSeverityEnum.NONE),
-                                getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN,
-                                        "sensor_name",
-                                        ThrottlingSeverityEnum.LIGHT),
-                                getThermalThrottlingSeverityStateChangedEvent(
-                                        TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN,
-                                        "sensor_name",
-                                        ThrottlingSeverityEnum.MODERATE)));
-
-        assertTrue(mThermalHelper.startCollecting());
-        Map<String, StringBuilder> metrics = mThermalHelper.getMetrics();
-        String key = getMetricKey(TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN, "sensor_name");
-        assertTrue(metrics.containsKey(key));
-        assertEquals(
-                metrics.get(key).toString(),
+                metrics.get(THROTTLING_KEY).toString(),
                 String.join(
                         ",",
-                        String.valueOf(ThrottlingSeverityEnum.NONE.getNumber()),
-                        String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber()),
-                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber())));
+                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()),
+                        String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber())));
         assertTrue(mThermalHelper.stopCollecting());
     }
 
-    /** Test that multiple, different events shows up as a multiple metrics with a single value. */
+    /** Test that multiple throttling events with different sources show up together. */
     @Test
     public void testMultipleDifferentEvents() throws Exception {
         when(mStatsdHelper.getEventMetrics())
@@ -140,7 +127,7 @@
                                 getThermalThrottlingSeverityStateChangedEvent(
                                         TemperatureTypeEnum.TEMPERATURE_TYPE_CPU,
                                         "sensor2_name",
-                                        ThrottlingSeverityEnum.LIGHT),
+                                        ThrottlingSeverityEnum.MODERATE),
                                 getThermalThrottlingSeverityStateChangedEvent(
                                         TemperatureTypeEnum.TEMPERATURE_TYPE_GPU,
                                         "sensor3_name",
@@ -148,21 +135,14 @@
 
         assertTrue(mThermalHelper.startCollecting());
         Map<String, StringBuilder> metrics = mThermalHelper.getMetrics();
-        String skinKey = getMetricKey(TemperatureTypeEnum.TEMPERATURE_TYPE_SKIN, "sensor1_name");
-        String cpuKey = getMetricKey(TemperatureTypeEnum.TEMPERATURE_TYPE_CPU, "sensor2_name");
-        String gpuKey = getMetricKey(TemperatureTypeEnum.TEMPERATURE_TYPE_GPU, "sensor3_name");
-        assertTrue(metrics.containsKey(skinKey));
-        assertTrue(metrics.containsKey(cpuKey));
-        assertTrue(metrics.containsKey(gpuKey));
         assertEquals(
-                metrics.get(skinKey).toString(),
-                String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber()));
-        assertEquals(
-                metrics.get(cpuKey).toString(),
-                String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber()));
-        assertEquals(
-                metrics.get(gpuKey).toString(),
-                String.valueOf(ThrottlingSeverityEnum.NONE.getNumber()));
+                metrics.get(THROTTLING_KEY).toString(),
+                String.join(
+                        ",",
+                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()),
+                        String.valueOf(ThrottlingSeverityEnum.LIGHT.getNumber()),
+                        String.valueOf(ThrottlingSeverityEnum.MODERATE.getNumber()),
+                        String.valueOf(ThrottlingSeverityEnum.NONE.getNumber())));
         assertTrue(mThermalHelper.stopCollecting());
     }
 
diff --git a/libraries/collectors-helper/system/Android.bp b/libraries/collectors-helper/system/Android.bp
new file mode 100644
index 0000000..9baeeed
--- /dev/null
+++ b/libraries/collectors-helper/system/Android.bp
@@ -0,0 +1,33 @@
+// 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.
+
+// Used for collecting the any of the metrics from the system.
+java_library {
+    name: "system-metric-helper",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.runner",
+        "collector-helper-utilities",
+        "guava",
+        "ub-uiautomator",
+    ],
+
+    sdk_version: "current",
+}
+
diff --git a/libraries/collectors-helper/system/src/com/android/helpers/ProcLoadHelper.java b/libraries/collectors-helper/system/src/com/android/helpers/ProcLoadHelper.java
new file mode 100644
index 0000000..025d53e
--- /dev/null
+++ b/libraries/collectors-helper/system/src/com/android/helpers/ProcLoadHelper.java
@@ -0,0 +1,158 @@
+/*
+ * 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.os.SystemClock;
+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.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** An {@link ProcLoadHelper} to check for cpu load in last minute is lesser or equal
+ *  to given threshold for a given timeout and collect the cpu load as metric.
+ */
+public class ProcLoadHelper implements ICollectorHelper<Double> {
+
+    private static final String LOG_TAG = ProcLoadHelper.class.getSimpleName();
+    private static final String LOAD_CMD = "cat /proc/loadavg";
+    public static final String LAST_MINUTE_LOAD_METRIC_KEY = "proc_loadavg_last_minute";
+
+    private static final Pattern LOAD_OUTPUT_PATTERN = Pattern.compile(
+            "(?<LASTMINUTELOAD>.*)\\s.*\\s.*\\s.*\\s.*");
+
+    private double mProcLoadThreshold = 0;
+    private long mProcLoadWaitTimeInMs = 0;
+    // Default to 500 msecs timeout.
+    private long mProcLoadIntervalInMs = 500;
+    private double mRecentLoad = 0;
+    private UiDevice mDevice;
+
+    /** Wait untill the proc/load reaches below the threshold or timeout expires */
+    @Override
+    public boolean startCollecting() {
+        mRecentLoad = 0;
+        long remainingWaitTime = mProcLoadWaitTimeInMs;
+        while (true) {
+            mRecentLoad = getProcLoadInLastMinute();
+            Log.i(LOG_TAG, String.format("Average cpu load in last minute is : %s", mRecentLoad));
+            if (mRecentLoad <= mProcLoadThreshold) {
+                break;
+            } else {
+                if (remainingWaitTime <= 0) {
+                    Log.i(LOG_TAG, "Timeout because proc/loadavg never went below the threshold.");
+                    return false;
+                }
+                long currWaitTime = (mProcLoadIntervalInMs < remainingWaitTime)
+                        ? mProcLoadIntervalInMs : remainingWaitTime;
+                Log.d(LOG_TAG, String.format("Waiting for %s msecs", currWaitTime));
+                SystemClock.sleep(currWaitTime);
+                remainingWaitTime = remainingWaitTime - mProcLoadIntervalInMs;
+            }
+        }
+        return true;
+    }
+
+    /** Collect the proc/load_avg last minute cpu load average metric. */
+    @Override
+    public Map<String, Double> getMetrics() {
+        // Adding the last recorded load in the metric that will be reported.
+        Map<String, Double> result = new HashMap<>();
+        Log.i(LOG_TAG, String.format("proc/loadavg in last minute before test is : %s",
+                mRecentLoad));
+        result.put(LAST_MINUTE_LOAD_METRIC_KEY, mRecentLoad);
+        return result;
+    }
+
+    /** Do nothing, because nothing is needed to disable cpuy load avg. */
+    @Override
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    /**
+     * Parse the last minute cpu load from proc/loadavg
+     *
+     * @return cpu load in last minute. Returns -1 in case if it is failed to parse.
+     */
+    private double getProcLoadInLastMinute() {
+        try {
+            String output = getDevice().executeShellCommand(LOAD_CMD);
+            Log.i(LOG_TAG, String.format("Output of proc_loadavg is : %s", output));
+            // Output of the load command
+            // 1.39 1.10 1.21 2/2679 6380
+            // 1.39 is the proc load in the last minute.
+
+            Matcher match = null;
+            if ((match = matches(LOAD_OUTPUT_PATTERN, output.trim())) != null) {
+                Log.i(LOG_TAG, String.format("Current load is : %s",
+                        match.group("LASTMINUTELOAD")));
+                return Double.parseDouble(match.group("LASTMINUTELOAD"));
+            } else {
+                Log.w(LOG_TAG, "Not able to parse the proc/loadavg");
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to get proc/loadavg.", e);
+        }
+
+        return -1;
+    }
+
+    /** Returns the {@link UiDevice} under test. */
+    private UiDevice getDevice() {
+        if (mDevice == null) {
+            mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        }
+        return mDevice;
+    }
+
+    /**
+     * Checks whether {@code line} matches the given {@link Pattern}.
+     *
+     * @return The resulting {@link Matcher} obtained by matching the {@code line} against
+     *         {@code pattern}, or null if the {@code line} does not match.
+     */
+    private static Matcher matches(Pattern pattern, String line) {
+        Matcher ret = pattern.matcher(line);
+        return ret.matches() ? ret : null;
+    }
+
+    /**
+     * Sets the threshold value which the device cpu load average should be lesser than or equal.
+     */
+    public void setProcLoadThreshold(double procLoadThreshold) {
+        mProcLoadThreshold = procLoadThreshold;
+    }
+
+    /**
+     * Sets the timeout in msecs checking for threshold before proceeding with the testing.
+     */
+    public void setProcLoadWaitTimeInMs(long procLoadWaitTimeInMs) {
+        mProcLoadWaitTimeInMs = procLoadWaitTimeInMs;
+    }
+
+    /**
+     * Sets the interval time in msecs to check continuosly untill the timeout expires.
+     */
+    public void setProcLoadIntervalInMs(long procLoadIntervalInMs) {
+        mProcLoadIntervalInMs = procLoadIntervalInMs;
+    }
+}
diff --git a/libraries/collectors-helper/system/test/Android.bp b/libraries/collectors-helper/system/test/Android.bp
new file mode 100644
index 0000000..4d94e5a
--- /dev/null
+++ b/libraries/collectors-helper/system/test/Android.bp
@@ -0,0 +1,30 @@
+// 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.
+
+java_library {
+    name: "system-helper-test",
+    defaults: ["tradefed_errorprone_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.runner",
+        "system-metric-helper",
+        "junit",
+        "mockito-target",
+        "truth-prebuilt",
+    ],
+
+    sdk_version: "current",
+}
diff --git a/libraries/collectors-helper/system/test/src/com/android/helpers/tests/ProcLoadHelperTest.java b/libraries/collectors-helper/system/test/src/com/android/helpers/tests/ProcLoadHelperTest.java
new file mode 100644
index 0000000..a66d8bc
--- /dev/null
+++ b/libraries/collectors-helper/system/test/src/com/android/helpers/tests/ProcLoadHelperTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.ProcLoadHelper;
+import static com.android.helpers.ProcLoadHelper.LAST_MINUTE_LOAD_METRIC_KEY;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Android Unit tests for {@link ProcLoadHelperTest}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.ProcLoadHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ProcLoadHelperTest {
+
+    private ProcLoadHelper mLoadHelper;
+
+    @Before
+    public void setUp() {
+        mLoadHelper = new ProcLoadHelper();
+    }
+
+    /** Test to verify succesfull threshold flow. **/
+    @Test
+    public void testThresholdSuccess() {
+        // By setting the threshold higher(i.e 100) the current load should be always
+        // lesser than the the 100 which should proceed with add the current
+        // load avg during the last minute in the metrics.
+        mLoadHelper.setProcLoadThreshold(100);
+        mLoadHelper.setProcLoadWaitTimeInMs(1100L);
+        mLoadHelper.setProcLoadIntervalInMs(100L);
+        assertTrue(mLoadHelper.startCollecting());
+        Map<String, Double> procLoadMetric = mLoadHelper.getMetrics();
+        assertTrue(procLoadMetric.containsKey(LAST_MINUTE_LOAD_METRIC_KEY));
+        assertTrue(procLoadMetric.get(LAST_MINUTE_LOAD_METRIC_KEY) > 0);
+    }
+
+    /** Test to verify threshold not met workflow. */
+    @Test
+    public void testMetricAfterTimeout() {
+        // By setting the threshold lower(i.e 0) the cpu utilization will never be met
+        // which should result in timeout.
+        mLoadHelper.setProcLoadThreshold(0);
+        mLoadHelper.setProcLoadWaitTimeInMs(4000L);
+        mLoadHelper.setProcLoadIntervalInMs(200L);
+        assertFalse(mLoadHelper.startCollecting());
+        Map<String, Double> procLoadMetric = mLoadHelper.getMetrics();
+        assertTrue(procLoadMetric.get(LAST_MINUTE_LOAD_METRIC_KEY) > 0);
+    }
+
+    /** Test to verify the timeout if the threshold did not meet. */
+    @Test
+    public void testWaitForTimeout() {
+        // By setting the threshold lower(i.e 0) the cpu utilization will never be met
+        // which should result in timeout.
+        mLoadHelper.setProcLoadThreshold(0);
+        mLoadHelper.setProcLoadWaitTimeInMs(4000L);
+        mLoadHelper.setProcLoadIntervalInMs(200L);
+        long startTime = System.currentTimeMillis();
+        assertFalse(mLoadHelper.startCollecting());
+        long waitTime = System.currentTimeMillis() - startTime;
+        assertTrue((waitTime > 4000L && waitTime < 6000L));
+        Map<String, Double> procLoadMetric = mLoadHelper.getMetrics();
+        assertTrue(procLoadMetric.get(LAST_MINUTE_LOAD_METRIC_KEY) > 0);
+    }
+
+    /** Test to verify metric exist and exits with the default threshold, timeout and interval */
+    @Test
+    public void testDefaults() {
+        long startTime = System.currentTimeMillis();
+        assertFalse(mLoadHelper.startCollecting());
+        Map<String, Double> procLoadMetric = mLoadHelper.getMetrics();
+        assertTrue(procLoadMetric.containsKey(LAST_MINUTE_LOAD_METRIC_KEY));
+        assertTrue(procLoadMetric.get(LAST_MINUTE_LOAD_METRIC_KEY) > 0);
+    }
+}
diff --git a/libraries/device-collectors/src/main/Android.bp b/libraries/device-collectors/src/main/Android.bp
index 9e144ed..e78c906 100644
--- a/libraries/device-collectors/src/main/Android.bp
+++ b/libraries/device-collectors/src/main/Android.bp
@@ -25,6 +25,7 @@
         "memory-helper",
         "perfetto-helper",
         "ub-uiautomator",
+        "system-metric-helper",
     ],
 
     sdk_version: "current",
diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ProcLoadListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/ProcLoadListener.java
new file mode 100644
index 0000000..adc4c7b
--- /dev/null
+++ b/libraries/device-collectors/src/main/java/android/device/collectors/ProcLoadListener.java
@@ -0,0 +1,92 @@
+/*
+ * 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 androidx.annotation.VisibleForTesting;
+
+import com.android.helpers.ProcLoadHelper;
+
+/**
+ * A {@link ProcLoadListener} that waits until the proc/load threshold is met
+ * or timeout expires.
+ *
+ * Options:
+ * <p>-e proc-loadavg-threshold 1 : The threshold the system cpu load has
+ * to be less than or equal in last minute.
+ *
+ * <p>-e proc-loadavg-timeout 1000 :
+ * Timeout to wait before the threshold is met.
+ *
+ * <p>-e proc-loadavg-interval 100 :
+ * Interval frequency to check if the threshold is met or not.
+ *
+ */
+@OptionClass(alias = "procload-collector")
+public class ProcLoadListener extends BaseCollectionListener<Double> {
+
+    private static final String TAG = ProcLoadListener.class.getSimpleName();
+    @VisibleForTesting
+    static final String PROC_LOAD_THRESHOLD = "proc-loadavg-threshold";
+    @VisibleForTesting
+    static final String PROC_THRESHOLD_TIMEOUT = "proc-loadavg-timeout";
+    @VisibleForTesting
+    static final String PROC_LOAD_INTERVAL = "proc-loadavg-interval";
+
+    private ProcLoadHelper mProcLoadHelper = new ProcLoadHelper();
+
+    public ProcLoadListener() {
+        createHelperInstance(mProcLoadHelper);
+    }
+
+    /**
+     * Constructor to simulate receiving the instrumentation arguments. Should not be used except
+     * for testing.
+     */
+    @VisibleForTesting
+    public ProcLoadListener(Bundle args, ProcLoadHelper helper) {
+        super(args, helper);
+        mProcLoadHelper = helper;
+        createHelperInstance(mProcLoadHelper);
+    }
+
+    /**
+     * Adds the options for total pss collector.
+     */
+    @Override
+    public void setupAdditionalArgs() {
+        Bundle args = getArgsBundle();
+
+        if (args.getString(PROC_LOAD_THRESHOLD) != null) {
+            mProcLoadHelper.setProcLoadThreshold(Double.parseDouble(args
+                    .getString(PROC_LOAD_THRESHOLD)));
+        }
+
+        if (args.getString(PROC_THRESHOLD_TIMEOUT) != null) {
+            mProcLoadHelper.setProcLoadWaitTimeInMs(Long.parseLong(args
+                    .getString(PROC_THRESHOLD_TIMEOUT)));
+        }
+
+        if (args.getString(PROC_LOAD_INTERVAL) != null) {
+            mProcLoadHelper.setProcLoadIntervalInMs(Long.parseLong(args
+                    .getString(PROC_LOAD_INTERVAL)));
+        }
+
+    }
+}
diff --git a/libraries/device-collectors/src/main/platform-collectors/Android.bp b/libraries/device-collectors/src/main/platform-collectors/Android.bp
index 719211c..89f3733 100644
--- a/libraries/device-collectors/src/main/platform-collectors/Android.bp
+++ b/libraries/device-collectors/src/main/platform-collectors/Android.bp
@@ -48,6 +48,7 @@
         "libprotobuf-java-lite",
         "statsd-config-protos",
         "statsd-helper",
+        "ub-uiautomator",
     ],
     platform_apis: true,
 }
diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
index 8652525..a9799f5 100644
--- a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
+++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdListener.java
@@ -67,6 +67,9 @@
     // Prefix template for test-level metric report files.
     static final String TEST_PREFIX_TEMPLATE = "%s-%d_";
 
+    // Common prefix for the metric key pointing to the report path.
+    static final String REPORT_KEY_PREFIX = "statsd-";
+
     // Configs used for the test run and each test, respectively.
     private Map<String, StatsdConfig> mRunLevelConfigs = new HashMap<String, StatsdConfig>();
     private Map<String, StatsdConfig> mTestLevelConfigs = new HashMap<String, StatsdConfig>();
@@ -103,7 +106,7 @@
                 pullReportsAndRemoveConfigs(
                         mRunLevelConfigIds, Paths.get(REPORT_PATH_ROOT, REPORT_PATH_RUN_LEVEL), "");
         for (String configName : configReports.keySet()) {
-            runData.addFileMetric(configName, configReports.get(configName));
+            runData.addFileMetric(REPORT_KEY_PREFIX + configName, configReports.get(configName));
         }
     }
 
@@ -128,7 +131,7 @@
                         Paths.get(REPORT_PATH_ROOT, REPORT_PATH_TEST_LEVEL),
                         getTestPrefix(description));
         for (String configName : configReports.keySet()) {
-            testData.addFileMetric(configName, configReports.get(configName));
+            testData.addFileMetric(REPORT_KEY_PREFIX + configName, configReports.get(configName));
         }
     }
 
diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ProcLoadListenerTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ProcLoadListenerTest.java
new file mode 100644
index 0000000..f90470e
--- /dev/null
+++ b/libraries/device-collectors/src/test/java/android/device/collectors/ProcLoadListenerTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ProcLoadListener.PROC_LOAD_THRESHOLD;
+import static android.device.collectors.ProcLoadListener.PROC_THRESHOLD_TIMEOUT;
+import static android.device.collectors.ProcLoadListener.PROC_LOAD_INTERVAL;
+
+
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.ProcLoadHelper;
+
+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 ProcLoadListener}.
+ *
+ * To run:
+ * atest CollectorDeviceLibTest:android.device.collectors.ProcLoadListenerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ProcLoadListenerTest {
+
+    @Mock
+    private Instrumentation mInstrumentation;
+    @Mock
+    private ProcLoadHelper mProcLoadHelper;
+
+    private ProcLoadListener mListener;
+    private Description mRunDesc;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mRunDesc = Description.createSuiteDescription("run");
+    }
+
+    private ProcLoadListener initListener(Bundle b) {
+        ProcLoadListener listener = new ProcLoadListener(b, mProcLoadHelper);
+        listener.setInstrumentation(mInstrumentation);
+        return listener;
+    }
+
+    @Test
+    public void testLoadProcAdditionalOptions() throws Exception {
+        Bundle b = new Bundle();
+        b.putString(PROC_LOAD_THRESHOLD, "1");
+        b.putString(PROC_THRESHOLD_TIMEOUT, "3");
+        b.putString(PROC_LOAD_INTERVAL, "2");
+        mListener = initListener(b);
+        mListener.testRunStarted(mRunDesc);
+
+        verify(mProcLoadHelper).setProcLoadThreshold(1);
+        verify(mProcLoadHelper).setProcLoadWaitTimeInMs(3L);
+        verify(mProcLoadHelper).setProcLoadIntervalInMs(2L);
+    }
+}
diff --git a/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java b/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
index e704a53..170ded2 100644
--- a/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
+++ b/libraries/device-collectors/src/test/platform/android/device/collectors/StatsdListenerTest.java
@@ -149,7 +149,7 @@
                         any());
         verify(runData, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getExactFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -167,7 +167,7 @@
                         any());
         verify(runData, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_2),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_2),
                         getExactFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -234,7 +234,7 @@
                         any());
         verify(testData, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -258,7 +258,7 @@
                         any());
         verify(testData, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_2),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_2),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -292,7 +292,7 @@
 
         verify(testData1, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -311,7 +311,7 @@
 
         verify(testData2, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -345,7 +345,7 @@
         // The metric file name should contain the iteration number (1).
         verify(testData1, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -365,7 +365,7 @@
         // The metric file name should contain the iteration number (2).
         verify(testData2, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -402,7 +402,7 @@
 
         verify(testData, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getPartialFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,
@@ -416,7 +416,7 @@
 
         verify(runData, times(1))
                 .addFileMetric(
-                        eq(CONFIG_NAME_1),
+                        eq(StatsdListener.REPORT_KEY_PREFIX + CONFIG_NAME_1),
                         getExactFileNameMatcher(
                                 Paths.get(
                                                 StatsdListener.REPORT_PATH_ROOT,