Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DevTools] Track virtual instances on the tracked path for selections #30802

Merged
merged 2 commits into from
Aug 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2259,16 +2259,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 @@ -2387,6 +2382,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 @@ -2405,13 +2408,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 @@ -2526,17 +2532,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 @@ -2575,14 +2584,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 @@ -2592,6 +2602,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 @@ -5422,13 +5433,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 @@ -5438,7 +5451,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 @@ -5466,6 +5482,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 @@ -5482,13 +5501,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 @@ -5588,6 +5663,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 @@ -5597,13 +5680,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 @@ -5619,20 +5709,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
Loading