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

[Fiber] Track debugInfo on module state instead of stack #29807

Merged
merged 1 commit into from
Jun 11, 2024

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jun 7, 2024

Stacked on #29804.

Transferring of debugInfo was added in #28286. It represents the parent stack between the current Fiber and into the next Fiber we're about to create. I.e. Server Components in between. I don't love passing DEV-only fields as arguments anyway since I don't trust closure to remove unused arguments that way. EDIT: Actually it seems like closure handled that just fine before which is why this is no change in prod.

Instead, I track it on the module scope. Notably with DON'T use try/finally because if something throws we want to observe what it was at the time we threw. Like the pattern we use many other places.

Now we can use this when we create the Throw Fiber to associate the Server Components that were part of the parent stack before this error threw. There by giving us the correct parent stacks at the location that threw.

@sebmarkbage sebmarkbage requested review from gnoff and acdlite June 7, 2024 20:20
Copy link

vercel bot commented Jun 7, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 11, 2024 8:55pm

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jun 7, 2024
@@ -1963,7 +1958,12 @@ function createChildReconciler(
// the error or suspending if needed.
const throwFiber = createFiberFromThrow(x, returnFiber.mode, lanes);
throwFiber.return = returnFiber;
if (__DEV__) {
throwFiber._debugInfo = currentDebugInfo;
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 the effective change. We now can pick this up from module scope before clearing it.

@react-sizebot
Copy link

react-sizebot commented Jun 7, 2024

Comparing: 270229f...25fc6fa

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.66 kB 6.66 kB = 1.82 kB 1.82 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 497.80 kB 497.80 kB = 89.24 kB 89.23 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.67 kB 6.67 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 502.62 kB 502.62 kB = 89.94 kB 89.94 kB
facebook-www/ReactDOM-prod.classic.js = 597.57 kB 597.56 kB = 105.37 kB 105.38 kB
facebook-www/ReactDOM-prod.modern.js = 571.50 kB 571.49 kB = 101.27 kB 101.27 kB
test_utils/ReactAllWarnings.js Deleted 63.88 kB 0.00 kB Deleted 15.96 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
test_utils/ReactAllWarnings.js Deleted 63.88 kB 0.00 kB Deleted 15.96 kB 0.00 kB

Generated by 🚫 dangerJS against 25fc6fa

Notably with DON'T use try/finally because if something throws we want
to observe what it was at the time we threw.

Now we can use this when we create the Throw Fiber to associate the
Server Components that were part of the parent stack before this error
threw.
@sebmarkbage sebmarkbage merged commit 2c959f1 into facebook:main Jun 11, 2024
44 checks passed
sebmarkbage added a commit that referenced this pull request Jun 11, 2024
Stacked on #29807.

Conceptually the error's owner/task should ideally be captured when the
Error constructor is called but neither `console.createTask` does this,
nor do we override `Error` to capture our `owner`. So instead, we use
the nearest parent as the owner/task of the error. This is usually the
same thing when it's thrown from the same async component but not if you
await a promise started from a different component/task.

Before this stack the "owner" and "task" of a Lazy that errors was the
nearest Fiber but if the thing erroring is a Server Component, we need
to get that as the owner from the inner most part of debugInfo.

To get the Task for that Server Component, we need to expose it on the
ReactComponentInfo object. Unfortunately that makes the object not
serializable so we need to special case this to exclude it from
serialization. It gets restored again on the client.

Before (Shell):
<img width="813" alt="Screenshot 2024-06-06 at 5 16 20 PM"
src="https://github.com/facebook/react/assets/63648/7da2d4c9-539b-494e-ba63-1abdc58ff13c">

After (App):
<img width="811" alt="Screenshot 2024-06-08 at 12 29 23 AM"
src="https://github.com/facebook/react/assets/63648/dbf40bd7-c24d-4200-81a6-5018bef55f6d">
github-actions bot pushed a commit that referenced this pull request Jun 11, 2024
Stacked on #29807.

Conceptually the error's owner/task should ideally be captured when the
Error constructor is called but neither `console.createTask` does this,
nor do we override `Error` to capture our `owner`. So instead, we use
the nearest parent as the owner/task of the error. This is usually the
same thing when it's thrown from the same async component but not if you
await a promise started from a different component/task.

Before this stack the "owner" and "task" of a Lazy that errors was the
nearest Fiber but if the thing erroring is a Server Component, we need
to get that as the owner from the inner most part of debugInfo.

To get the Task for that Server Component, we need to expose it on the
ReactComponentInfo object. Unfortunately that makes the object not
serializable so we need to special case this to exclude it from
serialization. It gets restored again on the client.

Before (Shell):
<img width="813" alt="Screenshot 2024-06-06 at 5 16 20 PM"
src="https://github.com/facebook/react/assets/63648/7da2d4c9-539b-494e-ba63-1abdc58ff13c">

After (App):
<img width="811" alt="Screenshot 2024-06-08 at 12 29 23 AM"
src="https://github.com/facebook/react/assets/63648/dbf40bd7-c24d-4200-81a6-5018bef55f6d">

DiffTrain build for commit 383b2a1.
github-actions bot pushed a commit that referenced this pull request Jun 11, 2024
Stacked on #29807.

Conceptually the error's owner/task should ideally be captured when the
Error constructor is called but neither `console.createTask` does this,
nor do we override `Error` to capture our `owner`. So instead, we use
the nearest parent as the owner/task of the error. This is usually the
same thing when it's thrown from the same async component but not if you
await a promise started from a different component/task.

Before this stack the "owner" and "task" of a Lazy that errors was the
nearest Fiber but if the thing erroring is a Server Component, we need
to get that as the owner from the inner most part of debugInfo.

To get the Task for that Server Component, we need to expose it on the
ReactComponentInfo object. Unfortunately that makes the object not
serializable so we need to special case this to exclude it from
serialization. It gets restored again on the client.

Before (Shell):
<img width="813" alt="Screenshot 2024-06-06 at 5 16 20 PM"
src="https://github.com/facebook/react/assets/63648/7da2d4c9-539b-494e-ba63-1abdc58ff13c">

After (App):
<img width="811" alt="Screenshot 2024-06-08 at 12 29 23 AM"
src="https://github.com/facebook/react/assets/63648/dbf40bd7-c24d-4200-81a6-5018bef55f6d">

DiffTrain build for [383b2a1](383b2a1)
sebmarkbage added a commit that referenced this pull request Jun 11, 2024
…azy (#29823)

Stacked on #29807.

This lets the nearest Suspense/Error Boundary handle it even if that
boundary is defined by the model itself.

It also ensures that when we have an error during serialization of
properties, those can be associated with the nearest JSX element and
since we have a stack/owner for that element we can use it to point to
the source code of that line. We can't track the source of any nested
arbitrary objects deeper inside since objects don’t track their stacks
but close enough. Ideally we have the property path but we don’t have
that right now. We have a partial in the message itself.

<img width="813" alt="Screenshot 2024-06-09 at 10 08 27 PM"
src="https://github.com/facebook/react/assets/63648/917fbe0c-053c-4204-93db-d68a66e3e874">

Note: The component name (Counter) is lost in the first message because
we don't print it in the Task. We use `"use client"` instead because we
expect the next stack frame to have the name. We also don't include it
in the actual error message because the Server doesn't know the
component name yet. Ideally Client References should be able to have a
name. If the nearest is a Host Component then we do use the name though.
However, it's not actually inside that Component that the error happens
it's in App and that points to the right line number.

An interesting case is that if something that's actually going to be
consumed by the props to a Suspense/Error Boundary or the Client
Component that wraps them fails, then it can't be handled by the
boundary. However, a counter intuitive case might be when that's on the
`children` props. E.g.
`<ErrorBoundary>{clientReferenceOrInvalidSerialization}</ErrorBoundary>`.
This value can be inspected by the boundary so it's not safe to pass it
so if it's errored it is not caught.

## Implementation

The first insight is that this is best solved on the Client rather than
in the Server because that way it also covers Client References that end
up erroring.

The key insight is that while we don't have a true stack when using
`JSON.parse` and therefore no begin/complete we can still infer these
phases for Elements because the first child of an Element is always
`'$'` which is also a leaf. In depth first that's our begin phase. When
the Element itself completes, we have the complete phase. Anything in
between is within the Element.

Using this idea I was able to refactor the blocking tracking mechanism
to stash the blocked information on `initializingHandler` and then on
the way up do we let whatever is nearest handle it - whether that's an
Element or the root Chunk. It's kind of like an Algebraic Effect.

cc @unstubbable This is something you might want to deep dive into to
find more edge cases. I'm sure I've missed something.

---------

Co-authored-by: eps1lon <[email protected]>
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