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

Commit 27f68f45 authored by Greg Kaiser's avatar Greg Kaiser
Browse files

BouncyBall: Test at default frame rate

We used to try to force the app to use 60Hz for testing.

However, our emphasis is on testing the default user experience,
and many devices default to more than 60Hz.  For those devices, it
makes more sense to test at the default frame rate.  Although we
do require a minimum of 60Hz.

As part of this change, instead of hard-coding the number of
initial frames we ignore, we make them based on time and adjust
it per the frame rate.

Bug: 436919584
Bug: 408044970
Test: Ran app on devices that default to 60Hz and 90Hz, and things worked as expected
Flag: EXEMPT for test app
Change-Id: Ia56d06cf8edaf0e8b1b61801c35d628b10fd7bf2
parent 36206e7d
Loading
Loading
Loading
Loading
+41 −30
Original line number Diff line number Diff line
@@ -46,17 +46,26 @@ public class BouncyBallActivity extends AppCompatActivity {

    private static final String LOG_TAG = "BouncyBall";

    private static final float DESIRED_FRAME_RATE = 60.0f;

    // Our focus isn't smoothness on startup; it's smoothness once we're
    // running.  So we ignore frame drops in the first 0.1 seconds.
    private static final int INITIAL_FRAMES_TO_IGNORE = 6;
    // The app needs to run at at least this frame rate to be a valid test.
    // If the system defaults us to a higher frame rate, we'll test with that,
    // but we need to at least meet this rate.
    private static final float MINIMUM_FRAME_RATE = 60.0f;

    // This test measures sustained frame rate, so it's safe to ignore
    // frame drops around the start time.
    // This value must be high enough to skip jank due to clocks not having
    // ramped up yet.
    // This value must not be too high as to miss jank due to clocks ramping
    // down.
    private static final float INITIAL_TIME_TO_IGNORE_IN_SECONDS = 0.1f;

    private int mDisplayId = -1;
    private boolean mHasFocus = false;
    private boolean mWarmedUp = false;
    private float mFrameRate;
    private long mFrameMaxDurationNanos;
    private int mFrameCount = 0;
    private int mFirstAutomatedTestFrame = -1;
    private int mNumFramesDropped = 0;
    private Choreographer mChoreographer;

@@ -83,11 +92,10 @@ public class BouncyBallActivity extends AppCompatActivity {
            new Choreographer.FrameCallback() {

                private long mLastFrameTimeNanos = -1;
                private int mFrameCount = 0;

                @Override
                public void doFrame(long frameTimeNanos) {
                    if (mFrameCount == INITIAL_FRAMES_TO_IGNORE) {
                    if (mFrameCount == mFirstAutomatedTestFrame) {
                        mWarmedUp = true;
                        if (!mHasFocus) {
                            String msg = "App does not have focus after "
@@ -99,7 +107,8 @@ public class BouncyBallActivity extends AppCompatActivity {
                        long elapsedNanos = frameTimeNanos - mLastFrameTimeNanos;
                        if (elapsedNanos > mFrameMaxDurationNanos) {
                            mNumFramesDropped++;
                            Log.e(LOG_TAG, "FRAME DROPPED (total " + mNumFramesDropped
                            Log.e(LOG_TAG, "DROPPED FRAME #" + mFrameCount
                                    + " (total " + mNumFramesDropped
                                    + "): Took " + nanosToMillis(elapsedNanos) + "ms");
                        } else if (LOG_EVERY_FRAME) {
                            Log.d(LOG_TAG, "Frame " + mFrameCount + " took "
@@ -138,7 +147,7 @@ public class BouncyBallActivity extends AppCompatActivity {
                                        DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE,
                                        mDisplayListener);

        setFrameRatePreference();
        initFrameRate();
        mChoreographer = Choreographer.getInstance();
        mChoreographer.postFrameCallback(mFrameCallback);
        Trace.endSection();
@@ -156,29 +165,27 @@ public class BouncyBallActivity extends AppCompatActivity {
        mHasFocus = hasFocus;
    }

    // If available at our current resolution, use 60Hz.  If not, use the
    // lowest refresh rate above 60Hz which is available.  Otherwise, throw
    // an exception which kills the app.
    //
    // The philosophy is that, for now, we only require this test to run
    // solidly at 60Hz.  If a device has a higher refresh rate than that,
    // we slow it down to 60Hz to make this easier to pass.  If a device
    // isn't able to go all the way down to 60Hz, we use the lowest refresh
    // rate above 60Hz.  If the device only supports below 30Hz, that's below
    // our standards so we abort.
    private void setFrameRatePreference() {
        float preferredRate = Float.POSITIVE_INFINITY;

    private void initFrameRate() {
        Display display = getDisplay();
        Display.Mode currentMode = display.getMode();
        mDisplayId = display.getDisplayId();
        setFrameRate(currentMode.getRefreshRate());
        if (mFrameRate == DESIRED_FRAME_RATE) {
            Log.i(LOG_TAG, "Already running at " + mFrameRate + "Hz");
            // We're already using what we want.  Nothing to do here.
        if (mFrameRate >= MINIMUM_FRAME_RATE) {
            // The default frame rate is sufficient for our testing.
            return;
        }

        String minRateStr = MINIMUM_FRAME_RATE + "Hz";
        // Using a Warning here, because this seems unexpected that a device
        // defaults to running at below 60Hz.
        Log.w(LOG_TAG, "Default frame rate (" + mFrameRate
                  + "Hz) is below the acceptable minimum (" + minRateStr + ")");

        // If available at our current resolution, use 60Hz.  If not, use the
        // lowest refresh rate above 60Hz which is available.  Otherwise, throw
        // an exception which kills the app.
        float preferredRate = Float.POSITIVE_INFINITY;

        for (Display.Mode mode : display.getSupportedModes()) {
            if ((currentMode.getPhysicalHeight() != mode.getPhysicalHeight())
                    || (currentMode.getPhysicalWidth() != mode.getPhysicalWidth())) {
@@ -186,24 +193,23 @@ public class BouncyBallActivity extends AppCompatActivity {
                continue;
            }
            float rate = mode.getRefreshRate();
            if (rate == DESIRED_FRAME_RATE) {
            if (rate == MINIMUM_FRAME_RATE) {
                // This is exactly what we were hoping for, so we can stop
                // looking.
                preferredRate = rate;
                break;
            }
            if ((rate > DESIRED_FRAME_RATE) && (rate < preferredRate)) {
            if ((rate > MINIMUM_FRAME_RATE) && (rate < preferredRate)) {
                // This is the best rate we've seen so far in terms of being
                // closest to our desired rate without being under it.
                preferredRate = rate;
            }
        }
        if (preferredRate == Float.POSITIVE_INFINITY) {
            String msg = "No display mode with at least " + DESIRED_FRAME_RATE + "Hz";
            String msg = "No display mode with at least " + minRateStr;
            throw new RuntimeException(msg);
        }
        Log.i(LOG_TAG, "Changing preferred rate from " + mFrameRate + "Hz to "
                + preferredRate + "Hz");
        Log.i(LOG_TAG, "Requesting to run at " + preferredRate + "Hz");
        Window window = getWindow();
        WindowManager.LayoutParams params = window.getAttributes();
        params.preferredRefreshRate = preferredRate;
@@ -221,6 +227,11 @@ public class BouncyBallActivity extends AppCompatActivity {
        // We store as nanoseconds, to avoid per-frame floating point math in
        // the common case.
        mFrameMaxDurationNanos = ((long) frameMaxDurationMillis) * 1_000_000;

        Log.i(LOG_TAG, "Running at frame rate " + mFrameRate + "Hz");

        mFirstAutomatedTestFrame =
            Math.round(INITIAL_TIME_TO_IGNORE_IN_SECONDS * mFrameRate);
    }

    private float nanosToMillis(long nanos) {