Repository: facebook/react
Pull request: [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-negligible 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.

Changed files:
filename: packages/scheduler/src/forks/Scheduler.js
patch: @@ -91,6 +91,15 @@ 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;
+
+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);
@@ -418,49 +427,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;
-let deadline = 0;
+// 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() {
-  if (
-    enableIsInputPending &&
-    navigator !== undefined &&
-    navigator.scheduling !== undefined &&
-    navigator.scheduling.isInputPending !== undefined
-  ) {
-    const scheduling = navigator.scheduling;
-    const currentTime = getCurrentTime();
-    if (currentTime >= deadline) {
-      // 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;
+  const timeElapsed = getCurrentTime() - startTime;
+  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(continuousOptions);
       }
-      // 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.
-      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 getCurrentTime() >= deadline;
   }
+
+  // `isInputPending` isn't available. Yield now.
+  return true;
 }
 
 function requestPaint() {
@@ -486,20 +503,19 @@ 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;
   }
 }
 
 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.
-    deadline = currentTime + yieldInterval;
+    // Keep track of the start time so we can measure how long the main thread
+    // has been blocked.
+    startTime = currentTime;
     const hasTimeRemaining = true;
 
     // If a scheduler task throws, exit the current browser task so the