diff --git a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js index f5a653592a67..75260d2311bc 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js @@ -260,4 +260,85 @@ describe('commit tree', () => { } }); }); + + it('should tolerate malformed operations without throwing', () => { + const {getCommitTree, invalidateCommitTrees} = require('react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder'); + const { + ElementTypeFunction, + ElementTypeRoot, + } = require('react-devtools-shared/src/frontend/types'); + + const rootID = 1; + const snapshots = new Map(); + snapshots.set(rootID, { + id: rootID, + children: [2], + displayName: 'Root', + hocDisplayNames: null, + key: null, + type: ElementTypeRoot, + compiledWithForget: false, + }); + snapshots.set(2, { + id: 2, + children: [], + displayName: 'Child', + hocDisplayNames: null, + key: null, + type: ElementTypeFunction, + compiledWithForget: false, + }); + const initialTreeBaseDurations = new Map(); + initialTreeBaseDurations.set(rootID, 0); + initialTreeBaseDurations.set(2, 0); + + const ops0 = [0, rootID, 0]; + const ops1 = [ + 0, rootID, 0, + 1, 2, ElementTypeFunction, rootID, 0, 0, 0, + 2, 1, 999, + 6, + 99, + ]; + + const profilingData = { + dataForRoots: new Map([ + [ + rootID, + { + commitData: [], + displayName: 'root', + initialTreeBaseDurations, + operations: [ops0, ops1], + rootID, + snapshots, + }, + ], + ]), + imported: false, + timelineData: [], + }; + + const fakeProfilerStore = {profilingData}; + + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + invalidateCommitTrees(); + expect(() => { + const commitTree = getCommitTree({ + commitIndex: 1, + profilerStore: fakeProfilerStore, + rootID, + }); + expect(commitTree.nodes.size).toBe(2); + }).not.toThrow(); + + expect(errorSpy).toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalled(); + + errorSpy.mockRestore(); + warnSpy.mockRestore(); + }); + }); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 02ecc98ec6d1..394331115bb6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -202,9 +202,11 @@ function updateTree( i += 3; if (nodes.has(id)) { - throw new Error( - `Commit tree already contains fiber "${id}". This is a bug in React DevTools.`, + // Duplicate add detected; log and skip instead of throwing to avoid crashing the UI. + console.error( + `Commit tree already contains fiber "${id}". Skipping duplicate add.`, ); + break; } if (type === ElementTypeRoot) { @@ -286,9 +288,11 @@ function updateTree( i++; if (!nodes.has(id)) { - throw new Error( - `Commit tree does not contain fiber "${id}". This is a bug in React DevTools.`, + // Missing node; log and skip this remove operation. + console.error( + `Commit tree does not contain fiber "${id}". Skipping remove.`, ); + continue; } const node = getClonedNode(id); @@ -312,6 +316,13 @@ function updateTree( } break; } + + case TREE_OPERATION_REMOVE_ROOT: { + console.warn('Operation REMOVE_ROOT is not supported while profiling. Ignoring.'); + break; +} + + case TREE_OPERATION_REORDER_CHILDREN: { id = ((operations[i + 1]: any): number); const numChildren = ((operations[i + 2]: any): number); @@ -490,7 +501,10 @@ function updateTree( } default: - throw Error(`Unsupported Bridge operation "${operation}"`); + // Unsupported operation; log and stop processing further operations to avoid unpredictable state. + console.error(`Unsupported Bridge operation "${operation}". Ignoring remaining operations.`); + i = operations.length; + break; } }