498 lines
14 KiB
JavaScript
498 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow
|
|
*/
|
|
'use strict';
|
|
|
|
const Platform = require('Platform');
|
|
const Systrace = require('Systrace');
|
|
|
|
const invariant = require('fbjs/lib/invariant');
|
|
const {Timing} = require('NativeModules');
|
|
|
|
import type {ExtendedError} from 'parseErrorStack';
|
|
|
|
let _performanceNow = null;
|
|
function performanceNow() {
|
|
if (!_performanceNow) {
|
|
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an
|
|
* error found when Flow v0.54 was deployed. To see the error delete this
|
|
* comment and run Flow. */
|
|
_performanceNow = require('fbjs/lib/performanceNow');
|
|
}
|
|
return _performanceNow();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
|
|
export type JSTimerType =
|
|
| 'setTimeout'
|
|
| 'setInterval'
|
|
| 'requestAnimationFrame'
|
|
| 'setImmediate'
|
|
| 'requestIdleCallback';
|
|
|
|
// These timing constants 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';
|
|
const ANDROID_LONG_TIMER_MESSAGE =
|
|
'Setting a timer for a long period of time, i.e. multiple minutes, is a ' +
|
|
'performance and correctness issue on Android as it keeps the timer ' +
|
|
'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) {
|
|
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an
|
|
* error found when Flow v0.54 was deployed. To see the error delete this
|
|
* comment and run Flow. */
|
|
require('fbjs/lib/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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs a single pass over the enqueued immediates. Returns whether
|
|
* more immediates are queued up (can be used as a condition a while loop).
|
|
*/
|
|
function _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;
|
|
}
|
|
|
|
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
|
|
* callback.
|
|
*/
|
|
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 {
|
|
if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) {
|
|
console.warn(
|
|
ANDROID_LONG_TIMER_MESSAGE +
|
|
'\n' +
|
|
'(Saw setTimeout with duration ' +
|
|
duration +
|
|
'ms)',
|
|
);
|
|
}
|
|
const id = _allocateCallback(
|
|
() => func.apply(undefined, args),
|
|
'setTimeout',
|
|
);
|
|
Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false);
|
|
return id;
|
|
},
|
|
|
|
/**
|
|
* @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 {
|
|
if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) {
|
|
console.warn(
|
|
ANDROID_LONG_TIMER_MESSAGE +
|
|
'\n' +
|
|
'(Saw setInterval with duration ' +
|
|
duration +
|
|
'ms)',
|
|
);
|
|
}
|
|
const id = _allocateCallback(
|
|
() => func.apply(undefined, args),
|
|
'setInterval',
|
|
);
|
|
Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ true);
|
|
return id;
|
|
},
|
|
|
|
/**
|
|
* @param {function} func Callback to be invoked before the end of the
|
|
* current JavaScript execution loop.
|
|
*/
|
|
setImmediate: function(func: Function, ...args: any) {
|
|
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) {
|
|
const id = _allocateCallback(func, 'requestAnimationFrame');
|
|
Timing.createTimer(id, 1, Date.now(), /* recurring */ false);
|
|
return id;
|
|
},
|
|
|
|
/**
|
|
* @param {function} func Callback to be invoked every frame and provided
|
|
* with time remaining in frame.
|
|
* @param {?object} options
|
|
*/
|
|
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 = requestIdleCallbackTimeouts[id];
|
|
if (timeoutId) {
|
|
JSTimers.clearTimeout(timeoutId);
|
|
delete requestIdleCallbackTimeouts[id];
|
|
}
|
|
return func(deadline);
|
|
}
|
|
: func,
|
|
'requestIdleCallback',
|
|
);
|
|
requestIdleCallbacks.push(id);
|
|
|
|
if (timeout != null) {
|
|
const timeoutId = JSTimers.setTimeout(() => {
|
|
const index = requestIdleCallbacks.indexOf(id);
|
|
if (index > -1) {
|
|
requestIdleCallbacks.splice(index, 1);
|
|
_callTimer(id, performanceNow(), true);
|
|
}
|
|
delete requestIdleCallbackTimeouts[id];
|
|
if (requestIdleCallbacks.length === 0) {
|
|
Timing.setSendIdleEvents(false);
|
|
}
|
|
}, timeout);
|
|
requestIdleCallbackTimeouts[id] = timeoutId;
|
|
}
|
|
return id;
|
|
},
|
|
|
|
cancelIdleCallback: function(timerID: number) {
|
|
_freeCallback(timerID);
|
|
const index = requestIdleCallbacks.indexOf(timerID);
|
|
if (index !== -1) {
|
|
requestIdleCallbacks.splice(index, 1);
|
|
}
|
|
|
|
const timeoutId = requestIdleCallbackTimeouts[timerID];
|
|
if (timeoutId) {
|
|
JSTimers.clearTimeout(timeoutId);
|
|
delete requestIdleCallbackTimeouts[timerID];
|
|
}
|
|
|
|
if (requestIdleCallbacks.length === 0) {
|
|
Timing.setSendIdleEvents(false);
|
|
}
|
|
},
|
|
|
|
clearTimeout: function(timerID: number) {
|
|
_freeCallback(timerID);
|
|
},
|
|
|
|
clearInterval: function(timerID: number) {
|
|
_freeCallback(timerID);
|
|
},
|
|
|
|
clearImmediate: function(timerID: number) {
|
|
_freeCallback(timerID);
|
|
const index = immediates.indexOf(timerID);
|
|
if (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),
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This is called after we execute any command we receive from native but
|
|
* before we hand control back to native.
|
|
*/
|
|
callImmediates() {
|
|
errors = null;
|
|
while (_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);
|
|
},
|
|
};
|
|
|
|
let ExportedJSTimers;
|
|
if (!Timing) {
|
|
console.warn("Timing native module is not available, can't set timers.");
|
|
// $FlowFixMe: we can assume timers are generally available
|
|
ExportedJSTimers = ({
|
|
callImmediates: JSTimers.callImmediates,
|
|
setImmediate: JSTimers.setImmediate,
|
|
}: typeof JSTimers);
|
|
} else {
|
|
ExportedJSTimers = JSTimers;
|
|
}
|
|
|
|
module.exports = ExportedJSTimers;
|