Skip to content

Commit 0e2c76a

Browse files
committed
[Scheduler] Store Tasks on a Min Binary Heap
Switches Scheduler's priority queue implementation (for both tasks and timers) to an array-based min binary heap. This replaces the naive linked-list implementation that was left over from the queue we once used to schedule React roots. A list was arguably fine when it was only used for roots, since the total number of roots is usually small, and is only 1 in the common case of a single-page app. Since Scheduler is now used for many types of JavaScript tasks (e.g. including timers), the total number of tasks can be much larger. Binary heaps are the standard way to implement priority queues. Insertion is O(1) in the average case (append to the end) and O(log n) in the worst. Deletion is O(log n). Peek is O(1).
1 parent 75ab53b commit 0e2c76a

File tree

2 files changed

+169
-196
lines changed

2 files changed

+169
-196
lines changed

packages/scheduler/src/Scheduler.js

+78-196
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
forceFrameRate,
1919
requestPaint,
2020
} from './SchedulerHostConfig';
21+
import {push, pop, peek} from './SchedulerMinHeap';
2122

2223
// TODO: Use symbols?
2324
var ImmediatePriority = 1;
@@ -40,9 +41,12 @@ var LOW_PRIORITY_TIMEOUT = 10000;
4041
// Never times out
4142
var IDLE_PRIORITY = maxSigned31BitInt;
4243

43-
// Tasks are stored as a circular, doubly linked list.
44-
var firstTask = null;
45-
var firstDelayedTask = null;
44+
// Tasks are stored on a min heap
45+
var taskQueue = [];
46+
var timerQueue = [];
47+
48+
// Incrementing id counter. Used to maintain insertion order.
49+
var taskIdCounter = 0;
4650

4751
// Pausing the scheduler is useful for debugging.
4852
var isSchedulerPaused = false;
@@ -73,25 +77,13 @@ function scheduler_flushTaskAtPriority_Idle(callback, didTimeout) {
7377
}
7478

7579
function flushTask(task, currentTime) {
76-
// Remove the task from the list before calling the callback. That way the
77-
// list is in a consistent state even if the callback throws.
78-
const next = task.next;
79-
if (next === task) {
80-
// This is the only scheduled task. Clear the list.
81-
firstTask = null;
82-
} else {
83-
// Remove the task from its position in the list.
84-
if (task === firstTask) {
85-
firstTask = next;
86-
}
87-
const previous = task.previous;
88-
previous.next = next;
89-
next.previous = previous;
90-
}
91-
task.next = task.previous = null;
92-
93-
// Now it's safe to execute the task.
9480
var callback = task.callback;
81+
if (callback === null) {
82+
// The task was canceled.
83+
return;
84+
}
85+
// Clearing the callback marks it as ready for removal from the task queue.
86+
task.callback = null;
9587
var previousPriorityLevel = currentPriorityLevel;
9688
var previousTask = currentTask;
9789
currentPriorityLevel = task.priorityLevel;
@@ -133,76 +125,34 @@ function flushTask(task, currentTime) {
133125
);
134126
break;
135127
}
136-
} catch (error) {
137-
throw error;
138128
} finally {
139129
currentPriorityLevel = previousPriorityLevel;
140130
currentTask = previousTask;
141131
}
142132

143-
// A callback may return a continuation. The continuation should be scheduled
144-
// with the same priority and expiration as the just-finished callback.
133+
// A callback may return a continuation.
145134
if (typeof continuationCallback === 'function') {
146-
var expirationTime = task.expirationTime;
147-
var continuationTask = task;
148-
continuationTask.callback = continuationCallback;
149-
150-
// Insert the new callback into the list, sorted by its timeout. This is
151-
// almost the same as the code in `scheduleCallback`, except the callback
152-
// is inserted into the list *before* callbacks of equal timeout instead
153-
// of after.
154-
if (firstTask === null) {
155-
// This is the first callback in the list.
156-
firstTask = continuationTask.next = continuationTask.previous = continuationTask;
157-
} else {
158-
var nextAfterContinuation = null;
159-
var t = firstTask;
160-
do {
161-
if (expirationTime <= t.expirationTime) {
162-
// This task times out at or after the continuation. We will insert
163-
// the continuation *before* this task.
164-
nextAfterContinuation = t;
165-
break;
166-
}
167-
t = t.next;
168-
} while (t !== firstTask);
169-
if (nextAfterContinuation === null) {
170-
// No equal or lower priority task was found, which means the new task
171-
// is the lowest priority task in the list.
172-
nextAfterContinuation = firstTask;
173-
} else if (nextAfterContinuation === firstTask) {
174-
// The new task is the highest priority task in the list.
175-
firstTask = continuationTask;
176-
}
177-
178-
const previous = nextAfterContinuation.previous;
179-
previous.next = nextAfterContinuation.previous = continuationTask;
180-
continuationTask.next = nextAfterContinuation;
181-
continuationTask.previous = previous;
182-
}
135+
task.callback = continuationCallback;
183136
}
184137
}
185138

186139
function advanceTimers(currentTime) {
187140
// Check for tasks that are no longer delayed and add them to the queue.
188-
if (firstDelayedTask !== null && firstDelayedTask.startTime <= currentTime) {
189-
do {
190-
const task = firstDelayedTask;
191-
const next = task.next;
192-
if (task === next) {
193-
firstDelayedTask = null;
194-
} else {
195-
firstDelayedTask = next;
196-
const previous = task.previous;
197-
previous.next = next;
198-
next.previous = previous;
199-
}
200-
task.next = task.previous = null;
201-
insertScheduledTask(task, task.expirationTime);
202-
} while (
203-
firstDelayedTask !== null &&
204-
firstDelayedTask.startTime <= currentTime
205-
);
141+
let timer = peek(timerQueue);
142+
while (timer !== null) {
143+
if (timer.callback === null) {
144+
// Timer was cancelled.
145+
pop(timerQueue);
146+
} else if (timer.startTime <= currentTime) {
147+
// Timer fired. Transfer to the task queue.
148+
pop(timerQueue);
149+
timer.sortIndex = timer.expirationTime;
150+
push(taskQueue, timer);
151+
} else {
152+
// Remaining timers are pending.
153+
return;
154+
}
155+
timer = peek(timerQueue);
206156
}
207157
}
208158

@@ -211,14 +161,14 @@ function handleTimeout(currentTime) {
211161
advanceTimers(currentTime);
212162

213163
if (!isHostCallbackScheduled) {
214-
if (firstTask !== null) {
164+
if (peek(taskQueue) !== null) {
215165
isHostCallbackScheduled = true;
216166
requestHostCallback(flushWork);
217-
} else if (firstDelayedTask !== null) {
218-
requestHostTimeout(
219-
handleTimeout,
220-
firstDelayedTask.startTime - currentTime,
221-
);
167+
} else {
168+
const firstTimer = peek(timerQueue);
169+
if (firstTimer !== null) {
170+
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
171+
}
222172
}
223173
}
224174
}
@@ -246,38 +196,53 @@ function flushWork(hasTimeRemaining, initialTime) {
246196
// Flush all the expired callbacks without yielding.
247197
// TODO: Split flushWork into two separate functions instead of using
248198
// a boolean argument?
199+
let task = peek(taskQueue);
249200
while (
250-
firstTask !== null &&
251-
firstTask.expirationTime <= currentTime &&
201+
task !== null &&
202+
task.expirationTime <= currentTime &&
252203
!(enableSchedulerDebugging && isSchedulerPaused)
253204
) {
254-
flushTask(firstTask, currentTime);
205+
flushTask(task, currentTime);
206+
// If the task completed, remove it from the queue. Need to confirm
207+
// that it's still the first task in the queue, in case additional
208+
// tasks were scheduled.
209+
if (task === peek(taskQueue) && task.callback === null) {
210+
pop(taskQueue);
211+
}
255212
currentTime = getCurrentTime();
256213
advanceTimers(currentTime);
214+
task = peek(taskQueue);
257215
}
258216
} else {
259217
// Keep flushing callbacks until we run out of time in the frame.
260-
if (firstTask !== null) {
218+
let task = peek(taskQueue);
219+
if (task !== null) {
261220
do {
262-
flushTask(firstTask, currentTime);
221+
flushTask(task, currentTime);
222+
// If the task completed, remove it from the queue. Need to confirm
223+
// that it's still the first task in the queue, in case additional
224+
// tasks were scheduled.
225+
if (task === peek(taskQueue) && task.callback === null) {
226+
pop(taskQueue);
227+
}
263228
currentTime = getCurrentTime();
264229
advanceTimers(currentTime);
230+
task = peek(taskQueue);
265231
} while (
266-
firstTask !== null &&
232+
task !== null &&
267233
!shouldYieldToHost() &&
268234
!(enableSchedulerDebugging && isSchedulerPaused)
269235
);
270236
}
271237
}
272238
// Return whether there's additional work
239+
let firstTask = peek(taskQueue);
273240
if (firstTask !== null) {
274241
return true;
275242
} else {
276-
if (firstDelayedTask !== null) {
277-
requestHostTimeout(
278-
handleTimeout,
279-
firstDelayedTask.startTime - currentTime,
280-
);
243+
let firstTimer = peek(timerQueue);
244+
if (firstTimer !== null) {
245+
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
281246
}
282247
return false;
283248
}
@@ -388,18 +353,19 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
388353
var expirationTime = startTime + timeout;
389354

390355
var newTask = {
356+
id: taskIdCounter++,
391357
callback,
392358
priorityLevel,
393359
startTime,
394360
expirationTime,
395-
next: null,
396-
previous: null,
361+
sortIndex: -1,
397362
};
398363

399364
if (startTime > currentTime) {
400365
// This is a delayed task.
401-
insertDelayedTask(newTask, startTime);
402-
if (firstTask === null && firstDelayedTask === newTask) {
366+
newTask.sortIndex = startTime;
367+
push(timerQueue, newTask);
368+
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
403369
// All tasks are delayed, and this is the task with the earliest delay.
404370
if (isHostTimeoutScheduled) {
405371
// Cancel an existing timeout.
@@ -411,7 +377,8 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
411377
requestHostTimeout(handleTimeout, startTime - currentTime);
412378
}
413379
} else {
414-
insertScheduledTask(newTask, expirationTime);
380+
newTask.sortIndex = expirationTime;
381+
push(taskQueue, newTask);
415382
// Schedule a host callback, if needed. If we're already performing work,
416383
// wait until the next time we yield.
417384
if (!isHostCallbackScheduled && !isPerformingWork) {
@@ -423,74 +390,6 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
423390
return newTask;
424391
}
425392

426-
function insertScheduledTask(newTask, expirationTime) {
427-
// Insert the new task into the list, ordered first by its timeout, then by
428-
// insertion. So the new task is inserted after any other task the
429-
// same timeout
430-
if (firstTask === null) {
431-
// This is the first task in the list.
432-
firstTask = newTask.next = newTask.previous = newTask;
433-
} else {
434-
var next = null;
435-
var task = firstTask;
436-
do {
437-
if (expirationTime < task.expirationTime) {
438-
// The new task times out before this one.
439-
next = task;
440-
break;
441-
}
442-
task = task.next;
443-
} while (task !== firstTask);
444-
445-
if (next === null) {
446-
// No task with a later timeout was found, which means the new task has
447-
// the latest timeout in the list.
448-
next = firstTask;
449-
} else if (next === firstTask) {
450-
// The new task has the earliest expiration in the entire list.
451-
firstTask = newTask;
452-
}
453-
454-
var previous = next.previous;
455-
previous.next = next.previous = newTask;
456-
newTask.next = next;
457-
newTask.previous = previous;
458-
}
459-
}
460-
461-
function insertDelayedTask(newTask, startTime) {
462-
// Insert the new task into the list, ordered by its start time.
463-
if (firstDelayedTask === null) {
464-
// This is the first task in the list.
465-
firstDelayedTask = newTask.next = newTask.previous = newTask;
466-
} else {
467-
var next = null;
468-
var task = firstDelayedTask;
469-
do {
470-
if (startTime < task.startTime) {
471-
// The new task times out before this one.
472-
next = task;
473-
break;
474-
}
475-
task = task.next;
476-
} while (task !== firstDelayedTask);
477-
478-
if (next === null) {
479-
// No task with a later timeout was found, which means the new task has
480-
// the latest timeout in the list.
481-
next = firstDelayedTask;
482-
} else if (next === firstDelayedTask) {
483-
// The new task has the earliest expiration in the entire list.
484-
firstDelayedTask = newTask;
485-
}
486-
487-
var previous = next.previous;
488-
previous.next = next.previous = newTask;
489-
newTask.next = next;
490-
newTask.previous = previous;
491-
}
492-
}
493-
494393
function unstable_pauseExecution() {
495394
isSchedulerPaused = true;
496395
}
@@ -504,34 +403,14 @@ function unstable_continueExecution() {
504403
}
505404

506405
function unstable_getFirstCallbackNode() {
507-
return firstTask;
406+
return peek(taskQueue);
508407
}
509408

510409
function unstable_cancelCallback(task) {
511-
var next = task.next;
512-
if (next === null) {
513-
// Already cancelled.
514-
return;
515-
}
516-
517-
if (task === next) {
518-
if (task === firstTask) {
519-
firstTask = null;
520-
} else if (task === firstDelayedTask) {
521-
firstDelayedTask = null;
522-
}
523-
} else {
524-
if (task === firstTask) {
525-
firstTask = next;
526-
} else if (task === firstDelayedTask) {
527-
firstDelayedTask = next;
528-
}
529-
var previous = task.previous;
530-
previous.next = next;
531-
next.previous = previous;
532-
}
533-
534-
task.next = task.previous = null;
410+
// Null out the callback to indicate the task has been canceled. (Can't remove
411+
// from the queue because you can't remove arbitrary nodes from an array based
412+
// heap, only the first one.)
413+
task.callback = null;
535414
}
536415

537416
function unstable_getCurrentPriorityLevel() {
@@ -541,9 +420,12 @@ function unstable_getCurrentPriorityLevel() {
541420
function unstable_shouldYield() {
542421
const currentTime = getCurrentTime();
543422
advanceTimers(currentTime);
423+
const firstTask = peek(taskQueue);
544424
return (
545-
(currentTask !== null &&
425+
(firstTask !== currentTask &&
426+
currentTask !== null &&
546427
firstTask !== null &&
428+
firstTask.callback !== null &&
547429
firstTask.startTime <= currentTime &&
548430
firstTask.expirationTime < currentTask.expirationTime) ||
549431
shouldYieldToHost()

0 commit comments

Comments
 (0)