diff --git a/shells/browser/chrome/manifest.json b/shells/browser/chrome/manifest.json index d6ee523d48..00dcf2b35e 100644 --- a/shells/browser/chrome/manifest.json +++ b/shells/browser/chrome/manifest.json @@ -44,7 +44,6 @@ "permissions": [ "", "background", - "downloads", "tabs", "file:///*", "http://*/*", diff --git a/shells/browser/firefox/manifest.json b/shells/browser/firefox/manifest.json index 3ef00fba01..f95a827a5b 100644 --- a/shells/browser/firefox/manifest.json +++ b/shells/browser/firefox/manifest.json @@ -46,7 +46,6 @@ "permissions": [ "", - "downloads", "activeTab", "tabs", "file:///*", diff --git a/shells/browser/shared/src/background.js b/shells/browser/shared/src/background.js index 64e0bfead8..1090ef76d8 100644 --- a/shells/browser/shared/src/background.js +++ b/shells/browser/shared/src/background.js @@ -110,17 +110,6 @@ chrome.runtime.onMessage.addListener((request, sender) => { setIconAndPopup(reactBuildType, sender.tab.id); } - if (request.exportFile) { - let { contents, filename } = request; - if (!Array.isArray(contents)) { - contents = [contents]; - } - - const blob = new Blob(contents, { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - chrome.downloads.download({ filename, saveAs: true, url }); - } - if (request.captureScreenshot) { const { commitIndex, rootID } = request; try { diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index d2da26ba2b..6561cd9664 100644 --- a/shells/browser/shared/src/main.js +++ b/shells/browser/shared/src/main.js @@ -85,13 +85,6 @@ function createPanelIfReactLoaded() { localStorage.setItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); chrome.devtools.inspectedWindow.eval('window.location.reload();'); }); - bridge.addListener('exportFile', ({ contents, filename }) => { - chrome.runtime.sendMessage({ - exportFile: true, - contents, - filename, - }); - }); bridge.addListener('captureScreenshot', ({ commitIndex, rootID }) => { chrome.runtime.sendMessage( { @@ -124,7 +117,6 @@ function createPanelIfReactLoaded() { store = new Store(bridge, { isProfiling, supportsCaptureScreenshots: true, - supportsFileDownloads: browserName === 'Chrome', supportsReloadAndProfile: true, supportsProfiling, }); 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..f9901da82a --- /dev/null +++ b/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -0,0 +1,1869 @@ +// 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`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "duration": 12, + "fiberActualDurations": Array [ + Array [ + 1, + 12, + ], + Array [ + 2, + 12, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 5, + 1, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 5, + 1, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 12, + }, + Object { + "duration": 13, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 6, + 2, + ], + Array [ + 2, + 13, + ], + Array [ + 1, + 13, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 6, + 2, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 25, + }, + Object { + "duration": 10, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 10, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 35, + }, + Object { + "duration": 10, + "fiberActualDurations": Array [ + Array [ + 2, + 10, + ], + Array [ + 1, + 10, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 45, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Array [], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 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, + ], + Array [ + 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, + ], + Array [ + 1, + 1, + 0, + 2, + 2, + 6, + 4, + 4, + 2, + 11000, + 3, + 2, + 2, + 3, + 5, + 4, + 1, + 11000, + ], + Array [ + 1, + 1, + 0, + 2, + 1, + 3, + ], + ], + "rootID": 1, + "snapshots": Array [], + }, + ], + "version": 4, +} +`; + +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`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "duration": 11, + "fiberActualDurations": Array [ + Array [ + 1, + 11, + ], + Array [ + 2, + 11, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 11, + }, + Object { + "duration": 11, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 5, + 1, + ], + Array [ + 2, + 11, + ], + Array [ + 1, + 11, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 5, + 1, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 22, + }, + Object { + "duration": 13, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 5, + 1, + ], + Array [ + 6, + 2, + ], + Array [ + 2, + 13, + ], + Array [ + 1, + 13, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 5, + 1, + ], + Array [ + 6, + 2, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 35, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Array [], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 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, + ], + Array [ + 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, + ], + Array [ + 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": Array [], + }, + ], + "version": 4, +} +`; + +exports[`ProfilingCache should collect data for each root (including ones added or mounted after profiling started): Data for root Parent 1`] = ` +Object { + "commitData": Array [ + Object { + "duration": 13, + "fiberActualDurations": Map { + 3 => 0, + 4 => 1, + 10 => 2, + 2 => 13, + 1 => 13, + }, + "fiberSelfDurations": Map { + 3 => 0, + 4 => 1, + 10 => 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": 34, + }, + Object { + "duration": 10, + "fiberActualDurations": Map { + 2 => 10, + 1 => 10, + }, + "fiberSelfDurations": Map { + 2 => 10, + 1 => 0, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 44, + }, + ], + "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, + 10, + 5, + 2, + 2, + 1, + 2, + 4, + 10, + 2000, + 4, + 2, + 14000, + 3, + 2, + 4, + 3, + 4, + 10, + 5, + 4, + 1, + 14000, + ], + Uint32Array [ + 1, + 1, + 0, + 2, + 2, + 10, + 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 (including ones added or mounted after profiling started): Data for root Parent 2`] = ` +Object { + "commitData": Array [ + Object { + "duration": 11, + "fiberActualDurations": Map { + 11 => 11, + 12 => 11, + 13 => 0, + 14 => 1, + }, + "fiberSelfDurations": Map { + 11 => 0, + 12 => 10, + 13 => 0, + 14 => 1, + }, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 24, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Map {}, + "interactionCommits": Map {}, + "interactions": Map {}, + "operations": Array [ + Uint32Array [ + 1, + 11, + 15, + 6, + 80, + 97, + 114, + 101, + 110, + 116, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 48, + 1, + 11, + 11, + 1, + 1, + 4, + 11, + 11000, + 1, + 12, + 5, + 11, + 0, + 1, + 0, + 4, + 12, + 11000, + 1, + 13, + 5, + 12, + 12, + 2, + 3, + 4, + 13, + 0, + 1, + 14, + 8, + 12, + 12, + 2, + 0, + 4, + 14, + 1000, + ], + ], + "rootID": 11, + "snapshots": Map {}, +} +`; + +exports[`ProfilingCache should collect data for each root (including ones added or mounted after profiling started): Data for root Parent 3`] = ` +Object { + "commitData": Array [ + Object { + "duration": 0, + "fiberActualDurations": Map {}, + "fiberSelfDurations": Map {}, + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 34, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Map { + 6 => 11, + 7 => 11, + 8 => 0, + 9 => 1, + }, + "interactionCommits": Map {}, + "interactions": Map {}, + "operations": Array [ + Uint32Array [ + 1, + 6, + 0, + 2, + 4, + 9, + 8, + 7, + 6, + ], + ], + "rootID": 6, + "snapshots": Map { + 6 => Object { + "children": Array [ + 7, + ], + "displayName": null, + "id": 6, + "key": null, + "type": 11, + }, + 7 => Object { + "children": Array [ + 8, + 9, + ], + "displayName": "Parent", + "id": 7, + "key": null, + "type": 5, + }, + 8 => Object { + "children": Array [], + "displayName": "Child", + "id": 8, + "key": "0", + "type": 5, + }, + 9 => Object { + "children": Array [], + "displayName": "Child", + "id": 9, + "key": null, + "type": 8, + }, + }, +} +`; + +exports[`ProfilingCache should collect data for each root (including ones added or mounted after profiling started): imported data 1`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "duration": 13, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 10, + 2, + ], + Array [ + 2, + 13, + ], + Array [ + 1, + 13, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 10, + 2, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 13, + }, + Object { + "duration": 10, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 10, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 34, + }, + Object { + "duration": 10, + "fiberActualDurations": Array [ + Array [ + 2, + 10, + ], + Array [ + 1, + 10, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 44, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Array [ + Array [ + 1, + 12, + ], + Array [ + 2, + 12, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + Array [ + 5, + 1, + ], + ], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 1, + 1, + 8, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 50, + 1, + 10, + 5, + 2, + 2, + 1, + 2, + 4, + 10, + 2000, + 4, + 2, + 14000, + 3, + 2, + 4, + 3, + 4, + 10, + 5, + 4, + 1, + 14000, + ], + Array [ + 1, + 1, + 0, + 2, + 2, + 10, + 4, + 4, + 2, + 11000, + 3, + 2, + 2, + 3, + 5, + 4, + 1, + 11000, + ], + Array [ + 1, + 1, + 0, + 2, + 1, + 3, + ], + ], + "rootID": 1, + "snapshots": Array [ + Array [ + 1, + Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "type": 11, + }, + ], + Array [ + 2, + Object { + "children": Array [ + 3, + 4, + 5, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "type": 5, + }, + ], + Array [ + 3, + Object { + "children": Array [], + "displayName": "Child", + "id": 3, + "key": "0", + "type": 5, + }, + ], + Array [ + 4, + Object { + "children": Array [], + "displayName": "Child", + "id": 4, + "key": "1", + "type": 5, + }, + ], + Array [ + 5, + Object { + "children": Array [], + "displayName": "Child", + "id": 5, + "key": null, + "type": 8, + }, + ], + ], + }, + Object { + "commitData": Array [ + Object { + "duration": 11, + "fiberActualDurations": Array [ + Array [ + 11, + 11, + ], + Array [ + 12, + 11, + ], + Array [ + 13, + 0, + ], + Array [ + 14, + 1, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 11, + 0, + ], + Array [ + 12, + 10, + ], + Array [ + 13, + 0, + ], + Array [ + 14, + 1, + ], + ], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 24, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Array [], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 1, + 11, + 15, + 6, + 80, + 97, + 114, + 101, + 110, + 116, + 5, + 67, + 104, + 105, + 108, + 100, + 1, + 48, + 1, + 11, + 11, + 1, + 1, + 4, + 11, + 11000, + 1, + 12, + 5, + 11, + 0, + 1, + 0, + 4, + 12, + 11000, + 1, + 13, + 5, + 12, + 12, + 2, + 3, + 4, + 13, + 0, + 1, + 14, + 8, + 12, + 12, + 2, + 0, + 4, + 14, + 1000, + ], + ], + "rootID": 11, + "snapshots": Array [], + }, + Object { + "commitData": Array [ + Object { + "duration": 0, + "fiberActualDurations": Array [], + "fiberSelfDurations": Array [], + "interactionIDs": Array [], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 34, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Array [ + Array [ + 6, + 11, + ], + Array [ + 7, + 11, + ], + Array [ + 8, + 0, + ], + Array [ + 9, + 1, + ], + ], + "interactionCommits": Array [], + "interactions": Array [], + "operations": Array [ + Array [ + 1, + 6, + 0, + 2, + 4, + 9, + 8, + 7, + 6, + ], + ], + "rootID": 6, + "snapshots": Array [ + Array [ + 6, + Object { + "children": Array [ + 7, + ], + "displayName": null, + "id": 6, + "key": null, + "type": 11, + }, + ], + Array [ + 7, + Object { + "children": Array [ + 8, + 9, + ], + "displayName": "Parent", + "id": 7, + "key": null, + "type": 5, + }, + ], + Array [ + 8, + Object { + "children": Array [], + "displayName": "Child", + "id": 8, + "key": "0", + "type": 5, + }, + ], + Array [ + 9, + Object { + "children": Array [], + "displayName": "Child", + "id": 9, + "key": null, + "type": 8, + }, + ], + ], + }, + ], + "version": 4, +} +`; + +exports[`ProfilingCache should report every traced interaction: Interactions 1`] = ` +Array [ + Object { + "__count": 1, + "id": 0, + "name": "mount: one child", + "timestamp": 0, + }, + Object { + "__count": 0, + "id": 1, + "name": "update: two children", + "timestamp": 11, + }, +] +`; + +exports[`ProfilingCache should report every traced interaction: imported data 1`] = ` +Object { + "dataForRoots": Array [ + Object { + "commitData": Array [ + Object { + "duration": 11, + "fiberActualDurations": Array [ + Array [ + 1, + 11, + ], + Array [ + 2, + 11, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 10, + ], + Array [ + 3, + 0, + ], + Array [ + 4, + 1, + ], + ], + "interactionIDs": Array [ + 0, + ], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 11, + }, + Object { + "duration": 11, + "fiberActualDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 5, + 1, + ], + Array [ + 2, + 11, + ], + Array [ + 1, + 11, + ], + ], + "fiberSelfDurations": Array [ + Array [ + 3, + 0, + ], + Array [ + 5, + 1, + ], + Array [ + 2, + 10, + ], + Array [ + 1, + 0, + ], + ], + "interactionIDs": Array [ + 1, + ], + "priorityLevel": "Immediate", + "screenshot": null, + "timestamp": 22, + }, + ], + "displayName": "Parent", + "initialTreeBaseDurations": Array [], + "interactionCommits": Array [ + Array [ + 0, + Array [ + 0, + ], + ], + Array [ + 1, + Array [ + 1, + ], + ], + ], + "interactions": Array [ + Array [ + 0, + Object { + "__count": 1, + "id": 0, + "name": "mount: one child", + "timestamp": 0, + }, + ], + Array [ + 1, + Object { + "__count": 0, + "id": 1, + "name": "update: two children", + "timestamp": 11, + }, + ], + ], + "operations": Array [ + Array [ + 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, + ], + Array [ + 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": Array [], + }, + ], + "version": 4, +} +`; 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..08f64b344b --- /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.profilerStore.startProfiling()); + + utils.act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + + utils.act(() => store.profilerStore.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..c93d347ea8 --- /dev/null +++ b/src/__tests__/profilingCache-test.js @@ -0,0 +1,455 @@ +// @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 (including ones added or mounted after profiling started)', () => { + 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 containerA = document.createElement('div'); + const containerB = document.createElement('div'); + const containerC = document.createElement('div'); + + utils.act(() => ReactDOM.render(, containerA)); + utils.act(() => ReactDOM.render(, containerB)); + utils.act(() => store.profilerStore.startProfiling()); + utils.act(() => ReactDOM.render(, containerA)); + utils.act(() => ReactDOM.render(, containerC)); + utils.act(() => ReactDOM.render(, containerA)); + utils.act(() => ReactDOM.unmountComponentAtNode(containerB)); + utils.act(() => ReactDOM.render(, containerA)); + utils.act(() => store.profilerStore.stopProfiling()); + + let allProfilingDataForRoots = []; + + function Validator({ previousProfilingDataForRoot, rootID }) { + const profilingDataForRoot = store.profilerStore.getDataForRoot(rootID); + if (previousProfilingDataForRoot != null) { + expect(profilingDataForRoot).toEqual(previousProfilingDataForRoot); + } else { + expect(profilingDataForRoot).toMatchSnapshot( + `Data for root ${profilingDataForRoot.displayName}` + ); + } + allProfilingDataForRoots.push(profilingDataForRoot); + return null; + } + + const dataForRoots = + store.profilerStore.profilingData !== null + ? store.profilerStore.profilingData.dataForRoots + : null; + + expect(dataForRoots).not.toBeNull(); + + if (dataForRoots !== null) { + dataForRoots.forEach(dataForRoot => { + utils.act(() => + TestRenderer.create( + + ) + ); + }); + } + + expect(allProfilingDataForRoots).toHaveLength(3); + + utils.exportImportHelper(bridge, store); + + allProfilingDataForRoots.forEach(profilingDataForRoot => { + utils.act(() => + TestRenderer.create( + + ) + ); + }); + }); + + it('should collect data for each commit', () => { + 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.profilerStore.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.profilerStore.stopProfiling()); + + const allCommitData = []; + + function Validator({ commitIndex, previousCommitDetails, rootID }) { + const commitData = store.profilerStore.getCommitData(rootID, commitIndex); + if (previousCommitDetails != null) { + expect(commitData).toEqual(previousCommitDetails); + } else { + allCommitData.push(commitData); + expect(commitData).toMatchSnapshot( + `CommitDetails commitIndex: ${commitIndex}` + ); + } + return null; + } + + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 4; commitIndex++) { + utils.act(() => { + TestRenderer.create( + + ); + }); + } + + expect(allCommitData).toHaveLength(4); + + utils.exportImportHelper(bridge, store); + + for (let commitIndex = 0; commitIndex < 4; commitIndex++) { + utils.act(() => { + TestRenderer.create( + + ); + }); + } + }); + + it('should calculate a self duration based on actual children (not filtered children)', () => { + 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.profilerStore.startProfiling()); + utils.act(() => + ReactDOM.render(, document.createElement('div')) + ); + utils.act(() => store.profilerStore.stopProfiling()); + + let commitData = null; + + function Validator({ commitIndex, rootID }) { + commitData = store.profilerStore.getCommitData(rootID, commitIndex); + expect(commitData).toMatchSnapshot( + `CommitDetails with filtered self durations` + ); + return null; + } + + const rootID = store.roots[0]; + + utils.act(() => { + TestRenderer.create(); + }); + + expect(commitData).not.toBeNull(); + }); + + 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.profilerStore.startProfiling()); + await utils.actAsync(() => + ReactDOM.render(, document.createElement('div')) + ); + utils.act(() => store.profilerStore.stopProfiling()); + + const allCommitData = []; + + function Validator({ 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++) { + utils.act(() => { + TestRenderer.create( + + ); + }); + } + + expect(allCommitData).toHaveLength(2); + + done(); + }); + + it('should collect data for each rendered fiber', () => { + 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.profilerStore.startProfiling()); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => ReactDOM.render(, container)); + utils.act(() => store.profilerStore.stopProfiling()); + + const allFiberCommits = []; + + function Validator({ fiberID, previousFiberCommits, rootID }) { + const fiberCommits = store.profilerStore.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++) { + utils.act(() => { + 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); + + for (let index = 0; index < store.numElements; index++) { + utils.act(() => { + const fiberID = store.getElementIDAtIndex(index); + if (fiberID == null) { + throw Error(`Unexpected null ID for element at index ${index}`); + } + TestRenderer.create( + + ); + }); + } + }); + + it('should report every traced interaction', () => { + 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.profilerStore.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.profilerStore.stopProfiling()); + + let interactions = null; + + function Validator({ previousInteractions, rootID }) { + interactions = store.profilerStore.profilingCache.getInteractionsChartData( + { + rootID, + } + ).interactions; + if (previousInteractions != null) { + expect(interactions).toEqual(previousInteractions); + } else { + expect(interactions).toMatchSnapshot('Interactions'); + } + return null; + } + + const rootID = store.roots[0]; + + utils.act(() => + TestRenderer.create( + + ) + ); + + expect(interactions).not.toBeNull(); + + utils.exportImportHelper(bridge, store); + + utils.act(() => + TestRenderer.create( + + ) + ); + }); +}); diff --git a/src/__tests__/profilingCharts-test.js b/src/__tests__/profilingCharts-test.js index 62e2c40b30..d83f8b5a0f 100644 --- a/src/__tests__/profilingCharts-test.js +++ b/src/__tests__/profilingCharts-test.js @@ -27,7 +27,7 @@ describe('profiling charts', () => { }); describe('flamegraph chart', () => { - it('should contain valid data', async done => { + it('should contain valid data', () => { const Parent = ({ count }) => { Scheduler.advanceTime(10); return ( @@ -47,7 +47,7 @@ describe('profiling charts', () => { const container = document.createElement('div'); - utils.act(() => store.startProfiling()); + utils.act(() => store.profilerStore.startProfiling()); utils.act(() => SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => ReactDOM.render(, container) @@ -60,68 +60,50 @@ describe('profiling charts', () => { () => ReactDOM.render(, container) ) ); - utils.act(() => store.stopProfiling()); + utils.act(() => store.profilerStore.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, + function Validator({ commitIndex, rootID }) { + const commitTree = store.profilerStore.profilingCache.getCommitTree({ + commitIndex, rootID, }); - const commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - suspenseResolved = true; - const commitTree = store.profilingCache.getCommitTree({ - commitIndex, - profilingSummary, - }); - const chartData = store.profilingCache.getFlamegraphChartData({ - commitDetails, - commitIndex, - commitTree, - }); + const chartData = store.profilerStore.profilingCache.getFlamegraphChartData( + { + 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 - ); + utils.act(() => { + TestRenderer.create( + + ); + }); - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); } - expect(suspenseResolved).toBe(true); - - done(); + expect(renderFinished).toBe(true); }); }); describe('ranked chart', () => { - it('should contain valid data', async done => { + it('should contain valid data', () => { const Parent = ({ count }) => { Scheduler.advanceTime(10); return ( @@ -141,7 +123,7 @@ describe('profiling charts', () => { const container = document.createElement('div'); - utils.act(() => store.startProfiling()); + utils.act(() => store.profilerStore.startProfiling()); utils.act(() => SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => ReactDOM.render(, container) @@ -154,64 +136,46 @@ describe('profiling charts', () => { () => ReactDOM.render(, container) ) ); - utils.act(() => store.stopProfiling()); + utils.act(() => store.profilerStore.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, + function Validator({ commitIndex, rootID }) { + const commitTree = store.profilerStore.profilingCache.getCommitTree({ + commitIndex, rootID, }); - const commitDetails = store.profilingCache.CommitDetails.read({ - commitIndex, - rendererID, - rootID, - }); - suspenseResolved = true; - const commitTree = store.profilingCache.getCommitTree({ - commitIndex, - profilingSummary, - }); - const chartData = store.profilingCache.getRankedChartData({ - commitDetails, - commitIndex, - commitTree, - }); + const chartData = store.profilerStore.profilingCache.getRankedChartData( + { + 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 - ); + utils.act(() => { + TestRenderer.create( + + ); + }); - expect(suspenseResolved).toBe(true); + expect(renderFinished).toBe(true); } - - done(); }); }); describe('interactions', () => { - it('should contain valid data', async done => { + it('should contain valid data', () => { const Parent = ({ count }) => { Scheduler.advanceTime(10); return ( @@ -231,7 +195,7 @@ describe('profiling charts', () => { const container = document.createElement('div'); - utils.act(() => store.startProfiling()); + utils.act(() => store.profilerStore.startProfiling()); utils.act(() => SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => ReactDOM.render(, container) @@ -244,52 +208,34 @@ describe('profiling charts', () => { () => ReactDOM.render(, container) ) ); - utils.act(() => store.stopProfiling()); + utils.act(() => store.profilerStore.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; - const chartData = store.profilingCache.getInteractionsChartData({ - interactions, - profilingSummary, - }); + function Validator({ commitIndex, rootID }) { + const chartData = store.profilerStore.profilingCache.getInteractionsChartData( + { + 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 - ); + utils.act(() => { + TestRenderer.create( + + ); + }); - 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..e6c98c1d5e 100644 --- a/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/src/__tests__/profilingCommitTreeBuilder-test.js @@ -24,7 +24,7 @@ describe('commit tree', () => { TestRenderer = utils.requireTestRenderer(); }); - it('should be able to rebuild the store tree for each commit', async done => { + it('should be able to rebuild the store tree for each commit', () => { const Parent = ({ count }) => { Scheduler.advanceTime(10); return new Array(count) @@ -38,52 +38,37 @@ describe('commit tree', () => { const container = document.createElement('div'); - utils.act(() => store.startProfiling()); + utils.act(() => store.profilerStore.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()); + utils.act(() => store.profilerStore.stopProfiling()); - let suspenseResolved = false; + let renderFinished = false; - function Suspender({ commitIndex, rendererID, rootID }) { - const profilingSummary = store.profilingCache.ProfilingSummary.read({ - rendererID, + function Validator({ commitIndex, rootID }) { + const commitTree = store.profilerStore.profilingCache.getCommitTree({ + commitIndex, rootID, }); - suspenseResolved = true; - const commitTree = store.profilingCache.getCommitTree({ - commitIndex, - profilingSummary, - }); 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 - ); + utils.act(() => { + TestRenderer.create( + + ); + }); - 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__/storeComponentFilters-test.js b/src/__tests__/storeComponentFilters-test.js index 3030646bf6..dff169e924 100644 --- a/src/__tests__/storeComponentFilters-test.js +++ b/src/__tests__/storeComponentFilters-test.js @@ -1,11 +1,13 @@ // @flow +import type Store from 'src/devtools/store'; + describe('Store component filters', () => { let React; let ReactDOM; let TestUtils; let Types; - let store; + let store: Store; let utils; const act = (callback: Function) => { @@ -28,7 +30,7 @@ describe('Store component filters', () => { }); it('should throw if filters are updated while profiling', () => { - act(() => store.startProfiling()); + act(() => store.profilerStore.startProfiling()); expect(() => (store.componentFilters = [])).toThrow( 'Cannot modify filter preferences while profiling' ); diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index 20d7bb511c..d30b9a7d3c 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'); @@ -133,57 +133,43 @@ export function requireTestRenderer(): ReactTestRenderer { } } -export function exportImportHelper( - bridge: Bridge, - store: Store, - rendererID: number, - rootID: number -): void { +export function exportImportHelper(bridge: Bridge, store: Store): 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 importedProfilingData = 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( - importedProfilingData.profilingSnapshots.get(rootID) - ); - expect(store.profilingOperations.get(rootID)).toEqual( - importedProfilingData.profilingOperations.get(rootID) - ); + expect(profilingDataFrontendInitial).toEqual(profilingDataFrontend); // Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer. - expect(importedProfilingData).toMatchSnapshot('imported data'); + expect(parsedProfilingDataExport).toMatchSnapshot('imported data'); act(() => { - store.importedProfilingData = importedProfilingData; + // 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..3097a0749e 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 = 4; diff --git a/src/devtools/ProfilerStore.js b/src/devtools/ProfilerStore.js new file mode 100644 index 0000000000..c6d714cb05 --- /dev/null +++ b/src/devtools/ProfilerStore.js @@ -0,0 +1,346 @@ +// @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}"`); + } + + // Profiling data has been recorded for at least one root. + get hasProfilingData(): boolean { + return ( + this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0 + ); + } + + get isProcessingData(): boolean { + return this._rendererQueue.size > 0 || this._dataBackends.length > 0; + } + + get isProfiling(): boolean { + return this._isProfiling; + } + + get profilingCache(): ProfilingCache { + return this._cache; + } + + 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(); + + this.emit('profilingData'); + } + + clear(): void { + this._dataBackends.splice(0); + this._dataFrontend = null; + this._initialSnapshotsByRootID.clear(); + this._inProgressOperationsByRootID.clear(); + this._inProgressScreenshotsByRootID.clear(); + this._rendererQueue.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(); + + this.emit('profilingData'); + } + + 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); + + 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(); + + 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 }); + } + } + + 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 c2f370f635..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,416 +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 importedProfilingData = this._store.importedProfilingData; - if (importedProfilingData !== null) { - const commitDetailsByCommitIndex = - importedProfilingData.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 importedProfilingData = this._store.importedProfilingData; - if (importedProfilingData !== null) { - const { commitDetails } = importedProfilingData; - 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 importedProfilingData = this._store.importedProfilingData; - if (importedProfilingData !== null) { - const interactionsFrontend: InteractionsFrontend = - importedProfilingData.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 importedProfilingData = this._store.importedProfilingData; - if (importedProfilingData !== null) { - const profilingSummaryFrontend: ProfilingSummaryFrontend = - importedProfilingData.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 1155b6fe18..41b4534cc0 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, @@ -17,14 +15,10 @@ import { utfDecodeString, } from '../utils'; 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, -} from './views/Profiler/types'; import type { Bridge, ComponentFilter, ElementType } from '../types'; const debug = (methodName, ...args) => { @@ -43,12 +37,9 @@ 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, - supportsFileDownloads?: boolean, supportsReloadAndProfile?: boolean, supportsProfiling?: boolean, |}; @@ -80,37 +71,11 @@ export default class Store extends EventEmitter { // The InspectedElementContext also relies on this mutability for its WeakMap usage. _idToElement: Map = new Map(); - // The user has imported a previously exported profiling session. - _importedProfilingData: ImportedProfilingData | null = null; - - // 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; - - // 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. @@ -128,7 +93,6 @@ export default class Store extends EventEmitter { // These options may be initially set by a confiugraiton option when constructing the Store. // In the case of "supportsProfiling", the option may be updated based on the injected renderers. _supportsCaptureScreenshots: boolean = false; - _supportsFileDownloads: boolean = false; _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; @@ -150,26 +114,21 @@ 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 = localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) === 'true'; } - if (supportsFileDownloads) { - this._supportsFileDownloads = true; - } if (supportsProfiling) { this._supportsProfiling = true; } @@ -180,15 +139,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 +150,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 +209,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,55 +231,22 @@ export default class Store extends EventEmitter { return this._hasOwnerMetadata; } - // Profiling data has been recorded for at least one root. - get hasProfilingData(): boolean { - return ( - this._importedProfilingData !== null || - this._profilingOperationsByRootID.size > 0 - ); - } - - get importedProfilingData(): ImportedProfilingData | null { - return this._importedProfilingData; - } - set importedProfilingData(value: ImportedProfilingData | null): void { - this._importedProfilingData = value; - this._profilingOperationsByRootID = new Map(); - this._profilingScreenshotsByRootID = new Map(); - this._profilingSnapshotsByRootID = new Map(); - this._profilingCache.invalidate(); - - this.emit('importedProfilingData'); - } - - get isProfiling(): boolean { - return this._isProfiling; - } - get numElements(): number { return this._weightAcrossRoots; } - get profilingCache(): ProfilingCache { - return this._profilingCache; - } - - get profilingOperations(): Map> { - return this._profilingOperationsByRootID; - } - - get profilingScreenshots(): Map> { - return this._profilingScreenshotsByRootID; - } - - get profilingSnapshots(): Map> { - return this._profilingSnapshotsByRootID; + get profilerStore(): ProfilerStore { + return this._profilerStore; } get revision(): number { return this._revision; } + get rootIDToRendererID(): Map { + return this._rootIDToRendererID; + } + get roots(): $ReadOnlyArray { return this._roots; } @@ -352,10 +255,6 @@ export default class Store extends EventEmitter { return this._supportsCaptureScreenshots; } - get supportsFileDownloads(): boolean { - return this._supportsFileDownloads; - } - get supportsProfiling(): boolean { return this._supportsProfiling; } @@ -364,19 +263,6 @@ export default class Store extends EventEmitter { return this._supportsReloadAndProfile; } - clearProfilingData(): void { - this._importedProfilingData = 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'); - } - containsElement(id: number): boolean { return this._idToElement.get(id) != null; } @@ -602,24 +488,6 @@ export default class Store extends EventEmitter { return false; } - 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. - } - // TODO Maybe split this into two methods: expand() and collapse() toggleIsCollapsed(id: number, isCollapsed: boolean): void { let didMutate = false; @@ -702,34 +570,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 @@ -770,23 +610,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: @@ -858,10 +683,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); @@ -957,10 +778,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__) { @@ -1066,60 +883,12 @@ export default class Store extends EventEmitter { this.emit('mutated', [addedElementIDs, removedElementIDs]); }; - onProfilingStatus = (isProfiling: boolean) => { - if (isProfiling) { - this._importedProfilingData = 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/Components/ComponentFiltersModal.js b/src/devtools/views/Components/ComponentFiltersModal.js index 45e00b7ff4..7797dcbf24 100644 --- a/src/devtools/views/Components/ComponentFiltersModal.js +++ b/src/devtools/views/Components/ComponentFiltersModal.js @@ -41,6 +41,7 @@ import type { export default function ComponentFiltersModalWrapper(_: {||}) { const store = useContext(StoreContext); + const { profilerStore } = store; const { isModalShowing, setIsModalShowing } = useContext( ComponentFiltersModalContext @@ -50,13 +51,13 @@ export default function ComponentFiltersModalWrapper(_: {||}) { // If necessary, we could support this- but it doesn't seem like a necessary use case. const isProfilingSubscription = useMemo( () => ({ - getCurrentValue: () => store.isProfiling, + getCurrentValue: () => profilerStore.isProfiling, subscribe: (callback: Function) => { - store.addListener('isProfiling', callback); - return () => store.removeListener('isProfiling', callback); + profilerStore.addListener('isProfiling', callback); + return () => profilerStore.removeListener('isProfiling', callback); }, }), - [store] + [profilerStore] ); const isProfiling = useSubscription(isProfilingSubscription); if (isProfiling && isModalShowing) { diff --git a/src/devtools/views/Components/ToggleComponentFiltersModalButton.js b/src/devtools/views/Components/ToggleComponentFiltersModalButton.js index 189454d349..e1d237f33a 100644 --- a/src/devtools/views/Components/ToggleComponentFiltersModalButton.js +++ b/src/devtools/views/Components/ToggleComponentFiltersModalButton.js @@ -14,6 +14,7 @@ import type { ComponentFilter } from 'src/types'; export default function ToggleComponentFiltersModalButton() { const store = useContext(StoreContext); + const { profilerStore } = store; const { isModalShowing, setIsModalShowing } = useContext( ComponentFiltersModalContext @@ -23,13 +24,13 @@ export default function ToggleComponentFiltersModalButton() { // If necessary, we could support this- but it doesn't seem like a necessary use case. const isProfilingSubscription = useMemo( () => ({ - getCurrentValue: () => store.isProfiling, + getCurrentValue: () => profilerStore.isProfiling, subscribe: (callback: Function) => { - store.addListener('isProfiling', callback); - return () => store.removeListener('isProfiling', callback); + profilerStore.addListener('isProfiling', callback); + return () => profilerStore.removeListener('isProfiling', callback); }, }), - [store] + [profilerStore] ); const isProfiling = useSubscription(isProfilingSubscription); diff --git a/src/devtools/views/Profiler/ClearProfilingDataButton.js b/src/devtools/views/Profiler/ClearProfilingDataButton.js index a8be6b0f6c..c99f370fce 100644 --- a/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -8,13 +8,14 @@ import { StoreContext } from '../context'; export default function ClearProfilingDataButton() { const store = useContext(StoreContext); - const { isProfiling } = useContext(ProfilerContext); + const { hasProfilingData, isProfiling } = useContext(ProfilerContext); + const { profilerStore } = store; - const clear = useCallback(() => store.clearProfilingData(), [store]); + const clear = useCallback(() => profilerStore.clear(), [profilerStore]); return ( - {store.supportsFileDownloads && ( - - )} + ); } diff --git a/src/devtools/views/Profiler/RankedChartBuilder.js b/src/devtools/views/Profiler/RankedChartBuilder.js index 9b53af9dbb..67b38fccbf 100644 --- a/src/devtools/views/Profiler/RankedChartBuilder.js +++ b/src/devtools/views/Profiler/RankedChartBuilder.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 = {| id: number, @@ -20,15 +21,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}`; @@ -39,7 +44,7 @@ export function getChartData({ let maxSelfDuration = 0; const chartNodes: Array = []; - actualDurations.forEach((actualDuration, id) => { + fiberActualDurations.forEach((actualDuration, id) => { const node = nodes.get(id); if (node == null) { @@ -52,7 +57,7 @@ export function getChartData({ if (parentID === 0) { return; } - const selfDuration = selfDurations.get(id) || 0; + const selfDuration = fiberSelfDurations.get(id) || 0; maxSelfDuration = Math.max(maxSelfDuration, selfDuration); const name = displayName || 'Anonymous'; diff --git a/src/devtools/views/Profiler/RootSelector.css b/src/devtools/views/Profiler/RootSelector.css new file mode 100644 index 0000000000..ff1a970d46 --- /dev/null +++ b/src/devtools/views/Profiler/RootSelector.css @@ -0,0 +1,3 @@ +.Spacer { + flex: 1; +} diff --git a/src/devtools/views/Profiler/RootSelector.js b/src/devtools/views/Profiler/RootSelector.js new file mode 100644 index 0000000000..b4e89af824 --- /dev/null +++ b/src/devtools/views/Profiler/RootSelector.js @@ -0,0 +1,42 @@ +// @flow + +import React, { Fragment, useCallback, useContext } from 'react'; +import { ProfilerContext } from './ProfilerContext'; + +import styles from './RootSelector.css'; + +export default function RootSelector(_: {||}) { + const { profilingData, rootID, setRootID } = useContext(ProfilerContext); + + const options = []; + if (profilingData !== null) { + profilingData.dataForRoots.forEach((dataForRoot, rootID) => { + options.push( + + ); + }); + } + + const handleChange = useCallback( + ({ currentTarget }) => { + setRootID(parseInt(currentTarget.value, 10)); + }, + [setRootID] + ); + + if (profilingData === null || profilingData.dataForRoots.size <= 1) { + // Don't take up visual space if there's only one root. + return null; + } + + return ( + +
+ + + ); +} diff --git a/src/devtools/views/Profiler/SidebarCommitInfo.js b/src/devtools/views/Profiler/SidebarCommitInfo.js index ff4040302b..f4b7ea9a24 100644 --- a/src/devtools/views/Profiler/SidebarCommitInfo.js +++ b/src/devtools/views/Profiler/SidebarCommitInfo.js @@ -12,24 +12,12 @@ export type Props = {||}; export default function SidebarCommitInfo(_: Props) { const { selectedCommitIndex, - rendererID, rootID, selectInteraction, selectTab, } = useContext(ProfilerContext); - const { - captureScreenshots, - profilingCache, - profilingScreenshots, - } = useContext(StoreContext); - - const screenshotsByCommitIndex = - rootID !== null ? profilingScreenshots.get(rootID) : null; - const screenshot = - screenshotsByCommitIndex != null && selectedCommitIndex !== null - ? screenshotsByCommitIndex.get(selectedCommitIndex) - : null; + const { captureScreenshots, profilerStore } = useContext(StoreContext); const [ isScreenshotModalVisible, @@ -45,26 +33,22 @@ export default function SidebarCommitInfo(_: Props) { [] ); - if (selectedCommitIndex === null) { + if (rootID === null || selectedCommitIndex === null) { return
Nothing selected
; } - const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read( - { - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - } - ); + const { interactions } = profilerStore.getDataForRoot(rootID); + const { + duration, + interactionIDs, + priorityLevel, + screenshot, + timestamp, + } = profilerStore.getCommitData(rootID, selectedCommitIndex); - const { interactions, priorityLevel } = profilingCache.CommitDetails.read({ - commitIndex: selectedCommitIndex, - rendererID: ((rendererID: any): number), - rootID: ((rootID: any): number), - }); - - const viewInteraction = interaction => { + const viewInteraction = interactionID => { selectTab('interactions'); - selectInteraction(interaction.id); + selectInteraction(interactionID); }; return ( @@ -80,34 +64,33 @@ export default function SidebarCommitInfo(_: Props) { )}
  • :{' '} - - {formatTime(commitTimes[((selectedCommitIndex: any): number)])}s - + {formatTime(timestamp)}s
  • :{' '} - - {formatDuration( - commitDurations[((selectedCommitIndex: any): number)] - )} - ms - + {formatDuration(duration)}ms
  • :
    - {interactions.length === 0 ? ( + {interactionIDs.length === 0 ? (
    None
    ) : null} - {interactions.map((interaction, index) => ( - - ))} + {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..c23a2106fc 100644 --- a/src/devtools/views/Profiler/SidebarInteractions.js +++ b/src/devtools/views/Profiler/SidebarInteractions.js @@ -13,48 +13,70 @@ export type Props = {||}; export default function SidebarInteractions(_: Props) { const { selectedInteractionID, - rendererID, rootID, selectCommitIndex, selectTab, } = useContext(ProfilerContext); - const { profilingCache } = useContext(StoreContext); + const { profilerStore } = useContext(StoreContext); + const { profilingCache } = profilerStore; 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 +84,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..c83b0bc00e 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 { profilerStore } = useContext(StoreContext); const { - rendererID, rootID, selectCommitIndex, selectedCommitIndex, @@ -22,23 +21,21 @@ export default function SidebarSelectedFiberInfo(_: Props) { selectedFiberName, selectFiber, } = useContext(ProfilerContext); + const { profilingCache } = profilerStore; - 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..9af30779be 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: 4, + dataForRoots: Array, |}; diff --git a/src/devtools/views/Profiler/utils.js b/src/devtools/views/Profiler/utils.js index 95c4fdc17d..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 importedProfilingData: 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 importedProfilingData; -}; +} export const getGradientColor = (value: number) => { const maxIndex = commitGradient.length - 1; diff --git a/src/devtools/views/root.css b/src/devtools/views/root.css index cc6c628106..9969752f87 100644 --- a/src/devtools/views/root.css +++ b/src/devtools/views/root.css @@ -22,7 +22,7 @@ --light-color-commit-did-not-render-fill: #cfd1d5; --light-color-commit-did-not-render-fill-text: #000000; --light-color-commit-did-not-render-pattern: #cfd1d5; - --light-color-commit-did-not-render-pattern-text: #000000; + --light-color-commit-did-not-render-pattern-text: #333333; --light-color-commit-gradient-0: #37afa9; --light-color-commit-gradient-1: #63b19e; --light-color-commit-gradient-2: #80b393; diff --git a/src/devtools/views/utils.js b/src/devtools/views/utils.js index 2db6fb82a7..8cebefa319 100644 --- a/src/devtools/views/utils.js +++ b/src/devtools/views/utils.js @@ -146,3 +146,27 @@ export function serializeHooksForCopy(hooks: HooksTree | null): string { return ''; } } + +// Keeping this in memory seems to be enough to enable the browser to download larger profiles. +// Without this, we would see a "Download failed: network error" failure. +let downloadUrl = null; + +export function downloadFile(filename: string, text: string): void { + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + + if (downloadUrl !== null) { + URL.revokeObjectURL(downloadUrl); + } + + downloadUrl = URL.createObjectURL(blob); + + const element = document.createElement('a'); + element.setAttribute('href', downloadUrl); + element.setAttribute('download', filename); + element.style.display = 'none'; + ((document.body: any): HTMLBodyElement).appendChild(element); + + element.click(); + + ((document.body: any): HTMLBodyElement).removeChild(element); +}