diff --git a/src/__tests__/__snapshots__/profilingCache-test.js.snap b/src/__tests__/__snapshots__/profilingCache-test.js.snap index f9901da8..85291acd 100644 --- a/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -2,6 +2,29 @@ exports[`ProfilingCache should calculate a self duration based on actual children (not filtered children): CommitDetails with filtered self durations 1`] = ` Object { + "changeDescriptions": Map { + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + }, "duration": 16, "fiberActualDurations": Map { 1 => 16, @@ -24,6 +47,22 @@ Object { exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 1`] = ` Object { + "changeDescriptions": Map { + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 4 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + }, "duration": 15, "fiberActualDurations": Map { 1 => 15, @@ -46,6 +85,15 @@ Object { exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 2`] = ` Object { + "changeDescriptions": Map { + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + }, "duration": 3, "fiberActualDurations": Map { 5 => 3, @@ -64,6 +112,36 @@ Object { exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 0 1`] = ` Object { + "changeDescriptions": Map { + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 4 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + }, "duration": 12, "fiberActualDurations": Map { 1 => 12, @@ -88,6 +166,38 @@ Object { exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 1 1`] = ` Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 4 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 6 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, "duration": 13, "fiberActualDurations": Map { 3 => 0, @@ -112,6 +222,24 @@ Object { exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 2 1`] = ` Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, "duration": 10, "fiberActualDurations": Map { 3 => 0, @@ -132,6 +260,17 @@ Object { exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 3 1`] = ` Object { + "changeDescriptions": Map { + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, "duration": 10, "fiberActualDurations": Map { 2 => 10, @@ -154,6 +293,48 @@ Object { Object { "commitData": Array [ Object { + "changeDescriptions": Array [ + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 4, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + ], "duration": 12, "fiberActualDurations": Array [ Array [ @@ -205,6 +386,50 @@ Object { "timestamp": 12, }, Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 4, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 6, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 13, "fiberActualDurations": Array [ Array [ @@ -256,6 +481,30 @@ Object { "timestamp": 25, }, Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 10, "fiberActualDurations": Array [ Array [ @@ -291,6 +540,20 @@ Object { "timestamp": 35, }, Object { + "changeDescriptions": Array [ + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 10, "fiberActualDurations": Array [ Array [ @@ -507,6 +770,38 @@ Object { Object { "commitData": Array [ Object { + "changeDescriptions": Array [ + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 4, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + ], "duration": 11, "fiberActualDurations": Array [ Array [ @@ -550,6 +845,40 @@ Object { "timestamp": 11, }, Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 11, "fiberActualDurations": Array [ Array [ @@ -593,6 +922,50 @@ Object { "timestamp": 22, }, Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 6, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 13, "fiberActualDurations": Array [ Array [ @@ -791,6 +1164,38 @@ exports[`ProfilingCache should collect data for each root (including ones added Object { "commitData": Array [ Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 4 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 10 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, "duration": 13, "fiberActualDurations": Map { 3 => 0, @@ -812,6 +1217,24 @@ Object { "timestamp": 13, }, Object { + "changeDescriptions": Map { + 3 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, "duration": 10, "fiberActualDurations": Map { 3 => 0, @@ -829,6 +1252,17 @@ Object { "timestamp": 34, }, Object { + "changeDescriptions": Map { + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + }, "duration": 10, "fiberActualDurations": Map { 2 => 10, @@ -971,6 +1405,29 @@ exports[`ProfilingCache should collect data for each root (including ones added Object { "commitData": Array [ Object { + "changeDescriptions": Map { + 12 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 13 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 14 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + }, "duration": 11, "fiberActualDurations": Map { 11 => 11, @@ -1063,6 +1520,7 @@ exports[`ProfilingCache should collect data for each root (including ones added Object { "commitData": Array [ Object { + "changeDescriptions": Map {}, "duration": 0, "fiberActualDurations": Map {}, "fiberSelfDurations": Map {}, @@ -1139,6 +1597,50 @@ Object { Object { "commitData": Array [ Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 4, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 10, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 13, "fiberActualDurations": Array [ Array [ @@ -1190,6 +1692,30 @@ Object { "timestamp": 13, }, Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 10, "fiberActualDurations": Array [ Array [ @@ -1225,6 +1751,20 @@ Object { "timestamp": 34, }, Object { + "changeDescriptions": Array [ + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 10, "fiberActualDurations": Array [ Array [ @@ -1406,6 +1946,38 @@ Object { Object { "commitData": Array [ Object { + "changeDescriptions": Array [ + Array [ + 12, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 13, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 14, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + ], "duration": 11, "fiberActualDurations": Array [ Array [ @@ -1519,6 +2091,7 @@ Object { Object { "commitData": Array [ Object { + "changeDescriptions": Array [], "duration": 0, "fiberActualDurations": Array [], "fiberSelfDurations": Array [], @@ -1616,74 +2189,1300 @@ Object { } `; -exports[`ProfilingCache should report every traced interaction: Interactions 1`] = ` -Array [ - Object { - "__count": 1, - "id": 0, - "name": "mount: one child", - "timestamp": 0, +exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 0 1`] = ` +Object { + "changeDescriptions": Map { + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 4 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 6 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + 7 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, }, - Object { - "__count": 0, - "id": 1, - "name": "update: two children", - "timestamp": 11, + "duration": 0, + "fiberActualDurations": Map { + 1 => 0, + 2 => 0, + 3 => 0, + 4 => 0, + 5 => 0, + 6 => 0, + 7 => 0, }, -] + "fiberSelfDurations": Map { + 1 => 0, + 2 => 0, + 3 => 0, + 4 => 0, + 5 => 0, + 6 => 0, + 7 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, +} `; -exports[`ProfilingCache should report every traced interaction: imported data 1`] = ` +exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 1 1`] = ` Object { - "dataForRoots": Array [ - Object { - "commitData": Array [ - Object { - "duration": 11, - "fiberActualDurations": Array [ - Array [ - 1, - 11, - ], - Array [ - 2, - 11, - ], - Array [ - 3, - 0, - ], - Array [ - 4, - 1, - ], - ], - "fiberSelfDurations": Array [ - Array [ - 1, - 0, - ], - Array [ - 2, - 10, - ], - Array [ - 3, - 0, - ], - Array [ - 4, - 1, - ], - ], - "interactionIDs": Array [ - 0, - ], - "priorityLevel": "Immediate", - "screenshot": null, - "timestamp": 11, - }, - Object { + "changeDescriptions": Map { + 5 => Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + 4 => Object { + "context": true, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 7 => Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + 6 => Object { + "context": Array [ + "count", + ], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": Array [ + "count", + ], + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + }, + "fiberSelfDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, +} +`; + +exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 2 1`] = ` +Object { + "changeDescriptions": Map { + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 4 => Object { + "context": false, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 7 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 6 => Object { + "context": Array [], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "foo", + ], + "state": Array [], + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + 1 => 0, + }, + "fiberSelfDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, +} +`; + +exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 3 1`] = ` +Object { + "changeDescriptions": Map { + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 4 => Object { + "context": false, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 7 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 6 => Object { + "context": Array [], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "foo", + "bar", + ], + "state": Array [], + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + 1 => 0, + }, + "fiberSelfDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, +} +`; + +exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 4 1`] = ` +Object { + "changeDescriptions": Map { + 5 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 4 => Object { + "context": false, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 7 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 6 => Object { + "context": Array [], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + 2 => Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "bar", + ], + "state": Array [], + }, + }, + "duration": 0, + "fiberActualDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + 1 => 0, + }, + "fiberSelfDurations": Map { + 5 => 0, + 4 => 0, + 7 => 0, + 6 => 0, + 3 => 0, + 2 => 0, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, +} +`; + +exports[`ProfilingCache should record changed props/state/context/hooks: imported data 1`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "changeDescriptions": Array [ + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 4, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 6, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 7, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 5, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 7, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 5, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 7, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 5, + Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + Array [ + 4, + Object { + "context": true, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 7, + Object { + "context": null, + "didHooksChange": true, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + Array [ + 6, + Object { + "context": Array [ + "count", + ], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": Array [ + "count", + ], + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 4, + Object { + "context": false, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 7, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 6, + Object { + "context": Array [], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "foo", + ], + "state": Array [], + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 4, + Object { + "context": false, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 7, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 6, + Object { + "context": Array [], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "foo", + "bar", + ], + "state": Array [], + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, + }, + Object { + "changeDescriptions": Array [ + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 4, + Object { + "context": false, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 7, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 6, + Object { + "context": Array [], + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "bar", + ], + "state": Array [], + }, + ], + ], + "duration": 0, + "fiberActualDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 5, + 0, + ], + Array [ + 4, + 0, + ], + Array [ + 7, + 0, + ], + Array [ + 6, + 0, + ], + Array [ + 3, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 0, + }, + ], + "displayName": "LegacyContextProvider", + "initialTreeBaseDurations": Array [], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 1, + 1, + 110, + 21, + 76, + 101, + 103, + 97, + 99, + 121, + 67, + 111, + 110, + 116, + 101, + 120, + 116, + 80, + 114, + 111, + 118, + 105, + 100, + 101, + 114, + 16, + 67, + 111, + 110, + 116, + 101, + 120, + 116, + 46, + 80, + 114, + 111, + 118, + 105, + 100, + 101, + 114, + 21, + 77, + 111, + 100, + 101, + 114, + 110, + 67, + 111, + 110, + 116, + 101, + 120, + 116, + 67, + 111, + 110, + 115, + 117, + 109, + 101, + 114, + 26, + 70, + 117, + 110, + 99, + 116, + 105, + 111, + 110, + 67, + 111, + 109, + 112, + 111, + 110, + 101, + 110, + 116, + 87, + 105, + 116, + 104, + 72, + 111, + 111, + 107, + 115, + 21, + 76, + 101, + 103, + 97, + 99, + 121, + 67, + 111, + 110, + 116, + 101, + 120, + 116, + 67, + 111, + 110, + 115, + 117, + 109, + 101, + 114, + 1, + 1, + 11, + 1, + 1, + 1, + 2, + 1, + 1, + 0, + 1, + 0, + 4, + 2, + 0, + 1, + 3, + 2, + 2, + 2, + 2, + 0, + 4, + 3, + 0, + 1, + 4, + 1, + 3, + 2, + 3, + 0, + 4, + 4, + 0, + 1, + 5, + 5, + 4, + 4, + 4, + 0, + 4, + 5, + 0, + 1, + 6, + 1, + 3, + 2, + 5, + 0, + 4, + 6, + 0, + 1, + 7, + 5, + 6, + 6, + 4, + 0, + 4, + 7, + 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 report every traced interaction: Interactions 1`] = ` +Array [ + Object { + "__count": 1, + "id": 0, + "name": "mount: one child", + "timestamp": 0, + }, + Object { + "__count": 0, + "id": 1, + "name": "update: two children", + "timestamp": 11, + }, +] +`; + +exports[`ProfilingCache should report every traced interaction: imported data 1`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "changeDescriptions": Array [ + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 4, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + ], + "duration": 11, + "fiberActualDurations": Array [ + Array [ + 1, + 11, + ], + Array [ + 2, + 11, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + ], + "interactionIDs": Array [ + 0, + ], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 11, + }, + Object { + "changeDescriptions": Array [ + Array [ + 3, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [], + "state": null, + }, + ], + Array [ + 5, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": true, + "props": null, + "state": null, + }, + ], + Array [ + 2, + Object { + "context": null, + "didHooksChange": false, + "isFirstMount": false, + "props": Array [ + "count", + ], + "state": null, + }, + ], + ], "duration": 11, "fiberActualDurations": Array [ Array [ diff --git a/src/__tests__/profilerContext-test.js b/src/__tests__/profilerContext-test.js index 3d3a68f5..6857ddb4 100644 --- a/src/__tests__/profilerContext-test.js +++ b/src/__tests__/profilerContext-test.js @@ -29,6 +29,7 @@ describe('ProfilerContext', () => { bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; + store.recordChangeDescriptions = true; React = require('react'); ReactDOM = require('react-dom'); diff --git a/src/__tests__/profilerStore-test.js b/src/__tests__/profilerStore-test.js index f6ffcfe7..0f4074f1 100644 --- a/src/__tests__/profilerStore-test.js +++ b/src/__tests__/profilerStore-test.js @@ -14,6 +14,7 @@ describe('ProfilerStore', () => { store = global.store; store.collapseNodesByDefault = false; + store.recordChangeDescriptions = true; React = require('react'); ReactDOM = require('react-dom'); diff --git a/src/__tests__/profilingCache-test.js b/src/__tests__/profilingCache-test.js index c93d347e..4e1688a7 100644 --- a/src/__tests__/profilingCache-test.js +++ b/src/__tests__/profilingCache-test.js @@ -5,6 +5,7 @@ import type Bridge from 'src/bridge'; import type Store from 'src/devtools/store'; describe('ProfilingCache', () => { + let PropTypes; let React; let ReactDOM; let Scheduler; @@ -21,7 +22,9 @@ describe('ProfilingCache', () => { bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; + store.recordChangeDescriptions = true; + PropTypes = require('prop-types'); React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); @@ -187,6 +190,117 @@ describe('ProfilingCache', () => { } }); + it('should record changed props/state/context/hooks', () => { + let instance = null; + + const ModernContext = React.createContext(0); + + class LegacyContextProvider extends React.Component< + any, + {| count: number |} + > { + static childContextTypes = { + count: PropTypes.number, + }; + state = { count: 0 }; + getChildContext() { + return this.state; + } + render() { + instance = this; + return ( + + + + + + + ); + } + } + + const FunctionComponentWithHooks = ({ count }) => { + React.useMemo(() => count, [count]); + return null; + }; + + class ModernContextConsumer extends React.Component { + static contextType = ModernContext; + render() { + return ; + } + } + + class LegacyContextConsumer extends React.Component { + static contextTypes = { + count: PropTypes.number, + }; + render() { + return ; + } + } + + const container = document.createElement('div'); + + utils.act(() => store.profilerStore.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + expect(instance).not.toBeNull(); + utils.act(() => (instance: any).setState({ count: 1 })); + utils.act(() => + ReactDOM.render(, container) + ); + utils.act(() => + ReactDOM.render(, container) + ); + 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); + + 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$')]; diff --git a/src/__tests__/profilingCharts-test.js b/src/__tests__/profilingCharts-test.js index d83f8b5a..9cb6d25d 100644 --- a/src/__tests__/profilingCharts-test.js +++ b/src/__tests__/profilingCharts-test.js @@ -18,6 +18,7 @@ describe('profiling charts', () => { store = global.store; store.collapseNodesByDefault = false; + store.recordChangeDescriptions = true; React = require('react'); ReactDOM = require('react-dom'); diff --git a/src/__tests__/profilingCommitTreeBuilder-test.js b/src/__tests__/profilingCommitTreeBuilder-test.js index e6c98c1d..68d42961 100644 --- a/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/src/__tests__/profilingCommitTreeBuilder-test.js @@ -17,6 +17,7 @@ describe('commit tree', () => { store = global.store; store.collapseNodesByDefault = false; + store.recordChangeDescriptions = true; React = require('react'); ReactDOM = require('react-dom'); diff --git a/src/__tests__/storeComponentFilters-test.js b/src/__tests__/storeComponentFilters-test.js index 6627912b..16b028ab 100644 --- a/src/__tests__/storeComponentFilters-test.js +++ b/src/__tests__/storeComponentFilters-test.js @@ -21,6 +21,7 @@ describe('Store component filters', () => { store = global.store; store.collapseNodesByDefault = false; store.componentFilters = []; + store.recordChangeDescriptions = true; React = require('react'); ReactDOM = require('react-dom'); diff --git a/src/backend/agent.js b/src/backend/agent.js index 4c1bf1b4..12ba9a8a 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -6,6 +6,7 @@ import throttle from 'lodash.throttle'; import { SESSION_STORAGE_LAST_SELECTION_KEY, SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, __DEBUG__, } from '../constants'; import { @@ -69,6 +70,7 @@ type PersistedSelection = {| export default class Agent extends EventEmitter { _bridge: Bridge; _isProfiling: boolean = false; + _recordChangeDescriptions: boolean = false; _rendererInterfaces: { [key: RendererID]: RendererInterface } = {}; _persistedSelection: PersistedSelection | null = null; _persistedSelectionMatch: PathMatch | null = null; @@ -79,8 +81,13 @@ export default class Agent extends EventEmitter { if ( sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' ) { + this._recordChangeDescriptions = + sessionStorageGetItem( + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY + ) === 'true'; this._isProfiling = true; + sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY); sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY); } @@ -250,8 +257,12 @@ export default class Agent extends EventEmitter { } }; - reloadAndProfile = () => { + reloadAndProfile = (recordChangeDescriptions: boolean) => { sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true'); + sessionStorageSetItem( + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, + recordChangeDescriptions ? 'true' : 'false' + ); // This code path should only be hit if the shell has explicitly told the Store that it supports profiling. // In that case, the shell must also listen for this specific message to know when it needs to reload the app. @@ -358,7 +369,7 @@ export default class Agent extends EventEmitter { this._rendererInterfaces[rendererID] = rendererInterface; if (this._isProfiling) { - rendererInterface.startProfiling(); + rendererInterface.startProfiling(this._recordChangeDescriptions); } // When the renderer is attached, we need to tell it whether @@ -397,13 +408,14 @@ export default class Agent extends EventEmitter { window.addEventListener('pointerup', this._onPointerUp, true); }; - startProfiling = () => { + startProfiling = (recordChangeDescriptions: boolean) => { + this._recordChangeDescriptions = recordChangeDescriptions; this._isProfiling = true; for (let rendererID in this._rendererInterfaces) { const renderer = ((this._rendererInterfaces[ (rendererID: any) ]: any): RendererInterface); - renderer.startProfiling(); + renderer.startProfiling(recordChangeDescriptions); } this._bridge.send('profilingStatus', this._isProfiling); }; @@ -422,6 +434,7 @@ export default class Agent extends EventEmitter { stopProfiling = () => { this._isProfiling = false; + this._recordChangeDescriptions = false; for (let rendererID in this._rendererInterfaces) { const renderer = ((this._rendererInterfaces[ (rendererID: any) diff --git a/src/backend/renderer.js b/src/backend/renderer.js index df28dd45..79ed649c 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -30,6 +30,7 @@ import { cleanForBridge, copyWithSet, setInObject } from './utils'; import { __DEBUG__, SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, @@ -38,6 +39,7 @@ import { import { inspectHooksOfFiber } from './ReactDebugHooks'; import type { + ChangeDescription, CommitDataBackend, DevToolsHook, Fiber, @@ -350,7 +352,7 @@ export function attach( // The unmount operations are already significantly smaller than mount opreations though. // This is something to keep in mind for later. function updateComponentFilters(componentFilters: Array) { - if (this._isProfiling) { + if (isProfiling) { // Re-mounting a tree while profiling is in progress might break a lot of assumptions. // If necessary, we could support this- but it doesn't seem like a necessary use case. throw Error('Cannot modify filter preferences while profiling'); @@ -646,8 +648,185 @@ export function attach( return ((fiberToIDMap.get(primaryFiber): any): number); } + function getChangeDescription( + prevFiber: Fiber | null, + nextFiber: Fiber + ): ChangeDescription | null { + switch (getElementTypeForFiber(nextFiber)) { + case ElementTypeClass: + case ElementTypeFunction: + case ElementTypeMemo: + case ElementTypeForwardRef: + if (prevFiber === null) { + return { + context: null, + didHooksChange: false, + isFirstMount: true, + props: null, + state: null, + }; + } else { + return { + context: getContextChangedKeys(nextFiber), + didHooksChange: didHooksChange( + prevFiber.memoizedState, + nextFiber.memoizedState + ), + isFirstMount: false, + props: getChangedKeys( + prevFiber.memoizedProps, + nextFiber.memoizedProps + ), + state: getChangedKeys( + prevFiber.memoizedState, + nextFiber.memoizedState + ), + }; + } + default: + return null; + } + } + + function updateContextsForFiber(fiber: Fiber) { + switch (getElementTypeForFiber(fiber)) { + case ElementTypeClass: + if (idToContextsMap !== null) { + const id = getFiberID(getPrimaryFiber(fiber)); + const contexts = getContextsForFiber(fiber); + if (contexts !== null) { + idToContextsMap.set(id, contexts); + } + } + break; + default: + break; + } + } + + // Differentiates between a null context value and no context. + const NO_CONTEXT = {}; + + function getContextsForFiber(fiber: Fiber): [Object, any] | null { + switch (getElementTypeForFiber(fiber)) { + case ElementTypeClass: + const instance = fiber.stateNode; + let legacyContext = NO_CONTEXT; + let modernContext = NO_CONTEXT; + if (instance != null) { + if ( + instance.constructor && + instance.constructor.contextType != null + ) { + modernContext = instance.context; + } else { + legacyContext = instance.context; + if (legacyContext && Object.keys(legacyContext).length === 0) { + legacyContext = NO_CONTEXT; + } + } + } + return [legacyContext, modernContext]; + default: + return null; + } + } + + // Record all contexts at the time profiling is started. + // Fibers only store the current context value, + // so we need to track them separatenly in order to determine changed keys. + function crawlToInitializeContextsMap(fiber: Fiber) { + updateContextsForFiber(fiber); + let current = fiber.child; + while (current !== null) { + crawlToInitializeContextsMap(current); + current = current.sibling; + } + } + + function getContextChangedKeys(fiber: Fiber): null | boolean | Array { + switch (getElementTypeForFiber(fiber)) { + case ElementTypeClass: + if (idToContextsMap !== null) { + const id = getFiberID(getPrimaryFiber(fiber)); + const prevContexts = idToContextsMap.has(id) + ? idToContextsMap.get(id) + : null; + const nextContexts = getContextsForFiber(fiber); + + if (prevContexts == null || nextContexts == null) { + return null; + } + + const [prevLegacyContext, prevModernContext] = prevContexts; + const [nextLegacyContext, nextModernContext] = nextContexts; + + if (nextLegacyContext !== NO_CONTEXT) { + return getChangedKeys(prevLegacyContext, nextLegacyContext); + } else if (nextModernContext !== NO_CONTEXT) { + return prevModernContext !== nextModernContext; + } + } + break; + default: + break; + } + return null; + } + + function didHooksChange(prev: any, next: any): boolean { + if (next == null) { + return false; + } + + // We can't report anything meaningful for hooks changes. + if ( + next.hasOwnProperty('baseState') && + next.hasOwnProperty('memoizedState') && + next.hasOwnProperty('next') && + next.hasOwnProperty('queue') + ) { + while (next !== null) { + if (next.memoizedState !== prev.memoizedState) { + return true; + } else { + next = next.next; + prev = prev.next; + } + } + } + + return false; + } + + function getChangedKeys(prev: any, next: any): null | Array { + if (prev == null || next == null) { + return null; + } + + // We can't report anything meaningful for hooks changes. + if ( + next.hasOwnProperty('baseState') && + next.hasOwnProperty('memoizedState') && + next.hasOwnProperty('next') && + next.hasOwnProperty('queue') + ) { + return null; + } + + const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); + const changedKeys = []; + for (let key of keys) { + if (prev[key] !== next[key]) { + changedKeys.push(key); + } + } + + return changedKeys; + } + // eslint-disable-next-line no-unused-vars - function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean { + function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean { switch (nextFiber.tag) { case ClassComponent: case FunctionComponent: @@ -1024,7 +1203,7 @@ export function attach( pushOperation(treeBaseDuration); } - if (alternate == null || hasDataChanged(alternate, fiber)) { + if (alternate == null || didFiberRender(alternate, fiber)) { if (actualDuration != null) { // The actual duration reported by React includes time spent working on children. // This is useful information, but it's also useful to be able to exclude child durations. @@ -1049,6 +1228,17 @@ export function attach( metadata.maxActualDuration, actualDuration ); + + if (recordChangeDescriptions) { + const changeDescription = getChangeDescription(alternate, fiber); + if (changeDescription !== null) { + if (metadata.changeDescriptions !== null) { + metadata.changeDescriptions.set(id, changeDescription); + } + } + + updateContextsForFiber(fiber); + } } } } @@ -1110,7 +1300,7 @@ export function attach( mostRecentlyInspectedElementID !== null && mostRecentlyInspectedElementID === getFiberID(getPrimaryFiber(nextFiber)) && - hasDataChanged(prevFiber, nextFiber) + didFiberRender(prevFiber, nextFiber) ) { // If this Fiber has updated, clear cached inspected data. // If it is inspected again, it may need to be re-run to obtain updated hooks values. @@ -1300,6 +1490,7 @@ export function attach( // If profiling is active, store commit time and duration, and the current interactions. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { + changeDescriptions: recordChangeDescriptions ? new Map() : null, durations: [], commitTime: performance.now() - profilingStartTime, interactions: Array.from(root.memoizedInteractions).map( @@ -1343,6 +1534,7 @@ export function attach( // If profiling is active, store commit time and duration, and the current interactions. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { + changeDescriptions: recordChangeDescriptions ? new Map() : null, durations: [], commitTime: performance.now() - profilingStartTime, interactions: Array.from(root.memoizedInteractions).map( @@ -2058,6 +2250,7 @@ export function attach( } type CommitProfilingData = {| + changeDescriptions: Map | null, commitTime: number, durations: Array, interactions: Array, @@ -2070,10 +2263,12 @@ export function attach( let currentCommitProfilingMetadata: CommitProfilingData | null = null; let displayNamesByRootID: DisplayNamesByRootID | null = null; + let idToContextsMap: Map | null = null; let initialTreeBaseDurationsMap: Map | null = null; let initialIDToRootMap: Map | null = null; let isProfiling: boolean = false; let profilingStartTime: number = 0; + let recordChangeDescriptions: boolean = false; let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null; function getProfilingData(): ProfilingDataBackend { @@ -2111,6 +2306,7 @@ export function attach( commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => { const { + changeDescriptions, durations, interactions, maxActualDuration, @@ -2144,6 +2340,10 @@ export function attach( } commitData.push({ + changeDescriptions: + changeDescriptions !== null + ? Array.from(changeDescriptions.entries()) + : null, duration: maxActualDuration, fiberActualDurations, fiberSelfDurations, @@ -2170,11 +2370,13 @@ export function attach( }; } - function startProfiling() { + function startProfiling(shouldRecordChangeDescriptions: boolean) { if (isProfiling) { return; } + recordChangeDescriptions = shouldRecordChangeDescriptions; + // Capture initial values as of the time profiling starts. // It's important we snapshot both the durations and the id-to-root map, // since either of these may change during the profiling session @@ -2182,6 +2384,7 @@ export function attach( displayNamesByRootID = new Map(); initialTreeBaseDurationsMap = new Map(idToTreeBaseDurationMap); initialIDToRootMap = new Map(idToRootMap); + idToContextsMap = new Map(); hook.getFiberRoots(rendererID).forEach(root => { const rootID = getFiberID(getPrimaryFiber(root.current)); @@ -2189,6 +2392,13 @@ export function attach( rootID, getDisplayNameForRoot(root.current) ); + + if (shouldRecordChangeDescriptions) { + // Record all contexts at the time profiling is started. + // Fibers only store the current context value, + // so we need to track them separatenly in order to determine changed keys. + crawlToInitializeContextsMap(root.current); + } }); isProfiling = true; @@ -2198,13 +2408,17 @@ export function attach( function stopProfiling() { isProfiling = false; + recordChangeDescriptions = false; } // Automatically start profiling so that we don't miss timing info from initial "mount". if ( sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' ) { - startProfiling(); + startProfiling( + sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) === + 'true' + ); } // React will switch between these implementations depending on whether diff --git a/src/backend/types.js b/src/backend/types.js index bce7238e..fe7b5dcf 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -112,7 +112,17 @@ export type ReactRenderer = { currentDispatcherRef?: {| current: null | Dispatcher |}, }; +export type ChangeDescription = {| + context: Array | boolean | null, + didHooksChange: boolean, + isFirstMount: boolean, + props: Array | null, + state: Array | null, +|}; + export type CommitDataBackend = {| + // Tuple of fiber ID and change description + changeDescriptions: Array<[number, ChangeDescription]> | null, duration: number, // Tuple of fiber ID and actual duration fiberActualDurations: Array<[number, number]>, @@ -226,7 +236,7 @@ export type RendererInterface = { setInProps: (id: number, path: Array, value: any) => void, setInState: (id: number, path: Array, value: any) => void, setTrackedPath: (path: Array | null) => void, - startProfiling: () => void, + startProfiling: (recordChangeDescriptions: boolean) => void, stopProfiling: () => void, updateComponentFilters: (somponentFilters: Array) => void, }; diff --git a/src/constants.js b/src/constants.js index b06ebcfa..9267c53e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -14,6 +14,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY = export const SESSION_STORAGE_LAST_SELECTION_KEY = 'React::DevTools::lastSelection'; +export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY = + 'React::DevTools::recordChangeDescriptions'; + export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY = 'React::DevTools::reloadAndProfile'; diff --git a/src/devtools/ProfilerStore.js b/src/devtools/ProfilerStore.js index f4901287..b15726c5 100644 --- a/src/devtools/ProfilerStore.js +++ b/src/devtools/ProfilerStore.js @@ -179,7 +179,7 @@ export default class ProfilerStore extends EventEmitter { } startProfiling(): void { - this._bridge.send('startProfiling'); + this._bridge.send('startProfiling', this._store.recordChangeDescriptions); // Don't actually update the local profiling boolean yet! // Wait for onProfilingStatus() to confirm the status has changed. diff --git a/src/devtools/store.js b/src/devtools/store.js index 6ad34d7e..d4501273 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -38,6 +38,8 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY = 'React::DevTools::captureScreenshots'; const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY = 'React::DevTools::collapseNodesByDefault'; +const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY = + 'React::DevTools::recordChangeDescriptions'; type Config = {| isProfiling?: boolean, @@ -83,6 +85,8 @@ export default class Store extends EventEmitter { _profilerStore: ProfilerStore; + _recordChangeDescriptions: boolean = false; + // Incremented each time the store is mutated. // This enables a passive effect to detect a mutation between render and commit phase. _revision: number = 0; @@ -118,6 +122,10 @@ export default class Store extends EventEmitter { localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !== 'false'; + this._recordChangeDescriptions = + localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) === + 'true'; + this._componentFilters = getSavedComponentFilters(); let isProfiling = false; @@ -248,6 +256,20 @@ export default class Store extends EventEmitter { return this._profilerStore; } + get recordChangeDescriptions(): boolean { + return this._recordChangeDescriptions; + } + set recordChangeDescriptions(value: boolean): void { + this._recordChangeDescriptions = value; + + localStorageSetItem( + LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, + value ? 'true' : 'false' + ); + + this.emit('recordChangeDescriptions'); + } + get revision(): number { return this._revision; } diff --git a/src/devtools/views/Profiler/Profiler.css b/src/devtools/views/Profiler/Profiler.css index 5ab88890..22d26a1c 100644 --- a/src/devtools/views/Profiler/Profiler.css +++ b/src/devtools/views/Profiler/Profiler.css @@ -14,6 +14,7 @@ display: flex; flex-direction: column; flex: 2 1 200px; + border-top: 1px solid var(--color-border); } .RightColumn { @@ -21,6 +22,7 @@ flex-direction: column; flex: 1 1 100px; max-width: 300px; + overflow-x: hidden; border-left: 1px solid var(--color-border); border-top: 1px solid var(--color-border); } @@ -60,7 +62,6 @@ display: flex; align-items: center; border-bottom: 1px solid var(--color-border); - border-top: 1px solid var(--color-border); } .VRule { diff --git a/src/devtools/views/Profiler/ReloadAndProfileButton.js b/src/devtools/views/Profiler/ReloadAndProfileButton.js index 110a1b61..933d453a 100644 --- a/src/devtools/views/Profiler/ReloadAndProfileButton.js +++ b/src/devtools/views/Profiler/ReloadAndProfileButton.js @@ -7,27 +7,41 @@ import { BridgeContext, StoreContext } from '../context'; import { useSubscription } from '../hooks'; import Store from 'src/devtools/store'; +type SubscriptionData = {| + recordChangeDescriptions: boolean, + supportsReloadAndProfile: boolean, +|}; + export default function ReloadAndProfileButton() { const bridge = useContext(BridgeContext); const store = useContext(StoreContext); - const supportsReloadAndProfileSubscription = useMemo( + const subscription = useMemo( () => ({ - getCurrentValue: () => store.supportsReloadAndProfile, + getCurrentValue: () => ({ + recordChangeDescriptions: store.recordChangeDescriptions, + supportsReloadAndProfile: store.supportsReloadAndProfile, + }), subscribe: (callback: Function) => { + store.addListener('recordChangeDescriptions', callback); store.addListener('supportsReloadAndProfile', callback); - return () => store.removeListener('supportsReloadAndProfile', callback); + return () => { + store.removeListener('recordChangeDescriptions', callback); + store.removeListener('supportsReloadAndProfile', callback); + }; }, }), [store] ); - const supportsReloadAndProfile = useSubscription( - supportsReloadAndProfileSubscription - ); + const { + recordChangeDescriptions, + supportsReloadAndProfile, + } = useSubscription(subscription); - const reloadAndProfile = useCallback(() => bridge.send('reloadAndProfile'), [ - bridge, - ]); + const reloadAndProfile = useCallback( + () => bridge.send('reloadAndProfile', recordChangeDescriptions), + [bridge, recordChangeDescriptions] + ); if (!supportsReloadAndProfile) { return null; diff --git a/src/devtools/views/Profiler/SidebarCommitInfo.css b/src/devtools/views/Profiler/SidebarCommitInfo.css index 0bbe6674..6a835e64 100644 --- a/src/devtools/views/Profiler/SidebarCommitInfo.css +++ b/src/devtools/views/Profiler/SidebarCommitInfo.css @@ -4,12 +4,12 @@ flex: 0 0 auto; display: flex; align-items: center; + border-bottom: 1px solid var(--color-border); } .Content { padding: 0.5rem; user-select: none; - border-top: 1px solid var(--color-border); overflow: auto; } diff --git a/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css b/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css index 30097488..a89175b6 100644 --- a/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css +++ b/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css @@ -4,18 +4,21 @@ flex: 0 0 auto; display: flex; align-items: center; + border-bottom: 1px solid var(--color-border); } .Content { padding: 0.5rem; user-select: none; - border-top: 1px solid var(--color-border); overflow-y: auto; } .Component { flex: 1; color: var(--color-component-name); + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; } .Component:before { white-space: nowrap; @@ -56,3 +59,22 @@ .CurrentCommit:focus { outline: none; } + +.WhatChangedItem { + margin-top: 0.25rem; +} + +.WhatChangedKey { + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-small); + line-height: 1; +} +.WhatChangedKey:first-of-type::before { + content: ' ('; +} +.WhatChangedKey::after { + content: ', '; +} +.WhatChangedKey:last-of-type::after { + content: ')'; +} diff --git a/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js b/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js index c83b0bc0..839b95d2 100644 --- a/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js +++ b/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js @@ -1,6 +1,7 @@ // @flow import React, { Fragment, useContext } from 'react'; +import ProfilerStore from 'src/devtools/ProfilerStore'; import { ProfilerContext } from './ProfilerContext'; import { formatDuration, formatTime } from './utils'; import { StoreContext } from '../context'; @@ -29,7 +30,7 @@ export default function SidebarSelectedFiberInfo(_: Props) { }); const listItems = []; - for (let i = 0; i < commitIndices.length; i += 2) { + for (let i = 0; i < commitIndices.length; i++) { const commitIndex = commitIndices[i]; const { duration, timestamp } = profilerStore.getCommitData( @@ -67,9 +68,140 @@ export default function SidebarSelectedFiberInfo(_: Props) { +
- : {listItems} + {listItems.length > 0 && ( + + : {listItems} + + )} + {listItems.length === 0 && ( +
Did not render during this profiling session.
+ )}
); } + +type WhatChangedProps = {| + commitIndex: number, + fiberID: number, + profilerStore: ProfilerStore, + rootID: number, +|}; + +function WhatChanged({ + commitIndex, + fiberID, + profilerStore, + rootID, +}: WhatChangedProps) { + const { changeDescriptions } = profilerStore.getCommitData( + ((rootID: any): number), + commitIndex + ); + if (changeDescriptions === null) { + return null; + } + + const changeDescription = changeDescriptions.get(fiberID); + if (changeDescription == null) { + return null; + } + + if (changeDescription.isFirstMount) { + return ( +
+ +
+ This is the first time the component rendered. +
+
+ ); + } + + const changes = []; + + if (changeDescription.context === true) { + changes.push( +
+ • Context changed +
+ ); + } else if ( + typeof changeDescription.context === 'object' && + changeDescription.context !== null && + changeDescription.context.length !== 0 + ) { + changes.push( +
+ • Context changed: + {changeDescription.context.map(key => ( + + {key} + + ))} +
+ ); + } + + if (changeDescription.didHooksChange) { + changes.push( +
+ • Hooks changed +
+ ); + } + + if ( + changeDescription.props !== null && + changeDescription.props.length !== 0 + ) { + changes.push( +
+ • Props changed: + {changeDescription.props.map(key => ( + + {key} + + ))} +
+ ); + } + + if ( + changeDescription.state !== null && + changeDescription.state.length !== 0 + ) { + changes.push( +
+ • State changed: + {changeDescription.state.map(key => ( + + {key} + + ))} +
+ ); + } + + if (changes.length === 0) { + changes.push( +
+ The parent component rendered. +
+ ); + } + + return ( +
+ + {changes} +
+ ); +} diff --git a/src/devtools/views/Profiler/types.js b/src/devtools/views/Profiler/types.js index 9af30779..c99a79fe 100644 --- a/src/devtools/views/Profiler/types.js +++ b/src/devtools/views/Profiler/types.js @@ -31,7 +31,18 @@ export type SnapshotNode = {| type: ElementType, |}; +export type ChangeDescription = {| + context: Array | boolean | null, + didHooksChange: boolean, + isFirstMount: boolean, + props: Array | null, + state: Array | null, +|}; + export type CommitDataFrontend = {| + // Map of Fiber (ID) to a description of what changed in this commit. + changeDescriptions: Map | null, + // How long was this commit? duration: number, @@ -93,6 +104,7 @@ export type ProfilingDataFrontend = {| |}; export type CommitDataExport = {| + changeDescriptions: Array<[number, ChangeDescription]> | null, duration: number, // Tuple of fiber ID and actual duration fiberActualDurations: Array<[number, number]>, diff --git a/src/devtools/views/Profiler/utils.js b/src/devtools/views/Profiler/utils.js index f192520f..bd4d1e9f 100644 --- a/src/devtools/views/Profiler/utils.js +++ b/src/devtools/views/Profiler/utils.js @@ -58,6 +58,10 @@ export function prepareProfilingDataFrontendFromBackendAndStore( dataForRoots.set(rootID, { commitData: commitData.map((commitDataBackend, commitIndex) => ({ + changeDescriptions: + commitDataBackend.changeDescriptions != null + ? new Map(commitDataBackend.changeDescriptions) + : null, duration: commitDataBackend.duration, fiberActualDurations: new Map( commitDataBackend.fiberActualDurations @@ -109,6 +113,7 @@ export function prepareProfilingDataFrontendFromExport( dataForRoots.set(rootID, { commitData: commitData.map( ({ + changeDescriptions, duration, fiberActualDurations, fiberSelfDurations, @@ -117,6 +122,8 @@ export function prepareProfilingDataFrontendFromExport( screenshot, timestamp, }) => ({ + changeDescriptions: + changeDescriptions != null ? new Map(changeDescriptions) : null, duration, fiberActualDurations: new Map(fiberActualDurations), fiberSelfDurations: new Map(fiberSelfDurations), @@ -159,6 +166,7 @@ export function prepareProfilingDataExport( dataForRoots.push({ commitData: commitData.map( ({ + changeDescriptions, duration, fiberActualDurations, fiberSelfDurations, @@ -167,6 +175,10 @@ export function prepareProfilingDataExport( screenshot, timestamp, }) => ({ + changeDescriptions: + changeDescriptions != null + ? Array.from(changeDescriptions.entries()) + : null, duration, fiberActualDurations: Array.from(fiberActualDurations.entries()), fiberSelfDurations: Array.from(fiberSelfDurations.entries()), diff --git a/src/devtools/views/Settings/Settings.js b/src/devtools/views/Settings/Settings.js index 645ee254..3e29c19d 100644 --- a/src/devtools/views/Settings/Settings.js +++ b/src/devtools/views/Settings/Settings.js @@ -1,6 +1,6 @@ // @flow -import React, { useCallback, useContext, useMemo } from 'react'; +import React, { Fragment, useCallback, useContext, useMemo } from 'react'; import { useSubscription } from '../hooks'; import { StoreContext } from '../context'; import { SettingsContext } from './SettingsContext'; @@ -43,6 +43,20 @@ function Settings(_: {||}) { collapseNodesByDefaultSubscription ); + const recordChangeDescriptionsSubscription = useMemo( + () => ({ + getCurrentValue: () => store.recordChangeDescriptions, + subscribe: (callback: Function) => { + store.addListener('recordChangeDescriptions', callback); + return () => store.removeListener('recordChangeDescriptions', callback); + }, + }), + [store] + ); + const recordChangeDescriptions = useSubscription( + recordChangeDescriptionsSubscription + ); + const updateDisplayDensity = useCallback( ({ currentTarget }) => { setDisplayDensity(currentTarget.value); @@ -69,6 +83,12 @@ function Settings(_: {||}) { }, [store] ); + const updateRecordChangeDescriptions = useCallback( + ({ currentTarget }) => { + store.recordChangeDescriptions = currentTarget.checked; + }, + [store] + ); return (
@@ -145,25 +165,37 @@ function Settings(_: {||}) {
- {store.supportsCaptureScreenshots && ( -
-
Profiler
- - {captureScreenshots && ( -
- Screenshots will be throttled in order to reduce the negative - impact on performance. -
- )} -
- )} +
+
Profiler
+ + + + {store.supportsCaptureScreenshots && ( + + + {captureScreenshots && ( +
+ Screenshots will be throttled in order to reduce the negative + impact on performance. +
+ )} +
+ )} +
); }