Skip to content

Commit

Permalink
[Scheduler] Check for continuous input events (facebook#22107)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
acdlite authored and zhengjitf committed Apr 15, 2022
1 parent fe19f41 commit cc6237c
Showing 1 changed file with 57 additions and 41 deletions.
98 changes: 57 additions & 41 deletions packages/scheduler/src/forks/Scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand Down

0 comments on commit cc6237c

Please sign in to comment.