@samitouri / QOS-React-2 / commits / cdccdbe171

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