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;