Skip to content

Commit 70d31fe

Browse files
committed
Fix stacks for unresolved I/O during abort
1 parent 4e61d6e commit 70d31fe

File tree

3 files changed

+89
-11
lines changed

3 files changed

+89
-11
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import type {
7575
AsyncSequence,
7676
IONode,
7777
PromiseNode,
78+
UnresolvedAwaitNode,
7879
UnresolvedPromiseNode,
7980
} from './ReactFlightAsyncSequence';
8081

@@ -95,6 +96,7 @@ import {
9596
markAsyncSequenceRootTask,
9697
getCurrentAsyncSequence,
9798
getAsyncSequenceFromPromise,
99+
getInternalAwaitNode,
98100
parseStackTrace,
99101
parseStackTracePrivate,
100102
supportsComponentStorage,
@@ -4511,7 +4513,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
45114513

45124514
function serializeIONode(
45134515
request: Request,
4514-
ioNode: IONode | PromiseNode | UnresolvedPromiseNode,
4516+
ioNode: IONode | PromiseNode | UnresolvedPromiseNode | UnresolvedAwaitNode,
45154517
promiseRef: null | WeakRef<Promise<mixed>>,
45164518
): string {
45174519
const existingRef = request.writtenDebugObjects.get(ioNode);
@@ -5452,9 +5454,16 @@ function forwardDebugInfoFromCurrentContext(
54525454
}
54535455
}
54545456
if (enableProfilerTimer && enableAsyncDebugInfo) {
5455-
const sequence = getCurrentAsyncSequence();
5456-
if (sequence !== null) {
5457-
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
5457+
if (request.status === ABORTING) {
5458+
// When aborting, skip forwarding debug info here.
5459+
// forwardDebugInfoFromAbortedTask will handle it more accurately.
5460+
} else {
5461+
// For normal resolve/reject, use the current execution context, as it
5462+
// shows what actually caused the Promise to settle.
5463+
const sequence = getCurrentAsyncSequence();
5464+
if (sequence !== null) {
5465+
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
5466+
}
54585467
}
54595468
}
54605469
}
@@ -5492,11 +5501,18 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
54925501
// See if any of the dependencies are resolved yet.
54935502
node = node.awaited;
54945503
}
5504+
// For unresolved promise nodes, check if we have an internal await node
5505+
// that shows what the async function is blocked on.
54955506
if (node.tag === UNRESOLVED_PROMISE_NODE) {
5496-
// We don't know what Promise will eventually end up resolving this Promise and if it
5497-
// was I/O at all. However, we assume that it was some kind of I/O since it didn't
5498-
// complete in time before aborting.
5499-
// The best we can do is try to emit the stack of where this Promise was created.
5507+
const internalAwait = getInternalAwaitNode(node);
5508+
if (internalAwait !== null) {
5509+
node = internalAwait;
5510+
}
5511+
}
5512+
if (
5513+
node.tag === UNRESOLVED_PROMISE_NODE ||
5514+
node.tag === UNRESOLVED_AWAIT_NODE
5515+
) {
55005516
serializeIONode(request, node, null);
55015517
request.pendingChunks++;
55025518
const env = (0, request.environmentName)();

packages/react-server/src/ReactFlightServerConfigDebugNode.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import type {ReactStackTrace} from 'shared/ReactTypes';
10+
import type {ReactStackTrace, ReactComponentInfo} from 'shared/ReactTypes';
1111

1212
import type {
1313
AsyncSequence,
@@ -40,6 +40,28 @@ const pendingOperations: Map<number, AsyncSequence> =
4040
// Keep the last resolved await as a workaround for async functions missing data.
4141
let lastRanAwait: null | AwaitNode = null;
4242

43+
// These two maps work together to track what async functions are blocked on when aborting:
44+
//
45+
// 1. unresolvedPromiseNodesByOwner: Maps owner -> Promise (to find the Promise to link)
46+
// When a Promise is created, we track it by its owner. This typically captures async
47+
// function return Promises. Sync components may also have Promises tracked here, but
48+
// they won't be linked since sync functions can't have awaits with matching owners.
49+
//
50+
// 2. internalAwaitNodesByPromise: Maps Promise -> await (stores the actual link)
51+
// When an await happens with the same owner as a tracked Promise, we link that Promise
52+
// to the await. This shows what the async function is currently blocked on.
53+
//
54+
// By storing the links separately from the regular awaited field, we can use this information
55+
// only during abort scenarios without affecting normal rendering.
56+
const unresolvedPromiseNodesByOwner: WeakMap<
57+
ReactComponentInfo,
58+
UnresolvedPromiseNode,
59+
> = new WeakMap();
60+
const internalAwaitNodesByPromise: WeakMap<
61+
UnresolvedPromiseNode | PromiseNode,
62+
UnresolvedAwaitNode | AwaitNode,
63+
> = new WeakMap();
64+
4365
function resolvePromiseOrAwaitNode(
4466
unresolvedNode: UnresolvedAwaitNode | UnresolvedPromiseNode,
4567
endTime: number,
@@ -114,16 +136,27 @@ export function initAsyncDebugInfo(): void {
114136
}
115137
}
116138
const current = pendingOperations.get(currentAsyncId);
139+
const owner = resolveOwner();
117140
node = ({
118141
tag: UNRESOLVED_AWAIT_NODE,
119-
owner: resolveOwner(),
142+
owner: owner,
120143
stack: stack,
121144
start: performance.now(),
122145
end: -1.1, // set when resolved.
123146
promise: promiseRef,
124147
awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
125148
previous: current === undefined ? null : current, // The path that led us here.
126149
}: UnresolvedAwaitNode);
150+
// Link the owner's Promise to this await so we can track what it's blocked on.
151+
// This only links when the await and Promise have the same owner (i.e., async functions
152+
// awaiting within themselves). Promises from sync components won't match any awaits.
153+
// We store this in a separate WeakMap to avoid affecting normal rendering.
154+
if (owner !== null) {
155+
const ownerPromiseNode = unresolvedPromiseNodesByOwner.get(owner);
156+
if (ownerPromiseNode !== undefined) {
157+
internalAwaitNodesByPromise.set(ownerPromiseNode, node);
158+
}
159+
}
127160
} else {
128161
const owner = resolveOwner();
129162
node = ({
@@ -140,6 +173,17 @@ export function initAsyncDebugInfo(): void {
140173
: trigger,
141174
previous: null,
142175
}: UnresolvedPromiseNode);
176+
// Track Promises by owner so awaits with matching owners can link to them.
177+
// Only track the first Promise per owner. This typically captures async function
178+
// return Promises, but may also track Promises from sync components - those won't
179+
// be linked since sync functions can't have awaits with matching owners.
180+
if (
181+
owner !== null &&
182+
trigger === undefined &&
183+
!unresolvedPromiseNodesByOwner.has(owner)
184+
) {
185+
unresolvedPromiseNodesByOwner.set(owner, node);
186+
}
143187
}
144188
} else if (
145189
type !== 'Microtask' &&
@@ -356,3 +400,10 @@ export function getAsyncSequenceFromPromise(
356400
}
357401
return node;
358402
}
403+
404+
export function getInternalAwaitNode(
405+
promiseNode: UnresolvedPromiseNode | PromiseNode,
406+
): null | UnresolvedAwaitNode | AwaitNode {
407+
const awaitNode = internalAwaitNodesByPromise.get(promiseNode);
408+
return awaitNode === undefined ? null : awaitNode;
409+
}

packages/react-server/src/ReactFlightServerConfigDebugNoop.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
* @flow
88
*/
99

10-
import type {AsyncSequence} from './ReactFlightAsyncSequence';
10+
import type {
11+
AsyncSequence,
12+
AwaitNode,
13+
PromiseNode,
14+
UnresolvedAwaitNode,
15+
UnresolvedPromiseNode,
16+
} from './ReactFlightAsyncSequence';
1117

1218
// Exported for runtimes that don't support Promise instrumentation for async debugging.
1319
export function initAsyncDebugInfo(): void {}
@@ -20,3 +26,8 @@ export function getAsyncSequenceFromPromise(
2026
): null | AsyncSequence {
2127
return null;
2228
}
29+
export function getInternalAwaitNode(
30+
promiseNode: UnresolvedPromiseNode | PromiseNode,
31+
): null | UnresolvedAwaitNode | AwaitNode {
32+
return null;
33+
}

0 commit comments

Comments
 (0)