@samitouri / QOS-React / commits / e33a7233a7

[DevTools] Track virtual instances on the tracked path for selections (#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.

Sebastian Markbåge committed Aug 29, 2024 at 12:45 UTC e33a7233a76e1164bd1a9c4b8115abb575b48c50
1 file changed +121 -39
packages/react-devtools-shared/src/backend/fiber/renderer.js
+121 -39
@@ -2266,16 +2266,11 @@ export function attach(
2266 debug('recordUnmount()', fiber, null);
2267 }
2268
2269 - if (trackedPathMatchFiber !== null) {
2269 + if (trackedPathMatchInstance === fiberInstance) {
2270 // We're in the process of trying to restore previous selection.
2271 // If this fiber matched but is being unmounted, there's no use trying.
2272 // Reset the state so we don't keep holding onto it.
2273 - if (
2274 - fiber === trackedPathMatchFiber ||
2275 - fiber === trackedPathMatchFiber.alternate
2276 - ) {
2277 - setTrackedPath(null);
2278 - }
2273 + setTrackedPath(null);
2274 }
2275
2276 const id = fiberInstance.id;
@@ -2386,6 +2381,14 @@ export function attach(
2381 traceNearestHostComponentUpdate: boolean,
2382 virtualLevel: number, // the nth level of virtual instances
2383 ): void {
2384 + // If we have the tree selection from previous reload, try to match this Instance.
2385 + // Also remember whether to do the same for siblings.
2386 + const mightSiblingsBeOnTrackedPath =
2387 + updateVirtualTrackedPathStateBeforeMount(
2388 + virtualInstance,
2389 + reconcilingParent,
2390 + );
2391 +
2392 const stashedParent = reconcilingParent;
2393 const stashedPrevious = previouslyReconciledSibling;
2394 const stashedRemaining = remainingReconcilingChildren;
@@ -2406,13 +2409,16 @@ export function attach(
2409 reconcilingParent = stashedParent;
2410 previouslyReconciledSibling = stashedPrevious;
2411 remainingReconcilingChildren = stashedRemaining;
2412 + updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
2413 }
2414 }
2415
2416 function recordVirtualUnmount(instance: VirtualInstance) {
2413 - if (trackedPathMatchFiber !== null) {
2417 + if (trackedPathMatchInstance === instance) {
2418 // We're in the process of trying to restore previous selection.
2415 - // TODO: Handle virtual instances on the tracked path.
2419 + // If this fiber matched but is being unmounted, there's no use trying.
2420 + // Reset the state so we don't keep holding onto it.
2421 + setTrackedPath(null);
2422 }
2423
2424 const id = instance.id;
@@ -2521,17 +2527,20 @@ export function attach(
2527 debug('mountFiberRecursively()', fiber, reconcilingParent);
2528 }
2529
2524 - // If we have the tree selection from previous reload, try to match this Fiber.
2525 - // Also remember whether to do the same for siblings.
2526 - const mightSiblingsBeOnTrackedPath =
2527 - updateTrackedPathStateBeforeMount(fiber);
2528 -
2530 const shouldIncludeInTree = !shouldFilterFiber(fiber);
2531 let newInstance = null;
2532 if (shouldIncludeInTree) {
2533 newInstance = recordMount(fiber, reconcilingParent);
2534 insertChild(newInstance);
2535 }
2536 +
2537 + // If we have the tree selection from previous reload, try to match this Fiber.
2538 + // Also remember whether to do the same for siblings.
2539 + const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
2540 + fiber,
2541 + newInstance,
2542 + );
2543 +
2544 const stashedParent = reconcilingParent;
2545 const stashedPrevious = previouslyReconciledSibling;
2546 const stashedRemaining = remainingReconcilingChildren;
@@ -2570,14 +2579,15 @@ export function attach(
2579 const fallbackChildFragment = primaryChildFragment
2580 ? primaryChildFragment.sibling
2581 : null;
2573 - const fallbackChild = fallbackChildFragment
2574 - ? fallbackChildFragment.child
2575 - : null;
2576 - if (fallbackChild !== null) {
2577 - mountChildrenRecursively(
2578 - fallbackChild,
2579 - traceNearestHostComponentUpdate,
2580 - );
2582 + if (fallbackChildFragment) {
2583 + const fallbackChild = fallbackChildFragment.child;
2584 + if (fallbackChild !== null) {
2585 + updateTrackedPathStateBeforeMount(fallbackChildFragment, null);
2586 + mountChildrenRecursively(
2587 + fallbackChild,
2588 + traceNearestHostComponentUpdate,
2589 + );
2590 + }
2591 }
2592 } else {
2593 let primaryChild: Fiber | null = null;
@@ -2587,6 +2597,7 @@ export function attach(
2597 primaryChild = fiber.child;
2598 } else if (fiber.child !== null) {
2599 primaryChild = fiber.child.child;
2600 + updateTrackedPathStateBeforeMount(fiber.child, null);
2601 }
2602 if (primaryChild !== null) {
2603 mountChildrenRecursively(
@@ -5262,13 +5273,15 @@ export function attach(
5273 // Remember if we're trying to restore the selection after reload.
5274 // In that case, we'll do some extra checks for matching mounts.
5275 let trackedPath: Array<PathFrame> | null = null;
5265 - let trackedPathMatchFiber: Fiber | null = null;
5276 + let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber.
5277 + let trackedPathMatchInstance: DevToolsInstance | null = null; // This is the deepest matched filtered Instance.
5278 let trackedPathMatchDepth = -1;
5279 let mightBeOnTrackedPath = false;
5280
5281 function setTrackedPath(path: Array<PathFrame> | null) {
5282 if (path === null) {
5283 trackedPathMatchFiber = null;
5284 + trackedPathMatchInstance = null;
5285 trackedPathMatchDepth = -1;
5286 mightBeOnTrackedPath = false;
5287 }
@@ -5278,7 +5291,10 @@ export function attach(
5291 // We call this before traversing a new mount.
5292 // It remembers whether this Fiber is the next best match for tracked path.
5293 // The return value signals whether we should keep matching siblings or not.
5281 - function updateTrackedPathStateBeforeMount(fiber: Fiber): boolean {
5294 + function updateTrackedPathStateBeforeMount(
5295 + fiber: Fiber,
5296 + fiberInstance: null | FiberInstance,
5297 + ): boolean {
5298 if (trackedPath === null || !mightBeOnTrackedPath) {
5299 // Fast path: there's nothing to track so do nothing and ignore siblings.
5300 return false;
@@ -5306,6 +5322,9 @@ export function attach(
5322 ) {
5323 // We have our next match.
5324 trackedPathMatchFiber = fiber;
5325 + if (fiberInstance !== null) {
5326 + trackedPathMatchInstance = fiberInstance;
5327 + }
5328 trackedPathMatchDepth++;
5329 // Are we out of frames to match?
5330 // $FlowFixMe[incompatible-use] found when upgrading Flow
@@ -5322,6 +5341,11 @@ export function attach(
5341 return false;
5342 }
5343 }
5344 + if (trackedPathMatchFiber === null && fiberInstance === null) {
5345 + // We're now looking for a Virtual Instance. It might be inside filtered Fibers
5346 + // so we keep looking below.
5347 + return true;
5348 + }
5349 // This Fiber's parent is on the path, but this Fiber itself isn't.
5350 // There's no need to check its children--they won't be on the path either.
5351 mightBeOnTrackedPath = false;
@@ -5329,6 +5353,57 @@ export function attach(
5353 return true;
5354 }
5355
5356 + function updateVirtualTrackedPathStateBeforeMount(
5357 + virtualInstance: VirtualInstance,
5358 + parentInstance: null | DevToolsInstance,
5359 + ): boolean {
5360 + if (trackedPath === null || !mightBeOnTrackedPath) {
5361 + // Fast path: there's nothing to track so do nothing and ignore siblings.
5362 + return false;
5363 + }
5364 + // Check if we've matched our nearest unfiltered parent so far.
5365 + if (trackedPathMatchInstance === parentInstance) {
5366 + const actualFrame = getVirtualPathFrame(virtualInstance);
5367 + // $FlowFixMe[incompatible-use] found when upgrading Flow
5368 + const expectedFrame = trackedPath[trackedPathMatchDepth + 1];
5369 + if (expectedFrame === undefined) {
5370 + throw new Error('Expected to see a frame at the next depth.');
5371 + }
5372 + if (
5373 + actualFrame.index === expectedFrame.index &&
5374 + actualFrame.key === expectedFrame.key &&
5375 + actualFrame.displayName === expectedFrame.displayName
5376 + ) {
5377 + // We have our next match.
5378 + trackedPathMatchFiber = null; // Don't bother looking in Fibers anymore. We're deeper now.
5379 + trackedPathMatchInstance = virtualInstance;
5380 + trackedPathMatchDepth++;
5381 + // Are we out of frames to match?
5382 + // $FlowFixMe[incompatible-use] found when upgrading Flow
5383 + if (trackedPathMatchDepth === trackedPath.length - 1) {
5384 + // There's nothing that can possibly match afterwards.
5385 + // Don't check the children.
5386 + mightBeOnTrackedPath = false;
5387 + } else {
5388 + // Check the children, as they might reveal the next match.
5389 + mightBeOnTrackedPath = true;
5390 + }
5391 + // In either case, since we have a match, we don't need
5392 + // to check the siblings. They'll never match.
5393 + return false;
5394 + }
5395 + }
5396 + if (trackedPathMatchFiber !== null) {
5397 + // We're still looking for a Fiber which might be underneath this instance.
5398 + return true;
5399 + }
5400 + // This Instance's parent is on the path, but this Instance itself isn't.
5401 + // There's no need to check its children--they won't be on the path either.
5402 + mightBeOnTrackedPath = false;
5403 + // However, one of its siblings may be on the path so keep searching.
5404 + return true;
5405 + }
5406 +
5407 function updateTrackedPathStateAfterMount(
5408 mightSiblingsBeOnTrackedPath: boolean,
5409 ) {
@@ -5428,6 +5503,14 @@ export function attach(
5503 };
5504 }
5505
5506 + function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
5507 + return {
5508 + displayName: virtualInstance.data.name || '',
5509 + key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
5510 + index: -1, // We use -1 to indicate that this is a virtual path frame.
5511 + };
5512 + }
5513 +
5514 // Produces a serializable representation that does a best effort
5515 // of identifying a particular Fiber between page reloads.
5516 // The return path will contain Fibers that are "invisible" to the store
@@ -5437,13 +5520,20 @@ export function attach(
5520 if (devtoolsInstance === undefined) {
5521 return null;
5522 }
5440 - if (devtoolsInstance.kind !== FIBER_INSTANCE) {
5441 - // TODO: Handle VirtualInstance.
5442 - return null;
5443 - }
5523
5445 - let fiber: null | Fiber = devtoolsInstance.data;
5524 const keyPath = [];
5525 +
5526 + let inst: DevToolsInstance = devtoolsInstance;
5527 + while (inst.kind === VIRTUAL_INSTANCE) {
5528 + keyPath.push(getVirtualPathFrame(inst));
5529 + if (inst.parent === null) {
5530 + // This is a bug but non-essential. We should've found a root instance.
5531 + return null;
5532 + }
5533 + inst = inst.parent;
5534 + }
5535 +
5536 + let fiber: null | Fiber = inst.data;
5537 while (fiber !== null) {
5538 // $FlowFixMe[incompatible-call] found when upgrading Flow
5539 keyPath.push(getPathFrame(fiber));
@@ -5459,20 +5549,12 @@ export function attach(
5549 // Nothing to match.
5550 return null;
5551 }
5462 - if (trackedPathMatchFiber === null) {
5552 + if (trackedPathMatchInstance === null) {
5553 // We didn't find anything.
5554 return null;
5555 }
5466 - // Find the closest Fiber store is aware of.
5467 - let fiber: null | Fiber = trackedPathMatchFiber;
5468 - while (fiber !== null && shouldFilterFiber(fiber)) {
5469 - fiber = fiber.return;
5470 - }
5471 - if (fiber === null) {
5472 - return null;
5473 - }
5556 return {
5475 - id: getFiberIDThrows(fiber),
5557 + id: trackedPathMatchInstance.id,
5558 // $FlowFixMe[incompatible-use] found when upgrading Flow
5559 isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
5560 };