From c6de014a9a70d3bff970a3a58df7ac6af28fa51b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 22 May 2019 06:05:25 -0700 Subject: [PATCH] Massively overhauled profiling data architecture --- .../__snapshots__/profiling-test.js.snap | 1106 ------------ .../__snapshots__/profilingCache-test.js.snap | 1495 +++++++++++++++++ .../profilingCharts-test.js.snap | 28 + src/__tests__/profilerStore-test.js | 57 + src/__tests__/profiling-test.js | 574 ------- src/__tests__/profilingCache-test.js | 465 +++++ src/__tests__/profilingCharts-test.js | 90 +- .../profilingCommitTreeBuilder-test.js | 23 +- src/__tests__/profilingUtils-test.js | 20 + src/__tests__/utils.js | 62 +- src/backend/agent.js | 103 +- src/backend/renderer.js | 338 ++-- src/backend/types.js | 72 +- src/constants.js | 2 +- src/devtools/ProfilerStore.js | 370 ++++ src/devtools/ProfilingCache.js | 421 +---- src/devtools/store.js | 255 +-- .../views/Profiler/CommitFlamegraph.js | 43 +- src/devtools/views/Profiler/CommitRanked.js | 43 +- .../views/Profiler/CommitTreeBuilder.js | 110 +- .../views/Profiler/FlamegraphChartBuilder.js | 23 +- .../views/Profiler/InteractionListItem.js | 26 +- src/devtools/views/Profiler/Interactions.js | 32 +- .../Profiler/InteractionsChartBuilder.js | 37 +- .../views/Profiler/ProfilerContext.js | 8 +- .../Profiler/ProfilingImportExportButtons.js | 46 +- .../views/Profiler/RankedChartBuilder.js | 19 +- .../views/Profiler/SidebarCommitInfo.js | 77 +- .../views/Profiler/SidebarInteractions.js | 85 +- .../Profiler/SidebarSelectedFiberInfo.js | 24 +- .../views/Profiler/SnapshotSelector.js | 28 +- src/devtools/views/Profiler/types.js | 164 +- src/devtools/views/Profiler/utils.js | 319 ++-- 33 files changed, 3276 insertions(+), 3289 deletions(-) delete mode 100644 src/__tests__/__snapshots__/profiling-test.js.snap create mode 100644 src/__tests__/__snapshots__/profilingCache-test.js.snap create mode 100644 src/__tests__/profilerStore-test.js delete mode 100644 src/__tests__/profiling-test.js create mode 100644 src/__tests__/profilingCache-test.js create mode 100644 src/__tests__/profilingUtils-test.js create mode 100644 src/devtools/ProfilerStore.js diff --git a/src/__tests__/__snapshots__/profiling-test.js.snap b/src/__tests__/__snapshots__/profiling-test.js.snap deleted file mode 100644 index 314e152dc4..0000000000 --- a/src/__tests__/__snapshots__/profiling-test.js.snap +++ /dev/null @@ -1,1106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 0 1`] = ` -Object { - "actualDurations": Map { - 1 => 12, - 2 => 12, - 3 => 0, - 4 => 1, - 5 => 1, - }, - "commitIndex": 0, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 1 => 0, - 2 => 10, - 3 => 0, - 4 => 1, - 5 => 1, - }, -} -`; - -exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 1 1`] = ` -Object { - "actualDurations": Map { - 3 => 0, - 4 => 1, - 6 => 2, - 2 => 13, - 1 => 13, - }, - "commitIndex": 1, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 4 => 1, - 6 => 2, - 2 => 10, - 1 => 0, - }, -} -`; - -exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 2 1`] = ` -Object { - "actualDurations": Map { - 3 => 0, - 2 => 10, - 1 => 10, - }, - "commitIndex": 2, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 2 => 10, - 1 => 0, - }, -} -`; - -exports[`profiling CommitDetails should be collected for each commit: CommitDetails commitIndex: 3 1`] = ` -Object { - "actualDurations": Map { - 2 => 10, - 1 => 10, - }, - "commitIndex": 3, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 2 => 10, - 1 => 0, - }, -} -`; - -exports[`profiling CommitDetails should be collected for each commit: imported data 1`] = ` -Object { - "commitDetails": Array [ - Object { - "actualDurations": Map { - 1 => 12, - 2 => 12, - 3 => 0, - 4 => 1, - 5 => 1, - }, - "commitIndex": 0, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 1 => 0, - 2 => 10, - 3 => 0, - 4 => 1, - 5 => 1, - }, - }, - Object { - "actualDurations": Map { - 3 => 0, - 4 => 1, - 6 => 2, - 2 => 13, - 1 => 13, - }, - "commitIndex": 1, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 4 => 1, - 6 => 2, - 2 => 10, - 1 => 0, - }, - }, - Object { - "actualDurations": Map { - 3 => 0, - 2 => 10, - 1 => 10, - }, - "commitIndex": 2, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 2 => 10, - 1 => 0, - }, - }, - Object { - "actualDurations": Map { - 2 => 10, - 1 => 10, - }, - "commitIndex": 3, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 2 => 10, - 1 => 0, - }, - }, - ], - "interactions": Object { - "interactions": Array [], - "rootID": 1, - }, - "profilingOperations": Map { - 1 => Array [ - Uint32Array [ - 1, - 1, - 17, - 6, - 80, - 97, - 114, - 101, - 110, - 116, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 48, - 1, - 49, - 1, - 1, - 11, - 1, - 1, - 4, - 1, - 12000, - 1, - 2, - 5, - 1, - 0, - 1, - 0, - 4, - 2, - 12000, - 1, - 3, - 5, - 2, - 2, - 2, - 3, - 4, - 3, - 0, - 1, - 4, - 5, - 2, - 2, - 2, - 4, - 4, - 4, - 1000, - 1, - 5, - 8, - 2, - 2, - 2, - 0, - 4, - 5, - 1000, - ], - Uint32Array [ - 1, - 1, - 8, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 50, - 1, - 6, - 5, - 2, - 2, - 1, - 2, - 4, - 6, - 2000, - 4, - 2, - 14000, - 3, - 2, - 4, - 3, - 4, - 6, - 5, - 4, - 1, - 14000, - ], - Uint32Array [ - 1, - 1, - 0, - 2, - 2, - 6, - 4, - 4, - 2, - 11000, - 3, - 2, - 2, - 3, - 5, - 4, - 1, - 11000, - ], - Uint32Array [ - 1, - 1, - 0, - 2, - 1, - 3, - ], - ], - }, - "profilingSnapshots": Map { - 1 => Map {}, - }, - "profilingSummary": Object { - "commitDurations": Array [ - 12, - 13, - 10, - 10, - ], - "commitTimes": Array [ - 12, - 25, - 35, - 45, - ], - "initialTreeBaseDurations": Map {}, - "interactionCount": 0, - "rootID": 1, - }, - "version": 3, -} -`; - -exports[`profiling CommitDetails should calculate a self duration based on actual children (not filtered children): CommitDetails with filtered self durations 1`] = ` -Object { - "actualDurations": Map { - 1 => 16, - 2 => 16, - 3 => 1, - 5 => 1, - }, - "commitIndex": 0, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 1 => 0, - 2 => 10, - 3 => 1, - 5 => 1, - }, -} -`; - -exports[`profiling CommitDetails should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 1`] = ` -Object { - "actualDurations": Map { - 1 => 15, - 2 => 15, - 3 => 5, - 4 => 2, - }, - "commitIndex": 0, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 1 => 0, - 2 => 10, - 3 => 3, - 4 => 2, - }, -} -`; - -exports[`profiling CommitDetails should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 2`] = ` -Object { - "actualDurations": Map { - 5 => 3, - 3 => 3, - }, - "commitIndex": 1, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 5 => 3, - 3 => 0, - }, -} -`; - -exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 2 1`] = ` -Object { - "commitDurations": Array [ - 0, - 10, - 1, - 10, - 2, - 10, - ], - "fiberID": 2, - "rootID": 1, -} -`; - -exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 3 1`] = ` -Object { - "commitDurations": Array [ - 0, - 0, - 1, - 0, - 2, - 0, - ], - "fiberID": 3, - "rootID": 1, -} -`; - -exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 4 1`] = ` -Object { - "commitDurations": Array [ - 0, - 1, - ], - "fiberID": 4, - "rootID": 1, -} -`; - -exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 5 1`] = ` -Object { - "commitDurations": Array [ - 1, - 1, - 2, - 1, - ], - "fiberID": 5, - "rootID": 1, -} -`; - -exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 6 1`] = ` -Object { - "commitDurations": Array [ - 2, - 2, - ], - "fiberID": 6, - "rootID": 1, -} -`; - -exports[`profiling FiberCommits should be collected for each rendered fiber: imported data 1`] = ` -Object { - "commitDetails": Array [ - Object { - "actualDurations": Map { - 1 => 11, - 2 => 11, - 3 => 0, - 4 => 1, - }, - "commitIndex": 0, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 1 => 0, - 2 => 10, - 3 => 0, - 4 => 1, - }, - }, - Object { - "actualDurations": Map { - 3 => 0, - 5 => 1, - 2 => 11, - 1 => 11, - }, - "commitIndex": 1, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 5 => 1, - 2 => 10, - 1 => 0, - }, - }, - Object { - "actualDurations": Map { - 3 => 0, - 5 => 1, - 6 => 2, - 2 => 13, - 1 => 13, - }, - "commitIndex": 2, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 5 => 1, - 6 => 2, - 2 => 10, - 1 => 0, - }, - }, - ], - "interactions": Object { - "interactions": Array [], - "rootID": 1, - }, - "profilingOperations": Map { - 1 => Array [ - Uint32Array [ - 1, - 1, - 15, - 6, - 80, - 97, - 114, - 101, - 110, - 116, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 48, - 1, - 1, - 11, - 1, - 1, - 4, - 1, - 11000, - 1, - 2, - 5, - 1, - 0, - 1, - 0, - 4, - 2, - 11000, - 1, - 3, - 5, - 2, - 2, - 2, - 3, - 4, - 3, - 0, - 1, - 4, - 8, - 2, - 2, - 2, - 0, - 4, - 4, - 1000, - ], - Uint32Array [ - 1, - 1, - 8, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 49, - 1, - 5, - 5, - 2, - 2, - 1, - 2, - 4, - 5, - 1000, - 4, - 2, - 12000, - 3, - 2, - 3, - 3, - 5, - 4, - 4, - 1, - 12000, - ], - Uint32Array [ - 1, - 1, - 8, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 50, - 1, - 6, - 5, - 2, - 2, - 1, - 2, - 4, - 6, - 2000, - 4, - 2, - 14000, - 3, - 2, - 4, - 3, - 5, - 6, - 4, - 4, - 1, - 14000, - ], - ], - }, - "profilingSnapshots": Map { - 1 => Map {}, - }, - "profilingSummary": Object { - "commitDurations": Array [ - 11, - 11, - 13, - ], - "commitTimes": Array [ - 11, - 22, - 35, - ], - "initialTreeBaseDurations": Map {}, - "interactionCount": 0, - "rootID": 1, - }, - "version": 3, -} -`; - -exports[`profiling Interactions should be collected for every traced interaction: Interactions 1`] = ` -Object { - "interactions": Array [ - Object { - "__count": 1, - "commits": Array [ - 0, - ], - "id": 0, - "name": "mount: one child", - "timestamp": 0, - }, - Object { - "__count": 0, - "commits": Array [ - 1, - ], - "id": 1, - "name": "update: two children", - "timestamp": 11, - }, - ], - "rootID": 1, -} -`; - -exports[`profiling Interactions should be collected for every traced interaction: imported data 1`] = ` -Object { - "commitDetails": Array [ - Object { - "actualDurations": Map { - 1 => 11, - 2 => 11, - 3 => 0, - 4 => 1, - }, - "commitIndex": 0, - "interactions": Array [ - Object { - "__count": 1, - "id": 0, - "name": "mount: one child", - "timestamp": 0, - }, - ], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 1 => 0, - 2 => 10, - 3 => 0, - 4 => 1, - }, - }, - Object { - "actualDurations": Map { - 3 => 0, - 5 => 1, - 2 => 11, - 1 => 11, - }, - "commitIndex": 1, - "interactions": Array [ - Object { - "__count": 0, - "id": 1, - "name": "update: two children", - "timestamp": 11, - }, - ], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 5 => 1, - 2 => 10, - 1 => 0, - }, - }, - ], - "interactions": Object { - "interactions": Array [ - Object { - "__count": 1, - "commits": Array [ - 0, - ], - "id": 0, - "name": "mount: one child", - "timestamp": 0, - }, - Object { - "__count": 0, - "commits": Array [ - 1, - ], - "id": 1, - "name": "update: two children", - "timestamp": 11, - }, - ], - "rootID": 1, - }, - "profilingOperations": Map { - 1 => Array [ - Uint32Array [ - 1, - 1, - 15, - 6, - 80, - 97, - 114, - 101, - 110, - 116, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 48, - 1, - 1, - 11, - 1, - 1, - 4, - 1, - 11000, - 1, - 2, - 5, - 1, - 0, - 1, - 0, - 4, - 2, - 11000, - 1, - 3, - 5, - 2, - 2, - 2, - 3, - 4, - 3, - 0, - 1, - 4, - 8, - 2, - 2, - 2, - 0, - 4, - 4, - 1000, - ], - Uint32Array [ - 1, - 1, - 8, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 49, - 1, - 5, - 5, - 2, - 2, - 1, - 2, - 4, - 5, - 1000, - 4, - 2, - 12000, - 3, - 2, - 3, - 3, - 5, - 4, - 4, - 1, - 12000, - ], - ], - }, - "profilingSnapshots": Map { - 1 => Map {}, - }, - "profilingSummary": Object { - "commitDurations": Array [ - 11, - 11, - ], - "commitTimes": Array [ - 11, - 22, - ], - "initialTreeBaseDurations": Map {}, - "interactionCount": 2, - "rootID": 1, - }, - "version": 3, -} -`; - -exports[`profiling ProfilingSummary should be collected for each commit: ProfilingSummary 1`] = ` -Object { - "commitDurations": Array [ - 13, - 10, - 10, - ], - "commitTimes": Array [ - 13, - 23, - 33, - ], - "initialTreeBaseDurations": Map { - 1 => 12, - 2 => 12, - 3 => 0, - 4 => 1, - 5 => 1, - }, - "interactionCount": 0, - "rootID": 1, -} -`; - -exports[`profiling ProfilingSummary should be collected for each commit: imported data 1`] = ` -Object { - "commitDetails": Array [ - Object { - "actualDurations": Map { - 3 => 0, - 4 => 1, - 6 => 2, - 2 => 13, - 1 => 13, - }, - "commitIndex": 0, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 4 => 1, - 6 => 2, - 2 => 10, - 1 => 0, - }, - }, - Object { - "actualDurations": Map { - 3 => 0, - 2 => 10, - 1 => 10, - }, - "commitIndex": 1, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 3 => 0, - 2 => 10, - 1 => 0, - }, - }, - Object { - "actualDurations": Map { - 2 => 10, - 1 => 10, - }, - "commitIndex": 2, - "interactions": Array [], - "priorityLevel": "Immediate", - "rootID": 1, - "selfDurations": Map { - 2 => 10, - 1 => 0, - }, - }, - ], - "interactions": Object { - "interactions": Array [], - "rootID": 1, - }, - "profilingOperations": Map { - 1 => Array [ - Uint32Array [ - 1, - 1, - 8, - 5, - 67, - 104, - 105, - 108, - 100, - 1, - 50, - 1, - 6, - 5, - 2, - 2, - 1, - 2, - 4, - 6, - 2000, - 4, - 2, - 14000, - 3, - 2, - 4, - 3, - 4, - 6, - 5, - 4, - 1, - 14000, - ], - Uint32Array [ - 1, - 1, - 0, - 2, - 2, - 6, - 4, - 4, - 2, - 11000, - 3, - 2, - 2, - 3, - 5, - 4, - 1, - 11000, - ], - Uint32Array [ - 1, - 1, - 0, - 2, - 1, - 3, - ], - ], - }, - "profilingSnapshots": Map { - 1 => Map { - 1 => Object { - "children": Array [ - 2, - ], - "displayName": null, - "id": 1, - "key": null, - "type": 11, - }, - 2 => Object { - "children": Array [ - 3, - 4, - 5, - ], - "displayName": "Parent", - "id": 2, - "key": null, - "type": 5, - }, - 3 => Object { - "children": Array [], - "displayName": "Child", - "id": 3, - "key": "0", - "type": 5, - }, - 4 => Object { - "children": Array [], - "displayName": "Child", - "id": 4, - "key": "1", - "type": 5, - }, - 5 => Object { - "children": Array [], - "displayName": "Child", - "id": 5, - "key": null, - "type": 8, - }, - }, - }, - "profilingSummary": Object { - "commitDurations": Array [ - 13, - 10, - 10, - ], - "commitTimes": Array [ - 13, - 23, - 33, - ], - "initialTreeBaseDurations": Map { - 1 => 12, - 2 => 12, - 3 => 0, - 4 => 1, - 5 => 1, - }, - "interactionCount": 0, - "rootID": 1, - }, - "version": 3, -} -`; diff --git a/src/__tests__/__snapshots__/profilingCache-test.js.snap b/src/__tests__/__snapshots__/profilingCache-test.js.snap new file mode 100644 index 0000000000..a6f2d1c4d6 --- /dev/null +++ b/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -0,0 +1,1495 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProfilingCache should calculate a self duration based on actual children (not filtered children): CommitDetails with filtered self durations 1`] = ` +Object { + "duration": 16, + "fiberActualDurations": Map { + 1 => 16, + 2 => 16, + 3 => 1, + 5 => 1, + }, + "fiberSelfDurations": Map { + 1 => 0, + 2 => 10, + 3 => 1, + 5 => 1, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 16, +} +`; + +exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 1`] = ` +Object { + "duration": 15, + "fiberActualDurations": Map { + 1 => 15, + 2 => 15, + 3 => 5, + 4 => 2, + }, + "fiberSelfDurations": Map { + 1 => 0, + 2 => 10, + 3 => 3, + 4 => 2, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 15, +} +`; + +exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 2`] = ` +Object { + "duration": 3, + "fiberActualDurations": Map { + 5 => 3, + 3 => 3, + }, + "fiberSelfDurations": Map { + 5 => 3, + 3 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 18, +} +`; + +exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 0 1`] = ` +Object { + "duration": 12, + "fiberActualDurations": Map { + 1 => 12, + 2 => 12, + 3 => 0, + 4 => 1, + 5 => 1, + }, + "fiberSelfDurations": Map { + 1 => 0, + 2 => 10, + 3 => 0, + 4 => 1, + 5 => 1, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 12, +} +`; + +exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 1 1`] = ` +Object { + "duration": 13, + "fiberActualDurations": Map { + 3 => 0, + 4 => 1, + 6 => 2, + 2 => 13, + 1 => 13, + }, + "fiberSelfDurations": Map { + 3 => 0, + 4 => 1, + 6 => 2, + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 25, +} +`; + +exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 2 1`] = ` +Object { + "duration": 10, + "fiberActualDurations": Map { + 3 => 0, + 2 => 10, + 1 => 10, + }, + "fiberSelfDurations": Map { + 3 => 0, + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 35, +} +`; + +exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 3 1`] = ` +Object { + "duration": 10, + "fiberActualDurations": Map { + 2 => 10, + 1 => 10, + }, + "fiberSelfDurations": Map { + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 45, +} +`; + +exports[`ProfilingCache should collect data for each commit: imported data 1`] = ` +"{ + \\"version\\": 5, + \\"dataForRoots\\": [ + { + \\"commitData\\": [ + { + \\"duration\\": 12, + \\"fiberActualDurations\\": [ + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 5, + 1 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 1, + 0 + ], + [ + 2, + 10 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 5, + 1 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 12 + }, + { + \\"duration\\": 13, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 6, + 2 + ], + [ + 2, + 13 + ], + [ + 1, + 13 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 6, + 2 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 25 + }, + { + \\"duration\\": 10, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 2, + 10 + ], + [ + 1, + 10 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 35 + }, + { + \\"duration\\": 10, + \\"fiberActualDurations\\": [ + [ + 2, + 10 + ], + [ + 1, + 10 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 45 + } + ], + \\"displayName\\": \\"Parent\\", + \\"initialTreeBaseDurations\\": [], + \\"interactionCommits\\": [], + \\"interactions\\": [], + \\"operations\\": [ + [ + 1, + 1, + 17, + 6, + 80, + 97, + 114, + 101, + 110, + 116, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 48, + 1, + 49, + 1, + 1, + 11, + 1, + 1, + 4, + 1, + 12000, + 1, + 2, + 5, + 1, + 0, + 1, + 0, + 4, + 2, + 12000, + 1, + 3, + 5, + 2, + 2, + 2, + 3, + 4, + 3, + 0, + 1, + 4, + 5, + 2, + 2, + 2, + 4, + 4, + 4, + 1000, + 1, + 5, + 8, + 2, + 2, + 2, + 0, + 4, + 5, + 1000 + ], + [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 50, + 1, + 6, + 5, + 2, + 2, + 1, + 2, + 4, + 6, + 2000, + 4, + 2, + 14000, + 3, + 2, + 4, + 3, + 4, + 6, + 5, + 4, + 1, + 14000 + ], + [ + 1, + 1, + 0, + 2, + 2, + 6, + 4, + 4, + 2, + 11000, + 3, + 2, + 2, + 3, + 5, + 4, + 1, + 11000 + ], + [ + 1, + 1, + 0, + 2, + 1, + 3 + ] + ], + \\"rootID\\": 1, + \\"snapshots\\": [] + } + ] +}" +`; + +exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 2 1`] = ` +Array [ + 0, + 1, + 2, +] +`; + +exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 3 1`] = ` +Array [ + 0, + 1, + 2, +] +`; + +exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 4 1`] = ` +Array [ + 0, +] +`; + +exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 5 1`] = ` +Array [ + 1, + 2, +] +`; + +exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 6 1`] = ` +Array [ + 2, +] +`; + +exports[`ProfilingCache should collect data for each rendered fiber: imported data 1`] = ` +"{ + \\"version\\": 5, + \\"dataForRoots\\": [ + { + \\"commitData\\": [ + { + \\"duration\\": 11, + \\"fiberActualDurations\\": [ + [ + 1, + 11 + ], + [ + 2, + 11 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 1, + 0 + ], + [ + 2, + 10 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 11 + }, + { + \\"duration\\": 11, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 5, + 1 + ], + [ + 2, + 11 + ], + [ + 1, + 11 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 5, + 1 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 22 + }, + { + \\"duration\\": 13, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 5, + 1 + ], + [ + 6, + 2 + ], + [ + 2, + 13 + ], + [ + 1, + 13 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 5, + 1 + ], + [ + 6, + 2 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 35 + } + ], + \\"displayName\\": \\"Parent\\", + \\"initialTreeBaseDurations\\": [], + \\"interactionCommits\\": [], + \\"interactions\\": [], + \\"operations\\": [ + [ + 1, + 1, + 15, + 6, + 80, + 97, + 114, + 101, + 110, + 116, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 48, + 1, + 1, + 11, + 1, + 1, + 4, + 1, + 11000, + 1, + 2, + 5, + 1, + 0, + 1, + 0, + 4, + 2, + 11000, + 1, + 3, + 5, + 2, + 2, + 2, + 3, + 4, + 3, + 0, + 1, + 4, + 8, + 2, + 2, + 2, + 0, + 4, + 4, + 1000 + ], + [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 49, + 1, + 5, + 5, + 2, + 2, + 1, + 2, + 4, + 5, + 1000, + 4, + 2, + 12000, + 3, + 2, + 3, + 3, + 5, + 4, + 4, + 1, + 12000 + ], + [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 50, + 1, + 6, + 5, + 2, + 2, + 1, + 2, + 4, + 6, + 2000, + 4, + 2, + 14000, + 3, + 2, + 4, + 3, + 5, + 6, + 4, + 4, + 1, + 14000 + ] + ], + \\"rootID\\": 1, + \\"snapshots\\": [] + } + ] +}" +`; + +exports[`ProfilingCache should collect data for each root: ProfilingSummary 1`] = ` +Object { + "commitData": Array [ + Object { + "duration": 13, + "fiberActualDurations": Map { + 3 => 0, + 4 => 1, + 6 => 2, + 2 => 13, + 1 => 13, + }, + "fiberSelfDurations": Map { + 3 => 0, + 4 => 1, + 6 => 2, + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 13, + }, + Object { + "duration": 10, + "fiberActualDurations": Map { + 3 => 0, + 2 => 10, + 1 => 10, + }, + "fiberSelfDurations": Map { + 3 => 0, + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 23, + }, + Object { + "duration": 10, + "fiberActualDurations": Map { + 2 => 10, + 1 => 10, + }, + "fiberSelfDurations": Map { + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 33, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Map { + 1 => 12, + 2 => 12, + 3 => 0, + 4 => 1, + 5 => 1, + }, + "interactionCommits": Map {}, + "interactions": Map {}, + "operations": Array [ + Uint32Array [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 50, + 1, + 6, + 5, + 2, + 2, + 1, + 2, + 4, + 6, + 2000, + 4, + 2, + 14000, + 3, + 2, + 4, + 3, + 4, + 6, + 5, + 4, + 1, + 14000, + ], + Uint32Array [ + 1, + 1, + 0, + 2, + 2, + 6, + 4, + 4, + 2, + 11000, + 3, + 2, + 2, + 3, + 5, + 4, + 1, + 11000, + ], + Uint32Array [ + 1, + 1, + 0, + 2, + 1, + 3, + ], + ], + "rootID": 1, + "snapshots": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "type": 11, + }, + 2 => Object { + "children": Array [ + 3, + 4, + 5, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "type": 5, + }, + 3 => Object { + "children": Array [], + "displayName": "Child", + "id": 3, + "key": "0", + "type": 5, + }, + 4 => Object { + "children": Array [], + "displayName": "Child", + "id": 4, + "key": "1", + "type": 5, + }, + 5 => Object { + "children": Array [], + "displayName": "Child", + "id": 5, + "key": null, + "type": 8, + }, + }, +} +`; + +exports[`ProfilingCache should collect data for each root: imported data 1`] = ` +"{ + \\"version\\": 5, + \\"dataForRoots\\": [ + { + \\"commitData\\": [ + { + \\"duration\\": 13, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 6, + 2 + ], + [ + 2, + 13 + ], + [ + 1, + 13 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 6, + 2 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 13 + }, + { + \\"duration\\": 10, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 2, + 10 + ], + [ + 1, + 10 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 23 + }, + { + \\"duration\\": 10, + \\"fiberActualDurations\\": [ + [ + 2, + 10 + ], + [ + 1, + 10 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 33 + } + ], + \\"displayName\\": \\"Parent\\", + \\"initialTreeBaseDurations\\": [ + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ], + [ + 5, + 1 + ] + ], + \\"interactionCommits\\": [], + \\"interactions\\": [], + \\"operations\\": [ + [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 50, + 1, + 6, + 5, + 2, + 2, + 1, + 2, + 4, + 6, + 2000, + 4, + 2, + 14000, + 3, + 2, + 4, + 3, + 4, + 6, + 5, + 4, + 1, + 14000 + ], + [ + 1, + 1, + 0, + 2, + 2, + 6, + 4, + 4, + 2, + 11000, + 3, + 2, + 2, + 3, + 5, + 4, + 1, + 11000 + ], + [ + 1, + 1, + 0, + 2, + 1, + 3 + ] + ], + \\"rootID\\": 1, + \\"snapshots\\": [ + [ + 1, + { + \\"id\\": 1, + \\"children\\": [ + 2 + ], + \\"displayName\\": null, + \\"key\\": null, + \\"type\\": 11 + } + ], + [ + 2, + { + \\"id\\": 2, + \\"children\\": [ + 3, + 4, + 5 + ], + \\"displayName\\": \\"Parent\\", + \\"key\\": null, + \\"type\\": 5 + } + ], + [ + 3, + { + \\"id\\": 3, + \\"children\\": [], + \\"displayName\\": \\"Child\\", + \\"key\\": \\"0\\", + \\"type\\": 5 + } + ], + [ + 4, + { + \\"id\\": 4, + \\"children\\": [], + \\"displayName\\": \\"Child\\", + \\"key\\": \\"1\\", + \\"type\\": 5 + } + ], + [ + 5, + { + \\"id\\": 5, + \\"children\\": [], + \\"displayName\\": \\"Child\\", + \\"key\\": null, + \\"type\\": 8 + } + ] + ] + } + ] +}" +`; + +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`] = ` +"{ + \\"version\\": 5, + \\"dataForRoots\\": [ + { + \\"commitData\\": [ + { + \\"duration\\": 11, + \\"fiberActualDurations\\": [ + [ + 1, + 11 + ], + [ + 2, + 11 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 1, + 0 + ], + [ + 2, + 10 + ], + [ + 3, + 0 + ], + [ + 4, + 1 + ] + ], + \\"interactionIDs\\": [ + 0 + ], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 11 + }, + { + \\"duration\\": 11, + \\"fiberActualDurations\\": [ + [ + 3, + 0 + ], + [ + 5, + 1 + ], + [ + 2, + 11 + ], + [ + 1, + 11 + ] + ], + \\"fiberSelfDurations\\": [ + [ + 3, + 0 + ], + [ + 5, + 1 + ], + [ + 2, + 10 + ], + [ + 1, + 0 + ] + ], + \\"interactionIDs\\": [ + 1 + ], + \\"priorityLevel\\": \\"Immediate\\", + \\"screenshot\\": null, + \\"timestamp\\": 22 + } + ], + \\"displayName\\": \\"Parent\\", + \\"initialTreeBaseDurations\\": [], + \\"interactionCommits\\": [ + [ + 0, + [ + 0 + ] + ], + [ + 1, + [ + 1 + ] + ] + ], + \\"interactions\\": [ + [ + 0, + { + \\"__count\\": 1, + \\"id\\": 0, + \\"name\\": \\"mount: one child\\", + \\"timestamp\\": 0 + } + ], + [ + 1, + { + \\"__count\\": 0, + \\"id\\": 1, + \\"name\\": \\"update: two children\\", + \\"timestamp\\": 11 + } + ] + ], + \\"operations\\": [ + [ + 1, + 1, + 15, + 6, + 80, + 97, + 114, + 101, + 110, + 116, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 48, + 1, + 1, + 11, + 1, + 1, + 4, + 1, + 11000, + 1, + 2, + 5, + 1, + 0, + 1, + 0, + 4, + 2, + 11000, + 1, + 3, + 5, + 2, + 2, + 2, + 3, + 4, + 3, + 0, + 1, + 4, + 8, + 2, + 2, + 2, + 0, + 4, + 4, + 1000 + ], + [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 49, + 1, + 5, + 5, + 2, + 2, + 1, + 2, + 4, + 5, + 1000, + 4, + 2, + 12000, + 3, + 2, + 3, + 3, + 5, + 4, + 4, + 1, + 12000 + ] + ], + \\"rootID\\": 1, + \\"snapshots\\": [] + } + ] +}" +`; diff --git a/src/__tests__/__snapshots__/profilingCharts-test.js.snap b/src/__tests__/__snapshots__/profilingCharts-test.js.snap index fb8003ce23..8399be81c6 100644 --- a/src/__tests__/__snapshots__/profilingCharts-test.js.snap +++ b/src/__tests__/__snapshots__/profilingCharts-test.js.snap @@ -247,6 +247,20 @@ Object { exports[`profiling charts interactions should contain valid data: Interactions 1`] = ` Object { + "interactions": Array [ + Object { + "__count": 1, + "id": 0, + "name": "mount", + "timestamp": 0, + }, + Object { + "__count": 0, + "id": 1, + "name": "update", + "timestamp": 15, + }, + ], "lastInteractionTime": 25, "maxCommitDuration": 15, } @@ -254,6 +268,20 @@ Object { exports[`profiling charts interactions should contain valid data: Interactions 2`] = ` Object { + "interactions": Array [ + Object { + "__count": 1, + "id": 0, + "name": "mount", + "timestamp": 0, + }, + Object { + "__count": 0, + "id": 1, + "name": "update", + "timestamp": 15, + }, + ], "lastInteractionTime": 25, "maxCommitDuration": 15, } diff --git a/src/__tests__/profilerStore-test.js b/src/__tests__/profilerStore-test.js new file mode 100644 index 0000000000..d2f7eefef7 --- /dev/null +++ b/src/__tests__/profilerStore-test.js @@ -0,0 +1,57 @@ +// @flow + +import type Store from 'src/devtools/store'; + +describe('ProfilerStore', () => { + let React; + let ReactDOM; + let store: Store; + let utils; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + }); + + it('should not remove profiling data when roots are unmounted', async () => { + const Parent = ({ count }) => + new Array(count) + .fill(true) + .map((_, index) => ); + const Child = () =>
Hi!
; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + utils.act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + + utils.act(() => store.startProfiling()); + + utils.act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + + utils.act(() => store.stopProfiling()); + + const rootA = store.roots[0]; + const rootB = store.roots[1]; + + utils.act(() => ReactDOM.unmountComponentAtNode(containerB)); + + expect(store.profilerStore.getDataForRoot(rootA)).not.toBeNull(); + + utils.act(() => ReactDOM.unmountComponentAtNode(containerA)); + + expect(store.profilerStore.getDataForRoot(rootB)).not.toBeNull(); + }); +}); diff --git a/src/__tests__/profiling-test.js b/src/__tests__/profiling-test.js deleted file mode 100644 index d992e72a88..0000000000 --- a/src/__tests__/profiling-test.js +++ /dev/null @@ -1,574 +0,0 @@ -// @flow - -import typeof ReactTestRenderer from 'react-test-renderer'; -import type Bridge from 'src/bridge'; -import type Store from 'src/devtools/store'; - -describe('profiling', () => { - let React; - let ReactDOM; - let Scheduler; - let SchedulerTracing; - let TestRenderer: ReactTestRenderer; - let bridge: Bridge; - let store: Store; - let utils; - - beforeEach(() => { - utils = require('./utils'); - utils.beforeEachProfiling(); - - bridge = global.bridge; - store = global.store; - store.collapseNodesByDefault = false; - - React = require('react'); - ReactDOM = require('react-dom'); - Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); - TestRenderer = utils.requireTestRenderer(); - }); - - it('should throw if importing older/unsupported data', () => { - const { - prepareImportedProfilingData, - } = require('src/devtools/views/Profiler/utils'); - expect(() => - prepareImportedProfilingData( - JSON.stringify({ - version: 0, - }) - ) - ).toThrow('Unsupported profiler export version "0"'); - }); - - describe('ProfilingSummary', () => { - it('should be collected for each commit', async done => { - const Parent = ({ count }) => { - Scheduler.advanceTime(10); - const children = new Array(count) - .fill(true) - .map((_, index) => ); - return ( - - {children} - - - ); - }; - const Child = ({ duration }) => { - Scheduler.advanceTime(duration); - return null; - }; - const MemoizedChild = React.memo(Child); - - const container = document.createElement('div'); - - utils.act(() => ReactDOM.render(, container)); - utils.act(() => store.startProfiling()); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => store.stopProfiling()); - - let profilingSummary = null; - - function Suspender({ previousPofilingSummary, rendererID, rootID }) { - profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, - rootID, - }); - if (previousPofilingSummary != null) { - expect(profilingSummary).toEqual(previousPofilingSummary); - } else { - expect(profilingSummary).toMatchSnapshot('ProfilingSummary'); - } - return null; - } - - const rendererID = utils.getRendererID(); - const rootID = store.roots[0]; - - await utils.actAsync(() => - TestRenderer.create( - - - - ) - ); - - expect(profilingSummary).not.toBeNull(); - - utils.exportImportHelper(bridge, store, rendererID, rootID); - - await utils.actAsync(() => - TestRenderer.create( - - - - ) - ); - - done(); - }); - }); - - describe('CommitDetails', () => { - it('should be collected for each commit', async done => { - const Parent = ({ count }) => { - Scheduler.advanceTime(10); - const children = new Array(count) - .fill(true) - .map((_, index) => ); - return ( - - {children} - - - ); - }; - const Child = ({ duration }) => { - Scheduler.advanceTime(duration); - return null; - }; - const MemoizedChild = React.memo(Child); - - const container = document.createElement('div'); - - utils.act(() => store.startProfiling()); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => store.stopProfiling()); - - const allCommitDetails = []; - - function Suspender({ - commitIndex, - previousCommitDetails, - rendererID, - rootID, - }) { - const commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - if (previousCommitDetails != null) { - expect(commitDetails).toEqual(previousCommitDetails); - } else { - allCommitDetails.push(commitDetails); - expect(commitDetails).toMatchSnapshot( - `CommitDetails commitIndex: ${commitIndex}` - ); - } - return null; - } - - const rendererID = utils.getRendererID(); - const rootID = store.roots[0]; - - for (let commitIndex = 0; commitIndex < 4; commitIndex++) { - await utils.actAsync(() => { - TestRenderer.create( - - - - ); - }); - } - - expect(allCommitDetails).toHaveLength(4); - - utils.exportImportHelper(bridge, store, rendererID, rootID); - - for (let commitIndex = 0; commitIndex < 4; commitIndex++) { - await utils.actAsync(() => { - TestRenderer.create( - - - - ); - }); - } - - done(); - }); - - it('should calculate a self duration based on actual children (not filtered children)', async done => { - store.componentFilters = [utils.createDisplayNameFilter('^Parent$')]; - - const Grandparent = () => { - Scheduler.advanceTime(10); - return ( - - - - - ); - }; - const Parent = () => { - Scheduler.advanceTime(2); - return ; - }; - const Child = () => { - Scheduler.advanceTime(1); - return null; - }; - - utils.act(() => store.startProfiling()); - utils.act(() => - ReactDOM.render(, document.createElement('div')) - ); - utils.act(() => store.stopProfiling()); - - let commitDetails = null; - - function Suspender({ commitIndex, rendererID, rootID }) { - commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - expect(commitDetails).toMatchSnapshot( - `CommitDetails with filtered self durations` - ); - return null; - } - - const rendererID = utils.getRendererID(); - const rootID = store.roots[0]; - - await utils.actAsync(() => { - TestRenderer.create( - - - - ); - }); - - expect(commitDetails).not.toBeNull(); - - done(); - }); - - it('should calculate self duration correctly for suspended views', async done => { - let data; - const getData = () => { - if (data) { - return data; - } else { - throw new Promise(resolve => { - data = 'abc'; - resolve(data); - }); - } - }; - - const Parent = () => { - Scheduler.advanceTime(10); - return ( - }> - - - ); - }; - const Fallback = () => { - Scheduler.advanceTime(2); - return 'Fallback...'; - }; - const Async = () => { - Scheduler.advanceTime(3); - const data = getData(); - return data; - }; - - utils.act(() => store.startProfiling()); - await utils.actAsync(() => - ReactDOM.render(, document.createElement('div')) - ); - utils.act(() => store.stopProfiling()); - - const allCommitDetails = []; - - function Suspender({ commitIndex, rendererID, rootID }) { - const commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - allCommitDetails.push(commitDetails); - expect(commitDetails).toMatchSnapshot( - `CommitDetails with filtered self durations` - ); - return null; - } - - const rendererID = utils.getRendererID(); - const rootID = store.roots[0]; - - for (let commitIndex = 0; commitIndex < 2; commitIndex++) { - await utils.actAsync(() => { - TestRenderer.create( - - - - ); - }); - } - - expect(allCommitDetails).toHaveLength(2); - - done(); - }); - }); - - describe('FiberCommits', () => { - it('should be collected for each rendered fiber', async done => { - const Parent = ({ count }) => { - Scheduler.advanceTime(10); - const children = new Array(count) - .fill(true) - .map((_, index) => ); - return ( - - {children} - - - ); - }; - const Child = ({ duration }) => { - Scheduler.advanceTime(duration); - return null; - }; - const MemoizedChild = React.memo(Child); - - const container = document.createElement('div'); - - utils.act(() => store.startProfiling()); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => ReactDOM.render(, container)); - utils.act(() => store.stopProfiling()); - - const allFiberCommits = []; - - function Suspender({ - fiberID, - previousFiberCommits, - rendererID, - rootID, - }) { - const fiberCommits = store.profilingCache.FiberCommits.read({ - fiberID, - rendererID, - rootID, - }); - if (previousFiberCommits != null) { - expect(fiberCommits).toEqual(previousFiberCommits); - } else { - allFiberCommits.push(fiberCommits); - expect(fiberCommits).toMatchSnapshot( - `FiberCommits: element ${fiberID}` - ); - } - return null; - } - - const rendererID = utils.getRendererID(); - const rootID = store.roots[0]; - - for (let index = 0; index < store.numElements; index++) { - await utils.actAsync(() => { - const fiberID = store.getElementIDAtIndex(index); - if (fiberID == null) { - throw Error(`Unexpected null ID for element at index ${index}`); - } - TestRenderer.create( - - - - ); - }); - } - - expect(allFiberCommits).toHaveLength(store.numElements); - - utils.exportImportHelper(bridge, store, rendererID, rootID); - - for (let index = 0; index < store.numElements; index++) { - await utils.actAsync(() => { - const fiberID = store.getElementIDAtIndex(index); - if (fiberID == null) { - throw Error(`Unexpected null ID for element at index ${index}`); - } - TestRenderer.create( - - - - ); - }); - } - - done(); - }); - }); - - describe('Interactions', () => { - it('should be collected for every traced interaction', async done => { - const Parent = ({ count }) => { - Scheduler.advanceTime(10); - const children = new Array(count) - .fill(true) - .map((_, index) => ); - return ( - - {children} - - - ); - }; - const Child = ({ duration }) => { - Scheduler.advanceTime(duration); - return null; - }; - const MemoizedChild = React.memo(Child); - - const container = document.createElement('div'); - - utils.act(() => store.startProfiling()); - utils.act(() => - SchedulerTracing.unstable_trace( - 'mount: one child', - Scheduler.unstable_now(), - () => ReactDOM.render(, container) - ) - ); - utils.act(() => - SchedulerTracing.unstable_trace( - 'update: two children', - Scheduler.unstable_now(), - () => ReactDOM.render(, container) - ) - ); - utils.act(() => store.stopProfiling()); - - let interactions = null; - - function Suspender({ previousInteractions, rendererID, rootID }) { - interactions = store.profilingCache.Interactions.read({ - rendererID, - rootID, - }); - if (previousInteractions != null) { - expect(interactions).toEqual(previousInteractions); - } else { - expect(interactions).toMatchSnapshot('Interactions'); - } - return null; - } - - const rendererID = utils.getRendererID(); - const rootID = store.roots[0]; - - await utils.actAsync(() => - TestRenderer.create( - - - - ) - ); - - expect(interactions).not.toBeNull(); - - utils.exportImportHelper(bridge, store, rendererID, rootID); - - await utils.actAsync(() => - TestRenderer.create( - - - - ) - ); - - done(); - }); - }); - - it('should remove profiling data when roots are unmounted', async () => { - const Parent = ({ count }) => - new Array(count) - .fill(true) - .map((_, index) => ); - const Child = () =>
Hi!
; - - const containerA = document.createElement('div'); - const containerB = document.createElement('div'); - - utils.act(() => { - ReactDOM.render(, containerA); - ReactDOM.render(, containerB); - }); - - utils.act(() => store.startProfiling()); - - utils.act(() => { - ReactDOM.render(, containerA); - ReactDOM.render(, containerB); - }); - - utils.act(() => ReactDOM.unmountComponentAtNode(containerB)); - - utils.act(() => ReactDOM.unmountComponentAtNode(containerA)); - - utils.act(() => store.stopProfiling()); - - // Assert all maps are empty - store.assertExpectedRootMapSizes(); - }); -}); diff --git a/src/__tests__/profilingCache-test.js b/src/__tests__/profilingCache-test.js new file mode 100644 index 0000000000..04e4a9fdda --- /dev/null +++ b/src/__tests__/profilingCache-test.js @@ -0,0 +1,465 @@ +// @flow + +import typeof ReactTestRenderer from 'react-test-renderer'; +import type Bridge from 'src/bridge'; +import type Store from 'src/devtools/store'; + +describe('ProfilingCache', () => { + let React; + let ReactDOM; + let Scheduler; + let SchedulerTracing; + let TestRenderer: ReactTestRenderer; + let bridge: Bridge; + let store: Store; + let utils; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + bridge = global.bridge; + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + SchedulerTracing = require('scheduler/tracing'); + TestRenderer = utils.requireTestRenderer(); + }); + + it('should collect data for each root', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + const children = new Array(count) + .fill(true) + .map((_, index) => ); + return ( + + {children} + + + ); + }; + const Child = ({ duration }) => { + Scheduler.advanceTime(duration); + return null; + }; + const MemoizedChild = React.memo(Child); + + const container = document.createElement('div'); + + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + let profilingDataForRoot = null; + + // TODO (profarc) Add multi roots + + function Suspender({ previousProfilingDataForRoot, rootID }) { + profilingDataForRoot = store.profilerStore.getDataForRoot(rootID); + if (previousProfilingDataForRoot != null) { + expect(profilingDataForRoot).toEqual(previousProfilingDataForRoot); + } else { + expect(profilingDataForRoot).toMatchSnapshot('ProfilingSummary'); + } + return null; + } + + const rootID = store.roots[0]; + + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + expect(profilingDataForRoot).not.toBeNull(); + + utils.exportImportHelper(bridge, store, rootID); + + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + done(); + }); + + it('should collect data for each commit', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + const children = new Array(count) + .fill(true) + .map((_, index) => ); + return ( + + {children} + + + ); + }; + const Child = ({ duration }) => { + Scheduler.advanceTime(duration); + return null; + }; + const MemoizedChild = React.memo(Child); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + const allCommitData = []; + + function Suspender({ 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 < 4; commitIndex++) { + await utils.actAsync(() => { + TestRenderer.create( + + + + ); + }); + } + + expect(allCommitData).toHaveLength(4); + + utils.exportImportHelper(bridge, store, rootID); + + for (let commitIndex = 0; commitIndex < 4; commitIndex++) { + await utils.actAsync(() => { + TestRenderer.create( + + + + ); + }); + } + + done(); + }); + + it('should calculate a self duration based on actual children (not filtered children)', async done => { + store.componentFilters = [utils.createDisplayNameFilter('^Parent$')]; + + const Grandparent = () => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + const Parent = () => { + Scheduler.advanceTime(2); + return ; + }; + const Child = () => { + Scheduler.advanceTime(1); + return null; + }; + + utils.act(() => store.startProfiling()); + utils.act(() => + ReactDOM.render(, document.createElement('div')) + ); + utils.act(() => store.stopProfiling()); + + let commitData = null; + + function Suspender({ commitIndex, rootID }) { + commitData = store.profilerStore.getCommitData(rootID, commitIndex); + expect(commitData).toMatchSnapshot( + `CommitDetails with filtered self durations` + ); + return null; + } + + const rootID = store.roots[0]; + + await utils.actAsync(() => { + TestRenderer.create( + + + + ); + }); + + expect(commitData).not.toBeNull(); + + done(); + }); + + it('should calculate self duration correctly for suspended views', async done => { + let data; + const getData = () => { + if (data) { + return data; + } else { + throw new Promise(resolve => { + data = 'abc'; + resolve(data); + }); + } + }; + + const Parent = () => { + Scheduler.advanceTime(10); + return ( + }> + + + ); + }; + const Fallback = () => { + Scheduler.advanceTime(2); + return 'Fallback...'; + }; + const Async = () => { + Scheduler.advanceTime(3); + const data = getData(); + return data; + }; + + utils.act(() => store.startProfiling()); + await utils.actAsync(() => + ReactDOM.render(, document.createElement('div')) + ); + utils.act(() => store.stopProfiling()); + + const allCommitData = []; + + function Suspender({ commitIndex, rootID }) { + const commitData = store.profilerStore.getCommitData(rootID, commitIndex); + allCommitData.push(commitData); + expect(commitData).toMatchSnapshot( + `CommitDetails with filtered self durations` + ); + return null; + } + + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + await utils.actAsync(() => { + TestRenderer.create( + + + + ); + }); + } + + expect(allCommitData).toHaveLength(2); + + done(); + }); + + it('should collect data for each rendered fiber', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + const children = new Array(count) + .fill(true) + .map((_, index) => ); + return ( + + {children} + + + ); + }; + const Child = ({ duration }) => { + Scheduler.advanceTime(duration); + return null; + }; + const MemoizedChild = React.memo(Child); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.stopProfiling()); + + const allFiberCommits = []; + + function Suspender({ fiberID, previousFiberCommits, rootID }) { + const fiberCommits = store.profilingCache.getFiberCommits({ + fiberID, + rootID, + }); + if (previousFiberCommits != null) { + expect(fiberCommits).toEqual(previousFiberCommits); + } else { + allFiberCommits.push(fiberCommits); + expect(fiberCommits).toMatchSnapshot( + `FiberCommits: element ${fiberID}` + ); + } + return null; + } + + const rootID = store.roots[0]; + + for (let index = 0; index < store.numElements; index++) { + await utils.actAsync(() => { + const fiberID = store.getElementIDAtIndex(index); + if (fiberID == null) { + throw Error(`Unexpected null ID for element at index ${index}`); + } + TestRenderer.create( + + + + ); + }); + } + + expect(allFiberCommits).toHaveLength(store.numElements); + + utils.exportImportHelper(bridge, store, rootID); + + for (let index = 0; index < store.numElements; index++) { + await utils.actAsync(() => { + const fiberID = store.getElementIDAtIndex(index); + if (fiberID == null) { + throw Error(`Unexpected null ID for element at index ${index}`); + } + TestRenderer.create( + + + + ); + }); + } + + done(); + }); + + it('should report every traced interaction', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + const children = new Array(count) + .fill(true) + .map((_, index) => ); + return ( + + {children} + + + ); + }; + const Child = ({ duration }) => { + Scheduler.advanceTime(duration); + return null; + }; + const MemoizedChild = React.memo(Child); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace( + 'mount: one child', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update: two children', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let interactions = null; + + function Suspender({ previousInteractions, rootID }) { + interactions = store.profilingCache.getInteractionsChartData({ + rootID, + }).interactions; + if (previousInteractions != null) { + expect(interactions).toEqual(previousInteractions); + } else { + expect(interactions).toMatchSnapshot('Interactions'); + } + return null; + } + + const rootID = store.roots[0]; + + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + expect(interactions).not.toBeNull(); + + utils.exportImportHelper(bridge, store, rootID); + + await utils.actAsync(() => + TestRenderer.create( + + + + ) + ); + + done(); + }); +}); diff --git a/src/__tests__/profilingCharts-test.js b/src/__tests__/profilingCharts-test.js index 62e2c40b30..33d698600b 100644 --- a/src/__tests__/profilingCharts-test.js +++ b/src/__tests__/profilingCharts-test.js @@ -62,59 +62,45 @@ describe('profiling charts', () => { ); utils.act(() => store.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, - rootID, - }); - const commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - suspenseResolved = true; + function Suspender({ commitIndex, rootID }) { const commitTree = store.profilingCache.getCommitTree({ commitIndex, - profilingSummary, + rootID, }); const chartData = store.profilingCache.getFlamegraphChartData({ - commitDetails, commitIndex, commitTree, + rootID, }); expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); expect(chartData).toMatchSnapshot( `${commitIndex}: FlamegraphChartData` ); + renderFinished = true; return null; } - const rendererID = utils.getRendererID(); const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 2; commitIndex++) { - suspenseResolved = false; + renderFinished = false; await utils.actAsync( () => TestRenderer.create( - + ), 3 ); - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); } - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); done(); }); @@ -156,54 +142,40 @@ describe('profiling charts', () => { ); utils.act(() => store.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, - rootID, - }); - const commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - suspenseResolved = true; + function Suspender({ commitIndex, rootID }) { const commitTree = store.profilingCache.getCommitTree({ commitIndex, - profilingSummary, + rootID, }); const chartData = store.profilingCache.getRankedChartData({ - commitDetails, commitIndex, commitTree, + rootID, }); expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`); + renderFinished = true; return null; } - const rendererID = utils.getRendererID(); const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 2; commitIndex++) { - suspenseResolved = false; + renderFinished = false; await utils.actAsync( () => TestRenderer.create( - + ), 3 ); - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); } done(); @@ -246,47 +218,33 @@ describe('profiling charts', () => { ); utils.act(() => store.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, - rootID, - }); - const { interactions } = store.profilingCache.Interactions.read({ - rendererID, - rootID, - }); - suspenseResolved = true; + function Suspender({ commitIndex, rootID }) { const chartData = store.profilingCache.getInteractionsChartData({ - interactions, - profilingSummary, + rootID, }); expect(chartData).toMatchSnapshot('Interactions'); + renderFinished = true; return null; } - const rendererID = utils.getRendererID(); const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 2; commitIndex++) { - suspenseResolved = false; + renderFinished = false; await utils.actAsync( () => TestRenderer.create( - + ), 3 ); - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); } done(); diff --git a/src/__tests__/profilingCommitTreeBuilder-test.js b/src/__tests__/profilingCommitTreeBuilder-test.js index 8356a0fd4a..31d7acafcf 100644 --- a/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/src/__tests__/profilingCommitTreeBuilder-test.js @@ -45,43 +45,34 @@ describe('commit tree', () => { utils.act(() => ReactDOM.render(, container)); utils.act(() => store.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, - rootID, - }); - suspenseResolved = true; + function Suspender({ commitIndex, rootID }) { const commitTree = store.profilingCache.getCommitTree({ commitIndex, - profilingSummary, + rootID, }); expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); + renderFinished = true; return null; } - const rendererID = utils.getRendererID(); const rootID = store.roots[0]; for (let commitIndex = 0; commitIndex < 4; commitIndex++) { - suspenseResolved = false; + renderFinished = false; await utils.actAsync( () => TestRenderer.create( - + ), 3 ); - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); } done(); diff --git a/src/__tests__/profilingUtils-test.js b/src/__tests__/profilingUtils-test.js new file mode 100644 index 0000000000..66029a2568 --- /dev/null +++ b/src/__tests__/profilingUtils-test.js @@ -0,0 +1,20 @@ +// @flow + +describe('profiling utils', () => { + let utils; + + beforeEach(() => { + utils = require('src/devtools/views/Profiler/utils'); + }); + + it('should throw if importing older/unsupported data', () => { + expect(() => + utils.prepareProfilingDataFrontendFromExport( + ({ + version: 0, + dataForRoots: [], + }: any) + ) + ).toThrow('Unsupported profiler export version "0"'); + }); +}); diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index dd5b4b4646..934e1a593b 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -2,10 +2,10 @@ import typeof ReactTestRenderer from 'react-test-renderer'; -import type { ElementType } from 'src/types'; - import type Bridge from 'src/bridge'; import type Store from 'src/devtools/store'; +import type { ProfilingDataFrontend } from 'src/devtools/views/Profiler/types'; +import type { ElementType } from 'src/types'; export function act(callback: Function): void { const TestUtils = require('react-dom/test-utils'); @@ -136,54 +136,44 @@ export function requireTestRenderer(): ReactTestRenderer { export function exportImportHelper( bridge: Bridge, store: Store, - rendererID: number, rootID: number ): void { const { act } = require('./utils'); const { - prepareExportedProfilingSummary, - prepareImportedProfilingData, + prepareProfilingDataExport, + prepareProfilingDataFrontendFromExport, } = require('src/devtools/views/Profiler/utils'); - let exportedProfilingDataJsonString = ''; - const onExportFile = ({ contents }) => { - if (typeof contents === 'string') { - exportedProfilingDataJsonString = (contents: string); - } - }; - bridge.addListener('exportFile', onExportFile); + const { profilerStore } = store; - act(() => { - const exportProfilingSummary = prepareExportedProfilingSummary( - store.profilingOperations, - store.profilingSnapshots, - rendererID, - rootID - ); - bridge.send('exportProfilingSummary', exportProfilingSummary); - }); + expect(profilerStore.profilingData).not.toBeNull(); - // Cleanup to be able to call this again on the same bridge without memory leaks. - bridge.removeListener('exportFile', onExportFile); + const profilingDataFrontendInitial = ((profilerStore.profilingData: any): ProfilingDataFrontend); - expect(typeof exportedProfilingDataJsonString).toBe('string'); - expect(exportedProfilingDataJsonString).not.toBe(''); - - const profilingData = prepareImportedProfilingData( - exportedProfilingDataJsonString + const profilingDataExport = prepareProfilingDataExport( + profilingDataFrontendInitial ); + + // Simulate writing/reading to disk. + const serializedProfilingDataExport = JSON.stringify( + profilingDataExport, + null, + 2 + ); + const parsedProfilingDataExport = JSON.parse(serializedProfilingDataExport); + + const profilingDataFrontend = prepareProfilingDataFrontendFromExport( + (parsedProfilingDataExport: any) + ); + // Sanity check that profiling snapshots are serialized correctly. - expect(store.profilingSnapshots.get(rootID)).toEqual( - profilingData.profilingSnapshots.get(rootID) - ); - expect(store.profilingOperations.get(rootID)).toEqual( - profilingData.profilingOperations.get(rootID) - ); + expect(profilingDataFrontendInitial).toEqual(profilingDataFrontend); // Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer. - expect(profilingData).toMatchSnapshot('imported data'); + expect(serializedProfilingDataExport).toMatchSnapshot('imported data'); act(() => { - store.profilingData = profilingData; + // Apply the new exported-then-reimported data so tests can re-run assertions. + profilerStore.profilingData = profilingDataFrontend; }); } diff --git a/src/backend/agent.js b/src/backend/agent.js index 097ad00827..08469d4d47 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -10,7 +10,6 @@ import { } from '../constants'; import { hideOverlay, showOverlay } from './views/Highlighter'; -import type { ExportedProfilingSummaryFromFrontend } from 'src/devtools/views/Profiler/types'; import type { PathFrame, PathMatch, @@ -20,8 +19,6 @@ import type { import type { OwnersList } from 'src/devtools/views/Components/types'; import type { Bridge, ComponentFilter } from '../types'; -import { prepareExportedProfilingData } from 'src/devtools/views/Profiler/utils'; - const debug = (methodName, ...args) => { if (__DEBUG__) { console.log( @@ -96,12 +93,8 @@ export default class Agent extends EventEmitter { 'clearHighlightedElementInDOM', this.clearHighlightedElementInDOM ); - bridge.addListener('exportProfilingSummary', this.exportProfilingSummary); - bridge.addListener('getCommitDetails', this.getCommitDetails); - bridge.addListener('getFiberCommits', this.getFiberCommits); - bridge.addListener('getInteractions', this.getInteractions); + bridge.addListener('getProfilingData', this.getProfilingData); bridge.addListener('getProfilingStatus', this.getProfilingStatus); - bridge.addListener('getProfilingSummary', this.getProfilingSummary); bridge.addListener('highlightElementInDOM', this.highlightElementInDOM); bridge.addListener('getOwnersList', this.getOwnersList); bridge.addListener('inspectElement', this.inspectElement); @@ -154,109 +147,19 @@ export default class Agent extends EventEmitter { return null; } - exportProfilingSummary = ( - exportedProfilingSummary: ExportedProfilingSummaryFromFrontend - ): void => { - const { rendererID, rootID } = exportedProfilingSummary; + getProfilingData = ({ rendererID }: {| rendererID: RendererID |}) => { const renderer = this._rendererInterfaces[rendererID]; if (renderer == null) { console.warn(`Invalid renderer id "${rendererID}"`); - return; } - try { - const exportedProfilingDataFromRenderer = renderer.getExportedProfilingData( - rootID - ); - const exportedProfilingData = prepareExportedProfilingData( - exportedProfilingDataFromRenderer, - exportedProfilingSummary - ); - this._bridge.send('exportFile', { - contents: JSON.stringify(exportedProfilingData, null, 2), - filename: 'profile-data.json', - }); - } catch (error) { - console.warn(`Unable to export file: ${error.stack}`); - } - }; - getCommitDetails = ({ - commitIndex, - rendererID, - rootID, - }: { - commitIndex: number, - rendererID: number, - rootID: number, - }) => { - const renderer = this._rendererInterfaces[rendererID]; - if (renderer == null) { - console.warn(`Invalid renderer id "${rendererID}"`); - } else { - this._bridge.send( - 'commitDetails', - renderer.getCommitDetails(rootID, commitIndex) - ); - } - }; - - getFiberCommits = ({ - fiberID, - rendererID, - rootID, - }: { - fiberID: number, - rendererID: number, - rootID: number, - }) => { - const renderer = this._rendererInterfaces[rendererID]; - if (renderer == null) { - console.warn(`Invalid renderer id "${rendererID}"`); - } else { - this._bridge.send( - 'fiberCommits', - renderer.getFiberCommits(rootID, fiberID) - ); - } - }; - - getInteractions = ({ - rendererID, - rootID, - }: { - rendererID: number, - rootID: number, - }) => { - const renderer = this._rendererInterfaces[rendererID]; - if (renderer == null) { - console.warn(`Invalid renderer id "${rendererID}"`); - } else { - this._bridge.send('interactions', renderer.getInteractions(rootID)); - } + this._bridge.send('profilingData', renderer.getProfilingData()); }; getProfilingStatus = () => { this._bridge.send('profilingStatus', this._isProfiling); }; - getProfilingSummary = ({ - rendererID, - rootID, - }: { - rendererID: number, - rootID: number, - }) => { - const renderer = this._rendererInterfaces[rendererID]; - if (renderer == null) { - console.warn(`Invalid renderer id "${rendererID}"`); - } else { - this._bridge.send( - 'profilingSummary', - renderer.getProfilingSummary(rootID) - ); - } - }; - clearHighlightedElementInDOM = () => { hideOverlay(); }; diff --git a/src/backend/renderer.js b/src/backend/renderer.js index b0532eefae..f4e8a6ffe9 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -18,7 +18,6 @@ import { ElementTypeRoot, ElementTypeSuspense, } from 'src/types'; -import { PROFILER_EXPORT_VERSION } from 'src/constants'; import { getDisplayName, getDefaultComponentFilters, @@ -37,17 +36,13 @@ import { import { inspectHooksOfFiber } from './ReactDebugHooks'; import type { - CommitDetailsBackend, + CommitDataBackend, DevToolsHook, - ExportedProfilingDataFromRenderer, Fiber, - FiberCommitsBackend, - InteractionBackend, - InteractionsBackend, - InteractionWithCommitsBackend, PathFrame, PathMatch, - ProfilingSummaryBackend, + ProfilingDataBackend, + ProfilingDataForRootBackend, ReactRenderer, RendererInterface, } from './types'; @@ -55,6 +50,7 @@ import type { InspectedElement, Owner, } from 'src/devtools/views/Components/types'; +import type { Interaction } from 'src/devtools/views/Profiler/types'; import type { ComponentFilter, ElementType } from 'src/types'; function getInternalReactConstants(version) { @@ -820,6 +816,12 @@ export function attach( pushOperation(ElementTypeRoot); pushOperation(isProfilingSupported ? 1 : 0); pushOperation(hasOwnerMetadata ? 1 : 0); + + if (isProfiling) { + if (displayNamesByRootID !== null) { + displayNamesByRootID.set(id, getDisplayNameForRoot(fiber)); + } + } } else { const { key } = fiber; const displayName = getDisplayNameForFiber(fiber); @@ -1286,7 +1288,7 @@ export function attach( durations: [], commitTime: performance.now() - profilingStartTime, interactions: Array.from(root.memoizedInteractions).map( - (interaction: InteractionBackend) => ({ + (interaction: Interaction) => ({ ...interaction, timestamp: interaction.timestamp - profilingStartTime, }) @@ -1329,7 +1331,7 @@ export function attach( durations: [], commitTime: performance.now() - profilingStartTime, interactions: Array.from(root.memoizedInteractions).map( - (interaction: InteractionBackend) => ({ + (interaction: Interaction) => ({ ...interaction, timestamp: interaction.timestamp - profilingStartTime, }) @@ -1981,188 +1983,113 @@ export function attach( type CommitProfilingData = {| commitTime: number, durations: Array, - interactions: Array, + interactions: Array, maxActualDuration: number, priorityLevel: string | null, |}; type CommitProfilingMetadataMap = Map>; + type DisplayNamesByRootID = Map; let currentCommitProfilingMetadata: CommitProfilingData | null = null; + let displayNamesByRootID: DisplayNamesByRootID | null = null; let initialTreeBaseDurationsMap: Map | null = null; let initialIDToRootMap: Map | null = null; let isProfiling: boolean = false; let profilingStartTime: number = 0; let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null; - function getCommitDetails( - rootID: number, - commitIndex: number - ): CommitDetailsBackend { - const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( - rootID - ); - if (commitProfilingMetadata != null) { - const commitProfilingData = commitProfilingMetadata[commitIndex]; - if (commitProfilingData != null) { - return { - commitIndex, - durations: commitProfilingData.durations, - interactions: commitProfilingData.interactions, - priorityLevel: commitProfilingData.priorityLevel, + function getProfilingData(): ProfilingDataBackend { + const dataForRoots: Array = []; + + if (rootToCommitProfilingMetadataMap === null) { + throw Error( + 'getProfilingData() called before any profiling data was recorded' + ); + } + + rootToCommitProfilingMetadataMap.forEach( + (commitProfilingMetadata, rootID) => { + const commitData: Array = []; + const initialTreeBaseDurations: Array<[number, number]> = []; + const allInteractions: Map = new Map(); + const interactionCommits: Map> = new Map(); + + const displayName = + (displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) || + 'Unknown'; + + if (initialTreeBaseDurationsMap != null) { + initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => { + if ( + initialIDToRootMap != null && + initialIDToRootMap.get(id) === rootID + ) { + // We don't need to convert milliseconds to microseconds in this case, + // because the profiling summary is JSON serialized. + initialTreeBaseDurations.push([id, treeBaseDuration]); + } + }); + } + + commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => { + const { + durations, + interactions, + maxActualDuration, + priorityLevel, + commitTime, + } = commitProfilingData; + + const interactionIDs: Array = []; + + interactions.forEach(interaction => { + if (!allInteractions.has(interaction.id)) { + allInteractions.set(interaction.id, interaction); + } + + interactionIDs.push(interaction.id); + + const commitIndices = interactionCommits.get(interaction.id); + if (commitIndices != null) { + commitIndices.push(commitIndex); + } else { + interactionCommits.set(interaction.id, [commitIndex]); + } + }); + + const fiberActualDurations: Array<[number, number]> = []; + const fiberSelfDurations: Array<[number, number]> = []; + for (let i = 0; i < durations.length; i += 3) { + const fiberID = durations[i]; + fiberActualDurations.push([fiberID, durations[i + 1]]); + fiberSelfDurations.push([fiberID, durations[i + 2]]); + } + + commitData.push({ + duration: maxActualDuration, + fiberActualDurations, + fiberSelfDurations, + interactionIDs, + priorityLevel, + timestamp: commitTime, + }); + }); + + dataForRoots.push({ + commitData, + displayName, + initialTreeBaseDurations, + interactionCommits: Array.from(interactionCommits.entries()), + interactions: Array.from(allInteractions.entries()), rootID, - }; - } - } - - console.warn( - `getCommitDetails(): No profiling info recorded for root "${rootID}" and commit ${commitIndex}` - ); - - return { - commitIndex, - durations: [], - interactions: [], - priorityLevel: null, - rootID, - }; - } - - function getFiberCommits( - rootID: number, - fiberID: number - ): FiberCommitsBackend { - const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( - rootID - ); - if (commitProfilingMetadata != null) { - const commitDurations = []; - commitProfilingMetadata.forEach(({ durations }, commitIndex) => { - for (let i = 0; i < durations.length; i += 3) { - if (durations[i] === fiberID) { - commitDurations.push(commitIndex, durations[i + 2]); - break; - } - } - }); - - return { - commitDurations, - fiberID, - rootID, - }; - } - - console.warn( - `getFiberCommits(): No profiling info recorded for root "${rootID}"` - ); - - return { - commitDurations: [], - fiberID, - rootID, - }; - } - - function getInteractions(rootID: number): InteractionsBackend { - const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( - rootID - ); - if (commitProfilingMetadata != null) { - const interactionsMap: Map< - number, - InteractionWithCommitsBackend - > = new Map(); - - commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => { - commitProfilingData.interactions.forEach(interaction => { - const interactionWithCommits = interactionsMap.get(interaction.id); - if (interactionWithCommits != null) { - interactionWithCommits.commits.push(commitIndex); - } else { - interactionsMap.set(interaction.id, { - ...interaction, - commits: [commitIndex], - }); - } }); - }); - - return { - interactions: Array.from(interactionsMap.values()), - rootID, - }; - } - - console.warn( - `getInteractions(): No interactions recorded for root "${rootID}"` - ); - - return { - interactions: [], - rootID, - }; - } - - function getExportedProfilingData( - rootID: number - ): ExportedProfilingDataFromRenderer { - const commitDetailsForEachCommit = []; - const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( - rootID - ); - if (commitProfilingMetadata != null) { - for (let index = 0; index < commitProfilingMetadata.length; index++) { - commitDetailsForEachCommit.push(getCommitDetails(rootID, index)); } - } - - return { - version: PROFILER_EXPORT_VERSION, - profilingSummary: getProfilingSummary(rootID), - commitDetails: commitDetailsForEachCommit, - interactions: getInteractions(rootID), - }; - } - - function getProfilingSummary(rootID: number): ProfilingSummaryBackend { - const interactions = new Set(); - const commitDurations = []; - const commitTimes = []; - - const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( - rootID ); - if (commitProfilingMetadata != null) { - commitProfilingMetadata.forEach(metadata => { - commitDurations.push(metadata.maxActualDuration); - commitTimes.push(metadata.commitTime); - metadata.interactions.forEach(({ name, timestamp }) => { - interactions.add(`${timestamp}:${name}`); - }); - }); - } - - const initialTreeBaseDurations = []; - if (initialTreeBaseDurationsMap != null) { - initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => { - if ( - initialIDToRootMap != null && - initialIDToRootMap.get(id) === rootID - ) { - // We don't need to convert milliseconds to microseconds in this case, - // because the profiling summary is JSON serialized. - initialTreeBaseDurations.push(id, treeBaseDuration); - } - }); - } return { - commitDurations, - commitTimes, - initialTreeBaseDurations, - interactionCount: interactions.size, - rootID, + dataForRoots, + rendererID, }; } @@ -2175,9 +2102,18 @@ export function attach( // It's important we snapshot both the durations and the id-to-root map, // since either of these may change during the profiling session // (e.g. when a fiber is re-rendered or when a fiber gets removed). + displayNamesByRootID = new Map(); initialTreeBaseDurationsMap = new Map(idToTreeBaseDurationMap); initialIDToRootMap = new Map(idToRootMap); + hook.getFiberRoots(rendererID).forEach(root => { + const rootID = getFiberID(getPrimaryFiber(root.current)); + ((displayNamesByRootID: any): DisplayNamesByRootID).set( + rootID, + getDisplayNameForRoot(root.current) + ); + }); + isProfiling = true; profilingStartTime = performance.now(); rootToCommitProfilingMetadataMap = new Map(); @@ -2313,6 +2249,32 @@ export function attach( const rootDisplayNameCounter: Map = new Map(); function setRootPseudoKey(id: number, fiber: Fiber) { + const name = getDisplayNameForRoot(fiber); + const counter = rootDisplayNameCounter.get(name) || 0; + rootDisplayNameCounter.set(name, counter + 1); + const pseudoKey = `${name}:${counter}`; + rootPseudoKeys.set(id, pseudoKey); + } + + function removeRootPseudoKey(id: number) { + const pseudoKey = rootPseudoKeys.get(id); + if (pseudoKey === undefined) { + throw new Error('Expected root pseudo key to be known.'); + } + const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':')); + const counter = rootDisplayNameCounter.get(name); + if (counter === undefined) { + throw new Error('Expected counter to be known.'); + } + if (counter > 1) { + rootDisplayNameCounter.set(name, counter - 1); + } else { + rootDisplayNameCounter.delete(name); + } + rootPseudoKeys.delete(id); + } + + function getDisplayNameForRoot(fiber: Fiber): string { let preferredDisplayName = null; let fallbackDisplayName = null; let child = fiber.child; @@ -2339,29 +2301,7 @@ export function attach( } child = child.child; } - const name = preferredDisplayName || fallbackDisplayName || 'Anonymous'; - const counter = rootDisplayNameCounter.get(name) || 0; - rootDisplayNameCounter.set(name, counter + 1); - const pseudoKey = `${name}:${counter}`; - rootPseudoKeys.set(id, pseudoKey); - } - - function removeRootPseudoKey(id: number) { - const pseudoKey = rootPseudoKeys.get(id); - if (pseudoKey === undefined) { - throw new Error('Expected root pseudo key to be known.'); - } - const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':')); - const counter = rootDisplayNameCounter.get(name); - if (counter === undefined) { - throw new Error('Expected counter to be known.'); - } - if (counter > 1) { - rootDisplayNameCounter.set(name, counter - 1); - } else { - rootDisplayNameCounter.delete(name); - } - rootPseudoKeys.delete(id); + return preferredDisplayName || fallbackDisplayName || 'Anonymous'; } function getPathFrame(fiber: Fiber): PathFrame { @@ -2459,15 +2399,11 @@ export function attach( cleanup, flushInitialOperations, getBestMatchForTrackedPath, - getCommitDetails, getFiberIDFromNative, - getFiberCommits, - getInteractions, findNativeByFiberID, getOwnersList, getPathForElement, - getExportedProfilingData, - getProfilingSummary, + getProfilingData, handleCommitFiberRoot, handleCommitFiberUnmount, inspectElement, diff --git a/src/backend/types.js b/src/backend/types.js index 431619db15..a2a17dd5a1 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -5,6 +5,7 @@ import type { InspectedElement, Owner, } from 'src/devtools/views/Components/types'; +import type { Interaction } from 'src/devtools/views/Profiler/types'; type BundleType = | 0 // PROD @@ -115,51 +116,33 @@ export type ReactRenderer = { currentDispatcherRef?: {| current: null | Dispatcher |}, }; -export type InteractionBackend = {| - id: number, - name: string, +export type CommitDataBackend = {| + duration: number, + // Tuple of fiber ID and actual duration + fiberActualDurations: Array<[number, number]>, + // Tuple of fiber ID and computed "self" duration + fiberSelfDurations: Array<[number, number]>, + interactionIDs: Array, + priorityLevel: string | null, timestamp: number, |}; -export type CommitDetailsBackend = {| - commitIndex: number, - // An interleaved array: fiberID at [i], actualDuration at [i + 1], computed selfDuration at [i + 2]. - durations: Array, - interactions: Array, - priorityLevel: string | null, +export type ProfilingDataForRootBackend = {| + commitData: Array, + displayName: string, + // Tuple of Fiber ID and base duration + initialTreeBaseDurations: Array<[number, number]>, + // Tuple of Interaction ID and commit indices + interactionCommits: Array<[number, Array]>, + interactions: Array<[number, Interaction]>, rootID: number, |}; -export type FiberCommitsBackend = {| - commitDurations: Array, - fiberID: number, - rootID: number, -|}; - -export type InteractionWithCommitsBackend = {| - ...InteractionBackend, - commits: Array, -|}; - -export type InteractionsBackend = {| - interactions: Array, - rootID: number, -|}; - -export type ProfilingSummaryBackend = {| - commitDurations: Array, - commitTimes: Array, - // An interleaved array: fiberID at [i], initialTreeBaseDuration at [i + 1]. - initialTreeBaseDurations: Array, - interactionCount: number, - rootID: number, -|}; - -export type ExportedProfilingDataFromRenderer = {| - version: 3, - profilingSummary: ProfilingSummaryBackend, - commitDetails: Array, - interactions: InteractionsBackend, +// Profiling data collected by the renderer interface. +// This information will be passed to the frontend and combined with info it collects. +export type ProfilingDataBackend = {| + dataForRoots: Array, + rendererID: number, |}; export type PathFrame = {| @@ -178,21 +161,12 @@ export type RendererInterface = { findNativeByFiberID: (id: number) => ?Array, flushInitialOperations: () => void, getBestMatchForTrackedPath: () => PathMatch | null, - getCommitDetails: ( - rootID: number, - commitIndex: number - ) => CommitDetailsBackend, getFiberIDFromNative: ( component: NativeType, findNearestUnfilteredAncestor?: boolean ) => number | null, - getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend, - getInteractions: (rootID: number) => InteractionsBackend, + getProfilingData(): ProfilingDataBackend, getOwnersList: (id: number) => Array | null, - getExportedProfilingData: ( - rootID: number - ) => ExportedProfilingDataFromRenderer, - getProfilingSummary: (rootID: number) => ProfilingSummaryBackend, getPathForElement: (id: number) => Array | null, handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void, handleCommitFiberUnmount: (fiber: Object) => void, diff --git a/src/constants.js b/src/constants.js index 66341ac584..5d43b93b5c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -16,4 +16,4 @@ export const SESSION_STORAGE_LAST_SELECTION_KEY = export const __DEBUG__ = false; -export const PROFILER_EXPORT_VERSION = 3; +export const PROFILER_EXPORT_VERSION = 5; diff --git a/src/devtools/ProfilerStore.js b/src/devtools/ProfilerStore.js new file mode 100644 index 0000000000..41b547682a --- /dev/null +++ b/src/devtools/ProfilerStore.js @@ -0,0 +1,370 @@ +// @flow + +import EventEmitter from 'events'; +import memoize from 'memoize-one'; +import throttle from 'lodash.throttle'; +import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils'; +import ProfilingCache from './ProfilingCache'; +import Store from './store'; + +import type { ProfilingDataBackend } from 'src/backend/types'; +import type { + CommitDataFrontend, + ProfilingDataForRootFrontend, + ProfilingDataFrontend, + SnapshotNode, +} from './views/Profiler/types'; +import type { Bridge } from '../types'; + +const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500; + +export default class ProfilerStore extends EventEmitter { + _bridge: Bridge; + + // Suspense cache for lazily calculating derived profiling data. + _cache: ProfilingCache; + + // Temporary store of profiling data from the backend renderer(s). + // This data will be converted to the ProfilingDataFrontend format after being collected from all renderers. + _dataBackends: Array = []; + + // Data from the most recently completed profiling session, + // or data that has been imported from a previously exported session. + // This object contains all necessary data to drive the Profiler UI interface, + // even though some of it is lazily parsed/derived via the ProfilingCache. + _dataFrontend: ProfilingDataFrontend | null = null; + + // Snapshot of the state of the main Store (including all roots) when profiling started. + // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling, + // to reconstruct the state of each root for each commit. + // It's okay to use a single root to store this information because node IDs are unique across all roots. + // + // This map is only updated while profiling is in progress; + // Upon completion, it is converted into the exportable ProfilingDataFrontend format. + _initialSnapshotsByRootID: Map> = new Map(); + + // Map of root (id) to a list of tree mutation that occur during profiling. + // Once profiling is finished, these mutations can be used, along with the initial tree snapshots, + // to reconstruct the state of each root for each commit. + // + // This map is only updated while profiling is in progress; + // Upon completion, it is converted into the exportable ProfilingDataFrontend format. + _inProgressOperationsByRootID: Map> = new Map(); + + // Map of root (id) to a Map of screenshots by commit ID. + // Stores screenshots for each commit (when profiling). + // + // This map is only updated while profiling is in progress; + // Upon completion, it is converted into the exportable ProfilingDataFrontend format. + _inProgressScreenshotsByRootID: Map> = new Map(); + + // The backend is currently profiling. + // When profiling is in progress, operations are stored so that we can later reconstruct past commit trees. + _isProfiling: boolean = false; + + // After profiling, data is requested from each attached renderer using this queue. + // So long as this queue is not empty, the store is retrieving and processing profiling data from the backend. + _rendererQueue: Set = new Set(); + + _store: Store; + + constructor(bridge: Bridge, store: Store, defaultIsProfiling: boolean) { + super(); + + this._bridge = bridge; + this._isProfiling = defaultIsProfiling; + this._store = store; + + bridge.addListener('operations', this.onBridgeOperations); + bridge.addListener('profilingData', this.onBridgeProfilingData); + bridge.addListener('profilingStatus', this.onProfilingStatus); + bridge.addListener('shutdown', this.onBridgeShutdown); + + // It's possible that profiling has already started (e.g. "reload and start profiling") + // so the frontend needs to ask the backend for its status after mounting. + bridge.send('getProfilingStatus'); + + this._cache = new ProfilingCache(this); + } + + getCommitData(rootID: number, commitIndex: number): CommitDataFrontend { + if (this._dataFrontend !== null) { + const dataForRoot = this._dataFrontend.dataForRoots.get(rootID); + if (dataForRoot != null) { + const commitDatum = dataForRoot.commitData[commitIndex]; + if (commitDatum != null) { + return commitDatum; + } + } + } + + throw Error( + `Could not find commit data for root "${rootID}" and commit ${commitIndex}` + ); + } + + getDataForRoot(rootID: number): ProfilingDataForRootFrontend { + if (this._dataFrontend !== null) { + const dataForRoot = this._dataFrontend.dataForRoots.get(rootID); + if (dataForRoot != null) { + return dataForRoot; + } + } + + throw Error(`Could not find commit data for root "${rootID}"`); + } + + get cache(): ProfilingCache { + return this._cache; + } + + // Profiling data has been recorded for at least one root. + get hasProfilingData(): boolean { + return ( + this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0 + ); + } + + // TODO (profarc) Remove this getter + get initialSnapshotsByRootID(): Map> { + return this._initialSnapshotsByRootID; + } + + // TODO (profarc) Remove this getter + get inProgressOperationsByRootID(): Map> { + return this._inProgressOperationsByRootID; + } + + // TODO (profarc) Remove this getter + get inProgressScreenshotsByRootID(): Map> { + return this._inProgressScreenshotsByRootID; + } + + get isProcessingData(): boolean { + return this._rendererQueue.size > 0 || this._dataBackends.length > 0; + } + + get isProfiling(): boolean { + return this._isProfiling; + } + + get profilingData(): ProfilingDataFrontend | null { + return this._dataFrontend; + } + set profilingData(value: ProfilingDataFrontend | null): void { + this._dataBackends.splice(0); + this._dataFrontend = value; + this._initialSnapshotsByRootID.clear(); + this._inProgressOperationsByRootID.clear(); + this._inProgressScreenshotsByRootID.clear(); + this._cache.invalidate(); + + // TODO (profarc) Remove subscriptions to Store for this + this._store.emit('profilingData'); + this.emit('profilingData'); + } + + clear(): void { + this._dataBackends.splice(0); + this._dataFrontend = null; + this._initialSnapshotsByRootID.clear(); + this._inProgressOperationsByRootID.clear(); + this._inProgressScreenshotsByRootID.clear(); + + // Invalidate suspense cache if profiling data is being (re-)recorded. + // Note that we clear now because any existing data is "stale". + this._cache.invalidate(); + + // TODO (profarc) Remove subscriptions to Store for this + this._store.emit('isProfiling'); + this.emit('isProfiling'); + } + + startProfiling(): void { + this._bridge.send('startProfiling'); + + // Don't actually update the local profiling boolean yet! + // Wait for onProfilingStatus() to confirm the status has changed. + // This ensures the frontend and backend are in sync wrt which commits were profiled. + // We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors. + } + + stopProfiling(): void { + this._bridge.send('stopProfiling'); + + // Don't actually update the local profiling boolean yet! + // Wait for onProfilingStatus() to confirm the status has changed. + // This ensures the frontend and backend are in sync wrt which commits were profiled. + // We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors. + } + + _captureScreenshot = throttle( + memoize((rootID: number, commitIndex: number) => { + this._bridge.send('captureScreenshot', { commitIndex, rootID }); + }), + THROTTLE_CAPTURE_SCREENSHOT_DURATION + ); + + _takeProfilingSnapshotRecursive = ( + elementID: number, + profilingSnapshots: Map + ) => { + const element = this._store.getElementByID(elementID); + if (element !== null) { + const snapshotNode: SnapshotNode = { + id: elementID, + children: element.children.slice(0), + displayName: element.displayName, + key: element.key, + type: element.type, + }; + profilingSnapshots.set(elementID, snapshotNode); + + element.children.forEach(childID => + this._takeProfilingSnapshotRecursive(childID, profilingSnapshots) + ); + } + }; + + onBridgeOperations = (operations: Uint32Array) => { + if (!(operations instanceof Uint32Array)) { + // $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array. + operations = Uint32Array.from(Object.values(operations)); + } + + // The first two values are always rendererID and rootID + const rootID = operations[1]; + + if (this._isProfiling) { + let profilingOperations = this._inProgressOperationsByRootID.get(rootID); + if (profilingOperations == null) { + profilingOperations = [operations]; + this._inProgressOperationsByRootID.set(rootID, profilingOperations); + } else { + profilingOperations.push(operations); + } + + if (!this._initialSnapshotsByRootID.has(rootID)) { + this._initialSnapshotsByRootID.set(rootID, new Map()); + } + + if (this._store.captureScreenshots) { + const commitIndex = profilingOperations.length - 1; + this._captureScreenshot(rootID, commitIndex); + } + } + }; + + onBridgeProfilingData = (dataBackend: ProfilingDataBackend) => { + if (this._isProfiling) { + // This should never happen, but if it does- ignore previous profiling data. + return; + } + + const { rendererID } = dataBackend; + + if (!this._rendererQueue.has(rendererID)) { + throw Error( + `Unexpected profiling data update from renderer "${rendererID}"` + ); + } + + this._dataBackends.push(dataBackend); + this._rendererQueue.delete(rendererID); + + if (this._rendererQueue.size === 0) { + this._dataFrontend = prepareProfilingDataFrontendFromBackendAndStore( + this._dataBackends, + this._inProgressOperationsByRootID, + this._inProgressScreenshotsByRootID, + this._initialSnapshotsByRootID + ); + + this._dataBackends.splice(0); + + // TODO (profarc) Remove subscriptions to Store for this + this._store.emit('isProcessingData'); + this.emit('isProcessingData'); + } + }; + + onBridgeShutdown = () => { + this._bridge.removeListener('operations', this.onBridgeOperations); + this._bridge.removeListener('profilingStatus', this.onProfilingStatus); + this._bridge.removeListener('shutdown', this.onBridgeShutdown); + }; + + onProfilingStatus = (isProfiling: boolean) => { + if (isProfiling) { + this._dataBackends.splice(0); + this._dataFrontend = null; + this._initialSnapshotsByRootID.clear(); + this._inProgressOperationsByRootID.clear(); + this._inProgressScreenshotsByRootID.clear(); + this._rendererQueue.clear(); + + // Record snapshot of tree at the time profiling is started. + // This info is required to handle cases of e.g. nodes being removed during profiling. + this._store.roots.forEach(rootID => { + const profilingSnapshots = new Map(); + this._initialSnapshotsByRootID.set(rootID, profilingSnapshots); + this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots); + }); + } + + if (this._isProfiling !== isProfiling) { + this._isProfiling = isProfiling; + + // Invalidate suspense cache if profiling data is being (re-)recorded. + // Note that we clear again, in case any views read from the cache while profiling. + // (That would have resolved a now-stale value without any profiling data.) + this._cache.invalidate(); + + // TODO (profarc) Remove subscriptions to Store for this + this._store.emit('isProfiling'); + this.emit('isProfiling'); + + // If we've just finished a profiling session, we need to fetch data stored in each renderer interface + // and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI. + // During this time, DevTools UI should probably not be interactive. + if (!isProfiling) { + this._dataBackends.splice(0); + this._rendererQueue.clear(); + + for (let rendererID of this._store.rootIDToRendererID.values()) { + if (!this._rendererQueue.has(rendererID)) { + this._rendererQueue.add(rendererID); + + this._bridge.send('getProfilingData', { rendererID }); + } + } + + // TODO (profarc) Remove subscriptions to Store for this + this._store.emit('isProcessingData'); + this.emit('isProcessingData'); + } + } + }; + + onScreenshotCaptured = ({ + commitIndex, + dataURL, + rootID, + }: {| + commitIndex: number, + dataURL: string, + rootID: number, + |}) => { + let screenshotsForRootByCommitIndex = this._inProgressScreenshotsByRootID.get( + rootID + ); + if (!screenshotsForRootByCommitIndex) { + screenshotsForRootByCommitIndex = new Map(); + this._inProgressScreenshotsByRootID.set( + rootID, + screenshotsForRootByCommitIndex + ); + } + screenshotsForRootByCommitIndex.set(commitIndex, dataURL); + }; +} diff --git a/src/devtools/ProfilingCache.js b/src/devtools/ProfilingCache.js index a334edd4c8..d8f1819ddb 100644 --- a/src/devtools/ProfilingCache.js +++ b/src/devtools/ProfilingCache.js @@ -1,7 +1,6 @@ // @flow -import { createResource } from './cache'; -import Store from './store'; +import ProfilerStore from './ProfilerStore'; import { getCommitTree, invalidateCommitTrees, @@ -19,415 +18,105 @@ import { invalidateChartData as invalidateRankedChartData, } from 'src/devtools/views/Profiler/RankedChartBuilder'; -import type { Resource } from './cache'; -import type { - CommitDetailsBackend, - FiberCommitsBackend, - InteractionsBackend, - ProfilingSummaryBackend, -} from 'src/backend/types'; -import type { - CommitDetailsFrontend, - FiberCommitsFrontend, - InteractionsFrontend, - InteractionWithCommitsFrontend, - CommitTreeFrontend, - ProfilingSummaryFrontend, -} from 'src/devtools/views/Profiler/types'; +import type { CommitTree } from 'src/devtools/views/Profiler/types'; import type { ChartData as FlamegraphChartData } from 'src/devtools/views/Profiler/FlamegraphChartBuilder'; import type { ChartData as InteractionsChartData } from 'src/devtools/views/Profiler/InteractionsChartBuilder'; import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/RankedChartBuilder'; -import type { Bridge } from 'src/types'; - -type CommitDetailsParams = {| - commitIndex: number, - rendererID: number, - rootID: number, -|}; - -type FiberCommitsParams = {| - fiberID: number, - rendererID: number, - rootID: number, -|}; - -type InteractionsParams = {| - rendererID: number, - rootID: number, -|}; - -type GetCommitTreeParams = {| - commitIndex: number, - profilingSummary: ProfilingSummaryFrontend, -|}; - -type ProfilingSummaryParams = {| - rendererID: number, - rootID: number, -|}; export default class ProfilingCache { - _bridge: Bridge; - _store: Store; + _fiberCommits: Map> = new Map(); + _profilerStore: ProfilerStore; - _pendingCommitDetailsMap: Map< - string, - (commitDetails: CommitDetailsFrontend) => void - > = new Map(); - - _pendingFiberCommitsMap: Map< - string, - (fiberCommits: FiberCommitsFrontend) => void - > = new Map(); - - _pendingInteractionsMap: Map< - number, - (interactions: InteractionsFrontend) => void - > = new Map(); - - _pendingProfileSummaryMap: Map< - number, - (profilingSummary: ProfilingSummaryFrontend) => void - > = new Map(); - - CommitDetails: Resource< - CommitDetailsParams, - string, - CommitDetailsFrontend - > = createResource( - ({ commitIndex, rendererID, rootID }: CommitDetailsParams) => { - return new Promise(resolve => { - const pendingKey = `${rootID}-${commitIndex}`; - const profilingData = this._store.profilingData; - if (profilingData !== null) { - const commitDetailsByCommitIndex = profilingData.commitDetails; - if ( - commitDetailsByCommitIndex != null && - commitIndex < commitDetailsByCommitIndex.length - ) { - const commitDetails = commitDetailsByCommitIndex[commitIndex]; - if (commitDetails != null) { - this._pendingCommitDetailsMap.delete(pendingKey); - resolve(commitDetails); - return; - } - } - } else if (this._store.profilingOperations.has(rootID)) { - this._pendingCommitDetailsMap.set(pendingKey, resolve); - this._bridge.send('getCommitDetails', { - commitIndex, - rendererID, - rootID, - }); - return; - } - - this._pendingCommitDetailsMap.delete(pendingKey); - - // If no profiling data was recorded for this root, skip the round trip. - resolve({ - rootID, - commitIndex, - actualDurations: new Map(), - priorityLevel: null, - interactions: [], - selfDurations: new Map(), - }); - }); - }, - ({ commitIndex, rendererID, rootID }: CommitDetailsParams) => - `${rootID}-${commitIndex}` - ); - - FiberCommits: Resource< - FiberCommitsParams, - string, - FiberCommitsFrontend - > = createResource( - ({ fiberID, rendererID, rootID }: FiberCommitsParams) => { - return new Promise(resolve => { - const pendingKey = `${rootID}-${fiberID}`; - const profilingData = this._store.profilingData; - if (profilingData !== null) { - const { commitDetails } = profilingData; - const commitDurations = []; - commitDetails.forEach(({ selfDurations }, commitIndex) => { - const selfDuration = selfDurations.get(fiberID); - if (selfDuration != null) { - commitDurations.push(commitIndex, selfDuration); - } - }); - this._pendingFiberCommitsMap.delete(pendingKey); - resolve({ - commitDurations, - fiberID, - rootID, - }); - return; - } else if (this._store.profilingOperations.has(rootID)) { - this._pendingFiberCommitsMap.set(pendingKey, resolve); - this._bridge.send('getFiberCommits', { - fiberID, - rendererID, - rootID, - }); - return; - } - - this._pendingFiberCommitsMap.delete(pendingKey); - - // If no profiling data was recorded for this root, skip the round trip. - resolve({ - commitDurations: [], - fiberID, - rootID, - }); - }); - }, - ({ fiberID, rendererID, rootID }: FiberCommitsParams) => - `${rootID}-${fiberID}` - ); - - Interactions: Resource< - InteractionsParams, - number, - InteractionsFrontend - > = createResource( - ({ rendererID, rootID }: InteractionsParams) => { - return new Promise(resolve => { - const pendingKey = rootID; - const profilingData = this._store.profilingData; - if (profilingData !== null) { - const interactionsFrontend: InteractionsFrontend = - profilingData.interactions; - if (interactionsFrontend != null) { - this._pendingInteractionsMap.delete(pendingKey); - resolve(interactionsFrontend); - return; - } - } else if (this._store.profilingOperations.has(rootID)) { - this._pendingInteractionsMap.set(pendingKey, resolve); - this._bridge.send('getInteractions', { - rendererID, - rootID, - }); - return; - } - - this._pendingInteractionsMap.delete(pendingKey); - - // If no profiling data was recorded for this root, skip the round trip. - resolve({ - interactions: [], - rootID, - }); - }); - }, - ({ rendererID, rootID }: ProfilingSummaryParams) => rootID - ); - - ProfilingSummary: Resource< - ProfilingSummaryParams, - number, - ProfilingSummaryFrontend - > = createResource( - ({ rendererID, rootID }: ProfilingSummaryParams) => { - return new Promise(resolve => { - const pendingKey = rootID; - const profilingData = this._store.profilingData; - if (profilingData !== null) { - const profilingSummaryFrontend: ProfilingSummaryFrontend = - profilingData.profilingSummary; - if (profilingSummaryFrontend != null) { - this._pendingProfileSummaryMap.delete(pendingKey); - resolve(profilingSummaryFrontend); - return; - } - } else if (this._store.profilingOperations.has(rootID)) { - this._pendingProfileSummaryMap.set(pendingKey, resolve); - this._bridge.send('getProfilingSummary', { rendererID, rootID }); - return; - } - - this._pendingProfileSummaryMap.delete(pendingKey); - - // If no profiling data was recorded for this root, skip the round trip. - resolve({ - rootID, - commitDurations: [], - commitTimes: [], - initialTreeBaseDurations: new Map(), - interactionCount: 0, - }); - }); - }, - ({ rendererID, rootID }: ProfilingSummaryParams) => rootID - ); - - constructor(bridge: Bridge, store: Store) { - this._bridge = bridge; - this._store = store; - - bridge.addListener('commitDetails', this.onCommitDetails); - bridge.addListener('fiberCommits', this.onFiberCommits); - bridge.addListener('interactions', this.onInteractions); - bridge.addListener('profilingSummary', this.onProfileSummary); + constructor(profilerStore: ProfilerStore) { + this._profilerStore = profilerStore; } - getCommitTree = ({ commitIndex, profilingSummary }: GetCommitTreeParams) => + getCommitTree = ({ + commitIndex, + rootID, + }: {| + commitIndex: number, + rootID: number, + |}) => getCommitTree({ commitIndex, - profilingSummary, - store: this._store, + profilerStore: this._profilerStore, + rootID, }); + getFiberCommits = ({ + fiberID, + rootID, + }: {| + fiberID: number, + rootID: number, + |}): Array => { + const cachedFiberCommits = this._fiberCommits.get(fiberID); + if (cachedFiberCommits != null) { + return cachedFiberCommits; + } + + const fiberCommits = []; + const dataForRoot = this._profilerStore.getDataForRoot(rootID); + dataForRoot.commitData.forEach((commitDatum, commitIndex) => { + if (commitDatum.fiberActualDurations.has(fiberID)) { + fiberCommits.push(commitIndex); + } + }); + + this._fiberCommits.set(fiberID, fiberCommits); + + return fiberCommits; + }; + getFlamegraphChartData = ({ - commitDetails, commitIndex, commitTree, + rootID, }: {| - commitDetails: CommitDetailsFrontend, commitIndex: number, - commitTree: CommitTreeFrontend, + commitTree: CommitTree, + rootID: number, |}): FlamegraphChartData => getFlamegraphChartData({ - commitDetails, commitIndex, commitTree, + profilerStore: this._profilerStore, + rootID, }); getInteractionsChartData = ({ - interactions, - profilingSummary, + rootID, }: {| - interactions: Array, - profilingSummary: ProfilingSummaryFrontend, + rootID: number, |}): InteractionsChartData => getInteractionsChartData({ - interactions, - profilingSummary, + profilerStore: this._profilerStore, + rootID, }); getRankedChartData = ({ - commitDetails, commitIndex, commitTree, + rootID, }: {| - commitDetails: CommitDetailsFrontend, commitIndex: number, - commitTree: CommitTreeFrontend, + commitTree: CommitTree, + rootID: number, |}): RankedChartData => getRankedChartData({ - commitDetails, commitIndex, commitTree, + profilerStore: this._profilerStore, + rootID, }); invalidate() { - // Invalidate Suspense caches. - this.CommitDetails.clear(); - this.FiberCommits.clear(); - this.Interactions.clear(); - this.ProfilingSummary.clear(); + this._fiberCommits.clear(); - // Invalidate non-Suspense caches too. invalidateCommitTrees(); invalidateFlamegraphChartData(); invalidateInteractionsChartData(); invalidateRankedChartData(); - - this._pendingCommitDetailsMap.clear(); - this._pendingFiberCommitsMap.clear(); - this._pendingInteractionsMap.clear(); - this._pendingProfileSummaryMap.clear(); } - - onCommitDetails = ({ - commitIndex, - durations, - interactions, - priorityLevel, - rootID, - }: CommitDetailsBackend) => { - const key = `${rootID}-${commitIndex}`; - const resolve = this._pendingCommitDetailsMap.get(key); - if (resolve != null) { - this._pendingCommitDetailsMap.delete(key); - - const actualDurationsMap = new Map(); - const selfDurationsMap = new Map(); - for (let i = 0; i < durations.length; i += 3) { - const fiberID = durations[i]; - actualDurationsMap.set(fiberID, durations[i + 1]); - selfDurationsMap.set(fiberID, durations[i + 2]); - } - - resolve({ - actualDurations: actualDurationsMap, - commitIndex, - interactions, - priorityLevel, - rootID, - selfDurations: selfDurationsMap, - }); - } - }; - - onFiberCommits = ({ - commitDurations, - fiberID, - rootID, - }: FiberCommitsBackend) => { - const key = `${rootID}-${fiberID}`; - const resolve = this._pendingFiberCommitsMap.get(key); - if (resolve != null) { - this._pendingFiberCommitsMap.delete(key); - - resolve({ - commitDurations, - fiberID, - rootID, - }); - } - }; - - onInteractions = ({ interactions, rootID }: InteractionsBackend) => { - const resolve = this._pendingInteractionsMap.get(rootID); - if (resolve != null) { - this._pendingInteractionsMap.delete(rootID); - - resolve({ - interactions, - rootID, - }); - } - }; - - onProfileSummary = ({ - commitDurations, - commitTimes, - initialTreeBaseDurations, - interactionCount, - rootID, - }: ProfilingSummaryBackend) => { - const resolve = this._pendingProfileSummaryMap.get(rootID); - if (resolve != null) { - this._pendingProfileSummaryMap.delete(rootID); - - const initialTreeBaseDurationsMap = new Map(); - for (let i = 0; i < initialTreeBaseDurations.length; i += 2) { - const fiberID = initialTreeBaseDurations[i]; - const initialTreeBaseDuration = initialTreeBaseDurations[i + 1]; - initialTreeBaseDurationsMap.set(fiberID, initialTreeBaseDuration); - } - - resolve({ - commitDurations, - commitTimes, - initialTreeBaseDurations: initialTreeBaseDurationsMap, - interactionCount, - rootID, - }); - } - }; } diff --git a/src/devtools/store.js b/src/devtools/store.js index 21ae3255d0..1385c07431 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -1,8 +1,6 @@ // @flow import EventEmitter from 'events'; -import memoize from 'memoize-one'; -import throttle from 'lodash.throttle'; import { inspect } from 'util'; import { TREE_OPERATION_ADD, @@ -19,11 +17,12 @@ import { import { __DEBUG__ } from '../constants'; import ProfilingCache from './ProfilingCache'; import { printStore } from 'src/__tests__/storeSerializer'; +import ProfilerStore from './ProfilerStore'; import type { Element } from './views/Components/types'; import type { - ImportedProfilingData, - ProfilingSnapshotNode, + ProfilingDataFrontend, + SnapshotNode, } from './views/Profiler/types'; import type { Bridge, ComponentFilter, ElementType } from '../types'; @@ -43,8 +42,6 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY = const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY = 'React::DevTools::collapseNodesByDefault'; -const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500; - type Config = {| isProfiling?: boolean, supportsCaptureScreenshots?: boolean, @@ -80,37 +77,11 @@ export default class Store extends EventEmitter { // The InspectedElementContext also relies on this mutability for its WeakMap usage. _idToElement: Map = new Map(); - // The backend is currently profiling. - // When profiling is in progress, operations are stored so that we can later reconstruct past commit trees. - _isProfiling: boolean = false; - // Map of element (id) to the set of elements (ids) it owns. // This map enables getOwnersListForElement() to avoid traversing the entire tree. _ownersMap: Map> = new Map(); - // Suspense cache for reading profiling data. - _profilingCache: ProfilingCache; - - // The user has imported a previously exported profiling session. - _profilingData: ImportedProfilingData | null = null; - - // Map of root (id) to a list of tree mutation that occur during profiling. - // Once profiling is finished, these mutations can be used, along with the initial tree snapshots, - // to reconstruct the state of each root for each commit. - _profilingOperationsByRootID: Map> = new Map(); - - // Map of root (id) to a Map of screenshots by commit ID. - // Stores screenshots for each commit (when profiling). - _profilingScreenshotsByRootID: Map> = new Map(); - - // Snapshot of the state of the main Store (including all roots) when profiling started. - // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling, - // to reconstruct the state of each root for each commit. - // It's okay to use a single root to store this information because node IDs are unique across all roots. - _profilingSnapshotsByRootID: Map< - number, - Map - > = new Map(); + _profilerStore: ProfilerStore; // Incremented each time the store is mutated. // This enables a passive effect to detect a mutation between render and commit phase. @@ -150,17 +121,16 @@ export default class Store extends EventEmitter { this._componentFilters = getSavedComponentFilters(); + let isProfiling = false; if (config != null) { + isProfiling = config.isProfiling === true; + const { - isProfiling, supportsCaptureScreenshots, supportsFileDownloads, supportsProfiling, supportsReloadAndProfile, } = config; - if (isProfiling) { - this._isProfiling = true; - } if (supportsCaptureScreenshots) { this._supportsCaptureScreenshots = true; this._captureScreenshots = @@ -180,15 +150,9 @@ export default class Store extends EventEmitter { this._bridge = bridge; bridge.addListener('operations', this.onBridgeOperations); - bridge.addListener('profilingStatus', this.onProfilingStatus); - bridge.addListener('screenshotCaptured', this.onScreenshotCaptured); bridge.addListener('shutdown', this.onBridgeShutdown); - // It's possible that profiling has already started (e.g. "reload and start profiling") - // so the frontend needs to ask the backend for its status after mounting. - bridge.send('getProfilingStatus'); - - this._profilingCache = new ProfilingCache(bridge, this); + this._profilerStore = new ProfilerStore(bridge, this, isProfiling); } // This is only used in tests to avoid memory leaks. @@ -197,23 +161,6 @@ export default class Store extends EventEmitter { // The only safe time to assert these maps are empty is when the store is empty. this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement'); this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap'); - - // These maps will be empty unless profiling mode has been started. - // After this, their size should always match the number of roots, - // but unless we want to track additional metadata about profiling history, - // the only safe time to assert this is when the store is empty. - this.assertMapSizeMatchesRootCount( - this._profilingOperationsByRootID, - '_profilingOperationsByRootID' - ); - this.assertMapSizeMatchesRootCount( - this._profilingScreenshotsByRootID, - '_profilingScreenshotsByRootID' - ); - this.assertMapSizeMatchesRootCount( - this._profilingSnapshotsByRootID, - '_profilingSnapshotsByRootID' - ); } // These maps should always be the same size as the number of roots @@ -273,7 +220,7 @@ export default class Store extends EventEmitter { return this._componentFilters; } set componentFilters(value: Array): void { - if (this._isProfiling) { + if (this._profilerStore.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'); @@ -295,54 +242,65 @@ export default class Store extends EventEmitter { return this._hasOwnerMetadata; } - // Profiling data has been recorded for at least one root. + // TODO (profarc) Update views to use ProfilerStore directly to access this value. get hasProfilingData(): boolean { - return ( - this._profilingData !== null || this._profilingOperationsByRootID.size > 0 - ); + return this._profilerStore.hasProfilingData; } + // TODO (profarc) Update views to use ProfilerStore directly to access this value. + get isProcessingProfilingData(): boolean { + return this._profilerStore.isProcessingData; + } + + // TODO (profarc) Update views to use ProfilerStore directly to access this value. get isProfiling(): boolean { - return this._isProfiling; + return this._profilerStore.isProfiling; } get numElements(): number { return this._weightAcrossRoots; } + // TODO (profarc) Update views to use ProfilerStore directly to access this value. get profilingCache(): ProfilingCache { - return this._profilingCache; + return this._profilerStore.cache; } - get profilingData(): ImportedProfilingData | null { - return this._profilingData; + // TODO (profarc) Update views to use ProfilerStore directly to access this value. + get profilingData(): ProfilingDataFrontend | null { + return this._profilerStore.profilingData; } - set profilingData(value: ImportedProfilingData | null): void { - this._profilingData = value; - this._profilingOperationsByRootID = new Map(); - this._profilingScreenshotsByRootID = new Map(); - this._profilingSnapshotsByRootID = new Map(); - this._profilingCache.invalidate(); - - this.emit('profilingData'); + set profilingData(value: ProfilingDataFrontend | null): void { + this._profilerStore.profilingData = value; } - get profilingOperations(): Map> { - return this._profilingOperationsByRootID; + // TODO (profarc) Update views to use ProfilerStore directly to access this value. + get profilingOperationsByRootID(): Map> { + return this._profilerStore.inProgressOperationsByRootID; } - get profilingScreenshots(): Map> { - return this._profilingScreenshotsByRootID; + // TODO (profarc) Update views to use ProfilerStore directly to access this value. + get profilingScreenshotsByRootID(): Map> { + return this._profilerStore.inProgressScreenshotsByRootID; } - get profilingSnapshots(): Map> { - return this._profilingSnapshotsByRootID; + // TODO (profarc) Update views to use ProfilerStore directly to access this value. + get profilingSnapshotsByRootID(): Map> { + return this._profilerStore.initialSnapshotsByRootID; + } + + get profilerStore(): ProfilerStore { + return this._profilerStore; } get revision(): number { return this._revision; } + get rootIDToRendererID(): Map { + return this._rootIDToRendererID; + } + get roots(): $ReadOnlyArray { return this._roots; } @@ -363,17 +321,9 @@ export default class Store extends EventEmitter { return this._supportsReloadAndProfile; } + // TODO (profarc) Update views to use ProfilerStore directly to access this method. clearProfilingData(): void { - this._profilingData = null; - this._profilingOperationsByRootID = new Map(); - this._profilingScreenshotsByRootID = new Map(); - this._profilingSnapshotsByRootID = new Map(); - - // Invalidate suspense cache if profiling data is being (re-)recorded. - // Note that we clear now because any existing data is "stale". - this._profilingCache.invalidate(); - - this.emit('isProfiling'); + this._profilerStore.clear(); } containsElement(id: number): boolean { @@ -601,22 +551,14 @@ export default class Store extends EventEmitter { return false; } + // TODO (profarc) Update views to use ProfilerStore directly to access this method. startProfiling(): void { - this._bridge.send('startProfiling'); - - // Don't actually update the local profiling boolean yet! - // Wait for onProfilingStatus() to confirm the status has changed. - // This ensures the frontend and backend are in sync wrt which commits were profiled. - // We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors. + this._profilerStore.startProfiling(); } + // TODO (profarc) Update views to use ProfilerStore directly to access this method. stopProfiling(): void { - this._bridge.send('stopProfiling'); - - // Don't actually update the local profiling boolean yet! - // Wait for onProfilingStatus() to confirm the status has changed. - // This ensures the frontend and backend are in sync wrt which commits were profiled. - // We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors. + this._profilerStore.stopProfiling(); } // TODO Maybe split this into two methods: expand() and collapse() @@ -701,34 +643,6 @@ export default class Store extends EventEmitter { } } - _captureScreenshot = throttle( - memoize((rootID: number, commitIndex: number) => { - this._bridge.send('captureScreenshot', { commitIndex, rootID }); - }), - THROTTLE_CAPTURE_SCREENSHOT_DURATION - ); - - _takeProfilingSnapshotRecursive = ( - elementID: number, - profilingSnapshots: Map - ) => { - const element = this.getElementByID(elementID); - if (element !== null) { - const snapshotNode: ProfilingSnapshotNode = { - id: elementID, - children: element.children.slice(0), - displayName: element.displayName, - key: element.key, - type: element.type, - }; - profilingSnapshots.set(elementID, snapshotNode); - - element.children.forEach(childID => - this._takeProfilingSnapshotRecursive(childID, profilingSnapshots) - ); - } - }; - _adjustParentTreeWeight = ( parentElement: Element | null, weightDelta: number @@ -769,23 +683,8 @@ export default class Store extends EventEmitter { let haveRootsChanged = false; + // The first two values are always rendererID and rootID const rendererID = operations[0]; - const rootID = operations[1]; - - if (this._isProfiling) { - let profilingOperations = this._profilingOperationsByRootID.get(rootID); - if (profilingOperations == null) { - profilingOperations = [operations]; - this._profilingOperationsByRootID.set(rootID, profilingOperations); - } else { - profilingOperations.push(operations); - } - - if (this._captureScreenshots) { - const commitIndex = profilingOperations.length - 1; - this._captureScreenshot(rootID, commitIndex); - } - } const addedElementIDs: Array = []; // This is a mapping of removed ID -> parent ID: @@ -857,10 +756,6 @@ export default class Store extends EventEmitter { weight: 0, }); - if (this._isProfiling) { - this._profilingSnapshotsByRootID.set(id, new Map()); - } - haveRootsChanged = true; } else { parentID = ((operations[i]: any): number); @@ -956,10 +851,6 @@ export default class Store extends EventEmitter { this._rootIDToRendererID.delete(id); this._rootIDToCapabilities.delete(id); - this._profilingOperationsByRootID.delete(id); - this._profilingScreenshotsByRootID.delete(id); - this._profilingSnapshotsByRootID.delete(id); - haveRootsChanged = true; } else { if (__DEBUG__) { @@ -1065,60 +956,12 @@ export default class Store extends EventEmitter { this.emit('mutated', [addedElementIDs, removedElementIDs]); }; - onProfilingStatus = (isProfiling: boolean) => { - if (isProfiling) { - this._profilingData = null; - this._profilingOperationsByRootID = new Map(); - this._profilingScreenshotsByRootID = new Map(); - this._profilingSnapshotsByRootID = new Map(); - this.roots.forEach(rootID => { - const profilingSnapshots = new Map(); - this._profilingSnapshotsByRootID.set(rootID, profilingSnapshots); - this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots); - }); - } - - if (this._isProfiling !== isProfiling) { - this._isProfiling = isProfiling; - - // Invalidate suspense cache if profiling data is being (re-)recorded. - // Note that we clear again, in case any views read from the cache while profiling. - // (That would have resolved a now-stale value without any profiling data.) - this._profilingCache.invalidate(); - - this.emit('isProfiling'); - } - }; - - onScreenshotCaptured = ({ - commitIndex, - dataURL, - rootID, - }: {| - commitIndex: number, - dataURL: string, - rootID: number, - |}) => { - let profilingScreenshotsForRootByCommitIndex = this._profilingScreenshotsByRootID.get( - rootID - ); - if (!profilingScreenshotsForRootByCommitIndex) { - profilingScreenshotsForRootByCommitIndex = new Map(); - this._profilingScreenshotsByRootID.set( - rootID, - profilingScreenshotsForRootByCommitIndex - ); - } - profilingScreenshotsForRootByCommitIndex.set(commitIndex, dataURL); - }; - onBridgeShutdown = () => { if (__DEBUG__) { debug('onBridgeShutdown', 'unsubscribing from Bridge'); } this._bridge.removeListener('operations', this.onBridgeOperations); - this._bridge.removeListener('profilingStatus', this.onProfilingStatus); this._bridge.removeListener('shutdown', this.onBridgeShutdown); }; } diff --git a/src/devtools/views/Profiler/CommitFlamegraph.js b/src/devtools/views/Profiler/CommitFlamegraph.js index 091fd96b61..58c714b523 100644 --- a/src/devtools/views/Profiler/CommitFlamegraph.js +++ b/src/devtools/views/Profiler/CommitFlamegraph.js @@ -13,7 +13,7 @@ import { StoreContext } from '../context'; import styles from './CommitFlamegraph.css'; import type { ChartData, ChartNode } from './FlamegraphChartBuilder'; -import type { CommitDetailsFrontend, CommitTreeFrontend } from './types'; +import type { CommitTree } from './types'; export type ItemData = {| chartData: ChartData, @@ -26,7 +26,7 @@ export type ItemData = {| export default function CommitFlamegraphAutoSizer(_: {||}) { const { profilingCache } = useContext(StoreContext); - const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext( + const { rootID, selectedCommitIndex, selectFiber } = useContext( ProfilerContext ); @@ -38,39 +38,22 @@ export default function CommitFlamegraphAutoSizer(_: {||}) { [selectFiber] ); - const profilingSummary = profilingCache.ProfilingSummary.read({ - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - }); - - let commitDetails: CommitDetailsFrontend | null = null; - let commitTree: CommitTreeFrontend | null = null; + let commitTree: CommitTree | null = null; let chartData: ChartData | null = null; if (selectedCommitIndex !== null) { - commitDetails = profilingCache.CommitDetails.read({ + commitTree = profilingCache.getCommitTree({ commitIndex: selectedCommitIndex, - rendererID: ((rendererID: any): number), rootID: ((rootID: any): number), }); - commitTree = profilingCache.getCommitTree({ - commitIndex: selectedCommitIndex, - profilingSummary, - }); - chartData = profilingCache.getFlamegraphChartData({ - commitDetails, commitIndex: selectedCommitIndex, commitTree, + rootID: ((rootID: any): number), }); } - if ( - commitDetails != null && - commitTree != null && - chartData != null && - chartData.depth > 0 - ) { + if (commitTree != null && chartData != null && chartData.depth > 0) { return (
@@ -79,8 +62,7 @@ export default function CommitFlamegraphAutoSizer(_: {||}) { // by the time this render prop function is called, the values of the `let` variables have not changed. @@ -95,19 +77,12 @@ export default function CommitFlamegraphAutoSizer(_: {||}) { type Props = {| chartData: ChartData, - commitDetails: CommitDetailsFrontend, - commitTree: CommitTreeFrontend, + commitTree: CommitTree, height: number, width: number, |}; -function CommitFlamegraph({ - chartData, - commitDetails, - commitTree, - height, - width, -}: Props) { +function CommitFlamegraph({ chartData, commitTree, height, width }: Props) { const { selectFiber, selectedFiberID } = useContext(ProfilerContext); const selectedChartNodeIndex = useMemo(() => { diff --git a/src/devtools/views/Profiler/CommitRanked.js b/src/devtools/views/Profiler/CommitRanked.js index 63200f37ba..466475c16b 100644 --- a/src/devtools/views/Profiler/CommitRanked.js +++ b/src/devtools/views/Profiler/CommitRanked.js @@ -13,7 +13,7 @@ import { StoreContext } from '../context'; import styles from './CommitRanked.css'; import type { ChartData } from './RankedChartBuilder'; -import type { CommitDetailsFrontend, CommitTreeFrontend } from './types'; +import type { CommitTree } from './types'; export type ItemData = {| chartData: ChartData, @@ -26,7 +26,7 @@ export type ItemData = {| export default function CommitRankedAutoSizer(_: {||}) { const { profilingCache } = useContext(StoreContext); - const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext( + const { rootID, selectedCommitIndex, selectFiber } = useContext( ProfilerContext ); @@ -38,47 +38,29 @@ export default function CommitRankedAutoSizer(_: {||}) { [selectFiber] ); - const profilingSummary = profilingCache.ProfilingSummary.read({ - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - }); - - let commitDetails: CommitDetailsFrontend | null = null; - let commitTree: CommitTreeFrontend | null = null; + let commitTree: CommitTree | null = null; let chartData: ChartData | null = null; if (selectedCommitIndex !== null) { - commitDetails = profilingCache.CommitDetails.read({ + commitTree = profilingCache.getCommitTree({ commitIndex: selectedCommitIndex, - rendererID: ((rendererID: any): number), rootID: ((rootID: any): number), }); - commitTree = profilingCache.getCommitTree({ - commitIndex: selectedCommitIndex, - profilingSummary, - }); - chartData = profilingCache.getRankedChartData({ - commitDetails, commitIndex: selectedCommitIndex, commitTree, + rootID: ((rootID: any): number), }); } - if ( - commitDetails != null && - commitTree != null && - chartData != null && - chartData.nodes.length > 0 - ) { + if (commitTree != null && chartData != null && chartData.nodes.length > 0) { return (
{({ height, width }) => ( @@ -93,19 +75,12 @@ export default function CommitRankedAutoSizer(_: {||}) { type Props = {| chartData: ChartData, - commitDetails: CommitDetailsFrontend, - commitTree: CommitTreeFrontend, + commitTree: CommitTree, height: number, width: number, |}; -function CommitRanked({ - chartData, - commitDetails, - commitTree, - height, - width, -}: Props) { +function CommitRanked({ chartData, commitTree, height, width }: Props) { const { selectedFiberID, selectFiber } = useContext(ProfilerContext); const selectedFiberIndex = useMemo( diff --git a/src/devtools/views/Profiler/CommitTreeBuilder.js b/src/devtools/views/Profiler/CommitTreeBuilder.js index eff090f4bd..ea18a53812 100644 --- a/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -9,14 +9,13 @@ import { } from 'src/constants'; import { utfDecodeString } from 'src/utils'; import { ElementTypeRoot } from 'src/types'; -import Store from 'src/devtools/store'; +import ProfilerStore from 'src/devtools/ProfilerStore'; import type { ElementType } from 'src/types'; import type { - CommitTreeFrontend, - CommitTreeNodeFrontend, - ProfilingSnapshotNode, - ProfilingSummaryFrontend, + CommitTree, + CommitTreeNode, + ProfilingDataForRootFrontend, } from 'src/devtools/views/Profiler/types'; const debug = (methodName, ...args) => { @@ -30,36 +29,40 @@ const debug = (methodName, ...args) => { } }; -const rootToCommitTreeMap: Map> = new Map(); +const rootToCommitTreeMap: Map> = new Map(); export function getCommitTree({ commitIndex, - profilingSummary, - store, + profilerStore, + rootID, }: {| commitIndex: number, - profilingSummary: ProfilingSummaryFrontend, - store: Store, -|}): CommitTreeFrontend { - const { rootID } = profilingSummary; - + profilerStore: ProfilerStore, + rootID: number, +|}): CommitTree { if (!rootToCommitTreeMap.has(rootID)) { rootToCommitTreeMap.set(rootID, []); } const commitTrees = ((rootToCommitTreeMap.get( rootID - ): any): Array); + ): any): Array); if (commitIndex < commitTrees.length) { return commitTrees[commitIndex]; } - const { profilingData } = store; - const profilingOperations = - profilingData != null - ? profilingData.profilingOperations - : store.profilingOperations; + const { profilingData } = profilerStore; + if (profilingData === null) { + throw Error(`No profiling data available`); + } + + const dataForRoot = profilingData.dataForRoots.get(rootID); + if (dataForRoot == null) { + throw Error(`Could not find profiling data for root "${rootID}"`); + } + + const { operations } = dataForRoot; // Commits are generated sequentially and cached. // If this is the very first commit, start with the cached snapshot and apply the first mutation. @@ -67,32 +70,12 @@ export function getCommitTree({ if (commitIndex === 0) { const nodes = new Map(); - const { profilingData } = store; - const profilingSnapshots = - profilingData != null - ? profilingData.profilingSnapshots.get(rootID) - : store.profilingSnapshots.get(rootID); - - if (profilingSnapshots == null) { - throw Error(`Could not find profiling snapshot for root "${rootID}"`); - } - // Construct the initial tree. - recursivelyInitializeTree( - rootID, - 0, - nodes, - profilingSummary.initialTreeBaseDurations, - profilingSnapshots - ); + recursivelyInitializeTree(rootID, 0, nodes, dataForRoot); // Mutate the tree - const commitOperations = profilingOperations.get(rootID); - if (commitOperations != null && commitIndex < commitOperations.length) { - const commitTree = updateTree( - { nodes, rootID }, - commitOperations[commitIndex] - ); + if (operations != null && commitIndex < operations.length) { + const commitTree = updateTree({ nodes, rootID }, operations[commitIndex]); if (__DEBUG__) { __printTree(commitTree); @@ -104,14 +87,14 @@ export function getCommitTree({ } else { const previousCommitTree = getCommitTree({ commitIndex: commitIndex - 1, - profilingSummary, - store, + profilerStore, + rootID, }); - const commitOperations = profilingOperations.get(rootID); - if (commitOperations != null && commitIndex < commitOperations.length) { + + if (operations != null && commitIndex < operations.length) { const commitTree = updateTree( previousCommitTree, - commitOperations[commitIndex] + operations[commitIndex] ); if (__DEBUG__) { @@ -131,11 +114,10 @@ export function getCommitTree({ function recursivelyInitializeTree( id: number, parentID: number, - nodes: Map, - initialTreeBaseDurations: Map, - profilingSnapshots: Map + nodes: Map, + dataForRoot: ProfilingDataForRootFrontend ): void { - const node = profilingSnapshots.get(id); + const node = dataForRoot.snapshots.get(id); if (node != null) { nodes.set(id, { id, @@ -143,35 +125,31 @@ function recursivelyInitializeTree( displayName: node.displayName, key: node.key, parentID, - treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number), + treeBaseDuration: ((dataForRoot.initialTreeBaseDurations.get( + id + ): any): number), type: node.type, }); node.children.forEach(childID => - recursivelyInitializeTree( - childID, - id, - nodes, - initialTreeBaseDurations, - profilingSnapshots - ) + recursivelyInitializeTree(childID, id, nodes, dataForRoot) ); } } function updateTree( - commitTree: CommitTreeFrontend, + commitTree: CommitTree, operations: Uint32Array -): CommitTreeFrontend { +): CommitTree { // Clone the original tree so edits don't affect it. const nodes = new Map(commitTree.nodes); // Clone nodes before mutating them so edits don't affect them. - const getClonedNode = (id: number): CommitTreeNodeFrontend => { + const getClonedNode = (id: number): CommitTreeNode => { const clonedNode = ((Object.assign( {}, nodes.get(id) - ): any): CommitTreeNodeFrontend); + ): any): CommitTreeNode); nodes.set(id, clonedNode); return clonedNode; }; @@ -219,7 +197,7 @@ function updateTree( debug('Add', `new root fiber ${id}`); } - const node: CommitTreeNodeFrontend = { + const node: CommitTreeNode = { children: [], displayName: null, id, @@ -254,7 +232,7 @@ function updateTree( const parentNode = getClonedNode(parentID); parentNode.children = parentNode.children.concat(id); - const node: CommitTreeNodeFrontend = { + const node: CommitTreeNode = { children: [], displayName, id, @@ -355,7 +333,7 @@ export function invalidateCommitTrees(): void { } // DEBUG -const __printTree = (commitTree: CommitTreeFrontend) => { +const __printTree = (commitTree: CommitTree) => { if (__DEBUG__) { const { nodes, rootID } = commitTree; console.group('__printTree()'); diff --git a/src/devtools/views/Profiler/FlamegraphChartBuilder.js b/src/devtools/views/Profiler/FlamegraphChartBuilder.js index 994a08c777..7287d09189 100644 --- a/src/devtools/views/Profiler/FlamegraphChartBuilder.js +++ b/src/devtools/views/Profiler/FlamegraphChartBuilder.js @@ -2,8 +2,9 @@ import { ElementTypeForwardRef, ElementTypeMemo } from 'src/types'; import { formatDuration } from './utils'; +import ProfilerStore from 'src/devtools/ProfilerStore'; -import type { CommitDetailsFrontend, CommitTreeFrontend } from './types'; +import type { CommitTree } from './types'; export type ChartNode = {| actualDuration: number, @@ -28,15 +29,19 @@ export type ChartData = {| const cachedChartData: Map = new Map(); export function getChartData({ - commitDetails, commitIndex, commitTree, + profilerStore, + rootID, }: {| - commitDetails: CommitDetailsFrontend, commitIndex: number, - commitTree: CommitTreeFrontend, + commitTree: CommitTree, + profilerStore: ProfilerStore, + rootID: number, |}): ChartData { - const { actualDurations, rootID, selfDurations } = commitDetails; + const commitDatum = profilerStore.getCommitData(rootID, commitIndex); + + const { fiberActualDurations, fiberSelfDurations } = commitDatum; const { nodes } = commitTree; const key = `${rootID}-${commitIndex}`; @@ -62,9 +67,9 @@ export function getChartData({ const { children, displayName, key, treeBaseDuration, type } = node; - const actualDuration = actualDurations.get(id) || 0; - const selfDuration = selfDurations.get(id) || 0; - const didRender = actualDurations.has(id); + const actualDuration = fiberActualDurations.get(id) || 0; + const selfDuration = fiberSelfDurations.get(id) || 0; + const didRender = fiberActualDurations.has(id); const name = displayName || 'Anonymous'; const maybeKey = key !== null ? ` key="${key}"` : ''; @@ -131,7 +136,7 @@ export function getChartData({ walkTree(id, baseDuration, 1); } - actualDurations.forEach((duration, id) => { + fiberActualDurations.forEach((duration, id) => { const node = nodes.get(id); if (node != null) { let currentID = node.parentID; diff --git a/src/devtools/views/Profiler/InteractionListItem.js b/src/devtools/views/Profiler/InteractionListItem.js index a4e3cd7054..4e6d8c746b 100644 --- a/src/devtools/views/Profiler/InteractionListItem.js +++ b/src/devtools/views/Profiler/InteractionListItem.js @@ -17,9 +17,8 @@ type Props = { function InteractionListItem({ data: itemData, index, style }: Props) { const { chartData, - interactions, + dataForRoot, labelWidth, - profilingSummary, scaleX, selectedInteractionID, selectCommitIndex, @@ -27,20 +26,22 @@ function InteractionListItem({ data: itemData, index, style }: Props) { selectTab, } = itemData; - const { maxCommitDuration } = chartData; - const { commitDurations, commitTimes } = profilingSummary; + const { commitData, interactionCommits } = dataForRoot; + const { interactions, lastInteractionTime, maxCommitDuration } = chartData; const interaction = interactions[index]; + if (interaction == null) { + throw Error(`Could not find interaction #${index}`); + } const handleClick = useCallback(() => { selectInteraction(interaction.id); }, [interaction, selectInteraction]); + const commits = interactionCommits.get(interaction.id) || []; + const startTime = interaction.timestamp; - const stopTime = - interaction.commits.length > 0 - ? commitTimes[interaction.commits[interaction.commits.length - 1]] - : interaction.timestamp; + const stopTime = lastInteractionTime; const viewCommit = (commitIndex: number) => { selectTab('flame-chart'); @@ -71,7 +72,7 @@ function InteractionListItem({ data: itemData, index, style }: Props) { width: scaleX(stopTime - startTime, 0), }} /> - {interaction.commits.map(commitIndex => ( + {commits.map(commitIndex => (
))} diff --git a/src/devtools/views/Profiler/Interactions.js b/src/devtools/views/Profiler/Interactions.js index c9c5d3f14f..6c560f7ed9 100644 --- a/src/devtools/views/Profiler/Interactions.js +++ b/src/devtools/views/Profiler/Interactions.js @@ -11,18 +11,14 @@ import { scale } from './utils'; import styles from './Interactions.css'; +import type { ProfilingDataForRootFrontend } from './types'; import type { ChartData } from './InteractionsChartBuilder'; import type { TabID } from './ProfilerContext'; -import type { - InteractionWithCommitsFrontend, - ProfilingSummaryFrontend, -} from './types'; export type ItemData = {| chartData: ChartData, - interactions: Array, + dataForRoot: ProfilingDataForRootFrontend, labelWidth: number, - profilingSummary: ProfilingSummaryFrontend, scaleX: (value: number, fallbackValue: number) => number, selectedInteractionID: number | null, selectCommitIndex: (id: number | null) => void, @@ -42,29 +38,21 @@ export default function InteractionsAutoSizer(_: {||}) { function Interactions({ height, width }: {| height: number, width: number |}) { const { - rendererID, rootID, selectedInteractionID, selectInteraction, selectCommitIndex, selectTab, } = useContext(ProfilerContext); - const { profilingCache } = useContext(StoreContext); + const { profilerStore } = useContext(StoreContext); - const { interactions } = profilingCache.Interactions.read({ - rendererID: ((rendererID: any): number), + const dataForRoot = profilerStore.getDataForRoot(((rootID: any): number)); + + const chartData = profilerStore.cache.getInteractionsChartData({ rootID: ((rootID: any): number), }); - const profilingSummary = profilingCache.ProfilingSummary.read({ - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - }); - - const chartData = profilingCache.getInteractionsChartData({ - interactions, - profilingSummary, - }); + const { interactions } = chartData; const handleKeyDown = useCallback( event => { @@ -110,9 +98,8 @@ function Interactions({ height, width }: {| height: number, width: number |}) { return { chartData, - interactions, + dataForRoot, labelWidth, - profilingSummary, scaleX: scale(0, chartData.lastInteractionTime, 0, timelineWidth), selectedInteractionID, selectCommitIndex, @@ -121,8 +108,7 @@ function Interactions({ height, width }: {| height: number, width: number |}) { }; }, [ chartData, - interactions, - profilingSummary, + dataForRoot, selectedInteractionID, selectCommitIndex, selectInteraction, diff --git a/src/devtools/views/Profiler/InteractionsChartBuilder.js b/src/devtools/views/Profiler/InteractionsChartBuilder.js index 08bf71be00..6171749ef0 100644 --- a/src/devtools/views/Profiler/InteractionsChartBuilder.js +++ b/src/devtools/views/Profiler/InteractionsChartBuilder.js @@ -1,11 +1,11 @@ // @flow -import type { - InteractionWithCommitsFrontend, - ProfilingSummaryFrontend, -} from './types'; +import ProfilerStore from 'src/devtools/ProfilerStore'; + +import type { Interaction } from './types'; export type ChartData = {| + interactions: Array, lastInteractionTime: number, maxCommitDuration: number, |}; @@ -13,30 +13,37 @@ export type ChartData = {| const cachedChartData: Map = new Map(); export function getChartData({ - interactions, - profilingSummary, + profilerStore, + rootID, }: {| - interactions: Array, - profilingSummary: ProfilingSummaryFrontend, + profilerStore: ProfilerStore, + rootID: number, |}): ChartData { - const { rootID } = profilingSummary; - if (cachedChartData.has(rootID)) { return ((cachedChartData.get(rootID): any): ChartData); } - const { commitDurations, commitTimes } = profilingSummary; + const dataForRoot = profilerStore.getDataForRoot(rootID); + if (dataForRoot == null) { + throw Error(`Could not find profiling data for root "${rootID}"`); + } + + const { commitData, interactions } = dataForRoot; const lastInteractionTime = - commitTimes.length > 0 ? commitTimes[commitTimes.length - 1] : 0; + commitData.length > 0 ? commitData[commitData.length - 1].timestamp : 0; let maxCommitDuration = 0; - commitDurations.forEach(commitDuration => { - maxCommitDuration = Math.max(maxCommitDuration, commitDuration); + commitData.forEach(commitDatum => { + maxCommitDuration = Math.max(maxCommitDuration, commitDatum.duration); }); - const chartData = { lastInteractionTime, maxCommitDuration }; + const chartData = { + interactions: Array.from(interactions.values()), + lastInteractionTime, + maxCommitDuration, + }; cachedChartData.set(rootID, chartData); diff --git a/src/devtools/views/Profiler/ProfilerContext.js b/src/devtools/views/Profiler/ProfilerContext.js index 1658e52430..c829516a14 100644 --- a/src/devtools/views/Profiler/ProfilerContext.js +++ b/src/devtools/views/Profiler/ProfilerContext.js @@ -16,7 +16,7 @@ import { import { StoreContext } from '../context'; import Store from '../../store'; -import type { ImportedProfilingData } from './types'; +import type { ProfilingDataFrontend } from './types'; export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions'; @@ -72,7 +72,7 @@ ProfilerContext.displayName = 'ProfilerContext'; type StoreProfilingState = {| hasProfilingData: boolean, - profilingData: ImportedProfilingData | null, + profilingData: ProfilingDataFrontend | null, isProfiling: boolean, |}; @@ -117,11 +117,11 @@ function ProfilerContextController({ children }: Props) { rendererID = store.getRendererIDForElement(selectedElementID); rootID = store.getRootIDForElement(selectedElementID); rootHasProfilingData = - rootID === null ? false : store.profilingOperations.has(rootID); + rootID === null ? false : store.profilingOperationsByRootID.has(rootID); } else if (store.roots.length > 0) { // If no root is selected, assume the first root; many React apps are single root anyway. rootID = store.roots[0]; - rootHasProfilingData = store.profilingOperations.has(rootID); + rootHasProfilingData = store.profilingOperationsByRootID.has(rootID); rendererID = store.getRendererIDForElement(rootID); } diff --git a/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/src/devtools/views/Profiler/ProfilingImportExportButtons.js index 14bf451d29..c196e6d6b2 100644 --- a/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -5,20 +5,20 @@ import { ProfilerContext } from './ProfilerContext'; import { ModalDialogContext } from '../ModalDialog'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; -import { BridgeContext, StoreContext } from '../context'; +import { StoreContext } from '../context'; import { - prepareExportedProfilingSummary, - prepareImportedProfilingData, + prepareProfilingDataExport, + prepareProfilingDataFrontendFromExport, } from './utils'; import styles from './ProfilingImportExportButtons.css'; +import type { ProfilingDataExport } from './types'; + export default function ProfilingImportExportButtons() { - const bridge = useContext(BridgeContext); - const { isProfiling, rendererID, rootHasProfilingData, rootID } = useContext( - ProfilerContext - ); + const { isProfiling, rendererID, rootID } = useContext(ProfilerContext); const store = useContext(StoreContext); + const { profilerStore } = store; const inputRef = useRef(null); @@ -29,20 +29,15 @@ export default function ProfilingImportExportButtons() { return; } - const exportedProfilingSummary = prepareExportedProfilingSummary( - store.profilingOperations, - store.profilingSnapshots, - rootID, - rendererID - ); - bridge.send('exportProfilingSummary', exportedProfilingSummary); - }, [ - bridge, - rendererID, - rootID, - store.profilingOperations, - store.profilingSnapshots, - ]); + if (profilerStore.profilingData !== null) { + const profilingDataExport = prepareProfilingDataExport( + profilerStore.profilingData + ); + + // TODO (profarc) Generate anchor "download" tag and click it + console.log('profilingDataExport:', profilingDataExport); + } + }, [rendererID, rootID, profilerStore.profilingData]); const uploadData = useCallback(() => { if (inputRef.current !== null) { @@ -57,7 +52,12 @@ export default function ProfilingImportExportButtons() { fileReader.addEventListener('load', () => { try { const raw = ((fileReader.result: any): string); - store.profilingData = prepareImportedProfilingData(raw); + const profilingDataExport = ((JSON.parse( + raw + ): any): ProfilingDataExport); + store.profilingData = prepareProfilingDataFrontendFromExport( + profilingDataExport + ); } catch (error) { modalDialogDispatch({ type: 'SHOW', @@ -97,7 +97,7 @@ export default function ProfilingImportExportButtons() { {store.supportsFileDownloads && ( - ))} + {interactionIDs.map(interactionID => { + const interaction = interactions.get(interactionID); + if (interaction == null) { + throw Error(`Invalid interaction "${interactionID}"`); + } + return ( + + ); + })}
{captureScreenshots && ( diff --git a/src/devtools/views/Profiler/SidebarInteractions.js b/src/devtools/views/Profiler/SidebarInteractions.js index 574c29141d..035dfec507 100644 --- a/src/devtools/views/Profiler/SidebarInteractions.js +++ b/src/devtools/views/Profiler/SidebarInteractions.js @@ -13,48 +13,69 @@ export type Props = {||}; export default function SidebarInteractions(_: Props) { const { selectedInteractionID, - rendererID, rootID, selectCommitIndex, selectTab, } = useContext(ProfilerContext); - const { profilingCache } = useContext(StoreContext); + const { profilingCache, profilerStore } = useContext(StoreContext); if (selectedInteractionID === null) { return
Nothing selected
; } - const { interactions } = profilingCache.Interactions.read({ - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - }); - const interaction = interactions.find( - interaction => interaction.id === selectedInteractionID + const { interactionCommits, interactions } = profilerStore.getDataForRoot( + ((rootID: any): number) ); + const interaction = interactions.get(selectedInteractionID); if (interaction == null) { throw Error( `Could not find interaction by selected interaction id "${selectedInteractionID}"` ); } - const profilingSummary = profilingCache.ProfilingSummary.read({ - rendererID: ((rendererID: any): number), + const { maxCommitDuration } = profilingCache.getInteractionsChartData({ rootID: ((rootID: any): number), }); - const { maxCommitDuration } = profilingCache.getInteractionsChartData({ - interactions, - profilingSummary, - }); - - const { commitDurations, commitTimes } = profilingSummary; - const viewCommit = (commitIndex: number) => { selectTab('flame-chart'); selectCommitIndex(commitIndex); }; + const listItems: Array = []; + const commitIndices = interactionCommits.get(selectedInteractionID); + if (commitIndices != null) { + commitIndices.forEach(commitIndex => { + const { duration, timestamp } = profilerStore.getCommitData( + ((rootID: any): number), + commitIndex + ); + + listItems.push( +
  • viewCommit(commitIndex)} + > +
    +
    + timestamp: {formatTime(timestamp)}s +
    + duration: {formatDuration(duration)}ms +
    +
  • + ); + }); + } + return (
    @@ -62,35 +83,7 @@ export default function SidebarInteractions(_: Props) {
    Commits:
    -
      - {interaction.commits.map(commitIndex => ( -
    • viewCommit(commitIndex)} - > -
      -
      - timestamp: {formatTime(commitTimes[commitIndex])}s -
      - duration: {formatDuration(commitDurations[commitIndex])}ms -
      -
    • - ))} -
    +
      {listItems}
    ); diff --git a/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js b/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js index 8739509575..c99d0cdb02 100644 --- a/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js +++ b/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js @@ -12,9 +12,8 @@ import styles from './SidebarSelectedFiberInfo.css'; export type Props = {||}; export default function SidebarSelectedFiberInfo(_: Props) { - const { profilingCache } = useContext(StoreContext); + const { profilingCache, profilerStore } = useContext(StoreContext); const { - rendererID, rootID, selectCommitIndex, selectedCommitIndex, @@ -23,22 +22,19 @@ export default function SidebarSelectedFiberInfo(_: Props) { selectFiber, } = useContext(ProfilerContext); - const { commitTimes } = profilingCache.ProfilingSummary.read({ - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - }); - - const { commitDurations } = profilingCache.FiberCommits.read({ + const commitIndices = profilingCache.getFiberCommits({ fiberID: ((selectedFiberID: any): number), - rendererID: ((rendererID: any): number), rootID: ((rootID: any): number), }); const listItems = []; - for (let i = 0; i < commitDurations.length; i += 2) { - const commitIndex = commitDurations[i]; - const duration = commitDurations[i + 1]; - const time = commitTimes[commitIndex]; + for (let i = 0; i < commitIndices.length; i += 2) { + const commitIndex = commitIndices[i]; + + const { duration, timestamp } = profilerStore.getCommitData( + ((rootID: any): number), + commitIndex + ); listItems.push( ); } diff --git a/src/devtools/views/Profiler/SnapshotSelector.js b/src/devtools/views/Profiler/SnapshotSelector.js index f609d2cdcf..a41187d89d 100644 --- a/src/devtools/views/Profiler/SnapshotSelector.js +++ b/src/devtools/views/Profiler/SnapshotSelector.js @@ -16,29 +16,33 @@ export default function SnapshotSelector(_: Props) { const { isCommitFilterEnabled, minCommitDuration, - rendererID, rootID, selectedCommitIndex, selectCommitIndex, } = useContext(ProfilerContext); - const { profilingCache } = useContext(StoreContext); - const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read( - { - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - } - ); + const { profilerStore } = useContext(StoreContext); + const { commitData } = profilerStore.getDataForRoot(((rootID: any): number)); + + const commitDurations: Array = []; + const commitTimes: Array = []; + commitData.forEach(commitDatum => { + commitDurations.push(commitDatum.duration); + commitTimes.push(commitDatum.timestamp); + }); const filteredCommitIndices = useMemo( () => - commitDurations.reduce((reduced, commitDuration, index) => { - if (!isCommitFilterEnabled || commitDuration >= minCommitDuration) { + commitData.reduce((reduced, commitDatum, index) => { + if ( + !isCommitFilterEnabled || + commitDatum.duration >= minCommitDuration + ) { reduced.push(index); } return reduced; }, []), - [commitDurations, isCommitFilterEnabled, minCommitDuration] + [commitData, isCommitFilterEnabled, minCommitDuration] ); const numFilteredCommits = filteredCommitIndices.length; @@ -112,7 +116,7 @@ export default function SnapshotSelector(_: Props) { [viewNextCommit, viewPrevCommit] ); - if (commitDurations.length === 0) { + if (commitData.length === 0) { return null; } diff --git a/src/devtools/views/Profiler/types.js b/src/devtools/views/Profiler/types.js index 99e0430465..d6a5bb4b2f 100644 --- a/src/devtools/views/Profiler/types.js +++ b/src/devtools/views/Profiler/types.js @@ -1,13 +1,8 @@ // @flow import type { ElementType } from 'src/types'; -import type { - CommitDetailsBackend, - InteractionsBackend, - ProfilingSummaryBackend, -} from 'src/backend/types'; -export type CommitTreeNodeFrontend = {| +export type CommitTreeNode = {| id: number, children: Array, displayName: string | null, @@ -17,58 +12,18 @@ export type CommitTreeNodeFrontend = {| type: ElementType, |}; -export type CommitTreeFrontend = {| - nodes: Map, +export type CommitTree = {| + nodes: Map, rootID: number, |}; -export type InteractionFrontend = {| +export type Interaction = {| id: number, name: string, timestamp: number, |}; -export type InteractionWithCommitsFrontend = {| - ...InteractionFrontend, - commits: Array, -|}; - -export type InteractionsFrontend = {| - interactions: Array, - rootID: number, -|}; - -export type CommitDetailsFrontend = {| - actualDurations: Map, - commitIndex: number, - interactions: Array, - priorityLevel: string | null, - rootID: number, - selfDurations: Map, -|}; - -export type FiberCommitsFrontend = {| - commitDurations: Array, - fiberID: number, - rootID: number, -|}; - -export type ProfilingSummaryFrontend = {| - rootID: number, - - // Commit durations - commitDurations: Array, - - // Commit times (relative to when profiling started) - commitTimes: Array, - - // Map of fiber id to (initial) tree base duration - initialTreeBaseDurations: Map, - - interactionCount: number, -|}; - -export type ProfilingSnapshotNode = {| +export type SnapshotNode = {| id: number, children: Array, displayName: string | null, @@ -76,35 +31,94 @@ export type ProfilingSnapshotNode = {| type: ElementType, |}; -export type ImportedProfilingData = {| - version: 3, - profilingOperations: Map>, - profilingSnapshots: Map>, - commitDetails: Array, - interactions: InteractionsFrontend, - profilingSummary: ProfilingSummaryFrontend, +export type CommitDataFrontend = {| + // How long was this commit? + duration: number, + + // Map of Fiber (ID) to actual duration for this commit; + // Fibers that did not render will not have entries in this Map. + fiberActualDurations: Map, + + // Map of Fiber (ID) to "self duration" for this commit; + // Fibers that did not render will not have entries in this Map. + fiberSelfDurations: Map, + + // Which interactions (IDs) were associated with this commit. + interactionIDs: Array, + + // Priority level of the commit (if React provided this info) + priorityLevel: string | null, + + // Screenshot data for this commit (if available). + screenshot: string | null, + + // When did this commit occur (relative to the start of profiling) + timestamp: number, |}; -export type SerializableProfilingDataOperationsByRootID = Array< - [number, Array>] ->; -export type SerializableProfilingDataSnapshotsByRootID = Array< - [number, Array<[number, ProfilingSnapshotNode]>] ->; +export type ProfilingDataForRootFrontend = {| + // Timing, duration, and other metadata about each commit. + commitData: Array, -export type ExportedProfilingSummaryFromFrontend = {| - version: 3, - profilingOperationsByRootID: SerializableProfilingDataOperationsByRootID, - profilingSnapshotsByRootID: SerializableProfilingDataSnapshotsByRootID, - rendererID: number, + // Display name of the nearest descendant component (ideally a function or class component). + // This value is used by the root selector UI. + displayName: string, + + // Map of fiber id to (initial) tree base duration when Profiling session was started. + // This info can be used along with commitOperations to reconstruct the tree for any commit. + initialTreeBaseDurations: Map, + + // All interactions recorded (for this root) during the current session. + interactionCommits: Map>, + + // All interactions recorded (for this root) during the current session. + interactions: Map, + + // List of tree mutation that occur during profiling. + // These mutations can be used along with initial snapshots to reconstruct the tree for any commit. + operations: Array, + + // Identifies the root this profiler data corresponds to. rootID: number, + + // Map of fiber id to node when the Profiling session was started. + // This info can be used along with commitOperations to reconstruct the tree for any commit. + snapshots: Map, |}; -export type ExportedProfilingData = {| - version: 3, - profilingOperationsByRootID: SerializableProfilingDataOperationsByRootID, - profilingSnapshotsByRootID: SerializableProfilingDataSnapshotsByRootID, - commitDetails: Array, - interactions: InteractionsBackend, - profilingSummary: ProfilingSummaryBackend, +// Combination of profiling data collected by the renderer interface (backend) and Store (frontend). +export type ProfilingDataFrontend = {| + // Profiling data per root. + dataForRoots: Map, +|}; + +export type CommitDataExport = {| + duration: number, + // Tuple of fiber ID and actual duration + fiberActualDurations: Array<[number, number]>, + // Tuple of fiber ID and computed "self" duration + fiberSelfDurations: Array<[number, number]>, + interactionIDs: Array, + priorityLevel: string | null, + screenshot: string | null, + timestamp: number, +|}; + +export type ProfilingDataForRootExport = {| + commitData: Array, + displayName: string, + // Tuple of Fiber ID and base duration + initialTreeBaseDurations: Array<[number, number]>, + // Tuple of Interaction ID and commit indices + interactionCommits: Array<[number, Array]>, + interactions: Array<[number, Interaction]>, + operations: Array>, + rootID: number, + snapshots: Array<[number, SnapshotNode]>, +|}; + +// Serializable vefrsion of ProfilingDataFrontend data. +export type ProfilingDataExport = {| + version: 5, + dataForRoots: Array, |}; diff --git a/src/devtools/views/Profiler/utils.js b/src/devtools/views/Profiler/utils.js index b9222cd58b..f192520f4f 100644 --- a/src/devtools/views/Profiler/utils.js +++ b/src/devtools/views/Profiler/utils.js @@ -2,15 +2,15 @@ import { PROFILER_EXPORT_VERSION } from 'src/constants'; +import type { ProfilingDataBackend } from 'src/backend/types'; import type { - ExportedProfilingSummaryFromFrontend, - ExportedProfilingData, - ImportedProfilingData, - ProfilingSnapshotNode, + ProfilingDataExport, + ProfilingDataForRootExport, + ProfilingDataForRootFrontend, + ProfilingDataFrontend, + SnapshotNode, } from './types'; -import type { ExportedProfilingDataFromRenderer } from 'src/backend/types'; - const commitGradient = [ 'var(--color-commit-gradient-0)', 'var(--color-commit-gradient-1)', @@ -24,153 +24,176 @@ const commitGradient = [ 'var(--color-commit-gradient-9)', ]; -export const prepareExportedProfilingSummary = ( - profilingOperations: Map>, - profilingSnapshots: Map>, - rendererID: number, - rootID: number -) => { - const profilingOperationsForRoot = []; - const operations = profilingOperations.get(rootID); - if (operations != null) { - operations.forEach(operationsTypedArray => { - // Convert typed array to plain array before JSON serialization, or it will be converted to an Object. - const operationsPlainArray = Array.from(operationsTypedArray); - profilingOperationsForRoot.push(operationsPlainArray); - }); - } +// Combines info from the Store (frontend) and renderer interfaces (backend) into the format required by the Profiler UI. +// This format can then be quickly exported (and re-imported). +export function prepareProfilingDataFrontendFromBackendAndStore( + dataBackends: Array, + operationsByRootID: Map>, + screenshotsByRootID: Map>, + snapshotsByRootID: Map> +): ProfilingDataFrontend { + const dataForRoots: Map = new Map(); - // Convert Map to Array of key-value pairs or JSON.stringify will clobber the contents. - const profilingSnapshotsForRoot = []; - const profilingSnapshotsMap = profilingSnapshots.get(rootID); - if (profilingSnapshotsMap != null) { - for (const [elementID, snapshotNode] of profilingSnapshotsMap.entries()) { - profilingSnapshotsForRoot.push([elementID, snapshotNode]); - } - } + dataBackends.forEach(dataBackend => { + dataBackend.dataForRoots.forEach( + ({ + commitData, + displayName, + initialTreeBaseDurations, + interactionCommits, + interactions, + rootID, + }) => { + const screenshots = screenshotsByRootID.get(rootID) || null; - const exportedProfilingSummary: ExportedProfilingSummaryFromFrontend = { - version: PROFILER_EXPORT_VERSION, - profilingOperationsByRootID: [[rootID, profilingOperationsForRoot]], - profilingSnapshotsByRootID: [[rootID, profilingSnapshotsForRoot]], - rendererID, - rootID, - }; - return exportedProfilingSummary; -}; - -export const prepareExportedProfilingData = ( - exportedProfilingDataFromRenderer: ExportedProfilingDataFromRenderer, - exportedProfilingSummary: ExportedProfilingSummaryFromFrontend -): ExportedProfilingData => { - if (exportedProfilingDataFromRenderer.version !== PROFILER_EXPORT_VERSION) { - throw new Error( - `Unsupported profiling data version ${ - exportedProfilingDataFromRenderer.version - } from renderer with id "${exportedProfilingSummary.rendererID}"` - ); - } - if (exportedProfilingSummary.version !== PROFILER_EXPORT_VERSION) { - throw new Error( - `Unsupported profiling summary version ${ - exportedProfilingSummary.version - } from renderer with id "${exportedProfilingSummary.rendererID}"` - ); - } - const exportedProfilingData: ExportedProfilingData = { - version: PROFILER_EXPORT_VERSION, - profilingSummary: exportedProfilingDataFromRenderer.profilingSummary, - commitDetails: exportedProfilingDataFromRenderer.commitDetails, - interactions: exportedProfilingDataFromRenderer.interactions, - profilingOperationsByRootID: - exportedProfilingSummary.profilingOperationsByRootID, - profilingSnapshotsByRootID: - exportedProfilingSummary.profilingSnapshotsByRootID, - }; - return exportedProfilingData; -}; - -/** - * This function should mirror `prepareExportedProfilingData` and `prepareExportedProfilingSummary`. - */ -export const prepareImportedProfilingData = ( - exportedProfilingDataJsonString: string -) => { - const parsed = JSON.parse(exportedProfilingDataJsonString); - - if (parsed.version !== PROFILER_EXPORT_VERSION) { - throw Error(`Unsupported profiler export version "${parsed.version}".`); - } - - // Some "exported" types in `parsed` are `...Backend`, see `prepareExportedProfilingData`, - // they come to `ExportedProfilingData` from `ExportedProfilingDataFromRenderer`. - // But the "imported" types in `ImportedProfilingData` are `...Frontend`, - // and some of them aren't exactly the same as `...Backend` (i.e. an interleaved array versus a map). - // The type annotations here help us to spot the incompatibilities and properly convert. - - const exportedProfilingData: ExportedProfilingData = parsed; - - const profilingSummaryExported = exportedProfilingData.profilingSummary; - const initialTreeBaseDurations = - profilingSummaryExported.initialTreeBaseDurations; - const initialTreeBaseDurationsMap = new Map(); - for (let i = 0; i < initialTreeBaseDurations.length; i += 2) { - const fiberID = initialTreeBaseDurations[i]; - const initialTreeBaseDuration = initialTreeBaseDurations[i + 1]; - initialTreeBaseDurationsMap.set(fiberID, initialTreeBaseDuration); - } - - const profilingData: ImportedProfilingData = { - version: parsed.version, - profilingOperations: new Map( - exportedProfilingData.profilingOperationsByRootID.map( - ([rootID, profilingOperationsForRoot]) => [ - rootID, - profilingOperationsForRoot.map(operations => - Uint32Array.from(operations) - ), - ] - ) - ), - profilingSnapshots: new Map( - exportedProfilingData.profilingSnapshotsByRootID.map( - ([rootID, profilingSnapshotsForRoot]) => [ - rootID, - new Map(profilingSnapshotsForRoot), - ] - ) - ), - commitDetails: exportedProfilingData.commitDetails.map( - commitDetailsBackendItem => { - const durations = commitDetailsBackendItem.durations; - const actualDurationsMap = new Map(); - const selfDurationsMap = new Map(); - for (let i = 0; i < durations.length; i += 3) { - const fiberID = durations[i]; - actualDurationsMap.set(fiberID, durations[i + 1]); - selfDurationsMap.set(fiberID, durations[i + 2]); + const operations = operationsByRootID.get(rootID); + if (operations == null) { + throw Error(`Could not find profiling operations for root ${rootID}`); } - return { - actualDurations: actualDurationsMap, - commitIndex: commitDetailsBackendItem.commitIndex, - interactions: commitDetailsBackendItem.interactions, - priorityLevel: commitDetailsBackendItem.priorityLevel, - rootID: commitDetailsBackendItem.rootID, - selfDurations: selfDurationsMap, - }; + + const snapshots = snapshotsByRootID.get(rootID); + if (snapshots == null) { + throw Error(`Could not find profiling snapshots for root ${rootID}`); + } + + dataForRoots.set(rootID, { + commitData: commitData.map((commitDataBackend, commitIndex) => ({ + duration: commitDataBackend.duration, + fiberActualDurations: new Map( + commitDataBackend.fiberActualDurations + ), + fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations), + interactionIDs: commitDataBackend.interactionIDs, + priorityLevel: commitDataBackend.priorityLevel, + screenshot: + (screenshots !== null && screenshots.get(commitIndex)) || null, + timestamp: commitDataBackend.timestamp, + })), + displayName, + initialTreeBaseDurations: new Map(initialTreeBaseDurations), + interactionCommits: new Map(interactionCommits), + interactions: new Map(interactions), + operations, + rootID, + snapshots, + }); } - ), - interactions: exportedProfilingData.interactions, - profilingSummary: { - rootID: profilingSummaryExported.rootID, - commitDurations: profilingSummaryExported.commitDurations, - commitTimes: profilingSummaryExported.commitTimes, - initialTreeBaseDurations: initialTreeBaseDurationsMap, - interactionCount: profilingSummaryExported.interactionCount, - }, + ); + }); + + return { dataForRoots }; +} + +// Converts a Profiling data export into the format required by the Store. +export function prepareProfilingDataFrontendFromExport( + profilingDataExport: ProfilingDataExport +): ProfilingDataFrontend { + const { version } = profilingDataExport; + + if (version !== PROFILER_EXPORT_VERSION) { + throw Error(`Unsupported profiler export version "${version}"`); + } + + const dataForRoots: Map = new Map(); + profilingDataExport.dataForRoots.forEach( + ({ + commitData, + displayName, + initialTreeBaseDurations, + interactionCommits, + interactions, + operations, + rootID, + snapshots, + }) => { + dataForRoots.set(rootID, { + commitData: commitData.map( + ({ + duration, + fiberActualDurations, + fiberSelfDurations, + interactionIDs, + priorityLevel, + screenshot, + timestamp, + }) => ({ + duration, + fiberActualDurations: new Map(fiberActualDurations), + fiberSelfDurations: new Map(fiberSelfDurations), + interactionIDs, + priorityLevel, + screenshot, + timestamp, + }) + ), + displayName, + initialTreeBaseDurations: new Map(initialTreeBaseDurations), + interactionCommits: new Map(interactionCommits), + interactions: new Map(interactions), + operations: operations.map(array => Uint32Array.from(array)), // Convert Array back to Uint32Array + rootID, + snapshots: new Map(snapshots), + }); + } + ); + + return { dataForRoots }; +} + +// Converts a Store Profiling data into a format that can be safely (JSON) serialized for export. +export function prepareProfilingDataExport( + profilingDataFrontend: ProfilingDataFrontend +): ProfilingDataExport { + const dataForRoots: Array = []; + profilingDataFrontend.dataForRoots.forEach( + ({ + commitData, + displayName, + initialTreeBaseDurations, + interactionCommits, + interactions, + operations, + rootID, + snapshots, + }) => { + dataForRoots.push({ + commitData: commitData.map( + ({ + duration, + fiberActualDurations, + fiberSelfDurations, + interactionIDs, + priorityLevel, + screenshot, + timestamp, + }) => ({ + duration, + fiberActualDurations: Array.from(fiberActualDurations.entries()), + fiberSelfDurations: Array.from(fiberSelfDurations.entries()), + interactionIDs, + priorityLevel, + screenshot, + timestamp, + }) + ), + displayName, + initialTreeBaseDurations: Array.from( + initialTreeBaseDurations.entries() + ), + interactionCommits: Array.from(interactionCommits.entries()), + interactions: Array.from(interactions.entries()), + operations: operations.map(array => Array.from(array)), // Convert Uint32Array to Array for serialization + rootID, + snapshots: Array.from(snapshots.entries()), + }); + } + ); + + return { + version: PROFILER_EXPORT_VERSION, + dataForRoots, }; - return profilingData; -}; +} export const getGradientColor = (value: number) => { const maxIndex = commitGradient.length - 1;