mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[Flight] Clone subsequent I/O nodes if it's resolved more than once (#35003)
IO tasks can execute more than once. E.g. a connection may fire each
time a new message or chunk comes in or a setInterval every time it
executes.
We used to treat these all as one I/O node and just updated the end time
as we go. Most of the time this was fine because typically you would
have a Promise instance whose end time is really the one that gets used
as the I/O anyway.
However, in a pattern like this it could be problematic:
```js
setTimeout(() => {
function App() {
return Promise.resolve(123);
}
renderToReadableStream(<App />);
});
```
Because the I/O's end time is before the render started so it should be
excluded from being considered I/O as part of the render. It happened
outside of render. But because the `Promise.resolve()` is inside render
its end time is after the render start so the promise is considered part
of the render. This is usually not a problem because the end time of the
I/O is still before the start of the render so even though the Promise
is valid it has no I/O source so it's properly excluded.
However, if the I/O's end time updates before we observe this then the
I/O can be considered part of the render. E.g. if this was a setInterval
it would be clearly wrong. But it turns out that even setTimeout can
sometimes execute more than once in the async_hooks because each run of
"process.nextTick" and microtasks respectively are ran in their own
before/after. When a micro task executes after this main body it'll
update the end time which can then turn the whole I/O as being inside
the render.
To solve this properly I create a new I/O node each time before() is
invoked so that each one gets to observe a different end time. This has
a potential CPU and memory allocation cost when there's a lot of them
like in a quick stream.
This commit is contained in:
parent
fb0d96073c
commit
0fa32506da
|
|
@ -208,10 +208,29 @@ export function initAsyncDebugInfo(): void {
|
|||
switch (node.tag) {
|
||||
case IO_NODE: {
|
||||
lastRanAwait = null;
|
||||
// Log the end time when we resolved the I/O. This can happen
|
||||
// more than once if it's a recurring resource like a connection.
|
||||
// Log the end time when we resolved the I/O.
|
||||
const ioNode: IONode = (node: any);
|
||||
ioNode.end = performance.now();
|
||||
if (ioNode.end < 0) {
|
||||
ioNode.end = performance.now();
|
||||
} else {
|
||||
// This can happen more than once if it's a recurring resource like a connection.
|
||||
// Even for single events like setTimeout, this can happen three times due to ticks
|
||||
// and microtasks each running its own scope.
|
||||
// To preserve each operation's separate end time, we create a clone of the IO node.
|
||||
// Any pre-existing reference will refer to the first resolution and any new resolutions
|
||||
// will refer to the new node.
|
||||
const clonedNode: IONode = {
|
||||
tag: IO_NODE,
|
||||
owner: ioNode.owner,
|
||||
stack: ioNode.stack,
|
||||
start: ioNode.start,
|
||||
end: performance.now(),
|
||||
promise: ioNode.promise,
|
||||
awaited: ioNode.awaited,
|
||||
previous: ioNode.previous,
|
||||
};
|
||||
pendingOperations.set(asyncId, clonedNode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UNRESOLVED_AWAIT_NODE: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user