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

Commit cb030576 authored by Greg Kaiser's avatar Greg Kaiser
Browse files

BouncyBall: Have app log/exit if it loses foreground

Credit shayba@ for noting in an earlier code review that our tracking
of frame drops would be broken if the app lost focus.  At the time,
we put a line in the README to note we assume we're always in focus.

But with this CL, we put in code to have the app actively notice, log,
and (by default) exit in this situation.  This forces any automation
or user to realize there's a more fundamental issue than a frame
drop.

We also move our README to a markdown file, and add more information
to it.  We'll fill out the perfetto section later as we develop our
automation's use of perfetto to evaluate app runs.

Bug: 408044970
Test: Ran with and without ASSUMPTION_FAILURE_FORCES_EXIT, got the expected behaviors when having the app go out of focus.
Flag: EXEMPT for test app
Change-Id: Icd18ec1faa409670e901595b99eb8a03769af81e
parent 3bf9f89b
Loading
Loading
Loading
Loading

tests/BouncyBall/README

deleted100644 → 0
+0 −20
Original line number Diff line number Diff line
This is a simple graphics app which draws a ball bouncing around a screen.

This app is intended for use in automated testing (for example, to make sure
no frames are dropped while running).  It can also be run manually, but there's
a fundamental assumption that this is the only foreground app running on the
device while testing.


From the top of tree, in a shell that has been set up for building, this app
can be built with:

$ mmma -j frameworks/base/tests/BouncyBall

And then installed with:

$ adb install ${ANDROID_PRODUCT_OUT}/system/app/BouncyBallTest/BouncyBallTest.apk

This can be launched via automation with:

$ adb shell am start com.prefabulated.bouncyball/com.prefabulated.bouncyball.BouncyBallActivity
+68 −0
Original line number Diff line number Diff line
# BouncyBall test app

This is a simple graphics app which draws a ball bouncing around the screen.

This app's primary use is in automated testing, to make sure no frames are
dropped while running.

The graphics tested here are quite simple.  This app is not just intended to
assure very basic graphics work, but that the system does not drop frames as
CPUs/GPU get turned off and downclocked while in the steady state.

See https://source.android.com/docs/core/tests/debug/eval_perf#touchlatency
for more details.

## Manual usage basics

This app can be used outside of automation to check and debug this behavior.

This app fundamentally assumes that it is the only foreground app running on
the device while testing.  If that assumption is broken, this app will log
to logcat, at the "E"rror level, noting an "ASSUMPTION FAILURE".

This app will log, at the "E"rror level, each time a frame is dropped.

On a properly set up device, it is expected that this app never drops a frame.

### Helpful "flags" to flip

The source code (in
`app/src/main/java/com/prefabulated/bouncyball/BouncyBallActivity.java`) has a
few constants which can be changed to help with debugging and testing.  The
app needs to be recompiled after any of these have been changed.

* `LOG_EVERY_FRAME`  If changed to `true`, the app will log, at the "D"ebug
level, every (non-dropped) frame.
* `FORCE_DROPPED_FRAMES`  If changed to `true`, the app will drop every 64th
frame.  This can be helpful for debugging automation pipelines and confirming
app behavior.
* `ASSUMPTION_FAILURE_FORCES_EXIT`  If changed to `false`, if the app fails
the assumption that it is always in foreground focus, then the app will
keep running (even though we know the results will be wrong).


## Local build and install

From the top of tree, in a shell that has been set up for building:

```
$ mmma -j frameworks/base/tests/BouncyBall
$ adb install ${ANDROID_PRODUCT_OUT}/system/app/BouncyBallTest/BouncyBallTest.apk
```

## Launch app from command line

Assuming the app is installed on the device:

```
$ adb shell am start com.prefabulated.bouncyball/com.prefabulated.bouncyball.BouncyBallActivity
```

## Using Perfetto for analysis

While the app will self-report when it drops frames, indicating an issue, that's
not very helpful for figuring out why.

TODO(b/408044970): Document how automation uses Perfetto, and how it can be used
here.
+41 −2
Original line number Diff line number Diff line
@@ -33,11 +33,19 @@ public class BouncyBallActivity extends AppCompatActivity {
    // Since logging (to logcat) takes system resources, we chose not to log
    // data every frame by default.
    private static final boolean LOG_EVERY_FRAME = false;

    // To help with debugging and verifying behavior when frames are dropped,
    // this will drop one in every 64 frames.
    private static final boolean FORCE_DROPPED_FRAMES = false;

    // If the app fails an assumption we have for it (primarily that it is
    // the only foreground app), then its results cannot be trusted.  By
    // default, we choose to immediately exit in this situation.  Note that
    // dropping a frame is not considered an assumption failure.
    private static final boolean ASSUMPTION_FAILURE_FORCES_EXIT = true;

    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
@@ -45,6 +53,8 @@ public class BouncyBallActivity extends AppCompatActivity {
    private static final int INITIAL_FRAMES_TO_IGNORE = 6;

    private int mDisplayId = -1;
    private boolean mHasFocus = false;
    private boolean mWarmedUp = false;
    private float mFrameRate;
    private long mFrameMaxDurationNanos;
    private int mNumFramesDropped = 0;
@@ -77,14 +87,23 @@ public class BouncyBallActivity extends AppCompatActivity {

                @Override
                public void doFrame(long frameTimeNanos) {
                    if (mFrameCount >= INITIAL_FRAMES_TO_IGNORE) {
                    if (mFrameCount == INITIAL_FRAMES_TO_IGNORE) {
                        mWarmedUp = true;
                        if (!mHasFocus) {
                            String msg = "App does not have focus after "
                                    + mFrameCount + " frames";
                            reportAssumptionFailure(msg);
                        }
                    }
                    if (mWarmedUp) {
                        long elapsedNanos = frameTimeNanos - mLastFrameTimeNanos;
                        if (elapsedNanos > mFrameMaxDurationNanos) {
                            mNumFramesDropped++;
                            Log.e(LOG_TAG, "FRAME DROPPED (total " + mNumFramesDropped
                                    + "): Took " + nanosToMillis(elapsedNanos) + "ms");
                        } else if (LOG_EVERY_FRAME) {
                            Log.d(LOG_TAG, "Frame took " + nanosToMillis(elapsedNanos) + "ms");
                            Log.d(LOG_TAG, "Frame " + mFrameCount + " took "
                                    + nanosToMillis(elapsedNanos) + "ms");
                        }
                    }
                    mLastFrameTimeNanos = frameTimeNanos;
@@ -125,6 +144,18 @@ public class BouncyBallActivity extends AppCompatActivity {
        Trace.endSection();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        if (mWarmedUp) {
            // After our initial frames, this app should always be in focus.
            String state = hasFocus ? "gain" : "loss";
            reportAssumptionFailure("Unexpected " + state + " of focus");
        }
        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.
@@ -195,4 +226,12 @@ public class BouncyBallActivity extends AppCompatActivity {
    private float nanosToMillis(long nanos) {
        return nanos / (1_000_000.0f);
    }

    private void reportAssumptionFailure(String msg) {
        Log.e(LOG_TAG, "ASSUMPTION FAILURE.  " + msg);
        if (ASSUMPTION_FAILURE_FORCES_EXIT) {
            Log.e(LOG_TAG, "Exiting app due to assumption failure.");
            System.exit(1);
        }
    }
}