-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
Bugfix: Expired partial tree infinite loops #17949
Bugfix: Expired partial tree infinite loops #17949
Conversation
Details of bundled changes.Comparing: d2158d6...7ce5183 react-dom
react-native-renderer
react-art
react-test-renderer
react-reconciler
Size changes (stable) |
Details of bundled changes.Comparing: d2158d6...7ce5183 react-art
react-native-renderer
react-dom
react-test-renderer
react-reconciler
Size changes (experimental) |
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 7ce5183:
|
74d172b
to
51ef007
Compare
38c38b6
to
010c09b
Compare
@sebmarkbage Based on your feedback I updated the PR in two commits. The first one moves the partial tree check to The second one removes |
acb551e
to
b074e6d
Compare
@@ -1493,6 +1440,53 @@ function workLoopSync() { | |||
} | |||
} | |||
|
|||
function renderRootConcurrent(root, expirationTime) { |
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.
Just to be clear, this is only called once so it didn't technically need to be extracted out, right?
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.
Yeah I only did it for symmetry purposes
849a102
to
9fb1194
Compare
* Failing test: Expiring a partially completed tree We should not throw out a partially completed tree if it expires in the middle of rendering. We should finish the rest of the tree without yielding, then finish any remaining expired levels in a single batch. * Check if there's a partial tree before restarting If a partial render expires, we should stay in the concurrent path (performConcurrentWorkOnRoot); we'll stop yielding, but the rest of the behavior remains the same. We will only revert to the sync path (performSyncWorkOnRoot) when starting on a new level. This approach prevents partially completed concurrent work from being discarded. * New test: retry after error during expired render
Adds regression tests that reproduce a scenario where a partially completed tree expired then fell into an infinite loop. The code change that exposed this bug made the assumption that if you call Scheduler's `shouldYield` from inside an expired task, Scheduler will always return `false`. But in fact, Scheduler sometimes returns `true` in that scenario, which is a bug. The reason it worked before is that once a task timed out, React would always switch to a synchronous work loop without checking `shouldYield`. My rationale for relying on `shouldYield` was to unify the code paths between a partially concurrent render (i.e. expires midway through) and a fully concurrent render, as opposed to a render that was synchronous the whole time. However, this bug indicates that we need a stronger guarantee within React for when tasks expire, given that the failure case is so catastrophic. Instead of relying on the result of a dynamic method call, we should use control flow to guarantee that the work is synchronously executed. (We should also fix the Scheduler bug so that `shouldYield` always returns false inside an expired task, but I'll address that separately.)
Refactors the `didTimeout` check so that it always switches to the synchronous work loop, like it did before the regression. This breaks the error handling behavior that I added in 5f7361f (an error during a partially concurrent render should retry once, synchronously). I'll fix this next. I need to change that behavior, anyway, to support retries that occur as a result of `flushSync`.
Except in legacy mode. This is to support the `useOpaqueReference` hook, which uses an error to trigger a retry at lower priority.
Splits the work loop and its surrounding enter/exit code into their own functions. Now we can do perform multiple render phase passes within a single call to performConcurrentWorkOnRoot or performSyncWorkOnRoot. This lets us get rid of the `didError` field.
9fb1194
to
7ce5183
Compare
Adds regression tests that reproduce a scenario where a partially completed tree expired then fell into an infinite loop.
The code change that exposed this bug made the assumption that if you call Scheduler's
shouldYield
from inside an expired task, Scheduler will always returnfalse
. But in fact, Scheduler sometimes returnstrue
in that scenario, which is a bug.The reason it worked before is that once a task timed out, React would always switch to a synchronous work loop without checking
shouldYield
.My rationale for relying on
shouldYield
was to unify the code paths between a partially concurrent render (i.e. expires midway through) and a fully concurrent render, as opposed to a render that was synchronous the whole time. However, this bug indicates that we need a stronger guarantee within React for when tasks expire, given that the failure case is so catastrophic. Instead of relying on the result of a dynamic method call, we should use control flow to guarantee that the work is synchronously executed.(We should also fix the Scheduler bug so that
shouldYield
always returns false inside an expired task, but I'll address that separately.)