[Flight] Create a fast path parseStackTrace which skips generating a string stack (#33735)

When we know that the object that we pass in is immediately parsed, then
we know it couldn't have been reified into a unstructured stack yet. In
this path we assume that we'll trigger `Error.prepareStackTrace`.

Since we know that nobody else will read the stack after us, we can skip
generating a string stack and just return empty. We can also skip
caching.
This commit is contained in:
Sebastian Markbåge 2025-07-09 09:06:55 -04:00 committed by GitHub
parent 8ba3501cd9
commit 3a43e72d66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 14 deletions

View File

@ -94,6 +94,7 @@ import {
getCurrentAsyncSequence,
getAsyncSequenceFromPromise,
parseStackTrace,
parseStackTracePrivate,
supportsComponentStorage,
componentStorage,
unbadgeConsole,
@ -316,7 +317,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
// one stack frame but keeping it simple for now and include all frames.
const stack = filterStackTrace(
request,
parseStackTrace(new Error('react-stack-top-frame'), 1),
parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
);
request.pendingDebugChunks++;
const owner: null | ReactComponentInfo = resolveOwner();

View File

@ -29,7 +29,7 @@ import {resolveOwner} from './flight/ReactFlightCurrentOwner';
import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
import {parseStackTrace} from './ReactFlightServerConfig';
import {parseStackTracePrivate} from './ReactFlightServerConfig';
// $FlowFixMe[method-unbinding]
const getAsyncId = AsyncResource.prototype.asyncId;
@ -129,8 +129,8 @@ export function initAsyncDebugInfo(): void {
if (request === null) {
// We don't collect stacks for awaits that weren't in the scope of a specific render.
} else {
stack = parseStackTrace(new Error(), 5);
if (!isAwaitInUserspace(request, stack)) {
stack = parseStackTracePrivate(new Error(), 5);
if (stack !== null && !isAwaitInUserspace(request, stack)) {
// If this await was not done directly in user space, then clear the stack. We won't use it
// anyway. This lets future awaits on this await know that we still need to get their stacks
// until we find one in user space.
@ -153,7 +153,8 @@ export function initAsyncDebugInfo(): void {
node = ({
tag: UNRESOLVED_PROMISE_NODE,
owner: owner,
stack: owner === null ? null : parseStackTrace(new Error(), 5),
stack:
owner === null ? null : parseStackTracePrivate(new Error(), 5),
start: performance.now(),
end: -1.1, // Set when we resolve.
promise: new WeakRef((resource: Promise<any>)),
@ -175,7 +176,8 @@ export function initAsyncDebugInfo(): void {
node = ({
tag: IO_NODE,
owner: owner,
stack: owner === null ? parseStackTrace(new Error(), 3) : null,
stack:
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
start: performance.now(),
end: -1.1, // Only set when pinged.
promise: null,
@ -191,7 +193,8 @@ export function initAsyncDebugInfo(): void {
node = ({
tag: IO_NODE,
owner: owner,
stack: owner === null ? parseStackTrace(new Error(), 3) : null,
stack:
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
start: performance.now(),
end: -1.1, // Only set when pinged.
promise: null,

View File

@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
return result;
}
function collectStackTrace(
function collectStackTracePrivate(
error: Error,
structuredStackTrace: CallSite[],
): string {
@ -79,11 +79,11 @@ function collectStackTrace(
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
if (filename === '<anonymous>') {
filename = '';
}
if (callSite.isEval() && !filename) {
const origin = callSite.getEvalOrigin();
if (origin) {
filename = origin.toString() + ', <anonymous>';
if (callSite.isEval()) {
const origin = callSite.getEvalOrigin();
if (origin) {
filename = origin.toString() + ', <anonymous>';
}
}
}
const line = callSite.getLineNumber() || 0;
@ -101,6 +101,15 @@ function collectStackTrace(
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
}
}
collectedStackTrace = result;
return '';
}
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
collectStackTracePrivate(error, structuredStackTrace);
// At the same time we generate a string stack trace just in case someone
// else reads it. Ideally, we'd call the previous prepareStackTrace to
// ensure it's in the expected format but it's common for that to be
@ -115,7 +124,6 @@ function collectStackTrace(
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
collectedStackTrace = result;
return stack;
}
@ -131,6 +139,26 @@ const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
? new WeakMap()
: (null: any);
// This version is only used when React fully owns the Error object and there's no risk of it having
// been already initialized and no risky that anyone else will initialize it later.
export function parseStackTracePrivate(
error: Error,
skipFrames: number,
): null | ReactStackTrace {
collectedStackTrace = null;
framesToSkip = skipFrames;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTracePrivate;
try {
if (error.stack !== '') {
return null;
}
} finally {
Error.prepareStackTrace = previousPrepare;
}
return collectedStackTrace;
}
export function parseStackTrace(
error: Error,
skipFrames: number,