Repository: facebook/react
Pull request: [Flight] Create a fast path parseStackTrace which skips generating a string stack

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.


Changed files:
filename: packages/react-server/src/ReactFlightServer.js
patch: @@ -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();

filename: packages/react-server/src/ReactFlightServerConfigDebugNode.js
patch: @@ -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,

filename: packages/react-server/src/ReactFlightStackConfigV8.js
patch: @@ -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,