Scheduling profiler UX changes (#21990)
Brian Vaughn committed
Aug 2, 2021 at 09:23 UTC
27bf6f9a832e4ccc20cfb5a81850ae8d4cc2f92b
40 files changed
+2005
-1255
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+186
-93
@@ -31,8 +31,9 @@ import {copy} from 'clipboard-js';
31
import prettyMilliseconds from 'pretty-ms';
32
33
import {
34
+ BackgroundColorView,
35
HorizontalPanAndZoomView,
35
- ResizableSplitView,
36
+ ResizableView,
37
Surface,
38
VerticalScrollView,
39
View,
@@ -45,8 +46,9 @@ import {
46
import {
47
FlamechartView,
48
NativeEventsView,
48
- ReactEventsView,
49
ReactMeasuresView,
50
+ SchedulingEventsView,
51
+ SuspenseEventsView,
52
TimeAxisMarkersView,
53
UserTimingMarksView,
54
} from './content-views';
@@ -128,7 +130,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
130
const surfaceRef = useRef(new Surface());
131
const userTimingMarksViewRef = useRef(null);
132
const nativeEventsViewRef = useRef(null);
131
- const reactEventsViewRef = useRef(null);
133
+ const schedulingEventsViewRef = useRef(null);
134
+ const suspenseEventsViewRef = useRef(null);
135
const reactMeasuresViewRef = useRef(null);
136
const flamechartViewRef = useRef(null);
137
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
@@ -152,21 +155,53 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
155
);
156
};
157
155
- // Top content
158
+ function createViewHelper(
159
+ view: View,
160
+ resizeLabel: string = '',
161
+ shouldScrollVertically: boolean = false,
162
+ shouldResizeVertically: boolean = false,
163
+ ): View {
164
+ let verticalScrollView = null;
165
+ if (shouldScrollVertically) {
166
+ verticalScrollView = new VerticalScrollView(
167
+ surface,
168
+ defaultFrame,
169
+ view,
170
+ );
171
+ }
172
+
173
+ const horizontalPanAndZoomView = new HorizontalPanAndZoomView(
174
+ surface,
175
+ defaultFrame,
176
+ verticalScrollView !== null ? verticalScrollView : view,
177
+ data.duration,
178
+ syncAllHorizontalPanAndZoomViewStates,
179
+ );
180
157
- const topContentStack = new View(
158
- surface,
159
- defaultFrame,
160
- verticallyStackedLayout,
161
- );
181
+ syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
182
+
183
+ let viewToReturn = horizontalPanAndZoomView;
184
+ if (shouldResizeVertically) {
185
+ viewToReturn = new ResizableView(
186
+ surface,
187
+ defaultFrame,
188
+ horizontalPanAndZoomView,
189
+ canvasRef,
190
+ resizeLabel,
191
+ );
192
+ }
193
+
194
+ return viewToReturn;
195
+ }
196
197
const axisMarkersView = new TimeAxisMarkersView(
198
surface,
199
defaultFrame,
200
data.duration,
201
);
168
- topContentStack.addSubview(axisMarkersView);
202
+ const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
203
204
+ let userTimingMarksViewWrapper = null;
205
if (data.otherUserTimingMarks.length > 0) {
206
const userTimingMarksView = new UserTimingMarksView(
207
surface,
@@ -175,50 +210,50 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
210
data.duration,
211
);
212
userTimingMarksViewRef.current = userTimingMarksView;
178
- topContentStack.addSubview(userTimingMarksView);
213
+ userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
214
}
215
216
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
217
nativeEventsViewRef.current = nativeEventsView;
183
- topContentStack.addSubview(nativeEventsView);
218
+ const nativeEventsViewWrapper = createViewHelper(
219
+ nativeEventsView,
220
+ 'events',
221
+ true,
222
+ true,
223
+ );
224
185
- const reactEventsView = new ReactEventsView(surface, defaultFrame, data);
186
- reactEventsViewRef.current = reactEventsView;
187
- topContentStack.addSubview(reactEventsView);
225
+ const schedulingEventsView = new SchedulingEventsView(
226
+ surface,
227
+ defaultFrame,
228
+ data,
229
+ );
230
+ schedulingEventsViewRef.current = schedulingEventsView;
231
+ const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
232
189
- const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
233
+ const suspenseEventsView = new SuspenseEventsView(
234
surface,
235
defaultFrame,
192
- topContentStack,
193
- data.duration,
194
- syncAllHorizontalPanAndZoomViewStates,
236
+ data,
237
);
196
- syncedHorizontalPanAndZoomViewsRef.current.push(
197
- topContentHorizontalPanAndZoomView,
238
+ suspenseEventsViewRef.current = suspenseEventsView;
239
+ const suspenseEventsViewWrapper = createViewHelper(
240
+ suspenseEventsView,
241
+ 'suspense',
242
+ true,
243
+ true,
244
);
245
200
- // Resizable content
201
-
246
const reactMeasuresView = new ReactMeasuresView(
247
surface,
248
defaultFrame,
249
data,
250
);
251
reactMeasuresViewRef.current = reactMeasuresView;
208
- const reactMeasuresVerticalScrollView = new VerticalScrollView(
209
- surface,
210
- defaultFrame,
252
+ const reactMeasuresViewWrapper = createViewHelper(
253
reactMeasuresView,
212
- );
213
- const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
214
- surface,
215
- defaultFrame,
216
- reactMeasuresVerticalScrollView,
217
- data.duration,
218
- syncAllHorizontalPanAndZoomViewStates,
219
- );
220
- syncedHorizontalPanAndZoomViewsRef.current.push(
221
- reactMeasuresHorizontalPanAndZoomView,
254
+ 'react',
255
+ true,
256
+ true,
257
);
258
259
const flamechartView = new FlamechartView(
@@ -228,30 +263,15 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
263
data.duration,
264
);
265
flamechartViewRef.current = flamechartView;
231
- const flamechartVerticalScrollView = new VerticalScrollView(
232
- surface,
233
- defaultFrame,
266
+ const flamechartViewWrapper = createViewHelper(
267
flamechartView,
235
- );
236
- const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
237
- surface,
238
- defaultFrame,
239
- flamechartVerticalScrollView,
240
- data.duration,
241
- syncAllHorizontalPanAndZoomViewStates,
242
- );
243
- syncedHorizontalPanAndZoomViewsRef.current.push(
244
- flamechartHorizontalPanAndZoomView,
245
- );
246
-
247
- const resizableContentStack = new ResizableSplitView(
248
- surface,
249
- defaultFrame,
250
- reactMeasuresHorizontalPanAndZoomView,
251
- flamechartHorizontalPanAndZoomView,
252
- canvasRef,
268
+ 'flamechart',
269
+ true,
270
+ true,
271
);
272
273
+ // Root view contains all of the sub views defined above.
274
+ // The order we add them below determines their vertical position.
275
const rootView = new View(
276
surface,
277
defaultFrame,
@@ -260,8 +280,18 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
280
lastViewTakesUpRemainingSpaceLayout,
281
),
282
);
263
- rootView.addSubview(topContentHorizontalPanAndZoomView);
264
- rootView.addSubview(resizableContentStack);
283
+ rootView.addSubview(axisMarkersViewWrapper);
284
+ if (userTimingMarksViewWrapper !== null) {
285
+ rootView.addSubview(userTimingMarksViewWrapper);
286
+ }
287
+ rootView.addSubview(nativeEventsViewWrapper);
288
+ rootView.addSubview(schedulingEventsViewWrapper);
289
+ rootView.addSubview(suspenseEventsViewWrapper);
290
+ rootView.addSubview(reactMeasuresViewWrapper);
291
+ rootView.addSubview(flamechartViewWrapper);
292
+
293
+ // If subviews are less than the available height, fill remaining height with a solid color.
294
+ rootView.addSubview(new BackgroundColorView(surface, defaultFrame));
295
296
surfaceRef.current.rootView = rootView;
297
}, [data]);
@@ -278,6 +308,39 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
308
return;
309
}
310
311
+ // Wheel events should always hide the current toolltip.
312
+ switch (interaction.type) {
313
+ case 'wheel-control':
314
+ case 'wheel-meta':
315
+ case 'wheel-plain':
316
+ case 'wheel-shift':
317
+ setHoveredEvent(prevHoverEvent => {
318
+ if (prevHoverEvent === null) {
319
+ return prevHoverEvent;
320
+ } else if (
321
+ prevHoverEvent.flamechartStackFrame !== null ||
322
+ prevHoverEvent.measure !== null ||
323
+ prevHoverEvent.nativeEvent !== null ||
324
+ prevHoverEvent.schedulingEvent !== null ||
325
+ prevHoverEvent.suspenseEvent !== null ||
326
+ prevHoverEvent.userTimingMark !== null
327
+ ) {
328
+ return {
329
+ data: prevHoverEvent.data,
330
+ flamechartStackFrame: null,
331
+ measure: null,
332
+ nativeEvent: null,
333
+ schedulingEvent: null,
334
+ suspenseEvent: null,
335
+ userTimingMark: null,
336
+ };
337
+ } else {
338
+ return prevHoverEvent;
339
+ }
340
+ });
341
+ break;
342
+ }
343
+
344
const surface = surfaceRef.current;
345
surface.handleInteraction(interaction);
346
@@ -310,12 +373,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
373
userTimingMarksView.onHover = userTimingMark => {
374
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
375
setHoveredEvent({
313
- userTimingMark,
314
- nativeEvent: null,
315
- reactEvent: null,
376
+ data,
377
flamechartStackFrame: null,
378
measure: null,
318
- data,
379
+ nativeEvent: null,
380
+ schedulingEvent: null,
381
+ suspenseEvent: null,
382
+ userTimingMark,
383
});
384
}
385
};
@@ -326,28 +390,47 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
390
nativeEventsView.onHover = nativeEvent => {
391
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
392
setHoveredEvent({
329
- userTimingMark: null,
330
- nativeEvent,
331
- reactEvent: null,
393
+ data,
394
flamechartStackFrame: null,
395
measure: null,
334
- data,
396
+ nativeEvent,
397
+ schedulingEvent: null,
398
+ suspenseEvent: null,
399
+ userTimingMark: null,
400
});
401
}
402
};
403
}
404
340
- const {current: reactEventsView} = reactEventsViewRef;
341
- if (reactEventsView) {
342
- reactEventsView.onHover = reactEvent => {
343
- if (!hoveredEvent || hoveredEvent.reactEvent !== reactEvent) {
405
+ const {current: schedulingEventsView} = schedulingEventsViewRef;
406
+ if (schedulingEventsView) {
407
+ schedulingEventsView.onHover = schedulingEvent => {
408
+ if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
409
setHoveredEvent({
345
- userTimingMark: null,
346
- nativeEvent: null,
347
- reactEvent,
410
+ data,
411
flamechartStackFrame: null,
412
measure: null,
413
+ nativeEvent: null,
414
+ schedulingEvent,
415
+ suspenseEvent: null,
416
+ userTimingMark: null,
417
+ });
418
+ }
419
+ };
420
+ }
421
+
422
+ const {current: suspenseEventsView} = suspenseEventsViewRef;
423
+ if (suspenseEventsView) {
424
+ suspenseEventsView.onHover = suspenseEvent => {
425
+ if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
426
+ setHoveredEvent({
427
data,
428
+ flamechartStackFrame: null,
429
+ measure: null,
430
+ nativeEvent: null,
431
+ schedulingEvent: null,
432
+ suspenseEvent,
433
+ userTimingMark: null,
434
});
435
}
436
};
@@ -358,12 +441,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
441
reactMeasuresView.onHover = measure => {
442
if (!hoveredEvent || hoveredEvent.measure !== measure) {
443
setHoveredEvent({
361
- userTimingMark: null,
362
- nativeEvent: null,
363
- reactEvent: null,
444
+ data,
445
flamechartStackFrame: null,
446
measure,
366
- data,
447
+ nativeEvent: null,
448
+ schedulingEvent: null,
449
+ suspenseEvent: null,
450
+ userTimingMark: null,
451
});
452
}
453
};
@@ -377,12 +461,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
461
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
462
) {
463
setHoveredEvent({
380
- userTimingMark: null,
381
- nativeEvent: null,
382
- reactEvent: null,
464
+ data,
465
flamechartStackFrame,
466
measure: null,
385
- data,
467
+ nativeEvent: null,
468
+ schedulingEvent: null,
469
+ suspenseEvent: null,
470
+ userTimingMark: null,
471
});
472
}
473
});
@@ -407,10 +492,17 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
492
);
493
}
494
410
- const {current: reactEventsView} = reactEventsViewRef;
411
- if (reactEventsView) {
412
- reactEventsView.setHoveredEvent(
413
- hoveredEvent ? hoveredEvent.reactEvent : null,
495
+ const {current: schedulingEventsView} = schedulingEventsViewRef;
496
+ if (schedulingEventsView) {
497
+ schedulingEventsView.setHoveredEvent(
498
+ hoveredEvent ? hoveredEvent.schedulingEvent : null,
499
+ );
500
+ }
501
+
502
+ const {current: suspenseEventsView} = suspenseEventsViewRef;
503
+ if (suspenseEventsView) {
504
+ suspenseEventsView.setHoveredEvent(
505
+ hoveredEvent ? hoveredEvent.suspenseEvent : null,
506
);
507
}
508
@@ -443,24 +535,25 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
535
return null;
536
}
537
const {
446
- reactEvent,
538
flamechartStackFrame,
539
measure,
540
+ schedulingEvent,
541
+ suspenseEvent,
542
} = contextData.hoveredEvent;
543
return (
544
<Fragment>
452
- {reactEvent !== null && (
545
+ {schedulingEvent !== null && (
546
<ContextMenuItem
454
- onClick={() => copy(reactEvent.componentName)}
547
+ onClick={() => copy(schedulingEvent.componentName)}
548
title="Copy component name">
549
Copy component name
550
</ContextMenuItem>
551
)}
459
- {reactEvent !== null && reactEvent.componentStack && (
552
+ {suspenseEvent !== null && (
553
<ContextMenuItem
461
- onClick={() => copy(reactEvent.componentStack)}
462
- title="Copy component stack">
463
- Copy component stack
554
+ onClick={() => copy(suspenseEvent.componentName)}
555
+ title="Copy component name">
556
+ Copy component name
557
</ContextMenuItem>
558
)}
559
{measure !== null && (
packages/react-devtools-scheduling-profiler/src/EventTooltip.css
+21
-26
@@ -1,6 +1,10 @@
1
.Tooltip {
2
position: fixed;
3
- display: inline-block;
3
+}
4
+
5
+.TooltipSection,
6
+.TooltipWarningSection {
7
+ display: block;
8
border-radius: 0.125rem;
9
max-width: 300px;
10
padding: 0.25rem;
@@ -12,11 +16,15 @@
16
color: var(--color-tooltip-text);
17
font-size: 11px;
18
}
19
+.TooltipWarningSection {
20
+ margin-top: 0.25rem;
21
+ background-color: var(--color-warning-background);
22
+}
23
24
.Divider {
25
height: 1px;
26
background-color: #aaa;
19
- margin: 0.5rem 0;
27
+ margin: 0.25rem 0;
28
}
29
30
.DetailsGrid {
@@ -40,7 +48,6 @@
48
49
.FlamechartStackFrameName {
50
word-break: break-word;
43
- margin-left: 0.4rem;
51
}
52
53
.ComponentName {
@@ -49,32 +56,20 @@
56
margin-right: 0.25rem;
57
}
58
52
-.ComponentStack {
53
- overflow: hidden;
54
- max-width: 35em;
55
- max-height: 10em;
56
- margin: 0;
57
- font-size: 0.9em;
58
- line-height: 1.5;
59
- -webkit-mask-image: linear-gradient(
60
- 180deg,
61
- var(--color-tooltip-background),
62
- var(--color-tooltip-background) 5em,
63
- transparent
64
- );
65
- mask-image: linear-gradient(
66
- 180deg,
67
- var(--color-tooltip-background),
68
- var(--color-tooltip-background) 5em,
69
- transparent
70
- );
71
- white-space: pre;
72
-}
73
-
59
.ReactMeasureLabel {
75
- margin-left: 0.4rem;
60
}
61
62
.UserTimingLabel {
63
word-break: break-word;
64
}
65
+
66
+.NativeEventName {
67
+ font-weight: bold;
68
+ word-break: break-word;
69
+ margin-right: 0.25rem;
70
+}
71
+
72
+.InfoText,
73
+.WarningText {
74
+ color: var(--color-warning-text-color);
75
+}
\ No newline at end of file
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+185
-157
@@ -11,18 +11,18 @@ import type {Point} from './view-base';
11
import type {
12
FlamechartStackFrame,
13
NativeEvent,
14
- ReactEvent,
14
ReactHoverContextInfo,
15
ReactMeasure,
16
ReactProfilerData,
17
Return,
18
+ SchedulingEvent,
19
+ SuspenseEvent,
20
UserTimingMark,
21
} from './types';
22
23
import * as React from 'react';
23
-import {Fragment, useRef} from 'react';
24
-import prettyMilliseconds from 'pretty-ms';
25
-import {COLORS} from './content-views/constants';
24
+import {useRef} from 'react';
25
+import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
26
import {getBatchRange} from './utils/getBatchRange';
27
import useSmartTooltip from './utils/useSmartTooltip';
28
import styles from './EventTooltip.css';
@@ -34,22 +34,7 @@ type Props = {|
34
origin: Point,
35
|};
36
37
-function formatTimestamp(ms) {
38
- return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms';
39
-}
40
-
41
-function formatDuration(ms) {
42
- return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3});
43
-}
44
-
45
-function trimmedString(string: string, length: number): string {
46
- if (string.length > length) {
47
- return `${string.substr(0, length - 1)}…`;
48
- }
49
- return string;
50
-}
51
-
52
-function getReactEventLabel(event: ReactEvent): string | null {
37
+function getSchedulingEventLabel(event: SchedulingEvent): string | null {
38
switch (event.type) {
39
case 'schedule-render':
40
return 'render scheduled';
@@ -57,30 +42,6 @@ function getReactEventLabel(event: ReactEvent): string | null {
42
return 'state update scheduled';
43
case 'schedule-force-update':
44
return 'force update scheduled';
60
- case 'suspense-suspend':
61
- return 'suspended';
62
- case 'suspense-resolved':
63
- return 'suspense resolved';
64
- case 'suspense-rejected':
65
- return 'suspense rejected';
66
- default:
67
- return null;
68
- }
69
-}
70
-
71
-function getReactEventColor(event: ReactEvent): string | null {
72
- switch (event.type) {
73
- case 'schedule-render':
74
- return COLORS.REACT_SCHEDULE_HOVER;
75
- case 'schedule-state-update':
76
- case 'schedule-force-update':
77
- return event.isCascading
78
- ? COLORS.REACT_SCHEDULE_CASCADING_HOVER
79
- : COLORS.REACT_SCHEDULE_HOVER;
80
- case 'suspense-suspend':
81
- case 'suspense-resolved':
82
- case 'suspense-rejected':
83
- return COLORS.REACT_SUSPEND_HOVER;
45
default:
46
return null;
47
}
@@ -89,15 +50,15 @@ function getReactEventColor(event: ReactEvent): string | null {
50
function getReactMeasureLabel(type): string | null {
51
switch (type) {
52
case 'commit':
92
- return 'commit';
53
+ return 'react commit';
54
case 'render-idle':
94
- return 'idle';
55
+ return 'react idle';
56
case 'render':
96
- return 'render';
57
+ return 'react render';
58
case 'layout-effects':
98
- return 'layout effects';
59
+ return 'react layout effects';
60
case 'passive-effects':
100
- return 'passive effects';
61
+ return 'react passive effects';
62
default:
63
return null;
64
}
@@ -120,10 +81,11 @@ export default function EventTooltip({
81
}
82
83
const {
123
- nativeEvent,
124
- reactEvent,
125
- measure,
84
flamechartStackFrame,
85
+ measure,
86
+ nativeEvent,
87
+ schedulingEvent,
88
+ suspenseEvent,
89
userTimingMark,
90
} = hoveredEvent;
91
@@ -131,9 +93,19 @@ export default function EventTooltip({
93
return (
94
<TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
95
);
134
- } else if (reactEvent !== null) {
96
+ } else if (schedulingEvent !== null) {
97
return (
136
- <TooltipReactEvent reactEvent={reactEvent} tooltipRef={tooltipRef} />
98
+ <TooltipSchedulingEvent
99
+ schedulingEvent={schedulingEvent}
100
+ tooltipRef={tooltipRef}
101
+ />
102
+ );
103
+ } else if (suspenseEvent !== null) {
104
+ return (
105
+ <TooltipSuspenseEvent
106
+ suspenseEvent={suspenseEvent}
107
+ tooltipRef={tooltipRef}
108
+ />
109
);
110
} else if (measure !== null) {
111
return (
@@ -158,16 +130,6 @@ export default function EventTooltip({
130
return null;
131
}
132
161
-function formatComponentStack(componentStack: string): string {
162
- const lines = componentStack.split('\n').map(line => line.trim());
163
- lines.shift();
164
-
165
- if (lines.length > 5) {
166
- return lines.slice(0, 5).join('\n') + '\n...';
167
- }
168
- return lines.join('\n');
169
-}
170
-
133
const TooltipFlamechartNode = ({
134
stackFrame,
135
tooltipRef,
@@ -175,35 +137,25 @@ const TooltipFlamechartNode = ({
137
stackFrame: FlamechartStackFrame,
138
tooltipRef: Return<typeof useRef>,
139
}) => {
178
- const {
179
- name,
180
- timestamp,
181
- duration,
182
- scriptUrl,
183
- locationLine,
184
- locationColumn,
185
- } = stackFrame;
140
+ const {name, timestamp, duration, locationLine, locationColumn} = stackFrame;
141
return (
142
<div className={styles.Tooltip} ref={tooltipRef}>
188
- {formatDuration(duration)}
189
- <span className={styles.FlamechartStackFrameName}>{name}</span>
190
- <div className={styles.DetailsGrid}>
191
- <div className={styles.DetailsGridLabel}>Timestamp:</div>
192
- <div>{formatTimestamp(timestamp)}</div>
193
- {scriptUrl && (
194
- <>
195
- <div className={styles.DetailsGridLabel}>Script URL:</div>
196
- <div className={styles.DetailsGridURL}>{scriptUrl}</div>
197
- </>
198
- )}
199
- {(locationLine !== undefined || locationColumn !== undefined) && (
200
- <>
201
- <div className={styles.DetailsGridLabel}>Location:</div>
202
- <div>
203
- line {locationLine}, column {locationColumn}
204
- </div>
205
- </>
206
- )}
143
+ <div className={styles.TooltipSection}>
144
+ <span className={styles.FlamechartStackFrameName}>{name}</span>
145
+ <div className={styles.DetailsGrid}>
146
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
147
+ <div>{formatTimestamp(timestamp)}</div>
148
+ <div className={styles.DetailsGridLabel}>Duration:</div>
149
+ <div>{formatDuration(duration)}</div>
150
+ {(locationLine !== undefined || locationColumn !== undefined) && (
151
+ <>
152
+ <div className={styles.DetailsGridLabel}>Location:</div>
153
+ <div>
154
+ line {locationLine}, column {locationColumn}
155
+ </div>
156
+ </>
157
+ )}
158
+ </div>
159
</div>
160
</div>
161
);
@@ -216,75 +168,142 @@ const TooltipNativeEvent = ({
168
nativeEvent: NativeEvent,
169
tooltipRef: Return<typeof useRef>,
170
}) => {
219
- const {duration, timestamp, type, warnings} = nativeEvent;
220
-
221
- const warningElements = [];
222
- if (warnings !== null) {
223
- warnings.forEach((warning, index) => {
224
- warningElements.push(
225
- <Fragment key={index}>
226
- <div className={styles.DetailsGridLabel}>Warning:</div>
227
- <div>{warning}</div>
228
- </Fragment>,
229
- );
230
- });
231
- }
171
+ const {duration, timestamp, type, warning} = nativeEvent;
172
173
return (
174
<div className={styles.Tooltip} ref={tooltipRef}>
235
- <span className={styles.ComponentName}>{trimmedString(type, 768)}</span>
236
- event
237
- <div className={styles.Divider} />
238
- <div className={styles.DetailsGrid}>
239
- <div className={styles.DetailsGridLabel}>Timestamp:</div>
240
- <div>{formatTimestamp(timestamp)}</div>
241
- <div className={styles.DetailsGridLabel}>Duration:</div>
242
- <div>{formatDuration(duration)}</div>
243
- {warningElements}
175
+ <div className={styles.TooltipSection}>
176
+ <span className={styles.NativeEventName}>{trimString(type, 768)}</span>
177
+ event
178
+ <div className={styles.Divider} />
179
+ <div className={styles.DetailsGrid}>
180
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
181
+ <div>{formatTimestamp(timestamp)}</div>
182
+ <div className={styles.DetailsGridLabel}>Duration:</div>
183
+ <div>{formatDuration(duration)}</div>
184
+ </div>
185
</div>
186
+ {warning !== null && (
187
+ <div className={styles.TooltipWarningSection}>
188
+ <div className={styles.WarningText}>{warning}</div>
189
+ </div>
190
+ )}
191
</div>
192
);
193
};
194
249
-const TooltipReactEvent = ({
250
- reactEvent,
195
+const TooltipSchedulingEvent = ({
196
+ schedulingEvent,
197
tooltipRef,
198
}: {
253
- reactEvent: ReactEvent,
199
+ schedulingEvent: SchedulingEvent,
200
tooltipRef: Return<typeof useRef>,
201
}) => {
256
- const label = getReactEventLabel(reactEvent);
257
- const color = getReactEventColor(reactEvent);
258
- if (!label || !color) {
202
+ const label = getSchedulingEventLabel(schedulingEvent);
203
+ if (!label) {
204
if (__DEV__) {
260
- console.warn('Unexpected reactEvent type "%s"', reactEvent.type);
205
+ console.warn(
206
+ 'Unexpected schedulingEvent type "%s"',
207
+ schedulingEvent.type,
208
+ );
209
}
210
return null;
211
}
212
265
- const {componentName, componentStack, timestamp} = reactEvent;
213
+ let laneLabels = null;
214
+ let lanes = null;
215
+ switch (schedulingEvent.type) {
216
+ case 'schedule-render':
217
+ case 'schedule-state-update':
218
+ case 'schedule-force-update':
219
+ laneLabels = schedulingEvent.laneLabels;
220
+ lanes = schedulingEvent.lanes;
221
+ break;
222
+ }
223
+
224
+ const {componentName, timestamp, warning} = schedulingEvent;
225
226
return (
227
<div className={styles.Tooltip} ref={tooltipRef}>
269
- {componentName && (
270
- <span className={styles.ComponentName} style={{color}}>
271
- {trimmedString(componentName, 768)}
272
- </span>
228
+ <div className={styles.TooltipSection}>
229
+ {componentName && (
230
+ <span className={styles.ComponentName}>
231
+ {trimString(componentName, 100)}
232
+ </span>
233
+ )}
234
+ {label}
235
+ <div className={styles.Divider} />
236
+ <div className={styles.DetailsGrid}>
237
+ {laneLabels !== null && lanes !== null && (
238
+ <>
239
+ <div className={styles.DetailsGridLabel}>Lanes:</div>
240
+ <div>
241
+ {laneLabels.join(', ')} ({lanes.join(', ')})
242
+ </div>
243
+ </>
244
+ )}
245
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
246
+ <div>{formatTimestamp(timestamp)}</div>
247
+ </div>
248
+ </div>
249
+ {warning !== null && (
250
+ <div className={styles.TooltipWarningSection}>
251
+ <div className={styles.WarningText}>{warning}</div>
252
+ </div>
253
)}
274
- {label}
275
- <div className={styles.Divider} />
276
- <div className={styles.DetailsGrid}>
277
- <div className={styles.DetailsGridLabel}>Timestamp:</div>
278
- <div>{formatTimestamp(timestamp)}</div>
279
- {componentStack && (
280
- <Fragment>
281
- <div className={styles.DetailsGridLabel}>Component stack:</div>
282
- <pre className={styles.ComponentStack}>
283
- {formatComponentStack(componentStack)}
284
- </pre>
285
- </Fragment>
254
+ </div>
255
+ );
256
+};
257
+
258
+const TooltipSuspenseEvent = ({
259
+ suspenseEvent,
260
+ tooltipRef,
261
+}: {
262
+ suspenseEvent: SuspenseEvent,
263
+ tooltipRef: Return<typeof useRef>,
264
+}) => {
265
+ const {
266
+ componentName,
267
+ duration,
268
+ phase,
269
+ resolution,
270
+ timestamp,
271
+ warning,
272
+ } = suspenseEvent;
273
+
274
+ let label = 'suspended';
275
+ if (phase !== null) {
276
+ label += ` during ${phase}`;
277
+ }
278
+
279
+ return (
280
+ <div className={styles.Tooltip} ref={tooltipRef}>
281
+ <div className={styles.TooltipSection}>
282
+ {componentName && (
283
+ <span className={styles.ComponentName}>
284
+ {trimString(componentName, 100)}
285
+ </span>
286
)}
287
+ {label}
288
+ <div className={styles.Divider} />
289
+ <div className={styles.DetailsGrid}>
290
+ <div className={styles.DetailsGridLabel}>Status:</div>
291
+ <div>{resolution}</div>
292
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
293
+ <div>{formatTimestamp(timestamp)}</div>
294
+ {duration !== null && (
295
+ <>
296
+ <div className={styles.DetailsGridLabel}>Duration:</div>
297
+ <div>{formatDuration(duration)}</div>
298
+ </>
299
+ )}
300
+ </div>
301
</div>
302
+ {warning !== null && (
303
+ <div className={styles.TooltipWarningSection}>
304
+ <div className={styles.WarningText}>{warning}</div>
305
+ </div>
306
+ )}
307
</div>
308
);
309
};
@@ -311,21 +330,28 @@ const TooltipReactMeasure = ({
330
331
return (
332
<div className={styles.Tooltip} ref={tooltipRef}>
314
- {formatDuration(duration)}
315
- <span className={styles.ReactMeasureLabel}>{label}</span>
316
- <div className={styles.Divider} />
317
- <div className={styles.DetailsGrid}>
318
- <div className={styles.DetailsGridLabel}>Timestamp:</div>
319
- <div>{formatTimestamp(timestamp)}</div>
320
- <div className={styles.DetailsGridLabel}>Batch duration:</div>
321
- <div>{formatDuration(stopTime - startTime)}</div>
322
- <div className={styles.DetailsGridLabel}>
323
- Lane{lanes.length === 1 ? '' : 's'}:
324
- </div>
325
- <div>
326
- {laneLabels.length > 0
327
- ? `${laneLabels.join(', ')} (${lanes.join(', ')})`
328
- : lanes.join(', ')}
333
+ <div className={styles.TooltipSection}>
334
+ <span className={styles.ReactMeasureLabel}>{label}</span>
335
+ <div className={styles.Divider} />
336
+ <div className={styles.DetailsGrid}>
337
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
338
+ <div>{formatTimestamp(timestamp)}</div>
339
+ {measure.type !== 'render-idle' && (
340
+ <>
341
+ <div className={styles.DetailsGridLabel}>Duration:</div>
342
+ <div>{formatDuration(duration)}</div>
343
+ </>
344
+ )}
345
+ <div className={styles.DetailsGridLabel}>Batch duration:</div>
346
+ <div>{formatDuration(stopTime - startTime)}</div>
347
+ <div className={styles.DetailsGridLabel}>
348
+ Lane{lanes.length === 1 ? '' : 's'}:
349
+ </div>
350
+ <div>
351
+ {laneLabels.length > 0
352
+ ? `${laneLabels.join(', ')} (${lanes.join(', ')})`
353
+ : lanes.join(', ')}
354
+ </div>
355
</div>
356
</div>
357
</div>
@@ -342,11 +368,13 @@ const TooltipUserTimingMark = ({
368
const {name, timestamp} = mark;
369
return (
370
<div className={styles.Tooltip} ref={tooltipRef}>
345
- <span className={styles.UserTimingLabel}>{name}</span>
346
- <div className={styles.Divider} />
347
- <div className={styles.DetailsGrid}>
348
- <div className={styles.DetailsGridLabel}>Timestamp:</div>
349
- <div>{formatTimestamp(timestamp)}</div>
371
+ <div className={styles.TooltipSection}>
372
+ <span className={styles.UserTimingLabel}>{name}</span>
373
+ <div className={styles.Divider} />
374
+ <div className={styles.DetailsGrid}>
375
+ <div className={styles.DetailsGridLabel}>Timestamp:</div>
376
+ <div>{formatTimestamp(timestamp)}</div>
377
+ </div>
378
</div>
379
</div>
380
);
packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
+12
-2
@@ -41,8 +41,18 @@ export function SchedulingProfiler(_: {||}) {
41
// The easiest way to guarangee this happens is to recreate the inner Canvas component.
42
const [key, setKey] = useState<string>(theme);
43
useLayoutEffect(() => {
44
- updateColorsToMatchTheme();
45
- setKey(deferredTheme);
44
+ const pollForTheme = () => {
45
+ if (updateColorsToMatchTheme()) {
46
+ clearInterval(intervalID);
47
+ setKey(deferredTheme);
48
+ }
49
+ };
50
+
51
+ const intervalID = setInterval(pollForTheme, 50);
52
+
53
+ return () => {
54
+ clearInterval(intervalID);
55
+ };
56
}, [deferredTheme]);
57
58
return (
packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js
+21
-81
@@ -21,12 +21,11 @@ import type {
21
} from '../view-base';
22
23
import {
24
- ColorView,
24
+ BackgroundColorView,
25
Surface,
26
View,
27
layeredLayout,
28
rectContainsPoint,
29
- rectEqualToRect,
29
intersectionOfRects,
30
rectIntersectsRect,
31
verticallyStackedLayout,
@@ -36,11 +35,10 @@ import {
35
positioningScaleFactor,
36
timestampToPosition,
37
} from './utils/positioning';
38
+import {drawText} from './utils/text';
39
import {
40
COLORS,
41
- FONT_SIZE,
41
FLAMECHART_FRAME_HEIGHT,
43
- TEXT_PADDING,
42
COLOR_HOVER_DIM_DELTA,
43
BORDER_SIZE,
44
} from './constants';
@@ -71,29 +69,6 @@ function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
69
return hslaColorToString(color);
70
}
71
74
-const cachedFlamechartTextWidths = new Map();
75
-const trimFlamechartText = (
76
- context: CanvasRenderingContext2D,
77
- text: string,
78
- width: number,
79
-) => {
80
- for (let i = text.length - 1; i >= 0; i--) {
81
- const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
82
-
83
- let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
84
- if (measuredWidth == null) {
85
- measuredWidth = context.measureText(trimmedText).width;
86
- cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
87
- }
88
-
89
- if (measuredWidth <= width) {
90
- return trimmedText;
91
- }
92
- }
93
-
94
- return null;
95
-};
96
-
72
class FlamechartStackLayerView extends View {
73
/** Layer to display */
74
_stackLayer: FlamechartStackLayer;
@@ -161,10 +136,6 @@ class FlamechartStackLayerView extends View {
136
visibleArea.size.height,
137
);
138
164
- context.textAlign = 'left';
165
- context.textBaseline = 'middle';
166
- context.font = `${FONT_SIZE}px sans-serif`;
167
-
139
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
140
141
for (let i = 0; i < _stackLayer.length; i++) {
@@ -201,45 +172,7 @@ class FlamechartStackLayerView extends View {
172
drawableRect.size.height,
173
);
174
204
- if (width > TEXT_PADDING * 2) {
205
- const trimmedName = trimFlamechartText(
206
- context,
207
- name,
208
- width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
209
- );
210
-
211
- if (trimmedName !== null) {
212
- context.fillStyle = COLORS.TEXT_COLOR;
213
-
214
- // Prevent text from being drawn outside `viewableArea`
215
- const textOverflowsViewableArea = !rectEqualToRect(
216
- drawableRect,
217
- nodeRect,
218
- );
219
- if (textOverflowsViewableArea) {
220
- context.save();
221
- context.beginPath();
222
- context.rect(
223
- drawableRect.origin.x,
224
- drawableRect.origin.y,
225
- drawableRect.size.width,
226
- drawableRect.size.height,
227
- );
228
- context.closePath();
229
- context.clip();
230
- }
231
-
232
- context.fillText(
233
- trimmedName,
234
- nodeRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
235
- nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
236
- );
237
-
238
- if (textOverflowsViewableArea) {
239
- context.restore();
240
- }
241
- }
242
- }
175
+ drawText(name, context, nodeRect, drawableRect, width);
176
}
177
}
178
@@ -262,13 +195,17 @@ class FlamechartStackLayerView extends View {
195
const flamechartStackFrame = _stackLayer[currentIndex];
196
const {timestamp, duration} = flamechartStackFrame;
197
265
- const width = durationToWidth(duration, scaleFactor);
198
const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
267
- if (x <= location.x && x + width >= location.x) {
268
- this.currentCursor = 'pointer';
269
- viewRefs.hoveredView = this;
270
- _onHover(flamechartStackFrame);
271
- return;
199
+ const width = durationToWidth(duration, scaleFactor);
200
+
201
+ // Don't show tooltips for nodes that are too small to render at this zoom level.
202
+ if (Math.floor(width - BORDER_SIZE) >= 1) {
203
+ if (x <= location.x && x + width >= location.x) {
204
+ this.currentCursor = 'context-menu';
205
+ viewRefs.hoveredView = this;
206
+ _onHover(flamechartStackFrame);
207
+ return;
208
+ }
209
}
210
211
if (x > location.x) {
@@ -336,10 +273,8 @@ export class FlamechartView extends View {
273
return rowView;
274
});
275
339
- // Add a plain background view to prevent gaps from appearing between
340
- // flamechartRowViews.
341
- const colorView = new ColorView(surface, frame, COLORS.BACKGROUND);
342
- this.addSubview(colorView);
276
+ // Add a plain background view to prevent gaps from appearing between flamechartRowViews.
277
+ this.addSubview(new BackgroundColorView(surface, frame));
278
this.addSubview(this._verticalStackView);
279
}
280
@@ -359,7 +294,12 @@ export class FlamechartView extends View {
294
295
desiredSize() {
296
// Ignore the wishes of the background color view
362
- return this._verticalStackView.desiredSize();
297
+ const intrinsicSize = this._verticalStackView.desiredSize();
298
+ return {
299
+ ...intrinsicSize,
300
+ // Collapsed by default
301
+ maxInitialHeight: 0,
302
+ };
303
}
304
305
/**
packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js
+18
-67
@@ -22,6 +22,8 @@ import {
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,
@@ -29,40 +31,10 @@ import {
31
rectIntersectsRect,
32
intersectionOfRects,
33
} from '../view-base';
32
-import {
33
- COLORS,
34
- TEXT_PADDING,
35
- NATIVE_EVENT_HEIGHT,
36
- FONT_SIZE,
37
- BORDER_SIZE,
38
-} from './constants';
34
+import {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants';
35
36
const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;
37
42
-// TODO (scheduling profiler) Make this a reusable util
43
-const cachedFlamechartTextWidths = new Map();
44
-const trimFlamechartText = (
45
- context: CanvasRenderingContext2D,
46
- text: string,
47
- width: number,
48
-) => {
49
- for (let i = text.length - 1; i >= 0; i--) {
50
- const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
51
-
52
- let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
53
- if (measuredWidth == null) {
54
- measuredWidth = context.measureText(trimmedText).width;
55
- cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
56
- }
57
-
58
- if (measuredWidth <= width) {
59
- return trimmedText;
60
- }
61
- }
62
-
63
- return null;
64
-};
65
-
38
export class NativeEventsView extends View {
39
_depthToNativeEvent: Map<number, NativeEvent[]>;
40
_hoveredEvent: NativeEvent | null = null;
@@ -117,7 +89,7 @@ export class NativeEventsView extends View {
89
}
90
91
/**
120
- * Draw a single `NativeEvent` as a circle in the canvas.
92
+ * Draw a single `NativeEvent` as a box/span with text inside of it.
93
*/
94
_drawSingleNativeEvent(
95
context: CanvasRenderingContext2D,
@@ -128,7 +100,7 @@ export class NativeEventsView extends View {
100
showHoverHighlight: boolean,
101
) {
102
const {frame} = this;
131
- const {depth, duration, timestamp, type, warnings} = event;
103
+ const {depth, duration, timestamp, type, warning} = event;
104
105
baseY += depth * ROW_WITH_BORDER_HEIGHT;
106
@@ -152,10 +124,10 @@ export class NativeEventsView extends View {
124
125
const drawableRect = intersectionOfRects(eventRect, rect);
126
context.beginPath();
155
- if (warnings !== null) {
127
+ if (warning !== null) {
128
context.fillStyle = showHoverHighlight
157
- ? COLORS.NATIVE_EVENT_WARNING_HOVER
158
- : COLORS.NATIVE_EVENT_WARNING;
129
+ ? COLORS.WARNING_BACKGROUND_HOVER
130
+ : COLORS.WARNING_BACKGROUND;
131
} else {
132
context.fillStyle = showHoverHighlight
133
? COLORS.NATIVE_EVENT_HOVER
@@ -168,32 +140,9 @@ export class NativeEventsView extends View {
140
drawableRect.size.height,
141
);
142
171
- // Render event type label
172
- context.textAlign = 'left';
173
- context.textBaseline = 'middle';
174
- context.font = `${FONT_SIZE}px sans-serif`;
175
-
176
- if (width > TEXT_PADDING * 2) {
177
- const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
178
- const trimmedName = trimFlamechartText(
179
- context,
180
- type,
181
- width - TEXT_PADDING * 2 + (x < 0 ? x : 0),
182
- );
143
+ const label = `${type} - ${formatDuration(duration)}`;
144
184
- if (trimmedName !== null) {
185
- context.fillStyle =
186
- warnings !== null
187
- ? COLORS.NATIVE_EVENT_WARNING_TEXT
188
- : COLORS.TEXT_COLOR;
189
-
190
- context.fillText(
191
- trimmedName,
192
- eventRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0),
193
- eventRect.origin.y + NATIVE_EVENT_HEIGHT / 2,
194
- );
195
- }
196
- }
145
+ drawText(label, context, eventRect, drawableRect, width);
146
}
147
148
draw(context: CanvasRenderingContext2D) {
@@ -242,12 +191,16 @@ export class NativeEventsView extends View {
191
},
192
};
193
if (rectIntersectsRect(borderFrame, visibleArea)) {
194
+ const borderDrawableRect = intersectionOfRects(
195
+ borderFrame,
196
+ visibleArea,
197
+ );
198
context.fillStyle = COLORS.PRIORITY_BORDER;
199
context.fillRect(
247
- visibleArea.origin.x,
248
- frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
249
- visibleArea.size.width,
250
- BORDER_SIZE,
200
+ borderDrawableRect.origin.x,
201
+ borderDrawableRect.origin.y,
202
+ borderDrawableRect.size.width,
203
+ borderDrawableRect.size.height,
204
);
205
}
206
}
@@ -285,8 +238,6 @@ export class NativeEventsView extends View {
238
hoverTimestamp >= timestamp &&
239
hoverTimestamp <= timestamp + duration
240
) {
288
- this.currentCursor = 'pointer';
289
-
241
viewRefs.hoveredView = this;
242
243
onHover(nativeEvent);
packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
+10
-8
@@ -12,7 +12,7 @@ import type {
12
Interaction,
13
MouseMoveInteraction,
14
Rect,
15
- Size,
15
+ SizeWithMaxHeight,
16
ViewRefs,
17
} from '../view-base';
18
@@ -34,6 +34,7 @@ import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
34
import {REACT_TOTAL_NUM_LANES} from '../constants';
35
36
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
37
+const MAX_ROWS_TO_SHOW_INITIALLY = 5;
38
39
function getMeasuresForLane(
40
allMeasures: ReactMeasure[],
@@ -44,7 +45,7 @@ function getMeasuresForLane(
45
46
export class ReactMeasuresView extends View {
47
_profilerData: ReactProfilerData;
47
- _intrinsicSize: Size;
48
+ _intrinsicSize: SizeWithMaxHeight;
49
50
_lanesToRender: ReactLane[];
51
_laneToMeasures: Map<ReactLane, ReactMeasure[]>;
@@ -77,6 +78,7 @@ export class ReactMeasuresView extends View {
78
this._intrinsicSize = {
79
width: this._profilerData.duration,
80
height: this._lanesToRender.length * REACT_LANE_HEIGHT,
81
+ maxInitialHeight: MAX_ROWS_TO_SHOW_INITIALLY * REACT_LANE_HEIGHT,
82
};
83
}
84
@@ -132,7 +134,7 @@ export class ReactMeasuresView extends View {
134
case 'commit':
135
fillStyle = COLORS.REACT_COMMIT;
136
hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
135
- groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED;
137
+ groupSelectedFillStyle = COLORS.REACT_COMMIT_HOVER;
138
break;
139
case 'render-idle':
140
// We could render idle time as diagonal hashes.
@@ -140,22 +142,22 @@ export class ReactMeasuresView extends View {
142
// color = context.createPattern(getIdlePattern(), 'repeat');
143
fillStyle = COLORS.REACT_IDLE;
144
hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
143
- groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED;
145
+ groupSelectedFillStyle = COLORS.REACT_IDLE_HOVER;
146
break;
147
case 'render':
148
fillStyle = COLORS.REACT_RENDER;
149
hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
148
- groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED;
150
+ groupSelectedFillStyle = COLORS.REACT_RENDER_HOVER;
151
break;
152
case 'layout-effects':
153
fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
154
hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
153
- groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED;
155
+ groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
156
break;
157
case 'passive-effects':
158
fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
159
hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
158
- groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED;
160
+ groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
161
break;
162
default:
163
throw new Error(`Unexpected measure type "${type}"`);
@@ -306,7 +308,7 @@ export class ReactMeasuresView extends View {
308
hoverTimestamp >= timestamp &&
309
hoverTimestamp <= timestamp + duration
310
) {
309
- this.currentCursor = 'pointer';
311
+ this.currentCursor = 'context-menu';
312
viewRefs.hoveredView = this;
313
onHover(measure);
314
return;
packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js
renamed
+35
-58
@@ -7,7 +7,7 @@
7
* @flow
8
*/
9
10
-import type {ReactEvent, ReactProfilerData} from '../types';
10
+import type {SchedulingEvent, ReactProfilerData} from '../types';
11
import type {
12
Interaction,
13
MouseMoveInteraction,
@@ -39,20 +39,12 @@ import {
39
const EVENT_ROW_HEIGHT_FIXED =
40
TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_ROW_PADDING;
41
42
-function isSuspenseEvent(event: ReactEvent): boolean %checks {
43
- return (
44
- event.type === 'suspense-suspend' ||
45
- event.type === 'suspense-resolved' ||
46
- event.type === 'suspense-rejected'
47
- );
48
-}
49
-
50
-export class ReactEventsView extends View {
42
+export class SchedulingEventsView extends View {
43
_profilerData: ReactProfilerData;
44
_intrinsicSize: Size;
45
54
- _hoveredEvent: ReactEvent | null = null;
55
- onHover: ((event: ReactEvent | null) => void) | null = null;
46
+ _hoveredEvent: SchedulingEvent | null = null;
47
+ onHover: ((event: SchedulingEvent | null) => void) | null = null;
48
49
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
50
super(surface, frame);
@@ -68,7 +60,7 @@ export class ReactEventsView extends View {
60
return this._intrinsicSize;
61
}
62
71
- setHoveredEvent(hoveredEvent: ReactEvent | null) {
63
+ setHoveredEvent(hoveredEvent: SchedulingEvent | null) {
64
if (this._hoveredEvent === hoveredEvent) {
65
return;
66
}
@@ -77,18 +69,18 @@ export class ReactEventsView extends View {
69
}
70
71
/**
80
- * Draw a single `ReactEvent` as a circle in the canvas.
72
+ * Draw a single `SchedulingEvent` as a circle in the canvas.
73
*/
82
- _drawSingleReactEvent(
74
+ _drawSingleSchedulingEvent(
75
context: CanvasRenderingContext2D,
76
rect: Rect,
85
- event: ReactEvent,
77
+ event: SchedulingEvent,
78
baseY: number,
79
scaleFactor: number,
80
showHoverHighlight: boolean,
81
) {
82
const {frame} = this;
91
- const {timestamp, type} = event;
83
+ const {timestamp, type, warning} = event;
84
85
const x = timestampToPosition(timestamp, scaleFactor, frame);
86
const radius = REACT_EVENT_DIAMETER / 2;
@@ -105,34 +97,25 @@ export class ReactEventsView extends View {
97
98
let fillStyle = null;
99
108
- switch (type) {
109
- case 'native-event':
110
- return;
111
- case 'schedule-render':
112
- case 'schedule-state-update':
113
- case 'schedule-force-update':
114
- if (event.isCascading) {
115
- fillStyle = showHoverHighlight
116
- ? COLORS.REACT_SCHEDULE_CASCADING_HOVER
117
- : COLORS.REACT_SCHEDULE_CASCADING;
118
- } else {
100
+ if (warning !== null) {
101
+ fillStyle = showHoverHighlight
102
+ ? COLORS.WARNING_BACKGROUND_HOVER
103
+ : COLORS.WARNING_BACKGROUND;
104
+ } else {
105
+ switch (type) {
106
+ case 'schedule-render':
107
+ case 'schedule-state-update':
108
+ case 'schedule-force-update':
109
fillStyle = showHoverHighlight
110
? COLORS.REACT_SCHEDULE_HOVER
111
: COLORS.REACT_SCHEDULE;
122
- }
123
- break;
124
- case 'suspense-suspend':
125
- case 'suspense-resolved':
126
- case 'suspense-rejected':
127
- fillStyle = showHoverHighlight
128
- ? COLORS.REACT_SUSPEND_HOVER
129
- : COLORS.REACT_SUSPEND;
130
- break;
131
- default:
132
- if (__DEV__) {
133
- console.warn('Unexpected event type "%s"', type);
134
- }
135
- break;
112
+ break;
113
+ default:
114
+ if (__DEV__) {
115
+ console.warn('Unexpected event type "%s"', type);
116
+ }
117
+ break;
118
+ }
119
}
120
121
if (fillStyle !== null) {
@@ -148,7 +131,7 @@ export class ReactEventsView extends View {
131
draw(context: CanvasRenderingContext2D) {
132
const {
133
frame,
151
- _profilerData: {reactEvents},
134
+ _profilerData: {schedulingEvents},
135
_hoveredEvent,
136
visibleArea,
137
} = this;
@@ -168,20 +151,14 @@ export class ReactEventsView extends View {
151
frame,
152
);
153
171
- const highlightedEvents: ReactEvent[] = [];
154
+ const highlightedEvents: SchedulingEvent[] = [];
155
173
- reactEvents.forEach(event => {
174
- if (
175
- event === _hoveredEvent ||
176
- (_hoveredEvent &&
177
- isSuspenseEvent(event) &&
178
- isSuspenseEvent(_hoveredEvent) &&
179
- event.id === _hoveredEvent.id)
180
- ) {
156
+ schedulingEvents.forEach(event => {
157
+ if (event === _hoveredEvent) {
158
highlightedEvents.push(event);
159
return;
160
}
184
- this._drawSingleReactEvent(
161
+ this._drawSingleSchedulingEvent(
162
context,
163
visibleArea,
164
event,
@@ -194,7 +171,7 @@ export class ReactEventsView extends View {
171
// Draw the highlighted items on top so they stand out.
172
// This is helpful if there are multiple (overlapping) items close to each other.
173
highlightedEvents.forEach(event => {
197
- this._drawSingleReactEvent(
174
+ this._drawSingleSchedulingEvent(
175
context,
176
visibleArea,
177
event,
@@ -244,7 +221,7 @@ export class ReactEventsView extends View {
221
}
222
223
const {
247
- _profilerData: {reactEvents},
224
+ _profilerData: {schedulingEvents},
225
} = this;
226
const scaleFactor = positioningScaleFactor(
227
this._intrinsicSize.width,
@@ -258,15 +235,15 @@ export class ReactEventsView extends View {
235
236
// Because data ranges may overlap, we want to find the last intersecting item.
237
// This will always be the one on "top" (the one the user is hovering over).
261
- for (let index = reactEvents.length - 1; index >= 0; index--) {
262
- const event = reactEvents[index];
238
+ for (let index = schedulingEvents.length - 1; index >= 0; index--) {
239
+ const event = schedulingEvents[index];
240
const {timestamp} = event;
241
242
if (
243
timestamp - eventTimestampAllowance <= hoverTimestamp &&
244
hoverTimestamp <= timestamp + eventTimestampAllowance
245
) {
269
- this.currentCursor = 'pointer';
246
+ this.currentCursor = 'context-menu';
247
viewRefs.hoveredView = this;
248
onHover(event);
249
return;
packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
new
+368
@@ -0,0 +1,368 @@
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 {SuspenseEvent, ReactProfilerData} from '../types';
11
+import type {
12
+ Interaction,
13
+ MouseMoveInteraction,
14
+ Rect,
15
+ SizeWithMaxHeight,
16
+ ViewRefs,
17
+} from '../view-base';
18
+
19
+import {
20
+ durationToWidth,
21
+ positioningScaleFactor,
22
+ positionToTimestamp,
23
+ timestampToPosition,
24
+ widthToDuration,
25
+} from './utils/positioning';
26
+import {drawText} from './utils/text';
27
+import {formatDuration} from '../utils/formatting';
28
+import {
29
+ View,
30
+ Surface,
31
+ rectContainsPoint,
32
+ rectIntersectsRect,
33
+ intersectionOfRects,
34
+} from '../view-base';
35
+import {
36
+ BORDER_SIZE,
37
+ COLORS,
38
+ PENDING_SUSPENSE_EVENT_SIZE,
39
+ SUSPENSE_EVENT_HEIGHT,
40
+} from './constants';
41
+
42
+const ROW_WITH_BORDER_HEIGHT = SUSPENSE_EVENT_HEIGHT + BORDER_SIZE;
43
+const MAX_ROWS_TO_SHOW_INITIALLY = 3;
44
+
45
+export class SuspenseEventsView extends View {
46
+ _depthToSuspenseEvent: Map<number, SuspenseEvent[]>;
47
+ _hoveredEvent: SuspenseEvent | null = null;
48
+ _intrinsicSize: SizeWithMaxHeight;
49
+ _maxDepth: number = 0;
50
+ _profilerData: ReactProfilerData;
51
+
52
+ onHover: ((event: SuspenseEvent | null) => void) | null = null;
53
+
54
+ constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
55
+ super(surface, frame);
56
+
57
+ this._profilerData = profilerData;
58
+
59
+ this._performPreflightComputations();
60
+ }
61
+
62
+ _performPreflightComputations() {
63
+ this._depthToSuspenseEvent = new Map();
64
+
65
+ const {duration, suspenseEvents} = this._profilerData;
66
+
67
+ suspenseEvents.forEach(event => {
68
+ const depth = event.depth;
69
+
70
+ this._maxDepth = Math.max(this._maxDepth, depth);
71
+
72
+ if (!this._depthToSuspenseEvent.has(depth)) {
73
+ this._depthToSuspenseEvent.set(depth, [event]);
74
+ } else {
75
+ // $FlowFixMe This is unnecessary.
76
+ this._depthToSuspenseEvent.get(depth).push(event);
77
+ }
78
+ });
79
+
80
+ this._intrinsicSize = {
81
+ width: duration,
82
+ height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
83
+ maxInitialHeight: ROW_WITH_BORDER_HEIGHT * MAX_ROWS_TO_SHOW_INITIALLY,
84
+ };
85
+ }
86
+
87
+ desiredSize() {
88
+ return this._intrinsicSize;
89
+ }
90
+
91
+ setHoveredEvent(hoveredEvent: SuspenseEvent | null) {
92
+ if (this._hoveredEvent === hoveredEvent) {
93
+ return;
94
+ }
95
+ this._hoveredEvent = hoveredEvent;
96
+ this.setNeedsDisplay();
97
+ }
98
+
99
+ /**
100
+ * Draw a single `SuspenseEvent` as a box/span with text inside of it.
101
+ */
102
+ _drawSingleSuspenseEvent(
103
+ context: CanvasRenderingContext2D,
104
+ rect: Rect,
105
+ event: SuspenseEvent,
106
+ baseY: number,
107
+ scaleFactor: number,
108
+ showHoverHighlight: boolean,
109
+ ) {
110
+ const {frame} = this;
111
+ const {
112
+ componentName,
113
+ depth,
114
+ duration,
115
+ phase,
116
+ resolution,
117
+ timestamp,
118
+ warning,
119
+ } = event;
120
+
121
+ baseY += depth * ROW_WITH_BORDER_HEIGHT;
122
+
123
+ let fillStyle = ((null: any): string);
124
+ if (warning !== null) {
125
+ fillStyle = showHoverHighlight
126
+ ? COLORS.WARNING_BACKGROUND_HOVER
127
+ : COLORS.WARNING_BACKGROUND;
128
+ } else {
129
+ switch (resolution) {
130
+ case 'rejected':
131
+ fillStyle = showHoverHighlight
132
+ ? COLORS.REACT_SUSPENSE_REJECTED_EVENT_HOVER
133
+ : COLORS.REACT_SUSPENSE_REJECTED_EVENT;
134
+ break;
135
+ case 'resolved':
136
+ fillStyle = showHoverHighlight
137
+ ? COLORS.REACT_SUSPENSE_RESOLVED_EVENT_HOVER
138
+ : COLORS.REACT_SUSPENSE_RESOLVED_EVENT;
139
+ break;
140
+ case 'unresolved':
141
+ fillStyle = showHoverHighlight
142
+ ? COLORS.REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER
143
+ : COLORS.REACT_SUSPENSE_UNRESOLVED_EVENT;
144
+ break;
145
+ }
146
+ }
147
+
148
+ const xStart = timestampToPosition(timestamp, scaleFactor, frame);
149
+
150
+ // Pending suspense events (ones that never resolved) won't have durations.
151
+ // So instead we draw them as diamonds.
152
+ if (duration === null) {
153
+ const size = PENDING_SUSPENSE_EVENT_SIZE;
154
+ const halfSize = size / 2;
155
+
156
+ baseY += (SUSPENSE_EVENT_HEIGHT - PENDING_SUSPENSE_EVENT_SIZE) / 2;
157
+
158
+ const y = baseY + halfSize;
159
+
160
+ const suspenseRect: Rect = {
161
+ origin: {
162
+ x: xStart - halfSize,
163
+ y: baseY,
164
+ },
165
+ size: {width: size, height: size},
166
+ };
167
+ if (!rectIntersectsRect(suspenseRect, rect)) {
168
+ return; // Not in view
169
+ }
170
+
171
+ const drawableRect = intersectionOfRects(suspenseRect, rect);
172
+
173
+ // Clip diamonds so they don't overflow if the view has been resized (smaller).
174
+ const region = new Path2D();
175
+ region.rect(
176
+ drawableRect.origin.x,
177
+ drawableRect.origin.y,
178
+ drawableRect.size.width,
179
+ drawableRect.size.height,
180
+ );
181
+ context.save();
182
+ context.clip(region);
183
+ context.beginPath();
184
+ context.fillStyle = fillStyle;
185
+ context.moveTo(xStart, y - halfSize);
186
+ context.lineTo(xStart + halfSize, y);
187
+ context.lineTo(xStart, y + halfSize);
188
+ context.lineTo(xStart - halfSize, y);
189
+ context.fill();
190
+ context.restore();
191
+ } else {
192
+ const xStop = timestampToPosition(
193
+ timestamp + duration,
194
+ scaleFactor,
195
+ frame,
196
+ );
197
+ const eventRect: Rect = {
198
+ origin: {
199
+ x: xStart,
200
+ y: baseY,
201
+ },
202
+ size: {width: xStop - xStart, height: SUSPENSE_EVENT_HEIGHT},
203
+ };
204
+ if (!rectIntersectsRect(eventRect, rect)) {
205
+ return; // Not in view
206
+ }
207
+
208
+ const width = durationToWidth(duration, scaleFactor);
209
+ if (width < 1) {
210
+ return; // Too small to render at this zoom level
211
+ }
212
+
213
+ const drawableRect = intersectionOfRects(eventRect, rect);
214
+ context.beginPath();
215
+ context.fillStyle = fillStyle;
216
+ context.fillRect(
217
+ drawableRect.origin.x,
218
+ drawableRect.origin.y,
219
+ drawableRect.size.width,
220
+ drawableRect.size.height,
221
+ );
222
+
223
+ let label = 'suspended';
224
+ if (componentName != null) {
225
+ label = `${componentName} ${label}`;
226
+ }
227
+ if (phase !== null) {
228
+ label += ` during ${phase}`;
229
+ }
230
+ if (resolution !== 'unresolved') {
231
+ label += ` - ${formatDuration(duration)}`;
232
+ }
233
+
234
+ drawText(label, context, eventRect, drawableRect, width);
235
+ }
236
+ }
237
+
238
+ draw(context: CanvasRenderingContext2D) {
239
+ const {
240
+ frame,
241
+ _profilerData: {suspenseEvents},
242
+ _hoveredEvent,
243
+ visibleArea,
244
+ } = this;
245
+
246
+ context.fillStyle = COLORS.PRIORITY_BACKGROUND;
247
+ context.fillRect(
248
+ visibleArea.origin.x,
249
+ visibleArea.origin.y,
250
+ visibleArea.size.width,
251
+ visibleArea.size.height,
252
+ );
253
+
254
+ // Draw events
255
+ const scaleFactor = positioningScaleFactor(
256
+ this._intrinsicSize.width,
257
+ frame,
258
+ );
259
+
260
+ suspenseEvents.forEach(event => {
261
+ this._drawSingleSuspenseEvent(
262
+ context,
263
+ visibleArea,
264
+ event,
265
+ frame.origin.y,
266
+ scaleFactor,
267
+ event === _hoveredEvent,
268
+ );
269
+ });
270
+
271
+ // Render bottom borders.
272
+ for (let i = 0; i <= this._maxDepth; i++) {
273
+ const borderFrame: Rect = {
274
+ origin: {
275
+ x: frame.origin.x,
276
+ y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
277
+ },
278
+ size: {
279
+ width: frame.size.width,
280
+ height: BORDER_SIZE,
281
+ },
282
+ };
283
+ if (rectIntersectsRect(borderFrame, visibleArea)) {
284
+ const borderDrawableRect = intersectionOfRects(
285
+ borderFrame,
286
+ visibleArea,
287
+ );
288
+ context.fillStyle = COLORS.PRIORITY_BORDER;
289
+ context.fillRect(
290
+ borderDrawableRect.origin.x,
291
+ borderDrawableRect.origin.y,
292
+ borderDrawableRect.size.width,
293
+ borderDrawableRect.size.height,
294
+ );
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * @private
301
+ */
302
+ _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
303
+ const {frame, _intrinsicSize, onHover, visibleArea} = this;
304
+ if (!onHover) {
305
+ return;
306
+ }
307
+
308
+ const {location} = interaction.payload;
309
+ if (!rectContainsPoint(location, visibleArea)) {
310
+ onHover(null);
311
+ return;
312
+ }
313
+
314
+ const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
315
+ const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
316
+
317
+ const adjustedCanvasMouseY = location.y - frame.origin.y;
318
+ const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
319
+ const suspenseEventsAtDepth = this._depthToSuspenseEvent.get(depth);
320
+
321
+ if (suspenseEventsAtDepth) {
322
+ // Find the event being hovered over.
323
+ for (let index = suspenseEventsAtDepth.length - 1; index >= 0; index--) {
324
+ const suspenseEvent = suspenseEventsAtDepth[index];
325
+ const {duration, timestamp} = suspenseEvent;
326
+
327
+ if (duration === null) {
328
+ const timestampAllowance = widthToDuration(
329
+ PENDING_SUSPENSE_EVENT_SIZE / 2,
330
+ scaleFactor,
331
+ );
332
+
333
+ if (
334
+ timestamp - timestampAllowance <= hoverTimestamp &&
335
+ hoverTimestamp <= timestamp + timestampAllowance
336
+ ) {
337
+ this.currentCursor = 'context-menu';
338
+
339
+ viewRefs.hoveredView = this;
340
+
341
+ onHover(suspenseEvent);
342
+ return;
343
+ }
344
+ } else if (
345
+ hoverTimestamp >= timestamp &&
346
+ hoverTimestamp <= timestamp + duration
347
+ ) {
348
+ this.currentCursor = 'context-menu';
349
+
350
+ viewRefs.hoveredView = this;
351
+
352
+ onHover(suspenseEvent);
353
+ return;
354
+ }
355
+ }
356
+ }
357
+
358
+ onHover(null);
359
+ }
360
+
361
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
362
+ switch (interaction.type) {
363
+ case 'mousemove':
364
+ this._handleMouseMove(interaction, viewRefs);
365
+ break;
366
+ }
367
+ }
368
+}
packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js
+4
-4
@@ -209,7 +209,7 @@ export class UserTimingMarksView extends View {
209
frame,
210
);
211
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
212
- const markTimestampAllowance = widthToDuration(
212
+ const timestampAllowance = widthToDuration(
213
USER_TIMING_MARK_SIZE / 2,
214
scaleFactor,
215
);
@@ -221,10 +221,10 @@ export class UserTimingMarksView extends View {
221
const {timestamp} = mark;
222
223
if (
224
- timestamp - markTimestampAllowance <= hoverTimestamp &&
225
- hoverTimestamp <= timestamp + markTimestampAllowance
224
+ timestamp - timestampAllowance <= hoverTimestamp &&
225
+ hoverTimestamp <= timestamp + timestampAllowance
226
) {
227
- this.currentCursor = 'pointer';
227
+ this.currentCursor = 'context-menu';
228
viewRefs.hoveredView = this;
229
onHover(mark);
230
return;
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
+58
-45
@@ -15,6 +15,8 @@ export const MARKER_TEXT_PADDING = 8;
15
export const COLOR_HOVER_DIM_DELTA = 5;
16
export const TOP_ROW_PADDING = 4;
17
export const NATIVE_EVENT_HEIGHT = 14;
18
+export const SUSPENSE_EVENT_HEIGHT = 14;
19
+export const PENDING_SUSPENSE_EVENT_SIZE = 8;
20
export const REACT_EVENT_DIAMETER = 6;
21
export const USER_TIMING_MARK_SIZE = 8;
22
export const REACT_MEASURE_HEIGHT = 9;
@@ -43,44 +45,51 @@ export let COLORS = {
45
BACKGROUND: '',
46
NATIVE_EVENT: '',
47
NATIVE_EVENT_HOVER: '',
46
- NATIVE_EVENT_WARNING: '',
47
- NATIVE_EVENT_WARNING_HOVER: '',
48
- NATIVE_EVENT_WARNING_TEXT: '',
48
PRIORITY_BACKGROUND: '',
49
PRIORITY_BORDER: '',
50
PRIORITY_LABEL: '',
51
USER_TIMING: '',
52
USER_TIMING_HOVER: '',
53
REACT_IDLE: '',
55
- REACT_IDLE_SELECTED: '',
54
REACT_IDLE_HOVER: '',
55
REACT_RENDER: '',
58
- REACT_RENDER_SELECTED: '',
56
REACT_RENDER_HOVER: '',
57
REACT_COMMIT: '',
61
- REACT_COMMIT_SELECTED: '',
58
REACT_COMMIT_HOVER: '',
59
REACT_LAYOUT_EFFECTS: '',
64
- REACT_LAYOUT_EFFECTS_SELECTED: '',
60
REACT_LAYOUT_EFFECTS_HOVER: '',
61
REACT_PASSIVE_EFFECTS: '',
67
- REACT_PASSIVE_EFFECTS_SELECTED: '',
62
REACT_PASSIVE_EFFECTS_HOVER: '',
63
REACT_RESIZE_BAR: '',
64
+ REACT_RESIZE_BAR_ACTIVE: '',
65
+ REACT_RESIZE_BAR_BORDER: '',
66
+ REACT_RESIZE_BAR_DOT: '',
67
REACT_SCHEDULE: '',
68
REACT_SCHEDULE_HOVER: '',
72
- REACT_SCHEDULE_CASCADING: '',
73
- REACT_SCHEDULE_CASCADING_HOVER: '',
74
- REACT_SUSPEND: '',
75
- REACT_SUSPEND_HOVER: '',
69
+ REACT_SUSPENSE_REJECTED_EVENT: '',
70
+ REACT_SUSPENSE_REJECTED_EVENT_HOVER: '',
71
+ REACT_SUSPENSE_RESOLVED_EVENT: '',
72
+ REACT_SUSPENSE_RESOLVED_EVENT_HOVER: '',
73
+ REACT_SUSPENSE_UNRESOLVED_EVENT: '',
74
+ REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: '',
75
REACT_WORK_BORDER: '',
76
+ SCROLL_CARET: '',
77
TEXT_COLOR: '',
78
TIME_MARKER_LABEL: '',
79
+ WARNING_BACKGROUND: '',
80
+ WARNING_BACKGROUND_HOVER: '',
81
+ WARNING_TEXT: '',
82
+ WARNING_TEXT_INVERED: '',
83
};
84
81
-export function updateColorsToMatchTheme(): void {
85
+export function updateColorsToMatchTheme(): boolean {
86
const computedStyle = getComputedStyle((document.body: any));
87
88
+ // Check to see if styles have been initialized...
89
+ if (computedStyle.getPropertyValue('--color-background') == null) {
90
+ return false;
91
+ }
92
+
93
COLORS = {
94
BACKGROUND: computedStyle.getPropertyValue('--color-background'),
95
NATIVE_EVENT: computedStyle.getPropertyValue(
@@ -89,15 +98,6 @@ export function updateColorsToMatchTheme(): void {
98
NATIVE_EVENT_HOVER: computedStyle.getPropertyValue(
99
'--color-scheduling-profiler-native-event-hover',
100
),
92
- NATIVE_EVENT_WARNING: computedStyle.getPropertyValue(
93
- '--color-scheduling-profiler-native-event-warning',
94
- ),
95
- NATIVE_EVENT_WARNING_HOVER: computedStyle.getPropertyValue(
96
- '--color-scheduling-profiler-native-event-warning-hover',
97
- ),
98
- NATIVE_EVENT_WARNING_TEXT: computedStyle.getPropertyValue(
99
- '--color-scheduling-profiler-native-event-warning-text',
100
- ),
101
PRIORITY_BACKGROUND: computedStyle.getPropertyValue(
102
'--color-scheduling-profiler-priority-background',
103
),
@@ -114,73 +114,86 @@ export function updateColorsToMatchTheme(): void {
114
REACT_IDLE: computedStyle.getPropertyValue(
115
'--color-scheduling-profiler-react-idle',
116
),
117
- REACT_IDLE_SELECTED: computedStyle.getPropertyValue(
118
- '--color-scheduling-profiler-react-idle-selected',
119
- ),
117
REACT_IDLE_HOVER: computedStyle.getPropertyValue(
118
'--color-scheduling-profiler-react-idle-hover',
119
),
120
REACT_RENDER: computedStyle.getPropertyValue(
121
'--color-scheduling-profiler-react-render',
122
),
126
- REACT_RENDER_SELECTED: computedStyle.getPropertyValue(
127
- '--color-scheduling-profiler-react-render-selected',
128
- ),
123
REACT_RENDER_HOVER: computedStyle.getPropertyValue(
124
'--color-scheduling-profiler-react-render-hover',
125
),
126
REACT_COMMIT: computedStyle.getPropertyValue(
127
'--color-scheduling-profiler-react-commit',
128
),
135
- REACT_COMMIT_SELECTED: computedStyle.getPropertyValue(
136
- '--color-scheduling-profiler-react-commit-selected',
137
- ),
129
REACT_COMMIT_HOVER: computedStyle.getPropertyValue(
130
'--color-scheduling-profiler-react-commit-hover',
131
),
132
REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue(
133
'--color-scheduling-profiler-react-layout-effects',
134
),
144
- REACT_LAYOUT_EFFECTS_SELECTED: computedStyle.getPropertyValue(
145
- '--color-scheduling-profiler-react-layout-effects-selected',
146
- ),
135
REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue(
136
'--color-scheduling-profiler-react-layout-effects-hover',
137
),
138
REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue(
139
'--color-scheduling-profiler-react-passive-effects',
140
),
153
- REACT_PASSIVE_EFFECTS_SELECTED: computedStyle.getPropertyValue(
154
- '--color-scheduling-profiler-react-passive-effects-selected',
155
- ),
141
REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue(
142
'--color-scheduling-profiler-react-passive-effects-hover',
143
),
144
REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'),
145
+ REACT_RESIZE_BAR_ACTIVE: computedStyle.getPropertyValue(
146
+ '--color-resize-bar-active',
147
+ ),
148
+ REACT_RESIZE_BAR_BORDER: computedStyle.getPropertyValue(
149
+ '--color-resize-bar-border',
150
+ ),
151
+ REACT_RESIZE_BAR_DOT: computedStyle.getPropertyValue(
152
+ '--color-resize-bar-dot',
153
+ ),
154
REACT_SCHEDULE: computedStyle.getPropertyValue(
155
'--color-scheduling-profiler-react-schedule',
156
),
157
REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue(
158
'--color-scheduling-profiler-react-schedule-hover',
159
),
166
- REACT_SCHEDULE_CASCADING: computedStyle.getPropertyValue(
167
- '--color-scheduling-profiler-react-schedule-cascading',
160
+ REACT_SUSPENSE_REJECTED_EVENT: computedStyle.getPropertyValue(
161
+ '--color-scheduling-profiler-react-suspense-rejected',
162
+ ),
163
+ REACT_SUSPENSE_REJECTED_EVENT_HOVER: computedStyle.getPropertyValue(
164
+ '--color-scheduling-profiler-react-suspense-rejected-hover',
165
+ ),
166
+ REACT_SUSPENSE_RESOLVED_EVENT: computedStyle.getPropertyValue(
167
+ '--color-scheduling-profiler-react-suspense-resolved',
168
),
169
- REACT_SCHEDULE_CASCADING_HOVER: computedStyle.getPropertyValue(
170
- '--color-scheduling-profiler-react-schedule-cascading-hover',
169
+ REACT_SUSPENSE_RESOLVED_EVENT_HOVER: computedStyle.getPropertyValue(
170
+ '--color-scheduling-profiler-react-suspense-resolved-hover',
171
),
172
- REACT_SUSPEND: computedStyle.getPropertyValue(
173
- '--color-scheduling-profiler-react-suspend',
172
+ REACT_SUSPENSE_UNRESOLVED_EVENT: computedStyle.getPropertyValue(
173
+ '--color-scheduling-profiler-react-suspense-unresolved',
174
),
175
- REACT_SUSPEND_HOVER: computedStyle.getPropertyValue(
176
- '--color-scheduling-profiler-react-suspend-hover',
175
+ REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: computedStyle.getPropertyValue(
176
+ '--color-scheduling-profiler-react-suspense-unresolved-hover',
177
),
178
REACT_WORK_BORDER: computedStyle.getPropertyValue(
179
'--color-scheduling-profiler-react-work-border',
180
),
181
+ SCROLL_CARET: computedStyle.getPropertyValue('--color-scroll-caret'),
182
TEXT_COLOR: computedStyle.getPropertyValue(
183
'--color-scheduling-profiler-text-color',
184
),
185
TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'),
186
+ WARNING_BACKGROUND: computedStyle.getPropertyValue(
187
+ '--color-warning-background',
188
+ ),
189
+ WARNING_BACKGROUND_HOVER: computedStyle.getPropertyValue(
190
+ '--color-warning-background-hover',
191
+ ),
192
+ WARNING_TEXT: computedStyle.getPropertyValue('--color-warning-text-color'),
193
+ WARNING_TEXT_INVERED: computedStyle.getPropertyValue(
194
+ '--color-warning-text-color-inverted',
195
+ ),
196
};
197
+
198
+ return true;
199
}
packages/react-devtools-scheduling-profiler/src/content-views/index.js
+2
-1
@@ -9,7 +9,8 @@
9
10
export * from './FlamechartView';
11
export * from './NativeEventsView';
12
-export * from './ReactEventsView';
12
export * from './ReactMeasuresView';
13
+export * from './SchedulingEventsView';
14
+export * from './SuspenseEventsView';
15
export * from './TimeAxisMarkersView';
16
export * from './UserTimingMarksView';
packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js
new
+98
@@ -0,0 +1,98 @@
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 {Rect} from '../../view-base';
11
+
12
+import {rectEqualToRect} from '../../view-base';
13
+import {COLORS, FONT_SIZE, TEXT_PADDING} from '../constants';
14
+
15
+const cachedTextWidths = new Map();
16
+
17
+export function trimText(
18
+ context: CanvasRenderingContext2D,
19
+ text: string,
20
+ width: number,
21
+): string | null {
22
+ for (let i = text.length - 1; i >= 0; i--) {
23
+ const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
24
+
25
+ let measuredWidth = cachedTextWidths.get(trimmedText);
26
+ if (measuredWidth == null) {
27
+ measuredWidth = context.measureText(trimmedText).width;
28
+ cachedTextWidths.set(trimmedText, measuredWidth);
29
+ }
30
+
31
+ if (measuredWidth <= width) {
32
+ return trimmedText;
33
+ }
34
+ }
35
+
36
+ return null;
37
+}
38
+
39
+export function drawText(
40
+ text: string,
41
+ context: CanvasRenderingContext2D,
42
+ fullRect: Rect,
43
+ drawableRect: Rect,
44
+ availableWidth: number,
45
+ textAlign: 'left' | 'center' = 'left',
46
+ fillStyle: string = COLORS.TEXT_COLOR,
47
+): void {
48
+ if (availableWidth > TEXT_PADDING * 2) {
49
+ context.textAlign = textAlign;
50
+ context.textBaseline = 'middle';
51
+ context.font = `${FONT_SIZE}px sans-serif`;
52
+
53
+ const {x, y} = fullRect.origin;
54
+
55
+ const trimmedName = trimText(
56
+ context,
57
+ text,
58
+ availableWidth - TEXT_PADDING * 2 + (x < 0 ? x : 0),
59
+ );
60
+
61
+ if (trimmedName !== null) {
62
+ context.fillStyle = fillStyle;
63
+
64
+ // Prevent text from visibly overflowing its container when clipped.
65
+ const textOverflowsViewableArea = !rectEqualToRect(
66
+ drawableRect,
67
+ fullRect,
68
+ );
69
+ if (textOverflowsViewableArea) {
70
+ context.save();
71
+ context.beginPath();
72
+ context.rect(
73
+ drawableRect.origin.x,
74
+ drawableRect.origin.y,
75
+ drawableRect.size.width,
76
+ drawableRect.size.height,
77
+ );
78
+ context.closePath();
79
+ context.clip();
80
+ }
81
+
82
+ let textX;
83
+ if (textAlign === 'center') {
84
+ textX = x + availableWidth / 2 + TEXT_PADDING - (x < 0 ? x : 0);
85
+ } else {
86
+ textX = x + TEXT_PADDING - (x < 0 ? x : 0);
87
+ }
88
+
89
+ const textY = y + fullRect.size.height / 2;
90
+
91
+ context.fillText(trimmedName, textX, textY);
92
+
93
+ if (textOverflowsViewableArea) {
94
+ context.restore();
95
+ }
96
+ }
97
+ }
98
+}
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+12
-9
@@ -208,8 +208,9 @@ describe(preprocessData, () => {
208
measures: [],
209
nativeEvents: [],
210
otherUserTimingMarks: [],
211
- reactEvents: [],
211
+ schedulingEvents: [],
212
startTime: 1,
213
+ suspenseEvents: [],
214
});
215
});
216
@@ -300,16 +301,17 @@ describe(preprocessData, () => {
301
],
302
nativeEvents: [],
303
otherUserTimingMarks: [],
303
- reactEvents: [
304
+ schedulingEvents: [
305
{
305
- componentStack: '',
306
laneLabels: [],
307
lanes: [9],
308
timestamp: 0.002,
309
type: 'schedule-render',
310
+ warning: null,
311
},
312
],
313
startTime: 1,
314
+ suspenseEvents: [],
315
});
316
});
317
@@ -372,16 +374,17 @@ describe(preprocessData, () => {
374
timestamp: 0.004,
375
},
376
],
375
- reactEvents: [
377
+ schedulingEvents: [
378
{
377
- componentStack: '',
379
laneLabels: ['Sync'],
380
lanes: [0],
381
timestamp: 0.005,
382
type: 'schedule-render',
383
+ warning: null,
384
},
385
],
386
startTime: 1,
387
+ suspenseEvents: [],
388
});
389
});
390
@@ -507,25 +510,25 @@ describe(preprocessData, () => {
510
timestamp: 0.004,
511
},
512
],
510
- reactEvents: [
513
+ schedulingEvents: [
514
{
512
- componentStack: '',
515
laneLabels: ['Default'],
516
lanes: [4],
517
timestamp: 0.005,
518
type: 'schedule-render',
519
+ warning: null,
520
},
521
{
522
componentName: 'App',
520
- componentStack: '',
521
- isCascading: false,
523
laneLabels: ['Default'],
524
lanes: [4],
525
timestamp: 0.013,
526
type: 'schedule-state-update',
527
+ warning: null,
528
},
529
],
530
startTime: 1,
531
+ suspenseEvents: [],
532
});
533
});
534
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+178
-108
@@ -20,6 +20,7 @@ import type {
20
ReactLane,
21
ReactMeasureType,
22
ReactProfilerData,
23
+ SuspenseEvent,
24
} from '../types';
25
26
import {REACT_TOTAL_NUM_LANES} from '../constants';
@@ -34,13 +35,25 @@ type MeasureStackElement = {|
35
|};
36
37
type ProcessorState = {|
37
- nextRenderShouldGenerateNewBatchID: boolean,
38
batchUID: BatchUID,
39
- uidCounter: BatchUID,
39
measureStack: MeasureStackElement[],
40
+ nativeEventStack: NativeEvent[],
41
+ nextRenderShouldGenerateNewBatchID: boolean,
42
+ uidCounter: BatchUID,
43
+ unresolvedSuspenseEvents: Map<string, SuspenseEvent>,
44
|};
45
43
-let nativeEventStack: Array<NativeEvent> = [];
46
+const NATIVE_EVENT_DURATION_THRESHOLD = 20;
47
+
48
+const WARNING_STRINGS = {
49
+ LONG_EVENT_HANDLER:
50
+ 'An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work.',
51
+ NESTED_UPDATE:
52
+ 'A nested update was scheduled during layout. These updates require React to re-render synchronously before the browser can paint.',
53
+ SUSPENDD_DURING_UPATE:
54
+ 'A component suspended during an update which caused a fallback to be shown. ' +
55
+ "Consider using the Transition API to avoid hiding components after they've been mounted.",
56
+};
57
58
// Exported for tests
59
export function getLanesFromTransportDecimalBitmask(
@@ -174,44 +187,54 @@ function processTimelineEvent(
187
const stackTrace = args.data.stackTrace;
188
if (stackTrace) {
189
const topFrame = stackTrace[stackTrace.length - 1];
177
- if (topFrame.url.includes('node_modules/react-dom')) {
190
+ if (topFrame.url.includes('/react-dom.')) {
191
// Filter out fake React events dispatched by invokeGuardedCallbackDev.
192
return;
193
}
194
}
195
}
196
184
- const timestamp = (ts - currentProfilerData.startTime) / 1000;
185
- const duration = event.dur / 1000;
186
-
187
- let depth = 0;
188
-
189
- while (nativeEventStack.length > 0) {
190
- const prevNativeEvent = nativeEventStack[nativeEventStack.length - 1];
191
- const prevStopTime =
192
- prevNativeEvent.timestamp + prevNativeEvent.duration;
193
-
194
- if (timestamp < prevStopTime) {
195
- depth = prevNativeEvent.depth + 1;
196
- break;
197
- } else {
198
- nativeEventStack.pop();
197
+ // Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
198
+ if (
199
+ type.startsWith('blur') ||
200
+ type.startsWith('click') ||
201
+ type.startsWith('focus') ||
202
+ type.startsWith('mouse') ||
203
+ type.startsWith('pointer')
204
+ ) {
205
+ const timestamp = (ts - currentProfilerData.startTime) / 1000;
206
+ const duration = event.dur / 1000;
207
+
208
+ let depth = 0;
209
+
210
+ while (state.nativeEventStack.length > 0) {
211
+ const prevNativeEvent =
212
+ state.nativeEventStack[state.nativeEventStack.length - 1];
213
+ const prevStopTime =
214
+ prevNativeEvent.timestamp + prevNativeEvent.duration;
215
+
216
+ if (timestamp < prevStopTime) {
217
+ depth = prevNativeEvent.depth + 1;
218
+ break;
219
+ } else {
220
+ state.nativeEventStack.pop();
221
+ }
222
}
200
- }
223
202
- const nativeEvent = {
203
- depth,
204
- duration,
205
- timestamp,
206
- type,
207
- warnings: null,
208
- };
224
+ const nativeEvent = {
225
+ depth,
226
+ duration,
227
+ timestamp,
228
+ type,
229
+ warning: null,
230
+ };
231
210
- currentProfilerData.nativeEvents.push(nativeEvent);
232
+ currentProfilerData.nativeEvents.push(nativeEvent);
233
212
- // Keep track of curent event in case future ones overlap.
213
- // We separate them into different vertical lanes in this case.
214
- nativeEventStack.push(nativeEvent);
234
+ // Keep track of curent event in case future ones overlap.
235
+ // We separate them into different vertical lanes in this case.
236
+ state.nativeEventStack.push(nativeEvent);
237
+ }
238
}
239
break;
240
case 'blink.user_timing':
@@ -219,92 +242,141 @@ function processTimelineEvent(
242
243
// React Events - schedule
244
if (name.startsWith('--schedule-render-')) {
222
- const [
223
- laneBitmaskString,
224
- laneLabels,
225
- ...splitComponentStack
226
- ] = name.substr(18).split('-');
227
- currentProfilerData.reactEvents.push({
245
+ const [laneBitmaskString, laneLabels] = name.substr(18).split('-');
246
+ currentProfilerData.schedulingEvents.push({
247
type: 'schedule-render',
248
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
249
laneLabels: laneLabels ? laneLabels.split(',') : [],
231
- componentStack: splitComponentStack.join('-'),
250
timestamp: startTime,
251
+ warning: null,
252
});
253
} else if (name.startsWith('--schedule-forced-update-')) {
235
- const [
236
- laneBitmaskString,
237
- laneLabels,
238
- componentName,
239
- ...splitComponentStack
240
- ] = name.substr(25).split('-');
241
- const isCascading = !!state.measureStack.find(
242
- ({type}) => type === 'commit',
243
- );
244
- currentProfilerData.reactEvents.push({
254
+ const [laneBitmaskString, laneLabels, componentName] = name
255
+ .substr(25)
256
+ .split('-');
257
+
258
+ let warning = null;
259
+ if (state.measureStack.find(({type}) => type === 'commit')) {
260
+ // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold.
261
+ warning = WARNING_STRINGS.NESTED_UPDATE;
262
+ }
263
+
264
+ currentProfilerData.schedulingEvents.push({
265
type: 'schedule-force-update',
266
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
267
laneLabels: laneLabels ? laneLabels.split(',') : [],
268
componentName,
249
- componentStack: splitComponentStack.join('-'),
269
timestamp: startTime,
251
- isCascading,
270
+ warning,
271
});
272
} else if (name.startsWith('--schedule-state-update-')) {
254
- const [
255
- laneBitmaskString,
256
- laneLabels,
257
- componentName,
258
- ...splitComponentStack
259
- ] = name.substr(24).split('-');
260
- const isCascading = !!state.measureStack.find(
261
- ({type}) => type === 'commit',
262
- );
263
- currentProfilerData.reactEvents.push({
273
+ const [laneBitmaskString, laneLabels, componentName] = name
274
+ .substr(24)
275
+ .split('-');
276
+
277
+ let warning = null;
278
+ if (state.measureStack.find(({type}) => type === 'commit')) {
279
+ // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold.
280
+ warning = WARNING_STRINGS.NESTED_UPDATE;
281
+ }
282
+
283
+ currentProfilerData.schedulingEvents.push({
284
type: 'schedule-state-update',
285
lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
286
laneLabels: laneLabels ? laneLabels.split(',') : [],
287
componentName,
268
- componentStack: splitComponentStack.join('-'),
288
timestamp: startTime,
270
- isCascading,
289
+ warning,
290
});
291
} // eslint-disable-line brace-style
292
293
// React Events - suspense
294
else if (name.startsWith('--suspense-suspend-')) {
276
- const [id, componentName, ...splitComponentStack] = name
277
- .substr(19)
278
- .split('-');
279
- currentProfilerData.reactEvents.push({
280
- type: 'suspense-suspend',
281
- id,
282
- componentName,
283
- componentStack: splitComponentStack.join('-'),
284
- timestamp: startTime,
295
+ const [id, componentName, ...rest] = name.substr(19).split('-');
296
+
297
+ // Older versions of the scheduling profiler data didn't contain phase or lane values.
298
+ let phase = null;
299
+ let warning = null;
300
+ if (rest.length === 3) {
301
+ switch (rest[0]) {
302
+ case 'mount':
303
+ case 'update':
304
+ phase = rest[0];
305
+ break;
306
+ }
307
+
308
+ if (phase === 'update') {
309
+ const laneLabels = rest[2];
310
+ // HACK This is a bit gross but the numeric lane value might change between render versions.
311
+ if (!laneLabels.includes('Transition')) {
312
+ warning = WARNING_STRINGS.SUSPENDD_DURING_UPATE;
313
+ }
314
+ }
315
+ }
316
+
317
+ const availableDepths = new Array(
318
+ state.unresolvedSuspenseEvents.size + 1,
319
+ ).fill(true);
320
+ state.unresolvedSuspenseEvents.forEach(({depth}) => {
321
+ availableDepths[depth] = false;
322
});
286
- } else if (name.startsWith('--suspense-resolved-')) {
287
- const [id, componentName, ...splitComponentStack] = name
288
- .substr(20)
289
- .split('-');
290
- currentProfilerData.reactEvents.push({
291
- type: 'suspense-resolved',
292
- id,
323
+
324
+ let depth = 0;
325
+ for (let i = 0; i < availableDepths.length; i++) {
326
+ if (availableDepths[i]) {
327
+ depth = i;
328
+ break;
329
+ }
330
+ }
331
+
332
+ // TODO (scheduling profiler) Maybe we should calculate depth in post,
333
+ // so unresolved Suspense requests don't take up space.
334
+ // We can't know if they'll be resolved or not at this point.
335
+ // We'll just give them a default (fake) duration width.
336
+
337
+ const suspenseEvent = {
338
componentName,
294
- componentStack: splitComponentStack.join('-'),
295
- timestamp: startTime,
296
- });
297
- } else if (name.startsWith('--suspense-rejected-')) {
298
- const [id, componentName, ...splitComponentStack] = name
299
- .substr(20)
300
- .split('-');
301
- currentProfilerData.reactEvents.push({
302
- type: 'suspense-rejected',
339
+ depth,
340
+ duration: null,
341
id,
304
- componentName,
305
- componentStack: splitComponentStack.join('-'),
342
+ phase,
343
+ resolution: 'unresolved',
344
+ resuspendTimestamps: null,
345
timestamp: startTime,
307
- });
346
+ type: 'suspense',
347
+ warning,
348
+ };
349
+
350
+ currentProfilerData.suspenseEvents.push(suspenseEvent);
351
+ state.unresolvedSuspenseEvents.set(id, suspenseEvent);
352
+ } else if (name.startsWith('--suspense-resuspend-')) {
353
+ const [id] = name.substr(21).split('-');
354
+ const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
355
+ if (suspenseEvent != null) {
356
+ if (suspenseEvent.resuspendTimestamps === null) {
357
+ suspenseEvent.resuspendTimestamps = [startTime];
358
+ } else {
359
+ suspenseEvent.resuspendTimestamps.push(startTime);
360
+ }
361
+ }
362
+ } else if (name.startsWith('--suspense-resolved-')) {
363
+ const [id] = name.substr(20).split('-');
364
+ const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
365
+ if (suspenseEvent != null) {
366
+ state.unresolvedSuspenseEvents.delete(id);
367
+
368
+ suspenseEvent.duration = startTime - suspenseEvent.timestamp;
369
+ suspenseEvent.resolution = 'resolved';
370
+ }
371
+ } else if (name.startsWith('--suspense-rejected-')) {
372
+ const [id] = name.substr(20).split('-');
373
+ const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
374
+ if (suspenseEvent != null) {
375
+ state.unresolvedSuspenseEvents.delete(id);
376
+
377
+ suspenseEvent.duration = startTime - suspenseEvent.timestamp;
378
+ suspenseEvent.resolution = 'rejected';
379
+ }
380
} // eslint-disable-line brace-style
381
382
// React Measures - render
@@ -335,17 +407,14 @@ function processTimelineEvent(
407
state,
408
);
409
338
- for (let i = 0; i < nativeEventStack.length; i++) {
339
- const nativeEvent = nativeEventStack[i];
410
+ for (let i = 0; i < state.nativeEventStack.length; i++) {
411
+ const nativeEvent = state.nativeEventStack[i];
412
const stopTime = nativeEvent.timestamp + nativeEvent.duration;
341
- if (stopTime > startTime) {
342
- const warning =
343
- 'An event handler scheduled a synchronous update with React.';
344
- if (nativeEvent.warnings === null) {
345
- nativeEvent.warnings = new Set([warning]);
346
- } else {
347
- nativeEvent.warnings.add(warning);
348
- }
413
+ if (
414
+ stopTime > startTime &&
415
+ nativeEvent.duration > NATIVE_EVENT_DURATION_THRESHOLD
416
+ ) {
417
+ nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER;
418
}
419
}
420
} else if (
@@ -509,18 +578,17 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
578
export default function preprocessData(
579
timeline: TimelineEvent[],
580
): ReactProfilerData {
512
- nativeEventStack = [];
513
-
581
const flamechart = preprocessFlamechart(timeline);
582
583
const profilerData: ReactProfilerData = {
517
- startTime: 0,
584
duration: 0,
519
- nativeEvents: [],
520
- reactEvents: [],
521
- measures: [],
585
flamechart,
586
+ measures: [],
587
+ nativeEvents: [],
588
otherUserTimingMarks: [],
589
+ schedulingEvents: [],
590
+ startTime: 0,
591
+ suspenseEvents: [],
592
};
593
594
// Sort `timeline`. JSON Array Format trace events need not be ordered. See:
@@ -551,9 +619,11 @@ export default function preprocessData(
619
620
const state: ProcessorState = {
621
batchUID: 0,
554
- uidCounter: 0,
555
- nextRenderShouldGenerateNewBatchID: true,
622
measureStack: [],
623
+ nativeEventStack: [],
624
+ nextRenderShouldGenerateNewBatchID: true,
625
+ uidCounter: 0,
626
+ unresolvedSuspenseEvents: new Map(),
627
};
628
629
timeline.forEach(event => processTimelineEvent(event, profilerData, state));
packages/react-devtools-scheduling-profiler/src/types.js
+25
-34
@@ -26,13 +26,13 @@ export type NativeEvent = {|
26
+duration: Milliseconds,
27
+timestamp: Milliseconds,
28
+type: string,
29
- warnings: Set<string> | null,
29
+ warning: string | null,
30
|};
31
32
type BaseReactEvent = {|
33
+componentName?: string,
34
- +componentStack?: string,
34
+timestamp: Milliseconds,
35
+ warning: string | null,
36
|};
37
38
type BaseReactScheduleEvent = {|
@@ -42,44 +42,33 @@ type BaseReactScheduleEvent = {|
42
|};
43
export type ReactScheduleRenderEvent = {|
44
...BaseReactScheduleEvent,
45
- type: 'schedule-render',
45
+ +type: 'schedule-render',
46
|};
47
export type ReactScheduleStateUpdateEvent = {|
48
...BaseReactScheduleEvent,
49
- type: 'schedule-state-update',
50
- isCascading: boolean,
49
+ +type: 'schedule-state-update',
50
|};
51
export type ReactScheduleForceUpdateEvent = {|
52
...BaseReactScheduleEvent,
54
- type: 'schedule-force-update',
55
- isCascading: boolean,
53
+ +type: 'schedule-force-update',
54
|};
55
58
-type BaseReactSuspenseEvent = {|
56
+export type SuspenseEvent = {|
57
...BaseReactEvent,
60
- id: string,
61
-|};
62
-export type ReactSuspenseSuspendEvent = {|
63
- ...BaseReactSuspenseEvent,
64
- type: 'suspense-suspend',
65
-|};
66
-export type ReactSuspenseResolvedEvent = {|
67
- ...BaseReactSuspenseEvent,
68
- type: 'suspense-resolved',
69
-|};
70
-export type ReactSuspenseRejectedEvent = {|
71
- ...BaseReactSuspenseEvent,
72
- type: 'suspense-rejected',
58
+ depth: number,
59
+ duration: number | null,
60
+ +id: string,
61
+ +phase: 'mount' | 'update' | null,
62
+ resolution: 'rejected' | 'resolved' | 'unresolved',
63
+ resuspendTimestamps: Array<number> | null,
64
+ +type: 'suspense',
65
|};
66
75
-export type ReactEvent =
67
+export type SchedulingEvent =
68
| ReactScheduleRenderEvent
69
| ReactScheduleStateUpdateEvent
78
- | ReactScheduleForceUpdateEvent
79
- | ReactSuspenseSuspendEvent
80
- | ReactSuspenseResolvedEvent
81
- | ReactSuspenseRejectedEvent;
82
-export type ReactEventType = $PropertyType<ReactEvent, 'type'>;
70
+ | ReactScheduleForceUpdateEvent;
71
+export type SchedulingEventType = $PropertyType<SchedulingEvent, 'type'>;
72
73
export type ReactMeasureType =
74
| 'commit'
@@ -128,20 +117,22 @@ export type FlamechartStackLayer = FlamechartStackFrame[];
117
export type Flamechart = FlamechartStackLayer[];
118
119
export type ReactProfilerData = {|
131
- startTime: number,
120
duration: number,
133
- nativeEvents: NativeEvent[],
134
- reactEvents: ReactEvent[],
135
- measures: ReactMeasure[],
121
flamechart: Flamechart,
122
+ measures: ReactMeasure[],
123
+ nativeEvents: NativeEvent[],
124
otherUserTimingMarks: UserTimingMark[],
125
+ schedulingEvents: SchedulingEvent[],
126
+ startTime: number,
127
+ suspenseEvents: SuspenseEvent[],
128
|};
129
130
export type ReactHoverContextInfo = {|
141
- nativeEvent: NativeEvent | null,
142
- reactEvent: ReactEvent | null,
143
- measure: ReactMeasure | null,
131
data: $ReadOnly<ReactProfilerData> | null,
132
flamechartStackFrame: FlamechartStackFrame | null,
133
+ measure: ReactMeasure | null,
134
+ nativeEvent: NativeEvent | null,
135
+ schedulingEvent: SchedulingEvent | null,
136
+ suspenseEvent: SuspenseEvent | null,
137
userTimingMark: UserTimingMark | null,
138
|};
packages/react-devtools-scheduling-profiler/src/utils/formatting.js
new
+30
@@ -0,0 +1,30 @@
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 prettyMilliseconds from 'pretty-ms';
11
+
12
+export function formatTimestamp(ms: number) {
13
+ return (
14
+ ms.toLocaleString(undefined, {
15
+ minimumFractionDigits: 1,
16
+ maximumFractionDigits: 1,
17
+ }) + 'ms'
18
+ );
19
+}
20
+
21
+export function formatDuration(ms: number) {
22
+ return prettyMilliseconds(ms, {millisecondsDecimalDigits: 1});
23
+}
24
+
25
+export function trimString(string: string, length: number): string {
26
+ if (string.length > length) {
27
+ return `${string.substr(0, length - 1)}…`;
28
+ }
29
+ return string;
30
+}
packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js
+10
-9
@@ -9,7 +9,8 @@
9
10
import {useLayoutEffect, useRef} from 'react';
11
12
-const TOOLTIP_OFFSET = 4;
12
+const TOOLTIP_OFFSET_BOTTOM = 10;
13
+const TOOLTIP_OFFSET_TOP = 5;
14
15
export default function useSmartTooltip({
16
canvasRef,
@@ -37,41 +38,41 @@ export default function useSmartTooltip({
38
const element = ref.current;
39
if (element !== null) {
40
// Let's check the vertical position.
40
- if (mouseY + TOOLTIP_OFFSET + element.offsetHeight >= height) {
41
+ if (mouseY + TOOLTIP_OFFSET_BOTTOM + element.offsetHeight >= height) {
42
// The tooltip doesn't fit below the mouse cursor (which is our
43
// default strategy). Therefore we try to position it either above the
44
// mouse cursor or finally aligned with the window's top edge.
44
- if (mouseY - TOOLTIP_OFFSET - element.offsetHeight > 0) {
45
+ if (mouseY - TOOLTIP_OFFSET_TOP - element.offsetHeight > 0) {
46
// We position the tooltip above the mouse cursor if it fits there.
47
element.style.top = `${mouseY -
48
element.offsetHeight -
48
- TOOLTIP_OFFSET}px`;
49
+ TOOLTIP_OFFSET_TOP}px`;
50
} else {
51
// Otherwise we align the tooltip with the window's top edge.
52
element.style.top = '0px';
53
}
54
} else {
54
- element.style.top = `${mouseY + TOOLTIP_OFFSET}px`;
55
+ element.style.top = `${mouseY + TOOLTIP_OFFSET_BOTTOM}px`;
56
}
57
58
// Now let's check the horizontal position.
58
- if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= width) {
59
+ if (mouseX + TOOLTIP_OFFSET_BOTTOM + element.offsetWidth >= width) {
60
// The tooltip doesn't fit at the right of the mouse cursor (which is
61
// our default strategy). Therefore we try to position it either at the
62
// left of the mouse cursor or finally aligned with the window's left
63
// edge.
63
- if (mouseX - TOOLTIP_OFFSET - element.offsetWidth > 0) {
64
+ if (mouseX - TOOLTIP_OFFSET_TOP - element.offsetWidth > 0) {
65
// We position the tooltip at the left of the mouse cursor if it fits
66
// there.
67
element.style.left = `${mouseX -
68
element.offsetWidth -
68
- TOOLTIP_OFFSET}px`;
69
+ TOOLTIP_OFFSET_TOP}px`;
70
} else {
71
// Otherwise, align the tooltip with the window's left edge.
72
element.style.left = '0px';
73
}
74
} else {
74
- element.style.left = `${mouseX + TOOLTIP_OFFSET}px`;
75
+ element.style.left = `${mouseX + TOOLTIP_OFFSET_BOTTOM}px`;
76
}
77
}
78
}, [mouseX, mouseY, ref]);
packages/react-devtools-scheduling-profiler/src/view-base/BackgroundColorView.js
renamed
+5
-21
@@ -7,33 +7,17 @@
7
* @flow
8
*/
9
10
-import type {Rect} from './geometry';
11
-
12
-import {Surface} from './Surface';
10
import {View} from './View';
11
+import {COLORS} from '../content-views/constants';
12
13
/**
14
* View that fills its visible area with a CSS color.
15
*/
18
-export class ColorView extends View {
19
- _color: string;
20
-
21
- constructor(surface: Surface, frame: Rect, color: string) {
22
- super(surface, frame);
23
- this._color = color;
24
- }
25
-
26
- setColor(color: string) {
27
- if (this._color === color) {
28
- return;
29
- }
30
- this._color = color;
31
- this.setNeedsDisplay();
32
- }
33
-
16
+export class BackgroundColorView extends View {
17
draw(context: CanvasRenderingContext2D) {
35
- const {_color, visibleArea} = this;
36
- context.fillStyle = _color;
18
+ const {visibleArea} = this;
19
+
20
+ context.fillStyle = COLORS.BACKGROUND;
21
context.fillRect(
22
visibleArea.origin.x,
23
visibleArea.origin.y,
packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
+35
-51
@@ -14,8 +14,6 @@ import type {
14
MouseUpInteraction,
15
WheelPlainInteraction,
16
WheelWithShiftInteraction,
17
- WheelWithControlInteraction,
18
- WheelWithMetaInteraction,
17
} from './useCanvasInteraction';
18
import type {Rect} from './geometry';
19
import type {ScrollState} from './utils/scrollState';
@@ -202,7 +200,7 @@ export class HorizontalPanAndZoomView extends View {
200
}
201
}
202
205
- _handleWheelPlain(interaction: WheelPlainInteraction) {
203
+ _handleWheel(interaction: WheelPlainInteraction | WheelWithShiftInteraction) {
204
const {
205
location,
206
delta: {deltaX, deltaY},
@@ -214,51 +212,41 @@ export class HorizontalPanAndZoomView extends View {
212
213
const absDeltaX = Math.abs(deltaX);
214
const absDeltaY = Math.abs(deltaY);
217
- if (absDeltaY > absDeltaX) {
218
- return; // Scrolling vertically
219
- }
220
- if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
221
- return;
222
- }
223
-
224
- const newState = translateState({
225
- state: this._scrollState,
226
- delta: -deltaX,
227
- containerLength: this.frame.size.width,
228
- });
229
- this._setStateAndInformCallbacksIfChanged(newState);
230
- }
231
-
232
- _handleWheelZoom(
233
- interaction:
234
- | WheelWithShiftInteraction
235
- | WheelWithControlInteraction
236
- | WheelWithMetaInteraction,
237
- ) {
238
- const {
239
- location,
240
- delta: {deltaY},
241
- } = interaction.payload;
215
243
- if (!rectContainsPoint(location, this.frame)) {
244
- return; // Not scrolling on view
245
- }
246
-
247
- const absDeltaY = Math.abs(deltaY);
248
- if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
249
- return;
216
+ // Vertical scrolling zooms in and out (unless the SHIFT modifier is used).
217
+ // Horizontal scrolling pans.
218
+ if (absDeltaY > absDeltaX) {
219
+ if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
220
+ return;
221
+ }
222
+
223
+ if (interaction.type === 'wheel-shift') {
224
+ // Shift modifier is for scrolling, not zooming.
225
+ return;
226
+ }
227
+
228
+ const newState = zoomState({
229
+ state: this._scrollState,
230
+ multiplier: 1 + 0.005 * -deltaY,
231
+ fixedPoint: location.x - this._scrollState.offset,
232
+
233
+ minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
234
+ maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
235
+ containerLength: this.frame.size.width,
236
+ });
237
+ this._setStateAndInformCallbacksIfChanged(newState);
238
+ } else {
239
+ if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
240
+ return;
241
+ }
242
+
243
+ const newState = translateState({
244
+ state: this._scrollState,
245
+ delta: -deltaX,
246
+ containerLength: this.frame.size.width,
247
+ });
248
+ this._setStateAndInformCallbacksIfChanged(newState);
249
}
251
-
252
- const newState = zoomState({
253
- state: this._scrollState,
254
- multiplier: 1 + 0.005 * -deltaY,
255
- fixedPoint: location.x - this._scrollState.offset,
256
-
257
- minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
258
- maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
259
- containerLength: this.frame.size.width,
260
- });
261
- this._setStateAndInformCallbacksIfChanged(newState);
250
}
251
252
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
@@ -273,12 +261,8 @@ export class HorizontalPanAndZoomView extends View {
261
this._handleMouseUp(interaction, viewRefs);
262
break;
263
case 'wheel-plain':
276
- this._handleWheelPlain(interaction);
277
- break;
264
case 'wheel-shift':
279
- case 'wheel-control':
280
- case 'wheel-meta':
281
- this._handleWheelZoom(interaction);
265
+ this._handleWheel(interaction);
266
break;
267
}
268
}
packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js
deleted
-339
@@ -1,339 +0,0 @@
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 {
11
- Interaction,
12
- MouseDownInteraction,
13
- MouseMoveInteraction,
14
- MouseUpInteraction,
15
-} from './useCanvasInteraction';
16
-import type {Rect, Size} from './geometry';
17
-import type {ViewRefs} from './Surface';
18
-
19
-import {COLORS} from '../content-views/constants';
20
-import nullthrows from 'nullthrows';
21
-import {Surface} from './Surface';
22
-import {View} from './View';
23
-import {rectContainsPoint} from './geometry';
24
-import {layeredLayout, noopLayout} from './layouter';
25
-import {ColorView} from './ColorView';
26
-import {clamp} from './utils/clamp';
27
-
28
-type ResizeBarState = 'normal' | 'hovered' | 'dragging';
29
-
30
-type ResizingState = $ReadOnly<{|
31
- /** Distance between top of resize bar and mouseY */
32
- cursorOffsetInBarFrame: number,
33
- /** Mouse's vertical coordinates relative to canvas */
34
- mouseY: number,
35
-|}>;
36
-
37
-type LayoutState = $ReadOnly<{|
38
- /** Resize bar's vertical position relative to resize view's frame.origin.y */
39
- barOffsetY: number,
40
-|}>;
41
-
42
-function getColorForBarState(state: ResizeBarState): string {
43
- switch (state) {
44
- case 'normal':
45
- case 'hovered':
46
- case 'dragging':
47
- return COLORS.REACT_RESIZE_BAR;
48
- }
49
- throw new Error(`Unknown resize bar state ${state}`);
50
-}
51
-
52
-class ResizeBar extends View {
53
- _intrinsicContentSize: Size = {
54
- width: 0,
55
- height: 5,
56
- };
57
-
58
- _interactionState: ResizeBarState = 'normal';
59
-
60
- constructor(surface: Surface, frame: Rect) {
61
- super(surface, frame, layeredLayout);
62
- this.addSubview(new ColorView(surface, frame, ''));
63
- this._updateColor();
64
- }
65
-
66
- desiredSize() {
67
- return this._intrinsicContentSize;
68
- }
69
-
70
- _getColorView(): ColorView {
71
- return (this.subviews[0]: any);
72
- }
73
-
74
- _updateColor() {
75
- this._getColorView().setColor(getColorForBarState(this._interactionState));
76
- }
77
-
78
- _setInteractionState(state: ResizeBarState) {
79
- if (this._interactionState === state) {
80
- return;
81
- }
82
- this._interactionState = state;
83
- this._updateColor();
84
- }
85
-
86
- _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
87
- const cursorInView = rectContainsPoint(
88
- interaction.payload.location,
89
- this.frame,
90
- );
91
- if (cursorInView) {
92
- this._setInteractionState('dragging');
93
- viewRefs.activeView = this;
94
- }
95
- }
96
-
97
- _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
98
- const cursorInView = rectContainsPoint(
99
- interaction.payload.location,
100
- this.frame,
101
- );
102
-
103
- if (cursorInView || viewRefs.activeView === this) {
104
- this.currentCursor = 'ns-resize';
105
- }
106
- if (cursorInView) {
107
- viewRefs.hoveredView = this;
108
- }
109
-
110
- if (this._interactionState === 'dragging') {
111
- return;
112
- }
113
- this._setInteractionState(cursorInView ? 'hovered' : 'normal');
114
- }
115
-
116
- _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
117
- const cursorInView = rectContainsPoint(
118
- interaction.payload.location,
119
- this.frame,
120
- );
121
- if (this._interactionState === 'dragging') {
122
- this._setInteractionState(cursorInView ? 'hovered' : 'normal');
123
- }
124
-
125
- if (viewRefs.activeView === this) {
126
- viewRefs.activeView = null;
127
- }
128
- }
129
-
130
- handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
131
- switch (interaction.type) {
132
- case 'mousedown':
133
- this._handleMouseDown(interaction, viewRefs);
134
- return;
135
- case 'mousemove':
136
- this._handleMouseMove(interaction, viewRefs);
137
- return;
138
- case 'mouseup':
139
- this._handleMouseUp(interaction, viewRefs);
140
- return;
141
- }
142
- }
143
-}
144
-
145
-export class ResizableSplitView extends View {
146
- _canvasRef: {current: HTMLCanvasElement | null};
147
- _resizingState: ResizingState | null = null;
148
- _layoutState: LayoutState;
149
-
150
- constructor(
151
- surface: Surface,
152
- frame: Rect,
153
- topSubview: View,
154
- bottomSubview: View,
155
- canvasRef: {current: HTMLCanvasElement | null},
156
- ) {
157
- super(surface, frame, noopLayout);
158
-
159
- this._canvasRef = canvasRef;
160
-
161
- this.addSubview(topSubview);
162
- this.addSubview(new ResizeBar(surface, frame));
163
- this.addSubview(bottomSubview);
164
-
165
- const topSubviewDesiredSize = topSubview.desiredSize();
166
- this._layoutState = {
167
- barOffsetY: topSubviewDesiredSize ? topSubviewDesiredSize.height : 0,
168
- };
169
- }
170
-
171
- _getTopSubview(): View {
172
- return this.subviews[0];
173
- }
174
-
175
- _getResizeBar(): View {
176
- return this.subviews[1];
177
- }
178
-
179
- _getBottomSubview(): View {
180
- return this.subviews[2];
181
- }
182
-
183
- _getResizeBarDesiredSize(): Size {
184
- return nullthrows(
185
- this._getResizeBar().desiredSize(),
186
- 'Resize bar must have desired size',
187
- );
188
- }
189
-
190
- desiredSize() {
191
- const topSubviewDesiredSize = this._getTopSubview().desiredSize();
192
- const resizeBarDesiredSize = this._getResizeBarDesiredSize();
193
- const bottomSubviewDesiredSize = this._getBottomSubview().desiredSize();
194
-
195
- const topSubviewDesiredWidth = topSubviewDesiredSize
196
- ? topSubviewDesiredSize.width
197
- : 0;
198
- const bottomSubviewDesiredWidth = bottomSubviewDesiredSize
199
- ? bottomSubviewDesiredSize.width
200
- : 0;
201
-
202
- const topSubviewDesiredHeight = topSubviewDesiredSize
203
- ? topSubviewDesiredSize.height
204
- : 0;
205
- const bottomSubviewDesiredHeight = bottomSubviewDesiredSize
206
- ? bottomSubviewDesiredSize.height
207
- : 0;
208
-
209
- return {
210
- width: Math.max(
211
- topSubviewDesiredWidth,
212
- resizeBarDesiredSize.width,
213
- bottomSubviewDesiredWidth,
214
- ),
215
- height:
216
- topSubviewDesiredHeight +
217
- resizeBarDesiredSize.height +
218
- bottomSubviewDesiredHeight,
219
- };
220
- }
221
-
222
- layoutSubviews() {
223
- this._updateLayoutState();
224
- this._updateSubviewFrames();
225
- super.layoutSubviews();
226
- }
227
-
228
- _updateLayoutState() {
229
- const {frame, visibleArea, _resizingState} = this;
230
-
231
- const resizeBarDesiredSize = this._getResizeBarDesiredSize();
232
- // Allow bar to travel to bottom of the visible area of this view but no further
233
- const maxPossibleBarOffset =
234
- visibleArea.size.height - resizeBarDesiredSize.height;
235
- const topSubviewDesiredSize = this._getTopSubview().desiredSize();
236
- const maxBarOffset = topSubviewDesiredSize
237
- ? Math.min(maxPossibleBarOffset, topSubviewDesiredSize.height)
238
- : maxPossibleBarOffset;
239
-
240
- let proposedBarOffsetY = this._layoutState.barOffsetY;
241
- // Update bar offset if dragging bar
242
- if (_resizingState) {
243
- const {mouseY, cursorOffsetInBarFrame} = _resizingState;
244
- proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame;
245
- }
246
-
247
- this._layoutState = {
248
- ...this._layoutState,
249
- barOffsetY: clamp(0, maxBarOffset, proposedBarOffsetY),
250
- };
251
- }
252
-
253
- _updateSubviewFrames() {
254
- const {
255
- frame: {
256
- origin: {x, y},
257
- size: {width, height},
258
- },
259
- _layoutState: {barOffsetY},
260
- } = this;
261
-
262
- const resizeBarDesiredSize = this._getResizeBarDesiredSize();
263
-
264
- let currentY = y;
265
-
266
- this._getTopSubview().setFrame({
267
- origin: {x, y: currentY},
268
- size: {width, height: barOffsetY},
269
- });
270
- currentY += this._getTopSubview().frame.size.height;
271
-
272
- this._getResizeBar().setFrame({
273
- origin: {x, y: currentY},
274
- size: {width, height: resizeBarDesiredSize.height},
275
- });
276
- currentY += this._getResizeBar().frame.size.height;
277
-
278
- this._getBottomSubview().setFrame({
279
- origin: {x, y: currentY},
280
- // Fill remaining height
281
- size: {width, height: height + y - currentY},
282
- });
283
- }
284
-
285
- _handleMouseDown(interaction: MouseDownInteraction) {
286
- const cursorLocation = interaction.payload.location;
287
- const resizeBarFrame = this._getResizeBar().frame;
288
- if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
289
- const mouseY = cursorLocation.y;
290
- this._resizingState = {
291
- cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
292
- mouseY,
293
- };
294
- }
295
- }
296
-
297
- _handleMouseMove(interaction: MouseMoveInteraction) {
298
- const {_resizingState} = this;
299
- if (_resizingState) {
300
- this._resizingState = {
301
- ..._resizingState,
302
- mouseY: interaction.payload.location.y,
303
- };
304
- this.setNeedsDisplay();
305
- }
306
- }
307
-
308
- _handleMouseUp(interaction: MouseUpInteraction) {
309
- if (this._resizingState) {
310
- this._resizingState = null;
311
- }
312
- }
313
-
314
- _didGrab: boolean = false;
315
-
316
- getCursorActiveSubView(interaction: Interaction): View | null {
317
- const cursorLocation = interaction.payload.location;
318
- const resizeBarFrame = this._getResizeBar().frame;
319
- if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
320
- return this;
321
- } else {
322
- return null;
323
- }
324
- }
325
-
326
- handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
327
- switch (interaction.type) {
328
- case 'mousedown':
329
- this._handleMouseDown(interaction);
330
- return;
331
- case 'mousemove':
332
- this._handleMouseMove(interaction);
333
- return;
334
- case 'mouseup':
335
- this._handleMouseUp(interaction);
336
- return;
337
- }
338
- }
339
-}
packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
new
+418
@@ -0,0 +1,418 @@
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 {
11
+ ClickInteraction,
12
+ DoubleClickInteraction,
13
+ Interaction,
14
+ MouseDownInteraction,
15
+ MouseMoveInteraction,
16
+ MouseUpInteraction,
17
+} from './useCanvasInteraction';
18
+import type {Rect} from './geometry';
19
+import type {ViewRefs} from './Surface';
20
+
21
+import {BORDER_SIZE, COLORS} from '../content-views/constants';
22
+import {drawText} from '../content-views/utils/text';
23
+import {Surface} from './Surface';
24
+import {View} from './View';
25
+import {intersectionOfRects, rectContainsPoint} from './geometry';
26
+import {noopLayout} from './layouter';
27
+import {clamp} from './utils/clamp';
28
+
29
+type ResizeBarState = 'normal' | 'hovered' | 'dragging';
30
+
31
+type ResizingState = $ReadOnly<{|
32
+ /** Distance between top of resize bar and mouseY */
33
+ cursorOffsetInBarFrame: number,
34
+ /** Mouse's vertical coordinates relative to canvas */
35
+ mouseY: number,
36
+|}>;
37
+
38
+type LayoutState = $ReadOnly<{|
39
+ /** Resize bar's vertical position relative to resize view's frame.origin.y */
40
+ barOffsetY: number,
41
+|}>;
42
+
43
+const RESIZE_BAR_DOT_RADIUS = 1;
44
+const RESIZE_BAR_DOT_SPACING = 4;
45
+const RESIZE_BAR_HEIGHT = 8;
46
+const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
47
+
48
+const HIDDEN_RECT = {
49
+ origin: {x: 0, y: 0},
50
+ size: {width: 0, height: 0},
51
+};
52
+
53
+class ResizeBar extends View {
54
+ _interactionState: ResizeBarState = 'normal';
55
+ _label: string;
56
+
57
+ showLabel: boolean = false;
58
+
59
+ constructor(surface: Surface, frame: Rect, label: string) {
60
+ super(surface, frame, noopLayout);
61
+
62
+ this._label = label;
63
+ }
64
+
65
+ desiredSize() {
66
+ return this.showLabel
67
+ ? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0}
68
+ : {height: RESIZE_BAR_HEIGHT, width: 0};
69
+ }
70
+
71
+ draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
72
+ const {visibleArea} = this;
73
+ const {x, y} = visibleArea.origin;
74
+ const {width, height} = visibleArea.size;
75
+
76
+ const isActive =
77
+ this._interactionState === 'dragging' ||
78
+ (this._interactionState === 'hovered' && viewRefs.activeView === null);
79
+
80
+ context.fillStyle = isActive
81
+ ? COLORS.REACT_RESIZE_BAR_ACTIVE
82
+ : COLORS.REACT_RESIZE_BAR;
83
+ context.fillRect(x, y, width, height);
84
+
85
+ context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
86
+ context.fillRect(x, y, width, BORDER_SIZE);
87
+ context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE);
88
+
89
+ const horizontalCenter = x + width / 2;
90
+ const verticalCenter = y + height / 2;
91
+
92
+ if (this.showLabel) {
93
+ // When the resize view is collapsed entirely,
94
+ // rather than showing a resize bar– this view displays a label.
95
+ const labelRect: Rect = {
96
+ origin: {
97
+ x: 0,
98
+ y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT,
99
+ },
100
+ size: {
101
+ width: visibleArea.size.width,
102
+ height: visibleArea.size.height,
103
+ },
104
+ };
105
+
106
+ const drawableRect = intersectionOfRects(labelRect, this.visibleArea);
107
+
108
+ drawText(
109
+ this._label,
110
+ context,
111
+ labelRect,
112
+ drawableRect,
113
+ visibleArea.size.width,
114
+ 'center',
115
+ COLORS.REACT_RESIZE_BAR_DOT,
116
+ );
117
+ } else {
118
+ // Otherwise draw horizontally centered resize bar dots
119
+ context.beginPath();
120
+ context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT;
121
+ context.arc(
122
+ horizontalCenter,
123
+ verticalCenter,
124
+ RESIZE_BAR_DOT_RADIUS,
125
+ 0,
126
+ 2 * Math.PI,
127
+ );
128
+ context.arc(
129
+ horizontalCenter + RESIZE_BAR_DOT_SPACING,
130
+ verticalCenter,
131
+ RESIZE_BAR_DOT_RADIUS,
132
+ 0,
133
+ 2 * Math.PI,
134
+ );
135
+ context.arc(
136
+ horizontalCenter - RESIZE_BAR_DOT_SPACING,
137
+ verticalCenter,
138
+ RESIZE_BAR_DOT_RADIUS,
139
+ 0,
140
+ 2 * Math.PI,
141
+ );
142
+ context.fill();
143
+ }
144
+ }
145
+
146
+ _setInteractionState(state: ResizeBarState) {
147
+ if (this._interactionState === state) {
148
+ return;
149
+ }
150
+ this._interactionState = state;
151
+ this.setNeedsDisplay();
152
+ }
153
+
154
+ _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
155
+ const cursorInView = rectContainsPoint(
156
+ interaction.payload.location,
157
+ this.frame,
158
+ );
159
+ if (cursorInView) {
160
+ this._setInteractionState('dragging');
161
+ viewRefs.activeView = this;
162
+ }
163
+ }
164
+
165
+ _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
166
+ const cursorInView = rectContainsPoint(
167
+ interaction.payload.location,
168
+ this.frame,
169
+ );
170
+
171
+ if (viewRefs.activeView === this) {
172
+ // If we're actively dragging this resize bar,
173
+ // show the cursor even if the pointer isn't hovering over this view.
174
+ this.currentCursor = 'ns-resize';
175
+ } else if (cursorInView) {
176
+ if (this.showLabel) {
177
+ this.currentCursor = 'pointer';
178
+ } else {
179
+ this.currentCursor = 'ns-resize';
180
+ }
181
+ }
182
+
183
+ if (cursorInView) {
184
+ viewRefs.hoveredView = this;
185
+ }
186
+
187
+ if (this._interactionState === 'dragging') {
188
+ return;
189
+ }
190
+ this._setInteractionState(cursorInView ? 'hovered' : 'normal');
191
+ }
192
+
193
+ _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
194
+ const cursorInView = rectContainsPoint(
195
+ interaction.payload.location,
196
+ this.frame,
197
+ );
198
+ if (this._interactionState === 'dragging') {
199
+ this._setInteractionState(cursorInView ? 'hovered' : 'normal');
200
+ }
201
+
202
+ if (viewRefs.activeView === this) {
203
+ viewRefs.activeView = null;
204
+ }
205
+ }
206
+
207
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
208
+ switch (interaction.type) {
209
+ case 'mousedown':
210
+ this._handleMouseDown(interaction, viewRefs);
211
+ return;
212
+ case 'mousemove':
213
+ this._handleMouseMove(interaction, viewRefs);
214
+ return;
215
+ case 'mouseup':
216
+ this._handleMouseUp(interaction, viewRefs);
217
+ return;
218
+ }
219
+ }
220
+}
221
+
222
+export class ResizableView extends View {
223
+ _canvasRef: {current: HTMLCanvasElement | null};
224
+ _layoutState: LayoutState;
225
+ _resizeBar: ResizeBar;
226
+ _resizingState: ResizingState | null = null;
227
+ _subview: View;
228
+
229
+ constructor(
230
+ surface: Surface,
231
+ frame: Rect,
232
+ subview: View,
233
+ canvasRef: {current: HTMLCanvasElement | null},
234
+ label: string,
235
+ ) {
236
+ super(surface, frame, noopLayout);
237
+
238
+ this._canvasRef = canvasRef;
239
+
240
+ this._subview = subview;
241
+ this._resizeBar = new ResizeBar(surface, frame, label);
242
+
243
+ this.addSubview(this._subview);
244
+ this.addSubview(this._resizeBar);
245
+
246
+ const subviewDesiredSize = subview.desiredSize();
247
+ this._updateLayoutStateAndResizeBar(
248
+ subviewDesiredSize.maxInitialHeight != null
249
+ ? Math.min(
250
+ subviewDesiredSize.maxInitialHeight,
251
+ subviewDesiredSize.height,
252
+ )
253
+ : subviewDesiredSize.height,
254
+ );
255
+ }
256
+
257
+ desiredSize() {
258
+ const resizeBarDesiredSize = this._resizeBar.desiredSize();
259
+
260
+ return {
261
+ width: this.frame.size.width,
262
+ height: this._layoutState.barOffsetY + resizeBarDesiredSize.height,
263
+ };
264
+ }
265
+
266
+ layoutSubviews() {
267
+ this._updateLayoutState();
268
+ this._updateSubviewFrames();
269
+
270
+ super.layoutSubviews();
271
+ }
272
+
273
+ _updateLayoutStateAndResizeBar(barOffsetY: number) {
274
+ if (barOffsetY <= RESIZE_BAR_WITH_LABEL_HEIGHT - RESIZE_BAR_HEIGHT) {
275
+ barOffsetY = 0;
276
+ }
277
+
278
+ this._layoutState = {
279
+ ...this._layoutState,
280
+ barOffsetY,
281
+ };
282
+
283
+ this._resizeBar.showLabel = barOffsetY === 0;
284
+ }
285
+
286
+ _updateLayoutState() {
287
+ const {frame, _resizingState} = this;
288
+
289
+ // Allow bar to travel to bottom of the visible area of this view but no further
290
+ const subviewDesiredSize = this._subview.desiredSize();
291
+ const maxBarOffset = subviewDesiredSize.height;
292
+
293
+ let proposedBarOffsetY = this._layoutState.barOffsetY;
294
+ // Update bar offset if dragging bar
295
+ if (_resizingState) {
296
+ const {mouseY, cursorOffsetInBarFrame} = _resizingState;
297
+ proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame;
298
+ }
299
+
300
+ this._updateLayoutStateAndResizeBar(
301
+ clamp(0, maxBarOffset, proposedBarOffsetY),
302
+ );
303
+ }
304
+
305
+ _updateSubviewFrames() {
306
+ const {
307
+ frame: {
308
+ origin: {x, y},
309
+ size: {width},
310
+ },
311
+ _layoutState: {barOffsetY},
312
+ } = this;
313
+
314
+ const resizeBarDesiredSize = this._resizeBar.desiredSize();
315
+
316
+ if (barOffsetY === 0) {
317
+ this._subview.setFrame(HIDDEN_RECT);
318
+ } else {
319
+ this._subview.setFrame({
320
+ origin: {x, y},
321
+ size: {width, height: barOffsetY},
322
+ });
323
+ }
324
+
325
+ this._resizeBar.setFrame({
326
+ origin: {x, y: y + barOffsetY},
327
+ size: {width, height: resizeBarDesiredSize.height},
328
+ });
329
+ }
330
+
331
+ _handleClick(interaction: ClickInteraction) {
332
+ const cursorInView = rectContainsPoint(
333
+ interaction.payload.location,
334
+ this.frame,
335
+ );
336
+ if (cursorInView) {
337
+ if (this._layoutState.barOffsetY === 0) {
338
+ // Clicking on the collapsed label should expand.
339
+ const subviewDesiredSize = this._subview.desiredSize();
340
+ this._updateLayoutStateAndResizeBar(subviewDesiredSize.height);
341
+ this.setNeedsDisplay();
342
+ }
343
+ }
344
+ }
345
+
346
+ _handleDoubleClick(interaction: DoubleClickInteraction) {
347
+ const cursorInView = rectContainsPoint(
348
+ interaction.payload.location,
349
+ this.frame,
350
+ );
351
+ if (cursorInView) {
352
+ if (this._layoutState.barOffsetY > 0) {
353
+ // Double clicking on the expanded view should collapse.
354
+ this._updateLayoutStateAndResizeBar(0);
355
+ this.setNeedsDisplay();
356
+ }
357
+ }
358
+ }
359
+
360
+ _handleMouseDown(interaction: MouseDownInteraction) {
361
+ const cursorLocation = interaction.payload.location;
362
+ const resizeBarFrame = this._resizeBar.frame;
363
+ if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
364
+ const mouseY = cursorLocation.y;
365
+ this._resizingState = {
366
+ cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
367
+ mouseY,
368
+ };
369
+ }
370
+ }
371
+
372
+ _handleMouseMove(interaction: MouseMoveInteraction) {
373
+ const {_resizingState} = this;
374
+ if (_resizingState) {
375
+ this._resizingState = {
376
+ ..._resizingState,
377
+ mouseY: interaction.payload.location.y,
378
+ };
379
+ this.setNeedsDisplay();
380
+ }
381
+ }
382
+
383
+ _handleMouseUp(interaction: MouseUpInteraction) {
384
+ if (this._resizingState) {
385
+ this._resizingState = null;
386
+ }
387
+ }
388
+
389
+ getCursorActiveSubView(interaction: Interaction): View | null {
390
+ const cursorLocation = interaction.payload.location;
391
+ const resizeBarFrame = this._resizeBar.frame;
392
+ if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
393
+ return this;
394
+ } else {
395
+ return null;
396
+ }
397
+ }
398
+
399
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
400
+ switch (interaction.type) {
401
+ case 'click':
402
+ this._handleClick(interaction);
403
+ return;
404
+ case 'double-click':
405
+ this._handleDoubleClick(interaction);
406
+ return;
407
+ case 'mousedown':
408
+ this._handleMouseDown(interaction);
409
+ return;
410
+ case 'mousemove':
411
+ this._handleMouseMove(interaction);
412
+ return;
413
+ case 'mouseup':
414
+ this._handleMouseUp(interaction);
415
+ return;
416
+ }
417
+ }
418
+}
packages/react-devtools-scheduling-profiler/src/view-base/Surface.js
+1
-1
@@ -92,7 +92,7 @@ export class Surface {
92
origin: zeroPoint,
93
size: _canvasSize,
94
});
95
- rootView.displayIfNeeded(_context);
95
+ rootView.displayIfNeeded(_context, this._viewRefs);
96
}
97
98
getCurrentCursor(): string | null {
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js
new
+3
@@ -0,0 +1,3 @@
1
+// TODO Vertically stack views (via verticallyStackedLayout).
2
+// If stacked views are taller than the available height, a vertical scrollbar will be shown on the side,
3
+// and width will be adjusted to subtract the width of the scrollbar.
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
+58
-4
@@ -12,10 +12,11 @@ import type {
12
MouseDownInteraction,
13
MouseMoveInteraction,
14
MouseUpInteraction,
15
- WheelPlainInteraction,
15
+ WheelWithShiftInteraction,
16
} from './useCanvasInteraction';
17
import type {Rect} from './geometry';
18
import type {ScrollState} from './utils/scrollState';
19
+import type {ViewRefs} from './Surface';
20
21
import {Surface} from './Surface';
22
import {View} from './View';
@@ -26,6 +27,11 @@ import {
27
translateState,
28
} from './utils/scrollState';
29
import {MOVE_WHEEL_DELTA_THRESHOLD} from './constants';
30
+import {COLORS} from '../content-views/constants';
31
+
32
+const CARET_MARGIN = 3;
33
+const CARET_WIDTH = 5;
34
+const CARET_HEIGHT = 3;
35
36
export class VerticalScrollView extends View {
37
_scrollState: ScrollState = {offset: 0, length: 0};
@@ -48,6 +54,54 @@ export class VerticalScrollView extends View {
54
return this._contentView.desiredSize();
55
}
56
57
+ draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
58
+ super.draw(context, viewRefs);
59
+
60
+ // Show carets if there's scroll overflow above or below the viewable area.
61
+ if (this.frame.size.height > CARET_HEIGHT * 2 + CARET_MARGIN * 3) {
62
+ const offset = this._scrollState.offset;
63
+ const desiredSize = this._contentView.desiredSize();
64
+
65
+ const above = offset;
66
+ const below = this.frame.size.height - desiredSize.height - offset;
67
+
68
+ if (above < 0 || below < 0) {
69
+ const {visibleArea} = this;
70
+ const {x, y} = visibleArea.origin;
71
+ const {width, height} = visibleArea.size;
72
+ const horizontalCenter = x + width / 2;
73
+
74
+ const halfWidth = CARET_WIDTH;
75
+ const left = horizontalCenter + halfWidth;
76
+ const right = horizontalCenter - halfWidth;
77
+
78
+ if (above < 0) {
79
+ const topY = y + CARET_MARGIN;
80
+
81
+ context.beginPath();
82
+ context.moveTo(horizontalCenter, topY);
83
+ context.lineTo(left, topY + CARET_HEIGHT);
84
+ context.lineTo(right, topY + CARET_HEIGHT);
85
+ context.closePath();
86
+ context.fillStyle = COLORS.SCROLL_CARET;
87
+ context.fill();
88
+ }
89
+
90
+ if (below < 0) {
91
+ const bottomY = y + height - CARET_MARGIN;
92
+
93
+ context.beginPath();
94
+ context.moveTo(horizontalCenter, bottomY);
95
+ context.lineTo(left, bottomY - CARET_HEIGHT);
96
+ context.lineTo(right, bottomY - CARET_HEIGHT);
97
+ context.closePath();
98
+ context.fillStyle = COLORS.SCROLL_CARET;
99
+ context.fill();
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
/**
106
* Reference to the content view. This view is also the only view in
107
* `this.subviews`.
@@ -103,7 +157,7 @@ export class VerticalScrollView extends View {
157
}
158
}
159
106
- _handleWheelPlain(interaction: WheelPlainInteraction) {
160
+ _handleWheelShift(interaction: WheelWithShiftInteraction) {
161
const {
162
location,
163
delta: {deltaX, deltaY},
@@ -141,8 +195,8 @@ export class VerticalScrollView extends View {
195
case 'mouseup':
196
this._handleMouseUp(interaction);
197
break;
144
- case 'wheel-plain':
145
- this._handleWheelPlain(interaction);
198
+ case 'wheel-shift':
199
+ this._handleWheelShift(interaction);
200
break;
201
}
202
}
packages/react-devtools-scheduling-profiler/src/view-base/View.js
+20
-12
@@ -8,7 +8,7 @@
8
*/
9
10
import type {Interaction} from './useCanvasInteraction';
11
-import type {Rect, Size} from './geometry';
11
+import type {Rect, Size, SizeWithMaxHeight} from './geometry';
12
import type {Layouter} from './layouter';
13
import type {ViewRefs} from './Surface';
14
@@ -140,7 +140,7 @@ export class View {
140
*
141
* Can be overridden by subclasses.
142
*/
143
- desiredSize(): ?Size {
143
+ desiredSize(): Size | SizeWithMaxHeight {
144
if (this._needsDisplay) {
145
this.layoutSubviews();
146
}
@@ -186,7 +186,7 @@ export class View {
186
* 1. Lays out subviews with `layoutSubviews`.
187
* 2. Draws content with `draw`.
188
*/
189
- displayIfNeeded(context: CanvasRenderingContext2D) {
189
+ displayIfNeeded(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
190
if (
191
(this._needsDisplay || this._subviewsNeedDisplay) &&
192
rectIntersectsRect(this.frame, this.visibleArea) &&
@@ -195,7 +195,7 @@ export class View {
195
this.layoutSubviews();
196
if (this._needsDisplay) this._needsDisplay = false;
197
if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
198
- this.draw(context);
198
+ this.draw(context, viewRefs);
199
}
200
}
201
@@ -239,11 +239,11 @@ export class View {
239
*
240
* @see displayIfNeeded
241
*/
242
- draw(context: CanvasRenderingContext2D) {
242
+ draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
243
const {subviews, visibleArea} = this;
244
subviews.forEach(subview => {
245
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
246
- subview.displayIfNeeded(context);
246
+ subview.displayIfNeeded(context, viewRefs);
247
}
248
});
249
}
@@ -252,10 +252,9 @@ export class View {
252
* Handle an `interaction`.
253
*
254
* To be overwritten by subclasses that wish to handle interactions.
255
+ *
256
+ * NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
257
*/
256
- // Internal note: Do not call directly! Use
257
- // `handleInteractionAndPropagateToSubviews` so that interactions are
258
- // propagated to subviews.
258
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {}
259
260
/**
@@ -272,9 +271,18 @@ export class View {
271
interaction: Interaction,
272
viewRefs: ViewRefs,
273
) {
274
+ const {subviews, visibleArea} = this;
275
+
276
+ if (visibleArea.size.height === 0) {
277
+ return;
278
+ }
279
+
280
this.handleInteraction(interaction, viewRefs);
276
- this.subviews.forEach(subview =>
277
- subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs),
278
- );
281
+
282
+ subviews.forEach(subview => {
283
+ if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
284
+ subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs);
285
+ }
286
+ });
287
}
288
}
packages/react-devtools-scheduling-profiler/src/view-base/geometry.js
+13
@@ -9,6 +9,10 @@
9
10
export type Point = $ReadOnly<{|x: number, y: number|}>;
11
export type Size = $ReadOnly<{|width: number, height: number|}>;
12
+export type SizeWithMaxHeight = {|
13
+ ...Size,
14
+ maxInitialHeight?: number,
15
+|};
16
export type Rect = $ReadOnly<{|origin: Point, size: Size|}>;
17
18
/**
@@ -70,6 +74,15 @@ function boxToRect(box: Box): Rect {
74
}
75
76
export function rectIntersectsRect(rect1: Rect, rect2: Rect): boolean {
77
+ if (
78
+ rect1.size.width === 0 ||
79
+ rect1.size.height === 0 ||
80
+ rect2.size.width === 0 ||
81
+ rect2.size.height === 0
82
+ ) {
83
+ return false;
84
+ }
85
+
86
const [top1, right1, bottom1, left1] = rectToBox(rect1);
87
const [top2, right2, bottom2, left2] = rectToBox(rect2);
88
return !(
packages/react-devtools-scheduling-profiler/src/view-base/index.js
+2
-2
@@ -7,9 +7,9 @@
7
* @flow
8
*/
9
10
-export * from './ColorView';
10
+export * from './BackgroundColorView';
11
export * from './HorizontalPanAndZoomView';
12
-export * from './ResizableSplitView';
12
+export * from './ResizableView';
13
export * from './Surface';
14
export * from './VerticalScrollView';
15
export * from './View';
packages/react-devtools-scheduling-profiler/src/view-base/layouter.js
+6
-6
@@ -43,8 +43,7 @@ export function collapseLayoutIntoViews(layout: Layout) {
43
export const noopLayout: Layouter = layout => layout;
44
45
/**
46
- * Layer views on top of each other. All views' frames will be set to
47
- * `containerFrame`.
46
+ * Layer views on top of each other. All views' frames will be set to `containerFrame`.
47
*
48
* Equivalent to composing:
49
* - `alignToContainerXLayout`,
@@ -52,12 +51,13 @@ export const noopLayout: Layouter = layout => layout;
51
* - `containerWidthLayout`, and
52
* - `containerHeightLayout`.
53
*/
55
-export const layeredLayout: Layouter = (layout, containerFrame) =>
56
- layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame}));
54
+export const layeredLayout: Layouter = (layout, containerFrame) => {
55
+ return layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame}));
56
+};
57
58
/**
59
- * Stacks `views` vertically in `frame`. All views in `views` will have their
60
- * widths set to the frame's width.
59
+ * Stacks `views` vertically in `frame`.
60
+ * All views in `views` will have their widths set to the frame's width.
61
*/
62
export const verticallyStackedLayout: Layouter = (layout, containerFrame) => {
63
let currentY = containerFrame.origin.y;
packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js
+61
-1
@@ -10,9 +10,23 @@
10
import type {NormalizedWheelDelta} from './utils/normalizeWheel';
11
import type {Point} from './geometry';
12
13
-import {useEffect} from 'react';
13
+import {useEffect, useRef} from 'react';
14
import {normalizeWheel} from './utils/normalizeWheel';
15
16
+export type ClickInteraction = {|
17
+ type: 'click',
18
+ payload: {|
19
+ event: MouseEvent,
20
+ location: Point,
21
+ |},
22
+|};
23
+export type DoubleClickInteraction = {|
24
+ type: 'double-click',
25
+ payload: {|
26
+ event: MouseEvent,
27
+ location: Point,
28
+ |},
29
+|};
30
export type MouseDownInteraction = {|
31
type: 'mousedown',
32
payload: {|
@@ -68,6 +82,8 @@ export type WheelWithMetaInteraction = {|
82
|};
83
84
export type Interaction =
85
+ | ClickInteraction
86
+ | DoubleClickInteraction
87
| MouseDownInteraction
88
| MouseMoveInteraction
89
| MouseUpInteraction
@@ -99,6 +115,9 @@ export function useCanvasInteraction(
115
canvasRef: {|current: HTMLCanvasElement | null|},
116
interactor: (interaction: Interaction) => void,
117
) {
118
+ const isMouseDownRef = useRef<boolean>(false);
119
+ const didMouseMoveWhileDownRef = useRef<boolean>(false);
120
+
121
useEffect(() => {
122
const canvas = canvasRef.current;
123
if (!canvas) {
@@ -113,7 +132,38 @@ export function useCanvasInteraction(
132
};
133
}
134
135
+ const onCanvasClick: MouseEventHandler = event => {
136
+ if (didMouseMoveWhileDownRef.current) {
137
+ return;
138
+ }
139
+
140
+ interactor({
141
+ type: 'click',
142
+ payload: {
143
+ event,
144
+ location: localToCanvasCoordinates({x: event.x, y: event.y}),
145
+ },
146
+ });
147
+ };
148
+
149
+ const onCanvasDoubleClick: MouseEventHandler = event => {
150
+ if (didMouseMoveWhileDownRef.current) {
151
+ return;
152
+ }
153
+
154
+ interactor({
155
+ type: 'double-click',
156
+ payload: {
157
+ event,
158
+ location: localToCanvasCoordinates({x: event.x, y: event.y}),
159
+ },
160
+ });
161
+ };
162
+
163
const onCanvasMouseDown: MouseEventHandler = event => {
164
+ didMouseMoveWhileDownRef.current = false;
165
+ isMouseDownRef.current = true;
166
+
167
interactor({
168
type: 'mousedown',
169
payload: {
@@ -124,6 +174,10 @@ export function useCanvasInteraction(
174
};
175
176
const onDocumentMouseMove: MouseEventHandler = event => {
177
+ if (isMouseDownRef.current) {
178
+ didMouseMoveWhileDownRef.current = true;
179
+ }
180
+
181
interactor({
182
type: 'mousemove',
183
payload: {
@@ -134,6 +188,8 @@ export function useCanvasInteraction(
188
};
189
190
const onDocumentMouseUp: MouseEventHandler = event => {
191
+ isMouseDownRef.current = false;
192
+
193
interactor({
194
type: 'mouseup',
195
payload: {
@@ -179,6 +235,8 @@ export function useCanvasInteraction(
235
ownerDocument.addEventListener('mousemove', onDocumentMouseMove);
236
ownerDocument.addEventListener('mouseup', onDocumentMouseUp);
237
238
+ canvas.addEventListener('click', onCanvasClick);
239
+ canvas.addEventListener('dblclick', onCanvasDoubleClick);
240
canvas.addEventListener('mousedown', onCanvasMouseDown);
241
canvas.addEventListener('wheel', onCanvasWheel);
242
@@ -186,6 +244,8 @@ export function useCanvasInteraction(
244
ownerDocument.removeEventListener('mousemove', onDocumentMouseMove);
245
ownerDocument.removeEventListener('mouseup', onDocumentMouseUp);
246
247
+ canvas.removeEventListener('click', onCanvasClick);
248
+ canvas.removeEventListener('dblclick', onCanvasDoubleClick);
249
canvas.removeEventListener('mousedown', onCanvasMouseDown);
250
canvas.removeEventListener('wheel', onCanvasWheel);
251
};
packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
+7
-4
@@ -45,20 +45,21 @@ function Profiler(_: {||}) {
45
46
const {supportsSchedulingProfiler} = useContext(StoreContext);
47
48
- let showRightColumn = true;
48
+ let isLegacyProfilerSelected = false;
49
50
let view = null;
51
if (didRecordCommits || selectedTabID === 'scheduling-profiler') {
52
switch (selectedTabID) {
53
case 'flame-chart':
54
+ isLegacyProfilerSelected = true;
55
view = <CommitFlamegraph />;
56
break;
57
case 'ranked-chart':
58
+ isLegacyProfilerSelected = true;
59
view = <CommitRanked />;
60
break;
61
case 'scheduling-profiler':
62
view = <SchedulingProfiler />;
61
- showRightColumn = false;
63
break;
64
default:
65
break;
@@ -119,7 +120,7 @@ function Profiler(_: {||}) {
120
<RootSelector />
121
<div className={styles.Spacer} />
122
<SettingsModalContextToggle />
122
- {didRecordCommits && (
123
+ {isLegacyProfilerSelected && didRecordCommits && (
124
<Fragment>
125
<div className={styles.VRule} />
126
<SnapshotSelector />
@@ -131,7 +132,9 @@ function Profiler(_: {||}) {
132
<ModalDialog />
133
</div>
134
</div>
134
- {showRightColumn && <div className={styles.RightColumn}>{sidebar}</div>}
135
+ {isLegacyProfilerSelected && (
136
+ <div className={styles.RightColumn}>{sidebar}</div>
137
+ )}
138
<SettingsModal />
139
</div>
140
</SettingsModalContextController>
packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
+4
-1
@@ -212,7 +212,10 @@ function ProfilerContextController({children}: Props) {
212
const [selectedCommitIndex, selectCommitIndex] = useState<number | null>(
213
null,
214
);
215
- const [selectedTabID, selectTab] = useState<TabID>('flame-chart');
215
+ const [selectedTabID, selectTab] = useLocalStorage<TabID>(
216
+ 'React::DevTools::Profiler::defaultTab',
217
+ 'flame-chart',
218
+ );
219
220
if (isProfiling) {
221
batchedUpdates(() => {
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+23
-41
@@ -412,6 +412,9 @@ export function updateThemeVariables(
412
updateStyleHelper(theme, 'color-record-hover', documentElements);
413
updateStyleHelper(theme, 'color-record-inactive', documentElements);
414
updateStyleHelper(theme, 'color-resize-bar', documentElements);
415
+ updateStyleHelper(theme, 'color-resize-bar-active', documentElements);
416
+ updateStyleHelper(theme, 'color-resize-bar-border', documentElements);
417
+ updateStyleHelper(theme, 'color-resize-bar-dot', documentElements);
418
updateStyleHelper(theme, 'color-color-scroll-thumb', documentElements);
419
updateStyleHelper(theme, 'color-color-scroll-track', documentElements);
420
updateStyleHelper(theme, 'color-search-match', documentElements);
@@ -431,21 +434,6 @@ export function updateThemeVariables(
434
'color-scheduling-profiler-native-event-hover',
435
documentElements,
436
);
434
- updateStyleHelper(
435
- theme,
436
- 'color-scheduling-profiler-native-event-warning',
437
- documentElements,
438
- );
439
- updateStyleHelper(
440
- theme,
441
- 'color-scheduling-profiler-native-event-warning-hover',
442
- documentElements,
443
- );
444
- updateStyleHelper(
445
- theme,
446
- 'color-scheduling-profiler-native-event-warning-text',
447
- documentElements,
448
- );
437
updateStyleHelper(
438
theme,
439
'color-selected-tree-highlight-active',
@@ -481,11 +469,6 @@ export function updateThemeVariables(
469
'color-scheduling-profiler-react-idle',
470
documentElements,
471
);
484
- updateStyleHelper(
485
- theme,
486
- 'color-scheduling-profiler-react-idle-selected',
487
- documentElements,
488
- );
472
updateStyleHelper(
473
theme,
474
'color-scheduling-profiler-react-idle-hover',
@@ -496,11 +479,6 @@ export function updateThemeVariables(
479
'color-scheduling-profiler-react-render',
480
documentElements,
481
);
499
- updateStyleHelper(
500
- theme,
501
- 'color-scheduling-profiler-react-render-selected',
502
- documentElements,
503
- );
482
updateStyleHelper(
483
theme,
484
'color-scheduling-profiler-react-render-hover',
@@ -511,11 +489,6 @@ export function updateThemeVariables(
489
'color-scheduling-profiler-react-commit',
490
documentElements,
491
);
514
- updateStyleHelper(
515
- theme,
516
- 'color-scheduling-profiler-react-commit-selected',
517
- documentElements,
518
- );
492
updateStyleHelper(
493
theme,
494
'color-scheduling-profiler-react-commit-hover',
@@ -528,57 +501,57 @@ export function updateThemeVariables(
501
);
502
updateStyleHelper(
503
theme,
531
- 'color-scheduling-profiler-react-layout-effects-selected',
504
+ 'color-scheduling-profiler-react-layout-effects-hover',
505
documentElements,
506
);
507
updateStyleHelper(
508
theme,
536
- 'color-scheduling-profiler-react-layout-effects-hover',
509
+ 'color-scheduling-profiler-react-passive-effects',
510
documentElements,
511
);
512
updateStyleHelper(
513
theme,
541
- 'color-scheduling-profiler-react-passive-effects',
514
+ 'color-scheduling-profiler-react-passive-effects-hover',
515
documentElements,
516
);
517
updateStyleHelper(
518
theme,
546
- 'color-scheduling-profiler-react-passive-effects-selected',
519
+ 'color-scheduling-profiler-react-schedule',
520
documentElements,
521
);
522
updateStyleHelper(
523
theme,
551
- 'color-scheduling-profiler-react-passive-effects-hover',
524
+ 'color-scheduling-profiler-react-schedule-hover',
525
documentElements,
526
);
527
updateStyleHelper(
528
theme,
556
- 'color-scheduling-profiler-react-schedule',
529
+ 'color-scheduling-profiler-react-suspense-rejected-event',
530
documentElements,
531
);
532
updateStyleHelper(
533
theme,
561
- 'color-scheduling-profiler-react-schedule-hover',
534
+ 'color-scheduling-profiler-react-suspense-rejected-hover',
535
documentElements,
536
);
537
updateStyleHelper(
538
theme,
566
- 'color-scheduling-profiler-react-schedule-cascading',
539
+ 'color-scheduling-profiler-react-suspense-resolved',
540
documentElements,
541
);
542
updateStyleHelper(
543
theme,
571
- 'color-scheduling-profiler-react-schedule-cascading-hover',
544
+ 'color-scheduling-profiler-react-suspense-resolved-hover',
545
documentElements,
546
);
547
updateStyleHelper(
548
theme,
576
- 'color-scheduling-profiler-react-suspend',
549
+ 'color-scheduling-profiler-react-suspense-unresolved',
550
documentElements,
551
);
552
updateStyleHelper(
553
theme,
581
- 'color-scheduling-profiler-react-suspend-hover',
554
+ 'color-scheduling-profiler-react-suspense-unresolved-hover',
555
documentElements,
556
);
557
updateStyleHelper(
@@ -586,6 +559,7 @@ export function updateThemeVariables(
559
'color-scheduling-profiler-react-work-border',
560
documentElements,
561
);
562
+ updateStyleHelper(theme, 'color-scroll-caret', documentElements);
563
updateStyleHelper(theme, 'color-shadow', documentElements);
564
updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
565
updateStyleHelper(theme, 'color-text', documentElements);
@@ -597,6 +571,14 @@ export function updateThemeVariables(
571
updateStyleHelper(theme, 'color-toggle-text', documentElements);
572
updateStyleHelper(theme, 'color-tooltip-background', documentElements);
573
updateStyleHelper(theme, 'color-tooltip-text', documentElements);
574
+ updateStyleHelper(theme, 'color-warning-background', documentElements);
575
+ updateStyleHelper(theme, 'color-warning-background-hover', documentElements);
576
+ updateStyleHelper(theme, 'color-warning-text-color', documentElements);
577
+ updateStyleHelper(
578
+ theme,
579
+ 'color-warning-text-color-inverted',
580
+ documentElements,
581
+ );
582
583
// Font smoothing varies based on the theme.
584
updateStyleHelper(theme, 'font-smoothing', documentElements);
packages/react-devtools-shared/src/devtools/views/TabBar.js
+1
-1
@@ -91,7 +91,7 @@ export default function TabBar({
91
<Fragment>
92
{tabs.map(tab => {
93
if (tab === null) {
94
- return <div className={styles.VRule} />;
94
+ return <div key="VRule" className={styles.VRule} />;
95
}
96
97
const {icon, id, label, title} = tab;
packages/react-devtools-shared/src/devtools/views/root.css
+55
-52
@@ -77,46 +77,43 @@
77
--light-color-record-active: #fc3a4b;
78
--light-color-record-hover: #3578e5;
79
--light-color-record-inactive: #0088fa;
80
- --light-color-resize-bar: #cccccc;
80
+ --light-color-resize-bar: #eeeeee;
81
+ --light-color-resize-bar-active: #dcdcdc;
82
+ --light-color-resize-bar-border: #d1d1d1;
83
+ --light-color-resize-bar-dot: #333333;
84
--light-color-scheduling-profiler-native-event: #ccc;
85
--light-color-scheduling-profiler-native-event-hover: #aaa;
83
- --light-color-scheduling-profiler-native-event-warning: #ee1638;
84
- --light-color-scheduling-profiler-native-event-warning-hover: #da1030;
85
- --light-color-scheduling-profiler-native-event-warning-text: #fff;
86
--light-color-scheduling-profiler-priority-background: #f6f6f6;
87
--light-color-scheduling-profiler-priority-border: #eeeeee;
88
--light-color-scheduling-profiler-user-timing: #c9cacd;
89
- --light-color-scheduling-profiler-user-timing-hover:#93959a;
90
- --light-color-scheduling-profiler-react-idle: #edf6ff;
91
- --light-color-scheduling-profiler-react-idle-selected:#EDF6FF;
92
- --light-color-scheduling-profiler-react-idle-hover:#EDF6FF;
89
+ --light-color-scheduling-profiler-user-timing-hover: #93959a;
90
+ --light-color-scheduling-profiler-react-idle: #d3e5f6;
91
+ --light-color-scheduling-profiler-react-idle-hover: #c3d9ef;
92
--light-color-scheduling-profiler-react-render: #9fc3f3;
94
- --light-color-scheduling-profiler-react-render-selected:#64A9F5;
95
- --light-color-scheduling-profiler-react-render-hover:#2683E2;
96
- --light-color-scheduling-profiler-react-commit: #ff718e;
97
- --light-color-scheduling-profiler-react-commit-selected:#FF5277;
98
- --light-color-scheduling-profiler-react-commit-hover:#ed0030;
99
- --light-color-scheduling-profiler-react-layout-effects:#c88ff0;
100
- --light-color-scheduling-profiler-react-layout-effects-selected:#934FC1;
101
- --light-color-scheduling-profiler-react-layout-effects-hover:#601593;
102
- --light-color-scheduling-profiler-react-passive-effects:#c88ff0;
103
- --light-color-scheduling-profiler-react-passive-effects-selected:#934FC1;
104
- --light-color-scheduling-profiler-react-passive-effects-hover:#601593;
93
+ --light-color-scheduling-profiler-react-render-hover: #83afe9;
94
+ --light-color-scheduling-profiler-react-commit: #c88ff0;
95
+ --light-color-scheduling-profiler-react-commit-hover: #b281d6;
96
+ --light-color-scheduling-profiler-react-layout-effects: #b281d6;
97
+ --light-color-scheduling-profiler-react-layout-effects-hover: #9d71bd;
98
+ --light-color-scheduling-profiler-react-passive-effects: #b281d6;
99
+ --light-color-scheduling-profiler-react-passive-effects-hover: #9d71bd;
100
--light-color-scheduling-profiler-react-schedule: #9fc3f3;
106
- --light-color-scheduling-profiler-react-schedule-hover:#2683E2;
107
- --light-color-scheduling-profiler-react-schedule-cascading:#ff718e;
108
- --light-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
109
- --light-color-scheduling-profiler-react-suspend: #a6e59f;
110
- --light-color-scheduling-profiler-react-suspend-hover:#13bc00;
101
+ --light-color-scheduling-profiler-react-schedule-hover: #2683E2;
102
+ --light-color-scheduling-profiler-react-suspense-rejected: #f1cc14;
103
+ --light-color-scheduling-profiler-react-suspense-rejected-hover: #ffdf37;
104
+ --light-color-scheduling-profiler-react-suspense-resolved: #a6e59f;
105
+ --light-color-scheduling-profiler-react-suspense-resolved-hover: #89d281;
106
+ --light-color-scheduling-profiler-react-suspense-unresolved: #c9cacd;
107
+ --light-color-scheduling-profiler-react-suspense-unresolved-hover: #93959a;
108
--light-color-scheduling-profiler-text-color: #000000;
112
- --light-color-scheduling-profiler-react-work-border:#ffffff;
109
+ --light-color-scheduling-profiler-react-work-border: #ffffff;
110
--light-color-scroll-thumb: #c2c2c2;
111
--light-color-scroll-track: #fafafa;
112
--light-color-search-match: yellow;
113
--light-color-search-match-current: #f7923b;
114
--light-color-selected-tree-highlight-active: rgba(0, 136, 250, 0.1);
115
--light-color-selected-tree-highlight-inactive: rgba(0, 0, 0, 0.05);
119
- --light-color-shadow: rgba(0, 0, 0, 0.25);
116
+ --light-color-scroll-caret: rgba(150, 150, 150, 0.5);
117
--light-color-tab-selected-border: #0088fa;
118
--light-color-text: #000000;
119
--light-color-text-invalid: #ff0000;
@@ -127,6 +124,10 @@
124
--light-color-toggle-text: #ffffff;
125
--light-color-tooltip-background: rgba(0, 0, 0, 0.9);
126
--light-color-tooltip-text: #ffffff;
127
+ --light-color-warning-background: #fb3655;
128
+ --light-color-warning-background-hover: #f82042;
129
+ --light-color-warning-text-color: #ffffff;
130
+ --light-color-warning-text-color-inverted: #fd4d69;
131
132
/* Dark theme */
133
--dark-color-attribute-name: #9d87d2;
@@ -202,45 +203,43 @@
203
--dark-color-record-active: #fc3a4b;
204
--dark-color-record-hover: #a2e9fc;
205
--dark-color-record-inactive: #61dafb;
205
- --dark-color-resize-bar: #3d424a;
206
+ --dark-color-resize-bar: #282c34;
207
+ --dark-color-resize-bar-active: #31363f;
208
+ --dark-color-resize-bar-border: #3d424a;
209
+ --dark-color-resize-bar-dot: #cfd1d5;
210
--dark-color-scheduling-profiler-native-event: #b2b2b2;
211
--dark-color-scheduling-profiler-native-event-hover: #949494;
208
- --dark-color-scheduling-profiler-native-event-warning: #ee1638;
209
- --dark-color-scheduling-profiler-native-event-warning-hover: #da1030;
210
- --dark-color-scheduling-profiler-native-event-warning-text: #fff;
212
--dark-color-scheduling-profiler-priority-background: #1d2129;
213
--dark-color-scheduling-profiler-priority-border: #282c34;
214
--dark-color-scheduling-profiler-user-timing: #c9cacd;
214
- --dark-color-scheduling-profiler-user-timing-hover:#93959a;
215
+ --dark-color-scheduling-profiler-user-timing-hover: #93959a;
216
--dark-color-scheduling-profiler-react-idle: #3d485b;
216
- --dark-color-scheduling-profiler-react-idle-selected:#465269;
217
- --dark-color-scheduling-profiler-react-idle-hover:#465269;
218
- --dark-color-scheduling-profiler-react-render: #9fc3f3;
219
- --dark-color-scheduling-profiler-react-render-selected:#64A9F5;
220
- --dark-color-scheduling-profiler-react-render-hover:#2683E2;
221
- --dark-color-scheduling-profiler-react-commit: #ff718e;
222
- --dark-color-scheduling-profiler-react-commit-selected:#FF5277;
223
- --dark-color-scheduling-profiler-react-commit-hover:#ed0030;
224
- --dark-color-scheduling-profiler-react-layout-effects:#c88ff0;
225
- --dark-color-scheduling-profiler-react-layout-effects-selected:#934FC1;
226
- --dark-color-scheduling-profiler-react-layout-effects-hover:#601593;
227
- --dark-color-scheduling-profiler-react-passive-effects:#c88ff0;
228
- --dark-color-scheduling-profiler-react-passive-effects-selected:#934FC1;
229
- --dark-color-scheduling-profiler-react-passive-effects-hover:#601593;
230
- --dark-color-scheduling-profiler-react-schedule: #9fc3f3;
231
- --dark-color-scheduling-profiler-react-schedule-hover:#2683E2;
232
- --dark-color-scheduling-profiler-react-schedule-cascading:#ff718e;
233
- --dark-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030;
234
- --dark-color-scheduling-profiler-react-suspend: #a6e59f;
235
- --dark-color-scheduling-profiler-react-suspend-hover:#13bc00;
217
+ --dark-color-scheduling-profiler-react-idle-hover: #465269;
218
+ --dark-color-scheduling-profiler-react-render: #2683E2;
219
+ --dark-color-scheduling-profiler-react-render-hover: #1a76d4;
220
+ --dark-color-scheduling-profiler-react-commit: #731fad;
221
+ --dark-color-scheduling-profiler-react-commit-hover: #611b94;
222
+ --dark-color-scheduling-profiler-react-layout-effects: #611b94;
223
+ --dark-color-scheduling-profiler-react-layout-effects-hover: #51167a;
224
+ --dark-color-scheduling-profiler-react-passive-effects: #611b94;
225
+ --dark-color-scheduling-profiler-react-passive-effects-hover: #51167a;
226
+ --dark-color-scheduling-profiler-react-schedule: #2683E2;
227
+ --dark-color-scheduling-profiler-react-schedule-hover: #1a76d4;
228
+ --dark-color-scheduling-profiler-react-suspense-rejected: #f1cc14;
229
+ --dark-color-scheduling-profiler-react-suspense-rejected-hover: #e4c00f;
230
+ --dark-color-scheduling-profiler-react-suspense-resolved: #a6e59f;
231
+ --dark-color-scheduling-profiler-react-suspense-resolved-hover: #89d281;
232
+ --dark-color-scheduling-profiler-react-suspense-unresolved: #c9cacd;
233
+ --dark-color-scheduling-profiler-react-suspense-unresolved-hover: #93959a;
234
--dark-color-scheduling-profiler-text-color: #000000;
237
- --dark-color-scheduling-profiler-react-work-border:#ffffff;
235
+ --dark-color-scheduling-profiler-react-work-border: #ffffff;
236
--dark-color-scroll-thumb: #afb3b9;
237
--dark-color-scroll-track: #313640;
238
--dark-color-search-match: yellow;
239
--dark-color-search-match-current: #f7923b;
240
--dark-color-selected-tree-highlight-active: rgba(23, 143, 185, 0.15);
241
--dark-color-selected-tree-highlight-inactive: rgba(255, 255, 255, 0.05);
242
+ --dark-color-scroll-caret: #4f5766;
243
--dark-color-shadow: rgba(0, 0, 0, 0.5);
244
--dark-color-tab-selected-border: #178fb9;
245
--dark-color-text: #ffffff;
@@ -252,6 +251,10 @@
251
--dark-color-toggle-text: #ffffff;
252
--dark-color-tooltip-background: rgba(255, 255, 255, 0.95);
253
--dark-color-tooltip-text: #000000;
254
+ --dark-color-warning-background: #ee1638;
255
+ --dark-color-warning-background-hover: #da1030;
256
+ --dark-color-warning-text-color: #ffffff;
257
+ --dark-color-warning-text-color-inverted: #ee1638;
258
259
/* Font smoothing */
260
--light-font-smoothing: auto;
packages/react-devtools-shared/src/hookNamesCache.js
+1
-8
@@ -103,14 +103,7 @@ export function loadHookNames(
103
104
let didTimeout = false;
105
106
- const response = loadHookNamesFunction(hooksTree);
107
- console.log(
108
- 'loadHookNamesFunction:',
109
- loadHookNamesFunction,
110
- '->',
111
- response,
112
- );
113
- response.then(
106
+ loadHookNamesFunction(hooksTree).then(
107
function onSuccess(hookNames) {
108
if (didTimeout) {
109
return;
packages/react-reconciler/src/ReactFiberThrow.new.js
+1
-1
@@ -244,7 +244,7 @@ function throwException(
244
}
245
246
if (enableSchedulingProfiler) {
247
- markComponentSuspended(sourceFiber, wakeable);
247
+ markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
248
}
249
250
// Reset the memoizedState to what it was before we attempted to render it.
packages/react-reconciler/src/ReactFiberThrow.old.js
+1
-1
@@ -244,7 +244,7 @@ function throwException(
244
}
245
246
if (enableSchedulingProfiler) {
247
- markComponentSuspended(sourceFiber, wakeable);
247
+ markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
248
}
249
250
// Reset the memoizedState to what it was before we attempted to render it.
packages/react-reconciler/src/SchedulingProfiler.js
+13
-3
@@ -109,13 +109,23 @@ function getWakeableID(wakeable: Wakeable): number {
109
return ((wakeableIDs.get(wakeable): any): number);
110
}
111
112
-export function markComponentSuspended(fiber: Fiber, wakeable: Wakeable): void {
112
+export function markComponentSuspended(
113
+ fiber: Fiber,
114
+ wakeable: Wakeable,
115
+ lanes: Lanes,
116
+): void {
117
if (enableSchedulingProfiler) {
118
if (supportsUserTimingV3) {
119
+ const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend';
120
const id = getWakeableID(wakeable);
121
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
117
- // TODO Add component stack id
118
- markAndClear(`--suspense-suspend-${id}-${componentName}`);
122
+ const phase = fiber.alternate === null ? 'mount' : 'update';
123
+ // TODO (scheduling profiler) Add component stack id if we re-add component stack info.
124
+ markAndClear(
125
+ `--suspense-${eventType}-${id}-${componentName}-${phase}-${formatLanes(
126
+ lanes,
127
+ )}`,
128
+ );
129
wakeable.then(
130
() => markAndClear(`--suspense-resolved-${id}-${componentName}`),
131
() => markAndClear(`--suspense-rejected-${id}-${componentName}`),
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+4
-4
@@ -208,7 +208,7 @@ describe('SchedulingProfiler', () => {
208
`--react-init-${ReactVersion}`,
209
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
210
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
211
- '--suspense-suspend-0-Example',
211
+ '--suspense-suspend-0-Example-mount-1-Sync',
212
'--render-stop',
213
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
214
`--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`,
@@ -239,7 +239,7 @@ describe('SchedulingProfiler', () => {
239
`--react-init-${ReactVersion}`,
240
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
241
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
242
- '--suspense-suspend-0-Example',
242
+ '--suspense-suspend-0-Example-mount-1-Sync',
243
'--render-stop',
244
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
245
`--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`,
@@ -278,7 +278,7 @@ describe('SchedulingProfiler', () => {
278
279
expectMarksToEqual([
280
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
281
- '--suspense-suspend-0-Example',
281
+ '--suspense-suspend-0-Example-mount-16-Default',
282
'--render-stop',
283
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
284
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@@ -317,7 +317,7 @@ describe('SchedulingProfiler', () => {
317
318
expectMarksToEqual([
319
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
320
- '--suspense-suspend-0-Example',
320
+ '--suspense-suspend-0-Example-mount-16-Default',
321
'--render-stop',
322
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
323
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,