Add trace based window manager perf test

This may be temporary solution that parses the output of
atrace in text format on the device side. Once the perfetto
processing infrastructure is available (b/139542646) for
generic test environment, we can migrate to use that.

Bug: 131727899
Test: atest InternalWindowOperationPerfTest
Change-Id: Ic0304c292e42159fb11dee37152867290808f281
diff --git a/apct-tests/perftests/core/src/android/wm/InternalWindowOperationPerfTest.java b/apct-tests/perftests/core/src/android/wm/InternalWindowOperationPerfTest.java
new file mode 100644
index 0000000..c096cd2
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/wm/InternalWindowOperationPerfTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.wm;
+
+import static android.perftests.utils.ManualBenchmarkState.StatsReport;
+
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.perftests.utils.ManualBenchmarkState;
+import android.perftests.utils.ManualBenchmarkState.ManualBenchmarkTest;
+import android.perftests.utils.PerfManualStatusReporter;
+import android.perftests.utils.TraceMarkParser;
+import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
+import android.util.Log;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.lifecycle.Stage;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.TimeUnit;
+
+/** Measure the performance of internal methods in window manager service by trace tag. */
+@LargeTest
+public class InternalWindowOperationPerfTest extends WindowManagerPerfTestBase {
+    private static final String TAG = InternalWindowOperationPerfTest.class.getSimpleName();
+
+    @Rule
+    public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
+
+    @Rule
+    public final PerfTestActivityRule mActivityRule = new PerfTestActivityRule();
+
+    private final TraceMarkParser mTraceMarkParser = new TraceMarkParser(
+            "applyPostLayoutPolicy",
+            "applySurfaceChanges",
+            "AppTransitionReady",
+            "closeSurfaceTransactiom",
+            "openSurfaceTransaction",
+            "performLayout",
+            "performSurfacePlacement",
+            "prepareSurfaces",
+            "updateInputWindows",
+            "WSA#startAnimation",
+            "activityIdle",
+            "activityPaused",
+            "activityStopped",
+            "activityDestroyed",
+            "finishActivity",
+            "startActivityInner");
+
+    @Test
+    @ManualBenchmarkTest(
+            targetTestDurationNs = 20 * TIME_1_S_IN_NS,
+            statsReport = @StatsReport(
+                    flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN
+                            | StatsReport.FLAG_MAX | StatsReport.FLAG_COEFFICIENT_VAR))
+    public void testLaunchAndFinishActivity() throws Throwable {
+        final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        long measuredTimeNs = 0;
+        boolean isTraceStarted = false;
+
+        while (state.keepRunning(measuredTimeNs)) {
+            if (!isTraceStarted && !state.isWarmingUp()) {
+                startAsyncAtrace();
+                isTraceStarted = true;
+            }
+            final long startTime = SystemClock.elapsedRealtimeNanos();
+            mActivityRule.launchActivity();
+            mActivityRule.finishActivity();
+            mActivityRule.waitForIdleSync(Stage.DESTROYED);
+            measuredTimeNs = SystemClock.elapsedRealtimeNanos() - startTime;
+        }
+
+        stopAsyncAtrace();
+
+        mTraceMarkParser.forAllSlices((key, slices) -> {
+            for (TraceMarkSlice slice : slices) {
+                state.addExtraResult(key, (long) (slice.getDurarionInSeconds() * NANOS_PER_S));
+            }
+        });
+
+        Log.i(TAG, String.valueOf(mTraceMarkParser));
+    }
+
+    private void startAsyncAtrace() throws IOException {
+        sUiAutomation.executeShellCommand("atrace -b 32768 --async_start wm");
+        // Avoid atrace isn't ready immediately.
+        SystemClock.sleep(TimeUnit.NANOSECONDS.toMillis(TIME_1_S_IN_NS));
+    }
+
+    private void stopAsyncAtrace() throws IOException {
+        final ParcelFileDescriptor pfd = sUiAutomation.executeShellCommand("atrace --async_stop");
+        final InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                mTraceMarkParser.visit(line);
+            }
+        }
+    }
+}
diff --git a/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java b/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java
index 0c3b9e5..73b4a19 100644
--- a/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java
+++ b/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java
@@ -16,16 +16,13 @@
 
 package android.wm;
 
-import static android.perftests.utils.ManualBenchmarkState.STATS_REPORT_COEFFICIENT_VAR;
-import static android.perftests.utils.ManualBenchmarkState.STATS_REPORT_ITERATION;
-import static android.perftests.utils.ManualBenchmarkState.STATS_REPORT_MEAN;
+import static android.perftests.utils.ManualBenchmarkState.StatsReport;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static org.hamcrest.core.AnyOf.anyOf;
 import static org.hamcrest.core.Is.is;
 
-import android.app.Activity;
 import android.app.ActivityManager.TaskSnapshot;
 import android.app.ActivityTaskManager;
 import android.app.IActivityTaskManager;
@@ -39,23 +36,16 @@
 import android.perftests.utils.ManualBenchmarkState;
 import android.perftests.utils.ManualBenchmarkState.ManualBenchmarkTest;
 import android.perftests.utils.PerfManualStatusReporter;
-import android.perftests.utils.PerfTestActivity;
 import android.util.Pair;
 import android.view.IRecentsAnimationController;
 import android.view.IRecentsAnimationRunner;
 import android.view.RemoteAnimationTarget;
-import android.view.WindowManager;
 
 import androidx.test.filters.LargeTest;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.lifecycle.ActivityLifecycleCallback;
-import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
 import androidx.test.runner.lifecycle.Stage;
 
-import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Assume;
-import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
@@ -77,11 +67,10 @@
     public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
 
     @Rule
-    public final ActivityTestRule<PerfTestActivity> mActivityRule = new ActivityTestRule<>(
-            PerfTestActivity.class, false /* initialTouchMode */, false /* launchActivity */);
+    public final PerfTestActivityRule mActivityRule =
+            new PerfTestActivityRule(true /* launchActivity */);
 
     private long mMeasuredTimeNs;
-    private LifecycleListener mLifecycleListener;
 
     @Parameterized.Parameter(0)
     public int intervalBetweenOperations;
@@ -127,24 +116,6 @@
         sUiAutomation.dropShellPermissionIdentity();
     }
 
-    @Before
-    @Override
-    public void setUp() {
-        super.setUp();
-        final Activity testActivity = mActivityRule.launchActivity(null /* intent */);
-        try {
-            mActivityRule.runOnUiThread(() -> testActivity.getWindow()
-                    .addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
-        } catch (Throwable ignored) { }
-        mLifecycleListener = new LifecycleListener(testActivity);
-        ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(mLifecycleListener);
-    }
-
-    @After
-    public void tearDown() {
-        ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(mLifecycleListener);
-    }
-
     /** Simulate the timing of touch. */
     private void makeInterval() {
         SystemClock.sleep(intervalBetweenOperations);
@@ -167,8 +138,8 @@
     @ManualBenchmarkTest(
             warmupDurationNs = TIME_1_S_IN_NS,
             targetTestDurationNs = TIME_5_S_IN_NS,
-            statsReportFlags =
-                    STATS_REPORT_ITERATION | STATS_REPORT_MEAN | STATS_REPORT_COEFFICIENT_VAR)
+            statsReport = @StatsReport(flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN
+                    | StatsReport.FLAG_COEFFICIENT_VAR))
     public void testRecentsAnimation() throws Throwable {
         final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         final IActivityTaskManager atm = ActivityTaskManager.getService();
@@ -201,7 +172,7 @@
                 state.addExtraResult(finishCase.first, elapsedTimeNsOfFinish);
 
                 if (moveRecentsToTop) {
-                    mLifecycleListener.waitForIdleSync(Stage.STOPPED);
+                    mActivityRule.waitForIdleSync(Stage.STOPPED);
 
                     startTime = SystemClock.elapsedRealtimeNanos();
                     atm.startActivityFromRecents(testActivityTaskId, null /* options */);
@@ -209,7 +180,7 @@
                     mMeasuredTimeNs += elapsedTimeNs;
                     state.addExtraResult("startFromRecents", elapsedTimeNs);
 
-                    mLifecycleListener.waitForIdleSync(Stage.RESUMED);
+                    mActivityRule.waitForIdleSync(Stage.RESUMED);
                 }
 
                 makeInterval();
@@ -223,55 +194,18 @@
             }
         };
 
+        recentsSemaphore.tryAcquire();
         while (state.keepRunning(mMeasuredTimeNs)) {
-            Assume.assumeTrue(recentsSemaphore.tryAcquire(TIME_5_S_IN_NS, TimeUnit.NANOSECONDS));
+            mMeasuredTimeNs = 0;
 
             final long startTime = SystemClock.elapsedRealtimeNanos();
             atm.startRecentsActivity(sRecentsIntent, null /* unused */, anim);
             final long elapsedTimeNsOfStart = SystemClock.elapsedRealtimeNanos() - startTime;
             mMeasuredTimeNs += elapsedTimeNsOfStart;
             state.addExtraResult("start", elapsedTimeNsOfStart);
-        }
 
-        // Ensure the last round of animation callback is done.
-        recentsSemaphore.tryAcquire(TIME_5_S_IN_NS, TimeUnit.NANOSECONDS);
-        recentsSemaphore.release();
-    }
-
-    private static class LifecycleListener implements ActivityLifecycleCallback {
-        private final Activity mTargetActivity;
-        private Stage mWaitingStage;
-        private Stage mReceivedStage;
-
-        LifecycleListener(Activity activity) {
-            mTargetActivity = activity;
-        }
-
-        void waitForIdleSync(Stage state) {
-            synchronized (this) {
-                if (state != mReceivedStage) {
-                    mWaitingStage = state;
-                    try {
-                        wait(TimeUnit.NANOSECONDS.toMillis(TIME_5_S_IN_NS));
-                    } catch (InterruptedException impossible) { }
-                }
-                mWaitingStage = mReceivedStage = null;
-            }
-            getInstrumentation().waitForIdleSync();
-        }
-
-        @Override
-        public void onActivityLifecycleChanged(Activity activity, Stage stage) {
-            if (mTargetActivity != activity) {
-                return;
-            }
-
-            synchronized (this) {
-                mReceivedStage = stage;
-                if (mWaitingStage == mReceivedStage) {
-                    notifyAll();
-                }
-            }
+            // Ensure the animation callback is done.
+            Assume.assumeTrue(recentsSemaphore.tryAcquire(TIME_5_S_IN_NS, TimeUnit.NANOSECONDS));
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java b/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java
index 4864da4..4d278c3 100644
--- a/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java
+++ b/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java
@@ -18,9 +18,21 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import android.app.Activity;
 import android.app.UiAutomation;
+import android.content.Intent;
+import android.perftests.utils.PerfTestActivity;
 
-import org.junit.Before;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.lifecycle.ActivityLifecycleCallback;
+import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
+import androidx.test.runner.lifecycle.Stage;
+
+import org.junit.BeforeClass;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.concurrent.TimeUnit;
 
 public class WindowManagerPerfTestBase {
     static final UiAutomation sUiAutomation = getInstrumentation().getUiAutomation();
@@ -28,10 +40,102 @@
     static final long TIME_1_S_IN_NS = 1 * NANOS_PER_S;
     static final long TIME_5_S_IN_NS = 5 * NANOS_PER_S;
 
-    @Before
-    public void setUp() {
+    @BeforeClass
+    public static void setUpOnce() {
         // In order to be closer to the real use case.
         sUiAutomation.executeShellCommand("input keyevent KEYCODE_WAKEUP");
         sUiAutomation.executeShellCommand("wm dismiss-keyguard");
+        getInstrumentation().getContext().startActivity(new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+    }
+
+    /**
+     * Provides an activity that keeps screen on and is able to wait for a stable lifecycle stage.
+     */
+    static class PerfTestActivityRule extends ActivityTestRule<PerfTestActivity> {
+        private final Intent mStartIntent =
+                new Intent().putExtra(PerfTestActivity.INTENT_EXTRA_KEEP_SCREEN_ON, true);
+        private final LifecycleListener mLifecycleListener = new LifecycleListener();
+
+        PerfTestActivityRule() {
+            this(false /* launchActivity */);
+        }
+
+        PerfTestActivityRule(boolean launchActivity) {
+            super(PerfTestActivity.class, false /* initialTouchMode */, launchActivity);
+        }
+
+        @Override
+        public Statement apply(Statement base, Description description) {
+            final Statement wrappedStatement = new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    ActivityLifecycleMonitorRegistry.getInstance()
+                            .addLifecycleCallback(mLifecycleListener);
+                    base.evaluate();
+                    ActivityLifecycleMonitorRegistry.getInstance()
+                            .removeLifecycleCallback(mLifecycleListener);
+                }
+            };
+            return super.apply(wrappedStatement, description);
+        }
+
+        @Override
+        protected Intent getActivityIntent() {
+            return mStartIntent;
+        }
+
+        @Override
+        public PerfTestActivity launchActivity(Intent intent) {
+            final PerfTestActivity activity = super.launchActivity(intent);
+            mLifecycleListener.setTargetActivity(activity);
+            return activity;
+        }
+
+        PerfTestActivity launchActivity() {
+            return launchActivity(mStartIntent);
+        }
+
+        void waitForIdleSync(Stage state) {
+            mLifecycleListener.waitForIdleSync(state);
+        }
+    }
+
+    static class LifecycleListener implements ActivityLifecycleCallback {
+        private Activity mTargetActivity;
+        private Stage mWaitingStage;
+        private Stage mReceivedStage;
+
+        void setTargetActivity(Activity activity) {
+            mTargetActivity = activity;
+            mReceivedStage = mWaitingStage = null;
+        }
+
+        void waitForIdleSync(Stage stage) {
+            synchronized (this) {
+                if (stage != mReceivedStage) {
+                    mWaitingStage = stage;
+                    try {
+                        wait(TimeUnit.NANOSECONDS.toMillis(TIME_5_S_IN_NS));
+                    } catch (InterruptedException impossible) { }
+                }
+                mWaitingStage = mReceivedStage = null;
+            }
+            getInstrumentation().waitForIdleSync();
+        }
+
+        @Override
+        public void onActivityLifecycleChanged(Activity activity, Stage stage) {
+            if (mTargetActivity != activity) {
+                return;
+            }
+
+            synchronized (this) {
+                mReceivedStage = stage;
+                if (mWaitingStage == mReceivedStage) {
+                    notifyAll();
+                }
+            }
+        }
     }
 }
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/ManualBenchmarkState.java b/apct-tests/perftests/utils/src/android/perftests/utils/ManualBenchmarkState.java
index ffe39e8..a83254b 100644
--- a/apct-tests/perftests/utils/src/android/perftests/utils/ManualBenchmarkState.java
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/ManualBenchmarkState.java
@@ -59,27 +59,37 @@
 public final class ManualBenchmarkState {
     private static final String TAG = ManualBenchmarkState.class.getSimpleName();
 
-    @IntDef(prefix = {"STATS_REPORT"}, value = {
-            STATS_REPORT_MEDIAN,
-            STATS_REPORT_MEAN,
-            STATS_REPORT_MIN,
-            STATS_REPORT_MAX,
-            STATS_REPORT_PERCENTILE90,
-            STATS_REPORT_PERCENTILE95,
-            STATS_REPORT_STDDEV,
-            STATS_REPORT_ITERATION,
-    })
-    public @interface StatsReport {}
+    @Target(ElementType.ANNOTATION_TYPE)
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface StatsReport {
+        int FLAG_MEDIAN = 0x00000001;
+        int FLAG_MEAN = 0x00000002;
+        int FLAG_MIN = 0x00000004;
+        int FLAG_MAX = 0x00000008;
+        int FLAG_STDDEV = 0x00000010;
+        int FLAG_COEFFICIENT_VAR = 0x00000020;
+        int FLAG_ITERATION = 0x00000040;
 
-    public static final int STATS_REPORT_MEDIAN = 0x00000001;
-    public static final int STATS_REPORT_MEAN = 0x00000002;
-    public static final int STATS_REPORT_MIN = 0x00000004;
-    public static final int STATS_REPORT_MAX = 0x00000008;
-    public static final int STATS_REPORT_PERCENTILE90 = 0x00000010;
-    public static final int STATS_REPORT_PERCENTILE95 = 0x00000020;
-    public static final int STATS_REPORT_STDDEV = 0x00000040;
-    public static final int STATS_REPORT_COEFFICIENT_VAR = 0x00000080;
-    public static final int STATS_REPORT_ITERATION = 0x00000100;
+        @Retention(RetentionPolicy.RUNTIME)
+        @IntDef(value = {
+                FLAG_MEDIAN,
+                FLAG_MEAN,
+                FLAG_MIN,
+                FLAG_MAX,
+                FLAG_STDDEV,
+                FLAG_COEFFICIENT_VAR,
+                FLAG_ITERATION,
+        })
+        @interface Flag {}
+
+        /** Defines which type of statistics should output. */
+        @Flag int flags() default -1;
+        /** An array with value 0~100 to provide the percentiles. */
+        int[] percentiles() default {};
+    }
+
+    /** It means the entire {@link StatsReport} is not given. */
+    private static final int DEFAULT_STATS_REPORT = -2;
 
     // TODO: Tune these values.
     // warm-up for duration
@@ -116,8 +126,9 @@
     // The computation needs double precision, but long int is fine for final reporting.
     private Stats mStats;
 
-    private int mStatsReportFlags = STATS_REPORT_MEDIAN | STATS_REPORT_MEAN
-            | STATS_REPORT_PERCENTILE90 | STATS_REPORT_PERCENTILE95 | STATS_REPORT_STDDEV;
+    private int mStatsReportFlags =
+            StatsReport.FLAG_MEDIAN | StatsReport.FLAG_MEAN | StatsReport.FLAG_STDDEV;
+    private int[] mStatsReportPercentiles = {90 , 95};
 
     private boolean shouldReport(int statsReportFlag) {
         return (mStatsReportFlags & statsReportFlag) != 0;
@@ -136,9 +147,10 @@
         if (targetTestDurationNs >= 0) {
             mTargetTestDurationNs = targetTestDurationNs;
         }
-        final int statsReportFlags = testAnnotation.statsReportFlags();
-        if (statsReportFlags >= 0) {
-            mStatsReportFlags = statsReportFlags;
+        final StatsReport statsReport = testAnnotation.statsReport();
+        if (statsReport != null && statsReport.flags() != DEFAULT_STATS_REPORT) {
+            mStatsReportFlags = statsReport.flags();
+            mStatsReportPercentiles = statsReport.percentiles();
         }
     }
 
@@ -189,11 +201,20 @@
     }
 
     /**
-     * Adds additional result while this benchmark is running. It is used when a sequence of
+     * @return {@code true} if the benchmark is in warmup state. It can be used to skip the
+     *         operations or measurements that are unnecessary while the test isn't running the
+     *         actual benchmark.
+     */
+    public boolean isWarmingUp() {
+        return mState == WARMUP;
+    }
+
+    /**
+     * Adds additional result while this benchmark isn't warming up. It is used when a sequence of
      * operations is executed consecutively, the duration of each operation can also be recorded.
      */
     public void addExtraResult(String key, long duration) {
-        if (mState != RUNNING) {
+        if (isWarmingUp()) {
             return;
         }
         if (mExtraResults == null) {
@@ -221,31 +242,30 @@
     }
 
     private void fillStatus(Bundle status, String key, Stats stats) {
-        if (shouldReport(STATS_REPORT_ITERATION)) {
+        if (shouldReport(StatsReport.FLAG_ITERATION)) {
             status.putLong(key + "_iteration", stats.getSize());
         }
-        if (shouldReport(STATS_REPORT_MEDIAN)) {
+        if (shouldReport(StatsReport.FLAG_MEDIAN)) {
             status.putLong(key + "_median", stats.getMedian());
         }
-        if (shouldReport(STATS_REPORT_MEAN)) {
+        if (shouldReport(StatsReport.FLAG_MEAN)) {
             status.putLong(key + "_mean", Math.round(stats.getMean()));
         }
-        if (shouldReport(STATS_REPORT_MIN)) {
+        if (shouldReport(StatsReport.FLAG_MIN)) {
             status.putLong(key + "_min", stats.getMin());
         }
-        if (shouldReport(STATS_REPORT_MAX)) {
+        if (shouldReport(StatsReport.FLAG_MAX)) {
             status.putLong(key + "_max", stats.getMax());
         }
-        if (shouldReport(STATS_REPORT_PERCENTILE90)) {
-            status.putLong(key + "_percentile90", stats.getPercentile90());
+        if (mStatsReportPercentiles != null) {
+            for (int percentile : mStatsReportPercentiles) {
+                status.putLong(key + "_percentile" + percentile, stats.getPercentile(percentile));
+            }
         }
-        if (shouldReport(STATS_REPORT_PERCENTILE95)) {
-            status.putLong(key + "_percentile95", stats.getPercentile95());
-        }
-        if (shouldReport(STATS_REPORT_STDDEV)) {
+        if (shouldReport(StatsReport.FLAG_STDDEV)) {
             status.putLong(key + "_stddev", Math.round(stats.getStandardDeviation()));
         }
-        if (shouldReport(STATS_REPORT_COEFFICIENT_VAR)) {
+        if (shouldReport(StatsReport.FLAG_COEFFICIENT_VAR)) {
             status.putLong(key + "_cv",
                     Math.round((100 * stats.getStandardDeviation() / stats.getMean())));
         }
@@ -276,6 +296,6 @@
     public @interface ManualBenchmarkTest {
         long warmupDurationNs() default -1;
         long targetTestDurationNs() default -1;
-        @StatsReport int statsReportFlags() default -1;
+        StatsReport statsReport() default @StatsReport(flags = DEFAULT_STATS_REPORT);
     }
 }
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java b/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java
index 375a901..e934feb 100644
--- a/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java
@@ -19,8 +19,24 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
 
+/**
+ * A simple activity used for testing, e.g. performance of activity switching, or as a base
+ * container of testing view.
+ */
 public class PerfTestActivity extends Activity {
+    public static final String INTENT_EXTRA_KEEP_SCREEN_ON = "keep_screen_on";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (getIntent().getBooleanExtra(INTENT_EXTRA_KEEP_SCREEN_ON, false)) {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        }
+    }
+
     public static Intent createLaunchIntent(Context context) {
         final Intent intent = new Intent();
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/Stats.java b/apct-tests/perftests/utils/src/android/perftests/utils/Stats.java
index f650e81..fb516a8 100644
--- a/apct-tests/perftests/utils/src/android/perftests/utils/Stats.java
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/Stats.java
@@ -16,34 +16,34 @@
 
 package android.perftests.utils;
 
+import android.annotation.IntRange;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 public class Stats {
-    private long mMedian, mMin, mMax, mPercentile90, mPercentile95;
+    private long mMedian, mMin, mMax;
     private double mMean, mStandardDeviation;
-    private final int mSize;
+    private final List<Long> mValues;
 
     /* Calculate stats in constructor. */
     public Stats(List<Long> values) {
-        // make a copy since we're modifying it
-        values = new ArrayList<>(values);
         final int size = values.size();
         if (size < 2) {
             throw new IllegalArgumentException("At least two results are necessary.");
         }
 
+        // Make a copy since we're modifying it.
+        mValues = values = new ArrayList<>(values);
+
         Collections.sort(values);
 
-        mSize = size;
         mMin = values.get(0);
         mMax = values.get(values.size() - 1);
 
         mMedian = size % 2 == 0 ? (values.get(size / 2) + values.get(size / 2 - 1)) / 2 :
                 values.get(size / 2);
-        mPercentile90 = getPercentile(values, 90);
-        mPercentile95 = getPercentile(values, 95);
 
         for (int i = 0; i < size; ++i) {
             long result = values.get(i);
@@ -59,7 +59,7 @@
     }
 
     public int getSize() {
-        return mSize;
+        return mValues.size();
     }
 
     public double getMean() {
@@ -82,12 +82,8 @@
         return mStandardDeviation;
     }
 
-    public long getPercentile90() {
-        return mPercentile90;
-    }
-
-    public long getPercentile95() {
-        return mPercentile95;
+    public long getPercentile(@IntRange(from = 0, to = 100) int percentile) {
+        return getPercentile(mValues, percentile);
     }
 
     private static long getPercentile(List<Long> values, int percentile) {
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/TraceMarkParser.java b/apct-tests/perftests/utils/src/android/perftests/utils/TraceMarkParser.java
new file mode 100644
index 0000000..1afed3a
--- /dev/null
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/TraceMarkParser.java
@@ -0,0 +1,232 @@
+/*
+ * 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.perftests.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+
+/**
+ * Utility to get the slice of tracing_mark_write S,F,B,E (Async: start, finish, Sync: begin, end).
+ * Use {@link #visit(String)} to process the trace in text form. The filtered results can be
+ * obtained by {@link #forAllSlices(BiConsumer)}.
+ *
+ * @see android.os.Trace
+ */
+public class TraceMarkParser {
+    /** All slices by the name of {@link TraceMarkLine}. */
+    private final Map<String, List<TraceMarkSlice>> mSlicesMap = new HashMap<>();
+    /** The nested depth of each task-pid. */
+    private final Map<String, Integer> mDepthMap = new HashMap<>();
+    /** The start trace lines that haven't matched the corresponding end. */
+    private final Map<String, TraceMarkLine> mPendingStarts = new HashMap<>();
+
+    private final Predicate<TraceMarkLine> mTraceLineFilter;
+
+    public TraceMarkParser(Predicate<TraceMarkLine> traceLineFilter) {
+        mTraceLineFilter = traceLineFilter;
+    }
+
+    /** Only accept the trace event with the given names. */
+    public TraceMarkParser(String... traceNames) {
+        this(line -> {
+            for (String name : traceNames) {
+                if (name.equals(line.name)) {
+                    return true;
+                }
+            }
+            return false;
+        });
+    }
+
+    /** Computes {@link TraceMarkSlice} by the given trace line. */
+    public void visit(String textTraceLine) {
+        final TraceMarkLine line = TraceMarkLine.parse(textTraceLine);
+        if (line == null) {
+            return;
+        }
+
+        if (line.isAsync) {
+            // Async-trace contains name in the start and finish event.
+            if (mTraceLineFilter.test(line)) {
+                if (line.isBegin) {
+                    mPendingStarts.put(line.name, line);
+                } else {
+                    final TraceMarkLine start = mPendingStarts.remove(line.name);
+                    if (start != null) {
+                        addSlice(start, line);
+                    }
+                }
+            }
+            return;
+        }
+
+        int depth = 1;
+        if (line.isBegin) {
+            final Integer existingDepth = mDepthMap.putIfAbsent(line.taskPid, 1);
+            if (existingDepth != null) {
+                mDepthMap.put(line.taskPid, depth = existingDepth + 1);
+            }
+            // Sync-trace only contains name in the begin event.
+            if (mTraceLineFilter.test(line)) {
+                mPendingStarts.put(getSyncPendingStartKey(line, depth), line);
+            }
+        } else {
+            final Integer existingDepth = mDepthMap.get(line.taskPid);
+            if (existingDepth != null) {
+                depth = existingDepth;
+                mDepthMap.put(line.taskPid, existingDepth - 1);
+            }
+            final TraceMarkLine begin = mPendingStarts.remove(getSyncPendingStartKey(line, depth));
+            if (begin != null) {
+                addSlice(begin, line);
+            }
+        }
+    }
+
+    private static String getSyncPendingStartKey(TraceMarkLine line, int depth) {
+        return line.taskPid + "@" + depth;
+    }
+
+    private void addSlice(TraceMarkLine begin, TraceMarkLine end) {
+        mSlicesMap.computeIfAbsent(
+                begin.name, k -> new ArrayList<>()).add(new TraceMarkSlice(begin, end));
+    }
+
+    public void forAllSlices(BiConsumer<String, List<TraceMarkSlice>> consumer) {
+        for (Map.Entry<String, List<TraceMarkSlice>> entry : mSlicesMap.entrySet()) {
+            consumer.accept(entry.getKey(), entry.getValue());
+        }
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        forAllSlices((key, slices) -> {
+            double totalMs = 0;
+            for (TraceMarkSlice s : slices) {
+                totalMs += s.getDurarionInSeconds() * 1000;
+            }
+            sb.append(key).append(" count=").append(slices.size()).append(" avg=")
+                    .append(totalMs / slices.size()).append("ms\n");
+        });
+
+        if (!mPendingStarts.isEmpty()) {
+            sb.append("[Warning] Unresolved events:").append(mPendingStarts).append("\n");
+        }
+        return sb.toString();
+    }
+
+    public static class TraceMarkSlice {
+        public final TraceMarkLine begin;
+        public final TraceMarkLine end;
+
+        TraceMarkSlice(TraceMarkLine begin, TraceMarkLine end) {
+            this.begin = begin;
+            this.end = end;
+        }
+
+        public double getDurarionInSeconds() {
+            return end.timestamp - begin.timestamp;
+        }
+    }
+
+    // taskPid                               timestamp                           name
+    // # Async:
+    // Binder:129_F-349  ( 1296) [003] ...1  12.2776: tracing_mark_write: S|1296|launching: a.test|0
+    // android.anim-135  ( 1296) [005] ...1  12.3361: tracing_mark_write: F|1296|launching: a.test|0
+    // # Normal:
+    // Binder:129_6-315  ( 1296) [007] ...1  97.4576: tracing_mark_write: B|1296|relayoutWindow: xxx
+    // ... there may have other nested begin/end
+    // Binder:129_6-315  ( 1296) [007] ...1  97.4580: tracing_mark_write: E|1296
+    public static class TraceMarkLine {
+        static final String EVENT_KEYWORD = ": tracing_mark_write: ";
+        static final char ASYNC_START = 'S';
+        static final char ASYNC_FINISH = 'F';
+        static final char SYNC_BEGIN = 'B';
+        static final char SYNC_END = 'E';
+
+        public final String taskPid;
+        public final double timestamp;
+        public final String name;
+        public final boolean isAsync;
+        public final boolean isBegin;
+
+        TraceMarkLine(String rawLine, int typePos, int type) throws IllegalArgumentException {
+            taskPid = rawLine.substring(0, rawLine.indexOf('(')).trim();
+            final int timeEnd = rawLine.indexOf(':', taskPid.length());
+            if (timeEnd < 0) {
+                throw new IllegalArgumentException("Timestamp end not found");
+            }
+            final int timeBegin = rawLine.lastIndexOf(' ', timeEnd);
+            if (timeBegin < 0) {
+                throw new IllegalArgumentException("Timestamp start not found");
+            }
+            timestamp = Double.parseDouble(rawLine.substring(timeBegin, timeEnd));
+            isAsync = type == ASYNC_START || type == ASYNC_FINISH;
+            isBegin = type == ASYNC_START || type == SYNC_BEGIN;
+
+            if (!isAsync && !isBegin) {
+                name = "";
+            } else {
+                // Get the position of the second '|' from "S|1234|name".
+                final int nameBegin = rawLine.indexOf('|', typePos + 2) + 1;
+                if (nameBegin == 0) {
+                    throw new IllegalArgumentException("Name begin not found");
+                }
+                if (isAsync) {
+                    // Get the name from "S|1234|name|0".
+                    name = rawLine.substring(nameBegin, rawLine.lastIndexOf('|'));
+                } else {
+                    name = rawLine.substring(nameBegin);
+                }
+            }
+        }
+
+        static TraceMarkLine parse(String rawLine) {
+            final int eventPos = rawLine.indexOf(EVENT_KEYWORD);
+            if (eventPos < 0) {
+                return null;
+            }
+            final int typePos = eventPos + EVENT_KEYWORD.length();
+            if (typePos >= rawLine.length()) {
+                return null;
+            }
+            final int type = rawLine.charAt(typePos);
+            if (type != ASYNC_START && type != ASYNC_FINISH
+                    && type != SYNC_BEGIN  && type != SYNC_END) {
+                return null;
+            }
+
+            try {
+                return new TraceMarkLine(rawLine, typePos, type);
+            } catch (IllegalArgumentException e) {
+                e.printStackTrace();
+            }
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return "TraceMarkLine{pid=" + taskPid + " time=" + timestamp + " name=" + name
+                    + " async=" + isAsync + " begin=" + isBegin + "}";
+        }
+    }
+}