-
Notifications
You must be signed in to change notification settings - Fork 49.9k
[Scheduler] shouldYield should always return false from inside an expired task #17968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The mock Scheduler that we use in our tests has its own fake timer implementation. The `unstable_advanceTime` method advances the timeline. Currently, a call to `unstable_advanceTime` will also flush any pending expired work. But that's not how it works in the browser: when a timer fires, the corresponding task is added to the Scheduler queue. However, we will still wait until the next message event before flushing it. This commit changes `unstable_advanceTime` to more closely resemble the browser behavior, by removing the automatic flushing of expired work. ```js // Before this commit Scheduler.unstable_advanceTime(ms); // Equivalent behavior after this commit Scheduler.unstable_advanceTime(ms); Scheduler.unstable_flushExpired(); ``` The general principle is to prefer separate APIs for scheduling tasks and flushing them. This change does not affect any public APIs. `unstable_advanceTime` is only used by our own test suite. It is not used by `act`. However, we may need to update tests in www, like Relay's.
The current behavior of shouldYield is unspecified except in cases that happen to be relied on by the React work loop's implementation. However, some of the unspecified cases are important and the consequences of assuming the wrong behavior are really bad. For example, it's natural to assume that `shouldYield` returns false when called from within an expired task. But the current behavior usually does that, unless there's a higher priority task or a main thread task (i.e. at the end of the 5ms message loop interval). This fixes `shouldYield` so that it always returns false inside an expired task, regardless of the other tasks in the queue.
Since the queue is already sorted by priority, it's faster and less error-prone for `shouldYield` to check if the current task is at the head of the list. The only caveat is that a higher priority task may have been canceled, but is still part of the queue. This is because we don't always remove canceled tasks from the queue immediately, because deletion of an arbitrary node from a binary heap is O(log n). We address this by popping high priority canceled tasks from the queue upon the next call to `shouldYield`. Since this is an edge case, and since it prevents us from yielding unnecessarily, the shifted cost is worth it. It's not extra work because we would have done this anyway the next time we enter the Scheduler work loop. Another argument in favor is that it's better to do this work during a yieldy, default priority task than at the beginning of a blocking, high priority task.
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 776b4e4:
|
|
It seems to me that shouldYield is already way above its budget for such a hot function. I'd expect a single atomic read basically. It also seems to me that we're getting rid of the notion of "expiration time" in the scheduler since we won't have a timeoutMs option and the native one won't work this way anyway so this doesn't translate. I think we should just simplify this check to only yield every 5ms regardless if higher pri tasks are scheduled or anything else. |
sebmarkbage
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the last thing we said was that we would instead make shouldYield always return true after 5ms regardless. This makes the code smaller and the hot path the fastest we can make it.
|
Closing as stale. |
Based on #17967
The current behavior of shouldYield is unspecified except in cases that happen to be relied on by the React work loop's implementation. However, some of the unspecified cases are important and the consequences of assuming the wrong behavior are really bad.
For example, it's natural to assume that
shouldYieldreturns false when called from within an expired task. But the current behavior usually does that, unless there's a higher priority task or a main thread task (i.e. at the end of the 5ms message loop interval).This fixes
shouldYieldso that it always returns false inside an expired task, regardless of the other tasks in the queue.