Skip to content

Commit

Permalink
Tag all user space call sites with the "react-stack-bottom-frame" name (
Browse files Browse the repository at this point in the history
#30369)

Ideally we wouldn't need to filter out React internals and it'd just be
covered by ignore listing by any downstream tool. E.g. a framework using
captureOwnerStack could have its own ignore listing. Printed owner
stacks would get browser source map ignore-listing. React DevTools could
have its own ignore list for internals. However, it's nice to be able to
provide nice owner stacks without a bunch of noise by default.
Especially on the server since they have to be serialized.

We currently call each function that calls into user space and track its
stack frame. However, this needs code for checking each one and doesn't
let us work across bundles.

Instead, we can name each of these frame something predictable by giving
the function a name.

Unfortunately, it's a common practice to rename functions or inline them
in compilers. Even if we didn't, others downstream from us or a dev-mode
minifier could. I use this `.bind()` trick to avoid minifying these
functions and ensure they get a unique name added to them in all
browsers. It's not 100% fool proof since a smart enough compiler could
also discover that the `this` value is not used and strip out the
function and then inline it but nobody does this yet at least.

This lets us find the bottom stack easily from stack traces just by
looking for the name.
  • Loading branch information
sebmarkbage authored Jul 22, 2024
1 parent f603426 commit 792f192
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 335 deletions.
81 changes: 52 additions & 29 deletions packages/react-reconciler/src/ReactFiberCallUserSpace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props, Arg, R>(
const callComponent = {
'react-stack-bottom-frame': function <Props, Arg, R>(
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: <Props, Arg, R>(
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<R> {
render(): R;
}

/** @noinline */
export function callRenderInDEV<R>(instance: ClassInstance<R>): R {
const wasRendering = isRendering;
setIsRendering(true);
try {
const result = instance.render();
return result;
} finally {
setIsRendering(wasRendering);
}
}
const callRender = {
'react-stack-bottom-frame': function <R>(instance: ClassInstance<R>): R {
const wasRendering = isRendering;
setIsRendering(true);
try {
const result = instance.render();
return result;
} finally {
setIsRendering(wasRendering);
}
},
};

/** @noinline */
export function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
}
export const callRenderInDEV: <R>(instance: ClassInstance<R>) => 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, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
},
};

export const callLazyInitInDEV: (lazy: LazyComponent<any, any>) => any = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any)
: (null: any);
84 changes: 7 additions & 77 deletions packages/react-reconciler/src/ReactFiberOwnerStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\/|\(\<anonymous\>\)/;

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
Expand All @@ -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');
}

Expand Down
53 changes: 38 additions & 15 deletions packages/react-server/src/ReactFizzCallUserSpace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props, Arg, R>(
const callComponent = {
'react-stack-bottom-frame': function <Props, Arg, R>(
Component: (p: Props, arg: Arg) => R,
props: Props,
secondArg: Arg,
): R {
return Component(props, secondArg);
},
};

export const callComponentInDEV: <Props, Arg, R>(
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<R> {
render(): R;
}

/** @noinline */
export function callRenderInDEV<R>(instance: ClassInstance<R>): R {
return instance.render();
}
const callRender = {
'react-stack-bottom-frame': function <R>(instance: ClassInstance<R>): R {
return instance.render();
},
};

/** @noinline */
export function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
}
export const callRenderInDEV: <R>(instance: ClassInstance<R>) => 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, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
},
};

export const callLazyInitInDEV: (lazy: LazyComponent<any, any>) => any = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any)
: (null: any);
84 changes: 7 additions & 77 deletions packages/react-server/src/ReactFizzOwnerStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\/|\(\<anonymous\>\)/;

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
Expand All @@ -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');
}

Expand Down
2 changes: 1 addition & 1 deletion packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,7 @@ function finishClassComponent(
): ReactNodeList {
let nextChildren;
if (__DEV__) {
nextChildren = callRenderInDEV(instance);
nextChildren = (callRenderInDEV(instance): any);
} else {
nextChildren = instance.render();
}
Expand Down
Loading

0 comments on commit 792f192

Please sign in to comment.