diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index 95f3c479b9..2ccf1f16af 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -2121,6 +2121,437 @@ Object { } `; +exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 0 1`] = ` +Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 1 => 0, + 2 => 0, + 3 => 0, + }, + "fiberSelfDurations": Map { + 1 => 0, + 2 => 0, + 3 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, +} +`; + +exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 1 1`] = ` +Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 3 => 0, + 2 => 0, + 1 => 0, + }, + "fiberSelfDurations": Map { + 3 => 0, + 2 => 0, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, +} +`; + +exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 2 1`] = ` +Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 3 => 0, + }, + "fiberSelfDurations": Map { + 3 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, +} +`; + +exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 3 1`] = ` +Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 3 => 0, + }, + "fiberSelfDurations": Map { + 3 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, +} +`; + +exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 4 1`] = ` +Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 3 => 0, + 2 => 0, + 1 => 0, + }, + "fiberSelfDurations": Map { + 3 => 0, + 2 => 0, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, +} +`; + +exports[`ProfilingCache should properly detect changed hooks: imported data 1`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 3, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 3, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "timestamp": 0, + }, + ], + "displayName": "Component", + "initialTreeBaseDurations": Array [], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 1, + 1, + 27, + 16, + 67, + 111, + 110, + 116, + 101, + 120, + 116, + 46, + 80, + 114, + 111, + 118, + 105, + 100, + 101, + 114, + 9, + 67, + 111, + 109, + 112, + 111, + 110, + 101, + 110, + 116, + 1, + 1, + 11, + 1, + 1, + 1, + 2, + 2, + 1, + 0, + 1, + 0, + 4, + 2, + 0, + 1, + 3, + 5, + 2, + 0, + 2, + 0, + 4, + 3, + 0, + ], + Array [ + 1, + 1, + 0, + ], + Array [ + 1, + 1, + 0, + ], + Array [ + 1, + 1, + 0, + ], + Array [ + 1, + 1, + 0, + ], + ], + "rootID": 1, + "snapshots": Array [], + }, + ], + "version": 4, +} +`; + exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 0 1`] = ` Object { "changeDescriptions": Map { diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index f99be7bc3b..b26c8dbe68 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -308,6 +308,130 @@ describe('ProfilingCache', () => { } }); + it('should properly detect changed hooks', () => { + const Context = React.createContext(0); + + function reducer(state, action) { + switch (action.type) { + case 'invert': + return {value: !state.value}; + default: + throw new Error(); + } + } + + let dispatch = null; + let setState = null; + + const Component = ({count, string}) => { + // These hooks may change and initiate re-renders. + setState = React.useState('abc')[1]; + dispatch = React.useReducer(reducer, {value: true})[1]; + + // This hook's return value may change between renders, + // but the hook itself isn't stateful. + React.useContext(Context); + + // These hooks and their dependencies may not change between renders. + // We're using them to ensure that they don't trigger false positives. + React.useCallback(() => () => {}, [string]); + React.useMemo(() => string, [string]); + + // These hooks never "change". + React.useEffect(() => {}, [string]); + React.useLayoutEffect(() => {}, [string]); + + return null; + }; + + const container = document.createElement('div'); + + utils.act(() => store.profilerStore.startProfiling()); + utils.act(() => + ReactDOM.render( + + + , + container, + ), + ); + + // Second render has no changed hooks, only changed props. + utils.act(() => + ReactDOM.render( + + + , + container, + ), + ); + + // Third render has a changed reducer hook + utils.act(() => dispatch({type: 'invert'})); + + // Fourth render has a changed state hook + utils.act(() => setState('def')); + + // Fifth render has a changed context value, but no changed hook. + // Technically, DevTools will miss this "context" change since it only tracks legacy context. + utils.act(() => + ReactDOM.render( + + + , + container, + ), + ); + + utils.act(() => store.profilerStore.stopProfiling()); + + const allCommitData = []; + + function Validator({commitIndex, previousCommitDetails, rootID}) { + const commitData = store.profilerStore.getCommitData(rootID, commitIndex); + if (previousCommitDetails != null) { + expect(commitData).toEqual(previousCommitDetails); + } else { + allCommitData.push(commitData); + expect(commitData).toMatchSnapshot( + `CommitDetails commitIndex: ${commitIndex}`, + ); + } + return null; + } + + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 5; commitIndex++) { + utils.act(() => { + TestRenderer.create( + , + ); + }); + } + + expect(allCommitData).toHaveLength(5); + + // Export and re-import profile data and make sure it is retained. + utils.exportImportHelper(bridge, store); + + for (let commitIndex = 0; commitIndex < 5; commitIndex++) { + utils.act(() => { + TestRenderer.create( + , + ); + }); + } + }); + it('should calculate a self duration based on actual children (not filtered children)', () => { store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];