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

Expiration should do nothing except disable time slicing #21345

Merged
merged 1 commit into from
Apr 24, 2021

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Apr 24, 2021

We have a feature called "expiration" whose purpose is to prevent a concurrent update from being starved by higher priority events. If a lane is CPU-bound for too long, we finish the rest of the work synchronously without allowing further interruptions.

In the current implementation, we do this in sort of a roundabout way: once a lane is determined to have expired, we entangle it with SyncLane and switch to the synchronous work loop.

There are a few flaws with the approach. One is that SyncLane has a particular semantic meaning besides its non-yieldiness. For example, flushSync will force remaining Sync work to finish; currently, that also includes expired work, which isn't an intended behavior, but rather an artifact of the implementation.

An even worse example is that passive effects triggered by a Sync update are flushed synchronously, before paint, so that its result is guaranteed to be observed by the next discrete event. But expired work has no such requirement: we're flushing expired effects before paint unnecessarily.

Aside from the behaviorial implications, the current implementation has proven to be fragile: more than once, we've accidentally regressed performance due to a subtle change in how expiration is handled.

This PR aims to radically simplify how we model starvation protection by scaling back the implementation as much as possible. In this new model, if a lane is expired, we disable time slicing. That's it. We don't entangle it with SyncLane. The only thing we do is skip the call to shouldYield in between each time slice. This is identical to how we model synchronous-by default updates in React 18.

We have a feature called "expiration" whose purpose is to prevent
a concurrent update from being starved by higher priority events.
If a lane is CPU-bound for too long, we finish the rest of the work
synchronously without allowing further interruptions.

In the current implementation, we do this in sort of a roundabout way:
once a lane is determined to have expired, we entangle it with SyncLane
and switch to the synchronous work loop.

There are a few flaws with the approach. One is that SyncLane has a
particular semantic meaning besides its non-yieldiness. For example,
`flushSync` will force remaining Sync work to finish; currently, that
also includes expired work, which isn't an intended behavior, but rather
an artifact of the implementation.

An event worse example is that passive effects triggered by a Sync
update are flushed synchronously, before paint, so that its result
is guaranteed to be observed by the next discrete event. But expired
work has no such requirement: we're flushing expired effects before
paint unnecessarily.

Aside from the behaviorial implications, the current implementation has
proven to be fragile: more than once, we've accidentally regressed
performance due to a subtle change in how expiration is handled.

This PR aims to radically simplify how we model starvation protection by
scaling back the implementation as much as possible. In this new model,
if a lane is expired, we disable time slicing. That's it. We don't
entangle it with SyncLane. The only thing we do is skip the call to
`shouldYield` in between each time slice. This is identical to how we
model synchronous-by-default updates in React 18.
@acdlite acdlite requested a review from rickhanlonii April 24, 2021 00:59
@sizebot
Copy link

sizebot commented Apr 24, 2021

Comparing: 0f5ebf3...ac0cde2

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 122.84 kB 122.76 kB = 39.45 kB 39.42 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 129.42 kB 129.34 kB = 41.55 kB 41.51 kB
facebook-www/ReactDOM-prod.classic.js = 412.67 kB 412.52 kB = 76.41 kB 76.35 kB
facebook-www/ReactDOM-prod.modern.js = 400.69 kB 400.55 kB = 74.50 kB 74.45 kB
facebook-www/ReactDOMForked-prod.classic.js = 412.67 kB 412.52 kB = 76.41 kB 76.35 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against ac0cde2

@acdlite acdlite force-pushed the expiration-simplify branch from 2177c16 to ef3816d Compare April 24, 2021 01:10
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 24, 2021
@acdlite acdlite force-pushed the expiration-simplify branch from ef3816d to ac0cde2 Compare April 24, 2021 01:15
@acdlite acdlite merged commit 4874042 into facebook:master Apr 24, 2021
facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request Apr 28, 2021
Summary:
This sync includes the following changes:
- **[9a2591681](facebook/react@9a2591681 )**: Fix export //<Sebastian Markbage>//
- **[4a8deb083](facebook/react@4a8deb083 )**: Switch the isPrimaryRender flag based on the stream config ([#21357](facebook/react#21357)) //<Sebastian Markbåge>//
- **[bd4f056a3](facebook/react@bd4f056a3 )**: [Fizz] Implement lazy components and nodes ([#21355](facebook/react#21355)) //<Sebastian Markbåge>//
- **[fc33f12bd](facebook/react@fc33f12bd )**: Remove unstable scheduler/tracing API ([#20037](facebook/react#20037)) //<Brian Vaughn>//
- **[721238394](facebook/react@721238394 )**: Enable strict effects mode for React Native Facebook builds ([#21354](facebook/react#21354)) //<Brian Vaughn>//
- **[48740429b](facebook/react@48740429b )**: Expiration: Do nothing except disable time slicing ([#21345](facebook/react#21345)) //<Andrew Clark>//
- **[0f5ebf366](facebook/react@0f5ebf366 )**: Delete unreferenced type ([#21343](facebook/react#21343)) //<Andrew Clark>//
- **[9cd52b27f](facebook/react@9cd52b27f )**: Restore context after an error happens ([#21341](facebook/react#21341)) //<Sebastian Markbåge>//
- **[ad091759a](facebook/react@ad091759a )**: Revert "Emit reactroot attribute on the first element we discover ([#21154](facebook/react#21154))" ([#21340](facebook/react#21340)) //<Sebastian Markbåge>//
- **[709f94841](facebook/react@709f94841 )**: [Fizz] Add FB specific streaming API and build ([#21337](facebook/react#21337)) //<Sebastian Markbåge>//
- **[e8cdce40d](facebook/react@e8cdce40d )**: Don't flush sync at end of discreteUpdates ([#21327](facebook/react#21327)) //<Andrew Clark>//
- **[a15586001](facebook/react@a15586001 )**: Fix: Don't flush discrete at end of batchedUpdates ([#21229](facebook/react#21229)) //<Andrew Clark>//
- **[89847bf6e](facebook/react@89847bf6e )**: Continuous updates should interrupt transitions ([#21323](facebook/react#21323)) //<Andrew Clark>//
- **[ef37d55b6](facebook/react@ef37d55b6 )**: Use performConcurrentWorkOnRoot for "sync default" ([#21322](facebook/react#21322)) //<Andrew Clark>//

Changelog:
[General][Changed] - React Native sync for revisions a632f7d...2a7bb41

jest_e2e[run_all_tests]

Reviewed By: JoshuaGross

Differential Revision: D28063006

fbshipit-source-id: 7e3535f80961706863b6c2188ee44b5796b2f000
koto pushed a commit to koto/react that referenced this pull request Jun 15, 2021
We have a feature called "expiration" whose purpose is to prevent
a concurrent update from being starved by higher priority events.
If a lane is CPU-bound for too long, we finish the rest of the work
synchronously without allowing further interruptions.

In the current implementation, we do this in sort of a roundabout way:
once a lane is determined to have expired, we entangle it with SyncLane
and switch to the synchronous work loop.

There are a few flaws with the approach. One is that SyncLane has a
particular semantic meaning besides its non-yieldiness. For example,
`flushSync` will force remaining Sync work to finish; currently, that
also includes expired work, which isn't an intended behavior, but rather
an artifact of the implementation.

An event worse example is that passive effects triggered by a Sync
update are flushed synchronously, before paint, so that its result
is guaranteed to be observed by the next discrete event. But expired
work has no such requirement: we're flushing expired effects before
paint unnecessarily.

Aside from the behaviorial implications, the current implementation has
proven to be fragile: more than once, we've accidentally regressed
performance due to a subtle change in how expiration is handled.

This PR aims to radically simplify how we model starvation protection by
scaling back the implementation as much as possible. In this new model,
if a lane is expired, we disable time slicing. That's it. We don't
entangle it with SyncLane. The only thing we do is skip the call to
`shouldYield` in between each time slice. This is identical to how we
model synchronous-by-default updates in React 18.
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