@@ -91,6 +91,15 @@ const localClearTimeout =
91
91
const localSetImmediate =
92
92
typeof setImmediate !== 'undefined' ? setImmediate : null ; // IE and Node.js + jsdom
93
93
94
+ const isInputPending =
95
+ typeof navigator !== 'undefined' &&
96
+ navigator . scheduling !== undefined &&
97
+ navigator . scheduling . isInputPending !== undefined
98
+ ? navigator . scheduling . isInputPending . bind ( navigator . scheduling )
99
+ : null ;
100
+
101
+ const continuousOptions = { includeContinuous : true } ;
102
+
94
103
function advanceTimers ( currentTime ) {
95
104
// Check for tasks that are no longer delayed and add them to the queue.
96
105
let timer = peek ( timerQueue ) ;
@@ -418,49 +427,57 @@ let taskTimeoutID = -1;
418
427
// thread, like user events. By default, it yields multiple times per frame.
419
428
// It does not attempt to align with frame boundaries, since most tasks don't
420
429
// need to be frame aligned; for those that do, use requestAnimationFrame.
421
- let yieldInterval = 5 ;
422
- let deadline = 0 ;
430
+ // TODO: Make these configurable
431
+ let frameInterval = 5 ;
432
+ const continuousInputInterval = 50 ;
433
+ const maxInterval = 300 ;
434
+ let startTime = - 1 ;
423
435
424
- // TODO: Make this configurable
425
- // TODO: Adjust this based on priority?
426
- const maxYieldInterval = 300 ;
427
436
let needsPaint = false ;
428
437
429
438
function shouldYieldToHost ( ) {
430
- if (
431
- enableIsInputPending &&
432
- navigator !== undefined &&
433
- navigator . scheduling !== undefined &&
434
- navigator . scheduling . isInputPending !== undefined
435
- ) {
436
- const scheduling = navigator . scheduling ;
437
- const currentTime = getCurrentTime ( ) ;
438
- if ( currentTime >= deadline ) {
439
- // There's no time left. We may want to yield control of the main
440
- // thread, so the browser can perform high priority tasks. The main ones
441
- // are painting and user input. If there's a pending paint or a pending
442
- // input, then we should yield. But if there's neither, then we can
443
- // yield less often while remaining responsive. We'll eventually yield
444
- // regardless, since there could be a pending paint that wasn't
445
- // accompanied by a call to `requestPaint`, or other main thread tasks
446
- // like network events.
447
- if ( needsPaint || scheduling . isInputPending ( ) ) {
448
- // There is either a pending paint or a pending input.
449
- return true ;
439
+ const timeElapsed = getCurrentTime ( ) - startTime ;
440
+ if ( timeElapsed < frameInterval ) {
441
+ // The main thread has only been blocked for a really short amount of time;
442
+ // smaller than a single frame. Don't yield yet.
443
+ return false ;
444
+ }
445
+
446
+ // The main thread has been blocked for a non-negligible amount of time. We
447
+ // may want to yield control of the main thread, so the browser can perform
448
+ // high priority tasks. The main ones are painting and user input. If there's
449
+ // a pending paint or a pending input, then we should yield. But if there's
450
+ // neither, then we can yield less often while remaining responsive. We'll
451
+ // eventually yield regardless, since there could be a pending paint that
452
+ // wasn't accompanied by a call to `requestPaint`, or other main thread tasks
453
+ // like network events.
454
+ if ( enableIsInputPending ) {
455
+ if ( needsPaint ) {
456
+ // There's a pending paint (signaled by `requestPaint`). Yield now.
457
+ return true ;
458
+ }
459
+ if ( timeElapsed < continuousInputInterval ) {
460
+ // We haven't blocked the thread for that long. Only yield if there's a
461
+ // pending discrete input (e.g. click). It's OK if there's pending
462
+ // continuous input (e.g. mouseover).
463
+ if ( isInputPending !== null ) {
464
+ return isInputPending ( ) ;
465
+ }
466
+ } else if ( timeElapsed < maxInterval ) {
467
+ // Yield if there's either a pending discrete or continuous input.
468
+ if ( isInputPending !== null ) {
469
+ return isInputPending ( continuousOptions ) ;
450
470
}
451
- // There's no pending input. Only yield if we've reached the max
452
- // yield interval.
453
- const timeElapsed = currentTime - ( deadline - yieldInterval ) ;
454
- return timeElapsed >= maxYieldInterval ;
455
471
} else {
456
- // There's still time left in the frame.
457
- return false ;
472
+ // We've blocked the thread for a long time. Even if there's no pending
473
+ // input, there may be some other scheduled work that we don't know about,
474
+ // like a network event. Yield now.
475
+ return true ;
458
476
}
459
- } else {
460
- // `isInputPending` is not available. Since we have no way of knowing if
461
- // there's pending input, always yield at the end of the frame.
462
- return getCurrentTime ( ) >= deadline ;
463
477
}
478
+
479
+ // `isInputPending` isn't available. Yield now.
480
+ return true ;
464
481
}
465
482
466
483
function requestPaint ( ) {
@@ -486,20 +503,19 @@ function forceFrameRate(fps) {
486
503
return ;
487
504
}
488
505
if ( fps > 0 ) {
489
- yieldInterval = Math . floor ( 1000 / fps ) ;
506
+ frameInterval = Math . floor ( 1000 / fps ) ;
490
507
} else {
491
508
// reset the framerate
492
- yieldInterval = 5 ;
509
+ frameInterval = 5 ;
493
510
}
494
511
}
495
512
496
513
const performWorkUntilDeadline = ( ) => {
497
514
if ( scheduledHostCallback !== null ) {
498
515
const currentTime = getCurrentTime ( ) ;
499
- // Yield after `yieldInterval` ms, regardless of where we are in the vsync
500
- // cycle. This means there's always time remaining at the beginning of
501
- // the message event.
502
- deadline = currentTime + yieldInterval ;
516
+ // Keep track of the start time so we can measure how long the main thread
517
+ // has been blocked.
518
+ startTime = currentTime ;
503
519
const hasTimeRemaining = true ;
504
520
505
521
// If a scheduler task throws, exit the current browser task so the
0 commit comments