Merge "Moved profile to RunListener interface."
diff --git a/libraries/collectors-helper/memory/src/com/android/helpers/ProcessShowmapHelper.java b/libraries/collectors-helper/memory/src/com/android/helpers/ProcessShowmapHelper.java
index cc57382..6584363 100644
--- a/libraries/collectors-helper/memory/src/com/android/helpers/ProcessShowmapHelper.java
+++ b/libraries/collectors-helper/memory/src/com/android/helpers/ProcessShowmapHelper.java
@@ -22,6 +22,8 @@
 import android.support.test.uiautomator.UiDevice;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 
 import java.io.IOException;
@@ -42,7 +44,6 @@
  * metrics = processShowmapHelper.getMetrics();
  * processShowmapHelper.stopCollecting();
  *
- * TODO(b/119675321) Take in multiple processes and output metrics for each
  * TODO(b/119684651) Add support for writing showmap output to file
  */
 public class ProcessShowmapHelper implements ICollectorHelper<Long> {
@@ -56,9 +57,9 @@
     private static final String VSS = "vss";
     private static final String DELTA = "delta";
 
-    private String mProcessName;
-    private ShowmapMetrics mTestStartMetrics;
-    private ShowmapMetrics mTestEndMetrics;
+    private String[] mProcessNames;
+    private ShowmapMetrics[] mTestStartMetrics;
+    private ShowmapMetrics[] mTestEndMetrics;
     private UiDevice mUiDevice;
 
     private static final class ShowmapMetrics {
@@ -70,16 +71,16 @@
     /**
      * Sets up the helper before it starts sampling.
      *
-     * @param processName process name to sample
+     * @param processNames process names to sample
      */
-    public void setUp(String processName) {
-        mProcessName = processName;
+    public void setUp(String... processNames) {
+        mProcessNames = processNames;
         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
     }
 
     @Override
     public boolean startCollecting() {
-        mTestStartMetrics = sampleMemory(mProcessName);
+        mTestStartMetrics = sampleMemoryOfProcesses(mProcessNames);
         return mTestStartMetrics != null;
     }
 
@@ -87,28 +88,37 @@
     public Map<String, Long> getMetrics() {
         // Collect end sample.
         HashMap<String, Long> showmapFinalMap = new HashMap<>();
-        mTestEndMetrics = sampleMemory(mProcessName);
+        mTestEndMetrics = sampleMemoryOfProcesses(mProcessNames);
         if (mTestEndMetrics == null) {
-            Log.e(TAG, "Unable to collect showmap memory at test end.");
+            Log.e(TAG, "Unable to collect any showmap metrics at end. Returning empty metrics");
             return showmapFinalMap;
         }
 
-        // Calculate and determine final metrics.
-        showmapFinalMap.put(constructKey(mProcessName, PSS), mTestEndMetrics.pss);
-        showmapFinalMap.put(constructKey(mProcessName, RSS), mTestEndMetrics.rss);
-        showmapFinalMap.put(constructKey(mProcessName, VSS), mTestEndMetrics.vss);
+        // Iterate over each process and collate start and end sample to build final metrics.
+        for (int i = 0; i < mTestEndMetrics.length; i++) {
+            String processName = mProcessNames[i];
+            ShowmapMetrics endMetrics = mTestEndMetrics[i];
+            if (endMetrics == null) {
+                // Failed to get end metrics for this process. Continue.
+                continue;
+            }
+            // Calculate and determine final metrics.
+            showmapFinalMap.put(constructKey(processName, PSS), endMetrics.pss);
+            showmapFinalMap.put(constructKey(processName, RSS), endMetrics.rss);
+            showmapFinalMap.put(constructKey(processName, VSS), endMetrics.vss);
 
-        if (mTestStartMetrics == null) {
-            Log.i(TAG, "Unable to get deltas because we were unable to sample memory when the "
-                    + "test began");
-            return showmapFinalMap;
+            if (mTestStartMetrics == null || mTestStartMetrics[i] == null) {
+                // Failed to get start metrics for this process. Continue.
+                continue;
+            }
+            ShowmapMetrics startMetrics = mTestStartMetrics[i];
+            showmapFinalMap.put(
+                    constructKey(processName, PSS, DELTA), endMetrics.pss - startMetrics.pss);
+            showmapFinalMap.put(
+                    constructKey(processName, RSS, DELTA), endMetrics.rss - startMetrics.rss);
+            showmapFinalMap.put(
+                    constructKey(processName, VSS, DELTA), endMetrics.vss - startMetrics.vss);
         }
-        showmapFinalMap.put(constructKey(mProcessName, PSS, DELTA),
-                mTestEndMetrics.pss - mTestStartMetrics.pss);
-        showmapFinalMap.put(constructKey(mProcessName, RSS, DELTA),
-                mTestEndMetrics.rss - mTestStartMetrics.rss);
-        showmapFinalMap.put(constructKey(mProcessName, VSS, DELTA),
-                mTestEndMetrics.vss - mTestStartMetrics.vss);
         return showmapFinalMap;
     }
 
@@ -119,16 +129,30 @@
     }
 
     /**
+     * Sample the current memory for a set of processes using showmap.
+     *
+     * @param processNames the process names to sample
+     * @return a list of showmap metrics for each process given in order. May be null if it is not
+     *     properly set up.
+     */
+    private @Nullable ShowmapMetrics[] sampleMemoryOfProcesses(String... processNames) {
+        if (processNames == null || mUiDevice == null) {
+            Log.e(TAG, "Process names or UI device is null. Make sure you've called setup.");
+            return null;
+        }
+        ShowmapMetrics[] metrics = new ShowmapMetrics[processNames.length];
+        for (int i = 0; i < processNames.length; i++) {
+            metrics[i] = sampleMemory(processNames[i]);
+        }
+        return metrics;
+    }
+
+    /**
      * Samples the current memory use of the process using showmap. Gets PSS, RSS, and VSS.
      *
      * @return metrics object with pss, rss, and vss
      */
-    private ShowmapMetrics sampleMemory(String processName) {
-        if (processName == null || mUiDevice == null) {
-            Log.e(TAG,"Process name or UI device is null. Make sure you've called setup.");
-            return null;
-        }
-
+    private @Nullable ShowmapMetrics sampleMemory(@NonNull String processName) {
         // Get pid
         int pid;
         try {
@@ -137,7 +161,7 @@
                 String.format(PIDOF_CMD, processName));
             pid = NumberFormat.getInstance().parse(pidofOutput).intValue();
         } catch (IOException | ParseException e) {
-            Log.e(TAG, "Unable to get pid of process", e);
+            Log.e(TAG, String.format("Unable to get pid of %s ", processName), e);
             return null;
         }
 
@@ -168,7 +192,7 @@
             metrics.rss = sc.nextLong();
             metrics.pss = sc.nextLong();
         } catch (IndexOutOfBoundsException | InputMismatchException e) {
-            Log.e(TAG, "Unexpected showmap format", e);
+            Log.e(TAG, String.format("Unexpected showmap format for %s ", processName), e);
             return null;
         }
         return metrics;
diff --git a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ProcessShowmapHelperTest.java b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ProcessShowmapHelperTest.java
index a45c80c..9bf48be 100644
--- a/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ProcessShowmapHelperTest.java
+++ b/libraries/collectors-helper/memory/test/src/com/android/helpers/tests/ProcessShowmapHelperTest.java
@@ -42,6 +42,8 @@
 
     // 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";
     // Pss string in key
     private static final String PSS = "pss";
     // Delta string in keys
@@ -63,13 +65,12 @@
         assertFalse(mShowmapHelper.startCollecting());
     }
 
-    /**
-     * Test start collecting returns false if the process name is empty.
-     */
+    /** Test no metrics are sampled if process name is empty. */
     @Test
     public void testEmptyProcessName() {
         mShowmapHelper.setUp("");
-        assertFalse(mShowmapHelper.startCollecting());
+        Map<String, Long> showmapMetrics = mShowmapHelper.getMetrics();
+        assertTrue(showmapMetrics.isEmpty());
     }
 
     /**
@@ -81,16 +82,26 @@
         assertTrue(mShowmapHelper.startCollecting());
     }
 
-    /**
-     * Test getting metrics based off sampled memory.
-     */
+    /** Test getting metrics based off sampled memory. */
     @Test
-    public void testGetMetrics() {
+    public void testGetMetrics_OneProcess() {
         mShowmapHelper.setUp(TEST_PROCESS_NAME);
         assertTrue(mShowmapHelper.startCollecting());
         Map<String, Long> showmapMetrics = mShowmapHelper.getMetrics();
-        assertTrue(!showmapMetrics.isEmpty());
+        assertFalse(showmapMetrics.isEmpty());
         assertTrue(showmapMetrics.containsKey(constructKey(TEST_PROCESS_NAME, PSS)));
         assertTrue(showmapMetrics.containsKey(constructKey(TEST_PROCESS_NAME, PSS, DELTA)));
     }
+
+    @Test
+    public void testGetMetrics_MultipleProcesses() {
+        mShowmapHelper.setUp(TEST_PROCESS_NAME, TEST_PROCESS_NAME_2);
+        assertTrue(mShowmapHelper.startCollecting());
+        Map<String, Long> showmapMetrics = mShowmapHelper.getMetrics();
+        assertFalse(showmapMetrics.isEmpty());
+        assertTrue(showmapMetrics.containsKey(constructKey(TEST_PROCESS_NAME, PSS)));
+        assertTrue(showmapMetrics.containsKey(constructKey(TEST_PROCESS_NAME, PSS, DELTA)));
+        assertTrue(showmapMetrics.containsKey(constructKey(TEST_PROCESS_NAME_2, PSS)));
+        assertTrue(showmapMetrics.containsKey(constructKey(TEST_PROCESS_NAME_2, PSS, DELTA)));
+    }
 }
diff --git a/libraries/rule/src/android/platform/test/rule/StopwatchRule.java b/libraries/rule/src/android/platform/test/rule/StopwatchRule.java
new file mode 100644
index 0000000..eed7071
--- /dev/null
+++ b/libraries/rule/src/android/platform/test/rule/StopwatchRule.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.platform.test.rule;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.rules.Stopwatch;
+import org.junit.runner.Description;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The rule measures the test execution time by extending {@link org.junit.rules.Stopwatch}. It will
+ * report the test time as key value pair to the instrumentation. For now, the rule will only report
+ * metric when the test succeed.
+ *
+ * <p>TODO: Consider implementing generic metric reporting library or tight listeners to report
+ * metric.
+ */
+public class StopwatchRule extends Stopwatch {
+    /**
+     * Metrics will be reported under the "status in progress" for test cases to be associated with
+     * the running use cases.
+     */
+    @VisibleForTesting static final int INST_STATUS_IN_PROGRESS = 2;
+
+    @VisibleForTesting static final String METRIC_FORMAT = "duration_ms_%s#%s";
+
+    private Bundle mResult = new Bundle();
+    private Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+    /**
+     * The method will report test time as milliseconds to instrumentation.
+     *
+     * @param nanos Test time input as nano seconds
+     * @param description Description for for the test
+     */
+    private void reportMetric(long nanos, Description description) {
+        String metricKey =
+                String.format(
+                        METRIC_FORMAT, description.getClassName(), description.getMethodName());
+        long millis = TimeUnit.NANOSECONDS.toMillis(nanos);
+        mResult.putLong(metricKey, millis);
+        mInstrumentation.sendStatus(INST_STATUS_IN_PROGRESS, mResult);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void succeeded(long nanos, Description description) {
+        reportMetric(nanos, description);
+    }
+
+    @VisibleForTesting
+    Bundle getMetric() {
+        return mResult;
+    }
+
+    @VisibleForTesting
+    void setInstrumentation(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+}
diff --git a/libraries/rule/tests/src/android/platform/test/rule/StopwatchRuleTest.java b/libraries/rule/tests/src/android/platform/test/rule/StopwatchRuleTest.java
new file mode 100644
index 0000000..fea19e8
--- /dev/null
+++ b/libraries/rule/tests/src/android/platform/test/rule/StopwatchRuleTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.platform.test.rule;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.Statement;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public class StopwatchRuleTest {
+
+    private static final int SLEEP_TIME_MS = 1000;
+    private static final int TIME_DELTA_MS = 20;
+
+    @Test
+    public void testMeasurementIsCorrect() throws Throwable {
+        StopwatchRule rule = new StopwatchRule();
+        rule.apply(
+                        new Statement() {
+                            //Mock a test that will take 1000ms long
+                            @Override
+                            public void evaluate() throws Throwable {
+                                Thread.sleep(SLEEP_TIME_MS);
+                            }
+                        },
+                        Description.createTestDescription("clzz", "method"))
+                .evaluate();
+
+        Bundle metric = rule.getMetric();
+        String metricKey = String.format(StopwatchRule.METRIC_FORMAT, "clzz", "method");
+        long value = metric.getLong(metricKey);
+        // Assert if StopwatchRule correctly measures the test time.
+        assertEquals(SLEEP_TIME_MS, value, TIME_DELTA_MS);
+    }
+
+    @Test
+    public void testMetricSendToInstr() throws Throwable {
+        StopwatchRule rule = new StopwatchRule();
+        Instrumentation instr = Mockito.mock(Instrumentation.class);
+        rule.setInstrumentation(instr);
+        rule.apply(
+                        new Statement() {
+                            @Override
+                            public void evaluate() throws Throwable {}
+                        },
+                        Description.EMPTY)
+                .evaluate();
+        verify(instr).sendStatus(StopwatchRule.INST_STATUS_IN_PROGRESS, rule.getMetric());
+    }
+}