Merge "Add performance test for operations of recents activity"
diff --git a/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java b/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java
new file mode 100644
index 0000000..4e2b281
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/wm/RecentsAnimationPerfTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.STATS_REPORT_COEFFICIENT_VAR;
+import static android.perftests.utils.ManualBenchmarkState.STATS_REPORT_ITERATION;
+import static android.perftests.utils.ManualBenchmarkState.STATS_REPORT_MEAN;
+
+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.ActivityTaskManager;
+import android.app.IActivityTaskManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.perftests.utils.ManualBenchmarkState;
+import android.perftests.utils.ManualBenchmarkState.ManualBenchmarkTest;
+import android.perftests.utils.PerfManualStatusReporter;
+import android.perftests.utils.StubActivity;
+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;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
+    private static Intent sRecentsIntent;
+
+    @Rule
+    public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
+
+    @Rule
+    public final ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule<>(
+            StubActivity.class, false /* initialTouchMode */, false /* launchActivity */);
+
+    private long mMeasuredTimeNs;
+    private LifecycleListener mLifecycleListener;
+
+    @Parameterized.Parameter(0)
+    public int intervalBetweenOperations;
+
+    @Parameterized.Parameters(name = "interval{0}ms")
+    public static Collection<Object[]> getParameters() {
+        return Arrays.asList(new Object[][] {
+                { 0 },
+                { 100 },
+                { 300 },
+        });
+    }
+
+    @BeforeClass
+    public static void setUpClass() {
+        // Get the permission to invoke startRecentsActivity.
+        sUiAutomation.adoptShellPermissionIdentity();
+
+        final Context context = getInstrumentation().getContext();
+        final PackageManager pm = context.getPackageManager();
+        final ComponentName defaultHome = pm.getHomeActivities(new ArrayList<>());
+
+        try {
+            final ComponentName recentsComponent =
+                    ComponentName.unflattenFromString(context.getResources().getString(
+                            com.android.internal.R.string.config_recentsComponentName));
+            final int enabledState = pm.getComponentEnabledSetting(recentsComponent);
+            Assume.assumeThat(enabledState, anyOf(
+                    is(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT),
+                    is(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)));
+
+            final boolean homeIsRecents =
+                    recentsComponent.getPackageName().equals(defaultHome.getPackageName());
+            sRecentsIntent =
+                    new Intent().setComponent(homeIsRecents ? defaultHome : recentsComponent);
+        } catch (Exception e) {
+            Assume.assumeNoException(e);
+        }
+    }
+
+    @AfterClass
+    public static void tearDownClass() {
+        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);
+    }
+
+    /**
+     * <pre>
+     * Steps:
+     * (1) Start recents activity (only make it visible).
+     * (2) Finish animation, take turns to execute (a), (b).
+     *     (a) Move recents activity to top.
+     * ({@link com.android.server.wm.RecentsAnimationController#REORDER_MOVE_TO_TOP})
+     *         Move test app to top by startActivityFromRecents.
+     *     (b) Cancel (it is similar to swipe a little distance and give up to enter recents).
+     * ({@link com.android.server.wm.RecentsAnimationController#REORDER_MOVE_TO_ORIGINAL_POSITION})
+     * (3) Loop (1).
+     * </pre>
+     */
+    @Test
+    @ManualBenchmarkTest(
+            warmupDurationNs = TIME_1_S_IN_NS,
+            targetTestDurationNs = TIME_5_S_IN_NS,
+            statsReportFlags =
+                    STATS_REPORT_ITERATION | STATS_REPORT_MEAN | STATS_REPORT_COEFFICIENT_VAR)
+    public void testRecentsAnimation() throws Throwable {
+        final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        final IActivityTaskManager atm = ActivityTaskManager.getService();
+
+        final ArrayList<Pair<String, Boolean>> finishCases = new ArrayList<>();
+        // Real launch the recents activity.
+        finishCases.add(new Pair<>("finishMoveToTop", true));
+        // Return to the original top.
+        finishCases.add(new Pair<>("finishCancel", false));
+
+        // Ensure startRecentsActivity won't be called before finishing the animation.
+        final Semaphore recentsSemaphore = new Semaphore(1);
+
+        final int testActivityTaskId = mActivityRule.getActivity().getTaskId();
+        final IRecentsAnimationRunner.Stub anim = new IRecentsAnimationRunner.Stub() {
+            int mIteration;
+
+            @Override
+            public void onAnimationStart(IRecentsAnimationController controller,
+                    RemoteAnimationTarget[] apps, Rect homeContentInsets,
+                    Rect minimizedHomeBounds) throws RemoteException {
+                final Pair<String, Boolean> finishCase = finishCases.get(mIteration++ % 2);
+                final boolean moveRecentsToTop = finishCase.second;
+                makeInterval();
+
+                long startTime = SystemClock.elapsedRealtimeNanos();
+                controller.finish(moveRecentsToTop, false /* sendUserLeaveHint */);
+                final long elapsedTimeNsOfFinish = SystemClock.elapsedRealtimeNanos() - startTime;
+                mMeasuredTimeNs += elapsedTimeNsOfFinish;
+                state.addExtraResult(finishCase.first, elapsedTimeNsOfFinish);
+
+                if (moveRecentsToTop) {
+                    mLifecycleListener.waitForIdleSync(Stage.STOPPED);
+
+                    startTime = SystemClock.elapsedRealtimeNanos();
+                    atm.startActivityFromRecents(testActivityTaskId, null /* options */);
+                    final long elapsedTimeNs = SystemClock.elapsedRealtimeNanos() - startTime;
+                    mMeasuredTimeNs += elapsedTimeNs;
+                    state.addExtraResult("startFromRecents", elapsedTimeNs);
+
+                    mLifecycleListener.waitForIdleSync(Stage.RESUMED);
+                }
+
+                makeInterval();
+                recentsSemaphore.release();
+            }
+
+            @Override
+            public void onAnimationCanceled(boolean deferredWithScreenshot) throws RemoteException {
+                Assume.assumeNoException(
+                        new AssertionError("onAnimationCanceled should not be called"));
+            }
+        };
+
+        while (state.keepRunning(mMeasuredTimeNs)) {
+            Assume.assumeTrue(recentsSemaphore.tryAcquire(TIME_5_S_IN_NS, TimeUnit.NANOSECONDS));
+
+            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();
+                }
+            }
+        }
+    }
+}
diff --git a/apct-tests/perftests/core/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/core/src/android/wm/WindowAddRemovePerfTest.java
index a951869..27790e6 100644
--- a/apct-tests/perftests/core/src/android/wm/WindowAddRemovePerfTest.java
+++ b/apct-tests/perftests/core/src/android/wm/WindowAddRemovePerfTest.java
@@ -60,7 +60,7 @@
     }
 
     @Test
-    @ManualBenchmarkTest(warmupDurationNs = WARMUP_DURATION, targetTestDurationNs = TEST_DURATION)
+    @ManualBenchmarkTest(warmupDurationNs = TIME_1_S_IN_NS, targetTestDurationNs = TIME_5_S_IN_NS)
     public void testAddRemoveWindow() throws Throwable {
         new TestWindow().runBenchmark(mPerfStatusReporter.getBenchmarkState());
     }
diff --git a/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java b/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java
index b2c6168..4864da4 100644
--- a/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java
+++ b/apct-tests/perftests/core/src/android/wm/WindowManagerPerfTestBase.java
@@ -25,8 +25,8 @@
 public class WindowManagerPerfTestBase {
     static final UiAutomation sUiAutomation = getInstrumentation().getUiAutomation();
     static final long NANOS_PER_S = 1000L * 1000 * 1000;
-    static final long WARMUP_DURATION = 1 * NANOS_PER_S;
-    static final long TEST_DURATION = 5 * NANOS_PER_S;
+    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() {
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 dd43ae7..ffe39e8 100644
--- a/apct-tests/perftests/utils/src/android/perftests/utils/ManualBenchmarkState.java
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/ManualBenchmarkState.java
@@ -16,6 +16,7 @@
 
 package android.perftests.utils;
 
+import android.annotation.IntDef;
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.os.Bundle;
@@ -58,6 +59,28 @@
 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 {}
+
+    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;
+
     // TODO: Tune these values.
     // warm-up for duration
     private static final long WARMUP_DURATION_NS = TimeUnit.SECONDS.toNanos(5);
@@ -93,6 +116,13 @@
     // 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 boolean shouldReport(int statsReportFlag) {
+        return (mStatsReportFlags & statsReportFlag) != 0;
+    }
+
     void configure(ManualBenchmarkTest testAnnotation) {
         if (testAnnotation == null) {
             return;
@@ -106,6 +136,10 @@
         if (targetTestDurationNs >= 0) {
             mTargetTestDurationNs = targetTestDurationNs;
         }
+        final int statsReportFlags = testAnnotation.statsReportFlags();
+        if (statsReportFlags >= 0) {
+            mStatsReportFlags = statsReportFlags;
+        }
     }
 
     private void beginBenchmark(long warmupDuration, int iterations) {
@@ -186,12 +220,35 @@
         return sb.toString();
     }
 
-    private static void fillStatus(Bundle status, String key, Stats stats) {
-        status.putLong(key + "_median", stats.getMedian());
-        status.putLong(key + "_mean", (long) stats.getMean());
-        status.putLong(key + "_percentile90", stats.getPercentile90());
-        status.putLong(key + "_percentile95", stats.getPercentile95());
-        status.putLong(key + "_stddev", (long) stats.getStandardDeviation());
+    private void fillStatus(Bundle status, String key, Stats stats) {
+        if (shouldReport(STATS_REPORT_ITERATION)) {
+            status.putLong(key + "_iteration", stats.getSize());
+        }
+        if (shouldReport(STATS_REPORT_MEDIAN)) {
+            status.putLong(key + "_median", stats.getMedian());
+        }
+        if (shouldReport(STATS_REPORT_MEAN)) {
+            status.putLong(key + "_mean", Math.round(stats.getMean()));
+        }
+        if (shouldReport(STATS_REPORT_MIN)) {
+            status.putLong(key + "_min", stats.getMin());
+        }
+        if (shouldReport(STATS_REPORT_MAX)) {
+            status.putLong(key + "_max", stats.getMax());
+        }
+        if (shouldReport(STATS_REPORT_PERCENTILE90)) {
+            status.putLong(key + "_percentile90", stats.getPercentile90());
+        }
+        if (shouldReport(STATS_REPORT_PERCENTILE95)) {
+            status.putLong(key + "_percentile95", stats.getPercentile95());
+        }
+        if (shouldReport(STATS_REPORT_STDDEV)) {
+            status.putLong(key + "_stddev", Math.round(stats.getStandardDeviation()));
+        }
+        if (shouldReport(STATS_REPORT_COEFFICIENT_VAR)) {
+            status.putLong(key + "_cv",
+                    Math.round((100 * stats.getStandardDeviation() / stats.getMean())));
+        }
     }
 
     public void sendFullStatusReport(Instrumentation instrumentation, String key) {
@@ -204,8 +261,9 @@
         if (mExtraResults != null) {
             for (int i = 0; i < mExtraResults.size(); i++) {
                 final String subKey = key + "_" + mExtraResults.keyAt(i);
-                final Stats stats = new Stats(mExtraResults.valueAt(i));
-                Log.i(TAG, summaryLine(subKey, mStats, mResults));
+                final ArrayList<Long> results = mExtraResults.valueAt(i);
+                final Stats stats = new Stats(results);
+                Log.i(TAG, summaryLine(subKey, stats, results));
                 fillStatus(status, subKey, stats);
             }
         }
@@ -218,5 +276,6 @@
     public @interface ManualBenchmarkTest {
         long warmupDurationNs() default -1;
         long targetTestDurationNs() default -1;
+        @StatsReport int statsReportFlags() default -1;
     }
 }
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 5e50073..f650e81 100644
--- a/apct-tests/perftests/utils/src/android/perftests/utils/Stats.java
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/Stats.java
@@ -23,6 +23,7 @@
 public class Stats {
     private long mMedian, mMin, mMax, mPercentile90, mPercentile95;
     private double mMean, mStandardDeviation;
+    private final int mSize;
 
     /* Calculate stats in constructor. */
     public Stats(List<Long> values) {
@@ -35,6 +36,7 @@
 
         Collections.sort(values);
 
+        mSize = size;
         mMin = values.get(0);
         mMax = values.get(values.size() - 1);
 
@@ -56,6 +58,10 @@
         mStandardDeviation = Math.sqrt(mStandardDeviation / (double) (size - 1));
     }
 
+    public int getSize() {
+        return mSize;
+    }
+
     public double getMean() {
         return mMean;
     }