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