@samitouri / QOS-React / commits / 8a70d31ba9

[DevTools] Track DOM nodes to Fiber map for HostHoistable Resources (#30590)

Follow up from #30584. You can already select a singleton or hoistable (that's not a resource) in the browser elements panel and it'll select the corresponding node in the RDT Components panel. That works because it uses the same mechanism as event dispatching and those need to be able to receive events. However, you can't select a resource. Because that's conceptually one to many. This keeps track of which fiber is acquiring which resource so we can find all the corresponding instances. E.g. now you can select the `<link rel="stylesheet">` in the Flight fixture in the Element panel and then the component that rendered it in the Components panel will be selected. If we had a concept multi-selection we could potentially select all of them. This similar to how a Server Component can be rendered in more than one place and if we want to select all matching ones. It's kind of weird though and both cases are edge cases. Notably imperative preloads do have elements that don't have any corresponding component but that's ok. So they'll just select `<head>`. Maybe in dev we could track the owners of those.

Sebastian Markbåge committed Aug 2, 2024 at 19:22 UTC 8a70d31ba977a8cdbd8ae2146fdeb5b427bf4ff9
1 file changed +112 -36
packages/react-devtools-shared/src/backend/fiber/renderer.js
+112 -36
@@ -696,6 +696,40 @@ const fiberToFiberInstanceMap: Map<Fiber, FiberInstance> = new Map();
696 // operations that should be the same whether the current and work-in-progress Fiber is used.
697 const idToDevToolsInstanceMap: Map<number, DevToolsInstance> = new Map();
698
699 +// Map of resource DOM nodes to all the Fibers that depend on it.
700 +const hostResourceToFiberMap: Map<HostInstance, Set<Fiber>> = new Map();
701 +
702 +function aquireHostResource(
703 + fiber: Fiber,
704 + resource: ?{instance?: HostInstance},
705 +): void {
706 + const hostInstance = resource && resource.instance;
707 + if (hostInstance) {
708 + let resourceFibers = hostResourceToFiberMap.get(hostInstance);
709 + if (resourceFibers === undefined) {
710 + resourceFibers = new Set();
711 + hostResourceToFiberMap.set(hostInstance, resourceFibers);
712 + }
713 + resourceFibers.add(fiber);
714 + }
715 +}
716 +
717 +function releaseHostResource(
718 + fiber: Fiber,
719 + resource: ?{instance?: HostInstance},
720 +): void {
721 + const hostInstance = resource && resource.instance;
722 + if (hostInstance) {
723 + const resourceFibers = hostResourceToFiberMap.get(hostInstance);
724 + if (resourceFibers !== undefined) {
725 + resourceFibers.delete(fiber);
726 + if (resourceFibers.size === 0) {
727 + hostResourceToFiberMap.delete(hostInstance);
728 + }
729 + }
730 + }
731 +}
732 +
733 export function attach(
734 hook: DevToolsHook,
735 rendererID: number,
@@ -2283,6 +2317,10 @@ export function attach(
2317 // because we don't want to highlight every host node inside of a newly mounted subtree.
2318 }
2319
2320 + if (fiber.tag === HostHoistable) {
2321 + aquireHostResource(fiber, fiber.memoizedState);
2322 + }
2323 +
2324 if (fiber.tag === SuspenseComponent) {
2325 const isTimedOut = fiber.memoizedState !== null;
2326 if (isTimedOut) {
@@ -2344,8 +2382,11 @@ export function attach(
2382
2383 // We might meet a nested Suspense on our way.
2384 const isTimedOutSuspense =
2347 - fiber.tag === ReactTypeOfWork.SuspenseComponent &&
2348 - fiber.memoizedState !== null;
2385 + fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2386 +
2387 + if (fiber.tag === HostHoistable) {
2388 + releaseHostResource(fiber, fiber.memoizedState);
2389 + }
2390
2391 let child = fiber.child;
2392 if (isTimedOutSuspense) {
@@ -2621,6 +2662,12 @@ export function attach(
2662 const newParentInstance = shouldIncludeInTree
2663 ? fiberInstance
2664 : parentInstance;
2665 +
2666 + if (nextFiber.tag === HostHoistable) {
2667 + releaseHostResource(prevFiber, prevFiber.memoizedState);
2668 + aquireHostResource(nextFiber, nextFiber.memoizedState);
2669 + }
2670 +
2671 const isSuspense = nextFiber.tag === SuspenseComponent;
2672 let shouldResetChildren = false;
2673 // The behavior of timed-out Suspense trees is unique.
@@ -3070,9 +3117,55 @@ export function attach(
3117 function getNearestMountedHostInstance(
3118 hostInstance: HostInstance,
3119 ): null | HostInstance {
3073 - const mountedHostInstance = renderer.findFiberByHostInstance(hostInstance);
3074 - if (mountedHostInstance != null) {
3075 - return mountedHostInstance.stateNode;
3120 + const mountedFiber = renderer.findFiberByHostInstance(hostInstance);
3121 + if (mountedFiber != null) {
3122 + if (mountedFiber.stateNode !== hostInstance) {
3123 + // If it's not a perfect match the specific one might be a resource.
3124 + // We don't need to look at any parents because host resources don't have
3125 + // children so it won't be in any parent if it's not this one.
3126 + if (hostResourceToFiberMap.has(hostInstance)) {
3127 + return hostInstance;
3128 + }
3129 + }
3130 + return mountedFiber.stateNode;
3131 + }
3132 + if (hostResourceToFiberMap.has(hostInstance)) {
3133 + return hostInstance;
3134 + }
3135 + return null;
3136 + }
3137 +
3138 + function findNearestUnfilteredElementID(searchFiber: Fiber) {
3139 + let fiber: null | Fiber = searchFiber;
3140 + while (fiber !== null) {
3141 + const fiberInstance = getFiberInstanceUnsafe(fiber);
3142 + if (fiberInstance !== null) {
3143 + // TODO: Ideally we would not have any filtered FiberInstances which
3144 + // would make this logic much simpler. Unfortunately, we sometimes
3145 + // eagerly add to the map and some times don't eagerly clean it up.
3146 + // TODO: If the fiber is filtered, the FiberInstance wouldn't really
3147 + // exist which would mean that we also don't have a way to get to the
3148 + // VirtualInstances.
3149 + if (!shouldFilterFiber(fiberInstance.data)) {
3150 + return fiberInstance.id;
3151 + }
3152 + // We couldn't use this Fiber but we might have a VirtualInstance
3153 + // that is the nearest unfiltered instance.
3154 + let parentInstance = fiberInstance.parent;
3155 + while (parentInstance !== null) {
3156 + if (parentInstance.kind === FIBER_INSTANCE) {
3157 + // If we find a parent Fiber, it might not be the nearest parent
3158 + // so we break out and continue walking the Fiber tree instead.
3159 + break;
3160 + } else {
3161 + if (!shouldFilterVirtual(parentInstance.data)) {
3162 + return parentInstance.id;
3163 + }
3164 + }
3165 + parentInstance = parentInstance.parent;
3166 + }
3167 + }
3168 + fiber = fiber.return;
3169 }
3170 return null;
3171 }
@@ -3081,42 +3174,25 @@ export function attach(
3174 hostInstance: HostInstance,
3175 findNearestUnfilteredAncestor: boolean = false,
3176 ): number | null {
3084 - let fiber = renderer.findFiberByHostInstance(hostInstance);
3177 + const resourceFibers = hostResourceToFiberMap.get(hostInstance);
3178 + if (resourceFibers !== undefined) {
3179 + // This is a resource. Find the first unfiltered instance.
3180 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
3181 + for (const resourceFiber of resourceFibers) {
3182 + const elementID = findNearestUnfilteredElementID(resourceFiber);
3183 + if (elementID !== null) {
3184 + return elementID;
3185 + }
3186 + }
3187 + // If we don't find one, fallthrough to select the parent instead.
3188 + }
3189 + const fiber = renderer.findFiberByHostInstance(hostInstance);
3190 if (fiber != null) {
3191 if (!findNearestUnfilteredAncestor) {
3192 // TODO: Remove this option. It's not used.
3193 return getFiberIDThrows(fiber);
3194 }
3090 - while (fiber !== null) {
3091 - const fiberInstance = getFiberInstanceUnsafe(fiber);
3092 - if (fiberInstance !== null) {
3093 - // TODO: Ideally we would not have any filtered FiberInstances which
3094 - // would make this logic much simpler. Unfortunately, we sometimes
3095 - // eagerly add to the map and some times don't eagerly clean it up.
3096 - // TODO: If the fiber is filtered, the FiberInstance wouldn't really
3097 - // exist which would mean that we also don't have a way to get to the
3098 - // VirtualInstances.
3099 - if (!shouldFilterFiber(fiberInstance.data)) {
3100 - return fiberInstance.id;
3101 - }
3102 - // We couldn't use this Fiber but we might have a VirtualInstance
3103 - // that is the nearest unfiltered instance.
3104 - let parentInstance = fiberInstance.parent;
3105 - while (parentInstance !== null) {
3106 - if (parentInstance.kind === FIBER_INSTANCE) {
3107 - // If we find a parent Fiber, it might not be the nearest parent
3108 - // so we break out and continue walking the Fiber tree instead.
3109 - break;
3110 - } else {
3111 - if (!shouldFilterVirtual(parentInstance.data)) {
3112 - return parentInstance.id;
3113 - }
3114 - }
3115 - parentInstance = parentInstance.parent;
3116 - }
3117 - }
3118 - fiber = fiber.return;
3119 - }
3195 + return findNearestUnfilteredElementID(fiber);
3196 }
3197 return null;
3198 }