DevTools scheduling profiler: Add React component measures (#22013)
Brian Vaughn committed
Aug 3, 2021 at 13:03 UTC
e3049bb850d8d6395f67d4171870a686e08c3dbd
24 files changed
+603
-39
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+53
@@ -44,6 +44,7 @@ import {
44
zeroPoint,
45
} from './view-base';
46
import {
47
+ ComponentMeasuresView,
48
FlamechartView,
49
NativeEventsView,
50
ReactMeasuresView,
@@ -132,6 +133,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
133
const nativeEventsViewRef = useRef(null);
134
const schedulingEventsViewRef = useRef(null);
135
const suspenseEventsViewRef = useRef(null);
136
+ const componentMeasuresViewRef = useRef(null);
137
const reactMeasuresViewRef = useRef(null);
138
const flamechartViewRef = useRef(null);
139
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
@@ -259,6 +261,17 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
261
true,
262
);
263
264
+ let componentMeasuresViewWrapper = null;
265
+ if (data.componentMeasures.length > 0) {
266
+ const componentMeasuresView = new ComponentMeasuresView(
267
+ surface,
268
+ defaultFrame,
269
+ data,
270
+ );
271
+ componentMeasuresViewRef.current = componentMeasuresView;
272
+ componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
273
+ }
274
+
275
const flamechartView = new FlamechartView(
276
surface,
277
defaultFrame,
@@ -293,6 +306,9 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
306
rootView.addSubview(suspenseEventsViewWrapper);
307
}
308
rootView.addSubview(reactMeasuresViewWrapper);
309
+ if (componentMeasuresViewWrapper !== null) {
310
+ rootView.addSubview(componentMeasuresViewWrapper);
311
+ }
312
rootView.addSubview(flamechartViewWrapper);
313
314
// If subviews are less than the available height, fill remaining height with a solid color.
@@ -323,6 +339,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
339
if (prevHoverEvent === null) {
340
return prevHoverEvent;
341
} else if (
342
+ prevHoverEvent.componentMeasure !== null ||
343
prevHoverEvent.flamechartStackFrame !== null ||
344
prevHoverEvent.measure !== null ||
345
prevHoverEvent.nativeEvent !== null ||
@@ -331,6 +348,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
348
prevHoverEvent.userTimingMark !== null
349
) {
350
return {
351
+ componentMeasure: null,
352
data: prevHoverEvent.data,
353
flamechartStackFrame: null,
354
measure: null,
@@ -378,6 +396,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
396
userTimingMarksView.onHover = userTimingMark => {
397
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
398
setHoveredEvent({
399
+ componentMeasure: null,
400
data,
401
flamechartStackFrame: null,
402
measure: null,
@@ -395,6 +414,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
414
nativeEventsView.onHover = nativeEvent => {
415
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
416
setHoveredEvent({
417
+ componentMeasure: null,
418
data,
419
flamechartStackFrame: null,
420
measure: null,
@@ -412,6 +432,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
432
schedulingEventsView.onHover = schedulingEvent => {
433
if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
434
setHoveredEvent({
435
+ componentMeasure: null,
436
data,
437
flamechartStackFrame: null,
438
measure: null,
@@ -429,6 +450,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
450
suspenseEventsView.onHover = suspenseEvent => {
451
if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
452
setHoveredEvent({
453
+ componentMeasure: null,
454
data,
455
flamechartStackFrame: null,
456
measure: null,
@@ -446,6 +468,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
468
reactMeasuresView.onHover = measure => {
469
if (!hoveredEvent || hoveredEvent.measure !== measure) {
470
setHoveredEvent({
471
+ componentMeasure: null,
472
data,
473
flamechartStackFrame: null,
474
measure,
@@ -458,6 +481,27 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
481
};
482
}
483
484
+ const {current: componentMeasuresView} = componentMeasuresViewRef;
485
+ if (componentMeasuresView) {
486
+ componentMeasuresView.onHover = componentMeasure => {
487
+ if (
488
+ !hoveredEvent ||
489
+ hoveredEvent.componentMeasure !== componentMeasure
490
+ ) {
491
+ setHoveredEvent({
492
+ componentMeasure,
493
+ data,
494
+ flamechartStackFrame: null,
495
+ measure: null,
496
+ nativeEvent: null,
497
+ schedulingEvent: null,
498
+ suspenseEvent: null,
499
+ userTimingMark: null,
500
+ });
501
+ }
502
+ };
503
+ }
504
+
505
const {current: flamechartView} = flamechartViewRef;
506
if (flamechartView) {
507
flamechartView.setOnHover(flamechartStackFrame => {
@@ -466,6 +510,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
510
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
511
) {
512
setHoveredEvent({
513
+ componentMeasure: null,
514
data,
515
flamechartStackFrame,
516
measure: null,
@@ -540,6 +585,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
585
return null;
586
}
587
const {
588
+ componentMeasure,
589
flamechartStackFrame,
590
measure,
591
schedulingEvent,
@@ -547,6 +593,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
593
} = contextData.hoveredEvent;
594
return (
595
<Fragment>
596
+ {componentMeasure !== null && (
597
+ <ContextMenuItem
598
+ onClick={() => copy(componentMeasure.componentName)}
599
+ title="Copy component name">
600
+ Copy component name
601
+ </ContextMenuItem>
602
+ )}
603
{schedulingEvent !== null && (
604
<ContextMenuItem
605
onClick={() => copy(schedulingEvent.componentName)}
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+42
-1
@@ -11,6 +11,7 @@ import type {Point} from './view-base';
11
import type {
12
FlamechartStackFrame,
13
NativeEvent,
14
+ ReactComponentMeasure,
15
ReactHoverContextInfo,
16
ReactMeasure,
17
ReactProfilerData,
@@ -81,6 +82,7 @@ export default function EventTooltip({
82
}
83
84
const {
85
+ componentMeasure,
86
flamechartStackFrame,
87
measure,
88
nativeEvent,
@@ -89,7 +91,14 @@ export default function EventTooltip({
91
userTimingMark,
92
} = hoveredEvent;
93
92
- if (nativeEvent !== null) {
94
+ if (componentMeasure !== null) {
95
+ return (
96
+ <TooltipReactComponentMeasure
97
+ componentMeasure={componentMeasure}
98
+ tooltipRef={tooltipRef}
99
+ />
100
+ );
101
+ } else if (nativeEvent !== null) {
102
return (
103
<TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
104
);
@@ -130,6 +139,38 @@ export default function EventTooltip({
139
return null;
140
}
141
142
+const TooltipReactComponentMeasure = ({
143
+ componentMeasure,
144
+ tooltipRef,
145
+}: {
146
+ componentMeasure: ReactComponentMeasure,
147
+ tooltipRef: Return<typeof useRef>,
148
+}) => {
149
+ const {componentName, duration, timestamp, warning} = componentMeasure;
150
+
151
+ const label = `${componentName} rendered`;
152
+
153
+ return (
154
+ <div className={styles.Tooltip} ref={tooltipRef}>
155
+ <div className={styles.TooltipSection}>
156
+ {trimString(label, 768)}
157
+ <div className={styles.Divider} />
158
+ <div className={styles.DetailsGrid}>
159
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
160
+ <div>{formatTimestamp(timestamp)}</div>
161
+ <div className={styles.DetailsGridLabel}>Duration:</div>
162
+ <div>{formatDuration(duration)}</div>
163
+ </div>
164
+ </div>
165
+ {warning !== null && (
166
+ <div className={styles.TooltipWarningSection}>
167
+ <div className={styles.WarningText}>{warning}</div>
168
+ </div>
169
+ )}
170
+ </div>
171
+ );
172
+};
173
+
174
const TooltipFlamechartNode = ({
175
stackFrame,
176
tooltipRef,
packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
+7
@@ -14,6 +14,7 @@ import createDataResourceFromImportedFile from './createDataResourceFromImported
14
import type {DataResource} from './createDataResourceFromImportedFile';
15
16
export type Context = {|
17
+ clearSchedulingProfilerData: () => void,
18
importSchedulingProfilerData: (file: File) => void,
19
schedulingProfilerData: DataResource | null,
20
|};
@@ -33,6 +34,10 @@ function SchedulingProfilerContextController({children}: Props) {
34
setSchedulingProfilerData,
35
] = useState<DataResource | null>(null);
36
37
+ const clearSchedulingProfilerData = useCallback(() => {
38
+ setSchedulingProfilerData(null);
39
+ }, []);
40
+
41
const importSchedulingProfilerData = useCallback((file: File) => {
42
setSchedulingProfilerData(createDataResourceFromImportedFile(file));
43
}, []);
@@ -41,11 +46,13 @@ function SchedulingProfilerContextController({children}: Props) {
46
47
const value = useMemo(
48
() => ({
49
+ clearSchedulingProfilerData,
50
importSchedulingProfilerData,
51
schedulingProfilerData,
52
// TODO (scheduling profiler)
53
}),
54
[
55
+ clearSchedulingProfilerData,
56
importSchedulingProfilerData,
57
schedulingProfilerData,
58
// TODO (scheduling profiler)
packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js
new
+222
@@ -0,0 +1,222 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactComponentMeasure, ReactProfilerData} from '../types';
11
+import type {
12
+ Interaction,
13
+ IntrinsicSize,
14
+ MouseMoveInteraction,
15
+ Rect,
16
+ ViewRefs,
17
+} from '../view-base';
18
+
19
+import {
20
+ durationToWidth,
21
+ positioningScaleFactor,
22
+ positionToTimestamp,
23
+ timestampToPosition,
24
+} from './utils/positioning';
25
+import {drawText} from './utils/text';
26
+import {formatDuration} from '../utils/formatting';
27
+import {
28
+ View,
29
+ Surface,
30
+ rectContainsPoint,
31
+ rectIntersectsRect,
32
+ intersectionOfRects,
33
+} from '../view-base';
34
+import {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants';
35
+
36
+const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;
37
+
38
+export class ComponentMeasuresView extends View {
39
+ _hoveredComponentMeasure: ReactComponentMeasure | null = null;
40
+ _intrinsicSize: IntrinsicSize;
41
+ _profilerData: ReactProfilerData;
42
+
43
+ onHover: ((event: ReactComponentMeasure | null) => void) | null = null;
44
+
45
+ constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
46
+ super(surface, frame);
47
+
48
+ this._profilerData = profilerData;
49
+
50
+ this._intrinsicSize = {
51
+ width: profilerData.duration,
52
+ height: ROW_WITH_BORDER_HEIGHT,
53
+ };
54
+ }
55
+
56
+ desiredSize() {
57
+ return this._intrinsicSize;
58
+ }
59
+
60
+ setHoveredEvent(hoveredEvent: ReactComponentMeasure | null) {
61
+ if (this._hoveredComponentMeasure === hoveredEvent) {
62
+ return;
63
+ }
64
+ this._hoveredComponentMeasure = hoveredEvent;
65
+ this.setNeedsDisplay();
66
+ }
67
+
68
+ /**
69
+ * Draw a single `ReactComponentMeasure` as a box/span with text inside of it.
70
+ */
71
+ _drawSingleReactComponentMeasure(
72
+ context: CanvasRenderingContext2D,
73
+ rect: Rect,
74
+ componentMeasure: ReactComponentMeasure,
75
+ scaleFactor: number,
76
+ showHoverHighlight: boolean,
77
+ ): boolean {
78
+ const {frame} = this;
79
+ const {componentName, duration, timestamp, warning} = componentMeasure;
80
+
81
+ const xStart = timestampToPosition(timestamp, scaleFactor, frame);
82
+ const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
83
+ const componentMeasureRect: Rect = {
84
+ origin: {
85
+ x: xStart,
86
+ y: frame.origin.y,
87
+ },
88
+ size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT},
89
+ };
90
+ if (!rectIntersectsRect(componentMeasureRect, rect)) {
91
+ return false; // Not in view
92
+ }
93
+
94
+ const width = durationToWidth(duration, scaleFactor);
95
+ if (width < 1) {
96
+ return false; // Too small to render at this zoom level
97
+ }
98
+
99
+ const drawableRect = intersectionOfRects(componentMeasureRect, rect);
100
+ context.beginPath();
101
+ if (warning !== null) {
102
+ context.fillStyle = showHoverHighlight
103
+ ? COLORS.WARNING_BACKGROUND_HOVER
104
+ : COLORS.WARNING_BACKGROUND;
105
+ } else {
106
+ context.fillStyle = showHoverHighlight
107
+ ? COLORS.REACT_COMPONENT_MEASURE_HOVER
108
+ : COLORS.REACT_COMPONENT_MEASURE;
109
+ }
110
+ context.fillRect(
111
+ drawableRect.origin.x,
112
+ drawableRect.origin.y,
113
+ drawableRect.size.width,
114
+ drawableRect.size.height,
115
+ );
116
+
117
+ const label = `${componentName} rendered - ${formatDuration(duration)}`;
118
+
119
+ drawText(label, context, componentMeasureRect, drawableRect);
120
+
121
+ return true;
122
+ }
123
+
124
+ draw(context: CanvasRenderingContext2D) {
125
+ const {
126
+ frame,
127
+ _profilerData: {componentMeasures},
128
+ _hoveredComponentMeasure,
129
+ visibleArea,
130
+ } = this;
131
+
132
+ context.fillStyle = COLORS.BACKGROUND;
133
+ context.fillRect(
134
+ visibleArea.origin.x,
135
+ visibleArea.origin.y,
136
+ visibleArea.size.width,
137
+ visibleArea.size.height,
138
+ );
139
+
140
+ // Draw events
141
+ const scaleFactor = positioningScaleFactor(
142
+ this._intrinsicSize.width,
143
+ frame,
144
+ );
145
+
146
+ let didDrawMeasure = false;
147
+ componentMeasures.forEach(componentMeasure => {
148
+ didDrawMeasure =
149
+ this._drawSingleReactComponentMeasure(
150
+ context,
151
+ visibleArea,
152
+ componentMeasure,
153
+ scaleFactor,
154
+ componentMeasure === _hoveredComponentMeasure,
155
+ ) || didDrawMeasure;
156
+ });
157
+
158
+ if (!didDrawMeasure) {
159
+ drawText(
160
+ '(zoom or pan to see React components)',
161
+ context,
162
+ visibleArea,
163
+ visibleArea,
164
+ 'center',
165
+ COLORS.TEXT_DIM_COLOR,
166
+ );
167
+ }
168
+
169
+ context.fillStyle = COLORS.PRIORITY_BORDER;
170
+ context.fillRect(
171
+ visibleArea.origin.x,
172
+ visibleArea.origin.y + ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
173
+ visibleArea.size.width,
174
+ BORDER_SIZE,
175
+ );
176
+ }
177
+
178
+ /**
179
+ * @private
180
+ */
181
+ _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
182
+ const {frame, _intrinsicSize, onHover, visibleArea} = this;
183
+ if (!onHover) {
184
+ return;
185
+ }
186
+
187
+ const {location} = interaction.payload;
188
+ if (!rectContainsPoint(location, visibleArea)) {
189
+ onHover(null);
190
+ return;
191
+ }
192
+
193
+ const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
194
+ const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
195
+
196
+ const componentMeasures = this._profilerData.componentMeasures;
197
+ for (let index = componentMeasures.length - 1; index >= 0; index--) {
198
+ const componentMeasure = componentMeasures[index];
199
+ const {duration, timestamp} = componentMeasure;
200
+
201
+ if (
202
+ hoverTimestamp >= timestamp &&
203
+ hoverTimestamp <= timestamp + duration
204
+ ) {
205
+ this.currentCursor = 'context-menu';
206
+ viewRefs.hoveredView = this;
207
+ onHover(componentMeasure);
208
+ return;
209
+ }
210
+ }
211
+
212
+ onHover(null);
213
+ }
214
+
215
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
216
+ switch (interaction.type) {
217
+ case 'mousemove':
218
+ this._handleMouseMove(interaction, viewRefs);
219
+ break;
220
+ }
221
+ }
222
+}
packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js
+24
-2
@@ -128,7 +128,7 @@ class FlamechartStackLayerView extends View {
128
visibleArea,
129
} = this;
130
131
- context.fillStyle = COLORS.BACKGROUND;
131
+ context.fillStyle = COLORS.PRIORITY_BACKGROUND;
132
context.fillRect(
133
visibleArea.origin.x,
134
visibleArea.origin.y,
@@ -172,7 +172,29 @@ class FlamechartStackLayerView extends View {
172
drawableRect.size.height,
173
);
174
175
- drawText(name, context, nodeRect, drawableRect, width);
175
+ drawText(name, context, nodeRect, drawableRect);
176
+ }
177
+
178
+ // Render bottom border.
179
+ const borderFrame: Rect = {
180
+ origin: {
181
+ x: frame.origin.x,
182
+ y: frame.origin.y + FLAMECHART_FRAME_HEIGHT - BORDER_SIZE,
183
+ },
184
+ size: {
185
+ width: frame.size.width,
186
+ height: BORDER_SIZE,
187
+ },
188
+ };
189
+ if (rectIntersectsRect(borderFrame, visibleArea)) {
190
+ const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
191
+ context.fillStyle = COLORS.PRIORITY_BORDER;
192
+ context.fillRect(
193
+ borderDrawableRect.origin.x,
194
+ borderDrawableRect.origin.y,
195
+ borderDrawableRect.size.width,
196
+ borderDrawableRect.size.height,
197
+ );
198
}
199
}
200
packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js
+1
-1
@@ -143,7 +143,7 @@ export class NativeEventsView extends View {
143
144
const label = `${type} - ${formatDuration(duration)}`;
145
146
- drawText(label, context, eventRect, drawableRect, width);
146
+ drawText(label, context, eventRect, drawableRect);
147
}
148
149
draw(context: CanvasRenderingContext2D) {
packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
+1
-1
@@ -231,7 +231,7 @@ export class SuspenseEventsView extends View {
231
label += ` - ${formatDuration(duration)}`;
232
}
233
234
- drawText(label, context, eventRect, drawableRect, width);
234
+ drawText(label, context, eventRect, drawableRect);
235
}
236
}
237
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+12
@@ -50,6 +50,8 @@ export let COLORS = {
50
PRIORITY_LABEL: '',
51
USER_TIMING: '',
52
USER_TIMING_HOVER: '',
53
+ REACT_COMPONENT_MEASURE: '',
54
+ REACT_COMPONENT_MEASURE_HOVER: '',
55
REACT_IDLE: '',
56
REACT_IDLE_HOVER: '',
57
REACT_RENDER: '',
@@ -75,6 +77,7 @@ export let COLORS = {
77
REACT_WORK_BORDER: '',
78
SCROLL_CARET: '',
79
TEXT_COLOR: '',
80
+ TEXT_DIM_COLOR: '',
81
TIME_MARKER_LABEL: '',
82
WARNING_BACKGROUND: '',
83
WARNING_BACKGROUND_HOVER: '',
@@ -111,6 +114,12 @@ export function updateColorsToMatchTheme(element: Element): boolean {
114
USER_TIMING_HOVER: computedStyle.getPropertyValue(
115
'--color-scheduling-profiler-user-timing-hover',
116
),
117
+ REACT_COMPONENT_MEASURE: computedStyle.getPropertyValue(
118
+ '--color-scheduling-profiler-react-render',
119
+ ),
120
+ REACT_COMPONENT_MEASURE_HOVER: computedStyle.getPropertyValue(
121
+ '--color-scheduling-profiler-react-render-hover',
122
+ ),
123
REACT_IDLE: computedStyle.getPropertyValue(
124
'--color-scheduling-profiler-react-idle',
125
),
@@ -182,6 +191,9 @@ export function updateColorsToMatchTheme(element: Element): boolean {
191
TEXT_COLOR: computedStyle.getPropertyValue(
192
'--color-scheduling-profiler-text-color',
193
),
194
+ TEXT_DIM_COLOR: computedStyle.getPropertyValue(
195
+ '--color-scheduling-profiler-text-dim-color',
196
+ ),
197
TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'),
198
WARNING_BACKGROUND: computedStyle.getPropertyValue(
199
'--color-warning-background',
packages/react-devtools-scheduling-profiler/src/content-views/index.js
+1
@@ -7,6 +7,7 @@
7
* @flow
8
*/
9
10
+export * from './ComponentMeasuresView';
11
export * from './FlamechartView';
12
export * from './NativeEventsView';
13
export * from './ReactMeasuresView';
packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js
+3
-4
@@ -41,11 +41,10 @@ export function drawText(
41
context: CanvasRenderingContext2D,
42
fullRect: Rect,
43
drawableRect: Rect,
44
- availableWidth: number,
44
textAlign: 'left' | 'center' = 'left',
45
fillStyle: string = COLORS.TEXT_COLOR,
46
): void {
48
- if (availableWidth > TEXT_PADDING * 2) {
47
+ if (fullRect.size.width > TEXT_PADDING * 2) {
48
context.textAlign = textAlign;
49
context.textBaseline = 'middle';
50
context.font = `${FONT_SIZE}px sans-serif`;
@@ -55,7 +54,7 @@ export function drawText(
54
const trimmedName = trimText(
55
context,
56
text,
58
- availableWidth - TEXT_PADDING * 2 + (x < 0 ? x : 0),
57
+ fullRect.size.width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
58
);
59
60
if (trimmedName !== null) {
@@ -81,7 +80,7 @@ export function drawText(
80
81
let textX;
82
if (textAlign === 'center') {
84
- textX = x + availableWidth / 2 + TEXT_PADDING - (x < 0 ? x : 0);
83
+ textX = x + fullRect.size.width / 2 + TEXT_PADDING - (x < 0 ? x : 0);
84
} else {
85
textX = x + TEXT_PADDING - (x < 0 ? x : 0);
86
}
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+31
-14
@@ -203,6 +203,7 @@ describe(preprocessData, () => {
203
});
204
205
expect(preprocessData([cpuProfilerSample, randomSample])).toStrictEqual({
206
+ componentMeasures: [],
207
duration: 0.002,
208
flamechart: [],
209
measures: [],
@@ -259,6 +260,7 @@ describe(preprocessData, () => {
260
}),
261
]),
262
).toStrictEqual({
263
+ componentMeasures: [],
264
duration: 0.008,
265
flamechart: [],
266
measures: [
@@ -323,6 +325,7 @@ describe(preprocessData, () => {
325
326
const userTimingData = createUserTimingData(clearedMarks);
327
expect(preprocessData(userTimingData)).toStrictEqual({
328
+ componentMeasures: [],
329
duration: 0.011,
330
flamechart: [],
331
measures: [
@@ -405,13 +408,27 @@ describe(preprocessData, () => {
408
409
const userTimingData = createUserTimingData(clearedMarks);
410
expect(preprocessData(userTimingData)).toStrictEqual({
408
- duration: 0.022,
411
+ componentMeasures: [
412
+ {
413
+ componentName: 'App',
414
+ duration: 0.001,
415
+ timestamp: 0.007,
416
+ warning: null,
417
+ },
418
+ {
419
+ componentName: 'App',
420
+ duration: 0.0010000000000000009,
421
+ timestamp: 0.018,
422
+ warning: null,
423
+ },
424
+ ],
425
+ duration: 0.026,
426
flamechart: [],
427
measures: [
428
{
429
batchUID: 0,
430
depth: 0,
414
- duration: 0.004999999999999999,
431
+ duration: 0.006999999999999999,
432
laneLabels: ['Default'],
433
lanes: [4],
434
timestamp: 0.006,
@@ -420,7 +437,7 @@ describe(preprocessData, () => {
437
{
438
batchUID: 0,
439
depth: 0,
423
- duration: 0.001,
440
+ duration: 0.002999999999999999,
441
laneLabels: ['Default'],
442
lanes: [4],
443
timestamp: 0.006,
@@ -432,7 +449,7 @@ describe(preprocessData, () => {
449
duration: 0.002999999999999999,
450
laneLabels: ['Default'],
451
lanes: [4],
435
- timestamp: 0.008,
452
+ timestamp: 0.01,
453
type: 'commit',
454
},
455
{
@@ -441,7 +458,7 @@ describe(preprocessData, () => {
458
duration: 0.0010000000000000009,
459
laneLabels: ['Default'],
460
lanes: [4],
444
- timestamp: 0.009,
461
+ timestamp: 0.011,
462
type: 'layout-effects',
463
},
464
{
@@ -450,25 +467,25 @@ describe(preprocessData, () => {
467
duration: 0.002,
468
laneLabels: ['Default'],
469
lanes: [4],
453
- timestamp: 0.012,
470
+ timestamp: 0.014,
471
type: 'passive-effects',
472
},
473
{
474
batchUID: 1,
475
depth: 0,
459
- duration: 0.005000000000000001,
476
+ duration: 0.006999999999999999,
477
laneLabels: ['Default'],
478
lanes: [4],
462
- timestamp: 0.015,
479
+ timestamp: 0.017,
480
type: 'render-idle',
481
},
482
{
483
batchUID: 1,
484
depth: 0,
468
- duration: 0.0010000000000000009,
485
+ duration: 0.002999999999999999,
486
laneLabels: ['Default'],
487
lanes: [4],
471
- timestamp: 0.015,
488
+ timestamp: 0.017,
489
type: 'render',
490
},
491
{
@@ -477,7 +494,7 @@ describe(preprocessData, () => {
494
duration: 0.002999999999999999,
495
laneLabels: ['Default'],
496
lanes: [4],
480
- timestamp: 0.017,
497
+ timestamp: 0.021,
498
type: 'commit',
499
},
500
{
@@ -486,7 +503,7 @@ describe(preprocessData, () => {
503
duration: 0.0010000000000000009,
504
laneLabels: ['Default'],
505
lanes: [4],
489
- timestamp: 0.018,
506
+ timestamp: 0.022,
507
type: 'layout-effects',
508
},
509
{
@@ -495,7 +512,7 @@ describe(preprocessData, () => {
512
duration: 0.0009999999999999974,
513
laneLabels: ['Default'],
514
lanes: [4],
498
- timestamp: 0.021,
515
+ timestamp: 0.025,
516
type: 'passive-effects',
517
},
518
],
@@ -522,7 +539,7 @@ describe(preprocessData, () => {
539
componentName: 'App',
540
laneLabels: ['Default'],
541
lanes: [4],
525
- timestamp: 0.013,
542
+ timestamp: 0.015,
543
type: 'schedule-state-update',
544
warning: null,
545
},
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+30
-2
@@ -18,6 +18,7 @@ import type {
18
Flamechart,
19
NativeEvent,
20
ReactLane,
21
+ ReactComponentMeasure,
22
ReactMeasureType,
23
ReactProfilerData,
24
SuspenseEvent,
@@ -36,6 +37,7 @@ type MeasureStackElement = {|
37
38
type ProcessorState = {|
39
batchUID: BatchUID,
40
+ currentReactComponentMeasure: ReactComponentMeasure | null,
41
measureStack: MeasureStackElement[],
42
nativeEventStack: NativeEvent[],
43
nextRenderShouldGenerateNewBatchID: boolean,
@@ -240,8 +242,32 @@ function processTimelineEvent(
242
case 'blink.user_timing':
243
const startTime = (ts - currentProfilerData.startTime) / 1000;
244
243
- // React Events - schedule
244
- if (name.startsWith('--schedule-render-')) {
245
+ if (name.startsWith('--component-render-start-')) {
246
+ const [componentName] = name.substr(25).split('-');
247
+
248
+ if (state.currentReactComponentMeasure !== null) {
249
+ console.error(
250
+ 'Render started while another render in progress:',
251
+ state.currentReactComponentMeasure,
252
+ );
253
+ }
254
+
255
+ state.currentReactComponentMeasure = {
256
+ componentName,
257
+ timestamp: startTime,
258
+ duration: 0,
259
+ warning: null,
260
+ };
261
+ } else if (name === '--component-render-stop') {
262
+ if (state.currentReactComponentMeasure !== null) {
263
+ const componentMeasure = state.currentReactComponentMeasure;
264
+ componentMeasure.duration = startTime - componentMeasure.timestamp;
265
+
266
+ state.currentReactComponentMeasure = null;
267
+
268
+ currentProfilerData.componentMeasures.push(componentMeasure);
269
+ }
270
+ } else if (name.startsWith('--schedule-render-')) {
271
const [laneBitmaskString, laneLabels] = name.substr(18).split('-');
272
currentProfilerData.schedulingEvents.push({
273
type: 'schedule-render',
@@ -581,6 +607,7 @@ export default function preprocessData(
607
const flamechart = preprocessFlamechart(timeline);
608
609
const profilerData: ReactProfilerData = {
610
+ componentMeasures: [],
611
duration: 0,
612
flamechart,
613
measures: [],
@@ -619,6 +646,7 @@ export default function preprocessData(
646
647
const state: ProcessorState = {
648
batchUID: 0,
649
+ currentReactComponentMeasure: null,
650
measureStack: [],
651
nativeEventStack: [],
652
nextRenderShouldGenerateNewBatchID: true,
packages/react-devtools-scheduling-profiler/src/types.js
+9
@@ -91,6 +91,13 @@ export type ReactMeasure = {|
91
+depth: number,
92
|};
93
94
+export type ReactComponentMeasure = {|
95
+ +componentName: string,
96
+ duration: Milliseconds,
97
+ +timestamp: Milliseconds,
98
+ warning: string | null,
99
+|};
100
+
101
/**
102
* A flamechart stack frame belonging to a stack trace.
103
*/
@@ -117,6 +124,7 @@ export type FlamechartStackLayer = FlamechartStackFrame[];
124
export type Flamechart = FlamechartStackLayer[];
125
126
export type ReactProfilerData = {|
127
+ componentMeasures: ReactComponentMeasure[],
128
duration: number,
129
flamechart: Flamechart,
130
measures: ReactMeasure[],
@@ -128,6 +136,7 @@ export type ReactProfilerData = {|
136
|};
137
138
export type ReactHoverContextInfo = {|
139
+ componentMeasure: ReactComponentMeasure | null,
140
data: $ReadOnly<ReactProfilerData> | null,
141
flamechartStackFrame: FlamechartStackFrame | null,
142
measure: ReactMeasure | null,
packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
-1
@@ -110,7 +110,6 @@ class ResizeBar extends View {
110
context,
111
labelRect,
112
drawableRect,
113
- visibleArea.size.width,
113
'center',
114
COLORS.REACT_RESIZE_BAR_DOT,
115
);
packages/react-devtools-shared/src/constants.js
+3
-1
@@ -164,6 +164,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
164
'--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd',
165
'--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a',
166
'--color-scheduling-profiler-text-color': '#000000',
167
+ '--color-scheduling-profiler-text-dim-color': '#ccc',
168
'--color-scheduling-profiler-react-work-border': '#ffffff',
169
'--color-search-match': 'yellow',
170
'--color-search-match-current': '#f7923b',
@@ -293,7 +294,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
294
'--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281',
295
'--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd',
296
'--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a',
296
- '--color-scheduling-profiler-text-color': '#000000',
297
+ '--color-scheduling-profiler-text-color': '#282c34',
298
+ '--color-scheduling-profiler-text-dim-color': '#555b66',
299
'--color-scheduling-profiler-react-work-border': '#ffffff',
300
'--color-search-match': 'yellow',
301
'--color-search-match-current': '#f7923b',
packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js
+23
-4
@@ -8,22 +8,41 @@
8
*/
9
10
import * as React from 'react';
11
-import {useCallback, useContext} from 'react';
11
+import {useContext} from 'react';
12
import {ProfilerContext} from './ProfilerContext';
13
import Button from '../Button';
14
import ButtonIcon from '../ButtonIcon';
15
import {StoreContext} from '../context';
16
+import {SchedulingProfilerContext} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext';
17
18
export default function ClearProfilingDataButton() {
19
const store = useContext(StoreContext);
19
- const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
20
+ const {didRecordCommits, isProfiling, selectedTabID} = useContext(
21
+ ProfilerContext,
22
+ );
23
+ const {clearSchedulingProfilerData, schedulingProfilerData} = useContext(
24
+ SchedulingProfilerContext,
25
+ );
26
const {profilerStore} = store;
27
22
- const clear = useCallback(() => profilerStore.clear(), [profilerStore]);
28
+ let doesHaveData = false;
29
+ if (selectedTabID === 'scheduling-profiler') {
30
+ doesHaveData = schedulingProfilerData !== null;
31
+ } else {
32
+ doesHaveData = didRecordCommits;
33
+ }
34
+
35
+ const clear = () => {
36
+ if (selectedTabID === 'scheduling-profiler') {
37
+ clearSchedulingProfilerData();
38
+ } else {
39
+ profilerStore.clear();
40
+ }
41
+ };
42
43
return (
44
<Button
26
- disabled={isProfiling || !didRecordCommits}
45
+ disabled={isProfiling || !doesHaveData}
46
onClick={clear}
47
title="Clear profiling data">
48
<ButtonIcon type="clear" />
packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
+10
-2
@@ -103,8 +103,16 @@ function Profiler(_: {||}) {
103
<div className={styles.Profiler}>
104
<div className={styles.LeftColumn}>
105
<div className={styles.Toolbar}>
106
- <RecordToggle disabled={!supportsProfiling} />
107
- <ReloadAndProfileButton />
106
+ <RecordToggle
107
+ disabled={
108
+ !supportsProfiling || selectedTabID === 'scheduling-profiler'
109
+ }
110
+ />
111
+ <ReloadAndProfileButton
112
+ disabled={
113
+ selectedTabID === 'scheduling-profiler' || !supportsProfiling
114
+ }
115
+ />
116
<ClearProfilingDataButton />
117
<ProfilingImportExportButtons />
118
<div className={styles.VRule} />
packages/react-devtools-shared/src/devtools/views/Profiler/ReloadAndProfileButton.js
+6
-2
@@ -19,7 +19,11 @@ type SubscriptionData = {|
19
supportsReloadAndProfile: boolean,
20
|};
21
22
-export default function ReloadAndProfileButton() {
22
+export default function ReloadAndProfileButton({
23
+ disabled,
24
+}: {|
25
+ disabled: boolean,
26
+|}) {
27
const bridge = useContext(BridgeContext);
28
const store = useContext(StoreContext);
29
@@ -61,7 +65,7 @@ export default function ReloadAndProfileButton() {
65
66
return (
67
<Button
64
- disabled={!store.supportsProfiling}
68
+ disabled={disabled}
69
onClick={reloadAndProfile}
70
title="Reload and start profiling">
71
<ButtonIcon type="reload" />
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+36
-1
@@ -31,7 +31,10 @@ import type {
31
import type {UpdateQueue} from './ReactUpdateQueue.new';
32
33
import checkPropTypes from 'shared/checkPropTypes';
34
-
34
+import {
35
+ markComponentRenderStarted,
36
+ markComponentRenderStopped,
37
+} from './SchedulingProfiler';
38
import {
39
IndeterminateComponent,
40
FunctionComponent,
@@ -85,6 +88,7 @@ import {
88
enableCache,
89
enableLazyContextPropagation,
90
enableSuspenseLayoutEffectSemantics,
91
+ enableSchedulingProfiler,
92
} from 'shared/ReactFeatureFlags';
93
import invariant from 'shared/invariant';
94
import isArray from 'shared/isArray';
@@ -357,6 +361,9 @@ function updateForwardRef(
361
// The rest is a fork of updateFunctionComponent
362
let nextChildren;
363
prepareToReadContext(workInProgress, renderLanes);
364
+ if (enableSchedulingProfiler) {
365
+ markComponentRenderStarted(workInProgress);
366
+ }
367
if (__DEV__) {
368
ReactCurrentOwner.current = workInProgress;
369
setIsRendering(true);
@@ -397,6 +404,9 @@ function updateForwardRef(
404
renderLanes,
405
);
406
}
407
+ if (enableSchedulingProfiler) {
408
+ markComponentRenderStopped();
409
+ }
410
411
if (current !== null && !didReceiveUpdate) {
412
bailoutHooks(current, workInProgress, renderLanes);
@@ -958,6 +968,9 @@ function updateFunctionComponent(
968
969
let nextChildren;
970
prepareToReadContext(workInProgress, renderLanes);
971
+ if (enableSchedulingProfiler) {
972
+ markComponentRenderStarted(workInProgress);
973
+ }
974
if (__DEV__) {
975
ReactCurrentOwner.current = workInProgress;
976
setIsRendering(true);
@@ -998,6 +1011,9 @@ function updateFunctionComponent(
1011
renderLanes,
1012
);
1013
}
1014
+ if (enableSchedulingProfiler) {
1015
+ markComponentRenderStopped();
1016
+ }
1017
1018
if (current !== null && !didReceiveUpdate) {
1019
bailoutHooks(current, workInProgress, renderLanes);
@@ -1177,6 +1193,9 @@ function finishClassComponent(
1193
stopProfilerTimerIfRunning(workInProgress);
1194
}
1195
} else {
1196
+ if (enableSchedulingProfiler) {
1197
+ markComponentRenderStarted(workInProgress);
1198
+ }
1199
if (__DEV__) {
1200
setIsRendering(true);
1201
nextChildren = instance.render();
@@ -1195,6 +1214,9 @@ function finishClassComponent(
1214
} else {
1215
nextChildren = instance.render();
1216
}
1217
+ if (enableSchedulingProfiler) {
1218
+ markComponentRenderStopped();
1219
+ }
1220
}
1221
1222
// React DevTools reads this flag.
@@ -1567,6 +1589,9 @@ function mountIndeterminateComponent(
1589
prepareToReadContext(workInProgress, renderLanes);
1590
let value;
1591
1592
+ if (enableSchedulingProfiler) {
1593
+ markComponentRenderStarted(workInProgress);
1594
+ }
1595
if (__DEV__) {
1596
if (
1597
Component.prototype &&
@@ -1610,6 +1635,10 @@ function mountIndeterminateComponent(
1635
renderLanes,
1636
);
1637
}
1638
+ if (enableSchedulingProfiler) {
1639
+ markComponentRenderStopped();
1640
+ }
1641
+
1642
// React DevTools reads this flag.
1643
workInProgress.flags |= PerformedWork;
1644
@@ -3195,6 +3224,9 @@ function updateContextConsumer(
3224
3225
prepareToReadContext(workInProgress, renderLanes);
3226
const newValue = readContext(context);
3227
+ if (enableSchedulingProfiler) {
3228
+ markComponentRenderStarted(workInProgress);
3229
+ }
3230
let newChildren;
3231
if (__DEV__) {
3232
ReactCurrentOwner.current = workInProgress;
@@ -3204,6 +3236,9 @@ function updateContextConsumer(
3236
} else {
3237
newChildren = render(newValue);
3238
}
3239
+ if (enableSchedulingProfiler) {
3240
+ markComponentRenderStopped();
3241
+ }
3242
3243
// React DevTools reads this flag.
3244
workInProgress.flags |= PerformedWork;
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+36
-1
@@ -31,7 +31,10 @@ import type {
31
import type {UpdateQueue} from './ReactUpdateQueue.old';
32
33
import checkPropTypes from 'shared/checkPropTypes';
34
-
34
+import {
35
+ markComponentRenderStarted,
36
+ markComponentRenderStopped,
37
+} from './SchedulingProfiler';
38
import {
39
IndeterminateComponent,
40
FunctionComponent,
@@ -85,6 +88,7 @@ import {
88
enableCache,
89
enableLazyContextPropagation,
90
enableSuspenseLayoutEffectSemantics,
91
+ enableSchedulingProfiler,
92
} from 'shared/ReactFeatureFlags';
93
import invariant from 'shared/invariant';
94
import isArray from 'shared/isArray';
@@ -357,6 +361,9 @@ function updateForwardRef(
361
// The rest is a fork of updateFunctionComponent
362
let nextChildren;
363
prepareToReadContext(workInProgress, renderLanes);
364
+ if (enableSchedulingProfiler) {
365
+ markComponentRenderStarted(workInProgress);
366
+ }
367
if (__DEV__) {
368
ReactCurrentOwner.current = workInProgress;
369
setIsRendering(true);
@@ -397,6 +404,9 @@ function updateForwardRef(
404
renderLanes,
405
);
406
}
407
+ if (enableSchedulingProfiler) {
408
+ markComponentRenderStopped();
409
+ }
410
411
if (current !== null && !didReceiveUpdate) {
412
bailoutHooks(current, workInProgress, renderLanes);
@@ -958,6 +968,9 @@ function updateFunctionComponent(
968
969
let nextChildren;
970
prepareToReadContext(workInProgress, renderLanes);
971
+ if (enableSchedulingProfiler) {
972
+ markComponentRenderStarted(workInProgress);
973
+ }
974
if (__DEV__) {
975
ReactCurrentOwner.current = workInProgress;
976
setIsRendering(true);
@@ -998,6 +1011,9 @@ function updateFunctionComponent(
1011
renderLanes,
1012
);
1013
}
1014
+ if (enableSchedulingProfiler) {
1015
+ markComponentRenderStopped();
1016
+ }
1017
1018
if (current !== null && !didReceiveUpdate) {
1019
bailoutHooks(current, workInProgress, renderLanes);
@@ -1177,6 +1193,9 @@ function finishClassComponent(
1193
stopProfilerTimerIfRunning(workInProgress);
1194
}
1195
} else {
1196
+ if (enableSchedulingProfiler) {
1197
+ markComponentRenderStarted(workInProgress);
1198
+ }
1199
if (__DEV__) {
1200
setIsRendering(true);
1201
nextChildren = instance.render();
@@ -1195,6 +1214,9 @@ function finishClassComponent(
1214
} else {
1215
nextChildren = instance.render();
1216
}
1217
+ if (enableSchedulingProfiler) {
1218
+ markComponentRenderStopped();
1219
+ }
1220
}
1221
1222
// React DevTools reads this flag.
@@ -1567,6 +1589,9 @@ function mountIndeterminateComponent(
1589
prepareToReadContext(workInProgress, renderLanes);
1590
let value;
1591
1592
+ if (enableSchedulingProfiler) {
1593
+ markComponentRenderStarted(workInProgress);
1594
+ }
1595
if (__DEV__) {
1596
if (
1597
Component.prototype &&
@@ -1610,6 +1635,10 @@ function mountIndeterminateComponent(
1635
renderLanes,
1636
);
1637
}
1638
+ if (enableSchedulingProfiler) {
1639
+ markComponentRenderStopped();
1640
+ }
1641
+
1642
// React DevTools reads this flag.
1643
workInProgress.flags |= PerformedWork;
1644
@@ -3195,6 +3224,9 @@ function updateContextConsumer(
3224
3225
prepareToReadContext(workInProgress, renderLanes);
3226
const newValue = readContext(context);
3227
+ if (enableSchedulingProfiler) {
3228
+ markComponentRenderStarted(workInProgress);
3229
+ }
3230
let newChildren;
3231
if (__DEV__) {
3232
ReactCurrentOwner.current = workInProgress;
@@ -3204,6 +3236,9 @@ function updateContextConsumer(
3236
} else {
3237
newChildren = render(newValue);
3238
}
3239
+ if (enableSchedulingProfiler) {
3240
+ markComponentRenderStopped();
3241
+ }
3242
3243
// React DevTools reads this flag.
3244
workInProgress.flags |= PerformedWork;
packages/react-reconciler/src/ReactFiberThrow.new.js
+5
-1
@@ -70,7 +70,10 @@ import {
70
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
71
import {logCapturedError} from './ReactFiberErrorLogger';
72
import {logComponentSuspended} from './DebugTracing';
73
-import {markComponentSuspended} from './SchedulingProfiler';
73
+import {
74
+ markComponentRenderStopped,
75
+ markComponentSuspended,
76
+} from './SchedulingProfiler';
77
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
78
import {
79
SyncLane,
@@ -244,6 +247,7 @@ function throwException(
247
}
248
249
if (enableSchedulingProfiler) {
250
+ markComponentRenderStopped();
251
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
252
}
253
packages/react-reconciler/src/ReactFiberThrow.old.js
+5
-1
@@ -70,7 +70,10 @@ import {
70
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
71
import {logCapturedError} from './ReactFiberErrorLogger';
72
import {logComponentSuspended} from './DebugTracing';
73
-import {markComponentSuspended} from './SchedulingProfiler';
73
+import {
74
+ markComponentRenderStopped,
75
+ markComponentSuspended,
76
+} from './SchedulingProfiler';
77
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
78
import {
79
SyncLane,
@@ -244,6 +247,7 @@ function throwException(
247
}
248
249
if (enableSchedulingProfiler) {
250
+ markComponentRenderStopped();
251
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
252
}
253
packages/react-reconciler/src/SchedulingProfiler.js
+17
@@ -97,6 +97,23 @@ export function markCommitStopped(): void {
97
}
98
}
99
100
+export function markComponentRenderStarted(fiber: Fiber): void {
101
+ if (enableSchedulingProfiler) {
102
+ if (supportsUserTimingV3) {
103
+ const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
104
+ markAndClear(`--component-render-start-${componentName}`);
105
+ }
106
+ }
107
+}
108
+
109
+export function markComponentRenderStopped(): void {
110
+ if (enableSchedulingProfiler) {
111
+ if (supportsUserTimingV3) {
112
+ markAndClear('--component-render-stop');
113
+ }
114
+ }
115
+}
116
+
117
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
118
119
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+26
@@ -174,6 +174,8 @@ describe('SchedulingProfiler', () => {
174
`--react-init-${ReactVersion}`,
175
`--schedule-render-${formatLanes(ReactFiberLane.TransitionLane1)}`,
176
`--render-start-${formatLanes(ReactFiberLane.TransitionLane1)}`,
177
+ '--component-render-start-Foo',
178
+ '--component-render-stop',
179
'--render-yield',
180
]);
181
} else {
@@ -208,6 +210,8 @@ describe('SchedulingProfiler', () => {
210
`--react-init-${ReactVersion}`,
211
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
212
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
213
+ '--component-render-start-Example',
214
+ '--component-render-stop',
215
'--suspense-suspend-0-Example-mount-1-Sync',
216
'--render-stop',
217
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
@@ -239,6 +243,8 @@ describe('SchedulingProfiler', () => {
243
`--react-init-${ReactVersion}`,
244
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
245
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
246
+ '--component-render-start-Example',
247
+ '--component-render-stop',
248
'--suspense-suspend-0-Example-mount-1-Sync',
249
'--render-stop',
250
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
@@ -278,6 +284,8 @@ describe('SchedulingProfiler', () => {
284
285
expectMarksToEqual([
286
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
287
+ '--component-render-start-Example',
288
+ '--component-render-stop',
289
'--suspense-suspend-0-Example-mount-16-Default',
290
'--render-stop',
291
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@@ -317,6 +325,8 @@ describe('SchedulingProfiler', () => {
325
326
expectMarksToEqual([
327
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
328
+ '--component-render-start-Example',
329
+ '--component-render-stop',
330
'--suspense-suspend-0-Example-mount-16-Default',
331
'--render-stop',
332
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@@ -356,12 +366,16 @@ describe('SchedulingProfiler', () => {
366
367
expectMarksToEqual([
368
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
369
+ '--component-render-start-Example',
370
+ '--component-render-stop',
371
'--render-stop',
372
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
373
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
374
`--schedule-state-update-${formatLanes(ReactFiberLane.SyncLane)}-Example`,
375
'--layout-effects-stop',
376
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
377
+ '--component-render-start-Example',
378
+ '--component-render-stop',
379
'--render-stop',
380
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
381
'--commit-stop',
@@ -393,6 +407,8 @@ describe('SchedulingProfiler', () => {
407
408
expectMarksToEqual([
409
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
410
+ '--component-render-start-Example',
411
+ '--component-render-stop',
412
'--render-stop',
413
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
414
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@@ -401,6 +417,8 @@ describe('SchedulingProfiler', () => {
417
)}-Example`,
418
'--layout-effects-stop',
419
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
420
+ '--component-render-start-Example',
421
+ '--component-render-stop',
422
'--render-stop',
423
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
424
'--commit-stop',
@@ -495,12 +513,16 @@ describe('SchedulingProfiler', () => {
513
514
expectMarksToEqual([
515
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
516
+ '--component-render-start-Example',
517
+ '--component-render-stop',
518
'--render-stop',
519
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
520
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
521
`--schedule-state-update-${formatLanes(ReactFiberLane.SyncLane)}-Example`,
522
'--layout-effects-stop',
523
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
524
+ '--component-render-start-Example',
525
+ '--component-render-stop',
526
'--render-stop',
527
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
528
'--commit-stop',
@@ -528,6 +550,8 @@ describe('SchedulingProfiler', () => {
550
`--react-init-${ReactVersion}`,
551
`--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
552
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
553
+ '--component-render-start-Example',
554
+ '--component-render-stop',
555
'--render-stop',
556
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
557
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@@ -539,6 +563,8 @@ describe('SchedulingProfiler', () => {
563
)}-Example`,
564
'--passive-effects-stop',
565
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
566
+ '--component-render-start-Example',
567
+ '--component-render-stop',
568
'--render-stop',
569
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
570
'--commit-stop',