@samitouri / QOS-React / commits / 25c584f567

[DevTools] Further Refactoring of Unmounts (#30658)

Stacked on #30625 and #30657. This ensures that we only create instances during the commit reconciliation and that we don't create unnecessary instances for things that are filtered or not mounted. This ensures that we also can rely on the reconciliation to do all the clean up. Now everything is created and deleted as a pair in the same pass. Previously we were including unfiltered components in the owner stack which probably doesn't make sense since you're intending to filter them everywhere presumably. However, it also means that those links were broken since you can't link into owners that don't exist in the parent tree. The main complication is the component filters. It relied on not unmounting the old instances. I had to update some tests that asserted on ids that are now shifted. For warnings/errors tracking I now restore them back into the pending set when they unmount. Basically it puts them back into their "pre-commit" state. That way when they remount they’re still there. For restoring the current selection I use the tracked path mechanism instead of relying on the id being unchanged. This is better anyway because if you filter out the currently selected item it's better to select the nearest match instead of just losing the selection.

Sebastian Markbåge committed Aug 12, 2024 at 12:41 UTC 25c584f5672d90ba67d72ea9ba9cc06b12311806
6 files changed +295 -226
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+137 -3
@@ -34,6 +34,8 @@ describe('InspectedElement', () => {
34 let SettingsContextController;
35 let StoreContext;
36 let TreeContextController;
37 + let TreeStateContext;
38 + let TreeDispatcherContext;
39
40 let TestUtilsAct;
41 let TestRendererAct;
@@ -73,6 +75,10 @@ describe('InspectedElement', () => {
75 require('react-devtools-shared/src/devtools/views/context').StoreContext;
76 TreeContextController =
77 require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController;
78 + TreeStateContext =
79 + require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeStateContext;
80 + TreeDispatcherContext =
81 + require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeDispatcherContext;
82
83 // Used by inspectElementAtIndex() helper function
84 utils.act(() => {
@@ -2142,7 +2148,7 @@ describe('InspectedElement', () => {
2148 "context": null,
2149 "events": undefined,
2150 "hooks": null,
2145 - "id": 2,
2151 + "id": 4,
2152 "owners": null,
2153 "props": {},
2154 "rootType": "createRoot()",
@@ -2893,7 +2899,7 @@ describe('InspectedElement', () => {
2899 "compiledWithForget": false,
2900 "displayName": "Child",
2901 "hocDisplayNames": null,
2896 - "id": 5,
2902 + "id": 8,
2903 "key": null,
2904 "type": 5,
2905 },
@@ -2901,7 +2907,7 @@ describe('InspectedElement', () => {
2907 "compiledWithForget": false,
2908 "displayName": "App",
2909 "hocDisplayNames": null,
2904 - "id": 4,
2910 + "id": 7,
2911 "key": null,
2912 "type": 5,
2913 },
@@ -3016,4 +3022,132 @@ describe('InspectedElement', () => {
3022 );
3023 });
3024 });
3025 +
3026 + it('should properly handle when components filters are updated', async () => {
3027 + const Wrapper = ({children}) => children;
3028 +
3029 + let state;
3030 + let dispatch;
3031 + const Capture = () => {
3032 + dispatch = React.useContext(TreeDispatcherContext);
3033 + state = React.useContext(TreeStateContext);
3034 + return null;
3035 + };
3036 +
3037 + function Child({logError = false, logWarning = false}) {
3038 + if (logError === true) {
3039 + console.error('test-only: error');
3040 + }
3041 + if (logWarning === true) {
3042 + console.warn('test-only: warning');
3043 + }
3044 + return null;
3045 + }
3046 +
3047 + async function selectNextErrorOrWarning() {
3048 + await utils.actAsync(
3049 + () =>
3050 + dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'}),
3051 + false,
3052 + );
3053 + }
3054 +
3055 + async function selectPreviousErrorOrWarning() {
3056 + await utils.actAsync(
3057 + () =>
3058 + dispatch({
3059 + type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
3060 + }),
3061 + false,
3062 + );
3063 + }
3064 +
3065 + withErrorsOrWarningsIgnored(['test-only:'], () =>
3066 + utils.act(() =>
3067 + render(
3068 + <React.Fragment>
3069 + <Wrapper>
3070 + <Child logWarning={true} />
3071 + </Wrapper>
3072 + <Wrapper>
3073 + <Wrapper>
3074 + <Child logWarning={true} />
3075 + </Wrapper>
3076 + </Wrapper>
3077 + </React.Fragment>,
3078 + ),
3079 + ),
3080 + );
3081 +
3082 + utils.act(() =>
3083 + TestRenderer.create(
3084 + <Contexts>
3085 + <Capture />
3086 + </Contexts>,
3087 + ),
3088 + );
3089 + expect(state).toMatchInlineSnapshot(`
3090 + ✕ 0, ⚠ 2
3091 + [root]
3092 + ▾ <Wrapper>
3093 + <Child> ⚠
3094 + ▾ <Wrapper>
3095 + ▾ <Wrapper>
3096 + <Child> ⚠
3097 + `);
3098 +
3099 + await selectNextErrorOrWarning();
3100 + expect(state).toMatchInlineSnapshot(`
3101 + ✕ 0, ⚠ 2
3102 + [root]
3103 + ▾ <Wrapper>
3104 + → <Child> ⚠
3105 + ▾ <Wrapper>
3106 + ▾ <Wrapper>
3107 + <Child> ⚠
3108 + `);
3109 +
3110 + await utils.actAsync(() => {
3111 + store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
3112 + }, false);
3113 +
3114 + expect(state).toMatchInlineSnapshot(`
3115 + ✕ 0, ⚠ 2
3116 + [root]
3117 + → <Child> ⚠
3118 + <Child> ⚠
3119 + `);
3120 +
3121 + await selectNextErrorOrWarning();
3122 + expect(state).toMatchInlineSnapshot(`
3123 + ✕ 0, ⚠ 2
3124 + [root]
3125 + <Child> ⚠
3126 + → <Child> ⚠
3127 + `);
3128 +
3129 + await utils.actAsync(() => {
3130 + store.componentFilters = [];
3131 + }, false);
3132 + expect(state).toMatchInlineSnapshot(`
3133 + ✕ 0, ⚠ 2
3134 + [root]
3135 + ▾ <Wrapper>
3136 + <Child> ⚠
3137 + ▾ <Wrapper>
3138 + ▾ <Wrapper>
3139 + → <Child> ⚠
3140 + `);
3141 +
3142 + await selectPreviousErrorOrWarning();
3143 + expect(state).toMatchInlineSnapshot(`
3144 + ✕ 0, ⚠ 2
3145 + [root]
3146 + ▾ <Wrapper>
3147 + → <Child> ⚠
3148 + ▾ <Wrapper>
3149 + ▾ <Wrapper>
3150 + <Child> ⚠
3151 + `);
3152 + });
3153 });
packages/react-devtools-shared/src/__tests__/ownersListContext-test.js
-1
@@ -156,7 +156,6 @@ describe('OwnersListContext', () => {
156 expect(await getOwnersListForOwner(firstChild)).toMatchInlineSnapshot(`
157 [
158 "Grandparent",
159 - "Parent",
159 "Child",
160 ]
161 `);
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+4 -4
@@ -1251,8 +1251,8 @@ describe('ProfilingCache', () => {
1251 Map {
1252 1 => 16,
1253 2 => 16,
1254 + 3 => 1,
1255 4 => 1,
1255 - 6 => 1,
1256 }
1257 `);
1258
@@ -1260,8 +1260,8 @@ describe('ProfilingCache', () => {
1260 Map {
1261 1 => 0,
1262 2 => 10,
1263 + 3 => 1,
1264 4 => 1,
1264 - 6 => 1,
1265 }
1266 `);
1267 });
@@ -1322,13 +1322,13 @@ describe('ProfilingCache', () => {
1322 `);
1323 expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
1324 Map {
1325 - 7 => 3,
1325 + 5 => 3,
1326 3 => 3,
1327 }
1328 `);
1329 expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
1330 Map {
1331 - 7 => 3,
1331 + 5 => 3,
1332 3 => 0,
1333 }
1334 `);
packages/react-devtools-shared/src/__tests__/treeContext-test.js
-85
@@ -2286,91 +2286,6 @@ describe('TreeListContext', () => {
2286 `);
2287 });
2288
2289 - it('should properly handle when components filters are updated', () => {
2290 - const Wrapper = ({children}) => children;
2291 -
2292 - withErrorsOrWarningsIgnored(['test-only:'], () =>
2293 - utils.act(() =>
2294 - render(
2295 - <React.Fragment>
2296 - <Wrapper>
2297 - <Child logWarning={true} />
2298 - </Wrapper>
2299 - <Wrapper>
2300 - <Wrapper>
2301 - <Child logWarning={true} />
2302 - </Wrapper>
2303 - </Wrapper>
2304 - </React.Fragment>,
2305 - ),
2306 - ),
2307 - );
2308 -
2309 - utils.act(() => TestRenderer.create(<Contexts />));
2310 - expect(state).toMatchInlineSnapshot(`
2311 - ✕ 0, ⚠ 2
2312 - [root]
2313 - ▾ <Wrapper>
2314 - <Child> ⚠
2315 - ▾ <Wrapper>
2316 - ▾ <Wrapper>
2317 - <Child> ⚠
2318 - `);
2319 -
2320 - selectNextErrorOrWarning();
2321 - expect(state).toMatchInlineSnapshot(`
2322 - ✕ 0, ⚠ 2
2323 - [root]
2324 - ▾ <Wrapper>
2325 - → <Child> ⚠
2326 - ▾ <Wrapper>
2327 - ▾ <Wrapper>
2328 - <Child> ⚠
2329 - `);
2330 -
2331 - utils.act(() => {
2332 - store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
2333 - });
2334 - expect(state).toMatchInlineSnapshot(`
2335 - ✕ 0, ⚠ 2
2336 - [root]
2337 - → <Child> ⚠
2338 - <Child> ⚠
2339 - `);
2340 -
2341 - selectNextErrorOrWarning();
2342 - expect(state).toMatchInlineSnapshot(`
2343 - ✕ 0, ⚠ 2
2344 - [root]
2345 - <Child> ⚠
2346 - → <Child> ⚠
2347 - `);
2348 -
2349 - utils.act(() => {
2350 - store.componentFilters = [];
2351 - });
2352 - expect(state).toMatchInlineSnapshot(`
2353 - ✕ 0, ⚠ 2
2354 - [root]
2355 - ▾ <Wrapper>
2356 - <Child> ⚠
2357 - ▾ <Wrapper>
2358 - ▾ <Wrapper>
2359 - → <Child> ⚠
2360 - `);
2361 -
2362 - selectPreviousErrorOrWarning();
2363 - expect(state).toMatchInlineSnapshot(`
2364 - ✕ 0, ⚠ 2
2365 - [root]
2366 - ▾ <Wrapper>
2367 - → <Child> ⚠
2368 - ▾ <Wrapper>
2369 - ▾ <Wrapper>
2370 - <Child> ⚠
2371 - `);
2372 - });
2373 -
2289 it('should preserve errors for fibers even if they are filtered out of the tree initially', () => {
2290 const Wrapper = ({children}) => children;
2291
packages/react-devtools-shared/src/backend/agent.js
+14 -1
@@ -795,10 +795,23 @@ export default class Agent extends EventEmitter<{
795
796 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
797 componentFilters => {
798 - for (const rendererID in this._rendererInterfaces) {
798 + for (const rendererIDString in this._rendererInterfaces) {
799 + const rendererID = +rendererIDString;
800 const renderer = ((this._rendererInterfaces[
801 (rendererID: any)
802 ]: any): RendererInterface);
803 + if (this._lastSelectedRendererID === rendererID) {
804 + // Changing component filters will unmount and remount the DevTools tree.
805 + // Track the last selection's path so we can restore the selection.
806 + const path = renderer.getPathForElement(this._lastSelectedElementID);
807 + if (path !== null) {
808 + renderer.setTrackedPath(path);
809 + this._persistedSelection = {
810 + rendererID,
811 + path,
812 + };
813 + }
814 + }
815 renderer.updateComponentFilters(componentFilters);
816 }
817 };
packages/react-devtools-shared/src/backend/fiber/renderer.js
+140 -132
@@ -63,7 +63,6 @@ import {
63 SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
64 TREE_OPERATION_ADD,
65 TREE_OPERATION_REMOVE,
66 - TREE_OPERATION_REMOVE_ROOT,
66 TREE_OPERATION_REORDER_CHILDREN,
67 TREE_OPERATION_SET_SUBTREE_MODE,
68 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
@@ -856,8 +855,14 @@ export function attach(
855 // (due to e.g. Suspense or error boundaries).
856 // onErrorOrWarning() adds Fibers and recordPendingErrorsAndWarnings() later clears them.
857 const fibersWithChangedErrorOrWarningCounts: Set<Fiber> = new Set();
859 - const pendingFiberToErrorsMap: Map<Fiber, Map<string, number>> = new Map();
860 - const pendingFiberToWarningsMap: Map<Fiber, Map<string, number>> = new Map();
858 + const pendingFiberToErrorsMap: WeakMap<
859 + Fiber,
860 + Map<string, number>,
861 + > = new WeakMap();
862 + const pendingFiberToWarningsMap: WeakMap<
863 + Fiber,
864 + Map<string, number>,
865 + > = new WeakMap();
866
867 function clearErrorsAndWarnings() {
868 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
@@ -876,7 +881,7 @@ export function attach(
881
882 function clearMessageCountHelper(
883 instanceID: number,
879 - pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
884 + pendingFiberToMessageCountMap: WeakMap<Fiber, Map<string, number>>,
885 forError: boolean,
886 ) {
887 const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
@@ -902,7 +907,7 @@ export function attach(
907 if (devtoolsInstance.kind === FIBER_INSTANCE) {
908 const fiber = devtoolsInstance.data;
909 // Throw out any pending changes.
905 - pendingFiberToErrorsMap.delete(fiber);
910 + pendingFiberToMessageCountMap.delete(fiber);
911
912 if (changed) {
913 // If previous flushed counts have changed, schedule an update too.
@@ -1143,11 +1148,8 @@ export function attach(
1148
1149 // Recursively unmount all roots.
1150 hook.getFiberRoots(rendererID).forEach(root => {
1146 - currentRootID = getOrGenerateFiberInstance(root.current).id;
1147 - // The TREE_OPERATION_REMOVE_ROOT operation serves two purposes:
1148 - // 1. It avoids sending unnecessary bridge traffic to clear a root.
1149 - // 2. It preserves Fiber IDs when remounting (below) which in turn ID to error/warning mapping.
1150 - pushOperation(TREE_OPERATION_REMOVE_ROOT);
1151 + currentRootID = getFiberInstanceThrows(root.current).id;
1152 + unmountFiberRecursively(root.current);
1153 flushPendingEvents(root);
1154 currentRootID = -1;
1155 });
@@ -1159,7 +1161,22 @@ export function attach(
1161
1162 // Recursively re-mount all roots with new filter criteria applied.
1163 hook.getFiberRoots(rendererID).forEach(root => {
1162 - currentRootID = getOrGenerateFiberInstance(root.current).id;
1164 + const current = root.current;
1165 + const alternate = current.alternate;
1166 + const newRoot = createFiberInstance(current);
1167 + idToDevToolsInstanceMap.set(newRoot.id, newRoot);
1168 + fiberToFiberInstanceMap.set(current, newRoot);
1169 + if (alternate) {
1170 + fiberToFiberInstanceMap.set(alternate, newRoot);
1171 + }
1172 +
1173 + // Before the traversals, remember to start tracking
1174 + // our path in case we have selection to restore.
1175 + if (trackedPath !== null) {
1176 + mightBeOnTrackedPath = true;
1177 + }
1178 +
1179 + currentRootID = newRoot.id;
1180 setRootPseudoKey(currentRootID, root.current);
1181 mountFiberRecursively(root.current, false);
1182 flushPendingEvents(root);
@@ -1322,43 +1339,6 @@ export function attach(
1339 // When a mount or update is in progress, this value tracks the root that is being operated on.
1340 let currentRootID: number = -1;
1341
1325 - // Returns the unique ID for a Fiber or generates and caches a new one if the Fiber hasn't been seen before.
1326 - // Once this method has been called for a Fiber, untrackFiberID() should always be called later to avoid leaking.
1327 - function getOrGenerateFiberInstance(fiber: Fiber): FiberInstance {
1328 - let fiberInstance = fiberToFiberInstanceMap.get(fiber);
1329 - if (fiberInstance === undefined) {
1330 - const {alternate} = fiber;
1331 - if (alternate !== null) {
1332 - fiberInstance = fiberToFiberInstanceMap.get(alternate);
1333 - if (fiberInstance !== undefined) {
1334 - // We found the other pair, so we need to make sure we track the other side.
1335 - fiberToFiberInstanceMap.set(fiber, fiberInstance);
1336 - }
1337 - }
1338 - }
1339 -
1340 - let didGenerateID = false;
1341 - if (fiberInstance === undefined) {
1342 - didGenerateID = true;
1343 - fiberInstance = createFiberInstance(fiber);
1344 - fiberToFiberInstanceMap.set(fiber, fiberInstance);
1345 - idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
1346 - }
1347 -
1348 - if (__DEBUG__) {
1349 - if (didGenerateID) {
1350 - debug(
1351 - 'getOrGenerateFiberInstance()',
1352 - fiber,
1353 - fiberInstance.parent,
1354 - 'Generated a new UID',
1355 - );
1356 - }
1357 - }
1358 -
1359 - return fiberInstance;
1360 - }
1361 -
1342 // Returns a FiberInstance if one has already been generated for the Fiber or throws.
1343 function getFiberInstanceThrows(fiber: Fiber): FiberInstance {
1344 const fiberInstance = getFiberInstanceUnsafe(fiber);
@@ -1412,9 +1392,21 @@ export function attach(
1392
1393 idToDevToolsInstanceMap.delete(fiberInstance.id);
1394
1415 - // Also clear any errors/warnings associated with this fiber.
1416 - clearErrorsForElementID(fiberInstance.id);
1417 - clearWarningsForElementID(fiberInstance.id);
1395 + const fiber = fiberInstance.data;
1396 +
1397 + // Restore any errors/warnings associated with this fiber to the pending
1398 + // map. I.e. treat it as before we tracked the instances. This lets us
1399 + // restore them if we remount the same Fibers later. Otherwise we rely
1400 + // on the GC of the Fibers to clean them up.
1401 + if (fiberInstance.errors !== null) {
1402 + pendingFiberToErrorsMap.set(fiber, fiberInstance.errors);
1403 + fiberInstance.errors = null;
1404 + }
1405 + if (fiberInstance.warnings !== null) {
1406 + pendingFiberToWarningsMap.set(fiber, fiberInstance.warnings);
1407 + fiberInstance.warnings = null;
1408 + }
1409 +
1410 if (fiberInstance.flags & FORCE_ERROR) {
1411 fiberInstance.flags &= ~FORCE_ERROR;
1412 forceErrorCount--;
@@ -1430,7 +1422,6 @@ export function attach(
1422 }
1423 }
1424
1433 - const fiber = fiberInstance.data;
1425 fiberToFiberInstanceMap.delete(fiber);
1426 const {alternate} = fiber;
1427 if (alternate !== null) {
@@ -1745,7 +1736,6 @@ export function attach(
1736
1737 const pendingOperations: OperationsArray = [];
1738 const pendingRealUnmountedIDs: Array<number> = [];
1748 - const pendingSimulatedUnmountedIDs: Array<number> = [];
1739 let pendingOperationsQueue: Array<OperationsArray> | null = [];
1740 const pendingStringTable: Map<string, StringTableEntry> = new Map();
1741 let pendingStringTableLength: number = 0;
@@ -1776,7 +1766,6 @@ export function attach(
1766 return (
1767 pendingOperations.length === 0 &&
1768 pendingRealUnmountedIDs.length === 0 &&
1779 - pendingSimulatedUnmountedIDs.length === 0 &&
1769 pendingUnmountedRootID === null
1770 );
1771 }
@@ -1856,7 +1845,7 @@ export function attach(
1845 function mergeMapsAndGetCountHelper(
1846 fiber: Fiber,
1847 fiberID: number,
1859 - pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
1848 + pendingFiberToMessageCountMap: WeakMap<Fiber, Map<string, number>>,
1849 forError: boolean,
1850 ): number {
1851 let newCount = 0;
@@ -1932,18 +1921,18 @@ export function attach(
1921 pushOperation(fiberID);
1922 pushOperation(errorCount);
1923 pushOperation(warningCount);
1935 - }
1924
1937 - // Always clean up so that we don't leak.
1938 - pendingFiberToErrorsMap.delete(fiber);
1939 - pendingFiberToWarningsMap.delete(fiber);
1925 + // Only clear the ones that we've already shown. Leave others in case
1926 + // they mount later.
1927 + pendingFiberToErrorsMap.delete(fiber);
1928 + pendingFiberToWarningsMap.delete(fiber);
1929 + }
1930 });
1931 fibersWithChangedErrorOrWarningCounts.clear();
1932 }
1933
1934 function flushPendingEvents(root: Object): void {
1935 // Add any pending errors and warnings to the operations array.
1946 - // We do this just before flushing, so we can ignore errors for no-longer-mounted Fibers.
1936 recordPendingErrorsAndWarnings();
1937
1938 if (shouldBailoutWithPendingOperations()) {
@@ -1960,7 +1949,6 @@ export function attach(
1949
1950 const numUnmountIDs =
1951 pendingRealUnmountedIDs.length +
1963 - pendingSimulatedUnmountedIDs.length +
1952 (pendingUnmountedRootID === null ? 0 : 1);
1953
1954 const operations = new Array<number>(
@@ -2013,15 +2001,6 @@ export function attach(
2001 for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
2002 operations[i++] = pendingRealUnmountedIDs[j];
2003 }
2016 - // Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
2017 - // (We want children to go before parents.)
2018 - // They go *after* the real unmounts because we know for sure they won't be
2019 - // children of already pushed "real" IDs. If they were, we wouldn't be able
2020 - // to discover them during the traversal, as they would have been deleted.
2021 - for (let j = 0; j < pendingSimulatedUnmountedIDs.length; j++) {
2022 - operations[i + j] = pendingSimulatedUnmountedIDs[j];
2023 - }
2024 - i += pendingSimulatedUnmountedIDs.length;
2004 // The root ID should always be unmounted last.
2005 if (pendingUnmountedRootID !== null) {
2006 operations[i] = pendingUnmountedRootID;
@@ -2040,7 +2019,6 @@ export function attach(
2019 // Reset all of the pending state now that we've told the frontend about it.
2020 pendingOperations.length = 0;
2021 pendingRealUnmountedIDs.length = 0;
2043 - pendingSimulatedUnmountedIDs.length = 0;
2022 pendingUnmountedRootID = null;
2023 pendingStringTable.clear();
2024 pendingStringTableLength = 0;
@@ -2078,7 +2056,24 @@ export function attach(
2056 parentInstance: DevToolsInstance | null,
2057 ): FiberInstance {
2058 const isRoot = fiber.tag === HostRoot;
2081 - const fiberInstance = getOrGenerateFiberInstance(fiber);
2059 + let fiberInstance;
2060 + if (isRoot) {
2061 + const entry = fiberToFiberInstanceMap.get(fiber);
2062 + if (entry === undefined) {
2063 + throw new Error('The root should have been registered at this point');
2064 + }
2065 + fiberInstance = entry;
2066 + } else if (
2067 + fiberToFiberInstanceMap.has(fiber) ||
2068 + (fiber.alternate !== null && fiberToFiberInstanceMap.has(fiber.alternate))
2069 + ) {
2070 + throw new Error('Did not expect to see this fiber being mounted twice.');
2071 + } else {
2072 + fiberInstance = createFiberInstance(fiber);
2073 + }
2074 + fiberToFiberInstanceMap.set(fiber, fiberInstance);
2075 + idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
2076 +
2077 const id = fiberInstance.id;
2078
2079 if (__DEBUG__) {
@@ -2131,7 +2126,12 @@ export function attach(
2126 let ownerID: number;
2127 if (debugOwner != null) {
2128 if (typeof debugOwner.tag === 'number') {
2134 - ownerID = getOrGenerateFiberInstance((debugOwner: any)).id;
2129 + const ownerFiberInstance = getFiberInstanceUnsafe((debugOwner: any));
2130 + if (ownerFiberInstance !== null) {
2131 + ownerID = ownerFiberInstance.id;
2132 + } else {
2133 + ownerID = 0;
2134 + }
2135 } else {
2136 // TODO: Track Server Component Owners.
2137 ownerID = 0;
@@ -2183,17 +2183,9 @@ export function attach(
2183 return fiberInstance;
2184 }
2185
2186 - function recordUnmount(
2187 - fiber: Fiber,
2188 - isSimulated: boolean,
2189 - ): null | FiberInstance {
2186 + function recordUnmount(fiber: Fiber): null | FiberInstance {
2187 if (__DEBUG__) {
2191 - debug(
2192 - 'recordUnmount()',
2193 - fiber,
2194 - null,
2195 - isSimulated ? 'unmount is simulated' : '',
2196 - );
2188 + debug('recordUnmount()', fiber, null);
2189 }
2190
2191 if (trackedPathMatchFiber !== null) {
@@ -2223,28 +2215,22 @@ export function attach(
2215 const id = fiberInstance.id;
2216 const isRoot = fiber.tag === HostRoot;
2217 if (isRoot) {
2226 - // Roots must be removed only after all children (pending and simulated) have been removed.
2218 + // Roots must be removed only after all children have been removed.
2219 // So we track it separately.
2220 pendingUnmountedRootID = id;
2221 } else if (!shouldFilterFiber(fiber)) {
2222 // To maintain child-first ordering,
2223 // we'll push it into one of these queues,
2224 // and later arrange them in the correct order.
2233 - if (isSimulated) {
2234 - pendingSimulatedUnmountedIDs.push(id);
2235 - } else {
2236 - pendingRealUnmountedIDs.push(id);
2237 - }
2225 + pendingRealUnmountedIDs.push(id);
2226 }
2227
2240 - if (!fiber._debugNeedsRemount) {
2241 - untrackFiber(fiberInstance);
2228 + untrackFiber(fiberInstance);
2229
2243 - const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
2244 - if (isProfilingSupported) {
2245 - idToRootMap.delete(id);
2246 - idToTreeBaseDurationMap.delete(id);
2247 - }
2230 + const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
2231 + if (isProfilingSupported) {
2232 + idToRootMap.delete(id);
2233 + idToTreeBaseDurationMap.delete(id);
2234 }
2235 return fiberInstance;
2236 }
@@ -2323,7 +2309,7 @@ export function attach(
2309 let child = remainingReconcilingChildren;
2310 while (child !== null) {
2311 if (child.kind === FIBER_INSTANCE) {
2326 - unmountFiberRecursively(child.data, false);
2312 + unmountFiberRecursively(child.data);
2313 }
2314 removeChild(child);
2315 child = remainingReconcilingChildren;
@@ -2347,10 +2333,6 @@ export function attach(
2333 fiber: Fiber,
2334 traceNearestHostComponentUpdate: boolean,
2335 ): void {
2350 - // Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling).
2351 - // TODO: Do we really need to do this eagerly?
2352 - getOrGenerateFiberInstance(fiber);
2353 -
2336 if (__DEBUG__) {
2337 debug('mountFiberRecursively()', fiber, reconcilingParent);
2338 }
@@ -2452,7 +2434,7 @@ export function attach(
2434
2435 // We use this to simulate unmounting for Suspense trees
2436 // when we switch from primary to fallback, or deleting a subtree.
2455 - function unmountFiberRecursively(fiber: Fiber, isSimulated: boolean) {
2437 + function unmountFiberRecursively(fiber: Fiber) {
2438 if (__DEBUG__) {
2439 debug('unmountFiberRecursively()', fiber, null);
2440 }
@@ -2493,7 +2475,7 @@ export function attach(
2475 child = fallbackChildFragment ? fallbackChildFragment.child : null;
2476 }
2477
2496 - unmountChildrenRecursively(child, isSimulated);
2478 + unmountChildrenRecursively(child);
2479 } finally {
2480 if (shouldIncludeInTree) {
2481 reconcilingParent = stashedParent;
@@ -2502,20 +2484,17 @@ export function attach(
2484 }
2485 }
2486 if (fiberInstance !== null) {
2505 - recordUnmount(fiber, isSimulated);
2487 + recordUnmount(fiber);
2488 removeChild(fiberInstance);
2489 }
2490 }
2491
2510 - function unmountChildrenRecursively(
2511 - firstChild: null | Fiber,
2512 - isSimulated: boolean,
2513 - ) {
2492 + function unmountChildrenRecursively(firstChild: null | Fiber) {
2493 let child: null | Fiber = firstChild;
2494 while (child !== null) {
2495 // Record simulated unmounts children-first.
2496 // We skip nodes without return because those are real unmounts.
2518 - unmountFiberRecursively(child, isSimulated);
2497 + unmountFiberRecursively(child);
2498 child = child.sibling;
2499 }
2500 }
@@ -2725,10 +2704,6 @@ export function attach(
2704 prevFiber: Fiber,
2705 traceNearestHostComponentUpdate: boolean,
2706 ): boolean {
2728 - // TODO: Do we really need to give this an instance eagerly if it's filtered?
2729 - const fiberInstance = getOrGenerateFiberInstance(nextFiber);
2730 - const id = fiberInstance.id;
2731 -
2707 if (__DEBUG__) {
2708 debug('updateFiberRecursively()', nextFiber, reconcilingParent);
2709 }
@@ -2758,26 +2733,38 @@ export function attach(
2733 }
2734 }
2735
2761 - if (
2762 - mostRecentlyInspectedElement !== null &&
2763 - mostRecentlyInspectedElement.id === id &&
2764 - didFiberRender(prevFiber, nextFiber)
2765 - ) {
2766 - // If this Fiber has updated, clear cached inspected data.
2767 - // If it is inspected again, it may need to be re-run to obtain updated hooks values.
2768 - hasElementUpdatedSinceLastInspected = true;
2769 - }
2770 -
2736 + let fiberInstance: null | FiberInstance = null;
2737 const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
2738 if (shouldIncludeInTree) {
2739 + const entry = fiberToFiberInstanceMap.get(prevFiber);
2740 + if (entry === undefined) {
2741 + throw new Error(
2742 + 'The previous version of the fiber should have already been registered.',
2743 + );
2744 + }
2745 + fiberInstance = entry;
2746 + // Register the new alternate in case it's not already in.
2747 + fiberToFiberInstanceMap.set(nextFiber, fiberInstance);
2748 +
2749 // Update the Fiber so we that we always keep the current Fiber on the data.
2750 fiberInstance.data = nextFiber;
2751 moveChild(fiberInstance);
2752 +
2753 + if (
2754 + mostRecentlyInspectedElement !== null &&
2755 + mostRecentlyInspectedElement.id === fiberInstance.id &&
2756 + didFiberRender(prevFiber, nextFiber)
2757 + ) {
2758 + // If this Fiber has updated, clear cached inspected data.
2759 + // If it is inspected again, it may need to be re-run to obtain updated hooks values.
2760 + hasElementUpdatedSinceLastInspected = true;
2761 + }
2762 }
2763 +
2764 const stashedParent = reconcilingParent;
2765 const stashedPrevious = previouslyReconciledSibling;
2766 const stashedRemaining = remainingReconcilingChildren;
2780 - if (shouldIncludeInTree) {
2767 + if (fiberInstance !== null) {
2768 // Push a new DevTools instance parent while reconciling this subtree.
2769 reconcilingParent = fiberInstance;
2770 previouslyReconciledSibling = null;
@@ -2856,9 +2843,7 @@ export function attach(
2843 } else if (!prevDidTimeout && nextDidTimeOut) {
2844 // Primary -> Fallback:
2845 // 1. Hide primary set
2859 - // This is not a real unmount, so it won't get reported by React.
2860 - // We need to manually walk the previous tree and record unmounts.
2861 - unmountChildrenRecursively(prevFiber.child, true);
2846 + unmountChildrenRecursively(prevFiber.child);
2847 // 2. Mount fallback set
2848 const nextFiberChild = nextFiber.child;
2849 const nextFallbackChildSet = nextFiberChild
@@ -2886,7 +2871,7 @@ export function attach(
2871 }
2872 } else {
2873 // Children are unchanged.
2889 - if (shouldIncludeInTree) {
2874 + if (fiberInstance !== null) {
2875 // All the remaining children will be children of this same fiber so we can just reuse them.
2876 // I.e. we just restore them by undoing what we did above.
2877 fiberInstance.firstChild = remainingReconcilingChildren;
@@ -3003,7 +2988,15 @@ export function attach(
2988 }
2989 // If we have not been profiling, then we can just walk the tree and build up its current state as-is.
2990 hook.getFiberRoots(rendererID).forEach(root => {
3006 - currentRootID = getOrGenerateFiberInstance(root.current).id;
2991 + const current = root.current;
2992 + const alternate = current.alternate;
2993 + const newRoot = createFiberInstance(current);
2994 + idToDevToolsInstanceMap.set(newRoot.id, newRoot);
2995 + fiberToFiberInstanceMap.set(current, newRoot);
2996 + if (alternate) {
2997 + fiberToFiberInstanceMap.set(alternate, newRoot);
2998 + }
2999 + currentRootID = newRoot.id;
3000 setRootPseudoKey(currentRootID, root.current);
3001
3002 // Handle multi-renderer edge-case where only some v16 renderers support profiling.
@@ -3060,7 +3053,20 @@ export function attach(
3053 const current = root.current;
3054 const alternate = current.alternate;
3055
3063 - currentRootID = getOrGenerateFiberInstance(current).id;
3056 + const existingRoot =
3057 + fiberToFiberInstanceMap.get(current) ||
3058 + (alternate && fiberToFiberInstanceMap.get(alternate));
3059 + if (!existingRoot) {
3060 + const newRoot = createFiberInstance(current);
3061 + idToDevToolsInstanceMap.set(newRoot.id, newRoot);
3062 + fiberToFiberInstanceMap.set(current, newRoot);
3063 + if (alternate) {
3064 + fiberToFiberInstanceMap.set(alternate, newRoot);
3065 + }
3066 + currentRootID = newRoot.id;
3067 + } else {
3068 + currentRootID = existingRoot.id;
3069 + }
3070
3071 // Before the traversals, remember to start tracking
3072 // our path in case we have selection to restore.
@@ -3117,7 +3123,7 @@ export function attach(
3123 } else if (wasMounted && !isMounted) {
3124 // Unmount an existing root.
3125 removeRootPseudoKey(currentRootID);
3120 - unmountFiberRecursively(alternate, false);
3126 + unmountFiberRecursively(alternate);
3127 }
3128 } else {
3129 // Mount a new root.
@@ -3617,7 +3623,9 @@ export function attach(
3623 while (owner != null) {
3624 if (typeof owner.tag === 'number') {
3625 const ownerFiber: Fiber = (owner: any); // Refined
3620 - owners.unshift(fiberToSerializedElement(ownerFiber));
3626 + if (!shouldFilterFiber(ownerFiber)) {
3627 + owners.unshift(fiberToSerializedElement(ownerFiber));
3628 + }
3629 owner = ownerFiber._debugOwner;
3630 } else {
3631 // TODO: Track Server Component Owners.