diff options
author | skyostil@chromium.org <skyostil@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-24 15:11:51 +0000 |
---|---|---|
committer | skyostil@chromium.org <skyostil@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-24 15:11:51 +0000 |
commit | 584a34a532d9b515211216c0d7ef6fe71508184e (patch) | |
tree | e8976512addd917343f1d2cdfc847a843135a292 /content | |
parent | fa9687be4f9b465b5c5aab54fec734512f366864 (diff) | |
download | chromium_src-584a34a532d9b515211216c0d7ef6fe71508184e.zip chromium_src-584a34a532d9b515211216c0d7ef6fe71508184e.tar.gz chromium_src-584a34a532d9b515211216c0d7ef6fe71508184e.tar.bz2 |
[Android] Implement vsync signal source
Implement a vsync signal source for locking compositor rendering
to the display frame period. It is based on the Choreographer API
introduced in Android Jellybean (API level 16).
Note that this patch does not tie the signal source to the compositor,
because all the necessary pieces have not been upstreamed yet.
BUG=http://crbug.com/137795
TEST=org.chromium.content.browser.test.VSyncMonitorTest
Review URL: https://chromiumcodereview.appspot.com/10854247
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@153214 0039d316-1c4b-4281-b951-d872f2087c98
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()); + } +} |