18
forceFrameRate,
19
requestPaint,
20
} from './SchedulerHostConfig';
21
+import {push, pop, peek} from './SchedulerMinHeap';
22
23
// TODO: Use symbols?
24
var ImmediatePriority = 1;
41
// Never times out
42
var IDLE_PRIORITY = maxSigned31BitInt;
43
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;
50
51
// Pausing the scheduler is useful for debugging.
52
var isSchedulerPaused = false;
76
return callback(didTimeout);
77
}
78
75
-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.
94
- var callback = task.callback;
79
+function flushTask(task, callback, currentTime) {
80
var previousPriorityLevel = currentPriorityLevel;
81
var previousTask = currentTask;
82
currentPriorityLevel = task.priorityLevel;
83
currentTask = task;
99
- var continuationCallback;
84
try {
85
var didUserCallbackTimeout = task.expirationTime <= currentTime;
86
// Add an extra function to the callstack. Profiling tools can use this
87
// to infer the priority of work that appears higher in the stack.
88
+ var continuationCallback;
89
switch (currentPriorityLevel) {
90
case ImmediatePriority:
91
continuationCallback = scheduler_flushTaskAtPriority_Immediate(
118
);
119
break;
120
}
136
- } catch (error) {
137
- throw error;
121
+ return typeof continuationCallback === 'function'
122
+ ? continuationCallback
123
+ : null;
124
} finally {
125
currentPriorityLevel = previousPriorityLevel;
126
currentTask = previousTask;
127
}
142
-
143
- // A callback may return a continuation. The continuation should be scheduled
144
- // with the same priority and expiration as the just-finished callback.
145
- 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
- }
183
- }
128
}
129
130
function advanceTimers(currentTime) {
131
// 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
- );
132
+ let timer = peek(timerQueue);
133
+ while (timer !== null) {
134
+ if (timer.callback === null) {
135
+ // Timer was cancelled.
136
+ pop(timerQueue);
137
+ } else if (timer.startTime <= currentTime) {
138
+ // Timer fired. Transfer to the task queue.
139
+ pop(timerQueue);
140
+ timer.sortIndex = timer.expirationTime;
141
+ push(taskQueue, timer);
142
+ } else {
143
+ // Remaining timers are pending.
144
+ return;
145
+ }
146
+ timer = peek(timerQueue);
147
}
148
}
149
152
advanceTimers(currentTime);
153
154
if (!isHostCallbackScheduled) {
214
- if (firstTask !== null) {
155
+ if (peek(taskQueue) !== null) {
156
isHostCallbackScheduled = true;
157
requestHostCallback(flushWork);
217
- } else if (firstDelayedTask !== null) {
218
- requestHostTimeout(
219
- handleTimeout,
220
- firstDelayedTask.startTime - currentTime,
221
- );
158
+ } else {
159
+ const firstTimer = peek(timerQueue);
160
+ if (firstTimer !== null) {
161
+ requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
162
+ }
163
}
164
}
165
}
166
167
function flushWork(hasTimeRemaining, initialTime) {
227
- // Exit right away if we're currently paused
228
- if (enableSchedulerDebugging && isSchedulerPaused) {
229
- return;
230
- }
231
-
168
// We'll need a host callback the next time work is scheduled.
169
isHostCallbackScheduled = false;
170
if (isHostTimeoutScheduled) {
173
cancelHostTimeout();
174
}
175
240
- let currentTime = initialTime;
241
- advanceTimers(currentTime);
242
-
176
isPerformingWork = true;
177
try {
245
- if (!hasTimeRemaining) {
246
- // Flush all the expired callbacks without yielding.
247
- // TODO: Split flushWork into two separate functions instead of using
248
- // a boolean argument?
249
- while (
250
- firstTask !== null &&
251
- firstTask.expirationTime <= currentTime &&
252
- !(enableSchedulerDebugging && isSchedulerPaused)
178
+ let currentTime = initialTime;
179
+ advanceTimers(currentTime);
180
+ let task = peek(taskQueue);
181
+ while (task !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
182
+ if (
183
+ task.expirationTime > currentTime &&
184
+ (!hasTimeRemaining || shouldYieldToHost())
185
) {
254
- flushTask(firstTask, currentTime);
186
+ // This task hasn't expired, and we've reached the deadline.
187
+ break;
188
+ }
189
+ const callback = task.callback;
190
+ if (callback !== null) {
191
+ task.callback = null;
192
+ const continuation = flushTask(task, callback, currentTime);
193
+ if (continuation !== null) {
194
+ task.callback = continuation;
195
+ } else {
196
+ if (task === peek(taskQueue)) {
197
+ pop(taskQueue);
198
+ }
199
+ }
200
currentTime = getCurrentTime();
201
advanceTimers(currentTime);
202
+ } else {
203
+ pop(taskQueue);
204
}
258
- } else {
259
- // Keep flushing callbacks until we run out of time in the frame.
260
- if (firstTask !== null) {
261
- do {
262
- flushTask(firstTask, currentTime);
263
- currentTime = getCurrentTime();
264
- advanceTimers(currentTime);
265
- } while (
266
- firstTask !== null &&
267
- !shouldYieldToHost() &&
268
- !(enableSchedulerDebugging && isSchedulerPaused)
269
- );
270
- }
205
+ task = peek(taskQueue);
206
}
207
// Return whether there's additional work
273
- if (firstTask !== null) {
208
+ if (task !== null) {
209
return true;
210
} else {
276
- if (firstDelayedTask !== null) {
277
- requestHostTimeout(
278
- handleTimeout,
279
- firstDelayedTask.startTime - currentTime,
280
- );
211
+ let firstTimer = peek(timerQueue);
212
+ if (firstTimer !== null) {
213
+ requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
214
}
215
return false;
216
}
321
var expirationTime = startTime + timeout;
322
323
var newTask = {
324
+ id: taskIdCounter++,
325
callback,
326
priorityLevel,
327
startTime,
328
expirationTime,
395
- next: null,
396
- previous: null,
329
+ sortIndex: -1,
330
};
331
332
if (startTime > currentTime) {
333
// This is a delayed task.
401
- insertDelayedTask(newTask, startTime);
402
- if (firstTask === null && firstDelayedTask === newTask) {
334
+ newTask.sortIndex = startTime;
335
+ push(timerQueue, newTask);
336
+ if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
337
// All tasks are delayed, and this is the task with the earliest delay.
338
if (isHostTimeoutScheduled) {
339
// Cancel an existing timeout.
345
requestHostTimeout(handleTimeout, startTime - currentTime);
346
}
347
} else {
414
- insertScheduledTask(newTask, expirationTime);
348
+ newTask.sortIndex = expirationTime;
349
+ push(taskQueue, newTask);
350
// Schedule a host callback, if needed. If we're already performing work,
351
// wait until the next time we yield.
352
if (!isHostCallbackScheduled && !isPerformingWork) {
358
return newTask;
359
}
360
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
-
361
function unstable_pauseExecution() {
362
isSchedulerPaused = true;
363
}
371
}
372
373
function unstable_getFirstCallbackNode() {
507
- return firstTask;
374
+ return peek(taskQueue);
375
}
376
377
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;
378
+ // Null out the callback to indicate the task has been canceled. (Can't remove
379
+ // from the queue because you can't remove arbitrary nodes from an array based
380
+ // heap, only the first one.)
381
+ task.callback = null;
382
}
383
384
function unstable_getCurrentPriorityLevel() {
388
function unstable_shouldYield() {
389
const currentTime = getCurrentTime();
390
advanceTimers(currentTime);
391
+ const firstTask = peek(taskQueue);
392
return (
545
- (currentTask !== null &&
393
+ (firstTask !== currentTask &&
394
+ currentTask !== null &&
395
firstTask !== null &&
396
+ firstTask.callback !== null &&
397
firstTask.startTime <= currentTime &&
398
firstTask.expirationTime < currentTask.expirationTime) ||
399
shouldYieldToHost()