Skip to content

Commit cee7939

Browse files
authored
[Fizz] Push a stalled await from debug info to the ownerStack/debugTask (#33634)
If an aborted task is not rendering, then this is an async abort. Conceptually it's as if the abort happened inside the async gap. The abort reason's stack frame won't have that on the stack so instead we use the owner stack and debug task of any halted async debug info. One thing that's a bit awkward is that if you do have a sync abort and you use that error as the "reason" then that thing still has a sync stack in a different component. In another approach I was exploring having different error objects for each component but I don't think that's worth it.
1 parent b42341d commit cee7939

File tree

3 files changed

+113
-17
lines changed

3 files changed

+113
-17
lines changed

packages/react-server/src/ReactFizzComponentStack.js

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

10-
import type {ReactComponentInfo} from 'shared/ReactTypes';
10+
import type {ReactComponentInfo, ReactAsyncInfo} from 'shared/ReactTypes';
1111
import type {LazyComponent} from 'react/src/ReactLazy';
1212

1313
import {
@@ -37,7 +37,8 @@ export type ComponentStackNode = {
3737
| string
3838
| Function
3939
| LazyComponent<any, any>
40-
| ReactComponentInfo,
40+
| ReactComponentInfo
41+
| ReactAsyncInfo,
4142
owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
4243
stack?: null | string | Error, // DEV only
4344
};

packages/react-server/src/ReactFizzServer.js

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
ReactFormState,
2222
ReactComponentInfo,
2323
ReactDebugInfo,
24+
ReactAsyncInfo,
2425
ViewTransitionProps,
2526
ActivityProps,
2627
SuspenseProps,
@@ -181,6 +182,7 @@ import {
181182
enableAsyncIterableChildren,
182183
enableViewTransition,
183184
enableFizzBlockingRender,
185+
enableAsyncDebugInfo,
184186
} from 'shared/ReactFeatureFlags';
185187

186188
import assign from 'shared/assign';
@@ -985,6 +987,45 @@ function getStackFromNode(stackNode: ComponentStackNode): string {
985987
return getStackByComponentStackNode(stackNode);
986988
}
987989

990+
function pushHaltedAwaitOnComponentStack(
991+
task: Task,
992+
debugInfo: void | null | ReactDebugInfo,
993+
): void {
994+
if (!__DEV__) {
995+
// eslint-disable-next-line react-internal/prod-error-codes
996+
throw new Error(
997+
'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',
998+
);
999+
}
1000+
if (debugInfo != null) {
1001+
for (let i = debugInfo.length - 1; i >= 0; i--) {
1002+
const info = debugInfo[i];
1003+
if (typeof info.name === 'string') {
1004+
// This is a Server Component. Any awaits in previous Server Components already resolved.
1005+
break;
1006+
}
1007+
if (typeof info.time === 'number') {
1008+
// This had an end time. Any awaits before this must have already resolved.
1009+
break;
1010+
}
1011+
if (info.awaited != null) {
1012+
const asyncInfo: ReactAsyncInfo = (info: any);
1013+
const bestStack =
1014+
asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;
1015+
if (bestStack.debugStack !== undefined) {
1016+
task.componentStack = {
1017+
parent: task.componentStack,
1018+
type: asyncInfo,
1019+
owner: bestStack.owner,
1020+
stack: bestStack.debugStack,
1021+
};
1022+
task.debugTask = (bestStack.debugTask: any);
1023+
}
1024+
}
1025+
}
1026+
}
1027+
}
1028+
9881029
function pushServerComponentStack(
9891030
task: Task,
9901031
debugInfo: void | null | ReactDebugInfo,
@@ -4612,6 +4653,20 @@ function abortTask(task: Task, request: Request, error: mixed): void {
46124653
}
46134654

46144655
const errorInfo = getThrownInfo(task.componentStack);
4656+
if (__DEV__ && enableAsyncDebugInfo) {
4657+
// If the task is not rendering, then this is an async abort. Conceptually it's as if
4658+
// the abort happened inside the async gap. The abort reason's stack frame won't have that
4659+
// on the stack so instead we use the owner stack and debug task of any halted async debug info.
4660+
const node: any = task.node;
4661+
if (node !== null && typeof node === 'object') {
4662+
// Push a fake component stack frame that represents the await.
4663+
pushHaltedAwaitOnComponentStack(task, node._debugInfo);
4664+
if (task.thenableState !== null) {
4665+
// TODO: If we were stalled inside use() of a Client Component then we should
4666+
// rerender to get the stack trace from the use() call.
4667+
}
4668+
}
4669+
}
46154670

46164671
if (boundary === null) {
46174672
if (request.status !== CLOSING && request.status !== CLOSED) {
@@ -4631,16 +4686,21 @@ function abortTask(task: Task, request: Request, error: mixed): void {
46314686
if (trackedPostpones !== null && segment !== null) {
46324687
// We are prerendering. We don't want to fatal when the shell postpones
46334688
// we just need to mark it as postponed.
4634-
logPostpone(request, postponeInstance.message, errorInfo, null);
4689+
logPostpone(
4690+
request,
4691+
postponeInstance.message,
4692+
errorInfo,
4693+
task.debugTask,
4694+
);
46354695
trackPostpone(request, trackedPostpones, task, segment);
46364696
finishedTask(request, null, task.row, segment);
46374697
} else {
46384698
const fatal = new Error(
46394699
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
46404700
postponeInstance.message,
46414701
);
4642-
logRecoverableError(request, fatal, errorInfo, null);
4643-
fatalError(request, fatal, errorInfo, null);
4702+
logRecoverableError(request, fatal, errorInfo, task.debugTask);
4703+
fatalError(request, fatal, errorInfo, task.debugTask);
46444704
}
46454705
} else if (
46464706
enableHalt &&
@@ -4650,12 +4710,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
46504710
const trackedPostpones = request.trackedPostpones;
46514711
// We are aborting a prerender and must treat the shell as halted
46524712
// We log the error but we still resolve the prerender
4653-
logRecoverableError(request, error, errorInfo, null);
4713+
logRecoverableError(request, error, errorInfo, task.debugTask);
46544714
trackPostpone(request, trackedPostpones, task, segment);
46554715
finishedTask(request, null, task.row, segment);
46564716
} else {
4657-
logRecoverableError(request, error, errorInfo, null);
4658-
fatalError(request, error, errorInfo, null);
4717+
logRecoverableError(request, error, errorInfo, task.debugTask);
4718+
fatalError(request, error, errorInfo, task.debugTask);
46594719
}
46604720
return;
46614721
} else {
@@ -4672,7 +4732,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
46724732
error.$$typeof === REACT_POSTPONE_TYPE
46734733
) {
46744734
const postponeInstance: Postpone = (error: any);
4675-
logPostpone(request, postponeInstance.message, errorInfo, null);
4735+
logPostpone(
4736+
request,
4737+
postponeInstance.message,
4738+
errorInfo,
4739+
task.debugTask,
4740+
);
46764741
// TODO: Figure out a better signal than a magic digest value.
46774742
errorDigest = 'POSTPONE';
46784743
} else {
@@ -4710,11 +4775,16 @@ function abortTask(task: Task, request: Request, error: mixed): void {
47104775
error.$$typeof === REACT_POSTPONE_TYPE
47114776
) {
47124777
const postponeInstance: Postpone = (error: any);
4713-
logPostpone(request, postponeInstance.message, errorInfo, null);
4778+
logPostpone(
4779+
request,
4780+
postponeInstance.message,
4781+
errorInfo,
4782+
task.debugTask,
4783+
);
47144784
} else {
47154785
// We are aborting a prerender and must halt this boundary.
47164786
// We treat this like other postpones during prerendering
4717-
logRecoverableError(request, error, errorInfo, null);
4787+
logRecoverableError(request, error, errorInfo, task.debugTask);
47184788
}
47194789
trackPostpone(request, trackedPostpones, task, segment);
47204790
// If this boundary was still pending then we haven't already cancelled its fallbacks.
@@ -4737,7 +4807,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
47374807
error.$$typeof === REACT_POSTPONE_TYPE
47384808
) {
47394809
const postponeInstance: Postpone = (error: any);
4740-
logPostpone(request, postponeInstance.message, errorInfo, null);
4810+
logPostpone(
4811+
request,
4812+
postponeInstance.message,
4813+
errorInfo,
4814+
task.debugTask,
4815+
);
47414816
if (request.trackedPostpones !== null && segment !== null) {
47424817
trackPostpone(request, request.trackedPostpones, task, segment);
47434818
finishedTask(request, task.blockedBoundary, task.row, segment);
@@ -4753,7 +4828,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
47534828
// TODO: Figure out a better signal than a magic digest value.
47544829
errorDigest = 'POSTPONE';
47554830
} else {
4756-
errorDigest = logRecoverableError(request, error, errorInfo, null);
4831+
errorDigest = logRecoverableError(
4832+
request,
4833+
error,
4834+
errorInfo,
4835+
task.debugTask,
4836+
);
47574837
}
47584838
boundary.status = CLIENT_RENDERED;
47594839
encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);

packages/react-server/src/ReactFlightServer.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,7 +2149,11 @@ function visitAsyncNode(
21492149
owner: node.owner,
21502150
stack: filterStackTrace(request, node.stack),
21512151
});
2152-
markOperationEndTime(request, task, endTime);
2152+
// Mark the end time of the await. If we're aborting then we don't emit this
2153+
// to signal that this never resolved inside this render.
2154+
if (request.status !== ABORTING) {
2155+
markOperationEndTime(request, task, endTime);
2156+
}
21532157
}
21542158
}
21552159
}
@@ -2210,7 +2214,12 @@ function emitAsyncSequence(
22102214
}
22112215
}
22122216
emitDebugChunk(request, task.id, debugInfo);
2213-
markOperationEndTime(request, task, awaitedNode.end);
2217+
// Mark the end time of the await. If we're aborting then we don't emit this
2218+
// to signal that this never resolved inside this render.
2219+
if (request.status !== ABORTING) {
2220+
// If we're currently aborting, then this never resolved into user space.
2221+
markOperationEndTime(request, task, awaitedNode.end);
2222+
}
22142223
}
22152224
}
22162225

@@ -3910,14 +3919,21 @@ function serializeIONode(
39103919
// The environment name may have changed from when the I/O was actually started.
39113920
const env = (0, request.environmentName)();
39123921

3922+
const endTime =
3923+
ioNode.tag === UNRESOLVED_PROMISE_NODE
3924+
? // Mark the end time as now. It's arbitrary since it's not resolved but this
3925+
// marks when we stopped trying.
3926+
performance.now()
3927+
: ioNode.end;
3928+
39133929
request.pendingChunks++;
39143930
const id = request.nextChunkId++;
39153931
emitIOInfoChunk(
39163932
request,
39173933
id,
39183934
name,
39193935
ioNode.start,
3920-
ioNode.end,
3936+
endTime,
39213937
value,
39223938
env,
39233939
owner,
@@ -4741,7 +4757,6 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
47414757
env: env,
47424758
};
47434759
emitDebugChunk(request, task.id, asyncInfo);
4744-
markOperationEndTime(request, task, performance.now());
47454760
} else {
47464761
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
47474762
}

0 commit comments

Comments
 (0)