From 4f7d2579fef31ccd5b8c2656a6551f760a567a2b Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Aug 2021 17:06:20 -0400 Subject: [PATCH 1/3] [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. --- packages/scheduler/src/forks/Scheduler.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/scheduler/src/forks/Scheduler.js b/packages/scheduler/src/forks/Scheduler.js index 8d1f1f994fe8e..2ac4f418154f2 100644 --- a/packages/scheduler/src/forks/Scheduler.js +++ b/packages/scheduler/src/forks/Scheduler.js @@ -419,7 +419,7 @@ let taskTimeoutID = -1; // It does not attempt to align with frame boundaries, since most tasks don't // need to be frame aligned; for those that do, use requestAnimationFrame. let yieldInterval = 5; -let deadline = 0; +let startTime = -1; // TODO: Make this configurable // TODO: Adjust this based on priority? @@ -427,6 +427,7 @@ const maxYieldInterval = 300; let needsPaint = false; function shouldYieldToHost() { + const timeElapsed = getCurrentTime() - startTime; if ( enableIsInputPending && navigator !== undefined && @@ -434,8 +435,7 @@ function shouldYieldToHost() { navigator.scheduling.isInputPending !== undefined ) { const scheduling = navigator.scheduling; - const currentTime = getCurrentTime(); - if (currentTime >= deadline) { + if (timeElapsed >= yieldInterval) { // There's no time left. We may want to yield control of the main // thread, so the browser can perform high priority tasks. The main ones // are painting and user input. If there's a pending paint or a pending @@ -450,7 +450,6 @@ function shouldYieldToHost() { } // There's no pending input. Only yield if we've reached the max // yield interval. - const timeElapsed = currentTime - (deadline - yieldInterval); return timeElapsed >= maxYieldInterval; } else { // There's still time left in the frame. @@ -459,7 +458,7 @@ function shouldYieldToHost() { } else { // `isInputPending` is not available. Since we have no way of knowing if // there's pending input, always yield at the end of the frame. - return getCurrentTime() >= deadline; + return timeElapsed >= yieldInterval; } } @@ -499,7 +498,7 @@ const performWorkUntilDeadline = () => { // Yield after `yieldInterval` ms, regardless of where we are in the vsync // cycle. This means there's always time remaining at the beginning of // the message event. - deadline = currentTime + yieldInterval; + startTime = currentTime; const hasTimeRemaining = true; // If a scheduler task throws, exit the current browser task so the From e28bcce403307e6ca758f50841175929bd8181ef Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Aug 2021 18:22:24 -0400 Subject: [PATCH 2/3] [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. --- packages/scheduler/src/forks/Scheduler.js | 84 ++++++++++++++--------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/scheduler/src/forks/Scheduler.js b/packages/scheduler/src/forks/Scheduler.js index 2ac4f418154f2..1ed50ab0a0497 100644 --- a/packages/scheduler/src/forks/Scheduler.js +++ b/packages/scheduler/src/forks/Scheduler.js @@ -91,6 +91,13 @@ const localClearTimeout = const localSetImmediate = typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom +const isInputPending = + typeof navigator !== 'undefined' && + navigator.scheduling !== undefined && + navigator.scheduling.isInputPending !== undefined + ? navigator.scheduling.isInputPending.bind(navigator.scheduling) + : null; + function advanceTimers(currentTime) { // Check for tasks that are no longer delayed and add them to the queue. let timer = peek(timerQueue); @@ -418,48 +425,57 @@ let taskTimeoutID = -1; // thread, like user events. By default, it yields multiple times per frame. // It does not attempt to align with frame boundaries, since most tasks don't // need to be frame aligned; for those that do, use requestAnimationFrame. -let yieldInterval = 5; +// TODO: Make these configurable +let frameInterval = 5; +const continuousInputInterval = 50; +const maxInterval = 300; let startTime = -1; -// TODO: Make this configurable -// TODO: Adjust this based on priority? -const maxYieldInterval = 300; let needsPaint = false; function shouldYieldToHost() { const timeElapsed = getCurrentTime() - startTime; - if ( - enableIsInputPending && - navigator !== undefined && - navigator.scheduling !== undefined && - navigator.scheduling.isInputPending !== undefined - ) { - const scheduling = navigator.scheduling; - if (timeElapsed >= yieldInterval) { - // There's no time left. We may want to yield control of the main - // thread, so the browser can perform high priority tasks. The main ones - // are painting and user input. If there's a pending paint or a pending - // input, then we should yield. But if there's neither, then we can - // yield less often while remaining responsive. We'll eventually yield - // regardless, since there could be a pending paint that wasn't - // accompanied by a call to `requestPaint`, or other main thread tasks - // like network events. - if (needsPaint || scheduling.isInputPending()) { - // There is either a pending paint or a pending input. - return true; + if (timeElapsed < frameInterval) { + // The main thread has only been blocked for a really short amount of time; + // smaller than a single frame. Don't yield yet. + return false; + } + + // The main thread has been blocked for a non-negligible amount of time. We + // may want to yield control of the main thread, so the browser can perform + // high priority tasks. The main ones are painting and user input. If there's + // a pending paint or a pending input, then we should yield. But if there's + // neither, then we can yield less often while remaining responsive. We'll + // eventually yield regardless, since there could be a pending paint that + // wasn't accompanied by a call to `requestPaint`, or other main thread tasks + // like network events. + if (enableIsInputPending) { + if (needsPaint) { + // There's a pending paint (signaled by `requestPaint`). Yield now. + return true; + } + if (timeElapsed < continuousInputInterval) { + // We haven't blocked the thread for that long. Only yield if there's a + // pending discrete input (e.g. click). It's OK if there's pending + // continuous input (e.g. mouseover). + if (isInputPending !== null) { + return isInputPending(); + } + } else if (timeElapsed < maxInterval) { + // Yield if there's either a pending discrete or continuous input. + if (isInputPending !== null) { + return isInputPending({includeContinuous: true}); } - // There's no pending input. Only yield if we've reached the max - // yield interval. - return timeElapsed >= maxYieldInterval; } else { - // There's still time left in the frame. - return false; + // We've blocked the thread for a long time. Even if there's no pending + // input, there may be some other scheduled work that we don't know about, + // like a network event. Yield now. + return true; } - } else { - // `isInputPending` is not available. Since we have no way of knowing if - // there's pending input, always yield at the end of the frame. - return timeElapsed >= yieldInterval; } + + // `isInputPending` isn't available. Yield now. + return true; } function requestPaint() { @@ -485,10 +501,10 @@ function forceFrameRate(fps) { return; } if (fps > 0) { - yieldInterval = Math.floor(1000 / fps); + frameInterval = Math.floor(1000 / fps); } else { // reset the framerate - yieldInterval = 5; + frameInterval = 5; } } From 5cce438f24b492dc0ecd9d522bebc8bb94ee4d7a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Aug 2021 18:42:22 -0400 Subject: [PATCH 3/3] Review nits --- packages/scheduler/src/forks/Scheduler.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/scheduler/src/forks/Scheduler.js b/packages/scheduler/src/forks/Scheduler.js index 1ed50ab0a0497..c32ef04ca3929 100644 --- a/packages/scheduler/src/forks/Scheduler.js +++ b/packages/scheduler/src/forks/Scheduler.js @@ -98,6 +98,8 @@ const isInputPending = ? navigator.scheduling.isInputPending.bind(navigator.scheduling) : null; +const continuousOptions = {includeContinuous: true}; + function advanceTimers(currentTime) { // Check for tasks that are no longer delayed and add them to the queue. let timer = peek(timerQueue); @@ -464,7 +466,7 @@ function shouldYieldToHost() { } else if (timeElapsed < maxInterval) { // Yield if there's either a pending discrete or continuous input. if (isInputPending !== null) { - return isInputPending({includeContinuous: true}); + return isInputPending(continuousOptions); } } else { // We've blocked the thread for a long time. Even if there's no pending @@ -511,9 +513,8 @@ function forceFrameRate(fps) { const performWorkUntilDeadline = () => { if (scheduledHostCallback !== null) { const currentTime = getCurrentTime(); - // Yield after `yieldInterval` ms, regardless of where we are in the vsync - // cycle. This means there's always time remaining at the beginning of - // the message event. + // Keep track of the start time so we can measure how long the main thread + // has been blocked. startTime = currentTime; const hasTimeRemaining = true;