Merge JSTimers and JSTimersExecution
Reviewed By: mmmulani Differential Revision: D5292923 fbshipit-source-id: d8331cbac28ba3bbf47c9746238a755b707903ea
This commit is contained in:
parent
e9f657f2bd
commit
0ae11f12f3
|
@ -15,7 +15,6 @@
|
|||
'use strict';
|
||||
|
||||
const ErrorUtils = require('ErrorUtils');
|
||||
const JSTimersExecution = require('JSTimersExecution');
|
||||
const Systrace = require('Systrace');
|
||||
|
||||
const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
|
||||
|
@ -41,6 +40,9 @@ const TRACE_TAG_REACT_APPS = 1 << 17;
|
|||
|
||||
const DEBUG_INFO_LIMIT = 32;
|
||||
|
||||
// Work around an initialization order issue
|
||||
let JSTimers = null;
|
||||
|
||||
class MessageQueue {
|
||||
_lazyCallableModules: {[key: string]: void => Object};
|
||||
_queue: [Array<number>, Array<number>, Array<any>, number];
|
||||
|
@ -235,8 +237,11 @@ class MessageQueue {
|
|||
}
|
||||
|
||||
__callImmediates() {
|
||||
Systrace.beginEvent('JSTimersExecution.callImmediates()');
|
||||
JSTimersExecution.callImmediates();
|
||||
Systrace.beginEvent('JSTimers.callImmediates()');
|
||||
if (!JSTimers) {
|
||||
JSTimers = require('JSTimers');
|
||||
}
|
||||
JSTimers.callImmediates();
|
||||
Systrace.endEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ ExceptionsManager.installConsoleErrorReporter();
|
|||
// TODO: Move these around to solve the cycle in a cleaner way
|
||||
const BatchedBridge = require('BatchedBridge');
|
||||
BatchedBridge.registerLazyCallableModule('Systrace', () => require('Systrace'));
|
||||
BatchedBridge.registerLazyCallableModule('JSTimersExecution', () => require('JSTimersExecution'));
|
||||
BatchedBridge.registerLazyCallableModule('JSTimersExecution', () => require('JSTimers'));
|
||||
BatchedBridge.registerLazyCallableModule('HeapCapture', () => require('HeapCapture'));
|
||||
BatchedBridge.registerLazyCallableModule('SamplingProfiler', () => require('SamplingProfiler'));
|
||||
BatchedBridge.registerLazyCallableModule('RCTLog', () => require('RCTLog'));
|
||||
|
|
|
@ -7,66 +7,39 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule JSTimers
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// Note that the module JSTimers is split into two in order to solve a cycle
|
||||
// in dependencies. NativeModules > BatchedBridge > MessageQueue > JSTimersExecution
|
||||
const JSTimersExecution = require('JSTimersExecution');
|
||||
const Platform = require('Platform');
|
||||
const Systrace = require('Systrace');
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
const performanceNow = require('fbjs/lib/performanceNow');
|
||||
const warning = require('fbjs/lib/warning');
|
||||
|
||||
const {Timing} = require('NativeModules');
|
||||
|
||||
import type {JSTimerType} from 'JSTimersExecution';
|
||||
import type {ExtendedError} from 'parseErrorStack';
|
||||
|
||||
// Returns a free index if one is available, and the next consecutive index otherwise.
|
||||
function _getFreeIndex(): number {
|
||||
let freeIndex = JSTimersExecution.timerIDs.indexOf(null);
|
||||
if (freeIndex === -1) {
|
||||
freeIndex = JSTimersExecution.timerIDs.length;
|
||||
}
|
||||
return freeIndex;
|
||||
}
|
||||
/**
|
||||
* JS implementation of timer functions. Must be completely driven by an
|
||||
* external clock signal, all that's stored here is timerID, timer type, and
|
||||
* callback.
|
||||
*/
|
||||
|
||||
function _allocateCallback(func: Function, type: JSTimerType): number {
|
||||
const id = JSTimersExecution.GUID++;
|
||||
const freeIndex = _getFreeIndex();
|
||||
JSTimersExecution.timerIDs[freeIndex] = id;
|
||||
JSTimersExecution.callbacks[freeIndex] = func;
|
||||
JSTimersExecution.types[freeIndex] = type;
|
||||
if (__DEV__) {
|
||||
const parseErrorStack = require('parseErrorStack');
|
||||
const error : ExtendedError = new Error();
|
||||
error.framesToPop = 1;
|
||||
const stack = parseErrorStack(error);
|
||||
if (stack) {
|
||||
JSTimersExecution.identifiers[freeIndex] = stack.shift();
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
export type JSTimerType =
|
||||
| 'setTimeout'
|
||||
| 'setInterval'
|
||||
| 'requestAnimationFrame'
|
||||
| 'setImmediate'
|
||||
| 'requestIdleCallback';
|
||||
|
||||
function _freeCallback(timerID: number) {
|
||||
// JSTimersExecution.timerIDs contains nulls after timers have been removed;
|
||||
// ignore nulls upfront so indexOf doesn't find them
|
||||
if (timerID == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = JSTimersExecution.timerIDs.indexOf(timerID);
|
||||
// See corresponding comment in `callTimers` for reasoning behind this
|
||||
if (index !== -1) {
|
||||
JSTimersExecution._clearIndex(index);
|
||||
const type = JSTimersExecution.types[index];
|
||||
if (type !== 'setImmediate' && type !== 'requestIdleCallback') {
|
||||
Timing.deleteTimer(timerID);
|
||||
}
|
||||
}
|
||||
}
|
||||
// These timing contants should be kept in sync with the ones in native ios and
|
||||
// android `RCTTiming` module.
|
||||
const FRAME_DURATION = 1000 / 60;
|
||||
const IDLE_CALLBACK_FRAME_DEADLINE = 1;
|
||||
|
||||
const MAX_TIMER_DURATION_MS = 60 * 1000;
|
||||
const IS_ANDROID = Platform.OS === 'android';
|
||||
|
@ -76,6 +49,152 @@ const ANDROID_LONG_TIMER_MESSAGE =
|
|||
'module awake, and timers can only be called when the app is in the foreground. ' +
|
||||
'See https://github.com/facebook/react-native/issues/12981 for more info.';
|
||||
|
||||
// Parallel arrays
|
||||
const callbacks: Array<?Function> = [];
|
||||
const types: Array<?JSTimerType> = [];
|
||||
const timerIDs: Array<?number> = [];
|
||||
let immediates: Array<number> = [];
|
||||
let requestIdleCallbacks: Array<number> = [];
|
||||
const requestIdleCallbackTimeouts: {[number]: number} = {};
|
||||
const identifiers: Array<null | {methodName: string}> = [];
|
||||
|
||||
let GUID = 1;
|
||||
let errors: ?Array<Error> = null;
|
||||
|
||||
let hasEmittedTimeDriftWarning = false;
|
||||
|
||||
// Returns a free index if one is available, and the next consecutive index otherwise.
|
||||
function _getFreeIndex(): number {
|
||||
let freeIndex = timerIDs.indexOf(null);
|
||||
if (freeIndex === -1) {
|
||||
freeIndex = timerIDs.length;
|
||||
}
|
||||
return freeIndex;
|
||||
}
|
||||
|
||||
function _allocateCallback(func: Function, type: JSTimerType): number {
|
||||
const id = GUID++;
|
||||
const freeIndex = _getFreeIndex();
|
||||
timerIDs[freeIndex] = id;
|
||||
callbacks[freeIndex] = func;
|
||||
types[freeIndex] = type;
|
||||
if (__DEV__) {
|
||||
const parseErrorStack = require('parseErrorStack');
|
||||
const error: ExtendedError = new Error();
|
||||
error.framesToPop = 1;
|
||||
const stack = parseErrorStack(error);
|
||||
if (stack) {
|
||||
identifiers[freeIndex] = stack.shift();
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
function _callTimer(timerID: number, frameTime: number, didTimeout: ?boolean) {
|
||||
warning(
|
||||
timerID <= GUID,
|
||||
'Tried to call timer with ID %s but no such timer exists.',
|
||||
timerID,
|
||||
);
|
||||
|
||||
// timerIndex of -1 means that no timer with that ID exists. There are
|
||||
// two situations when this happens, when a garbage timer ID was given
|
||||
// and when a previously existing timer was deleted before this callback
|
||||
// fired. In both cases we want to ignore the timer id, but in the former
|
||||
// case we warn as well.
|
||||
const timerIndex = timerIDs.indexOf(timerID);
|
||||
if (timerIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = types[timerIndex];
|
||||
const callback = callbacks[timerIndex];
|
||||
if (!callback || !type) {
|
||||
console.error('No callback found for timerID ' + timerID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const identifier = identifiers[timerIndex] || {};
|
||||
Systrace.beginEvent('Systrace.callTimer: ' + identifier.methodName);
|
||||
}
|
||||
|
||||
// Clear the metadata
|
||||
if (
|
||||
type === 'setTimeout' ||
|
||||
type === 'setImmediate' ||
|
||||
type === 'requestAnimationFrame' ||
|
||||
type === 'requestIdleCallback'
|
||||
) {
|
||||
_clearIndex(timerIndex);
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
type === 'setTimeout' ||
|
||||
type === 'setInterval' ||
|
||||
type === 'setImmediate'
|
||||
) {
|
||||
callback();
|
||||
} else if (type === 'requestAnimationFrame') {
|
||||
callback(performanceNow());
|
||||
} else if (type === 'requestIdleCallback') {
|
||||
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, FRAME_DURATION - (performanceNow() - frameTime));
|
||||
},
|
||||
didTimeout: !!didTimeout,
|
||||
});
|
||||
} else {
|
||||
console.error('Tried to call a callback with invalid type: ' + type);
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't rethrow so that we can run all timers.
|
||||
if (!errors) {
|
||||
errors = [e];
|
||||
} else {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
Systrace.endEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function _clearIndex(i: number) {
|
||||
timerIDs[i] = null;
|
||||
callbacks[i] = null;
|
||||
types[i] = null;
|
||||
identifiers[i] = null;
|
||||
}
|
||||
|
||||
function _freeCallback(timerID: number) {
|
||||
// timerIDs contains nulls after timers have been removed;
|
||||
// ignore nulls upfront so indexOf doesn't find them
|
||||
if (timerID == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = timerIDs.indexOf(timerID);
|
||||
// See corresponding comment in `callTimers` for reasoning behind this
|
||||
if (index !== -1) {
|
||||
_clearIndex(index);
|
||||
const type = types[index];
|
||||
if (type !== 'setImmediate' && type !== 'requestIdleCallback') {
|
||||
Timing.deleteTimer(timerID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JS implementation of timer functions. Must be completely driven by an
|
||||
* external clock signal, all that's stored here is timerID, timer type, and
|
||||
|
@ -86,13 +205,24 @@ const JSTimers = {
|
|||
* @param {function} func Callback to be invoked after `duration` ms.
|
||||
* @param {number} duration Number of milliseconds.
|
||||
*/
|
||||
setTimeout: function(func: Function, duration: number, ...args?: any): number {
|
||||
setTimeout: function(
|
||||
func: Function,
|
||||
duration: number,
|
||||
...args?: any
|
||||
): number {
|
||||
if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) {
|
||||
console.warn(
|
||||
ANDROID_LONG_TIMER_MESSAGE + '\n' + '(Saw setTimeout with duration ' +
|
||||
duration + 'ms)');
|
||||
ANDROID_LONG_TIMER_MESSAGE +
|
||||
'\n' +
|
||||
'(Saw setTimeout with duration ' +
|
||||
duration +
|
||||
'ms)',
|
||||
);
|
||||
}
|
||||
const id = _allocateCallback(() => func.apply(undefined, args), 'setTimeout');
|
||||
const id = _allocateCallback(
|
||||
() => func.apply(undefined, args),
|
||||
'setTimeout',
|
||||
);
|
||||
Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false);
|
||||
return id;
|
||||
},
|
||||
|
@ -101,13 +231,24 @@ const JSTimers = {
|
|||
* @param {function} func Callback to be invoked every `duration` ms.
|
||||
* @param {number} duration Number of milliseconds.
|
||||
*/
|
||||
setInterval: function(func: Function, duration: number, ...args?: any): number {
|
||||
setInterval: function(
|
||||
func: Function,
|
||||
duration: number,
|
||||
...args?: any
|
||||
): number {
|
||||
if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) {
|
||||
console.warn(
|
||||
ANDROID_LONG_TIMER_MESSAGE + '\n' + '(Saw setInterval with duration ' +
|
||||
duration + 'ms)');
|
||||
ANDROID_LONG_TIMER_MESSAGE +
|
||||
'\n' +
|
||||
'(Saw setInterval with duration ' +
|
||||
duration +
|
||||
'ms)',
|
||||
);
|
||||
}
|
||||
const id = _allocateCallback(() => func.apply(undefined, args), 'setInterval');
|
||||
const id = _allocateCallback(
|
||||
() => func.apply(undefined, args),
|
||||
'setInterval',
|
||||
);
|
||||
Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ true);
|
||||
return id;
|
||||
},
|
||||
|
@ -117,15 +258,18 @@ const JSTimers = {
|
|||
* current JavaScript execution loop.
|
||||
*/
|
||||
setImmediate: function(func: Function, ...args?: any) {
|
||||
const id = _allocateCallback(() => func.apply(undefined, args), 'setImmediate');
|
||||
JSTimersExecution.immediates.push(id);
|
||||
const id = _allocateCallback(
|
||||
() => func.apply(undefined, args),
|
||||
'setImmediate',
|
||||
);
|
||||
immediates.push(id);
|
||||
return id;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {function} func Callback to be invoked every frame.
|
||||
*/
|
||||
requestAnimationFrame: function(func : Function) {
|
||||
requestAnimationFrame: function(func: Function) {
|
||||
const id = _allocateCallback(func, 'requestAnimationFrame');
|
||||
Timing.createTimer(id, 1, Date.now(), /* recurring */ false);
|
||||
return id;
|
||||
|
@ -136,58 +280,58 @@ const JSTimers = {
|
|||
* with time remaining in frame.
|
||||
* @param {?object} options
|
||||
*/
|
||||
requestIdleCallback: function(func : Function, options : ?Object) {
|
||||
if (JSTimersExecution.requestIdleCallbacks.length === 0) {
|
||||
requestIdleCallback: function(func: Function, options: ?Object) {
|
||||
if (requestIdleCallbacks.length === 0) {
|
||||
Timing.setSendIdleEvents(true);
|
||||
}
|
||||
|
||||
const timeout = options && options.timeout;
|
||||
const id = _allocateCallback(
|
||||
timeout != null ?
|
||||
deadline => {
|
||||
const timeoutId = JSTimersExecution.requestIdleCallbackTimeouts[id];
|
||||
if (timeoutId) {
|
||||
JSTimers.clearTimeout(timeoutId);
|
||||
JSTimersExecution.requestIdleCallbackTimeouts[id];
|
||||
timeout != null
|
||||
? deadline => {
|
||||
const timeoutId = requestIdleCallbackTimeouts[id];
|
||||
if (timeoutId) {
|
||||
JSTimers.clearTimeout(timeoutId);
|
||||
requestIdleCallbackTimeouts[id];
|
||||
}
|
||||
return func(deadline);
|
||||
}
|
||||
return func(deadline);
|
||||
} :
|
||||
func,
|
||||
'requestIdleCallback'
|
||||
: func,
|
||||
'requestIdleCallback',
|
||||
);
|
||||
JSTimersExecution.requestIdleCallbacks.push(id);
|
||||
requestIdleCallbacks.push(id);
|
||||
|
||||
if (timeout != null) {
|
||||
const timeoutId = JSTimers.setTimeout(() => {
|
||||
const index = JSTimersExecution.requestIdleCallbacks.indexOf(id);
|
||||
const index = requestIdleCallbacks.indexOf(id);
|
||||
if (index > -1) {
|
||||
JSTimersExecution.requestIdleCallbacks.splice(index, 1);
|
||||
JSTimersExecution.callTimer(id, performanceNow(), true);
|
||||
requestIdleCallbacks.splice(index, 1);
|
||||
_callTimer(id, performanceNow(), true);
|
||||
}
|
||||
delete JSTimersExecution.requestIdleCallbackTimeouts[id];
|
||||
if (JSTimersExecution.requestIdleCallbacks.length === 0) {
|
||||
delete requestIdleCallbackTimeouts[id];
|
||||
if (requestIdleCallbacks.length === 0) {
|
||||
Timing.setSendIdleEvents(false);
|
||||
}
|
||||
}, timeout);
|
||||
JSTimersExecution.requestIdleCallbackTimeouts[id] = timeoutId;
|
||||
requestIdleCallbackTimeouts[id] = timeoutId;
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
cancelIdleCallback: function(timerID: number) {
|
||||
_freeCallback(timerID);
|
||||
const index = JSTimersExecution.requestIdleCallbacks.indexOf(timerID);
|
||||
const index = requestIdleCallbacks.indexOf(timerID);
|
||||
if (index !== -1) {
|
||||
JSTimersExecution.requestIdleCallbacks.splice(index, 1);
|
||||
requestIdleCallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
const timeoutId = JSTimersExecution.requestIdleCallbackTimeouts[timerID];
|
||||
const timeoutId = requestIdleCallbackTimeouts[timerID];
|
||||
if (timeoutId) {
|
||||
JSTimers.clearTimeout(timeoutId);
|
||||
delete JSTimersExecution.requestIdleCallbackTimeouts[timerID];
|
||||
delete requestIdleCallbackTimeouts[timerID];
|
||||
}
|
||||
|
||||
if (JSTimersExecution.requestIdleCallbacks.length === 0) {
|
||||
if (requestIdleCallbacks.length === 0) {
|
||||
Timing.setSendIdleEvents(false);
|
||||
}
|
||||
},
|
||||
|
@ -202,15 +346,137 @@ const JSTimers = {
|
|||
|
||||
clearImmediate: function(timerID: number) {
|
||||
_freeCallback(timerID);
|
||||
const index = JSTimersExecution.immediates.indexOf(timerID);
|
||||
const index = immediates.indexOf(timerID);
|
||||
if (index !== -1) {
|
||||
JSTimersExecution.immediates.splice(index, 1);
|
||||
immediates.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
cancelAnimationFrame: function(timerID: number) {
|
||||
_freeCallback(timerID);
|
||||
},
|
||||
|
||||
/**
|
||||
* This is called from the native side. We are passed an array of timerIDs,
|
||||
* and
|
||||
*/
|
||||
callTimers: function(timersToCall: Array<number>) {
|
||||
invariant(
|
||||
timersToCall.length !== 0,
|
||||
'Cannot call `callTimers` with an empty list of IDs.',
|
||||
);
|
||||
|
||||
// $FlowFixMe: optionals do not allow assignment from null
|
||||
errors = null;
|
||||
for (let i = 0; i < timersToCall.length; i++) {
|
||||
_callTimer(timersToCall[i], 0);
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
const errorCount = errors.length;
|
||||
if (errorCount > 1) {
|
||||
// Throw all the other errors in a setTimeout, which will throw each
|
||||
// error one at a time
|
||||
for (let ii = 1; ii < errorCount; ii++) {
|
||||
JSTimers.setTimeout(
|
||||
(error => {
|
||||
throw error;
|
||||
}).bind(null, errors[ii]),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw errors[0];
|
||||
}
|
||||
},
|
||||
|
||||
callIdleCallbacks: function(frameTime: number) {
|
||||
if (
|
||||
FRAME_DURATION - (performanceNow() - frameTime) <
|
||||
IDLE_CALLBACK_FRAME_DEADLINE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// $FlowFixMe: optionals do not allow assignment from null
|
||||
errors = null;
|
||||
if (requestIdleCallbacks.length > 0) {
|
||||
const passIdleCallbacks = requestIdleCallbacks.slice();
|
||||
requestIdleCallbacks = [];
|
||||
|
||||
for (let i = 0; i < passIdleCallbacks.length; ++i) {
|
||||
_callTimer(passIdleCallbacks[i], frameTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestIdleCallbacks.length === 0) {
|
||||
Timing.setSendIdleEvents(false);
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
errors.forEach(error =>
|
||||
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).
|
||||
*/
|
||||
callImmediatesPass() {
|
||||
if (__DEV__) {
|
||||
Systrace.beginEvent('callImmediatesPass()');
|
||||
}
|
||||
|
||||
// The main reason to extract a single pass is so that we can track
|
||||
// in the system trace
|
||||
if (immediates.length > 0) {
|
||||
const passImmediates = immediates.slice();
|
||||
immediates = [];
|
||||
|
||||
// Use for loop rather than forEach as per @vjeux's advice
|
||||
// https://github.com/facebook/react-native/commit/c8fd9f7588ad02d2293cac7224715f4af7b0f352#commitcomment-14570051
|
||||
for (let i = 0; i < passImmediates.length; ++i) {
|
||||
_callTimer(passImmediates[i], 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
Systrace.endEvent();
|
||||
}
|
||||
return immediates.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is called after we execute any command we receive from native but
|
||||
* before we hand control back to native.
|
||||
*/
|
||||
callImmediates() {
|
||||
errors = null;
|
||||
while (JSTimers.callImmediatesPass()) {
|
||||
}
|
||||
if (errors) {
|
||||
errors.forEach(error =>
|
||||
JSTimers.setTimeout(() => {
|
||||
throw error;
|
||||
}, 0),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called from native (in development) when environment times are out-of-sync.
|
||||
*/
|
||||
emitTimeDriftWarning(warningMessage: string) {
|
||||
if (hasEmittedTimeDriftWarning) {
|
||||
return;
|
||||
}
|
||||
hasEmittedTimeDriftWarning = true;
|
||||
console.warn(warningMessage);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = JSTimers;
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015-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.
|
||||
*
|
||||
* @providesModule JSTimersExecution
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Systrace = require('Systrace');
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
const performanceNow = require('fbjs/lib/performanceNow');
|
||||
const warning = require('fbjs/lib/warning');
|
||||
|
||||
// These timing contants should be kept in sync with the ones in native ios and
|
||||
// android `RCTTiming` module.
|
||||
const FRAME_DURATION = 1000 / 60;
|
||||
const IDLE_CALLBACK_FRAME_DEADLINE = 1;
|
||||
|
||||
let hasEmittedTimeDriftWarning = false;
|
||||
|
||||
export type JSTimerType =
|
||||
'setTimeout' |
|
||||
'setInterval' |
|
||||
'requestAnimationFrame' |
|
||||
'setImmediate' |
|
||||
'requestIdleCallback';
|
||||
|
||||
/**
|
||||
* JS implementation of timer functions. Must be completely driven by an
|
||||
* external clock signal, all that's stored here is timerID, timer type, and
|
||||
* callback.
|
||||
*/
|
||||
const JSTimersExecution = {
|
||||
GUID: 1,
|
||||
|
||||
// Parallel arrays
|
||||
callbacks: ([] : Array<?Function>),
|
||||
types: ([] : Array<?JSTimerType>),
|
||||
timerIDs: ([] : Array<?number>),
|
||||
immediates: [],
|
||||
requestIdleCallbacks: [],
|
||||
requestIdleCallbackTimeouts: ({} : {[number]: number}),
|
||||
identifiers: ([] : Array<null | {methodName: string}>),
|
||||
|
||||
errors: (null : ?Array<Error>),
|
||||
|
||||
/**
|
||||
* 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: number, frameTime: number, didTimeout: ?boolean) {
|
||||
warning(
|
||||
timerID <= JSTimersExecution.GUID,
|
||||
'Tried to call timer with ID %s but no such timer exists.',
|
||||
timerID
|
||||
);
|
||||
|
||||
// timerIndex of -1 means that no timer with that ID exists. There are
|
||||
// two situations when this happens, when a garbage timer ID was given
|
||||
// and when a previously existing timer was deleted before this callback
|
||||
// fired. In both cases we want to ignore the timer id, but in the former
|
||||
// case we warn as well.
|
||||
const timerIndex = JSTimersExecution.timerIDs.indexOf(timerID);
|
||||
if (timerIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = JSTimersExecution.types[timerIndex];
|
||||
const callback = JSTimersExecution.callbacks[timerIndex];
|
||||
if (!callback || !type) {
|
||||
console.error('No callback found for timerID ' + timerID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const identifier = JSTimersExecution.identifiers[timerIndex] || {};
|
||||
Systrace.beginEvent('Systrace.callTimer: ' + identifier.methodName);
|
||||
}
|
||||
|
||||
// Clear the metadata
|
||||
if (type === 'setTimeout' || type === 'setImmediate' ||
|
||||
type === 'requestAnimationFrame' || type === 'requestIdleCallback') {
|
||||
JSTimersExecution._clearIndex(timerIndex);
|
||||
}
|
||||
|
||||
try {
|
||||
if (type === 'setTimeout' || type === 'setInterval' ||
|
||||
type === 'setImmediate') {
|
||||
callback();
|
||||
} else if (type === 'requestAnimationFrame') {
|
||||
callback(performanceNow());
|
||||
} else if (type === 'requestIdleCallback') {
|
||||
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, FRAME_DURATION - (performanceNow() - frameTime));
|
||||
},
|
||||
didTimeout: !!didTimeout,
|
||||
});
|
||||
} else {
|
||||
console.error('Tried to call a callback with invalid type: ' + type);
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't rethrow so that we can run all timers.
|
||||
if (!JSTimersExecution.errors) {
|
||||
JSTimersExecution.errors = [e];
|
||||
} else {
|
||||
JSTimersExecution.errors.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
Systrace.endEvent();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This is called from the native side. We are passed an array of timerIDs,
|
||||
* and
|
||||
*/
|
||||
callTimers(timerIDs: [number]) {
|
||||
invariant(
|
||||
timerIDs.length !== 0,
|
||||
'Cannot call `callTimers` with an empty list of IDs.'
|
||||
);
|
||||
|
||||
JSTimersExecution.errors = null;
|
||||
for (let i = 0; i < timerIDs.length; i++) {
|
||||
JSTimersExecution.callTimer(timerIDs[i], 0);
|
||||
}
|
||||
|
||||
const errors = JSTimersExecution.errors;
|
||||
if (errors) {
|
||||
const errorCount = errors.length;
|
||||
if (errorCount > 1) {
|
||||
// Throw all the other errors in a setTimeout, which will throw each
|
||||
// error one at a time
|
||||
for (let ii = 1; ii < errorCount; ii++) {
|
||||
require('JSTimers').setTimeout(
|
||||
((error) => { throw error; }).bind(null, errors[ii]),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
throw errors[0];
|
||||
}
|
||||
},
|
||||
|
||||
callIdleCallbacks: function(frameTime: number) {
|
||||
if (FRAME_DURATION - (performanceNow() - frameTime) < IDLE_CALLBACK_FRAME_DEADLINE) {
|
||||
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) {
|
||||
const { Timing } = require('NativeModules');
|
||||
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).
|
||||
*/
|
||||
callImmediatesPass() {
|
||||
if (__DEV__) {
|
||||
Systrace.beginEvent('JSTimersExecution.callImmediatesPass()');
|
||||
}
|
||||
|
||||
// The main reason to extract a single pass is so that we can track
|
||||
// in the system trace
|
||||
if (JSTimersExecution.immediates.length > 0) {
|
||||
const passImmediates = JSTimersExecution.immediates.slice();
|
||||
JSTimersExecution.immediates = [];
|
||||
|
||||
// Use for loop rather than forEach as per @vjeux's advice
|
||||
// https://github.com/facebook/react-native/commit/c8fd9f7588ad02d2293cac7224715f4af7b0f352#commitcomment-14570051
|
||||
for (let i = 0; i < passImmediates.length; ++i) {
|
||||
JSTimersExecution.callTimer(passImmediates[i], 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
Systrace.endEvent();
|
||||
}
|
||||
return JSTimersExecution.immediates.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is called after we execute any command we receive from native but
|
||||
* before we hand control back to native.
|
||||
*/
|
||||
callImmediates() {
|
||||
JSTimersExecution.errors = null;
|
||||
while (JSTimersExecution.callImmediatesPass()) {}
|
||||
if (JSTimersExecution.errors) {
|
||||
JSTimersExecution.errors.forEach((error) =>
|
||||
require('JSTimers').setTimeout(() => { throw error; }, 0)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called from native (in development) when environment times are out-of-sync.
|
||||
*/
|
||||
emitTimeDriftWarning(warningMessage: string) {
|
||||
if (hasEmittedTimeDriftWarning) {
|
||||
return;
|
||||
}
|
||||
hasEmittedTimeDriftWarning = true;
|
||||
console.warn(warningMessage);
|
||||
},
|
||||
|
||||
_clearIndex(i: number) {
|
||||
JSTimersExecution.timerIDs[i] = null;
|
||||
JSTimersExecution.callbacks[i] = null;
|
||||
JSTimersExecution.types[i] = null;
|
||||
JSTimersExecution.identifiers[i] = null;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = JSTimersExecution;
|
Loading…
Reference in New Issue