Skip to content

Commit

Permalink
[scheduler] Priority levels, continuations, and wrapped callbacks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Sep 25, 2018
1 parent 2c7b78f commit 1268872
Show file tree
Hide file tree
Showing 2 changed files with 528 additions and 72 deletions.
246 changes: 212 additions & 34 deletions packages/scheduler/src/Scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 DefaultPriority = 3;
var MaybePriority = 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 DEFAULT_PRIORITY_TIMEOUT = 5000;
// Never times out
var MAYBE_PRIORITY_TIMEOUT = maxSigned31BitInt;

// Callbacks are stored as a circular, doubly linked list.
var firstCallbackNode = null;

var isPerformingWork = false;
var currentPriorityLevel = DefaultPriority;
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;

Expand All @@ -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();
Expand All @@ -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;
};
Expand All @@ -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();
}
requestCallback(flushWork, timesOutAt);
requestCallback(flushWork, expirationTime);
}

function flushFirstCallback(node) {
function flushFirstCallback() {
var flushedNode = firstCallbackNode;

// Remove the node from the list before calling the callback. That way the
Expand All @@ -70,20 +104,101 @@ 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;
}

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 timeout.
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 is equal or lower priority than the new one.
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 (
currentEventStartTime === -1 &&
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
) {
isExecutingCallback = true;
deadlineObject.didTimeout = true;
try {
do {
flushFirstCallback();
} while (
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) {
Expand All @@ -93,12 +208,12 @@ function flushWork(didTimeout) {
// 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;
}
Expand All @@ -116,36 +231,93 @@ function flushWork(didTimeout) {
}
}
} finally {
isPerformingWork = false;
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled(firstCallbackNode);
} else {
isHostCallbackScheduled = false;
}
flushImmediateWork();
}
}

function unstable_runWithPriority(eventHandler, priorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
case InteractivePriority:
case DefaultPriority:
case MaybePriority:
break;
default:
priorityLevel = DefaultPriority;
}

var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = priorityLevel;
currentEventStartTime = getCurrentTime();

try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
flushImmediateWork();
}
}

function unstable_scheduleWork(callback, options) {
var currentTime = getCurrentTime();
function unstable_wrap(callback) {
var parentPriorityLevel = currentPriorityLevel;
return function() {
var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = parentPriorityLevel;
currentEventStartTime = getCurrentTime();

try {
return callback.apply(this, arguments);
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
flushImmediateWork();
}
};
}

var timesOutAt;
function unstable_scheduleWork(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 MaybePriority:
expirationTime = startTime + MAYBE_PRIORITY_TIMEOUT;
break;
case DefaultPriority:
default:
expirationTime = startTime + DEFAULT_PRIORITY_TIMEOUT;
}
}

var newNode = {
callback,
timesOutAt,
priorityLevel: currentPriorityLevel,
expirationTime,
next: null,
previous: null,
};
Expand All @@ -159,20 +331,20 @@ 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;
}
node = node.next;
} 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);
}
Expand Down Expand Up @@ -299,6 +471,7 @@ if (typeof window === 'undefined') {
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. " +
Expand Down Expand Up @@ -441,7 +614,12 @@ if (typeof window === 'undefined') {
}

export {
ImmediatePriority as unstable_ImmediatePriority,
InteractivePriority as unstable_InteractivePriority,
DefaultPriority as unstable_DefaultPriority,
unstable_runWithPriority,
unstable_scheduleWork,
unstable_cancelScheduledWork,
unstable_wrap,
getCurrentTime as unstable_now,
};

0 comments on commit 1268872

Please sign in to comment.