Loading tests/BouncyBall/README +3 −1 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). 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 Loading tests/BouncyBall/app/src/main/java/com/prefabulated/bouncyball/BouncyBallActivity.java +78 −5 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.hardware.display.DisplayManager; import android.os.Bundle; import android.os.Trace; import android.util.Log; import android.view.Choreographer; import android.view.Display; import android.view.Window; import android.view.WindowManager; Loading @@ -29,11 +30,26 @@ import androidx.appcompat.app.AppCompatActivity; import java.util.concurrent.Executors; 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; 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; private int mDisplayId = -1; private float mFrameRate; private long mFrameMaxDurationNanos; private int mNumFramesDropped = 0; private Choreographer mChoreographer; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { Loading @@ -48,8 +64,46 @@ public class BouncyBallActivity extends AppCompatActivity { if (displayId != mDisplayId) { return; } mFrameRate = getDisplay().getMode().getRefreshRate(); Log.d(LOG_TAG, "Using frame rate " + mFrameRate + "Hz"); setFrameRate(getDisplay().getMode().getRefreshRate()); Log.i(LOG_TAG, "Using frame rate " + mFrameRate + "Hz"); } }; private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { private long mLastFrameTimeNanos = -1; private int mFrameCount = 0; @Override public void doFrame(long frameTimeNanos) { if (mFrameCount >= INITIAL_FRAMES_TO_IGNORE) { 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"); } } mLastFrameTimeNanos = frameTimeNanos; mFrameCount++; if (FORCE_DROPPED_FRAMES) { dropFrameSometimes(); } // Request the next frame callback mChoreographer.postFrameCallback(this); } private void dropFrameSometimes() { if ((mFrameCount % 64) == 0) { try { Thread.sleep((long) nanosToMillis(mFrameMaxDurationNanos) + 1); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } }; Loading @@ -66,6 +120,8 @@ public class BouncyBallActivity extends AppCompatActivity { mDisplayListener); setFrameRatePreference(); mChoreographer = Choreographer.getInstance(); mChoreographer.postFrameCallback(mFrameCallback); Trace.endSection(); } Loading @@ -85,9 +141,9 @@ public class BouncyBallActivity extends AppCompatActivity { Display display = getDisplay(); Display.Mode currentMode = display.getMode(); mDisplayId = display.getDisplayId(); mFrameRate = currentMode.getRefreshRate(); setFrameRate(currentMode.getRefreshRate()); if (mFrameRate == DESIRED_FRAME_RATE) { Log.d(LOG_TAG, "Already running at " + mFrameRate + "Hz"); Log.i(LOG_TAG, "Already running at " + mFrameRate + "Hz"); // We're already using what we want. Nothing to do here. return; } Loading Loading @@ -115,11 +171,28 @@ public class BouncyBallActivity extends AppCompatActivity { String msg = "No display mode with at least " + DESIRED_FRAME_RATE + "Hz"; throw new RuntimeException(msg); } Log.d(LOG_TAG, "Changing preferred rate from " + mFrameRate + "Hz to " Log.i(LOG_TAG, "Changing preferred rate from " + mFrameRate + "Hz to " + preferredRate + "Hz"); Window window = getWindow(); WindowManager.LayoutParams params = window.getAttributes(); params.preferredRefreshRate = preferredRate; window.setAttributes(params); } private void setFrameRate(float frameRate) { mFrameRate = frameRate; float frameMaxDurationMillis = 1_000.0f / mFrameRate; // There is a little +/- of when our callback is called. So we allow // up to 25% beyond this before considering it a frame drop. Since // a frame drop should mean getting a value near double (or higher), // allowing 25% shouldn't have us missing legitimate drops. frameMaxDurationMillis *= 1.25f; // We store as nanoseconds, to avoid per-frame floating point math in // the common case. mFrameMaxDurationNanos = ((long) frameMaxDurationMillis) * 1_000_000; } private float nanosToMillis(long nanos) { return nanos / (1_000_000.0f); } } Loading
tests/BouncyBall/README +3 −1 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). 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 Loading
tests/BouncyBall/app/src/main/java/com/prefabulated/bouncyball/BouncyBallActivity.java +78 −5 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.hardware.display.DisplayManager; import android.os.Bundle; import android.os.Trace; import android.util.Log; import android.view.Choreographer; import android.view.Display; import android.view.Window; import android.view.WindowManager; Loading @@ -29,11 +30,26 @@ import androidx.appcompat.app.AppCompatActivity; import java.util.concurrent.Executors; 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; 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; private int mDisplayId = -1; private float mFrameRate; private long mFrameMaxDurationNanos; private int mNumFramesDropped = 0; private Choreographer mChoreographer; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { Loading @@ -48,8 +64,46 @@ public class BouncyBallActivity extends AppCompatActivity { if (displayId != mDisplayId) { return; } mFrameRate = getDisplay().getMode().getRefreshRate(); Log.d(LOG_TAG, "Using frame rate " + mFrameRate + "Hz"); setFrameRate(getDisplay().getMode().getRefreshRate()); Log.i(LOG_TAG, "Using frame rate " + mFrameRate + "Hz"); } }; private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { private long mLastFrameTimeNanos = -1; private int mFrameCount = 0; @Override public void doFrame(long frameTimeNanos) { if (mFrameCount >= INITIAL_FRAMES_TO_IGNORE) { 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"); } } mLastFrameTimeNanos = frameTimeNanos; mFrameCount++; if (FORCE_DROPPED_FRAMES) { dropFrameSometimes(); } // Request the next frame callback mChoreographer.postFrameCallback(this); } private void dropFrameSometimes() { if ((mFrameCount % 64) == 0) { try { Thread.sleep((long) nanosToMillis(mFrameMaxDurationNanos) + 1); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } }; Loading @@ -66,6 +120,8 @@ public class BouncyBallActivity extends AppCompatActivity { mDisplayListener); setFrameRatePreference(); mChoreographer = Choreographer.getInstance(); mChoreographer.postFrameCallback(mFrameCallback); Trace.endSection(); } Loading @@ -85,9 +141,9 @@ public class BouncyBallActivity extends AppCompatActivity { Display display = getDisplay(); Display.Mode currentMode = display.getMode(); mDisplayId = display.getDisplayId(); mFrameRate = currentMode.getRefreshRate(); setFrameRate(currentMode.getRefreshRate()); if (mFrameRate == DESIRED_FRAME_RATE) { Log.d(LOG_TAG, "Already running at " + mFrameRate + "Hz"); Log.i(LOG_TAG, "Already running at " + mFrameRate + "Hz"); // We're already using what we want. Nothing to do here. return; } Loading Loading @@ -115,11 +171,28 @@ public class BouncyBallActivity extends AppCompatActivity { String msg = "No display mode with at least " + DESIRED_FRAME_RATE + "Hz"; throw new RuntimeException(msg); } Log.d(LOG_TAG, "Changing preferred rate from " + mFrameRate + "Hz to " Log.i(LOG_TAG, "Changing preferred rate from " + mFrameRate + "Hz to " + preferredRate + "Hz"); Window window = getWindow(); WindowManager.LayoutParams params = window.getAttributes(); params.preferredRefreshRate = preferredRate; window.setAttributes(params); } private void setFrameRate(float frameRate) { mFrameRate = frameRate; float frameMaxDurationMillis = 1_000.0f / mFrameRate; // There is a little +/- of when our callback is called. So we allow // up to 25% beyond this before considering it a frame drop. Since // a frame drop should mean getting a value near double (or higher), // allowing 25% shouldn't have us missing legitimate drops. frameMaxDurationMillis *= 1.25f; // We store as nanoseconds, to avoid per-frame floating point math in // the common case. mFrameMaxDurationNanos = ((long) frameMaxDurationMillis) * 1_000_000; } private float nanosToMillis(long nanos) { return nanos / (1_000_000.0f); } }