Skip to content

Commit 1268872

Browse files
committed
[scheduler] Priority levels, continuations, and wrapped callbacks
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.
1 parent 2c7b78f commit 1268872

2 files changed

Lines changed: 528 additions & 72 deletions

File tree

packages/scheduler/src/Scheduler.js

Lines changed: 212 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,34 @@
88

99
/* eslint-disable no-var */
1010

11-
// TODO: Currently there's only a single priority level, Deferred. Will add
12-
// additional priorities.
13-
var DEFERRED_TIMEOUT = 5000;
11+
// TODO: Use symbols?
12+
var ImmediatePriority = 1;
13+
var InteractivePriority = 2;
14+
var DefaultPriority = 3;
15+
var MaybePriority = 4;
16+
17+
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
18+
// Math.pow(2, 30) - 1
19+
// 0b111111111111111111111111111111
20+
var maxSigned31BitInt = 1073741823;
21+
22+
// Times out immediately
23+
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
24+
// Eventually times out
25+
var INTERACTIVE_PRIORITY_TIMEOUT = 250;
26+
var DEFAULT_PRIORITY_TIMEOUT = 5000;
27+
// Never times out
28+
var MAYBE_PRIORITY_TIMEOUT = maxSigned31BitInt;
1429

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

18-
var isPerformingWork = false;
33+
var currentPriorityLevel = DefaultPriority;
34+
var currentEventStartTime = -1;
35+
var currentExpirationTime = -1;
36+
37+
// This is set when a callback is being executed, to prevent re-entrancy.
38+
var isExecutingCallback = false;
1939

2040
var isHostCallbackScheduled = false;
2141

@@ -25,6 +45,14 @@ var hasNativePerformanceNow =
2545
var timeRemaining;
2646
if (hasNativePerformanceNow) {
2747
timeRemaining = function() {
48+
if (
49+
firstCallbackNode !== null &&
50+
firstCallbackNode.expirationTime < currentExpirationTime
51+
) {
52+
// A higher priority callback was scheduled. Yield so we can switch to
53+
// working on that.
54+
return 0;
55+
}
2856
// We assume that if we have a performance timer that the rAF callback
2957
// gets a performance timer value. Not sure if this is always true.
3058
var remaining = getFrameDeadline() - performance.now();
@@ -33,6 +61,12 @@ if (hasNativePerformanceNow) {
3361
} else {
3462
timeRemaining = function() {
3563
// Fallback to Date.now()
64+
if (
65+
firstCallbackNode !== null &&
66+
firstCallbackNode.expirationTime < currentExpirationTime
67+
) {
68+
return 0;
69+
}
3670
var remaining = getFrameDeadline() - Date.now();
3771
return remaining > 0 ? remaining : 0;
3872
};
@@ -44,22 +78,22 @@ var deadlineObject = {
4478
};
4579

4680
function ensureHostCallbackIsScheduled() {
47-
if (isPerformingWork) {
81+
if (isExecutingCallback) {
4882
// Don't schedule work yet; wait until the next time we yield.
4983
return;
5084
}
51-
// Schedule the host callback using the earliest timeout in the list.
52-
var timesOutAt = firstCallbackNode.timesOutAt;
85+
// Schedule the host callback using the earliest expiration in the list.
86+
var expirationTime = firstCallbackNode.expirationTime;
5387
if (!isHostCallbackScheduled) {
5488
isHostCallbackScheduled = true;
5589
} else {
5690
// Cancel the existing host callback.
5791
cancelCallback();
5892
}
59-
requestCallback(flushWork, timesOutAt);
93+
requestCallback(flushWork, expirationTime);
6094
}
6195

62-
function flushFirstCallback(node) {
96+
function flushFirstCallback() {
6397
var flushedNode = firstCallbackNode;
6498

6599
// Remove the node from the list before calling the callback. That way the
@@ -70,20 +104,101 @@ function flushFirstCallback(node) {
70104
firstCallbackNode = null;
71105
next = null;
72106
} else {
73-
var previous = firstCallbackNode.previous;
74-
firstCallbackNode = previous.next = next;
75-
next.previous = previous;
107+
var lastCallbackNode = firstCallbackNode.previous;
108+
firstCallbackNode = lastCallbackNode.next = next;
109+
next.previous = lastCallbackNode;
76110
}
77111

78112
flushedNode.next = flushedNode.previous = null;
79113

80114
// Now it's safe to call the callback.
81115
var callback = flushedNode.callback;
82-
callback(deadlineObject);
116+
var expirationTime = flushedNode.expirationTime;
117+
var priorityLevel = flushedNode.priorityLevel;
118+
var previousPriorityLevel = currentPriorityLevel;
119+
var previousExpirationTime = currentExpirationTime;
120+
currentPriorityLevel = priorityLevel;
121+
currentExpirationTime = expirationTime;
122+
var continuationCallback;
123+
try {
124+
continuationCallback = callback(deadlineObject);
125+
} finally {
126+
currentPriorityLevel = previousPriorityLevel;
127+
currentExpirationTime = previousExpirationTime;
128+
}
129+
130+
if (typeof continuationCallback === 'function') {
131+
var continuationNode: CallbackNode = {
132+
callback: continuationCallback,
133+
priorityLevel,
134+
expirationTime,
135+
next: null,
136+
previous: null,
137+
};
138+
139+
// Insert the new callback into the list, sorted by its timeout.
140+
if (firstCallbackNode === null) {
141+
// This is the first callback in the list.
142+
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
143+
} else {
144+
var nextAfterContinuation = null;
145+
var node = firstCallbackNode;
146+
do {
147+
if (node.expirationTime >= expirationTime) {
148+
// This callback is equal or lower priority than the new one.
149+
nextAfterContinuation = node;
150+
break;
151+
}
152+
node = node.next;
153+
} while (node !== firstCallbackNode);
154+
155+
if (nextAfterContinuation === null) {
156+
// No equal or lower priority callback was found, which means the new
157+
// callback is the lowest priority callback in the list.
158+
nextAfterContinuation = firstCallbackNode;
159+
} else if (nextAfterContinuation === firstCallbackNode) {
160+
// The new callback is the highest priority callback in the list.
161+
firstCallbackNode = continuationNode;
162+
ensureHostCallbackIsScheduled(firstCallbackNode);
163+
}
164+
165+
var previous = nextAfterContinuation.previous;
166+
previous.next = nextAfterContinuation.previous = continuationNode;
167+
continuationNode.next = nextAfterContinuation;
168+
continuationNode.previous = previous;
169+
}
170+
}
171+
}
172+
173+
function flushImmediateWork() {
174+
if (
175+
currentEventStartTime === -1 &&
176+
firstCallbackNode !== null &&
177+
firstCallbackNode.priorityLevel === ImmediatePriority
178+
) {
179+
isExecutingCallback = true;
180+
deadlineObject.didTimeout = true;
181+
try {
182+
do {
183+
flushFirstCallback();
184+
} while (
185+
firstCallbackNode !== null &&
186+
firstCallbackNode.priorityLevel === ImmediatePriority
187+
);
188+
} finally {
189+
isExecutingCallback = false;
190+
if (firstCallbackNode !== null) {
191+
// There's still work remaining. Request another callback.
192+
ensureHostCallbackIsScheduled(firstCallbackNode);
193+
} else {
194+
isHostCallbackScheduled = false;
195+
}
196+
}
197+
}
83198
}
84199

85200
function flushWork(didTimeout) {
86-
isPerformingWork = true;
201+
isExecutingCallback = true;
87202
deadlineObject.didTimeout = didTimeout;
88203
try {
89204
if (didTimeout) {
@@ -93,12 +208,12 @@ function flushWork(didTimeout) {
93208
// earlier than that time. Then read the current time again and repeat.
94209
// This optimizes for as few performance.now calls as possible.
95210
var currentTime = getCurrentTime();
96-
if (firstCallbackNode.timesOutAt <= currentTime) {
211+
if (firstCallbackNode.expirationTime <= currentTime) {
97212
do {
98213
flushFirstCallback();
99214
} while (
100215
firstCallbackNode !== null &&
101-
firstCallbackNode.timesOutAt <= currentTime
216+
firstCallbackNode.expirationTime <= currentTime
102217
);
103218
continue;
104219
}
@@ -116,36 +231,93 @@ function flushWork(didTimeout) {
116231
}
117232
}
118233
} finally {
119-
isPerformingWork = false;
234+
isExecutingCallback = false;
120235
if (firstCallbackNode !== null) {
121236
// There's still work remaining. Request another callback.
122237
ensureHostCallbackIsScheduled(firstCallbackNode);
123238
} else {
124239
isHostCallbackScheduled = false;
125240
}
241+
flushImmediateWork();
242+
}
243+
}
244+
245+
function unstable_runWithPriority(eventHandler, priorityLevel) {
246+
switch (priorityLevel) {
247+
case ImmediatePriority:
248+
case InteractivePriority:
249+
case DefaultPriority:
250+
case MaybePriority:
251+
break;
252+
default:
253+
priorityLevel = DefaultPriority;
254+
}
255+
256+
var previousPriorityLevel = currentPriorityLevel;
257+
var previousEventStartTime = currentEventStartTime;
258+
currentPriorityLevel = priorityLevel;
259+
currentEventStartTime = getCurrentTime();
260+
261+
try {
262+
return eventHandler();
263+
} finally {
264+
currentPriorityLevel = previousPriorityLevel;
265+
currentEventStartTime = previousEventStartTime;
266+
flushImmediateWork();
126267
}
127268
}
128269

129-
function unstable_scheduleWork(callback, options) {
130-
var currentTime = getCurrentTime();
270+
function unstable_wrap(callback) {
271+
var parentPriorityLevel = currentPriorityLevel;
272+
return function() {
273+
var previousPriorityLevel = currentPriorityLevel;
274+
var previousEventStartTime = currentEventStartTime;
275+
currentPriorityLevel = parentPriorityLevel;
276+
currentEventStartTime = getCurrentTime();
277+
278+
try {
279+
return callback.apply(this, arguments);
280+
} finally {
281+
currentPriorityLevel = previousPriorityLevel;
282+
currentEventStartTime = previousEventStartTime;
283+
flushImmediateWork();
284+
}
285+
};
286+
}
131287

132-
var timesOutAt;
288+
function unstable_scheduleWork(callback, deprecated_options) {
289+
var startTime =
290+
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
291+
292+
var expirationTime;
133293
if (
134-
options !== undefined &&
135-
options !== null &&
136-
options.timeout !== null &&
137-
options.timeout !== undefined
294+
typeof deprecated_options === 'object' &&
295+
deprecated_options !== null &&
296+
typeof deprecated_options.timeout === 'number'
138297
) {
139-
// Check for an explicit timeout
140-
timesOutAt = currentTime + options.timeout;
298+
// FIXME: Remove this branch once we lift expiration times out of React.
299+
expirationTime = startTime + deprecated_options.timeout;
141300
} else {
142-
// Compute an absolute timeout using the default constant.
143-
timesOutAt = currentTime + DEFERRED_TIMEOUT;
301+
switch (currentPriorityLevel) {
302+
case ImmediatePriority:
303+
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
304+
break;
305+
case InteractivePriority:
306+
expirationTime = startTime + INTERACTIVE_PRIORITY_TIMEOUT;
307+
break;
308+
case MaybePriority:
309+
expirationTime = startTime + MAYBE_PRIORITY_TIMEOUT;
310+
break;
311+
case DefaultPriority:
312+
default:
313+
expirationTime = startTime + DEFAULT_PRIORITY_TIMEOUT;
314+
}
144315
}
145316

146317
var newNode = {
147318
callback,
148-
timesOutAt,
319+
priorityLevel: currentPriorityLevel,
320+
expirationTime,
149321
next: null,
150322
previous: null,
151323
};
@@ -159,20 +331,20 @@ function unstable_scheduleWork(callback, options) {
159331
var next = null;
160332
var node = firstCallbackNode;
161333
do {
162-
if (node.timesOutAt > timesOutAt) {
163-
// The new callback times out before this one.
334+
if (node.expirationTime > expirationTime) {
335+
// The new callback expires before this one.
164336
next = node;
165337
break;
166338
}
167339
node = node.next;
168340
} while (node !== firstCallbackNode);
169341

170342
if (next === null) {
171-
// No callback with a later timeout was found, which means the new
172-
// callback has the latest timeout in the list.
343+
// No callback with a later expiration was found, which means the new
344+
// callback has the latest expiration in the list.
173345
next = firstCallbackNode;
174346
} else if (next === firstCallbackNode) {
175-
// The new callback has the earliest timeout in the entire list.
347+
// The new callback has the earliest expiration in the entire list.
176348
firstCallbackNode = newNode;
177349
ensureHostCallbackIsScheduled(firstCallbackNode);
178350
}
@@ -299,6 +471,7 @@ if (typeof window === 'undefined') {
299471
getFrameDeadline = impl[2];
300472
} else {
301473
if (typeof console !== 'undefined') {
474+
// TODO: Remove fb.me link
302475
if (typeof localRequestAnimationFrame !== 'function') {
303476
console.error(
304477
"This browser doesn't support requestAnimationFrame. " +
@@ -441,7 +614,12 @@ if (typeof window === 'undefined') {
441614
}
442615

443616
export {
617+
ImmediatePriority as unstable_ImmediatePriority,
618+
InteractivePriority as unstable_InteractivePriority,
619+
DefaultPriority as unstable_DefaultPriority,
620+
unstable_runWithPriority,
444621
unstable_scheduleWork,
445622
unstable_cancelScheduledWork,
623+
unstable_wrap,
446624
getCurrentTime as unstable_now,
447625
};

0 commit comments

Comments
 (0)