Display warnings in tooltips for native events that render sync updates (#21975)
Brian Vaughn committed
Jul 27, 2021 at 17:22 UTC
cdccdbe1710dec6b954b2294d165da5671cf6443
6 files changed
+53
-16
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+1
@@ -509,6 +509,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
509
</ContextMenu>
510
{!isContextMenuShown && !surfaceRef.current.hasActiveView() && (
511
<EventTooltip
512
+ canvasRef={canvasRef}
513
data={data}
514
hoveredEvent={hoveredEvent}
515
origin={mouseLocation}
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+22
-2
@@ -28,6 +28,7 @@ import useSmartTooltip from './utils/useSmartTooltip';
28
import styles from './EventTooltip.css';
29
30
type Props = {|
31
+ canvasRef: {|current: HTMLCanvasElement | null|},
32
data: ReactProfilerData,
33
hoveredEvent: ReactHoverContextInfo | null,
34
origin: Point,
@@ -102,8 +103,14 @@ function getReactMeasureLabel(type): string | null {
103
}
104
}
105
105
-export default function EventTooltip({data, hoveredEvent, origin}: Props) {
106
+export default function EventTooltip({
107
+ canvasRef,
108
+ data,
109
+ hoveredEvent,
110
+ origin,
111
+}: Props) {
112
const tooltipRef = useSmartTooltip({
113
+ canvasRef,
114
mouseX: origin.x,
115
mouseY: origin.y,
116
});
@@ -209,7 +216,19 @@ const TooltipNativeEvent = ({
216
nativeEvent: NativeEvent,
217
tooltipRef: Return<typeof useRef>,
218
}) => {
212
- const {duration, timestamp, type} = nativeEvent;
219
+ const {duration, timestamp, type, warnings} = nativeEvent;
220
+
221
+ const warningElements = [];
222
+ if (warnings !== null) {
223
+ warnings.forEach((warning, index) => {
224
+ warningElements.push(
225
+ <Fragment key={index}>
226
+ <div className={styles.DetailsGridLabel}>Warning:</div>
227
+ <div>{warning}</div>
228
+ </Fragment>,
229
+ );
230
+ });
231
+ }
232
233
return (
234
<div className={styles.Tooltip} ref={tooltipRef}>
@@ -221,6 +240,7 @@ const TooltipNativeEvent = ({
240
<div>{formatTimestamp(timestamp)}</div>
241
<div className={styles.DetailsGridLabel}>Duration:</div>
242
<div>{formatDuration(duration)}</div>
243
+ {warningElements}
244
</div>
245
</div>
246
);
packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js
+6
-5
@@ -128,7 +128,7 @@ export class NativeEventsView extends View {
128
showHoverHighlight: boolean,
129
) {
130
const {frame} = this;
131
- const {depth, duration, highlight, timestamp, type} = event;
131
+ const {depth, duration, timestamp, type, warnings} = event;
132
133
baseY += depth * ROW_WITH_BORDER_HEIGHT;
134
@@ -152,7 +152,7 @@ export class NativeEventsView extends View {
152
153
const drawableRect = intersectionOfRects(eventRect, rect);
154
context.beginPath();
155
- if (highlight) {
155
+ if (warnings !== null) {
156
context.fillStyle = showHoverHighlight
157
? COLORS.NATIVE_EVENT_WARNING_HOVER
158
: COLORS.NATIVE_EVENT_WARNING;
@@ -182,9 +182,10 @@ export class NativeEventsView extends View {
182
);
183
184
if (trimmedName !== null) {
185
- context.fillStyle = highlight
186
- ? COLORS.NATIVE_EVENT_WARNING_TEXT
187
- : COLORS.TEXT_COLOR;
185
+ context.fillStyle =
186
+ warnings !== null
187
+ ? COLORS.NATIVE_EVENT_WARNING_TEXT
188
+ : COLORS.TEXT_COLOR;
189
190
context.fillText(
191
trimmedName,
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+8
-3
@@ -202,9 +202,9 @@ function processTimelineEvent(
202
const nativeEvent = {
203
depth,
204
duration,
205
- highlight: false,
205
timestamp,
206
type,
207
+ warnings: null,
208
};
209
210
currentProfilerData.nativeEvents.push(nativeEvent);
@@ -339,8 +339,13 @@ function processTimelineEvent(
339
const nativeEvent = nativeEventStack[i];
340
const stopTime = nativeEvent.timestamp + nativeEvent.duration;
341
if (stopTime > startTime) {
342
- // Warn about sync updates that happen an event handler.
343
- nativeEvent.highlight = true;
342
+ const warning =
343
+ 'An event handler scheduled a synchronous update with React.';
344
+ if (nativeEvent.warnings === null) {
345
+ nativeEvent.warnings = new Set([warning]);
346
+ } else {
347
+ nativeEvent.warnings.add(warning);
348
+ }
349
}
350
}
351
} else if (
packages/react-devtools-scheduling-profiler/src/types.js
+1
-1
@@ -24,9 +24,9 @@ export type ReactLane = number;
24
export type NativeEvent = {|
25
+depth: number,
26
+duration: Milliseconds,
27
- highlight: boolean,
27
+timestamp: Milliseconds,
28
+type: string,
29
+ warnings: Set<string> | null,
30
|};
31
32
type BaseReactEvent = {|
packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js
+15
-5
@@ -12,22 +12,32 @@ import {useLayoutEffect, useRef} from 'react';
12
const TOOLTIP_OFFSET = 4;
13
14
export default function useSmartTooltip({
15
+ canvasRef,
16
mouseX,
17
mouseY,
18
}: {
19
+ canvasRef: {|current: HTMLCanvasElement | null|},
20
mouseX: number,
21
mouseY: number,
22
}) {
23
const ref = useRef<HTMLElement | null>(null);
24
25
+ // HACK: Browser extension reports window.innerHeight of 0,
26
+ // so we fallback to using the tooltip target element.
27
+ let height = window.innerHeight;
28
+ let width = window.innerWidth;
29
+ const target = canvasRef.current;
30
+ if (target !== null) {
31
+ const rect = target.getBoundingClientRect();
32
+ height = rect.top + rect.height;
33
+ width = rect.left + rect.width;
34
+ }
35
+
36
useLayoutEffect(() => {
37
const element = ref.current;
38
if (element !== null) {
39
// Let's check the vertical position.
27
- if (
28
- mouseY + TOOLTIP_OFFSET + element.offsetHeight >=
29
- window.innerHeight
30
- ) {
40
+ if (mouseY + TOOLTIP_OFFSET + element.offsetHeight >= height) {
41
// The tooltip doesn't fit below the mouse cursor (which is our
42
// default strategy). Therefore we try to position it either above the
43
// mouse cursor or finally aligned with the window's top edge.
@@ -45,7 +55,7 @@ export default function useSmartTooltip({
55
}
56
57
// Now let's check the horizontal position.
48
- if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= window.innerWidth) {
58
+ if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= width) {
59
// The tooltip doesn't fit at the right of the mouse cursor (which is
60
// our default strategy). Therefore we try to position it either at the
61
// left of the mouse cursor or finally aligned with the window's left