From b158495336b3cceeedeb7f48567d99dc69bc10d8 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Aug 2021 18:22:24 -0400 Subject: [PATCH] [Scheduler] Check for continuous input events `isInputPending` supports a `includesContinuous` 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 | 85 ++++++++++++++--------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/scheduler/src/forks/Scheduler.js b/packages/scheduler/src/forks/Scheduler.js index 2ac4f418154f2..62953aecec6fd 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,58 @@ 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({includesContinuous: 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; } + + // Either there's no pending input, or `isInputPending` isn't available. + // Yield now. + return true; } function requestPaint() { @@ -485,10 +502,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; } }