/** * 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 ReactDOM; let ReactDOMClient; let Scheduler; let utils; let assertLog; let waitFor; describe('User Timing API', () => { let clearedMarks; let featureDetectionMarkName = null; let marks; let setPerformanceMock; function createUserTimingPolyfill() { featureDetectionMarkName = null; clearedMarks = []; marks = []; // Remove file-system specific bits or version-specific bits of information from the module range marks. function filterMarkData(markName) { if (markName.startsWith('--react-internal-module-start')) { return '--react-internal-module-start- at filtered (:0:0)'; } else if (markName.startsWith('--react-internal-module-stop')) { return '--react-internal-module-stop- at filtered (:1:1)'; } else if (markName.startsWith('--react-version')) { return '--react-version-'; } else { return markName; } } // 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); } beforeEach(() => { utils = require('./utils'); utils.beforeEachProfiling(); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; waitFor = InternalTestUtils.waitFor; setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING; setPerformanceMock(createUserTimingPolyfill()); const store = global.store; // Start profiling so that data will actually be recorded. utils.act(() => store.profilerStore.startProfiling()); global.IS_REACT_ACT_ENVIRONMENT = true; }); afterEach(() => { // Verify all logged marks also get cleared. expect(marks).toHaveLength(0); setPerformanceMock(null); }); describe('getLanesFromTransportDecimalBitmask', () => { let getLanesFromTransportDecimalBitmask; beforeEach(() => { getLanesFromTransportDecimalBitmask = require('react-devtools-timeline/src/import-worker/preprocessData').getLanesFromTransportDecimalBitmask; }); // @reactVersion >= 18.0 it('should return array of lane numbers from bitmask string', () => { expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]); expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]); expect(getLanesFromTransportDecimalBitmask('3')).toEqual([0, 1]); expect(getLanesFromTransportDecimalBitmask('1234')).toEqual([ 1, 4, 6, 7, 10, ]); // 2 + 16 + 64 + 128 + 1024 expect( getLanesFromTransportDecimalBitmask('1073741824'), // 0b1000000000000000000000000000000 ).toEqual([30]); expect( getLanesFromTransportDecimalBitmask('2147483647'), // 0b1111111111111111111111111111111 ).toEqual(Array.from(Array(31).keys())); }); // @reactVersion >= 18.0 it('should return empty array if laneBitmaskString is not a bitmask', () => { expect(getLanesFromTransportDecimalBitmask('')).toEqual([]); expect(getLanesFromTransportDecimalBitmask('hello')).toEqual([]); expect(getLanesFromTransportDecimalBitmask('-1')).toEqual([]); expect(getLanesFromTransportDecimalBitmask('-0')).toEqual([]); }); // @reactVersion >= 18.0 it('should ignore lanes outside REACT_TOTAL_NUM_LANES', () => { const REACT_TOTAL_NUM_LANES = require('react-devtools-timeline/src/constants').REACT_TOTAL_NUM_LANES; // Sanity check; this test may need to be updated when the no. of fiber lanes are changed. expect(REACT_TOTAL_NUM_LANES).toBe(31); expect( getLanesFromTransportDecimalBitmask( '4294967297', // 2^32 + 1 ), ).toEqual([0]); }); }); describe('preprocessData', () => { let preprocessData; beforeEach(() => { preprocessData = require('react-devtools-timeline/src/import-worker/preprocessData').default; }); // These should be dynamic to mimic a real profile, // but reprooducible between test runs. let pid = 0; let tid = 0; let startTime = 0; function createUserTimingEntry(data) { return { pid: ++pid, tid: ++tid, ts: ++startTime, ...data, }; } function createProfilerVersionEntry() { const SCHEDULING_PROFILER_VERSION = require('react-devtools-timeline/src/constants').SCHEDULING_PROFILER_VERSION; return createUserTimingEntry({ cat: 'blink.user_timing', name: '--profiler-version-' + SCHEDULING_PROFILER_VERSION, }); } function createReactVersionEntry() { return createUserTimingEntry({ cat: 'blink.user_timing', name: '--react-version-', }); } function createLaneLabelsEntry() { return createUserTimingEntry({ cat: 'blink.user_timing', name: '--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen', }); } function createNativeEventEntry(type, duration) { return createUserTimingEntry({ cat: 'devtools.timeline', name: 'EventDispatch', args: {data: {type}}, dur: duration, tdur: duration, }); } function creactCpuProfilerSample() { return createUserTimingEntry({ args: {data: {startTime: ++startTime}}, cat: 'disabled-by-default-v8.cpu_profiler', id: '0x1', name: 'Profile', ph: 'P', }); } function createBoilerplateEntries() { return [ createProfilerVersionEntry(), createReactVersionEntry(), createLaneLabelsEntry(), ]; } function createUserTimingData(sampleMarks) { const cpuProfilerSample = creactCpuProfilerSample(); const randomSample = createUserTimingEntry({ dur: 100, tdur: 200, ph: 'X', cat: 'disabled-by-default-devtools.timeline', name: 'RunTask', args: {}, }); const userTimingData = [cpuProfilerSample, randomSample]; sampleMarks.forEach(markName => { userTimingData.push({ pid: ++pid, tid: ++tid, ts: ++startTime, args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', }); }); return userTimingData; } beforeEach(() => { tid = 0; pid = 0; startTime = 0; }); // @reactVersion >= 18.0 it('should throw given an empty timeline', async () => { await expect(async () => preprocessData([])).rejects.toThrow(); }); // @reactVersion >= 18.0 it('should throw given a timeline with no Profile event', async () => { const randomSample = createUserTimingEntry({ dur: 100, tdur: 200, ph: 'X', cat: 'disabled-by-default-devtools.timeline', name: 'RunTask', args: {}, }); await expect(async () => preprocessData([randomSample]), ).rejects.toThrow(); }); // @reactVersion >= 18.0 it('should throw given a timeline without an explicit profiler version mark nor any other React marks', async () => { const cpuProfilerSample = creactCpuProfilerSample(); await expect( async () => await preprocessData([cpuProfilerSample]), ).rejects.toThrow( 'Please provide profiling data from an React application', ); }); // @reactVersion >= 18.0 it('should throw given a timeline with React scheduling marks, but without an explicit profiler version mark', async () => { const cpuProfilerSample = creactCpuProfilerSample(); const scheduleRenderSample = createUserTimingEntry({ cat: 'blink.user_timing', name: '--schedule-render-512-', }); const samples = [cpuProfilerSample, scheduleRenderSample]; await expect(async () => await preprocessData(samples)).rejects.toThrow( 'This version of profiling data is not supported', ); }); // @reactVersion >= 18.0 it('should return empty data given a timeline with no React scheduling profiling marks', async () => { const cpuProfilerSample = creactCpuProfilerSample(); const randomSample = createUserTimingEntry({ dur: 100, tdur: 200, ph: 'X', cat: 'disabled-by-default-devtools.timeline', name: 'RunTask', args: {}, }); const data = await preprocessData([ ...createBoilerplateEntries(), cpuProfilerSample, randomSample, ]); expect(data).toMatchInlineSnapshot(` { "batchUIDToMeasuresMap": Map {}, "componentMeasures": [], "duration": 0.005, "flamechart": [], "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", 2 => "InputContinuous", 3 => "DefaultHydration", 4 => "Default", 5 => "TransitionHydration", 6 => "Transition", 7 => "Transition", 8 => "Transition", 9 => "Transition", 10 => "Transition", 11 => "Transition", 12 => "Transition", 13 => "Transition", 14 => "Transition", 15 => "Transition", 16 => "Transition", 17 => "Transition", 18 => "Transition", 19 => "Transition", 20 => "Transition", 21 => "Transition", 22 => "Retry", 23 => "Retry", 24 => "Retry", 25 => "Retry", 26 => "Retry", 27 => "SelectiveHydration", 28 => "IdleHydration", 29 => "Idle", 30 => "Offscreen", }, "laneToReactMeasureMap": Map { 0 => [], 1 => [], 2 => [], 3 => [], 4 => [], 5 => [], 6 => [], 7 => [], 8 => [], 9 => [], 10 => [], 11 => [], 12 => [], 13 => [], 14 => [], 15 => [], 16 => [], 17 => [], 18 => [], 19 => [], 20 => [], 21 => [], 22 => [], 23 => [], 24 => [], 25 => [], 26 => [], 27 => [], 28 => [], 29 => [], 30 => [], }, "nativeEvents": [], "networkMeasures": [], "otherUserTimingMarks": [], "reactVersion": "", "schedulingEvents": [], "snapshotHeight": 0, "snapshots": [], "startTime": 1, "suspenseEvents": [], "thrownErrors": [], } `); }); // @reactVersion >= 18.0 it('should process legacy data format (before lane labels were added)', async () => { const cpuProfilerSample = creactCpuProfilerSample(); // Data below is hard-coded based on an older profile sample. // Should be fine since this is explicitly a legacy-format test. const data = await preprocessData([ ...createBoilerplateEntries(), cpuProfilerSample, createUserTimingEntry({ cat: 'blink.user_timing', name: '--schedule-render-512-', }), createUserTimingEntry({ cat: 'blink.user_timing', name: '--render-start-512', }), createUserTimingEntry({ cat: 'blink.user_timing', name: '--render-stop', }), createUserTimingEntry({ cat: 'blink.user_timing', name: '--commit-start-512', }), createUserTimingEntry({ cat: 'blink.user_timing', name: '--layout-effects-start-512', }), createUserTimingEntry({ cat: 'blink.user_timing', name: '--layout-effects-stop', }), createUserTimingEntry({ cat: 'blink.user_timing', name: '--commit-stop', }), ]); expect(data).toMatchInlineSnapshot(` { "batchUIDToMeasuresMap": Map { 0 => [ { "batchUID": 0, "depth": 0, "duration": 0.005, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.006, "type": "render-idle", }, { "batchUID": 0, "depth": 0, "duration": 0.001, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.006, "type": "render", }, { "batchUID": 0, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.008, "type": "commit", }, { "batchUID": 0, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.009, "type": "layout-effects", }, ], }, "componentMeasures": [], "duration": 0.011, "flamechart": [], "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", 2 => "InputContinuous", 3 => "DefaultHydration", 4 => "Default", 5 => "TransitionHydration", 6 => "Transition", 7 => "Transition", 8 => "Transition", 9 => "Transition", 10 => "Transition", 11 => "Transition", 12 => "Transition", 13 => "Transition", 14 => "Transition", 15 => "Transition", 16 => "Transition", 17 => "Transition", 18 => "Transition", 19 => "Transition", 20 => "Transition", 21 => "Transition", 22 => "Retry", 23 => "Retry", 24 => "Retry", 25 => "Retry", 26 => "Retry", 27 => "SelectiveHydration", 28 => "IdleHydration", 29 => "Idle", 30 => "Offscreen", }, "laneToReactMeasureMap": Map { 0 => [], 1 => [], 2 => [], 3 => [], 4 => [], 5 => [], 6 => [], 7 => [], 8 => [], 9 => [ { "batchUID": 0, "depth": 0, "duration": 0.005, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.006, "type": "render-idle", }, { "batchUID": 0, "depth": 0, "duration": 0.001, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.006, "type": "render", }, { "batchUID": 0, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.008, "type": "commit", }, { "batchUID": 0, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000001001", "timestamp": 0.009, "type": "layout-effects", }, ], 10 => [], 11 => [], 12 => [], 13 => [], 14 => [], 15 => [], 16 => [], 17 => [], 18 => [], 19 => [], 20 => [], 21 => [], 22 => [], 23 => [], 24 => [], 25 => [], 26 => [], 27 => [], 28 => [], 29 => [], 30 => [], }, "nativeEvents": [], "networkMeasures": [], "otherUserTimingMarks": [], "reactVersion": "", "schedulingEvents": [ { "lanes": "0b0000000000000000000000000001001", "timestamp": 0.005, "type": "schedule-render", "warning": null, }, ], "snapshotHeight": 0, "snapshots": [], "startTime": 1, "suspenseEvents": [], "thrownErrors": [], } `); }); it('should process a sample legacy render sequence', async () => { utils.legacyRender(
, document.createElement('div')); const data = await preprocessData([ ...createBoilerplateEntries(), ...createUserTimingData(clearedMarks), ]); expect(data).toMatchInlineSnapshot(` { "batchUIDToMeasuresMap": Map { 0 => [ { "batchUID": 0, "depth": 0, "duration": 0.01, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.006, "type": "render-idle", }, { "batchUID": 0, "depth": 0, "duration": 0.001, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.006, "type": "render", }, { "batchUID": 0, "depth": 0, "duration": 0.008, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.008, "type": "commit", }, { "batchUID": 0, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.014, "type": "layout-effects", }, ], }, "componentMeasures": [], "duration": 0.016, "flamechart": [], "internalModuleSourceToRanges": Map { undefined => [ [ { "columnNumber": 0, "functionName": "filtered", "lineNumber": 0, "source": " at filtered (:0:0)", }, { "columnNumber": 1, "functionName": "filtered", "lineNumber": 1, "source": " at filtered (:1:1)", }, ], ], }, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", 2 => "InputContinuous", 3 => "DefaultHydration", 4 => "Default", 5 => "TransitionHydration", 6 => "Transition", 7 => "Transition", 8 => "Transition", 9 => "Transition", 10 => "Transition", 11 => "Transition", 12 => "Transition", 13 => "Transition", 14 => "Transition", 15 => "Transition", 16 => "Transition", 17 => "Transition", 18 => "Transition", 19 => "Transition", 20 => "Transition", 21 => "Transition", 22 => "Retry", 23 => "Retry", 24 => "Retry", 25 => "Retry", 26 => "Retry", 27 => "SelectiveHydration", 28 => "IdleHydration", 29 => "Idle", 30 => "Offscreen", }, "laneToReactMeasureMap": Map { 0 => [], 1 => [ { "batchUID": 0, "depth": 0, "duration": 0.01, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.006, "type": "render-idle", }, { "batchUID": 0, "depth": 0, "duration": 0.001, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.006, "type": "render", }, { "batchUID": 0, "depth": 0, "duration": 0.008, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.008, "type": "commit", }, { "batchUID": 0, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000000001", "timestamp": 0.014, "type": "layout-effects", }, ], 2 => [], 3 => [], 4 => [], 5 => [], 6 => [], 7 => [], 8 => [], 9 => [], 10 => [], 11 => [], 12 => [], 13 => [], 14 => [], 15 => [], 16 => [], 17 => [], 18 => [], 19 => [], 20 => [], 21 => [], 22 => [], 23 => [], 24 => [], 25 => [], 26 => [], 27 => [], 28 => [], 29 => [], 30 => [], }, "nativeEvents": [], "networkMeasures": [], "otherUserTimingMarks": [], "reactVersion": "", "schedulingEvents": [ { "lanes": "0b0000000000000000000000000000001", "timestamp": 0.005, "type": "schedule-render", "warning": null, }, ], "snapshotHeight": 0, "snapshots": [], "startTime": 4, "suspenseEvents": [], "thrownErrors": [], } `); }); it('should process a sample createRoot render sequence', async () => { function App() { const [didMount, setDidMount] = React.useState(false); React.useEffect(() => { if (!didMount) { setDidMount(true); } }); return true; } const root = ReactDOMClient.createRoot(document.createElement('div')); utils.act(() => root.render()); const data = await preprocessData([ ...createBoilerplateEntries(), ...createUserTimingData(clearedMarks), ]); expect(data).toMatchInlineSnapshot(` { "batchUIDToMeasuresMap": Map { 0 => [ { "batchUID": 0, "depth": 0, "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.006, "type": "render-idle", }, { "batchUID": 0, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.006, "type": "render", }, { "batchUID": 0, "depth": 0, "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.01, "type": "commit", }, { "batchUID": 0, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.016, "type": "layout-effects", }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.019, "type": "passive-effects", }, ], 1 => [ { "batchUID": 1, "depth": 0, "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.024, "type": "render-idle", }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.024, "type": "render", }, { "batchUID": 1, "depth": 0, "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.028, "type": "commit", }, { "batchUID": 1, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.034, "type": "layout-effects", }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.037, "type": "passive-effects", }, ], }, "componentMeasures": [ { "componentName": "App", "duration": 0.001, "timestamp": 0.007, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0.002, "timestamp": 0.02, "type": "passive-effect-mount", "warning": null, }, { "componentName": "App", "duration": 0.001, "timestamp": 0.025, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0.001, "timestamp": 0.038, "type": "passive-effect-mount", "warning": null, }, ], "duration": 0.04, "flamechart": [], "internalModuleSourceToRanges": Map { undefined => [ [ { "columnNumber": 0, "functionName": "filtered", "lineNumber": 0, "source": " at filtered (:0:0)", }, { "columnNumber": 1, "functionName": "filtered", "lineNumber": 1, "source": " at filtered (:1:1)", }, ], ], }, "laneToLabelMap": Map { 0 => "Sync", 1 => "InputContinuousHydration", 2 => "InputContinuous", 3 => "DefaultHydration", 4 => "Default", 5 => "TransitionHydration", 6 => "Transition", 7 => "Transition", 8 => "Transition", 9 => "Transition", 10 => "Transition", 11 => "Transition", 12 => "Transition", 13 => "Transition", 14 => "Transition", 15 => "Transition", 16 => "Transition", 17 => "Transition", 18 => "Transition", 19 => "Transition", 20 => "Transition", 21 => "Transition", 22 => "Retry", 23 => "Retry", 24 => "Retry", 25 => "Retry", 26 => "Retry", 27 => "SelectiveHydration", 28 => "IdleHydration", 29 => "Idle", 30 => "Offscreen", }, "laneToReactMeasureMap": Map { 0 => [], 1 => [], 2 => [], 3 => [], 4 => [], 5 => [ { "batchUID": 0, "depth": 0, "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.006, "type": "render-idle", }, { "batchUID": 0, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.006, "type": "render", }, { "batchUID": 0, "depth": 0, "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.01, "type": "commit", }, { "batchUID": 0, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.016, "type": "layout-effects", }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.019, "type": "passive-effects", }, { "batchUID": 1, "depth": 0, "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.024, "type": "render-idle", }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.024, "type": "render", }, { "batchUID": 1, "depth": 0, "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.028, "type": "commit", }, { "batchUID": 1, "depth": 1, "duration": 0.001, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.034, "type": "layout-effects", }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.037, "type": "passive-effects", }, ], 6 => [], 7 => [], 8 => [], 9 => [], 10 => [], 11 => [], 12 => [], 13 => [], 14 => [], 15 => [], 16 => [], 17 => [], 18 => [], 19 => [], 20 => [], 21 => [], 22 => [], 23 => [], 24 => [], 25 => [], 26 => [], 27 => [], 28 => [], 29 => [], 30 => [], }, "nativeEvents": [], "networkMeasures": [], "otherUserTimingMarks": [], "reactVersion": "", "schedulingEvents": [ { "lanes": "0b0000000000000000000000000000101", "timestamp": 0.005, "type": "schedule-render", "warning": null, }, { "componentName": "App", "lanes": "0b0000000000000000000000000000101", "timestamp": 0.021, "type": "schedule-state-update", "warning": null, }, ], "snapshotHeight": 0, "snapshots": [], "startTime": 4, "suspenseEvents": [], "thrownErrors": [], } `); }); // @reactVersion >= 18.0 it('should error if events and measures are incomplete', async () => { const container = document.createElement('div'); utils.legacyRender(
, container); const invalidMarks = clearedMarks.filter( mark => !mark.includes('render-stop'), ); const invalidUserTimingData = createUserTimingData(invalidMarks); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); preprocessData([ ...createBoilerplateEntries(), ...invalidUserTimingData, ]); expect(error).toHaveBeenCalled(); }); // @reactVersion >= 18.0 it('should error if work is completed without being started', async () => { const container = document.createElement('div'); utils.legacyRender(
, container); const invalidMarks = clearedMarks.filter( mark => !mark.includes('render-start'), ); const invalidUserTimingData = createUserTimingData(invalidMarks); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); preprocessData([ ...createBoilerplateEntries(), ...invalidUserTimingData, ]); expect(error).toHaveBeenCalled(); }); // @reactVersion >= 18.0 it('should populate other user timing marks', async () => { const userTimingData = createUserTimingData([]); userTimingData.push( createUserTimingEntry({ args: {}, cat: 'blink.user_timing', id: '0xcdf75f7c', name: 'VCWithoutImage: root', ph: 'n', scope: 'blink.user_timing', }), ); userTimingData.push( createUserTimingEntry({ cat: 'blink.user_timing', name: '--a-mark-that-looks-like-one-of-ours', ph: 'R', }), ); userTimingData.push( createUserTimingEntry({ cat: 'blink.user_timing', name: 'Some other mark', ph: 'R', }), ); const data = await preprocessData([ ...createBoilerplateEntries(), ...userTimingData, ]); expect(data.otherUserTimingMarks).toMatchInlineSnapshot(` [ { "name": "VCWithoutImage: root", "timestamp": 0.003, }, { "name": "--a-mark-that-looks-like-one-of-ours", "timestamp": 0.004, }, { "name": "Some other mark", "timestamp": 0.005, }, ] `); }); // @reactVersion >= 18.0 it('should include a suspended resource "displayName" if one is set', async () => { let promise = null; let resolvedValue = null; function readValue(value) { if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { promise = Promise.resolve(true).then(() => { resolvedValue = value; }); promise.displayName = 'Testing displayName'; } throw promise; } function Component() { const value = readValue(123); return value; } const testMarks = [creactCpuProfilerSample()]; const root = ReactDOMClient.createRoot(document.createElement('div')); utils.act(() => root.render( , ), ); testMarks.push(...createUserTimingData(clearedMarks)); let data; await utils.actAsync(async () => { data = await preprocessData(testMarks); }); expect(data.suspenseEvents).toHaveLength(1); expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName'); }); describe('warnings', () => { describe('long event handlers', () => { // @reactVersion >= 18.0 it('should not warn when React scedules a (sync) update inside of a short event handler', async () => { function App() { return null; } const testMarks = [ creactCpuProfilerSample(), ...createBoilerplateEntries(), createNativeEventEntry('click', 5), ]; clearPendingMarks(); utils.legacyRender(, document.createElement('div')); testMarks.push(...createUserTimingData(clearedMarks)); const data = await preprocessData(testMarks); const event = data.nativeEvents.find(({type}) => type === 'click'); expect(event.warning).toBe(null); }); // @reactVersion >= 18.0 it('should not warn about long events if the cause was non-React JavaScript', async () => { function App() { return null; } const testMarks = [ creactCpuProfilerSample(), ...createBoilerplateEntries(), createNativeEventEntry('click', 25000), ]; startTime += 2000; clearPendingMarks(); utils.legacyRender(, document.createElement('div')); testMarks.push(...createUserTimingData(clearedMarks)); const data = await preprocessData(testMarks); const event = data.nativeEvents.find(({type}) => type === 'click'); expect(event.warning).toBe(null); }); // @reactVersion >= 18.0 it('should warn when React scedules a long (sync) update inside of an event', async () => { function App() { return null; } const testMarks = [ creactCpuProfilerSample(), ...createBoilerplateEntries(), createNativeEventEntry('click', 25000), ]; clearPendingMarks(); utils.legacyRender(, document.createElement('div')); clearedMarks.forEach(markName => { if (markName === '--render-stop') { // Fake a long running render startTime += 20000; } testMarks.push({ pid: ++pid, tid: ++tid, ts: ++startTime, args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', }); }); const data = await preprocessData(testMarks); const event = data.nativeEvents.find(({type}) => type === 'click'); expect(event.warning).toMatchInlineSnapshot( `"An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work."`, ); }); // @reactVersion >= 18.2 it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', async () => { function Yield({id, value}) { Scheduler.log(`${id}:${value}`); return null; } const testMarks = [ creactCpuProfilerSample(), ...createBoilerplateEntries(), ]; // Advance the clock by some arbitrary amount. startTime += 50000; const root = ReactDOMClient.createRoot( document.createElement('div'), ); // Temporarily turn off the act environment, since we're intentionally using Scheduler instead. global.IS_REACT_ACT_ENVIRONMENT = false; React.startTransition(() => { // Start rendering an async update (but don't finish). root.render( <> , ); }); await waitFor(['A:1']); testMarks.push(...createUserTimingData(clearedMarks)); clearPendingMarks(); // Advance the clock some more to make the pending React update seem long. startTime += 20000; // Fake a long "click" event in the middle // and schedule a sync update that will also flush the previous work. testMarks.push(createNativeEventEntry('click', 25000)); ReactDOM.flushSync(() => { root.render( <> , ); }); assertLog(['A:2', 'B:2']); testMarks.push(...createUserTimingData(clearedMarks)); const data = await preprocessData(testMarks); const event = data.nativeEvents.find(({type}) => type === 'click'); expect(event.warning).toBe(null); }); }); describe('nested updates', () => { // @reactVersion >= 18.2 it('should not warn about short nested (state) updates during layout effects', async () => { function Component() { const [didMount, setDidMount] = React.useState(false); Scheduler.log(`Component ${didMount ? 'update' : 'mount'}`); React.useLayoutEffect(() => { setDidMount(true); }, []); return didMount; } const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => { root.render(); }); assertLog(['Component mount', 'Component update']); const data = await preprocessData([ ...createBoilerplateEntries(), ...createUserTimingData(clearedMarks), ]); const event = data.schedulingEvents.find( ({type}) => type === 'schedule-state-update', ); expect(event.warning).toBe(null); }); // @reactVersion >= 18.2 it('should not warn about short (forced) updates during layout effects', async () => { class Component extends React.Component { _didMount: boolean = false; componentDidMount() { this._didMount = true; this.forceUpdate(); } render() { Scheduler.log( `Component ${this._didMount ? 'update' : 'mount'}`, ); return null; } } const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => { root.render(); }); assertLog(['Component mount', 'Component update']); const data = await preprocessData([ ...createBoilerplateEntries(), ...createUserTimingData(clearedMarks), ]); const event = data.schedulingEvents.find( ({type}) => type === 'schedule-force-update', ); expect(event.warning).toBe(null); }); // This is temporarily disabled because the warning doesn't work // with useDeferredValue it.skip('should warn about long nested (state) updates during layout effects', async () => { function Component() { const [didMount, setDidMount] = React.useState(false); Scheduler.log(`Component ${didMount ? 'update' : 'mount'}`); // Fake a long render startTime += 20000; React.useLayoutEffect(() => { setDidMount(true); }, []); return didMount; } const cpuProfilerSample = creactCpuProfilerSample(); const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => { root.render(); }); assertLog(['Component mount', 'Component update']); const testMarks = []; clearedMarks.forEach(markName => { if (markName === '--component-render-start-Component') { // Fake a long running render startTime += 20000; } testMarks.push({ pid: ++pid, tid: ++tid, ts: ++startTime, args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', }); }); const data = await preprocessData([ cpuProfilerSample, ...createBoilerplateEntries(), ...testMarks, ]); const event = data.schedulingEvents.find( ({type}) => type === 'schedule-state-update', ); expect(event.warning).toMatchInlineSnapshot( `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`, ); }); // This is temporarily disabled because the warning doesn't work // with useDeferredValue it.skip('should warn about long nested (forced) updates during layout effects', async () => { class Component extends React.Component { _didMount: boolean = false; componentDidMount() { this._didMount = true; this.forceUpdate(); } render() { Scheduler.log( `Component ${this._didMount ? 'update' : 'mount'}`, ); return null; } } const cpuProfilerSample = creactCpuProfilerSample(); const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => { root.render(); }); assertLog(['Component mount', 'Component update']); const testMarks = []; clearedMarks.forEach(markName => { if (markName === '--component-render-start-Component') { // Fake a long running render startTime += 20000; } testMarks.push({ pid: ++pid, tid: ++tid, ts: ++startTime, args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', }); }); const data = await preprocessData([ cpuProfilerSample, ...createBoilerplateEntries(), ...testMarks, ]); const event = data.schedulingEvents.find( ({type}) => type === 'schedule-force-update', ); expect(event.warning).toMatchInlineSnapshot( `"A big nested update was scheduled during layout. Nested updates require React to re-render synchronously before the browser can paint. Consider delaying this update by moving it to a passive effect (useEffect)."`, ); }); // @reactVersion >= 18.2 it('should not warn about transition updates scheduled during commit phase', async () => { function Component() { const [value, setValue] = React.useState(0); // eslint-disable-next-line no-unused-vars const [isPending, startTransition] = React.useTransition(); Scheduler.log(`Component rendered with value ${value}`); // Fake a long render if (value !== 0) { Scheduler.log('Long render'); startTime += 20000; } React.useLayoutEffect(() => { startTransition(() => { setValue(1); }); }, []); return value; } const cpuProfilerSample = creactCpuProfilerSample(); const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => { root.render(); }); assertLog([ 'Component rendered with value 0', 'Component rendered with value 0', 'Component rendered with value 1', 'Long render', ]); const testMarks = []; clearedMarks.forEach(markName => { if (markName === '--component-render-start-Component') { // Fake a long running render startTime += 20000; } testMarks.push({ pid: ++pid, tid: ++tid, ts: ++startTime, args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', }); }); const data = await preprocessData([ cpuProfilerSample, ...createBoilerplateEntries(), ...testMarks, ]); data.schedulingEvents.forEach(event => { expect(event.warning).toBeNull(); }); }); // This is temporarily disabled because the warning doesn't work // with useDeferredValue it.skip('should not warn about deferred value updates scheduled during commit phase', async () => { function Component() { const [value, setValue] = React.useState(0); const deferredValue = React.useDeferredValue(value); Scheduler.log( `Component rendered with value ${value} and deferredValue ${deferredValue}`, ); // Fake a long render if (deferredValue !== 0) { Scheduler.log('Long render'); startTime += 20000; } React.useLayoutEffect(() => { setValue(1); }, []); return value + deferredValue; } const cpuProfilerSample = creactCpuProfilerSample(); const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => { root.render(); }); assertLog([ 'Component rendered with value 0 and deferredValue 0', 'Component rendered with value 1 and deferredValue 0', 'Component rendered with value 1 and deferredValue 1', 'Long render', ]); const testMarks = []; clearedMarks.forEach(markName => { if (markName === '--component-render-start-Component') { // Fake a long running render startTime += 20000; } testMarks.push({ pid: ++pid, tid: ++tid, ts: ++startTime, args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', }); }); const data = await preprocessData([ cpuProfilerSample, ...createBoilerplateEntries(), ...testMarks, ]); data.schedulingEvents.forEach(event => { expect(event.warning).toBeNull(); }); }); }); describe('errors thrown while rendering', () => { // @reactVersion >= 18.0 it('shoult parse Errors thrown during render', async () => { jest.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'); } const testMarks = [creactCpuProfilerSample()]; // Mount and commit the app const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => root.render( , ), ); testMarks.push(...createUserTimingData(clearedMarks)); const data = await preprocessData(testMarks); expect(data.thrownErrors).toHaveLength(2); expect(data.thrownErrors[0].message).toMatchInlineSnapshot( '"Expected error"', ); }); }); describe('suspend during an update', () => { // This also tests an edge case where a component suspends while profiling // before the first commit is logged (so the lane-to-labels map will not yet exist). // @reactVersion >= 18.2 it('should warn about suspending during an update', async () => { let promise = null; let resolvedValue = null; function readValue(value) { if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { promise = Promise.resolve(true).then(() => { resolvedValue = value; }); } throw promise; } function Component({shouldSuspend}) { Scheduler.log(`Component ${shouldSuspend}`); if (shouldSuspend) { readValue(123); } return null; } // Mount and commit the app const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => root.render( , ), ); const testMarks = [creactCpuProfilerSample()]; // Start profiling and suspend during a render. utils.act(() => root.render( , ), ); testMarks.push(...createUserTimingData(clearedMarks)); let data; await utils.actAsync(async () => { data = await preprocessData(testMarks); }); expect(data.suspenseEvents).toHaveLength(1); expect(data.suspenseEvents[0].warning).toMatchInlineSnapshot( `"A component suspended during an update which caused a fallback to be shown. Consider using the Transition API to avoid hiding components after they've been mounted."`, ); }); // @reactVersion >= 18.2 it('should not warn about suspending during an transition', async () => { let promise = null; let resolvedValue = null; function readValue(value) { if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { promise = Promise.resolve(true).then(() => { resolvedValue = value; }); } throw promise; } function Component({shouldSuspend}) { Scheduler.log(`Component ${shouldSuspend}`); if (shouldSuspend) { readValue(123); } return null; } // Mount and commit the app const root = ReactDOMClient.createRoot( document.createElement('div'), ); utils.act(() => root.render( , ), ); const testMarks = [creactCpuProfilerSample()]; // Start profiling and suspend during a render. await utils.actAsync(async () => React.startTransition(() => root.render( , ), ), ); testMarks.push(...createUserTimingData(clearedMarks)); let data; await utils.actAsync(async () => { data = await preprocessData(testMarks); }); expect(data.suspenseEvents).toHaveLength(1); expect(data.suspenseEvents[0].warning).toBe(null); }); }); }); // TODO: Add test for snapshot base64 parsing // TODO: Add test for flamechart parsing }); }); // Note the in-memory tests vary slightly (e.g. timestamp values, lane numbers) from the above tests. // That's okay; the important thing is the lane-to-label matches the subsequent events/measures. describe('DevTools hook (in memory)', () => { let store; beforeEach(() => { utils = require('./utils'); utils.beforeEachProfiling(); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); store = global.store; // Start profiling so that data will actually be recorded. utils.act(() => store.profilerStore.startProfiling()); global.IS_REACT_ACT_ENVIRONMENT = true; }); it('should process a sample legacy render sequence', async () => { utils.legacyRender(
, document.createElement('div')); utils.act(() => store.profilerStore.stopProfiling()); const data = store.profilerStore.profilingData?.timelineData; expect(data).toHaveLength(1); const timelineData = data[0]; expect(timelineData).toMatchInlineSnapshot(` { "batchUIDToMeasuresMap": Map { 1 => [ { "batchUID": 1, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "render-idle", }, { "batchUID": 1, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "render", }, { "batchUID": 1, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "commit", }, { "batchUID": 1, "depth": 1, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "layout-effects", }, ], }, "componentMeasures": [], "duration": 20, "flamechart": [], "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 1 => "SyncHydrationLane", 2 => "Sync", 4 => "InputContinuousHydration", 8 => "InputContinuous", 16 => "DefaultHydration", 32 => "Default", 64 => "TransitionHydration", 128 => "Transition", 256 => "Transition", 512 => "Transition", 1024 => "Transition", 2048 => "Transition", 4096 => "Transition", 8192 => "Transition", 16384 => "Transition", 32768 => "Transition", 65536 => "Transition", 131072 => "Transition", 262144 => "Transition", 524288 => "Transition", 1048576 => "Transition", 2097152 => "Transition", 4194304 => "Transition", 8388608 => "Retry", 16777216 => "Retry", 33554432 => "Retry", 67108864 => "Retry", 134217728 => "SelectiveHydration", 268435456 => "IdleHydration", 536870912 => "Idle", 1073741824 => "Offscreen", }, "laneToReactMeasureMap": Map { 1 => [], 2 => [ { "batchUID": 1, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "render-idle", }, { "batchUID": 1, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "render", }, { "batchUID": 1, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "commit", }, { "batchUID": 1, "depth": 1, "duration": 0, "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "layout-effects", }, ], 4 => [], 8 => [], 16 => [], 32 => [], 64 => [], 128 => [], 256 => [], 512 => [], 1024 => [], 2048 => [], 4096 => [], 8192 => [], 16384 => [], 32768 => [], 65536 => [], 131072 => [], 262144 => [], 524288 => [], 1048576 => [], 2097152 => [], 4194304 => [], 8388608 => [], 16777216 => [], 33554432 => [], 67108864 => [], 134217728 => [], 268435456 => [], 536870912 => [], 1073741824 => [], }, "nativeEvents": [], "networkMeasures": [], "otherUserTimingMarks": [], "reactVersion": "", "schedulingEvents": [ { "lanes": "0b0000000000000000000000000000010", "timestamp": 10, "type": "schedule-render", "warning": null, }, ], "snapshotHeight": 0, "snapshots": [], "startTime": -10, "suspenseEvents": [], "thrownErrors": [], } `); }); it('should process a sample createRoot render sequence', async () => { function App() { const [didMount, setDidMount] = React.useState(false); React.useEffect(() => { if (!didMount) { setDidMount(true); } }); return true; } const root = ReactDOMClient.createRoot(document.createElement('div')); utils.act(() => root.render()); utils.act(() => store.profilerStore.stopProfiling()); const data = store.profilerStore.profilingData?.timelineData; expect(data).toHaveLength(1); const timelineData = data[0]; // normalize the location for component stack source // for snapshot testing timelineData.schedulingEvents.forEach(event => { if (event.componentStack) { event.componentStack = normalizeCodeLocInfo(event.componentStack); } }); expect(timelineData).toMatchInlineSnapshot(` { "batchUIDToMeasuresMap": 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": "0b0000000000000000000000000100000", "timestamp": 10, "type": "render-idle", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "render", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "commit", }, { "batchUID": 2, "depth": 1, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "layout-effects", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "passive-effects", }, ], }, "componentMeasures": [ { "componentName": "App", "duration": 0, "timestamp": 10, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0, "timestamp": 10, "type": "passive-effect-mount", "warning": null, }, { "componentName": "App", "duration": 0, "timestamp": 10, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0, "timestamp": 10, "type": "passive-effect-mount", "warning": null, }, ], "duration": 20, "flamechart": [], "internalModuleSourceToRanges": Map {}, "laneToLabelMap": Map { 1 => "SyncHydrationLane", 2 => "Sync", 4 => "InputContinuousHydration", 8 => "InputContinuous", 16 => "DefaultHydration", 32 => "Default", 64 => "TransitionHydration", 128 => "Transition", 256 => "Transition", 512 => "Transition", 1024 => "Transition", 2048 => "Transition", 4096 => "Transition", 8192 => "Transition", 16384 => "Transition", 32768 => "Transition", 65536 => "Transition", 131072 => "Transition", 262144 => "Transition", 524288 => "Transition", 1048576 => "Transition", 2097152 => "Transition", 4194304 => "Transition", 8388608 => "Retry", 16777216 => "Retry", 33554432 => "Retry", 67108864 => "Retry", 134217728 => "SelectiveHydration", 268435456 => "IdleHydration", 536870912 => "Idle", 1073741824 => "Offscreen", }, "laneToReactMeasureMap": Map { 1 => [], 2 => [], 4 => [], 8 => [], 16 => [], 32 => [ { "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", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "render-idle", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "render", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "commit", }, { "batchUID": 2, "depth": 1, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "layout-effects", }, { "batchUID": 2, "depth": 0, "duration": 0, "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "passive-effects", }, ], 64 => [], 128 => [], 256 => [], 512 => [], 1024 => [], 2048 => [], 4096 => [], 8192 => [], 16384 => [], 32768 => [], 65536 => [], 131072 => [], 262144 => [], 524288 => [], 1048576 => [], 2097152 => [], 4194304 => [], 8388608 => [], 16777216 => [], 33554432 => [], 67108864 => [], 134217728 => [], 268435456 => [], 536870912 => [], 1073741824 => [], }, "nativeEvents": [], "networkMeasures": [], "otherUserTimingMarks": [], "reactVersion": "", "schedulingEvents": [ { "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "schedule-render", "warning": null, }, { "componentName": "App", "componentStack": " in App (at **)", "lanes": "0b0000000000000000000000000100000", "timestamp": 10, "type": "schedule-state-update", "warning": null, }, ], "snapshotHeight": 0, "snapshots": [], "startTime": -10, "suspenseEvents": [], "thrownErrors": [], } `); }); }); });