Skip to content

[Flight] Emit Partial Debug Info if we have any at the point of aborting a render #33632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import type {
AsyncSequence,
IONode,
PromiseNode,
UnresolvedPromiseNode,
} from './ReactFlightAsyncSequence';

import {
Expand Down Expand Up @@ -734,6 +735,12 @@ function serializeDebugThenable(
}
}

if (request.status === ABORTING) {
// Ensure that we have time to emit the halt chunk if we're sync aborting.
emitDebugHaltChunk(request, id);
return ref;
}

let cancelled = false;

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

function serializeIONode(
request: Request,
ioNode: IONode | PromiseNode,
ioNode: IONode | PromiseNode | UnresolvedPromiseNode,
promiseRef: null | WeakRef<Promise<mixed>>,
): string {
const existingRef = request.writtenDebugObjects.get(ioNode);
Expand Down Expand Up @@ -4679,6 +4686,74 @@ function forwardDebugInfoFromCurrentContext(
}
}

function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
// If a task is aborted, we can still include as much debug info as we can from the
// value that we have so far.
const model: any = task.model;
if (typeof model !== 'object' || model === null) {
return;
}
let debugInfo: ?ReactDebugInfo;
if (__DEV__) {
// If this came from Flight, forward any debug info into this new row.
debugInfo = model._debugInfo;
if (debugInfo) {
forwardDebugInfo(request, task, debugInfo);
}
}
if (
enableProfilerTimer &&
enableComponentPerformanceTrack &&
enableAsyncDebugInfo
) {
let thenable: null | Thenable<any> = null;
if (typeof model.then === 'function') {
thenable = (model: any);
} else if (model.$$typeof === REACT_LAZY_TYPE) {
const payload = model._payload;
const init = model._init;
try {
init(payload);
} catch (x) {
if (
typeof x === 'object' &&
x !== null &&
typeof x.then === 'function'
) {
thenable = (x: any);
}
}
}
if (thenable !== null) {
const sequence = getAsyncSequenceFromPromise(thenable);
if (sequence !== null) {
let node = sequence;
while (node.tag === UNRESOLVED_AWAIT_NODE && node.awaited !== null) {
// See if any of the dependencies are resolved yet.
node = node.awaited;
}
if (node.tag === UNRESOLVED_PROMISE_NODE) {
// We don't know what Promise will eventually end up resolving this Promise and if it
// was I/O at all. However, we assume that it was some kind of I/O since it didn't
// complete in time before aborting.
// The best we can do is try to emit the stack of where this Promise was created.
serializeIONode(request, node, null);
request.pendingChunks++;
const env = (0, request.environmentName)();
const asyncInfo: ReactAsyncInfo = {
awaited: ((node: any): ReactIOInfo), // This is deduped by this reference.
env: env,
};
emitDebugChunk(request, task.id, asyncInfo);
markOperationEndTime(request, task, performance.now());
} else {
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
}
}
}
}
}

function emitTimingChunk(
request: Request,
id: number,
Expand Down Expand Up @@ -5028,6 +5103,7 @@ function abortTask(task: Task, request: Request, errorId: number): void {
return;
}
task.status = ABORTED;
forwardDebugInfoFromAbortedTask(request, task);
// Track when we aborted this task as its end time.
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (task.timed) {
Expand All @@ -5047,6 +5123,7 @@ function haltTask(task: Task, request: Request): void {
return;
}
task.status = ABORTED;
forwardDebugInfoFromAbortedTask(request, task);
// We don't actually emit anything for this task id because we are intentionally
// leaving the reference unfulfilled.
request.pendingChunks--;
Expand Down
Loading