@samitouri / QOS-React-2 / commits / b1811ebf01

[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;