From 1c73ceed5f6248679d04c5e637290cfc6d3b5264 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 24 Sep 2021 12:56:27 -0400 Subject: [PATCH] Scheduling Profiler marks should include thrown Errors (#22419) --- .../src/CanvasPage.js | 113 +++----- .../src/EventTooltip.js | 30 +++ .../src/content-views/ReactMeasuresView.js | 26 +- .../src/content-views/SchedulingEventsView.js | 2 +- .../src/content-views/SuspenseEventsView.js | 2 +- .../src/content-views/ThrownErrorsView.js | 241 ++++++++++++++++++ .../src/content-views/constants.js | 8 + .../src/content-views/index.js | 1 + .../__tests__/preprocessData-test.internal.js | 50 ++++ .../src/import-worker/preprocessData.js | 11 + .../src/types.js | 10 + .../react-devtools-shared/src/constants.js | 8 +- 12 files changed, 417 insertions(+), 85 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 710d9cf03eedc..48efcf8105e7c 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -49,6 +49,7 @@ import { SchedulingEventsView, SnapshotsView, SuspenseEventsView, + ThrownErrorsView, TimeAxisMarkersView, UserTimingMarksView, } from './content-views'; @@ -138,6 +139,7 @@ const EMPTY_CONTEXT_INFO: ReactHoverContextInfo = { schedulingEvent: null, snapshot: null, suspenseEvent: null, + thrownError: null, userTimingMark: null, }; @@ -178,6 +180,7 @@ function AutoSizedCanvas({ const flamechartViewRef = useRef(null); const networkMeasuresViewRef = useRef(null); const snapshotsViewRef = useRef(null); + const thrownErrorsViewRef = useRef(null); const {hideMenu: hideContextMenu} = useContext(RegistryContext); @@ -271,6 +274,20 @@ function AutoSizedCanvas({ true, ); + let thrownErrorsViewWrapper = null; + if (data.thrownErrors.length > 0) { + const thrownErrorsView = new ThrownErrorsView( + surface, + defaultFrame, + data, + ); + thrownErrorsViewRef.current = thrownErrorsView; + thrownErrorsViewWrapper = createViewHelper( + thrownErrorsView, + 'thrown errors', + ); + } + const schedulingEventsView = new SchedulingEventsView( surface, defaultFrame, @@ -382,6 +399,9 @@ function AutoSizedCanvas({ } rootView.addSubview(nativeEventsViewWrapper); rootView.addSubview(schedulingEventsViewWrapper); + if (thrownErrorsViewWrapper !== null) { + rootView.addSubview(thrownErrorsViewWrapper); + } if (suspenseEventsViewWrapper !== null) { rootView.addSubview(suspenseEventsViewWrapper); } @@ -461,14 +481,7 @@ function AutoSizedCanvas({ userTimingMarksView.onHover = userTimingMark => { if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, - measure: null, - nativeEvent: null, - networkMeasure: null, - schedulingEvent: null, - snapshot: null, - suspenseEvent: null, + ...EMPTY_CONTEXT_INFO, userTimingMark, }); } @@ -480,15 +493,8 @@ function AutoSizedCanvas({ nativeEventsView.onHover = nativeEvent => { if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, - measure: null, + ...EMPTY_CONTEXT_INFO, nativeEvent, - networkMeasure: null, - schedulingEvent: null, - snapshot: null, - suspenseEvent: null, - userTimingMark: null, }); } }; @@ -499,15 +505,8 @@ function AutoSizedCanvas({ schedulingEventsView.onHover = schedulingEvent => { if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, - measure: null, - nativeEvent: null, - networkMeasure: null, + ...EMPTY_CONTEXT_INFO, schedulingEvent, - snapshot: null, - suspenseEvent: null, - userTimingMark: null, }); } }; @@ -518,15 +517,8 @@ function AutoSizedCanvas({ suspenseEventsView.onHover = suspenseEvent => { if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, - measure: null, - nativeEvent: null, - networkMeasure: null, - schedulingEvent: null, - snapshot: null, + ...EMPTY_CONTEXT_INFO, suspenseEvent, - userTimingMark: null, }); } }; @@ -537,15 +529,8 @@ function AutoSizedCanvas({ reactMeasuresView.onHover = measure => { if (!hoveredEvent || hoveredEvent.measure !== measure) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, + ...EMPTY_CONTEXT_INFO, measure, - nativeEvent: null, - networkMeasure: null, - schedulingEvent: null, - snapshot: null, - suspenseEvent: null, - userTimingMark: null, }); } }; @@ -559,15 +544,8 @@ function AutoSizedCanvas({ hoveredEvent.componentMeasure !== componentMeasure ) { setHoveredEvent({ + ...EMPTY_CONTEXT_INFO, componentMeasure, - flamechartStackFrame: null, - measure: null, - nativeEvent: null, - networkMeasure: null, - schedulingEvent: null, - snapshot: null, - suspenseEvent: null, - userTimingMark: null, }); } }; @@ -578,15 +556,8 @@ function AutoSizedCanvas({ snapshotsView.onHover = snapshot => { if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, - measure: null, - nativeEvent: null, - networkMeasure: null, - schedulingEvent: null, + ...EMPTY_CONTEXT_INFO, snapshot, - suspenseEvent: null, - userTimingMark: null, }); } }; @@ -600,15 +571,8 @@ function AutoSizedCanvas({ hoveredEvent.flamechartStackFrame !== flamechartStackFrame ) { setHoveredEvent({ - componentMeasure: null, + ...EMPTY_CONTEXT_INFO, flamechartStackFrame, - measure: null, - nativeEvent: null, - networkMeasure: null, - schedulingEvent: null, - snapshot: null, - suspenseEvent: null, - userTimingMark: null, }); } }); @@ -619,15 +583,20 @@ function AutoSizedCanvas({ networkMeasuresView.onHover = networkMeasure => { if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) { setHoveredEvent({ - componentMeasure: null, - flamechartStackFrame: null, - measure: null, - nativeEvent: null, + ...EMPTY_CONTEXT_INFO, networkMeasure, - schedulingEvent: null, - snapshot: null, - suspenseEvent: null, - userTimingMark: null, + }); + } + }; + } + + const {current: thrownErrorsView} = thrownErrorsViewRef; + if (thrownErrorsView) { + thrownErrorsView.onHover = thrownError => { + if (!hoveredEvent || hoveredEvent.thrownError !== thrownError) { + setHoveredEvent({ + ...EMPTY_CONTEXT_INFO, + thrownError, }); } }; diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index 5a8cf625f943f..353cf7fc00bb8 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -19,6 +19,7 @@ import type { SchedulingEvent, Snapshot, SuspenseEvent, + ThrownError, UserTimingMark, } from './types'; @@ -92,6 +93,7 @@ export default function EventTooltip({ schedulingEvent, snapshot, suspenseEvent, + thrownError, userTimingMark, } = hoveredEvent; @@ -118,6 +120,8 @@ export default function EventTooltip({ content = ; } else if (userTimingMark !== null) { content = ; + } else if (thrownError !== null) { + content = ; } if (content !== null) { @@ -436,3 +440,29 @@ const TooltipUserTimingMark = ({mark}: {|mark: UserTimingMark|}) => { ); }; + +const TooltipThrownError = ({thrownError}: {|thrownError: ThrownError|}) => { + const {componentName, message, phase, timestamp} = thrownError; + const label = `threw an error during ${phase}`; + return ( +
+ {componentName && ( + + {trimString(componentName, 100)} + + )} + {label} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+ {message !== '' && ( + <> +
Error:
+
{message}
+ + )} +
+
+ ); +}; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js index 7009356b4ce46..87a377be1a1f8 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -136,15 +136,23 @@ export class ReactMeasuresView extends View { // Commit phase rects are overlapped by layout and passive rects, // and it looks bad if text flows underneath/behind these overlayed rects. if (nextMeasure != null) { - textRect = { - ...measureRect, - size: { - width: - timestampToPosition(nextMeasure.timestamp, scaleFactor, frame) - - x, - height: REACT_MEASURE_HEIGHT, - }, - }; + // This clipping shouldn't apply for measures that don't overlap though, + // like passive effects that are processed after a delay, + // or if there are now layout or passive effects and the next measure is render or idle. + if (nextMeasure.timestamp < measure.timestamp + measure.duration) { + textRect = { + ...measureRect, + size: { + width: + timestampToPosition( + nextMeasure.timestamp, + scaleFactor, + frame, + ) - x, + height: REACT_MEASURE_HEIGHT, + }, + }; + } } break; case 'render-idle': diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js index 37ff0832e0261..39921a727366e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js @@ -195,7 +195,7 @@ export class SchedulingEventsView extends View { }; if (rectIntersectsRect(borderFrame, visibleArea)) { const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea); - context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillStyle = COLORS.REACT_WORK_BORDER; context.fillRect( borderDrawableRect.origin.x, borderDrawableRect.origin.y, diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index b7237079c5d5b..b6fa643f60054 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -272,7 +272,7 @@ export class SuspenseEventsView extends View { borderFrame, visibleArea, ); - context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillStyle = COLORS.REACT_WORK_BORDER; context.fillRect( borderDrawableRect.origin.x, borderDrawableRect.origin.y, diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js new file mode 100644 index 0000000000000..e1d084ee40935 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ThrownErrorsView.js @@ -0,0 +1,241 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ThrownError, ReactProfilerData} from '../types'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + Size, + ViewRefs, +} from '../view-base'; + +import { + positioningScaleFactor, + timestampToPosition, + positionToTimestamp, + widthToDuration, +} from './utils/positioning'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + COLORS, + TOP_ROW_PADDING, + REACT_EVENT_DIAMETER, + BORDER_SIZE, +} from './constants'; + +const EVENT_ROW_HEIGHT_FIXED = + TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING; + +export class ThrownErrorsView extends View { + _profilerData: ReactProfilerData; + _intrinsicSize: Size; + _hoveredEvent: ThrownError | null = null; + onHover: ((event: ThrownError | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + this._profilerData = profilerData; + + this._intrinsicSize = { + width: this._profilerData.duration, + height: EVENT_ROW_HEIGHT_FIXED, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredEvent(hoveredEvent: ThrownError | null) { + if (this._hoveredEvent === hoveredEvent) { + return; + } + this._hoveredEvent = hoveredEvent; + this.setNeedsDisplay(); + } + + /** + * Draw a single `ThrownError` as a circle in the canvas. + */ + _drawSingleThrownError( + context: CanvasRenderingContext2D, + rect: Rect, + thrownError: ThrownError, + baseY: number, + scaleFactor: number, + showHoverHighlight: boolean, + ) { + const {frame} = this; + const {timestamp} = thrownError; + + const x = timestampToPosition(timestamp, scaleFactor, frame); + const radius = REACT_EVENT_DIAMETER / 2; + const eventRect: Rect = { + origin: { + x: x - radius, + y: baseY, + }, + size: {width: REACT_EVENT_DIAMETER, height: REACT_EVENT_DIAMETER}, + }; + if (!rectIntersectsRect(eventRect, rect)) { + return; // Not in view + } + + const fillStyle = showHoverHighlight + ? COLORS.REACT_THROWN_ERROR_HOVER + : COLORS.REACT_THROWN_ERROR; + + const y = eventRect.origin.y + radius; + + context.beginPath(); + context.fillStyle = fillStyle; + context.arc(x, y, radius, 0, 2 * Math.PI); + context.fill(); + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {thrownErrors}, + _hoveredEvent, + visibleArea, + } = this; + + context.fillStyle = COLORS.BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + // Draw events + const baseY = frame.origin.y + TOP_ROW_PADDING; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + const highlightedEvents: ThrownError[] = []; + + thrownErrors.forEach(thrownError => { + if (thrownError === _hoveredEvent) { + highlightedEvents.push(thrownError); + return; + } + this._drawSingleThrownError( + context, + visibleArea, + thrownError, + baseY, + scaleFactor, + false, + ); + }); + + // Draw the highlighted items on top so they stand out. + // This is helpful if there are multiple (overlapping) items close to each other. + highlightedEvents.forEach(thrownError => { + this._drawSingleThrownError( + context, + visibleArea, + thrownError, + baseY, + scaleFactor, + true, + ); + }); + + // Render bottom borders. + // Propose border rect, check if intersects with `rect`, draw intersection. + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE, + }, + size: { + width: frame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea); + context.fillStyle = COLORS.REACT_WORK_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) { + const {frame, onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + const { + _profilerData: {thrownErrors}, + } = this; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + const eventTimestampAllowance = widthToDuration( + REACT_EVENT_DIAMETER / 2, + scaleFactor, + ); + + // Because data ranges may overlap, we want to find the last intersecting item. + // This will always be the one on "top" (the one the user is hovering over). + for (let index = thrownErrors.length - 1; index >= 0; index--) { + const event = thrownErrors[index]; + const {timestamp} = event; + + if ( + timestamp - eventTimestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + eventTimestampAllowance + ) { + this.currentCursor = 'context-menu'; + viewRefs.hoveredView = this; + onHover(event); + return; + } + } + + onHover(null); + } + + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction, viewRefs); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index 48ba22aed30ae..a715945f77e92 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -84,6 +84,8 @@ export let COLORS = { REACT_SUSPENSE_RESOLVED_EVENT_HOVER: '', REACT_SUSPENSE_UNRESOLVED_EVENT: '', REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: '', + REACT_THROWN_ERROR: '', + REACT_THROWN_ERROR_HOVER: '', REACT_WORK_BORDER: '', SCROLL_CARET: '', TEXT_COLOR: '', @@ -218,6 +220,12 @@ export function updateColorsToMatchTheme(element: Element): boolean { REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-suspense-unresolved-hover', ), + REACT_THROWN_ERROR: computedStyle.getPropertyValue( + '--color-scheduling-profiler-thrown-error', + ), + REACT_THROWN_ERROR_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-thrown-error-hover', + ), REACT_WORK_BORDER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-work-border', ), diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/index.js b/packages/react-devtools-scheduling-profiler/src/content-views/index.js index 21c8cb570b35c..5cf1e543b50c6 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/index.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/index.js @@ -15,5 +15,6 @@ export * from './ReactMeasuresView'; export * from './SchedulingEventsView'; export * from './SnapshotsView'; export * from './SuspenseEventsView'; +export * from './ThrownErrorsView'; export * from './TimeAxisMarkersView'; export * from './UserTimingMarksView'; diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index 3ea5051bcf53e..11af34c9680a8 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -354,6 +354,7 @@ describe('preprocessData', () => { "snapshots": Array [], "startTime": 1, "suspenseEvents": Array [], + "thrownErrors": Array [], } `); } @@ -570,6 +571,7 @@ describe('preprocessData', () => { "snapshots": Array [], "startTime": 1, "suspenseEvents": Array [], + "thrownErrors": Array [], } `); } @@ -761,6 +763,7 @@ describe('preprocessData', () => { "snapshots": Array [], "startTime": 4, "suspenseEvents": Array [], + "thrownErrors": Array [], } `); } @@ -1107,6 +1110,7 @@ describe('preprocessData', () => { "snapshots": Array [], "startTime": 4, "suspenseEvents": Array [], + "thrownErrors": Array [], } `); } @@ -1537,6 +1541,52 @@ describe('preprocessData', () => { }); }); + describe('errors thrown while rendering', () => { + it('shoult parse Errors thrown during render', async () => { + spyOnDev(console, 'error'); + spyOnProd(console, 'error'); + + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return null; + } + return this.props.children; + } + } + + function ExampleThatThrows() { + throw Error('Expected error'); + } + + if (gate(flags => flags.enableSchedulingProfiler)) { + const testMarks = [creactCpuProfilerSample()]; + + // Mount and commit the app + const root = ReactDOM.createRoot(document.createElement('div')); + act(() => + root.render( + + + , + ), + ); + + testMarks.push(...createUserTimingData(clearedMarks)); + + const data = await preprocessData(testMarks); + expect(data.thrownErrors).toHaveLength(2); + expect(data.thrownErrors[0].message).toMatchInlineSnapshot( + '"Expected error"', + ); + } + }); + }); + describe('suspend during an update', () => { // This also tests an edge case where the a component suspends while profiling // before the first commit is logged (so the lane-to-labels map will not yet exist). diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index 5b1ec5636f07e..02ba5c31c8c29 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -550,6 +550,16 @@ function processTimelineEvent( } currentProfilerData.schedulingEvents.push(stateUpdateEvent); + } else if (name.startsWith('--error-')) { + const [componentName, phase, message] = name.substr(8).split('-'); + + currentProfilerData.thrownErrors.push({ + componentName, + message, + phase: ((phase: any): Phase), + timestamp: startTime, + type: 'thrown-error', + }); } // eslint-disable-line brace-style // React Events - suspense @@ -865,6 +875,7 @@ export default async function preprocessData( snapshots: [], startTime: 0, suspenseEvents: [], + thrownErrors: [], }; // Sort `timeline`. JSON Array Format trace events need not be ordered. See: diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index 073f4e1fbff10..ff68f3435e5ff 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -65,6 +65,14 @@ export type SuspenseEvent = {| +type: 'suspense', |}; +export type ThrownError = {| + +componentName?: string, + +message: string, + +phase: Phase, + +timestamp: Milliseconds, + +type: 'thrown-error', +|}; + export type SchedulingEvent = | ReactScheduleRenderEvent | ReactScheduleStateUpdateEvent @@ -175,6 +183,7 @@ export type ReactProfilerData = {| snapshots: Snapshot[], startTime: number, suspenseEvents: SuspenseEvent[], + thrownErrors: ThrownError[], |}; export type ReactHoverContextInfo = {| @@ -186,5 +195,6 @@ export type ReactHoverContextInfo = {| schedulingEvent: SchedulingEvent | null, suspenseEvent: SuspenseEvent | null, snapshot: Snapshot | null, + thrownError: ThrownError | null, userTimingMark: UserTimingMark | null, |}; diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 29d805890dedb..60ce987e267f5 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -177,9 +177,11 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281', '--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd', '--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a', + '--color-scheduling-profiler-thrown-error': '#ee1638', + '--color-scheduling-profiler-thrown-error-hover': '#da1030', '--color-scheduling-profiler-text-color': '#000000', '--color-scheduling-profiler-text-dim-color': '#ccc', - '--color-scheduling-profiler-react-work-border': '#ffffff', + '--color-scheduling-profiler-react-work-border': '#eeeeee', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', '--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)', @@ -316,9 +318,11 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281', '--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd', '--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a', + '--color-scheduling-profiler-thrown-error': '#fb3655', + '--color-scheduling-profiler-thrown-error-hover': '#f82042', '--color-scheduling-profiler-text-color': '#282c34', '--color-scheduling-profiler-text-dim-color': '#555b66', - '--color-scheduling-profiler-react-work-border': '#ffffff', + '--color-scheduling-profiler-react-work-border': '#3d424a', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', '--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)',