Skip to content

Commit

Permalink
Implement component stacks
Browse files Browse the repository at this point in the history
This uses a reverse linked list in DEV-only to keep track of where we're
currently executing.
  • Loading branch information
sebmarkbage committed Jun 3, 2021
1 parent 86715ef commit ccc880a
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 4 deletions.
61 changes: 61 additions & 0 deletions packages/react-server/src/ReactFizzComponentStack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {
describeBuiltInComponentFrame,
describeFunctionComponentFrame,
describeClassComponentFrame,
} from 'shared/ReactComponentStackFrame';

// DEV-only reverse linked list representing the current component stack
type BuiltInComponentStackNode = {
tag: 0,
parent: null | ComponentStackNode,
type: string,
};
type FunctionComponentStackNode = {
tag: 1,
parent: null | ComponentStackNode,
type: Function,
};
type ClassComponentStackNode = {
tag: 2,
parent: null | ComponentStackNode,
type: Function,
};
export type ComponentStackNode =
| BuiltInComponentStackNode
| FunctionComponentStackNode
| ClassComponentStackNode;

export function getStackByComponentStackNode(
componentStack: ComponentStackNode,
): string {
try {
let info = '';
let node = componentStack;
do {
switch (node.tag) {
case 0:
info += describeBuiltInComponentFrame(node.type, null, null);
break;
case 1:
info += describeFunctionComponentFrame(node.type, null, null);
break;
case 2:
info += describeClassComponentFrame(node.type, null, null);
break;
}
node = node.parent;
} while (node);
return info;
} catch (x) {
return '\nError generating stack: ' + x.message + '\n' + x.stack;
}
}
109 changes: 105 additions & 4 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
FormatContext,
} from './ReactServerFormatConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';

import {
scheduleWork,
Expand Down Expand Up @@ -77,6 +78,7 @@ import {
currentResponseState,
setCurrentResponseState,
} from './ReactFizzHooks';
import {getStackByComponentStackNode} from './ReactFizzComponentStack';

import {
getIteratorFn,
Expand Down Expand Up @@ -110,6 +112,7 @@ import invariant from 'shared/invariant';
import isArray from 'shared/isArray';

const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;

type LegacyContext = {
[key: string]: any,
Expand All @@ -135,6 +138,7 @@ type Task = {
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
assignID: null | SuspenseBoundaryID, // id to assign to the content
componentStack: null | ComponentStackNode, // DEV-only component stack
};

const PENDING = 0;
Expand Down Expand Up @@ -299,7 +303,7 @@ function createTask(
} else {
blockedBoundary.pendingTasks++;
}
const task = {
const task: Task = ({
node,
ping: () => pingTask(request, task),
blockedBoundary,
Expand All @@ -308,7 +312,10 @@ function createTask(
legacyContext,
context,
assignID,
};
}: any);
if (__DEV__) {
task.componentStack = currentTaskInDEV ? task.componentStack : null;
}
abortSet.add(task);
return task;
}
Expand All @@ -331,6 +338,55 @@ function createPendingSegment(
};
}

// DEV-only global reference to the currently executing task
let currentTaskInDEV: null | Task = null;
function getCurrentStackInDEV(): string {
if (__DEV__) {
if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {
return '';
}
return getStackByComponentStackNode(currentTaskInDEV.componentStack);
}
return '';
}

function pushBuiltInComponentStackInDEV(task: Task, type: string): void {
if (__DEV__) {
task.componentStack = {
tag: 0,
parent: task.componentStack,
type,
};
}
}
function pushFunctionComponentStackInDEV(task: Task, type: Function): void {
if (__DEV__) {
task.componentStack = {
tag: 1,
parent: task.componentStack,
type,
};
}
}
function pushClassComponentStackInDEV(task: Task, type: Function): void {
if (__DEV__) {
task.componentStack = {
tag: 2,
parent: task.componentStack,
type,
};
}
}
function popComponentStackInDEV(task: Task): void {
if (__DEV__) {
invariant(
task.componentStack !== null,
'Unexpectedly popped too many stack frames. This is a bug in React.',
);
task.componentStack = task.componentStack.parent;
}
}

function reportError(request: Request, error: mixed): void {
// If this callback errors, we intentionally let that error bubble up to become a fatal error
// so that someone fixes the error reporting instead of hiding it.
Expand All @@ -351,6 +407,7 @@ function renderSuspenseBoundary(
task: Task,
props: Object,
): void {
pushBuiltInComponentStackInDEV(task, 'Suspense');
const parentBoundary = task.blockedBoundary;
const parentSegment = task.blockedSegment;

Expand Down Expand Up @@ -418,6 +475,7 @@ function renderSuspenseBoundary(
} finally {
task.blockedBoundary = parentBoundary;
task.blockedSegment = parentSegment;
popComponentStackInDEV(task);
}

// This injects an extra segment just to contain an empty tag with an ID.
Expand Down Expand Up @@ -456,6 +514,7 @@ function renderHostElement(
type: string,
props: Object,
): void {
pushBuiltInComponentStackInDEV(task, type);
const segment = task.blockedSegment;
const children = pushStartInstance(
segment.chunks,
Expand All @@ -476,6 +535,7 @@ function renderHostElement(
// the correct context. Therefore this is not in a finally.
segment.formatContext = prevContext;
pushEndInstance(segment.chunks, type, props);
popComponentStackInDEV(task);
}

function shouldConstruct(Component) {
Expand Down Expand Up @@ -564,12 +624,14 @@ function renderClassComponent(
Component: any,
props: any,
): void {
pushClassComponentStackInDEV(task, Component);
const maskedContext = !disableLegacyContext
? getMaskedContext(Component, task.legacyContext)
: undefined;
const instance = constructClassInstance(Component, props, maskedContext);
mountClassInstance(instance, Component, props, maskedContext);
finishClassComponent(request, task, instance, Component, props);
popComponentStackInDEV(task);
}

const didWarnAboutBadClass = {};
Expand All @@ -594,6 +656,7 @@ function renderIndeterminateComponent(
if (!disableLegacyContext) {
legacyContext = getMaskedContext(Component, task.legacyContext);
}
pushFunctionComponentStackInDEV(task, Component);

if (__DEV__) {
if (
Expand Down Expand Up @@ -688,6 +751,7 @@ function renderIndeterminateComponent(
// the previous task every again, so we can use the destructive recursive form.
renderNodeDestructive(request, task, value);
}
popComponentStackInDEV(task);
}

function validateFunctionComponentInDev(Component: any): void {
Expand Down Expand Up @@ -768,8 +832,10 @@ function renderForwardRef(
props: Object,
ref: any,
): void {
pushFunctionComponentStackInDEV(task, type.render);
const children = renderWithHooks(request, task, type.render, props, ref);
renderNodeDestructive(request, task, children);
popComponentStackInDEV(task);
}

function renderMemo(
Expand Down Expand Up @@ -866,11 +932,13 @@ function renderLazyComponent(
props: Object,
ref: any,
): void {
pushBuiltInComponentStackInDEV(task, 'Lazy');
const payload = lazyComponent._payload;
const init = lazyComponent._init;
const Component = init(payload);
const resolvedProps = resolveDefaultProps(Component, props);
return renderElement(request, task, Component, resolvedProps, ref);
renderElement(request, task, Component, resolvedProps, ref);
popComponentStackInDEV(task);
}

function renderElement(
Expand Down Expand Up @@ -907,11 +975,17 @@ function renderElement(
case REACT_DEBUG_TRACING_MODE_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
case REACT_FRAGMENT_TYPE: {
renderNodeDestructive(request, task, props.children);
return;
}
case REACT_SUSPENSE_LIST_TYPE: {
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
// TODO: SuspenseList should control the boundaries.
renderNodeDestructive(request, task, props.children);
popComponentStackInDEV(task);
return;
}
case REACT_SCOPE_TYPE: {
if (enableScopeAPI) {
renderNodeDestructive(request, task, props.children);
Expand Down Expand Up @@ -1174,6 +1248,10 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
const previousFormatContext = task.blockedSegment.formatContext;
const previousLegacyContext = task.legacyContext;
const previousContext = task.context;
let previousComponentStack = null;
if (__DEV__) {
previousComponentStack = task.componentStack;
}
try {
return renderNodeDestructive(request, task, node);
} catch (x) {
Expand All @@ -1187,6 +1265,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
task.context = previousContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
task.componentStack = previousComponentStack;
}
} else {
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
Expand All @@ -1195,6 +1276,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
task.context = previousContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
task.componentStack = previousComponentStack;
}
// We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.
throw x;
Expand Down Expand Up @@ -1360,6 +1444,11 @@ function retryTask(request: Request, task: Task): void {
// We don't restore it after we leave because it's likely that we'll end up
// needing a very similar context soon again.
switchContext(task.context);
let prevTaskInDEV = null;
if (__DEV__) {
prevTaskInDEV = currentTaskInDEV;
currentTaskInDEV = task;
}
try {
// We call the destructive form that mutates this task. That way if something
// suspends again, we can reuse the same task instead of spawning a new one.
Expand All @@ -1379,6 +1468,10 @@ function retryTask(request: Request, task: Task): void {
segment.status = ERRORED;
erroredTask(request, task.blockedBoundary, segment, x);
}
} finally {
if (__DEV__) {
currentTaskInDEV = prevTaskInDEV;
}
}
}

Expand All @@ -1389,6 +1482,11 @@ export function performWork(request: Request): void {
const prevContext = getActiveContext();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = Dispatcher;
let prevGetCurrentStackImpl;
if (__DEV__) {
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
ReactDebugCurrentFrame.getCurrentStack = getCurrentStackInDEV;
}
const prevResponseState = currentResponseState;
setCurrentResponseState(request.responseState);
try {
Expand All @@ -1408,6 +1506,9 @@ export function performWork(request: Request): void {
} finally {
setCurrentResponseState(prevResponseState);
ReactCurrentDispatcher.current = prevDispatcher;
if (__DEV__) {
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
}
if (prevDispatcher === Dispatcher) {
// This means that we were in a reentrant work loop. This could happen
// in a renderer that supports synchronous work like renderToString,
Expand Down

0 comments on commit ccc880a

Please sign in to comment.