@@ -18,6 +18,7 @@ import {
18
18
forceFrameRate ,
19
19
requestPaint ,
20
20
} from './SchedulerHostConfig' ;
21
+ import { push , pop , peek } from './SchedulerMinHeap' ;
21
22
22
23
// TODO: Use symbols?
23
24
var ImmediatePriority = 1 ;
@@ -40,9 +41,12 @@ var LOW_PRIORITY_TIMEOUT = 10000;
40
41
// Never times out
41
42
var IDLE_PRIORITY = maxSigned31BitInt ;
42
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 ;
46
50
47
51
// Pausing the scheduler is useful for debugging.
48
52
var isSchedulerPaused = false ;
@@ -73,25 +77,13 @@ function scheduler_flushTaskAtPriority_Idle(callback, didTimeout) {
73
77
}
74
78
75
79
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
80
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 ;
95
87
var previousPriorityLevel = currentPriorityLevel ;
96
88
var previousTask = currentTask ;
97
89
currentPriorityLevel = task . priorityLevel ;
@@ -133,76 +125,34 @@ function flushTask(task, currentTime) {
133
125
) ;
134
126
break ;
135
127
}
136
- } catch ( error ) {
137
- throw error ;
138
128
} finally {
139
129
currentPriorityLevel = previousPriorityLevel ;
140
130
currentTask = previousTask ;
141
131
}
142
132
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.
145
134
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 ;
183
136
}
184
137
}
185
138
186
139
function advanceTimers ( currentTime ) {
187
140
// 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 ) ;
206
156
}
207
157
}
208
158
@@ -211,14 +161,14 @@ function handleTimeout(currentTime) {
211
161
advanceTimers ( currentTime ) ;
212
162
213
163
if ( ! isHostCallbackScheduled ) {
214
- if ( firstTask !== null ) {
164
+ if ( peek ( taskQueue ) !== null ) {
215
165
isHostCallbackScheduled = true ;
216
166
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
+ }
222
172
}
223
173
}
224
174
}
@@ -246,38 +196,53 @@ function flushWork(hasTimeRemaining, initialTime) {
246
196
// Flush all the expired callbacks without yielding.
247
197
// TODO: Split flushWork into two separate functions instead of using
248
198
// a boolean argument?
199
+ let task = peek ( taskQueue ) ;
249
200
while (
250
- firstTask !== null &&
251
- firstTask . expirationTime <= currentTime &&
201
+ task !== null &&
202
+ task . expirationTime <= currentTime &&
252
203
! ( enableSchedulerDebugging && isSchedulerPaused )
253
204
) {
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
+ }
255
212
currentTime = getCurrentTime ( ) ;
256
213
advanceTimers ( currentTime ) ;
214
+ task = peek ( taskQueue ) ;
257
215
}
258
216
} else {
259
217
// 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 ) {
261
220
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
+ }
263
228
currentTime = getCurrentTime ( ) ;
264
229
advanceTimers ( currentTime ) ;
230
+ task = peek ( taskQueue ) ;
265
231
} while (
266
- firstTask !== null &&
232
+ task !== null &&
267
233
! shouldYieldToHost ( ) &&
268
234
! ( enableSchedulerDebugging && isSchedulerPaused )
269
235
) ;
270
236
}
271
237
}
272
238
// Return whether there's additional work
239
+ let firstTask = peek ( taskQueue ) ;
273
240
if ( firstTask !== null ) {
274
241
return true ;
275
242
} 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 ) ;
281
246
}
282
247
return false ;
283
248
}
@@ -388,18 +353,19 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
388
353
var expirationTime = startTime + timeout ;
389
354
390
355
var newTask = {
356
+ id : taskIdCounter ++ ,
391
357
callback,
392
358
priorityLevel,
393
359
startTime,
394
360
expirationTime,
395
- next : null ,
396
- previous : null ,
361
+ sortIndex : - 1 ,
397
362
} ;
398
363
399
364
if ( startTime > currentTime ) {
400
365
// 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 ) ) {
403
369
// All tasks are delayed, and this is the task with the earliest delay.
404
370
if ( isHostTimeoutScheduled ) {
405
371
// Cancel an existing timeout.
@@ -411,7 +377,8 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
411
377
requestHostTimeout ( handleTimeout , startTime - currentTime ) ;
412
378
}
413
379
} else {
414
- insertScheduledTask ( newTask , expirationTime ) ;
380
+ newTask . sortIndex = expirationTime ;
381
+ push ( taskQueue , newTask ) ;
415
382
// Schedule a host callback, if needed. If we're already performing work,
416
383
// wait until the next time we yield.
417
384
if ( ! isHostCallbackScheduled && ! isPerformingWork ) {
@@ -423,74 +390,6 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
423
390
return newTask ;
424
391
}
425
392
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
-
494
393
function unstable_pauseExecution ( ) {
495
394
isSchedulerPaused = true ;
496
395
}
@@ -504,34 +403,14 @@ function unstable_continueExecution() {
504
403
}
505
404
506
405
function unstable_getFirstCallbackNode ( ) {
507
- return firstTask ;
406
+ return peek ( taskQueue ) ;
508
407
}
509
408
510
409
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 ;
535
414
}
536
415
537
416
function unstable_getCurrentPriorityLevel ( ) {
@@ -541,9 +420,12 @@ function unstable_getCurrentPriorityLevel() {
541
420
function unstable_shouldYield ( ) {
542
421
const currentTime = getCurrentTime ( ) ;
543
422
advanceTimers ( currentTime ) ;
423
+ const firstTask = peek ( taskQueue ) ;
544
424
return (
545
- ( currentTask !== null &&
425
+ ( firstTask !== currentTask &&
426
+ currentTask !== null &&
546
427
firstTask !== null &&
428
+ firstTask . callback !== null &&
547
429
firstTask . startTime <= currentTime &&
548
430
firstTask . expirationTime < currentTask . expirationTime ) ||
549
431
shouldYieldToHost ( )
0 commit comments