diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index 80935bd69be6a..8f3c2e2c0b1b7 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -105,6 +105,7 @@ export default function EventTooltip({ } else if (schedulingEvent !== null) { return ( @@ -234,9 +235,11 @@ const TooltipNativeEvent = ({ }; const TooltipSchedulingEvent = ({ + data, schedulingEvent, tooltipRef, }: { + data: ReactProfilerData, schedulingEvent: SchedulingEvent, tooltipRef: Return, }) => { @@ -257,8 +260,10 @@ const TooltipSchedulingEvent = ({ case 'schedule-render': case 'schedule-state-update': case 'schedule-force-update': - laneLabels = schedulingEvent.laneLabels; lanes = schedulingEvent.lanes; + laneLabels = lanes.map( + lane => ((data.laneToLabelMap.get(lane): any): string), + ); break; } @@ -366,9 +371,13 @@ const TooltipReactMeasure = ({ return null; } - const {batchUID, duration, timestamp, lanes, laneLabels} = measure; + const {batchUID, duration, timestamp, lanes} = measure; const [startTime, stopTime] = getBatchRange(batchUID, data); + const laneLabels = lanes.map( + lane => ((data.laneToLabelMap.get(lane): any): string), + ); + return (
diff --git a/packages/react-devtools-scheduling-profiler/src/constants.js b/packages/react-devtools-scheduling-profiler/src/constants.js index 2e463ae632d95..e29b8901d81fd 100644 --- a/packages/react-devtools-scheduling-profiler/src/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/constants.js @@ -13,3 +13,6 @@ export { } from 'react-devtools-shared/src/constants.js'; export const REACT_TOTAL_NUM_LANES = 31; + +// Increment this number any time a backwards breaking change is made to the profiler metadata. +export const SCHEDULING_PROFILER_VERSION = 1; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js index 4d9ecfa5d17ee..c7de8f8897c5e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js @@ -161,8 +161,7 @@ export class ComponentMeasuresView extends View { context, visibleArea, visibleArea, - 'center', - COLORS.TEXT_DIM_COLOR, + {fillStyle: COLORS.TEXT_DIM_COLOR, textAlign: 'center'}, ); } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js index f8e4cb99b94c1..976e37ebb9d39 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -16,6 +16,8 @@ import type { ViewRefs, } from '../view-base'; +import {formatDuration} from '../utils/formatting'; +import {drawText} from './utils/text'; import { durationToWidth, positioningScaleFactor, @@ -102,17 +104,19 @@ export class ReactMeasuresView extends View { context: CanvasRenderingContext2D, rect: Rect, measure: ReactMeasure, + nextMeasure: ReactMeasure | null, baseY: number, scaleFactor: number, showGroupHighlight: boolean, showHoverHighlight: boolean, ) { - const {frame} = this; + const {frame, visibleArea} = this; const {timestamp, type, duration} = measure; let fillStyle = null; let hoveredFillStyle = null; let groupSelectedFillStyle = null; + let textFillStyle = null; // We could change the max to 0 and just skip over rendering anything that small, // but this has the effect of making the chart look very empty when zoomed out. @@ -131,11 +135,29 @@ export class ReactMeasuresView extends View { return; // Not in view } + const drawableRect = intersectionOfRects(measureRect, rect); + let textRect = measureRect; + switch (type) { case 'commit': fillStyle = COLORS.REACT_COMMIT; hoveredFillStyle = COLORS.REACT_COMMIT_HOVER; groupSelectedFillStyle = COLORS.REACT_COMMIT_HOVER; + textFillStyle = COLORS.REACT_COMMIT_TEXT; + + // Commit phase rects are overlapped by layout and passive rects, + // and it looks bad if text flows underneath/behind these overlayed rects. + if (nextMeasure != null) { + textRect = { + ...measureRect, + size: { + width: + timestampToPosition(nextMeasure.timestamp, scaleFactor, frame) - + x, + height: REACT_MEASURE_HEIGHT, + }, + }; + } break; case 'render-idle': // We could render idle time as diagonal hashes. @@ -149,22 +171,24 @@ export class ReactMeasuresView extends View { fillStyle = COLORS.REACT_RENDER; hoveredFillStyle = COLORS.REACT_RENDER_HOVER; groupSelectedFillStyle = COLORS.REACT_RENDER_HOVER; + textFillStyle = COLORS.REACT_RENDER_TEXT; break; case 'layout-effects': fillStyle = COLORS.REACT_LAYOUT_EFFECTS; hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER; groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER; + textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT; break; case 'passive-effects': fillStyle = COLORS.REACT_PASSIVE_EFFECTS; hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER; groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER; + textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT; break; default: throw new Error(`Unexpected measure type "${type}"`); } - const drawableRect = intersectionOfRects(measureRect, rect); context.fillStyle = showHoverHighlight ? hoveredFillStyle : showGroupHighlight @@ -176,6 +200,12 @@ export class ReactMeasuresView extends View { drawableRect.size.width, drawableRect.size.height, ); + + if (textFillStyle !== null) { + drawText(formatDuration(duration), context, textRect, visibleArea, { + fillStyle: textFillStyle, + }); + } } draw(context: CanvasRenderingContext2D) { @@ -211,6 +241,27 @@ export class ReactMeasuresView extends View { ); } + // Render lane labels + const label = this._profilerData.laneToLabelMap.get(lane); + if (label == null) { + console.warn(`Could not find label for lane ${lane}.`); + } else { + const labelRect = { + origin: { + x: visibleArea.origin.x, + y: baseY, + }, + size: { + width: visibleArea.size.width, + height: REACT_LANE_HEIGHT, + }, + }; + + drawText(label, context, labelRect, visibleArea, { + fillStyle: COLORS.TEXT_DIM_COLOR, + }); + } + // Draw measures for (let j = 0; j < measuresForLane.length; j++) { const measure = measuresForLane[j]; @@ -222,6 +273,7 @@ export class ReactMeasuresView extends View { context, visibleArea, measure, + measuresForLane[j + 1] || null, baseY, scaleFactor, showGroupHighlight, diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index ec8fec8d1e461..6cda416ad8bb3 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -19,7 +19,7 @@ export const SUSPENSE_EVENT_HEIGHT = 14; export const PENDING_SUSPENSE_EVENT_SIZE = 8; export const REACT_EVENT_DIAMETER = 6; export const USER_TIMING_MARK_SIZE = 8; -export const REACT_MEASURE_HEIGHT = 9; +export const REACT_MEASURE_HEIGHT = 14; export const BORDER_SIZE = 1; export const FLAMECHART_FRAME_HEIGHT = 14; export const TEXT_PADDING = 3; @@ -56,12 +56,16 @@ export let COLORS = { REACT_IDLE_HOVER: '', REACT_RENDER: '', REACT_RENDER_HOVER: '', + REACT_RENDER_TEXT: '', REACT_COMMIT: '', REACT_COMMIT_HOVER: '', + REACT_COMMIT_TEXT: '', REACT_LAYOUT_EFFECTS: '', REACT_LAYOUT_EFFECTS_HOVER: '', + REACT_LAYOUT_EFFECTS_TEXT: '', REACT_PASSIVE_EFFECTS: '', REACT_PASSIVE_EFFECTS_HOVER: '', + REACT_PASSIVE_EFFECTS_TEXT: '', REACT_RESIZE_BAR: '', REACT_RESIZE_BAR_ACTIVE: '', REACT_RESIZE_BAR_BORDER: '', @@ -132,24 +136,36 @@ export function updateColorsToMatchTheme(element: Element): boolean { REACT_RENDER_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-render-hover', ), + REACT_RENDER_TEXT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render-text', + ), REACT_COMMIT: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-commit', ), REACT_COMMIT_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-commit-hover', ), + REACT_COMMIT_TEXT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-commit-text', + ), REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-layout-effects', ), REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-layout-effects-hover', ), + REACT_LAYOUT_EFFECTS_TEXT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-layout-effects-text', + ), REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-passive-effects', ), REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-passive-effects-hover', ), + REACT_PASSIVE_EFFECTS_TEXT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-passive-effects-text', + ), REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'), REACT_RESIZE_BAR_ACTIVE: computedStyle.getPropertyValue( '--color-resize-bar-active', diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js index 229b9f50f8dcd..4cd7d94821589 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js @@ -14,6 +14,19 @@ import {COLORS, FONT_SIZE, TEXT_PADDING} from '../constants'; const cachedTextWidths = new Map(); +export function getTextWidth( + context: CanvasRenderingContext2D, + text: string, +): number { + let measuredWidth = cachedTextWidths.get(text); + if (measuredWidth == null) { + measuredWidth = context.measureText(text).width; + cachedTextWidths.set(text, measuredWidth); + } + + return ((measuredWidth: any): number); +} + export function trimText( context: CanvasRenderingContext2D, text: string, @@ -21,14 +34,7 @@ export function trimText( ): string | null { for (let i = text.length - 1; i >= 0; i--) { const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…'; - - let measuredWidth = cachedTextWidths.get(trimmedText); - if (measuredWidth == null) { - measuredWidth = context.measureText(trimmedText).width; - cachedTextWidths.set(trimmedText, measuredWidth); - } - - if (measuredWidth <= width) { + if (getTextWidth(context, trimmedText) <= width) { return trimmedText; } } @@ -36,18 +42,29 @@ export function trimText( return null; } +type TextConfig = {| + fillStyle?: string, + fontSize?: number, + textAlign?: 'left' | 'center', +|}; + export function drawText( text: string, context: CanvasRenderingContext2D, fullRect: Rect, drawableRect: Rect, - textAlign: 'left' | 'center' = 'left', - fillStyle: string = COLORS.TEXT_COLOR, + config?: TextConfig, ): void { + const { + fillStyle = COLORS.TEXT_COLOR, + fontSize = FONT_SIZE, + textAlign = 'left', + } = config || {}; + if (fullRect.size.width > TEXT_PADDING * 2) { context.textAlign = textAlign; context.textBaseline = 'middle'; - context.font = `${FONT_SIZE}px sans-serif`; + context.font = `${fontSize}px sans-serif`; const {x, y} = fullRect.origin; 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 f47200c6f0e21..f08dcd2512b1e 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 @@ -7,10 +7,15 @@ 'use strict'; +import {getLaneLabels} from 'react-reconciler/src/SchedulingProfiler'; import preprocessData, { getLanesFromTransportDecimalBitmask, } from '../preprocessData'; -import {REACT_TOTAL_NUM_LANES} from '../../constants'; +import { + REACT_TOTAL_NUM_LANES, + SCHEDULING_PROFILER_VERSION, +} from '../../constants'; +import REACT_VERSION from 'shared/ReactVersion'; describe(getLanesFromTransportDecimalBitmask, () => { it('should return array of lane numbers from bitmask string', () => { @@ -109,6 +114,35 @@ describe(preprocessData, () => { }; } + function createProfilerVersionEntry() { + return createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--profiler-version-' + SCHEDULING_PROFILER_VERSION, + }); + } + + function createReactVersionEntry() { + return createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--react-version-' + REACT_VERSION, + }); + } + + function createLaneLabelsEntry() { + return createUserTimingEntry({ + cat: 'blink.user_timing', + name: '--react-lane-labels-' + getLaneLabels().join(','), + }); + } + + function createBoilerplateEntries() { + return [ + createProfilerVersionEntry(), + createReactVersionEntry(), + createLaneLabelsEntry(), + ]; + } + function createUserTimingData(sampleMarks) { const cpuProfilerSample = createUserTimingEntry({ args: {data: {startTime: ++startTime}}, @@ -192,7 +226,6 @@ describe(preprocessData, () => { name: 'Profile', ph: 'P', }); - const randomSample = createUserTimingEntry({ dur: 100, tdur: 200, @@ -202,20 +235,62 @@ describe(preprocessData, () => { args: {}, }); - expect(preprocessData([cpuProfilerSample, randomSample])).toStrictEqual({ - componentMeasures: [], - duration: 0.002, - flamechart: [], - measures: [], - nativeEvents: [], - otherUserTimingMarks: [], - schedulingEvents: [], - startTime: 1, - suspenseEvents: [], - }); + if (gate(flags => flags.enableSchedulingProfiler)) { + const data = preprocessData([ + ...createBoilerplateEntries(), + cpuProfilerSample, + randomSample, + ]); + expect(data).toMatchInlineSnapshot(` + Object { + "componentMeasures": Array [], + "duration": 0.005, + "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 [], + "nativeEvents": Array [], + "otherUserTimingMarks": Array [], + "reactVersion": "17.0.3", + "schedulingEvents": Array [], + "startTime": 1, + "suspenseEvents": Array [], + } + `); + } }); - // NOTE This test doesn't have to be gated because it has hard-coded profiler samples. it('should process legacy data format (before lane labels were added)', () => { const cpuProfilerSample = createUserTimingEntry({ args: {data: {startTime: ++startTime}}, @@ -225,10 +300,11 @@ describe(preprocessData, () => { ph: 'P', }); - expect( + if (gate(flags => flags.enableSchedulingProfiler)) { // Data below is hard-coded based on an older profile sample. // Should be fine since this is explicitly a legacy-format test. - preprocessData([ + const data = preprocessData([ + ...createBoilerplateEntries(), cpuProfilerSample, createUserTimingEntry({ cat: 'blink.user_timing', @@ -258,140 +334,220 @@ describe(preprocessData, () => { cat: 'blink.user_timing', name: '--commit-stop', }), - ]), - ).toStrictEqual({ - componentMeasures: [], - duration: 0.008, - flamechart: [], - measures: [ - { - batchUID: 0, - depth: 0, - duration: 0.005, - laneLabels: [], - lanes: [9], - timestamp: 0.003, - type: 'render-idle', - }, - { - batchUID: 0, - depth: 0, - duration: 0.001, - laneLabels: [], - lanes: [9], - timestamp: 0.003, - type: 'render', - }, - { - batchUID: 0, - depth: 0, - duration: 0.003, - laneLabels: [], - lanes: [9], - timestamp: 0.005, - type: 'commit', + ]); + 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", }, - { - batchUID: 0, - depth: 1, - duration: 0.001, - laneLabels: [], - lanes: [9], - timestamp: 0.006, - type: 'layout-effects', - }, - ], - nativeEvents: [], - otherUserTimingMarks: [], - schedulingEvents: [ - { - laneLabels: [], - lanes: [9], - timestamp: 0.002, - type: 'schedule-render', - warning: null, - }, - ], - startTime: 1, - suspenseEvents: [], - }); + "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 [], + } + `); + } }); - // @gate enableSchedulingProfiler it('should process a sample legacy render sequence', () => { ReactDOM.render(
, document.createElement('div')); - const reactVersion = require('shared/ReactVersion').default; - - const userTimingData = createUserTimingData(clearedMarks); - expect(preprocessData(userTimingData)).toStrictEqual({ - componentMeasures: [], - duration: 0.011, - flamechart: [], - measures: [ - { - batchUID: 0, - depth: 0, - duration: 0.004999999999999999, - laneLabels: ['Sync'], - lanes: [0], - timestamp: 0.006, - type: 'render-idle', - }, - { - batchUID: 0, - depth: 0, - duration: 0.001, - laneLabels: ['Sync'], - lanes: [0], - timestamp: 0.006, - type: 'render', - }, - { - batchUID: 0, - depth: 0, - duration: 0.002999999999999999, - laneLabels: ['Sync'], - lanes: [0], - timestamp: 0.008, - type: 'commit', - }, - { - batchUID: 0, - depth: 1, - duration: 0.0010000000000000009, - laneLabels: ['Sync'], - lanes: [0], - timestamp: 0.009, - type: 'layout-effects', - }, - ], - nativeEvents: [], - otherUserTimingMarks: [ - { - name: '__v3', - timestamp: 0.003, - }, - { - name: `--react-init-${reactVersion}`, - timestamp: 0.004, + if (gate(flags => flags.enableSchedulingProfiler)) { + const data = preprocessData([ + ...createBoilerplateEntries(), + ...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", }, - ], - schedulingEvents: [ - { - laneLabels: ['Sync'], - lanes: [0], - timestamp: 0.005, - type: 'schedule-render', - warning: null, - }, - ], - startTime: 1, - suspenseEvents: [], - }); + "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 [], + } + `); + } }); - // @gate enableSchedulingProfiler it('should process a sample createRoot render sequence', () => { function App() { const [didMount, setDidMount] = React.useState(false); @@ -403,150 +559,199 @@ describe(preprocessData, () => { return true; } - const root = ReactDOM.createRoot(document.createElement('div')); - act(() => root.render()); - - const userTimingData = createUserTimingData(clearedMarks); - expect(preprocessData(userTimingData)).toStrictEqual({ - componentMeasures: [ - { - componentName: 'App', - duration: 0.001, - timestamp: 0.007, - warning: null, - }, - { - componentName: 'App', - duration: 0.0010000000000000009, - timestamp: 0.018, - warning: null, - }, - ], - duration: 0.026, - flamechart: [], - measures: [ - { - batchUID: 0, - depth: 0, - duration: 0.006999999999999999, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.006, - type: 'render-idle', - }, - { - batchUID: 0, - depth: 0, - duration: 0.002999999999999999, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.006, - type: 'render', - }, - { - batchUID: 0, - depth: 0, - duration: 0.002999999999999999, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.01, - type: 'commit', - }, - { - batchUID: 0, - depth: 1, - duration: 0.0010000000000000009, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.011, - type: 'layout-effects', - }, - { - batchUID: 0, - depth: 0, - duration: 0.002, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.014, - type: 'passive-effects', - }, - { - batchUID: 1, - depth: 0, - duration: 0.006999999999999999, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.017, - type: 'render-idle', - }, - { - batchUID: 1, - depth: 0, - duration: 0.002999999999999999, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.017, - type: 'render', - }, - { - batchUID: 1, - depth: 0, - duration: 0.002999999999999999, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.021, - type: 'commit', - }, - { - batchUID: 1, - depth: 1, - duration: 0.0010000000000000009, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.022, - type: 'layout-effects', + if (gate(flags => flags.enableSchedulingProfiler)) { + const root = ReactDOM.createRoot(document.createElement('div')); + act(() => root.render()); + + const data = preprocessData([ + ...createBoilerplateEntries(), + ...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", }, - { - batchUID: 1, - depth: 0, - duration: 0.0009999999999999974, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.025, - type: 'passive-effects', - }, - ], - nativeEvents: [], - otherUserTimingMarks: [ - { - name: '__v3', - timestamp: 0.003, - }, - { - name: '--react-init-17.0.3', - timestamp: 0.004, - }, - ], - schedulingEvents: [ - { - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.005, - type: 'schedule-render', - warning: null, - }, - { - componentName: 'App', - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.015, - type: 'schedule-state-update', - warning: null, - }, - ], - startTime: 1, - suspenseEvents: [], - }); + "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 [], + } + `); + } }); // @gate enableSchedulingProfiler @@ -560,7 +765,7 @@ describe(preprocessData, () => { const invalidUserTimingData = createUserTimingData(invalidMarks); const error = spyOnDevAndProd(console, 'error'); - preprocessData(invalidUserTimingData); + preprocessData([...createBoilerplateEntries(), ...invalidUserTimingData]); expect(error).toHaveBeenCalled(); }); @@ -575,7 +780,7 @@ describe(preprocessData, () => { const invalidUserTimingData = createUserTimingData(invalidMarks); const error = spyOnDevAndProd(console, 'error'); - preprocessData(invalidUserTimingData); + preprocessData([...createBoilerplateEntries(), ...invalidUserTimingData]); expect(error).toHaveBeenCalled(); }); @@ -606,20 +811,26 @@ describe(preprocessData, () => { }), ); - expect(preprocessData(userTimingData).otherUserTimingMarks).toStrictEqual([ - { - 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, - }, + const data = preprocessData([ + ...createBoilerplateEntries(), + ...userTimingData, ]); + expect(data.otherUserTimingMarks).toMatchInlineSnapshot(` + Array [ + Object { + "name": "VCWithoutImage: root", + "timestamp": 0.003, + }, + Object { + "name": "--a-mark-that-looks-like-one-of-ours", + "timestamp": 0.004, + }, + Object { + "name": "Some other mark", + "timestamp": 0.005, + }, + ] + `); }); // 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 d3d47e111bcaa..36beebac0b137 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -17,6 +17,7 @@ import type { BatchUID, Flamechart, NativeEvent, + Phase, ReactLane, ReactComponentMeasure, ReactMeasureType, @@ -24,7 +25,7 @@ import type { SuspenseEvent, } from '../types'; -import {REACT_TOTAL_NUM_LANES} from '../constants'; +import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants'; import InvalidProfileError from './InvalidProfileError'; type MeasureStackElement = {| @@ -52,7 +53,7 @@ const WARNING_STRINGS = { 'An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work.', NESTED_UPDATE: 'A nested update was scheduled during layout. These updates require React to re-render synchronously before the browser can paint.', - SUSPENDD_DURING_UPATE: + SUSPEND_DURING_UPATE: '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.", }; @@ -80,6 +81,23 @@ export function getLanesFromTransportDecimalBitmask( return lanes; } +const laneToLabelMap: Map = new Map(); +function updateLaneToLabelMap(laneLabelTuplesString: string): void { + // These marks appear multiple times in the data; + // We only need to extact them once. + if (laneToLabelMap.size === 0) { + const laneLabelTuples = laneLabelTuplesString.split(','); + for (let laneIndex = 0; laneIndex < laneLabelTuples.length; laneIndex++) { + // The numeric lane value (e.g. 64) isn't important. + // The profiler parses and stores the lane's position within the bitmap, + // (e.g. lane 1 is index 0, lane 16 is index 4). + laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]); + } + } +} + +let profilerVersion = null; + function getLastType(stack: $PropertyType) { if (stack.length > 0) { const {type} = stack[stack.length - 1]; @@ -100,7 +118,6 @@ function markWorkStarted( type: ReactMeasureType, startTime: Milliseconds, lanes: ReactLane[], - laneLabels: Array, currentProfilerData: ReactProfilerData, state: ProcessorState, ) { @@ -115,7 +132,6 @@ function markWorkStarted( batchUID, depth, lanes, - laneLabels, timestamp: startTime, duration: 0, }); @@ -242,7 +258,21 @@ function processTimelineEvent( case 'blink.user_timing': const startTime = (ts - currentProfilerData.startTime) / 1000; - if (name.startsWith('--component-render-start-')) { + if (name.startsWith('--react-version-')) { + const [reactVersion] = name.substr(16).split('-'); + currentProfilerData.reactVersion = reactVersion; + } else if (name.startsWith('--profiler-version-')) { + const [versionString] = name.substr(19).split('-'); + profilerVersion = parseInt(versionString, 10); + if (profilerVersion !== SCHEDULING_PROFILER_VERSION) { + throw new InvalidProfileError( + `This version of profiling data (${versionString}) is not supported by the current profiler.`, + ); + } + } else if (name.startsWith('--react-lane-labels-')) { + const [laneLabelTuplesString] = name.substr(20).split('-'); + updateLaneToLabelMap(laneLabelTuplesString); + } else if (name.startsWith('--component-render-start-')) { const [componentName] = name.substr(25).split('-'); if (state.currentReactComponentMeasure !== null) { @@ -268,48 +298,44 @@ function processTimelineEvent( currentProfilerData.componentMeasures.push(componentMeasure); } } else if (name.startsWith('--schedule-render-')) { - const [laneBitmaskString, laneLabels] = name.substr(18).split('-'); + const [laneBitmaskString] = name.substr(18).split('-'); + currentProfilerData.schedulingEvents.push({ type: 'schedule-render', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), - laneLabels: laneLabels ? laneLabels.split(',') : [], timestamp: startTime, warning: null, }); } else if (name.startsWith('--schedule-forced-update-')) { - const [laneBitmaskString, laneLabels, componentName] = name - .substr(25) - .split('-'); + const [laneBitmaskString, componentName] = name.substr(25).split('-'); let warning = null; if (state.measureStack.find(({type}) => type === 'commit')) { // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold. + // This might be easier to do if we separated warnings into a second pass. warning = WARNING_STRINGS.NESTED_UPDATE; } currentProfilerData.schedulingEvents.push({ type: 'schedule-force-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), - laneLabels: laneLabels ? laneLabels.split(',') : [], componentName, timestamp: startTime, warning, }); } else if (name.startsWith('--schedule-state-update-')) { - const [laneBitmaskString, laneLabels, componentName] = name - .substr(24) - .split('-'); + const [laneBitmaskString, componentName] = name.substr(24).split('-'); let warning = null; if (state.measureStack.find(({type}) => type === 'commit')) { // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold. + // This might be easier to do if we separated warnings into a second pass. warning = WARNING_STRINGS.NESTED_UPDATE; } currentProfilerData.schedulingEvents.push({ type: 'schedule-state-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), - laneLabels: laneLabels ? laneLabels.split(',') : [], componentName, timestamp: startTime, warning, @@ -318,25 +344,18 @@ function processTimelineEvent( // React Events - suspense else if (name.startsWith('--suspense-suspend-')) { - const [id, componentName, ...rest] = name.substr(19).split('-'); + const [id, componentName, phase, laneBitmaskString] = name + .substr(19) + .split('-'); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - // Older versions of the scheduling profiler data didn't contain phase or lane values. - let phase = null; + // TODO 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 (rest.length === 3) { - switch (rest[0]) { - case 'mount': - case 'update': - phase = rest[0]; - break; - } - - if (phase === 'update') { - const laneLabels = rest[2]; - // HACK This is a bit gross but the numeric lane value might change between render versions. - if (!laneLabels.includes('Transition')) { - warning = WARNING_STRINGS.SUSPENDD_DURING_UPATE; - } + if (phase === 'update') { + // HACK This is a bit gross but the numeric lane value might change between render versions. + if (lanes.some(lane => laneToLabelMap.get(lane) === 'Transition')) { + warning = WARNING_STRINGS.SUSPEND_DURING_UPATE; } } @@ -365,7 +384,7 @@ function processTimelineEvent( depth, duration: null, id, - phase, + phase: ((phase: any): Phase), resolution: 'unresolved', resuspendTimestamps: null, timestamp: startTime, @@ -411,15 +430,14 @@ function processTimelineEvent( state.nextRenderShouldGenerateNewBatchID = false; state.batchUID = ((state.uidCounter++: any): BatchUID); } - const [laneBitmaskString, laneLabels] = name.substr(15).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + const [laneBitmaskString] = name.substr(15).split('-'); + throwIfIncomplete('render', state.measureStack); if (getLastType(state.measureStack) !== 'render-idle') { markWorkStarted( 'render-idle', startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], + getLanesFromTransportDecimalBitmask(laneBitmaskString), currentProfilerData, state, ); @@ -427,8 +445,7 @@ function processTimelineEvent( markWorkStarted( 'render', startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], + getLanesFromTransportDecimalBitmask(laneBitmaskString), currentProfilerData, state, ); @@ -472,13 +489,12 @@ function processTimelineEvent( // React Measures - commits else if (name.startsWith('--commit-start-')) { state.nextRenderShouldGenerateNewBatchID = true; - const [laneBitmaskString, laneLabels] = name.substr(15).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + const [laneBitmaskString] = name.substr(15).split('-'); + markWorkStarted( 'commit', startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], + getLanesFromTransportDecimalBitmask(laneBitmaskString), currentProfilerData, state, ); @@ -499,13 +515,12 @@ function processTimelineEvent( // React Measures - layout effects else if (name.startsWith('--layout-effects-start-')) { - const [laneBitmaskString, laneLabels] = name.substr(23).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + const [laneBitmaskString] = name.substr(23).split('-'); + markWorkStarted( 'layout-effects', startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], + getLanesFromTransportDecimalBitmask(laneBitmaskString), currentProfilerData, state, ); @@ -520,13 +535,12 @@ function processTimelineEvent( // React Measures - passive effects else if (name.startsWith('--passive-effects-start-')) { - const [laneBitmaskString, laneLabels] = name.substr(24).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + const [laneBitmaskString] = name.substr(24).split('-'); + markWorkStarted( 'passive-effects', startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], + getLanesFromTransportDecimalBitmask(laneBitmaskString), currentProfilerData, state, ); @@ -610,9 +624,11 @@ export default function preprocessData( componentMeasures: [], duration: 0, flamechart, + laneToLabelMap, measures: [], nativeEvents: [], otherUserTimingMarks: [], + reactVersion: null, schedulingEvents: [], startTime: 0, suspenseEvents: [], @@ -656,6 +672,12 @@ export default function preprocessData( timeline.forEach(event => processTimelineEvent(event, profilerData, state)); + if (profilerVersion === null) { + throw new InvalidProfileError( + `This version of profiling data is not supported by the current profiler.`, + ); + } + // Validate that all events and measures are complete const {measureStack} = state; if (measureStack.length > 0) { diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index 106e4048363a5..edd683043a56e 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -38,7 +38,6 @@ type BaseReactEvent = {| type BaseReactScheduleEvent = {| ...BaseReactEvent, +lanes: ReactLane[], - +laneLabels: string[], |}; export type ReactScheduleRenderEvent = {| ...BaseReactScheduleEvent, @@ -53,12 +52,14 @@ export type ReactScheduleForceUpdateEvent = {| +type: 'schedule-force-update', |}; +export type Phase = 'mount' | 'update'; + export type SuspenseEvent = {| ...BaseReactEvent, depth: number, duration: number | null, +id: string, - +phase: 'mount' | 'update' | null, + +phase: Phase | null, resolution: 'rejected' | 'resolved' | 'unresolved', resuspendTimestamps: Array | null, +type: 'suspense', @@ -84,7 +85,6 @@ export type BatchUID = number; export type ReactMeasure = {| +type: ReactMeasureType, +lanes: ReactLane[], - +laneLabels: string[], +timestamp: Milliseconds, +duration: Milliseconds, +batchUID: BatchUID, @@ -127,9 +127,11 @@ export type ReactProfilerData = {| componentMeasures: ReactComponentMeasure[], duration: number, flamechart: Flamechart, + laneToLabelMap: Map, measures: ReactMeasure[], nativeEvents: NativeEvent[], otherUserTimingMarks: UserTimingMark[], + reactVersion: string | null, schedulingEvents: SchedulingEvent[], startTime: number, suspenseEvents: SuspenseEvent[], diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 1b99d20a54c3a..8394bcf105770 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -105,14 +105,10 @@ class ResizeBar extends View { const drawableRect = intersectionOfRects(labelRect, this.visibleArea); - drawText( - this._label, - context, - labelRect, - drawableRect, - 'center', - COLORS.REACT_RESIZE_BAR_DOT, - ); + drawText(this._label, context, labelRect, drawableRect, { + fillStyle: COLORS.REACT_RESIZE_BAR_DOT, + textAlign: 'center', + }); } else { // Otherwise draw horizontally centered resize bar dots context.beginPath(); diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 8afcf85d41519..2476ce32839d3 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -149,12 +149,16 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-scheduling-profiler-react-idle-hover': '#c3d9ef', '--color-scheduling-profiler-react-render': '#9fc3f3', '--color-scheduling-profiler-react-render-hover': '#83afe9', + '--color-scheduling-profiler-react-render-text': '#11365e', '--color-scheduling-profiler-react-commit': '#c88ff0', '--color-scheduling-profiler-react-commit-hover': '#b281d6', + '--color-scheduling-profiler-react-commit-text': '#3e2c4a', '--color-scheduling-profiler-react-layout-effects': '#b281d6', '--color-scheduling-profiler-react-layout-effects-hover': '#9d71bd', + '--color-scheduling-profiler-react-layout-effects-text': '#3e2c4a', '--color-scheduling-profiler-react-passive-effects': '#b281d6', '--color-scheduling-profiler-react-passive-effects-hover': '#9d71bd', + '--color-scheduling-profiler-react-passive-effects-text': '#3e2c4a', '--color-scheduling-profiler-react-schedule': '#9fc3f3', '--color-scheduling-profiler-react-schedule-hover': '#2683E2', '--color-scheduling-profiler-react-suspense-rejected': '#f1cc14', @@ -280,12 +284,16 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-scheduling-profiler-react-idle-hover': '#465269', '--color-scheduling-profiler-react-render': '#2683E2', '--color-scheduling-profiler-react-render-hover': '#1a76d4', + '--color-scheduling-profiler-react-render-text': '#11365e', '--color-scheduling-profiler-react-commit': '#731fad', '--color-scheduling-profiler-react-commit-hover': '#611b94', + '--color-scheduling-profiler-react-commit-text': '#e5c1ff', '--color-scheduling-profiler-react-layout-effects': '#611b94', '--color-scheduling-profiler-react-layout-effects-hover': '#51167a', + '--color-scheduling-profiler-react-layout-effects-text': '#e5c1ff', '--color-scheduling-profiler-react-passive-effects': '#611b94', '--color-scheduling-profiler-react-passive-effects-hover': '#51167a', + '--color-scheduling-profiler-react-passive-effects-text': '#e5c1ff', '--color-scheduling-profiler-react-schedule': '#2683E2', '--color-scheduling-profiler-react-schedule-hover': '#1a76d4', '--color-scheduling-profiler-react-suspense-rejected': '#f1cc14', diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 59e04b1865d4d..2ad6b6a69a495 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -25,7 +25,7 @@ import { import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; -// Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler. +// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler. // If those values are changed that package should be rebuilt and redeployed. export const TotalLanes = 31; @@ -80,46 +80,44 @@ export const OffscreenLane: Lane = /* */ 0b1000000000000000000 // This function is used for the experimental scheduling profiler (react-devtools-scheduling-profiler) // It should be kept in sync with the Lanes values above. -export function getLabelsForLanes(lanes: Lanes): Array | void { +export function getLabelForLane(lane: Lane): string | void { if (enableSchedulingProfiler) { - const labels = []; - if (lanes & SyncLane) { - labels.push('Sync'); + if (lane & SyncLane) { + return 'Sync'; } - if (lanes & InputContinuousHydrationLane) { - labels.push('InputContinuousHydration'); + if (lane & InputContinuousHydrationLane) { + return 'InputContinuousHydration'; } - if (lanes & InputContinuousLane) { - labels.push('InputContinuous'); + if (lane & InputContinuousLane) { + return 'InputContinuous'; } - if (lanes & DefaultHydrationLane) { - labels.push('DefaultHydration'); + if (lane & DefaultHydrationLane) { + return 'DefaultHydration'; } - if (lanes & DefaultLane) { - labels.push('Default'); + if (lane & DefaultLane) { + return 'Default'; } - if (lanes & TransitionHydrationLane) { - labels.push('TransitionHydration'); + if (lane & TransitionHydrationLane) { + return 'TransitionHydration'; } - if (lanes & TransitionLanes) { - labels.push('Transition(s)'); + if (lane & TransitionLanes) { + return 'Transition'; } - if (lanes & RetryLanes) { - labels.push('Retry(s)'); + if (lane & RetryLanes) { + return 'Retry'; } - if (lanes & SelectiveHydrationLane) { - labels.push('SelectiveHydration'); + if (lane & SelectiveHydrationLane) { + return 'SelectiveHydration'; } - if (lanes & IdleHydrationLane) { - labels.push('IdleHydration'); + if (lane & IdleHydrationLane) { + return 'IdleHydration'; } - if (lanes & IdleLane) { - labels.push('Idle'); + if (lane & IdleLane) { + return 'Idle'; } - if (lanes & OffscreenLane) { - labels.push('Offscreen'); + if (lane & OffscreenLane) { + return 'Offscreen'; } - return labels; } } diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 458d3ec6290ec..3e704f54e6761 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -25,7 +25,7 @@ import { import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; -// Lane values below should be kept in sync with getLabelsForLanes(), used by react-devtools-scheduling-profiler. +// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler. // If those values are changed that package should be rebuilt and redeployed. export const TotalLanes = 31; @@ -80,46 +80,44 @@ export const OffscreenLane: Lane = /* */ 0b1000000000000000000 // This function is used for the experimental scheduling profiler (react-devtools-scheduling-profiler) // It should be kept in sync with the Lanes values above. -export function getLabelsForLanes(lanes: Lanes): Array | void { +export function getLabelForLane(lane: Lane): string | void { if (enableSchedulingProfiler) { - const labels = []; - if (lanes & SyncLane) { - labels.push('Sync'); + if (lane & SyncLane) { + return 'Sync'; } - if (lanes & InputContinuousHydrationLane) { - labels.push('InputContinuousHydration'); + if (lane & InputContinuousHydrationLane) { + return 'InputContinuousHydration'; } - if (lanes & InputContinuousLane) { - labels.push('InputContinuous'); + if (lane & InputContinuousLane) { + return 'InputContinuous'; } - if (lanes & DefaultHydrationLane) { - labels.push('DefaultHydration'); + if (lane & DefaultHydrationLane) { + return 'DefaultHydration'; } - if (lanes & DefaultLane) { - labels.push('Default'); + if (lane & DefaultLane) { + return 'Default'; } - if (lanes & TransitionHydrationLane) { - labels.push('TransitionHydration'); + if (lane & TransitionHydrationLane) { + return 'TransitionHydration'; } - if (lanes & TransitionLanes) { - labels.push('Transition(s)'); + if (lane & TransitionLanes) { + return 'Transition'; } - if (lanes & RetryLanes) { - labels.push('Retry(s)'); + if (lane & RetryLanes) { + return 'Retry'; } - if (lanes & SelectiveHydrationLane) { - labels.push('SelectiveHydration'); + if (lane & SelectiveHydrationLane) { + return 'SelectiveHydration'; } - if (lanes & IdleHydrationLane) { - labels.push('IdleHydration'); + if (lane & IdleHydrationLane) { + return 'IdleHydration'; } - if (lanes & IdleLane) { - labels.push('Idle'); + if (lane & IdleLane) { + return 'Idle'; } - if (lanes & OffscreenLane) { - labels.push('Offscreen'); + if (lane & OffscreenLane) { + return 'Offscreen'; } - return labels; } } diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index fc7a7499c6014..b4e7f83d3a157 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -17,13 +17,22 @@ import { } from 'shared/ReactFeatureFlags'; import ReactVersion from 'shared/ReactVersion'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import {SCHEDULING_PROFILER_VERSION} from 'react-devtools-scheduling-profiler/src/constants'; -import {getLabelsForLanes as getLabelsForLanes_old} from 'react-reconciler/src/ReactFiberLane.old'; -import {getLabelsForLanes as getLabelsForLanes_new} from 'react-reconciler/src/ReactFiberLane.new'; +import { + getLabelForLane as getLabelForLane_old, + TotalLanes as TotalLanes_old, +} from 'react-reconciler/src/ReactFiberLane.old'; +import { + getLabelForLane as getLabelForLane_new, + TotalLanes as TotalLanes_new, +} from 'react-reconciler/src/ReactFiberLane.new'; -const getLabelsForLanes = enableNewReconciler - ? getLabelsForLanes_new - : getLabelsForLanes_old; +const getLabelForLane = enableNewReconciler + ? getLabelForLane_new + : getLabelForLane_old; + +const TotalLanes = enableNewReconciler ? TotalLanes_new : TotalLanes_old; /** * If performance exists and supports the subset of the User Timing API that we @@ -59,14 +68,24 @@ if (enableSchedulingProfiler) { } } -export function formatLanes(laneOrLanes: Lane | Lanes): string { - let labels = getLabelsForLanes(laneOrLanes); - if (labels != null) { - labels = labels.sort().join(','); - } else { - labels = ''; +const laneLabels: Array = []; + +export function getLaneLabels(): Array { + if (laneLabels.length === 0) { + let lane = 1; + for (let index = 0; index < TotalLanes; index++) { + laneLabels.push(((getLabelForLane(lane): any): string)); + + lane *= 2; + } } - return `${laneOrLanes}-${labels}`; + return laneLabels; +} + +function markLaneToLabelMetadata() { + getLaneLabels(); + + markAndClear(`--react-lane-labels-${laneLabels.join(',')}`); } function markAndClear(name) { @@ -74,17 +93,27 @@ function markAndClear(name) { performance.clearMarks(name); } -// Create a mark on React initialization -if (enableSchedulingProfiler) { - if (supportsUserTimingV3) { - markAndClear(`--react-init-${ReactVersion}`); - } +function markVersionMetadata() { + markAndClear(`--react-version-${ReactVersion}`); + markAndClear(`--profiler-version-${SCHEDULING_PROFILER_VERSION}`); } export function markCommitStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { - markAndClear(`--commit-start-${formatLanes(lanes)}`); + markAndClear(`--commit-start-${lanes}`); + + // Certain types of metadata should be logged infrequently. + // Normally we would log this during module init, + // but there's no guarantee a user is profiling at that time. + // Commits happen infrequently (less than renders or state updates) + // so we log this extra information along with a commit. + // It will likely be logged more than once but that's okay. + // + // TODO Once DevTools supports starting/stopping the profiler, + // we can log this data only once (when started) and remove the per-commit logging. + markVersionMetadata(); + markLaneToLabelMetadata(); } } } @@ -101,6 +130,7 @@ export function markComponentRenderStarted(fiber: Fiber): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; + // TODO (scheduling profiler) Add component stack id markAndClear(`--component-render-start-${componentName}`); } } @@ -137,11 +167,9 @@ export function markComponentSuspended( const id = getWakeableID(wakeable); const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; const phase = fiber.alternate === null ? 'mount' : 'update'; - // TODO (scheduling profiler) Add component stack id if we re-add component stack info. + // TODO (scheduling profiler) Add component stack id markAndClear( - `--suspense-${eventType}-${id}-${componentName}-${phase}-${formatLanes( - lanes, - )}`, + `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}`, ); wakeable.then( () => markAndClear(`--suspense-resolved-${id}-${componentName}`), @@ -154,7 +182,7 @@ export function markComponentSuspended( export function markLayoutEffectsStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { - markAndClear(`--layout-effects-start-${formatLanes(lanes)}`); + markAndClear(`--layout-effects-start-${lanes}`); } } } @@ -170,7 +198,7 @@ export function markLayoutEffectsStopped(): void { export function markPassiveEffectsStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { - markAndClear(`--passive-effects-start-${formatLanes(lanes)}`); + markAndClear(`--passive-effects-start-${lanes}`); } } } @@ -186,7 +214,7 @@ export function markPassiveEffectsStopped(): void { export function markRenderStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { - markAndClear(`--render-start-${formatLanes(lanes)}`); + markAndClear(`--render-start-${lanes}`); } } } @@ -210,7 +238,7 @@ export function markRenderStopped(): void { export function markRenderScheduled(lane: Lane): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { - markAndClear(`--schedule-render-${formatLanes(lane)}`); + markAndClear(`--schedule-render-${lane}`); } } } @@ -219,10 +247,8 @@ export function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO Add component stack id - markAndClear( - `--schedule-forced-update-${formatLanes(lane)}-${componentName}`, - ); + // TODO (scheduling profiler) Add component stack id + markAndClear(`--schedule-forced-update-${lane}-${componentName}`); } } } @@ -231,10 +257,8 @@ export function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO Add component stack id - markAndClear( - `--schedule-state-update-${formatLanes(lane)}-${componentName}`, - ); + // TODO (scheduling profiler) Add component stack id + markAndClear(`--schedule-state-update-${lane}-${componentName}`); } } } diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js index 4b496b732a21d..8680d19b710bf 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js @@ -10,17 +10,6 @@ 'use strict'; -// This test is *.internal so that it can import this shared file. -import ReactVersion from 'shared/ReactVersion'; - -// Hard-coding because importing will not work with bundle tests and to -// avoid leaking exports for lanes that are only imported in this test. -const ReactFiberLane = { - SyncLane: /* */ 0b0000000000000000000000000000001, - DefaultLane: /* */ 0b0000000000000000000000000010000, - TransitionLane1: /* */ 0b0000000000000000000000001000000, -}; - describe('SchedulingProfiler', () => { let React; let ReactTestRenderer; @@ -30,7 +19,6 @@ describe('SchedulingProfiler', () => { let clearedMarks; let featureDetectionMarkName = null; - let formatLanes; let marks; function createUserTimingPolyfill() { @@ -63,16 +51,10 @@ describe('SchedulingProfiler', () => { clearedMarks.splice(0); } - function expectMarksToContain(expectedMarks) { - expect(clearedMarks).toContain(expectedMarks); - } - - function expectMarksToEqual(expectedMarks) { - expect( - clearedMarks[0] === featureDetectionMarkName - ? clearedMarks.slice(1) - : clearedMarks, - ).toEqual(expectedMarks); + function getMarks() { + return clearedMarks[0] === featureDetectionMarkName + ? clearedMarks.slice(1) + : clearedMarks; } beforeEach(() => { @@ -88,9 +70,6 @@ describe('SchedulingProfiler', () => { Scheduler = require('scheduler'); act = require('jest-react').act; - - const SchedulingProfiler = require('react-reconciler/src/SchedulingProfiler'); - formatLanes = SchedulingProfiler.formatLanes; }); afterEach(() => { @@ -103,54 +82,62 @@ describe('SchedulingProfiler', () => { // @gate !enableSchedulingProfiler it('should not mark if enableSchedulingProfiler is false', () => { ReactTestRenderer.create(
); - expectMarksToEqual([]); + expect(getMarks()).toEqual([]); }); - // @gate enableSchedulingProfiler - it('should log React version on initialization', () => { - expectMarksToEqual([`--react-init-${ReactVersion}`]); - }); - - // @gate enableSchedulingProfiler it('should mark sync render without suspends or state updates', () => { ReactTestRenderer.create(
); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`, - `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--layout-effects-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark concurrent render without suspends or state updates', () => { ReactTestRenderer.create(
, {unstable_isConcurrent: true}); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expectMarksToEqual([ - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--layout-effects-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark render yields', async () => { function Bar() { Scheduler.unstable_yieldValue('Bar'); @@ -170,30 +157,31 @@ describe('SchedulingProfiler', () => { // Do one step of work. expect(ReactNoop.flushNextYield()).toEqual(['Foo']); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.TransitionLane1)}`, - `--render-start-${formatLanes(ReactFiberLane.TransitionLane1)}`, - '--component-render-start-Foo', - '--component-render-stop', - '--render-yield', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-64", + "--render-start-64", + "--component-render-start-Foo", + "--component-render-stop", + "--render-yield", + ] + `); + } } else { ReactNoop.render(); // Do one step of work. expect(ReactNoop.flushNextYield()).toEqual(['Foo']); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--render-yield', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [] + `); + } } }); - // @gate enableSchedulingProfiler it('should mark sync render with suspense that resolves', async () => { const fakeSuspensePromise = Promise.resolve(true); function Example() { @@ -206,27 +194,38 @@ describe('SchedulingProfiler', () => { , ); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`, - `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--suspense-suspend-0-Example-mount-1-Sync', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--layout-effects-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } clearPendingMarks(); await fakeSuspensePromise; - expectMarksToEqual(['--suspense-resolved-0-Example']); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark sync render with suspense that rejects', async () => { const fakeSuspensePromise = Promise.reject(new Error('error')); function Example() { @@ -239,27 +238,38 @@ describe('SchedulingProfiler', () => { , ); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`, - `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--suspense-suspend-0-Example-mount-1-Sync', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--layout-effects-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-1", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-1", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } clearPendingMarks(); await expect(fakeSuspensePromise).rejects.toThrow(); - expectMarksToEqual(['--suspense-rejected-0-Example']); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--suspense-rejected-0-Example", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark concurrent render with suspense that resolves', async () => { const fakeSuspensePromise = Promise.resolve(true); function Example() { @@ -273,34 +283,49 @@ describe('SchedulingProfiler', () => { {unstable_isConcurrent: true}, ); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expectMarksToEqual([ - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--suspense-suspend-0-Example-mount-16-Default', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--layout-effects-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-16", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } clearPendingMarks(); await fakeSuspensePromise; - expectMarksToEqual(['--suspense-resolved-0-Example']); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--suspense-resolved-0-Example", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark concurrent render with suspense that rejects', async () => { const fakeSuspensePromise = Promise.reject(new Error('error')); function Example() { @@ -314,34 +339,49 @@ describe('SchedulingProfiler', () => { {unstable_isConcurrent: true}, ); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expectMarksToEqual([ - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--suspense-suspend-0-Example-mount-16-Default', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--layout-effects-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--suspense-suspend-0-Example-mount-16", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } clearPendingMarks(); await expect(fakeSuspensePromise).rejects.toThrow(); - expectMarksToEqual(['--suspense-rejected-0-Example']); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--suspense-rejected-0-Example", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark cascading class component state updates', () => { class Example extends React.Component { state = {didMount: false}; @@ -355,35 +395,47 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expectMarksToEqual([ - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--schedule-state-update-${formatLanes(ReactFiberLane.SyncLane)}-Example`, - '--layout-effects-stop', - `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--commit-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-state-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark cascading class component force updates', () => { class Example extends React.Component { componentDidMount() { @@ -396,37 +448,47 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expectMarksToEqual([ - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--schedule-forced-update-${formatLanes( - ReactFiberLane.SyncLane, - )}-Example`, - '--layout-effects-stop', - `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--commit-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-forced-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark render phase state updates for class component', () => { class Example extends React.Component { state = {didRender: false}; @@ -440,10 +502,13 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); @@ -451,14 +516,26 @@ describe('SchedulingProfiler', () => { expect(Scheduler).toFlushUntilNextPaint([]); }).toErrorDev('Cannot update during an existing state transition'); - expectMarksToContain( - `--schedule-state-update-${formatLanes( - ReactFiberLane.DefaultLane, - )}-Example`, - ); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--schedule-state-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark render phase force updates for class component', () => { class Example extends React.Component { state = {didRender: false}; @@ -472,10 +549,13 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); @@ -483,14 +563,26 @@ describe('SchedulingProfiler', () => { expect(Scheduler).toFlushUntilNextPaint([]); }).toErrorDev('Cannot update during an existing state transition'); - expectMarksToContain( - `--schedule-forced-update-${formatLanes( - ReactFiberLane.DefaultLane, - )}-Example`, - ); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--schedule-forced-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark cascading layout updates', () => { function Example() { const [didMount, setDidMount] = React.useState(false); @@ -502,37 +594,49 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + ] + `); + } clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expectMarksToEqual([ - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--schedule-state-update-${formatLanes(ReactFiberLane.SyncLane)}-Example`, - '--layout-effects-stop', - `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--commit-stop', - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--schedule-state-update-1-Example", + "--layout-effects-stop", + "--render-start-1", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + "--commit-stop", + ] + `); + } }); // This test is coupled to lane implementation details, so I'm disabling it in // the new fork until it stabilizes so we don't have to repeatedly update it. - // @gate enableSchedulingProfiler it('should mark cascading passive updates', () => { function Example() { const [didMount, setDidMount] = React.useState(false); @@ -546,32 +650,38 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); - expectMarksToEqual([ - `--react-init-${ReactVersion}`, - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--layout-effects-stop', - '--commit-stop', - `--passive-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - `--schedule-state-update-${formatLanes( - ReactFiberLane.DefaultLane, - )}-Example`, - '--passive-effects-stop', - `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--component-render-start-Example', - '--component-render-stop', - '--render-stop', - `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--commit-stop', - ]); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + "--passive-effects-start-16", + "--schedule-state-update-16-Example", + "--passive-effects-stop", + "--render-start-16", + "--component-render-start-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('should mark render phase updates', () => { function Example() { const [didRender, setDidRender] = React.useState(false); @@ -585,10 +695,24 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); - expectMarksToContain( - `--schedule-state-update-${formatLanes( - ReactFiberLane.DefaultLane, - )}-Example`, - ); + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(getMarks()).toMatchInlineSnapshot(` + Array [ + "--schedule-render-16", + "--render-start-16", + "--component-render-start-Example", + "--schedule-state-update-16-Example", + "--component-render-stop", + "--render-stop", + "--commit-start-16", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-16", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } }); }); diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js index 226607eaed2e2..09fdb95b01e6c 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js @@ -15,12 +15,10 @@ describe('SchedulingProfiler labels', () => { let React; let ReactDOM; - let ReactFiberLane; let act; let clearedMarks; let featureDetectionMarkName = null; - let formatLanes; let marks; function polyfillJSDomUserTiming() { @@ -75,14 +73,6 @@ describe('SchedulingProfiler labels', () => { const TestUtils = require('react-dom/test-utils'); act = TestUtils.act; - - const SchedulingProfiler = require('react-reconciler/src/SchedulingProfiler'); - formatLanes = SchedulingProfiler.formatLanes; - - const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFiberLane = ReactFeatureFlags.enableNewReconciler - ? require('react-reconciler/src/ReactFiberLane.new') - : require('react-reconciler/src/ReactFiberLane.old'); }); afterEach(() => { @@ -92,29 +82,45 @@ describe('SchedulingProfiler labels', () => { delete global.performance; }); - // @gate enableSchedulingProfiler it('regression test SyncLane', () => { ReactDOM.render(
, document.createElement('div')); - expect(clearedMarks).toContain( - `--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`, - ); + + if (gate(flags => flags.enableSchedulingProfiler)) { + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "__v3", + "--schedule-render-1", + "--render-start-1", + "--render-stop", + "--commit-start-1", + "--react-version-17.0.3", + "--profiler-version-1", + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--layout-effects-start-1", + "--layout-effects-stop", + "--commit-stop", + ] + `); + } }); - // @gate enableSchedulingProfiler it('regression test DefaultLane', () => { - const container = document.createElement('div'); - const root = ReactDOM.createRoot(container); - - act(() => { - root.render(
); - expect(clearedMarks).toContain( - `--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`, - ); - }); + if (gate(flags => flags.enableSchedulingProfiler)) { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container); + + root.render(
); + expect(clearedMarks).toMatchInlineSnapshot(` + Array [ + "__v3", + "--schedule-render-16", + ] + `); + }); + } }); - // @gate enableSchedulingProfiler - // @gate !enableLegacyFBSupport it('regression test InputDiscreteLane', () => { const container = document.createElement('div'); const root = ReactDOM.createRoot(container); @@ -128,24 +134,41 @@ describe('SchedulingProfiler labels', () => { return