[DevTools] Add native events to the scheduling profiler (#21947)
Brian Vaughn committed
Jul 26, 2021 at 10:36 UTC
b1811ebf01e174fb1e12c7f748bf1ed8aeedef0b
12 files changed
+679
-316
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+47
-12
@@ -44,6 +44,7 @@ import {
44
} from './view-base';
45
import {
46
FlamechartView,
47
+ NativeEventsView,
48
ReactEventsView,
49
ReactMeasuresView,
50
TimeAxisMarkersView,
@@ -126,6 +127,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
127
128
const surfaceRef = useRef(new Surface());
129
const userTimingMarksViewRef = useRef(null);
130
+ const nativeEventsViewRef = useRef(null);
131
const reactEventsViewRef = useRef(null);
132
const reactMeasuresViewRef = useRef(null);
133
const flamechartViewRef = useRef(null);
@@ -176,6 +178,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
178
topContentStack.addSubview(userTimingMarksView);
179
}
180
181
+ const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
182
+ nativeEventsViewRef.current = nativeEventsView;
183
+ topContentStack.addSubview(nativeEventsView);
184
+
185
const reactEventsView = new ReactEventsView(surface, defaultFrame, data);
186
reactEventsViewRef.current = reactEventsView;
187
topContentStack.addSubview(reactEventsView);
@@ -299,7 +305,24 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
305
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
306
setHoveredEvent({
307
userTimingMark,
302
- event: null,
308
+ nativeEvent: null,
309
+ reactEvent: null,
310
+ flamechartStackFrame: null,
311
+ measure: null,
312
+ data,
313
+ });
314
+ }
315
+ };
316
+ }
317
+
318
+ const {current: nativeEventsView} = nativeEventsViewRef;
319
+ if (nativeEventsView) {
320
+ nativeEventsView.onHover = nativeEvent => {
321
+ if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
322
+ setHoveredEvent({
323
+ userTimingMark: null,
324
+ nativeEvent,
325
+ reactEvent: null,
326
flamechartStackFrame: null,
327
measure: null,
328
data,
@@ -310,11 +333,12 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
333
334
const {current: reactEventsView} = reactEventsViewRef;
335
if (reactEventsView) {
313
- reactEventsView.onHover = event => {
314
- if (!hoveredEvent || hoveredEvent.event !== event) {
336
+ reactEventsView.onHover = reactEvent => {
337
+ if (!hoveredEvent || hoveredEvent.reactEvent !== reactEvent) {
338
setHoveredEvent({
339
userTimingMark: null,
317
- event,
340
+ nativeEvent: null,
341
+ reactEvent,
342
flamechartStackFrame: null,
343
measure: null,
344
data,
@@ -329,7 +353,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
353
if (!hoveredEvent || hoveredEvent.measure !== measure) {
354
setHoveredEvent({
355
userTimingMark: null,
332
- event: null,
356
+ nativeEvent: null,
357
+ reactEvent: null,
358
flamechartStackFrame: null,
359
measure,
360
data,
@@ -347,7 +372,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
372
) {
373
setHoveredEvent({
374
userTimingMark: null,
350
- event: null,
375
+ nativeEvent: null,
376
+ reactEvent: null,
377
flamechartStackFrame,
378
measure: null,
379
data,
@@ -368,9 +394,18 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
394
);
395
}
396
397
+ const {current: nativeEventsView} = nativeEventsViewRef;
398
+ if (nativeEventsView) {
399
+ nativeEventsView.setHoveredEvent(
400
+ hoveredEvent ? hoveredEvent.nativeEvent : null,
401
+ );
402
+ }
403
+
404
const {current: reactEventsView} = reactEventsViewRef;
405
if (reactEventsView) {
373
- reactEventsView.setHoveredEvent(hoveredEvent ? hoveredEvent.event : null);
406
+ reactEventsView.setHoveredEvent(
407
+ hoveredEvent ? hoveredEvent.reactEvent : null,
408
+ );
409
}
410
411
const {current: reactMeasuresView} = reactMeasuresViewRef;
@@ -402,22 +437,22 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
437
return null;
438
}
439
const {
405
- event,
440
+ reactEvent,
441
flamechartStackFrame,
442
measure,
443
} = contextData.hoveredEvent;
444
return (
445
<Fragment>
411
- {event !== null && (
446
+ {reactEvent !== null && (
447
<ContextMenuItem
413
- onClick={() => copy(event.componentName)}
448
+ onClick={() => copy(reactEvent.componentName)}
449
title="Copy component name">
450
Copy component name
451
</ContextMenuItem>
452
)}
418
- {event !== null && event.componentStack && (
453
+ {reactEvent !== null && reactEvent.componentStack && (
454
<ContextMenuItem
420
- onClick={() => copy(event.componentStack)}
455
+ onClick={() => copy(reactEvent.componentStack)}
456
title="Copy component stack">
457
Copy component stack
458
</ContextMenuItem>
packages/react-devtools-scheduling-profiler/src/EventTooltip.css
+2
-1
@@ -29,6 +29,7 @@
29
.DetailsGridLabel {
30
color: var(--color-dim);
31
text-align: right;
32
+ white-space: nowrap;
33
}
34
35
.DetailsGridURL {
@@ -45,7 +46,7 @@
46
.ComponentName {
47
font-weight: bold;
48
word-break: break-word;
48
- margin-right: 0.4rem;
49
+ margin-right: 0.25rem;
50
}
51
52
.ComponentStack {
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+48
-11
@@ -10,6 +10,7 @@
10
import type {Point} from './view-base';
11
import type {
12
FlamechartStackFrame,
13
+ NativeEvent,
14
ReactEvent,
15
ReactHoverContextInfo,
16
ReactMeasure,
@@ -47,8 +48,8 @@ function trimmedString(string: string, length: number): string {
48
return string;
49
}
50
50
-function getReactEventLabel(type): string | null {
51
- switch (type) {
51
+function getReactEventLabel(event: ReactEvent): string | null {
52
+ switch (event.type) {
53
case 'schedule-render':
54
return 'render scheduled';
55
case 'schedule-state-update':
@@ -111,10 +112,22 @@ export default function EventTooltip({data, hoveredEvent, origin}: Props) {
112
return null;
113
}
114
114
- const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent;
115
+ const {
116
+ nativeEvent,
117
+ reactEvent,
118
+ measure,
119
+ flamechartStackFrame,
120
+ userTimingMark,
121
+ } = hoveredEvent;
122
116
- if (event !== null) {
117
- return <TooltipReactEvent event={event} tooltipRef={tooltipRef} />;
123
+ if (nativeEvent !== null) {
124
+ return (
125
+ <TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
126
+ );
127
+ } else if (reactEvent !== null) {
128
+ return (
129
+ <TooltipReactEvent reactEvent={reactEvent} tooltipRef={tooltipRef} />
130
+ );
131
} else if (measure !== null) {
132
return (
133
<TooltipReactMeasure
@@ -189,23 +202,47 @@ const TooltipFlamechartNode = ({
202
);
203
};
204
205
+const TooltipNativeEvent = ({
206
+ nativeEvent,
207
+ tooltipRef,
208
+}: {
209
+ nativeEvent: NativeEvent,
210
+ tooltipRef: Return<typeof useRef>,
211
+}) => {
212
+ const {duration, timestamp, type} = nativeEvent;
213
+
214
+ return (
215
+ <div className={styles.Tooltip} ref={tooltipRef}>
216
+ <span className={styles.ComponentName}>{trimmedString(type, 768)}</span>
217
+ event
218
+ <div className={styles.Divider} />
219
+ <div className={styles.DetailsGrid}>
220
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
221
+ <div>{formatTimestamp(timestamp)}</div>
222
+ <div className={styles.DetailsGridLabel}>Duration:</div>
223
+ <div>{formatDuration(duration)}</div>
224
+ </div>
225
+ </div>
226
+ );
227
+};
228
+
229
const TooltipReactEvent = ({
193
- event,
230
+ reactEvent,
231
tooltipRef,
232
}: {
196
- event: ReactEvent,
233
+ reactEvent: ReactEvent,
234
tooltipRef: Return<typeof useRef>,
235
}) => {
199
- const label = getReactEventLabel(event.type);
200
- const color = getReactEventColor(event);
236
+ const label = getReactEventLabel(reactEvent);
237
+ const color = getReactEventColor(reactEvent);
238
if (!label || !color) {
239
if (__DEV__) {
203
- console.warn('Unexpected event type "%s"', event.type);
240
+ console.warn('Unexpected reactEvent type "%s"', reactEvent.type);
241
}
242
return null;
243
}
244
208
- const {componentName, componentStack, timestamp} = event;
245
+ const {componentName, componentStack, timestamp} = reactEvent;
246
247
return (
248
<div className={styles.Tooltip} ref={tooltipRef}>
packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js
new
+224
@@ -0,0 +1,224 @@
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 {NativeEvent, ReactProfilerData} from '../types';
11
+import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
12
+
13
+import {
14
+ positioningScaleFactor,
15
+ timestampToPosition,
16
+ positionToTimestamp,
17
+} from './utils/positioning';
18
+import {
19
+ View,
20
+ Surface,
21
+ rectContainsPoint,
22
+ rectIntersectsRect,
23
+ intersectionOfRects,
24
+} from '../view-base';
25
+import {
26
+ COLORS,
27
+ EVENT_ROW_PADDING,
28
+ EVENT_DIAMETER,
29
+ BORDER_SIZE,
30
+} from './constants';
31
+
32
+const EVENT_ROW_HEIGHT_FIXED =
33
+ EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
34
+
35
+export class NativeEventsView extends View {
36
+ _profilerData: ReactProfilerData;
37
+ _intrinsicSize: Size;
38
+
39
+ _hoveredEvent: NativeEvent | null = null;
40
+ onHover: ((event: NativeEvent | null) => void) | null = null;
41
+
42
+ constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
43
+ super(surface, frame);
44
+ this._profilerData = profilerData;
45
+
46
+ this._intrinsicSize = {
47
+ width: this._profilerData.duration,
48
+ height: EVENT_ROW_HEIGHT_FIXED,
49
+ };
50
+ }
51
+
52
+ desiredSize() {
53
+ return this._intrinsicSize;
54
+ }
55
+
56
+ setHoveredEvent(hoveredEvent: NativeEvent | null) {
57
+ if (this._hoveredEvent === hoveredEvent) {
58
+ return;
59
+ }
60
+ this._hoveredEvent = hoveredEvent;
61
+ this.setNeedsDisplay();
62
+ }
63
+
64
+ /**
65
+ * Draw a single `NativeEvent` as a circle in the canvas.
66
+ */
67
+ _drawSingleNativeEvent(
68
+ context: CanvasRenderingContext2D,
69
+ rect: Rect,
70
+ event: NativeEvent,
71
+ baseY: number,
72
+ scaleFactor: number,
73
+ showHoverHighlight: boolean,
74
+ ) {
75
+ const {frame} = this;
76
+ const {duration, timestamp} = event;
77
+
78
+ const xStart = timestampToPosition(timestamp, scaleFactor, frame);
79
+ const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
80
+ const eventRect: Rect = {
81
+ origin: {
82
+ x: xStart,
83
+ y: baseY,
84
+ },
85
+ size: {width: xStop - xStart, height: EVENT_DIAMETER},
86
+ };
87
+ if (!rectIntersectsRect(eventRect, rect)) {
88
+ return; // Not in view
89
+ }
90
+
91
+ const fillStyle = showHoverHighlight
92
+ ? COLORS.NATIVE_EVENT_HOVER
93
+ : COLORS.NATIVE_EVENT;
94
+
95
+ const drawableRect = intersectionOfRects(eventRect, rect);
96
+ context.beginPath();
97
+ context.fillStyle = fillStyle;
98
+ context.fillRect(
99
+ drawableRect.origin.x,
100
+ drawableRect.origin.y,
101
+ drawableRect.size.width,
102
+ drawableRect.size.height,
103
+ );
104
+ }
105
+
106
+ draw(context: CanvasRenderingContext2D) {
107
+ const {
108
+ frame,
109
+ _profilerData: {nativeEvents},
110
+ _hoveredEvent,
111
+ visibleArea,
112
+ } = this;
113
+
114
+ context.fillStyle = COLORS.BACKGROUND;
115
+ context.fillRect(
116
+ visibleArea.origin.x,
117
+ visibleArea.origin.y,
118
+ visibleArea.size.width,
119
+ visibleArea.size.height,
120
+ );
121
+
122
+ // Draw events
123
+ const baseY = frame.origin.y + EVENT_ROW_PADDING;
124
+ const scaleFactor = positioningScaleFactor(
125
+ this._intrinsicSize.width,
126
+ frame,
127
+ );
128
+
129
+ nativeEvents.forEach(event => {
130
+ if (event === _hoveredEvent) {
131
+ // Draw the highlighted items on top so they stand out.
132
+ // This is helpful if there are multiple (overlapping) items close to each other.
133
+ this._drawSingleNativeEvent(
134
+ context,
135
+ visibleArea,
136
+ event,
137
+ baseY,
138
+ scaleFactor,
139
+ true,
140
+ );
141
+ } else {
142
+ this._drawSingleNativeEvent(
143
+ context,
144
+ visibleArea,
145
+ event,
146
+ baseY,
147
+ scaleFactor,
148
+ false,
149
+ );
150
+ }
151
+ });
152
+
153
+ // Render bottom border.
154
+ // Propose border rect, check if intersects with `rect`, draw intersection.
155
+ const borderFrame: Rect = {
156
+ origin: {
157
+ x: frame.origin.x,
158
+ y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
159
+ },
160
+ size: {
161
+ width: frame.size.width,
162
+ height: BORDER_SIZE,
163
+ },
164
+ };
165
+ if (rectIntersectsRect(borderFrame, visibleArea)) {
166
+ const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
167
+ context.fillStyle = COLORS.PRIORITY_BORDER;
168
+ context.fillRect(
169
+ borderDrawableRect.origin.x,
170
+ borderDrawableRect.origin.y,
171
+ borderDrawableRect.size.width,
172
+ borderDrawableRect.size.height,
173
+ );
174
+ }
175
+ }
176
+
177
+ /**
178
+ * @private
179
+ */
180
+ _handleMouseMove(interaction: MouseMoveInteraction) {
181
+ const {frame, _intrinsicSize, onHover, visibleArea} = this;
182
+ if (!onHover) {
183
+ return;
184
+ }
185
+
186
+ const {location} = interaction.payload;
187
+ if (!rectContainsPoint(location, visibleArea)) {
188
+ onHover(null);
189
+ return;
190
+ }
191
+
192
+ const {nativeEvents} = this._profilerData;
193
+
194
+ const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
195
+ const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
196
+
197
+ // Find the event being hovered over.
198
+ //
199
+ // Because data ranges may overlap, we want to find the last intersecting item.
200
+ // This will always be the one on "top" (the one the user is hovering over).
201
+ for (let index = nativeEvents.length - 1; index >= 0; index--) {
202
+ const nativeEvent = nativeEvents[index];
203
+ const {duration, timestamp} = nativeEvent;
204
+
205
+ if (
206
+ hoverTimestamp >= timestamp &&
207
+ hoverTimestamp <= timestamp + duration
208
+ ) {
209
+ onHover(nativeEvent);
210
+ return;
211
+ }
212
+ }
213
+
214
+ onHover(null);
215
+ }
216
+
217
+ handleInteraction(interaction: Interaction) {
218
+ switch (interaction.type) {
219
+ case 'mousemove':
220
+ this._handleMouseMove(interaction);
221
+ break;
222
+ }
223
+ }
224
+}
packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js
+7
-5
@@ -100,6 +100,8 @@ export class ReactEventsView extends View {
100
let fillStyle = null;
101
102
switch (type) {
103
+ case 'native-event':
104
+ return;
105
case 'schedule-render':
106
case 'schedule-state-update':
107
case 'schedule-force-update':
@@ -140,7 +142,7 @@ export class ReactEventsView extends View {
142
draw(context: CanvasRenderingContext2D) {
143
const {
144
frame,
143
- _profilerData: {events},
145
+ _profilerData: {reactEvents},
146
_hoveredEvent,
147
visibleArea,
148
} = this;
@@ -162,7 +164,7 @@ export class ReactEventsView extends View {
164
165
const highlightedEvents: ReactEvent[] = [];
166
165
- events.forEach(event => {
167
+ reactEvents.forEach(event => {
168
if (
169
event === _hoveredEvent ||
170
(_hoveredEvent &&
@@ -236,7 +238,7 @@ export class ReactEventsView extends View {
238
}
239
240
const {
239
- _profilerData: {events},
241
+ _profilerData: {reactEvents},
242
} = this;
243
const scaleFactor = positioningScaleFactor(
244
this._intrinsicSize.width,
@@ -250,8 +252,8 @@ export class ReactEventsView extends View {
252
253
// Because data ranges may overlap, we want to find the last intersecting item.
254
// This will always be the one on "top" (the one the user is hovering over).
253
- for (let index = events.length - 1; index >= 0; index--) {
254
- const event = events[index];
255
+ for (let index = reactEvents.length - 1; index >= 0; index--) {
256
+ const event = reactEvents[index];
257
const {timestamp} = event;
258
259
if (
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+12
-4
@@ -44,10 +44,12 @@ export const FLAMECHART_TEXT_PADDING = 3;
44
// TODO Replace this with "export let" vars
45
export let COLORS = {
46
BACKGROUND: '',
47
+ FLAME_GRAPH_LABEL: '',
48
+ NATIVE_EVENT: '',
49
+ NATIVE_EVENT_HOVER: '',
50
PRIORITY_BACKGROUND: '',
51
PRIORITY_BORDER: '',
52
PRIORITY_LABEL: '',
50
- FLAME_GRAPH_LABEL: '',
53
USER_TIMING: '',
54
USER_TIMING_HOVER: '',
55
REACT_IDLE: '',
@@ -81,6 +83,15 @@ export function updateColorsToMatchTheme(): void {
83
84
COLORS = {
85
BACKGROUND: computedStyle.getPropertyValue('--color-background'),
86
+ FLAME_GRAPH_LABEL: computedStyle.getPropertyValue(
87
+ '--color-scheduling-profiler-flame-graph-label',
88
+ ),
89
+ NATIVE_EVENT: computedStyle.getPropertyValue(
90
+ '--color-scheduling-profiler-native-event',
91
+ ),
92
+ NATIVE_EVENT_HOVER: computedStyle.getPropertyValue(
93
+ '--color-scheduling-profiler-native-event-hover',
94
+ ),
95
PRIORITY_BACKGROUND: computedStyle.getPropertyValue(
96
'--color-scheduling-profiler-priority-background',
97
),
@@ -88,9 +99,6 @@ export function updateColorsToMatchTheme(): void {
99
'--color-scheduling-profiler-priority-border',
100
),
101
PRIORITY_LABEL: computedStyle.getPropertyValue('--color-text'),
91
- FLAME_GRAPH_LABEL: computedStyle.getPropertyValue(
92
- '--color-scheduling-profiler-flame-graph-label',
93
- ),
102
USER_TIMING: computedStyle.getPropertyValue(
103
'--color-scheduling-profiler-user-timing',
104
),
packages/react-devtools-scheduling-profiler/src/content-views/index.js
+1
@@ -8,6 +8,7 @@
8
*/
9
10
export * from './FlamechartView';
11
+export * from './NativeEventsView';
12
export * from './ReactEventsView';
13
export * from './ReactMeasuresView';
14
export * from './TimeAxisMarkersView';
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+41
-37
@@ -204,10 +204,11 @@ describe(preprocessData, () => {
204
205
expect(preprocessData([cpuProfilerSample, randomSample])).toStrictEqual({
206
duration: 0.002,
207
- events: [],
207
flamechart: [],
208
measures: [],
209
+ nativeEvents: [],
210
otherUserTimingMarks: [],
211
+ reactEvents: [],
212
startTime: 1,
213
});
214
});
@@ -258,15 +259,6 @@ describe(preprocessData, () => {
259
]),
260
).toStrictEqual({
261
duration: 0.008,
261
- events: [
262
- {
263
- componentStack: '',
264
- laneLabels: [],
265
- lanes: [9],
266
- timestamp: 0.002,
267
- type: 'schedule-render',
268
- },
269
- ],
262
flamechart: [],
263
measures: [
264
{
@@ -306,7 +298,17 @@ describe(preprocessData, () => {
298
type: 'layout-effects',
299
},
300
],
301
+ nativeEvents: [],
302
otherUserTimingMarks: [],
303
+ reactEvents: [
304
+ {
305
+ componentStack: '',
306
+ laneLabels: [],
307
+ lanes: [9],
308
+ timestamp: 0.002,
309
+ type: 'schedule-render',
310
+ },
311
+ ],
312
startTime: 1,
313
});
314
});
@@ -320,15 +322,6 @@ describe(preprocessData, () => {
322
const userTimingData = createUserTimingData(clearedMarks);
323
expect(preprocessData(userTimingData)).toStrictEqual({
324
duration: 0.011,
323
- events: [
324
- {
325
- componentStack: '',
326
- laneLabels: ['Sync'],
327
- lanes: [0],
328
- timestamp: 0.005,
329
- type: 'schedule-render',
330
- },
331
- ],
325
flamechart: [],
326
measures: [
327
{
@@ -368,6 +361,7 @@ describe(preprocessData, () => {
361
type: 'layout-effects',
362
},
363
],
364
+ nativeEvents: [],
365
otherUserTimingMarks: [
366
{
367
name: '__v3',
@@ -378,6 +372,15 @@ describe(preprocessData, () => {
372
timestamp: 0.004,
373
},
374
],
375
+ reactEvents: [
376
+ {
377
+ componentStack: '',
378
+ laneLabels: ['Sync'],
379
+ lanes: [0],
380
+ timestamp: 0.005,
381
+ type: 'schedule-render',
382
+ },
383
+ ],
384
startTime: 1,
385
});
386
});
@@ -400,24 +403,6 @@ describe(preprocessData, () => {
403
const userTimingData = createUserTimingData(clearedMarks);
404
expect(preprocessData(userTimingData)).toStrictEqual({
405
duration: 0.022,
403
- events: [
404
- {
405
- componentStack: '',
406
- laneLabels: ['Default'],
407
- lanes: [4],
408
- timestamp: 0.005,
409
- type: 'schedule-render',
410
- },
411
- {
412
- componentName: 'App',
413
- componentStack: '',
414
- isCascading: false,
415
- laneLabels: ['Default'],
416
- lanes: [4],
417
- timestamp: 0.013,
418
- type: 'schedule-state-update',
419
- },
420
- ],
406
flamechart: [],
407
measures: [
408
{
@@ -511,6 +496,7 @@ describe(preprocessData, () => {
496
type: 'passive-effects',
497
},
498
],
499
+ nativeEvents: [],
500
otherUserTimingMarks: [
501
{
502
name: '__v3',
@@ -521,6 +507,24 @@ describe(preprocessData, () => {
507
timestamp: 0.004,
508
},
509
],
510
+ reactEvents: [
511
+ {
512
+ componentStack: '',
513
+ laneLabels: ['Default'],
514
+ lanes: [4],
515
+ timestamp: 0.005,
516
+ type: 'schedule-render',
517
+ },
518
+ {
519
+ componentName: 'App',
520
+ componentStack: '',
521
+ isCascading: false,
522
+ laneLabels: ['Default'],
523
+ lanes: [4],
524
+ timestamp: 0.013,
525
+ type: 'schedule-state-update',
526
+ },
527
+ ],
528
startTime: 1,
529
});
530
});
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+271
-242
@@ -161,247 +161,275 @@ function processTimelineEvent(
161
/** Intermediate processor state. May be mutated. */
162
state: ProcessorState,
163
) {
164
- const {cat, name, ts, ph} = event;
165
- if (cat !== 'blink.user_timing') {
166
- return;
167
- }
168
-
169
- const startTime = (ts - currentProfilerData.startTime) / 1000;
170
-
171
- // React Events - schedule
172
- if (name.startsWith('--schedule-render-')) {
173
- const [laneBitmaskString, laneLabels, ...splitComponentStack] = name
174
- .substr(18)
175
- .split('-');
176
- currentProfilerData.events.push({
177
- type: 'schedule-render',
178
- lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
179
- laneLabels: laneLabels ? laneLabels.split(',') : [],
180
- componentStack: splitComponentStack.join('-'),
181
- timestamp: startTime,
182
- });
183
- } else if (name.startsWith('--schedule-forced-update-')) {
184
- const [
185
- laneBitmaskString,
186
- laneLabels,
187
- componentName,
188
- ...splitComponentStack
189
- ] = name.substr(25).split('-');
190
- const isCascading = !!state.measureStack.find(
191
- ({type}) => type === 'commit',
192
- );
193
- currentProfilerData.events.push({
194
- type: 'schedule-force-update',
195
- lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
196
- laneLabels: laneLabels ? laneLabels.split(',') : [],
197
- componentName,
198
- componentStack: splitComponentStack.join('-'),
199
- timestamp: startTime,
200
- isCascading,
201
- });
202
- } else if (name.startsWith('--schedule-state-update-')) {
203
- const [
204
- laneBitmaskString,
205
- laneLabels,
206
- componentName,
207
- ...splitComponentStack
208
- ] = name.substr(24).split('-');
209
- const isCascading = !!state.measureStack.find(
210
- ({type}) => type === 'commit',
211
- );
212
- currentProfilerData.events.push({
213
- type: 'schedule-state-update',
214
- lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
215
- laneLabels: laneLabels ? laneLabels.split(',') : [],
216
- componentName,
217
- componentStack: splitComponentStack.join('-'),
218
- timestamp: startTime,
219
- isCascading,
220
- });
221
- } // eslint-disable-line brace-style
222
-
223
- // React Events - suspense
224
- else if (name.startsWith('--suspense-suspend-')) {
225
- const [id, componentName, ...splitComponentStack] = name
226
- .substr(19)
227
- .split('-');
228
- currentProfilerData.events.push({
229
- type: 'suspense-suspend',
230
- id,
231
- componentName,
232
- componentStack: splitComponentStack.join('-'),
233
- timestamp: startTime,
234
- });
235
- } else if (name.startsWith('--suspense-resolved-')) {
236
- const [id, componentName, ...splitComponentStack] = name
237
- .substr(20)
238
- .split('-');
239
- currentProfilerData.events.push({
240
- type: 'suspense-resolved',
241
- id,
242
- componentName,
243
- componentStack: splitComponentStack.join('-'),
244
- timestamp: startTime,
245
- });
246
- } else if (name.startsWith('--suspense-rejected-')) {
247
- const [id, componentName, ...splitComponentStack] = name
248
- .substr(20)
249
- .split('-');
250
- currentProfilerData.events.push({
251
- type: 'suspense-rejected',
252
- id,
253
- componentName,
254
- componentStack: splitComponentStack.join('-'),
255
- timestamp: startTime,
256
- });
257
- } // eslint-disable-line brace-style
258
-
259
- // React Measures - render
260
- else if (name.startsWith('--render-start-')) {
261
- if (state.nextRenderShouldGenerateNewBatchID) {
262
- state.nextRenderShouldGenerateNewBatchID = false;
263
- state.batchUID = ((state.uidCounter++: any): BatchUID);
264
- }
265
- const [laneBitmaskString, laneLabels] = name.substr(15).split('-');
266
- const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
267
- throwIfIncomplete('render', state.measureStack);
268
- if (getLastType(state.measureStack) !== 'render-idle') {
269
- markWorkStarted(
270
- 'render-idle',
271
- startTime,
272
- lanes,
273
- laneLabels ? laneLabels.split(',') : [],
274
- currentProfilerData,
275
- state,
276
- );
277
- }
278
- markWorkStarted(
279
- 'render',
280
- startTime,
281
- lanes,
282
- laneLabels ? laneLabels.split(',') : [],
283
- currentProfilerData,
284
- state,
285
- );
286
- } else if (
287
- name.startsWith('--render-stop') ||
288
- name.startsWith('--render-yield')
289
- ) {
290
- markWorkCompleted(
291
- 'render',
292
- startTime,
293
- currentProfilerData,
294
- state.measureStack,
295
- );
296
- } else if (name.startsWith('--render-cancel')) {
297
- state.nextRenderShouldGenerateNewBatchID = true;
298
- markWorkCompleted(
299
- 'render',
300
- startTime,
301
- currentProfilerData,
302
- state.measureStack,
303
- );
304
- markWorkCompleted(
305
- 'render-idle',
306
- startTime,
307
- currentProfilerData,
308
- state.measureStack,
309
- );
310
- } // eslint-disable-line brace-style
311
-
312
- // React Measures - commits
313
- else if (name.startsWith('--commit-start-')) {
314
- state.nextRenderShouldGenerateNewBatchID = true;
315
- const [laneBitmaskString, laneLabels] = name.substr(15).split('-');
316
- const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
317
- markWorkStarted(
318
- 'commit',
319
- startTime,
320
- lanes,
321
- laneLabels ? laneLabels.split(',') : [],
322
- currentProfilerData,
323
- state,
324
- );
325
- } else if (name.startsWith('--commit-stop')) {
326
- markWorkCompleted(
327
- 'commit',
328
- startTime,
329
- currentProfilerData,
330
- state.measureStack,
331
- );
332
- markWorkCompleted(
333
- 'render-idle',
334
- startTime,
335
- currentProfilerData,
336
- state.measureStack,
337
- );
338
- } // eslint-disable-line brace-style
339
-
340
- // React Measures - layout effects
341
- else if (name.startsWith('--layout-effects-start-')) {
342
- const [laneBitmaskString, laneLabels] = name.substr(23).split('-');
343
- const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
344
- markWorkStarted(
345
- 'layout-effects',
346
- startTime,
347
- lanes,
348
- laneLabels ? laneLabels.split(',') : [],
349
- currentProfilerData,
350
- state,
351
- );
352
- } else if (name.startsWith('--layout-effects-stop')) {
353
- markWorkCompleted(
354
- 'layout-effects',
355
- startTime,
356
- currentProfilerData,
357
- state.measureStack,
358
- );
359
- } // eslint-disable-line brace-style
360
-
361
- // React Measures - passive effects
362
- else if (name.startsWith('--passive-effects-start-')) {
363
- const [laneBitmaskString, laneLabels] = name.substr(24).split('-');
364
- const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
365
- markWorkStarted(
366
- 'passive-effects',
367
- startTime,
368
- lanes,
369
- laneLabels ? laneLabels.split(',') : [],
370
- currentProfilerData,
371
- state,
372
- );
373
- } else if (name.startsWith('--passive-effects-stop')) {
374
- markWorkCompleted(
375
- 'passive-effects',
376
- startTime,
377
- currentProfilerData,
378
- state.measureStack,
379
- );
380
- } // eslint-disable-line brace-style
381
-
382
- // Other user timing marks/measures
383
- else if (ph === 'R' || ph === 'n') {
384
- // User Timing mark
385
- currentProfilerData.otherUserTimingMarks.push({
386
- name,
387
- timestamp: startTime,
388
- });
389
- } else if (ph === 'b') {
390
- // TODO: Begin user timing measure
391
- } else if (ph === 'e') {
392
- // TODO: End user timing measure
393
- } else if (ph === 'i' || ph === 'I') {
394
- // Instant events.
395
- // Note that the capital "I" is a deprecated value that exists in Chrome Canary traces.
396
- } // eslint-disable-line brace-style
397
-
398
- // Unrecognized event
399
- else {
400
- throw new InvalidProfileError(
401
- `Unrecognized event ${JSON.stringify(
402
- event,
403
- )}! This is likely a bug in this profiler tool.`,
404
- );
164
+ const {args, cat, name, ts, ph} = event;
165
+ switch (cat) {
166
+ case 'devtools.timeline':
167
+ if (name === 'EventDispatch') {
168
+ const type = args.data.type;
169
+
170
+ if (type.startsWith('react-')) {
171
+ const stackTrace = args.data.stackTrace;
172
+ if (stackTrace) {
173
+ const topFrame = stackTrace[stackTrace.length - 1];
174
+ if (topFrame.url.includes('node_modules/react-dom')) {
175
+ // Filter out fake React events dispatched by invokeGuardedCallbackDev.
176
+ return;
177
+ }
178
+ }
179
+ }
180
+
181
+ const startTime = (ts - currentProfilerData.startTime) / 1000;
182
+ const duration = event.dur / 1000;
183
+
184
+ // TODO (scheduling profiler) Should we filter out certain event types?
185
+ currentProfilerData.nativeEvents.push({
186
+ duration,
187
+ timestamp: startTime,
188
+ type,
189
+ });
190
+ }
191
+ break;
192
+ case 'blink.user_timing':
193
+ const startTime = (ts - currentProfilerData.startTime) / 1000;
194
+
195
+ // React Events - schedule
196
+ if (name.startsWith('--schedule-render-')) {
197
+ const [
198
+ laneBitmaskString,
199
+ laneLabels,
200
+ ...splitComponentStack
201
+ ] = name.substr(18).split('-');
202
+ currentProfilerData.reactEvents.push({
203
+ type: 'schedule-render',
204
+ lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
205
+ laneLabels: laneLabels ? laneLabels.split(',') : [],
206
+ componentStack: splitComponentStack.join('-'),
207
+ timestamp: startTime,
208
+ });
209
+ } else if (name.startsWith('--schedule-forced-update-')) {
210
+ const [
211
+ laneBitmaskString,
212
+ laneLabels,
213
+ componentName,
214
+ ...splitComponentStack
215
+ ] = name.substr(25).split('-');
216
+ const isCascading = !!state.measureStack.find(
217
+ ({type}) => type === 'commit',
218
+ );
219
+ currentProfilerData.reactEvents.push({
220
+ type: 'schedule-force-update',
221
+ lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
222
+ laneLabels: laneLabels ? laneLabels.split(',') : [],
223
+ componentName,
224
+ componentStack: splitComponentStack.join('-'),
225
+ timestamp: startTime,
226
+ isCascading,
227
+ });
228
+ } else if (name.startsWith('--schedule-state-update-')) {
229
+ const [
230
+ laneBitmaskString,
231
+ laneLabels,
232
+ componentName,
233
+ ...splitComponentStack
234
+ ] = name.substr(24).split('-');
235
+ const isCascading = !!state.measureStack.find(
236
+ ({type}) => type === 'commit',
237
+ );
238
+ currentProfilerData.reactEvents.push({
239
+ type: 'schedule-state-update',
240
+ lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
241
+ laneLabels: laneLabels ? laneLabels.split(',') : [],
242
+ componentName,
243
+ componentStack: splitComponentStack.join('-'),
244
+ timestamp: startTime,
245
+ isCascading,
246
+ });
247
+ } // eslint-disable-line brace-style
248
+
249
+ // React Events - suspense
250
+ else if (name.startsWith('--suspense-suspend-')) {
251
+ const [id, componentName, ...splitComponentStack] = name
252
+ .substr(19)
253
+ .split('-');
254
+ currentProfilerData.reactEvents.push({
255
+ type: 'suspense-suspend',
256
+ id,
257
+ componentName,
258
+ componentStack: splitComponentStack.join('-'),
259
+ timestamp: startTime,
260
+ });
261
+ } else if (name.startsWith('--suspense-resolved-')) {
262
+ const [id, componentName, ...splitComponentStack] = name
263
+ .substr(20)
264
+ .split('-');
265
+ currentProfilerData.reactEvents.push({
266
+ type: 'suspense-resolved',
267
+ id,
268
+ componentName,
269
+ componentStack: splitComponentStack.join('-'),
270
+ timestamp: startTime,
271
+ });
272
+ } else if (name.startsWith('--suspense-rejected-')) {
273
+ const [id, componentName, ...splitComponentStack] = name
274
+ .substr(20)
275
+ .split('-');
276
+ currentProfilerData.reactEvents.push({
277
+ type: 'suspense-rejected',
278
+ id,
279
+ componentName,
280
+ componentStack: splitComponentStack.join('-'),
281
+ timestamp: startTime,
282
+ });
283
+ } // eslint-disable-line brace-style
284
+
285
+ // React Measures - render
286
+ else if (name.startsWith('--render-start-')) {
287
+ if (state.nextRenderShouldGenerateNewBatchID) {
288
+ state.nextRenderShouldGenerateNewBatchID = false;
289
+ state.batchUID = ((state.uidCounter++: any): BatchUID);
290
+ }
291
+ const [laneBitmaskString, laneLabels] = name.substr(15).split('-');
292
+ const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
293
+ throwIfIncomplete('render', state.measureStack);
294
+ if (getLastType(state.measureStack) !== 'render-idle') {
295
+ markWorkStarted(
296
+ 'render-idle',
297
+ startTime,
298
+ lanes,
299
+ laneLabels ? laneLabels.split(',') : [],
300
+ currentProfilerData,
301
+ state,
302
+ );
303
+ }
304
+ markWorkStarted(
305
+ 'render',
306
+ startTime,
307
+ lanes,
308
+ laneLabels ? laneLabels.split(',') : [],
309
+ currentProfilerData,
310
+ state,
311
+ );
312
+ } else if (
313
+ name.startsWith('--render-stop') ||
314
+ name.startsWith('--render-yield')
315
+ ) {
316
+ markWorkCompleted(
317
+ 'render',
318
+ startTime,
319
+ currentProfilerData,
320
+ state.measureStack,
321
+ );
322
+ } else if (name.startsWith('--render-cancel')) {
323
+ state.nextRenderShouldGenerateNewBatchID = true;
324
+ markWorkCompleted(
325
+ 'render',
326
+ startTime,
327
+ currentProfilerData,
328
+ state.measureStack,
329
+ );
330
+ markWorkCompleted(
331
+ 'render-idle',
332
+ startTime,
333
+ currentProfilerData,
334
+ state.measureStack,
335
+ );
336
+ } // eslint-disable-line brace-style
337
+
338
+ // React Measures - commits
339
+ else if (name.startsWith('--commit-start-')) {
340
+ state.nextRenderShouldGenerateNewBatchID = true;
341
+ const [laneBitmaskString, laneLabels] = name.substr(15).split('-');
342
+ const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
343
+ markWorkStarted(
344
+ 'commit',
345
+ startTime,
346
+ lanes,
347
+ laneLabels ? laneLabels.split(',') : [],
348
+ currentProfilerData,
349
+ state,
350
+ );
351
+ } else if (name.startsWith('--commit-stop')) {
352
+ markWorkCompleted(
353
+ 'commit',
354
+ startTime,
355
+ currentProfilerData,
356
+ state.measureStack,
357
+ );
358
+ markWorkCompleted(
359
+ 'render-idle',
360
+ startTime,
361
+ currentProfilerData,
362
+ state.measureStack,
363
+ );
364
+ } // eslint-disable-line brace-style
365
+
366
+ // React Measures - layout effects
367
+ else if (name.startsWith('--layout-effects-start-')) {
368
+ const [laneBitmaskString, laneLabels] = name.substr(23).split('-');
369
+ const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
370
+ markWorkStarted(
371
+ 'layout-effects',
372
+ startTime,
373
+ lanes,
374
+ laneLabels ? laneLabels.split(',') : [],
375
+ currentProfilerData,
376
+ state,
377
+ );
378
+ } else if (name.startsWith('--layout-effects-stop')) {
379
+ markWorkCompleted(
380
+ 'layout-effects',
381
+ startTime,
382
+ currentProfilerData,
383
+ state.measureStack,
384
+ );
385
+ } // eslint-disable-line brace-style
386
+
387
+ // React Measures - passive effects
388
+ else if (name.startsWith('--passive-effects-start-')) {
389
+ const [laneBitmaskString, laneLabels] = name.substr(24).split('-');
390
+ const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
391
+ markWorkStarted(
392
+ 'passive-effects',
393
+ startTime,
394
+ lanes,
395
+ laneLabels ? laneLabels.split(',') : [],
396
+ currentProfilerData,
397
+ state,
398
+ );
399
+ } else if (name.startsWith('--passive-effects-stop')) {
400
+ markWorkCompleted(
401
+ 'passive-effects',
402
+ startTime,
403
+ currentProfilerData,
404
+ state.measureStack,
405
+ );
406
+ } // eslint-disable-line brace-style
407
+
408
+ // Other user timing marks/measures
409
+ else if (ph === 'R' || ph === 'n') {
410
+ // User Timing mark
411
+ currentProfilerData.otherUserTimingMarks.push({
412
+ name,
413
+ timestamp: startTime,
414
+ });
415
+ } else if (ph === 'b') {
416
+ // TODO: Begin user timing measure
417
+ } else if (ph === 'e') {
418
+ // TODO: End user timing measure
419
+ } else if (ph === 'i' || ph === 'I') {
420
+ // Instant events.
421
+ // Note that the capital "I" is a deprecated value that exists in Chrome Canary traces.
422
+ } // eslint-disable-line brace-style
423
+
424
+ // Unrecognized event
425
+ else {
426
+ throw new InvalidProfileError(
427
+ `Unrecognized event ${JSON.stringify(
428
+ event,
429
+ )}! This is likely a bug in this profiler tool.`,
430
+ );
431
+ }
432
+ break;
433
}
434
}
435
@@ -447,7 +475,8 @@ export default function preprocessData(
475
const profilerData: ReactProfilerData = {
476
startTime: 0,
477
duration: 0,
450
- events: [],
478
+ nativeEvents: [],
479
+ reactEvents: [],
480
measures: [],
481
flamechart,
482
otherUserTimingMarks: [],
packages/react-devtools-scheduling-profiler/src/types.js
+10
-2
@@ -21,6 +21,12 @@ export type Milliseconds = number;
21
22
export type ReactLane = number;
23
24
+export type NativeEvent = {|
25
+ +duration: Milliseconds,
26
+ +timestamp: Milliseconds,
27
+ +type: string,
28
+|};
29
+
30
type BaseReactEvent = {|
31
+componentName?: string,
32
+componentStack?: string,
@@ -122,14 +128,16 @@ export type Flamechart = FlamechartStackLayer[];
128
export type ReactProfilerData = {|
129
startTime: number,
130
duration: number,
125
- events: ReactEvent[],
131
+ nativeEvents: NativeEvent[],
132
+ reactEvents: ReactEvent[],
133
measures: ReactMeasure[],
134
flamechart: Flamechart,
135
otherUserTimingMarks: UserTimingMark[],
136
|};
137
138
export type ReactHoverContextInfo = {|
132
- event: ReactEvent | null,
139
+ nativeEvent: NativeEvent | null,
140
+ reactEvent: ReactEvent | null,
141
measure: ReactMeasure | null,
142
data: $ReadOnly<ReactProfilerData> | null,
143
flamechartStackFrame: FlamechartStackFrame | null,
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+10
@@ -421,6 +421,16 @@ export function updateThemeVariables(
421
'color-scheduling-profiler-flame-graph-label',
422
documentElements,
423
);
424
+ updateStyleHelper(
425
+ theme,
426
+ 'color-scheduling-profiler-native-event',
427
+ documentElements,
428
+ );
429
+ updateStyleHelper(
430
+ theme,
431
+ 'color-scheduling-profiler-native-event-hover',
432
+ documentElements,
433
+ );
434
updateStyleHelper(
435
theme,
436
'color-selected-tree-highlight-active',
packages/react-devtools-shared/src/devtools/views/root.css
+6
-2
@@ -79,8 +79,10 @@
79
--light-color-record-inactive: #0088fa;
80
--light-color-resize-bar: #cccccc;
81
--light-color-scheduling-profiler-flame-graph-label: #000000;
82
- --light-color-scheduling-profiler-priority-background: #ededf0;
83
- --light-color-scheduling-profiler-priority-border: #d7d7db;
82
+ --light-color-scheduling-profiler-native-event: #aaaaaa;
83
+ --light-color-scheduling-profiler-native-event-hover: #888888;
84
+ --light-color-scheduling-profiler-priority-background: #f6f6f6;
85
+ --light-color-scheduling-profiler-priority-border: #eeeeee;
86
--light-color-scheduling-profiler-user-timing: #c9cacd;
87
--light-color-scheduling-profiler-user-timing-hover:#93959a;
88
--light-color-scheduling-profiler-react-idle: #edf6ff;
@@ -199,6 +201,8 @@
201
--dark-color-record-inactive: #61dafb;
202
--dark-color-resize-bar: #3d424a;
203
--dark-color-scheduling-profiler-flame-graph-label: #000000;
204
+ --dark-color-scheduling-profiler-native-event: #aaaaaa;
205
+ --dark-color-scheduling-profiler-native-event-hover: #888888;
206
--dark-color-scheduling-profiler-priority-background: #1d2129;
207
--dark-color-scheduling-profiler-priority-border: #282c34;
208
--dark-color-scheduling-profiler-user-timing: #c9cacd;