diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js index 1a369acca27bd..f010261a616e6 100644 --- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -15,72 +15,10 @@ describe('Timeline profiler', () => { let Scheduler; let renderHelper; let renderRootHelper; + let store; let unmountFns; let utils; - let clearedMarks; - let featureDetectionMarkName = null; - let marks; - let setPerformanceMock; - - function createUserTimingPolyfill() { - featureDetectionMarkName = null; - - clearedMarks = []; - marks = []; - - // Remove file-system specific bits or version-specific bits of information from the module range marks. - function filterMarkData(markName) { - if (markName.startsWith('--react-internal-module-start')) { - return `${markName.substr(0, 29)}-`; - } else if (markName.startsWith('--react-internal-module-stop')) { - return `${markName.substr(0, 28)}-`; - } else if (markName.startsWith('--react-version')) { - return `${markName.substr(0, 15)}-`; - } else { - return markName; - } - } - - // This is not a true polyfill, but it gives us enough to capture marks. - // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API - return { - clearMarks(markName) { - markName = filterMarkData(markName); - - clearedMarks.push(markName); - marks = marks.filter(mark => mark !== markName); - }, - mark(markName, markOptions) { - markName = filterMarkData(markName); - - if (featureDetectionMarkName === null) { - featureDetectionMarkName = markName; - } - - marks.push(markName); - - if (markOptions != null) { - // This is triggers the feature detection. - markOptions.startTime++; - } - }, - }; - } - - function clearPendingMarks() { - clearedMarks.splice(0); - } - - function dispatchAndSetCurrentEvent(element, event) { - try { - window.event = event; - element.dispatchEvent(event); - } finally { - window.event = undefined; - } - } - beforeEach(() => { utils = require('./utils'); utils.beforeEachProfiling(); @@ -104,1021 +42,2376 @@ describe('Timeline profiler', () => { ReactDOM = require('react-dom'); Scheduler = require('scheduler'); - setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks') - .setPerformanceMock_ONLY_FOR_TESTING; - setPerformanceMock(createUserTimingPolyfill()); - }); - - afterEach(() => { - // Verify all logged marks also get cleared. - expect(marks).toHaveLength(0); - - unmountFns.forEach(unmountFn => unmountFn()); - - setPerformanceMock(null); + store = global.store; }); - it('should mark sync render without suspends or state updates', () => { - renderHelper(
); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); - }); + describe('User Timing API', () => { + let clearedMarks; + let featureDetectionMarkName = null; + let marks; + let setPerformanceMock; + + function createUserTimingPolyfill() { + featureDetectionMarkName = null; + + clearedMarks = []; + marks = []; + + // Remove file-system specific bits or version-specific bits of information from the module range marks. + function filterMarkData(markName) { + if (markName.startsWith('--react-internal-module-start')) { + return '--react-internal-module-start- at filtered (:0:0)'; + } else if (markName.startsWith('--react-internal-module-stop')) { + return '--react-internal-module-stop- at filtered (:1:1)'; + } else if (markName.startsWith('--react-version')) { + return '--react-version-'; + } else { + return markName; + } + } - it('should mark concurrent render without suspends or state updates', () => { - renderRootHelper(
); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); - }); + // This is not a true polyfill, but it gives us enough to capture marks. + // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API + return { + clearMarks(markName) { + markName = filterMarkData(markName); + + clearedMarks.push(markName); + marks = marks.filter(mark => mark !== markName); + }, + mark(markName, markOptions) { + markName = filterMarkData(markName); + + if (featureDetectionMarkName === null) { + featureDetectionMarkName = markName; + } + + marks.push(markName); + + if (markOptions != null) { + // This is triggers the feature detection. + markOptions.startTime++; + } + }, + }; + } - it('should mark render yields', async () => { - function Bar() { - Scheduler.unstable_yieldValue('Bar'); - return null; + function clearPendingMarks() { + clearedMarks.splice(0); } - function Foo() { - Scheduler.unstable_yieldValue('Foo'); - return ; + function dispatchAndSetCurrentEvent(element, event) { + try { + window.event = event; + element.dispatchEvent(event); + } finally { + window.event = undefined; + } } - React.startTransition(() => { - renderRootHelper(); + beforeEach(() => { + setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks') + .setPerformanceMock_ONLY_FOR_TESTING; + setPerformanceMock(createUserTimingPolyfill()); }); - // Do one step of work. - expect(Scheduler).toFlushAndYieldThrough(['Foo']); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-64", - "--render-start-64", - "--component-render-start-Foo", - "--component-render-stop", - "--render-yield", - ] - `); - }); + afterEach(() => { + // Verify all logged marks also get cleared. + expect(marks).toHaveLength(0); - it('should mark sync render with suspense that resolves', async () => { - const fakeSuspensePromise = Promise.resolve(true); - function Example() { - throw fakeSuspensePromise; - } + unmountFns.forEach(unmountFn => unmountFn()); + + setPerformanceMock(null); + }); + + it('should mark sync render without suspends or state updates', () => { + renderHelper(
); - renderHelper( - - - , - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--suspense-suspend-0-Example-mount-1-", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); - - clearPendingMarks(); - - await fakeSuspensePromise; - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--suspense-resolved-0-Example", + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", ] - `); - }); - - it('should mark sync render with suspense that rejects', async () => { - const fakeSuspensePromise = Promise.reject(new Error('error')); - function Example() { - throw fakeSuspensePromise; - } + `); + }); - renderHelper( - - - , - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--suspense-suspend-0-Example-mount-1-", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", - "--commit-stop", - ] - `); - - clearPendingMarks(); - - await expect(fakeSuspensePromise).rejects.toThrow(); - expect(clearedMarks).toContain(`--suspense-rejected-0-Example`); - }); + it('should mark concurrent render without suspends or state updates', () => { + renderRootHelper(
); - it('should mark concurrent render with suspense that resolves', async () => { - const fakeSuspensePromise = Promise.resolve(true); - function Example() { - throw fakeSuspensePromise; - } + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); - renderRootHelper( - - - , - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--suspense-suspend-0-Example-mount-16-", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); - - clearPendingMarks(); - - await fakeSuspensePromise; - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--suspense-resolved-0-Example", - ] - `); - }); + clearPendingMarks(); - it('should mark concurrent render with suspense that rejects', async () => { - const fakeSuspensePromise = Promise.reject(new Error('error')); - function Example() { - throw fakeSuspensePromise; - } + expect(Scheduler).toFlushUntilNextPaint([]); - renderRootHelper( - - - , - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--suspense-suspend-0-Example-mount-16-", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); - - clearPendingMarks(); - - await expect(fakeSuspensePromise).rejects.toThrow(); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--suspense-rejected-0-Example", + "--render-start-16", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", ] - `); - }); + `); + }); - it('should mark cascading class component state updates', () => { - class Example extends React.Component { - state = {didMount: false}; - componentDidMount() { - this.setState({didMount: true}); - } - render() { + it('should mark render yields', async () => { + function Bar() { + Scheduler.unstable_yieldValue('Bar'); return null; } - } - - renderRootHelper(); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--schedule-state-update-1-Example", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); - }); - it('should mark cascading class component force updates', () => { - class Example extends React.Component { - componentDidMount() { - this.forceUpdate(); - } - render() { - return null; + function Foo() { + Scheduler.unstable_yieldValue('Foo'); + return ; } - } - renderRootHelper(); + React.startTransition(() => { + renderRootHelper(); + }); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--schedule-forced-update-1-Example", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); - }); + // Do one step of work. + expect(Scheduler).toFlushAndYieldThrough(['Foo']); - it('should mark render phase state updates for class component', () => { - class Example extends React.Component { - state = {didRender: false}; - render() { - if (this.state.didRender === false) { - this.setState({didRender: true}); - } - return null; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-64", + "--render-start-64", + "--component-render-start-Foo", + "--component-render-stop", + "--render-yield", + ] + `); + }); + + it('should mark sync render with suspense that resolves', async () => { + const fakeSuspensePromise = Promise.resolve(true); + function Example() { + throw fakeSuspensePromise; } - } - renderRootHelper(); + renderHelper( + + + , + ); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-render-16", + "--schedule-render-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1-", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", ] - `); + `); - clearPendingMarks(); + clearPendingMarks(); - let errorMessage; - spyOn(console, 'error').and.callFake(message => { - errorMessage = message; + await fakeSuspensePromise; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); }); - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(console.error).toHaveBeenCalledTimes(1); - expect(errorMessage).toContain( - 'Cannot update during an existing state transition', - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--schedule-state-update-16-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); - }); - - it('should mark render phase force updates for class component', () => { - class Example extends React.Component { - state = {didRender: false}; - render() { - if (this.state.didRender === false) { - this.forceUpdate(() => this.setState({didRender: true})); - } - return null; + it('should mark sync render with suspense that rejects', async () => { + const fakeSuspensePromise = Promise.reject(new Error('error')); + function Example() { + throw fakeSuspensePromise; } - } - renderRootHelper(); + renderHelper( + + + , + ); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-render-16", + "--schedule-render-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1-", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", ] - `); + `); - clearPendingMarks(); + clearPendingMarks(); - let errorMessage; - spyOn(console, 'error').and.callFake(message => { - errorMessage = message; + await expect(fakeSuspensePromise).rejects.toThrow(); + expect(clearedMarks).toContain(`--suspense-rejected-0-Example`); }); - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(console.error).toHaveBeenCalledTimes(1); - expect(errorMessage).toContain( - 'Cannot update during an existing state transition', - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--schedule-forced-update-16-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); - }); + it('should mark concurrent render with suspense that resolves', async () => { + const fakeSuspensePromise = Promise.resolve(true); + function Example() { + throw fakeSuspensePromise; + } - it('should mark cascading layout updates', () => { - function Example() { - const [didMount, setDidMount] = React.useState(false); - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - return didMount; - } + renderRootHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); - renderRootHelper(); + expect(Scheduler).toFlushUntilNextPaint([]); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-16-", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--component-layout-effect-mount-start-Example", - "--schedule-state-update-1-Example", - "--component-layout-effect-mount-stop", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); - }); + `); - // This test is coupled to lane implementation details, so I'm disabling it in - // the new fork until it stabilizes so we don't have to repeatedly update it. - it('should mark cascading passive updates', () => { - function Example() { - const [didMount, setDidMount] = React.useState(false); - React.useEffect(() => { - setDidMount(true); - }, []); - return didMount; - } + clearPendingMarks(); - renderRootHelper(); - - expect(Scheduler).toFlushAndYield([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - "--passive-effects-start-16", - "--component-passive-effect-mount-start-Example", - "--schedule-state-update-16-Example", - "--component-passive-effect-mount-stop", - "--passive-effects-stop", - "--render-start-16", - "--component-render-start-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - ] - `); - }); + await fakeSuspensePromise; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); + }); - it('should mark render phase updates', () => { - function Example() { - const [didRender, setDidRender] = React.useState(false); - if (!didRender) { - setDidRender(true); + it('should mark concurrent render with suspense that rejects', async () => { + const fakeSuspensePromise = Promise.reject(new Error('error')); + function Example() { + throw fakeSuspensePromise; } - return didRender; - } - renderRootHelper(); - - expect(Scheduler).toFlushAndYield([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - "--render-start-16", - "--component-render-start-Example", - "--schedule-state-update-16-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--layout-effects-stop", - "--commit-stop", - ] - `); - }); + renderRootHelper( + + + , + ); - it('should mark sync render that throws', async () => { - spyOn(console, 'error'); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { - return null; - } - return this.props.children; - } - } + clearPendingMarks(); - function ExampleThatThrows() { - throw Error('Expected error'); - } + expect(Scheduler).toFlushUntilNextPaint([]); - renderHelper( - - - , - ); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--component-render-start-ExampleThatThrows", - "--component-render-start-ExampleThatThrows", - "--component-render-stop", - "--error-ExampleThatThrows-mount-Expected error", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--schedule-state-update-1-ErrorBoundary", - "--layout-effects-stop", - "--commit-stop", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - ] - `); - }); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-16-", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); - it('should mark concurrent render that throws', async () => { - spyOn(console, 'error'); + clearPendingMarks(); - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { + await expect(fakeSuspensePromise).rejects.toThrow(); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-rejected-0-Example", + ] + `); + }); + + it('should mark cascading class component state updates', () => { + class Example extends React.Component { + state = {didMount: false}; + componentDidMount() { + this.setState({didMount: true}); + } + render() { return null; } - return this.props.children; } - } - function ExampleThatThrows() { - // eslint-disable-next-line no-throw-literal - throw 'Expected error'; - } + renderRootHelper(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); - renderRootHelper( - - - , - ); + expect(Scheduler).toFlushUntilNextPaint([]); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-state-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--component-render-start-ExampleThatThrows", - "--component-render-start-ExampleThatThrows", - "--component-render-stop", - "--error-ExampleThatThrows-mount-Expected error", - "--render-stop", - "--render-start-16", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--component-render-start-ExampleThatThrows", - "--component-render-start-ExampleThatThrows", - "--component-render-stop", - "--error-ExampleThatThrows-mount-Expected error", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--schedule-state-update-1-ErrorBoundary", - "--layout-effects-stop", - "--render-start-1", - "--component-render-start-ErrorBoundary", - "--component-render-stop", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--commit-stop", - "--commit-stop", - ] - `); - }); + `); + }); - it('should mark passive and layout effects', async () => { - function ComponentWithEffects() { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('layout 1 mount'); - return () => { - Scheduler.unstable_yieldValue('layout 1 unmount'); - }; - }, []); - - React.useEffect(() => { - Scheduler.unstable_yieldValue('passive 1 mount'); - return () => { - Scheduler.unstable_yieldValue('passive 1 unmount'); - }; - }, []); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('layout 2 mount'); - return () => { - Scheduler.unstable_yieldValue('layout 2 unmount'); - }; - }, []); - - React.useEffect(() => { - Scheduler.unstable_yieldValue('passive 2 mount'); - return () => { - Scheduler.unstable_yieldValue('passive 2 unmount'); - }; - }, []); - - React.useEffect(() => { - Scheduler.unstable_yieldValue('passive 3 mount'); - return () => { - Scheduler.unstable_yieldValue('passive 3 unmount'); - }; - }, []); - - return null; - } + it('should mark cascading class component force updates', () => { + class Example extends React.Component { + componentDidMount() { + this.forceUpdate(); + } + render() { + return null; + } + } - const unmount = renderRootHelper(); - - expect(Scheduler).toFlushUntilNextPaint([ - 'layout 1 mount', - 'layout 2 mount', - ]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - "--render-start-16", - "--component-render-start-ComponentWithEffects", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-16", - "--component-layout-effect-mount-start-ComponentWithEffects", - "--component-layout-effect-mount-stop", - "--component-layout-effect-mount-start-ComponentWithEffects", - "--component-layout-effect-mount-stop", - "--layout-effects-stop", - "--commit-stop", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushAndYield([ - 'passive 1 mount', - 'passive 2 mount', - 'passive 3 mount', - ]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--passive-effects-start-16", - "--component-passive-effect-mount-start-ComponentWithEffects", - "--component-passive-effect-mount-stop", - "--component-passive-effect-mount-start-ComponentWithEffects", - "--component-passive-effect-mount-stop", - "--component-passive-effect-mount-start-ComponentWithEffects", - "--component-passive-effect-mount-stop", - "--passive-effects-stop", - ] - `); - - clearPendingMarks(); - - expect(Scheduler).toFlushAndYield([]); - - unmount(); - - expect(Scheduler).toHaveYielded([ - 'layout 1 unmount', - 'layout 2 unmount', - 'passive 1 unmount', - 'passive 2 unmount', - 'passive 3 unmount', - ]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--render-stop", - "--commit-start-1", - "--react-version-", - "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--component-layout-effect-unmount-start-ComponentWithEffects", - "--component-layout-effect-unmount-stop", - "--component-layout-effect-unmount-start-ComponentWithEffects", - "--component-layout-effect-unmount-stop", - "--layout-effects-start-1", - "--layout-effects-stop", - "--passive-effects-start-1", - "--component-passive-effect-unmount-start-ComponentWithEffects", - "--component-passive-effect-unmount-stop", - "--component-passive-effect-unmount-start-ComponentWithEffects", - "--component-passive-effect-unmount-stop", - "--component-passive-effect-unmount-start-ComponentWithEffects", - "--component-passive-effect-unmount-stop", - "--passive-effects-stop", - "--commit-stop", - ] - `); - }); + renderRootHelper(); - describe('lane labels', () => { - it('regression test SyncLane', () => { - renderHelper(
); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-render-1", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-forced-update-1-Example", + "--layout-effects-stop", "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", "--render-stop", "--commit-start-1", "--react-version-", "--profiler-version-1", - "--react-internal-module-start-", - "--react-internal-module-stop-", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", - "--layout-effects-start-1", - "--layout-effects-stop", + "--commit-stop", "--commit-stop", ] `); }); - it('regression test DefaultLane', () => { - renderRootHelper(
); + it('should mark render phase state updates for class component', () => { + class Example extends React.Component { + state = {didRender: false}; + render() { + if (this.state.didRender === false) { + this.setState({didRender: true}); + } + return null; + } + } + + renderRootHelper(); + expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + let errorMessage; + spyOn(console, 'error').and.callFake(message => { + errorMessage = message; + }); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(errorMessage).toContain( + 'Cannot update during an existing state transition', + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--schedule-state-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start- at filtered (:0:0)", + "--react-internal-module-stop- at filtered (:1:1)", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] `); }); - it('regression test InputDiscreteLane', async () => { - const targetRef = React.createRef(null); - - function App() { - const [count, setCount] = React.useState(0); - const handleClick = () => { - setCount(count + 1); - }; - return - - ); -} diff --git a/packages/react-devtools-timeline/src/Timeline.js b/packages/react-devtools-timeline/src/Timeline.js index 290830840356e..bfd2a35f733e7 100644 --- a/packages/react-devtools-timeline/src/Timeline.js +++ b/packages/react-devtools-timeline/src/Timeline.js @@ -9,7 +9,6 @@ import type {ViewState} from './types'; -import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; import * as React from 'react'; import { Suspense, @@ -20,18 +19,28 @@ import { useState, } from 'react'; import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; +import {ProfilerContext} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext'; +import NoProfilingData from 'react-devtools-shared/src/devtools/views/Profiler/NoProfilingData'; +import RecordingInProgress from 'react-devtools-shared/src/devtools/views/Profiler/RecordingInProgress'; import {updateColorsToMatchTheme} from './content-views/constants'; import {TimelineContext} from './TimelineContext'; -import ImportButton from './ImportButton'; import CanvasPage from './CanvasPage'; import {importFile} from './timelineCache'; import TimelineSearchInput from './TimelineSearchInput'; +import TimelineNotSupported from './TimelineNotSupported'; import {TimelineSearchContextController} from './TimelineSearchContext'; import styles from './Timeline.css'; export function Timeline(_: {||}) { - const {file, setFile, viewState} = useContext(TimelineContext); + const { + file, + inMemoryTimelineData, + isTimelineSupported, + setFile, + viewState, + } = useContext(TimelineContext); + const {didRecordCommits, isProfiling} = useContext(ProfilerContext); const ref = useRef(null); @@ -60,66 +69,47 @@ export function Timeline(_: {||}) { }; }, [deferredTheme]); + let content = null; + if (isProfiling) { + content = ; + } else if (inMemoryTimelineData && inMemoryTimelineData.length > 0) { + // TODO (timeline) Support multiple renderers. + const timelineData = inMemoryTimelineData[0]; + + content = ( + + + + + ); + } else if (file) { + content = ( + }> + + + ); + } else if (didRecordCommits) { + content = ; + } else if (isTimelineSupported) { + content = ; + } else { + content = ; + } + return (
- {file ? ( - }> - - - ) : ( - - )} + {content}
); } -const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => ( -
    - {isInternalFacebookBuild && ( -
  1. - Enable the - - react_enable_scheduling_profiler GK - - . -
  2. - )} -
  3. - Open a website that's built with the - - profiling build of ReactDOM - - (version 18 or newer). -
  4. -
  5. - Open the "Performance" tab in Chrome and record some performance data. -
  6. -
  7. - Click the "Save profile..." button in Chrome to export the data. -
  8. -
  9. - Import the data into the profiler: -
    - - Import - -
  10. -
-); - const ProcessingData = () => (
Processing data...
@@ -136,9 +126,15 @@ const CouldNotLoadProfile = ({error, onFileSelect}) => (
)}
- Try importing - - another Chrome performance profile. + Try importing another Chrome performance profile. +
+
+); + +const NoTimelineData = () => ( +
+
+ This current profile does not contain timeline data.
); diff --git a/packages/react-devtools-timeline/src/TimelineContext.js b/packages/react-devtools-timeline/src/TimelineContext.js index cbf57a275c0c5..f024ec68dccf6 100644 --- a/packages/react-devtools-timeline/src/TimelineContext.js +++ b/packages/react-devtools-timeline/src/TimelineContext.js @@ -8,10 +8,19 @@ */ import * as React from 'react'; -import {createContext, useMemo, useRef, useState} from 'react'; +import { + createContext, + useContext, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from 'react'; +import {StoreContext} from 'react-devtools-shared/src/devtools/views/context'; import type { HorizontalScrollStateChangeCallback, + TimelineData, SearchRegExpStateChangeCallback, ViewState, } from './types'; @@ -19,6 +28,8 @@ import type {RefObject} from 'shared/ReactTypes'; export type Context = {| file: File | null, + inMemoryTimelineData: Array | null, + isTimelineSupported: boolean, searchInputContainerRef: RefObject, setFile: (file: File | null) => void, viewState: ViewState, @@ -35,6 +46,34 @@ function TimelineContextController({children}: Props) { const searchInputContainerRef = useRef(null); const [file, setFile] = useState(null); + const store = useContext(StoreContext); + + const isTimelineSupported = useSyncExternalStore( + function subscribe(callback) { + store.addListener('rootSupportsTimelineProfiling', callback); + return function unsubscribe() { + store.removeListener('rootSupportsTimelineProfiling', callback); + }; + }, + function getState() { + return store.rootSupportsTimelineProfiling; + }, + ); + + const inMemoryTimelineData = useSyncExternalStore | null>( + function subscribe(callback) { + store.profilerStore.addListener('isProcessingData', callback); + store.profilerStore.addListener('profilingData', callback); + return function unsubscribe() { + store.profilerStore.removeListener('isProcessingData', callback); + store.profilerStore.removeListener('profilingData', callback); + }; + }, + function getState() { + return store.profilerStore.profilingData?.timelineData || null; + }, + ); + // Recreate view state any time new profiling data is imported. const viewState = useMemo(() => { const horizontalScrollStateChangeCallbacks: Set = new Set(); @@ -85,11 +124,13 @@ function TimelineContextController({children}: Props) { const value = useMemo( () => ({ file, + inMemoryTimelineData, + isTimelineSupported, searchInputContainerRef, setFile, viewState, }), - [file, setFile, viewState], + [file, inMemoryTimelineData, isTimelineSupported, setFile, viewState], ); return ( diff --git a/packages/react-devtools-timeline/src/TimelineNotSupported.css b/packages/react-devtools-timeline/src/TimelineNotSupported.css new file mode 100644 index 0000000000000..971723520a456 --- /dev/null +++ b/packages/react-devtools-timeline/src/TimelineNotSupported.css @@ -0,0 +1,38 @@ +.Column { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0 1rem; +} + +.Header { + font-size: var(--font-size-sans-large); + margin-bottom: 0.5rem; +} + +.Paragraph { + text-align: center; + margin: 0; +} + +.Link { + color: var(--color-link); +} + +.LearnMoreRow { + margin-top: 1rem; + color: var(--color-dim); + font-size: var(--font-size-sans-small); +} + +.Code { + color: var(--color-bridge-version-number); +} + +.MetaGKRow { + background: var(--color-background-hover); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/packages/react-devtools-timeline/src/TimelineNotSupported.js b/packages/react-devtools-timeline/src/TimelineNotSupported.js new file mode 100644 index 0000000000000..9a21df8ab8cb3 --- /dev/null +++ b/packages/react-devtools-timeline/src/TimelineNotSupported.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; + +import styles from './TimelineNotSupported.css'; + +export default function TimelineNotSupported() { + return ( +
+
Timeline profiling not supported.
+

+ + Timeline profiler requires a development or profiling build of{' '} + react-dom@^18. + +

+
+ Click{' '} + + here + {' '} + to learn more about profiling. +
+ + {isInternalFacebookBuild && ( +
+ Meta only: Enable the{' '} + + react_enable_scheduling_profiler GK + + . +
+ )} +
+ ); +} diff --git a/packages/react-devtools-timeline/src/TimelineSearchContext.js b/packages/react-devtools-timeline/src/TimelineSearchContext.js index 46d05710ac091..c545f83888e91 100644 --- a/packages/react-devtools-timeline/src/TimelineSearchContext.js +++ b/packages/react-devtools-timeline/src/TimelineSearchContext.js @@ -10,14 +10,10 @@ import * as React from 'react'; import {createContext, useMemo, useReducer} from 'react'; -import type { - ReactComponentMeasure, - ReactProfilerData, - ViewState, -} from './types'; +import type {ReactComponentMeasure, TimelineData, ViewState} from './types'; type State = {| - profilerData: ReactProfilerData, + profilerData: TimelineData, searchIndex: number, searchRegExp: RegExp | null, searchResults: Array, @@ -116,7 +112,7 @@ function reducer(state: State, action: Action): State { } export type Context = {| - profilerData: ReactProfilerData, + profilerData: TimelineData, // Search state dispatch: Dispatch, @@ -131,7 +127,7 @@ TimelineSearchContext.displayName = 'TimelineSearchContext'; type Props = {| children: React$Node, - profilerData: ReactProfilerData, + profilerData: TimelineData, viewState: ViewState, |}; diff --git a/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js index 2194c79e87fca..c4fd41bdfa250 100644 --- a/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js @@ -7,11 +7,7 @@ * @flow */ -import type { - ReactComponentMeasure, - ReactProfilerData, - ViewState, -} from '../types'; +import type {ReactComponentMeasure, TimelineData, ViewState} from '../types'; import type { Interaction, IntrinsicSize, @@ -44,7 +40,7 @@ export class ComponentMeasuresView extends View { _cachedSearchRegExp: RegExp | null = null; _hoveredComponentMeasure: ReactComponentMeasure | null = null; _intrinsicSize: IntrinsicSize; - _profilerData: ReactProfilerData; + _profilerData: TimelineData; _viewState: ViewState; onHover: ((event: ReactComponentMeasure | null) => void) | null = null; @@ -52,7 +48,7 @@ export class ComponentMeasuresView extends View { constructor( surface: Surface, frame: Rect, - profilerData: ReactProfilerData, + profilerData: TimelineData, viewState: ViewState, ) { super(surface, frame); diff --git a/packages/react-devtools-timeline/src/content-views/NativeEventsView.js b/packages/react-devtools-timeline/src/content-views/NativeEventsView.js index 29e2535de6ab9..5db13dbc675e3 100644 --- a/packages/react-devtools-timeline/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-timeline/src/content-views/NativeEventsView.js @@ -7,7 +7,7 @@ * @flow */ -import type {NativeEvent, ReactProfilerData} from '../types'; +import type {NativeEvent, TimelineData} from '../types'; import type { Interaction, IntrinsicSize, @@ -40,11 +40,11 @@ export class NativeEventsView extends View { _hoveredEvent: NativeEvent | null = null; _intrinsicSize: IntrinsicSize; _maxDepth: number = 0; - _profilerData: ReactProfilerData; + _profilerData: TimelineData; onHover: ((event: NativeEvent | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._profilerData = profilerData; diff --git a/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js b/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js index 321bfcca1d5aa..547af24bb26ab 100644 --- a/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js @@ -7,7 +7,7 @@ * @flow */ -import type {NetworkMeasure, ReactProfilerData} from '../types'; +import type {NetworkMeasure, TimelineData} from '../types'; import type { Interaction, IntrinsicSize, @@ -43,11 +43,11 @@ export class NetworkMeasuresView extends View { _hoveredNetworkMeasure: NetworkMeasure | null = null; _intrinsicSize: IntrinsicSize; _maxDepth: number = 0; - _profilerData: ReactProfilerData; + _profilerData: TimelineData; onHover: ((event: NetworkMeasure | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._profilerData = profilerData; diff --git a/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js index 87a377be1a1f8..f46d435738d39 100644 --- a/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactLane, ReactMeasure, ReactProfilerData} from '../types'; +import type {ReactLane, ReactMeasure, TimelineData} from '../types'; import type { Interaction, IntrinsicSize, @@ -33,7 +33,6 @@ import { } from '../view-base'; import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants'; -import {REACT_TOTAL_NUM_LANES} from '../constants'; const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE; const MAX_ROWS_TO_SHOW_INITIALLY = 5; @@ -41,12 +40,12 @@ const MAX_ROWS_TO_SHOW_INITIALLY = 5; export class ReactMeasuresView extends View { _intrinsicSize: IntrinsicSize; _lanesToRender: ReactLane[]; - _profilerData: ReactProfilerData; + _profilerData: TimelineData; _hoveredMeasure: ReactMeasure | null = null; onHover: ((measure: ReactMeasure | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._profilerData = profilerData; this._performPreflightComputations(); @@ -55,12 +54,11 @@ export class ReactMeasuresView extends View { _performPreflightComputations() { this._lanesToRender = []; - for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) { - const measuresForLane = this._profilerData.laneToReactMeasureMap.get( - lane, - ); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const [lane, measuresForLane] of this._profilerData + .laneToReactMeasureMap) { // Only show lanes with measures - if (measuresForLane != null && measuresForLane.length > 0) { + if (measuresForLane.length > 0) { this._lanesToRender.push(lane); } } diff --git a/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js b/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js index 39921a727366e..3798d261e0b1b 100644 --- a/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js +++ b/packages/react-devtools-timeline/src/content-views/SchedulingEventsView.js @@ -7,7 +7,7 @@ * @flow */ -import type {SchedulingEvent, ReactProfilerData} from '../types'; +import type {SchedulingEvent, TimelineData} from '../types'; import type { Interaction, MouseMoveInteraction, @@ -40,13 +40,13 @@ const EVENT_ROW_HEIGHT_FIXED = TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING; export class SchedulingEventsView extends View { - _profilerData: ReactProfilerData; + _profilerData: TimelineData; _intrinsicSize: Size; _hoveredEvent: SchedulingEvent | null = null; onHover: ((event: SchedulingEvent | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._profilerData = profilerData; diff --git a/packages/react-devtools-timeline/src/content-views/SnapshotsView.js b/packages/react-devtools-timeline/src/content-views/SnapshotsView.js index b027acae37693..eaaf44659ea33 100644 --- a/packages/react-devtools-timeline/src/content-views/SnapshotsView.js +++ b/packages/react-devtools-timeline/src/content-views/SnapshotsView.js @@ -7,7 +7,7 @@ * @flow */ -import type {Snapshot, ReactProfilerData} from '../types'; +import type {Snapshot, TimelineData} from '../types'; import type { Interaction, Point, @@ -31,11 +31,11 @@ type OnHover = (node: Snapshot | null) => void; export class SnapshotsView extends View { _hoverLocation: Point | null = null; _intrinsicSize: Size; - _profilerData: ReactProfilerData; + _profilerData: TimelineData; onHover: OnHover | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._intrinsicSize = { diff --git a/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js b/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js index 58601d3590f53..370c8f763275c 100644 --- a/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js @@ -7,7 +7,7 @@ * @flow */ -import type {SuspenseEvent, ReactProfilerData} from '../types'; +import type {SuspenseEvent, TimelineData} from '../types'; import type { Interaction, IntrinsicSize, @@ -47,11 +47,11 @@ export class SuspenseEventsView extends View { _hoveredEvent: SuspenseEvent | null = null; _intrinsicSize: IntrinsicSize; _maxDepth: number = 0; - _profilerData: ReactProfilerData; + _profilerData: TimelineData; onHover: ((event: SuspenseEvent | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._profilerData = profilerData; diff --git a/packages/react-devtools-timeline/src/content-views/ThrownErrorsView.js b/packages/react-devtools-timeline/src/content-views/ThrownErrorsView.js index e1d084ee40935..d2a70e9524b14 100644 --- a/packages/react-devtools-timeline/src/content-views/ThrownErrorsView.js +++ b/packages/react-devtools-timeline/src/content-views/ThrownErrorsView.js @@ -7,7 +7,7 @@ * @flow */ -import type {ThrownError, ReactProfilerData} from '../types'; +import type {ThrownError, TimelineData} from '../types'; import type { Interaction, MouseMoveInteraction, @@ -40,12 +40,12 @@ const EVENT_ROW_HEIGHT_FIXED = TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING; export class ThrownErrorsView extends View { - _profilerData: ReactProfilerData; + _profilerData: TimelineData; _intrinsicSize: Size; _hoveredEvent: ThrownError | null = null; onHover: ((event: ThrownError | null) => void) | null = null; - constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + constructor(surface: Surface, frame: Rect, profilerData: TimelineData) { super(surface, frame); this._profilerData = profilerData; diff --git a/packages/react-devtools-timeline/src/createDataResourceFromImportedFile.js b/packages/react-devtools-timeline/src/createDataResourceFromImportedFile.js index 3c7e74326094d..66ac1e3ff068f 100644 --- a/packages/react-devtools-timeline/src/createDataResourceFromImportedFile.js +++ b/packages/react-devtools-timeline/src/createDataResourceFromImportedFile.js @@ -11,17 +11,17 @@ import {createResource} from 'react-devtools-shared/src/devtools/cache'; import {importFile} from './import-worker'; import type {Resource} from 'react-devtools-shared/src/devtools/cache'; -import type {ReactProfilerData} from './types'; +import type {TimelineData} from './types'; import type {ImportWorkerOutputData} from './import-worker/index'; -export type DataResource = Resource; +export type DataResource = Resource; export default function createDataResourceFromImportedFile( file: File, ): DataResource { return createResource( () => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const promise = ((importFile( file, ): any): Promise); diff --git a/packages/react-devtools-timeline/src/import-worker/index.js b/packages/react-devtools-timeline/src/import-worker/index.js index 3ce3a0ff93da6..636a20ed5f7b6 100644 --- a/packages/react-devtools-timeline/src/import-worker/index.js +++ b/packages/react-devtools-timeline/src/import-worker/index.js @@ -13,7 +13,7 @@ import * as importFileModule from './importFile'; import WorkerizedImportFile from './importFile.worker'; -import type {ReactProfilerData} from '../types'; +import type {TimelineData} from '../types'; type ImportFileModule = typeof importFileModule; @@ -22,7 +22,7 @@ const workerizedImportFile: ImportFileModule = window.Worker : importFileModule; export type ImportWorkerOutputData = - | {|status: 'SUCCESS', processedData: ReactProfilerData|} + | {|status: 'SUCCESS', processedData: TimelineData|} | {|status: 'INVALID_PROFILE_ERROR', error: Error|} | {|status: 'UNEXPECTED_ERROR', error: Error|}; diff --git a/packages/react-devtools-timeline/src/import-worker/preprocessData.js b/packages/react-devtools-timeline/src/import-worker/preprocessData.js index 98e802793f6a8..258eef931a382 100644 --- a/packages/react-devtools-timeline/src/import-worker/preprocessData.js +++ b/packages/react-devtools-timeline/src/import-worker/preprocessData.js @@ -25,7 +25,7 @@ import type { ReactComponentMeasureType, ReactMeasure, ReactMeasureType, - ReactProfilerData, + TimelineData, SchedulingEvent, SuspenseEvent, } from '../types'; @@ -105,7 +105,7 @@ export function getLanesFromTransportDecimalBitmask( } function updateLaneToLabelMap( - profilerData: ReactProfilerData, + profilerData: TimelineData, laneLabelTuplesString: string, ): void { // These marks appear multiple times in the data; @@ -143,7 +143,7 @@ function markWorkStarted( type: ReactMeasureType, startTime: Milliseconds, lanes: ReactLane[], - currentProfilerData: ReactProfilerData, + currentProfilerData: TimelineData, state: ProcessorState, ) { const {batchUID, measureStack} = state; @@ -179,7 +179,7 @@ function markWorkStarted( function markWorkCompleted( type: ReactMeasureType, stopTime: Milliseconds, - currentProfilerData: ReactProfilerData, + currentProfilerData: TimelineData, stack: $PropertyType, ) { if (stack.length === 0) { @@ -229,7 +229,7 @@ function throwIfIncomplete( function processEventDispatch( event: TimelineEvent, timestamp: Milliseconds, - profilerData: ReactProfilerData, + profilerData: TimelineData, state: ProcessorState, ) { const data = event.args.data; @@ -292,7 +292,7 @@ function processEventDispatch( function processResourceFinish( event: TimelineEvent, timestamp: Milliseconds, - profilerData: ReactProfilerData, + profilerData: TimelineData, state: ProcessorState, ) { const requestId = event.args.data.requestId; @@ -314,7 +314,7 @@ function processResourceFinish( function processResourceReceivedData( event: TimelineEvent, timestamp: Milliseconds, - profilerData: ReactProfilerData, + profilerData: TimelineData, state: ProcessorState, ) { const requestId = event.args.data.requestId; @@ -331,7 +331,7 @@ function processResourceReceivedData( function processResourceReceiveResponse( event: TimelineEvent, timestamp: Milliseconds, - profilerData: ReactProfilerData, + profilerData: TimelineData, state: ProcessorState, ) { const requestId = event.args.data.requestId; @@ -344,7 +344,7 @@ function processResourceReceiveResponse( function processScreenshot( event: TimelineEvent, timestamp: Milliseconds, - profilerData: ReactProfilerData, + profilerData: TimelineData, state: ProcessorState, ) { const encodedSnapshot = event.args.snapshot; // Base 64 encoded @@ -385,7 +385,7 @@ function processScreenshot( function processResourceSendRequest( event: TimelineEvent, timestamp: Milliseconds, - profilerData: ReactProfilerData, + profilerData: TimelineData, state: ProcessorState, ) { const data = event.args.data; @@ -428,7 +428,7 @@ function processResourceSendRequest( function processTimelineEvent( event: TimelineEvent, /** Finalized profiler data up to `event`. May be mutated. */ - currentProfilerData: ReactProfilerData, + currentProfilerData: TimelineData, /** Intermediate processor state. May be mutated. */ state: ProcessorState, ) { @@ -551,10 +551,7 @@ function processTimelineEvent( timestamp: startTime, type: 'thrown-error', }); - } // eslint-disable-line brace-style - - // React Events - suspense - else if (name.startsWith('--suspense-suspend-')) { + } else if (name.startsWith('--suspense-suspend-')) { const [ id, componentName, @@ -592,7 +589,6 @@ function processTimelineEvent( phase: ((phase: any): Phase), promiseName: promiseName || null, resolution: 'unresolved', - resuspendTimestamps: null, timestamp: startTime, type: 'suspense', warning: null, @@ -611,16 +607,6 @@ function processTimelineEvent( currentProfilerData.suspenseEvents.push(suspenseEvent); state.unresolvedSuspenseEvents.set(id, suspenseEvent); - } else if (name.startsWith('--suspense-resuspend-')) { - const [id] = name.substr(21).split('-'); - const suspenseEvent = state.unresolvedSuspenseEvents.get(id); - if (suspenseEvent != null) { - if (suspenseEvent.resuspendTimestamps === null) { - suspenseEvent.resuspendTimestamps = [startTime]; - } else { - suspenseEvent.resuspendTimestamps.push(startTime); - } - } } else if (name.startsWith('--suspense-resolved-')) { const [id] = name.substr(20).split('-'); const suspenseEvent = state.unresolvedSuspenseEvents.get(id); @@ -639,10 +625,7 @@ function processTimelineEvent( suspenseEvent.duration = startTime - suspenseEvent.timestamp; suspenseEvent.resolution = 'rejected'; } - } // eslint-disable-line brace-style - - // React Measures - render - else if (name.startsWith('--render-start-')) { + } else if (name.startsWith('--render-start-')) { if (state.nextRenderShouldGenerateNewBatchID) { state.nextRenderShouldGenerateNewBatchID = false; state.batchUID = ((state.uidCounter++: any): BatchUID); @@ -701,24 +684,7 @@ function processTimelineEvent( currentProfilerData, state.measureStack, ); - } else if (name.startsWith('--render-cancel')) { - state.nextRenderShouldGenerateNewBatchID = true; - markWorkCompleted( - 'render', - startTime, - currentProfilerData, - state.measureStack, - ); - markWorkCompleted( - 'render-idle', - startTime, - currentProfilerData, - state.measureStack, - ); - } // eslint-disable-line brace-style - - // React Measures - commits - else if (name.startsWith('--commit-start-')) { + } else if (name.startsWith('--commit-start-')) { state.nextRenderShouldGenerateNewBatchID = true; const [laneBitmaskString] = name.substr(15).split('-'); @@ -742,10 +708,7 @@ function processTimelineEvent( currentProfilerData, state.measureStack, ); - } // eslint-disable-line brace-style - - // React Measures - layout effects - else if (name.startsWith('--layout-effects-start-')) { + } else if (name.startsWith('--layout-effects-start-')) { const [laneBitmaskString] = name.substr(23).split('-'); markWorkStarted( @@ -762,10 +725,7 @@ function processTimelineEvent( currentProfilerData, state.measureStack, ); - } // eslint-disable-line brace-style - - // React Measures - passive effects - else if (name.startsWith('--passive-effects-start-')) { + } else if (name.startsWith('--passive-effects-start-')) { const [laneBitmaskString] = name.substr(24).split('-'); markWorkStarted( @@ -782,10 +742,7 @@ function processTimelineEvent( currentProfilerData, state.measureStack, ); - } // eslint-disable-line brace-style - - // Internal module ranges - else if (name.startsWith('--react-internal-module-start-')) { + } else if (name.startsWith('--react-internal-module-start-')) { const stackFrameStart = name.substr(30); if (!state.internalModuleStackStringSet.has(stackFrameStart)) { @@ -796,7 +753,7 @@ function processTimelineEvent( state.internalModuleCurrentStackFrame = parsedStackFrameStart; } } else if (name.startsWith('--react-internal-module-stop-')) { - const stackFrameStop = name.substr(19); + const stackFrameStop = name.substr(29); if (!state.internalModuleStackStringSet.has(stackFrameStop)) { state.internalModuleStackStringSet.add(stackFrameStop); @@ -825,10 +782,7 @@ function processTimelineEvent( } } } - } // eslint-disable-line brace-style - - // Other user timing marks/measures - else if (ph === 'R' || ph === 'n') { + } else if (ph === 'R' || ph === 'n') { // User Timing mark currentProfilerData.otherUserTimingMarks.push({ name, @@ -841,10 +795,7 @@ function processTimelineEvent( } else if (ph === 'i' || ph === 'I') { // Instant events. // Note that the capital "I" is a deprecated value that exists in Chrome Canary traces. - } // eslint-disable-line brace-style - - // Unrecognized event - else { + } else { throw new InvalidProfileError( `Unrecognized event ${JSON.stringify( event, @@ -882,7 +833,7 @@ function assertCurrentComponentMeasureType( function processReactComponentMeasure( name: string, startTime: Milliseconds, - currentProfilerData: ReactProfilerData, + currentProfilerData: TimelineData, state: ProcessorState, ): void { if (name.startsWith('--component-render-start-')) { @@ -1048,7 +999,7 @@ function parseStackFrame(stackFrame: string): ErrorStackFrame | null { export default async function preprocessData( timeline: TimelineEvent[], -): Promise { +): Promise { const flamechart = preprocessFlamechart(timeline); const laneToReactMeasureMap = new Map(); @@ -1056,7 +1007,7 @@ export default async function preprocessData( laneToReactMeasureMap.set(lane, []); } - const profilerData: ReactProfilerData = { + const profilerData: TimelineData = { batchUIDToMeasuresMap: new Map(), componentMeasures: [], duration: 0, diff --git a/packages/react-devtools-timeline/src/timelineCache.js b/packages/react-devtools-timeline/src/timelineCache.js index 4e0948a48193c..0616f70008640 100644 --- a/packages/react-devtools-timeline/src/timelineCache.js +++ b/packages/react-devtools-timeline/src/timelineCache.js @@ -8,7 +8,7 @@ */ import type {Wakeable} from 'shared/ReactTypes'; -import type {ReactProfilerData} from './types'; +import type {TimelineData} from './types'; import {importFile as importFileWorker} from './import-worker'; @@ -36,10 +36,7 @@ type Record = PendingRecord | ResolvedRecord | RejectedRecord; // This is intentionally a module-level Map, rather than a React-managed one. // Otherwise, refreshing the inspected element cache would also clear this cache. // Profiler file contents are static anyway. -const fileNameToProfilerDataMap: Map< - string, - Record, -> = new Map(); +const fileNameToProfilerDataMap: Map> = new Map(); function readRecord(record: Record): ResolvedRecord | RejectedRecord { if (record.status === Resolved) { @@ -53,7 +50,7 @@ function readRecord(record: Record): ResolvedRecord | RejectedRecord { } } -export function importFile(file: File): ReactProfilerData | Error { +export function importFile(file: File): TimelineData | Error { const fileName = file.name; let record = fileNameToProfilerDataMap.get(fileName); @@ -74,7 +71,7 @@ export function importFile(file: File): ReactProfilerData | Error { callbacks.clear(); }; - const newRecord: Record = (record = { + const newRecord: Record = (record = { status: Pending, value: wakeable, }); @@ -82,7 +79,7 @@ export function importFile(file: File): ReactProfilerData | Error { importFileWorker(file).then(data => { switch (data.status) { case 'SUCCESS': - const resolvedRecord = ((newRecord: any): ResolvedRecord); + const resolvedRecord = ((newRecord: any): ResolvedRecord); resolvedRecord.status = Resolved; resolvedRecord.value = data.processedData; break; diff --git a/packages/react-devtools-timeline/src/types.js b/packages/react-devtools-timeline/src/types.js index 3e9867c8b8c65..9469523a1e2c2 100644 --- a/packages/react-devtools-timeline/src/types.js +++ b/packages/react-devtools-timeline/src/types.js @@ -68,7 +68,6 @@ export type SuspenseEvent = {| +phase: Phase | null, promiseName: string | null, resolution: 'rejected' | 'resolved' | 'unresolved', - resuspendTimestamps: Array | null, +type: 'suspense', |}; @@ -196,13 +195,15 @@ export type InternalModuleSourceToRanges = Map< Array<[ErrorStackFrame, ErrorStackFrame]>, >; -export type ReactProfilerData = {| +export type LaneToLabelMap = Map; + +export type TimelineData = {| batchUIDToMeasuresMap: Map, componentMeasures: ReactComponentMeasure[], duration: number, flamechart: Flamechart, internalModuleSourceToRanges: InternalModuleSourceToRanges, - laneToLabelMap: Map, + laneToLabelMap: LaneToLabelMap, laneToReactMeasureMap: Map, nativeEvents: NativeEvent[], networkMeasures: NetworkMeasure[], @@ -216,6 +217,28 @@ export type ReactProfilerData = {| thrownErrors: ThrownError[], |}; +export type TimelineDataExport = {| + batchUIDToMeasuresKeyValueArray: Array<[BatchUID, ReactMeasure[]]>, + componentMeasures: ReactComponentMeasure[], + duration: number, + flamechart: Flamechart, + internalModuleSourceToRanges: Array< + [string, Array<[ErrorStackFrame, ErrorStackFrame]>], + >, + laneToLabelKeyValueArray: Array<[ReactLane, string]>, + laneToReactMeasureKeyValueArray: Array<[ReactLane, ReactMeasure[]]>, + nativeEvents: NativeEvent[], + networkMeasures: NetworkMeasure[], + otherUserTimingMarks: UserTimingMark[], + reactVersion: string | null, + schedulingEvents: SchedulingEvent[], + snapshots: Snapshot[], + snapshotHeight: number, + startTime: number, + suspenseEvents: SuspenseEvent[], + thrownErrors: ThrownError[], +|}; + export type ReactHoverContextInfo = {| componentMeasure: ReactComponentMeasure | null, flamechartStackFrame: FlamechartStackFrame | null, diff --git a/packages/react-devtools-timeline/src/utils/getBatchRange.js b/packages/react-devtools-timeline/src/utils/getBatchRange.js index 840a105f8d687..8dd425d263e05 100644 --- a/packages/react-devtools-timeline/src/utils/getBatchRange.js +++ b/packages/react-devtools-timeline/src/utils/getBatchRange.js @@ -13,12 +13,12 @@ import type { BatchUID, Milliseconds, ReactMeasure, - ReactProfilerData, + TimelineData, } from '../types'; function unmemoizedGetBatchRange( batchUID: BatchUID, - data: ReactProfilerData, + data: TimelineData, minStartTime?: number = 0, ): [Milliseconds, Milliseconds] { const measures = data.batchUIDToMeasuresMap.get(batchUID); diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js index 276d209054745..7f0688c272ebc 100644 --- a/scripts/jest/config.build-devtools.js +++ b/scripts/jest/config.build-devtools.js @@ -70,9 +70,15 @@ module.exports = Object.assign({}, baseConfig, { require.resolve( '../../packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js' ), + require.resolve( + '../../packages/react-devtools-shared/src/__tests__/__serializers__/profilingSerializer.js' + ), require.resolve( '../../packages/react-devtools-shared/src/__tests__/__serializers__/storeSerializer.js' ), + require.resolve( + '../../packages/react-devtools-shared/src/__tests__/__serializers__/timelineDataSerializer.js' + ), require.resolve( '../../packages/react-devtools-shared/src/__tests__/__serializers__/treeContextStateSerializer.js' ),