diff --git a/src/__tests__/__snapshots__/profilingCharts-test.js.snap b/src/__tests__/__snapshots__/profilingCharts-test.js.snap new file mode 100644 index 0000000000..36e4c8cf90 --- /dev/null +++ b/src/__tests__/__snapshots__/profilingCharts-test.js.snap @@ -0,0 +1,333 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`profiling charts flamegraph chart should contain valid data: 0: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts flamegraph chart should contain valid data: 0: FlamegraphChartData 1`] = ` +Object { + "baseDuration": 14, + "depth": 2, + "idToDepthMap": Map { + 2 => 1, + 4 => 2, + 3 => 2, + }, + "maxSelfDuration": 10, + "rows": Array [ + Array [ + Object { + "actualDuration": 14, + "didRender": true, + "id": 2, + "label": "Parent (10.0ms of 14.0ms)", + "name": "Parent", + "offset": 0, + "selfDuration": 10, + "treeBaseDuration": 14, + }, + ], + Array [ + Object { + "actualDuration": 2, + "didRender": true, + "id": 4, + "label": "Memo(Child) key=\\"second\\" (2.0ms of 2.0ms)", + "name": "Memo(Child)", + "offset": 12, + "selfDuration": 2, + "treeBaseDuration": 2, + }, + Object { + "actualDuration": 2, + "didRender": true, + "id": 3, + "label": "Memo(Child) key=\\"first\\" (2.0ms of 2.0ms)", + "name": "Memo(Child)", + "offset": 10, + "selfDuration": 2, + "treeBaseDuration": 2, + }, + ], + ], +} +`; + +exports[`profiling charts flamegraph chart should contain valid data: 1: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts flamegraph chart should contain valid data: 1: FlamegraphChartData 1`] = ` +Object { + "baseDuration": 14, + "depth": 2, + "idToDepthMap": Map { + 2 => 1, + 4 => 2, + 3 => 2, + }, + "maxSelfDuration": 10, + "rows": Array [ + Array [ + Object { + "actualDuration": 10, + "didRender": true, + "id": 2, + "label": "Parent (10.0ms of 10.0ms)", + "name": "Parent", + "offset": 0, + "selfDuration": 10, + "treeBaseDuration": 14, + }, + ], + Array [ + Object { + "actualDuration": 0, + "didRender": false, + "id": 4, + "label": "Memo(Child) key=\\"second\\"", + "name": "Memo(Child)", + "offset": 12, + "selfDuration": 0, + "treeBaseDuration": 2, + }, + Object { + "actualDuration": 0, + "didRender": false, + "id": 3, + "label": "Memo(Child) key=\\"first\\"", + "name": "Memo(Child)", + "offset": 10, + "selfDuration": 0, + "treeBaseDuration": 2, + }, + ], + ], +} +`; + +exports[`profiling charts interactions should contain valid data: Interactions 1`] = ` +Object { + "lastInteractionTime": 24, + "maxCommitDuration": 14, +} +`; + +exports[`profiling charts interactions should contain valid data: Interactions 2`] = ` +Object { + "lastInteractionTime": 24, + "maxCommitDuration": 14, +} +`; + +exports[`profiling charts ranked chart should contain valid data: 0: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts ranked chart should contain valid data: 0: RankedChartData 1`] = ` +Object { + "maxValue": 10, + "nodes": Array [ + Object { + "id": 2, + "label": "Parent (10.0ms)", + "name": "Parent", + "value": 10, + }, + Object { + "id": 3, + "label": "Memo(Child) key=\\"first\\" (2.0ms)", + "name": "Memo(Child)", + "value": 2, + }, + Object { + "id": 4, + "label": "Memo(Child) key=\\"second\\" (2.0ms)", + "name": "Memo(Child)", + "value": 2, + }, + ], +} +`; + +exports[`profiling charts ranked chart should contain valid data: 1: CommitTree 1`] = ` +Object { + "nodes": Map { + 1 => Object { + "children": Array [ + 2, + ], + "displayName": null, + "id": 1, + "key": null, + "parentID": 0, + "treeBaseDuration": 14, + }, + 2 => Object { + "children": Array [ + 3, + 4, + ], + "displayName": "Parent", + "id": 2, + "key": null, + "parentID": 1, + "treeBaseDuration": 14, + }, + 3 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 3, + "key": "first", + "parentID": 2, + "treeBaseDuration": 2, + }, + 4 => Object { + "children": Array [], + "displayName": "Memo(Child)", + "id": 4, + "key": "second", + "parentID": 2, + "treeBaseDuration": 2, + }, + }, + "rootID": 1, +} +`; + +exports[`profiling charts ranked chart should contain valid data: 1: RankedChartData 1`] = ` +Object { + "maxValue": 10, + "nodes": Array [ + Object { + "id": 2, + "label": "Parent (10.0ms)", + "name": "Parent", + "value": 10, + }, + ], +} +`; diff --git a/src/__tests__/profiling-test.js b/src/__tests__/profiling-test.js index 6c49272847..39dd595e49 100644 --- a/src/__tests__/profiling-test.js +++ b/src/__tests__/profiling-test.js @@ -1,13 +1,11 @@ // @flow -import type React from 'react'; -import type ReactDOM from 'react-dom'; import typeof ReactTestRenderer from 'react-test-renderer'; import type Store from 'src/devtools/store'; describe('profiling', () => { - let React: React; - let ReactDOM: ReactDOM; + let React; + let ReactDOM; let Scheduler; let SchedulerTracing; let TestRenderer: ReactTestRenderer; diff --git a/src/__tests__/profilingCharts-test.js b/src/__tests__/profilingCharts-test.js new file mode 100644 index 0000000000..3e2613b5af --- /dev/null +++ b/src/__tests__/profilingCharts-test.js @@ -0,0 +1,292 @@ +// @flow + +import typeof TestRendererType from 'react-test-renderer'; +import type Store from 'src/devtools/store'; + +describe('profiling charts', () => { + let React; + let ReactDOM; + let Scheduler; + let SchedulerTracing; + let TestRenderer: TestRendererType; + let store: Store; + let utils; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + SchedulerTracing = require('scheduler/tracing'); + TestRenderer = utils.requireTestRenderer(); + }); + + describe('flamegraph chart', () => { + it('should contain valid data', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + + // Memoize children to verify that chart doesn't include in the update. + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => + ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + const commitDetails = store.profilingCache.CommitDetails.read({ + commitIndex, + rendererID, + rootID, + }); + suspenseResolved = true; + const commitTree = store.profilingCache.getCommitTree({ + commitIndex, + profilingSummary, + }); + const chartData = store.profilingCache.getFlamegraphChartData({ + commitDetails, + commitIndex, + commitTree, + }); + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); + expect(chartData).toMatchSnapshot( + `${commitIndex}: FlamegraphChartData` + ); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + expect(suspenseResolved).toBe(true); + + done(); + }); + }); + + describe('ranked chart', () => { + it('should contain valid data', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + + // Memoize children to verify that chart doesn't include in the update. + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => + ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = false; + + function Suspender({ commitIndex, rendererID, rootID }) { + const profilingSummary = store.profilingCache.ProfilingSummary.read({ + rendererID, + rootID, + }); + const commitDetails = store.profilingCache.CommitDetails.read({ + commitIndex, + rendererID, + rootID, + }); + suspenseResolved = true; + const commitTree = store.profilingCache.getCommitTree({ + commitIndex, + profilingSummary, + }); + const chartData = store.profilingCache.getRankedChartData({ + commitDetails, + commitIndex, + commitTree, + }); + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`); + expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + done(); + }); + }); + + describe('interactions', () => { + it('should contain valid data', async done => { + const Parent = ({ count }) => { + Scheduler.advanceTime(10); + return ( + + + + + ); + }; + + // Memoize children to verify that chart doesn't include in the update. + const Child = React.memo(function Child() { + Scheduler.advanceTime(2); + return null; + }); + + const container = document.createElement('div'); + + utils.act(() => store.startProfiling()); + utils.act(() => + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () => + ReactDOM.render(, container) + ) + ); + utils.act(() => + SchedulerTracing.unstable_trace( + 'update', + Scheduler.unstable_now(), + () => ReactDOM.render(, container) + ) + ); + utils.act(() => store.stopProfiling()); + + let suspenseResolved = 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, + }); + expect(chartData).toMatchSnapshot('Interactions'); + return null; + } + + const rendererID = utils.getRendererID(); + const rootID = store.roots[0]; + + for (let commitIndex = 0; commitIndex < 2; commitIndex++) { + suspenseResolved = false; + + await utils.actSuspense( + () => + TestRenderer.create( + + + + ), + 3 + ); + + expect(suspenseResolved).toBe(true); + } + + done(); + }); + }); +}); diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index 1e3acc55ac..89c7ffd6b1 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -12,7 +12,10 @@ export function act(callback: Function): void { jest.runAllTimers(); } -export async function actSuspense(callback: Function): Promise { +export async function actSuspense( + callback: Function, + numTimesToFlush: number = 1 +): Promise { const TestUtils = require('react-dom/test-utils'); const Scheduler = require('scheduler'); @@ -24,8 +27,16 @@ export async function actSuspense(callback: Function): Promise { jest.runAllTimers(); }); - // Re-render after resolved promises - Scheduler.flushAll(); + // Run cascading microtasks and flush scheduled React work. + // Components that suspend multiple times will need to do this once per suspend operation. + // HACK Ideally the mock scheduler would provide an API to ask if there was outstanding work. + while (--numTimesToFlush >= 0) { + // $FlowFixMe Flow doens't know about "await act()" yet + await TestUtils.act(async () => { + jest.runAllTimers(); + Scheduler.flushAll(); + }); + } } export function beforeEachProfiling(): void {