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 https://github.com/facebook/react-native/pull/8569 Differential Revision: D3558869 Pulled By: astreet fbshipit-source-id: 61fa82eb26001d2b8c2ea69c35bf3eb5ce5454ba
This commit is contained in:
parent
22eabe59a2
commit
18394fb179
|
@ -21,6 +21,7 @@
|
||||||
"__fbBatchedBridgeConfig": false,
|
"__fbBatchedBridgeConfig": false,
|
||||||
"alert": false,
|
"alert": false,
|
||||||
"cancelAnimationFrame": false,
|
"cancelAnimationFrame": false,
|
||||||
|
"cancelIdleCallback": false,
|
||||||
"clearImmediate": true,
|
"clearImmediate": true,
|
||||||
"clearInterval": false,
|
"clearInterval": false,
|
||||||
"clearTimeout": false,
|
"clearTimeout": false,
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
"process": false,
|
"process": false,
|
||||||
"Promise": true,
|
"Promise": true,
|
||||||
"requestAnimationFrame": true,
|
"requestAnimationFrame": true,
|
||||||
|
"requestIdleCallback": true,
|
||||||
"require": false,
|
"require": false,
|
||||||
"Set": true,
|
"Set": true,
|
||||||
"setImmediate": true,
|
"setImmediate": true,
|
||||||
|
|
|
@ -28,10 +28,101 @@ var {
|
||||||
AlertIOS,
|
AlertIOS,
|
||||||
Platform,
|
Platform,
|
||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
|
Text,
|
||||||
View,
|
View,
|
||||||
} = ReactNative;
|
} = ReactNative;
|
||||||
var TimerMixin = require('react-timer-mixin');
|
var TimerMixin = require('react-timer-mixin');
|
||||||
var UIExplorerButton = require('./UIExplorerButton');
|
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({
|
var TimerTester = React.createClass({
|
||||||
mixins: [TimerMixin],
|
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 (
|
||||||
|
<View>
|
||||||
|
<RequestIdleCallbackTester />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'this.setImmediate(fn)',
|
title: 'this.setImmediate(fn)',
|
||||||
description: 'Execute function fn at the end of the current JS event loop.',
|
description: 'Execute function fn at the end of the current JS event loop.',
|
||||||
|
|
|
@ -144,6 +144,8 @@ function setUpTimers(): void {
|
||||||
defineLazyTimer('clearImmediate');
|
defineLazyTimer('clearImmediate');
|
||||||
defineLazyTimer('requestAnimationFrame');
|
defineLazyTimer('requestAnimationFrame');
|
||||||
defineLazyTimer('cancelAnimationFrame');
|
defineLazyTimer('cancelAnimationFrame');
|
||||||
|
defineLazyTimer('requestIdleCallback');
|
||||||
|
defineLazyTimer('cancelIdleCallback');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpAlert(): void {
|
function setUpAlert(): void {
|
||||||
|
|
|
@ -96,6 +96,41 @@ var JSTimers = {
|
||||||
return newID;
|
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) {
|
clearTimeout: function(timerID) {
|
||||||
JSTimers._clearTimerID(timerID);
|
JSTimers._clearTimerID(timerID);
|
||||||
},
|
},
|
||||||
|
@ -127,7 +162,9 @@ var JSTimers = {
|
||||||
// See corresponding comment in `callTimers` for reasoning behind this
|
// See corresponding comment in `callTimers` for reasoning behind this
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
JSTimersExecution._clearIndex(index);
|
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);
|
RCTTiming.deleteTimer(timerID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ const JSTimersExecution = {
|
||||||
setInterval: null,
|
setInterval: null,
|
||||||
requestAnimationFrame: null,
|
requestAnimationFrame: null,
|
||||||
setImmediate: null,
|
setImmediate: null,
|
||||||
|
requestIdleCallback: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Parallel arrays:
|
// Parallel arrays:
|
||||||
|
@ -38,13 +39,14 @@ const JSTimersExecution = {
|
||||||
types: [],
|
types: [],
|
||||||
timerIDs: [],
|
timerIDs: [],
|
||||||
immediates: [],
|
immediates: [],
|
||||||
|
requestIdleCallbacks: [],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the callback associated with the ID. Also unregister that callback
|
* 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
|
* if it was a one time timer (setTimeout), and not unregister it if it was
|
||||||
* recurring (setInterval).
|
* recurring (setInterval).
|
||||||
*/
|
*/
|
||||||
callTimer(timerID) {
|
callTimer(timerID, frameTime) {
|
||||||
warning(
|
warning(
|
||||||
timerID <= JSTimersExecution.GUID,
|
timerID <= JSTimersExecution.GUID,
|
||||||
'Tried to call timer with ID %s but no such timer exists.',
|
'Tried to call timer with ID %s but no such timer exists.',
|
||||||
|
@ -65,7 +67,8 @@ const JSTimersExecution = {
|
||||||
// Clear the metadata
|
// Clear the metadata
|
||||||
if (type === JSTimersExecution.Type.setTimeout ||
|
if (type === JSTimersExecution.Type.setTimeout ||
|
||||||
type === JSTimersExecution.Type.setImmediate ||
|
type === JSTimersExecution.Type.setImmediate ||
|
||||||
type === JSTimersExecution.Type.requestAnimationFrame) {
|
type === JSTimersExecution.Type.requestAnimationFrame ||
|
||||||
|
type === JSTimersExecution.Type.requestIdleCallback) {
|
||||||
JSTimersExecution._clearIndex(timerIndex);
|
JSTimersExecution._clearIndex(timerIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +80,16 @@ const JSTimersExecution = {
|
||||||
} else if (type === JSTimersExecution.Type.requestAnimationFrame) {
|
} else if (type === JSTimersExecution.Type.requestAnimationFrame) {
|
||||||
const currentTime = performanceNow();
|
const currentTime = performanceNow();
|
||||||
callback(currentTime);
|
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 {
|
} else {
|
||||||
console.error('Tried to call a callback with invalid type: ' + type);
|
console.error('Tried to call a callback with invalid type: ' + type);
|
||||||
return;
|
return;
|
||||||
|
@ -99,7 +112,7 @@ const JSTimersExecution = {
|
||||||
);
|
);
|
||||||
|
|
||||||
JSTimersExecution.errors = null;
|
JSTimersExecution.errors = null;
|
||||||
timerIDs.forEach(JSTimersExecution.callTimer);
|
timerIDs.forEach((id) => { JSTimersExecution.callTimer(id); });
|
||||||
|
|
||||||
const errors = JSTimersExecution.errors;
|
const errors = JSTimersExecution.errors;
|
||||||
if (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
|
* Performs a single pass over the enqueued immediates. Returns whether
|
||||||
* more immediates are queued up (can be used as a condition a while loop).
|
* more immediates are queued up (can be used as a condition a while loop).
|
||||||
|
|
|
@ -32,7 +32,7 @@ import com.facebook.react.uimanager.UIImplementationProvider;
|
||||||
import com.facebook.react.uimanager.ViewManager;
|
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
|
* 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
|
* instance. It also sets up connection between the instance and developers support functionality
|
||||||
* of the framework.
|
* of the framework.
|
||||||
|
|
|
@ -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;
|
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
|
* This class manages instances of {@link CatalystInstance}. It exposes a way to configure
|
||||||
* catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that
|
* 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
|
* instance. It also sets up connection between the instance and developers support functionality
|
||||||
* of the framework.
|
* of the framework.
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,4 +22,9 @@ public class SystemClock {
|
||||||
public static long nanoTime() {
|
public static long nanoTime() {
|
||||||
return System.nanoTime();
|
return System.nanoTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long uptimeMillis() {
|
||||||
|
return android.os.SystemClock.uptimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,7 @@ import com.facebook.react.bridge.WritableArray;
|
||||||
|
|
||||||
@SupportsWebWorkers
|
@SupportsWebWorkers
|
||||||
public interface JSTimersExecution extends JavaScriptModule {
|
public interface JSTimersExecution extends JavaScriptModule {
|
||||||
|
void callTimers(WritableArray timerIDs);
|
||||||
public void callTimers(WritableArray timerIDs);
|
void callIdleCallbacks(double frameTime);
|
||||||
|
void emitTimeDriftWarning(String warningMessage);
|
||||||
public void emitTimeDriftWarning(String warningMessage);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,6 @@
|
||||||
|
|
||||||
package com.facebook.react.modules.core;
|
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.util.SparseArray;
|
||||||
import android.view.Choreographer;
|
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.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
import com.facebook.react.bridge.ReactMethod;
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.UiThreadUtil;
|
||||||
import com.facebook.react.bridge.WritableArray;
|
import com.facebook.react.bridge.WritableArray;
|
||||||
|
import com.facebook.react.common.MapBuilder;
|
||||||
import com.facebook.react.common.SystemClock;
|
import com.facebook.react.common.SystemClock;
|
||||||
import com.facebook.react.devsupport.DevSupportManager;
|
import com.facebook.react.devsupport.DevSupportManager;
|
||||||
import com.facebook.react.uimanager.ReactChoreographer;
|
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.
|
* Native module for JS timer execution. Timers fire on frame boundaries.
|
||||||
*/
|
*/
|
||||||
public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener,
|
public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener,
|
||||||
OnExecutorUnregisteredListener {
|
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 final DevSupportManager mDevSupportManager;
|
||||||
|
|
||||||
private static class Timer {
|
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
|
// Temporary map for constructing the individual arrays of timers per ExecutorToken
|
||||||
private final HashMap<ExecutorToken, WritableArray> mTimersToCall = new HashMap<>();
|
private final HashMap<ExecutorToken, WritableArray> 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 mTimerGuard = new Object();
|
||||||
|
private final Object mIdleCallbackGuard = new Object();
|
||||||
private final PriorityQueue<Timer> mTimers;
|
private final PriorityQueue<Timer> mTimers;
|
||||||
private final HashMap<ExecutorToken, SparseArray<Timer>> mTimerIdsToTimers;
|
private final Map<ExecutorToken, SparseArray<Timer>> mTimerIdsToTimers;
|
||||||
private final AtomicBoolean isPaused = new AtomicBoolean(true);
|
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 @Nullable ReactChoreographer mReactChoreographer;
|
||||||
private boolean mFrameCallbackPosted = false;
|
private boolean mFrameCallbackPosted = false;
|
||||||
|
private boolean mFrameIdleCallbackPosted = false;
|
||||||
|
private final Set<ExecutorToken> mSendIdleEventsExecutorTokens;
|
||||||
|
// Temporary array used to dipatch idle callbacks on the JS thread.
|
||||||
|
private final List<ExecutorToken> mIdleCallbackContextsToCall;
|
||||||
|
|
||||||
public Timing(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
|
public Timing(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
@ -135,6 +219,8 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mTimerIdsToTimers = new HashMap<>();
|
mTimerIdsToTimers = new HashMap<>();
|
||||||
|
mSendIdleEventsExecutorTokens = new HashSet<>();
|
||||||
|
mIdleCallbackContextsToCall = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -148,11 +234,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||||
public void onHostPause() {
|
public void onHostPause() {
|
||||||
isPaused.set(true);
|
isPaused.set(true);
|
||||||
clearChoreographerCallback();
|
clearChoreographerCallback();
|
||||||
|
clearChoreographerIdleCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onHostDestroy() {
|
public void onHostDestroy() {
|
||||||
clearChoreographerCallback();
|
clearChoreographerCallback();
|
||||||
|
clearChoreographerIdleCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
// TODO(5195192) Investigate possible problems related to restarting all tasks at the same
|
||||||
// moment
|
// moment
|
||||||
setChoreographerCallback();
|
setChoreographerCallback();
|
||||||
|
|
||||||
|
synchronized (mIdleCallbackGuard) {
|
||||||
|
if (mSendIdleEventsExecutorTokens.size() > 0) {
|
||||||
|
setChoreographerIdleCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCatalystInstanceDestroy() {
|
public void onCatalystInstanceDestroy() {
|
||||||
clearChoreographerCallback();
|
clearChoreographerCallback();
|
||||||
|
clearChoreographerIdleCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setChoreographerCallback() {
|
private void setChoreographerCallback() {
|
||||||
if (!mFrameCallbackPosted) {
|
if (!mFrameCallbackPosted) {
|
||||||
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
|
Assertions.assertNotNull(mReactChoreographer).postFrameCallback(
|
||||||
ReactChoreographer.CallbackType.TIMERS_EVENTS,
|
ReactChoreographer.CallbackType.TIMERS_EVENTS,
|
||||||
mFrameCallback);
|
mTimerFrameCallback);
|
||||||
mFrameCallbackPosted = true;
|
mFrameCallbackPosted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,14 +276,32 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||||
if (mFrameCallbackPosted) {
|
if (mFrameCallbackPosted) {
|
||||||
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
|
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
|
||||||
ReactChoreographer.CallbackType.TIMERS_EVENTS,
|
ReactChoreographer.CallbackType.TIMERS_EVENTS,
|
||||||
mFrameCallback);
|
mTimerFrameCallback);
|
||||||
mFrameCallbackPosted = false;
|
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
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "RKTiming";
|
return "RCTTiming";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -196,6 +309,13 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getConstants() {
|
||||||
|
return MapBuilder.<String, Object>of(
|
||||||
|
"frameDuration", FRAME_DURATION_MS,
|
||||||
|
"idleCallbackFrameDeadline", IDLE_CALLBACK_FRAME_DEADLINE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExecutorDestroyed(ExecutorToken executorToken) {
|
public void onExecutorDestroyed(ExecutorToken executorToken) {
|
||||||
synchronized (mTimerGuard) {
|
synchronized (mTimerGuard) {
|
||||||
|
@ -208,6 +328,10 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||||
mTimers.remove(timer);
|
mTimers.remove(timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized (mIdleCallbackGuard) {
|
||||||
|
mSendIdleEventsExecutorTokens.remove(executorToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
|
@ -272,4 +396,28 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||||
mTimers.remove(timer);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,12 @@ public class ReactChoreographer {
|
||||||
* Events that make JS do things.
|
* Events that make JS do things.
|
||||||
*/
|
*/
|
||||||
TIMERS_EVENTS(3),
|
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;
|
private final int mOrder;
|
||||||
|
|
|
@ -52,8 +52,9 @@ public class TimingModuleTest {
|
||||||
private static final long FRAME_TIME_NS = 17 * 1000 * 1000; // 17 ms
|
private static final long FRAME_TIME_NS = 17 * 1000 * 1000; // 17 ms
|
||||||
|
|
||||||
private Timing mTiming;
|
private Timing mTiming;
|
||||||
private ReactChoreographer mChoreographerMock;
|
private ReactChoreographer mReactChoreographerMock;
|
||||||
private PostFrameCallbackHandler mPostFrameCallbackHandler;
|
private PostFrameCallbackHandler mPostFrameCallbackHandler;
|
||||||
|
private PostFrameIdleCallbackHandler mIdlePostFrameCallbackHandler;
|
||||||
private long mCurrentTimeNs;
|
private long mCurrentTimeNs;
|
||||||
private JSTimersExecution mJSTimersMock;
|
private JSTimersExecution mJSTimersMock;
|
||||||
private ExecutorToken mExecutorTokenMock;
|
private ExecutorToken mExecutorTokenMock;
|
||||||
|
@ -73,12 +74,13 @@ public class TimingModuleTest {
|
||||||
});
|
});
|
||||||
|
|
||||||
PowerMockito.mockStatic(SystemClock.class);
|
PowerMockito.mockStatic(SystemClock.class);
|
||||||
|
when(SystemClock.uptimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
|
||||||
when(SystemClock.currentTimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
|
when(SystemClock.currentTimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
|
||||||
when(SystemClock.nanoTime()).thenReturn(mCurrentTimeNs);
|
when(SystemClock.nanoTime()).thenReturn(mCurrentTimeNs);
|
||||||
|
|
||||||
mChoreographerMock = mock(ReactChoreographer.class);
|
mReactChoreographerMock = mock(ReactChoreographer.class);
|
||||||
PowerMockito.mockStatic(ReactChoreographer.class);
|
PowerMockito.mockStatic(ReactChoreographer.class);
|
||||||
when(ReactChoreographer.getInstance()).thenReturn(mChoreographerMock);
|
when(ReactChoreographer.getInstance()).thenReturn(mReactChoreographerMock);
|
||||||
|
|
||||||
CatalystInstance reactInstance = mock(CatalystInstance.class);
|
CatalystInstance reactInstance = mock(CatalystInstance.class);
|
||||||
ReactApplicationContext reactContext = mock(ReactApplicationContext.class);
|
ReactApplicationContext reactContext = mock(ReactApplicationContext.class);
|
||||||
|
@ -86,26 +88,49 @@ public class TimingModuleTest {
|
||||||
|
|
||||||
mCurrentTimeNs = 0;
|
mCurrentTimeNs = 0;
|
||||||
mPostFrameCallbackHandler = new PostFrameCallbackHandler();
|
mPostFrameCallbackHandler = new PostFrameCallbackHandler();
|
||||||
|
mIdlePostFrameCallbackHandler = new PostFrameIdleCallbackHandler();
|
||||||
|
|
||||||
doAnswer(mPostFrameCallbackHandler)
|
doAnswer(mPostFrameCallbackHandler)
|
||||||
.when(mChoreographerMock)
|
.when(mReactChoreographerMock)
|
||||||
.postFrameCallback(
|
.postFrameCallback(
|
||||||
eq(ReactChoreographer.CallbackType.TIMERS_EVENTS),
|
eq(ReactChoreographer.CallbackType.TIMERS_EVENTS),
|
||||||
any(Choreographer.FrameCallback.class));
|
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));
|
mTiming = new Timing(reactContext, mock(DevSupportManager.class));
|
||||||
mJSTimersMock = mock(JSTimersExecution.class);
|
mJSTimersMock = mock(JSTimersExecution.class);
|
||||||
mExecutorTokenMock = mock(ExecutorToken.class);
|
mExecutorTokenMock = mock(ExecutorToken.class);
|
||||||
when(reactContext.getJSModule(mExecutorTokenMock, JSTimersExecution.class)).thenReturn(mJSTimersMock);
|
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();
|
mTiming.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stepChoreographerFrame() {
|
private void stepChoreographerFrame() {
|
||||||
Choreographer.FrameCallback callback = mPostFrameCallbackHandler.getAndResetFrameCallback();
|
Choreographer.FrameCallback callback = mPostFrameCallbackHandler.getAndResetFrameCallback();
|
||||||
|
Choreographer.FrameCallback idleCallback = mIdlePostFrameCallbackHandler.getAndResetFrameCallback();
|
||||||
|
|
||||||
mCurrentTimeNs += FRAME_TIME_NS;
|
mCurrentTimeNs += FRAME_TIME_NS;
|
||||||
|
when(SystemClock.uptimeMillis()).thenReturn(mCurrentTimeNs / 1000000);
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.doFrame(mCurrentTimeNs);
|
callback.doFrame(mCurrentTimeNs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (idleCallback != null) {
|
||||||
|
idleCallback.doFrame(mCurrentTimeNs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -170,6 +195,33 @@ public class TimingModuleTest {
|
||||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(100));
|
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<Void> {
|
||||||
|
|
||||||
|
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<Void> {
|
private static class PostFrameCallbackHandler implements Answer<Void> {
|
||||||
|
|
||||||
private Choreographer.FrameCallback mFrameCallback;
|
private Choreographer.FrameCallback mFrameCallback;
|
||||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue