diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index f08dcd2512b1e..5f3a0fc5dbe9b 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -60,6 +60,7 @@ describe(getLanesFromTransportDecimalBitmask, () => { describe(preprocessData, () => { let React; let ReactDOM; + let Scheduler; let act; let clearedMarks; @@ -135,6 +136,26 @@ describe(preprocessData, () => { }); } + 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(), @@ -144,13 +165,7 @@ describe(preprocessData, () => { } function createUserTimingData(sampleMarks) { - const cpuProfilerSample = createUserTimingEntry({ - args: {data: {startTime: ++startTime}}, - cat: 'disabled-by-default-v8.cpu_profiler', - id: '0x1', - name: 'Profile', - ph: 'P', - }); + const cpuProfilerSample = creactCpuProfilerSample(); const randomSample = createUserTimingEntry({ dur: 100, @@ -168,7 +183,7 @@ describe(preprocessData, () => { pid: ++pid, tid: ++tid, ts: ++startTime, - args: {data: {navigationId: 'fake-navigation-id'}}, + args: {data: {}}, cat: 'blink.user_timing', name: markName, ph: 'R', @@ -185,6 +200,7 @@ describe(preprocessData, () => { React = require('react'); ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); const TestUtils = require('react-dom/test-utils'); act = TestUtils.act; @@ -219,13 +235,7 @@ describe(preprocessData, () => { }); it('should return empty data given a timeline with no React scheduling profiling marks', () => { - const cpuProfilerSample = createUserTimingEntry({ - args: {data: {startTime: ++startTime}}, - cat: 'disabled-by-default-v8.cpu_profiler', - id: '0x1', - name: 'Profile', - ph: 'P', - }); + const cpuProfilerSample = creactCpuProfilerSample(); const randomSample = createUserTimingEntry({ dur: 100, tdur: 200, @@ -292,13 +302,7 @@ describe(preprocessData, () => { }); it('should process legacy data format (before lane labels were added)', () => { - const cpuProfilerSample = createUserTimingEntry({ - args: {data: {startTime: ++startTime}}, - cat: 'disabled-by-default-v8.cpu_profiler', - id: '0x1', - name: 'Profile', - ph: 'P', - }); + const cpuProfilerSample = creactCpuProfilerSample(); if (gate(flags => flags.enableSchedulingProfiler)) { // Data below is hard-coded based on an older profile sample. @@ -336,102 +340,102 @@ describe(preprocessData, () => { }), ]); expect(data).toMatchInlineSnapshot(` - Object { - "componentMeasures": Array [], - "duration": 0.011, - "flamechart": Array [], - "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", - }, - "measures": Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.004999999999999999, - "lanes": Array [ - 9, - ], - "timestamp": 0.006, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.001, - "lanes": Array [ - 9, - ], - "timestamp": 0.006, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.002999999999999999, - "lanes": Array [ - 9, - ], - "timestamp": 0.008, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.0010000000000000009, - "lanes": Array [ - 9, - ], - "timestamp": 0.009, - "type": "layout-effects", - }, - ], - "nativeEvents": Array [], - "otherUserTimingMarks": Array [], - "reactVersion": "17.0.3", - "schedulingEvents": Array [ - Object { - "lanes": Array [ - 9, - ], - "timestamp": 0.005, - "type": "schedule-render", - "warning": null, - }, - ], - "startTime": 1, - "suspenseEvents": Array [], - } - `); + Object { + "componentMeasures": Array [], + "duration": 0.011, + "flamechart": Array [], + "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", + }, + "measures": Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.004999999999999999, + "lanes": Array [ + 9, + ], + "timestamp": 0.006, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.001, + "lanes": Array [ + 9, + ], + "timestamp": 0.006, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.002999999999999999, + "lanes": Array [ + 9, + ], + "timestamp": 0.008, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.0010000000000000009, + "lanes": Array [ + 9, + ], + "timestamp": 0.009, + "type": "layout-effects", + }, + ], + "nativeEvents": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "17.0.3", + "schedulingEvents": Array [ + Object { + "lanes": Array [ + 9, + ], + "timestamp": 0.005, + "type": "schedule-render", + "warning": null, + }, + ], + "startTime": 1, + "suspenseEvents": Array [], + } + `); } }); @@ -444,107 +448,107 @@ describe(preprocessData, () => { ...createUserTimingData(clearedMarks), ]); expect(data).toMatchInlineSnapshot(` - Object { - "componentMeasures": Array [], - "duration": 0.013, - "flamechart": Array [], - "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", - }, - "measures": Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.008, - "lanes": Array [ - 0, - ], - "timestamp": 0.005, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.001, - "lanes": Array [ - 0, - ], - "timestamp": 0.005, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.005999999999999999, - "lanes": Array [ - 0, - ], - "timestamp": 0.007, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.0010000000000000009, - "lanes": Array [ - 0, - ], - "timestamp": 0.011, - "type": "layout-effects", - }, - ], - "nativeEvents": Array [], - "otherUserTimingMarks": Array [ - Object { - "name": "__v3", - "timestamp": 0.003, - }, - ], - "reactVersion": "17.0.3", - "schedulingEvents": Array [ - Object { - "lanes": Array [ - 0, - ], - "timestamp": 0.004, - "type": "schedule-render", - "warning": null, - }, - ], - "startTime": 4, - "suspenseEvents": Array [], - } - `); + Object { + "componentMeasures": Array [], + "duration": 0.013, + "flamechart": Array [], + "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", + }, + "measures": Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.008, + "lanes": Array [ + 0, + ], + "timestamp": 0.005, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.001, + "lanes": Array [ + 0, + ], + "timestamp": 0.005, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.005999999999999999, + "lanes": Array [ + 0, + ], + "timestamp": 0.007, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.0010000000000000009, + "lanes": Array [ + 0, + ], + "timestamp": 0.011, + "type": "layout-effects", + }, + ], + "nativeEvents": Array [], + "otherUserTimingMarks": Array [ + Object { + "name": "__v3", + "timestamp": 0.003, + }, + ], + "reactVersion": "17.0.3", + "schedulingEvents": Array [ + Object { + "lanes": Array [ + 0, + ], + "timestamp": 0.004, + "type": "schedule-render", + "warning": null, + }, + ], + "startTime": 4, + "suspenseEvents": Array [], + } + `); } }); @@ -568,189 +572,189 @@ describe(preprocessData, () => { ...createUserTimingData(clearedMarks), ]); expect(data).toMatchInlineSnapshot(` - Object { - "componentMeasures": Array [ - Object { - "componentName": "App", - "duration": 0.001, - "timestamp": 0.006, - "warning": null, - }, - Object { - "componentName": "App", - "duration": 0.0010000000000000009, - "timestamp": 0.02, - "warning": null, - }, - ], - "duration": 0.031, - "flamechart": Array [], - "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", - }, - "measures": Array [ - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.009999999999999998, - "lanes": Array [ - 4, - ], - "timestamp": 0.005, - "type": "render-idle", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.003, - "lanes": Array [ - 4, - ], - "timestamp": 0.005, - "type": "render", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.006, - "lanes": Array [ - 4, - ], - "timestamp": 0.009, - "type": "commit", - }, - Object { - "batchUID": 0, - "depth": 1, - "duration": 0.0010000000000000009, - "lanes": Array [ - 4, - ], - "timestamp": 0.013, - "type": "layout-effects", - }, - Object { - "batchUID": 0, - "depth": 0, - "duration": 0.0019999999999999983, - "lanes": Array [ - 4, - ], - "timestamp": 0.016, - "type": "passive-effects", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.010000000000000002, - "lanes": Array [ - 4, - ], - "timestamp": 0.019, - "type": "render-idle", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.002999999999999999, - "lanes": Array [ - 4, - ], - "timestamp": 0.019, - "type": "render", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.006000000000000002, - "lanes": Array [ - 4, - ], - "timestamp": 0.023, - "type": "commit", - }, - Object { - "batchUID": 1, - "depth": 1, - "duration": 0.0010000000000000009, - "lanes": Array [ - 4, - ], - "timestamp": 0.027, - "type": "layout-effects", - }, - Object { - "batchUID": 1, - "depth": 0, - "duration": 0.0010000000000000009, - "lanes": Array [ - 4, - ], - "timestamp": 0.03, - "type": "passive-effects", - }, - ], - "nativeEvents": Array [], - "otherUserTimingMarks": Array [ - Object { - "name": "__v3", - "timestamp": 0.003, - }, - ], - "reactVersion": "17.0.3", - "schedulingEvents": Array [ - Object { - "lanes": Array [ - 4, - ], - "timestamp": 0.004, - "type": "schedule-render", - "warning": null, - }, - Object { - "componentName": "App", - "lanes": Array [ - 4, - ], - "timestamp": 0.017, - "type": "schedule-state-update", - "warning": null, - }, - ], - "startTime": 4, - "suspenseEvents": Array [], - } - `); + Object { + "componentMeasures": Array [ + Object { + "componentName": "App", + "duration": 0.001, + "timestamp": 0.006, + "warning": null, + }, + Object { + "componentName": "App", + "duration": 0.0010000000000000009, + "timestamp": 0.02, + "warning": null, + }, + ], + "duration": 0.031, + "flamechart": Array [], + "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", + }, + "measures": Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.009999999999999998, + "lanes": Array [ + 4, + ], + "timestamp": 0.005, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.003, + "lanes": Array [ + 4, + ], + "timestamp": 0.005, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.006, + "lanes": Array [ + 4, + ], + "timestamp": 0.009, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.0010000000000000009, + "lanes": Array [ + 4, + ], + "timestamp": 0.013, + "type": "layout-effects", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.0019999999999999983, + "lanes": Array [ + 4, + ], + "timestamp": 0.016, + "type": "passive-effects", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.010000000000000002, + "lanes": Array [ + 4, + ], + "timestamp": 0.019, + "type": "render-idle", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.002999999999999999, + "lanes": Array [ + 4, + ], + "timestamp": 0.019, + "type": "render", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.006000000000000002, + "lanes": Array [ + 4, + ], + "timestamp": 0.023, + "type": "commit", + }, + Object { + "batchUID": 1, + "depth": 1, + "duration": 0.0010000000000000009, + "lanes": Array [ + 4, + ], + "timestamp": 0.027, + "type": "layout-effects", + }, + Object { + "batchUID": 1, + "depth": 0, + "duration": 0.0010000000000000009, + "lanes": Array [ + 4, + ], + "timestamp": 0.03, + "type": "passive-effects", + }, + ], + "nativeEvents": Array [], + "otherUserTimingMarks": Array [ + Object { + "name": "__v3", + "timestamp": 0.003, + }, + ], + "reactVersion": "17.0.3", + "schedulingEvents": Array [ + Object { + "lanes": Array [ + 4, + ], + "timestamp": 0.004, + "type": "schedule-render", + "warning": null, + }, + Object { + "componentName": "App", + "lanes": Array [ + 4, + ], + "timestamp": 0.017, + "type": "schedule-state-update", + "warning": null, + }, + ], + "startTime": 4, + "suspenseEvents": Array [], + } + `); } }); @@ -833,5 +837,156 @@ describe(preprocessData, () => { `); }); + describe('warnings', () => { + describe('long event handlers', () => { + it('should not warn when React scedules a (sync) update inside of a short event handler', () => { + function App() { + return null; + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + createNativeEventEntry('click', 5), + ]; + + clearedMarks.splice(0); + + ReactDOM.render(, document.createElement('div')); + + testMarks.push(...createUserTimingData(clearedMarks)); + + const data = preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toBe(null); + } + }); + + it('should not warn about long events if the cause was non-React JavaScript', () => { + function App() { + return null; + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + createNativeEventEntry('click', 25000), + ]; + + startTime += 2000; + + clearedMarks.splice(0); + + ReactDOM.render(, document.createElement('div')); + + testMarks.push(...createUserTimingData(clearedMarks)); + + const data = preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toBe(null); + } + }); + + it('should warn when React scedules a long (sync) update inside of an event', () => { + function App() { + return null; + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + createNativeEventEntry('click', 25000), + ]; + + clearedMarks.splice(0); + + ReactDOM.render(, 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 = preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toMatchInlineSnapshot( + `"An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work."`, + ); + } + }); + + it('should not warn when React finishes a previously long (async) update with a short (sync) update inside of an event', () => { + function Yield({id, value}) { + Scheduler.unstable_yieldValue(`${id}:${value}`); + return null; + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const testMarks = [ + creactCpuProfilerSample(), + ...createBoilerplateEntries(), + ]; + + // Advance the clock by some arbitrary amount. + startTime += 50000; + + const root = ReactDOM.createRoot(document.createElement('div')); + + React.startTransition(() => { + // Start rendering an async update (but don't finish). + root.render( + <> + + + , + ); + expect(Scheduler).toFlushAndYieldThrough(['A:1']); + + testMarks.push(...createUserTimingData(clearedMarks)); + clearedMarks.splice(0); + + // 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( + <> + + + , + ); + }); + }); + + expect(Scheduler).toHaveYielded(['A:2', 'B:2']); + + testMarks.push(...createUserTimingData(clearedMarks)); + + const data = preprocessData(testMarks); + const event = data.nativeEvents.find(({type}) => type === 'click'); + expect(event.warning).toBe(null); + } + }); + }); + }); + // TODO: Add test for flamechart parsing }); diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index 36beebac0b137..f19f94546938b 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -27,6 +27,7 @@ import type { import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants'; import InvalidProfileError from './InvalidProfileError'; +import {getBatchRange} from '../utils/getBatchRange'; type MeasureStackElement = {| type: ReactMeasureType, @@ -42,6 +43,7 @@ type ProcessorState = {| measureStack: MeasureStackElement[], nativeEventStack: NativeEvent[], nextRenderShouldGenerateNewBatchID: boolean, + potentialLongEvents: Array<[NativeEvent, BatchUID]>, uidCounter: BatchUID, unresolvedSuspenseEvents: Map, |}; @@ -349,7 +351,7 @@ function processTimelineEvent( .split('-'); const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - // TODO It's possible we don't have lane-to-label mapping yet (since it's logged during commit phase) + // TODO (scheduling profiler) It's possible we don't have lane-to-label mapping yet (since it's logged during commit phase) // We may need to do this sort of error checking in a separate pass. let warning = null; if (phase === 'update') { @@ -453,11 +455,14 @@ function processTimelineEvent( for (let i = 0; i < state.nativeEventStack.length; i++) { const nativeEvent = state.nativeEventStack[i]; const stopTime = nativeEvent.timestamp + nativeEvent.duration; - if ( - stopTime > startTime && - nativeEvent.duration > NATIVE_EVENT_DURATION_THRESHOLD - ) { - nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER; + + // If React work was scheduled during an event handler, and the event had a long duration, + // it might be because the React render was long and stretched the event. + // It might also be that the React work was short and that something else stretched the event. + // Make a note of this event for now and we'll examine the batch of React render work later. + // (We can't know until we're done processing the React update anyway.) + if (stopTime > startTime) { + state.potentialLongEvents.push([nativeEvent, state.batchUID]); } } } else if ( @@ -666,6 +671,7 @@ export default function preprocessData( measureStack: [], nativeEventStack: [], nextRenderShouldGenerateNewBatchID: true, + potentialLongEvents: [], uidCounter: 0, unresolvedSuspenseEvents: new Map(), }; @@ -684,5 +690,19 @@ export default function preprocessData( console.error('Incomplete events or measures', measureStack); } + // Check for warnings. + state.potentialLongEvents.forEach(([nativeEvent, batchUID]) => { + // See how long the subsequent batch of React work was. + // Ignore any work that was already started. + const [startTime, stopTime] = getBatchRange( + batchUID, + profilerData, + nativeEvent.timestamp, + ); + if (stopTime - startTime > NATIVE_EVENT_DURATION_THRESHOLD) { + nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER; + } + }); + return profilerData; } diff --git a/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js b/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js index cba61a47161aa..44da98d6b4cf9 100644 --- a/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js +++ b/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js @@ -14,26 +14,30 @@ import type {BatchUID, Milliseconds, ReactProfilerData} from '../types'; function unmemoizedGetBatchRange( batchUID: BatchUID, data: ReactProfilerData, + minStartTime?: number = 0, ): [Milliseconds, Milliseconds] { const {measures} = data; - let startTime = 0; + let startTime = minStartTime; let stopTime = Infinity; let i = 0; + // Find the first measure in the current batch. for (i; i < measures.length; i++) { const measure = measures[i]; if (measure.batchUID === batchUID) { - startTime = measure.timestamp; + startTime = Math.max(startTime, measure.timestamp); break; } } + // Find the last measure in the current batch. for (i; i < measures.length; i++) { const measure = measures[i]; - stopTime = measure.timestamp; - if (measure.batchUID !== batchUID) { + if (measure.batchUID === batchUID) { + stopTime = measure.timestamp; + } else { break; } }