From fb0d96073c588ca2f652f9c97849234576db534f Mon Sep 17 00:00:00 2001 From: Ricky Date: Tue, 28 Oct 2025 12:53:30 -0400 Subject: [PATCH 1/2] [tests] disableLegacyMode in test-renderer (#35002) 500 tests failed from not using async act. Will fix the tests and then re-land this. --- .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 0ff044250cb2e..eb56da603d309 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -16,7 +16,7 @@ export const disableCommentsAsDOMContainers = true; export const disableInputAttributeSyncing = false; export const disableLegacyContext = false; export const disableLegacyContextForFunctionComponents = false; -export const disableLegacyMode = true; +export const disableLegacyMode = false; export const disableSchedulerTimeoutInWorkLoop = false; export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; From 0fa32506dab4293dfffae662e181d2f970aa95ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 28 Oct 2025 13:27:35 -0400 Subject: [PATCH 2/2] [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(); }); ``` 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. --- .../src/ReactFlightServerConfigDebugNode.js | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/react-server/src/ReactFlightServerConfigDebugNode.js b/packages/react-server/src/ReactFlightServerConfigDebugNode.js index e79c19cc73aa6..9bb521be4065d 100644 --- a/packages/react-server/src/ReactFlightServerConfigDebugNode.js +++ b/packages/react-server/src/ReactFlightServerConfigDebugNode.js @@ -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: {