Scheduling Profiler marks should include thrown Errors (#22419)
Brian Vaughn committed
Sep 24, 2021 at 12:56 UTC
1c73ceed5f6248679d04c5e637290cfc6d3b5264
12 files changed
+417
-85
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+41
-72
@@ -49,6 +49,7 @@ import {
49
SchedulingEventsView,
50
SnapshotsView,
51
SuspenseEventsView,
52
+ ThrownErrorsView,
53
TimeAxisMarkersView,
54
UserTimingMarksView,
55
} from './content-views';
@@ -138,6 +139,7 @@ const EMPTY_CONTEXT_INFO: ReactHoverContextInfo = {
139
schedulingEvent: null,
140
snapshot: null,
141
suspenseEvent: null,
142
+ thrownError: null,
143
userTimingMark: null,
144
};
145
@@ -178,6 +180,7 @@ function AutoSizedCanvas({
180
const flamechartViewRef = useRef(null);
181
const networkMeasuresViewRef = useRef(null);
182
const snapshotsViewRef = useRef(null);
183
+ const thrownErrorsViewRef = useRef(null);
184
185
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
186
@@ -271,6 +274,20 @@ function AutoSizedCanvas({
274
true,
275
);
276
277
+ let thrownErrorsViewWrapper = null;
278
+ if (data.thrownErrors.length > 0) {
279
+ const thrownErrorsView = new ThrownErrorsView(
280
+ surface,
281
+ defaultFrame,
282
+ data,
283
+ );
284
+ thrownErrorsViewRef.current = thrownErrorsView;
285
+ thrownErrorsViewWrapper = createViewHelper(
286
+ thrownErrorsView,
287
+ 'thrown errors',
288
+ );
289
+ }
290
+
291
const schedulingEventsView = new SchedulingEventsView(
292
surface,
293
defaultFrame,
@@ -382,6 +399,9 @@ function AutoSizedCanvas({
399
}
400
rootView.addSubview(nativeEventsViewWrapper);
401
rootView.addSubview(schedulingEventsViewWrapper);
402
+ if (thrownErrorsViewWrapper !== null) {
403
+ rootView.addSubview(thrownErrorsViewWrapper);
404
+ }
405
if (suspenseEventsViewWrapper !== null) {
406
rootView.addSubview(suspenseEventsViewWrapper);
407
}
@@ -461,14 +481,7 @@ function AutoSizedCanvas({
481
userTimingMarksView.onHover = userTimingMark => {
482
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
483
setHoveredEvent({
464
- componentMeasure: null,
465
- flamechartStackFrame: null,
466
- measure: null,
467
- nativeEvent: null,
468
- networkMeasure: null,
469
- schedulingEvent: null,
470
- snapshot: null,
471
- suspenseEvent: null,
484
+ ...EMPTY_CONTEXT_INFO,
485
userTimingMark,
486
});
487
}
@@ -480,15 +493,8 @@ function AutoSizedCanvas({
493
nativeEventsView.onHover = nativeEvent => {
494
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
495
setHoveredEvent({
483
- componentMeasure: null,
484
- flamechartStackFrame: null,
485
- measure: null,
496
+ ...EMPTY_CONTEXT_INFO,
497
nativeEvent,
487
- networkMeasure: null,
488
- schedulingEvent: null,
489
- snapshot: null,
490
- suspenseEvent: null,
491
- userTimingMark: null,
498
});
499
}
500
};
@@ -499,15 +505,8 @@ function AutoSizedCanvas({
505
schedulingEventsView.onHover = schedulingEvent => {
506
if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
507
setHoveredEvent({
502
- componentMeasure: null,
503
- flamechartStackFrame: null,
504
- measure: null,
505
- nativeEvent: null,
506
- networkMeasure: null,
508
+ ...EMPTY_CONTEXT_INFO,
509
schedulingEvent,
508
- snapshot: null,
509
- suspenseEvent: null,
510
- userTimingMark: null,
510
});
511
}
512
};
@@ -518,15 +517,8 @@ function AutoSizedCanvas({
517
suspenseEventsView.onHover = suspenseEvent => {
518
if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
519
setHoveredEvent({
521
- componentMeasure: null,
522
- flamechartStackFrame: null,
523
- measure: null,
524
- nativeEvent: null,
525
- networkMeasure: null,
526
- schedulingEvent: null,
527
- snapshot: null,
520
+ ...EMPTY_CONTEXT_INFO,
521
suspenseEvent,
529
- userTimingMark: null,
522
});
523
}
524
};
@@ -537,15 +529,8 @@ function AutoSizedCanvas({
529
reactMeasuresView.onHover = measure => {
530
if (!hoveredEvent || hoveredEvent.measure !== measure) {
531
setHoveredEvent({
540
- componentMeasure: null,
541
- flamechartStackFrame: null,
532
+ ...EMPTY_CONTEXT_INFO,
533
measure,
543
- nativeEvent: null,
544
- networkMeasure: null,
545
- schedulingEvent: null,
546
- snapshot: null,
547
- suspenseEvent: null,
548
- userTimingMark: null,
534
});
535
}
536
};
@@ -559,15 +544,8 @@ function AutoSizedCanvas({
544
hoveredEvent.componentMeasure !== componentMeasure
545
) {
546
setHoveredEvent({
547
+ ...EMPTY_CONTEXT_INFO,
548
componentMeasure,
563
- flamechartStackFrame: null,
564
- measure: null,
565
- nativeEvent: null,
566
- networkMeasure: null,
567
- schedulingEvent: null,
568
- snapshot: null,
569
- suspenseEvent: null,
570
- userTimingMark: null,
549
});
550
}
551
};
@@ -578,15 +556,8 @@ function AutoSizedCanvas({
556
snapshotsView.onHover = snapshot => {
557
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
558
setHoveredEvent({
581
- componentMeasure: null,
582
- flamechartStackFrame: null,
583
- measure: null,
584
- nativeEvent: null,
585
- networkMeasure: null,
586
- schedulingEvent: null,
559
+ ...EMPTY_CONTEXT_INFO,
560
snapshot,
588
- suspenseEvent: null,
589
- userTimingMark: null,
561
});
562
}
563
};
@@ -600,15 +571,8 @@ function AutoSizedCanvas({
571
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
572
) {
573
setHoveredEvent({
603
- componentMeasure: null,
574
+ ...EMPTY_CONTEXT_INFO,
575
flamechartStackFrame,
605
- measure: null,
606
- nativeEvent: null,
607
- networkMeasure: null,
608
- schedulingEvent: null,
609
- snapshot: null,
610
- suspenseEvent: null,
611
- userTimingMark: null,
576
});
577
}
578
});
@@ -619,15 +583,20 @@ function AutoSizedCanvas({
583
networkMeasuresView.onHover = networkMeasure => {
584
if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) {
585
setHoveredEvent({
622
- componentMeasure: null,
623
- flamechartStackFrame: null,
624
- measure: null,
625
- nativeEvent: null,
586
+ ...EMPTY_CONTEXT_INFO,
587
networkMeasure,
627
- schedulingEvent: null,
628
- snapshot: null,
629
- suspenseEvent: null,
630
- userTimingMark: null,
588
+ });
589
+ }
590
+ };
591
+ }
592
+
593
+ const {current: thrownErrorsView} = thrownErrorsViewRef;
594
+ if (thrownErrorsView) {
595
+ thrownErrorsView.onHover = thrownError => {
596
+ if (!hoveredEvent || hoveredEvent.thrownError !== thrownError) {
597
+ setHoveredEvent({
598
+ ...EMPTY_CONTEXT_INFO,
599
+ thrownError,
600
});
601
}
602
};
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+30
@@ -19,6 +19,7 @@ import type {
19
SchedulingEvent,
20
Snapshot,
21
SuspenseEvent,
22
+ ThrownError,
23
UserTimingMark,
24
} from './types';
25
@@ -92,6 +93,7 @@ export default function EventTooltip({
93
schedulingEvent,
94
snapshot,
95
suspenseEvent,
96
+ thrownError,
97
userTimingMark,
98
} = hoveredEvent;
99
@@ -118,6 +120,8 @@ export default function EventTooltip({
120
content = <TooltipFlamechartNode stackFrame={flamechartStackFrame} />;
121
} else if (userTimingMark !== null) {
122
content = <TooltipUserTimingMark mark={userTimingMark} />;
123
+ } else if (thrownError !== null) {
124
+ content = <TooltipThrownError thrownError={thrownError} />;
125
}
126
127
if (content !== null) {
@@ -436,3 +440,29 @@ const TooltipUserTimingMark = ({mark}: {|mark: UserTimingMark|}) => {
440
</div>
441
);
442
};
443
+
444
+const TooltipThrownError = ({thrownError}: {|thrownError: ThrownError|}) => {
445
+ const {componentName, message, phase, timestamp} = thrownError;
446
+ const label = `threw an error during ${phase}`;
447
+ return (
448
+ <div className={styles.TooltipSection}>
449
+ {componentName && (
450
+ <span className={styles.ComponentName}>
451
+ {trimString(componentName, 100)}
452
+ </span>
453
+ )}
454
+ <span className={styles.UserTimingLabel}>{label}</span>
455
+ <div className={styles.Divider} />
456
+ <div className={styles.DetailsGrid}>
457
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
458
+ <div>{formatTimestamp(timestamp)}</div>
459
+ {message !== '' && (
460
+ <>
461
+ <div className={styles.DetailsGridLabel}>Error:</div>
462
+ <div>{message}</div>
463
+ </>
464
+ )}
465
+ </div>
466
+ </div>
467
+ );
468
+};
packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
+17
-9
@@ -136,15 +136,23 @@ export class ReactMeasuresView extends View {
136
// Commit phase rects are overlapped by layout and passive rects,
137
// and it looks bad if text flows underneath/behind these overlayed rects.
138
if (nextMeasure != null) {
139
- textRect = {
140
- ...measureRect,
141
- size: {
142
- width:
143
- timestampToPosition(nextMeasure.timestamp, scaleFactor, frame) -
144
- x,
145
- height: REACT_MEASURE_HEIGHT,
146
- },
147
- };
139
+ // This clipping shouldn't apply for measures that don't overlap though,
140
+ // like passive effects that are processed after a delay,
141
+ // or if there are now layout or passive effects and the next measure is render or idle.
142
+ if (nextMeasure.timestamp < measure.timestamp + measure.duration) {
143
+ textRect = {
144
+ ...measureRect,
145
+ size: {
146
+ width:
147
+ timestampToPosition(
148
+ nextMeasure.timestamp,
149
+ scaleFactor,
150
+ frame,
151
+ ) - x,
152
+ height: REACT_MEASURE_HEIGHT,
153
+ },
154
+ };
155
+ }
156
}
157
break;
158
case 'render-idle':
packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js
+1
-1
@@ -195,7 +195,7 @@ export class SchedulingEventsView extends View {
195
};
196
if (rectIntersectsRect(borderFrame, visibleArea)) {
197
const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
198
- context.fillStyle = COLORS.PRIORITY_BORDER;
198
+ context.fillStyle = COLORS.REACT_WORK_BORDER;
199
context.fillRect(
200
borderDrawableRect.origin.x,
201
borderDrawableRect.origin.y,
packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
+1
-1
@@ -272,7 +272,7 @@ export class SuspenseEventsView extends View {
272
borderFrame,
273
visibleArea,
274
);
275
- context.fillStyle = COLORS.PRIORITY_BORDER;
275
+ context.fillStyle = COLORS.REACT_WORK_BORDER;
276
context.fillRect(
277
borderDrawableRect.origin.x,
278
borderDrawableRect.origin.y,
packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js
new
+241
@@ -0,0 +1,241 @@
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 {ThrownError, ReactProfilerData} from '../types';
11
+import type {
12
+ Interaction,
13
+ MouseMoveInteraction,
14
+ Rect,
15
+ Size,
16
+ ViewRefs,
17
+} from '../view-base';
18
+
19
+import {
20
+ positioningScaleFactor,
21
+ timestampToPosition,
22
+ positionToTimestamp,
23
+ widthToDuration,
24
+} from './utils/positioning';
25
+import {
26
+ View,
27
+ Surface,
28
+ rectContainsPoint,
29
+ rectIntersectsRect,
30
+ intersectionOfRects,
31
+} from '../view-base';
32
+import {
33
+ COLORS,
34
+ TOP_ROW_PADDING,
35
+ REACT_EVENT_DIAMETER,
36
+ BORDER_SIZE,
37
+} from './constants';
38
+
39
+const EVENT_ROW_HEIGHT_FIXED =
40
+ TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
41
+
42
+export class ThrownErrorsView extends View {
43
+ _profilerData: ReactProfilerData;
44
+ _intrinsicSize: Size;
45
+ _hoveredEvent: ThrownError | null = null;
46
+ onHover: ((event: ThrownError | null) => void) | null = null;
47
+
48
+ constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
49
+ super(surface, frame);
50
+ this._profilerData = profilerData;
51
+
52
+ this._intrinsicSize = {
53
+ width: this._profilerData.duration,
54
+ height: EVENT_ROW_HEIGHT_FIXED,
55
+ };
56
+ }
57
+
58
+ desiredSize() {
59
+ return this._intrinsicSize;
60
+ }
61
+
62
+ setHoveredEvent(hoveredEvent: ThrownError | null) {
63
+ if (this._hoveredEvent === hoveredEvent) {
64
+ return;
65
+ }
66
+ this._hoveredEvent = hoveredEvent;
67
+ this.setNeedsDisplay();
68
+ }
69
+
70
+ /**
71
+ * Draw a single `ThrownError` as a circle in the canvas.
72
+ */
73
+ _drawSingleThrownError(
74
+ context: CanvasRenderingContext2D,
75
+ rect: Rect,
76
+ thrownError: ThrownError,
77
+ baseY: number,
78
+ scaleFactor: number,
79
+ showHoverHighlight: boolean,
80
+ ) {
81
+ const {frame} = this;
82
+ const {timestamp} = thrownError;
83
+
84
+ const x = timestampToPosition(timestamp, scaleFactor, frame);
85
+ const radius = REACT_EVENT_DIAMETER / 2;
86
+ const eventRect: Rect = {
87
+ origin: {
88
+ x: x - radius,
89
+ y: baseY,
90
+ },
91
+ size: {width: REACT_EVENT_DIAMETER, height: REACT_EVENT_DIAMETER},
92
+ };
93
+ if (!rectIntersectsRect(eventRect, rect)) {
94
+ return; // Not in view
95
+ }
96
+
97
+ const fillStyle = showHoverHighlight
98
+ ? COLORS.REACT_THROWN_ERROR_HOVER
99
+ : COLORS.REACT_THROWN_ERROR;
100
+
101
+ const y = eventRect.origin.y + radius;
102
+
103
+ context.beginPath();
104
+ context.fillStyle = fillStyle;
105
+ context.arc(x, y, radius, 0, 2 * Math.PI);
106
+ context.fill();
107
+ }
108
+
109
+ draw(context: CanvasRenderingContext2D) {
110
+ const {
111
+ frame,
112
+ _profilerData: {thrownErrors},
113
+ _hoveredEvent,
114
+ visibleArea,
115
+ } = this;
116
+
117
+ context.fillStyle = COLORS.BACKGROUND;
118
+ context.fillRect(
119
+ visibleArea.origin.x,
120
+ visibleArea.origin.y,
121
+ visibleArea.size.width,
122
+ visibleArea.size.height,
123
+ );
124
+
125
+ // Draw events
126
+ const baseY = frame.origin.y + TOP_ROW_PADDING;
127
+ const scaleFactor = positioningScaleFactor(
128
+ this._intrinsicSize.width,
129
+ frame,
130
+ );
131
+
132
+ const highlightedEvents: ThrownError[] = [];
133
+
134
+ thrownErrors.forEach(thrownError => {
135
+ if (thrownError === _hoveredEvent) {
136
+ highlightedEvents.push(thrownError);
137
+ return;
138
+ }
139
+ this._drawSingleThrownError(
140
+ context,
141
+ visibleArea,
142
+ thrownError,
143
+ baseY,
144
+ scaleFactor,
145
+ false,
146
+ );
147
+ });
148
+
149
+ // Draw the highlighted items on top so they stand out.
150
+ // This is helpful if there are multiple (overlapping) items close to each other.
151
+ highlightedEvents.forEach(thrownError => {
152
+ this._drawSingleThrownError(
153
+ context,
154
+ visibleArea,
155
+ thrownError,
156
+ baseY,
157
+ scaleFactor,
158
+ true,
159
+ );
160
+ });
161
+
162
+ // Render bottom borders.
163
+ // Propose border rect, check if intersects with `rect`, draw intersection.
164
+ const borderFrame: Rect = {
165
+ origin: {
166
+ x: frame.origin.x,
167
+ y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
168
+ },
169
+ size: {
170
+ width: frame.size.width,
171
+ height: BORDER_SIZE,
172
+ },
173
+ };
174
+ if (rectIntersectsRect(borderFrame, visibleArea)) {
175
+ const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
176
+ context.fillStyle = COLORS.REACT_WORK_BORDER;
177
+ context.fillRect(
178
+ borderDrawableRect.origin.x,
179
+ borderDrawableRect.origin.y,
180
+ borderDrawableRect.size.width,
181
+ borderDrawableRect.size.height,
182
+ );
183
+ }
184
+ }
185
+
186
+ /**
187
+ * @private
188
+ */
189
+ _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
190
+ const {frame, onHover, visibleArea} = this;
191
+ if (!onHover) {
192
+ return;
193
+ }
194
+
195
+ const {location} = interaction.payload;
196
+ if (!rectContainsPoint(location, visibleArea)) {
197
+ onHover(null);
198
+ return;
199
+ }
200
+
201
+ const {
202
+ _profilerData: {thrownErrors},
203
+ } = this;
204
+ const scaleFactor = positioningScaleFactor(
205
+ this._intrinsicSize.width,
206
+ frame,
207
+ );
208
+ const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
209
+ const eventTimestampAllowance = widthToDuration(
210
+ REACT_EVENT_DIAMETER / 2,
211
+ scaleFactor,
212
+ );
213
+
214
+ // Because data ranges may overlap, we want to find the last intersecting item.
215
+ // This will always be the one on "top" (the one the user is hovering over).
216
+ for (let index = thrownErrors.length - 1; index >= 0; index--) {
217
+ const event = thrownErrors[index];
218
+ const {timestamp} = event;
219
+
220
+ if (
221
+ timestamp - eventTimestampAllowance <= hoverTimestamp &&
222
+ hoverTimestamp <= timestamp + eventTimestampAllowance
223
+ ) {
224
+ this.currentCursor = 'context-menu';
225
+ viewRefs.hoveredView = this;
226
+ onHover(event);
227
+ return;
228
+ }
229
+ }
230
+
231
+ onHover(null);
232
+ }
233
+
234
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
235
+ switch (interaction.type) {
236
+ case 'mousemove':
237
+ this._handleMouseMove(interaction, viewRefs);
238
+ break;
239
+ }
240
+ }
241
+}
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+8
@@ -84,6 +84,8 @@ export let COLORS = {
84
REACT_SUSPENSE_RESOLVED_EVENT_HOVER: '',
85
REACT_SUSPENSE_UNRESOLVED_EVENT: '',
86
REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: '',
87
+ REACT_THROWN_ERROR: '',
88
+ REACT_THROWN_ERROR_HOVER: '',
89
REACT_WORK_BORDER: '',
90
SCROLL_CARET: '',
91
TEXT_COLOR: '',
@@ -218,6 +220,12 @@ export function updateColorsToMatchTheme(element: Element): boolean {
220
REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: computedStyle.getPropertyValue(
221
'--color-scheduling-profiler-react-suspense-unresolved-hover',
222
),
223
+ REACT_THROWN_ERROR: computedStyle.getPropertyValue(
224
+ '--color-scheduling-profiler-thrown-error',
225
+ ),
226
+ REACT_THROWN_ERROR_HOVER: computedStyle.getPropertyValue(
227
+ '--color-scheduling-profiler-thrown-error-hover',
228
+ ),
229
REACT_WORK_BORDER: computedStyle.getPropertyValue(
230
'--color-scheduling-profiler-react-work-border',
231
),
packages/react-devtools-scheduling-profiler/src/content-views/index.js
+1
@@ -15,5 +15,6 @@ export * from './ReactMeasuresView';
15
export * from './SchedulingEventsView';
16
export * from './SnapshotsView';
17
export * from './SuspenseEventsView';
18
+export * from './ThrownErrorsView';
19
export * from './TimeAxisMarkersView';
20
export * from './UserTimingMarksView';
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+50
@@ -354,6 +354,7 @@ describe('preprocessData', () => {
354
"snapshots": Array [],
355
"startTime": 1,
356
"suspenseEvents": Array [],
357
+ "thrownErrors": Array [],
358
}
359
`);
360
}
@@ -570,6 +571,7 @@ describe('preprocessData', () => {
571
"snapshots": Array [],
572
"startTime": 1,
573
"suspenseEvents": Array [],
574
+ "thrownErrors": Array [],
575
}
576
`);
577
}
@@ -761,6 +763,7 @@ describe('preprocessData', () => {
763
"snapshots": Array [],
764
"startTime": 4,
765
"suspenseEvents": Array [],
766
+ "thrownErrors": Array [],
767
}
768
`);
769
}
@@ -1107,6 +1110,7 @@ describe('preprocessData', () => {
1110
"snapshots": Array [],
1111
"startTime": 4,
1112
"suspenseEvents": Array [],
1113
+ "thrownErrors": Array [],
1114
}
1115
`);
1116
}
@@ -1537,6 +1541,52 @@ describe('preprocessData', () => {
1541
});
1542
});
1543
1544
+ describe('errors thrown while rendering', () => {
1545
+ it('shoult parse Errors thrown during render', async () => {
1546
+ spyOnDev(console, 'error');
1547
+ spyOnProd(console, 'error');
1548
+
1549
+ class ErrorBoundary extends React.Component {
1550
+ state = {error: null};
1551
+ componentDidCatch(error) {
1552
+ this.setState({error});
1553
+ }
1554
+ render() {
1555
+ if (this.state.error) {
1556
+ return null;
1557
+ }
1558
+ return this.props.children;
1559
+ }
1560
+ }
1561
+
1562
+ function ExampleThatThrows() {
1563
+ throw Error('Expected error');
1564
+ }
1565
+
1566
+ if (gate(flags => flags.enableSchedulingProfiler)) {
1567
+ const testMarks = [creactCpuProfilerSample()];
1568
+
1569
+ // Mount and commit the app
1570
+ const root = ReactDOM.createRoot(document.createElement('div'));
1571
+ act(() =>
1572
+ root.render(
1573
+ <ErrorBoundary>
1574
+ <ExampleThatThrows />
1575
+ </ErrorBoundary>,
1576
+ ),
1577
+ );
1578
+
1579
+ testMarks.push(...createUserTimingData(clearedMarks));
1580
+
1581
+ const data = await preprocessData(testMarks);
1582
+ expect(data.thrownErrors).toHaveLength(2);
1583
+ expect(data.thrownErrors[0].message).toMatchInlineSnapshot(
1584
+ '"Expected error"',
1585
+ );
1586
+ }
1587
+ });
1588
+ });
1589
+
1590
describe('suspend during an update', () => {
1591
// This also tests an edge case where the a component suspends while profiling
1592
// before the first commit is logged (so the lane-to-labels map will not yet exist).
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+11
@@ -550,6 +550,16 @@ function processTimelineEvent(
550
}
551
552
currentProfilerData.schedulingEvents.push(stateUpdateEvent);
553
+ } else if (name.startsWith('--error-')) {
554
+ const [componentName, phase, message] = name.substr(8).split('-');
555
+
556
+ currentProfilerData.thrownErrors.push({
557
+ componentName,
558
+ message,
559
+ phase: ((phase: any): Phase),
560
+ timestamp: startTime,
561
+ type: 'thrown-error',
562
+ });
563
} // eslint-disable-line brace-style
564
565
// React Events - suspense
@@ -865,6 +875,7 @@ export default async function preprocessData(
875
snapshots: [],
876
startTime: 0,
877
suspenseEvents: [],
878
+ thrownErrors: [],
879
};
880
881
// Sort `timeline`. JSON Array Format trace events need not be ordered. See:
packages/react-devtools-scheduling-profiler/src/types.js
+10
@@ -65,6 +65,14 @@ export type SuspenseEvent = {|
65
+type: 'suspense',
66
|};
67
68
+export type ThrownError = {|
69
+ +componentName?: string,
70
+ +message: string,
71
+ +phase: Phase,
72
+ +timestamp: Milliseconds,
73
+ +type: 'thrown-error',
74
+|};
75
+
76
export type SchedulingEvent =
77
| ReactScheduleRenderEvent
78
| ReactScheduleStateUpdateEvent
@@ -175,6 +183,7 @@ export type ReactProfilerData = {|
183
snapshots: Snapshot[],
184
startTime: number,
185
suspenseEvents: SuspenseEvent[],
186
+ thrownErrors: ThrownError[],
187
|};
188
189
export type ReactHoverContextInfo = {|
@@ -186,5 +195,6 @@ export type ReactHoverContextInfo = {|
195
schedulingEvent: SchedulingEvent | null,
196
suspenseEvent: SuspenseEvent | null,
197
snapshot: Snapshot | null,
198
+ thrownError: ThrownError | null,
199
userTimingMark: UserTimingMark | null,
200
|};
packages/react-devtools-shared/src/constants.js
+6
-2
@@ -177,9 +177,11 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
177
'--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281',
178
'--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd',
179
'--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a',
180
+ '--color-scheduling-profiler-thrown-error': '#ee1638',
181
+ '--color-scheduling-profiler-thrown-error-hover': '#da1030',
182
'--color-scheduling-profiler-text-color': '#000000',
183
'--color-scheduling-profiler-text-dim-color': '#ccc',
182
- '--color-scheduling-profiler-react-work-border': '#ffffff',
184
+ '--color-scheduling-profiler-react-work-border': '#eeeeee',
185
'--color-search-match': 'yellow',
186
'--color-search-match-current': '#f7923b',
187
'--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)',
@@ -316,9 +318,11 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
318
'--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281',
319
'--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd',
320
'--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a',
321
+ '--color-scheduling-profiler-thrown-error': '#fb3655',
322
+ '--color-scheduling-profiler-thrown-error-hover': '#f82042',
323
'--color-scheduling-profiler-text-color': '#282c34',
324
'--color-scheduling-profiler-text-dim-color': '#555b66',
321
- '--color-scheduling-profiler-react-work-border': '#ffffff',
325
+ '--color-scheduling-profiler-react-work-border': '#3d424a',
326
'--color-search-match': 'yellow',
327
'--color-search-match-current': '#f7923b',
328
'--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)',