/**
* Copyright (c) Meta Platforms, Inc. and 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';
import {normalizeCodeLocInfo} from './utils';
describe('Timeline profiler', () => {
let React;
let ReactDOMClient;
let Scheduler;
let renderHelper;
let renderRootHelper;
let store;
let unmountFns;
let utils;
let waitFor;
let waitForAll;
let waitForPaint;
let assertLog;
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 = ReactDOMClient.createRoot(container);
root.render(element);
const unmountFn = () => root.unmount();
unmountFns.push(unmountFn);
return unmountFn;
};
React = require('react');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
store = global.store;
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('User Timing API', () => {
let clearedMarks;
let featureDetectionMarkName = null;
let marks;
let setPerformanceMock;
function createUserTimingPolyfill() {
featureDetectionMarkName = null;
clearedMarks = [];
marks = [];
// Remove file-system specific bits or version-specific bits of information from the module range marks.
function filterMarkData(markName) {
if (markName.startsWith('--react-internal-module-start')) {
return '--react-internal-module-start- at filtered (<anonymous>:0:0)';
} else if (markName.startsWith('--react-internal-module-stop')) {
return '--react-internal-module-stop- at filtered (<anonymous>:1:1)';
} else if (markName.startsWith('--react-version')) {
return '--react-version-<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++;
}
},
};
}
function clearPendingMarks() {
clearedMarks.splice(0);
}
function dispatchAndSetCurrentEvent(element, event) {
try {
window.event = event;
element.dispatchEvent(event);
} finally {
window.event = undefined;
}
}
beforeEach(() => {
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(<div />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-2",
"--render-start-2",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-2",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
it('should mark concurrent render without suspends or state updates', async () => {
renderRootHelper(<div />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
it('should mark render yields', async () => {
function Bar() {
Scheduler.log('Bar');
return null;
}
function Foo() {
Scheduler.log('Foo');
return <Bar />;
}
React.startTransition(() => {
renderRootHelper(<Foo />);
});
await waitFor(['Foo']);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-128",
"--render-start-128",
"--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(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-2",
"--render-start-2",
"--component-render-start-Example",
"--component-render-stop",
"--suspense-suspend-0-Example-mount-2-",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-2",
"--layout-effects-stop",
"--commit-stop",
]
`);
clearPendingMarks();
await fakeSuspensePromise;
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--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(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-2",
"--render-start-2",
"--component-render-start-Example",
"--component-render-stop",
"--suspense-suspend-0-Example-mount-2-",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-2",
"--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 () => {
let resolveFakePromise;
const fakeSuspensePromise = new Promise(
resolve => (resolveFakePromise = resolve),
);
function Example() {
throw fakeSuspensePromise;
}
renderRootHelper(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--suspense-suspend-0-Example-mount-32-",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
]
`);
clearPendingMarks();
await resolveFakePromise();
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--suspense-resolved-0-Example",
]
`);
});
it('should mark concurrent render with suspense that rejects', async () => {
let rejectFakePromise;
const fakeSuspensePromise = new Promise(
(_, reject) => (rejectFakePromise = reject),
);
function Example() {
throw fakeSuspensePromise;
}
renderRootHelper(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--suspense-suspend-0-Example-mount-32-",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
]
`);
clearPendingMarks();
await expect(() => {
rejectFakePromise(new Error('error'));
return fakeSuspensePromise;
}).rejects.toThrow();
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--suspense-rejected-0-Example",
]
`);
});
it('should mark cascading class component state updates', async () => {
class Example extends React.Component {
state = {didMount: false};
componentDidMount() {
this.setState({didMount: true});
}
render() {
return null;
}
}
renderRootHelper(<Example />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--schedule-state-update-2-Example",
"--layout-effects-stop",
"--render-start-2",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--commit-stop",
"--commit-stop",
]
`);
});
it('should mark cascading class component force updates', async () => {
class Example extends React.Component {
componentDidMount() {
this.forceUpdate();
}
render() {
return null;
}
}
renderRootHelper(<Example />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--schedule-forced-update-2-Example",
"--layout-effects-stop",
"--render-start-2",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--commit-stop",
"--commit-stop",
]
`);
});
it('should mark render phase state updates for class component', async () => {
class Example extends React.Component {
state = {didRender: false};
render() {
if (this.state.didRender === false) {
this.setState({didRender: true});
}
return null;
}
}
renderRootHelper(<Example />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
let errorMessage;
jest.spyOn(console, 'error').mockImplementation(message => {
errorMessage = message;
});
await waitForPaint([]);
expect(console.error).toHaveBeenCalledTimes(1);
expect(errorMessage).toContain(
'Cannot update during an existing state transition',
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--schedule-state-update-32-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
it('should mark render phase force updates for class component', async () => {
let forced = false;
class Example extends React.Component {
render() {
if (!forced) {
forced = true;
this.forceUpdate();
}
return null;
}
}
renderRootHelper(<Example />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
let errorMessage;
jest.spyOn(console, 'error').mockImplementation(message => {
errorMessage = message;
});
await waitForPaint([]);
expect(console.error).toHaveBeenCalledTimes(1);
expect(errorMessage).toContain(
'Cannot update during an existing state transition',
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--schedule-forced-update-32-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
it('should mark cascading layout updates', async () => {
function Example() {
const [didMount, setDidMount] = React.useState(false);
React.useLayoutEffect(() => {
setDidMount(true);
}, []);
return didMount;
}
renderRootHelper(<Example />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--component-layout-effect-mount-start-Example",
"--schedule-state-update-2-Example",
"--component-layout-effect-mount-stop",
"--layout-effects-stop",
"--render-start-2",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--commit-stop",
"--commit-stop",
]
`);
});
it('should mark cascading passive updates', async () => {
function Example() {
const [didMount, setDidMount] = React.useState(false);
React.useEffect(() => {
setDidMount(true);
}, []);
return didMount;
}
renderRootHelper(<Example />);
await waitForAll([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
"--passive-effects-start-32",
"--component-passive-effect-mount-start-Example",
"--schedule-state-update-32-Example",
"--component-passive-effect-mount-stop",
"--passive-effects-stop",
"--render-start-32",
"--component-render-start-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--commit-stop",
]
`);
});
it('should mark render phase updates', async () => {
function Example() {
const [didRender, setDidRender] = React.useState(false);
if (!didRender) {
setDidRender(true);
}
return didRender;
}
renderRootHelper(<Example />);
await waitForAll([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
"--render-start-32",
"--component-render-start-Example",
"--schedule-state-update-32-Example",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
it('should mark sync render that throws', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
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(
<ErrorBoundary>
<ExampleThatThrows />
</ErrorBoundary>,
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-2",
"--render-start-2",
"--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-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-2",
"--schedule-state-update-2-ErrorBoundary",
"--layout-effects-stop",
"--commit-stop",
"--render-start-2",
"--component-render-start-ErrorBoundary",
"--component-render-stop",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--commit-stop",
]
`);
});
it('should mark concurrent render that throws', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
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(
<ErrorBoundary>
<ExampleThatThrows />
</ErrorBoundary>,
);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
clearPendingMarks();
await waitForPaint([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--render-start-32",
"--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-32",
"--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-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--schedule-state-update-2-ErrorBoundary",
"--layout-effects-stop",
"--render-start-2",
"--component-render-start-ErrorBoundary",
"--component-render-stop",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--commit-stop",
"--commit-stop",
]
`);
});
it('should mark passive and layout effects', async () => {
function ComponentWithEffects() {
React.useLayoutEffect(() => {
Scheduler.log('layout 1 mount');
return () => {
Scheduler.log('layout 1 unmount');
};
}, []);
React.useEffect(() => {
Scheduler.log('passive 1 mount');
return () => {
Scheduler.log('passive 1 unmount');
};
}, []);
React.useLayoutEffect(() => {
Scheduler.log('layout 2 mount');
return () => {
Scheduler.log('layout 2 unmount');
};
}, []);
React.useEffect(() => {
Scheduler.log('passive 2 mount');
return () => {
Scheduler.log('passive 2 unmount');
};
}, []);
React.useEffect(() => {
Scheduler.log('passive 3 mount');
return () => {
Scheduler.log('passive 3 unmount');
};
}, []);
return null;
}
const unmount = renderRootHelper(<ComponentWithEffects />);
await waitForPaint(['layout 1 mount', 'layout 2 mount']);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
"--render-start-32",
"--component-render-start-ComponentWithEffects",
"--component-render-stop",
"--render-stop",
"--commit-start-32",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-32",
"--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();
await waitForAll([
'passive 1 mount',
'passive 2 mount',
'passive 3 mount',
]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--passive-effects-start-32",
"--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();
await waitForAll([]);
unmount();
assertLog([
'layout 1 unmount',
'layout 2 unmount',
'passive 1 unmount',
'passive 2 unmount',
'passive 3 unmount',
]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-2",
"--render-start-2",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--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-2",
"--layout-effects-stop",
"--passive-effects-start-2",
"--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(`
[
"--schedule-render-2",
"--render-start-2",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-2",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
it('regression test DefaultLane', () => {
renderRootHelper(<div />);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-render-32",
]
`);
});
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 />);
await waitForAll([]);
clearedMarks.splice(0);
targetRef.current.click();
// Wait a frame, for React to process the "click" update.
await Promise.resolve();
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-state-update-2-App",
"--render-start-2",
"--component-render-start-App",
"--component-render-stop",
"--render-stop",
"--commit-start-2",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-2",
"--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 />);
await waitForAll([]);
clearedMarks.splice(0);
const event = document.createEvent('MouseEvents');
event.initEvent('mouseover', true, true);
dispatchAndSetCurrentEvent(targetRef.current, event);
await waitForAll([]);
expect(clearedMarks).toMatchInlineSnapshot(`
[
"--schedule-state-update-8-App",
"--render-start-8",
"--component-render-start-App",
"--component-render-stop",
"--render-stop",
"--commit-start-8",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred",
"--layout-effects-start-8",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
});
});
describe('DevTools hook (in memory)', () => {
let getBatchOfWork;
let stopProfilingAndGetTimelineData;
beforeEach(() => {
getBatchOfWork = index => {
const timelineData = stopProfilingAndGetTimelineData();
if (timelineData) {
if (timelineData.batchUIDToMeasuresMap.size > index) {
return Array.from(timelineData.batchUIDToMeasuresMap.values())[
index
];
}
}
return null;
};
stopProfilingAndGetTimelineData = () => {
utils.act(() => store.profilerStore.stopProfiling());
const timelineData = store.profilerStore.profilingData?.timelineData;
if (timelineData) {
expect(timelineData).toHaveLength(1);
// normalize the location for component stack source
// for snapshot testing
timelineData.forEach(data => {
data.schedulingEvents.forEach(event => {
if (event.componentStack) {
event.componentStack = normalizeCodeLocInfo(
event.componentStack,
);
}
});
});
return timelineData[0];
} else {
return null;
}
};
});
afterEach(() => {
unmountFns.forEach(unmountFn => unmountFn());
});
describe('when profiling', () => {
beforeEach(() => {
utils.act(() => store.profilerStore.startProfiling());
});
it('should mark sync render without suspends or state updates', () => {
renderHelper(<div />);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
]
`);
});
it('should mark concurrent render without suspends or state updates', () => {
utils.act(() => renderRootHelper(<div />));
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
]
`);
});
it('should mark concurrent render without suspends or state updates', () => {
let updaterFn;
function Example() {
const setHigh = React.useState(0)[1];
const setLow = React.useState(0)[1];
updaterFn = () => {
React.startTransition(() => {
setLow(prevLow => prevLow + 1);
});
setHigh(prevHigh => prevHigh + 1);
};
Scheduler.unstable_advanceTime(10);
return null;
}
utils.act(() => renderRootHelper(<Example />));
utils.act(() => store.profilerStore.stopProfiling());
utils.act(() => store.profilerStore.startProfiling());
utils.act(updaterFn);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000010000000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
]
`);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 0,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
});
// @reactVersion >=18.2
it('should mark render yields', async () => {
function Bar() {
Scheduler.log('Bar');
return null;
}
function Foo() {
Scheduler.log('Foo');
return <Bar />;
}
React.startTransition(() => {
renderRootHelper(<Foo />);
});
// Do one step of work.
await waitFor(['Foo']);
// Finish flushing so React commits;
// Unless we do this, the ProfilerStore won't collect Profiling data.
await waitForAll(['Bar']);
// Since we yielded, the batch should report two separate "render" chunks.
const batch = getBatchOfWork(0);
expect(batch.filter(({type}) => type === 'render')).toHaveLength(2);
});
it('should mark sync render with suspense that resolves', async () => {
let resolveFn;
let resolved = false;
const suspensePromise = new Promise(resolve => {
resolveFn = () => {
resolved = true;
resolve();
};
});
function Example() {
Scheduler.log(resolved ? 'resolved' : 'suspended');
if (!resolved) {
throw suspensePromise;
}
return null;
}
renderHelper(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
assertLog(['suspended']);
Scheduler.unstable_advanceTime(10);
resolveFn();
await suspensePromise;
await waitForAll(['resolved']);
const timelineData = stopProfilingAndGetTimelineData();
// Verify the Suspense event and duration was recorded.
expect(timelineData.suspenseEvents).toHaveLength(1);
const suspenseEvent = timelineData.suspenseEvents[0];
expect(suspenseEvent).toMatchInlineSnapshot(`
{
"componentName": "Example",
"depth": 0,
"duration": 10,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "resolved",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
// There should be two batches of renders: Suspeneded and resolved.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toHaveLength(2);
});
// @reactVersion >=18.2
it('should mark sync render with suspense that rejects', async () => {
let rejectFn;
let rejected = false;
const suspensePromise = new Promise((resolve, reject) => {
rejectFn = () => {
rejected = true;
reject(new Error('error'));
};
});
function Example() {
Scheduler.log(rejected ? 'rejected' : 'suspended');
if (!rejected) {
throw suspensePromise;
}
return null;
}
renderHelper(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
assertLog(['suspended']);
Scheduler.unstable_advanceTime(10);
rejectFn();
await expect(suspensePromise).rejects.toThrow();
assertLog(['rejected']);
const timelineData = stopProfilingAndGetTimelineData();
// Verify the Suspense event and duration was recorded.
expect(timelineData.suspenseEvents).toHaveLength(1);
const suspenseEvent = timelineData.suspenseEvents[0];
expect(suspenseEvent).toMatchInlineSnapshot(`
{
"componentName": "Example",
"depth": 0,
"duration": 10,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "rejected",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
// There should be two batches of renders: Suspeneded and resolved.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toHaveLength(2);
});
// @reactVersion >=18.2
it('should mark concurrent render with suspense that resolves', async () => {
let resolveFn;
let resolved = false;
const suspensePromise = new Promise(resolve => {
resolveFn = () => {
resolved = true;
resolve();
};
});
function Example() {
Scheduler.log(resolved ? 'resolved' : 'suspended');
if (!resolved) {
throw suspensePromise;
}
return null;
}
renderRootHelper(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
await waitForAll(['suspended']);
Scheduler.unstable_advanceTime(10);
resolveFn();
await suspensePromise;
await waitForAll(['resolved']);
const timelineData = stopProfilingAndGetTimelineData();
// Verify the Suspense event and duration was recorded.
expect(timelineData.suspenseEvents).toHaveLength(1);
const suspenseEvent = timelineData.suspenseEvents[0];
expect(suspenseEvent).toMatchInlineSnapshot(`
{
"componentName": "Example",
"depth": 0,
"duration": 10,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "resolved",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
// There should be two batches of renders: Suspeneded and resolved.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toHaveLength(2);
});
// @reactVersion >=18.2
it('should mark concurrent render with suspense that rejects', async () => {
let rejectFn;
let rejected = false;
const suspensePromise = new Promise((resolve, reject) => {
rejectFn = () => {
rejected = true;
reject(new Error('error'));
};
});
function Example() {
Scheduler.log(rejected ? 'rejected' : 'suspended');
if (!rejected) {
throw suspensePromise;
}
return null;
}
renderRootHelper(
<React.Suspense fallback={null}>
<Example />
</React.Suspense>,
);
await waitForAll(['suspended']);
Scheduler.unstable_advanceTime(10);
rejectFn();
await expect(suspensePromise).rejects.toThrow();
await waitForAll(['rejected']);
const timelineData = stopProfilingAndGetTimelineData();
// Verify the Suspense event and duration was recorded.
expect(timelineData.suspenseEvents).toHaveLength(1);
const suspenseEvent = timelineData.suspenseEvents[0];
expect(suspenseEvent).toMatchInlineSnapshot(`
{
"componentName": "Example",
"depth": 0,
"duration": 10,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "rejected",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
// There should be two batches of renders: Suspeneded and resolved.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toHaveLength(2);
});
it('should mark cascading class component state updates', async () => {
class Example extends React.Component {
state = {didMount: false};
componentDidMount() {
this.setState({didMount: true});
}
render() {
Scheduler.unstable_advanceTime(10);
Scheduler.log(this.state.didMount ? 'update' : 'mount');
return null;
}
}
renderRootHelper(<Example />);
await waitForPaint(['mount', 'update']);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 20,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000000010",
"timestamp": 20,
"type": "schedule-state-update",
"warning": null,
},
]
`);
});
it('should mark cascading class component force updates', async () => {
let forced = false;
class Example extends React.Component {
componentDidMount() {
forced = true;
this.forceUpdate();
}
render() {
Scheduler.unstable_advanceTime(10);
Scheduler.log(forced ? 'force update' : 'mount');
return null;
}
}
renderRootHelper(<Example />);
await waitForPaint(['mount', 'force update']);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 20,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"lanes": "0b0000000000000000000000000000010",
"timestamp": 20,
"type": "schedule-force-update",
"warning": null,
},
]
`);
});
it('should mark render phase state updates for class component', async () => {
class Example extends React.Component {
state = {didRender: false};
render() {
if (this.state.didRender === false) {
this.setState({didRender: true});
}
Scheduler.unstable_advanceTime(10);
Scheduler.log(
this.state.didRender ? 'second render' : 'first render',
);
return null;
}
}
renderRootHelper(<Example />);
let errorMessage;
jest.spyOn(console, 'error').mockImplementation(message => {
errorMessage = message;
});
await waitForAll(['first render', 'second render']);
expect(console.error).toHaveBeenCalledTimes(1);
expect(errorMessage).toContain(
'Cannot update during an existing state transition',
);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 20,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
]
`);
});
it('should mark render phase force updates for class component', async () => {
let forced = false;
class Example extends React.Component {
render() {
Scheduler.unstable_advanceTime(10);
Scheduler.log(forced ? 'force update' : 'render');
if (!forced) {
forced = true;
this.forceUpdate();
}
return null;
}
}
renderRootHelper(<Example />);
let errorMessage;
jest.spyOn(console, 'error').mockImplementation(message => {
errorMessage = message;
});
await waitForAll(['render', 'force update']);
expect(console.error).toHaveBeenCalledTimes(1);
expect(errorMessage).toContain(
'Cannot update during an existing state transition',
);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 20,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"lanes": "0b0000000000000000000000000100000",
"timestamp": 20,
"type": "schedule-force-update",
"warning": null,
},
]
`);
});
it('should mark cascading layout updates', async () => {
function Example() {
const [didMount, setDidMount] = React.useState(false);
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(1);
setDidMount(true);
}, []);
Scheduler.unstable_advanceTime(10);
Scheduler.log(didMount ? 'update' : 'mount');
return didMount;
}
renderRootHelper(<Example />);
await waitForAll(['mount', 'update']);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 1,
"timestamp": 20,
"type": "layout-effect-mount",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 21,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000000010",
"timestamp": 21,
"type": "schedule-state-update",
"warning": null,
},
]
`);
});
it('should mark cascading passive updates', async () => {
function Example() {
const [didMount, setDidMount] = React.useState(false);
React.useEffect(() => {
Scheduler.unstable_advanceTime(1);
setDidMount(true);
}, []);
Scheduler.unstable_advanceTime(10);
Scheduler.log(didMount ? 'update' : 'mount');
return didMount;
}
renderRootHelper(<Example />);
await waitForAll(['mount', 'update']);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "Example",
"duration": 1,
"timestamp": 20,
"type": "passive-effect-mount",
"warning": null,
},
{
"componentName": "Example",
"duration": 10,
"timestamp": 21,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000100000",
"timestamp": 21,
"type": "schedule-state-update",
"warning": null,
},
]
`);
});
it('should mark render phase updates', async () => {
function Example() {
const [didRender, setDidRender] = React.useState(false);
Scheduler.unstable_advanceTime(10);
if (!didRender) {
setDidRender(true);
}
Scheduler.log(didRender ? 'update' : 'mount');
return didRender;
}
renderRootHelper(<Example />);
await waitForAll(['mount', 'update']);
const timelineData = stopProfilingAndGetTimelineData();
// Render phase updates should be retried as part of the same batch.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(1);
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"duration": 20,
"timestamp": 10,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000100000",
"timestamp": 20,
"type": "schedule-state-update",
"warning": null,
},
]
`);
});
it('should mark sync render that throws', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
Scheduler.unstable_advanceTime(10);
if (this.state.error) {
Scheduler.log('ErrorBoundary fallback');
return null;
}
Scheduler.log('ErrorBoundary render');
return this.props.children;
}
}
function ExampleThatThrows() {
Scheduler.log('ExampleThatThrows');
throw Error('Expected error');
}
renderHelper(
<ErrorBoundary>
<ExampleThatThrows />
</ErrorBoundary>,
);
assertLog([
'ErrorBoundary render',
'ExampleThatThrows',
'ExampleThatThrows',
'ErrorBoundary fallback',
]);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "ErrorBoundary",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "ExampleThatThrows",
"duration": 0,
"timestamp": 20,
"type": "render",
"warning": null,
},
{
"componentName": "ErrorBoundary",
"duration": 10,
"timestamp": 20,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "ErrorBoundary",
"componentStack": "
in ErrorBoundary (at **)",
"lanes": "0b0000000000000000000000000000010",
"timestamp": 20,
"type": "schedule-state-update",
"warning": null,
},
]
`);
expect(timelineData.thrownErrors).toMatchInlineSnapshot(`
[
{
"componentName": "ExampleThatThrows",
"message": "Expected error",
"phase": "mount",
"timestamp": 20,
"type": "thrown-error",
},
]
`);
});
it('should mark concurrent render that throws', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
Scheduler.unstable_advanceTime(10);
if (this.state.error) {
Scheduler.log('ErrorBoundary fallback');
return null;
}
Scheduler.log('ErrorBoundary render');
return this.props.children;
}
}
function ExampleThatThrows() {
Scheduler.log('ExampleThatThrows');
// eslint-disable-next-line no-throw-literal
throw 'Expected error';
}
renderRootHelper(
<ErrorBoundary>
<ExampleThatThrows />
</ErrorBoundary>,
);
await waitForAll([
'ErrorBoundary render',
'ExampleThatThrows',
'ExampleThatThrows',
'ErrorBoundary render',
'ExampleThatThrows',
'ExampleThatThrows',
'ErrorBoundary fallback',
]);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "ErrorBoundary",
"duration": 10,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "ExampleThatThrows",
"duration": 0,
"timestamp": 20,
"type": "render",
"warning": null,
},
{
"componentName": "ErrorBoundary",
"duration": 10,
"timestamp": 20,
"type": "render",
"warning": null,
},
{
"componentName": "ExampleThatThrows",
"duration": 0,
"timestamp": 30,
"type": "render",
"warning": null,
},
{
"componentName": "ErrorBoundary",
"duration": 10,
"timestamp": 30,
"type": "render",
"warning": null,
},
]
`);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "ErrorBoundary",
"componentStack": "
in ErrorBoundary (at **)",
"lanes": "0b0000000000000000000000000000010",
"timestamp": 30,
"type": "schedule-state-update",
"warning": null,
},
]
`);
expect(timelineData.thrownErrors).toMatchInlineSnapshot(`
[
{
"componentName": "ExampleThatThrows",
"message": "Expected error",
"phase": "mount",
"timestamp": 20,
"type": "thrown-error",
},
{
"componentName": "ExampleThatThrows",
"message": "Expected error",
"phase": "mount",
"timestamp": 30,
"type": "thrown-error",
},
]
`);
});
it('should mark passive and layout effects', async () => {
function ComponentWithEffects() {
React.useLayoutEffect(() => {
Scheduler.log('layout 1 mount');
return () => {
Scheduler.log('layout 1 unmount');
};
}, []);
React.useEffect(() => {
Scheduler.log('passive 1 mount');
return () => {
Scheduler.log('passive 1 unmount');
};
}, []);
React.useLayoutEffect(() => {
Scheduler.log('layout 2 mount');
return () => {
Scheduler.log('layout 2 unmount');
};
}, []);
React.useEffect(() => {
Scheduler.log('passive 2 mount');
return () => {
Scheduler.log('passive 2 unmount');
};
}, []);
React.useEffect(() => {
Scheduler.log('passive 3 mount');
return () => {
Scheduler.log('passive 3 unmount');
};
}, []);
return null;
}
const unmount = renderRootHelper(<ComponentWithEffects />);
await waitForPaint(['layout 1 mount', 'layout 2 mount']);
await waitForAll([
'passive 1 mount',
'passive 2 mount',
'passive 3 mount',
]);
await waitForAll([]);
unmount();
assertLog([
'layout 1 unmount',
'layout 2 unmount',
'passive 1 unmount',
'passive 2 unmount',
'passive 3 unmount',
]);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
[
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "render",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "layout-effect-mount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "layout-effect-mount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "passive-effect-mount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "passive-effect-mount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "passive-effect-mount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "layout-effect-unmount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "layout-effect-unmount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "passive-effect-unmount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "passive-effect-unmount",
"warning": null,
},
{
"componentName": "ComponentWithEffects",
"duration": 0,
"timestamp": 10,
"type": "passive-effect-unmount",
"warning": null,
},
]
`);
expect(timelineData.batchUIDToMeasuresMap).toMatchInlineSnapshot(`
Map {
1 => [
{
"batchUID": 1,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "render-idle",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "render",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "commit",
},
{
"batchUID": 1,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "layout-effects",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "passive-effects",
},
],
2 => [
{
"batchUID": 2,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "render-idle",
},
{
"batchUID": 2,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "render",
},
{
"batchUID": 2,
"depth": 0,
"duration": 0,
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "commit",
},
{
"batchUID": 2,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "layout-effects",
},
{
"batchUID": 2,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000000010",
"timestamp": 10,
"type": "passive-effects",
},
],
}
`);
});
it('should generate component stacks for state update', async () => {
function CommponentWithChildren({initialRender}) {
Scheduler.log('Render ComponentWithChildren');
return <Child initialRender={initialRender} />;
}
function Child({initialRender}) {
const [didRender, setDidRender] = React.useState(initialRender);
if (!didRender) {
setDidRender(true);
}
Scheduler.log('Render Child');
return null;
}
renderRootHelper(<CommponentWithChildren initialRender={false} />);
await waitForAll([
'Render ComponentWithChildren',
'Render Child',
'Render Child',
]);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-render",
"warning": null,
},
{
"componentName": "Child",
"componentStack": "
in Child (at **)
in CommponentWithChildren (at **)",
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
]
`);
});
});
describe('when not profiling', () => {
// @reactVersion >=18.0
it('should not log any marks', () => {
renderHelper(<div />);
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData).toBeNull();
});
});
});
});