From dceff27374ba8d8d036e1ec171df2fa7e2e93a96 Mon Sep 17 00:00:00 2001 From: Brian Vaughn <bvaughn@fb.com> Date: Fri, 21 Jan 2022 15:54:54 -0500 Subject: [PATCH] Squashed PRs 23137, 23141, 23157, 23164 --- .../src/__tests__/TimelineProfiler-test.js | 1914 +++++----- .../__snapshots__/profilingCache-test.js.snap | 14 +- .../src/__tests__/preprocessData-test.js | 3401 +++++++++-------- .../src/__tests__/store-test.js | 8 +- .../src/backend/legacy/renderer.js | 2 +- .../src/backend/profilingHooks.js | 106 +- .../src/backend/renderer.js | 43 +- .../src/backend/types.js | 1 + .../react-devtools-shared/src/constants.js | 3 + .../src/devtools/store.js | 62 +- .../Profiler/ClearProfilingDataButton.js | 21 +- .../views/Profiler/CommitTreeBuilder.js | 2 +- .../views/Profiler/NoProfilingData.js | 35 + .../src/devtools/views/Profiler/Profiler.css | 12 + .../src/devtools/views/Profiler/Profiler.js | 37 +- .../views/Profiler/ProfilerContext.js | 6 +- .../views/Profiler/ProfilingNotSupported.js | 35 + .../src/devtools/views/Profiler/types.js | 2 + .../react-devtools-timeline/src/Timeline.js | 53 +- .../src/TimelineContext.js | 28 +- .../src/TimelineNotSupported.css | 38 + .../src/TimelineNotSupported.js | 52 + 22 files changed, 3064 insertions(+), 2811 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/Profiler/NoProfilingData.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingNotSupported.js create mode 100644 packages/react-devtools-timeline/src/TimelineNotSupported.css create mode 100644 packages/react-devtools-timeline/src/TimelineNotSupported.js diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js index 1a369acca27bd..2b1908d3c29d0 100644 --- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -18,1005 +18,766 @@ describe('Timeline profiler', () => { 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)}-<filtered-file-system-path>`; - } else if (markName.startsWith('--react-internal-module-stop')) { - return `${markName.substr(0, 28)}-<filtered-file-system-path>`; - } else if (markName.startsWith('--react-version')) { - return `${markName.substr(0, 15)}-<filtered-version>`; - } else { - return markName; + 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 `${markName.substr(0, 29)}-<filtered-file-system-path>`; + } else if (markName.startsWith('--react-internal-module-stop')) { + return `${markName.substr(0, 28)}-<filtered-file-system-path>`; + } else if (markName.startsWith('--react-version')) { + return `${markName.substr(0, 15)}-<filtered-version>`; + } 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++; + } + }, + }; } - // 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); + function clearPendingMarks() { + clearedMarks.splice(0); + } - clearedMarks.push(markName); - marks = marks.filter(mark => mark !== markName); - }, - mark(markName, markOptions) { - markName = filterMarkData(markName); + function dispatchAndSetCurrentEvent(element, event) { + try { + window.event = event; + element.dispatchEvent(event); + } finally { + window.event = undefined; + } + } - if (featureDetectionMarkName === null) { - featureDetectionMarkName = markName; - } + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + unmountFns = []; + renderHelper = element => { + const unmountFn = utils.legacyRender(element); + unmountFns.push(unmountFn); + return unmountFn; + }; + renderRootHelper = element => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container); + root.render(element); + const unmountFn = () => root.unmount(); + unmountFns.push(unmountFn); + return unmountFn; + }; + + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + + setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks') + .setPerformanceMock_ONLY_FOR_TESTING; + setPerformanceMock(createUserTimingPolyfill()); + }); - marks.push(markName); + afterEach(() => { + // Verify all logged marks also get cleared. + expect(marks).toHaveLength(0); - 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(); - - unmountFns = []; - renderHelper = element => { - const unmountFn = utils.legacyRender(element); - unmountFns.push(unmountFn); - return unmountFn; - }; - renderRootHelper = element => { - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - root.render(element); - const unmountFn = () => root.unmount(); - unmountFns.push(unmountFn); - return unmountFn; - }; - - React = require('react'); - ReactDOM = require('react-dom'); - Scheduler = require('scheduler'); - - setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks') - .setPerformanceMock_ONLY_FOR_TESTING; - setPerformanceMock(createUserTimingPolyfill()); - }); + unmountFns.forEach(unmountFn => unmountFn()); - afterEach(() => { - // Verify all logged marks also get cleared. - expect(marks).toHaveLength(0); + setPerformanceMock(null); + }); - unmountFns.forEach(unmountFn => unmountFn()); + describe('when profiling', () => { + beforeEach(() => { + const store = global.store; + utils.act(() => store.profilerStore.startProfiling()); - setPerformanceMock(null); - }); + // Clear inital metadata marks to make tests below less noisy. + clearPendingMarks(); + }); - it('should mark sync render without suspends or state updates', () => { - renderHelper(<div />); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-1", - "--render-start-1", - "--render-stop", - "--commit-start-1", - "--react-version-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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 without suspends or state updates', () => { + renderHelper(<div />); - it('should mark concurrent render without suspends or state updates', () => { - renderRootHelper(<div />); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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", + ] + `); + }); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); + it('should mark concurrent render without suspends or state updates', () => { + renderRootHelper(<div />); - clearPendingMarks(); - - expect(Scheduler).toFlushUntilNextPaint([]); - - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--render-start-16", - "--render-stop", - "--commit-start-16", - "--react-version-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); - }); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); - it('should mark render yields', async () => { - function Bar() { - Scheduler.unstable_yieldValue('Bar'); - return null; - } + clearPendingMarks(); - function Foo() { - Scheduler.unstable_yieldValue('Foo'); - return <Bar />; - } + expect(Scheduler).toFlushUntilNextPaint([]); - React.startTransition(() => { - renderRootHelper(<Foo />); - }); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--render-stop", + "--commit-start-16", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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", + ] + `); + }); - // 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", - ] - `); - }); + it('should mark render yields', async () => { + function Bar() { + Scheduler.unstable_yieldValue('Bar'); + return null; + } - it('should mark sync render with suspense that resolves', async () => { - const fakeSuspensePromise = Promise.resolve(true); - function Example() { - throw fakeSuspensePromise; - } + function Foo() { + Scheduler.unstable_yieldValue('Foo'); + return <Bar />; + } - renderHelper( - <React.Suspense fallback={null}> - <Example /> - </React.Suspense>, - ); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); + React.startTransition(() => { + renderRootHelper(<Foo />); + }); + + // 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", + ] + `); + }); + + it('should mark sync render with suspense that resolves', async () => { + const fakeSuspensePromise = Promise.resolve(true); + function Example() { + throw fakeSuspensePromise; + } - clearPendingMarks(); + renderHelper( + <React.Suspense fallback={null}> + <Example /> + </React.Suspense>, + ); - await fakeSuspensePromise; - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--suspense-resolved-0-Example", + "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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; - } + clearPendingMarks(); - renderHelper( - <React.Suspense fallback={null}> - <Example /> - </React.Suspense>, - ); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] + await fakeSuspensePromise; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] `); + }); - clearPendingMarks(); + it('should mark sync render with suspense that rejects', async () => { + const fakeSuspensePromise = Promise.reject(new Error('error')); + function Example() { + throw fakeSuspensePromise; + } - await expect(fakeSuspensePromise).rejects.toThrow(); - expect(clearedMarks).toContain(`--suspense-rejected-0-Example`); - }); + renderHelper( + <React.Suspense fallback={null}> + <Example /> + </React.Suspense>, + ); - 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-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1-", + "--render-stop", + "--commit-start-1", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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", + ] + `); - renderRootHelper( - <React.Suspense fallback={null}> - <Example /> - </React.Suspense>, - ); + clearPendingMarks(); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); + await expect(fakeSuspensePromise).rejects.toThrow(); + expect(clearedMarks).toContain(`--suspense-rejected-0-Example`); + }); - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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; + } - clearPendingMarks(); + renderRootHelper( + <React.Suspense fallback={null}> + <Example /> + </React.Suspense>, + ); - await fakeSuspensePromise; - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--suspense-resolved-0-Example", - ] - `); - }); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); - it('should mark concurrent render with suspense that rejects', async () => { - const fakeSuspensePromise = Promise.reject(new Error('error')); - function Example() { - throw fakeSuspensePromise; - } + clearPendingMarks(); - renderRootHelper( - <React.Suspense fallback={null}> - <Example /> - </React.Suspense>, - ); + expect(Scheduler).toFlushUntilNextPaint([]); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); + 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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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", - "--suspense-suspend-0-Example-mount-16-", - "--render-stop", - "--commit-start-16", - "--react-version-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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(); - clearPendingMarks(); + await fakeSuspensePromise; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); + }); - await expect(fakeSuspensePromise).rejects.toThrow(); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--suspense-rejected-0-Example", - ] - `); - }); + it('should mark concurrent render with suspense that rejects', async () => { + const fakeSuspensePromise = Promise.reject(new Error('error')); + function Example() { + throw fakeSuspensePromise; + } - it('should mark cascading class component state updates', () => { - class Example extends React.Component { - state = {didMount: false}; - componentDidMount() { - this.setState({didMount: true}); - } - render() { - return null; - } - } + renderRootHelper( + <React.Suspense fallback={null}> + <Example /> + </React.Suspense>, + ); - renderRootHelper(<Example />); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); + clearPendingMarks(); - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); - }); + expect(Scheduler).toFlushUntilNextPaint([]); - it('should mark cascading class component force updates', () => { - class Example extends React.Component { - componentDidMount() { - this.forceUpdate(); - } - render() { - return null; - } - } + 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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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(<Example />); + clearPendingMarks(); - expect(clearedMarks).toMatchInlineSnapshot(` + await expect(fakeSuspensePromise).rejects.toThrow(); + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-render-16", + "--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; + } + } - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); - }); + renderRootHelper(<Example />); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); - 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}); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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; + } } - return null; - } - } - renderRootHelper(<Example />); + renderRootHelper(<Example />); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ "--schedule-render-16", ] `); - clearPendingMarks(); + clearPendingMarks(); - let errorMessage; - spyOn(console, 'error').and.callFake(message => { - errorMessage = message; - }); + expect(Scheduler).toFlushUntilNextPaint([]); - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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})); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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 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; + } } - return null; - } - } - renderRootHelper(<Example />); + renderRootHelper(<Example />); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ "--schedule-render-16", ] `); - clearPendingMarks(); + clearPendingMarks(); - let errorMessage; - spyOn(console, 'error').and.callFake(message => { - errorMessage = message; - }); + 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-forced-update-16-Example", - "--component-render-stop", - "--render-stop", - "--commit-start-16", - "--react-version-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); - }); + expect(Scheduler).toFlushUntilNextPaint([]); - it('should mark cascading layout updates', () => { - function Example() { - const [didMount, setDidMount] = React.useState(false); - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - return didMount; - } + expect(console.error).toHaveBeenCalledTimes(1); + expect(errorMessage).toContain( + 'Cannot update during an existing state transition', + ); - renderRootHelper(<Example />); + 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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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; + } + } + + renderRootHelper(<Example />); - expect(clearedMarks).toMatchInlineSnapshot(` + 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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(); - // 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; - } + let errorMessage; + spyOn(console, 'error').and.callFake(message => { + errorMessage = message; + }); - renderRootHelper(<Example />); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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(Scheduler).toFlushUntilNextPaint([]); - it('should mark render phase updates', () => { - function Example() { - const [didRender, setDidRender] = React.useState(false); - if (!didRender) { - setDidRender(true); - } - return didRender; - } + expect(console.error).toHaveBeenCalledTimes(1); + expect(errorMessage).toContain( + 'Cannot update during an existing state transition', + ); - renderRootHelper(<Example />); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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 sync render that throws', async () => { - spyOn(console, 'error'); - - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { - return null; + 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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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 layout updates', () => { + function Example() { + const [didMount, setDidMount] = React.useState(false); + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + return didMount; } - return this.props.children; - } - } - function ExampleThatThrows() { - throw Error('Expected error'); - } + renderRootHelper(<Example />); - renderHelper( - <ErrorBoundary> - <ExampleThatThrows /> - </ErrorBoundary>, - ); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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 [ + "--schedule-render-16", + ] `); - }); - 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) { - return null; + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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; } - return this.props.children; - } - } - function ExampleThatThrows() { - // eslint-disable-next-line no-throw-literal - throw 'Expected error'; - } + renderRootHelper(<Example />); - renderRootHelper( - <ErrorBoundary> - <ExampleThatThrows /> - </ErrorBoundary>, - ); + expect(Scheduler).toFlushAndYield([]); - 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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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", ] - `); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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 render phase updates', () => { + function Example() { + const [didRender, setDidRender] = React.useState(false); + if (!didRender) { + setDidRender(true); + } + return didRender; + } - 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; - } + renderRootHelper(<Example />); - const unmount = renderRootHelper(<ComponentWithEffects />); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); + expect(Scheduler).toFlushAndYield([]); - 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", - ] - `); + 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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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 sync render that throws', async () => { + spyOn(console, 'error'); + + 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(); - - 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-<filtered-version>", - "--profiler-version-1", - "--react-internal-module-start-<filtered-file-system-path>", - "--react-internal-module-stop-<filtered-file-system-path>", - "--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", - ] - `); - }); + function ExampleThatThrows() { + throw Error('Expected error'); + } - describe('lane labels', () => { - it('regression test SyncLane', () => { - renderHelper(<div />); + renderHelper( + <ErrorBoundary> + <ExampleThatThrows /> + </ErrorBoundary>, + ); - expect(clearedMarks).toMatchInlineSnapshot(` + 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-<filtered-version>", @@ -1025,47 +786,90 @@ describe('Timeline profiler', () => { "--react-internal-module-stop-<filtered-file-system-path>", "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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", ] `); - }); - - it('regression test DefaultLane', () => { - renderRootHelper(<div />); - expect(clearedMarks).toMatchInlineSnapshot(` - Array [ - "--schedule-render-16", - ] - `); - }); - - it('regression test InputDiscreteLane', async () => { - const targetRef = React.createRef(null); + }); + + it('should mark concurrent render that throws', async () => { + spyOn(console, 'error'); + + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return null; + } + return this.props.children; + } + } - function App() { - const [count, setCount] = React.useState(0); - const handleClick = () => { - setCount(count + 1); - }; - return <button ref={targetRef} onClick={handleClick} />; - } + function ExampleThatThrows() { + // eslint-disable-next-line no-throw-literal + throw 'Expected error'; + } - renderRootHelper(<App />); - expect(Scheduler).toFlushAndYield([]); + renderRootHelper( + <ErrorBoundary> + <ExampleThatThrows /> + </ErrorBoundary>, + ); - clearedMarks.splice(0); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); - targetRef.current.click(); + clearPendingMarks(); - // Wait a frame, for React to process the "click" update. - await Promise.resolve(); + expect(Scheduler).toFlushUntilNextPaint([]); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-state-update-1-App", + "--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-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-App", + "--component-render-start-ErrorBoundary", "--component-render-stop", "--render-stop", "--commit-start-1", @@ -1074,51 +878,267 @@ describe('Timeline profiler', () => { "--react-internal-module-start-<filtered-file-system-path>", "--react-internal-module-stop-<filtered-file-system-path>", "--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 InputContinuousLane', async () => { - const targetRef = React.createRef(null); + }); + + 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'); + }; + }, []); - function App() { - const [count, setCount] = React.useState(0); - const handleMouseOver = () => setCount(count + 1); - return <div ref={targetRef} onMouseOver={handleMouseOver} />; - } - - renderRootHelper(<App />); - expect(Scheduler).toFlushAndYield([]); - - clearedMarks.splice(0); + return null; + } - const event = document.createEvent('MouseEvents'); - event.initEvent('mouseover', true, true); - dispatchAndSetCurrentEvent(targetRef.current, event); + const unmount = renderRootHelper(<ComponentWithEffects />); - expect(Scheduler).toFlushAndYield([]); + expect(Scheduler).toFlushUntilNextPaint([ + 'layout 1 mount', + 'layout 2 mount', + ]); - expect(clearedMarks).toMatchInlineSnapshot(` + expect(clearedMarks).toMatchInlineSnapshot(` Array [ - "--schedule-state-update-4-App", - "--render-start-4", - "--component-render-start-App", + "--schedule-render-16", + "--render-start-16", + "--component-render-start-ComponentWithEffects", "--component-render-stop", "--render-stop", - "--commit-start-4", + "--commit-start-16", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-<filtered-version>", "--profiler-version-1", "--react-internal-module-start-<filtered-file-system-path>", "--react-internal-module-stop-<filtered-file-system-path>", "--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-4", + "--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", ] `); + }); + + describe('lane labels', () => { + it('regression test SyncLane', () => { + renderHelper(<div />); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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('regression test DefaultLane', () => { + renderRootHelper(<div />); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + }); + + it('regression test InputDiscreteLane', async () => { + const targetRef = React.createRef(null); + + function App() { + const [count, setCount] = React.useState(0); + const handleClick = () => { + setCount(count + 1); + }; + return <button ref={targetRef} onClick={handleClick} />; + } + + renderRootHelper(<App />); + expect(Scheduler).toFlushAndYield([]); + + clearedMarks.splice(0); + + targetRef.current.click(); + + // Wait a frame, for React to process the "click" update. + await Promise.resolve(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-state-update-1-App", + "--render-start-1", + "--component-render-start-App", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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('regression test InputContinuousLane', async () => { + const targetRef = React.createRef(null); + + function App() { + const [count, setCount] = React.useState(0); + const handleMouseOver = () => setCount(count + 1); + return <div ref={targetRef} onMouseOver={handleMouseOver} />; + } + + renderRootHelper(<App />); + expect(Scheduler).toFlushAndYield([]); + + clearedMarks.splice(0); + + const event = document.createEvent('MouseEvents'); + event.initEvent('mouseover', true, true); + dispatchAndSetCurrentEvent(targetRef.current, event); + + expect(Scheduler).toFlushAndYield([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-state-update-4-App", + "--render-start-4", + "--component-render-start-App", + "--component-render-stop", + "--render-stop", + "--commit-start-4", + "--react-version-<filtered-version>", + "--profiler-version-1", + "--react-internal-module-start-<filtered-file-system-path>", + "--react-internal-module-stop-<filtered-file-system-path>", + "--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-4", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + }); + }); + + describe('when not profiling', () => { + it('should not log any marks', () => { + renderHelper(<div />); + + expect(clearedMarks).toMatchInlineSnapshot(`Array []`); + }); }); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index dd4717bb5cb6b..450d176f0ac55 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -720,7 +720,7 @@ Object { 1, 11, 0, - 1, + 3, 1, 1, 4, @@ -1186,7 +1186,7 @@ Object { 1, 11, 0, - 1, + 3, 1, 1, 4, @@ -1663,7 +1663,7 @@ Object { 13, 11, 0, - 1, + 3, 1, 1, 4, @@ -2209,7 +2209,7 @@ Object { 13, 11, 0, - 1, + 3, 1, 1, 4, @@ -2304,7 +2304,7 @@ Object { 1, 11, 0, - 1, + 3, 1, 1, 1, @@ -2954,7 +2954,7 @@ Object { 1, 11, 0, - 1, + 3, 1, 1, 1, @@ -4227,7 +4227,7 @@ Object { 1, 11, 0, - 1, + 3, 1, 1, 1, diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 09ad50ecae49e..d244858b70bb7 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -15,1915 +15,1926 @@ describe('Timeline profiler', () => { let Scheduler; 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)}-<filtered-file-system-path>`; - } else if (markName.startsWith('--react-internal-module-stop')) { - return `${markName.substr(0, 28)}-<filtered-file-system-path>`; - } else if (markName.startsWith('--react-version')) { - return `${markName.substr(0, 15)}-0.0.0`; - } 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++; + 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 `${markName.substr(0, 29)}-<filtered-file-system-path>`; + } else if (markName.startsWith('--react-internal-module-stop')) { + return `${markName.substr(0, 28)}-<filtered-file-system-path>`; + } else if (markName.startsWith('--react-version')) { + return `${markName.substr(0, 15)}-0.0.0`; + } else { + return markName; } - }, - }; - } - - function clearPendingMarks() { - clearedMarks.splice(0); - } - - beforeEach(() => { - utils = require('./utils'); - utils.beforeEachProfiling(); + } - React = require('react'); - ReactDOM = require('react-dom'); - Scheduler = require('scheduler'); + // 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); - setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks') - .setPerformanceMock_ONLY_FOR_TESTING; - setPerformanceMock(createUserTimingPolyfill()); + clearedMarks.push(markName); + marks = marks.filter(mark => mark !== markName); + }, + mark(markName, markOptions) { + markName = filterMarkData(markName); - global.IS_REACT_ACT_ENVIRONMENT = true; - }); + if (featureDetectionMarkName === null) { + featureDetectionMarkName = markName; + } - afterEach(() => { - // Verify all logged marks also get cleared. - expect(marks).toHaveLength(0); + marks.push(markName); - setPerformanceMock(null); - }); + if (markOptions != null) { + // This is triggers the feature detection. + markOptions.startTime++; + } + }, + }; + } - describe('getLanesFromTransportDecimalBitmask', () => { - let getLanesFromTransportDecimalBitmask; + function clearPendingMarks() { + clearedMarks.splice(0); + } beforeEach(() => { - getLanesFromTransportDecimalBitmask = require('react-devtools-timeline/src/import-worker/preprocessData') - .getLanesFromTransportDecimalBitmask; - }); + utils = require('./utils'); + utils.beforeEachProfiling(); - it('should return array of lane numbers from bitmask string', () => { - expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]); - expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]); - expect(getLanesFromTransportDecimalBitmask('3')).toEqual([0, 1]); - expect(getLanesFromTransportDecimalBitmask('1234')).toEqual([ - 1, - 4, - 6, - 7, - 10, - ]); // 2 + 16 + 64 + 128 + 1024 - expect( - getLanesFromTransportDecimalBitmask('1073741824'), // 0b1000000000000000000000000000000 - ).toEqual([30]); - expect( - getLanesFromTransportDecimalBitmask('2147483647'), // 0b1111111111111111111111111111111 - ).toEqual(Array.from(Array(31).keys())); - }); + React = require('react'); + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); - it('should return empty array if laneBitmaskString is not a bitmask', () => { - expect(getLanesFromTransportDecimalBitmask('')).toEqual([]); - expect(getLanesFromTransportDecimalBitmask('hello')).toEqual([]); - expect(getLanesFromTransportDecimalBitmask('-1')).toEqual([]); - expect(getLanesFromTransportDecimalBitmask('-0')).toEqual([]); - }); + setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks') + .setPerformanceMock_ONLY_FOR_TESTING; + setPerformanceMock(createUserTimingPolyfill()); - it('should ignore lanes outside REACT_TOTAL_NUM_LANES', () => { - const REACT_TOTAL_NUM_LANES = require('react-devtools-timeline/src/constants') - .REACT_TOTAL_NUM_LANES; + const store = global.store; - // Sanity check; this test may need to be updated when the no. of fiber lanes are changed. - expect(REACT_TOTAL_NUM_LANES).toBe(31); + // Start profiling so that data will actually be recorded. + utils.act(() => store.profilerStore.startProfiling()); - expect( - getLanesFromTransportDecimalBitmask( - '4294967297', // 2^32 + 1 - ), - ).toEqual([0]); + global.IS_REACT_ACT_ENVIRONMENT = true; }); - }); - describe('preprocessData', () => { - let preprocessData; + afterEach(() => { + // Verify all logged marks also get cleared. + expect(marks).toHaveLength(0); - beforeEach(() => { - preprocessData = require('react-devtools-timeline/src/import-worker/preprocessData') - .default; + setPerformanceMock(null); }); - // These should be dynamic to mimic a real profile, - // but reprooducible between test runs. - let pid = 0; - let tid = 0; - let startTime = 0; + describe('getLanesFromTransportDecimalBitmask', () => { + let getLanesFromTransportDecimalBitmask; - function createUserTimingEntry(data) { - return { - pid: ++pid, - tid: ++tid, - ts: ++startTime, - ...data, - }; - } - - function createProfilerVersionEntry() { - const SCHEDULING_PROFILER_VERSION = require('react-devtools-timeline/src/constants') - .SCHEDULING_PROFILER_VERSION; - return createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--profiler-version-' + SCHEDULING_PROFILER_VERSION, + beforeEach(() => { + getLanesFromTransportDecimalBitmask = require('react-devtools-timeline/src/import-worker/preprocessData') + .getLanesFromTransportDecimalBitmask; }); - } - function createReactVersionEntry() { - return createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--react-version-0.0.0', + it('should return array of lane numbers from bitmask string', () => { + expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]); + expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]); + expect(getLanesFromTransportDecimalBitmask('3')).toEqual([0, 1]); + expect(getLanesFromTransportDecimalBitmask('1234')).toEqual([ + 1, + 4, + 6, + 7, + 10, + ]); // 2 + 16 + 64 + 128 + 1024 + expect( + getLanesFromTransportDecimalBitmask('1073741824'), // 0b1000000000000000000000000000000 + ).toEqual([30]); + expect( + getLanesFromTransportDecimalBitmask('2147483647'), // 0b1111111111111111111111111111111 + ).toEqual(Array.from(Array(31).keys())); }); - } - function createLaneLabelsEntry() { - return createUserTimingEntry({ - cat: 'blink.user_timing', - name: - '--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', + it('should return empty array if laneBitmaskString is not a bitmask', () => { + expect(getLanesFromTransportDecimalBitmask('')).toEqual([]); + expect(getLanesFromTransportDecimalBitmask('hello')).toEqual([]); + expect(getLanesFromTransportDecimalBitmask('-1')).toEqual([]); + expect(getLanesFromTransportDecimalBitmask('-0')).toEqual([]); }); - } - function createNativeEventEntry(type, duration) { - return createUserTimingEntry({ - cat: 'devtools.timeline', - name: 'EventDispatch', - args: {data: {type}}, - dur: duration, - tdur: duration, - }); - } + it('should ignore lanes outside REACT_TOTAL_NUM_LANES', () => { + const REACT_TOTAL_NUM_LANES = require('react-devtools-timeline/src/constants') + .REACT_TOTAL_NUM_LANES; - function creactCpuProfilerSample() { - return createUserTimingEntry({ - args: {data: {startTime: ++startTime}}, - cat: 'disabled-by-default-v8.cpu_profiler', - id: '0x1', - name: 'Profile', - ph: 'P', - }); - } + // Sanity check; this test may need to be updated when the no. of fiber lanes are changed. + expect(REACT_TOTAL_NUM_LANES).toBe(31); - function createBoilerplateEntries() { - return [ - createProfilerVersionEntry(), - createReactVersionEntry(), - createLaneLabelsEntry(), - ]; - } + expect( + getLanesFromTransportDecimalBitmask( + '4294967297', // 2^32 + 1 + ), + ).toEqual([0]); + }); + }); - function createUserTimingData(sampleMarks) { - const cpuProfilerSample = creactCpuProfilerSample(); + describe('preprocessData', () => { + let preprocessData; - const randomSample = createUserTimingEntry({ - dur: 100, - tdur: 200, - ph: 'X', - cat: 'disabled-by-default-devtools.timeline', - name: 'RunTask', - args: {}, + beforeEach(() => { + preprocessData = require('react-devtools-timeline/src/import-worker/preprocessData') + .default; }); - const userTimingData = [cpuProfilerSample, randomSample]; + // These should be dynamic to mimic a real profile, + // but reprooducible between test runs. + let pid = 0; + let tid = 0; + let startTime = 0; - sampleMarks.forEach(markName => { - userTimingData.push({ + function createUserTimingEntry(data) { + return { pid: ++pid, tid: ++tid, ts: ++startTime, - args: {data: {}}, + ...data, + }; + } + + function createProfilerVersionEntry() { + const SCHEDULING_PROFILER_VERSION = require('react-devtools-timeline/src/constants') + .SCHEDULING_PROFILER_VERSION; + return createUserTimingEntry({ cat: 'blink.user_timing', - name: markName, - ph: 'R', + name: '--profiler-version-' + SCHEDULING_PROFILER_VERSION, }); - }); + } - return userTimingData; - } + function createReactVersionEntry() { + return createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--react-version-0.0.0', + }); + } - beforeEach(() => { - tid = 0; - pid = 0; - startTime = 0; - }); + function createLaneLabelsEntry() { + return createUserTimingEntry({ + cat: 'blink.user_timing', + name: + '--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', + }); + } - it('should throw given an empty timeline', async () => { - await expect(async () => preprocessData([])).rejects.toThrow(); - }); + function createNativeEventEntry(type, duration) { + return createUserTimingEntry({ + cat: 'devtools.timeline', + name: 'EventDispatch', + args: {data: {type}}, + dur: duration, + tdur: duration, + }); + } - it('should throw given a timeline with no Profile event', async () => { - const randomSample = createUserTimingEntry({ - dur: 100, - tdur: 200, - ph: 'X', - cat: 'disabled-by-default-devtools.timeline', - name: 'RunTask', - args: {}, - }); + function creactCpuProfilerSample() { + return createUserTimingEntry({ + args: {data: {startTime: ++startTime}}, + cat: 'disabled-by-default-v8.cpu_profiler', + id: '0x1', + name: 'Profile', + ph: 'P', + }); + } - await expect(async () => - preprocessData([randomSample]), - ).rejects.toThrow(); - }); + function createBoilerplateEntries() { + return [ + createProfilerVersionEntry(), + createReactVersionEntry(), + createLaneLabelsEntry(), + ]; + } - it('should throw given a timeline without an explicit profiler version mark nor any other React marks', async () => { - const cpuProfilerSample = creactCpuProfilerSample(); + function createUserTimingData(sampleMarks) { + const cpuProfilerSample = creactCpuProfilerSample(); - await expect( - async () => await preprocessData([cpuProfilerSample]), - ).rejects.toThrow( - 'Please provide profiling data from an React application', - ); - }); + const randomSample = createUserTimingEntry({ + dur: 100, + tdur: 200, + ph: 'X', + cat: 'disabled-by-default-devtools.timeline', + name: 'RunTask', + args: {}, + }); + + const userTimingData = [cpuProfilerSample, randomSample]; + + sampleMarks.forEach(markName => { + userTimingData.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); + }); - it('should throw given a timeline with React scheduling marks, but without an explicit profiler version mark', async () => { - const cpuProfilerSample = creactCpuProfilerSample(); - const scheduleRenderSample = createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--schedule-render-512-', + return userTimingData; + } + + beforeEach(() => { + tid = 0; + pid = 0; + startTime = 0; }); - const samples = [cpuProfilerSample, scheduleRenderSample]; - await expect(async () => await preprocessData(samples)).rejects.toThrow( - 'This version of profiling data is not supported', - ); - }); + it('should throw given an empty timeline', async () => { + await expect(async () => preprocessData([])).rejects.toThrow(); + }); + + it('should throw given a timeline with no Profile event', async () => { + const randomSample = createUserTimingEntry({ + dur: 100, + tdur: 200, + ph: 'X', + cat: 'disabled-by-default-devtools.timeline', + name: 'RunTask', + args: {}, + }); - it('should return empty data given a timeline with no React scheduling profiling marks', async () => { - const cpuProfilerSample = creactCpuProfilerSample(); - const randomSample = createUserTimingEntry({ - dur: 100, - tdur: 200, - ph: 'X', - cat: 'disabled-by-default-devtools.timeline', - name: 'RunTask', - args: {}, + await expect(async () => + preprocessData([randomSample]), + ).rejects.toThrow(); }); - const data = await preprocessData([ - ...createBoilerplateEntries(), - cpuProfilerSample, - randomSample, - ]); - expect(data).toMatchInlineSnapshot(` - Object { - "batchUIDToMeasuresMap": Map {}, - "componentMeasures": Array [], - "duration": 0.005, - "flamechart": Array [], - "internalModuleSourceToRanges": Map {}, - "laneToLabelMap": Map { - 0 => "Sync", - 1 => "InputContinuousHydration", - 2 => "InputContinuous", - 3 => "DefaultHydration", - 4 => "Default", - 5 => "TransitionHydration", - 6 => "Transition", - 7 => "Transition", - 8 => "Transition", - 9 => "Transition", - 10 => "Transition", - 11 => "Transition", - 12 => "Transition", - 13 => "Transition", - 14 => "Transition", - 15 => "Transition", - 16 => "Transition", - 17 => "Transition", - 18 => "Transition", - 19 => "Transition", - 20 => "Transition", - 21 => "Transition", - 22 => "Retry", - 23 => "Retry", - 24 => "Retry", - 25 => "Retry", - 26 => "Retry", - 27 => "SelectiveHydration", - 28 => "IdleHydration", - 29 => "Idle", - 30 => "Offscreen", - }, - "laneToReactMeasureMap": Map { - 0 => Array [], - 1 => Array [], - 2 => Array [], - 3 => Array [], - 4 => Array [], - 5 => Array [], - 6 => Array [], - 7 => Array [], - 8 => Array [], - 9 => Array [], - 10 => Array [], - 11 => Array [], - 12 => Array [], - 13 => Array [], - 14 => Array [], - 15 => Array [], - 16 => Array [], - 17 => Array [], - 18 => Array [], - 19 => Array [], - 20 => Array [], - 21 => Array [], - 22 => Array [], - 23 => Array [], - 24 => Array [], - 25 => Array [], - 26 => Array [], - 27 => Array [], - 28 => Array [], - 29 => Array [], - 30 => Array [], - }, - "nativeEvents": Array [], - "networkMeasures": Array [], - "otherUserTimingMarks": Array [], - "reactVersion": "0.0.0", - "schedulingEvents": Array [], - "snapshotHeight": 0, - "snapshots": Array [], - "startTime": 1, - "suspenseEvents": Array [], - "thrownErrors": Array [], - } - `); - }); + it('should throw given a timeline without an explicit profiler version mark nor any other React marks', async () => { + const cpuProfilerSample = creactCpuProfilerSample(); - it('should process legacy data format (before lane labels were added)', async () => { - const cpuProfilerSample = creactCpuProfilerSample(); + await expect( + async () => await preprocessData([cpuProfilerSample]), + ).rejects.toThrow( + 'Please provide profiling data from an React application', + ); + }); - // Data below is hard-coded based on an older profile sample. - // Should be fine since this is explicitly a legacy-format test. - const data = await preprocessData([ - ...createBoilerplateEntries(), - cpuProfilerSample, - createUserTimingEntry({ + it('should throw given a timeline with React scheduling marks, but without an explicit profiler version mark', async () => { + const cpuProfilerSample = creactCpuProfilerSample(); + const scheduleRenderSample = createUserTimingEntry({ cat: 'blink.user_timing', name: '--schedule-render-512-', - }), - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--render-start-512', - }), - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--render-stop', - }), - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--commit-start-512', - }), - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--layout-effects-start-512', - }), - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--layout-effects-stop', - }), - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--commit-stop', - }), - ]); - expect(data).toMatchInlineSnapshot(` - Object { - "batchUIDToMeasuresMap": Map { - 0 => Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.005, - "lanes": Array [ - 9, - ], - "timestamp": 0.006, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.001, - "lanes": Array [ - 9, - ], - "timestamp": 0.006, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 9, - ], - "timestamp": 0.008, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 9, - ], - "timestamp": 0.009, - "type": "layout-effects", - }, - ], - }, - "componentMeasures": Array [], - "duration": 0.011, - "flamechart": Array [], - "internalModuleSourceToRanges": Map {}, - "laneToLabelMap": Map { - 0 => "Sync", - 1 => "InputContinuousHydration", - 2 => "InputContinuous", - 3 => "DefaultHydration", - 4 => "Default", - 5 => "TransitionHydration", - 6 => "Transition", - 7 => "Transition", - 8 => "Transition", - 9 => "Transition", - 10 => "Transition", - 11 => "Transition", - 12 => "Transition", - 13 => "Transition", - 14 => "Transition", - 15 => "Transition", - 16 => "Transition", - 17 => "Transition", - 18 => "Transition", - 19 => "Transition", - 20 => "Transition", - 21 => "Transition", - 22 => "Retry", - 23 => "Retry", - 24 => "Retry", - 25 => "Retry", - 26 => "Retry", - 27 => "SelectiveHydration", - 28 => "IdleHydration", - 29 => "Idle", - 30 => "Offscreen", - }, - "laneToReactMeasureMap": Map { - 0 => Array [], - 1 => Array [], - 2 => Array [], - 3 => Array [], - 4 => Array [], - 5 => Array [], - 6 => Array [], - 7 => Array [], - 8 => Array [], - 9 => Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.005, - "lanes": Array [ - 9, - ], - "timestamp": 0.006, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.001, - "lanes": Array [ - 9, - ], - "timestamp": 0.006, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 9, - ], - "timestamp": 0.008, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 9, - ], - "timestamp": 0.009, - "type": "layout-effects", - }, - ], - 10 => Array [], - 11 => Array [], - 12 => Array [], - 13 => Array [], - 14 => Array [], - 15 => Array [], - 16 => Array [], - 17 => Array [], - 18 => Array [], - 19 => Array [], - 20 => Array [], - 21 => Array [], - 22 => Array [], - 23 => Array [], - 24 => Array [], - 25 => Array [], - 26 => Array [], - 27 => Array [], - 28 => Array [], - 29 => Array [], - 30 => Array [], - }, - "nativeEvents": Array [], - "networkMeasures": Array [], - "otherUserTimingMarks": Array [], - "reactVersion": "0.0.0", - "schedulingEvents": Array [ - Object { - "lanes": Array [ - 9, - ], - "timestamp": 0.005, - "type": "schedule-render", - "warning": null, - }, - ], - "snapshotHeight": 0, - "snapshots": Array [], - "startTime": 1, - "suspenseEvents": Array [], - "thrownErrors": Array [], - } - `); - }); + }); + const samples = [cpuProfilerSample, scheduleRenderSample]; + + await expect(async () => await preprocessData(samples)).rejects.toThrow( + 'This version of profiling data is not supported', + ); + }); + + it('should return empty data given a timeline with no React scheduling profiling marks', async () => { + const cpuProfilerSample = creactCpuProfilerSample(); + const randomSample = createUserTimingEntry({ + dur: 100, + tdur: 200, + ph: 'X', + cat: 'disabled-by-default-devtools.timeline', + name: 'RunTask', + args: {}, + }); + + const data = await preprocessData([ + ...createBoilerplateEntries(), + cpuProfilerSample, + randomSample, + ]); + expect(data).toMatchInlineSnapshot(` + Object { + "batchUIDToMeasuresMap": Map {}, + "componentMeasures": Array [], + "duration": 0.005, + "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, + "laneToLabelMap": Map { + 0 => "Sync", + 1 => "InputContinuousHydration", + 2 => "InputContinuous", + 3 => "DefaultHydration", + 4 => "Default", + 5 => "TransitionHydration", + 6 => "Transition", + 7 => "Transition", + 8 => "Transition", + 9 => "Transition", + 10 => "Transition", + 11 => "Transition", + 12 => "Transition", + 13 => "Transition", + 14 => "Transition", + 15 => "Transition", + 16 => "Transition", + 17 => "Transition", + 18 => "Transition", + 19 => "Transition", + 20 => "Transition", + 21 => "Transition", + 22 => "Retry", + 23 => "Retry", + 24 => "Retry", + 25 => "Retry", + 26 => "Retry", + 27 => "SelectiveHydration", + 28 => "IdleHydration", + 29 => "Idle", + 30 => "Offscreen", + }, + "laneToReactMeasureMap": Map { + 0 => Array [], + 1 => Array [], + 2 => Array [], + 3 => Array [], + 4 => Array [], + 5 => Array [], + 6 => Array [], + 7 => Array [], + 8 => Array [], + 9 => Array [], + 10 => Array [], + 11 => Array [], + 12 => Array [], + 13 => Array [], + 14 => Array [], + 15 => Array [], + 16 => Array [], + 17 => Array [], + 18 => Array [], + 19 => Array [], + 20 => Array [], + 21 => Array [], + 22 => Array [], + 23 => Array [], + 24 => Array [], + 25 => Array [], + 26 => Array [], + 27 => Array [], + 28 => Array [], + 29 => Array [], + 30 => Array [], + }, + "nativeEvents": Array [], + "networkMeasures": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "0.0.0", + "schedulingEvents": Array [], + "snapshotHeight": 0, + "snapshots": Array [], + "startTime": 1, + "suspenseEvents": Array [], + "thrownErrors": Array [], + } + `); + }); - it('should process a sample legacy render sequence', async () => { - utils.legacyRender(<div />, document.createElement('div')); - - const data = await preprocessData([ - ...createBoilerplateEntries(), - ...createUserTimingData(clearedMarks), - ]); - expect(data).toMatchInlineSnapshot(` - Object { - "batchUIDToMeasuresMap": Map { - 0 => Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.01, - "lanes": Array [ - 0, + it('should process legacy data format (before lane labels were added)', async () => { + const cpuProfilerSample = creactCpuProfilerSample(); + + // Data below is hard-coded based on an older profile sample. + // Should be fine since this is explicitly a legacy-format test. + const data = await preprocessData([ + ...createBoilerplateEntries(), + cpuProfilerSample, + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--schedule-render-512-', + }), + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--render-start-512', + }), + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--render-stop', + }), + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--commit-start-512', + }), + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--layout-effects-start-512', + }), + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--layout-effects-stop', + }), + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--commit-stop', + }), + ]); + expect(data).toMatchInlineSnapshot(` + Object { + "batchUIDToMeasuresMap": Map { + 0 => Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.005, + "lanes": Array [ + 9, + ], + "timestamp": 0.006, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.001, + "lanes": Array [ + 9, + ], + "timestamp": 0.006, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 9, + ], + "timestamp": 0.008, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 9, + ], + "timestamp": 0.009, + "type": "layout-effects", + }, + ], + }, + "componentMeasures": Array [], + "duration": 0.011, + "flamechart": Array [], + "internalModuleSourceToRanges": Map {}, + "laneToLabelMap": Map { + 0 => "Sync", + 1 => "InputContinuousHydration", + 2 => "InputContinuous", + 3 => "DefaultHydration", + 4 => "Default", + 5 => "TransitionHydration", + 6 => "Transition", + 7 => "Transition", + 8 => "Transition", + 9 => "Transition", + 10 => "Transition", + 11 => "Transition", + 12 => "Transition", + 13 => "Transition", + 14 => "Transition", + 15 => "Transition", + 16 => "Transition", + 17 => "Transition", + 18 => "Transition", + 19 => "Transition", + 20 => "Transition", + 21 => "Transition", + 22 => "Retry", + 23 => "Retry", + 24 => "Retry", + 25 => "Retry", + 26 => "Retry", + 27 => "SelectiveHydration", + 28 => "IdleHydration", + 29 => "Idle", + 30 => "Offscreen", + }, + "laneToReactMeasureMap": Map { + 0 => Array [], + 1 => Array [], + 2 => Array [], + 3 => Array [], + 4 => Array [], + 5 => Array [], + 6 => Array [], + 7 => Array [], + 8 => Array [], + 9 => Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.005, + "lanes": Array [ + 9, + ], + "timestamp": 0.006, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.001, + "lanes": Array [ + 9, + ], + "timestamp": 0.006, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 9, + ], + "timestamp": 0.008, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 9, + ], + "timestamp": 0.009, + "type": "layout-effects", + }, + ], + 10 => Array [], + 11 => Array [], + 12 => Array [], + 13 => Array [], + 14 => Array [], + 15 => Array [], + 16 => Array [], + 17 => Array [], + 18 => Array [], + 19 => Array [], + 20 => Array [], + 21 => Array [], + 22 => Array [], + 23 => Array [], + 24 => Array [], + 25 => Array [], + 26 => Array [], + 27 => Array [], + 28 => Array [], + 29 => Array [], + 30 => Array [], + }, + "nativeEvents": Array [], + "networkMeasures": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "0.0.0", + "schedulingEvents": Array [ + Object { + "lanes": Array [ + 9, + ], + "timestamp": 0.005, + "type": "schedule-render", + "warning": null, + }, + ], + "snapshotHeight": 0, + "snapshots": Array [], + "startTime": 1, + "suspenseEvents": Array [], + "thrownErrors": Array [], + } + `); + }); + + it('should process a sample legacy render sequence', async () => { + utils.legacyRender(<div />, document.createElement('div')); + + const data = await preprocessData([ + ...createBoilerplateEntries(), + ...createUserTimingData(clearedMarks), + ]); + expect(data).toMatchInlineSnapshot(` + Object { + "batchUIDToMeasuresMap": Map { + 0 => Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.01, + "lanes": Array [ + 0, + ], + "timestamp": 0.004, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.001, + "lanes": Array [ + 0, + ], + "timestamp": 0.004, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 0, + ], + "timestamp": 0.006, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 0, + ], + "timestamp": 0.012, + "type": "layout-effects", + }, ], - "timestamp": 0.004, - "type": "render-idle", }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.001, - "lanes": Array [ - 0, + "componentMeasures": Array [], + "duration": 0.014, + "flamechart": Array [], + "internalModuleSourceToRanges": Map { + undefined => Array [ + Array [ + Object { + "functionName": "<filtered-file-system-path>", + }, + Object { + "functionName": "dule-stop-<filtered-file-system-path>", + }, + ], ], - "timestamp": 0.004, - "type": "render", }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 0, - ], - "timestamp": 0.006, - "type": "commit", + "laneToLabelMap": Map { + 0 => "Sync", + 1 => "InputContinuousHydration", + 2 => "InputContinuous", + 3 => "DefaultHydration", + 4 => "Default", + 5 => "TransitionHydration", + 6 => "Transition", + 7 => "Transition", + 8 => "Transition", + 9 => "Transition", + 10 => "Transition", + 11 => "Transition", + 12 => "Transition", + 13 => "Transition", + 14 => "Transition", + 15 => "Transition", + 16 => "Transition", + 17 => "Transition", + 18 => "Transition", + 19 => "Transition", + 20 => "Transition", + 21 => "Transition", + 22 => "Retry", + 23 => "Retry", + 24 => "Retry", + 25 => "Retry", + 26 => "Retry", + 27 => "SelectiveHydration", + 28 => "IdleHydration", + 29 => "Idle", + 30 => "Offscreen", }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 0, + "laneToReactMeasureMap": Map { + 0 => Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.01, + "lanes": Array [ + 0, + ], + "timestamp": 0.004, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.001, + "lanes": Array [ + 0, + ], + "timestamp": 0.004, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 0, + ], + "timestamp": 0.006, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 0, + ], + "timestamp": 0.012, + "type": "layout-effects", + }, ], - "timestamp": 0.012, - "type": "layout-effects", + 1 => Array [], + 2 => Array [], + 3 => Array [], + 4 => Array [], + 5 => Array [], + 6 => Array [], + 7 => Array [], + 8 => Array [], + 9 => Array [], + 10 => Array [], + 11 => Array [], + 12 => Array [], + 13 => Array [], + 14 => Array [], + 15 => Array [], + 16 => Array [], + 17 => Array [], + 18 => Array [], + 19 => Array [], + 20 => Array [], + 21 => Array [], + 22 => Array [], + 23 => Array [], + 24 => Array [], + 25 => Array [], + 26 => Array [], + 27 => Array [], + 28 => Array [], + 29 => Array [], + 30 => Array [], }, - ], - }, - "componentMeasures": Array [], - "duration": 0.014, - "flamechart": Array [], - "internalModuleSourceToRanges": Map { - undefined => Array [ - Array [ - Object { - "functionName": "<filtered-file-system-path>", - }, + "nativeEvents": Array [], + "networkMeasures": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "0.0.0", + "schedulingEvents": Array [ Object { - "functionName": "dule-stop-<filtered-file-system-path>", + "lanes": Array [ + 0, + ], + "timestamp": 0.003, + "type": "schedule-render", + "warning": null, }, ], - ], - }, - "laneToLabelMap": Map { - 0 => "Sync", - 1 => "InputContinuousHydration", - 2 => "InputContinuous", - 3 => "DefaultHydration", - 4 => "Default", - 5 => "TransitionHydration", - 6 => "Transition", - 7 => "Transition", - 8 => "Transition", - 9 => "Transition", - 10 => "Transition", - 11 => "Transition", - 12 => "Transition", - 13 => "Transition", - 14 => "Transition", - 15 => "Transition", - 16 => "Transition", - 17 => "Transition", - 18 => "Transition", - 19 => "Transition", - 20 => "Transition", - 21 => "Transition", - 22 => "Retry", - 23 => "Retry", - 24 => "Retry", - 25 => "Retry", - 26 => "Retry", - 27 => "SelectiveHydration", - 28 => "IdleHydration", - 29 => "Idle", - 30 => "Offscreen", - }, - "laneToReactMeasureMap": Map { - 0 => Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.01, - "lanes": Array [ - 0, - ], - "timestamp": 0.004, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.001, - "lanes": Array [ - 0, - ], - "timestamp": 0.004, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 0, - ], - "timestamp": 0.006, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 0, - ], - "timestamp": 0.012, - "type": "layout-effects", - }, - ], - 1 => Array [], - 2 => Array [], - 3 => Array [], - 4 => Array [], - 5 => Array [], - 6 => Array [], - 7 => Array [], - 8 => Array [], - 9 => Array [], - 10 => Array [], - 11 => Array [], - 12 => Array [], - 13 => Array [], - 14 => Array [], - 15 => Array [], - 16 => Array [], - 17 => Array [], - 18 => Array [], - 19 => Array [], - 20 => Array [], - 21 => Array [], - 22 => Array [], - 23 => Array [], - 24 => Array [], - 25 => Array [], - 26 => Array [], - 27 => Array [], - 28 => Array [], - 29 => Array [], - 30 => Array [], - }, - "nativeEvents": Array [], - "networkMeasures": Array [], - "otherUserTimingMarks": Array [], - "reactVersion": "0.0.0", - "schedulingEvents": Array [ - Object { - "lanes": Array [ - 0, - ], - "timestamp": 0.003, - "type": "schedule-render", - "warning": null, - }, - ], - "snapshotHeight": 0, - "snapshots": Array [], - "startTime": 4, - "suspenseEvents": Array [], - "thrownErrors": Array [], - } - `); - }); - - it('should process a sample createRoot render sequence', async () => { - function App() { - const [didMount, setDidMount] = React.useState(false); - React.useEffect(() => { - if (!didMount) { - setDidMount(true); + "snapshotHeight": 0, + "snapshots": Array [], + "startTime": 4, + "suspenseEvents": Array [], + "thrownErrors": Array [], } - }); - return true; - } + `); + }); - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => root.render(<App />)); - - const data = await preprocessData([ - ...createBoilerplateEntries(), - ...createUserTimingData(clearedMarks), - ]); - expect(data).toMatchInlineSnapshot(` - Object { - "batchUIDToMeasuresMap": Map { - 0 => Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.012, - "lanes": Array [ - 4, - ], - "timestamp": 0.004, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, - ], - "timestamp": 0.004, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 4, - ], - "timestamp": 0.008, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 4, - ], - "timestamp": 0.014, - "type": "layout-effects", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.004, - "lanes": Array [ - 4, - ], - "timestamp": 0.017, - "type": "passive-effects", - }, - ], - 1 => Array [ - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.012, - "lanes": Array [ - 4, - ], - "timestamp": 0.022, - "type": "render-idle", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, - ], - "timestamp": 0.022, - "type": "render", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 4, - ], - "timestamp": 0.026, - "type": "commit", - }, - Object { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 4, + it('should process a sample createRoot render sequence', async () => { + function App() { + const [didMount, setDidMount] = React.useState(false); + React.useEffect(() => { + if (!didMount) { + setDidMount(true); + } + }); + return true; + } + + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => root.render(<App />)); + + const data = await preprocessData([ + ...createBoilerplateEntries(), + ...createUserTimingData(clearedMarks), + ]); + expect(data).toMatchInlineSnapshot(` + Object { + "batchUIDToMeasuresMap": Map { + 0 => Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.012, + "lanes": Array [ + 4, + ], + "timestamp": 0.004, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.004, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 4, + ], + "timestamp": 0.008, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 4, + ], + "timestamp": 0.014, + "type": "layout-effects", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.004, + "lanes": Array [ + 4, + ], + "timestamp": 0.017, + "type": "passive-effects", + }, ], - "timestamp": 0.032, - "type": "layout-effects", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, + 1 => Array [ + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.012, + "lanes": Array [ + 4, + ], + "timestamp": 0.022, + "type": "render-idle", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.022, + "type": "render", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 4, + ], + "timestamp": 0.026, + "type": "commit", + }, + Object { + "batchUID": 1, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 4, + ], + "timestamp": 0.032, + "type": "layout-effects", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.035, + "type": "passive-effects", + }, ], - "timestamp": 0.035, - "type": "passive-effects", }, - ], - }, - "componentMeasures": Array [ - Object { - "componentName": "App", - "duration": 0.001, - "timestamp": 0.005, - "type": "render", - "warning": null, - }, - Object { - "componentName": "App", - "duration": 0.002, - "timestamp": 0.018, - "type": "passive-effect-mount", - "warning": null, - }, - Object { - "componentName": "App", - "duration": 0.001, - "timestamp": 0.023, - "type": "render", - "warning": null, - }, - Object { - "componentName": "App", - "duration": 0.001, - "timestamp": 0.036, - "type": "passive-effect-mount", - "warning": null, - }, - ], - "duration": 0.038, - "flamechart": Array [], - "internalModuleSourceToRanges": Map { - undefined => Array [ - Array [ + "componentMeasures": Array [ Object { - "functionName": "<filtered-file-system-path>", + "componentName": "App", + "duration": 0.001, + "timestamp": 0.005, + "type": "render", + "warning": null, }, Object { - "functionName": "dule-stop-<filtered-file-system-path>", + "componentName": "App", + "duration": 0.002, + "timestamp": 0.018, + "type": "passive-effect-mount", + "warning": null, + }, + Object { + "componentName": "App", + "duration": 0.001, + "timestamp": 0.023, + "type": "render", + "warning": null, + }, + Object { + "componentName": "App", + "duration": 0.001, + "timestamp": 0.036, + "type": "passive-effect-mount", + "warning": null, }, ], - ], - }, - "laneToLabelMap": Map { - 0 => "Sync", - 1 => "InputContinuousHydration", - 2 => "InputContinuous", - 3 => "DefaultHydration", - 4 => "Default", - 5 => "TransitionHydration", - 6 => "Transition", - 7 => "Transition", - 8 => "Transition", - 9 => "Transition", - 10 => "Transition", - 11 => "Transition", - 12 => "Transition", - 13 => "Transition", - 14 => "Transition", - 15 => "Transition", - 16 => "Transition", - 17 => "Transition", - 18 => "Transition", - 19 => "Transition", - 20 => "Transition", - 21 => "Transition", - 22 => "Retry", - 23 => "Retry", - 24 => "Retry", - 25 => "Retry", - 26 => "Retry", - 27 => "SelectiveHydration", - 28 => "IdleHydration", - 29 => "Idle", - 30 => "Offscreen", - }, - "laneToReactMeasureMap": Map { - 0 => Array [], - 1 => Array [], - 2 => Array [], - 3 => Array [], - 4 => Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.012, - "lanes": Array [ - 4, - ], - "timestamp": 0.004, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, - ], - "timestamp": 0.004, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 4, - ], - "timestamp": 0.008, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 4, - ], - "timestamp": 0.014, - "type": "layout-effects", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.004, - "lanes": Array [ - 4, - ], - "timestamp": 0.017, - "type": "passive-effects", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.012, - "lanes": Array [ - 4, + "duration": 0.038, + "flamechart": Array [], + "internalModuleSourceToRanges": Map { + undefined => Array [ + Array [ + Object { + "functionName": "<filtered-file-system-path>", + }, + Object { + "functionName": "dule-stop-<filtered-file-system-path>", + }, + ], ], - "timestamp": 0.022, - "type": "render-idle", }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, - ], - "timestamp": 0.022, - "type": "render", + "laneToLabelMap": Map { + 0 => "Sync", + 1 => "InputContinuousHydration", + 2 => "InputContinuous", + 3 => "DefaultHydration", + 4 => "Default", + 5 => "TransitionHydration", + 6 => "Transition", + 7 => "Transition", + 8 => "Transition", + 9 => "Transition", + 10 => "Transition", + 11 => "Transition", + 12 => "Transition", + 13 => "Transition", + 14 => "Transition", + 15 => "Transition", + 16 => "Transition", + 17 => "Transition", + 18 => "Transition", + 19 => "Transition", + 20 => "Transition", + 21 => "Transition", + 22 => "Retry", + 23 => "Retry", + 24 => "Retry", + 25 => "Retry", + 26 => "Retry", + 27 => "SelectiveHydration", + 28 => "IdleHydration", + 29 => "Idle", + 30 => "Offscreen", }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 4, + "laneToReactMeasureMap": Map { + 0 => Array [], + 1 => Array [], + 2 => Array [], + 3 => Array [], + 4 => Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.012, + "lanes": Array [ + 4, + ], + "timestamp": 0.004, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.004, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 4, + ], + "timestamp": 0.008, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 4, + ], + "timestamp": 0.014, + "type": "layout-effects", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.004, + "lanes": Array [ + 4, + ], + "timestamp": 0.017, + "type": "passive-effects", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.012, + "lanes": Array [ + 4, + ], + "timestamp": 0.022, + "type": "render-idle", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.022, + "type": "render", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 4, + ], + "timestamp": 0.026, + "type": "commit", + }, + Object { + "batchUID": 1, + "depth": 1, + "duration": 0.001, + "lanes": Array [ + 4, + ], + "timestamp": 0.032, + "type": "layout-effects", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.035, + "type": "passive-effects", + }, ], - "timestamp": 0.026, - "type": "commit", + 5 => Array [], + 6 => Array [], + 7 => Array [], + 8 => Array [], + 9 => Array [], + 10 => Array [], + 11 => Array [], + 12 => Array [], + 13 => Array [], + 14 => Array [], + 15 => Array [], + 16 => Array [], + 17 => Array [], + 18 => Array [], + 19 => Array [], + 20 => Array [], + 21 => Array [], + 22 => Array [], + 23 => Array [], + 24 => Array [], + 25 => Array [], + 26 => Array [], + 27 => Array [], + 28 => Array [], + 29 => Array [], + 30 => Array [], }, - Object { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": Array [ - 4, - ], - "timestamp": 0.032, - "type": "layout-effects", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, - ], - "timestamp": 0.035, - "type": "passive-effects", - }, - ], - 5 => Array [], - 6 => Array [], - 7 => Array [], - 8 => Array [], - 9 => Array [], - 10 => Array [], - 11 => Array [], - 12 => Array [], - 13 => Array [], - 14 => Array [], - 15 => Array [], - 16 => Array [], - 17 => Array [], - 18 => Array [], - 19 => Array [], - 20 => Array [], - 21 => Array [], - 22 => Array [], - 23 => Array [], - 24 => Array [], - 25 => Array [], - 26 => Array [], - 27 => Array [], - 28 => Array [], - 29 => Array [], - 30 => Array [], - }, - "nativeEvents": Array [], - "networkMeasures": Array [], - "otherUserTimingMarks": Array [], - "reactVersion": "0.0.0", - "schedulingEvents": Array [ - Object { - "lanes": Array [ - 4, - ], - "timestamp": 0.003, - "type": "schedule-render", - "warning": null, - }, - Object { - "componentName": "App", - "lanes": Array [ - 4, + "nativeEvents": Array [], + "networkMeasures": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "0.0.0", + "schedulingEvents": Array [ + Object { + "lanes": Array [ + 4, + ], + "timestamp": 0.003, + "type": "schedule-render", + "warning": null, + }, + Object { + "componentName": "App", + "lanes": Array [ + 4, + ], + "timestamp": 0.019, + "type": "schedule-state-update", + "warning": null, + }, ], - "timestamp": 0.019, - "type": "schedule-state-update", - "warning": null, - }, - ], - "snapshotHeight": 0, - "snapshots": Array [], - "startTime": 4, - "suspenseEvents": Array [], - "thrownErrors": Array [], - } - `); - }); - - it('should error if events and measures are incomplete', async () => { - const container = document.createElement('div'); - utils.legacyRender(<div />, container); - - const invalidMarks = clearedMarks.filter( - mark => !mark.includes('render-stop'), - ); - const invalidUserTimingData = createUserTimingData(invalidMarks); - - const error = spyOn(console, 'error'); - preprocessData([...createBoilerplateEntries(), ...invalidUserTimingData]); - expect(error).toHaveBeenCalled(); - }); - - it('should error if work is completed without being started', async () => { - const container = document.createElement('div'); - utils.legacyRender(<div />, container); + "snapshotHeight": 0, + "snapshots": Array [], + "startTime": 4, + "suspenseEvents": Array [], + "thrownErrors": Array [], + } + `); + }); - const invalidMarks = clearedMarks.filter( - mark => !mark.includes('render-start'), - ); - const invalidUserTimingData = createUserTimingData(invalidMarks); + it('should error if events and measures are incomplete', async () => { + const container = document.createElement('div'); + utils.legacyRender(<div />, container); + + const invalidMarks = clearedMarks.filter( + mark => !mark.includes('render-stop'), + ); + const invalidUserTimingData = createUserTimingData(invalidMarks); + + const error = spyOn(console, 'error'); + preprocessData([ + ...createBoilerplateEntries(), + ...invalidUserTimingData, + ]); + expect(error).toHaveBeenCalled(); + }); - const error = spyOn(console, 'error'); - preprocessData([...createBoilerplateEntries(), ...invalidUserTimingData]); - expect(error).toHaveBeenCalled(); - }); + it('should error if work is completed without being started', async () => { + const container = document.createElement('div'); + utils.legacyRender(<div />, container); + + const invalidMarks = clearedMarks.filter( + mark => !mark.includes('render-start'), + ); + const invalidUserTimingData = createUserTimingData(invalidMarks); + + const error = spyOn(console, 'error'); + preprocessData([ + ...createBoilerplateEntries(), + ...invalidUserTimingData, + ]); + expect(error).toHaveBeenCalled(); + }); - it('should populate other user timing marks', async () => { - const userTimingData = createUserTimingData([]); - userTimingData.push( - createUserTimingEntry({ - args: {}, - cat: 'blink.user_timing', - id: '0xcdf75f7c', - name: 'VCWithoutImage: root', - ph: 'n', - scope: 'blink.user_timing', - }), - ); - userTimingData.push( - createUserTimingEntry({ - cat: 'blink.user_timing', - name: '--a-mark-that-looks-like-one-of-ours', - ph: 'R', - }), - ); - userTimingData.push( - createUserTimingEntry({ - cat: 'blink.user_timing', - name: 'Some other mark', - ph: 'R', - }), - ); - - const data = await preprocessData([ - ...createBoilerplateEntries(), - ...userTimingData, - ]); - expect(data.otherUserTimingMarks).toMatchInlineSnapshot(` - Array [ - Object { - "name": "VCWithoutImage: root", - "timestamp": 0.003, - }, - Object { - "name": "--a-mark-that-looks-like-one-of-ours", - "timestamp": 0.004, - }, - Object { - "name": "Some other mark", - "timestamp": 0.005, - }, - ] - `); - }); + it('should populate other user timing marks', async () => { + const userTimingData = createUserTimingData([]); + userTimingData.push( + createUserTimingEntry({ + args: {}, + cat: 'blink.user_timing', + id: '0xcdf75f7c', + name: 'VCWithoutImage: root', + ph: 'n', + scope: 'blink.user_timing', + }), + ); + userTimingData.push( + createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--a-mark-that-looks-like-one-of-ours', + ph: 'R', + }), + ); + userTimingData.push( + createUserTimingEntry({ + cat: 'blink.user_timing', + name: 'Some other mark', + ph: 'R', + }), + ); + + const data = await preprocessData([ + ...createBoilerplateEntries(), + ...userTimingData, + ]); + expect(data.otherUserTimingMarks).toMatchInlineSnapshot(` + Array [ + Object { + "name": "VCWithoutImage: root", + "timestamp": 0.003, + }, + Object { + "name": "--a-mark-that-looks-like-one-of-ours", + "timestamp": 0.004, + }, + Object { + "name": "Some other mark", + "timestamp": 0.005, + }, + ] + `); + }); - it('should include a suspended resource "displayName" if one is set', async () => { - let promise = null; - let resolvedValue = null; - function readValue(value) { - if (resolvedValue !== null) { - return resolvedValue; - } else if (promise === null) { - promise = Promise.resolve(true).then(() => { - resolvedValue = value; - }); - promise.displayName = 'Testing displayName'; + it('should include a suspended resource "displayName" if one is set', async () => { + let promise = null; + let resolvedValue = null; + function readValue(value) { + if (resolvedValue !== null) { + return resolvedValue; + } else if (promise === null) { + promise = Promise.resolve(true).then(() => { + resolvedValue = value; + }); + promise.displayName = 'Testing displayName'; + } + throw promise; } - throw promise; - } - function Component() { - const value = readValue(123); - return value; - } + function Component() { + const value = readValue(123); + return value; + } - const testMarks = [creactCpuProfilerSample()]; + const testMarks = [creactCpuProfilerSample()]; - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => - root.render( - <React.Suspense fallback="Loading..."> - <Component /> - </React.Suspense>, - ), - ); + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => + root.render( + <React.Suspense fallback="Loading..."> + <Component /> + </React.Suspense>, + ), + ); - testMarks.push(...createUserTimingData(clearedMarks)); + testMarks.push(...createUserTimingData(clearedMarks)); - let data; - await utils.actAsync(async () => { - data = await preprocessData(testMarks); + let data; + await utils.actAsync(async () => { + data = await preprocessData(testMarks); + }); + expect(data.suspenseEvents).toHaveLength(1); + expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName'); }); - expect(data.suspenseEvents).toHaveLength(1); - expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName'); - }); - describe('warnings', () => { - describe('long event handlers', () => { - it('should not warn when React scedules a (sync) update inside of a short event handler', async () => { - function App() { - return null; - } + describe('warnings', () => { + describe('long event handlers', () => { + it('should not warn when React scedules a (sync) update inside of a short event handler', async () => { + function App() { + return null; + } - const testMarks = [ - creactCpuProfilerSample(), - ...createBoilerplateEntries(), - createNativeEventEntry('click', 5), - ]; + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + createNativeEventEntry('click', 5), + ]; - clearPendingMarks(); + clearPendingMarks(); - utils.legacyRender(<App />, document.createElement('div')); + utils.legacyRender(<App />, document.createElement('div')); - testMarks.push(...createUserTimingData(clearedMarks)); + testMarks.push(...createUserTimingData(clearedMarks)); - const data = await preprocessData(testMarks); - const event = data.nativeEvents.find(({type}) => type === 'click'); - expect(event.warning).toBe(null); - }); + const data = await preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toBe(null); + }); - it('should not warn about long events if the cause was non-React JavaScript', async () => { - function App() { - return null; - } + it('should not warn about long events if the cause was non-React JavaScript', async () => { + function App() { + return null; + } - const testMarks = [ - creactCpuProfilerSample(), - ...createBoilerplateEntries(), - createNativeEventEntry('click', 25000), - ]; + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + createNativeEventEntry('click', 25000), + ]; - startTime += 2000; + startTime += 2000; - clearPendingMarks(); + clearPendingMarks(); - utils.legacyRender(<App />, document.createElement('div')); + utils.legacyRender(<App />, document.createElement('div')); - testMarks.push(...createUserTimingData(clearedMarks)); + testMarks.push(...createUserTimingData(clearedMarks)); - const data = await preprocessData(testMarks); - const event = data.nativeEvents.find(({type}) => type === 'click'); - expect(event.warning).toBe(null); - }); + const data = await preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toBe(null); + }); - it('should warn when React scedules a long (sync) update inside of an event', async () => { - function App() { - return null; - } + it('should warn when React scedules a long (sync) update inside of an event', async () => { + function App() { + return null; + } - const testMarks = [ - creactCpuProfilerSample(), - ...createBoilerplateEntries(), - createNativeEventEntry('click', 25000), - ]; + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + createNativeEventEntry('click', 25000), + ]; - clearPendingMarks(); + clearPendingMarks(); - utils.legacyRender(<App />, document.createElement('div')); + utils.legacyRender(<App />, document.createElement('div')); - clearedMarks.forEach(markName => { - if (markName === '--render-stop') { - // Fake a long running render - startTime += 20000; - } + clearedMarks.forEach(markName => { + if (markName === '--render-stop') { + // Fake a long running render + startTime += 20000; + } - testMarks.push({ - pid: ++pid, - tid: ++tid, - ts: ++startTime, - args: {data: {}}, - cat: 'blink.user_timing', - name: markName, - ph: 'R', + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); }); - }); - const data = await preprocessData(testMarks); - const event = data.nativeEvents.find(({type}) => type === 'click'); - expect(event.warning).toMatchInlineSnapshot( - `"An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work."`, - ); - }); + const data = await preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toMatchInlineSnapshot( + `"An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work."`, + ); + }); - it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', async () => { - function Yield({id, value}) { - Scheduler.unstable_yieldValue(`${id}:${value}`); - return null; - } + it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', async () => { + function Yield({id, value}) { + Scheduler.unstable_yieldValue(`${id}:${value}`); + return null; + } - const testMarks = [ - creactCpuProfilerSample(), - ...createBoilerplateEntries(), - ]; - - // Advance the clock by some arbitrary amount. - startTime += 50000; - - const root = ReactDOM.createRoot(document.createElement('div')); - - // Temporarily turn off the act environment, since we're intentionally using Scheduler instead. - global.IS_REACT_ACT_ENVIRONMENT = false; - React.startTransition(() => { - // Start rendering an async update (but don't finish). - root.render( - <> - <Yield id="A" value={1} /> - <Yield id="B" value={1} /> - </>, - ); - expect(Scheduler).toFlushAndYieldThrough(['A:1']); + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + ]; - testMarks.push(...createUserTimingData(clearedMarks)); - clearPendingMarks(); + // Advance the clock by some arbitrary amount. + startTime += 50000; - // Advance the clock some more to make the pending React update seem long. - startTime += 20000; + const root = ReactDOM.createRoot(document.createElement('div')); - // Fake a long "click" event in the middle - // and schedule a sync update that will also flush the previous work. - testMarks.push(createNativeEventEntry('click', 25000)); - ReactDOM.flushSync(() => { + // Temporarily turn off the act environment, since we're intentionally using Scheduler instead. + global.IS_REACT_ACT_ENVIRONMENT = false; + React.startTransition(() => { + // Start rendering an async update (but don't finish). root.render( <> - <Yield id="A" value={2} /> - <Yield id="B" value={2} /> + <Yield id="A" value={1} /> + <Yield id="B" value={1} /> </>, ); - }); - }); + expect(Scheduler).toFlushAndYieldThrough(['A:1']); - expect(Scheduler).toHaveYielded(['A:2', 'B:2']); + testMarks.push(...createUserTimingData(clearedMarks)); + clearPendingMarks(); - testMarks.push(...createUserTimingData(clearedMarks)); - - const data = await preprocessData(testMarks); - const event = data.nativeEvents.find(({type}) => type === 'click'); - expect(event.warning).toBe(null); - }); - }); - - describe('nested updates', () => { - it('should not warn about short nested (state) updates during layout effects', async () => { - function Component() { - const [didMount, setDidMount] = React.useState(false); - Scheduler.unstable_yieldValue( - `Component ${didMount ? 'update' : 'mount'}`, - ); - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - return didMount; - } + // Advance the clock some more to make the pending React update seem long. + startTime += 20000; - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => { - root.render(<Component />); - }); + // Fake a long "click" event in the middle + // and schedule a sync update that will also flush the previous work. + testMarks.push(createNativeEventEntry('click', 25000)); + ReactDOM.flushSync(() => { + root.render( + <> + <Yield id="A" value={2} /> + <Yield id="B" value={2} /> + </>, + ); + }); + }); - expect(Scheduler).toHaveYielded([ - 'Component mount', - 'Component update', - ]); + expect(Scheduler).toHaveYielded(['A:2', 'B:2']); - const data = await preprocessData([ - ...createBoilerplateEntries(), - ...createUserTimingData(clearedMarks), - ]); + testMarks.push(...createUserTimingData(clearedMarks)); - const event = data.schedulingEvents.find( - ({type}) => type === 'schedule-state-update', - ); - expect(event.warning).toBe(null); + const data = await preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toBe(null); + }); }); - it('should not warn about short (forced) updates during layout effects', async () => { - class Component extends React.Component { - _didMount: boolean = false; - componentDidMount() { - this._didMount = true; - this.forceUpdate(); - } - render() { + describe('nested updates', () => { + it('should not warn about short nested (state) updates during layout effects', async () => { + function Component() { + const [didMount, setDidMount] = React.useState(false); Scheduler.unstable_yieldValue( - `Component ${this._didMount ? 'update' : 'mount'}`, + `Component ${didMount ? 'update' : 'mount'}`, ); - return null; + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + return didMount; } - } - - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => { - root.render(<Component />); - }); - expect(Scheduler).toHaveYielded([ - 'Component mount', - 'Component update', - ]); + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => { + root.render(<Component />); + }); - const data = await preprocessData([ - ...createBoilerplateEntries(), - ...createUserTimingData(clearedMarks), - ]); + expect(Scheduler).toHaveYielded([ + 'Component mount', + 'Component update', + ]); - const event = data.schedulingEvents.find( - ({type}) => type === 'schedule-force-update', - ); - expect(event.warning).toBe(null); - }); + const data = await preprocessData([ + ...createBoilerplateEntries(), + ...createUserTimingData(clearedMarks), + ]); - it('should warn about long nested (state) updates during layout effects', async () => { - function Component() { - const [didMount, setDidMount] = React.useState(false); - Scheduler.unstable_yieldValue( - `Component ${didMount ? 'update' : 'mount'}`, + const event = data.schedulingEvents.find( + ({type}) => type === 'schedule-state-update', ); - // Fake a long render - startTime += 20000; - React.useLayoutEffect(() => { - setDidMount(true); - }, []); - return didMount; - } - - const cpuProfilerSample = creactCpuProfilerSample(); - - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => { - root.render(<Component />); + expect(event.warning).toBe(null); }); - expect(Scheduler).toHaveYielded([ - 'Component mount', - 'Component update', - ]); - - const testMarks = []; - clearedMarks.forEach(markName => { - if (markName === '--component-render-start-Component') { - // Fake a long running render - startTime += 20000; + it('should not warn about short (forced) updates during layout effects', async () => { + class Component extends React.Component { + _didMount: boolean = false; + componentDidMount() { + this._didMount = true; + this.forceUpdate(); + } + render() { + Scheduler.unstable_yieldValue( + `Component ${this._didMount ? 'update' : 'mount'}`, + ); + return null; + } } - testMarks.push({ - pid: ++pid, - tid: ++tid, - ts: ++startTime, - args: {data: {}}, - cat: 'blink.user_timing', - name: markName, - ph: 'R', + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => { + root.render(<Component />); }); - }); - const data = await preprocessData([ - cpuProfilerSample, - ...createBoilerplateEntries(), - ...testMarks, - ]); - - const event = data.schedulingEvents.find( - ({type}) => type === 'schedule-state-update', - ); - expect(event.warning).toMatchInlineSnapshot( - `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`, - ); - }); + expect(Scheduler).toHaveYielded([ + 'Component mount', + 'Component update', + ]); - it('should warn about long nested (forced) updates during layout effects', async () => { - class Component extends React.Component { - _didMount: boolean = false; - componentDidMount() { - this._didMount = true; - this.forceUpdate(); - } - render() { + const data = await preprocessData([ + ...createBoilerplateEntries(), + ...createUserTimingData(clearedMarks), + ]); + + const event = data.schedulingEvents.find( + ({type}) => type === 'schedule-force-update', + ); + expect(event.warning).toBe(null); + }); + + it('should warn about long nested (state) updates during layout effects', async () => { + function Component() { + const [didMount, setDidMount] = React.useState(false); Scheduler.unstable_yieldValue( - `Component ${this._didMount ? 'update' : 'mount'}`, + `Component ${didMount ? 'update' : 'mount'}`, ); - return null; + // Fake a long render + startTime += 20000; + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + return didMount; } - } - const cpuProfilerSample = creactCpuProfilerSample(); + const cpuProfilerSample = creactCpuProfilerSample(); - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => { - root.render(<Component />); - }); + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => { + root.render(<Component />); + }); - expect(Scheduler).toHaveYielded([ - 'Component mount', - 'Component update', - ]); + expect(Scheduler).toHaveYielded([ + 'Component mount', + 'Component update', + ]); - const testMarks = []; - clearedMarks.forEach(markName => { - if (markName === '--component-render-start-Component') { - // Fake a long running render - startTime += 20000; - } + const testMarks = []; + clearedMarks.forEach(markName => { + if (markName === '--component-render-start-Component') { + // Fake a long running render + startTime += 20000; + } - testMarks.push({ - pid: ++pid, - tid: ++tid, - ts: ++startTime, - args: {data: {}}, - cat: 'blink.user_timing', - name: markName, - ph: 'R', + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); }); - }); - const data = await preprocessData([ - cpuProfilerSample, - ...createBoilerplateEntries(), - ...testMarks, - ]); - - const event = data.schedulingEvents.find( - ({type}) => type === 'schedule-force-update', - ); - expect(event.warning).toMatchInlineSnapshot( - `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`, - ); - }); + const data = await preprocessData([ + cpuProfilerSample, + ...createBoilerplateEntries(), + ...testMarks, + ]); - it('should not warn about transition updates scheduled during commit phase', async () => { - function Component() { - const [value, setValue] = React.useState(0); - // eslint-disable-next-line no-unused-vars - const [isPending, startTransition] = React.useTransition(); - - Scheduler.unstable_yieldValue( - `Component rendered with value ${value}`, + const event = data.schedulingEvents.find( + ({type}) => type === 'schedule-state-update', + ); + expect(event.warning).toMatchInlineSnapshot( + `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`, ); + }); - // Fake a long render - if (value !== 0) { - Scheduler.unstable_yieldValue('Long render'); - startTime += 20000; + it('should warn about long nested (forced) updates during layout effects', async () => { + class Component extends React.Component { + _didMount: boolean = false; + componentDidMount() { + this._didMount = true; + this.forceUpdate(); + } + render() { + Scheduler.unstable_yieldValue( + `Component ${this._didMount ? 'update' : 'mount'}`, + ); + return null; + } } - React.useLayoutEffect(() => { - startTransition(() => { - setValue(1); - }); - }, []); + const cpuProfilerSample = creactCpuProfilerSample(); - return value; - } - - const cpuProfilerSample = creactCpuProfilerSample(); + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => { + root.render(<Component />); + }); - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => { - root.render(<Component />); - }); + expect(Scheduler).toHaveYielded([ + 'Component mount', + 'Component update', + ]); - expect(Scheduler).toHaveYielded([ - 'Component rendered with value 0', - 'Component rendered with value 0', - 'Component rendered with value 1', - 'Long render', - ]); - - const testMarks = []; - clearedMarks.forEach(markName => { - if (markName === '--component-render-start-Component') { - // Fake a long running render - startTime += 20000; - } + const testMarks = []; + clearedMarks.forEach(markName => { + if (markName === '--component-render-start-Component') { + // Fake a long running render + startTime += 20000; + } - testMarks.push({ - pid: ++pid, - tid: ++tid, - ts: ++startTime, - args: {data: {}}, - cat: 'blink.user_timing', - name: markName, - ph: 'R', + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); }); - }); - const data = await preprocessData([ - cpuProfilerSample, - ...createBoilerplateEntries(), - ...testMarks, - ]); + const data = await preprocessData([ + cpuProfilerSample, + ...createBoilerplateEntries(), + ...testMarks, + ]); - data.schedulingEvents.forEach(event => { - expect(event.warning).toBeNull(); + const event = data.schedulingEvents.find( + ({type}) => type === 'schedule-force-update', + ); + expect(event.warning).toMatchInlineSnapshot( + `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`, + ); }); - }); - it('should not warn about deferred value updates scheduled during commit phase', async () => { - function Component() { - const [value, setValue] = React.useState(0); - const deferredValue = React.useDeferredValue(value); + it('should not warn about transition updates scheduled during commit phase', async () => { + function Component() { + const [value, setValue] = React.useState(0); + // eslint-disable-next-line no-unused-vars + const [isPending, startTransition] = React.useTransition(); - Scheduler.unstable_yieldValue( - `Component rendered with value ${value} and deferredValue ${deferredValue}`, - ); + Scheduler.unstable_yieldValue( + `Component rendered with value ${value}`, + ); - // Fake a long render - if (deferredValue !== 0) { - Scheduler.unstable_yieldValue('Long render'); - startTime += 20000; + // Fake a long render + if (value !== 0) { + Scheduler.unstable_yieldValue('Long render'); + startTime += 20000; + } + + React.useLayoutEffect(() => { + startTransition(() => { + setValue(1); + }); + }, []); + + return value; } - React.useLayoutEffect(() => { - setValue(1); - }, []); + const cpuProfilerSample = creactCpuProfilerSample(); - return value + deferredValue; - } + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => { + root.render(<Component />); + }); - const cpuProfilerSample = creactCpuProfilerSample(); + expect(Scheduler).toHaveYielded([ + 'Component rendered with value 0', + 'Component rendered with value 0', + 'Component rendered with value 1', + 'Long render', + ]); + + const testMarks = []; + clearedMarks.forEach(markName => { + if (markName === '--component-render-start-Component') { + // Fake a long running render + startTime += 20000; + } - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => { - root.render(<Component />); + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); + }); + + const data = await preprocessData([ + cpuProfilerSample, + ...createBoilerplateEntries(), + ...testMarks, + ]); + + data.schedulingEvents.forEach(event => { + expect(event.warning).toBeNull(); + }); }); - expect(Scheduler).toHaveYielded([ - 'Component rendered with value 0 and deferredValue 0', - 'Component rendered with value 1 and deferredValue 0', - 'Component rendered with value 1 and deferredValue 1', - 'Long render', - ]); - - const testMarks = []; - clearedMarks.forEach(markName => { - if (markName === '--component-render-start-Component') { - // Fake a long running render - startTime += 20000; + it('should not warn about deferred value updates scheduled during commit phase', async () => { + function Component() { + const [value, setValue] = React.useState(0); + const deferredValue = React.useDeferredValue(value); + + Scheduler.unstable_yieldValue( + `Component rendered with value ${value} and deferredValue ${deferredValue}`, + ); + + // Fake a long render + if (deferredValue !== 0) { + Scheduler.unstable_yieldValue('Long render'); + startTime += 20000; + } + + React.useLayoutEffect(() => { + setValue(1); + }, []); + + return value + deferredValue; } - testMarks.push({ - pid: ++pid, - tid: ++tid, - ts: ++startTime, - args: {data: {}}, - cat: 'blink.user_timing', - name: markName, - ph: 'R', + const cpuProfilerSample = creactCpuProfilerSample(); + + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => { + root.render(<Component />); }); - }); - const data = await preprocessData([ - cpuProfilerSample, - ...createBoilerplateEntries(), - ...testMarks, - ]); + expect(Scheduler).toHaveYielded([ + 'Component rendered with value 0 and deferredValue 0', + 'Component rendered with value 1 and deferredValue 0', + 'Component rendered with value 1 and deferredValue 1', + 'Long render', + ]); + + const testMarks = []; + clearedMarks.forEach(markName => { + if (markName === '--component-render-start-Component') { + // Fake a long running render + startTime += 20000; + } - data.schedulingEvents.forEach(event => { - expect(event.warning).toBeNull(); + testMarks.push({ + pid: ++pid, + tid: ++tid, + ts: ++startTime, + args: {data: {}}, + cat: 'blink.user_timing', + name: markName, + ph: 'R', + }); + }); + + const data = await preprocessData([ + cpuProfilerSample, + ...createBoilerplateEntries(), + ...testMarks, + ]); + + data.schedulingEvents.forEach(event => { + expect(event.warning).toBeNull(); + }); }); }); - }); - describe('errors thrown while rendering', () => { - it('shoult parse Errors thrown during render', async () => { - spyOn(console, 'error'); + describe('errors thrown while rendering', () => { + it('shoult parse Errors thrown during render', async () => { + spyOn(console, 'error'); - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { - return null; + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return null; + } + return this.props.children; } - return this.props.children; } - } - function ExampleThatThrows() { - throw Error('Expected error'); - } + function ExampleThatThrows() { + throw Error('Expected error'); + } + + const testMarks = [creactCpuProfilerSample()]; - const testMarks = [creactCpuProfilerSample()]; - - // Mount and commit the app - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => - root.render( - <ErrorBoundary> - <ExampleThatThrows /> - </ErrorBoundary>, - ), - ); - - testMarks.push(...createUserTimingData(clearedMarks)); - - const data = await preprocessData(testMarks); - expect(data.thrownErrors).toHaveLength(2); - expect(data.thrownErrors[0].message).toMatchInlineSnapshot( - '"Expected error"', - ); + // Mount and commit the app + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => + root.render( + <ErrorBoundary> + <ExampleThatThrows /> + </ErrorBoundary>, + ), + ); + + testMarks.push(...createUserTimingData(clearedMarks)); + + const data = await preprocessData(testMarks); + expect(data.thrownErrors).toHaveLength(2); + expect(data.thrownErrors[0].message).toMatchInlineSnapshot( + '"Expected error"', + ); + }); }); - }); - describe('suspend during an update', () => { - // This also tests an edge case where the a component suspends while profiling - // before the first commit is logged (so the lane-to-labels map will not yet exist). - it('should warn about suspending during an udpate', async () => { - let promise = null; - let resolvedValue = null; - function readValue(value) { - if (resolvedValue !== null) { - return resolvedValue; - } else if (promise === null) { - promise = Promise.resolve(true).then(() => { - resolvedValue = value; - }); + describe('suspend during an update', () => { + // This also tests an edge case where the a component suspends while profiling + // before the first commit is logged (so the lane-to-labels map will not yet exist). + it('should warn about suspending during an udpate', async () => { + let promise = null; + let resolvedValue = null; + function readValue(value) { + if (resolvedValue !== null) { + return resolvedValue; + } else if (promise === null) { + promise = Promise.resolve(true).then(() => { + resolvedValue = value; + }); + } + throw promise; } - throw promise; - } - function Component({shouldSuspend}) { - Scheduler.unstable_yieldValue(`Component ${shouldSuspend}`); - if (shouldSuspend) { - readValue(123); + function Component({shouldSuspend}) { + Scheduler.unstable_yieldValue(`Component ${shouldSuspend}`); + if (shouldSuspend) { + readValue(123); + } + return null; } - return null; - } - // Mount and commit the app - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => - root.render( - <React.Suspense fallback="Loading..."> - <Component shouldSuspend={false} /> - </React.Suspense>, - ), - ); - - clearPendingMarks(); - - const testMarks = [creactCpuProfilerSample()]; - - // Start profiling and suspend during a render. - utils.act(() => - root.render( - <React.Suspense fallback="Loading..."> - <Component shouldSuspend={true} /> - </React.Suspense>, - ), - ); - - testMarks.push(...createUserTimingData(clearedMarks)); - - let data; - await utils.actAsync(async () => { - data = await preprocessData(testMarks); + // Mount and commit the app + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => + root.render( + <React.Suspense fallback="Loading..."> + <Component shouldSuspend={false} /> + </React.Suspense>, + ), + ); + + const testMarks = [creactCpuProfilerSample()]; + + // Start profiling and suspend during a render. + utils.act(() => + root.render( + <React.Suspense fallback="Loading..."> + <Component shouldSuspend={true} /> + </React.Suspense>, + ), + ); + + testMarks.push(...createUserTimingData(clearedMarks)); + + let data; + await utils.actAsync(async () => { + data = await preprocessData(testMarks); + }); + expect(data.suspenseEvents).toHaveLength(1); + expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot( + `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`, + ); }); - expect(data.suspenseEvents).toHaveLength(1); - expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot( - `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`, - ); - }); - it('should not warn about suspending during an transition', async () => { - let promise = null; - let resolvedValue = null; - function readValue(value) { - if (resolvedValue !== null) { - return resolvedValue; - } else if (promise === null) { - promise = Promise.resolve(true).then(() => { - resolvedValue = value; - }); + it('should not warn about suspending during an transition', async () => { + let promise = null; + let resolvedValue = null; + function readValue(value) { + if (resolvedValue !== null) { + return resolvedValue; + } else if (promise === null) { + promise = Promise.resolve(true).then(() => { + resolvedValue = value; + }); + } + throw promise; } - throw promise; - } - function Component({shouldSuspend}) { - Scheduler.unstable_yieldValue(`Component ${shouldSuspend}`); - if (shouldSuspend) { - readValue(123); + function Component({shouldSuspend}) { + Scheduler.unstable_yieldValue(`Component ${shouldSuspend}`); + if (shouldSuspend) { + readValue(123); + } + return null; } - return null; - } - // Mount and commit the app - const root = ReactDOM.createRoot(document.createElement('div')); - utils.act(() => - root.render( - <React.Suspense fallback="Loading..."> - <Component shouldSuspend={false} /> - </React.Suspense>, - ), - ); - - const testMarks = [creactCpuProfilerSample()]; - - // Start profiling and suspend during a render. - await utils.actAsync(async () => - React.startTransition(() => + // Mount and commit the app + const root = ReactDOM.createRoot(document.createElement('div')); + utils.act(() => root.render( <React.Suspense fallback="Loading..."> - <Component shouldSuspend={true} /> + <Component shouldSuspend={false} /> </React.Suspense>, ), - ), - ); + ); - testMarks.push(...createUserTimingData(clearedMarks)); + const testMarks = [creactCpuProfilerSample()]; - let data; - await utils.actAsync(async () => { - data = await preprocessData(testMarks); + // Start profiling and suspend during a render. + await utils.actAsync(async () => + React.startTransition(() => + root.render( + <React.Suspense fallback="Loading..."> + <Component shouldSuspend={true} /> + </React.Suspense>, + ), + ), + ); + + testMarks.push(...createUserTimingData(clearedMarks)); + + let data; + await utils.actAsync(async () => { + data = await preprocessData(testMarks); + }); + expect(data.suspenseEvents).toHaveLength(1); + expect(data.suspenseEvents[0].warning).toBe(null); }); - expect(data.suspenseEvents).toHaveLength(1); - expect(data.suspenseEvents[0].warning).toBe(null); }); }); - }); - // TODO: Add test for snapshot base64 parsing + // TODO: Add test for snapshot base64 parsing - // TODO: Add test for flamechart parsing + // TODO: Add test for flamechart parsing + }); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 4cf491363262c..ee7c65a156827 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -915,17 +915,17 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - expect(store.supportsProfiling).toBe(false); + expect(store.rootSupportsBasicProfiling).toBe(false); act(() => legacyRender(<Component />, containerA)); - expect(store.supportsProfiling).toBe(true); + expect(store.rootSupportsBasicProfiling).toBe(true); act(() => legacyRender(<Component />, containerB)); act(() => ReactDOM.unmountComponentAtNode(containerA)); - expect(store.supportsProfiling).toBe(true); + expect(store.rootSupportsBasicProfiling).toBe(true); act(() => ReactDOM.unmountComponentAtNode(containerB)); - expect(store.supportsProfiling).toBe(false); + expect(store.rootSupportsBasicProfiling).toBe(false); }); it('should properly serialize non-string key values', () => { diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 0a1896ee322c4..f24b5bac85bb6 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -387,7 +387,7 @@ export function attach( pushOperation(id); pushOperation(ElementTypeRoot); pushOperation(0); // StrictMode compliant? - pushOperation(0); // Profiling supported? + pushOperation(0); // Profiling flag pushOperation(0); // StrictMode supported? pushOperation(hasOwnerMetadata ? 1 : 0); } else { diff --git a/packages/react-devtools-shared/src/backend/profilingHooks.js b/packages/react-devtools-shared/src/backend/profilingHooks.js index 2280d6156e683..f09137f5b8ef9 100644 --- a/packages/react-devtools-shared/src/backend/profilingHooks.js +++ b/packages/react-devtools-shared/src/backend/profilingHooks.js @@ -63,21 +63,26 @@ export function setPerformanceMock_ONLY_FOR_TESTING( supportsUserTimingV3 = performanceMock !== null; } -function markAndClear(markName) { - // This method won't be called unless these functions are defined, so we can skip the extra typeof check. - ((performanceTarget: any): Performance).mark(markName); - ((performanceTarget: any): Performance).clearMarks(markName); -} +export type ToggleProfilingStatus = (value: boolean) => void; + +type Response = {| + profilingHooks: DevToolsProfilingHooks, + toggleProfilingStatus: ToggleProfilingStatus, +|}; export function createProfilingHooks({ getDisplayNameForFiber, + getIsProfiling, getLaneLabelMap, reactVersion, }: {| getDisplayNameForFiber: (fiber: Fiber) => string | null, + getIsProfiling: () => boolean, getLaneLabelMap?: () => Map<Lane, string> | null, reactVersion: string, -|}): DevToolsProfilingHooks { +|}): Response { + let isProfiling: boolean = false; + function markMetadata() { markAndClear(`--react-version-${reactVersion}`); markAndClear(`--profiler-version-${SCHEDULING_PROFILER_VERSION}`); @@ -115,19 +120,25 @@ export function createProfilingHooks({ } } + function markAndClear(markName) { + // Only record User Timing marks if DevTools is profiling. + if (!isProfiling) { + return; + } + + // This method won't be called unless these functions are defined, so we can skip the extra typeof check. + ((performanceTarget: any): Performance).mark(markName); + ((performanceTarget: any): Performance).clearMarks(markName); + } + function markCommitStarted(lanes: Lanes): void { if (supportsUserTimingV3) { markAndClear(`--commit-start-${lanes}`); - // Certain types of metadata should be logged infrequently. - // Normally we would log this during module init, - // but there's no guarantee a user is profiling at that time. - // Commits happen infrequently (less than renders or state updates) - // so we log this extra information along with a commit. - // It will likely be logged more than once but that's okay. - // - // TODO (timeline) Only log this once, when profiling starts. - // For the first phase– refactoring– we'll match the previous behavior. + // Some metadata only needs to be logged once per session, + // but if profiling information is being recorded via the Performance tab, + // DevTools has no way of knowing when the recording starts. + // Because of that, we log thie type of data periodically (once per commit). markMetadata(); } } @@ -337,30 +348,47 @@ export function createProfilingHooks({ } } + function toggleProfilingStatus(value: boolean) { + if (isProfiling !== value) { + isProfiling = value; + + if (supportsUserTimingV3) { + if (isProfiling) { + // TODO (timeline) + // Some metadata only needs to be logged once per session. + // Store it at the start of the session. + } + } + } + } + return { - markCommitStarted, - markCommitStopped, - markComponentRenderStarted, - markComponentRenderStopped, - markComponentPassiveEffectMountStarted, - markComponentPassiveEffectMountStopped, - markComponentPassiveEffectUnmountStarted, - markComponentPassiveEffectUnmountStopped, - markComponentLayoutEffectMountStarted, - markComponentLayoutEffectMountStopped, - markComponentLayoutEffectUnmountStarted, - markComponentLayoutEffectUnmountStopped, - markComponentErrored, - markComponentSuspended, - markLayoutEffectsStarted, - markLayoutEffectsStopped, - markPassiveEffectsStarted, - markPassiveEffectsStopped, - markRenderStarted, - markRenderYielded, - markRenderStopped, - markRenderScheduled, - markForceUpdateScheduled, - markStateUpdateScheduled, + profilingHooks: { + markCommitStarted, + markCommitStopped, + markComponentRenderStarted, + markComponentRenderStopped, + markComponentPassiveEffectMountStarted, + markComponentPassiveEffectMountStopped, + markComponentPassiveEffectUnmountStarted, + markComponentPassiveEffectUnmountStopped, + markComponentLayoutEffectMountStarted, + markComponentLayoutEffectMountStopped, + markComponentLayoutEffectUnmountStarted, + markComponentLayoutEffectUnmountStopped, + markComponentErrored, + markComponentSuspended, + markLayoutEffectsStarted, + markLayoutEffectsStopped, + markPassiveEffectsStarted, + markPassiveEffectsStopped, + markRenderStarted, + markRenderYielded, + markRenderStopped, + markRenderScheduled, + markForceUpdateScheduled, + markStateUpdateScheduled, + }, + toggleProfilingStatus, }; } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 9a60fe42376a2..9f4d19a67285b 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -47,6 +47,8 @@ import { } from './utils'; import { __DEBUG__, + PROFILING_FLAG_BASIC_SUPPORT, + PROFILING_FLAG_TIMELINE_SUPPORT, SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, TREE_OPERATION_ADD, @@ -94,6 +96,7 @@ import hasOwnProperty from 'shared/hasOwnProperty'; import {getStyleXData} from './StyleX/utils'; import {createProfilingHooks} from './profilingHooks'; +import type {ToggleProfilingStatus} from './profilingHooks'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type { ChangeDescription, @@ -627,14 +630,20 @@ export function attach( }; } + let toggleProfilingStatus: null | ToggleProfilingStatus = null; if (typeof injectProfilingHooks === 'function') { - injectProfilingHooks( - createProfilingHooks({ - getDisplayNameForFiber, - getLaneLabelMap, - reactVersion: version, - }), - ); + const response = createProfilingHooks({ + getDisplayNameForFiber, + getIsProfiling: () => isProfiling, + getLaneLabelMap, + reactVersion: version, + }); + + // Pass the Profiling hooks to the reconciler for it to call during render. + injectProfilingHooks(response.profilingHooks); + + // Hang onto this toggle so we can notify the external methods of profiling status changes. + toggleProfilingStatus = response.toggleProfilingStatus; } // Tracks Fibers with recently changed number of error/warning messages. @@ -1910,12 +1919,22 @@ export function attach( const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner'); const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); + // Adding a new field here would require a bridge protocol version bump (a backwads breaking change). + // Instead let's re-purpose a pre-existing field to carry more information. + let profilingFlags = 0; + if (isProfilingSupported) { + profilingFlags = PROFILING_FLAG_BASIC_SUPPORT; + if (typeof injectProfilingHooks === 'function') { + profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT; + } + } + if (isRoot) { pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(ElementTypeRoot); pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0); - pushOperation(isProfilingSupported ? 1 : 0); + pushOperation(profilingFlags); pushOperation(StrictModeBits !== 0 ? 1 : 0); pushOperation(hasOwnerMetadata ? 1 : 0); @@ -3999,11 +4018,19 @@ export function attach( isProfiling = true; profilingStartTime = getCurrentTime(); rootToCommitProfilingMetadataMap = new Map(); + + if (toggleProfilingStatus !== null) { + toggleProfilingStatus(true); + } } function stopProfiling() { isProfiling = false; recordChangeDescriptions = false; + + if (toggleProfilingStatus !== null) { + toggleProfilingStatus(false); + } } // Automatically start profiling so that we don't miss timing info from initial "mount". diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 99a86464236f2..befab73daa976 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -195,6 +195,7 @@ export type ProfilingDataForRootBackend = {| export type ProfilingDataBackend = {| dataForRoots: Array<ProfilingDataForRootBackend>, rendererID: number, + // TODO (timeline) Add (optional) Timeline data. |}; export type PathFrame = {| diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 6480222e1e414..d7fafc082ce24 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -25,6 +25,9 @@ export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5; export const TREE_OPERATION_REMOVE_ROOT = 6; export const TREE_OPERATION_SET_SUBTREE_MODE = 7; +export const PROFILING_FLAG_BASIC_SUPPORT = 0b01; +export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10; + export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab'; export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY = diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index b68ea3d26f92e..a2fda1825fcaa 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -10,6 +10,8 @@ import EventEmitter from '../events'; import {inspect} from 'util'; import { + PROFILING_FLAG_BASIC_SUPPORT, + PROFILING_FLAG_TIMELINE_SUPPORT, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, @@ -72,9 +74,10 @@ type Config = {| |}; export type Capabilities = {| + supportsBasicProfiling: boolean, hasOwnerMetadata: boolean, - supportsProfiling: boolean, supportsStrictMode: boolean, + supportsTimeline: boolean, |}; /** @@ -88,8 +91,9 @@ export default class Store extends EventEmitter<{| mutated: [[Array<number>, Map<number, number>]], recordChangeDescriptions: [], roots: [], + rootSupportsBasicProfiling: [], + rootSupportsTimelineProfiling: [], supportsNativeStyleEditor: [], - supportsProfiling: [], supportsReloadAndProfile: [], unsupportedBridgeProtocolDetected: [], unsupportedRendererVersionDetected: [], @@ -161,13 +165,16 @@ export default class Store extends EventEmitter<{| _rootIDToRendererID: Map<number, number> = new Map(); // These options may be initially set by a confiugraiton option when constructing the Store. - // In the case of "supportsProfiling", the option may be updated based on the injected renderers. _supportsNativeInspection: boolean = true; _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; _supportsTimeline: boolean = false; _supportsTraceUpdates: boolean = false; + // These options default to false but may be updated as roots are added and removed. + _rootSupportsBasicProfiling: boolean = false; + _rootSupportsTimelineProfiling: boolean = false; + _unsupportedBridgeProtocol: BridgeProtocol | null = null; _unsupportedRendererVersionDetected: boolean = false; @@ -402,6 +409,16 @@ export default class Store extends EventEmitter<{| return this._roots; } + // At least one of the currently mounted roots support the Legacy profiler. + get rootSupportsBasicProfiling(): boolean { + return this._rootSupportsBasicProfiling; + } + + // At least one of the currently mounted roots support the Timeline profiler. + get rootSupportsTimelineProfiling(): boolean { + return this._rootSupportsTimelineProfiling; + } + get supportsNativeInspection(): boolean { return this._supportsNativeInspection; } @@ -410,6 +427,8 @@ export default class Store extends EventEmitter<{| return this._isNativeStyleEditorSupported; } + // This build of DevTools supports the legacy profiler. + // This is a static flag, controled by the Store config. get supportsProfiling(): boolean { return this._supportsProfiling; } @@ -425,6 +444,8 @@ export default class Store extends EventEmitter<{| ); } + // This build of DevTools supports the Timeline profiler. + // This is a static flag, controled by the Store config. get supportsTimeline(): boolean { return this._supportsTimeline; } @@ -903,7 +924,10 @@ export default class Store extends EventEmitter<{| const isStrictModeCompliant = operations[i] > 0; i++; - const supportsProfiling = operations[i] > 0; + const supportsBasicProfiling = + (operations[i] & PROFILING_FLAG_BASIC_SUPPORT) !== 0; + const supportsTimeline = + (operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0; i++; const supportsStrictMode = operations[i] > 0; @@ -915,9 +939,10 @@ export default class Store extends EventEmitter<{| this._roots = this._roots.concat(id); this._rootIDToRendererID.set(id, rendererID); this._rootIDToCapabilities.set(id, { + supportsBasicProfiling, hasOwnerMetadata, - supportsProfiling, supportsStrictMode, + supportsTimeline, }); // Not all roots support StrictMode; @@ -1224,25 +1249,38 @@ export default class Store extends EventEmitter<{| } if (haveRootsChanged) { - const prevSupportsProfiling = this._supportsProfiling; + const prevRootSupportsProfiling = this._rootSupportsBasicProfiling; + const prevRootSupportsTimelineProfiling = this + ._rootSupportsTimelineProfiling; this._hasOwnerMetadata = false; - this._supportsProfiling = false; + this._rootSupportsBasicProfiling = false; + this._rootSupportsTimelineProfiling = false; this._rootIDToCapabilities.forEach( - ({hasOwnerMetadata, supportsProfiling}) => { + ({supportsBasicProfiling, hasOwnerMetadata, supportsTimeline}) => { + if (supportsBasicProfiling) { + this._rootSupportsBasicProfiling = true; + } if (hasOwnerMetadata) { this._hasOwnerMetadata = true; } - if (supportsProfiling) { - this._supportsProfiling = true; + if (supportsTimeline) { + this._rootSupportsTimelineProfiling = true; } }, ); this.emit('roots'); - if (this._supportsProfiling !== prevSupportsProfiling) { - this.emit('supportsProfiling'); + if (this._rootSupportsBasicProfiling !== prevRootSupportsProfiling) { + this.emit('rootSupportsBasicProfiling'); + } + + if ( + this._rootSupportsTimelineProfiling !== + prevRootSupportsTimelineProfiling + ) { + this.emit('rootSupportsTimelineProfiling'); } } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js index 8fbebfeba86dc..53f918d206679 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -17,30 +17,25 @@ import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext'; export default function ClearProfilingDataButton() { const store = useContext(StoreContext); - const {didRecordCommits, isProfiling, selectedTabID} = useContext( - ProfilerContext, - ); + const {didRecordCommits, isProfiling} = useContext(ProfilerContext); const {file, setFile} = useContext(TimelineContext); const {profilerStore} = store; - let doesHaveData = false; - if (selectedTabID === 'timeline') { - doesHaveData = file !== null; - } else { - doesHaveData = didRecordCommits; - } + const doesHaveLegacyData = didRecordCommits; + const doesHaveTimelineData = file !== null; const clear = () => { - if (selectedTabID === 'timeline') { - setFile(null); - } else { + if (doesHaveLegacyData) { profilerStore.clear(); } + if (doesHaveTimelineData) { + setFile(null); + } }; return ( <Button - disabled={isProfiling || !doesHaveData} + disabled={isProfiling || !(doesHaveLegacyData || doesHaveTimelineData)} onClick={clear} title="Clear profiling data"> <ButtonIcon type="clear" /> diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 286048a172bf5..537f226f2d69b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -194,7 +194,7 @@ function updateTree( if (type === ElementTypeRoot) { i++; // isStrictModeCompliant - i++; // supportsProfiling flag + i++; // Profiling flag i++; // supportsStrictMode flag i++; // hasOwnerMetadata flag diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/NoProfilingData.js b/packages/react-devtools-shared/src/devtools/views/Profiler/NoProfilingData.js new file mode 100644 index 0000000000000..231de1c1d5a1a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/NoProfilingData.js @@ -0,0 +1,35 @@ +/** + * 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 RecordToggle from './RecordToggle'; + +import styles from './Profiler.css'; + +export default function NoProfilingData() { + return ( + <div className={styles.Column}> + <div className={styles.Header}>No profiling data has been recorded.</div> + <div className={styles.Row}> + Click the record button <RecordToggle /> to start recording. + </div> + <div className={`${styles.Row} ${styles.LearnMoreRow}`}> + Click{' '} + <a + className={styles.LearnMoreLink} + href="https://fb.me/react-devtools-profiling" + rel="noopener noreferrer" + target="_blank"> + here + </a>{' '} + to learn more about profiling. + </div> + </div> + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css index 45f3352cec6a9..a317e3fcf4294 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css @@ -61,6 +61,12 @@ justify-content: center; } +.LearnMoreRow { + margin-top: 1rem; + color: var(--color-dim); + font-size: var(--font-size-sans-small); +} + .Header { font-size: var(--font-size-sans-large); margin-bottom: 0.5rem; @@ -121,3 +127,9 @@ display: flex; align-items: center; } + +.LearnMoreLink { + color: var(--color-link); + margin-left: 0.25rem; + margin-right: 0.25rem; +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 8f5de6237e865..3ac740324609a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -22,6 +22,8 @@ import ReloadAndProfileButton from './ReloadAndProfileButton'; import ProfilingImportExportButtons from './ProfilingImportExportButtons'; import SnapshotSelector from './SnapshotSelector'; import SidebarCommitInfo from './SidebarCommitInfo'; +import NoProfilingData from './NoProfilingData'; +import ProfilingNotSupported from './ProfilingNotSupported'; import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; @@ -104,9 +106,7 @@ function Profiler(_: {||}) { <div className={styles.Profiler}> <div className={styles.LeftColumn}> <div className={styles.Toolbar}> - <RecordToggle - disabled={!supportsProfiling || selectedTabID === 'timeline'} - /> + <RecordToggle disabled={!supportsProfiling} /> <ReloadAndProfileButton disabled={selectedTabID === 'timeline' || !supportsProfiling} /> @@ -175,37 +175,6 @@ const tabsWithTimeline = [ title: 'Timeline', }, ]; - -const NoProfilingData = () => ( - <div className={styles.Column}> - <div className={styles.Header}>No profiling data has been recorded.</div> - <div className={styles.Row}> - Click the record button <RecordToggle /> to start recording. - </div> - </div> -); - -const ProfilingNotSupported = () => ( - <div className={styles.Column}> - <div className={styles.Header}>Profiling not supported.</div> - <p className={styles.Paragraph}> - Profiling support requires either a development or production-profiling - build of React v16.5+. - </p> - <p className={styles.Paragraph}> - Learn more at{' '} - <a - className={styles.Link} - href="https://reactjs.org/link/profiling" - rel="noopener noreferrer" - target="_blank"> - reactjs.org/link/profiling - </a> - . - </p> - </div> -); - const ProcessingData = () => ( <div className={styles.Column}> <div className={styles.Header}>Processing data...</div> diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index 6e486091e1419..8b08e73328432 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -96,18 +96,18 @@ function ProfilerContextController({children}: Props) { isProcessingData: profilerStore.isProcessingData, isProfiling: profilerStore.isProfiling, profilingData: profilerStore.profilingData, - supportsProfiling: store.supportsProfiling, + supportsProfiling: store.rootSupportsBasicProfiling, }), subscribe: (callback: Function) => { profilerStore.addListener('profilingData', callback); profilerStore.addListener('isProcessingData', callback); profilerStore.addListener('isProfiling', callback); - store.addListener('supportsProfiling', callback); + store.addListener('rootSupportsBasicProfiling', callback); return () => { profilerStore.removeListener('profilingData', callback); profilerStore.removeListener('isProcessingData', callback); profilerStore.removeListener('isProfiling', callback); - store.removeListener('supportsProfiling', callback); + store.removeListener('rootSupportsBasicProfiling', callback); }; }, }), diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingNotSupported.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingNotSupported.js new file mode 100644 index 0000000000000..4dcbe64cef069 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingNotSupported.js @@ -0,0 +1,35 @@ +/** + * 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 styles from './Profiler.css'; + +export default function ProfilingNotSupported() { + return ( + <div className={styles.Column}> + <div className={styles.Header}>Profiling not supported.</div> + <p className={styles.Paragraph}> + Profiling support requires either a development or profiling build of + React v16.5+. + </p> + <p className={styles.Paragraph}> + Learn more at{' '} + <a + className={styles.Link} + href="https://fb.me/react-devtools-profiling" + rel="noopener noreferrer" + target="_blank"> + reactjs.org/link/profiling + </a> + . + </p> + </div> + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js index c3889d47c2b32..2ab7c95170e14 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js @@ -106,6 +106,7 @@ export type ProfilingDataFrontend = {| // Profiling data per root. dataForRoots: Map<number, ProfilingDataForRootFrontend>, imported: boolean, + // TODO (timeline) Add (optional) Timeline data. |}; export type CommitDataExport = {| @@ -136,4 +137,5 @@ export type ProfilingDataForRootExport = {| export type ProfilingDataExport = {| version: 5, dataForRoots: Array<ProfilingDataForRootExport>, + // TODO (timeline) Add (optional) Timeline data. |}; diff --git a/packages/react-devtools-timeline/src/Timeline.js b/packages/react-devtools-timeline/src/Timeline.js index 290830840356e..20feb8eb4fc65 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,22 @@ import { useState, } from 'react'; import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext'; +import NoProfilingData from 'react-devtools-shared/src/devtools/views/Profiler/NoProfilingData'; 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, isTimelineSupported, setFile, viewState} = useContext( + TimelineContext, + ); const ref = useRef(null); @@ -71,55 +74,15 @@ export function Timeline(_: {||}) { viewState={viewState} /> </Suspense> + ) : isTimelineSupported ? ( + <NoProfilingData /> ) : ( - <Welcome onFileSelect={setFile} /> + <TimelineNotSupported /> )} </div> ); } -const Welcome = ({onFileSelect}: {|onFileSelect: (file: File) => void|}) => ( - <ol className={styles.WelcomeInstructionsList}> - {isInternalFacebookBuild && ( - <li className={styles.WelcomeInstructionsListItem}> - Enable the - <a - className={styles.WelcomeInstructionsListItemLink} - href="https://fburl.com/react-devtools-scheduling-profiler-gk" - rel="noopener noreferrer" - target="_blank"> - <code>react_enable_scheduling_profiler</code> GK - </a> - . - </li> - )} - <li className={styles.WelcomeInstructionsListItem}> - Open a website that's built with the - <a - className={styles.WelcomeInstructionsListItemLink} - href="https://reactjs.org/link/profiling" - rel="noopener noreferrer" - target="_blank"> - profiling build of ReactDOM - </a> - (version 18 or newer). - </li> - <li className={styles.WelcomeInstructionsListItem}> - Open the "Performance" tab in Chrome and record some performance data. - </li> - <li className={styles.WelcomeInstructionsListItem}> - Click the "Save profile..." button in Chrome to export the data. - </li> - <li className={styles.WelcomeInstructionsListItem}> - Import the data into the profiler: - <br /> - <ImportButton onFileSelect={onFileSelect}> - <span className={styles.ImportButtonLabel}>Import</span> - </ImportButton> - </li> - </ol> -); - const ProcessingData = () => ( <div className={styles.EmptyStateContainer}> <div className={styles.Header}>Processing data...</div> diff --git a/packages/react-devtools-timeline/src/TimelineContext.js b/packages/react-devtools-timeline/src/TimelineContext.js index cbf57a275c0c5..fbf52b5802479 100644 --- a/packages/react-devtools-timeline/src/TimelineContext.js +++ b/packages/react-devtools-timeline/src/TimelineContext.js @@ -8,7 +8,15 @@ */ 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, @@ -19,6 +27,7 @@ import type {RefObject} from 'shared/ReactTypes'; export type Context = {| file: File | null, + isTimelineSupported: boolean, searchInputContainerRef: RefObject, setFile: (file: File | null) => void, viewState: ViewState, @@ -35,6 +44,20 @@ function TimelineContextController({children}: Props) { const searchInputContainerRef = useRef(null); const [file, setFile] = useState<string | null>(null); + const store = useContext(StoreContext); + + const isTimelineSupported = useSyncExternalStore<boolean>( + function subscribe(callback) { + store.addListener('rootSupportsTimelineProfiling', callback); + return function unsubscribe() { + store.removeListener('rootSupportsTimelineProfiling', callback); + }; + }, + function getState() { + return store.rootSupportsTimelineProfiling; + }, + ); + // Recreate view state any time new profiling data is imported. const viewState = useMemo<ViewState>(() => { const horizontalScrollStateChangeCallbacks: Set<HorizontalScrollStateChangeCallback> = new Set(); @@ -85,11 +108,12 @@ function TimelineContextController({children}: Props) { const value = useMemo( () => ({ file, + isTimelineSupported, searchInputContainerRef, setFile, viewState, }), - [file, setFile, viewState], + [file, 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 ( + <div className={styles.Column}> + <div className={styles.Header}>Timeline profiling not supported.</div> + <p className={styles.Paragraph}> + <span> + Timeline profiler requires a development or profiling build of{' '} + <code className={styles.Code}>react-dom@^18</code>. + </span> + </p> + <div className={styles.LearnMoreRow}> + Click{' '} + <a + className={styles.Link} + href="https://fb.me/react-devtools-profiling" + rel="noopener noreferrer" + target="_blank"> + here + </a>{' '} + to learn more about profiling. + </div> + + {isInternalFacebookBuild && ( + <div className={styles.MetaGKRow}> + <strong>Meta only</strong>: Enable the{' '} + <a + className={styles.Link} + href="https://fburl.com/react-devtools-scheduling-profiler-gk" + rel="noopener noreferrer" + target="_blank"> + react_enable_scheduling_profiler GK + </a> + . + </div> + )} + </div> + ); +}