Merge JSTimers and JSTimersExecution

Reviewed By: mmmulani

Differential Revision: D5292923

fbshipit-source-id: d8331cbac28ba3bbf47c9746238a755b707903ea
This commit is contained in:
Pieter De Baets 2017-06-22 09:46:28 -07:00 committed by Facebook Github Bot
parent e9f657f2bd
commit 0ae11f12f3
4 changed files with 358 additions and 333 deletions

View File

@ -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();
}

View File

@ -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'));

View File

@ -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;

View File

@ -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;