@samitouri / QOS-React / commits / 01ae2ddaa9

[DevTools] Include some Filtered Fiber Instances (#30865)

When we filter Fiber Instances where have no way to recover our position in the Fiber tree. The extreme form of this is if you filter out all the Fibers and keep only Server Components. This affects operations that are performed against fibers such as collecting Host Instances for highlighting or emulating suspending/erroring. Conceptually we don't need to add this into the DevToolsInstance tree because we only need to get to some Fibers from a VirtualInstance. A Virtual Instance can contain more than one conceptual child Fiber. It would be easier if we didn't include them in the tree on one hand because we could just traverse the tree and assume it looks like the one on the frontend. But it's also tricky to manage the lifetime. So I went with a special FilteredFiberInstance node in the tree. Currently I only add it if its parent would've been a VirtualInstance since we don't need it in any other cases. If the parent was another FiberInstance it already has a Fiber. There might be need for always tracking all Instances whether they're filtered or not or just moving filtering to the frontend but for now I'm keeping the general architecture as is.

Sebastian Markbåge committed Sep 4, 2024 at 19:35 UTC 01ae2ddaa94386b2250230004f1051218ada2de1
1 file changed +127 -27
packages/react-devtools-shared/src/backend/fiber/renderer.js
+127 -27
@@ -146,6 +146,7 @@ import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
146 // Kinds
147 const FIBER_INSTANCE = 0;
148 const VIRTUAL_INSTANCE = 1;
149 +const FILTERED_FIBER_INSTANCE = 2;
150
151 // Flags
152 const FORCE_SUSPENSE_FALLBACK = /* */ 0b001;
@@ -157,9 +158,9 @@ const FORCE_ERROR_RESET = /* */ 0b100;
158 type FiberInstance = {
159 kind: 0,
160 id: number,
160 - parent: null | DevToolsInstance, // filtered parent, including virtual
161 - firstChild: null | DevToolsInstance, // filtered first child, including virtual
162 - nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
161 + parent: null | DevToolsInstance,
162 + firstChild: null | DevToolsInstance,
163 + nextSibling: null | DevToolsInstance,
164 flags: number, // Force Error/Suspense
165 source: null | string | Error | Source, // source location of this component function, or owned child stack
166 errors: null | Map<string, number>, // error messages and count
@@ -184,6 +185,39 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
185 };
186 }
187
188 +type FilteredFiberInstance = {
189 + kind: 2,
190 + // We exclude id from the type to get errors if we try to access it.
191 + // However it is still in the object to preserve hidden class.
192 + // id: number,
193 + parent: null | DevToolsInstance,
194 + firstChild: null | DevToolsInstance,
195 + nextSibling: null | DevToolsInstance,
196 + flags: number, // Force Error/Suspense
197 + source: null | string | Error | Source, // always null here.
198 + errors: null, // error messages and count
199 + warnings: null, // warning messages and count
200 + treeBaseDuration: number, // the profiled time of the last render of this subtree
201 + data: Fiber, // one of a Fiber pair
202 +};
203 +
204 +// This is used to represent a filtered Fiber but still lets us find its host instance.
205 +function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
206 + return ({
207 + kind: FILTERED_FIBER_INSTANCE,
208 + id: 0,
209 + parent: null,
210 + firstChild: null,
211 + nextSibling: null,
212 + flags: 0,
213 + componentStack: null,
214 + errors: null,
215 + warnings: null,
216 + treeBaseDuration: 0,
217 + data: fiber,
218 + }: any);
219 +}
220 +
221 // This type represents a stateful instance of a Server Component or a Component
222 // that gets optimized away - e.g. call-through without creating a Fiber.
223 // It's basically a virtual Fiber. This is not a semantic concept in React.
@@ -192,9 +226,9 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
226 type VirtualInstance = {
227 kind: 1,
228 id: number,
195 - parent: null | DevToolsInstance, // filtered parent, including virtual
196 - firstChild: null | DevToolsInstance, // filtered first child, including virtual
197 - nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
229 + parent: null | DevToolsInstance,
230 + firstChild: null | DevToolsInstance,
231 + nextSibling: null | DevToolsInstance,
232 flags: number,
233 source: null | string | Error | Source, // source location of this server component, or owned child stack
234 // Errors and Warnings happen per ReactComponentInfo which can appear in
@@ -226,7 +260,7 @@ function createVirtualInstance(
260 };
261 }
262
229 -type DevToolsInstance = FiberInstance | VirtualInstance;
263 +type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance;
264
265 type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
266 type getTypeSymbolType = (type: any) => symbol | number;
@@ -739,7 +773,8 @@ const fiberToFiberInstanceMap: Map<Fiber, FiberInstance> = new Map();
773 // Map of id to one (arbitrary) Fiber in a pair.
774 // This Map is used to e.g. get the display name for a Fiber or schedule an update,
775 // operations that should be the same whether the current and work-in-progress Fiber is used.
742 -const idToDevToolsInstanceMap: Map<number, DevToolsInstance> = new Map();
776 +const idToDevToolsInstanceMap: Map<number, FiberInstance | VirtualInstance> =
777 + new Map();
778
779 // Map of canonical HostInstances to the nearest parent DevToolsInstance.
780 const publicInstanceToDevToolsInstanceMap: Map<HostInstance, DevToolsInstance> =
@@ -1144,13 +1179,22 @@ export function attach(
1179 function debugTree(instance: DevToolsInstance, indent: number = 0) {
1180 if (__DEBUG__) {
1181 const name =
1147 - (instance.kind === FIBER_INSTANCE
1182 + (instance.kind !== VIRTUAL_INSTANCE
1183 ? getDisplayNameForFiber(instance.data)
1184 : instance.data.name) || '';
1185 console.log(
1151 - ' '.repeat(indent) + '- ' + instance.id + ' (' + name + ')',
1186 + ' '.repeat(indent) +
1187 + '- ' +
1188 + (instance.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.id) +
1189 + ' (' +
1190 + name +
1191 + ')',
1192 'parent',
1153 - instance.parent === null ? ' ' : instance.parent.id,
1193 + instance.parent === null
1194 + ? ' '
1195 + : instance.parent.kind === FILTERED_FIBER_INSTANCE
1196 + ? 0
1197 + : instance.parent.id,
1198 'next',
1199 instance.nextSibling === null ? ' ' : instance.nextSibling.id,
1200 );
@@ -2263,7 +2307,12 @@ export function attach(
2307 ownerInstance.source = fiber._debugStack;
2308 }
2309 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
2266 - const parentID = parentInstance ? parentInstance.id : 0;
2310 + const parentID = parentInstance
2311 + ? parentInstance.kind === FILTERED_FIBER_INSTANCE
2312 + ? // A Filtered Fiber Instance will always have a Virtual Instance as a parent.
2313 + ((parentInstance.parent: any): VirtualInstance).id
2314 + : parentInstance.id
2315 + : 0;
2316
2317 const displayNameStringID = getStringID(displayName);
2318
@@ -2347,7 +2396,12 @@ export function attach(
2396 ownerInstance.source = componentInfo.debugStack;
2397 }
2398 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
2350 - const parentID = parentInstance ? parentInstance.id : 0;
2399 + const parentID = parentInstance
2400 + ? parentInstance.kind === FILTERED_FIBER_INSTANCE
2401 + ? // A Filtered Fiber Instance will always have a Virtual Instance as a parent.
2402 + ((parentInstance.parent: any): VirtualInstance).id
2403 + : parentInstance.id
2404 + : 0;
2405
2406 const displayNameStringID = getStringID(displayName);
2407
@@ -2712,6 +2766,25 @@ export function attach(
2766 if (shouldIncludeInTree) {
2767 newInstance = recordMount(fiber, reconcilingParent);
2768 insertChild(newInstance);
2769 + } else if (
2770 + reconcilingParent !== null &&
2771 + reconcilingParent.kind === VIRTUAL_INSTANCE
2772 + ) {
2773 + // If the parent is a Virtual Instance and we filtered this Fiber we include a
2774 + // hidden node.
2775 +
2776 + if (
2777 + reconcilingParent.data === fiber._debugOwner &&
2778 + fiber._debugStack != null &&
2779 + reconcilingParent.source === null
2780 + ) {
2781 + // The new Fiber is directly owned by the parent. Therefore somewhere on the
2782 + // debugStack will be a stack frame inside parent that we can use as its soruce.
2783 + reconcilingParent.source = fiber._debugStack;
2784 + }
2785 +
2786 + newInstance = createFilteredFiberInstance(fiber);
2787 + insertChild(newInstance);
2788 }
2789
2790 // If we have the tree selection from previous reload, try to match this Fiber.
@@ -2724,7 +2797,7 @@ export function attach(
2797 const stashedParent = reconcilingParent;
2798 const stashedPrevious = previouslyReconciledSibling;
2799 const stashedRemaining = remainingReconcilingChildren;
2727 - if (shouldIncludeInTree) {
2800 + if (newInstance !== null) {
2801 // Push a new DevTools instance parent while reconciling this subtree.
2802 reconcilingParent = newInstance;
2803 previouslyReconciledSibling = null;
@@ -2809,7 +2882,7 @@ export function attach(
2882 }
2883 }
2884 } finally {
2812 - if (shouldIncludeInTree) {
2885 + if (newInstance !== null) {
2886 reconcilingParent = stashedParent;
2887 previouslyReconciledSibling = stashedPrevious;
2888 remainingReconcilingChildren = stashedRemaining;
@@ -2849,8 +2922,10 @@ export function attach(
2922 }
2923 if (instance.kind === FIBER_INSTANCE) {
2924 recordUnmount(instance);
2852 - } else {
2925 + } else if (instance.kind === VIRTUAL_INSTANCE) {
2926 recordVirtualUnmount(instance);
2927 + } else {
2928 + untrackFiber(instance, instance.data);
2929 }
2930 removeChild(instance, null);
2931 }
@@ -2955,7 +3030,9 @@ export function attach(
3030 virtualInstance.treeBaseDuration = treeBaseDuration;
3031 }
3032
2958 - function recordResetChildren(parentInstance: DevToolsInstance) {
3033 + function recordResetChildren(
3034 + parentInstance: FiberInstance | VirtualInstance,
3035 + ) {
3036 if (__DEBUG__) {
3037 if (
3038 parentInstance.firstChild !== null &&
@@ -2975,7 +3052,17 @@ export function attach(
3052
3053 let child: null | DevToolsInstance = parentInstance.firstChild;
3054 while (child !== null) {
2978 - nextChildren.push(child.id);
3055 + if (child.kind === FILTERED_FIBER_INSTANCE) {
3056 + for (
3057 + let innerChild: null | DevToolsInstance = parentInstance.firstChild;
3058 + innerChild !== null;
3059 + innerChild = innerChild.nextSibling
3060 + ) {
3061 + nextChildren.push((innerChild: any).id);
3062 + }
3063 + } else {
3064 + nextChildren.push(child.id);
3065 + }
3066 child = child.nextSibling;
3067 }
3068
@@ -3791,7 +3878,7 @@ export function attach(
3878 devtoolsInstance: DevToolsInstance,
3879 hostInstances: Array<HostInstance>,
3880 ) {
3794 - if (devtoolsInstance.kind === FIBER_INSTANCE) {
3881 + if (devtoolsInstance.kind !== VIRTUAL_INSTANCE) {
3882 const fiber = devtoolsInstance.data;
3883 appendHostInstancesByFiber(fiber, hostInstances);
3884 return;
@@ -3892,6 +3979,10 @@ export function attach(
3979 ): number | null {
3980 const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance);
3981 if (instance !== undefined) {
3982 + if (instance.kind === FILTERED_FIBER_INSTANCE) {
3983 + // A Filtered Fiber Instance will always have a Virtual Instance as a parent.
3984 + return ((instance.parent: any): VirtualInstance).id;
3985 + }
3986 return instance.id;
3987 }
3988 return null;
@@ -3944,7 +4035,7 @@ export function attach(
4035 }
4036
4037 function instanceToSerializedElement(
3947 - instance: DevToolsInstance,
4038 + instance: FiberInstance | VirtualInstance,
4039 ): SerializedElement {
4040 if (instance.kind === FIBER_INSTANCE) {
4041 const fiber = instance.data;
@@ -4039,7 +4130,7 @@ export function attach(
4130 function findNearestOwnerInstance(
4131 parentInstance: null | DevToolsInstance,
4132 owner: void | null | ReactComponentInfo | Fiber,
4042 - ): null | DevToolsInstance {
4133 + ): null | FiberInstance | VirtualInstance {
4134 if (owner == null) {
4135 return null;
4136 }
@@ -4054,6 +4145,9 @@ export function attach(
4145 // needs a duck type check anyway.
4146 parentInstance.data === (owner: any).alternate
4147 ) {
4148 + if (parentInstance.kind === FILTERED_FIBER_INSTANCE) {
4149 + return null;
4150 + }
4151 return parentInstance;
4152 }
4153 parentInstance = parentInstance.parent;
@@ -4131,7 +4225,11 @@ export function attach(
4225 if (devtoolsInstance.kind === VIRTUAL_INSTANCE) {
4226 return inspectVirtualInstanceRaw(devtoolsInstance);
4227 }
4134 - return inspectFiberInstanceRaw(devtoolsInstance);
4228 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
4229 + return inspectFiberInstanceRaw(devtoolsInstance);
4230 + }
4231 + (devtoolsInstance: FilteredFiberInstance); // assert exhaustive
4232 + throw new Error('Unsupported instance kind');
4233 }
4234
4235 function inspectFiberInstanceRaw(
@@ -4434,7 +4532,7 @@ export function attach(
4532 let targetErrorBoundaryID = null;
4533 let parent = virtualInstance.parent;
4534 while (parent !== null) {
4437 - if (parent.kind === FIBER_INSTANCE) {
4535 + if (parent.kind !== VIRTUAL_INSTANCE) {
4536 targetErrorBoundaryID = getNearestErrorBoundaryID(parent.data);
4537 let current = parent.data;
4538 while (current.return !== null) {
@@ -5225,7 +5323,9 @@ export function attach(
5323 ) {
5324 // We don't need to convert milliseconds to microseconds in this case,
5325 // because the profiling summary is JSON serialized.
5228 - target.push([instance.id, instance.treeBaseDuration]);
5326 + if (instance.kind !== FILTERED_FIBER_INSTANCE) {
5327 + target.push([instance.id, instance.treeBaseDuration]);
5328 + }
5329 for (
5330 let child = instance.firstChild;
5331 child !== null;
@@ -5444,7 +5544,7 @@ export function attach(
5544 // In that case, we'll do some extra checks for matching mounts.
5545 let trackedPath: Array<PathFrame> | null = null;
5546 let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber.
5447 - let trackedPathMatchInstance: DevToolsInstance | null = null; // This is the deepest matched filtered Instance.
5547 + let trackedPathMatchInstance: FiberInstance | VirtualInstance | null = null; // This is the deepest matched filtered Instance.
5548 let trackedPathMatchDepth = -1;
5549 let mightBeOnTrackedPath = false;
5550
@@ -5463,7 +5563,7 @@ export function attach(
5563 // The return value signals whether we should keep matching siblings or not.
5564 function updateTrackedPathStateBeforeMount(
5565 fiber: Fiber,
5466 - fiberInstance: null | FiberInstance,
5566 + fiberInstance: null | FiberInstance | FilteredFiberInstance,
5567 ): boolean {
5568 if (trackedPath === null || !mightBeOnTrackedPath) {
5569 // Fast path: there's nothing to track so do nothing and ignore siblings.
@@ -5492,7 +5592,7 @@ export function attach(
5592 ) {
5593 // We have our next match.
5594 trackedPathMatchFiber = fiber;
5495 - if (fiberInstance !== null) {
5595 + if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
5596 trackedPathMatchInstance = fiberInstance;
5597 }
5598 trackedPathMatchDepth++;