diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js new file mode 100644 index 0000000000000..1a369acca27bd --- /dev/null +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -0,0 +1,1124 @@ +/** + * 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 + */ + +'use strict'; + +describe('Timeline profiler', () => { + let React; + let ReactDOM; + let Scheduler; + let renderHelper; + let renderRootHelper; + let unmountFns; + let utils; + + let clearedMarks; + let featureDetectionMarkName = null; + let marks; + let setPerformanceMock; + + function createUserTimingPolyfill() { + featureDetectionMarkName = null; + + clearedMarks = []; + marks = []; + + // Remove file-system specific bits or version-specific bits of information from the module range marks. + function filterMarkData(markName) { + if (markName.startsWith('--react-internal-module-start')) { + return `${markName.substr(0, 29)}-`; + } else if (markName.startsWith('--react-internal-module-stop')) { + return `${markName.substr(0, 28)}-`; + } else if (markName.startsWith('--react-version')) { + return `${markName.substr(0, 15)}-`; + } else { + return markName; + } + } + + // This is not a true polyfill, but it gives us enough to capture marks. + // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API + return { + clearMarks(markName) { + markName = filterMarkData(markName); + + clearedMarks.push(markName); + marks = marks.filter(mark => mark !== markName); + }, + mark(markName, markOptions) { + markName = filterMarkData(markName); + + if (featureDetectionMarkName === null) { + featureDetectionMarkName = markName; + } + + marks.push(markName); + + if (markOptions != null) { + // This is triggers the feature detection. + markOptions.startTime++; + } + }, + }; + } + + function clearPendingMarks() { + clearedMarks.splice(0); + } + + function dispatchAndSetCurrentEvent(element, event) { + try { + window.event = event; + element.dispatchEvent(event); + } finally { + window.event = undefined; + } + } + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + 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()); + }); + + afterEach(() => { + // Verify all logged marks also get cleared. + expect(marks).toHaveLength(0); + + unmountFns.forEach(unmountFn => unmountFn()); + + setPerformanceMock(null); + }); + + it('should mark sync render without suspends or state updates', () => { + renderHelper(
); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + + it('should mark concurrent render without suspends or state updates', () => { + renderRootHelper(
); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + + it('should mark render yields', async () => { + function Bar() { + Scheduler.unstable_yieldValue('Bar'); + return null; + } + + function Foo() { + Scheduler.unstable_yieldValue('Foo'); + return ; + } + + React.startTransition(() => { + renderRootHelper(); + }); + + // 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; + } + + renderHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1-", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + + clearPendingMarks(); + + await fakeSuspensePromise; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); + }); + + it('should mark sync render with suspense that rejects', async () => { + const fakeSuspensePromise = Promise.reject(new Error('error')); + function Example() { + throw fakeSuspensePromise; + } + + renderHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1-", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + + clearPendingMarks(); + + await expect(fakeSuspensePromise).rejects.toThrow(); + expect(clearedMarks).toContain(`--suspense-rejected-0-Example`); + }); + + it('should mark concurrent render with suspense that resolves', async () => { + const fakeSuspensePromise = Promise.resolve(true); + function Example() { + throw fakeSuspensePromise; + } + + renderRootHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-16-", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + + clearPendingMarks(); + + await fakeSuspensePromise; + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); + }); + + it('should mark concurrent render with suspense that rejects', async () => { + const fakeSuspensePromise = Promise.reject(new Error('error')); + function Example() { + throw fakeSuspensePromise; + } + + renderRootHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-16-", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + + clearPendingMarks(); + + await expect(fakeSuspensePromise).rejects.toThrow(); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--suspense-rejected-0-Example", + ] + `); + }); + + it('should mark cascading class component state updates', () => { + class Example extends React.Component { + state = {didMount: false}; + componentDidMount() { + this.setState({didMount: true}); + } + render() { + return null; + } + } + + renderRootHelper(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-state-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + }); + + it('should mark cascading class component force updates', () => { + class Example extends React.Component { + componentDidMount() { + this.forceUpdate(); + } + render() { + return null; + } + } + + renderRootHelper(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-forced-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + }); + + it('should mark render phase state updates for class component', () => { + class Example extends React.Component { + state = {didRender: false}; + render() { + if (this.state.didRender === false) { + this.setState({didRender: true}); + } + return null; + } + } + + renderRootHelper(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + let errorMessage; + spyOn(console, 'error').and.callFake(message => { + errorMessage = message; + }); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(errorMessage).toContain( + 'Cannot update during an existing state transition', + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--schedule-state-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + + it('should mark render phase force updates for class component', () => { + class Example extends React.Component { + state = {didRender: false}; + render() { + if (this.state.didRender === false) { + this.forceUpdate(() => this.setState({didRender: true})); + } + return null; + } + } + + renderRootHelper(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + let errorMessage; + spyOn(console, 'error').and.callFake(message => { + errorMessage = message; + }); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(errorMessage).toContain( + 'Cannot update during an existing state transition', + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--schedule-forced-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + + it('should mark cascading layout updates', () => { + function Example() { + const [didMount, setDidMount] = React.useState(false); + React.useLayoutEffect(() => { + setDidMount(true); + }, []); + return didMount; + } + + renderRootHelper(); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--component-layout-effect-mount-start-Example", + "--schedule-state-update-1-Example", + "--component-layout-effect-mount-stop", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + }); + + // This test is coupled to lane implementation details, so I'm disabling it in + // the new fork until it stabilizes so we don't have to repeatedly update it. + it('should mark cascading passive updates', () => { + function Example() { + const [didMount, setDidMount] = React.useState(false); + React.useEffect(() => { + setDidMount(true); + }, []); + return didMount; + } + + renderRootHelper(); + + expect(Scheduler).toFlushAndYield([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + "--passive-effects-start-16", + "--component-passive-effect-mount-start-Example", + "--schedule-state-update-16-Example", + "--component-passive-effect-mount-stop", + "--passive-effects-stop", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + ] + `); + }); + + it('should mark render phase updates', () => { + function Example() { + const [didRender, setDidRender] = React.useState(false); + if (!didRender) { + setDidRender(true); + } + return didRender; + } + + renderRootHelper(); + + expect(Scheduler).toFlushAndYield([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--schedule-state-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + + 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; + } + } + + function ExampleThatThrows() { + throw Error('Expected error'); + } + + renderHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--component-render-start-ExampleThatThrows", + "--component-render-start-ExampleThatThrows", + "--component-render-stop", + "--error-ExampleThatThrows-mount-Expected error", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--schedule-state-update-1-ErrorBoundary", + "--layout-effects-stop", + "--commit-stop", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + ] + `); + }); + + 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 ExampleThatThrows() { + // eslint-disable-next-line no-throw-literal + throw 'Expected error'; + } + + renderRootHelper( + + + , + ); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushUntilNextPaint([]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--component-render-start-ExampleThatThrows", + "--component-render-start-ExampleThatThrows", + "--component-render-stop", + "--error-ExampleThatThrows-mount-Expected error", + "--render-stop", + "--render-start-16", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--component-render-start-ExampleThatThrows", + "--component-render-start-ExampleThatThrows", + "--component-render-stop", + "--error-ExampleThatThrows-mount-Expected error", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-state-update-1-ErrorBoundary", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-ErrorBoundary", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + }); + + it('should mark passive and layout effects', async () => { + function ComponentWithEffects() { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout 1 mount'); + return () => { + Scheduler.unstable_yieldValue('layout 1 unmount'); + }; + }, []); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive 1 mount'); + return () => { + Scheduler.unstable_yieldValue('passive 1 unmount'); + }; + }, []); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout 2 mount'); + return () => { + Scheduler.unstable_yieldValue('layout 2 unmount'); + }; + }, []); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive 2 mount'); + return () => { + Scheduler.unstable_yieldValue('passive 2 unmount'); + }; + }, []); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive 3 mount'); + return () => { + Scheduler.unstable_yieldValue('passive 3 unmount'); + }; + }, []); + + return null; + } + + const unmount = renderRootHelper(); + + expect(Scheduler).toFlushUntilNextPaint([ + 'layout 1 mount', + 'layout 2 mount', + ]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-ComponentWithEffects", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--component-layout-effect-mount-start-ComponentWithEffects", + "--component-layout-effect-mount-stop", + "--component-layout-effect-mount-start-ComponentWithEffects", + "--component-layout-effect-mount-stop", + "--layout-effects-stop", + "--commit-stop", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushAndYield([ + 'passive 1 mount', + 'passive 2 mount', + 'passive 3 mount', + ]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--passive-effects-start-16", + "--component-passive-effect-mount-start-ComponentWithEffects", + "--component-passive-effect-mount-stop", + "--component-passive-effect-mount-start-ComponentWithEffects", + "--component-passive-effect-mount-stop", + "--component-passive-effect-mount-start-ComponentWithEffects", + "--component-passive-effect-mount-stop", + "--passive-effects-stop", + ] + `); + + clearPendingMarks(); + + expect(Scheduler).toFlushAndYield([]); + + unmount(); + + expect(Scheduler).toHaveYielded([ + 'layout 1 unmount', + 'layout 2 unmount', + 'passive 1 unmount', + 'passive 2 unmount', + 'passive 3 unmount', + ]); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--component-layout-effect-unmount-start-ComponentWithEffects", + "--component-layout-effect-unmount-stop", + "--component-layout-effect-unmount-start-ComponentWithEffects", + "--component-layout-effect-unmount-stop", + "--layout-effects-start-1", + "--layout-effects-stop", + "--passive-effects-start-1", + "--component-passive-effect-unmount-start-ComponentWithEffects", + "--component-passive-effect-unmount-stop", + "--component-passive-effect-unmount-start-ComponentWithEffects", + "--component-passive-effect-unmount-stop", + "--component-passive-effect-unmount-start-ComponentWithEffects", + "--component-passive-effect-unmount-stop", + "--passive-effects-stop", + "--commit-stop", + ] + `); + }); + + describe('lane labels', () => { + it('regression test SyncLane', () => { + renderHelper(
); + + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-", + "--profiler-version-1", + "--react-internal-module-start-", + "--react-internal-module-stop-", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + }); + + it('regression test DefaultLane', () => { + renderRootHelper(
); + 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