Skip to content

Commit

Permalink
[DevTools] Track virtual instances on the tracked path for selections (
Browse files Browse the repository at this point in the history
…#30802)

This appends a (filtered) virtual instance path at the end of the fiber
path. If a virtual instance is selected inside the fiber.

The main part of the path is still just the fiber path since that's the
semantically stateful part. Then we just tack on a few virtual path
frames at the end if we're currently selecting a specific Server
Component within the nearest Fiber.

I also took the opportunity to fix a bug which caused selections inside
Suspense boundaries to not be tracked.
  • Loading branch information
sebmarkbage authored Aug 29, 2024
1 parent 18bf7bf commit e33a723
Showing 1 changed file with 121 additions and 39 deletions.
160 changes: 121 additions & 39 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2266,16 +2266,11 @@ export function attach(
debug('recordUnmount()', fiber, null);
}

if (trackedPathMatchFiber !== null) {
if (trackedPathMatchInstance === fiberInstance) {
// We're in the process of trying to restore previous selection.
// If this fiber matched but is being unmounted, there's no use trying.
// Reset the state so we don't keep holding onto it.
if (
fiber === trackedPathMatchFiber ||
fiber === trackedPathMatchFiber.alternate
) {
setTrackedPath(null);
}
setTrackedPath(null);
}

const id = fiberInstance.id;
Expand Down Expand Up @@ -2386,6 +2381,14 @@ export function attach(
traceNearestHostComponentUpdate: boolean,
virtualLevel: number, // the nth level of virtual instances
): void {
// If we have the tree selection from previous reload, try to match this Instance.
// Also remember whether to do the same for siblings.
const mightSiblingsBeOnTrackedPath =
updateVirtualTrackedPathStateBeforeMount(
virtualInstance,
reconcilingParent,
);

const stashedParent = reconcilingParent;
const stashedPrevious = previouslyReconciledSibling;
const stashedRemaining = remainingReconcilingChildren;
Expand All @@ -2406,13 +2409,16 @@ export function attach(
reconcilingParent = stashedParent;
previouslyReconciledSibling = stashedPrevious;
remainingReconcilingChildren = stashedRemaining;
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
}
}

function recordVirtualUnmount(instance: VirtualInstance) {
if (trackedPathMatchFiber !== null) {
if (trackedPathMatchInstance === instance) {
// We're in the process of trying to restore previous selection.
// TODO: Handle virtual instances on the tracked path.
// If this fiber matched but is being unmounted, there's no use trying.
// Reset the state so we don't keep holding onto it.
setTrackedPath(null);
}

const id = instance.id;
Expand Down Expand Up @@ -2521,17 +2527,20 @@ export function attach(
debug('mountFiberRecursively()', fiber, reconcilingParent);
}

// 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);
let newInstance = null;
if (shouldIncludeInTree) {
newInstance = recordMount(fiber, reconcilingParent);
insertChild(newInstance);
}

// 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,
newInstance,
);

const stashedParent = reconcilingParent;
const stashedPrevious = previouslyReconciledSibling;
const stashedRemaining = remainingReconcilingChildren;
Expand Down Expand Up @@ -2570,14 +2579,15 @@ export function attach(
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
const fallbackChild = fallbackChildFragment
? fallbackChildFragment.child
: null;
if (fallbackChild !== null) {
mountChildrenRecursively(
fallbackChild,
traceNearestHostComponentUpdate,
);
if (fallbackChildFragment) {
const fallbackChild = fallbackChildFragment.child;
if (fallbackChild !== null) {
updateTrackedPathStateBeforeMount(fallbackChildFragment, null);
mountChildrenRecursively(
fallbackChild,
traceNearestHostComponentUpdate,
);
}
}
} else {
let primaryChild: Fiber | null = null;
Expand All @@ -2587,6 +2597,7 @@ export function attach(
primaryChild = fiber.child;
} else if (fiber.child !== null) {
primaryChild = fiber.child.child;
updateTrackedPathStateBeforeMount(fiber.child, null);
}
if (primaryChild !== null) {
mountChildrenRecursively(
Expand Down Expand Up @@ -5262,13 +5273,15 @@ export function attach(
// Remember if we're trying to restore the selection after reload.
// In that case, we'll do some extra checks for matching mounts.
let trackedPath: Array<PathFrame> | null = null;
let trackedPathMatchFiber: Fiber | null = null;
let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber.
let trackedPathMatchInstance: DevToolsInstance | null = null; // This is the deepest matched filtered Instance.
let trackedPathMatchDepth = -1;
let mightBeOnTrackedPath = false;

function setTrackedPath(path: Array<PathFrame> | null) {
if (path === null) {
trackedPathMatchFiber = null;
trackedPathMatchInstance = null;
trackedPathMatchDepth = -1;
mightBeOnTrackedPath = false;
}
Expand All @@ -5278,7 +5291,10 @@ export function attach(
// We call this before traversing a new mount.
// It remembers whether this Fiber is the next best match for tracked path.
// The return value signals whether we should keep matching siblings or not.
function updateTrackedPathStateBeforeMount(fiber: Fiber): boolean {
function updateTrackedPathStateBeforeMount(
fiber: Fiber,
fiberInstance: null | FiberInstance,
): boolean {
if (trackedPath === null || !mightBeOnTrackedPath) {
// Fast path: there's nothing to track so do nothing and ignore siblings.
return false;
Expand Down Expand Up @@ -5306,6 +5322,9 @@ export function attach(
) {
// We have our next match.
trackedPathMatchFiber = fiber;
if (fiberInstance !== null) {
trackedPathMatchInstance = fiberInstance;
}
trackedPathMatchDepth++;
// Are we out of frames to match?
// $FlowFixMe[incompatible-use] found when upgrading Flow
Expand All @@ -5322,13 +5341,69 @@ export function attach(
return false;
}
}
if (trackedPathMatchFiber === null && fiberInstance === null) {
// We're now looking for a Virtual Instance. It might be inside filtered Fibers
// so we keep looking below.
return true;
}
// This Fiber's parent is on the path, but this Fiber itself isn't.
// There's no need to check its children--they won't be on the path either.
mightBeOnTrackedPath = false;
// However, one of its siblings may be on the path so keep searching.
return true;
}

function updateVirtualTrackedPathStateBeforeMount(
virtualInstance: VirtualInstance,
parentInstance: null | DevToolsInstance,
): boolean {
if (trackedPath === null || !mightBeOnTrackedPath) {
// Fast path: there's nothing to track so do nothing and ignore siblings.
return false;
}
// Check if we've matched our nearest unfiltered parent so far.
if (trackedPathMatchInstance === parentInstance) {
const actualFrame = getVirtualPathFrame(virtualInstance);
// $FlowFixMe[incompatible-use] found when upgrading Flow
const expectedFrame = trackedPath[trackedPathMatchDepth + 1];
if (expectedFrame === undefined) {
throw new Error('Expected to see a frame at the next depth.');
}
if (
actualFrame.index === expectedFrame.index &&
actualFrame.key === expectedFrame.key &&
actualFrame.displayName === expectedFrame.displayName
) {
// We have our next match.
trackedPathMatchFiber = null; // Don't bother looking in Fibers anymore. We're deeper now.
trackedPathMatchInstance = virtualInstance;
trackedPathMatchDepth++;
// Are we out of frames to match?
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (trackedPathMatchDepth === trackedPath.length - 1) {
// There's nothing that can possibly match afterwards.
// Don't check the children.
mightBeOnTrackedPath = false;
} else {
// Check the children, as they might reveal the next match.
mightBeOnTrackedPath = true;
}
// In either case, since we have a match, we don't need
// to check the siblings. They'll never match.
return false;
}
}
if (trackedPathMatchFiber !== null) {
// We're still looking for a Fiber which might be underneath this instance.
return true;
}
// This Instance's parent is on the path, but this Instance itself isn't.
// There's no need to check its children--they won't be on the path either.
mightBeOnTrackedPath = false;
// However, one of its siblings may be on the path so keep searching.
return true;
}

function updateTrackedPathStateAfterMount(
mightSiblingsBeOnTrackedPath: boolean,
) {
Expand Down Expand Up @@ -5428,6 +5503,14 @@ export function attach(
};
}

function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
return {
displayName: virtualInstance.data.name || '',
key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
index: -1, // We use -1 to indicate that this is a virtual path frame.
};
}

// Produces a serializable representation that does a best effort
// of identifying a particular Fiber between page reloads.
// The return path will contain Fibers that are "invisible" to the store
Expand All @@ -5437,13 +5520,20 @@ export function attach(
if (devtoolsInstance === undefined) {
return null;
}
if (devtoolsInstance.kind !== FIBER_INSTANCE) {
// TODO: Handle VirtualInstance.
return null;
}

let fiber: null | Fiber = devtoolsInstance.data;
const keyPath = [];

let inst: DevToolsInstance = devtoolsInstance;
while (inst.kind === VIRTUAL_INSTANCE) {
keyPath.push(getVirtualPathFrame(inst));
if (inst.parent === null) {
// This is a bug but non-essential. We should've found a root instance.
return null;
}
inst = inst.parent;
}

let fiber: null | Fiber = inst.data;
while (fiber !== null) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
keyPath.push(getPathFrame(fiber));
Expand All @@ -5459,20 +5549,12 @@ export function attach(
// Nothing to match.
return null;
}
if (trackedPathMatchFiber === null) {
if (trackedPathMatchInstance === null) {
// We didn't find anything.
return null;
}
// Find the closest Fiber store is aware of.
let fiber: null | Fiber = trackedPathMatchFiber;
while (fiber !== null && shouldFilterFiber(fiber)) {
fiber = fiber.return;
}
if (fiber === null) {
return null;
}
return {
id: getFiberIDThrows(fiber),
id: trackedPathMatchInstance.id,
// $FlowFixMe[incompatible-use] found when upgrading Flow
isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
};
Expand Down

0 comments on commit e33a723

Please sign in to comment.