Provide option to add iteration number microbenchmark runner.

Suffix with iteration number to uniquely identify the test results.

Bug: b/139395137

Test: MicrobenchmarkTest
Change-Id: Ibf1c9431c0e486b589dac18919b62e9683ae3675
(cherry picked from commit 85112086f22951249a2efd1e2c62bf7826d3b666)
diff --git a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
index 0cafd5c..980b11e 100644
--- a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
+++ b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
@@ -25,9 +25,13 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
 import org.junit.runners.BlockJUnit4ClassRunner;
 import org.junit.runners.model.InitializationError;
 import org.junit.runners.model.FrameworkMethod;
@@ -41,6 +45,17 @@
 public class Microbenchmark extends BlockJUnit4ClassRunner {
     private Bundle mArguments;
 
+    @VisibleForTesting static final String ITERATION_SEP_OPTION = "iteration-separator";
+    @VisibleForTesting static final String ITERATION_SEP_DEFAULT = "$";
+    // A constant to indicate that the iteration number is not set.
+    @VisibleForTesting static final int ITERATION_NOT_SET = -1;
+    public static final String RENAME_ITERATION_OPTION = "rename-iterations";
+
+    private String mIterationSep = ITERATION_SEP_DEFAULT;
+
+    private boolean mRenameIterations;
+    private Map<Description, Integer> mIterations = new HashMap<>();
+
     /**
      * Called reflectively on classes annotated with {@code @RunWith(Microbenchmark.class)}.
      */
@@ -55,6 +70,12 @@
     Microbenchmark(Class<?> klass, Bundle arguments) throws InitializationError {
         super(klass);
         mArguments = arguments;
+        // Parse out additional options.
+        mRenameIterations = Boolean.valueOf(arguments.getString(RENAME_ITERATION_OPTION));
+        mIterationSep =
+                arguments.containsKey(ITERATION_SEP_OPTION)
+                        ? arguments.getString(ITERATION_SEP_OPTION)
+                        : mIterationSep;
     }
 
     /**
@@ -115,4 +136,40 @@
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD, ElementType.METHOD})
     public @interface TightMethodRule { }
+
+
+    /**
+     * Rename the child class name to add iterations if the renaming iteration option is enabled.
+     *
+     * <p>Renaming the class here is chosen over renaming the method name because
+     *
+     * <ul>
+     *   <li>Conceptually, the runner is running a class multiple times, as opposed to a method.
+     *   <li>When instrumenting a suite in command line, by default the instrumentation command
+     *       outputs the class name only. Renaming the class helps with interpretation in this case.
+     */
+    @Override
+    protected Description describeChild(FrameworkMethod method) {
+        Description original = super.describeChild(method);
+        if (!mRenameIterations) {
+            return original;
+        }
+        return Description.createTestDescription(
+                String.join(mIterationSep, original.getClassName(),
+                        String.valueOf(mIterations.get(original))), original.getMethodName());
+    }
+
+    /**
+     * Keep track of the number of iterations for a particular method and
+     * set the current iteration count for changing the current description.
+     */
+    @Override
+    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
+        if (mRenameIterations) {
+            Description original = super.describeChild(method);
+            mIterations.computeIfPresent(original, (k, v) -> v + 1);
+            mIterations.computeIfAbsent(original, k -> 1);
+        }
+        super.runChild(method, notifier);
+    }
 }
diff --git a/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java b/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
index 0de79e3..c644fc8 100644
--- a/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
+++ b/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
@@ -80,6 +80,106 @@
     }
 
     /**
+     * Test iterations number are added to the test name with default suffix.
+     *
+     * Before --> TightBefore --> Trace (begin) with suffix @1 --> Test --> Trace(end)
+     *  --> TightAfter --> After --> Before --> TightBefore --> Trace (begin) with suffix @2
+     *  --> Test --> Trace(end) --> TightAfter --> After
+     */
+    @Test
+    public void testMultipleIterationsWithRename() throws InitializationError {
+        Bundle args = new Bundle();
+        args.putString("iterations", "2");
+        args.putString("rename-iterations", "true");
+        LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
+        loggingRunner.setOperationLog(new ArrayList<String>());
+        Result result = new JUnitCore().run(loggingRunner);
+        assertThat(result.wasSuccessful()).isTrue();
+        assertThat(loggingRunner.getOperationLog()).containsExactly(
+                "before",
+                "tight before",
+                "begin: testMethod("
+                    + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest$1)",
+                "test",
+                "end",
+                "tight after",
+                "after",
+                "before",
+                "tight before",
+                "begin: testMethod("
+                    + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest$2)",
+                "test",
+                "end",
+                "tight after",
+                "after")
+            .inOrder();
+    }
+
+    /**
+     * Test iterations number are added to the test name with custom suffix.
+     *
+     * Before --> TightBefore --> Trace (begin) with suffix --1 --> Test --> Trace(end)
+     *  --> TightAfter --> After --> Before --> TightBefore --> Trace (begin) with suffix --2
+     *   --> Test --> Trace(end) --> TightAfter --> After
+     */
+    @Test
+    public void testMultipleIterationsWithDifferentSuffix() throws InitializationError {
+        Bundle args = new Bundle();
+        args.putString("iterations", "2");
+        args.putString("rename-iterations", "true");
+        args.putString("iteration-separator", "--");
+        LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
+        loggingRunner.setOperationLog(new ArrayList<String>());
+        Result result = new JUnitCore().run(loggingRunner);
+        assertThat(result.wasSuccessful()).isTrue();
+        assertThat(loggingRunner.getOperationLog()).containsExactly(
+                "before",
+                "tight before",
+                "begin: testMethod("
+                    + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest--1)",
+                "test",
+                "end",
+                "tight after",
+                "after",
+                "before",
+                "tight before",
+                "begin: testMethod("
+                    + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest--2)",
+                "test",
+                "end",
+                "tight after",
+                "after")
+            .inOrder();
+    }
+
+    /**
+     * Test iteration number are not added to the test name when explictly disabled.
+     *
+     * Before --> TightBefore --> Trace (begin) --> Test --> Trace(end) --> TightAfter
+     *  --> After
+     */
+    @Test
+    public void testMultipleIterationsWithoutRename() throws InitializationError {
+        Bundle args = new Bundle();
+        args.putString("iterations", "1");
+        args.putString("rename-iterations", "false");
+        LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
+        loggingRunner.setOperationLog(new ArrayList<String>());
+        Result result = new JUnitCore().run(loggingRunner);
+        assertThat(result.wasSuccessful()).isTrue();
+        assertThat(loggingRunner.getOperationLog()).containsExactly(
+                "before",
+                "tight before",
+                "begin: testMethod("
+                    + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest)",
+                "test",
+                "end",
+                "tight after",
+                "after")
+            .inOrder();
+    }
+
+    /**
      * An extensions of the {@link Microbenchmark} runner that logs the start and end of collecting
      * traces. It also passes the operation log to the provided test {@code Class}, if it is a
      * {@link LoggingTest}. This is used for ensuring the proper order for evaluating test {@link