Skip to content

Commit e67b4fe

Browse files
[Flight] Emit Partial Debug Info if we have any at the point of aborting a render (#33632)
When we abort a render we don't really have much information about the task that was aborted. Because before a Promise resolves there's no indication about would have resolved it. In particular we don't know which I/O would've ultimately called resolve(). However, we can at least emit any information we do have at the point where we emit it. At the least the stack of the top most Promise. Currently we synchronously flush at the end of an `abort()` but we should ideally schedule the flush in a macrotask and emit this debug information right before that. That way we would give an opportunity for any `cacheSignal()` abort to trigger rejections all the way up and those rejections informs the awaited stack. --------- Co-authored-by: Hendrik Liebau <[email protected]>
1 parent 4a52348 commit e67b4fe

File tree

1 file changed

+79
-2
lines changed

1 file changed

+79
-2
lines changed

packages/react-server/src/ReactFlightServer.js

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

8081
import {
@@ -734,6 +735,12 @@ function serializeDebugThenable(
734735
}
735736
}
736737

738+
if (request.status === ABORTING) {
739+
// Ensure that we have time to emit the halt chunk if we're sync aborting.
740+
emitDebugHaltChunk(request, id);
741+
return ref;
742+
}
743+
737744
let cancelled = false;
738745

739746
thenable.then(
@@ -804,7 +811,7 @@ function serializeThenable(
804811
): number {
805812
const newTask = createTask(
806813
request,
807-
null,
814+
(thenable: any), // will be replaced by the value before we retry. used for debug info.
808815
task.keyPath, // the server component sequence continues through Promise-as-a-child.
809816
task.implicitSlot,
810817
request.abortableTasks,
@@ -3869,7 +3876,7 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
38693876

38703877
function serializeIONode(
38713878
request: Request,
3872-
ioNode: IONode | PromiseNode,
3879+
ioNode: IONode | PromiseNode | UnresolvedPromiseNode,
38733880
promiseRef: null | WeakRef<Promise<mixed>>,
38743881
): string {
38753882
const existingRef = request.writtenDebugObjects.get(ioNode);
@@ -4679,6 +4686,74 @@ function forwardDebugInfoFromCurrentContext(
46794686
}
46804687
}
46814688

4689+
function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
4690+
// If a task is aborted, we can still include as much debug info as we can from the
4691+
// value that we have so far.
4692+
const model: any = task.model;
4693+
if (typeof model !== 'object' || model === null) {
4694+
return;
4695+
}
4696+
let debugInfo: ?ReactDebugInfo;
4697+
if (__DEV__) {
4698+
// If this came from Flight, forward any debug info into this new row.
4699+
debugInfo = model._debugInfo;
4700+
if (debugInfo) {
4701+
forwardDebugInfo(request, task, debugInfo);
4702+
}
4703+
}
4704+
if (
4705+
enableProfilerTimer &&
4706+
enableComponentPerformanceTrack &&
4707+
enableAsyncDebugInfo
4708+
) {
4709+
let thenable: null | Thenable<any> = null;
4710+
if (typeof model.then === 'function') {
4711+
thenable = (model: any);
4712+
} else if (model.$$typeof === REACT_LAZY_TYPE) {
4713+
const payload = model._payload;
4714+
const init = model._init;
4715+
try {
4716+
init(payload);
4717+
} catch (x) {
4718+
if (
4719+
typeof x === 'object' &&
4720+
x !== null &&
4721+
typeof x.then === 'function'
4722+
) {
4723+
thenable = (x: any);
4724+
}
4725+
}
4726+
}
4727+
if (thenable !== null) {
4728+
const sequence = getAsyncSequenceFromPromise(thenable);
4729+
if (sequence !== null) {
4730+
let node = sequence;
4731+
while (node.tag === UNRESOLVED_AWAIT_NODE && node.awaited !== null) {
4732+
// See if any of the dependencies are resolved yet.
4733+
node = node.awaited;
4734+
}
4735+
if (node.tag === UNRESOLVED_PROMISE_NODE) {
4736+
// We don't know what Promise will eventually end up resolving this Promise and if it
4737+
// was I/O at all. However, we assume that it was some kind of I/O since it didn't
4738+
// complete in time before aborting.
4739+
// The best we can do is try to emit the stack of where this Promise was created.
4740+
serializeIONode(request, node, null);
4741+
request.pendingChunks++;
4742+
const env = (0, request.environmentName)();
4743+
const asyncInfo: ReactAsyncInfo = {
4744+
awaited: ((node: any): ReactIOInfo), // This is deduped by this reference.
4745+
env: env,
4746+
};
4747+
emitDebugChunk(request, task.id, asyncInfo);
4748+
markOperationEndTime(request, task, performance.now());
4749+
} else {
4750+
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
4751+
}
4752+
}
4753+
}
4754+
}
4755+
}
4756+
46824757
function emitTimingChunk(
46834758
request: Request,
46844759
id: number,
@@ -5028,6 +5103,7 @@ function abortTask(task: Task, request: Request, errorId: number): void {
50285103
return;
50295104
}
50305105
task.status = ABORTED;
5106+
forwardDebugInfoFromAbortedTask(request, task);
50315107
// Track when we aborted this task as its end time.
50325108
if (enableProfilerTimer && enableComponentPerformanceTrack) {
50335109
if (task.timed) {
@@ -5047,6 +5123,7 @@ function haltTask(task: Task, request: Request): void {
50475123
return;
50485124
}
50495125
task.status = ABORTED;
5126+
forwardDebugInfoFromAbortedTask(request, task);
50505127
// We don't actually emit anything for this task id because we are intentionally
50515128
// leaving the reference unfulfilled.
50525129
request.pendingChunks--;

0 commit comments

Comments
 (0)