Skip to content

Commit

Permalink
DevTools should iterate over siblings during mount (facebook#21377)
Browse files Browse the repository at this point in the history
Previously, DevTools recursed over both children and siblings during mount. This caused potential stack overflows when there were a lot of children (e.g. a list containing many items).

Given the following example component tree:

       A
    B  C  D
    E     F
          G

A method that recurses for every child and sibling leads to a max depth of 6:

    A
    A -> B
    A -> B -> E
    A -> B -> C
    A -> B -> C -> D
    A -> B -> C -> D -> F
    A -> B -> C -> D -> F -> G

The stack gets deeper as the tree gets either deeper or wider.

A method that recurses for every child and iterates over siblings leads to a max depth of 4:

    A
    A -> B
    A -> B -> E
    A -> C
    A -> D
    A -> D -> F
    A -> D -> F -> G

The stack gets deeper as the tree gets deeper but is resilient to wide trees (e.g. lists containing many items).
  • Loading branch information
Brian Vaughn authored and koto committed Jun 15, 2021
1 parent f0160fe commit 275c6b8
Showing 1 changed file with 73 additions and 75 deletions.
148 changes: 73 additions & 75 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1647,102 +1647,100 @@ export function attach(
}

function mountFiberRecursively(
fiber: Fiber,
firstChild: Fiber,
parentFiber: Fiber | null,
traverseSiblings: boolean,
traceNearestHostComponentUpdate: boolean,
) {
if (__DEBUG__) {
debug('mountFiberRecursively()', fiber, parentFiber);
}
// Iterate over siblings rather than recursing.
// This reduces the chance of stack overflow for wide trees (e.g. lists with many items).
let fiber: Fiber | null = firstChild;
while (fiber !== null) {
if (__DEBUG__) {
debug('mountFiberRecursively()', fiber, parentFiber);
}

// If we have the tree selection from previous reload, try to match this Fiber.
// Also remember whether to do the same for siblings.
const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
fiber,
);
// If we have the tree selection from previous reload, try to match this Fiber.
// Also remember whether to do the same for siblings.
const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
fiber,
);

const shouldIncludeInTree = !shouldFilterFiber(fiber);
if (shouldIncludeInTree) {
recordMount(fiber, parentFiber);
}
const shouldIncludeInTree = !shouldFilterFiber(fiber);
if (shouldIncludeInTree) {
recordMount(fiber, parentFiber);
}

if (traceUpdatesEnabled) {
if (traceNearestHostComponentUpdate) {
const elementType = getElementTypeForFiber(fiber);
// If an ancestor updated, we should mark the nearest host nodes for highlighting.
if (elementType === ElementTypeHostComponent) {
traceUpdatesForNodes.add(fiber.stateNode);
traceNearestHostComponentUpdate = false;
if (traceUpdatesEnabled) {
if (traceNearestHostComponentUpdate) {
const elementType = getElementTypeForFiber(fiber);
// If an ancestor updated, we should mark the nearest host nodes for highlighting.
if (elementType === ElementTypeHostComponent) {
traceUpdatesForNodes.add(fiber.stateNode);
traceNearestHostComponentUpdate = false;
}
}
}

// We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
// because we don't want to highlight every host node inside of a newly mounted subtree.
}
// We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
// because we don't want to highlight every host node inside of a newly mounted subtree.
}

const isSuspense = fiber.tag === ReactTypeOfWork.SuspenseComponent;
if (isSuspense) {
const isTimedOut = fiber.memoizedState !== null;
if (isTimedOut) {
// Special case: if Suspense mounts in a timed-out state,
// get the fallback child from the inner fragment and mount
// it as if it was our own child. Updates handle this too.
const primaryChildFragment = fiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
const fallbackChild = fallbackChildFragment
? fallbackChildFragment.child
: null;
if (fallbackChild !== null) {
mountFiberRecursively(
fallbackChild,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
const isSuspense = fiber.tag === ReactTypeOfWork.SuspenseComponent;
if (isSuspense) {
const isTimedOut = fiber.memoizedState !== null;
if (isTimedOut) {
// Special case: if Suspense mounts in a timed-out state,
// get the fallback child from the inner fragment and mount
// it as if it was our own child. Updates handle this too.
const primaryChildFragment = fiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
const fallbackChild = fallbackChildFragment
? fallbackChildFragment.child
: null;
if (fallbackChild !== null) {
mountFiberRecursively(
fallbackChild,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
} else {
let primaryChild: Fiber | null = null;
const areSuspenseChildrenConditionallyWrapped =
OffscreenComponent === -1;
if (areSuspenseChildrenConditionallyWrapped) {
primaryChild = fiber.child;
} else if (fiber.child !== null) {
primaryChild = fiber.child.child;
}
if (primaryChild !== null) {
mountFiberRecursively(
primaryChild,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
}
} else {
let primaryChild: Fiber | null = null;
const areSuspenseChildrenConditionallyWrapped =
OffscreenComponent === -1;
if (areSuspenseChildrenConditionallyWrapped) {
primaryChild = fiber.child;
} else if (fiber.child !== null) {
primaryChild = fiber.child.child;
}
if (primaryChild !== null) {
if (fiber.child !== null) {
mountFiberRecursively(
primaryChild,
fiber.child,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
}
} else {
if (fiber.child !== null) {
mountFiberRecursively(
fiber.child,
shouldIncludeInTree ? fiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
}

// We're exiting this Fiber now, and entering its siblings.
// If we have selection to restore, we might need to re-activate tracking.
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
// We're exiting this Fiber now, and entering its siblings.
// If we have selection to restore, we might need to re-activate tracking.
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);

if (traverseSiblings && fiber.sibling !== null) {
mountFiberRecursively(
fiber.sibling,
parentFiber,
true,
traceNearestHostComponentUpdate,
);
fiber = traverseSiblings ? fiber.sibling : null;
}
}

Expand Down

0 comments on commit 275c6b8

Please sign in to comment.