diff --git a/packages/react-reconciler/src/ReactFiberCallUserSpace.js b/packages/react-reconciler/src/ReactFiberCallUserSpace.js index e85d0431b99b..2012d395ef03 100644 --- a/packages/react-reconciler/src/ReactFiberCallUserSpace.js +++ b/packages/react-reconciler/src/ReactFiberCallUserSpace.js @@ -14,41 +14,64 @@ import {isRendering, setIsRendering} from './ReactCurrentFiber'; // These indirections exists so we can exclude its stack frame in DEV (and anything below it). // TODO: Consider marking the whole bundle instead of these boundaries. -/** @noinline */ -export function callComponentInDEV( +const callComponent = { + 'react-stack-bottom-frame': function ( + Component: (p: Props, arg: Arg) => R, + props: Props, + secondArg: Arg, + ): R { + const wasRendering = isRendering; + setIsRendering(true); + try { + const result = Component(props, secondArg); + return result; + } finally { + setIsRendering(wasRendering); + } + }, +}; + +export const callComponentInDEV: ( Component: (p: Props, arg: Arg) => R, props: Props, secondArg: Arg, -): R { - const wasRendering = isRendering; - setIsRendering(true); - try { - const result = Component(props, secondArg); - return result; - } finally { - setIsRendering(wasRendering); - } -} +) => R = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callComponent['react-stack-bottom-frame'].bind(callComponent): any) + : (null: any); interface ClassInstance { render(): R; } -/** @noinline */ -export function callRenderInDEV(instance: ClassInstance): R { - const wasRendering = isRendering; - setIsRendering(true); - try { - const result = instance.render(); - return result; - } finally { - setIsRendering(wasRendering); - } -} +const callRender = { + 'react-stack-bottom-frame': function (instance: ClassInstance): R { + const wasRendering = isRendering; + setIsRendering(true); + try { + const result = instance.render(); + return result; + } finally { + setIsRendering(wasRendering); + } + }, +}; -/** @noinline */ -export function callLazyInitInDEV(lazy: LazyComponent): any { - const payload = lazy._payload; - const init = lazy._init; - return init(payload); -} +export const callRenderInDEV: (instance: ClassInstance) => R => R = + __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callRender['react-stack-bottom-frame'].bind(callRender): any) + : (null: any); + +const callLazyInit = { + 'react-stack-bottom-frame': function (lazy: LazyComponent): any { + const payload = lazy._payload; + const init = lazy._init; + return init(payload); + }, +}; + +export const callLazyInitInDEV: (lazy: LazyComponent) => any = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any) + : (null: any); diff --git a/packages/react-reconciler/src/ReactFiberOwnerStack.js b/packages/react-reconciler/src/ReactFiberOwnerStack.js index fe9e4f1cfd61..0ccbdd905171 100644 --- a/packages/react-reconciler/src/ReactFiberOwnerStack.js +++ b/packages/react-reconciler/src/ReactFiberOwnerStack.js @@ -7,71 +7,13 @@ * @flow */ -import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; - -import { - callLazyInitInDEV, - callComponentInDEV, - callRenderInDEV, -} from './ReactFiberCallUserSpace'; - // TODO: Make this configurable on the root. const externalRegExp = /\/node\_modules\/|\(\\)/; -let callComponentFrame: null | string = null; -let callIteratorFrame: null | string = null; -let callLazyInitFrame: null | string = null; - function isNotExternal(stackFrame: string): boolean { return !externalRegExp.test(stackFrame); } -function initCallComponentFrame(): string { - // Extract the stack frame of the callComponentInDEV function. - const error = callComponentInDEV(Error, 'react-stack-top-frame', {}); - const stack = error.stack; - const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); -} - -function initCallRenderFrame(): string { - // Extract the stack frame of the callRenderInDEV function. - try { - (callRenderInDEV: any)({render: null}); - return ''; - } catch (error) { - const stack = error.stack; - const startIdx = stack.startsWith('TypeError: ') - ? stack.indexOf('\n') + 1 - : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); - } -} - -function initCallLazyInitFrame(): string { - // Extract the stack frame of the callLazyInitInDEV function. - const error = callLazyInitInDEV({ - $$typeof: REACT_LAZY_TYPE, - _init: Error, - _payload: 'react-stack-top-frame', - }); - const stack = error.stack; - const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); -} - function filterDebugStack(error: Error): string { // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly // to save bandwidth even in DEV. We'll also replay these stacks on the client so by @@ -83,32 +25,20 @@ function filterDebugStack(error: Error): string { // don't want/need. stack = stack.slice(29); } - const frames = stack.split('\n').slice(1); - if (callComponentFrame === null) { - callComponentFrame = initCallComponentFrame(); - } - let lastFrameIdx = frames.indexOf(callComponentFrame); - if (lastFrameIdx === -1) { - if (callLazyInitFrame === null) { - callLazyInitFrame = initCallLazyInitFrame(); - } - lastFrameIdx = frames.indexOf(callLazyInitFrame); - if (lastFrameIdx === -1) { - if (callIteratorFrame === null) { - callIteratorFrame = initCallRenderFrame(); - } - lastFrameIdx = frames.indexOf(callIteratorFrame); - } + let idx = stack.indexOf('react-stack-bottom-frame'); + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); } - if (lastFrameIdx !== -1) { - // Cut off everything after our "callComponent" slot since it'll be Fiber internals. - frames.length = lastFrameIdx; + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); } else { // We didn't find any internal callsite out to user space. // This means that this was called outside an owner or the owner is fully internal. // To keep things light we exclude the entire trace in this case. return ''; } + const frames = stack.split('\n').slice(1); return frames.filter(isNotExternal).join('\n'); } diff --git a/packages/react-server/src/ReactFizzCallUserSpace.js b/packages/react-server/src/ReactFizzCallUserSpace.js index 4a376607dc6d..3995d6c2b2cd 100644 --- a/packages/react-server/src/ReactFizzCallUserSpace.js +++ b/packages/react-server/src/ReactFizzCallUserSpace.js @@ -12,27 +12,50 @@ import type {LazyComponent} from 'react/src/ReactLazy'; // These indirections exists so we can exclude its stack frame in DEV (and anything below it). // TODO: Consider marking the whole bundle instead of these boundaries. -/** @noinline */ -export function callComponentInDEV( +const callComponent = { + 'react-stack-bottom-frame': function ( + Component: (p: Props, arg: Arg) => R, + props: Props, + secondArg: Arg, + ): R { + return Component(props, secondArg); + }, +}; + +export const callComponentInDEV: ( Component: (p: Props, arg: Arg) => R, props: Props, secondArg: Arg, -): R { - return Component(props, secondArg); -} +) => R = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callComponent['react-stack-bottom-frame'].bind(callComponent): any) + : (null: any); interface ClassInstance { render(): R; } -/** @noinline */ -export function callRenderInDEV(instance: ClassInstance): R { - return instance.render(); -} +const callRender = { + 'react-stack-bottom-frame': function (instance: ClassInstance): R { + return instance.render(); + }, +}; -/** @noinline */ -export function callLazyInitInDEV(lazy: LazyComponent): any { - const payload = lazy._payload; - const init = lazy._init; - return init(payload); -} +export const callRenderInDEV: (instance: ClassInstance) => R => R = + __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callRender['react-stack-bottom-frame'].bind(callRender): any) + : (null: any); + +const callLazyInit = { + 'react-stack-bottom-frame': function (lazy: LazyComponent): any { + const payload = lazy._payload; + const init = lazy._init; + return init(payload); + }, +}; + +export const callLazyInitInDEV: (lazy: LazyComponent) => any = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any) + : (null: any); diff --git a/packages/react-server/src/ReactFizzOwnerStack.js b/packages/react-server/src/ReactFizzOwnerStack.js index 8d6b5d94fd0d..0ccbdd905171 100644 --- a/packages/react-server/src/ReactFizzOwnerStack.js +++ b/packages/react-server/src/ReactFizzOwnerStack.js @@ -7,71 +7,13 @@ * @flow */ -import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; - -import { - callLazyInitInDEV, - callComponentInDEV, - callRenderInDEV, -} from './ReactFizzCallUserSpace'; - // TODO: Make this configurable on the root. const externalRegExp = /\/node\_modules\/|\(\\)/; -let callComponentFrame: null | string = null; -let callIteratorFrame: null | string = null; -let callLazyInitFrame: null | string = null; - function isNotExternal(stackFrame: string): boolean { return !externalRegExp.test(stackFrame); } -function initCallComponentFrame(): string { - // Extract the stack frame of the callComponentInDEV function. - const error = callComponentInDEV(Error, 'react-stack-top-frame', {}); - const stack = error.stack; - const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); -} - -function initCallRenderFrame(): string { - // Extract the stack frame of the callRenderInDEV function. - try { - (callRenderInDEV: any)({render: null}); - return ''; - } catch (error) { - const stack = error.stack; - const startIdx = stack.startsWith('TypeError: ') - ? stack.indexOf('\n') + 1 - : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); - } -} - -function initCallLazyInitFrame(): string { - // Extract the stack frame of the callLazyInitInDEV function. - const error = callLazyInitInDEV({ - $$typeof: REACT_LAZY_TYPE, - _init: Error, - _payload: 'react-stack-top-frame', - }); - const stack = error.stack; - const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); -} - function filterDebugStack(error: Error): string { // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly // to save bandwidth even in DEV. We'll also replay these stacks on the client so by @@ -83,32 +25,20 @@ function filterDebugStack(error: Error): string { // don't want/need. stack = stack.slice(29); } - const frames = stack.split('\n').slice(1); - if (callComponentFrame === null) { - callComponentFrame = initCallComponentFrame(); - } - let lastFrameIdx = frames.indexOf(callComponentFrame); - if (lastFrameIdx === -1) { - if (callLazyInitFrame === null) { - callLazyInitFrame = initCallLazyInitFrame(); - } - lastFrameIdx = frames.indexOf(callLazyInitFrame); - if (lastFrameIdx === -1) { - if (callIteratorFrame === null) { - callIteratorFrame = initCallRenderFrame(); - } - lastFrameIdx = frames.indexOf(callIteratorFrame); - } + let idx = stack.indexOf('react-stack-bottom-frame'); + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); } - if (lastFrameIdx !== -1) { - // Cut off everything after our "callComponent" slot since it'll be Fiber internals. - frames.length = lastFrameIdx; + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); } else { // We didn't find any internal callsite out to user space. // This means that this was called outside an owner or the owner is fully internal. // To keep things light we exclude the entire trace in this case. return ''; } + const frames = stack.split('\n').slice(1); return frames.filter(isNotExternal).join('\n'); } diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index e068db78dcf8..6efa59be6b35 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1528,7 +1528,7 @@ function finishClassComponent( ): ReactNodeList { let nextChildren; if (__DEV__) { - nextChildren = callRenderInDEV(instance); + nextChildren = (callRenderInDEV(instance): any); } else { nextChildren = instance.render(); } diff --git a/packages/react-server/src/ReactFlightCallUserSpace.js b/packages/react-server/src/ReactFlightCallUserSpace.js new file mode 100644 index 000000000000..01c0492cc337 --- /dev/null +++ b/packages/react-server/src/ReactFlightCallUserSpace.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {LazyComponent} from 'react/src/ReactLazy'; + +import type {ReactComponentInfo} from 'shared/ReactTypes'; + +import type {ReactClientValue} from './ReactFlightServer'; + +import {setCurrentOwner} from './flight/ReactFlightCurrentOwner'; + +import { + supportsComponentStorage, + componentStorage, +} from './ReactFlightServerConfig'; + +import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; + +// These indirections exists so we can exclude its stack frame in DEV (and anything below it). +// TODO: Consider marking the whole bundle instead of these boundaries. + +const callComponent = { + 'react-stack-bottom-frame': function ( + Component: (p: Props, arg: void) => R, + props: Props, + componentDebugInfo: ReactComponentInfo, + debugTask: null | ConsoleTask, + ): R { + // The secondArg is always undefined in Server Components since refs error early. + const secondArg = undefined; + setCurrentOwner(componentDebugInfo); + try { + if (supportsComponentStorage) { + // Run the component in an Async Context that tracks the current owner. + if (enableOwnerStacks && debugTask) { + return debugTask.run( + // $FlowFixMe[method-unbinding] + componentStorage.run.bind( + componentStorage, + componentDebugInfo, + Component, + props, + secondArg, + ), + ); + } + return componentStorage.run( + componentDebugInfo, + Component, + props, + secondArg, + ); + } else { + if (enableOwnerStacks && debugTask) { + return debugTask.run(Component.bind(null, props, secondArg)); + } + return Component(props, secondArg); + } + } finally { + setCurrentOwner(null); + } + }, +}; + +export const callComponentInDEV: ( + Component: (p: Props, arg: void) => R, + props: Props, + componentDebugInfo: ReactComponentInfo, + debugTask: null | ConsoleTask, +) => R = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callComponent['react-stack-bottom-frame'].bind(callComponent): any) + : (null: any); + +const callLazyInit = { + 'react-stack-bottom-frame': function (lazy: LazyComponent): any { + const payload = lazy._payload; + const init = lazy._init; + return init(payload); + }, +}; + +export const callLazyInitInDEV: (lazy: LazyComponent) => any = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any) + : (null: any); + +const callIterator = { + 'react-stack-bottom-frame': function ( + iterator: $AsyncIterator, + progress: ( + entry: + | {done: false, +value: ReactClientValue, ...} + | {done: true, +value: ReactClientValue, ...}, + ) => void, + error: (reason: mixed) => void, + ): void { + iterator.next().then(progress, error); + }, +}; + +export const callIteratorInDEV: ( + iterator: $AsyncIterator, + progress: ( + entry: + | {done: false, +value: ReactClientValue, ...} + | {done: true, +value: ReactClientValue, ...}, + ) => void, + error: (reason: mixed) => void, +) => void = __DEV__ + ? // We use this technique to trick minifiers to preserve the function name. + (callIterator['react-stack-bottom-frame'].bind(callIterator): any) + : (null: any); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 1eaf5369c79c..7d0bc63c1ad6 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -75,8 +75,6 @@ import { isServerReference, supportsRequestStorage, requestStorage, - supportsComponentStorage, - componentStorage, createHints, initAsyncDebugInfo, } from './ReactFlightServerConfig'; @@ -99,6 +97,12 @@ import {resolveOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner'; import {getOwnerStackByComponentInfoInDev} from './flight/ReactFlightComponentStack'; +import { + callComponentInDEV, + callLazyInitInDEV, + callIteratorInDEV, +} from './ReactFlightCallUserSpace'; + import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -129,10 +133,6 @@ import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable'; // TODO: Make this configurable on the Request. const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\\)/; -let callComponentFrame: null | string = null; -let callIteratorFrame: null | string = null; -let callLazyInitFrame: null | string = null; - function isNotExternal(stackFrame: string): boolean { return !externalRegExp.test(stackFrame); } @@ -168,52 +168,6 @@ function getStack(error: Error): string { } } -function initCallComponentFrame(): string { - // Extract the stack frame of the callComponentInDEV function. - const error = callComponentInDEV(Error, 'react-stack-top-frame', {}, null); - const stack = getStack(error); - const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); -} - -function initCallIteratorFrame(): string { - // Extract the stack frame of the callIteratorInDEV function. - try { - (callIteratorInDEV: any)({next: null}); - return ''; - } catch (error) { - const stack = getStack(error); - const startIdx = stack.startsWith('TypeError: ') - ? stack.indexOf('\n') + 1 - : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); - } -} - -function initCallLazyInitFrame(): string { - // Extract the stack frame of the callLazyInitInDEV function. - const error = callLazyInitInDEV({ - $$typeof: REACT_LAZY_TYPE, - _init: Error, - _payload: 'react-stack-top-frame', - }); - const stack = getStack(error); - const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; - const endIdx = stack.indexOf('\n', startIdx); - if (endIdx === -1) { - return stack.slice(startIdx); - } - return stack.slice(startIdx, endIdx); -} - function filterDebugStack(error: Error): string { // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly // to save bandwidth even in DEV. We'll also replay these stacks on the client so by @@ -225,27 +179,15 @@ function filterDebugStack(error: Error): string { // don't want/need. stack = stack.slice(29); } - const frames = stack.split('\n').slice(1); - if (callComponentFrame === null) { - callComponentFrame = initCallComponentFrame(); - } - let lastFrameIdx = frames.indexOf(callComponentFrame); - if (lastFrameIdx === -1) { - if (callLazyInitFrame === null) { - callLazyInitFrame = initCallLazyInitFrame(); - } - lastFrameIdx = frames.indexOf(callLazyInitFrame); - if (lastFrameIdx === -1) { - if (callIteratorFrame === null) { - callIteratorFrame = initCallIteratorFrame(); - } - lastFrameIdx = frames.indexOf(callIteratorFrame); - } + let idx = stack.indexOf('react-stack-bottom-frame'); + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); } - if (lastFrameIdx !== -1) { - // Cut off everything after our "callComponent" slot since it'll be Flight internals. - frames.length = lastFrameIdx; + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); } + const frames = stack.split('\n').slice(1); return frames.filter(isNotExternal).join('\n'); } @@ -816,20 +758,6 @@ function serializeReadableStream( return serializeByValueID(streamTask.id); } -// This indirect exists so we can exclude its stack frame in DEV (and anything below it). -/** @noinline */ -function callIteratorInDEV( - iterator: $AsyncIterator, - progress: ( - entry: - | {done: false, +value: ReactClientValue, ...} - | {done: true, +value: ReactClientValue, ...}, - ) => void, - error: (reason: mixed) => void, -) { - iterator.next().then(progress, error); -} - function serializeAsyncIterable( request: Request, task: Task, @@ -1029,57 +957,6 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { return lazyType; } -// This indirect exists so we can exclude its stack frame in DEV (and anything below it). -/** @noinline */ -function callComponentInDEV( - Component: (p: Props, arg: void) => R, - props: Props, - componentDebugInfo: ReactComponentInfo, - debugTask: null | ConsoleTask, -): R { - // The secondArg is always undefined in Server Components since refs error early. - const secondArg = undefined; - setCurrentOwner(componentDebugInfo); - try { - if (supportsComponentStorage) { - // Run the component in an Async Context that tracks the current owner. - if (enableOwnerStacks && debugTask) { - return debugTask.run( - // $FlowFixMe[method-unbinding] - componentStorage.run.bind( - componentStorage, - componentDebugInfo, - Component, - props, - secondArg, - ), - ); - } - return componentStorage.run( - componentDebugInfo, - Component, - props, - secondArg, - ); - } else { - if (enableOwnerStacks && debugTask) { - return debugTask.run(Component.bind(null, props, secondArg)); - } - return Component(props, secondArg); - } - } finally { - setCurrentOwner(null); - } -} - -// This indirect exists so we can exclude its stack frame in DEV (and anything below it). -/** @noinline */ -function callLazyInitInDEV(lazy: LazyComponent): any { - const payload = lazy._payload; - const init = lazy._init; - return init(payload); -} - function callWithDebugContextInDEV( task: Task, callback: A => T,