There's a couple of issues with serializing Buffer in the debug renders.
For one, the Node.js Buffer has a `toJSON` on it which turns the binary
data into a JSON array which is very inefficient to serialize compared
to the real buffer. For debug info we never really want to resolve these
and unlike the regular render we can't error. So this uses the trick
where we read the original value. It's still unfortunate that this
intermediate gets created at all but at least now we're not serializing
it.
Second, we have a limit on depth of objects but we didn't have a limit
on width like large arrays or typed arrays. This omits large arrays from
the payload when possible and make them deferred when there's a debug
channel.
Flight doesn't have any semantically sound notion of a parent context.
That's why we removed Server Context. Each root can really start
anywhere in the tree when you refetch subtrees. Additionally when you
dedupe elements they can end up in multiple different parent contexts.
However, we do have a DEV only version of this with debugTask being
tracked for the nearest parent element to track the context of
properties inside of it.
To apply certain DOM specific hints and optimizations when you render
host components we need some information of the context. This is usually
very local so doesn't suffer from the likelihood that you refetch in the
middle. We'll also only use this information for optimistic hints and
not hard semantics so getting it wrong isn't terrible.
```
<picture>
<img />
</picture>
<noscript>
<p>
<img />
</p>
</noscript>
```
For example, in these cases we should exclude preloading the image but
we have to know if that's the scope we're in.
We can easily get this wrong if they're split or even if they're wrapped
in client components that we don't know about like:
```
<NoScript>
<p>
<img />
</p>
</NoScript>
```
However, getting it wrong in either direction is not the end of the
world. It's about covering the common cases well.
If we don't handle Lazy types specifically in `renderDebugModel`, all of
their properties will be emitted using `renderDebugModel` as well. This
also includes its `_debugInfo` property, if the Lazy comes from the
Flight client. That array might contain objects that are deduped, and
resolving those references in the client can cause runtime errors, e.g.:
```
TypeError: Cannot read properties of undefined (reading '$$typeof')
```
This happened specifically when an "RSC stream" debug info entry, coming
from the Flight client through IO tracking, was emitted and its
`debugTask` property was deduped, which couldn't be resolved in the
client.
To avoid actually initializing a lazy causing a side-effect, we make
some assumptions about the structure of its payload, and only emit
resolved or rejected values, otherwise we emit a halted chunk.
When we report an error we typically log the owner stack of the thing
that caught the error. Similarly we restore the `console.createTask`
scope of the catching component when we call `reportError` or
`console.error`.
We also have a special case if something throws during reconciliation
which uses the Server Component task as far as we got before we threw.
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactChildFiber.js#L1952-L1960
Chrome has since fixed it (on our request) that the Error constructor
snapshots the Task at the time the constructor was created and logs that
in `reportError`. This is a good thing since it means we get a coherent
stack. Unfortunately, it means that the fake Errors that we create in
Flight Client gets a snapshot of the task where they were created so
when they're reported in the console they get the root Task instead of
the Task of the handler of the error.
Ideally we'd transfer the Task from the server and restore it. However,
since we don't instrument the Error object to snapshot the owner and we
can't read the native Task (if it's even enabled on the server) we don't
actually have a correct snapshot to transfer for a Server Component
Error. However, we can use the parent's task for where the error was
observed by Flight Server and then encode that as a pseudo owner of the
Error.
Then we use this owner as the Task which the Error is created within.
Now the client snapshots that Task which is reported by `reportError` so
now we have an async stack for Server Component errors again. (Note that
this owner may differ from the one observed by `captureOwnerStack` which
gets the nearest Server Component from where it was caught. We could
attach the owner to the Error object and use that owner when calling
`onCaughtError`/`onUncaughtError`).
Before:
<img width="911" height="57" alt="Screenshot 2025-09-10 at 10 57 54 AM"
src="https://github.com/user-attachments/assets/0446ef96-fad9-4e17-8a9a-d89c334233ec"
/>
After:
<img width="910" height="128" alt="Screenshot 2025-09-10 at 11 06 20 AM"
src="https://github.com/user-attachments/assets/b30e5892-cf40-4246-a588-0f309575439b"
/>
Similarly, there are Errors and warnings created by ChildFiber itself.
Those execute in the scope of the general render of the parent Fiber.
They used to get the scope of the nearest client component parent (e.g.
div in this case) but that's the parent of the Server Component. It
would be too expensive to run every level of reconciliation in its own
task optimistically, so this does it only when we know that we'll throw
or log an error that needs this context. Unfortunately this doesn't
cover user space errors (such as if an iterable errors).
Before:
<img width="903" height="298" alt="Screenshot 2025-09-10 at 11 31 55 AM"
src="https://github.com/user-attachments/assets/cffc94da-8c14-4d6e-9a5b-bf0833b8b762"
/>
After:
<img width="1216" height="252" alt="Screenshot 2025-09-10 at 11 50
54 AM"
src="https://github.com/user-attachments/assets/f85f93cf-ab73-4046-af3d-dd93b73b3552"
/>
<img width="412" height="115" alt="Screenshot 2025-09-10 at 11 52 46 AM"
src="https://github.com/user-attachments/assets/a76cef7b-b162-4ecf-9b0a-68bf34afc239"
/>
When we emit objects of type `ReactAsyncInfo`, we need to make sure that
their owners are outlined, using `outlineComponentInfo`. Otherwise we
would end up accidentally emitting stashed fields that are not part of
the transport protocol, specifically `debugStack`, `debugTask`, and
`debugLocation`. This would lead to runtime errors in the client, when
for example, the stack for a `debugLocation` is processed in
`buildFakeCallStack`, but the stack was actually omitted from the RSC
payload, because for those fields we don't ensure that the object limit
is increased by the length of the stack, as we do when we're emitting
the `stack` of a `ReactComponentInfo` object in `outlineComponentInfo`.
This ensures that if the name is set manually after the declaration,
then we get that name when we log the value. For example Node.js
`Response` is declared as `_Response` and then later assigned a new
name.
We should probably really serialize all static enumerable properties but
"name" is non-enumerable so it's still a special case.
There's a lot of overlap between `enableComponentPerformanceTrack` and
`enableAsyncDebugInfo` because they both rely on timing information. The
former is mainly emit timestamps for how long server components and
awaits took. The latter how long I/O took.
`enableAsyncDebugInfo` is currently primarily for the component
performance track but its meta data is useful for other debug tools too.
This promotes that flag to stable.
However, `enableComponentPerformanceTrack` needs more work due to
performance concerns with Chrome DevTools so I need to separate them.
This keeps doing most of the timing tracking on the server but doesn't
emit the per-server component time stamps when
`enableComponentPerformanceTrack` is false.
There is an edge case when prerendering where if you have nothing to
write you can end up in a state where the prerender is in status closed
before you can provide a destination. In this case the destination is
never closed becuase it assumes it already would have been.
This condition can happen now because of the introduction of the deubg
stream. Before this a request would never entere closed status if there
was no active destination. When a destination was added it would perform
a flush and possibly close the stream. Now, it is possible to flush
without a destination because you might have debug chunks to stream and
you can end up closing the stream independent of an active destination.
There are a number of ways we can solve this but the one that seems to
adhere best to the original design is to only set the status to CLOSED
when a destination is active. This means that if you don't have an
active destination when the pendingChunks count hits zero it will not
enter CLOSED status until you startFlowing.
We need a "value" to represent the I/O that was loaded. We don't
normally actually use the Promise at the callsite that started the I/O
because that's usually deep inside internals. Instead we override the
value of the I/O entry with the Promise that was first awaited in user
space. This means that you could potentially have different values
depending on if multiple things await the same I/O. We just take one of
them. (Maybe we should actually just write the first user space awaited
Promise as the I/O entry? This might instead have other implications
like less deduping.)
When you pass a Promise forward, we may skip the awaits that happened in
earlier components because they're not part of the currently rendering
component. That's mainly for the stack and time stamps though. The value
is still probably conceptually the best value because it represents the
I/O value as far user space is concerned.
This writes the I/O early with the first await we find in user space
even if we're not going to use that particular await for the stack.
If you pass a promise to a client component to be rendered `<Client
promise={promise} />` then there's an internal await inside Flight.
There might also be user space awaits but those awaits may already have
happened before we render this component. Conceptually they were part of
the parent component and not this component. It's tricky to attribute
which await should be used for the stack in this case.
If we can't find an await we can use the JSX callsite as the stack
frame.
However, we don't want to do this for simple cases like if you return a
non-native Promise from a Server Component. Since that would now use the
stack of the thing that rendered the Server Component which is worse
than the stack of the I/O. To fix this, I update the
`debugOwner`/`debugTask`/`debugStack` when we start rendering inside the
Server Component. Conceptually these represent the "parent" component
and is used for errors referring to the parent like when we serialize
client component props the parent is the JSX of the client component.
However, when we're directly inside the Server Component we don't have a
callsite of the parent really. Conceptually it would be the return call
of the Server Component. This might negatively affect other types of
errors but I think this is ok since this feature mainly exists for the
case when you enter the child JSX.
This resolves an outstanding issue where it was possible for debug info
and console logs to become out of order if they up blocked. E.g. by a
future reference or a client reference that hasn't loaded yet. Such as
if you console.log a client reference followed by one that doesn't. This
encodes the order similar to how the stream chunks work.
This also blocks the main chunk from resolving until the last debug info
has fully loaded, including future references and client references.
This also ensures that we could send some of that data in a different
stream, since then it can come out of order.
We already do this with `"new Promise"` and `"Promise.then"`. There are
also many helpers that both create promises and awaits other promises
inside of it like `Promise.all`.
The way this is filtered is different from just filtering out all
anonymous stacks since they're used to determine where the boundary is
between ignore listed and user space.
Ideally we'd cover more wrappers that are internal to Promise libraries.
This lets us pass a writable on the server side and readable on the
client side to send debug info through a separate channel so that it
doesn't interfere with the main payload as much. The main payload refers
to chunks defined in the debug info which means it's still blocked on it
though. This ensures that the debug data has loaded by the time the
value is rendered so that the next step can forward the data.
This will be a bit fragile to race conditions until #33665 lands.
Another follow up needed is the ability to skip the debug channel on the
receiving side. Right now it'll block forever if you don't provide one
since we're blocking on the debug data.
This is the same as we do for currently rendering tasks. They get
effectively sync aborted when the listener is invoked.
We potentially miss out on some debug info in that case but that would
only apply to any entries inside the stream which doesn't really have
their own debug info anyway.
If we have the ability to lazy load Promise values, i.e. if we have a
debug channel, then we should always use it for Promises that aren't
already resolved and instrumented.
There's little downside to this since they're async anyway.
This also lets us avoid adding `.then()` listeners too early. E.g. if
adding the listener would have side-effect. This avoids covering up
"unhandled rejection" errors. Since if we listen to a promise eagerly,
including reject listeners, we'd have marked that Promise's rejection as
handled where as maybe it wouldn't have been otherwise.
In this mode we can also indefinitely wait for the Promise to resolve
instead of just waiting a microtask for it to resolve.
We use the stack of a Promise as the start of the I/O instead of the
actual I/O since that can symbolize the start of the operation even if
the actual I/O is batched, deduped or pooled. It can also group multiple
I/O operations into one.
We want the deepest possible Promise since otherwise it would just be
the Component's Promise.
However, we don't really need deeper than the boundary between first
party and third party. We can't just take the outer most that has third
party things on the stack though because third party can have callbacks
into first party and then we want the inner one. So we take the inner
most Promise that depends on I/O that has a first party stack on it.
The realization is that for the purposes of determining whether we have
a first party stack we need to ignore async stack frames. They can
appear on the stack when we resume third party code inside a resumption
frame of a first party stack.
<img width="832" alt="Screenshot 2025-07-08 at 6 34 25 PM"
src="https://github.com/user-attachments/assets/1636f980-be4c-4340-ad49-8d2b31953436"
/>
---------
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
When we know that the object that we pass in is immediately parsed, then
we know it couldn't have been reified into a unstructured stack yet. In
this path we assume that we'll trigger `Error.prepareStackTrace`.
Since we know that nobody else will read the stack after us, we can skip
generating a string stack and just return empty. We can also skip
caching.
If we're about to defer an object, then we shouldn't store a reference
to it because then we can end up deduping by referring to the deferred
string. If in a different context, we should still be able to emit the
object.
Because the object limit is unfortunately depth first due to limitations
of JSON stringify, we need to ensure that things we really don't want
outlined are first in the enumeration order.
We add the stack length to the object limit to ensure that the stack
frames aren't outlined. In console all the user space arguments are at
the end of the args. In server component props, the props are at the end
of the properties of the element.
For the `value` of I/O we had it before the stack so it could steal the
limit from the stack. The fix is to put it at the end.
When a debug channel is available, we now allow objects to be lazily
requested though the debug channel and only then will the server send
it.
The client will actually eagerly ask for the next level of objects once
it parses its payload. That way those objects have likely loaded by the
time you actually expand that deep e.g. in the console repl. This is
needed since the console repl is synchronous when you ask it to invoke
getters.
Each level is lazily parsed which means that we don't parse the next
level even though we eagerly loaded it. We parse it once the getter is
invoked (in Chrome DevTools you have to click a little `(...)` to invoke
the getter). When the getter is invoked, the chunk is initialized and
parsed. This then causes the next level to be asked for through the
debug channel. Ensuring that if you expand one more level you can do so
synchronously.
Currently debug chunks are eagerly parsed, which means that if you have
things like server component props that are lazy they can end up being
immediately asked for, but I'm trying to move to make the debug chunks
lazy.
We need to optimize the collection of debug info for dev mode. This is
an incredibly hot path since it instruments all I/O and Promises in the
app.
These optimizations focus primarily on the collection of stack traces.
They are expensive to collect because we need to eagerly collect the
stacks since they can otherwise cause memory leaks. We also need to do
some of the processing of them up front. We also end up only using a few
of them in the end but we don't know which ones we'll use.
The first compromise here is that I now only collect the stacks of
"awaits" if they were in a specific request's render. In some cases it's
useful to collect them even outside of this if they're part of a
sequence that started early. I still collect stacks for the created
Promises outside of this though which can still provide some context.
The other optimization to awaits, is that since we'll only use the inner
most one that had an await directly in userspace, we can stop collecting
stacks on a chain of awaits after we find one. This requires a quick
filter on a single callsite to determine. Since we now only collect
stacks from awaits that belongs to a specific Request we can use that
request's specific filter option. Technically this might not be quite
correct if that same thing ends up deduped across Requests but that's an
edge case.
Additionally, I now stop collecting stack for I/O nodes. They're almost
always superseded by the Promise that wraps them anyway. Even if you
write mostly Promise free code, you'll likely end up with a Promise at
the root of the component eventually anyway and then you end up using
its stack anyway. You have to really contort the code to end up with
zero Promises at which point it's not very useful anyway. At best it's
maybe mostly useful for giving a name to the I/O when the rest is just
stuff like `new Promise`.
However, a possible alternative optimization could be to *only* collect
the stack of spawned I/O and not the stack of Promises. The issue with
Promises (not awaits) is that we never know what will end up resolving
them in the end when they're created so we have to always eagerly
collect stacks. This could be an issue when you have a lot of
abstractions that end up not actually be related to I/O at all. The
issue with collecting stacks only for I/O is that the actual I/O can be
pooled or batched so you end up not having the stack when the conceptual
start of each operation within the batch started. Which is why I decided
to keep the Promise stack.
Same as #33716 but without the separate close signal.
We'll need the ref count for separate debug channel anyway but I'm not
sure we'll need the separate close signal.
If I/O is not awaited in user space in a "previous" path we used to just
drop it on the floor. There's a few strategies we could apply here. My
first commit just emits it without an await but that would mean we don't
have an await stack when there's no I/O in a follow up.
I went with a strategy where the "previous" I/O is used only if the
"next" didn't have I/O. This may still drop I/O on the floor if there's
two back to back within internals for example. It would only log the
first one even though the outer await may have started earlier.
It may also log deeper in the "next" path if that had user space stacks
and then the outer await will appear as if it awaited after.
So it's not perfect.
When a `.then()` callback returns another Promise, there's effectively
another "await" on that Promise that happens in the internals but that
was not modeled. In effect the Promise returned by `.then()` is blocked
on both the original Promise AND the promise returned by the callback.
This models that by cloning the original node and treat that as the
await on the original Promise. Then we use the existing Node to await
the new Promise but its "previous" points to the clone. That way we have
a forked node that awaits both.
---------
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
This delays the abort by splitting the abort into a first step that just
flags a task as abort and tracks the time that we aborted. This first
step also invokes the `cacheSignal()` abort handler.
Then in a macrotask do we finish flushing the abort (or halt). This
ensures that any microtasks after the abort signal can finish flushing
which may emit rejections or fulfill (e.g. if you try/catch the abort or
if it was allSettled). These rejections are themselves signals for which
promise was blocked on what promise which forms a graph that we can use
for debug info. Notably this doesn't include any additional data in the
output since we don't include any data produced after the abort. It just
uses the additional execution to collect more debug info.
The abort itself might not have been spawned from I/O but it's still
interesting to mark Promises that aborted as interesting since they may
have been blocked on I/O. So we take the inner most Promise that
resolved after the end time (presumably due to the abort signal but also
could've just finished after but that's still after the abort).
Since the microtasks can spawn new Promises after the ones that reject
we ignore any of those that started after the abort.
If a FlightClient runs inside a FlightServer like fetching from a third
party and that logs, then we currently double badge them since we just
add on another badge. The issue is that this might be unnecessarily
noisy but we also transfer the original format of the current server
into the second badge.
This extracts our own badge and then adds the environment name as
structured data which lets the client decide how to format it.
Before:
<img width="599" alt="Screenshot 2025-07-02 at 2 30 07 PM"
src="https://github.com/user-attachments/assets/4bf26a29-b3a8-4024-8eb9-a3f90dbff97a"
/>
After:
<img width="590" alt="Screenshot 2025-07-02 at 2 32 56 PM"
src="https://github.com/user-attachments/assets/f06bbb6d-fbb1-4ae6-b0e3-775849fe3c53"
/>
This writes all debug info to a separate priority queue. In the future
I'll put this on a different channel.
Ideally I think we'd put it in the bottom of the stream but because it
actually blocks the elements from resolving anyway it ends up being
better to put them ahead. At least for now.
When we have two separate channels it's not possible to rely on the
order for consistency Even then we might write to that queue first for
this reason. We can't rely on it though. Which will show up like things
turning into Lazy instead of Element similar to how outlining can.
There's a special case where if we create a new task, e.g. to serialize
a promise like `<div>{promise}</div>` then that row doesn't have any
start time emitted but it has a `task.time` inherited. We mostly don't
need this because every other operation emits its own start time. E.g.
when we started rendering a Server Component or the real start time of a
real `await`.
For these implied awaits we don't have a start time. Ideally it would
probably be when we started the serialization, like when we called
`.then()` but we can't just emit that eagerly and we can't just advance
the `task.time` because that time represents the last render or previous
await and we use that to cut off awaits. However for this case we don't
want to cut off any inner awaits inside the node we're serializing if
they happened before the `.then()`.
Therefore, I just use the time of the previous operation - which is
likely either the resolution of a previous promise that blocked the
`<div>` like the promise of the Server Component that rendered it, or
just the start of the Server Component if it was sync.
<img width="926" alt="Screenshot 2025-06-25 at 1 02 14 PM"
src="https://github.com/user-attachments/assets/1877d13d-5259-4cc4-8f48-12981e3073fe"
/>
The I/O entry doesn't show as aborted in the Server Request track
because technically it wasn't. The end time is just made up. It's still
going. It's not aborted until the abort signal propagates and if we do
get that signal wired up before it emits, it instead would show up as
rejected.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
If an aborted task is not rendering, then this is an async abort.
Conceptually it's as if the abort happened inside the async gap. The
abort reason's stack frame won't have that on the stack so instead we
use the owner stack and debug task of any halted async debug info.
One thing that's a bit awkward is that if you do have a sync abort and
you use that error as the "reason" then that thing still has a sync
stack in a different component. In another approach I was exploring
having different error objects for each component but I don't think
that's worth it.
Now that we have `cacheSignal()` we can just use that instead of the
`abortListeners` concept which was really just the same thing for
cancelling the streams (ReadableStream, Blob, AsyncIterable).
When we abort a render we don't really have much information about the
task that was aborted. Because before a Promise resolves there's no
indication about would have resolved it. In particular we don't know
which I/O would've ultimately called resolve().
However, we can at least emit any information we do have at the point
where we emit it. At the least the stack of the top most Promise.
Currently we synchronously flush at the end of an `abort()` but we
should ideally schedule the flush in a macrotask and emit this debug
information right before that. That way we would give an opportunity for
any `cacheSignal()` abort to trigger rejections all the way up and those
rejections informs the awaited stack.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
This adds plumbing for opening a stream from the Flight Client to the
Flight Server so it can ask for more data on-demand. In this mode, the
Flight Server keeps the connection open as long as the client is still
alive and there's more objects to load. It retains any depth limited
objects so that they can be asked for later. In this first PR it just
releases the object when it's discovered on the server and doesn't
actually lazy load it yet. That's coming in a follow up.
This strategy is built on the model that each request has its own
channel for this. Instead of some global registry. That ensures that
referential identity is preserved within a Request and the Request can
refer to previously written objects by reference.
The fixture implements a WebSocket per request but it doesn't have to be
done that way. It can be multiplexed through an existing WebSocket for
example. The current protocol is just a Readable(Stream) on the server
and WritableStream on the client. It could even be sent through a HTTP
request body if browsers implemented full duplex (which they don't).
This PR only implements the direction of messages from Client to Server.
However, I also plan on adding Debug Channel in the other direction to
allow debug info (optionally) be sent from Server to Client through this
channel instead of through the main RSC request. So the `debugChannel`
option will be able to take writable or readable or both.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
Stacked on #33588, #33589 and #33590.
This lets us automatically show the resolved value in the UI.
<img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM"
src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4"
/>
We can also show rejected I/O that may or may not have been handled with
the error message.
<img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM"
src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1"
/>
To get this working we need to keep the Promise around for longer so
that we can access it once we want to emit an async sequence. I do this
by storing the WeakRefs but to ensure that the Promise doesn't get
garbage collected, I keep a WeakMap of Promise to the Promise that it
depended on. This lets the VM still clean up any Promise chains that
have leaves that are cleaned up. So this makes Promises live until the
last Promise downstream is done. At that point we can go back up the
chain to read the values out of them.
Additionally, to get the best possible value we don't want to get a
Promise that's used by internals of a third-party function. We want the
value that the first party gets to observe. To do this I had to change
the logic for which "await" to use, to be the one that is the first
await that happened in user space. It's not enough that the await has
any first party at all on the stack - it has to be the very first frame.
This is a little sketchy because it relies on the `.then()` call or
`await` call not having any third party wrappers. But it gives the best
object since it hides all the internals. For example when you call
`fetch()` we now log that actual `Response` object.
This adds better support for serializing class instances as Debug
values.
It adds a new marker on the object `{ "": "$P...", ... }` which
indicates which constructor's prototype to use for this object's
prototype. It doesn't encode arbitrary prototypes and it doesn't encode
any of the properties on the prototype. It might get some of the
properties from the prototype by virtue of `toString` on a `class`
constructor will include the whole class's body.
This will ensure that the instance gets the right name in logs.
Additionally, this now also invokes getters if they're enumerable on the
prototype. This lets us reify values that can only be read from native
classes.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
We already support serializing the values of instrumented Promises as
debug values such as in console logs. However, we don't support plain
native promises.
This waits a microtask to see if we can read the value within a
microtask and if so emit it. This is so that we can still close the
connection.
Otherwise, we emit a "halted" row into its row id which replaces the old
"Infinite Promise" reference.
We could potentially wait until the end of the render before cancelling
so that if it resolves before we exit we can still include its value but
that would require a bit more work. Ideally we'd have a way to get these
lazily later anyway.
There's a memory leak in DebugNode where the `Error` objects that we
instantiate retains their callstacks which can have Promises on them. In
fact, it's very likely since the current callsite has the "resource" on
it which is the Promise itself. If those Promises are retained then
their `destroy` async hook is never fired which doesn't clean up our map
which can contains the `Error` object. Creating a cycle that can't be
cleaned up.
This fix is just eagerly reifying and parsing the stacks.
I totally expect this to be crazy slow since there's so many Promises
that we end up not needing to visit otherwise. We'll need to optimize it
somehow. Perhaps by being smarter about which ones we might need stacks
for. However, at least it doesn't leak indefinitely.
Stacked on #33539.
Stores dedupes of `renderConsoleValue` in a separate set. This allows us
to dedupe objects safely since we can't write objects using this
algorithm if they might also be referenced by the "real" serialization.
Also renamed it to `renderDebugModel` since it's not just for console
anymore.
On pages that have a high number of server components (e.g. common when
doing syntax highlighting), the debug outlining can produce extremely
large RSC payloads. For example a documentation page I was working on
had a 13.8 MB payload. I noticed that a majority of this was the source
code for the same function components repeated over and over again (over
4000 times) within `$E()` eval commands.
This PR deduplicates the same functions by serializing by reference,
similar to what is already done for objects. Doing this reduced the
payload size of my page from 13.8 MB to 4.6 MB, and resulted in only 31
evals instead of over 4000. As a result it reduced development page load
and hydration time from 4 seconds to 1.5 seconds. It also means the
deserialized functions will have reference equality just as they did on
the server.
This was really meant to be there from the beginning. A `cache()`:ed
entry has a life time. On the server this ends when the render finishes.
On the client this ends when the cache of that scope gets refreshed.
When a cache is no longer needed, it should be possible to abort any
outstanding network requests or other resources. That's what
`cacheSignal()` gives you. It returns an `AbortSignal` which aborts when
the cache lifetime is done based on the same execution scope as a
`cache()`ed function - i.e. `AsyncLocalStorage` on the server or the
render scope on the client.
```js
import {cacheSignal} from 'react';
async function Component() {
await fetch(url, { signal: cacheSignal() });
}
```
For `fetch` in particular, a patch should really just do this
automatically for you. But it's useful for other resources like database
connections.
Another reason it's useful to have a `cacheSignal()` is to ignore any
errors that might have triggered from the act of being aborted. This is
just a general useful JavaScript pattern if you have access to a signal:
```js
async function getData(id, signal) {
try {
await queryDatabase(id, { signal });
} catch (x) {
if (!signal.aborted) {
logError(x); // only log if it's a real error and not due to cancellation
}
return null;
}
}
```
This just gets you a convenient way to get to it without drilling
through so a more idiomatic code in React might look something like.
```js
import {cacheSignal} from "react";
async function getData(id) {
try {
await queryDatabase(id);
} catch (x) {
if (!cacheSignal()?.aborted) {
logError(x);
}
return null;
}
}
```
If it's called outside of a React render, we normally treat any cached
functions as uncached. They're not an error call. They can still load
data. It's just not cached. This is not like an aborted signal because
then you couldn't issue any requests. It's also not like an infinite
abort signal because it's not actually cached forever. Therefore,
`cacheSignal()` returns `null` when called outside of a React render
scope.
Notably the `signal` option passed to `renderToReadableStream` in both
SSR (Fizz) and RSC (Flight Server) is not the same instance that comes
out of `cacheSignal()`. If you abort the `signal` passed in, then the
`cacheSignal()` is also aborted with the same reason. However, the
`cacheSignal()` can also get aborted if the render completes
successfully or fatally errors during render - allowing any outstanding
work that wasn't used to clean up. In the future we might also expand on
this to give different
[`TaskSignal`](https://developer.mozilla.org/en-US/docs/Web/API/TaskSignal)
to different scopes to pass different render or network priorities.
On the client version of `"react"` this exposes a noop (both for
Fiber/Fizz) due to `disableClientCache` flag but it's exposed so that
you can write shared code.