Skip to content
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

Merged
merged 6 commits into from
Mar 3, 2020

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Jan 31, 2020

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.)

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jan 31, 2020
@sizebot
Copy link

sizebot commented Jan 31, 2020

Details of bundled changes.

Comparing: d2158d6...7ce5183

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactDOM-profiling.js -0.1% 0.0% 406.05 KB 405.57 KB 73.9 KB 73.92 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 134.59 KB 134.59 KB 34.59 KB 34.59 KB UMD_DEV
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 55.86 KB 55.86 KB 14.46 KB 14.46 KB NODE_DEV
react-dom.development.js +0.1% +0.1% 882.62 KB 883.42 KB 194.46 KB 194.61 KB UMD_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.2% 115.83 KB 115.95 KB 37.06 KB 37.13 KB UMD_PROD
react-dom.profiling.min.js +0.1% +0.2% 119.35 KB 119.48 KB 38.28 KB 38.34 KB UMD_PROFILING
react-dom.development.js +0.1% +0.1% 839.95 KB 840.7 KB 192.09 KB 192.23 KB NODE_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.1% 115.9 KB 116.03 KB 36.44 KB 36.49 KB NODE_PROD
react-dom.profiling.min.js +0.1% +0.2% 119.58 KB 119.71 KB 37.5 KB 37.58 KB NODE_PROFILING
ReactDOM-dev.js +0.1% +0.1% 937.29 KB 937.99 KB 208.8 KB 208.92 KB FB_WWW_DEV
ReactDOM-prod.js -0.1% -0.0% 394.76 KB 394.24 KB 71.74 KB 71.74 KB FB_WWW_PROD
ReactDOMTesting-dev.js +0.1% +0.1% 896.64 KB 897.62 KB 200.32 KB 200.44 KB FB_WWW_DEV
ReactDOMTesting-prod.js -0.1% 0.0% 381.82 KB 381.49 KB 69.46 KB 69.48 KB FB_WWW_PROD
ReactDOMTesting-profiling.js -0.1% 0.0% 381.82 KB 381.49 KB 69.46 KB 69.48 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 0.0% 0.0% 19.9 KB 19.9 KB 7.36 KB 7.36 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.1% 1.17 KB 1.17 KB 690 B 691 B UMD_PROD
ReactDOMServer-dev.js 0.0% -0.0% 137.53 KB 137.53 KB 35.04 KB 35.04 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 49.55 KB 49.55 KB 13.51 KB 13.51 KB NODE_DEV
ReactDOMServer-prod.js 0.0% -0.0% 47.39 KB 47.39 KB 11.03 KB 11.03 KB FB_WWW_PROD
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 58.9 KB 58.9 KB 14.7 KB 14.7 KB UMD_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% 0.0% 10.26 KB 10.26 KB 3.49 KB 3.49 KB UMD_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.1% +0.1% 613.41 KB 614.11 KB 132.6 KB 132.71 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +0.1% +0.1% 631.31 KB 632.01 KB 136.87 KB 136.98 KB RN_OSS_DEV
ReactNativeRenderer-prod.js -0.4% -0.1% 260.64 KB 259.69 KB 45.23 KB 45.18 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js -0.3% -0.1% 271.79 KB 271.03 KB 47.35 KB 47.32 KB RN_OSS_PROFILING
ReactFabric-prod.js -0.4% -0.2% 252.6 KB 251.66 KB 43.76 KB 43.69 KB RN_OSS_PROD
ReactFabric-profiling.js -0.3% -0.1% 263.76 KB 262.99 KB 45.89 KB 45.86 KB RN_OSS_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.1% +0.1% 566.42 KB 567.12 KB 118.73 KB 118.81 KB FB_WWW_DEV
ReactART-prod.js -0.4% -0.2% 234.67 KB 233.73 KB 39.81 KB 39.72 KB FB_WWW_PROD
react-art.development.js +0.1% +0.1% 613.61 KB 614.41 KB 129.54 KB 129.62 KB UMD_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.2% 104.09 KB 104.15 KB 31.52 KB 31.59 KB UMD_PROD
react-art.development.js +0.1% +0.1% 518.65 KB 519.4 KB 111.9 KB 112.01 KB NODE_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.3% 69.12 KB 69.18 KB 20.73 KB 20.78 KB NODE_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactTestRenderer-dev.js +0.1% +0.1% 552.19 KB 552.88 KB 116.56 KB 116.66 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 38.39 KB 38.39 KB 9.29 KB 9.29 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% 0.0% 12.91 KB 12.91 KB 3.93 KB 3.93 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 550.93 KB 551.73 KB 114.74 KB 114.82 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.1% 🔺+0.2% 71.1 KB 71.16 KB 21.61 KB 21.65 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 525.06 KB 525.81 KB 113.39 KB 113.52 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.1% 🔺+0.2% 70.91 KB 70.97 KB 21.29 KB 21.33 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.1% 554.59 KB 555.36 KB 117.28 KB 117.44 KB NODE_DEV
react-reconciler-reflection.development.js 0.0% 0.0% 16 KB 16 KB 4.89 KB 4.89 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.1% 🔺+0.3% 73.54 KB 73.64 KB 21.7 KB 21.76 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% +0.1% 553.23 KB 554 KB 117.13 KB 117.29 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.1% 🔺+0.3% 73.55 KB 73.65 KB 21.71 KB 21.77 KB NODE_PROD

Size changes (stable)

Generated by 🚫 dangerJS against 7ce5183

@sizebot
Copy link

sizebot commented Jan 31, 2020

Details of bundled changes.

Comparing: d2158d6...7ce5183

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% +0.1% 632.78 KB 633.58 KB 132.96 KB 133.04 KB UMD_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.2% 106.52 KB 106.58 KB 32.18 KB 32.23 KB UMD_PROD
react-art.development.js +0.1% +0.1% 537.07 KB 537.82 KB 115.34 KB 115.45 KB NODE_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.2% 71.5 KB 71.56 KB 21.38 KB 21.43 KB NODE_PROD
ReactART-dev.js +0.2% +0.1% 540.45 KB 541.41 KB 113.45 KB 113.54 KB FB_WWW_DEV
ReactART-prod.js -0.4% -0.1% 227.49 KB 226.55 KB 38.55 KB 38.51 KB FB_WWW_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-prod.js -0.4% -0.1% 260.65 KB 259.71 KB 45.23 KB 45.19 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js -0.3% -0.1% 271.81 KB 271.04 KB 47.36 KB 47.33 KB RN_OSS_PROFILING
ReactFabric-prod.js -0.4% -0.2% 252.61 KB 251.67 KB 43.77 KB 43.7 KB RN_OSS_PROD
ReactFabric-profiling.js -0.3% -0.1% 263.77 KB 263 KB 45.9 KB 45.87 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.1% +0.1% 616.05 KB 616.76 KB 132.94 KB 133.05 KB RN_FB_DEV
ReactFabric-prod.js -0.4% -0.2% 252.77 KB 251.82 KB 43.8 KB 43.73 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.1% +0.1% 631.32 KB 632.03 KB 136.87 KB 136.99 KB RN_OSS_DEV
ReactFabric-profiling.js -0.3% -0.1% 263.92 KB 263.15 KB 45.94 KB 45.92 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js +0.1% +0.1% 633.95 KB 634.65 KB 137.21 KB 137.32 KB RN_FB_DEV
ReactNativeRenderer-prod.js -0.4% -0.1% 260.79 KB 259.85 KB 45.26 KB 45.21 KB RN_FB_PROD
ReactNativeRenderer-profiling.js -0.3% -0.1% 271.95 KB 271.18 KB 47.41 KB 47.38 KB RN_FB_PROFILING
ReactFabric-dev.js +0.1% +0.1% 613.42 KB 614.13 KB 132.61 KB 132.71 KB RN_OSS_DEV

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.1% +0.2% 123.67 KB 123.78 KB 38.56 KB 38.63 KB NODE_PROFILING
ReactDOM-dev.js +0.1% +0.1% 895.85 KB 896.8 KB 200.26 KB 200.39 KB FB_WWW_DEV
ReactDOMServer-dev.js 0.0% -0.0% 136.67 KB 136.67 KB 34.9 KB 34.9 KB FB_WWW_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 10.96 KB 10.96 KB 4.17 KB 4.17 KB UMD_PROD
ReactDOMTesting-dev.js +0.1% +0.1% 871.05 KB 872.03 KB 195.15 KB 195.26 KB FB_WWW_DEV
ReactDOMTesting-prod.js -0.1% 0.0% 368.93 KB 368.6 KB 67.44 KB 67.46 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% -0.0% 49.56 KB 49.56 KB 13.52 KB 13.52 KB NODE_DEV
ReactDOMTesting-profiling.js -0.1% 0.0% 368.93 KB 368.6 KB 67.44 KB 67.46 KB FB_WWW_PROFILING
react-dom.development.js +0.1% +0.1% 911.3 KB 912.1 KB 199.39 KB 199.53 KB UMD_DEV
react-dom-server.browser.development.js 0.0% 0.0% 136.18 KB 136.18 KB 34.79 KB 34.79 KB UMD_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.2% 119.72 KB 119.83 KB 38.22 KB 38.29 KB UMD_PROD
react-dom.profiling.min.js +0.1% +0.2% 123.37 KB 123.48 KB 39.4 KB 39.47 KB UMD_PROFILING
react-dom.development.js +0.1% +0.1% 867.45 KB 868.19 KB 196.98 KB 197.12 KB NODE_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.1% 119.87 KB 119.98 KB 37.45 KB 37.5 KB NODE_PROD
ReactDOM-prod.js -0.1% 0.0% 370.91 KB 370.57 KB 67.53 KB 67.55 KB FB_WWW_PROD
ReactDOMServer-prod.js 0.0% 0.0% 46.68 KB 46.68 KB 10.84 KB 10.84 KB FB_WWW_PROD
ReactDOM-profiling.js -0.1% 0.0% 382.12 KB 381.83 KB 69.66 KB 69.67 KB FB_WWW_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactTestRenderer-dev.js +0.1% +0.1% 552.2 KB 552.9 KB 116.57 KB 116.67 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 38.4 KB 38.4 KB 9.3 KB 9.3 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% 0.0% 12.92 KB 12.92 KB 3.94 KB 3.94 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 550.96 KB 551.75 KB 114.75 KB 114.83 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.1% 🔺+0.2% 71.12 KB 71.18 KB 21.62 KB 21.66 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 525.09 KB 525.84 KB 113.4 KB 113.53 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.1% 🔺+0.2% 70.94 KB 71 KB 21.3 KB 21.35 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-persistent.development.js +0.1% +0.1% 553.25 KB 554.02 KB 117.14 KB 117.3 KB NODE_DEV
react-reconciler-reflection.development.js 0.0% 0.0% 16.01 KB 16.01 KB 4.9 KB 4.9 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.1% 🔺+0.3% 73.56 KB 73.67 KB 21.71 KB 21.78 KB NODE_PROD
react-reconciler.development.js +0.1% +0.1% 575.35 KB 576.12 KB 121.17 KB 121.33 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.1% 🔺+0.2% 76.31 KB 76.42 KB 22.37 KB 22.43 KB NODE_PROD

Size changes (experimental)

Generated by 🚫 dangerJS against 7ce5183

@codesandbox-ci
Copy link

codesandbox-ci bot commented Feb 1, 2020

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:

Sandbox Source
lingering-silence-su7pr Configuration

@acdlite acdlite changed the title Failing regression tests: Expired partial tree infinite loops Bugfix: Expired partial tree infinite loops Feb 1, 2020
@acdlite acdlite requested a review from sebmarkbage February 1, 2020 01:02
@acdlite acdlite force-pushed the regression-expired-shouldyield branch 2 times, most recently from 74d172b to 51ef007 Compare February 1, 2020 01:46
@acdlite acdlite force-pushed the regression-expired-shouldyield branch 2 times, most recently from 38c38b6 to 010c09b Compare February 4, 2020 22:12
@acdlite
Copy link
Collaborator Author

acdlite commented Feb 5, 2020

@sebmarkbage Based on your feedback I updated the PR in two commits. The first one moves the partial tree check to performSyncWorkOnRoot. That one is pretty simple by itself.

The second one removes didError by performing multiple render passes within the same call to performSyncWorkOnRoot or performConcurrentWorkOnRoot. To do this, I factored out the render phase. I think this refactor makes a lot of sense, though it's inherently riskier.

@acdlite acdlite force-pushed the regression-expired-shouldyield branch from acb551e to b074e6d Compare February 5, 2020 00:44
@@ -1493,6 +1440,53 @@ function workLoopSync() {
}
}

function renderRootConcurrent(root, expirationTime) {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@acdlite acdlite force-pushed the regression-expired-shouldyield branch 2 times, most recently from 849a102 to 9fb1194 Compare March 3, 2020 19:47
acdlite added 6 commits March 3, 2020 12:52
* 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.
@acdlite acdlite force-pushed the regression-expired-shouldyield branch from 9fb1194 to 7ce5183 Compare March 3, 2020 20:52
@acdlite acdlite merged commit ec652f4 into facebook:master Mar 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants