diff --git a/.eslintrc b/.eslintrc index a45786367..7bd17b585 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,6 +21,7 @@ "__fbBatchedBridgeConfig": false, "alert": false, "cancelAnimationFrame": false, + "cancelIdleCallback": false, "clearImmediate": true, "clearInterval": false, "clearTimeout": false, @@ -40,6 +41,7 @@ "process": false, "Promise": true, "requestAnimationFrame": true, + "requestIdleCallback": true, "require": false, "Set": true, "setImmediate": true, diff --git a/Examples/UIExplorer/js/TimerExample.js b/Examples/UIExplorer/js/TimerExample.js index 3b365ce2c..7b577821f 100644 --- a/Examples/UIExplorer/js/TimerExample.js +++ b/Examples/UIExplorer/js/TimerExample.js @@ -28,10 +28,101 @@ var { AlertIOS, Platform, ToastAndroid, + Text, View, } = ReactNative; var TimerMixin = require('react-timer-mixin'); var UIExplorerButton = require('./UIExplorerButton'); +var performanceNow = require('fbjs/lib/performanceNow'); + +function burnCPU(milliseconds) { + const start = performanceNow(); + while (performanceNow() < (start + milliseconds)) {} +} + +var RequestIdleCallbackTester = React.createClass({ + _idleTimer: (null: any), + _iters: 0, + + getInitialState() { + return { + message: '-', + }; + }, + + componentWillUnmount() { + cancelIdleCallback(this._idleTimer); + }, + + render() { + return ( + + {Platform.OS === 'ios' ? this._renderIOS() : this._renderAndroid()} + + ); + }, + + _renderIOS() { + return ( + Not implemented on iOS, falls back to requestAnimationFrame + ); + }, + + _renderAndroid() { + return ( + + + Run requestIdleCallback + + + + Burn CPU inside of requestIdleCallback + + + + Run background task + + + + Stop background task + + + {this.state.message} + + ); + }, + + _run(shouldBurnCPU) { + cancelIdleCallback(this._idleTimer); + this._idleTimer = requestIdleCallback((deadline) => { + let message = ''; + + if (shouldBurnCPU) { + burnCPU(10); + message = 'Burned CPU for 10ms,'; + } + this.setState({message: `${message} ${deadline.timeRemaining()}ms remaining in frame`}); + }); + }, + + _runBackground() { + cancelIdleCallback(this._idleTimer); + const handler = (deadline) => { + while (deadline.timeRemaining() > 5) { + burnCPU(5); + this.setState({message: `Burned CPU for 5ms ${this._iters++} times, ${deadline.timeRemaining()}ms remaining in frame`}); + } + + this._idleTimer = requestIdleCallback(handler); + }; + this._idleTimer = requestIdleCallback(handler); + }, + + _stopBackground() { + this._iters = 0; + cancelIdleCallback(this._idleTimer); + } +}); var TimerTester = React.createClass({ mixins: [TimerMixin], @@ -141,6 +232,17 @@ exports.examples = [ ); }, }, + { + title: 'this.requestIdleCallback(fn)', + description: 'Execute function fn on the next JS frame that has idle time', + render: function() { + return ( + + + + ); + }, + }, { title: 'this.setImmediate(fn)', description: 'Execute function fn at the end of the current JS event loop.', diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 60bbb7e79..60b574709 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -144,6 +144,8 @@ function setUpTimers(): void { defineLazyTimer('clearImmediate'); defineLazyTimer('requestAnimationFrame'); defineLazyTimer('cancelAnimationFrame'); + defineLazyTimer('requestIdleCallback'); + defineLazyTimer('cancelIdleCallback'); } function setUpAlert(): void { diff --git a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js index 0ec0a4c08..e751711a9 100644 --- a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js +++ b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js @@ -96,6 +96,41 @@ var JSTimers = { return newID; }, + /** + * @param {function} func Callback to be invoked every frame and provided + * with time remaining in frame. + */ + requestIdleCallback: function(func) { + if (!RCTTiming.setSendIdleEvents) { + console.warn('requestIdleCallback is not currently supported on this platform'); + return requestAnimationFrame(func); + } + + if (JSTimersExecution.requestIdleCallbacks.length === 0) { + RCTTiming.setSendIdleEvents(true); + } + + var newID = JSTimersExecution.GUID++; + var freeIndex = JSTimers._getFreeIndex(); + JSTimersExecution.timerIDs[freeIndex] = newID; + JSTimersExecution.callbacks[freeIndex] = func; + JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.requestIdleCallback; + JSTimersExecution.requestIdleCallbacks.push(newID); + return newID; + }, + + cancelIdleCallback: function(timerID) { + JSTimers._clearTimerID(timerID); + var index = JSTimersExecution.requestIdleCallbacks.indexOf(timerID); + if (index !== -1) { + JSTimersExecution.requestIdleCallbacks.splice(index, 1); + } + + if (JSTimersExecution.requestIdleCallbacks.length === 0) { + RCTTiming.setSendIdleEvents(false); + } + }, + clearTimeout: function(timerID) { JSTimers._clearTimerID(timerID); }, @@ -127,7 +162,9 @@ var JSTimers = { // See corresponding comment in `callTimers` for reasoning behind this if (index !== -1) { JSTimersExecution._clearIndex(index); - if (JSTimersExecution.types[index] !== JSTimersExecution.Type.setImmediate) { + var type = JSTimersExecution.types[index]; + if (type !== JSTimersExecution.Type.setImmediate && + type !== JSTimersExecution.Type.requestIdleCallback) { RCTTiming.deleteTimer(timerID); } } diff --git a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimersExecution.js b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimersExecution.js index a4938ed40..cbb9bf804 100644 --- a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimersExecution.js +++ b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimersExecution.js @@ -31,6 +31,7 @@ const JSTimersExecution = { setInterval: null, requestAnimationFrame: null, setImmediate: null, + requestIdleCallback: null, }), // Parallel arrays: @@ -38,13 +39,14 @@ const JSTimersExecution = { types: [], timerIDs: [], immediates: [], + requestIdleCallbacks: [], /** * Calls the callback associated with the ID. Also unregister that callback * if it was a one time timer (setTimeout), and not unregister it if it was * recurring (setInterval). */ - callTimer(timerID) { + callTimer(timerID, frameTime) { warning( timerID <= JSTimersExecution.GUID, 'Tried to call timer with ID %s but no such timer exists.', @@ -65,7 +67,8 @@ const JSTimersExecution = { // Clear the metadata if (type === JSTimersExecution.Type.setTimeout || type === JSTimersExecution.Type.setImmediate || - type === JSTimersExecution.Type.requestAnimationFrame) { + type === JSTimersExecution.Type.requestAnimationFrame || + type === JSTimersExecution.Type.requestIdleCallback) { JSTimersExecution._clearIndex(timerIndex); } @@ -77,6 +80,16 @@ const JSTimersExecution = { } else if (type === JSTimersExecution.Type.requestAnimationFrame) { const currentTime = performanceNow(); callback(currentTime); + } else if (type === JSTimersExecution.Type.requestIdleCallback) { + const { Timing } = require('NativeModules'); + callback({ + timeRemaining: function() { + // TODO: Optimisation: allow running for longer than one frame if + // there are no pending JS calls on the bridge from native. This + // would require a way to check the bridge queue synchronously. + return Math.max(0, Timing.frameDuration - (performanceNow() - frameTime)); + }, + }); } else { console.error('Tried to call a callback with invalid type: ' + type); return; @@ -99,7 +112,7 @@ const JSTimersExecution = { ); JSTimersExecution.errors = null; - timerIDs.forEach(JSTimersExecution.callTimer); + timerIDs.forEach((id) => { JSTimersExecution.callTimer(id); }); const errors = JSTimersExecution.errors; if (errors) { @@ -118,6 +131,35 @@ const JSTimersExecution = { } }, + callIdleCallbacks: function(frameTime) { + const { Timing } = require('NativeModules'); + + if (Timing.frameDuration - (performanceNow() - frameTime) < Timing.idleCallbackFrameDeadline) { + return; + } + + JSTimersExecution.errors = null; + + if (JSTimersExecution.requestIdleCallbacks.length > 0) { + const passIdleCallbacks = JSTimersExecution.requestIdleCallbacks.slice(); + JSTimersExecution.requestIdleCallbacks = []; + + for (let i = 0; i < passIdleCallbacks.length; ++i) { + JSTimersExecution.callTimer(passIdleCallbacks[i], frameTime); + } + } + + if (JSTimersExecution.requestIdleCallbacks.length === 0) { + Timing.setSendIdleEvents(false); + } + + if (JSTimersExecution.errors) { + JSTimersExecution.errors.forEach((error) => + require('JSTimers').setTimeout(() => { throw error; }, 0) + ); + } + }, + /** * Performs a single pass over the enqueued immediates. Returns whether * more immediates are queued up (can be used as a condition a while loop). diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index ee4a8a2c3..02bfb32c8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -32,7 +32,7 @@ import com.facebook.react.uimanager.UIImplementationProvider; import com.facebook.react.uimanager.ViewManager; /** - * This class is managing instances of {@link CatalystInstance}. It expose a way to configure + * This class is managing instances of {@link CatalystInstance}. It exposes a way to configure * catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that * instance. It also sets up connection between the instance and developers support functionality * of the framework. diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java index 68d024369..7da997411 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java @@ -80,8 +80,8 @@ import static com.facebook.react.bridge.ReactMarkerConstants.PROCESS_PACKAGES_ST import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_START; /** - * This class is managing instances of {@link CatalystInstance}. It expose a way to configure - * catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that + * This class manages instances of {@link CatalystInstance}. It exposes a way to configure + * catalyst instances using {@link ReactPackage} and keeps track of the lifecycle of that * instance. It also sets up connection between the instance and developers support functionality * of the framework. * diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java b/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java index 29c31b416..348855edb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/SystemClock.java @@ -22,4 +22,9 @@ public class SystemClock { public static long nanoTime() { return System.nanoTime(); } + + public static long uptimeMillis() { + return android.os.SystemClock.uptimeMillis(); + } + } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java index 41b241fad..e8ff7c906 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/JSTimersExecution.java @@ -15,8 +15,7 @@ import com.facebook.react.bridge.WritableArray; @SupportsWebWorkers public interface JSTimersExecution extends JavaScriptModule { - - public void callTimers(WritableArray timerIDs); - - public void emitTimeDriftWarning(String warningMessage); + void callTimers(WritableArray timerIDs); + void callIdleCallbacks(double frameTime); + void emitTimeDriftWarning(String warningMessage); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java index 8b88c05b4..508141f12 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/Timing.java @@ -9,14 +9,6 @@ package com.facebook.react.modules.core; -import javax.annotation.Nullable; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.PriorityQueue; -import java.util.concurrent.atomic.AtomicBoolean; - import android.util.SparseArray; import android.view.Choreographer; @@ -28,17 +20,38 @@ import com.facebook.react.bridge.OnExecutorUnregisteredListener; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.MapBuilder; import com.facebook.react.common.SystemClock; import com.facebook.react.devsupport.DevSupportManager; import com.facebook.react.uimanager.ReactChoreographer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.Nullable; + /** * Native module for JS timer execution. Timers fire on frame boundaries. */ public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener, OnExecutorUnregisteredListener { + // The minimum time in milliseconds left in the frame to call idle callbacks. + private static final float IDLE_CALLBACK_FRAME_DEADLINE_MS = 1.f; + // The total duration of a frame in milliseconds, this assumes that devices run at 60 fps. + // TODO: Lower frame duration on devices that are too slow to run consistently + // at 60 fps. + private static final float FRAME_DURATION_MS = 1000.f / 60.f; + private final DevSupportManager mDevSupportManager; private static class Timer { @@ -63,7 +76,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl } } - private class FrameCallback implements Choreographer.FrameCallback { + private class TimerFrameCallback implements Choreographer.FrameCallback { // Temporary map for constructing the individual arrays of timers per ExecutorToken private final HashMap mTimersToCall = new HashMap<>(); @@ -107,13 +120,84 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl } } + private class IdleFrameCallback implements Choreographer.FrameCallback { + + @Override + public void doFrame(long frameTimeNanos) { + if (isPaused.get()) { + return; + } + + // If the JS thread is busy for multiple frames we cancel any other pending runnable. + if (mCurrentIdleCallbackRunnable != null) { + mCurrentIdleCallbackRunnable.cancel(); + } + + mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos); + getReactApplicationContext().runOnJSQueueThread(mCurrentIdleCallbackRunnable); + + Assertions.assertNotNull(mReactChoreographer).postFrameCallback( + ReactChoreographer.CallbackType.IDLE_EVENT, + this); + } + } + + private class IdleCallbackRunnable implements Runnable { + private volatile boolean mCancelled = false; + private final long mFrameStartTime; + + public IdleCallbackRunnable(long frameStartTime) { + mFrameStartTime = frameStartTime; + } + + @Override + public void run() { + if (mCancelled) { + return; + } + + long frameTimeMillis = mFrameStartTime / 1000000; + long timeSinceBoot = SystemClock.uptimeMillis(); + long frameTimeElapsed = timeSinceBoot - frameTimeMillis; + long time = SystemClock.currentTimeMillis(); + long absoluteFrameStartTime = time - frameTimeElapsed; + + if (FRAME_DURATION_MS - (float)frameTimeElapsed < IDLE_CALLBACK_FRAME_DEADLINE_MS) { + return; + } + + mIdleCallbackContextsToCall.clear(); + synchronized (mIdleCallbackGuard) { + mIdleCallbackContextsToCall.addAll(mSendIdleEventsExecutorTokens); + } + + for (ExecutorToken context : mIdleCallbackContextsToCall) { + getReactApplicationContext().getJSModule(context, JSTimersExecution.class) + .callIdleCallbacks(absoluteFrameStartTime); + } + + mCurrentIdleCallbackRunnable = null; + } + + public void cancel() { + mCancelled = true; + } + } + private final Object mTimerGuard = new Object(); + private final Object mIdleCallbackGuard = new Object(); private final PriorityQueue mTimers; - private final HashMap> mTimerIdsToTimers; + private final Map> mTimerIdsToTimers; private final AtomicBoolean isPaused = new AtomicBoolean(true); - private final FrameCallback mFrameCallback = new FrameCallback(); + private final TimerFrameCallback mTimerFrameCallback = new TimerFrameCallback(); + private final IdleFrameCallback mIdleFrameCallback = new IdleFrameCallback(); + private @Nullable IdleCallbackRunnable mCurrentIdleCallbackRunnable; private @Nullable ReactChoreographer mReactChoreographer; private boolean mFrameCallbackPosted = false; + private boolean mFrameIdleCallbackPosted = false; + private final Set mSendIdleEventsExecutorTokens; + // Temporary array used to dipatch idle callbacks on the JS thread. + private final List mIdleCallbackContextsToCall; public Timing(ReactApplicationContext reactContext, DevSupportManager devSupportManager) { super(reactContext); @@ -135,6 +219,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl } }); mTimerIdsToTimers = new HashMap<>(); + mSendIdleEventsExecutorTokens = new HashSet<>(); + mIdleCallbackContextsToCall = new ArrayList<>(); } @Override @@ -148,11 +234,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl public void onHostPause() { isPaused.set(true); clearChoreographerCallback(); + clearChoreographerIdleCallback(); } @Override public void onHostDestroy() { clearChoreographerCallback(); + clearChoreographerIdleCallback(); } @Override @@ -161,18 +249,25 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl // TODO(5195192) Investigate possible problems related to restarting all tasks at the same // moment setChoreographerCallback(); + + synchronized (mIdleCallbackGuard) { + if (mSendIdleEventsExecutorTokens.size() > 0) { + setChoreographerIdleCallback(); + } + } } @Override public void onCatalystInstanceDestroy() { clearChoreographerCallback(); + clearChoreographerIdleCallback(); } private void setChoreographerCallback() { if (!mFrameCallbackPosted) { Assertions.assertNotNull(mReactChoreographer).postFrameCallback( ReactChoreographer.CallbackType.TIMERS_EVENTS, - mFrameCallback); + mTimerFrameCallback); mFrameCallbackPosted = true; } } @@ -181,14 +276,32 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl if (mFrameCallbackPosted) { Assertions.assertNotNull(mReactChoreographer).removeFrameCallback( ReactChoreographer.CallbackType.TIMERS_EVENTS, - mFrameCallback); + mTimerFrameCallback); mFrameCallbackPosted = false; } } + private void setChoreographerIdleCallback() { + if (!mFrameIdleCallbackPosted) { + Assertions.assertNotNull(mReactChoreographer).postFrameCallback( + ReactChoreographer.CallbackType.IDLE_EVENT, + mIdleFrameCallback); + mFrameIdleCallbackPosted = true; + } + } + + private void clearChoreographerIdleCallback() { + if (mFrameIdleCallbackPosted) { + Assertions.assertNotNull(mReactChoreographer).removeFrameCallback( + ReactChoreographer.CallbackType.IDLE_EVENT, + mIdleFrameCallback); + mFrameIdleCallbackPosted = false; + } + } + @Override public String getName() { - return "RKTiming"; + return "RCTTiming"; } @Override @@ -196,6 +309,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl return true; } + @Override + public Map getConstants() { + return MapBuilder.of( + "frameDuration", FRAME_DURATION_MS, + "idleCallbackFrameDeadline", IDLE_CALLBACK_FRAME_DEADLINE_MS); + } + @Override public void onExecutorDestroyed(ExecutorToken executorToken) { synchronized (mTimerGuard) { @@ -208,6 +328,10 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl mTimers.remove(timer); } } + + synchronized (mIdleCallbackGuard) { + mSendIdleEventsExecutorTokens.remove(executorToken); + } } @ReactMethod @@ -272,4 +396,28 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl mTimers.remove(timer); } } + + @ReactMethod + public void setSendIdleEvents(ExecutorToken executorToken, boolean sendIdleEvents) { + synchronized (mIdleCallbackGuard) { + if (sendIdleEvents) { + mSendIdleEventsExecutorTokens.add(executorToken); + } else { + mSendIdleEventsExecutorTokens.remove(executorToken); + } + } + + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + synchronized (mIdleCallbackGuard) { + if (mSendIdleEventsExecutorTokens.size() > 0) { + setChoreographerIdleCallback(); + } else { + clearChoreographerIdleCallback(); + } + } + } + }); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java index 5ceed3f6c..cbd6408cb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactChoreographer.java @@ -46,6 +46,12 @@ public class ReactChoreographer { * Events that make JS do things. */ TIMERS_EVENTS(3), + + /** + * Event used to trigger the idle callback. Called after all UI work has been + * dispatched to JS. + */ + IDLE_EVENT(4), ; private final int mOrder; diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java index 7eebda7f1..5ca8e67e0 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java @@ -52,8 +52,9 @@ public class TimingModuleTest { private static final long FRAME_TIME_NS = 17 * 1000 * 1000; // 17 ms private Timing mTiming; - private ReactChoreographer mChoreographerMock; + private ReactChoreographer mReactChoreographerMock; private PostFrameCallbackHandler mPostFrameCallbackHandler; + private PostFrameIdleCallbackHandler mIdlePostFrameCallbackHandler; private long mCurrentTimeNs; private JSTimersExecution mJSTimersMock; private ExecutorToken mExecutorTokenMock; @@ -73,12 +74,13 @@ public class TimingModuleTest { }); PowerMockito.mockStatic(SystemClock.class); + when(SystemClock.uptimeMillis()).thenReturn(mCurrentTimeNs / 1000000); when(SystemClock.currentTimeMillis()).thenReturn(mCurrentTimeNs / 1000000); when(SystemClock.nanoTime()).thenReturn(mCurrentTimeNs); - mChoreographerMock = mock(ReactChoreographer.class); + mReactChoreographerMock = mock(ReactChoreographer.class); PowerMockito.mockStatic(ReactChoreographer.class); - when(ReactChoreographer.getInstance()).thenReturn(mChoreographerMock); + when(ReactChoreographer.getInstance()).thenReturn(mReactChoreographerMock); CatalystInstance reactInstance = mock(CatalystInstance.class); ReactApplicationContext reactContext = mock(ReactApplicationContext.class); @@ -86,26 +88,49 @@ public class TimingModuleTest { mCurrentTimeNs = 0; mPostFrameCallbackHandler = new PostFrameCallbackHandler(); + mIdlePostFrameCallbackHandler = new PostFrameIdleCallbackHandler(); doAnswer(mPostFrameCallbackHandler) - .when(mChoreographerMock) + .when(mReactChoreographerMock) .postFrameCallback( eq(ReactChoreographer.CallbackType.TIMERS_EVENTS), any(Choreographer.FrameCallback.class)); + doAnswer(mIdlePostFrameCallbackHandler) + .when(mReactChoreographerMock) + .postFrameCallback( + eq(ReactChoreographer.CallbackType.IDLE_EVENT), + any(Choreographer.FrameCallback.class)); + mTiming = new Timing(reactContext, mock(DevSupportManager.class)); mJSTimersMock = mock(JSTimersExecution.class); mExecutorTokenMock = mock(ExecutorToken.class); when(reactContext.getJSModule(mExecutorTokenMock, JSTimersExecution.class)).thenReturn(mJSTimersMock); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ((Runnable)invocation.getArguments()[0]).run(); + return null; + } + }).when(reactContext).runOnJSQueueThread(any(Runnable.class)); + mTiming.initialize(); } private void stepChoreographerFrame() { Choreographer.FrameCallback callback = mPostFrameCallbackHandler.getAndResetFrameCallback(); + Choreographer.FrameCallback idleCallback = mIdlePostFrameCallbackHandler.getAndResetFrameCallback(); + mCurrentTimeNs += FRAME_TIME_NS; + when(SystemClock.uptimeMillis()).thenReturn(mCurrentTimeNs / 1000000); if (callback != null) { callback.doFrame(mCurrentTimeNs); } + + if (idleCallback != null) { + idleCallback.doFrame(mCurrentTimeNs); + } } @Test @@ -170,6 +195,33 @@ public class TimingModuleTest { verify(mJSTimersMock).callTimers(JavaOnlyArray.of(100)); } + @Test + public void testIdleCallback() { + mTiming.onHostResume(); + mTiming.setSendIdleEvents(mExecutorTokenMock, true); + + stepChoreographerFrame(); + verify(mJSTimersMock).callIdleCallbacks(SystemClock.currentTimeMillis()); + } + + private static class PostFrameIdleCallbackHandler implements Answer { + + private Choreographer.FrameCallback mFrameCallback; + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + mFrameCallback = (Choreographer.FrameCallback) args[1]; + return null; + } + + public Choreographer.FrameCallback getAndResetFrameCallback() { + Choreographer.FrameCallback callback = mFrameCallback; + mFrameCallback = null; + return callback; + } + } + private static class PostFrameCallbackHandler implements Answer { private Choreographer.FrameCallback mFrameCallback; diff --git a/flow/global.js b/flow/global.js new file mode 100644 index 000000000..775492726 --- /dev/null +++ b/flow/global.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +/* eslint-disable */ + +// Add missing flow definitions, can be removed when included in flow. +declare function requestIdleCallback(callback: any): number; +declare function cancelIdleCallback(idleCallbackId: ?number): void;