diff options
Diffstat (limited to 'content')
-rw-r--r-- | content/public/android/java/src/org/chromium/content/browser/VSyncMonitor.java | 168 | ||||
-rw-r--r-- | content/public/android/javatests/src/org/chromium/content/browser/VSyncMonitorTest.java | 168 |
2 files changed, 336 insertions, 0 deletions
diff --git a/content/public/android/java/src/org/chromium/content/browser/VSyncMonitor.java b/content/public/android/java/src/org/chromium/content/browser/VSyncMonitor.java new file mode 100644 index 0000000..0aca7f3 --- /dev/null +++ b/content/public/android/java/src/org/chromium/content/browser/VSyncMonitor.java @@ -0,0 +1,168 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.view.Choreographer; +import android.view.View; +import android.view.WindowManager; + +/** + * Notifies clients of the default displays's vertical sync pulses. + */ +public class VSyncMonitor { + public interface Listener { + /** + * Called very soon after the start of the display's vertical sync period. + * @param monitor The VSyncMonitor that triggered the signal. + * @param vsyncTimeMicros Absolute frame time in microseconds. + */ + public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros); + } + + /** Time source used for test the timeout functionality. */ + interface TestTimeSource { + public long currentTimeMillis(); + } + + // To save battery, we don't run vsync more than this time unless requestUpdate() is called. + public static final long VSYNC_TIMEOUT_MILLISECONDS = 3000; + + private static final long MICROSECONDS_PER_SECOND = 1000000; + private static final long NANOSECONDS_PER_MICROSECOND = 1000; + + private static final String TAG = VSyncMonitor.class.getName(); + + // Display refresh rate as reported by the system. + private long mRefreshPeriodMicros; + + // Last time requestUpdate() was called. + private long mLastUpdateRequestMillis; + + // Whether we are currently getting notified of vsync events. + private boolean mVSyncCallbackActive = false; + + // Choreographer is used to detect vsync on >= JB. + private Choreographer mChoreographer; + private Choreographer.FrameCallback mFrameCallback; + + private Listener mListener; + private View mView; + private VSyncTerminator mVSyncTerminator; + private TestTimeSource mTestTimeSource; + + // VSyncTerminator keeps vsync running for a period of VSYNC_TIMEOUT_MILLISECONDS. If there is + // no update request during this period, the monitor will be stopped automatically. + private class VSyncTerminator implements Runnable { + public void run() { + if (currentTimeMillis() > mLastUpdateRequestMillis + + VSYNC_TIMEOUT_MILLISECONDS) { + stop(); + } else if (mVSyncTerminator == this) { + mView.postDelayed(this, VSYNC_TIMEOUT_MILLISECONDS); + } + } + } + + /** + * Constructor. + * + * @param context Resource context. + * @param view View to be used for timeout scheduling. + * @param listener Listener to be notified of vsync events. + */ + public VSyncMonitor(Context context, View view, Listener listener) { + mListener = listener; + mView = view; + + float refreshRate = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay().getRefreshRate(); + if (refreshRate <= 0) { + refreshRate = 60; + } + mRefreshPeriodMicros = (long) (MICROSECONDS_PER_SECOND / refreshRate); + + // Use Choreographer on JB+ to get notified of vsync. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mChoreographer = Choreographer.getInstance(); + mFrameCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + postVSyncCallback(); + mListener.onVSync(VSyncMonitor.this, + frameTimeNanos / NANOSECONDS_PER_MICROSECOND); + } + }; + } + } + + /** + * Returns the time interval between two consecutive vsync pulses in microseconds. + */ + public long getVSyncPeriodInMicroseconds() { + return mRefreshPeriodMicros; + } + + /** + * Determine whether a true vsync signal is available on this platform. + */ + public boolean isVSyncSignalAvailable() { + return mChoreographer != null; + } + + /** + * Request to be notified of display vsync events. Listener.onVSync() will be called soon after + * the upcoming vsync pulses. If VSYNC_TIMEOUT_MILLISECONDS passes after the last time this + * function is called, the updates will cease automatically. + * + * This function throws an IllegalStateException if isVSyncSignalAvailable() returns false. + */ + public void requestUpdate() { + if (!isVSyncSignalAvailable()) { + throw new IllegalStateException("VSync signal not available"); + } + mLastUpdateRequestMillis = currentTimeMillis(); + if (!mVSyncCallbackActive) { + mVSyncCallbackActive = true; + mVSyncTerminator = new VSyncTerminator(); + mView.postDelayed(mVSyncTerminator, VSYNC_TIMEOUT_MILLISECONDS); + postVSyncCallback(); + } + } + + private void postVSyncCallback() { + if (!mVSyncCallbackActive) { + return; + } + mChoreographer.postFrameCallback(mFrameCallback); + } + + /** + * Stop reporting vsync events. Note that at most one pending vsync event can still be delivered + * after this function is called. + */ + public void stop() { + mVSyncCallbackActive = false; + mVSyncTerminator = null; + } + + public boolean isActive() { + return mVSyncCallbackActive; + } + + void setTestDependencies(View testView, TestTimeSource testTimeSource) { + mView = testView; + mTestTimeSource = testTimeSource; + } + + private long currentTimeMillis() { + if (mTestTimeSource != null) { + return mTestTimeSource.currentTimeMillis(); + } + return System.currentTimeMillis(); + } +} diff --git a/content/public/android/javatests/src/org/chromium/content/browser/VSyncMonitorTest.java b/content/public/android/javatests/src/org/chromium/content/browser/VSyncMonitorTest.java new file mode 100644 index 0000000..730a874 --- /dev/null +++ b/content/public/android/javatests/src/org/chromium/content/browser/VSyncMonitorTest.java @@ -0,0 +1,168 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content.browser; + +import android.content.Context; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.MediumTest; +import android.view.View; + +import java.util.Arrays; +import java.util.concurrent.Callable; + +import org.chromium.base.ThreadUtils; +import org.chromium.content.browser.VSyncMonitor; + +public class VSyncMonitorTest extends InstrumentationTestCase { + private class VSyncDataCollector implements VSyncMonitor.Listener { + public long framePeriods[]; + public int frameCount; + + private long previousVSyncTimeMicros; + + VSyncDataCollector(int frames) { + framePeriods = new long[frames]; + } + + @Override + public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros) { + if (previousVSyncTimeMicros == 0) { + previousVSyncTimeMicros = vsyncTimeMicros; + return; + } + if (frameCount >= framePeriods.length) { + synchronized (this) { + notify(); + } + return; + } + framePeriods[frameCount++] = vsyncTimeMicros - previousVSyncTimeMicros; + previousVSyncTimeMicros = vsyncTimeMicros; + monitor.requestUpdate(); + } + }; + + // The vsync monitor must be created on the UI thread to avoid associating the underlying + // Choreographer with the Looper from the test runner thread. + private VSyncMonitor createVSyncMonitor(final VSyncMonitor.Listener listener) { + return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<VSyncMonitor>() { + @Override + public VSyncMonitor call() { + Context context = getInstrumentation().getContext(); + View dummyView = new View(context); + return new VSyncMonitor(context, dummyView, listener); + } + }); + } + + // Check that the vsync period roughly matches the timestamps that the monitor generates. + @MediumTest + public void testVSyncPeriod() throws InterruptedException { + // Collect roughly two seconds of data on a 60 fps display. + VSyncDataCollector collector = new VSyncDataCollector(60 * 2); + VSyncMonitor monitor = createVSyncMonitor(collector); + + long reportedFramePeriod = monitor.getVSyncPeriodInMicroseconds(); + assertTrue(reportedFramePeriod > 0); + + if (!monitor.isVSyncSignalAvailable()) { + // Cannot do timing test without real vsync signal. We can still verify that the monitor + // throws if we try to use it. + try { + monitor.requestUpdate(); + fail("IllegalStateException not thrown."); + } catch (IllegalStateException e) { + } + return; + } + + monitor.requestUpdate(); + synchronized (collector) { + collector.wait(); + } + monitor.stop(); + + // Check that the median frame rate is within 10% of the reported frame period. + Arrays.sort(collector.framePeriods, 0, collector.framePeriods.length); + long medianFramePeriod = collector.framePeriods[collector.framePeriods.length / 2]; + if (Math.abs(medianFramePeriod - reportedFramePeriod) > reportedFramePeriod * .1) { + fail("Measured median frame period " + medianFramePeriod + + " differs by more than 10% from the reported frame period " + + reportedFramePeriod); + } + } + + private class TestView extends View { + private Runnable mPendingAction; + + public TestView(Context context) { + super(context); + } + + @Override + public boolean postDelayed(Runnable action, long delayMillis) { + mPendingAction = action; + return true; + } + + public void runPendingAction() { + assertNotNull(mPendingAction); + mPendingAction.run(); + } + }; + + private class TestTimeSource implements VSyncMonitor.TestTimeSource { + long mCurrentTimeMillis; + + @Override + public long currentTimeMillis() { + return mCurrentTimeMillis; + } + + void setCurrentTimeMillis(long currentTimeMillis) { + mCurrentTimeMillis = currentTimeMillis; + } + }; + + // Check the the vsync monitor terminates after we have stopped requesting updates for some + // time. + @MediumTest + public void testVSyncTimeout() throws InterruptedException { + VSyncMonitor.Listener listener = new VSyncMonitor.Listener() { + @Override + public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros) { + // No-op vsync listener. + } + }; + VSyncMonitor monitor = createVSyncMonitor(listener); + if (!monitor.isVSyncSignalAvailable()) { + // Cannot do timeout test without real vsync signal. + return; + } + + TestTimeSource testTimeSource = new TestTimeSource(); + TestView testView = new TestView(getInstrumentation().getContext()); + monitor.setTestDependencies(testView, testTimeSource); + + // Requesting an update should make the monitor active. + testTimeSource.setCurrentTimeMillis(0); + monitor.requestUpdate(); + assertTrue(monitor.isActive()); + + // Running the timeout callback should have no effect when no time has passed. + testView.runPendingAction(); + assertTrue(monitor.isActive()); + + // Ditto after half the timeout interval. + testTimeSource.setCurrentTimeMillis(VSyncMonitor.VSYNC_TIMEOUT_MILLISECONDS / 2); + testView.runPendingAction(); + assertTrue(monitor.isActive()); + + // The monitor should be stopped after the timeout interval has passed. + testTimeSource.setCurrentTimeMillis(VSyncMonitor.VSYNC_TIMEOUT_MILLISECONDS + 1); + testView.runPendingAction(); + assertFalse(monitor.isActive()); + } +} |