• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.gameframerate.cts;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import android.app.Activity;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.hardware.display.DisplayManager;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.support.test.uiautomator.UiDevice;
29 import android.sysprop.SurfaceFlingerProperties;
30 import android.util.Log;
31 import android.view.Choreographer;
32 import android.view.Surface;
33 import android.view.SurfaceHolder;
34 import android.view.SurfaceView;
35 import android.view.ViewGroup;
36 
37 import java.io.IOException;
38 import java.util.ArrayList;
39 
40 /**
41  * An Activity to help with frame rate testing.
42  */
43 public class GameFrameRateCtsActivity extends Activity {
44     private static final String TAG = "GameFrameRateCtsActivity";
45     private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS = 2 * 1_000_000_000L;
46     private static final long STABLE_FRAME_RATE_WAIT_NANOSECONDS = 1 * 1_000_000_000L;
47     private static final long POST_BUFFER_INTERVAL_NANOSECONDS = 500_000_000L;
48     private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5;
49     private static final long PRECONDITION_WAIT_TIMEOUT_NANOSECONDS = 20 * 1_000_000_000L;
50     private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS = 3 * 1_000_000_000L;
51     private static final float FRAME_RATE_TOLERANCE = 1.01f;
52     private static final float FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE = 5;
53     private static final long FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS = 1 * 1_000_000_000L;
54     private static final long FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS = 10 * 1_000_000_000L;
55 
56     // Default game frame rate sets the frame rate to 60 by default, even if the system properties
57     // "ro.surface_flinger.game_default_frame_rate_override" is not set. Ref: GameManagerService
58     // {@link com.android.server.app.GameManagerService#onBootCompleted()}
59     private static final Integer GAME_DEFAULT_FRAMERATE_INT =
60             SurfaceFlingerProperties.game_default_frame_rate_override().orElse(60);
61 
62     private DisplayManager mDisplayManager;
63     private SurfaceView mSurfaceView;
64     private Handler mHandler = new Handler(Looper.getMainLooper());
65     private Object mLock = new Object();
66     private Surface mSurface = null;
67     private float mReportedDisplayRefreshRate;
68     private float mReportedDisplayModeRefreshRate;
69     private ArrayList<Float> mRefreshRateChangedEvents = new ArrayList<Float>();
70 
71     private long mLastBufferPostTime;
72 
73     private enum ActivityState { RUNNING, PAUSED, DESTROYED }
74     private ActivityState mActivityState;
75 
76     SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
77         @Override
78         public void surfaceCreated(SurfaceHolder holder) {
79             synchronized (mLock) {
80                 mSurface = holder.getSurface();
81                 mLock.notify();
82             }
83         }
84 
85         @Override
86         public void surfaceDestroyed(SurfaceHolder holder) {
87             synchronized (mLock) {
88                 mSurface = null;
89                 mLock.notify();
90             }
91         }
92 
93         @Override
94         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
95         }
96     };
97 
98     DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
99         @Override
100         public void onDisplayAdded(int displayId) {
101         }
102 
103         @Override
104         public void onDisplayChanged(int displayId) {
105             synchronized (mLock) {
106                 float refreshRate = getDisplay().getRefreshRate();
107                 float displayModeRefreshRate = getDisplay().getMode().getRefreshRate();
108                 if (refreshRate != mReportedDisplayRefreshRate
109                         || displayModeRefreshRate != mReportedDisplayModeRefreshRate) {
110                     Log.i(TAG, String.format("Frame rate changed: (%.2f, %.2f) --> (%.2f, %.2f)",
111                             mReportedDisplayModeRefreshRate,
112                             mReportedDisplayRefreshRate,
113                             displayModeRefreshRate,
114                             refreshRate));
115                     mReportedDisplayRefreshRate = refreshRate;
116                     mReportedDisplayModeRefreshRate = displayModeRefreshRate;
117                     mRefreshRateChangedEvents.add(refreshRate);
118                     mLock.notify();
119                 }
120             }
121         }
122 
123         @Override
124         public void onDisplayRemoved(int displayId) {
125         }
126     };
127 
128     private static class PreconditionViolatedException extends RuntimeException { }
129 
130     private static class FrameRateTimeoutException extends RuntimeException {
FrameRateTimeoutException(FrameRateRange appRequestedFrameRate, float deviceRefreshRate)131         FrameRateTimeoutException(FrameRateRange appRequestedFrameRate, float deviceRefreshRate) {
132             this.appRequestedFrameRate = appRequestedFrameRate;
133             this.deviceRefreshRate = deviceRefreshRate;
134         }
135 
136         public FrameRateRange appRequestedFrameRate;
137         public float deviceRefreshRate;
138     }
139 
140     private static class FrameRateRange {
FrameRateRange(float min, float max)141         FrameRateRange(float min, float max) {
142             this.min = min;
143             this.max = max;
144         }
145         public float min;
146         public float max;
147     }
148 
postBufferToSurface(int color)149     public void postBufferToSurface(int color) {
150         mLastBufferPostTime = System.nanoTime();
151         Canvas canvas = mSurface.lockCanvas(null);
152         canvas.drawColor(color);
153         mSurface.unlockCanvasAndPost(canvas);
154     }
155 
156     @Override
onCreate(Bundle savedInstanceState)157     protected void onCreate(Bundle savedInstanceState) {
158         super.onCreate(savedInstanceState);
159         synchronized (mLock) {
160             mDisplayManager = getSystemService(DisplayManager.class);
161             mReportedDisplayRefreshRate = getDisplay().getRefreshRate();
162             mReportedDisplayModeRefreshRate = getDisplay().getMode().getRefreshRate();
163             mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
164             mSurfaceView = new SurfaceView(this);
165             mSurfaceView.setWillNotDraw(false);
166             setContentView(mSurfaceView,
167                     new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
168                             ViewGroup.LayoutParams.MATCH_PARENT));
169             mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
170         }
171     }
172 
173     @Override
onDestroy()174     protected void onDestroy() {
175         super.onDestroy();
176         mDisplayManager.unregisterDisplayListener(mDisplayListener);
177         synchronized (mLock) {
178             mActivityState = ActivityState.DESTROYED;
179             mLock.notify();
180         }
181     }
182 
183     @Override
onPause()184     public void onPause() {
185         super.onPause();
186         synchronized (mLock) {
187             mActivityState = ActivityState.PAUSED;
188             mLock.notify();
189         }
190     }
191 
192     @Override
onResume()193     public void onResume() {
194         super.onResume();
195         synchronized (mLock) {
196             mActivityState = ActivityState.RUNNING;
197             mLock.notify();
198         }
199     }
200 
frameRatesEqual(float frameRate1, float frameRate2)201     private static boolean frameRatesEqual(float frameRate1, float frameRate2) {
202         return Math.abs(frameRate1 - frameRate2) <= FRAME_RATE_TOLERANCE;
203     }
204 
frameRatesMatchesOverride(float frameRate1, float frameRate2)205     private static boolean frameRatesMatchesOverride(float frameRate1, float frameRate2) {
206         return Math.abs(frameRate1 - frameRate2) <= FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE;
207     }
208 
frameRatesMatchesOverride( float fps, FrameRateRange expectedFrameRateRange)209     private static boolean frameRatesMatchesOverride(
210             float fps, FrameRateRange expectedFrameRateRange) {
211         return (fps + FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE >= expectedFrameRateRange.min)
212                 && (fps - FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE <= expectedFrameRateRange.max);
213     }
214 
215     // Waits until our SurfaceHolder has a surface and the activity is resumed.
waitForPreconditions()216     private void waitForPreconditions() throws InterruptedException {
217         assertTrue(
218                 "Activity was unexpectedly destroyed", !isDestroyed());
219         if (mSurface == null || mActivityState != ActivityState.RUNNING) {
220             Log.i(TAG, String.format(
221                     "Waiting for preconditions. Have surface? %b. Activity resumed? %b.",
222                     mSurface != null, mActivityState == ActivityState.RUNNING));
223         }
224         long nowNanos = System.nanoTime();
225         long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_NANOSECONDS;
226         while (mSurface == null || mActivityState != ActivityState.RUNNING) {
227             long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
228             assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b."
229                                     + " Activity resumed? %b.",
230                             mSurface != null, mActivityState == ActivityState.RUNNING),
231                     timeRemainingMillis > 0);
232             mLock.wait(timeRemainingMillis);
233             assertTrue(
234                     "Activity was unexpectedly destroyed", !isDestroyed());
235             nowNanos = System.nanoTime();
236         }
237     }
238 
239     // Returns true if we encounter a precondition violation, false otherwise.
waitForPreconditionViolation()240     private boolean waitForPreconditionViolation() throws InterruptedException {
241         assertTrue(
242                 "Activity was unexpectedly destroyed", !isDestroyed());
243         long nowNanos = System.nanoTime();
244         long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS;
245         while (mSurface != null && mActivityState == ActivityState.RUNNING) {
246             long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
247             if (timeRemainingMillis <= 0) {
248                 break;
249             }
250             mLock.wait(timeRemainingMillis);
251             assertTrue(
252                     "Activity was unexpectedly destroyed", !isDestroyed());
253             nowNanos = System.nanoTime();
254         }
255         return mSurface == null || mActivityState != ActivityState.RUNNING;
256     }
257 
verifyPreconditions()258     private void verifyPreconditions() {
259         if (mSurface == null || mActivityState != ActivityState.RUNNING) {
260             throw new PreconditionViolatedException();
261         }
262     }
263 
264     // Returns true if we reached waitUntilNanos, false if some other event occurred.
waitForEvents(long waitUntilNanos)265     private boolean waitForEvents(long waitUntilNanos)
266             throws InterruptedException {
267         mRefreshRateChangedEvents.clear();
268         long nowNanos = System.nanoTime();
269         while (nowNanos < waitUntilNanos) {
270             long surfacePostTime = mLastBufferPostTime + POST_BUFFER_INTERVAL_NANOSECONDS;
271             long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos;
272             long timeoutMs = timeoutNs / 1_000_000L;
273             int remainderNs = (int) (timeoutNs % 1_000_000L);
274             // Don't call wait(0, 0) - it blocks indefinitely.
275             if (timeoutMs > 0 || remainderNs > 0) {
276                 mLock.wait(timeoutMs, remainderNs);
277             }
278             nowNanos = System.nanoTime();
279             verifyPreconditions();
280             if (!mRefreshRateChangedEvents.isEmpty()) {
281                 return false;
282             }
283             if (nowNanos >= surfacePostTime) {
284                 postBufferToSurface(Color.RED);
285             }
286         }
287         return true;
288     }
289 
waitForRefreshRateChange(FrameRateRange expectedRefreshRate)290     private void waitForRefreshRateChange(FrameRateRange expectedRefreshRate)
291             throws InterruptedException {
292         Log.i(TAG, "Waiting for the refresh rate to change");
293         long nowNanos = System.nanoTime();
294         long gracePeriodEndTimeNanos =
295                 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS;
296         while (true) {
297             // Wait until we switch to the expected refresh rate
298             while (!frameRatesMatchesOverride(mReportedDisplayRefreshRate, expectedRefreshRate)
299                     && !waitForEvents(gracePeriodEndTimeNanos)) {
300                 // Empty
301             }
302             nowNanos = System.nanoTime();
303             if (nowNanos >= gracePeriodEndTimeNanos) {
304                 throw new FrameRateTimeoutException(expectedRefreshRate,
305                         mReportedDisplayRefreshRate);
306             }
307 
308             // We've switched to a compatible frame rate. Now wait for a while to see if we stay at
309             // that frame rate.
310             long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_NANOSECONDS;
311             while (endTimeNanos > nowNanos) {
312                 if (waitForEvents(endTimeNanos)) {
313                     Log.i(TAG, String.format("Stable frame rate %.2f verified",
314                             mReportedDisplayRefreshRate));
315                     return;
316                 }
317                 nowNanos = System.nanoTime();
318                 if (!mRefreshRateChangedEvents.isEmpty()) {
319                     break;
320                 }
321             }
322         }
323     }
324 
325     // Returns a range of frame rate that is accepted to
326     // make this test more flexible for VRR devices.
327     // For example, a frame rate of 80 is valid for a 120 Hz VRR display,
328     // but only 60 is available when 80 is requested on non-VRR display.
329     // Here we return the range of the requested override +- the gap between the closest divisor.
getExpectedFrameRate(float refreshRate, int frameRateOverride)330     private FrameRateRange getExpectedFrameRate(float refreshRate, int frameRateOverride) {
331         float divisorOverrideGap = Float.MAX_VALUE;
332         FrameRateRange expectedFrameRateRange = new FrameRateRange(0.f, 0.f);
333         final float refreshRateRounded = Math.round(refreshRate);
334         for (int divisor = 1; refreshRateRounded / divisor >= 30; ++divisor) {
335             float frameRate = refreshRateRounded / divisor;
336             if (frameRate > frameRateOverride) {
337                 continue;
338             }
339             if (Math.abs(frameRateOverride - frameRate) <= divisorOverrideGap) {
340                 divisorOverrideGap = Math.abs(frameRateOverride - frameRate);
341             }
342         }
343         expectedFrameRateRange.min = frameRateOverride - divisorOverrideGap;
344         expectedFrameRateRange.max = frameRateOverride + divisorOverrideGap;
345         Log.i(TAG, String.format("getExpectedFrameRate expectedFrameRate %.2f %.2f",
346                 expectedFrameRateRange.min, expectedFrameRateRange.max));
347         return expectedFrameRateRange;
348     }
349 
350     interface FrameRateObserver {
observe(float initialRefreshRate, FrameRateRange expectedFrameRate, String condition)351         void observe(float initialRefreshRate, FrameRateRange expectedFrameRate, String condition)
352                 throws InterruptedException;
353     }
354 
355     class BackpressureFrameRateObserver implements FrameRateObserver {
356         @Override
observe(float initialRefreshRate, FrameRateRange expectedFrameRate, String condition)357         public void observe(float initialRefreshRate,
358                 FrameRateRange expectedFrameRate, String condition) {
359             long startTime = System.nanoTime();
360             int totalBuffers = 0;
361             float fps = 0;
362             while (System.nanoTime() - startTime <= FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) {
363                 postBufferToSurface(Color.BLACK + totalBuffers);
364                 totalBuffers++;
365                 if (System.nanoTime() - startTime >= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) {
366                     float testDuration = (System.nanoTime() - startTime) / 1e9f;
367                     fps = totalBuffers / testDuration;
368                     if (frameRatesMatchesOverride(fps, expectedFrameRate)) {
369                         Log.i(TAG,
370                                 String.format("%s: backpressure observed refresh rate %.2f",
371                                         condition,
372                                         fps));
373                         return;
374                     }
375                 }
376             }
377 
378             assertTrue(String.format(
379                             "%s: backpressure observed refresh rate doesn't match the current"
380                                     + "refresh rate. "
381                                     + "expected: (%.2f, %.2f) observed: %.2f",
382                             condition, expectedFrameRate.min, expectedFrameRate.max, fps),
383                     frameRatesMatchesOverride(fps, expectedFrameRate));
384         }
385     }
386 
387     class ChoreographerFrameRateObserver implements FrameRateObserver {
388         class ChoreographerThread extends Thread implements Choreographer.FrameCallback {
389             Choreographer mChoreographer;
390             long mStartTime;
391             public Handler mHandler;
392             Looper mLooper;
393             int mTotalCallbacks = 0;
394             long mEndTime;
395             FrameRateRange mExpectedRefreshRate;
396             String mCondition;
397 
ChoreographerThread(FrameRateRange expectedRefreshRate, String condition)398             ChoreographerThread(FrameRateRange expectedRefreshRate, String condition)
399                     throws InterruptedException {
400                 mExpectedRefreshRate = expectedRefreshRate;
401                 mCondition = condition;
402             }
403 
404             @Override
run()405             public void run() {
406                 Looper.prepare();
407                 mChoreographer = Choreographer.getInstance();
408                 mHandler = new Handler();
409                 mLooper = Looper.myLooper();
410                 mStartTime = System.nanoTime();
411                 mChoreographer.postFrameCallback(this);
412                 Looper.loop();
413             }
414 
415             @Override
doFrame(long frameTimeNanos)416             public void doFrame(long frameTimeNanos) {
417                 mTotalCallbacks++;
418                 mEndTime = System.nanoTime();
419                 if (mEndTime - mStartTime <= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) {
420                     mChoreographer.postFrameCallback(this);
421                     return;
422                 } else if (frameRatesMatchesOverride(getFps(), mExpectedRefreshRate)
423                         || mEndTime - mStartTime > FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) {
424                     mLooper.quitSafely();
425                     return;
426                 }
427                 mChoreographer.postFrameCallback(this);
428             }
429 
verifyFrameRate()430             public void verifyFrameRate() throws InterruptedException {
431                 float fps = getFps();
432                 Log.i(TAG,
433                         String.format("%s: choreographer observed refresh rate %.2f",
434                                 mCondition,
435                                 fps));
436                 assertTrue(String.format(
437                                 "%s: choreographer observed refresh rate doesn't match the current "
438                                         + "refresh rate. expected: (%.2f, %.2f) observed: %.2f",
439                                 mCondition,
440                                 mExpectedRefreshRate.min,
441                                 mExpectedRefreshRate.max,
442                                 fps),
443                         frameRatesMatchesOverride(fps, mExpectedRefreshRate));
444             }
445 
getFps()446             private float getFps() {
447                 return mTotalCallbacks / ((mEndTime - mStartTime) / 1e9f);
448             }
449         }
450 
451         @Override
observe(float initialRefreshRate, FrameRateRange expectedFrameRate, String condition)452         public void observe(float initialRefreshRate, FrameRateRange expectedFrameRate,
453                 String condition) throws InterruptedException {
454             ChoreographerThread thread = new ChoreographerThread(expectedFrameRate, condition);
455             thread.start();
456             thread.join();
457             thread.verifyFrameRate();
458         }
459     }
460 
461     class DisplayGetRefreshRateFrameRateObserver implements FrameRateObserver {
462         @Override
observe(float initialRefreshRate, FrameRateRange expectedFrameRate, String condition)463         public void observe(float initialRefreshRate,
464                 FrameRateRange expectedFrameRate, String condition) {
465             Log.i(TAG,
466                     String.format("%s: Display.getRefreshRate() returned refresh rate %.2f",
467                             condition, mReportedDisplayRefreshRate));
468             assertTrue(String.format("%s: Display.getRefreshRate() doesn't match the "
469                                     + "current refresh. expected: (%.2f, %.2f) observed: %.2f",
470                             condition,  expectedFrameRate.min,
471                             expectedFrameRate.max, mReportedDisplayRefreshRate),
472                     frameRatesMatchesOverride(mReportedDisplayRefreshRate, expectedFrameRate));
473         }
474     }
475     class DisplayModeGetRefreshRateFrameRateObserver implements FrameRateObserver {
476 
477         @Override
observe(float initialRefreshRate, FrameRateRange expectedFrameRate, String condition)478         public void observe(float initialRefreshRate,
479                 FrameRateRange expectedFrameRate, String condition) {
480             Log.i(TAG,
481                     String.format(
482                             "%s: Display.getMode().getRefreshRate() returned refresh rate %.2f",
483                             condition, mReportedDisplayModeRefreshRate));
484             assertTrue(String.format("%s: Display.getMode().getRefreshRate() doesn't match the "
485                                     + "current refresh. expected: %.2f observed: %.2f", condition,
486                             initialRefreshRate, mReportedDisplayModeRefreshRate),
487                     frameRatesMatchesOverride(mReportedDisplayModeRefreshRate, initialRefreshRate));
488         }
489     }
490 
491     interface TestScenario {
test(FrameRateObserver frameRateObserver, float initialRefreshRate, int[] frameRateOverrides)492         void test(FrameRateObserver frameRateObserver, float initialRefreshRate,
493                 int[] frameRateOverrides) throws InterruptedException, IOException;
494     }
495 
496     class GameModeTest implements TestScenario {
497         private UiDevice mUiDevice;
GameModeTest(UiDevice uiDevice)498         GameModeTest(UiDevice uiDevice) {
499             mUiDevice = uiDevice;
500         }
501         @Override
test(FrameRateObserver frameRateObserver, float initialRefreshRate, int[] frameRateOverrides)502         public void test(FrameRateObserver frameRateObserver, float initialRefreshRate,
503                 int[] frameRateOverrides) throws InterruptedException, IOException {
504             Log.i(TAG, "Starting testGameModeFrameRateOverride");
505 
506             for (int frameRateOverride : frameRateOverrides) {
507 
508                 Log.i(TAG, String.format("Setting Frame Rate to %d using Game Mode",
509                         frameRateOverride));
510 
511                 // Given that the frame rate we attempt to override is not always the divisor of
512                 // the current refresh rate, get the expected frame rate, which is the closest
513                 // divisor of the refresh rate here.
514                 FrameRateRange expectedFrameRate =
515                         getExpectedFrameRate(initialRefreshRate, frameRateOverride);
516 
517                 mUiDevice.executeShellCommand(String.format("cmd game set --mode 2 --fps %d %s",
518                         frameRateOverride, getPackageName()));
519 
520                 waitForRefreshRateChange(expectedFrameRate);
521                 frameRateObserver.observe(initialRefreshRate, expectedFrameRate,
522                         String.format("Game Mode Override(%d), expectedFrameRate(%.2f %.2f)",
523                                 frameRateOverride, expectedFrameRate.min, expectedFrameRate.max));
524             }
525 
526 
527             Log.i(TAG, String.format("Resetting game mode."));
528 
529             FrameRateRange expectedFrameRate =
530                     getExpectedFrameRate(initialRefreshRate, GAME_DEFAULT_FRAMERATE_INT);
531             mUiDevice.executeShellCommand(String.format("cmd game reset %s", getPackageName()));
532             waitForRefreshRateChange(expectedFrameRate);
533             frameRateObserver.observe(initialRefreshRate, expectedFrameRate,
534                     String.format("Game Default Frame Rate(%d), expectedFrameRate(%.2f %.2f)",
535                             GAME_DEFAULT_FRAMERATE_INT,
536                             expectedFrameRate.min,
537                             expectedFrameRate.max));
538         }
539     }
540 
541     // The activity being intermittently paused/resumed has been observed to
542     // cause test failures in practice, so we run the test with retry logic.
testFrameRateOverride(TestScenario frameRateOverrideBehavior, FrameRateObserver frameRateObserver, float initialRefreshRate, int[] frameRateOverrides)543     public void testFrameRateOverride(TestScenario frameRateOverrideBehavior,
544             FrameRateObserver frameRateObserver,
545             float initialRefreshRate,
546             int[] frameRateOverrides)
547             throws InterruptedException, IOException {
548         synchronized (mLock) {
549             Log.i(TAG, "testFrameRateOverride started with initial refresh rate "
550                     + initialRefreshRate);
551             int attempts = 0;
552             boolean testPassed = false;
553             try {
554                 while (!testPassed) {
555                     waitForPreconditions();
556                     try {
557                         frameRateOverrideBehavior.test(frameRateObserver,
558                                 initialRefreshRate, frameRateOverrides);
559                         testPassed = true;
560                     } catch (PreconditionViolatedException exc) {
561                         // The logic below will retry if we're below max attempts.
562                     } catch (FrameRateTimeoutException exc) {
563                         // Sometimes we get a test timeout failure before we get the
564                         // notification that the activity was paused, and it was the pause that
565                         // caused the timeout failure. Wait for a bit to see if we get notified
566                         // of a precondition violation, and if so, retry the test. Otherwise,
567                         // fail.
568                         assertTrue(
569                                 String.format(
570                                         "Timed out waiting for a stable and compatible frame"
571                                                 + " rate. requested=(%.2f, %.2f) received=%.2f.",
572                                         exc.appRequestedFrameRate.min,
573                                         exc.appRequestedFrameRate.max,
574                                         exc.deviceRefreshRate),
575                                 waitForPreconditionViolation());
576                     }
577 
578                     if (!testPassed) {
579                         Log.i(TAG,
580                                 String.format("Preconditions violated while running the test."
581                                                 + " Have surface? %b. Activity resumed? %b.",
582                                         mSurface != null,
583                                         mActivityState == ActivityState.RUNNING));
584                         attempts++;
585                         assertTrue(String.format(
586                                         "Exceeded %d precondition wait attempts. Giving up.",
587                                         PRECONDITION_WAIT_MAX_ATTEMPTS),
588                                 attempts < PRECONDITION_WAIT_MAX_ATTEMPTS);
589                     }
590                 }
591             } finally {
592                 if (testPassed) {
593                     Log.i(TAG, "**** PASS ****");
594                 } else {
595                     Log.i(TAG, "**** FAIL ****");
596                 }
597             }
598 
599         }
600     }
601 }
602