Skip to content

Commit 0fa3250

Browse files
authored
[Flight] Clone subsequent I/O nodes if it's resolved more than once (facebook#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.
1 parent fb0d960 commit 0fa3250

File tree

1 file changed

+22
-3
lines changed

1 file changed

+22
-3
lines changed

packages/react-server/src/ReactFlightServerConfigDebugNode.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,29 @@ export function initAsyncDebugInfo(): void {
208208
switch (node.tag) {
209209
case IO_NODE: {
210210
lastRanAwait = null;
211-
// Log the end time when we resolved the I/O. This can happen
212-
// more than once if it's a recurring resource like a connection.
211+
// Log the end time when we resolved the I/O.
213212
const ioNode: IONode = (node: any);
214-
ioNode.end = performance.now();
213+
if (ioNode.end < 0) {
214+
ioNode.end = performance.now();
215+
} else {
216+
// This can happen more than once if it's a recurring resource like a connection.
217+
// Even for single events like setTimeout, this can happen three times due to ticks
218+
// and microtasks each running its own scope.
219+
// To preserve each operation's separate end time, we create a clone of the IO node.
220+
// Any pre-existing reference will refer to the first resolution and any new resolutions
221+
// will refer to the new node.
222+
const clonedNode: IONode = {
223+
tag: IO_NODE,
224+
owner: ioNode.owner,
225+
stack: ioNode.stack,
226+
start: ioNode.start,
227+
end: performance.now(),
228+
promise: ioNode.promise,
229+
awaited: ioNode.awaited,
230+
previous: ioNode.previous,
231+
};
232+
pendingOperations.set(asyncId, clonedNode);
233+
}
215234
break;
216235
}
217236
case UNRESOLVED_AWAIT_NODE: {

0 commit comments

Comments
 (0)