From 02f4df818f145cbcd67b09a5c26767f21da6344f Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 13 Jan 2022 14:55:54 -0500 Subject: [PATCH] Refactored how React/DevTools log Timeline performance data (#23102) Until now, DEV and PROFILING builds of React recorded Timeline profiling data using the User Timing API. This commit changes things so that React records this data by calling methods on the DevTools hook. (For now, DevTools still records that data using the User Timing API, to match previous behavior.) This commit is large but most of it is just moving things around: * New methods have been added to the DevTools hook (in "backend/profilingHooks") for recording the Timeline performance events. * Reconciler's "ReactFiberDevToolsHook" has been updated to call these new methods (when they're present). * User Timing method calls in "SchedulingProfiler" have been moved to DevTools "backend/profilingHooks" (to match previous behavior, for now). * The old reconciler tests, "SchedulingProfiler-test" and "SchedulingProfilerLabels-test", have been moved into DevTools "TimelineProfiler-test" to ensure behavior didn't change unexpectedly. * Two new methods have been added to the injected renderer interface: injectProfilingHooks() and getLaneLabelMap(). Relates to #22529. --- .../src/__tests__/TimelineProfiler-test.js | 1124 ++++++++++ .../src/__tests__/preprocessData-test.js | 1929 +++++++++++++++++ .../src/__tests__/setupTests.js | 4 + .../src/__tests__/utils.js | 8 + .../src/backend/profilingHooks.js | 364 ++++ .../src/backend/renderer.js | 13 + .../src/backend/types.js | 54 +- packages/react-devtools-shared/src/hook.js | 3 +- .../__tests__/preprocessData-test.internal.js | 1915 ---------------- .../src/ReactFiberBeginWork.new.js | 4 +- .../src/ReactFiberBeginWork.old.js | 4 +- .../src/ReactFiberClassComponent.new.js | 5 +- .../src/ReactFiberClassComponent.old.js | 5 +- .../src/ReactFiberCommitWork.new.js | 6 +- .../src/ReactFiberCommitWork.old.js | 6 +- .../src/ReactFiberDevToolsHook.new.js | 324 ++- .../src/ReactFiberDevToolsHook.old.js | 324 ++- .../src/ReactFiberHooks.new.js | 2 +- .../src/ReactFiberHooks.old.js | 2 +- .../src/ReactFiberReconciler.new.js | 7 +- .../src/ReactFiberReconciler.old.js | 7 +- .../src/ReactFiberWorkLoop.new.js | 28 +- .../src/ReactFiberWorkLoop.old.js | 28 +- .../src/SchedulingProfiler.js | 395 ---- .../__tests__/ReactUpdaters-test.internal.js | 26 + .../SchedulingProfiler-test.internal.js | 998 --------- .../SchedulingProfilerLabels-test.internal.js | 216 -- scripts/jest/matchers/profilerMatchers.js | 0 28 files changed, 4208 insertions(+), 3593 deletions(-) create mode 100644 packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js create mode 100644 packages/react-devtools-shared/src/__tests__/preprocessData-test.js create mode 100644 packages/react-devtools-shared/src/backend/profilingHooks.js delete mode 100644 packages/react-devtools-timeline/src/import-worker/__tests__/preprocessData-test.internal.js delete mode 100644 packages/react-reconciler/src/SchedulingProfiler.js delete mode 100644 packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js delete mode 100644 packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js delete mode 100644 scripts/jest/matchers/profilerMatchers.js 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