Skip to content

Commit 3a43e72

Browse files
authored
[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.
1 parent 8ba3501 commit 3a43e72

File tree

3 files changed

+46
-14
lines changed

3 files changed

+46
-14
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import {
9494
getCurrentAsyncSequence,
9595
getAsyncSequenceFromPromise,
9696
parseStackTrace,
97+
parseStackTracePrivate,
9798
supportsComponentStorage,
9899
componentStorage,
99100
unbadgeConsole,
@@ -316,7 +317,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
316317
// one stack frame but keeping it simple for now and include all frames.
317318
const stack = filterStackTrace(
318319
request,
319-
parseStackTrace(new Error('react-stack-top-frame'), 1),
320+
parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
320321
);
321322
request.pendingDebugChunks++;
322323
const owner: null | ReactComponentInfo = resolveOwner();

packages/react-server/src/ReactFlightServerConfigDebugNode.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {resolveOwner} from './flight/ReactFlightCurrentOwner';
2929
import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
3030
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
3131
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
32-
import {parseStackTrace} from './ReactFlightServerConfig';
32+
import {parseStackTracePrivate} from './ReactFlightServerConfig';
3333

3434
// $FlowFixMe[method-unbinding]
3535
const getAsyncId = AsyncResource.prototype.asyncId;
@@ -129,8 +129,8 @@ export function initAsyncDebugInfo(): void {
129129
if (request === null) {
130130
// We don't collect stacks for awaits that weren't in the scope of a specific render.
131131
} 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)) {
134134
// If this await was not done directly in user space, then clear the stack. We won't use it
135135
// anyway. This lets future awaits on this await know that we still need to get their stacks
136136
// until we find one in user space.
@@ -153,7 +153,8 @@ export function initAsyncDebugInfo(): void {
153153
node = ({
154154
tag: UNRESOLVED_PROMISE_NODE,
155155
owner: owner,
156-
stack: owner === null ? null : parseStackTrace(new Error(), 5),
156+
stack:
157+
owner === null ? null : parseStackTracePrivate(new Error(), 5),
157158
start: performance.now(),
158159
end: -1.1, // Set when we resolve.
159160
promise: new WeakRef((resource: Promise<any>)),
@@ -175,7 +176,8 @@ export function initAsyncDebugInfo(): void {
175176
node = ({
176177
tag: IO_NODE,
177178
owner: owner,
178-
stack: owner === null ? parseStackTrace(new Error(), 3) : null,
179+
stack:
180+
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
179181
start: performance.now(),
180182
end: -1.1, // Only set when pinged.
181183
promise: null,
@@ -191,7 +193,8 @@ export function initAsyncDebugInfo(): void {
191193
node = ({
192194
tag: IO_NODE,
193195
owner: owner,
194-
stack: owner === null ? parseStackTrace(new Error(), 3) : null,
196+
stack:
197+
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
195198
start: performance.now(),
196199
end: -1.1, // Only set when pinged.
197200
promise: null,

packages/react-server/src/ReactFlightStackConfigV8.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
4949
return result;
5050
}
5151

52-
function collectStackTrace(
52+
function collectStackTracePrivate(
5353
error: Error,
5454
structuredStackTrace: CallSite[],
5555
): string {
@@ -79,11 +79,11 @@ function collectStackTrace(
7979
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
8080
if (filename === '<anonymous>') {
8181
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+
}
8787
}
8888
}
8989
const line = callSite.getLineNumber() || 0;
@@ -101,6 +101,15 @@ function collectStackTrace(
101101
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
102102
}
103103
}
104+
collectedStackTrace = result;
105+
return '';
106+
}
107+
108+
function collectStackTrace(
109+
error: Error,
110+
structuredStackTrace: CallSite[],
111+
): string {
112+
collectStackTracePrivate(error, structuredStackTrace);
104113
// At the same time we generate a string stack trace just in case someone
105114
// else reads it. Ideally, we'd call the previous prepareStackTrace to
106115
// ensure it's in the expected format but it's common for that to be
@@ -115,7 +124,6 @@ function collectStackTrace(
115124
for (let i = 0; i < structuredStackTrace.length; i++) {
116125
stack += '\n at ' + structuredStackTrace[i].toString();
117126
}
118-
collectedStackTrace = result;
119127
return stack;
120128
}
121129

@@ -131,6 +139,26 @@ const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
131139
? new WeakMap()
132140
: (null: any);
133141

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+
134162
export function parseStackTrace(
135163
error: Error,
136164
skipFrames: number,

0 commit comments

Comments
 (0)