Skip to content

Commit aebf3b4

Browse files
authored
[Scheduler] Check for continuous input events (#22107)
* [Scheduler] Track start of current chunk Currently in `shouldYield`, we compare the current time to a deadline that is pre-computed at the beginning of the current chunk. However, since we use different deadlines depending on whether an input event is pending, it makes more sense to track the start of the current chunk and check how much time has elapsed since then. Doesn't change any behavior, just refactors the logic. * [Scheduler] Check for continuous input events `isInputPending` supports a `includeContinuous` option. When set to `true`, the method will check for pending continuous inputs, like `mousemove`, in addition to discrete ones, like `click`. We will only check for pending continuous inputs if we've blocked the main thread for a non-neglible amount of time. If we've only blocked the main thread for, say, a few frames, then we'll only check for discrete inputs. I wrote a test for this but didn't include it because we haven't yet set up the `gate` flag infra to work with Scheduler feature flags. For now, I ran the test locally. * Review nits
1 parent 152ecce commit aebf3b4

File tree

1 file changed

+57
-41
lines changed

1 file changed

+57
-41
lines changed

packages/scheduler/src/forks/Scheduler.js

+57-41
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ const localClearTimeout =
9191
const localSetImmediate =
9292
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
9393

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+
94103
function advanceTimers(currentTime) {
95104
// Check for tasks that are no longer delayed and add them to the queue.
96105
let timer = peek(timerQueue);
@@ -418,49 +427,57 @@ let taskTimeoutID = -1;
418427
// thread, like user events. By default, it yields multiple times per frame.
419428
// It does not attempt to align with frame boundaries, since most tasks don't
420429
// 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;
423435

424-
// TODO: Make this configurable
425-
// TODO: Adjust this based on priority?
426-
const maxYieldInterval = 300;
427436
let needsPaint = false;
428437

429438
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);
450470
}
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;
455471
} 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;
458476
}
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;
463477
}
478+
479+
// `isInputPending` isn't available. Yield now.
480+
return true;
464481
}
465482

466483
function requestPaint() {
@@ -486,20 +503,19 @@ function forceFrameRate(fps) {
486503
return;
487504
}
488505
if (fps > 0) {
489-
yieldInterval = Math.floor(1000 / fps);
506+
frameInterval = Math.floor(1000 / fps);
490507
} else {
491508
// reset the framerate
492-
yieldInterval = 5;
509+
frameInterval = 5;
493510
}
494511
}
495512

496513
const performWorkUntilDeadline = () => {
497514
if (scheduledHostCallback !== null) {
498515
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;
503519
const hasTimeRemaining = true;
504520

505521
// If a scheduler task throws, exit the current browser task so the

0 commit comments

Comments
 (0)