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

Scheduling Profiler: Add network measures (#22112)

Brian Vaughn committed Aug 18, 2021 at 12:17 UTC f51579fffeac46a783f765786f5287644b1fa7eb
14 files changed +872 -136
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+90 -16
@@ -44,6 +44,7 @@ import {
44 ComponentMeasuresView,
45 FlamechartView,
46 NativeEventsView,
47 + NetworkMeasuresView,
48 ReactMeasuresView,
49 SchedulingEventsView,
50 SnapshotsView,
@@ -128,6 +129,18 @@ const zoomToBatch = (
129 viewState.updateHorizontalScrollState(scrollState);
130 };
131
132 +const EMPTY_CONTEXT_INFO: ReactHoverContextInfo = {
133 + componentMeasure: null,
134 + flamechartStackFrame: null,
135 + measure: null,
136 + nativeEvent: null,
137 + networkMeasure: null,
138 + schedulingEvent: null,
139 + snapshot: null,
140 + suspenseEvent: null,
141 + userTimingMark: null,
142 +};
143 +
144 type AutoSizedCanvasProps = {|
145 data: ReactProfilerData,
146 height: number,
@@ -150,7 +163,12 @@ function AutoSizedCanvas({
163 setHoveredEvent,
164 ] = useState<ReactHoverContextInfo | null>(null);
165
153 - const surfaceRef = useRef(new Surface());
166 + const resetHoveredEvent = useCallback(
167 + () => setHoveredEvent(EMPTY_CONTEXT_INFO),
168 + [],
169 + );
170 +
171 + const surfaceRef = useRef(new Surface(resetHoveredEvent));
172 const userTimingMarksViewRef = useRef(null);
173 const nativeEventsViewRef = useRef(null);
174 const schedulingEventsViewRef = useRef(null);
@@ -158,6 +176,7 @@ function AutoSizedCanvas({
176 const componentMeasuresViewRef = useRef(null);
177 const reactMeasuresViewRef = useRef(null);
178 const flamechartViewRef = useRef(null);
179 + const networkMeasuresViewRef = useRef(null);
180 const snapshotsViewRef = useRef(null);
181
182 const {hideMenu: hideContextMenu} = useContext(RegistryContext);
@@ -318,6 +337,22 @@ function AutoSizedCanvas({
337 );
338 }
339
340 + let networkMeasuresViewWrapper = null;
341 + if (data.snapshots.length > 0) {
342 + const networkMeasuresView = new NetworkMeasuresView(
343 + surface,
344 + defaultFrame,
345 + data,
346 + );
347 + networkMeasuresViewRef.current = networkMeasuresView;
348 + networkMeasuresViewWrapper = createViewHelper(
349 + networkMeasuresView,
350 + 'network',
351 + true,
352 + true,
353 + );
354 + }
355 +
356 const flamechartView = new FlamechartView(
357 surface,
358 defaultFrame,
@@ -357,6 +392,9 @@ function AutoSizedCanvas({
392 if (snapshotsViewWrapper !== null) {
393 rootView.addSubview(snapshotsViewWrapper);
394 }
395 + if (networkMeasuresViewWrapper !== null) {
396 + rootView.addSubview(networkMeasuresViewWrapper);
397 + }
398 rootView.addSubview(flamechartViewWrapper);
399
400 const verticalScrollOverflowView = new VerticalScrollOverflowView(
@@ -395,16 +433,18 @@ function AutoSizedCanvas({
433 prevHoverEvent.flamechartStackFrame !== null ||
434 prevHoverEvent.measure !== null ||
435 prevHoverEvent.nativeEvent !== null ||
436 + prevHoverEvent.networkMeasure !== null ||
437 prevHoverEvent.schedulingEvent !== null ||
438 + prevHoverEvent.snapshot !== null ||
439 prevHoverEvent.suspenseEvent !== null ||
440 prevHoverEvent.userTimingMark !== null
441 ) {
442 return {
443 componentMeasure: null,
404 - data: prevHoverEvent.data,
444 flamechartStackFrame: null,
445 measure: null,
446 nativeEvent: null,
447 + networkMeasure: null,
448 schedulingEvent: null,
449 snapshot: null,
450 suspenseEvent: null,
@@ -460,10 +500,10 @@ function AutoSizedCanvas({
500 if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
501 setHoveredEvent({
502 componentMeasure: null,
463 - data,
503 flamechartStackFrame: null,
504 measure: null,
505 nativeEvent: null,
506 + networkMeasure: null,
507 schedulingEvent: null,
508 snapshot: null,
509 suspenseEvent: null,
@@ -479,10 +519,10 @@ function AutoSizedCanvas({
519 if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
520 setHoveredEvent({
521 componentMeasure: null,
482 - data,
522 flamechartStackFrame: null,
523 measure: null,
524 nativeEvent,
525 + networkMeasure: null,
526 schedulingEvent: null,
527 snapshot: null,
528 suspenseEvent: null,
@@ -498,10 +538,10 @@ function AutoSizedCanvas({
538 if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
539 setHoveredEvent({
540 componentMeasure: null,
501 - data,
541 flamechartStackFrame: null,
542 measure: null,
543 nativeEvent: null,
544 + networkMeasure: null,
545 schedulingEvent,
546 snapshot: null,
547 suspenseEvent: null,
@@ -517,10 +557,10 @@ function AutoSizedCanvas({
557 if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
558 setHoveredEvent({
559 componentMeasure: null,
520 - data,
560 flamechartStackFrame: null,
561 measure: null,
562 nativeEvent: null,
563 + networkMeasure: null,
564 schedulingEvent: null,
565 snapshot: null,
566 suspenseEvent,
@@ -536,10 +576,10 @@ function AutoSizedCanvas({
576 if (!hoveredEvent || hoveredEvent.measure !== measure) {
577 setHoveredEvent({
578 componentMeasure: null,
539 - data,
579 flamechartStackFrame: null,
580 measure,
581 nativeEvent: null,
582 + networkMeasure: null,
583 schedulingEvent: null,
584 snapshot: null,
585 suspenseEvent: null,
@@ -558,10 +598,10 @@ function AutoSizedCanvas({
598 ) {
599 setHoveredEvent({
600 componentMeasure,
561 - data,
601 flamechartStackFrame: null,
602 measure: null,
603 nativeEvent: null,
604 + networkMeasure: null,
605 schedulingEvent: null,
606 snapshot: null,
607 suspenseEvent: null,
@@ -577,10 +617,10 @@ function AutoSizedCanvas({
617 if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
618 setHoveredEvent({
619 componentMeasure: null,
580 - data,
620 flamechartStackFrame: null,
621 measure: null,
622 nativeEvent: null,
623 + networkMeasure: null,
624 schedulingEvent: null,
625 snapshot,
626 suspenseEvent: null,
@@ -599,10 +639,10 @@ function AutoSizedCanvas({
639 ) {
640 setHoveredEvent({
641 componentMeasure: null,
602 - data,
642 flamechartStackFrame,
643 measure: null,
644 nativeEvent: null,
645 + networkMeasure: null,
646 schedulingEvent: null,
647 snapshot: null,
648 suspenseEvent: null,
@@ -611,53 +651,79 @@ function AutoSizedCanvas({
651 }
652 });
653 }
654 +
655 + const {current: networkMeasuresView} = networkMeasuresViewRef;
656 + if (networkMeasuresView) {
657 + networkMeasuresView.onHover = networkMeasure => {
658 + if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) {
659 + setHoveredEvent({
660 + componentMeasure: null,
661 + flamechartStackFrame: null,
662 + measure: null,
663 + nativeEvent: null,
664 + networkMeasure,
665 + schedulingEvent: null,
666 + snapshot: null,
667 + suspenseEvent: null,
668 + userTimingMark: null,
669 + });
670 + }
671 + };
672 + }
673 }, [
674 hoveredEvent,
675 data, // Attach onHover callbacks when views are re-created on data change
676 ]);
677
678 useLayoutEffect(() => {
620 - const {current: userTimingMarksView} = userTimingMarksViewRef;
679 + const userTimingMarksView = userTimingMarksViewRef.current;
680 if (userTimingMarksView) {
681 userTimingMarksView.setHoveredMark(
682 hoveredEvent ? hoveredEvent.userTimingMark : null,
683 );
684 }
685
627 - const {current: nativeEventsView} = nativeEventsViewRef;
686 + const nativeEventsView = nativeEventsViewRef.current;
687 if (nativeEventsView) {
688 nativeEventsView.setHoveredEvent(
689 hoveredEvent ? hoveredEvent.nativeEvent : null,
690 );
691 }
692
634 - const {current: schedulingEventsView} = schedulingEventsViewRef;
693 + const schedulingEventsView = schedulingEventsViewRef.current;
694 if (schedulingEventsView) {
695 schedulingEventsView.setHoveredEvent(
696 hoveredEvent ? hoveredEvent.schedulingEvent : null,
697 );
698 }
699
641 - const {current: suspenseEventsView} = suspenseEventsViewRef;
700 + const suspenseEventsView = suspenseEventsViewRef.current;
701 if (suspenseEventsView) {
702 suspenseEventsView.setHoveredEvent(
703 hoveredEvent ? hoveredEvent.suspenseEvent : null,
704 );
705 }
706
648 - const {current: reactMeasuresView} = reactMeasuresViewRef;
707 + const reactMeasuresView = reactMeasuresViewRef.current;
708 if (reactMeasuresView) {
709 reactMeasuresView.setHoveredMeasure(
710 hoveredEvent ? hoveredEvent.measure : null,
711 );
712 }
713
655 - const {current: flamechartView} = flamechartViewRef;
714 + const flamechartView = flamechartViewRef.current;
715 if (flamechartView) {
716 flamechartView.setHoveredFlamechartStackFrame(
717 hoveredEvent ? hoveredEvent.flamechartStackFrame : null,
718 );
719 }
720 +
721 + const networkMeasuresView = networkMeasuresViewRef.current;
722 + if (networkMeasuresView) {
723 + networkMeasuresView.setHoveredEvent(
724 + hoveredEvent ? hoveredEvent.networkMeasure : null,
725 + );
726 + }
727 }, [hoveredEvent]);
728
729 // Draw to canvas in React's commit phase
@@ -677,6 +743,7 @@ function AutoSizedCanvas({
743 componentMeasure,
744 flamechartStackFrame,
745 measure,
746 + networkMeasure,
747 schedulingEvent,
748 suspenseEvent,
749 } = contextData.hoveredEvent;
@@ -689,6 +756,13 @@ function AutoSizedCanvas({
756 Copy component name
757 </ContextMenuItem>
758 )}
759 + {networkMeasure !== null && (
760 + <ContextMenuItem
761 + onClick={() => copy(networkMeasure.url)}
762 + title="Copy URL">
763 + Copy URL
764 + </ContextMenuItem>
765 + )}
766 {schedulingEvent !== null && (
767 <ContextMenuItem
768 onClick={() => copy(schedulingEvent.componentName)}
packages/react-devtools-scheduling-profiler/src/EventTooltip.css
+13 -2
@@ -3,10 +3,10 @@
3 }
4
5 .TooltipSection,
6 -.TooltipWarningSection {
6 +.TooltipWarningSection,
7 +.SingleLineTextSection {
8 display: block;
9 border-radius: 0.125rem;
9 - max-width: 300px;
10 padding: 0.25rem;
11 user-select: none;
12 pointer-events: none;
@@ -19,6 +19,13 @@
19 margin-top: 0.25rem;
20 background-color: var(--color-warning-background);
21 }
22 +.TooltipSection,
23 +.TooltipWarningSection {
24 + max-width: 300px;
25 +}
26 +.SingleLineTextSection {
27 + white-space: nowrap;
28 +}
29
30 .Divider {
31 height: 1px;
@@ -75,4 +82,8 @@
82
83 .Image {
84 border: 1px solid var(--color-border);
85 +}
86 +
87 +.DimText {
88 + color: var(--color-dim);
89 }
\ No newline at end of file
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+49
@@ -11,6 +11,7 @@ import type {Point} from './view-base';
11 import type {
12 FlamechartStackFrame,
13 NativeEvent,
14 + NetworkMeasure,
15 ReactComponentMeasure,
16 ReactHoverContextInfo,
17 ReactMeasure,
@@ -29,6 +30,8 @@ import {getBatchRange} from './utils/getBatchRange';
30 import useSmartTooltip from './utils/useSmartTooltip';
31 import styles from './EventTooltip.css';
32
33 +const MAX_TOOLTIP_TEXT_LENGTH = 60;
34 +
35 type Props = {|
36 canvasRef: {|current: HTMLCanvasElement | null|},
37 data: ReactProfilerData,
@@ -87,6 +90,7 @@ export default function EventTooltip({
90 flamechartStackFrame,
91 measure,
92 nativeEvent,
93 + networkMeasure,
94 schedulingEvent,
95 snapshot,
96 suspenseEvent,
@@ -104,6 +108,13 @@ export default function EventTooltip({
108 return (
109 <TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
110 );
111 + } else if (networkMeasure !== null) {
112 + return (
113 + <TooltipNetworkMeasure
114 + networkMeasure={networkMeasure}
115 + tooltipRef={tooltipRef}
116 + />
117 + );
118 } else if (schedulingEvent !== null) {
119 return (
120 <TooltipSchedulingEvent
@@ -238,6 +249,44 @@ const TooltipNativeEvent = ({
249 );
250 };
251
252 +const TooltipNetworkMeasure = ({
253 + networkMeasure,
254 + tooltipRef,
255 +}: {
256 + networkMeasure: NetworkMeasure,
257 + tooltipRef: Return<typeof useRef>,
258 +}) => {
259 + const {
260 + finishTimestamp,
261 + lastReceivedDataTimestamp,
262 + priority,
263 + sendRequestTimestamp,
264 + url,
265 + } = networkMeasure;
266 +
267 + let urlToDisplay = url;
268 + if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
269 + const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
270 + urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half);
271 + }
272 +
273 + const timestampBegin = sendRequestTimestamp;
274 + const timestampEnd = finishTimestamp || lastReceivedDataTimestamp;
275 + const duration =
276 + timestampEnd > 0
277 + ? formatDuration(finishTimestamp - timestampBegin)
278 + : '(incomplete)';
279 +
280 + return (
281 + <div className={styles.Tooltip} ref={tooltipRef}>
282 + <div className={styles.SingleLineTextSection}>
283 + {duration} <span className={styles.DimText}>{priority}</span>{' '}
284 + {urlToDisplay}
285 + </div>
286 + </div>
287 + );
288 +};
289 +
290 const TooltipSchedulingEvent = ({
291 data,
292 schedulingEvent,
packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js new
+337
@@ -0,0 +1,337 @@
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 {NetworkMeasure, ReactProfilerData} from '../types';
11 +import type {
12 + Interaction,
13 + IntrinsicSize,
14 + MouseMoveInteraction,
15 + Rect,
16 + ViewRefs,
17 +} from '../view-base';
18 +
19 +import {
20 + durationToWidth,
21 + positioningScaleFactor,
22 + positionToTimestamp,
23 + timestampToPosition,
24 +} from './utils/positioning';
25 +import {drawText} from './utils/text';
26 +import {formatDuration} from '../utils/formatting';
27 +import {
28 + View,
29 + Surface,
30 + rectContainsPoint,
31 + rectIntersectsRect,
32 + intersectionOfRects,
33 +} from '../view-base';
34 +import {BORDER_SIZE, COLORS, SUSPENSE_EVENT_HEIGHT} from './constants';
35 +
36 +const HEIGHT = SUSPENSE_EVENT_HEIGHT; // TODO Constant name
37 +const ROW_WITH_BORDER_HEIGHT = HEIGHT + BORDER_SIZE;
38 +
39 +const BASE_URL_REGEX = /([^:]+:\/\/[^\/]+)/;
40 +
41 +export class NetworkMeasuresView extends View {
42 + _depthToNetworkMeasure: Map<number, NetworkMeasure[]>;
43 + _hoveredNetworkMeasure: NetworkMeasure | null = null;
44 + _intrinsicSize: IntrinsicSize;
45 + _maxDepth: number = 0;
46 + _profilerData: ReactProfilerData;
47 +
48 + onHover: ((event: NetworkMeasure | null) => void) | null = null;
49 +
50 + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
51 + super(surface, frame);
52 +
53 + this._profilerData = profilerData;
54 +
55 + this._performPreflightComputations();
56 + }
57 +
58 + _performPreflightComputations() {
59 + this._depthToNetworkMeasure = new Map();
60 +
61 + const {duration, networkMeasures} = this._profilerData;
62 +
63 + networkMeasures.forEach(event => {
64 + const depth = event.depth;
65 +
66 + this._maxDepth = Math.max(this._maxDepth, depth);
67 +
68 + if (!this._depthToNetworkMeasure.has(depth)) {
69 + this._depthToNetworkMeasure.set(depth, [event]);
70 + } else {
71 + // $FlowFixMe This is unnecessary.
72 + this._depthToNetworkMeasure.get(depth).push(event);
73 + }
74 + });
75 +
76 + this._intrinsicSize = {
77 + width: duration,
78 + height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
79 + // Collapsed by default
80 + maxInitialHeight: 0,
81 + };
82 + }
83 +
84 + desiredSize() {
85 + return this._intrinsicSize;
86 + }
87 +
88 + setHoveredEvent(networkMeasure: NetworkMeasure | null) {
89 + if (this._hoveredNetworkMeasure === networkMeasure) {
90 + return;
91 + }
92 + this._hoveredNetworkMeasure = networkMeasure;
93 + this.setNeedsDisplay();
94 + }
95 +
96 + /**
97 + * Draw a single `NetworkMeasure` as a box/span with text inside of it.
98 + */
99 + _drawSingleNetworkMeasure(
100 + context: CanvasRenderingContext2D,
101 + networkMeasure: NetworkMeasure,
102 + baseY: number,
103 + scaleFactor: number,
104 + showHoverHighlight: boolean,
105 + ) {
106 + const {frame, visibleArea} = this;
107 + const {
108 + depth,
109 + finishTimestamp,
110 + firstReceivedDataTimestamp,
111 + lastReceivedDataTimestamp,
112 + receiveResponseTimestamp,
113 + sendRequestTimestamp,
114 + url,
115 + } = networkMeasure;
116 +
117 + // Account for requests that did not complete while we were profiling.
118 + // As well as requests that did not receive data before finish (cached?).
119 + const duration = this._profilerData.duration;
120 + const timestampBegin = sendRequestTimestamp;
121 + const timestampEnd =
122 + finishTimestamp || lastReceivedDataTimestamp || duration;
123 + const timestampMiddle =
124 + receiveResponseTimestamp || firstReceivedDataTimestamp || timestampEnd;
125 +
126 + // Convert all timestamps to x coordinates.
127 + const xStart = timestampToPosition(timestampBegin, scaleFactor, frame);
128 + const xMiddle = timestampToPosition(timestampMiddle, scaleFactor, frame);
129 + const xStop = timestampToPosition(timestampEnd, scaleFactor, frame);
130 +
131 + const width = durationToWidth(xStop - xStart, scaleFactor);
132 + if (width < 1) {
133 + return; // Too small to render at this zoom level
134 + }
135 +
136 + baseY += depth * ROW_WITH_BORDER_HEIGHT;
137 +
138 + const outerRect: Rect = {
139 + origin: {
140 + x: xStart,
141 + y: baseY,
142 + },
143 + size: {
144 + width: xStop - xStart,
145 + height: HEIGHT,
146 + },
147 + };
148 + if (!rectIntersectsRect(outerRect, visibleArea)) {
149 + return; // Not in view
150 + }
151 +
152 + // Draw the secondary rect first (since it also shows as a thin border around the primary rect).
153 + let rect = {
154 + origin: {
155 + x: xStart,
156 + y: baseY,
157 + },
158 + size: {
159 + width: xStop - xStart,
160 + height: HEIGHT,
161 + },
162 + };
163 + if (rectIntersectsRect(rect, visibleArea)) {
164 + context.beginPath();
165 + context.fillStyle =
166 + this._hoveredNetworkMeasure === networkMeasure
167 + ? COLORS.NETWORK_SECONDARY_HOVER
168 + : COLORS.NETWORK_SECONDARY;
169 + context.fillRect(
170 + rect.origin.x,
171 + rect.origin.y,
172 + rect.size.width,
173 + rect.size.height,
174 + );
175 + }
176 +
177 + rect = {
178 + origin: {
179 + x: xStart + BORDER_SIZE,
180 + y: baseY + BORDER_SIZE,
181 + },
182 + size: {
183 + width: xMiddle - xStart - BORDER_SIZE,
184 + height: HEIGHT - BORDER_SIZE * 2,
185 + },
186 + };
187 + if (rectIntersectsRect(rect, visibleArea)) {
188 + context.beginPath();
189 + context.fillStyle =
190 + this._hoveredNetworkMeasure === networkMeasure
191 + ? COLORS.NETWORK_PRIMARY_HOVER
192 + : COLORS.NETWORK_PRIMARY;
193 + context.fillRect(
194 + rect.origin.x,
195 + rect.origin.y,
196 + rect.size.width,
197 + rect.size.height,
198 + );
199 + }
200 +
201 + const baseUrl = url.match(BASE_URL_REGEX);
202 + const displayUrl = baseUrl !== null ? baseUrl[1] : url;
203 +
204 + const durationLabel =
205 + finishTimestamp !== 0
206 + ? `${formatDuration(finishTimestamp - sendRequestTimestamp)} - `
207 + : '';
208 +
209 + const label = durationLabel + displayUrl;
210 +
211 + drawText(label, context, outerRect, visibleArea);
212 + }
213 +
214 + draw(context: CanvasRenderingContext2D) {
215 + const {
216 + frame,
217 + _profilerData: {networkMeasures},
218 + _hoveredNetworkMeasure,
219 + visibleArea,
220 + } = this;
221 +
222 + context.fillStyle = COLORS.PRIORITY_BACKGROUND;
223 + context.fillRect(
224 + visibleArea.origin.x,
225 + visibleArea.origin.y,
226 + visibleArea.size.width,
227 + visibleArea.size.height,
228 + );
229 +
230 + const scaleFactor = positioningScaleFactor(
231 + this._intrinsicSize.width,
232 + frame,
233 + );
234 +
235 + networkMeasures.forEach(networkMeasure => {
236 + this._drawSingleNetworkMeasure(
237 + context,
238 + networkMeasure,
239 + frame.origin.y,
240 + scaleFactor,
241 + networkMeasure === _hoveredNetworkMeasure,
242 + );
243 + });
244 +
245 + // Render bottom borders.
246 + for (let i = 0; i <= this._maxDepth; i++) {
247 + const borderFrame: Rect = {
248 + origin: {
249 + x: frame.origin.x,
250 + y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
251 + },
252 + size: {
253 + width: frame.size.width,
254 + height: BORDER_SIZE,
255 + },
256 + };
257 + if (rectIntersectsRect(borderFrame, visibleArea)) {
258 + const borderDrawableRect = intersectionOfRects(
259 + borderFrame,
260 + visibleArea,
261 + );
262 + context.fillStyle = COLORS.PRIORITY_BORDER;
263 + context.fillRect(
264 + borderDrawableRect.origin.x,
265 + borderDrawableRect.origin.y,
266 + borderDrawableRect.size.width,
267 + borderDrawableRect.size.height,
268 + );
269 + }
270 + }
271 + }
272 +
273 + /**
274 + * @private
275 + */
276 + _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
277 + const {frame, _intrinsicSize, onHover, visibleArea} = this;
278 + if (!onHover) {
279 + return;
280 + }
281 +
282 + const {location} = interaction.payload;
283 + if (!rectContainsPoint(location, visibleArea)) {
284 + onHover(null);
285 + return;
286 + }
287 +
288 + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
289 + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
290 +
291 + const adjustedCanvasMouseY = location.y - frame.origin.y;
292 + const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
293 + const networkMeasuresAtDepth = this._depthToNetworkMeasure.get(depth);
294 +
295 + const duration = this._profilerData.duration;
296 +
297 + if (networkMeasuresAtDepth) {
298 + // Find the event being hovered over.
299 + for (let index = networkMeasuresAtDepth.length - 1; index >= 0; index--) {
300 + const networkMeasure = networkMeasuresAtDepth[index];
301 + const {
302 + finishTimestamp,
303 + lastReceivedDataTimestamp,
304 + sendRequestTimestamp,
305 + } = networkMeasure;
306 +
307 + const timestampBegin = sendRequestTimestamp;
308 + const timestampEnd =
309 + finishTimestamp || lastReceivedDataTimestamp || duration;
310 +
311 + if (
312 + hoverTimestamp >= timestampBegin &&
313 + hoverTimestamp <= timestampEnd
314 + ) {
315 + this.currentCursor = 'context-menu';
316 + viewRefs.hoveredView = this;
317 + onHover(networkMeasure);
318 + return;
319 + }
320 + }
321 + }
322 +
323 + if (viewRefs.hoveredView === this) {
324 + viewRefs.hoveredView = null;
325 + }
326 +
327 + onHover(null);
328 + }
329 +
330 + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
331 + switch (interaction.type) {
332 + case 'mousemove':
333 + this._handleMouseMove(interaction, viewRefs);
334 + break;
335 + }
336 + }
337 +}
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+18 -1
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +export const DPR: number = window.devicePixelRatio || 1;
11 export const LABEL_SIZE = 80;
12 export const MARKER_HEIGHT = 20;
13 export const MARKER_TICK_HEIGHT = 8;
@@ -20,7 +21,7 @@ export const PENDING_SUSPENSE_EVENT_SIZE = 8;
21 export const REACT_EVENT_DIAMETER = 6;
22 export const USER_TIMING_MARK_SIZE = 8;
23 export const REACT_MEASURE_HEIGHT = 14;
23 -export const BORDER_SIZE = 1;
24 +export const BORDER_SIZE = 1 / DPR;
25 export const FLAMECHART_FRAME_HEIGHT = 14;
26 export const TEXT_PADDING = 3;
27 export const SNAPSHOT_HEIGHT = 35;
@@ -46,6 +47,10 @@ export let COLORS = {
47 BACKGROUND: '',
48 NATIVE_EVENT: '',
49 NATIVE_EVENT_HOVER: '',
50 + NETWORK_PRIMARY: '',
51 + NETWORK_PRIMARY_HOVER: '',
52 + NETWORK_SECONDARY: '',
53 + NETWORK_SECONDARY_HOVER: '',
54 PRIORITY_BACKGROUND: '',
55 PRIORITY_BORDER: '',
56 PRIORITY_LABEL: '',
@@ -106,6 +111,18 @@ export function updateColorsToMatchTheme(element: Element): boolean {
111 NATIVE_EVENT_HOVER: computedStyle.getPropertyValue(
112 '--color-scheduling-profiler-native-event-hover',
113 ),
114 + NETWORK_PRIMARY: computedStyle.getPropertyValue(
115 + '--color-scheduling-profiler-network-primary',
116 + ),
117 + NETWORK_PRIMARY_HOVER: computedStyle.getPropertyValue(
118 + '--color-scheduling-profiler-network-primary-hover',
119 + ),
120 + NETWORK_SECONDARY: computedStyle.getPropertyValue(
121 + '--color-scheduling-profiler-network-secondary',
122 + ),
123 + NETWORK_SECONDARY_HOVER: computedStyle.getPropertyValue(
124 + '--color-scheduling-profiler-network-secondary-hover',
125 + ),
126 PRIORITY_BACKGROUND: computedStyle.getPropertyValue(
127 '--color-scheduling-profiler-priority-background',
128 ),
packages/react-devtools-scheduling-profiler/src/content-views/index.js
+1
@@ -10,6 +10,7 @@
10 export * from './ComponentMeasuresView';
11 export * from './FlamechartView';
12 export * from './NativeEventsView';
13 +export * from './NetworkMeasuresView';
14 export * from './ReactMeasuresView';
15 export * from './SchedulingEventsView';
16 export * from './SnapshotsView';
packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js
+24 -4
@@ -32,14 +32,34 @@ export function trimText(
32 text: string,
33 width: number,
34 ): string | null {
35 - for (let i = text.length - 1; i >= 0; i--) {
36 - const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
35 + const maxIndex = text.length - 1;
36 +
37 + let startIndex = 0;
38 + let stopIndex = maxIndex;
39 +
40 + let longestValidIndex = 0;
41 + let longestValidText = null;
42 +
43 + // Trimming long text could be really slow if we decrease only 1 character at a time.
44 + // Trimming with more of a binary search approach is faster in the worst cases.
45 + while (startIndex <= stopIndex) {
46 + const currentIndex = Math.floor((startIndex + stopIndex) / 2);
47 + const trimmedText =
48 + currentIndex === maxIndex ? text : text.substr(0, currentIndex) + '…';
49 +
50 if (getTextWidth(context, trimmedText) <= width) {
38 - return trimmedText;
51 + if (longestValidIndex < currentIndex) {
52 + longestValidIndex = currentIndex;
53 + longestValidText = trimmedText;
54 + }
55 +
56 + startIndex = currentIndex + 1;
57 + } else {
58 + stopIndex = currentIndex - 1;
59 }
60 }
61
42 - return null;
62 + return longestValidText;
63 }
64
65 type TextConfig = {|
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+4
@@ -324,6 +324,7 @@ describe('preprocessData', () => {
324 30 => Array [],
325 },
326 "nativeEvents": Array [],
327 + "networkMeasures": Array [],
328 "otherUserTimingMarks": Array [],
329 "reactVersion": "17.0.3",
330 "schedulingEvents": Array [],
@@ -530,6 +531,7 @@ describe('preprocessData', () => {
531 30 => Array [],
532 },
533 "nativeEvents": Array [],
534 + "networkMeasures": Array [],
535 "otherUserTimingMarks": Array [],
536 "reactVersion": "17.0.3",
537 "schedulingEvents": Array [
@@ -715,6 +717,7 @@ describe('preprocessData', () => {
717 30 => Array [],
718 },
719 "nativeEvents": Array [],
720 + "networkMeasures": Array [],
721 "otherUserTimingMarks": Array [
722 Object {
723 "name": "__v3",
@@ -1051,6 +1054,7 @@ describe('preprocessData', () => {
1054 30 => Array [],
1055 },
1056 "nativeEvents": Array [],
1057 + "networkMeasures": Array [],
1058 "otherUserTimingMarks": Array [
1059 Object {
1060 "name": "__v3",
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+239 -93
@@ -17,6 +17,7 @@ import type {
17 Flamechart,
18 Milliseconds,
19 NativeEvent,
20 + NetworkMeasure,
21 Phase,
22 ReactLane,
23 ReactComponentMeasure,
@@ -51,6 +52,7 @@ type ProcessorState = {|
52 potentialSuspenseEventsOutsideOfTransition: Array<
53 [SuspenseEvent, ReactLane[]],
54 >,
55 + requestIdToNetworkMeasureMap: Map<string, NetworkMeasure>,
56 uidCounter: BatchUID,
57 unresolvedSuspenseEvents: Map<string, SuspenseEvent>,
58 |};
@@ -215,6 +217,205 @@ function throwIfIncomplete(
217 }
218 }
219
220 +function processEventDispatch(
221 + event: TimelineEvent,
222 + timestamp: Milliseconds,
223 + profilerData: ReactProfilerData,
224 + state: ProcessorState,
225 +) {
226 + const data = event.args.data;
227 + const type = data.type;
228 +
229 + if (type.startsWith('react-')) {
230 + const stackTrace = data.stackTrace;
231 + if (stackTrace) {
232 + const topFrame = stackTrace[stackTrace.length - 1];
233 + if (topFrame.url.includes('/react-dom.')) {
234 + // Filter out fake React events dispatched by invokeGuardedCallbackDev.
235 + return;
236 + }
237 + }
238 + }
239 +
240 + // Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
241 + if (
242 + type === 'blur' ||
243 + type === 'click' ||
244 + type === 'input' ||
245 + type.startsWith('focus') ||
246 + type.startsWith('key') ||
247 + type.startsWith('mouse') ||
248 + type.startsWith('pointer')
249 + ) {
250 + const duration = event.dur / 1000;
251 +
252 + let depth = 0;
253 +
254 + while (state.nativeEventStack.length > 0) {
255 + const prevNativeEvent =
256 + state.nativeEventStack[state.nativeEventStack.length - 1];
257 + const prevStopTime = prevNativeEvent.timestamp + prevNativeEvent.duration;
258 +
259 + if (timestamp < prevStopTime) {
260 + depth = prevNativeEvent.depth + 1;
261 + break;
262 + } else {
263 + state.nativeEventStack.pop();
264 + }
265 + }
266 +
267 + const nativeEvent = {
268 + depth,
269 + duration,
270 + timestamp,
271 + type,
272 + warning: null,
273 + };
274 +
275 + profilerData.nativeEvents.push(nativeEvent);
276 +
277 + // Keep track of curent event in case future ones overlap.
278 + // We separate them into different vertical lanes in this case.
279 + state.nativeEventStack.push(nativeEvent);
280 + }
281 +}
282 +
283 +function processResourceFinish(
284 + event: TimelineEvent,
285 + timestamp: Milliseconds,
286 + profilerData: ReactProfilerData,
287 + state: ProcessorState,
288 +) {
289 + const requestId = event.args.data.requestId;
290 + const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
291 + if (networkMeasure != null) {
292 + networkMeasure.finishTimestamp = timestamp;
293 + if (networkMeasure.firstReceivedDataTimestamp === 0) {
294 + networkMeasure.firstReceivedDataTimestamp = timestamp;
295 + }
296 + if (networkMeasure.lastReceivedDataTimestamp === 0) {
297 + networkMeasure.lastReceivedDataTimestamp = timestamp;
298 + }
299 +
300 + // Clean up now that the resource is done.
301 + state.requestIdToNetworkMeasureMap.delete(event.args.data.requestId);
302 + }
303 +}
304 +
305 +function processResourceReceivedData(
306 + event: TimelineEvent,
307 + timestamp: Milliseconds,
308 + profilerData: ReactProfilerData,
309 + state: ProcessorState,
310 +) {
311 + const requestId = event.args.data.requestId;
312 + const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
313 + if (networkMeasure != null) {
314 + if (networkMeasure.firstReceivedDataTimestamp === 0) {
315 + networkMeasure.firstReceivedDataTimestamp = timestamp;
316 + }
317 + networkMeasure.lastReceivedDataTimestamp = timestamp;
318 + networkMeasure.finishTimestamp = timestamp;
319 + }
320 +}
321 +
322 +function processResourceReceiveResponse(
323 + event: TimelineEvent,
324 + timestamp: Milliseconds,
325 + profilerData: ReactProfilerData,
326 + state: ProcessorState,
327 +) {
328 + const requestId = event.args.data.requestId;
329 + const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
330 + if (networkMeasure != null) {
331 + networkMeasure.receiveResponseTimestamp = timestamp;
332 + }
333 +}
334 +
335 +function processScreenshot(
336 + event: TimelineEvent,
337 + timestamp: Milliseconds,
338 + profilerData: ReactProfilerData,
339 + state: ProcessorState,
340 +) {
341 + const encodedSnapshot = event.args.snapshot; // Base 64 encoded
342 +
343 + const snapshot = {
344 + height: 0,
345 + image: null,
346 + imageSource: `data:image/png;base64,${encodedSnapshot}`,
347 + timestamp,
348 + width: 0,
349 + };
350 +
351 + // Delay processing until we've extracted snapshot dimensions.
352 + let resolveFn = ((null: any): Function);
353 + state.asyncProcessingPromises.push(
354 + new Promise(resolve => {
355 + resolveFn = resolve;
356 + }),
357 + );
358 +
359 + // Parse the Base64 image data to determine native size.
360 + // This will be used later to scale for display within the thumbnail strip.
361 + fetch(snapshot.imageSource)
362 + .then(response => response.blob())
363 + .then(blob => {
364 + // $FlowFixMe createImageBitmap
365 + createImageBitmap(blob).then(bitmap => {
366 + snapshot.height = bitmap.height;
367 + snapshot.width = bitmap.width;
368 +
369 + resolveFn();
370 + });
371 + });
372 +
373 + profilerData.snapshots.push(snapshot);
374 +}
375 +
376 +function processResourceSendRequest(
377 + event: TimelineEvent,
378 + timestamp: Milliseconds,
379 + profilerData: ReactProfilerData,
380 + state: ProcessorState,
381 +) {
382 + const data = event.args.data;
383 + const requestId = data.requestId;
384 +
385 + const availableDepths = new Array(
386 + state.requestIdToNetworkMeasureMap.size + 1,
387 + ).fill(true);
388 + state.requestIdToNetworkMeasureMap.forEach(({depth}) => {
389 + availableDepths[depth] = false;
390 + });
391 +
392 + let depth = 0;
393 + for (let i = 0; i < availableDepths.length; i++) {
394 + if (availableDepths[i]) {
395 + depth = i;
396 + break;
397 + }
398 + }
399 +
400 + const networkMeasure: NetworkMeasure = {
401 + depth,
402 + finishTimestamp: 0,
403 + firstReceivedDataTimestamp: 0,
404 + lastReceivedDataTimestamp: 0,
405 + requestId,
406 + requestMethod: data.requestMethod,
407 + priority: data.priority,
408 + sendRequestTimestamp: timestamp,
409 + receiveResponseTimestamp: 0,
410 + url: data.url,
411 + };
412 +
413 + state.requestIdToNetworkMeasureMap.set(requestId, networkMeasure);
414 +
415 + profilerData.networkMeasures.push(networkMeasure);
416 + networkMeasure.sendRequestTimestamp = timestamp;
417 +}
418 +
419 function processTimelineEvent(
420 event: TimelineEvent,
421 /** Finalized profiler data up to `event`. May be mutated. */
@@ -222,106 +423,49 @@ function processTimelineEvent(
423 /** Intermediate processor state. May be mutated. */
424 state: ProcessorState,
425 ) {
225 - const {args, cat, name, ts, ph} = event;
226 - switch (cat) {
227 - case 'disabled-by-default-devtools.screenshot':
228 - const encodedSnapshot = args.snapshot; // Base 64 encoded
229 -
230 - const snapshot = {
231 - height: 0,
232 - image: null,
233 - imageSource: `data:image/png;base64,${encodedSnapshot}`,
234 - timestamp: (ts - currentProfilerData.startTime) / 1000,
235 - width: 0,
236 - };
237 -
238 - // Delay processing until we've extracted snapshot dimensions.
239 - let resolveFn = ((null: any): Function);
240 - state.asyncProcessingPromises.push(
241 - new Promise(resolve => {
242 - resolveFn = resolve;
243 - }),
244 - );
426 + const {cat, name, ts, ph} = event;
427
246 - // Parse the Base64 image data to determine native size.
247 - // This will be used later to scale for display within the thumbnail strip.
248 - fetch(snapshot.imageSource)
249 - .then(response => response.blob())
250 - .then(blob => {
251 - // $FlowFixMe createImageBitmap
252 - createImageBitmap(blob).then(bitmap => {
253 - snapshot.height = bitmap.height;
254 - snapshot.width = bitmap.width;
255 -
256 - resolveFn();
257 - });
258 - });
428 + const startTime = (ts - currentProfilerData.startTime) / 1000;
429
260 - currentProfilerData.snapshots.push(snapshot);
430 + switch (cat) {
431 + case 'disabled-by-default-devtools.screenshot':
432 + processScreenshot(event, startTime, currentProfilerData, state);
433 break;
434 case 'devtools.timeline':
263 - if (name === 'EventDispatch') {
264 - const type = args.data.type;
265 -
266 - if (type.startsWith('react-')) {
267 - const stackTrace = args.data.stackTrace;
268 - if (stackTrace) {
269 - const topFrame = stackTrace[stackTrace.length - 1];
270 - if (topFrame.url.includes('/react-dom.')) {
271 - // Filter out fake React events dispatched by invokeGuardedCallbackDev.
272 - return;
273 - }
274 - }
275 - }
276 -
277 - // Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
278 - if (
279 - type === 'blur' ||
280 - type === 'click' ||
281 - type === 'input' ||
282 - type.startsWith('focus') ||
283 - type.startsWith('key') ||
284 - type.startsWith('mouse') ||
285 - type.startsWith('pointer')
286 - ) {
287 - const timestamp = (ts - currentProfilerData.startTime) / 1000;
288 - const duration = event.dur / 1000;
289 -
290 - let depth = 0;
291 -
292 - while (state.nativeEventStack.length > 0) {
293 - const prevNativeEvent =
294 - state.nativeEventStack[state.nativeEventStack.length - 1];
295 - const prevStopTime =
296 - prevNativeEvent.timestamp + prevNativeEvent.duration;
297 -
298 - if (timestamp < prevStopTime) {
299 - depth = prevNativeEvent.depth + 1;
300 - break;
301 - } else {
302 - state.nativeEventStack.pop();
303 - }
304 - }
305 -
306 - const nativeEvent = {
307 - depth,
308 - duration,
309 - timestamp,
310 - type,
311 - warning: null,
312 - };
313 -
314 - currentProfilerData.nativeEvents.push(nativeEvent);
315 -
316 - // Keep track of curent event in case future ones overlap.
317 - // We separate them into different vertical lanes in this case.
318 - state.nativeEventStack.push(nativeEvent);
319 - }
435 + switch (name) {
436 + case 'EventDispatch':
437 + processEventDispatch(event, startTime, currentProfilerData, state);
438 + break;
439 + case 'ResourceFinish':
440 + processResourceFinish(event, startTime, currentProfilerData, state);
441 + break;
442 + case 'ResourceReceivedData':
443 + processResourceReceivedData(
444 + event,
445 + startTime,
446 + currentProfilerData,
447 + state,
448 + );
449 + break;
450 + case 'ResourceReceiveResponse':
451 + processResourceReceiveResponse(
452 + event,
453 + startTime,
454 + currentProfilerData,
455 + state,
456 + );
457 + break;
458 + case 'ResourceSendRequest':
459 + processResourceSendRequest(
460 + event,
461 + startTime,
462 + currentProfilerData,
463 + state,
464 + );
465 + break;
466 }
467 break;
468 case 'blink.user_timing':
323 - const startTime = (ts - currentProfilerData.startTime) / 1000;
324 -
469 if (name.startsWith('--react-version-')) {
470 const [reactVersion] = name.substr(16).split('-');
471 currentProfilerData.reactVersion = reactVersion;
@@ -714,6 +858,7 @@ export default async function preprocessData(
858 laneToLabelMap: new Map(),
859 laneToReactMeasureMap,
860 nativeEvents: [],
861 + networkMeasures: [],
862 otherUserTimingMarks: [],
863 reactVersion: null,
864 schedulingEvents: [],
@@ -759,6 +904,7 @@ export default async function preprocessData(
904 potentialLongNestedUpdate: null,
905 potentialLongNestedUpdates: [],
906 potentialSuspenseEventsOutsideOfTransition: [],
907 + requestIdToNetworkMeasureMap: new Map(),
908 uidCounter: 0,
909 unresolvedSuspenseEvents: new Map(),
910 };
packages/react-devtools-scheduling-profiler/src/types.js
+15 -1
@@ -91,6 +91,19 @@ export type ReactMeasure = {|
91 +depth: number,
92 |};
93
94 +export type NetworkMeasure = {|
95 + +depth: number,
96 + finishTimestamp: Milliseconds,
97 + firstReceivedDataTimestamp: Milliseconds,
98 + lastReceivedDataTimestamp: Milliseconds,
99 + priority: string,
100 + receiveResponseTimestamp: Milliseconds,
101 + +requestId: string,
102 + requestMethod: string,
103 + sendRequestTimestamp: Milliseconds,
104 + url: string,
105 +|};
106 +
107 export type ReactComponentMeasure = {|
108 +componentName: string,
109 duration: Milliseconds,
@@ -155,6 +168,7 @@ export type ReactProfilerData = {|
168 laneToLabelMap: Map<ReactLane, string>,
169 laneToReactMeasureMap: Map<ReactLane, ReactMeasure[]>,
170 nativeEvents: NativeEvent[],
171 + networkMeasures: NetworkMeasure[],
172 otherUserTimingMarks: UserTimingMark[],
173 reactVersion: string | null,
174 schedulingEvents: SchedulingEvent[],
@@ -165,10 +179,10 @@ export type ReactProfilerData = {|
179
180 export type ReactHoverContextInfo = {|
181 componentMeasure: ReactComponentMeasure | null,
168 - data: $ReadOnly<ReactProfilerData> | null,
182 flamechartStackFrame: FlamechartStackFrame | null,
183 measure: ReactMeasure | null,
184 nativeEvent: NativeEvent | null,
185 + networkMeasure: NetworkMeasure | null,
186 schedulingEvent: SchedulingEvent | null,
187 suspenseEvent: SuspenseEvent | null,
188 snapshot: Snapshot | null,
packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
+9 -2
@@ -156,7 +156,7 @@ export class HorizontalPanAndZoomView extends View {
156 interaction.payload.location,
157 this.frame,
158 );
159 - if (isHovered) {
159 + if (isHovered && viewRefs.hoveredView === null) {
160 viewRefs.hoveredView = this;
161 }
162
@@ -169,9 +169,16 @@ export class HorizontalPanAndZoomView extends View {
169 if (!this._isPanning) {
170 return;
171 }
172 +
173 + // Don't prevent mouse-move events from bubbling if they are vertical drags.
174 + const {movementX, movementY} = interaction.payload.event;
175 + if (Math.abs(movementX) < Math.abs(movementY)) {
176 + return;
177 + }
178 +
179 const newState = translateState({
180 state: this._viewState.horizontalScrollState,
174 - delta: interaction.payload.event.movementX,
181 + delta: movementX,
182 containerLength: this.frame.size.width,
183 });
184 this._viewState.updateHorizontalScrollState(newState);
packages/react-devtools-scheduling-profiler/src/view-base/Surface.js
+43 -12
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {ReactHoverContextInfo} from '../types';
11 import type {Interaction} from './useCanvasInteraction';
12 import type {Size} from './geometry';
13
@@ -14,6 +15,7 @@ import memoize from 'memoize-one';
15
16 import {View} from './View';
17 import {zeroPoint} from './geometry';
18 +import {DPR} from '../content-views/constants';
19
20 export type ViewRefs = {|
21 activeView: View | null,
@@ -22,12 +24,10 @@ export type ViewRefs = {|
24
25 // hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
26 function configureRetinaCanvas(canvas, height, width) {
25 - const dpr: number = window.devicePixelRatio || 1;
26 - canvas.width = width * dpr;
27 - canvas.height = height * dpr;
27 + canvas.width = width * DPR;
28 + canvas.height = height * DPR;
29 canvas.style.width = `${width}px`;
30 canvas.style.height = `${height}px`;
30 - return dpr;
31 }
32
33 const getCanvasContext = memoize(
@@ -39,14 +39,19 @@ const getCanvasContext = memoize(
39 ): CanvasRenderingContext2D => {
40 const context = canvas.getContext('2d', {alpha: false});
41 if (scaleCanvas) {
42 - const dpr = configureRetinaCanvas(canvas, height, width);
42 + configureRetinaCanvas(canvas, height, width);
43 +
44 // Scale all drawing operations by the dpr, so you don't have to worry about the difference.
44 - context.scale(dpr, dpr);
45 + context.scale(DPR, DPR);
46 }
47 return context;
48 },
49 );
50
51 +type ResetHoveredEventFn = (
52 + partialState: $Shape<ReactHoverContextInfo>,
53 +) => void;
54 +
55 /**
56 * Represents the canvas surface and a view heirarchy. A surface is also the
57 * place where all interactions enter the view heirarchy.
@@ -57,11 +62,17 @@ export class Surface {
62 _context: ?CanvasRenderingContext2D;
63 _canvasSize: ?Size;
64
65 + _resetHoveredEvent: ResetHoveredEventFn;
66 +
67 _viewRefs: ViewRefs = {
68 activeView: null,
69 hoveredView: null,
70 };
71
72 + constructor(resetHoveredEvent: ResetHoveredEventFn) {
73 + this._resetHoveredEvent = resetHoveredEvent;
74 + }
75 +
76 hasActiveView(): boolean {
77 return this._viewRefs.activeView !== null;
78 }
@@ -107,12 +118,32 @@ export class Surface {
118 }
119
120 handleInteraction(interaction: Interaction) {
110 - if (!this.rootView) {
111 - return;
121 + const rootView = this.rootView;
122 + if (rootView != null) {
123 + const viewRefs = this._viewRefs;
124 + switch (interaction.type) {
125 + case 'mousemove':
126 + // Clean out the hovered view before processing a new mouse move interaction.
127 + const hoveredView = viewRefs.hoveredView;
128 + viewRefs.hoveredView = null;
129 +
130 + rootView.handleInteractionAndPropagateToSubviews(
131 + interaction,
132 + viewRefs,
133 + );
134 +
135 + // If a previously hovered view is no longer hovered, update the outer state.
136 + if (hoveredView !== null && viewRefs.hoveredView === null) {
137 + this._resetHoveredEvent({});
138 + }
139 + break;
140 + default:
141 + rootView.handleInteractionAndPropagateToSubviews(
142 + interaction,
143 + viewRefs,
144 + );
145 + break;
146 + }
147 }
113 - this.rootView.handleInteractionAndPropagateToSubviews(
114 - interaction,
115 - this._viewRefs,
116 - );
148 }
149 }
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
+22 -5
@@ -199,12 +199,20 @@ export class VerticalScrollView extends View {
199 if (!this._isPanning) {
200 return;
201 }
202 +
203 + // Don't prevent mouse-move events from bubbling if they are horizontal drags.
204 + const {movementX, movementY} = interaction.payload.event;
205 + if (Math.abs(movementX) > Math.abs(movementY)) {
206 + return;
207 + }
208 +
209 const newState = translateState({
210 state: this._scrollState,
211 delta: interaction.payload.event.movementY,
212 containerLength: this.frame.size.height,
213 });
214 this._setScrollState(newState);
215 +
216 return true;
217 }
218
@@ -255,12 +263,14 @@ export class VerticalScrollView extends View {
263 }
264
265 _setScrollState(proposedState: ScrollState): boolean {
258 - const height = this._contentView.frame.size.height;
266 + const contentHeight = this._contentView.frame.size.height;
267 + const containerHeight = this.frame.size.height;
268 +
269 const clampedState = clampState({
270 state: proposedState,
261 - minContentLength: height,
262 - maxContentLength: height,
263 - containerLength: this.frame.size.height,
271 + minContentLength: contentHeight,
272 + maxContentLength: contentHeight,
273 + containerLength: containerHeight,
274 });
275 if (!areScrollStatesEqual(clampedState, this._scrollState)) {
276 this._scrollState.offset = clampedState.offset;
@@ -275,6 +285,13 @@ export class VerticalScrollView extends View {
285 return true;
286 }
287
278 - return false;
288 + // Don't allow wheel events to bubble past this view even if we've scrolled to the edge.
289 + // It just feels bad to have the scrolling jump unexpectedly from in a container to the outer page.
290 + // The only exception is when the container fitst the contnet (no scrolling).
291 + if (contentHeight === containerHeight) {
292 + return false;
293 + }
294 +
295 + return true;
296 }
297 }
packages/react-devtools-shared/src/constants.js
+8
@@ -141,6 +141,10 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
141 '--color-resize-bar-dot': '#333333',
142 '--color-scheduling-profiler-native-event': '#ccc',
143 '--color-scheduling-profiler-native-event-hover': '#aaa',
144 + '--color-scheduling-profiler-network-primary': '#fcf3dc',
145 + '--color-scheduling-profiler-network-primary-hover': '#f0e7d1',
146 + '--color-scheduling-profiler-network-secondary': '#efc457',
147 + '--color-scheduling-profiler-network-secondary-hover': '#e3ba52',
148 '--color-scheduling-profiler-priority-background': '#f6f6f6',
149 '--color-scheduling-profiler-priority-border': '#eeeeee',
150 '--color-scheduling-profiler-user-timing': '#c9cacd',
@@ -276,6 +280,10 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
280 '--color-resize-bar-dot': '#cfd1d5',
281 '--color-scheduling-profiler-native-event': '#b2b2b2',
282 '--color-scheduling-profiler-native-event-hover': '#949494',
283 + '--color-scheduling-profiler-network-primary': '#fcf3dc',
284 + '--color-scheduling-profiler-network-primary-hover': '#e3dbc5',
285 + '--color-scheduling-profiler-network-secondary': '#efc457',
286 + '--color-scheduling-profiler-network-secondary-hover': '#d6af4d',
287 '--color-scheduling-profiler-priority-background': '#1d2129',
288 '--color-scheduling-profiler-priority-border': '#282c34',
289 '--color-scheduling-profiler-user-timing': '#c9cacd',