Permalink
Browse files

Initial implementation of requestIdleCallback on Android

Summary:
This is a follow up of the work by brentvatne in #5052. This addresses the feedback by astreet.

- Uses ReactChoreographer with a new callback type
- Callback dispatch logic moved to JS
- Only calls into JS when needed, when there are pending callbacks, it even removes the Choreographer listener when no JS context listen for idle events.

** Test plan **
Tested by running a background task that burns all remaining idle time (see new UIExplorer example) and made sure that UI and JS fps stayed near 60 on a real device (Nexus 6) with dev mode disabled. Also tried adding a JS driven animation and it stayed smooth.

Tested that native only calls into JS when there are pending idle callbacks.

Also tested that timers are executed before idle callback.
```
requestIdleCallback(() => console.log(1));
setTimeout(() => console.log(2), 100);
burnCPU(1000);
// 2
// 1
```

I did *not* test with webworkers but it should work as I'm using executor tokens.
Closes #8569

Differential Revision: D3558869

Pulled By: astreet

fbshipit-source-id: 61fa82eb26001d2b8c2ea69c35bf3eb5ce5454ba
  • Loading branch information...
1 parent 22eabe5 commit 18394fb1795c84672b3486756f2ed8db43a095b4 @janicduplessis janicduplessis committed with Facebook Github Bot 5 Jul 14, 2016
View
@@ -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,
@@ -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 (
+ <View>
+ {Platform.OS === 'ios' ? this._renderIOS() : this._renderAndroid()}
+ </View>
+ );
+ },
+
+ _renderIOS() {
+ return (
+ <Text>Not implemented on iOS, falls back to requestAnimationFrame</Text>
+ );
+ },
+
+ _renderAndroid() {
+ return (
+ <View>
+ <UIExplorerButton onPress={this._run.bind(this, false)}>
+ Run requestIdleCallback
+ </UIExplorerButton>
+
+ <UIExplorerButton onPress={this._run.bind(this, true)}>
+ Burn CPU inside of requestIdleCallback
+ </UIExplorerButton>
+
+ <UIExplorerButton onPress={this._runBackground}>
+ Run background task
+ </UIExplorerButton>
+
+ <UIExplorerButton onPress={this._stopBackground}>
+ Stop background task
+ </UIExplorerButton>
+
+ <Text>{this.state.message}</Text>
+ </View>
+ );
+ },
+
+ _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],
@@ -142,6 +233,17 @@ exports.examples = [
},
},
{
+ title: 'this.requestIdleCallback(fn)',
+ description: 'Execute function fn on the next JS frame that has idle time',
+ render: function() {
+ return (
+ <View>
+ <RequestIdleCallbackTester />
+ </View>
+ );
+ },
+ },
+ {
title: 'this.setImmediate(fn)',
description: 'Execute function fn at the end of the current JS event loop.',
render: function() {
@@ -144,6 +144,8 @@ function setUpTimers(): void {
defineLazyTimer('clearImmediate');
defineLazyTimer('requestAnimationFrame');
defineLazyTimer('cancelAnimationFrame');
+ defineLazyTimer('requestIdleCallback');
+ defineLazyTimer('cancelIdleCallback');
}
function setUpAlert(): void {
@@ -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);
}
}
@@ -31,20 +31,22 @@ const JSTimersExecution = {
setInterval: null,
requestAnimationFrame: null,
setImmediate: null,
+ requestIdleCallback: null,
}),
// Parallel arrays:
callbacks: [],
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).
@@ -32,7 +32,7 @@
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.
@@ -80,8 +80,8 @@
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.
*
@@ -22,4 +22,9 @@ public static long currentTimeMillis() {
public static long nanoTime() {
return System.nanoTime();
}
+
+ public static long uptimeMillis() {
+ return android.os.SystemClock.uptimeMillis();
+ }
+
}
@@ -15,8 +15,7 @@
@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);
}
Oops, something went wrong.

1 comment on commit 18394fb

@mkonicek
Contributor

Whoa, awesome!

Please sign in to comment.