[scheduler] Priority levels, continuations, and wrapped callbacks (#13720)

All of these features are based on features of React's internal
scheduler. The eventual goal is to lift as much as possible out of the
React internals into the Scheduler package.

Includes some renaming of existing methods.

- `scheduleWork` is now `scheduleCallback`
- `cancelScheduledWork` is now `cancelCallback`


Priority levels
---------------

Adds the ability to schedule callbacks at different priority levels.
The current levels are (final names TBD):

- Immediate priority. Fires at the end of the outermost currently
executing (similar to a microtask).
- Interactive priority. Fires within a few hundred milliseconds. This
should only be used to provide quick feedback to the user as a result
of an interaction.
- Normal priority. This is the default. Fires within several seconds.
- "Maybe" priority. Only fires if there's nothing else to do. Used for
prerendering or warming a cache.

The priority is changed using `runWithPriority`:

```js
runWithPriority(InteractivePriority, () => {
  scheduleCallback(callback);
});
```


Continuations
-------------

Adds the ability for a callback to yield without losing its place
in the queue, by returning a continuation. The continuation will have
the same expiration as the callback that yielded.


Wrapped callbacks
-----------------

Adds the ability to wrap a callback so that, when it is called, it
receives the priority of the current execution context.
This commit is contained in:
Andrew Clark
2018-09-25 15:11:42 -07:00
committed by GitHub
parent 970a34baed
commit f305d2a489
15 changed files with 914 additions and 302 deletions
+78 -76
View File
@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%;">
<head>
<meta charset="utf-8">
<title>Scheduler Test Page</title>
<head>
<meta charset="utf-8">
<title>Scheduler Test Page</title>
<style>
.correct {
border: solid green 2px;
@@ -11,91 +12,92 @@
border: dashed red 2px;
}
</style>
</head>
<body>
<h1>Scheduler Fixture</h1>
<p>
This fixture is for manual testing purposes, and the patterns used in
implementing it should not be used as a model. This is mainly for anyone
working on making changes to the `schedule` module.
</p>
<h2>Tests:</h2>
<ol>
<li>
<button onClick="runTestOne()">Run Test 1</button>
<p>Calls the callback within the frame when not blocked:</p>
<div><b>Expected:</b></div>
<div id="test-1-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
</head>
<body>
<h1>Scheduler Fixture</h1>
<p>
This fixture is for manual testing purposes, and the patterns used in
implementing it should not be used as a model. This is mainly for anyone
working on making changes to the `schedule` module.
</p>
<h2>Tests:</h2>
<ol>
<li>
<button onClick="runTestOne()">Run Test 1</button>
<p>Calls the callback within the frame when not blocked:</p>
<div><b>Expected:</b></div>
<div id="test-1-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
<div> -------------------------------------------------</div>
<div><b>Actual:</b></div>
<div id="test-1"></div>
</li>
<li>
<p>Accepts multiple callbacks and calls within frame when not blocked</p>
<button onClick="runTestTwo()">Run Test 2</button>
<div><b>Expected:</b></div>
<div id="test-2-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
</li>
<li>
<p>Accepts multiple callbacks and calls within frame when not blocked</p>
<button onClick="runTestTwo()">Run Test 2</button>
<div><b>Expected:</b></div>
<div id="test-2-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
<div> -------------------------------------------------</div>
<div><b>Actual:</b></div>
<div id="test-2"></div>
</li>
<li>
<p>Schedules callbacks in correct order when they use scheduleWork to schedule themselves</p>
<button onClick="runTestThree()">Run Test 3</button>
<div><b>Expected:</b></div>
<div id="test-3-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
</li>
<li>
<p>Schedules callbacks in correct order when they use scheduleWork to schedule themselves</p>
<button onClick="runTestThree()">Run Test 3</button>
<div><b>Expected:</b></div>
<div id="test-3-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
<div> -------------------------------------------------</div>
<div><b>Actual:</b></div>
<div id="test-3"></div>
</li>
<li>
<p>Calls timed out callbacks and then any more pending callbacks, defers others if time runs out</p>
<button onClick="runTestFour()">Run Test 4</button>
<div><b>Expected:</b></div>
<div id="test-4-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
</li>
<li>
<p>Calls timed out callbacks and then any more pending callbacks, defers others if time runs out</p>
<button onClick="runTestFour()">Run Test 4</button>
<div><b>Expected:</b></div>
<div id="test-4-expected">
</div>
<div> -------------------------------------------------</div>
<div> If you see the same above and below it's correct.
<div> -------------------------------------------------</div>
<div><b>Actual:</b></div>
<div id="test-4"></div>
</li>
<li>
<p>When some callbacks throw errors, still calls them all within the same frame</p>
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
<button onClick="runTestFive()">Run Test 5</button>
</li>
<li>
<p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p>
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
<button onClick="runTestSix()">Run Test 6</button>
</li>
<li>
<p>Continues calling callbacks even when user switches away from this tab</p>
<button onClick="runTestSeven()">Run Test 7</button>
<div><b>Click the button above, observe the counter, then switch to
another tab and switch back:</b></div>
<div id="test-7">
</div>
<div> If the counter advanced while you were away from this tab, it's correct.</div>
</li>
</ol>
<script src="../../build/dist/react.development.js"></script>
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
</li>
<li>
<p>When some callbacks throw errors, still calls them all within the same frame</p>
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
<button onClick="runTestFive()">Run Test 5</button>
</li>
<li>
<p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p>
<p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p>
<button onClick="runTestSix()">Run Test 6</button>
</li>
<li>
<p>Continues calling callbacks even when user switches away from this tab</p>
<button onClick="runTestSeven()">Run Test 7</button>
<div><b>Click the button above, observe the counter, then switch to
another tab and switch back:</b></div>
<div id="test-7">
</div>
<div> If the counter advanced while you were away from this tab, it's correct.</div>
</li>
</ol>
<script src="../../build/dist/react.development.js"></script>
<script src="../../build/node_modules/scheduler/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
const {
unstable_scheduleWork: scheduleWork,
unstable_cancelWork: cancelWork,
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
unstable_now: now
} = Scheduler;
function displayTestResult(testNumber) {
@@ -496,4 +498,4 @@ function runTestSeven() {
}
</script type="text/babel">
</body>
</html>
</html>
+2 -2
View File
@@ -30,8 +30,8 @@ function checkSchedulerAPI() {
if (
typeof Scheduler === 'undefined' ||
typeof Scheduler.unstable_now !== 'function' ||
typeof Scheduler.unstable_scheduleWork !== 'function' ||
typeof Scheduler.unstable_cancelScheduledWork !== 'function'
typeof Scheduler.unstable_scheduleCallback !== 'function' ||
typeof Scheduler.unstable_cancelCallback !== 'function'
) {
throw 'API is not defined';
}
@@ -1,5 +1,5 @@
import React, {Placeholder, PureComponent} from 'react';
import {unstable_scheduleWork} from 'scheduler';
import {unstable_scheduleCallback} from 'scheduler';
import {
unstable_trace as trace,
unstable_wrap as wrap,
@@ -38,7 +38,7 @@ export default class App extends PureComponent {
currentId: id,
})
);
unstable_scheduleWork(
unstable_scheduleCallback(
wrap(() =>
trace(`View ${id} (low-pri)`, performance.now(), () =>
this.setState({
@@ -1,6 +1,6 @@
import React, {PureComponent} from 'react';
import {flushSync, render} from 'react-dom';
import {unstable_scheduleWork} from 'scheduler';
import {unstable_scheduleCallback} from 'scheduler';
import _ from 'lodash';
import Charts from './Charts';
import Clock from './Clock';
@@ -67,7 +67,7 @@ class App extends PureComponent {
}
this._ignoreClick = true;
unstable_scheduleWork(() => {
unstable_scheduleCallback(() => {
this.setState({showDemo: true}, () => {
this._ignoreClick = false;
});
@@ -107,7 +107,7 @@ class App extends PureComponent {
this.debouncedHandleChange(value);
break;
case 'async':
unstable_scheduleWork(() => {
unstable_scheduleCallback(() => {
this.setState({value});
});
break;
+2 -2
View File
@@ -7,8 +7,8 @@
export {
unstable_now as now,
unstable_scheduleWork as scheduleDeferredCallback,
unstable_cancelScheduledWork as cancelDeferredCallback,
unstable_scheduleCallback as scheduleDeferredCallback,
unstable_cancelCallback as cancelDeferredCallback,
} from 'scheduler';
import Transform from 'art/core/transform';
import Mode from 'art/modes/current';
+2 -2
View File
@@ -62,8 +62,8 @@ export type NoTimeout = -1;
export {
unstable_now as now,
unstable_scheduleWork as scheduleDeferredCallback,
unstable_cancelScheduledWork as cancelDeferredCallback,
unstable_scheduleCallback as scheduleDeferredCallback,
unstable_cancelCallback as cancelDeferredCallback,
} from 'scheduler';
let SUPPRESS_HYDRATION_WARNING;
+10 -4
View File
@@ -7,9 +7,12 @@
import assign from 'object-assign';
import {
unstable_cancelScheduledWork,
unstable_cancelCallback,
unstable_now,
unstable_scheduleWork,
unstable_scheduleCallback,
unstable_runWithPriority,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
} from 'scheduler';
import {
__interactionsRef,
@@ -39,9 +42,12 @@ if (__UMD__) {
// CJS bundles use the shared NPM package.
Object.assign(ReactSharedInternals, {
Scheduler: {
unstable_cancelScheduledWork,
unstable_cancelCallback,
unstable_now,
unstable_scheduleWork,
unstable_scheduleCallback,
unstable_runWithPriority,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
},
SchedulerTracing: {
__interactionsRef,
@@ -7,6 +7,8 @@
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable max-len */
'use strict';
(function(global, factory) {
@@ -23,15 +25,36 @@
);
}
function unstable_scheduleWork() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply(
function unstable_scheduleCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply(
this,
arguments
);
}
function unstable_cancelScheduledWork() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply(
function unstable_cancelCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply(
this,
arguments
);
}
function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this,
arguments
);
}
function unstable_wrapCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply(
this,
arguments
);
}
function unstable_getCurrentPriorityLevel() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply(
this,
arguments
);
@@ -39,7 +62,10 @@
return Object.freeze({
unstable_now: unstable_now,
unstable_scheduleWork: unstable_scheduleWork,
unstable_cancelScheduledWork: unstable_cancelScheduledWork,
unstable_scheduleCallback: unstable_scheduleCallback,
unstable_cancelCallback: unstable_cancelCallback,
unstable_runWithPriority: unstable_runWithPriority,
unstable_wrapCallback: unstable_wrapCallback,
unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
});
});
+32 -6
View File
@@ -7,6 +7,8 @@
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable max-len */
'use strict';
(function(global, factory) {
@@ -23,15 +25,36 @@
);
}
function unstable_scheduleWork() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply(
function unstable_scheduleCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply(
this,
arguments
);
}
function unstable_cancelScheduledWork() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply(
function unstable_cancelCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply(
this,
arguments
);
}
function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this,
arguments
);
}
function unstable_wrapCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply(
this,
arguments
);
}
function unstable_getCurrentPriorityLevel() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply(
this,
arguments
);
@@ -39,7 +62,10 @@
return Object.freeze({
unstable_now: unstable_now,
unstable_scheduleWork: unstable_scheduleWork,
unstable_cancelScheduledWork: unstable_cancelScheduledWork,
unstable_scheduleCallback: unstable_scheduleCallback,
unstable_cancelCallback: unstable_cancelCallback,
unstable_runWithPriority: unstable_runWithPriority,
unstable_wrapCallback: unstable_wrapCallback,
unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
});
});
+32 -6
View File
@@ -7,6 +7,8 @@
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable max-len */
'use strict';
(function(global, factory) {
@@ -23,15 +25,36 @@
);
}
function unstable_scheduleWork() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleWork.apply(
function unstable_scheduleCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_scheduleCallback.apply(
this,
arguments
);
}
function unstable_cancelScheduledWork() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelScheduledWork.apply(
function unstable_cancelCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_cancelCallback.apply(
this,
arguments
);
}
function unstable_runWithPriority() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_runWithPriority.apply(
this,
arguments
);
}
function unstable_wrapCallback() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply(
this,
arguments
);
}
function unstable_getCurrentPriorityLevel() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_getCurrentPriorityLevel.apply(
this,
arguments
);
@@ -39,7 +62,10 @@
return Object.freeze({
unstable_now: unstable_now,
unstable_scheduleWork: unstable_scheduleWork,
unstable_cancelScheduledWork: unstable_cancelScheduledWork,
unstable_scheduleCallback: unstable_scheduleCallback,
unstable_cancelCallback: unstable_cancelCallback,
unstable_runWithPriority: unstable_runWithPriority,
unstable_wrapCallback: unstable_wrapCallback,
unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,
});
});
+247 -50
View File
@@ -8,14 +8,34 @@
/* eslint-disable no-var */
// TODO: Currently there's only a single priority level, Deferred. Will add
// additional priorities.
var DEFERRED_TIMEOUT = 5000;
// TODO: Use symbols?
var ImmediatePriority = 1;
var InteractivePriority = 2;
var NormalPriority = 3;
var WheneverPriority = 4;
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var INTERACTIVE_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
// Never times out
var WHENEVER_PRIORITY_TIMEOUT = maxSigned31BitInt;
// Callbacks are stored as a circular, doubly linked list.
var firstCallbackNode = null;
var isPerformingWork = false;
var currentPriorityLevel = NormalPriority;
var currentEventStartTime = -1;
var currentExpirationTime = -1;
// This is set when a callback is being executed, to prevent re-entrancy.
var isExecutingCallback = false;
var isHostCallbackScheduled = false;
@@ -25,6 +45,14 @@ var hasNativePerformanceNow =
var timeRemaining;
if (hasNativePerformanceNow) {
timeRemaining = function() {
if (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime < currentExpirationTime
) {
// A higher priority callback was scheduled. Yield so we can switch to
// working on that.
return 0;
}
// We assume that if we have a performance timer that the rAF callback
// gets a performance timer value. Not sure if this is always true.
var remaining = getFrameDeadline() - performance.now();
@@ -33,6 +61,12 @@ if (hasNativePerformanceNow) {
} else {
timeRemaining = function() {
// Fallback to Date.now()
if (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime < currentExpirationTime
) {
return 0;
}
var remaining = getFrameDeadline() - Date.now();
return remaining > 0 ? remaining : 0;
};
@@ -44,22 +78,22 @@ var deadlineObject = {
};
function ensureHostCallbackIsScheduled() {
if (isPerformingWork) {
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest timeout in the list.
var timesOutAt = firstCallbackNode.timesOutAt;
// Schedule the host callback using the earliest expiration in the list.
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
cancelCallback();
cancelHostCallback();
}
requestCallback(flushWork, timesOutAt);
requestHostCallback(flushWork, expirationTime);
}
function flushFirstCallback(node) {
function flushFirstCallback() {
var flushedNode = firstCallbackNode;
// Remove the node from the list before calling the callback. That way the
@@ -70,35 +104,124 @@ function flushFirstCallback(node) {
firstCallbackNode = null;
next = null;
} else {
var previous = firstCallbackNode.previous;
firstCallbackNode = previous.next = next;
next.previous = previous;
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
}
flushedNode.next = flushedNode.previous = null;
// Now it's safe to call the callback.
var callback = flushedNode.callback;
callback(deadlineObject);
var expirationTime = flushedNode.expirationTime;
var priorityLevel = flushedNode.priorityLevel;
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try {
continuationCallback = callback(deadlineObject);
} finally {
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
}
// A callback may return a continuation. The continuation should be scheduled
// with the same priority and expiration as the just-finished callback.
if (typeof continuationCallback === 'function') {
var continuationNode: CallbackNode = {
callback: continuationCallback,
priorityLevel,
expirationTime,
next: null,
previous: null,
};
// Insert the new callback into the list, sorted by its expiration. This is
// almost the same as the code in `scheduleCallback`, except the callback
// is inserted into the list *before* callbacks of equal expiration instead
// of after.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
} else {
var nextAfterContinuation = null;
var node = firstCallbackNode;
do {
if (node.expirationTime >= expirationTime) {
// This callback expires at or after the continuation. We will insert
// the continuation *before* this callback.
nextAfterContinuation = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (nextAfterContinuation === null) {
// No equal or lower priority callback was found, which means the new
// callback is the lowest priority callback in the list.
nextAfterContinuation = firstCallbackNode;
} else if (nextAfterContinuation === firstCallbackNode) {
// The new callback is the highest priority callback in the list.
firstCallbackNode = continuationNode;
ensureHostCallbackIsScheduled(firstCallbackNode);
}
var previous = nextAfterContinuation.previous;
previous.next = nextAfterContinuation.previous = continuationNode;
continuationNode.next = nextAfterContinuation;
continuationNode.previous = previous;
}
}
}
function flushImmediateWork() {
if (
// Confirm we've exited the outer most event handler
currentEventStartTime === -1 &&
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
) {
isExecutingCallback = true;
deadlineObject.didTimeout = true;
try {
do {
flushFirstCallback();
} while (
// Keep flushing until there are no more immediate callbacks
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
);
} finally {
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
isHostCallbackScheduled = false;
}
}
}
}
function flushWork(didTimeout) {
isPerformingWork = true;
isExecutingCallback = true;
deadlineObject.didTimeout = didTimeout;
try {
if (didTimeout) {
// Flush all the timed out callbacks without yielding.
// Flush all the expired callbacks without yielding.
while (firstCallbackNode !== null) {
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
var currentTime = getCurrentTime();
if (firstCallbackNode.timesOutAt <= currentTime) {
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.timesOutAt <= currentTime
firstCallbackNode.expirationTime <= currentTime
);
continue;
}
@@ -116,41 +239,104 @@ function flushWork(didTimeout) {
}
}
} finally {
isPerformingWork = false;
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
function unstable_scheduleWork(callback, options) {
var currentTime = getCurrentTime();
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case InteractivePriority:
case NormalPriority:
case WheneverPriority:
break;
default:
priorityLevel = NormalPriority;
}
var timesOutAt;
var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = priorityLevel;
currentEventStartTime = getCurrentTime();
try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
function unstable_wrapCallback(callback) {
var parentPriorityLevel = currentPriorityLevel;
return function() {
// This is a fork of runWithPriority, inlined for performance.
var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = parentPriorityLevel;
currentEventStartTime = getCurrentTime();
try {
return callback.apply(this, arguments);
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
flushImmediateWork();
}
};
}
function unstable_scheduleCallback(callback, deprecated_options) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
var expirationTime;
if (
options !== undefined &&
options !== null &&
options.timeout !== null &&
options.timeout !== undefined
typeof deprecated_options === 'object' &&
deprecated_options !== null &&
typeof deprecated_options.timeout === 'number'
) {
// Check for an explicit timeout
timesOutAt = currentTime + options.timeout;
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
// Compute an absolute timeout using the default constant.
timesOutAt = currentTime + DEFERRED_TIMEOUT;
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case InteractivePriority:
expirationTime = startTime + INTERACTIVE_PRIORITY_TIMEOUT;
break;
case WheneverPriority:
expirationTime = startTime + WHENEVER_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
var newNode = {
callback,
timesOutAt,
priorityLevel: currentPriorityLevel,
expirationTime,
next: null,
previous: null,
};
// Insert the new callback into the list, sorted by its timeout.
// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
@@ -159,8 +345,8 @@ function unstable_scheduleWork(callback, options) {
var next = null;
var node = firstCallbackNode;
do {
if (node.timesOutAt > timesOutAt) {
// The new callback times out before this one.
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
@@ -168,11 +354,11 @@ function unstable_scheduleWork(callback, options) {
} while (node !== firstCallbackNode);
if (next === null) {
// No callback with a later timeout was found, which means the new
// callback has the latest timeout in the list.
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest timeout in the entire list.
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled(firstCallbackNode);
}
@@ -186,7 +372,7 @@ function unstable_scheduleWork(callback, options) {
return newNode;
}
function unstable_cancelScheduledWork(callbackNode) {
function unstable_cancelCallback(callbackNode) {
var next = callbackNode.next;
if (next === null) {
// Already cancelled.
@@ -209,6 +395,10 @@ function unstable_cancelScheduledWork(callbackNode) {
callbackNode.next = callbackNode.previous = null;
}
function unstable_getCurrentPriorityLevel() {
return currentPriorityLevel;
}
// The remaining code is essentially a polyfill for requestIdleCallback. It
// works by scheduling a requestAnimationFrame, storing the time for the start
// of the frame, then scheduling a postMessage which gets scheduled after paint.
@@ -274,18 +464,18 @@ if (hasNativePerformanceNow) {
};
}
var requestCallback;
var cancelCallback;
var requestHostCallback;
var cancelHostCallback;
var getFrameDeadline;
if (typeof window === 'undefined') {
// If this accidentally gets imported in a non-browser environment, fallback
// to a naive implementation.
var timeoutID = -1;
requestCallback = function(callback, absoluteTimeout) {
requestHostCallback = function(callback, absoluteTimeout) {
timeoutID = setTimeout(callback, 0, true);
};
cancelCallback = function() {
cancelHostCallback = function() {
clearTimeout(timeoutID);
};
getFrameDeadline = function() {
@@ -294,11 +484,12 @@ if (typeof window === 'undefined') {
} else if (window._schedMock) {
// Dynamic injection, only for testing purposes.
var impl = window._schedMock;
requestCallback = impl[0];
cancelCallback = impl[1];
requestHostCallback = impl[0];
cancelHostCallback = impl[1];
getFrameDeadline = impl[2];
} else {
if (typeof console !== 'undefined') {
// TODO: Remove fb.me link
if (typeof localRequestAnimationFrame !== 'function') {
console.error(
"This browser doesn't support requestAnimationFrame. " +
@@ -416,11 +607,10 @@ if (typeof window === 'undefined') {
}
};
requestCallback = function(callback, absoluteTimeout) {
requestHostCallback = function(callback, absoluteTimeout) {
scheduledCallback = callback;
timeoutTime = absoluteTimeout;
if (isPerformingIdleWork) {
// If we're already performing idle work, an error must have been thrown.
if (isPerformingIdleWork || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
window.postMessage(messageKey, '*');
} else if (!isAnimationFrameScheduled) {
@@ -433,7 +623,7 @@ if (typeof window === 'undefined') {
}
};
cancelCallback = function() {
cancelHostCallback = function() {
scheduledCallback = null;
isIdleScheduled = false;
timeoutTime = -1;
@@ -441,7 +631,14 @@ if (typeof window === 'undefined') {
}
export {
unstable_scheduleWork,
unstable_cancelScheduledWork,
ImmediatePriority as unstable_ImmediatePriority,
InteractivePriority as unstable_InteractivePriority,
NormalPriority as unstable_NormalPriority,
WheneverPriority as unstable_WheneverPriority,
unstable_runWithPriority,
unstable_scheduleCallback,
unstable_cancelCallback,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
getCurrentTime as unstable_now,
};
@@ -9,8 +9,14 @@
'use strict';
let scheduleWork;
let cancelScheduledWork;
let runWithPriority;
let ImmediatePriority;
let InteractivePriority;
let NormalPriority;
let scheduleCallback;
let cancelCallback;
let wrapCallback;
let getCurrentPriorityLevel;
let flushWork;
let advanceTime;
let doWork;
@@ -24,12 +30,16 @@ describe('Scheduler', () => {
jest.resetModules();
let _flushWork = null;
let isFlushing = false;
let timeoutID = -1;
let endOfFrame = -1;
let currentTime = 0;
flushWork = frameSize => {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (frameSize === null || frameSize === undefined) {
frameSize = Infinity;
}
@@ -39,8 +49,10 @@ describe('Scheduler', () => {
timeoutID = -1;
endOfFrame = currentTime + frameSize;
try {
_flushWork();
isFlushing = true;
_flushWork(false);
} finally {
isFlushing = false;
endOfFrame = -1;
}
const yields = yieldedValues;
@@ -54,6 +66,9 @@ describe('Scheduler', () => {
};
doWork = (label, timeCost) => {
if (typeof timeCost !== 'number') {
throw new Error('Second arg must be a number.');
}
advanceTime(timeCost);
yieldValue(label);
};
@@ -69,16 +84,32 @@ describe('Scheduler', () => {
return yields;
};
function requestCallback(fw, absoluteTimeout) {
function onTimeout() {
if (_flushWork === null) {
return;
}
if (isFlushing) {
// Jest fires timers synchronously when jest.advanceTimersByTime is
// called. Use setImmediate to prevent re-entrancy.
setImmediate(onTimeout);
} else {
try {
isFlushing = true;
_flushWork(true);
} finally {
isFlushing = false;
}
}
}
function requestHostCallback(fw, absoluteTimeout) {
if (_flushWork !== null) {
throw new Error('Work is already scheduled.');
}
_flushWork = fw;
timeoutID = setTimeout(() => {
_flushWork(true);
}, absoluteTimeout - currentTime);
timeoutID = setTimeout(onTimeout, absoluteTimeout - currentTime);
}
function cancelCallback() {
function cancelHostCallback() {
if (_flushWork === null) {
throw new Error('No work is scheduled.');
}
@@ -91,19 +122,31 @@ describe('Scheduler', () => {
// Override host implementation
delete global.performance;
global.Date.now = () => currentTime;
window._schedMock = [requestCallback, cancelCallback, getTimeRemaining];
global.Date.now = () => {
return currentTime;
};
window._schedMock = [
requestHostCallback,
cancelHostCallback,
getTimeRemaining,
];
const Scheduler = require('scheduler');
scheduleWork = Scheduler.unstable_scheduleWork;
cancelScheduledWork = Scheduler.unstable_cancelScheduledWork;
const Schedule = require('scheduler');
runWithPriority = Schedule.unstable_runWithPriority;
ImmediatePriority = Schedule.unstable_ImmediatePriority;
InteractivePriority = Schedule.unstable_InteractivePriority;
NormalPriority = Schedule.unstable_NormalPriority;
scheduleCallback = Schedule.unstable_scheduleCallback;
cancelCallback = Schedule.unstable_cancelCallback;
wrapCallback = Schedule.unstable_wrapCallback;
getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel;
});
it('flushes work incrementally', () => {
scheduleWork(() => doWork('A', 100));
scheduleWork(() => doWork('B', 200));
scheduleWork(() => doWork('C', 300));
scheduleWork(() => doWork('D', 400));
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => doWork('B', 200));
scheduleCallback(() => doWork('C', 300));
scheduleCallback(() => doWork('D', 400));
expect(flushWork(300)).toEqual(['A', 'B']);
expect(flushWork(300)).toEqual(['C']);
@@ -111,11 +154,11 @@ describe('Scheduler', () => {
});
it('cancels work', () => {
scheduleWork(() => doWork('A', 100));
const callbackHandleB = scheduleWork(() => doWork('B', 200));
scheduleWork(() => doWork('C', 300));
scheduleCallback(() => doWork('A', 100));
const callbackHandleB = scheduleCallback(() => doWork('B', 200));
scheduleCallback(() => doWork('C', 300));
cancelScheduledWork(callbackHandleB);
cancelCallback(callbackHandleB);
expect(flushWork()).toEqual([
'A',
@@ -124,51 +167,336 @@ describe('Scheduler', () => {
]);
});
it('prioritizes callbacks according to their timeouts', () => {
scheduleWork(() => doWork('A', 10), {timeout: 5000});
scheduleWork(() => doWork('B', 20), {timeout: 5000});
scheduleWork(() => doWork('C', 30), {timeout: 1000});
scheduleWork(() => doWork('D', 40), {timeout: 5000});
it('executes the highest priority callbacks first', () => {
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => doWork('B', 100));
// C should be first because it has the earliest timeout
expect(flushWork()).toEqual(['C', 'A', 'B', 'D']);
// Yield before B is flushed
expect(flushWork(100)).toEqual(['A']);
runWithPriority(InteractivePriority, () => {
scheduleCallback(() => doWork('C', 100));
scheduleCallback(() => doWork('D', 100));
});
// C and D should come first, because they are higher priority
expect(flushWork()).toEqual(['C', 'D', 'B']);
});
it('times out work', () => {
scheduleWork(() => doWork('A', 100), {timeout: 5000});
scheduleWork(() => doWork('B', 200), {timeout: 5000});
scheduleWork(() => doWork('C', 300), {timeout: 1000});
scheduleWork(() => doWork('D', 400), {timeout: 5000});
it('expires work', () => {
scheduleCallback(() => doWork('A', 100));
runWithPriority(InteractivePriority, () => {
scheduleCallback(() => doWork('B', 100));
});
scheduleCallback(() => doWork('C', 100));
runWithPriority(InteractivePriority, () => {
scheduleCallback(() => doWork('D', 100));
});
// Advance time, but not by enough to flush any work
advanceTime(999);
// Advance time, but not by enough to expire any work
advanceTime(249);
expect(clearYieldedValues()).toEqual([]);
// Advance by just a bit more to flush C
// Advance by just a bit more to expire the high pri callbacks
advanceTime(1);
expect(clearYieldedValues()).toEqual(['C']);
expect(clearYieldedValues()).toEqual(['B', 'D']);
// Flush the rest
advanceTime(4000);
expect(clearYieldedValues()).toEqual(['A', 'B', 'D']);
// Expire the rest
advanceTime(10000);
expect(clearYieldedValues()).toEqual(['A', 'C']);
});
it('has a default timeout of 5 seconds', () => {
scheduleWork(() => doWork('A', 100));
scheduleWork(() => doWork('B', 200));
scheduleWork(() => doWork('C', 300), {timeout: 1000});
scheduleWork(() => doWork('D', 400));
it('has a default expiration of ~5 seconds', () => {
scheduleCallback(() => doWork('A', 100));
// Flush C
advanceTime(1000);
expect(clearYieldedValues()).toEqual(['C']);
// Advance time until right before the rest of the work expires
advanceTime(3699);
advanceTime(4999);
expect(clearYieldedValues()).toEqual([]);
// Now advance by just a bit more
advanceTime(1);
expect(clearYieldedValues()).toEqual(['A', 'B', 'D']);
expect(clearYieldedValues()).toEqual(['A']);
});
it('continues working on same task after yielding', () => {
scheduleCallback(() => doWork('A', 100));
scheduleCallback(() => doWork('B', 100));
const tasks = [['C1', 100], ['C2', 100], ['C3', 100]];
const C = deadline => {
while (tasks.length > 0) {
doWork(...tasks.shift());
if (
tasks.length > 0 &&
!deadline.didTimeout &&
deadline.timeRemaining() <= 0
) {
yieldValue('Yield!');
return C;
}
}
};
scheduleCallback(C);
scheduleCallback(() => doWork('D', 100));
scheduleCallback(() => doWork('E', 100));
expect(flushWork(300)).toEqual(['A', 'B', 'C1', 'Yield!']);
expect(flushWork()).toEqual(['C2', 'C3', 'D', 'E']);
});
it('continuation callbacks inherit the expiration of the previous callback', () => {
const tasks = [['A', 125], ['B', 125], ['C', 125], ['D', 125]];
const work = deadline => {
while (tasks.length > 0) {
doWork(...tasks.shift());
if (
tasks.length > 0 &&
!deadline.didTimeout &&
deadline.timeRemaining() <= 0
) {
yieldValue('Yield!');
return work;
}
}
};
// Schedule a high priority callback
runWithPriority(InteractivePriority, () => scheduleCallback(work));
// Flush until just before the expiration time
expect(flushWork(249)).toEqual(['A', 'B', 'Yield!']);
// Advance time by just a bit more. This should expire all the remaining work.
advanceTime(1);
expect(clearYieldedValues()).toEqual(['C', 'D']);
});
it('nested callbacks inherit the priority of the currently executing callback', () => {
runWithPriority(InteractivePriority, () => {
scheduleCallback(() => {
doWork('Parent callback', 100);
scheduleCallback(() => {
doWork('Nested callback', 100);
});
});
});
expect(flushWork(100)).toEqual(['Parent callback']);
// The nested callback has interactive priority, so it should
// expire quickly.
advanceTime(250 + 100);
expect(clearYieldedValues()).toEqual(['Nested callback']);
});
it('continuations are interrupted by higher priority work', () => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = deadline => {
while (tasks.length > 0) {
doWork(...tasks.shift());
if (
tasks.length > 0 &&
!deadline.didTimeout &&
deadline.timeRemaining() <= 0
) {
yieldValue('Yield!');
return work;
}
}
};
scheduleCallback(work);
expect(flushWork(100)).toEqual(['A', 'Yield!']);
runWithPriority(InteractivePriority, () => {
scheduleCallback(() => doWork('High pri', 100));
});
expect(flushWork()).toEqual(['High pri', 'B', 'C', 'D']);
});
it(
'continutations are interrupted by higher priority work scheduled ' +
'inside an executing callback',
() => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = deadline => {
while (tasks.length > 0) {
const task = tasks.shift();
doWork(...task);
if (task[0] === 'B') {
// Schedule high pri work from inside another callback
yieldValue('Schedule high pri');
runWithPriority(InteractivePriority, () =>
scheduleCallback(() => doWork('High pri', 100)),
);
}
if (
tasks.length > 0 &&
!deadline.didTimeout &&
deadline.timeRemaining() <= 0
) {
yieldValue('Yield!');
return work;
}
}
};
scheduleCallback(work);
expect(flushWork()).toEqual([
'A',
'B',
'Schedule high pri',
// Even though there's time left in the frame, the low pri callback
// should yield to the high pri callback
'Yield!',
'High pri',
// Continue low pri work
'C',
'D',
]);
},
);
it('immediate callbacks fire at the end of outermost event', () => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => yieldValue('A'));
scheduleCallback(() => yieldValue('B'));
// Nested event
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => yieldValue('C'));
// Nothing should have fired yet
expect(clearYieldedValues()).toEqual([]);
});
// Nothing should have fired yet
expect(clearYieldedValues()).toEqual([]);
});
// The callbacks were called at the end of the outer event
expect(clearYieldedValues()).toEqual(['A', 'B', 'C']);
});
it('wrapped callbacks have same signature as original callback', () => {
const wrappedCallback = wrapCallback((...args) => ({args}));
expect(wrappedCallback('a', 'b')).toEqual({args: ['a', 'b']});
});
it('wrapped callbacks inherit the current priority', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
doWork('Normal', 100);
});
});
const wrappedInteractiveCallback = runWithPriority(
InteractivePriority,
() =>
wrapCallback(() => {
scheduleCallback(() => {
doWork('Interactive', 100);
});
}),
);
// This should schedule a normal callback
wrappedCallback();
// This should schedule an interactive callback
wrappedInteractiveCallback();
advanceTime(249);
expect(clearYieldedValues()).toEqual([]);
advanceTime(1);
expect(clearYieldedValues()).toEqual(['Interactive']);
advanceTime(10000);
expect(clearYieldedValues()).toEqual(['Normal']);
});
it('wrapped callbacks inherit the current priority even when nested', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
doWork('Normal', 100);
});
});
const wrappedInteractiveCallback = runWithPriority(
InteractivePriority,
() =>
wrapCallback(() => {
scheduleCallback(() => {
doWork('Interactive', 100);
});
}),
);
runWithPriority(InteractivePriority, () => {
// This should schedule a normal callback
wrappedCallback();
// This should schedule an interactive callback
wrappedInteractiveCallback();
});
advanceTime(249);
expect(clearYieldedValues()).toEqual([]);
advanceTime(1);
expect(clearYieldedValues()).toEqual(['Interactive']);
advanceTime(10000);
expect(clearYieldedValues()).toEqual(['Normal']);
});
it('immediate callbacks fire at the end of callback', () => {
const immediateCallback = runWithPriority(ImmediatePriority, () =>
wrapCallback(() => {
scheduleCallback(() => yieldValue('callback'));
}),
);
immediateCallback();
// The callback was called at the end of the outer event
expect(clearYieldedValues()).toEqual(['callback']);
});
it("immediate callbacks fire even if there's an error", () => {
expect(() => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => {
yieldValue('A');
throw new Error('Oops A');
});
scheduleCallback(() => {
yieldValue('B');
});
scheduleCallback(() => {
yieldValue('C');
throw new Error('Oops C');
});
});
}).toThrow('Oops A');
expect(clearYieldedValues()).toEqual(['A']);
// B and C flush in a subsequent event. That way, the second error is not
// swallowed.
expect(() => flushWork(0)).toThrow('Oops C');
expect(clearYieldedValues()).toEqual(['B', 'C']);
});
it('exposes the current priority level', () => {
yieldValue(getCurrentPriorityLevel());
runWithPriority(ImmediatePriority, () => {
yieldValue(getCurrentPriorityLevel());
runWithPriority(NormalPriority, () => {
yieldValue(getCurrentPriorityLevel());
runWithPriority(InteractivePriority, () => {
yieldValue(getCurrentPriorityLevel());
});
});
yieldValue(getCurrentPriorityLevel());
});
expect(clearYieldedValues()).toEqual([
NormalPriority,
ImmediatePriority,
NormalPriority,
InteractivePriority,
ImmediatePriority,
]);
});
});
@@ -97,11 +97,11 @@ describe('SchedulerDOM', () => {
Scheduler = require('scheduler');
});
describe('scheduleWork', () => {
describe('scheduleCallback', () => {
it('calls the callback within the frame when not blocked', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const cb = jest.fn();
scheduleWork(cb);
scheduleCallback(cb);
advanceOneFrame({timeLeftInFrame: 15});
expect(cb).toHaveBeenCalledTimes(1);
// should not have timed out and should include a timeRemaining method
@@ -111,15 +111,15 @@ describe('SchedulerDOM', () => {
describe('with multiple callbacks', () => {
it('accepts multiple callbacks and calls within frame when not blocked', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
scheduleWork(callbackA);
scheduleCallback(callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
// waits while second callback is passed
scheduleWork(callbackB);
scheduleCallback(callbackB);
expect(callbackLog).toEqual([]);
// after a delay, calls as many callbacks as it has time for
advanceOneFrame({timeLeftInFrame: 15});
@@ -137,17 +137,17 @@ describe('SchedulerDOM', () => {
});
it("accepts callbacks betweeen animationFrame and postMessage and doesn't stall", () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
scheduleWork(callbackA);
scheduleCallback(callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
runRAFCallbacks();
// this should schedule work *after* the requestAnimationFrame but before the message handler
scheduleWork(callbackB);
scheduleCallback(callbackB);
expect(callbackLog).toEqual([]);
// now it should drain the message queue and do all scheduled work
runPostMessageCallbacks({timeLeftInFrame: 15});
@@ -157,7 +157,7 @@ describe('SchedulerDOM', () => {
advanceOneFrame({timeLeftInFrame: 15});
// see if more work can be done now.
scheduleWork(callbackC);
scheduleCallback(callbackC);
expect(callbackLog).toEqual(['A', 'B']);
advanceOneFrame({timeLeftInFrame: 15});
expect(callbackLog).toEqual(['A', 'B', 'C']);
@@ -167,11 +167,11 @@ describe('SchedulerDOM', () => {
'schedules callbacks in correct order and' +
'keeps calling them if there is time',
() => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
scheduleWork(callbackC);
scheduleCallback(callbackC);
});
const callbackB = jest.fn(() => {
callbackLog.push('B');
@@ -180,11 +180,11 @@ describe('SchedulerDOM', () => {
callbackLog.push('C');
});
scheduleWork(callbackA);
scheduleCallback(callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
// continues waiting while B is scheduled
scheduleWork(callbackB);
scheduleCallback(callbackB);
expect(callbackLog).toEqual([]);
// after a delay, calls the scheduled callbacks,
// and also calls new callbacks scheduled by current callbacks
@@ -193,18 +193,18 @@ describe('SchedulerDOM', () => {
},
);
it('schedules callbacks in correct order when callbacks have many nested scheduleWork calls', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
it('schedules callbacks in correct order when callbacks have many nested scheduleCallback calls', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
scheduleWork(callbackC);
scheduleWork(callbackD);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
});
const callbackB = jest.fn(() => {
callbackLog.push('B');
scheduleWork(callbackE);
scheduleWork(callbackF);
scheduleCallback(callbackE);
scheduleCallback(callbackF);
});
const callbackC = jest.fn(() => {
callbackLog.push('C');
@@ -219,8 +219,8 @@ describe('SchedulerDOM', () => {
callbackLog.push('F');
});
scheduleWork(callbackA);
scheduleWork(callbackB);
scheduleCallback(callbackA);
scheduleCallback(callbackB);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
// while flushing callbacks, calls as many as it has time for
@@ -228,23 +228,23 @@ describe('SchedulerDOM', () => {
expect(callbackLog).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
});
it('schedules callbacks in correct order when they use scheduleWork to schedule themselves', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
it('schedules callbacks in correct order when they use scheduleCallback to schedule themselves', () => {
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
let callbackAIterations = 0;
const callbackA = jest.fn(() => {
if (callbackAIterations < 1) {
scheduleWork(callbackA);
scheduleCallback(callbackA);
}
callbackLog.push('A' + callbackAIterations);
callbackAIterations++;
});
const callbackB = jest.fn(() => callbackLog.push('B'));
scheduleWork(callbackA);
scheduleCallback(callbackA);
// initially waits to call the callback
expect(callbackLog).toEqual([]);
scheduleWork(callbackB);
scheduleCallback(callbackB);
expect(callbackLog).toEqual([]);
// after a delay, calls the latest callback passed
advanceOneFrame({timeLeftInFrame: 15});
@@ -260,7 +260,7 @@ describe('SchedulerDOM', () => {
describe('when there is no more time left in the frame', () => {
it('calls any callback which has timed out, waits for others', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
const callbackLog = [];
@@ -269,9 +269,9 @@ describe('SchedulerDOM', () => {
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
scheduleWork(callbackA); // won't time out
scheduleWork(callbackB, {timeout: 100}); // times out later
scheduleWork(callbackC, {timeout: 2}); // will time out fast
scheduleCallback(callbackA); // won't time out
scheduleCallback(callbackB, {timeout: 100}); // times out later
scheduleCallback(callbackC, {timeout: 2}); // will time out fast
// push time ahead a bit so that we have no idle time
advanceOneFrame({timePastFrameDeadline: 16});
@@ -295,7 +295,7 @@ describe('SchedulerDOM', () => {
describe('when there is some time left in the frame', () => {
it('calls timed out callbacks and then any more pending callbacks, defers others if time runs out', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
const callbackLog = [];
@@ -309,10 +309,10 @@ describe('SchedulerDOM', () => {
const callbackC = jest.fn(() => callbackLog.push('C'));
const callbackD = jest.fn(() => callbackLog.push('D'));
scheduleWork(callbackA, {timeout: 100}); // won't time out
scheduleWork(callbackB, {timeout: 100}); // times out later
scheduleWork(callbackC, {timeout: 2}); // will time out fast
scheduleWork(callbackD, {timeout: 200}); // won't time out
scheduleCallback(callbackA, {timeout: 100}); // won't time out
scheduleCallback(callbackB, {timeout: 100}); // times out later
scheduleCallback(callbackC, {timeout: 2}); // will time out fast
scheduleCallback(callbackD, {timeout: 200}); // won't time out
advanceOneFrame({timeLeftInFrame: 15}); // runs rAF and postMessage callbacks
@@ -341,16 +341,16 @@ describe('SchedulerDOM', () => {
});
});
describe('cancelScheduledWork', () => {
describe('cancelCallback', () => {
it('cancels the scheduled callback', () => {
const {
unstable_scheduleWork: scheduleWork,
unstable_cancelScheduledWork: cancelScheduledWork,
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
} = Scheduler;
const cb = jest.fn();
const callbackId = scheduleWork(cb);
const callbackId = scheduleCallback(cb);
expect(cb).toHaveBeenCalledTimes(0);
cancelScheduledWork(callbackId);
cancelCallback(callbackId);
advanceOneFrame({timeLeftInFrame: 15});
expect(cb).toHaveBeenCalledTimes(0);
});
@@ -358,19 +358,19 @@ describe('SchedulerDOM', () => {
describe('with multiple callbacks', () => {
it('when called more than once', () => {
const {
unstable_scheduleWork: scheduleWork,
unstable_cancelScheduledWork: cancelScheduledWork,
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => callbackLog.push('B'));
const callbackC = jest.fn(() => callbackLog.push('C'));
scheduleWork(callbackA);
const callbackId = scheduleWork(callbackB);
scheduleWork(callbackC);
cancelScheduledWork(callbackId);
cancelScheduledWork(callbackId);
cancelScheduledWork(callbackId);
scheduleCallback(callbackA);
const callbackId = scheduleCallback(callbackB);
scheduleCallback(callbackC);
cancelCallback(callbackId);
cancelCallback(callbackId);
cancelCallback(callbackId);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
advanceOneFrame({timeLeftInFrame: 15});
@@ -382,18 +382,18 @@ describe('SchedulerDOM', () => {
it('when one callback cancels the next one', () => {
const {
unstable_scheduleWork: scheduleWork,
unstable_cancelScheduledWork: cancelScheduledWork,
unstable_scheduleCallback: scheduleCallback,
unstable_cancelCallback: cancelCallback,
} = Scheduler;
const callbackLog = [];
let callbackBId;
const callbackA = jest.fn(() => {
callbackLog.push('A');
cancelScheduledWork(callbackBId);
cancelCallback(callbackBId);
});
const callbackB = jest.fn(() => callbackLog.push('B'));
scheduleWork(callbackA);
callbackBId = scheduleWork(callbackB);
scheduleCallback(callbackA);
callbackBId = scheduleCallback(callbackB);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
advanceOneFrame({timeLeftInFrame: 15});
@@ -421,7 +421,7 @@ describe('SchedulerDOM', () => {
*
*/
it('still calls all callbacks within same frame', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => callbackLog.push('A'));
const callbackB = jest.fn(() => {
@@ -434,11 +434,11 @@ describe('SchedulerDOM', () => {
throw new Error('D error');
});
const callbackE = jest.fn(() => callbackLog.push('E'));
scheduleWork(callbackA);
scheduleWork(callbackB);
scheduleWork(callbackC);
scheduleWork(callbackD);
scheduleWork(callbackE);
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(callbackE);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@@ -467,7 +467,7 @@ describe('SchedulerDOM', () => {
*
*/
it('and with some timed out callbacks, still calls all callbacks within same frame', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
@@ -480,11 +480,11 @@ describe('SchedulerDOM', () => {
throw new Error('D error');
});
const callbackE = jest.fn(() => callbackLog.push('E'));
scheduleWork(callbackA);
scheduleWork(callbackB);
scheduleWork(callbackC, {timeout: 2}); // times out fast
scheduleWork(callbackD, {timeout: 2}); // times out fast
scheduleWork(callbackE, {timeout: 2}); // times out fast
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC, {timeout: 2}); // times out fast
scheduleCallback(callbackD, {timeout: 2}); // times out fast
scheduleCallback(callbackE, {timeout: 2}); // times out fast
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@@ -513,7 +513,7 @@ describe('SchedulerDOM', () => {
*
*/
it('still calls all callbacks within same frame', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
@@ -535,11 +535,11 @@ describe('SchedulerDOM', () => {
callbackLog.push('E');
throw new Error('E error');
});
scheduleWork(callbackA);
scheduleWork(callbackB);
scheduleWork(callbackC);
scheduleWork(callbackD);
scheduleWork(callbackE);
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(callbackE);
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@@ -574,7 +574,7 @@ describe('SchedulerDOM', () => {
*
*/
it('and with all timed out callbacks, still calls all callbacks within same frame', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
const callbackLog = [];
const callbackA = jest.fn(() => {
callbackLog.push('A');
@@ -596,11 +596,11 @@ describe('SchedulerDOM', () => {
callbackLog.push('E');
throw new Error('E error');
});
scheduleWork(callbackA, {timeout: 2}); // times out fast
scheduleWork(callbackB, {timeout: 2}); // times out fast
scheduleWork(callbackC, {timeout: 2}); // times out fast
scheduleWork(callbackD, {timeout: 2}); // times out fast
scheduleWork(callbackE, {timeout: 2}); // times out fast
scheduleCallback(callbackA, {timeout: 2}); // times out fast
scheduleCallback(callbackB, {timeout: 2}); // times out fast
scheduleCallback(callbackC, {timeout: 2}); // times out fast
scheduleCallback(callbackD, {timeout: 2}); // times out fast
scheduleCallback(callbackE, {timeout: 2}); // times out fast
// Initially doesn't call anything
expect(callbackLog).toEqual([]);
catchPostMessageErrors = true;
@@ -655,7 +655,7 @@ describe('SchedulerDOM', () => {
*
*/
it('still calls all callbacks within same frame', () => {
const {unstable_scheduleWork: scheduleWork} = Scheduler;
const {unstable_scheduleCallback: scheduleCallback} = Scheduler;
startOfLatestFrame = 1000000000000;
currentTime = startOfLatestFrame - 10;
catchPostMessageErrors = true;
@@ -689,13 +689,13 @@ describe('SchedulerDOM', () => {
});
const callbackG = jest.fn(() => callbackLog.push('G'));
scheduleWork(callbackA);
scheduleWork(callbackB);
scheduleWork(callbackC);
scheduleWork(callbackD);
scheduleWork(callbackE);
scheduleWork(callbackF);
scheduleWork(callbackG);
scheduleCallback(callbackA);
scheduleCallback(callbackB);
scheduleCallback(callbackC);
scheduleCallback(callbackD);
scheduleCallback(callbackE);
scheduleCallback(callbackF);
scheduleCallback(callbackG);
// does nothing initially
expect(callbackLog).toEqual([]);
@@ -17,7 +17,8 @@ describe('Scheduling UMD bundle', () => {
});
function filterPrivateKeys(name) {
return !name.startsWith('_');
// TODO: Figure out how to forward priority levels.
return !name.startsWith('_') && !name.endsWith('Priority');
}
function validateForwardedAPIs(api, forwardedAPIs) {
+3 -3
View File
@@ -12,9 +12,9 @@ import React from 'react';
const ReactInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
const {
unstable_cancelScheduledWork,
unstable_cancelCallback,
unstable_now,
unstable_scheduleWork,
unstable_scheduleCallback,
} = ReactInternals.Scheduler;
export {unstable_cancelScheduledWork, unstable_now, unstable_scheduleWork};
export {unstable_cancelCallback, unstable_now, unstable_scheduleCallback};