@samitouri / QOS-React-2 / commits / 1c73ceed5f

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)',