@samitouri / QOS-React-2 / commits / 27bf6f9a83

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)}`,