@samitouri / QOS-React-1 / commits / a7a116577d

[Flight] Don't track Promise stack if there's no owner (#33734)

This is a compromise because there can be a lot of Promise instances created. They're useful because they generally provide a better stack when batching/pooled connections are used. This restores stack collection for I/O nodes so we have something to fallback on if there's no owner. That way we can at least get a name or something out of I/O that was spawned outside a render but mostly avoids collecting starting I/O outside of render.

Sebastian Markbåge committed Jul 8, 2025 at 13:02 UTC a7a116577daf3b135c226ed9db8a8c2f9166c023
2 files changed +10 -7
packages/react-server/src/ReactFlightAsyncSequence.js
+1 -1
@@ -26,7 +26,7 @@ type PromiseWithDebugInfo = interface extends Promise<any> {
26 export type IONode = {
27 tag: 0,
28 owner: null | ReactComponentInfo,
29 - stack: null, // callsite that spawned the I/O
29 + stack: null | ReactStackTrace, // callsite that spawned the I/O
30 start: number, // start time when the first part of the I/O sequence started
31 end: number, // we typically don't use this. only when there's no promise intermediate.
32 promise: null, // not used on I/O
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+9 -6
@@ -149,10 +149,11 @@ export function initAsyncDebugInfo(): void {
149 previous: current === undefined ? null : current, // The path that led us here.
150 }: UnresolvedAwaitNode);
151 } else {
152 + const owner = resolveOwner();
153 node = ({
154 tag: UNRESOLVED_PROMISE_NODE,
154 - owner: resolveOwner(),
155 - stack: parseStackTrace(new Error(), 5),
155 + owner: owner,
156 + stack: owner === null ? null : parseStackTrace(new Error(), 5),
157 start: performance.now(),
158 end: -1.1, // Set when we resolve.
159 promise: new WeakRef((resource: Promise<any>)),
@@ -170,10 +171,11 @@ export function initAsyncDebugInfo(): void {
171 ) {
172 if (trigger === undefined) {
173 // We have begun a new I/O sequence.
174 + const owner = resolveOwner();
175 node = ({
176 tag: IO_NODE,
175 - owner: resolveOwner(),
176 - stack: null,
177 + owner: owner,
178 + stack: owner === null ? parseStackTrace(new Error(), 3) : null,
179 start: performance.now(),
180 end: -1.1, // Only set when pinged.
181 promise: null,
@@ -185,10 +187,11 @@ export function initAsyncDebugInfo(): void {
187 trigger.tag === UNRESOLVED_AWAIT_NODE
188 ) {
189 // We have begun a new I/O sequence after the await.
190 + const owner = resolveOwner();
191 node = ({
192 tag: IO_NODE,
190 - owner: resolveOwner(),
191 - stack: null,
193 + owner: owner,
194 + stack: owner === null ? parseStackTrace(new Error(), 3) : null,
195 start: performance.now(),
196 end: -1.1, // Only set when pinged.
197 promise: null,