@samitouri / QOS-React / commits / 3a43e72d66

[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.

Sebastian Markbåge committed Jul 9, 2025 at 09:06 UTC 3a43e72d665af98b62d9c3c54bd288597dd27e6e
3 files changed +46 -14
packages/react-server/src/ReactFlightServer.js
+2 -1
@@ -94,6 +94,7 @@ import {
94 getCurrentAsyncSequence,
95 getAsyncSequenceFromPromise,
96 parseStackTrace,
97 + parseStackTracePrivate,
98 supportsComponentStorage,
99 componentStorage,
100 unbadgeConsole,
@@ -316,7 +317,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
317 // one stack frame but keeping it simple for now and include all frames.
318 const stack = filterStackTrace(
319 request,
319 - parseStackTrace(new Error('react-stack-top-frame'), 1),
320 + parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
321 );
322 request.pendingDebugChunks++;
323 const owner: null | ReactComponentInfo = resolveOwner();
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+9 -6
@@ -29,7 +29,7 @@ import {resolveOwner} from './flight/ReactFlightCurrentOwner';
29 import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
30 import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
31 import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
32 -import {parseStackTrace} from './ReactFlightServerConfig';
32 +import {parseStackTracePrivate} from './ReactFlightServerConfig';
33
34 // $FlowFixMe[method-unbinding]
35 const getAsyncId = AsyncResource.prototype.asyncId;
@@ -129,8 +129,8 @@ export function initAsyncDebugInfo(): void {
129 if (request === null) {
130 // We don't collect stacks for awaits that weren't in the scope of a specific render.
131 } else {
132 - stack = parseStackTrace(new Error(), 5);
133 - if (!isAwaitInUserspace(request, stack)) {
132 + stack = parseStackTracePrivate(new Error(), 5);
133 + if (stack !== null && !isAwaitInUserspace(request, stack)) {
134 // If this await was not done directly in user space, then clear the stack. We won't use it
135 // anyway. This lets future awaits on this await know that we still need to get their stacks
136 // until we find one in user space.
@@ -153,7 +153,8 @@ export function initAsyncDebugInfo(): void {
153 node = ({
154 tag: UNRESOLVED_PROMISE_NODE,
155 owner: owner,
156 - stack: owner === null ? null : parseStackTrace(new Error(), 5),
156 + stack:
157 + owner === null ? null : parseStackTracePrivate(new Error(), 5),
158 start: performance.now(),
159 end: -1.1, // Set when we resolve.
160 promise: new WeakRef((resource: Promise<any>)),
@@ -175,7 +176,8 @@ export function initAsyncDebugInfo(): void {
176 node = ({
177 tag: IO_NODE,
178 owner: owner,
178 - stack: owner === null ? parseStackTrace(new Error(), 3) : null,
179 + stack:
180 + owner === null ? parseStackTracePrivate(new Error(), 3) : null,
181 start: performance.now(),
182 end: -1.1, // Only set when pinged.
183 promise: null,
@@ -191,7 +193,8 @@ export function initAsyncDebugInfo(): void {
193 node = ({
194 tag: IO_NODE,
195 owner: owner,
194 - stack: owner === null ? parseStackTrace(new Error(), 3) : null,
196 + stack:
197 + owner === null ? parseStackTracePrivate(new Error(), 3) : null,
198 start: performance.now(),
199 end: -1.1, // Only set when pinged.
200 promise: null,
packages/react-server/src/ReactFlightStackConfigV8.js
+35 -7
@@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
49 return result;
50 }
51
52 -function collectStackTrace(
52 +function collectStackTracePrivate(
53 error: Error,
54 structuredStackTrace: CallSite[],
55 ): string {
@@ -79,11 +79,11 @@ function collectStackTrace(
79 let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
80 if (filename === '<anonymous>') {
81 filename = '';
82 - }
83 - if (callSite.isEval() && !filename) {
84 - const origin = callSite.getEvalOrigin();
85 - if (origin) {
86 - filename = origin.toString() + ', <anonymous>';
82 + if (callSite.isEval()) {
83 + const origin = callSite.getEvalOrigin();
84 + if (origin) {
85 + filename = origin.toString() + ', <anonymous>';
86 + }
87 }
88 }
89 const line = callSite.getLineNumber() || 0;
@@ -101,6 +101,15 @@ function collectStackTrace(
101 result.push([name, filename, line, col, enclosingLine, enclosingCol]);
102 }
103 }
104 + collectedStackTrace = result;
105 + return '';
106 +}
107 +
108 +function collectStackTrace(
109 + error: Error,
110 + structuredStackTrace: CallSite[],
111 +): string {
112 + collectStackTracePrivate(error, structuredStackTrace);
113 // At the same time we generate a string stack trace just in case someone
114 // else reads it. Ideally, we'd call the previous prepareStackTrace to
115 // ensure it's in the expected format but it's common for that to be
@@ -115,7 +124,6 @@ function collectStackTrace(
124 for (let i = 0; i < structuredStackTrace.length; i++) {
125 stack += '\n at ' + structuredStackTrace[i].toString();
126 }
118 - collectedStackTrace = result;
127 return stack;
128 }
129
@@ -131,6 +139,26 @@ const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
139 ? new WeakMap()
140 : (null: any);
141
142 +// This version is only used when React fully owns the Error object and there's no risk of it having
143 +// been already initialized and no risky that anyone else will initialize it later.
144 +export function parseStackTracePrivate(
145 + error: Error,
146 + skipFrames: number,
147 +): null | ReactStackTrace {
148 + collectedStackTrace = null;
149 + framesToSkip = skipFrames;
150 + const previousPrepare = Error.prepareStackTrace;
151 + Error.prepareStackTrace = collectStackTracePrivate;
152 + try {
153 + if (error.stack !== '') {
154 + return null;
155 + }
156 + } finally {
157 + Error.prepareStackTrace = previousPrepare;
158 + }
159 + return collectedStackTrace;
160 +}
161 +
162 export function parseStackTrace(
163 error: Error,
164 skipFrames: number,