Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f5622d25 authored by Riddle Hsu's avatar Riddle Hsu
Browse files

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
parent 1be1dd6b
Loading
Loading
Loading
Loading
+121 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }
}
+11 −77
Original line number Diff line number Diff line
@@ -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.os.SystemClock;
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 class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
    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 @@ public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
        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 @@ public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
    @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 @@ public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
                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 @@ public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
                    mMeasuredTimeNs += elapsedTimeNs;
                    state.addExtraResult("startFromRecents", elapsedTimeNs);

                    mLifecycleListener.waitForIdleSync(Stage.RESUMED);
                    mActivityRule.waitForIdleSync(Stage.RESUMED);
                }

                makeInterval();
@@ -223,55 +194,18 @@ public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
            }
        };

        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));
        }
    }
}
+107 −3
Original line number Diff line number Diff line
@@ -18,9 +18,21 @@ package android.wm;

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 @@ public class WindowManagerPerfTestBase {
    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();
                }
            }
        }
    }
}
+60 −40
Original line number Diff line number Diff line
@@ -59,27 +59,37 @@ import java.util.concurrent.TimeUnit;
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,
    @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;

        @Retention(RetentionPolicy.RUNTIME)
        @IntDef(value = {
                FLAG_MEDIAN,
                FLAG_MEAN,
                FLAG_MIN,
                FLAG_MAX,
                FLAG_STDDEV,
                FLAG_COEFFICIENT_VAR,
                FLAG_ITERATION,
        })
    public @interface StatsReport {}
        @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 {};
    }

    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;
    /** 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 @@ public final class ManualBenchmarkState {
    // 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 @@ public final class ManualBenchmarkState {
        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 @@ public final class ManualBenchmarkState {
    }

    /**
     * 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 @@ public final class ManualBenchmarkState {
    }

    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 final class ManualBenchmarkState {
    public @interface ManualBenchmarkTest {
        long warmupDurationNs() default -1;
        long targetTestDurationNs() default -1;
        @StatsReport int statsReportFlags() default -1;
        StatsReport statsReport() default @StatsReport(flags = DEFAULT_STATS_REPORT);
    }
}
+16 −0
Original line number Diff line number Diff line
@@ -19,8 +19,24 @@ package android.perftests.utils;
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);
Loading