• 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 com.android.cts.graphics.framerateoverride;
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.util.Log;
30 import android.view.Choreographer;
31 import android.view.Surface;
32 import android.view.SurfaceHolder;
33 import android.view.SurfaceView;
34 import android.view.ViewGroup;
35 
36 import java.io.IOException;
37 import java.util.ArrayList;
38 
39 /**
40  * An Activity to help with frame rate testing.
41  */
42 public class FrameRateOverrideTestActivity extends Activity {
43     private static final String TAG = "FrameRateOverrideTestActivity";
44     private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS = 2 * 1_000_000_000L;
45     private static final long STABLE_FRAME_RATE_WAIT_NANOSECONDS = 1 * 1_000_000_000L;
46     private static final long POST_BUFFER_INTERVAL_NANOSECONDS = 500_000_000L;
47     private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5;
48     private static final long PRECONDITION_WAIT_TIMEOUT_NANOSECONDS = 20 * 1_000_000_000L;
49     private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS = 3 * 1_000_000_000L;
50     private static final float FRAME_RATE_TOLERANCE = 0.01f;
51     private static final float FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE = 5;
52     private static final long FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS = 1 * 1_000_000_000L;
53     private static final long FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS = 10 * 1_000_000_000L;
54 
55     private DisplayManager mDisplayManager;
56     private SurfaceView mSurfaceView;
57     private Handler mHandler = new Handler(Looper.getMainLooper());
58     private Object mLock = new Object();
59     private Surface mSurface = null;
60     private float mReportedDisplayRefreshRate;
61     private float mReportedDisplayModeRefreshRate;
62     private ArrayList<Float> mRefreshRateChangedEvents = new ArrayList<Float>();
63 
64     private long mLastBufferPostTime;
65 
66     SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
67         @Override
68         public void surfaceCreated(SurfaceHolder holder) {
69             synchronized (mLock) {
70                 mSurface = holder.getSurface();
71                 mLock.notify();
72             }
73         }
74 
75         @Override
76         public void surfaceDestroyed(SurfaceHolder holder) {
77             synchronized (mLock) {
78                 mSurface = null;
79                 mLock.notify();
80             }
81         }
82 
83         @Override
84         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
85         }
86     };
87 
88     DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
89         @Override
90         public void onDisplayAdded(int displayId) {
91         }
92 
93         @Override
94         public void onDisplayChanged(int displayId) {
95             synchronized (mLock) {
96                 float refreshRate = getDisplay().getRefreshRate();
97                 float displayModeRefreshRate = getDisplay().getMode().getRefreshRate();
98                 if (refreshRate != mReportedDisplayRefreshRate
99                         || displayModeRefreshRate != mReportedDisplayModeRefreshRate) {
100                     Log.i(TAG, String.format("Frame rate changed: (%.2f, %.2f) --> (%.2f, %.2f)",
101                                     mReportedDisplayModeRefreshRate,
102                                     mReportedDisplayRefreshRate,
103                                     displayModeRefreshRate,
104                                     refreshRate));
105                     mReportedDisplayRefreshRate = refreshRate;
106                     mReportedDisplayModeRefreshRate = displayModeRefreshRate;
107                     mRefreshRateChangedEvents.add(refreshRate);
108                     mLock.notify();
109                 }
110             }
111         }
112 
113         @Override
114         public void onDisplayRemoved(int displayId) {
115         }
116     };
117 
118     private static class PreconditionViolatedException extends RuntimeException { }
119 
120     private static class FrameRateTimeoutException extends RuntimeException {
FrameRateTimeoutException(float appRequestedFrameRate, float deviceRefreshRate)121         FrameRateTimeoutException(float appRequestedFrameRate, float deviceRefreshRate) {
122             this.appRequestedFrameRate = appRequestedFrameRate;
123             this.deviceRefreshRate = deviceRefreshRate;
124         }
125 
126         public float appRequestedFrameRate;
127         public float deviceRefreshRate;
128     }
129 
postBufferToSurface(int color)130     public void postBufferToSurface(int color) {
131         mLastBufferPostTime = System.nanoTime();
132         Canvas canvas = mSurface.lockCanvas(null);
133         canvas.drawColor(color);
134         mSurface.unlockCanvasAndPost(canvas);
135     }
136 
137     @Override
onCreate(Bundle savedInstanceState)138     protected void onCreate(Bundle savedInstanceState) {
139         super.onCreate(savedInstanceState);
140         synchronized (mLock) {
141             mDisplayManager = getSystemService(DisplayManager.class);
142             mReportedDisplayRefreshRate = getDisplay().getRefreshRate();
143             mReportedDisplayModeRefreshRate = getDisplay().getMode().getRefreshRate();
144             mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
145             mSurfaceView = new SurfaceView(this);
146             mSurfaceView.setWillNotDraw(false);
147             setContentView(mSurfaceView,
148                     new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
149                             ViewGroup.LayoutParams.MATCH_PARENT));
150             mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
151         }
152     }
153 
154     @Override
onDestroy()155     protected void onDestroy() {
156         super.onDestroy();
157         mDisplayManager.unregisterDisplayListener(mDisplayListener);
158         synchronized (mLock) {
159             mLock.notify();
160         }
161     }
162 
frameRatesEqual(float frameRate1, float frameRate2)163     private static boolean frameRatesEqual(float frameRate1, float frameRate2) {
164         return Math.abs(frameRate1 - frameRate2) <= FRAME_RATE_TOLERANCE;
165     }
166 
frameRatesMatchesOverride(float frameRate1, float frameRate2)167     private static boolean frameRatesMatchesOverride(float frameRate1, float frameRate2) {
168         return Math.abs(frameRate1 - frameRate2) <= FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE;
169     }
170 
171     // Waits until our SurfaceHolder has a surface and the activity is resumed.
waitForPreconditions()172     private void waitForPreconditions() throws InterruptedException {
173         assertTrue(
174                 "Activity was unexpectedly destroyed", !isDestroyed());
175         if (mSurface == null || !isResumed()) {
176             Log.i(TAG, String.format(
177                     "Waiting for preconditions. Have surface? %b. Activity resumed? %b.",
178                             mSurface != null, isResumed()));
179         }
180         long nowNanos = System.nanoTime();
181         long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_NANOSECONDS;
182         while (mSurface == null || !isResumed()) {
183             long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
184             assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b."
185                             + " Activity resumed? %b.",
186                     mSurface != null, isResumed()),
187                     timeRemainingMillis > 0);
188             mLock.wait(timeRemainingMillis);
189             assertTrue(
190                     "Activity was unexpectedly destroyed", !isDestroyed());
191             nowNanos = System.nanoTime();
192         }
193     }
194 
195     // Returns true if we encounter a precondition violation, false otherwise.
waitForPreconditionViolation()196     private boolean waitForPreconditionViolation() throws InterruptedException {
197         assertTrue(
198                 "Activity was unexpectedly destroyed", !isDestroyed());
199         long nowNanos = System.nanoTime();
200         long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS;
201         while (mSurface != null && isResumed()) {
202             long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
203             if (timeRemainingMillis <= 0) {
204                 break;
205             }
206             mLock.wait(timeRemainingMillis);
207             assertTrue(
208                     "Activity was unexpectedly destroyed", !isDestroyed());
209             nowNanos = System.nanoTime();
210         }
211         return mSurface == null || !isResumed();
212     }
213 
verifyPreconditions()214     private void verifyPreconditions() {
215         if (mSurface == null || !isResumed()) {
216             throw new PreconditionViolatedException();
217         }
218     }
219 
220     // Returns true if we reached waitUntilNanos, false if some other event occurred.
waitForEvents(long waitUntilNanos)221     private boolean waitForEvents(long waitUntilNanos)
222             throws InterruptedException {
223         mRefreshRateChangedEvents.clear();
224         long nowNanos = System.nanoTime();
225         while (nowNanos < waitUntilNanos) {
226             long surfacePostTime = mLastBufferPostTime + POST_BUFFER_INTERVAL_NANOSECONDS;
227             long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos;
228             long timeoutMs = timeoutNs / 1_000_000L;
229             int remainderNs = (int) (timeoutNs % 1_000_000L);
230             // Don't call wait(0, 0) - it blocks indefinitely.
231             if (timeoutMs > 0 || remainderNs > 0) {
232                 mLock.wait(timeoutMs, remainderNs);
233             }
234             nowNanos = System.nanoTime();
235             verifyPreconditions();
236             if (!mRefreshRateChangedEvents.isEmpty()) {
237                 return false;
238             }
239             if (nowNanos >= surfacePostTime) {
240                 postBufferToSurface(Color.RED);
241             }
242         }
243         return true;
244     }
245 
waitForRefreshRateChange(float expectedRefreshRate)246     private void waitForRefreshRateChange(float expectedRefreshRate) throws InterruptedException {
247         Log.i(TAG, "Waiting for the refresh rate to change");
248         long nowNanos = System.nanoTime();
249         long gracePeriodEndTimeNanos =
250                 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS;
251         while (true) {
252             // Wait until we switch to the expected refresh rate
253             while (!frameRatesEqual(mReportedDisplayRefreshRate, expectedRefreshRate)
254                     && !waitForEvents(gracePeriodEndTimeNanos)) {
255                 // Empty
256             }
257             nowNanos = System.nanoTime();
258             if (nowNanos >= gracePeriodEndTimeNanos) {
259                 throw new FrameRateTimeoutException(expectedRefreshRate,
260                         mReportedDisplayRefreshRate);
261             }
262 
263             // We've switched to a compatible frame rate. Now wait for a while to see if we stay at
264             // that frame rate.
265             long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_NANOSECONDS;
266             while (endTimeNanos > nowNanos) {
267                 if (waitForEvents(endTimeNanos)) {
268                     Log.i(TAG, String.format("Stable frame rate %.2f verified",
269                             mReportedDisplayRefreshRate));
270                     return;
271                 }
272                 nowNanos = System.nanoTime();
273                 if (!mRefreshRateChangedEvents.isEmpty()) {
274                     break;
275                 }
276             }
277         }
278     }
279 
280     interface FrameRateObserver {
observe(float initialRefreshRate, float expectedFrameRate, String condition)281         void observe(float initialRefreshRate, float expectedFrameRate, String condition)
282                 throws InterruptedException;
283     }
284 
285     class BackpressureFrameRateObserver implements FrameRateObserver {
286         @Override
observe(float initialRefreshRate, float expectedFrameRate, String condition)287         public void observe(float initialRefreshRate, float expectedFrameRate, String condition) {
288             long startTime = System.nanoTime();
289             int totalBuffers = 0;
290             float fps = 0;
291             while (System.nanoTime() - startTime <= FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) {
292                 postBufferToSurface(Color.BLACK + totalBuffers);
293                 totalBuffers++;
294                 if (System.nanoTime() - startTime >= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) {
295                     float testDuration = (System.nanoTime() - startTime) / 1e9f;
296                     fps = totalBuffers / testDuration;
297                     if (frameRatesMatchesOverride(fps, expectedFrameRate)) {
298                         Log.i(TAG,
299                                 String.format("%s: backpressure observed refresh rate %.2f",
300                                         condition,
301                                         fps));
302                         return;
303                     }
304                 }
305             }
306 
307             assertTrue(String.format(
308                     "%s: backpressure observed refresh rate doesn't match the current refresh "
309                             + "rate. "
310                             + "expected: %.2f observed: %.2f", condition, expectedFrameRate, fps),
311                     frameRatesMatchesOverride(fps, expectedFrameRate));
312         }
313     }
314 
315     class ChoreographerFrameRateObserver implements FrameRateObserver {
316         class ChoreographerThread extends Thread implements Choreographer.FrameCallback {
317             Choreographer mChoreographer;
318             long mStartTime;
319             public Handler mHandler;
320             Looper mLooper;
321             int mTotalCallbacks = 0;
322             long mEndTime;
323             float mExpectedRefreshRate;
324             String mCondition;
325 
ChoreographerThread(float expectedRefreshRate, String condition)326             ChoreographerThread(float expectedRefreshRate, String condition)
327                     throws InterruptedException {
328                 mExpectedRefreshRate = expectedRefreshRate;
329                 mCondition = condition;
330             }
331 
332             @Override
run()333             public void run() {
334                 Looper.prepare();
335                 mChoreographer = Choreographer.getInstance();
336                 mHandler = new Handler();
337                 mLooper = Looper.myLooper();
338                 mStartTime = System.nanoTime();
339                 mChoreographer.postFrameCallback(this);
340                 Looper.loop();
341             }
342 
343             @Override
doFrame(long frameTimeNanos)344             public void doFrame(long frameTimeNanos) {
345                 mTotalCallbacks++;
346                 mEndTime = System.nanoTime();
347                 if (mEndTime - mStartTime <= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) {
348                     mChoreographer.postFrameCallback(this);
349                     return;
350                 } else if (frameRatesMatchesOverride(mExpectedRefreshRate, getFps())
351                         || mEndTime - mStartTime > FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) {
352                     mLooper.quitSafely();
353                     return;
354                 }
355                 mChoreographer.postFrameCallback(this);
356             }
357 
verifyFrameRate()358             public void verifyFrameRate() throws InterruptedException {
359                 float fps = getFps();
360                 Log.i(TAG,
361                         String.format("%s: choreographer observed refresh rate %.2f",
362                                 mCondition,
363                                 fps));
364                 assertTrue(String.format(
365                         "%s: choreographer observed refresh rate doesn't match the current "
366                                 + "refresh rate. expected: %.2f observed: %.2f",
367                         mCondition, mExpectedRefreshRate, fps),
368                         frameRatesMatchesOverride(mExpectedRefreshRate, fps));
369             }
370 
getFps()371             private float getFps() {
372                 return mTotalCallbacks / ((mEndTime - mStartTime) / 1e9f);
373             }
374         }
375 
376         @Override
observe(float initialRefreshRate, float expectedFrameRate, String condition)377         public void observe(float initialRefreshRate, float expectedFrameRate, String condition)
378                 throws InterruptedException {
379             ChoreographerThread thread = new ChoreographerThread(expectedFrameRate, condition);
380             thread.start();
381             thread.join();
382             thread.verifyFrameRate();
383         }
384     }
385 
386     class DisplayGetRefreshRateFrameRateObserver implements FrameRateObserver {
387         @Override
observe(float initialRefreshRate, float expectedFrameRate, String condition)388         public void observe(float initialRefreshRate, float expectedFrameRate, String condition) {
389             Log.i(TAG,
390                     String.format("%s: Display.getRefreshRate() returned refresh rate %.2f",
391                             condition, mReportedDisplayRefreshRate));
392             assertTrue(String.format("%s: Display.getRefreshRate() doesn't match the "
393                             + "current refresh. expected: %.2f observed: %.2f", condition,
394                     expectedFrameRate, mReportedDisplayRefreshRate),
395                     frameRatesMatchesOverride(mReportedDisplayRefreshRate, expectedFrameRate));
396         }
397     }
398     class DisplayModeGetRefreshRateFrameRateObserver implements FrameRateObserver {
399         private final boolean mDisplayModeReturnsPhysicalRefreshRateEnabled;
400 
DisplayModeGetRefreshRateFrameRateObserver( boolean displayModeReturnsPhysicalRefreshRateEnabled)401         DisplayModeGetRefreshRateFrameRateObserver(
402                 boolean displayModeReturnsPhysicalRefreshRateEnabled) {
403             mDisplayModeReturnsPhysicalRefreshRateEnabled =
404                     displayModeReturnsPhysicalRefreshRateEnabled;
405         }
406 
407         @Override
observe(float initialRefreshRate, float expectedFrameRate, String condition)408         public void observe(float initialRefreshRate, float expectedFrameRate, String condition) {
409             float expectedDisplayModeRefreshRate =
410                     mDisplayModeReturnsPhysicalRefreshRateEnabled ? initialRefreshRate
411                             : expectedFrameRate;
412             Log.i(TAG,
413                     String.format(
414                             "%s: Display.getMode().getRefreshRate() returned refresh rate %.2f",
415                             condition, mReportedDisplayModeRefreshRate));
416             assertTrue(String.format("%s: Display.getMode().getRefreshRate() doesn't match the "
417                             + "current refresh. expected: %.2f observed: %.2f", condition,
418                     expectedDisplayModeRefreshRate, mReportedDisplayModeRefreshRate),
419                     frameRatesMatchesOverride(mReportedDisplayModeRefreshRate,
420                             expectedDisplayModeRefreshRate));
421         }
422     }
423 
424     interface TestScenario {
test(FrameRateObserver frameRateObserver, float initialRefreshRate)425         void test(FrameRateObserver frameRateObserver,
426                 float initialRefreshRate) throws InterruptedException, IOException;
427     }
428 
429     class GameModeTest implements TestScenario {
430         private UiDevice mUiDevice;
GameModeTest(UiDevice uiDevice)431         GameModeTest(UiDevice uiDevice) {
432             mUiDevice = uiDevice;
433         }
434         @Override
test(FrameRateObserver frameRateObserver, float initialRefreshRate)435         public void test(FrameRateObserver frameRateObserver,
436                 float initialRefreshRate) throws InterruptedException, IOException {
437             Log.i(TAG, "Starting testGameModeFrameRateOverride");
438 
439             int initialRefreshRateInt = (int) initialRefreshRate;
440             for (int divisor = 1; initialRefreshRateInt / divisor >= 30; ++divisor) {
441                 int overrideFrameRate = initialRefreshRateInt / divisor;
442                 if (initialRefreshRateInt % divisor != 0) {
443                     // skip if the overriding frame rate is not a divisor of initial refresh rate
444                     Log.i(TAG, String.format("Skipping Frame rate %d as it is not a divisor of"
445                             + " refresh rate of %d", overrideFrameRate, initialRefreshRateInt));
446                     continue;
447                 }
448                 Log.i(TAG, String.format("Setting Frame Rate to %d using Game Mode",
449                         overrideFrameRate));
450 
451                 mUiDevice.executeShellCommand(String.format("cmd game set --mode 2 --fps %d %s",
452                         overrideFrameRate, getPackageName()));
453                 waitForRefreshRateChange(overrideFrameRate);
454                 frameRateObserver.observe(initialRefreshRate, overrideFrameRate,
455                         String.format("Game Mode Override(%d)", overrideFrameRate));
456             }
457 
458             Log.i(TAG, "Resetting Frame Rate setting");
459             mUiDevice.executeShellCommand(String.format("cmd game reset %s", getPackageName()));
460             waitForRefreshRateChange(initialRefreshRate);
461             frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Reset");
462         }
463     }
464 
465     // The activity being intermittently paused/resumed has been observed to
466     // cause test failures in practice, so we run the test with retry logic.
testFrameRateOverride(TestScenario frameRateOverrideBehavior, FrameRateObserver frameRateObserver, float initialRefreshRate)467     public void testFrameRateOverride(TestScenario frameRateOverrideBehavior,
468             FrameRateObserver frameRateObserver, float initialRefreshRate)
469             throws InterruptedException, IOException {
470         synchronized (mLock) {
471             Log.i(TAG, "testFrameRateOverride started with initial refresh rate "
472                     + initialRefreshRate);
473             int attempts = 0;
474             boolean testPassed = false;
475             try {
476                 while (!testPassed) {
477                     waitForPreconditions();
478                     try {
479                         frameRateOverrideBehavior.test(frameRateObserver,
480                                 initialRefreshRate);
481                         testPassed = true;
482                     } catch (PreconditionViolatedException exc) {
483                         // The logic below will retry if we're below max attempts.
484                     } catch (FrameRateTimeoutException exc) {
485                         // Sometimes we get a test timeout failure before we get the
486                         // notification that the activity was paused, and it was the pause that
487                         // caused the timeout failure. Wait for a bit to see if we get notified
488                         // of a precondition violation, and if so, retry the test. Otherwise
489                         // fail.
490                         assertTrue(
491                                 String.format(
492                                         "Timed out waiting for a stable and compatible frame"
493                                                 + " rate. requested=%.2f received=%.2f.",
494                                         exc.appRequestedFrameRate, exc.deviceRefreshRate),
495                                 waitForPreconditionViolation());
496                     }
497 
498                     if (!testPassed) {
499                         Log.i(TAG,
500                                 String.format("Preconditions violated while running the test."
501                                                 + " Have surface? %b. Activity resumed? %b.",
502                                         mSurface != null,
503                                         isResumed()));
504                         attempts++;
505                         assertTrue(String.format(
506                                 "Exceeded %d precondition wait attempts. Giving up.",
507                                 PRECONDITION_WAIT_MAX_ATTEMPTS),
508                                 attempts < PRECONDITION_WAIT_MAX_ATTEMPTS);
509                     }
510                 }
511             } finally {
512                 if (testPassed) {
513                     Log.i(TAG, "**** PASS ****");
514                 } else {
515                     Log.i(TAG, "**** FAIL ****");
516                 }
517             }
518 
519         }
520     }
521 }
522