Skip to content

Detect and prevent render starvation, per lane #18864

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

Merged
merged 2 commits into from
May 8, 2020

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented May 7, 2020

If an update is CPU-bound for longer than expected according to its priority, we assume it's being starved by other work on the main thread.

To detect this, we keep track of the elapsed time using a fixed-size array where each slot corresponds to a lane. What we actually store is the event time when the lane first became CPU-bound. Changed it to store the expiration time, not the start time. See second commit for explanation.

Then, when receiving a new update or yielding to the main thread, we check how long each lane has been pending. If the time exceeds a threshold constant corresponding to its priority, we mark it as expired to force it to synchronously finish, instead of being interrupted again.

We don't want to mistake time elapsed while an update is IO-bound (waiting for data to resolve) for time when it is CPU-bound. So when a lane suspends, we clear its associated event time from the array. When it receives a signal to try again, either a ping or an update, we assign a new event time to restart the clock.

If an update is CPU-bound for longer than expected according to its
priority, we assume it's being starved by other work on the main thread.

To detect this, we keep track of the elapsed time using a fixed-size
array where each slot corresponds to a lane. What we actually store is
the event time when the lane first became CPU-bound.

Then, when receiving a new update or yielding to the main thread, we
check how long each lane has been pending. If the time exceeds a
threshold constant corresponding to its priority, we mark it as expired
to force it to synchronously finish.

We don't want to mistake time elapsed while an update is IO-bound
(waiting for data to resolve) for time when it is CPU-bound. So when a
lane suspends, we clear its associated event time from the array. When
it receives a signal to try again, either a ping or an update, we assign
a new event time to restart the clock.
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels May 7, 2020
@codesandbox-ci
Copy link

codesandbox-ci bot commented May 7, 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 fc843d6:

Sandbox Source
nostalgic-haslett-3805m Configuration

@acdlite acdlite force-pushed the lane-expiration branch from 5be4ecd to e6f8ba6 Compare May 7, 2020 23:55
@sizebot
Copy link

sizebot commented May 8, 2020

Warnings
⚠️ Failed to fetch build artifacts for base commit: fb3f0ac

Size changes (experimental)

Generated by 🚫 dangerJS against fc843d6

@sizebot
Copy link

sizebot commented May 8, 2020

Details of bundled changes.

Comparing: fb3f0ac...fc843d6

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactDOM-profiling.js 0.0% 0.0% 445.39 KB 445.41 KB 78.63 KB 78.64 KB FB_WWW_PROFILING
react-dom-test-utils.production.min.js 0.0% 0.0% 13.05 KB 13.05 KB 4.8 KB 4.8 KB NODE_PROD
react-dom-server.browser.development.js 0.0% 0.0% 154.95 KB 154.95 KB 39.49 KB 39.49 KB UMD_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% 🔺+0.3% 1.16 KB 1.16 KB 657 B 659 B NODE_PROD
react-dom-server.browser.production.min.js 0.0% 0.0% 22.97 KB 22.97 KB 8.53 KB 8.54 KB UMD_PROD
ReactDOMForked-dev.js +0.3% +0.3% 1.02 MB 1.02 MB 233.75 KB 234.46 KB FB_WWW_DEV
ReactDOMForked-prod.js 🔺+0.2% 🔺+0.3% 430.51 KB 431.58 KB 77.53 KB 77.75 KB FB_WWW_PROD
react-dom.development.js 0.0% 0.0% 911.34 KB 911.36 KB 200.43 KB 200.45 KB UMD_DEV
ReactDOMForked-profiling.js +0.2% +0.3% 440.27 KB 441.34 KB 79.18 KB 79.41 KB FB_WWW_PROFILING
react-dom.production.min.js 0.0% 0.0% 119.85 KB 119.87 KB 38.46 KB 38.47 KB UMD_PROD
react-dom.profiling.min.js 0.0% 0.0% 123.62 KB 123.64 KB 39.63 KB 39.64 KB UMD_PROFILING
ReactDOMTesting-dev.js 0.0% 0.0% 956.91 KB 956.94 KB 213.7 KB 213.72 KB FB_WWW_DEV
react-dom.development.js 0.0% 0.0% 867.47 KB 867.5 KB 197.91 KB 197.92 KB NODE_DEV
ReactDOMTesting-prod.js 0.0% 0.0% 404.85 KB 404.87 KB 74.71 KB 74.71 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% 0.0% 147.01 KB 147.01 KB 38.97 KB 38.97 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 119.97 KB 120 KB 37.59 KB 37.59 KB NODE_PROD
react-dom-test-utils.development.js 0.0% 0.0% 75.28 KB 75.28 KB 20.18 KB 20.18 KB UMD_DEV
react-dom-server.node.production.min.js 0.0% 0.0% 23.27 KB 23.27 KB 8.67 KB 8.67 KB NODE_PROD
react-dom.profiling.min.js 0.0% 0.0% 123.86 KB 123.89 KB 38.81 KB 38.81 KB NODE_PROFILING
react-dom-test-utils.production.min.js 0.0% 0.0% 13.17 KB 13.17 KB 4.89 KB 4.89 KB UMD_PROD
ReactDOM-dev.js 0.0% 0.0% 1.02 MB 1.02 MB 232.5 KB 232.51 KB FB_WWW_DEV
ReactDOM-prod.js 0.0% 0.0% 434.43 KB 434.45 KB 76.79 KB 76.79 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% 0.0% 70.11 KB 70.11 KB 19.67 KB 19.67 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% +0.1% 5.6 KB 5.6 KB 1.85 KB 1.86 KB NODE_DEV
react-dom-server.browser.development.js 0.0% 0.0% 145.79 KB 145.79 KB 38.72 KB 38.72 KB NODE_DEV
react-dom-server.browser.production.min.js 0.0% 0.0% 22.87 KB 22.87 KB 8.51 KB 8.51 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.3% 5.35 KB 5.35 KB 1.79 KB 1.8 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.4% 1.19 KB 1.19 KB 695 B 698 B UMD_PROD
ReactDOMServer-dev.js 0.0% 0.0% 163.94 KB 163.94 KB 41.78 KB 41.79 KB FB_WWW_DEV
ReactDOMServer-prod.js 0.0% 0.0% 52.9 KB 52.9 KB 12.74 KB 12.74 KB FB_WWW_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 4.85 KB 4.85 KB 1.69 KB 1.7 KB NODE_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.3% 1 KB 1 KB 607 B 609 B NODE_PROD

ReactDOM: size: 0.0%, gzip: 0.0%

Size changes (stable)

Generated by 🚫 dangerJS against fc843d6

@acdlite acdlite force-pushed the lane-expiration branch from e6f8ba6 to 63d1e6d Compare May 8, 2020 02:58
scheduleUpdateOnFiber(current, attemptHydrationAtLane);
// TODO: The event time is used to prevent starvation if a lane takes
// too long to finish rendering. Ideally this would be the time at which
// we started rendering. For spawned work, it's not a big deal, though,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No big deal 😎

priority >= InputContinuousLanePriority ? 1000 : 5000;
if (eventTime + expirationHeuristic <= currentTime) {
// This lane expired
root.expiredLanes |= lane;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the implications for entangled updates if we only mark a subset of lanes as expired?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entanglement would happen in getNextLanes. First we'd detect that a lane had expired, so we'd choose it to work on. Then we'd combine it with the entangled lanes before returning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

@@ -555,7 +596,15 @@ export function higherPriorityLane(a: Lane, b: Lane) {
return a !== NoLane && a < b ? a : b;
}

export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
export function createLaneMap<T>(initial: T): LaneMap<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why T instead of a more concrete type? Do we expect to have other types of lane maps in the future?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the ones I have in mind are numbers, but not necessarily. Like the interactions map, for example.

while (lanes > 0) {
const index = ctrz(lanes);

// Assumes event times are monotonically increasing. Last one wins.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, maybe this is me being dumb because it's early, but I'm not sure I understand this comment. Isn't the only assumption this code is making is that eventTime is positive? What does "last one wins" mean in this context?

Was this comment more applicable to old expiration times implementation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I meant "first one wins"

The idea is that the times always increase so there's no need to check if an existing time is earlier than the new time. The existing time will always be earlier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, ok that makes a lot more sense. I even wondered if maybe that's what you meant but discarded it.

while (lanes > 0) {
const index = ctrz(lanes);

eventTimes[index] = -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why we use an explicit constant for e.g. NoLane = 0 but we pass around a naked -1 in so many places. Should we use a constant for this too (to make it easier to grep for later if nothing else)?

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 can do that. It won't add any type safety, unfortunately, but it does make it more obvious.

}

// Count trailing zeros. Only used on lanes, so assume input is an integer.
function ctrz(lanes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why no Flow type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing with Lanes won't add type safety because inside this module, Lane is only an alias for number. If it were exported, however, then adding Lanes would prevent someone from passing a number that's not of type Lanes. I'll add it anyway, though, in case this ever gets exported (though I don't think we would).

@acdlite acdlite force-pushed the lane-expiration branch from 63d1e6d to d67c710 Compare May 8, 2020 18:19
I originally stored the start time because I thought I could use this
in the future to also measure Suspense timeouts. (Event times are
currently stored on each update object for this purpose.) But that
won't work because in the case of expiration times, we reset the clock
whenever the update becomes IO-bound. So to replace the per-update
field, I'm going to have to track those on the room separately from
expiration times.
@acdlite acdlite merged commit 6edaf6f into facebook:master May 8, 2020
acdlite added a commit to acdlite/react that referenced this pull request May 8, 2020
* Detect and prevent render starvation, per lane

If an update is CPU-bound for longer than expected according to its
priority, we assume it's being starved by other work on the main thread.

To detect this, we keep track of the elapsed time using a fixed-size
array where each slot corresponds to a lane. What we actually store is
the event time when the lane first became CPU-bound.

Then, when receiving a new update or yielding to the main thread, we
check how long each lane has been pending. If the time exceeds a
threshold constant corresponding to its priority, we mark it as expired
to force it to synchronously finish.

We don't want to mistake time elapsed while an update is IO-bound
(waiting for data to resolve) for time when it is CPU-bound. So when a
lane suspends, we clear its associated event time from the array. When
it receives a signal to try again, either a ping or an update, we assign
a new event time to restart the clock.

* Store as expiration time, not start time

I originally stored the start time because I thought I could use this
in the future to also measure Suspense timeouts. (Event times are
currently stored on each update object for this purpose.) But that
won't work because in the case of expiration times, we reset the clock
whenever the update becomes IO-bound. So to replace the per-update
field, I'm going to have to track those on the room separately from
expiration times.
@acdlite acdlite mentioned this pull request May 8, 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