Skip to content

[Flight] Track Awaits on I/O as Debug Info #33388

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 20 commits into from
Jun 3, 2025

Conversation

sebmarkbage
Copy link
Collaborator

This lets us track what data each Server Component depended on. This will be used by Performance Track and React DevTools.

We use Node.js async_hooks. This has a number of downside. It is Node.js specific so this feature is not available in other runtimes until something equivalent becomes available. It's discouraged by Node.js docs. It's also slow which makes this approach only really viable in development mode. At least with stack traces. However, it's really the only solution that gives us the data that we need.

The Diagnostic Channel API is not sufficient. Not only is many Node.js built-in APIs missing but all libraries like databases are also missing. Were as async_hooks covers pretty much anything async in the Node.js ecosystem.

However, even if coverage was wider it's not actually showing the information we want. It's not enough to show the low level I/O that is happening because that doesn't provide the context. We need the stack trace in user space code where it was initiated and where it was awaited. It's also not each low level socket operation that we want to surface but some higher level concept which can span a sequence of I/O operations but as far as user space is concerned.

Therefore this solution is anchored on stack traces and ignore listing to determine what the interesting span is. It is somewhat Promise-centric (and in particular async/await) because it allows us to model an abstract span instead of just random I/O. Async/await points are also especially useful because this allows Async Stacks to show the full sequence which is not supported by random callbacks. However, if no Promises are involved we still to our best to show the stack causing plain I/O callbacks.

Additionally, we don't want to track all possible I/O. For example, side-effects like logging that doesn't affect the rendering performance doesn't need to be included. We only want to include things that actually block the rendering output. We also need to track which data blocks each component so that we can track which data caused a particular subtree to suspend.

We can do this using async_hooks because we can track the graph of what resolved what and then spawned what.

To track what suspended what, something has to resolve. Therefore it needs to run to completion before we can show what it was suspended on. So something that never resolves, won't be tracked for example.

We use the async_hooks in ReactFlightServerConfigDebugNode to build up an ReactFlightAsyncSequence graph that collects the stack traces for basically all I/O and Promises allocated in the whole app. This is pretty heavy, especially the stack traces, but it's because we don't know which ones we'll need until they resolve. We don't materialize the stacks until we need them though.

Once they end up pinging the Flight runtime, we collect which current executing task that pinged the runtime and then log the sequence that led up until that runtime into the RSC protocol. Currently we only include things that weren't already resolved before we started rendering this task/component, so that we don't log the entire history each time.

Each operation is split into two parts. First a ReactIOInfo which represents an I/O operation and its start/end time. Basically the start point where it was start. This is basically represents where you called new Promise() or when entering an async function which has an implied Promise. It can be started in a different component than where it's awaited and it can be awaited in multiple places. Therefore this is global information and not associated with a specific Component.

The second part is ReactAsyncInfo. This represents where this I/O was await:ed or .then() called. This is associated with a point in the tree (usually the Promise that's a direct child of a Component). Since you can have multiple different I/O awaited in a sequence technically it forms a dependency graph but to simplify the model these awaits as flattened into the ReactDebugInfo list. Basically it contains each await in a sequence that affected this part from unblocking.

This means that the same ReactAsyncInfo can appear in mutliple components if they all await the same ReactIOInfo but the same Promise only appears once.

Promises that are only resolved by other Promises or immediately are not considered here. Only if they're resolved by an I/O operation. We pick the Promise basically on the border between user space code and ignored listed code (node_modules) to pick the most specific span but abstract enough to not give too much detail irrelevant to the current audience. Similarly, the deepest await in user space is marked as the relevant await point.

This feature is only available in the node builds of React. Not if you use the edge builds inside of Node.js.

@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label May 31, 2025
@react-sizebot
Copy link

react-sizebot commented May 31, 2025

Comparing: 1ae0a84...dddd08a

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.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 529.82 kB 529.82 kB = 93.51 kB 93.51 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 650.91 kB 650.91 kB = 114.63 kB 114.63 kB
facebook-www/ReactDOM-prod.classic.js = 675.86 kB 675.86 kB = 118.91 kB 118.91 kB
facebook-www/ReactDOM-prod.modern.js = 666.14 kB 666.14 kB = 117.30 kB 117.30 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +4.39% 155.71 kB 162.54 kB +4.33% 29.04 kB 30.30 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +4.39% 155.74 kB 162.57 kB +4.30% 29.04 kB 30.29 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +4.21% 162.41 kB 169.24 kB +4.37% 30.09 kB 31.40 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +4.18% 163.55 kB 170.38 kB +4.29% 30.38 kB 31.68 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +4.17% 163.61 kB 170.44 kB +4.29% 30.39 kB 31.69 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +4.39% 155.71 kB 162.54 kB +4.33% 29.04 kB 30.30 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +4.39% 155.74 kB 162.57 kB +4.30% 29.04 kB 30.29 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +4.21% 162.41 kB 169.24 kB +4.37% 30.09 kB 31.40 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +4.18% 163.55 kB 170.38 kB +4.29% 30.38 kB 31.68 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +4.17% 163.61 kB 170.44 kB +4.29% 30.39 kB 31.69 kB
oss-experimental/react-server/cjs/react-server-flight.development.js +1.39% 112.12 kB 113.68 kB +1.20% 20.84 kB 21.08 kB
oss-stable-semver/react-server/cjs/react-server-flight.development.js +1.26% 105.29 kB 106.61 kB +1.21% 19.63 kB 19.87 kB
oss-stable/react-server/cjs/react-server-flight.development.js +1.26% 105.29 kB 106.61 kB +1.21% 19.63 kB 19.87 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.development.js +1.03% 151.02 kB 152.58 kB +0.87% 28.07 kB 28.32 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.development.js +0.98% 158.76 kB 160.32 kB +0.83% 29.45 kB 29.70 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.98% 159.30 kB 160.86 kB +0.84% 29.58 kB 29.82 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.development.js +0.92% 144.17 kB 145.50 kB +0.83% 26.88 kB 27.10 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.browser.development.js +0.92% 144.17 kB 145.50 kB +0.83% 26.88 kB 27.10 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.89% 148.57 kB 149.90 kB +0.82% 27.73 kB 27.95 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.89% 148.57 kB 149.90 kB +0.82% 27.73 kB 27.95 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +0.89% 148.60 kB 149.92 kB +0.80% 27.70 kB 27.92 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.node.development.js +0.89% 148.60 kB 149.92 kB +0.80% 27.70 kB 27.92 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.development.js +0.87% 151.91 kB 153.24 kB +0.83% 28.20 kB 28.43 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.browser.development.js +0.87% 151.91 kB 153.24 kB +0.83% 28.20 kB 28.43 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.87% 152.46 kB 153.78 kB +0.84% 28.33 kB 28.57 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.87% 152.46 kB 153.78 kB +0.84% 28.33 kB 28.57 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.85% 155.27 kB 156.59 kB +0.83% 28.82 kB 29.05 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.85% 155.27 kB 156.59 kB +0.83% 28.82 kB 29.05 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +0.85% 156.41 kB 157.73 kB +0.77% 29.09 kB 29.32 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.node.development.js +0.85% 156.41 kB 157.73 kB +0.77% 29.09 kB 29.32 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.84% 156.47 kB 157.79 kB +0.76% 29.11 kB 29.33 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.84% 156.47 kB 157.79 kB +0.76% 29.11 kB 29.33 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.development.js +0.73% 147.99 kB 149.07 kB +0.55% 27.47 kB 27.62 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.development.js +0.73% 147.99 kB 149.07 kB +0.55% 27.47 kB 27.62 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.69% 155.74 kB 156.82 kB +0.50% 28.83 kB 28.97 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.69% 155.74 kB 156.82 kB +0.50% 28.83 kB 28.97 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +0.69% 155.77 kB 156.84 kB +0.51% 28.82 kB 28.97 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +0.69% 155.77 kB 156.84 kB +0.51% 28.82 kB 28.97 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.development.js +0.66% 155.47 kB 156.50 kB +0.37% 28.77 kB 28.88 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.63% 163.22 kB 164.25 kB +0.44% 30.11 kB 30.24 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.development.js +0.63% 163.25 kB 164.27 kB +0.43% 30.11 kB 30.24 kB
oss-experimental/react-markup/cjs/react-markup.react-server.development.js +0.27% 572.30 kB 573.86 kB +0.28% 102.31 kB 102.60 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js = 100.13 kB 99.91 kB = 20.43 kB 20.37 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js = 100.11 kB 99.89 kB = 20.43 kB 20.37 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js = 95.45 kB 95.22 kB = 19.55 kB 19.49 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-server.edge.production.js = 95.45 kB 95.22 kB = 19.55 kB 19.49 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js = 95.43 kB 95.20 kB = 19.56 kB 19.50 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.js = 95.43 kB 95.20 kB = 19.56 kB 19.50 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.production.js = 93.20 kB 92.98 kB = 19.21 kB 19.15 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.production.js = 88.51 kB 88.29 kB = 18.43 kB 18.38 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-server.edge.production.js = 88.51 kB 88.29 kB = 18.43 kB 18.38 kB

Generated by 🚫 dangerJS against dddd08a

@sebmarkbage sebmarkbage force-pushed the asyncdebuginfo branch 4 times, most recently from 450d81d to a7ee51b Compare May 31, 2025 04:30
@sebmarkbage sebmarkbage force-pushed the asyncdebuginfo branch 2 times, most recently from a187226 to 7bee1d6 Compare May 31, 2025 05:19
@@ -293,3 +293,18 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
return require('internal-test-utils/ReactJSDOM.js');
});
}

// We mock createHook so that we can automatically clean it up.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is needed because in a jest.resetModules() environments will keep installing a new hook every time it gets reset. Those compound to each one collecting the data independently which runs out of memory.

In a real environment we expect that the module is only initialized once but you’d have to be careful with something like HMR on the server.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It could also be an issue if someone else runs RSC rendering in a jest set up that resets modules without restarting the node runtime.

});

it('can track the start of I/O when no native promise is used', async () => {
function Component() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because the snapshots above can change the line number of these tests, you might have to rerun the snapshots twice when updating them.

This lets us track both the thing we're awaiting and the path that led us
to awaiting in the first place.
Track end time of I/O only if we need to
ReactAsyncInfo represents an "await" where as the ReactIOInfo represents
when the start of that I/O was requested.

Also make sure we don't try to encode debugStack or debugTask.
We'll use this as a heuristic to exclude awaits that happened before this.
sebmarkbage and others added 10 commits June 3, 2025 14:06
This conveniently ignores our own scheduling tasks.
We're not going to use this.
…anceTrack

We rely on enableComponentPerformanceTrack flag for the timing tracking in
general but then we additionally need the async debug info flag to use
the async tracking.
Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
@sebmarkbage sebmarkbage merged commit acee65d into facebook:main Jun 3, 2025
240 checks passed
sebmarkbage added a commit that referenced this pull request Jun 3, 2025
Stacked on #33388.

This encodes the I/O entries as their own row type (`"J"`). This makes
it possible to parse them directly without first parsing the debug info
for each component. E.g. if you're just interested in logging the I/O
without all the places it was awaited.

This is not strictly necessary since the debug info is also readily
available without parsing the actual trees. (That's how the Server
Components Performance Track works.) However, we might want to exclude
this information in profiling builds while retaining some limited form
of I/O tracking.

It also allows for logging side-effects that are not awaited if we
wanted to.
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jun 3, 2025
Stacked on facebook#33388.

This encodes the I/O entries as their own row type (`"J"`). This makes
it possible to parse them directly without first parsing the debug info
for each component. E.g. if you're just interested in logging the I/O
without all the places it was awaited.

This is not strictly necessary since the debug info is also readily
available without parsing the actual trees. (That's how the Server
Components Performance Track works.) However, we might want to exclude
this information in profiling builds while retaining some limited form
of I/O tracking.

It also allows for logging side-effects that are not awaited if we
wanted to.

DiffTrain build for [3fb17d1](facebook@3fb17d1)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Jun 3, 2025
Stacked on facebook#33388.

This encodes the I/O entries as their own row type (`"J"`). This makes
it possible to parse them directly without first parsing the debug info
for each component. E.g. if you're just interested in logging the I/O
without all the places it was awaited.

This is not strictly necessary since the debug info is also readily
available without parsing the actual trees. (That's how the Server
Components Performance Track works.) However, we might want to exclude
this information in profiling builds while retaining some limited form
of I/O tracking.

It also allows for logging side-effects that are not awaited if we
wanted to.

DiffTrain build for [3fb17d1](facebook@3fb17d1)
sebmarkbage added a commit that referenced this pull request Jun 5, 2025
We highly recommend using Node Streams in Node.js because it's much
faster and it is less likely to cause issues when chained in things like
compression algorithms that need explicit flushing which the Web Streams
ecosystem doesn't have a good solution for. However, that said, people
want to be able to use the worse option for various reasons.

The `.edge` builds aren't technically intended for Node.js. A Node.js
environments needs to be patched in various ways to support it. It's
also less optimal since it can't use [Node.js exclusive
features](#33388) and have to use
[the lowest common
denominator](#27399) such as JS
implementations instead of native.

This adds a Web Streams build of Fizz but exclusively for Node.js so
that in it we can rely on Node.js modules. The main difference compared
to Edge is that SSR now uses `createHash` from the `"crypto"` module and
imports `TextEncoder` from `"util"`. We use `setImmediate` instead of
`setTimeout`.

The public API is just `react-dom/server` which in Node.js automatically
imports `react-dom/server.node` which re-exports the legacy bundle, Node
Streams bundle and Node Web Streams bundle. The main downside is if your
bundler isn't smart to DCE this barrel file.

With Flight the difference is larger but that's a bigger lift.
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