@samitouri / QOS-React-2 / commits / 2a879cdc95

[DevTools] Fix broken commit tree builder for initial operations (#35710)

Sebastian "Sebbie" Silbermann committed Feb 6, 2026 at 15:16 UTC 2a879cdc95228b1b3b4cdc81cfc04599716b5562
5 files changed +28 -66
packages/react-devtools-shared/src/backend/fiber/renderer.js
+27 -14
@@ -1138,7 +1138,7 @@ export function attach(
1138 // if any passive effects called console.warn / console.error.
1139 let needsToFlushComponentLogs = false;
1140
1141 - function bruteForceFlushErrorsAndWarnings() {
1141 + function bruteForceFlushErrorsAndWarnings(root: FiberInstance) {
1142 // Refresh error/warning count for all mounted unfiltered Fibers.
1143 let hasChanges = false;
1144 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
@@ -1156,7 +1156,7 @@ export function attach(
1156 }
1157 }
1158 if (hasChanges) {
1159 - flushPendingEvents();
1159 + flushPendingEvents(root);
1160 }
1161 }
1162
@@ -1183,7 +1183,7 @@ export function attach(
1183 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1184 }
1185 }
1186 - flushPendingEvents();
1186 + flushPendingEvents(null);
1187 }
1188
1189 function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {
@@ -1211,7 +1211,7 @@ export function attach(
1211 }
1212 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
1213 if (changed) {
1214 - flushPendingEvents();
1214 + flushPendingEvents(null);
1215 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1216 }
1217 }
@@ -1533,6 +1533,8 @@ export function attach(
1533 if (isProfiling) {
1534 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
1535 // If necessary, we could support this- but it doesn't seem like a necessary use case.
1536 + // Supporting change of filters while profiling would require a refactor
1537 + // to flush after each root instead of at the end.
1538 throw Error('Cannot modify filter preferences while profiling');
1539 }
1540
@@ -1647,7 +1649,8 @@ export function attach(
1649 focusedActivityFilter.activityID = focusedActivityID;
1650 }
1651
1650 - flushPendingEvents();
1652 + // We're not profiling so it's safe to flush without a specific root.
1653 + flushPendingEvents(null);
1654
1655 needsToFlushComponentLogs = false;
1656 }
@@ -2203,7 +2206,12 @@ export function attach(
2206 }
2207 }
2208
2206 - function flushPendingEvents(): void {
2209 + /**
2210 + * Allowed to flush pending events without a specific root when:
2211 + * - pending operations don't record tree mutations e.g. TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS
2212 + * - not profiling (the commit tree builder requires the root of the mutations)
2213 + */
2214 + function flushPendingEvents(root: FiberInstance | null): void {
2215 if (shouldBailoutWithPendingOperations()) {
2216 // If we aren't profiling, we can just bail out here.
2217 // No use sending an empty update over the bridge.
@@ -2245,11 +2253,10 @@ export function attach(
2253 // Which in turn enables fiber props, states, and hooks to be inspected.
2254 let i = 0;
2255 operations[i++] = rendererID;
2248 - if (currentRoot === null) {
2249 - // TODO: This is not always safe so this field is probably not needed.
2256 + if (root === null) {
2257 operations[i++] = -1;
2258 } else {
2252 - operations[i++] = currentRoot.id;
2259 + operations[i++] = root.id;
2260 }
2261
2262 // Now fill in the string table.
@@ -5746,11 +5753,11 @@ export function attach(
5753
5754 mountFiberRecursively(root.current, false);
5755
5756 + flushPendingEvents(currentRoot);
5757 +
5758 currentRoot = (null: any);
5759 });
5760
5752 - flushPendingEvents();
5753 -
5761 needsToFlushComponentLogs = false;
5762 }
5763 }
@@ -5760,7 +5767,7 @@ export function attach(
5767 // safe to stop calling it from Fiber.
5768 }
5769
5763 - function handlePostCommitFiberRoot(root: any) {
5770 + function handlePostCommitFiberRoot(root: FiberRoot) {
5771 if (isProfiling && rootSupportsProfiling(root)) {
5772 if (currentCommitProfilingMetadata !== null) {
5773 const {effectDuration, passiveEffectDuration} =
@@ -5774,12 +5781,18 @@ export function attach(
5781 }
5782
5783 if (needsToFlushComponentLogs) {
5784 + const rootInstance = rootToFiberInstanceMap.get(root);
5785 + if (rootInstance === undefined) {
5786 + throw new Error(
5787 + 'Should have a root instance for a committed root. This is a bug in React DevTools.',
5788 + );
5789 + }
5790 // We received new logs after commit. I.e. in a passive effect. We need to
5791 // traverse the tree to find the affected ones. If we just moved the whole
5792 // tree traversal from handleCommitFiberRoot to handlePostCommitFiberRoot
5793 // this wouldn't be needed. For now we just brute force check all instances.
5794 // This is not that common of a case.
5782 - bruteForceFlushErrorsAndWarnings();
5795 + bruteForceFlushErrorsAndWarnings(rootInstance);
5796 }
5797 }
5798
@@ -5876,7 +5889,7 @@ export function attach(
5889 }
5890
5891 // We're done here.
5879 - flushPendingEvents();
5892 + flushPendingEvents(currentRoot);
5893
5894 needsToFlushComponentLogs = false;
5895
packages/react-devtools-shared/src/constants.js
+1 -1
@@ -22,7 +22,7 @@ export const TREE_OPERATION_REMOVE = 2;
22 export const TREE_OPERATION_REORDER_CHILDREN = 3;
23 export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
24 export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
25 -export const TREE_OPERATION_REMOVE_ROOT = 6;
25 +// Removed `TREE_OPERATION_REMOVE_ROOT`
26 export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
27 export const SUSPENSE_TREE_OPERATION_ADD = 8;
28 export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
packages/react-devtools-shared/src/devtools/store.js
-40
@@ -16,7 +16,6 @@ import {
16 PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
17 TREE_OPERATION_ADD,
18 TREE_OPERATION_REMOVE,
19 - TREE_OPERATION_REMOVE_ROOT,
19 TREE_OPERATION_REORDER_CHILDREN,
20 TREE_OPERATION_SET_SUBTREE_MODE,
21 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
@@ -1644,45 +1643,6 @@ export default class Store extends EventEmitter<{
1643
1644 break;
1645 }
1647 - case TREE_OPERATION_REMOVE_ROOT: {
1648 - i += 1;
1649 -
1650 - const id = operations[1];
1651 -
1652 - if (__DEBUG__) {
1653 - debug(`Remove root ${id}`);
1654 - }
1655 -
1656 - const recursivelyDeleteElements = (elementID: number) => {
1657 - const element = this._idToElement.get(elementID);
1658 - this._idToElement.delete(elementID);
1659 - if (element) {
1660 - // Mostly for Flow's sake
1661 - for (let index = 0; index < element.children.length; index++) {
1662 - recursivelyDeleteElements(element.children[index]);
1663 - }
1664 - }
1665 - };
1666 -
1667 - const root = this._idToElement.get(id);
1668 - if (root === undefined) {
1669 - this._throwAndEmitError(
1670 - Error(
1671 - `Cannot remove root "${id}": no matching node was found in the Store.`,
1672 - ),
1673 - );
1674 -
1675 - break;
1676 - }
1677 -
1678 - recursivelyDeleteElements(id);
1679 -
1680 - this._rootIDToCapabilities.delete(id);
1681 - this._rootIDToRendererID.delete(id);
1682 - this._roots = this._roots.filter(rootID => rootID !== id);
1683 - this._weightAcrossRoots -= root.weight;
1684 - break;
1685 - }
1646 case TREE_OPERATION_REORDER_CHILDREN: {
1647 const id = operations[i + 1];
1648 const numChildren = operations[i + 2];
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
-4
@@ -11,7 +11,6 @@ import {
11 __DEBUG__,
12 TREE_OPERATION_ADD,
13 TREE_OPERATION_REMOVE,
14 - TREE_OPERATION_REMOVE_ROOT,
14 TREE_OPERATION_REORDER_CHILDREN,
15 TREE_OPERATION_SET_SUBTREE_MODE,
16 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
@@ -313,9 +312,6 @@ function updateTree(
312 }
313 break;
314 }
316 - case TREE_OPERATION_REMOVE_ROOT: {
317 - throw Error('Operation REMOVE_ROOT is not supported while profiling.');
318 - }
315 case TREE_OPERATION_REORDER_CHILDREN: {
316 id = ((operations[i + 1]: any): number);
317 const numChildren = ((operations[i + 2]: any): number);
packages/react-devtools-shared/src/utils.js
-7
@@ -28,7 +28,6 @@ import {
28 import {
29 TREE_OPERATION_ADD,
30 TREE_OPERATION_REMOVE,
31 - TREE_OPERATION_REMOVE_ROOT,
31 TREE_OPERATION_REORDER_CHILDREN,
32 TREE_OPERATION_SET_SUBTREE_MODE,
33 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
@@ -295,12 +294,6 @@ export function printOperationsArray(operations: Array<number>) {
294 }
295 break;
296 }
298 - case TREE_OPERATION_REMOVE_ROOT: {
299 - i += 1;
300 -
301 - logs.push(`Remove root ${rootID}`);
302 - break;
303 - }
297 case TREE_OPERATION_SET_SUBTREE_MODE: {
298 const id = operations[i + 1];
299 const mode = operations[i + 2];