Skip to content

Commit

Permalink
DevTools should iterate over siblings during mount
Browse files Browse the repository at this point in the history
Previously, DevTools recursed over 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 deepers 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 deepers, but is resiliant to wide trees (e.g. lists containing many items).
  • Loading branch information
Brian Vaughn committed Apr 28, 2021
1 parent a5267fa commit dbfab82
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 dbfab82

Please sign in to comment.