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