@samitouri / QOS-React / commits / 2bea3fb0b8

Import React Concurrent Mode Profiler (#19634)

Co-authored-by: Brian Vaughn <bvaughn@fb.com> Co-authored-by: Kartik Choudhary <kartikc.918@gmail.com>

E-Liang Tan committed Aug 21, 2020 at 02:06 UTC 2bea3fb0b8f13db702da14b2b02e85127ed397a4
58 files changed +8946 -19
.eslintignore
+3 -1
@@ -18,4 +18,6 @@ packages/react-devtools-extensions/chrome/build
18 packages/react-devtools-extensions/firefox/build
19 packages/react-devtools-extensions/shared/build
20 packages/react-devtools-inline/dist
21 -packages/react-devtools-shell/dist
\ No newline at end of file
21 +packages/react-devtools-shell/dist
22 +packages/react-devtools-scheduling-profiler/dist
23 +packages/react-devtools-scheduling-profiler/static
\ No newline at end of file
.gitignore
+2 -1
@@ -34,4 +34,5 @@ packages/react-devtools-extensions/firefox/*.pem
34 packages/react-devtools-extensions/shared/build
35 packages/react-devtools-extensions/.tempUserDataDir
36 packages/react-devtools-inline/dist
37 -packages/react-devtools-shell/dist
\ No newline at end of file
37 +packages/react-devtools-shell/dist
38 +packages/react-devtools-scheduling-profiler/dist
\ No newline at end of file
.prettierignore
+3 -1
@@ -3,4 +3,6 @@ packages/react-devtools-extensions/chrome/build
3 packages/react-devtools-extensions/firefox/build
4 packages/react-devtools-extensions/shared/build
5 packages/react-devtools-inline/dist
6 -packages/react-devtools-shell/dist
\ No newline at end of file
6 +packages/react-devtools-shell/dist
7 +packages/react-devtools-scheduling-profiler/dist
8 +packages/react-devtools-scheduling-profiler/static
\ No newline at end of file
packages/react-devtools-scheduling-profiler/README.md new
+3
@@ -0,0 +1,3 @@
1 +# Experimental React Concurrent Mode Profiler
2 +
3 +- Deployed at: https://react-scheduling-profiler.vercel.app
packages/react-devtools-scheduling-profiler/package.json new
+30
@@ -0,0 +1,30 @@
1 +{
2 + "private": true,
3 + "name": "react-devtools-scheduling-profiler",
4 + "version": "0.0.1",
5 + "license": "MIT",
6 + "scripts": {
7 + "build": "cross-env NODE_ENV=production cross-env TARGET=remote webpack --config webpack.config.js",
8 + "start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
9 + },
10 + "dependencies": {
11 + "@elg/speedscope": "1.9.0-a6f84db",
12 + "clipboard-js": "^0.3.6",
13 + "memoize-one": "^5.1.1",
14 + "nullthrows": "^1.1.1",
15 + "pretty-ms": "^7.0.0",
16 + "react-virtualized-auto-sizer": "^1.0.2",
17 + "regenerator-runtime": "^0.13.7"
18 + },
19 + "devDependencies": {
20 + "babel-loader": "^8.1.0",
21 + "css-loader": "^4.2.1",
22 + "file-loader": "^6.0.0",
23 + "html-webpack-plugin": "^4.3.0",
24 + "style-loader": "^1.2.1",
25 + "url-loader": "^4.1.0",
26 + "webpack": "^4.44.1",
27 + "webpack-cli": "^3.3.12",
28 + "webpack-dev-server": "^3.11.0"
29 + }
30 +}
packages/react-devtools-scheduling-profiler/src/App.js new
+28
@@ -0,0 +1,28 @@
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 {ReactProfilerData} from './types';
11 +
12 +import * as React from 'react';
13 +import {useState} from 'react';
14 +
15 +import ImportPage from './ImportPage';
16 +import CanvasPage from './CanvasPage';
17 +
18 +export default function App() {
19 + const [profilerData, setProfilerData] = useState<ReactProfilerData | null>(
20 + null,
21 + );
22 +
23 + if (profilerData) {
24 + return <CanvasPage profilerData={profilerData} />;
25 + } else {
26 + return <ImportPage onDataImported={setProfilerData} />;
27 + }
28 +}
packages/react-devtools-scheduling-profiler/src/CanvasPage.css new
+7
@@ -0,0 +1,7 @@
1 +.CanvasPage {
2 + position: absolute;
3 + top: 0.5rem;
4 + bottom: 0.5rem;
5 + left: 0.5rem;
6 + right: 0.5rem;
7 +}
packages/react-devtools-scheduling-profiler/src/CanvasPage.js new
+480
@@ -0,0 +1,480 @@
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 + Point,
12 + HorizontalPanAndZoomViewOnChangeCallback,
13 +} from './view-base';
14 +import type {
15 + ReactHoverContextInfo,
16 + ReactProfilerData,
17 + ReactMeasure,
18 +} from './types';
19 +
20 +import * as React from 'react';
21 +import {
22 + Fragment,
23 + useEffect,
24 + useLayoutEffect,
25 + useRef,
26 + useState,
27 + useCallback,
28 +} from 'react';
29 +import AutoSizer from 'react-virtualized-auto-sizer';
30 +import {copy} from 'clipboard-js';
31 +import prettyMilliseconds from 'pretty-ms';
32 +
33 +import {
34 + HorizontalPanAndZoomView,
35 + ResizableSplitView,
36 + Surface,
37 + VerticalScrollView,
38 + View,
39 + createComposedLayout,
40 + lastViewTakesUpRemainingSpaceLayout,
41 + useCanvasInteraction,
42 + verticallyStackedLayout,
43 + zeroPoint,
44 +} from './view-base';
45 +import {
46 + FlamechartView,
47 + ReactEventsView,
48 + ReactMeasuresView,
49 + TimeAxisMarkersView,
50 + UserTimingMarksView,
51 +} from './content-views';
52 +import {COLORS} from './content-views/constants';
53 +
54 +import EventTooltip from './EventTooltip';
55 +import {ContextMenu, ContextMenuItem, useContextMenu} from './context';
56 +import {getBatchRange} from './utils/getBatchRange';
57 +
58 +import styles from './CanvasPage.css';
59 +
60 +const CONTEXT_MENU_ID = 'canvas';
61 +
62 +type ContextMenuContextData = {|
63 + data: ReactProfilerData,
64 + hoveredEvent: ReactHoverContextInfo | null,
65 +|};
66 +
67 +type Props = {|
68 + profilerData: ReactProfilerData,
69 +|};
70 +
71 +function CanvasPage({profilerData}: Props) {
72 + return (
73 + <div
74 + className={styles.CanvasPage}
75 + style={{backgroundColor: COLORS.BACKGROUND}}>
76 + <AutoSizer>
77 + {({height, width}: {height: number, width: number}) => (
78 + <AutoSizedCanvas data={profilerData} height={height} width={width} />
79 + )}
80 + </AutoSizer>
81 + </div>
82 + );
83 +}
84 +
85 +const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
86 + const {batchUID, duration, timestamp, type} = measure;
87 +
88 + const [startTime, stopTime] = getBatchRange(batchUID, data);
89 +
90 + copy(
91 + JSON.stringify({
92 + type,
93 + timestamp: prettyMilliseconds(timestamp),
94 + duration: prettyMilliseconds(duration),
95 + batchDuration: prettyMilliseconds(stopTime - startTime),
96 + }),
97 + );
98 +};
99 +
100 +const zoomToBatch = (
101 + data: ReactProfilerData,
102 + measure: ReactMeasure,
103 + syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
104 +) => {
105 + const {batchUID} = measure;
106 + const [startTime, stopTime] = getBatchRange(batchUID, data);
107 + syncedHorizontalPanAndZoomViews.forEach(syncedView =>
108 + // Using time as range works because the views' intrinsic content size is
109 + // based on time.
110 + syncedView.zoomToRange(startTime, stopTime),
111 + );
112 +};
113 +
114 +type AutoSizedCanvasProps = {|
115 + data: ReactProfilerData,
116 + height: number,
117 + width: number,
118 +|};
119 +
120 +function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
121 + const canvasRef = useRef<HTMLCanvasElement | null>(null);
122 +
123 + const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
124 + const [mouseLocation, setMouseLocation] = useState<Point>(zeroPoint); // DOM coordinates
125 + const [
126 + hoveredEvent,
127 + setHoveredEvent,
128 + ] = useState<ReactHoverContextInfo | null>(null);
129 +
130 + const surfaceRef = useRef(new Surface());
131 + const userTimingMarksViewRef = useRef(null);
132 + const reactEventsViewRef = useRef(null);
133 + const reactMeasuresViewRef = useRef(null);
134 + const flamechartViewRef = useRef(null);
135 + const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
136 + [],
137 + );
138 +
139 + useLayoutEffect(() => {
140 + const surface = surfaceRef.current;
141 + const defaultFrame = {origin: zeroPoint, size: {width, height}};
142 +
143 + // Clear synced views
144 + syncedHorizontalPanAndZoomViewsRef.current = [];
145 +
146 + const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
147 + newState,
148 + triggeringView?: HorizontalPanAndZoomView,
149 + ) => {
150 + syncedHorizontalPanAndZoomViewsRef.current.forEach(
151 + syncedView =>
152 + triggeringView !== syncedView &&
153 + syncedView.setPanAndZoomState(newState),
154 + );
155 + };
156 +
157 + // Top content
158 +
159 + const topContentStack = new View(
160 + surface,
161 + defaultFrame,
162 + verticallyStackedLayout,
163 + );
164 +
165 + const axisMarkersView = new TimeAxisMarkersView(
166 + surface,
167 + defaultFrame,
168 + data.duration,
169 + );
170 + topContentStack.addSubview(axisMarkersView);
171 +
172 + if (data.otherUserTimingMarks.length > 0) {
173 + const userTimingMarksView = new UserTimingMarksView(
174 + surface,
175 + defaultFrame,
176 + data.otherUserTimingMarks,
177 + data.duration,
178 + );
179 + userTimingMarksViewRef.current = userTimingMarksView;
180 + topContentStack.addSubview(userTimingMarksView);
181 + }
182 +
183 + const reactEventsView = new ReactEventsView(surface, defaultFrame, data);
184 + reactEventsViewRef.current = reactEventsView;
185 + topContentStack.addSubview(reactEventsView);
186 +
187 + const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
188 + surface,
189 + defaultFrame,
190 + topContentStack,
191 + data.duration,
192 + syncAllHorizontalPanAndZoomViewStates,
193 + );
194 + syncedHorizontalPanAndZoomViewsRef.current.push(
195 + topContentHorizontalPanAndZoomView,
196 + );
197 +
198 + // Resizable content
199 +
200 + const reactMeasuresView = new ReactMeasuresView(
201 + surface,
202 + defaultFrame,
203 + data,
204 + );
205 + reactMeasuresViewRef.current = reactMeasuresView;
206 + const reactMeasuresVerticalScrollView = new VerticalScrollView(
207 + surface,
208 + defaultFrame,
209 + reactMeasuresView,
210 + );
211 + const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
212 + surface,
213 + defaultFrame,
214 + reactMeasuresVerticalScrollView,
215 + data.duration,
216 + syncAllHorizontalPanAndZoomViewStates,
217 + );
218 + syncedHorizontalPanAndZoomViewsRef.current.push(
219 + reactMeasuresHorizontalPanAndZoomView,
220 + );
221 +
222 + const flamechartView = new FlamechartView(
223 + surface,
224 + defaultFrame,
225 + data.flamechart,
226 + data.duration,
227 + );
228 + flamechartViewRef.current = flamechartView;
229 + const flamechartVerticalScrollView = new VerticalScrollView(
230 + surface,
231 + defaultFrame,
232 + flamechartView,
233 + );
234 + const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView(
235 + surface,
236 + defaultFrame,
237 + flamechartVerticalScrollView,
238 + data.duration,
239 + syncAllHorizontalPanAndZoomViewStates,
240 + );
241 + syncedHorizontalPanAndZoomViewsRef.current.push(
242 + flamechartHorizontalPanAndZoomView,
243 + );
244 +
245 + const resizableContentStack = new ResizableSplitView(
246 + surface,
247 + defaultFrame,
248 + reactMeasuresHorizontalPanAndZoomView,
249 + flamechartHorizontalPanAndZoomView,
250 + );
251 +
252 + const rootView = new View(
253 + surface,
254 + defaultFrame,
255 + createComposedLayout(
256 + verticallyStackedLayout,
257 + lastViewTakesUpRemainingSpaceLayout,
258 + ),
259 + );
260 + rootView.addSubview(topContentHorizontalPanAndZoomView);
261 + rootView.addSubview(resizableContentStack);
262 +
263 + surfaceRef.current.rootView = rootView;
264 + }, [data]);
265 +
266 + useLayoutEffect(() => {
267 + if (canvasRef.current) {
268 + surfaceRef.current.setCanvas(canvasRef.current, {width, height});
269 + }
270 + }, [width, height]);
271 +
272 + const interactor = useCallback(interaction => {
273 + if (canvasRef.current === null) {
274 + return;
275 + }
276 + surfaceRef.current.handleInteraction(interaction);
277 + // Defer drawing to canvas until React's commit phase, to avoid drawing
278 + // twice and to ensure that both the canvas and DOM elements managed by
279 + // React are in sync.
280 + setMouseLocation({
281 + x: interaction.payload.event.x,
282 + y: interaction.payload.event.y,
283 + });
284 + }, []);
285 +
286 + useCanvasInteraction(canvasRef, interactor);
287 +
288 + useContextMenu<ContextMenuContextData>({
289 + data: {
290 + data,
291 + hoveredEvent,
292 + },
293 + id: CONTEXT_MENU_ID,
294 + onChange: setIsContextMenuShown,
295 + ref: canvasRef,
296 + });
297 +
298 + useEffect(() => {
299 + const {current: userTimingMarksView} = userTimingMarksViewRef;
300 + if (userTimingMarksView) {
301 + userTimingMarksView.onHover = userTimingMark => {
302 + if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
303 + setHoveredEvent({
304 + userTimingMark,
305 + event: null,
306 + flamechartStackFrame: null,
307 + measure: null,
308 + data,
309 + });
310 + }
311 + };
312 + }
313 +
314 + const {current: reactEventsView} = reactEventsViewRef;
315 + if (reactEventsView) {
316 + reactEventsView.onHover = event => {
317 + if (!hoveredEvent || hoveredEvent.event !== event) {
318 + setHoveredEvent({
319 + userTimingMark: null,
320 + event,
321 + flamechartStackFrame: null,
322 + measure: null,
323 + data,
324 + });
325 + }
326 + };
327 + }
328 +
329 + const {current: reactMeasuresView} = reactMeasuresViewRef;
330 + if (reactMeasuresView) {
331 + reactMeasuresView.onHover = measure => {
332 + if (!hoveredEvent || hoveredEvent.measure !== measure) {
333 + setHoveredEvent({
334 + userTimingMark: null,
335 + event: null,
336 + flamechartStackFrame: null,
337 + measure,
338 + data,
339 + });
340 + }
341 + };
342 + }
343 +
344 + const {current: flamechartView} = flamechartViewRef;
345 + if (flamechartView) {
346 + flamechartView.setOnHover(flamechartStackFrame => {
347 + if (
348 + !hoveredEvent ||
349 + hoveredEvent.flamechartStackFrame !== flamechartStackFrame
350 + ) {
351 + setHoveredEvent({
352 + userTimingMark: null,
353 + event: null,
354 + flamechartStackFrame,
355 + measure: null,
356 + data,
357 + });
358 + }
359 + });
360 + }
361 + }, [hoveredEvent]);
362 +
363 + useLayoutEffect(() => {
364 + const {current: userTimingMarksView} = userTimingMarksViewRef;
365 + if (userTimingMarksView) {
366 + userTimingMarksView.setHoveredMark(
367 + hoveredEvent ? hoveredEvent.userTimingMark : null,
368 + );
369 + }
370 +
371 + const {current: reactEventsView} = reactEventsViewRef;
372 + if (reactEventsView) {
373 + reactEventsView.setHoveredEvent(hoveredEvent ? hoveredEvent.event : null);
374 + }
375 +
376 + const {current: reactMeasuresView} = reactMeasuresViewRef;
377 + if (reactMeasuresView) {
378 + reactMeasuresView.setHoveredMeasure(
379 + hoveredEvent ? hoveredEvent.measure : null,
380 + );
381 + }
382 +
383 + const {current: flamechartView} = flamechartViewRef;
384 + if (flamechartView) {
385 + flamechartView.setHoveredFlamechartStackFrame(
386 + hoveredEvent ? hoveredEvent.flamechartStackFrame : null,
387 + );
388 + }
389 + }, [hoveredEvent]);
390 +
391 + // Draw to canvas in React's commit phase
392 + useLayoutEffect(() => {
393 + surfaceRef.current.displayIfNeeded();
394 + });
395 +
396 + return (
397 + <Fragment>
398 + <canvas ref={canvasRef} height={height} width={width} />
399 + <ContextMenu id={CONTEXT_MENU_ID}>
400 + {(contextData: ContextMenuContextData) => {
401 + if (contextData.hoveredEvent == null) {
402 + return null;
403 + }
404 + const {
405 + event,
406 + flamechartStackFrame,
407 + measure,
408 + } = contextData.hoveredEvent;
409 + return (
410 + <Fragment>
411 + {event !== null && (
412 + <ContextMenuItem
413 + onClick={() => copy(event.componentName)}
414 + title="Copy component name">
415 + Copy component name
416 + </ContextMenuItem>
417 + )}
418 + {event !== null && (
419 + <ContextMenuItem
420 + onClick={() => copy(event.componentStack)}
421 + title="Copy component stack">
422 + Copy component stack
423 + </ContextMenuItem>
424 + )}
425 + {measure !== null && (
426 + <ContextMenuItem
427 + onClick={() =>
428 + zoomToBatch(
429 + contextData.data,
430 + measure,
431 + syncedHorizontalPanAndZoomViewsRef.current,
432 + )
433 + }
434 + title="Zoom to batch">
435 + Zoom to batch
436 + </ContextMenuItem>
437 + )}
438 + {measure !== null && (
439 + <ContextMenuItem
440 + onClick={() => copySummary(contextData.data, measure)}
441 + title="Copy summary">
442 + Copy summary
443 + </ContextMenuItem>
444 + )}
445 + {flamechartStackFrame !== null && (
446 + <ContextMenuItem
447 + onClick={() => copy(flamechartStackFrame.scriptUrl)}
448 + title="Copy file path">
449 + Copy file path
450 + </ContextMenuItem>
451 + )}
452 + {flamechartStackFrame !== null && (
453 + <ContextMenuItem
454 + onClick={() =>
455 + copy(
456 + `line ${flamechartStackFrame.locationLine ??
457 + ''}, column ${flamechartStackFrame.locationColumn ??
458 + ''}`,
459 + )
460 + }
461 + title="Copy location">
462 + Copy location
463 + </ContextMenuItem>
464 + )}
465 + </Fragment>
466 + );
467 + }}
468 + </ContextMenu>
469 + {!isContextMenuShown && (
470 + <EventTooltip
471 + data={data}
472 + hoveredEvent={hoveredEvent}
473 + origin={mouseLocation}
474 + />
475 + )}
476 + </Fragment>
477 + );
478 +}
479 +
480 +export default CanvasPage;
packages/react-devtools-scheduling-profiler/src/EventTooltip.css new
+78
@@ -0,0 +1,78 @@
1 +.Tooltip {
2 + position: fixed;
3 + display: inline-block;
4 + border-radius: 0.125rem;
5 + max-width: 300px;
6 + padding: 0.25rem;
7 + user-select: none;
8 + pointer-events: none;
9 + background-color: #ffffff;
10 + border: 1px solid #ccc;
11 + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
12 + font-size: 11px;
13 +}
14 +
15 +.Divider {
16 + height: 1px;
17 + background-color: #aaa;
18 + margin: 0.5rem 0;
19 +}
20 +
21 +.DetailsGrid {
22 + display: grid;
23 + padding-top: 5px;
24 + grid-gap: 2px 5px;
25 + grid-template-columns: min-content auto;
26 +}
27 +
28 +.DetailsGridLabel {
29 + color: #666;
30 + text-align: right;
31 +}
32 +
33 +.DetailsGridURL {
34 + word-break: break-all;
35 + max-height: 50vh;
36 + overflow: hidden;
37 +}
38 +
39 +.FlamechartStackFrameName {
40 + word-break: break-word;
41 + margin-left: 0.4rem;
42 +}
43 +
44 +.ComponentName {
45 + font-weight: bold;
46 + word-break: break-word;
47 + margin-right: 0.4rem;
48 +}
49 +
50 +.ComponentStack {
51 + overflow: hidden;
52 + max-width: 35em;
53 + max-height: 10em;
54 + margin: 0;
55 + font-size: 0.9em;
56 + line-height: 1.5;
57 + -webkit-mask-image: linear-gradient(
58 + 180deg,
59 + #fff,
60 + #fff 5em,
61 + transparent
62 + );
63 + mask-image: linear-gradient(
64 + 180deg,
65 + #fff,
66 + #fff 5em,
67 + transparent
68 + );
69 + white-space: pre;
70 +}
71 +
72 +.ReactMeasureLabel {
73 + margin-left: 0.4rem;
74 +}
75 +
76 +.UserTimingLabel {
77 + word-break: break-word;
78 +}
packages/react-devtools-scheduling-profiler/src/EventTooltip.js new
+292
@@ -0,0 +1,292 @@
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 {Point} from './view-base';
11 +import type {
12 + FlamechartStackFrame,
13 + ReactEvent,
14 + ReactHoverContextInfo,
15 + ReactMeasure,
16 + ReactProfilerData,
17 + Return,
18 + UserTimingMark,
19 +} from './types';
20 +
21 +import * as React from 'react';
22 +import {Fragment, useRef} from 'react';
23 +import prettyMilliseconds from 'pretty-ms';
24 +import {COLORS} from './content-views/constants';
25 +import {getBatchRange} from './utils/getBatchRange';
26 +import useSmartTooltip from './utils/useSmartTooltip';
27 +import styles from './EventTooltip.css';
28 +
29 +type Props = {|
30 + data: ReactProfilerData,
31 + hoveredEvent: ReactHoverContextInfo | null,
32 + origin: Point,
33 +|};
34 +
35 +function formatTimestamp(ms) {
36 + return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms';
37 +}
38 +
39 +function formatDuration(ms) {
40 + return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3});
41 +}
42 +
43 +function trimmedString(string: string, length: number): string {
44 + if (string.length > length) {
45 + return `${string.substr(0, length - 1)}…`;
46 + }
47 + return string;
48 +}
49 +
50 +function getReactEventLabel(type): string | null {
51 + switch (type) {
52 + case 'schedule-render':
53 + return 'render scheduled';
54 + case 'schedule-state-update':
55 + return 'state update scheduled';
56 + case 'schedule-force-update':
57 + return 'force update scheduled';
58 + case 'suspense-suspend':
59 + return 'suspended';
60 + case 'suspense-resolved':
61 + return 'suspense resolved';
62 + case 'suspense-rejected':
63 + return 'suspense rejected';
64 + default:
65 + return null;
66 + }
67 +}
68 +
69 +function getReactEventColor(event: ReactEvent): string | null {
70 + switch (event.type) {
71 + case 'schedule-render':
72 + return COLORS.REACT_SCHEDULE_HOVER;
73 + case 'schedule-state-update':
74 + case 'schedule-force-update':
75 + return event.isCascading
76 + ? COLORS.REACT_SCHEDULE_CASCADING_HOVER
77 + : COLORS.REACT_SCHEDULE_HOVER;
78 + case 'suspense-suspend':
79 + case 'suspense-resolved':
80 + case 'suspense-rejected':
81 + return COLORS.REACT_SUSPEND_HOVER;
82 + default:
83 + return null;
84 + }
85 +}
86 +
87 +function getReactMeasureLabel(type): string | null {
88 + switch (type) {
89 + case 'commit':
90 + return 'commit';
91 + case 'render-idle':
92 + return 'idle';
93 + case 'render':
94 + return 'render';
95 + case 'layout-effects':
96 + return 'layout effects';
97 + case 'passive-effects':
98 + return 'passive effects';
99 + default:
100 + return null;
101 + }
102 +}
103 +
104 +export default function EventTooltip({data, hoveredEvent, origin}: Props) {
105 + const tooltipRef = useSmartTooltip({
106 + mouseX: origin.x,
107 + mouseY: origin.y,
108 + });
109 +
110 + if (hoveredEvent === null) {
111 + return null;
112 + }
113 +
114 + const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent;
115 +
116 + if (event !== null) {
117 + return <TooltipReactEvent event={event} tooltipRef={tooltipRef} />;
118 + } else if (measure !== null) {
119 + return (
120 + <TooltipReactMeasure
121 + data={data}
122 + measure={measure}
123 + tooltipRef={tooltipRef}
124 + />
125 + );
126 + } else if (flamechartStackFrame !== null) {
127 + return (
128 + <TooltipFlamechartNode
129 + stackFrame={flamechartStackFrame}
130 + tooltipRef={tooltipRef}
131 + />
132 + );
133 + } else if (userTimingMark !== null) {
134 + return (
135 + <TooltipUserTimingMark mark={userTimingMark} tooltipRef={tooltipRef} />
136 + );
137 + }
138 + return null;
139 +}
140 +
141 +function formatComponentStack(componentStack: string): string {
142 + const lines = componentStack.split('\n').map(line => line.trim());
143 + lines.shift();
144 +
145 + if (lines.length > 5) {
146 + return lines.slice(0, 5).join('\n') + '\n...';
147 + }
148 + return lines.join('\n');
149 +}
150 +
151 +const TooltipFlamechartNode = ({
152 + stackFrame,
153 + tooltipRef,
154 +}: {
155 + stackFrame: FlamechartStackFrame,
156 + tooltipRef: Return<typeof useRef>,
157 +}) => {
158 + const {
159 + name,
160 + timestamp,
161 + duration,
162 + scriptUrl,
163 + locationLine,
164 + locationColumn,
165 + } = stackFrame;
166 + return (
167 + <div className={styles.Tooltip} ref={tooltipRef}>
168 + {formatDuration(duration)}
169 + <span className={styles.FlamechartStackFrameName}>{name}</span>
170 + <div className={styles.DetailsGrid}>
171 + <div className={styles.DetailsGridLabel}>Timestamp:</div>
172 + <div>{formatTimestamp(timestamp)}</div>
173 + {scriptUrl && (
174 + <>
175 + <div className={styles.DetailsGridLabel}>Script URL:</div>
176 + <div className={styles.DetailsGridURL}>{scriptUrl}</div>
177 + </>
178 + )}
179 + {(locationLine !== undefined || locationColumn !== undefined) && (
180 + <>
181 + <div className={styles.DetailsGridLabel}>Location:</div>
182 + <div>
183 + line {locationLine}, column {locationColumn}
184 + </div>
185 + </>
186 + )}
187 + </div>
188 + </div>
189 + );
190 +};
191 +
192 +const TooltipReactEvent = ({
193 + event,
194 + tooltipRef,
195 +}: {
196 + event: ReactEvent,
197 + tooltipRef: Return<typeof useRef>,
198 +}) => {
199 + const label = getReactEventLabel(event.type);
200 + const color = getReactEventColor(event);
201 + if (!label || !color) {
202 + if (__DEV__) {
203 + console.warn('Unexpected event type "%s"', event.type);
204 + }
205 + return null;
206 + }
207 +
208 + const {componentName, componentStack, timestamp} = event;
209 +
210 + return (
211 + <div className={styles.Tooltip} ref={tooltipRef}>
212 + {componentName && (
213 + <span className={styles.ComponentName} style={{color}}>
214 + {trimmedString(componentName, 768)}
215 + </span>
216 + )}
217 + {label}
218 + <div className={styles.Divider} />
219 + <div className={styles.DetailsGrid}>
220 + <div className={styles.DetailsGridLabel}>Timestamp:</div>
221 + <div>{formatTimestamp(timestamp)}</div>
222 + {componentStack && (
223 + <Fragment>
224 + <div className={styles.DetailsGridLabel}>Component stack:</div>
225 + <pre className={styles.ComponentStack}>
226 + {formatComponentStack(componentStack)}
227 + </pre>
228 + </Fragment>
229 + )}
230 + </div>
231 + </div>
232 + );
233 +};
234 +
235 +const TooltipReactMeasure = ({
236 + data,
237 + measure,
238 + tooltipRef,
239 +}: {
240 + data: ReactProfilerData,
241 + measure: ReactMeasure,
242 + tooltipRef: Return<typeof useRef>,
243 +}) => {
244 + const label = getReactMeasureLabel(measure.type);
245 + if (!label) {
246 + if (__DEV__) {
247 + console.warn('Unexpected measure type "%s"', measure.type);
248 + }
249 + return null;
250 + }
251 +
252 + const {batchUID, duration, timestamp, lanes} = measure;
253 + const [startTime, stopTime] = getBatchRange(batchUID, data);
254 +
255 + return (
256 + <div className={styles.Tooltip} ref={tooltipRef}>
257 + {formatDuration(duration)}
258 + <span className={styles.ReactMeasureLabel}>{label}</span>
259 + <div className={styles.Divider} />
260 + <div className={styles.DetailsGrid}>
261 + <div className={styles.DetailsGridLabel}>Timestamp:</div>
262 + <div>{formatTimestamp(timestamp)}</div>
263 + <div className={styles.DetailsGridLabel}>Batch duration:</div>
264 + <div>{formatDuration(stopTime - startTime)}</div>
265 + <div className={styles.DetailsGridLabel}>
266 + Lane{lanes.length === 1 ? '' : 's'}:
267 + </div>
268 + <div>{lanes.join(', ')}</div>
269 + </div>
270 + </div>
271 + );
272 +};
273 +
274 +const TooltipUserTimingMark = ({
275 + mark,
276 + tooltipRef,
277 +}: {
278 + mark: UserTimingMark,
279 + tooltipRef: Return<typeof useRef>,
280 +}) => {
281 + const {name, timestamp} = mark;
282 + return (
283 + <div className={styles.Tooltip} ref={tooltipRef}>
284 + <span className={styles.UserTimingLabel}>{name}</span>
285 + <div className={styles.Divider} />
286 + <div className={styles.DetailsGrid}>
287 + <div className={styles.DetailsGridLabel}>Timestamp:</div>
288 + <div>{formatTimestamp(timestamp)}</div>
289 + </div>
290 + </div>
291 + );
292 +};
packages/react-devtools-scheduling-profiler/src/ImportPage.css new
+206
@@ -0,0 +1,206 @@
1 +.App {
2 + background-color: #282c34;
3 + text-align: center;
4 +}
5 +
6 +.container {
7 + display: flex;
8 + flex-wrap: wrap;
9 + align-items: center;
10 + justify-content: center;
11 + font-size: calc(10px + 1.5vmin);
12 + min-height: 100vh;
13 +}
14 +
15 +.link {
16 + color: #282c34;
17 + transition: 0.2s;
18 + font-size: calc(10px + 1.5vmin);
19 +}
20 +.link:hover {
21 + color: #61dafb;
22 + transition: 0.2s;
23 + font-size: calc(10px + 1.5vmin);
24 +}
25 +
26 +kbd {
27 + display: inline-block;
28 + padding: 0 0.5em;
29 + border: 1px solid #d7dfe4;
30 + margin: 0 0.2em;
31 + background-color: #f6f6f6;
32 + border-radius: 0.2em;
33 +}
34 +
35 +/* Landing Graphic */
36 +.browserScreenshot {
37 + width: 35rem;
38 + max-width: inherit;
39 + align-self: center;
40 + justify-content: center;
41 + border: 1px solid #d7dfe4;
42 + border-radius: 0.4em;
43 + box-shadow: 0 2px 4px #ddd;
44 +}
45 +
46 +.legendKey {
47 + font-size: calc(8px + 1.5vmin);
48 + margin: 0;
49 + margin-bottom: 1rem;
50 +}
51 +
52 +.legendKey > svg {
53 + padding-left: 2rem;
54 +}
55 +
56 +@keyframes spin {
57 + from {
58 + transform: rotate(0deg);
59 + }
60 + to {
61 + transform: rotate(360deg);
62 + }
63 +}
64 +
65 +hr {
66 + margin-top: 0px;
67 + margin-left: 4px;
68 + width: 80%;
69 +}
70 +
71 +.row {
72 + display: flex;
73 + flex-direction: row;
74 + flex-wrap: wrap;
75 + width: 100%;
76 +}
77 +
78 +.column {
79 + padding: 1rem;
80 + display: flex;
81 + flex-direction: column;
82 + flex-basis: 100%;
83 + flex: 1;
84 +}
85 +
86 +.columncontent {
87 + display: flex;
88 + flex-direction: column;
89 + flex-basis: 100%;
90 + flex: 1;
91 + text-align: left;
92 +}
93 +
94 +/* Card Styling */
95 +.card {
96 + background-color: white;
97 + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
98 + border-radius: 5px;
99 + transition: 0.3s;
100 + width: 80%;
101 + color: black;
102 +}
103 +
104 +.card:hover {
105 + box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
106 +}
107 +
108 +.cardcontainer {
109 + padding: 40px 16px;
110 +}
111 +
112 +.inputbtn {
113 + display: none;
114 +}
115 +
116 +.buttongrp {
117 + float: left; /* Float the buttons side by side */
118 +}
119 +
120 +/* Import Button Styling */
121 +.ImportButton {
122 + background-color: #61dafb;
123 + border: none;
124 + color: #ffffff;
125 + text-align: center;
126 + font-size: 28px;
127 + padding: 20px;
128 + width: 200px;
129 + transition: all 0.3s;
130 + cursor: pointer;
131 + margin: 5px;
132 +}
133 +
134 +.ImportButton span {
135 + cursor: pointer;
136 + display: inline-block;
137 + position: relative;
138 + transition: 0.3s;
139 +}
140 +
141 +.ImportButton span:after {
142 + content: '\00bb';
143 + position: absolute;
144 + opacity: 0;
145 + top: 0;
146 + right: -20px;
147 + transition: 0.3s;
148 +}
149 +
150 +.ImportButton:hover {
151 + background-color: white;
152 + color: black;
153 +}
154 +
155 +.ImportButton:hover span {
156 + padding-right: 25px;
157 +}
158 +
159 +.ImportButton:hover span:after {
160 + opacity: 1;
161 + right: 0;
162 +}
163 +
164 +/* ViewSource Button styling */
165 +.ViewSourceButton {
166 + background-color: transparent;
167 + color: black;
168 + border: none;
169 + text-align: center;
170 + font-size: 28px;
171 + padding: 20px;
172 + width: 200px;
173 + transition: all 0.3s;
174 + cursor: pointer;
175 + margin: 5px;
176 +}
177 +
178 +.ViewSourceButton span {
179 + cursor: pointer;
180 + display: inline-block;
181 + position: relative;
182 + transition: 0.3s;
183 +}
184 +
185 +.ViewSourceButton span:after {
186 + content: '\00bb';
187 + position: absolute;
188 + opacity: 0;
189 + top: 0;
190 + right: -20px;
191 + transition: 0.3s;
192 +}
193 +
194 +.ViewSourceButton:hover {
195 + background-color: white;
196 + color: black;
197 +}
198 +
199 +.ViewSourceButton:hover span {
200 + padding-right: 25px;
201 +}
202 +
203 +.ViewSourceButton:hover span:after {
204 + opacity: 1;
205 + right: 0;
206 +}
packages/react-devtools-scheduling-profiler/src/ImportPage.js new
+119
@@ -0,0 +1,119 @@
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 {TimelineEvent} from '@elg/speedscope';
11 +import type {ReactProfilerData} from './types';
12 +
13 +import * as React from 'react';
14 +import {useCallback, useRef} from 'react';
15 +
16 +import profilerBrowser from './assets/profilerBrowser.png';
17 +import style from './ImportPage.css';
18 +
19 +import preprocessData from './utils/preprocessData';
20 +import {readInputData} from './utils/readInputData';
21 +
22 +type Props = {|
23 + onDataImported: (profilerData: ReactProfilerData) => void,
24 +|};
25 +
26 +export default function ImportPage({onDataImported}: Props) {
27 + const processTimeline = useCallback(
28 + (events: TimelineEvent[]) => {
29 + if (events.length > 0) {
30 + onDataImported(preprocessData(events));
31 + }
32 + },
33 + [onDataImported],
34 + );
35 +
36 + const handleProfilerInput = useCallback(
37 + async (event: SyntheticInputEvent<HTMLInputElement>) => {
38 + const readFile = await readInputData(event.target.files[0]);
39 + processTimeline(JSON.parse(readFile));
40 + },
41 + [processTimeline],
42 + );
43 +
44 + const upload = useRef(null);
45 +
46 + return (
47 + <div className={style.App}>
48 + <div className={style.container}>
49 + <div className={style.card}>
50 + <div className={style.cardcontainer}>
51 + <div className={style.row}>
52 + <div className={style.column}>
53 + <img
54 + src={profilerBrowser}
55 + className={style.browserScreenshot}
56 + alt="logo"
57 + />
58 + </div>
59 + <div className={style.columncontent}>
60 + <h2>React Concurrent Mode Profiler</h2>
61 + <hr />
62 + <p>
63 + Import a captured{' '}
64 + <a
65 + className={style.link}
66 + href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance">
67 + performance profile
68 + </a>{' '}
69 + from Chrome Devtools.
70 + <br />
71 + To zoom, scroll while holding down <kbd>Ctrl</kbd> or{' '}
72 + <kbd>Shift</kbd>
73 + </p>
74 + <p className={style.legendKey}>
75 + <svg height="20" width="20">
76 + <circle cx="10" cy="10" r="5" fill="#ff718e" />
77 + </svg>
78 + State Update Scheduled
79 + <br />
80 + <svg height="20" width="20">
81 + <circle cx="10" cy="10" r="5" fill="#9fc3f3" />
82 + </svg>
83 + State Update Scheduled
84 + <br />
85 + <svg height="20" width="20">
86 + <circle cx="10" cy="10" r="5" fill="#a6e59f" />
87 + </svg>
88 + Suspended
89 + </p>
90 +
91 + <div className={style.buttongrp}>
92 + <label htmlFor="upload">
93 + <button
94 + className={style.ImportButton}
95 + onClick={() => upload.current && upload.current.click()}>
96 + Import
97 + </button>
98 + <input
99 + type="file"
100 + ref={upload}
101 + className={style.inputbtn}
102 + onChange={handleProfilerInput}
103 + accept="application/json"
104 + />
105 + </label>
106 + <a href="https://github.com/MLH-Fellowship/scheduling-profiler-prototype">
107 + <button className={style.ViewSourceButton}>
108 + <span>Source </span>
109 + </button>
110 + </a>
111 + </div>
112 + </div>
113 + </div>
114 + </div>
115 + </div>
116 + </div>
117 + </div>
118 + );
119 +}
packages/react-devtools-scheduling-profiler/src/assets/logo.svg new
+1
@@ -0,0 +1 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170.96 159.93"><defs><style>.cls-1{fill:#09d3ac;}</style></defs><path class="cls-1" d="M141.35,73.27c0-6.89-8.11-13-20.58-16.73,3-12.67,1.69-22.75-4.28-26.16a9.55,9.55,0,0,0-4.79-1.2c-5.57,0-12.61,3.89-19.72,10.62-7.11-6.68-14.13-10.55-19.69-10.55a9.46,9.46,0,0,0-4.86,1.22c-5.94,3.42-7.17,13.44-4.21,26.05-12.41,3.71-20.48,9.77-20.5,16.63s8.12,13,20.59,16.72c-3,12.68-1.7,22.75,4.28,26.16a9.41,9.41,0,0,0,4.78,1.2c5.58,0,12.62-3.89,19.73-10.62,7.1,6.68,14.12,10.55,19.69,10.55a9.59,9.59,0,0,0,4.86-1.22c5.94-3.42,7.16-13.44,4.21-26C133.27,86.18,141.34,80.12,141.35,73.27ZM96.56,42.06c8.19-7.33,13.31-8.12,15.13-8.12h0a4.71,4.71,0,0,1,2.42.58c2.86,1.63,4,7.38,3.09,15a56.73,56.73,0,0,1-1,5.79,97.7,97.7,0,0,0-12.58-2.07,98.47,98.47,0,0,0-8.24-10.08C95.75,42.79,96.15,42.42,96.56,42.06ZM71.77,78.94c.78,1.5,1.61,3,2.47,4.51S76,86.49,77,88c-2.71-.39-5.34-.88-7.84-1.46C69.86,84,70.75,81.5,71.77,78.94ZM69,59.9c2.53-.59,5.18-1.08,7.93-1.46-1,1.48-1.86,3-2.76,4.59s-1.69,3-2.46,4.52Q70.18,63.65,69,59.9Zm5.21,13.34q1.86-3.93,4.09-7.86c1.5-2.62,3.11-5.17,4.77-7.61,2.91-.22,5.91-.34,9-.33s6,.12,8.89.35c1.66,2.42,3.25,5,4.75,7.55s2.88,5.22,4.12,7.83c-1.23,2.62-2.6,5.25-4.08,7.85s-3.11,5.18-4.77,7.62c-2.91.23-5.91.34-9,.34s-6-.13-8.89-.36c-1.66-2.42-3.26-4.94-4.76-7.55S75.5,75.85,74.25,73.24Zm32.86-14.77c2.72.38,5.35.87,7.84,1.45-.74,2.47-1.62,5-2.64,7.55-.79-1.5-1.61-3-2.48-4.51S108,59.93,107.11,58.47Zm2.76,24.92q1.29-2.27,2.46-4.53c1,2.6,2,5.16,2.7,7.66-2.52.59-5.17,1.07-7.92,1.45Q108.52,85.75,109.87,83.39ZM92,46.56c1.8,1.92,3.57,4,5.3,6.23-1.71-.07-3.46-.12-5.23-.12s-3.58,0-5.33.12C88.45,50.57,90.2,48.48,92,46.56Zm-22.18-12A4.82,4.82,0,0,1,72.29,34a13.11,13.11,0,0,1,5.19,1.31,39.07,39.07,0,0,1,10,6.78l1.17,1.07a98.49,98.49,0,0,0-8.16,10,98.88,98.88,0,0,0-12.65,2.06c-.44-1.94-.8-3.84-1-5.67C65.8,42,67,36.24,69.81,34.6ZM64.53,85.26a58.75,58.75,0,0,1-5.54-2c-7.1-3-11.5-6.85-11.5-10.14S51.91,66,59,63.05a57.63,57.63,0,0,1,5.44-1.94A97.93,97.93,0,0,0,69,73.25,98.72,98.72,0,0,0,64.53,85.26Zm23,19.1c-8.19,7.33-13.31,8.11-15.14,8.11a4.69,4.69,0,0,1-2.42-.58c-2.86-1.63-4-7.38-3.09-15a56.07,56.07,0,0,1,1-5.78,99.51,99.51,0,0,0,12.58,2.06,97.17,97.17,0,0,0,8.24,10.08Zm4.57-4.51c-1.8-1.92-3.57-4-5.31-6.23,1.72.08,3.47.12,5.24.12s3.58,0,5.33-.11C95.63,95.85,93.87,97.93,92.09,99.85Zm22.18,12a4.82,4.82,0,0,1-2.48.59c-1.82,0-7-.8-15.16-8.1l-1.17-1.07a98.44,98.44,0,0,0,8.15-10,97,97,0,0,0,12.66-2.06c.44,1.94.79,3.84,1,5.67C118.27,104.42,117.12,110.18,114.27,111.81Zm10.8-28.44c-1.71.7-3.52,1.35-5.44,1.93a98.54,98.54,0,0,0-4.57-12.14,98.1,98.1,0,0,0,4.49-12,58.75,58.75,0,0,1,5.54,2c7.09,3,11.5,6.85,11.49,10.14S132.17,80.42,125.07,83.37ZM92,82.39a9.18,9.18,0,1,0-9.17-9.19A9.17,9.17,0,0,0,92,82.39ZM31,17.88V128.53H153.07V17.88ZM148.3,123.77H35.78V22.65H148.3Zm-85-33.9c-3,12.68-1.7,22.75,4.28,26.16a9.41,9.41,0,0,0,4.78,1.2c5.58,0,12.62-3.89,19.73-10.62,7.1,6.68,14.12,10.55,19.69,10.55a9.59,9.59,0,0,0,4.86-1.22c5.94-3.42,7.16-13.44,4.21-26,12.41-3.72,20.48-9.78,20.49-16.63s-8.11-13-20.58-16.73c3-12.67,1.69-22.75-4.28-26.16a9.55,9.55,0,0,0-4.79-1.2c-5.57,0-12.61,3.89-19.72,10.62-7.11-6.68-14.13-10.55-19.69-10.55a9.46,9.46,0,0,0-4.86,1.22c-5.94,3.42-7.17,13.44-4.21,26.05-12.41,3.71-20.48,9.77-20.5,16.63S50.84,86.13,63.31,89.87Zm24.21,14.49c-8.19,7.33-13.31,8.11-15.14,8.11a4.69,4.69,0,0,1-2.42-.58c-2.86-1.63-4-7.38-3.09-15a56.07,56.07,0,0,1,1-5.78,99.51,99.51,0,0,0,12.58,2.06,97.17,97.17,0,0,0,8.24,10.08Zm24.79-36.89c-.79-1.5-1.61-3-2.48-4.51s-1.8-3-2.72-4.49c2.72.38,5.35.87,7.84,1.45C114.21,62.39,113.33,64.92,112.31,67.47Zm2.72,19c-2.52.59-5.17,1.07-7.92,1.45q1.41-2.22,2.76-4.58t2.46-4.53C113.37,81.46,114.28,84,115,86.52Zm-5.21-13.35c-1.23,2.62-2.6,5.25-4.08,7.85s-3.11,5.18-4.77,7.62c-2.91.23-5.91.34-9,.34s-6-.13-8.89-.36c-1.66-2.42-3.26-4.94-4.76-7.55s-2.87-5.22-4.12-7.83q1.86-3.93,4.09-7.86c1.5-2.62,3.11-5.17,4.77-7.61,2.91-.22,5.91-.34,9-.33s6,.12,8.89.35c1.66,2.42,3.25,5,4.75,7.55S108.58,70.56,109.82,73.17ZM77,88c-2.71-.39-5.34-.88-7.84-1.46.74-2.46,1.63-5,2.65-7.55.78,1.5,1.61,3,2.47,4.51S76,86.49,77,88ZM74.21,63c-.87,1.5-1.69,3-2.46,4.52Q70.18,63.65,69,59.9c2.53-.59,5.18-1.08,7.93-1.46C76,59.92,75.11,61.45,74.21,63ZM92.09,99.85c-1.8-1.92-3.57-4-5.31-6.23,1.72.08,3.47.12,5.24.12s3.58,0,5.33-.11C95.63,95.85,93.87,97.93,92.09,99.85Zm22.18,12a4.82,4.82,0,0,1-2.48.59c-1.82,0-7-.8-15.16-8.1l-1.17-1.07a98.44,98.44,0,0,0,8.15-10,97,97,0,0,0,12.66-2.06c.44,1.94.79,3.84,1,5.67C118.27,104.42,117.12,110.18,114.27,111.81Zm5.28-50.66a58.75,58.75,0,0,1,5.54,2c7.09,3,11.5,6.85,11.49,10.14s-4.41,7.16-11.51,10.11c-1.71.7-3.52,1.35-5.44,1.93a98.54,98.54,0,0,0-4.57-12.14A98.1,98.1,0,0,0,119.55,61.15Zm-23-19.09c8.19-7.33,13.31-8.12,15.13-8.12h0a4.71,4.71,0,0,1,2.42.58c2.86,1.63,4,7.38,3.09,15a56.73,56.73,0,0,1-1,5.79,97.7,97.7,0,0,0-12.58-2.07,98.47,98.47,0,0,0-8.24-10.08C95.75,42.79,96.15,42.42,96.56,42.06ZM92,46.56c1.8,1.92,3.57,4,5.3,6.23-1.71-.07-3.46-.12-5.23-.12s-3.58,0-5.33.12C88.45,50.57,90.2,48.48,92,46.56Zm-22.18-12A4.82,4.82,0,0,1,72.29,34a13.11,13.11,0,0,1,5.19,1.31,39.07,39.07,0,0,1,10,6.78l1.17,1.07a98.49,98.49,0,0,0-8.16,10,98.88,98.88,0,0,0-12.65,2.06c-.44-1.94-.8-3.84-1-5.67C65.8,42,67,36.24,69.81,34.6ZM59,63.05a57.63,57.63,0,0,1,5.44-1.94A97.93,97.93,0,0,0,69,73.25a98.72,98.72,0,0,0-4.49,12,58.75,58.75,0,0,1-5.54-2c-7.1-3-11.5-6.85-11.5-10.14S51.91,66,59,63.05Zm33,1a9.18,9.18,0,1,0,9.17,9.19A9.17,9.17,0,0,0,92,64Zm0,0a9.18,9.18,0,1,0,9.17,9.19A9.17,9.17,0,0,0,92,64Zm0,0a9.18,9.18,0,1,0,9.17,9.19A9.17,9.17,0,0,0,92,64Zm49.35,9.24c0-6.89-8.11-13-20.58-16.73,3-12.67,1.69-22.75-4.28-26.16a9.55,9.55,0,0,0-4.79-1.2c-5.57,0-12.61,3.89-19.72,10.62-7.11-6.68-14.13-10.55-19.69-10.55a9.46,9.46,0,0,0-4.86,1.22c-5.94,3.42-7.17,13.44-4.21,26.05-12.41,3.71-20.48,9.77-20.5,16.63s8.12,13,20.59,16.72c-3,12.68-1.7,22.75,4.28,26.16a9.41,9.41,0,0,0,4.78,1.2c5.58,0,12.62-3.89,19.73-10.62,7.1,6.68,14.12,10.55,19.69,10.55a9.59,9.59,0,0,0,4.86-1.22c5.94-3.42,7.16-13.44,4.21-26C133.27,86.18,141.34,80.12,141.35,73.27ZM96.56,42.06c8.19-7.33,13.31-8.12,15.13-8.12h0a4.71,4.71,0,0,1,2.42.58c2.86,1.63,4,7.38,3.09,15a56.73,56.73,0,0,1-1,5.79,97.7,97.7,0,0,0-12.58-2.07,98.47,98.47,0,0,0-8.24-10.08C95.75,42.79,96.15,42.42,96.56,42.06ZM71.77,78.94c.78,1.5,1.61,3,2.47,4.51S76,86.49,77,88c-2.71-.39-5.34-.88-7.84-1.46C69.86,84,70.75,81.5,71.77,78.94ZM69,59.9c2.53-.59,5.18-1.08,7.93-1.46-1,1.48-1.86,3-2.76,4.59s-1.69,3-2.46,4.52Q70.18,63.65,69,59.9Zm5.21,13.34q1.86-3.93,4.09-7.86c1.5-2.62,3.11-5.17,4.77-7.61,2.91-.22,5.91-.34,9-.33s6,.12,8.89.35c1.66,2.42,3.25,5,4.75,7.55s2.88,5.22,4.12,7.83c-1.23,2.62-2.6,5.25-4.08,7.85s-3.11,5.18-4.77,7.62c-2.91.23-5.91.34-9,.34s-6-.13-8.89-.36c-1.66-2.42-3.26-4.94-4.76-7.55S75.5,75.85,74.25,73.24Zm32.86-14.77c2.72.38,5.35.87,7.84,1.45-.74,2.47-1.62,5-2.64,7.55-.79-1.5-1.61-3-2.48-4.51S108,59.93,107.11,58.47Zm2.76,24.92q1.29-2.27,2.46-4.53c1,2.6,2,5.16,2.7,7.66-2.52.59-5.17,1.07-7.92,1.45Q108.52,85.75,109.87,83.39ZM92,46.56c1.8,1.92,3.57,4,5.3,6.23-1.71-.07-3.46-.12-5.23-.12s-3.58,0-5.33.12C88.45,50.57,90.2,48.48,92,46.56Zm-22.18-12A4.82,4.82,0,0,1,72.29,34a13.11,13.11,0,0,1,5.19,1.31,39.07,39.07,0,0,1,10,6.78l1.17,1.07a98.49,98.49,0,0,0-8.16,10,98.88,98.88,0,0,0-12.65,2.06c-.44-1.94-.8-3.84-1-5.67C65.8,42,67,36.24,69.81,34.6ZM64.53,85.26a58.75,58.75,0,0,1-5.54-2c-7.1-3-11.5-6.85-11.5-10.14S51.91,66,59,63.05a57.63,57.63,0,0,1,5.44-1.94A97.93,97.93,0,0,0,69,73.25,98.72,98.72,0,0,0,64.53,85.26Zm23,19.1c-8.19,7.33-13.31,8.11-15.14,8.11a4.69,4.69,0,0,1-2.42-.58c-2.86-1.63-4-7.38-3.09-15a56.07,56.07,0,0,1,1-5.78,99.51,99.51,0,0,0,12.58,2.06,97.17,97.17,0,0,0,8.24,10.08Zm4.57-4.51c-1.8-1.92-3.57-4-5.31-6.23,1.72.08,3.47.12,5.24.12s3.58,0,5.33-.11C95.63,95.85,93.87,97.93,92.09,99.85Zm22.18,12a4.82,4.82,0,0,1-2.48.59c-1.82,0-7-.8-15.16-8.1l-1.17-1.07a98.44,98.44,0,0,0,8.15-10,97,97,0,0,0,12.66-2.06c.44,1.94.79,3.84,1,5.67C118.27,104.42,117.12,110.18,114.27,111.81Zm10.8-28.44c-1.71.7-3.52,1.35-5.44,1.93a98.54,98.54,0,0,0-4.57-12.14,98.1,98.1,0,0,0,4.49-12,58.75,58.75,0,0,1,5.54,2c7.09,3,11.5,6.85,11.49,10.14S132.17,80.42,125.07,83.37ZM92,82.39a9.18,9.18,0,1,0-9.17-9.19A9.17,9.17,0,0,0,92,82.39Zm-69.32,54.5V26.2L17.89,31V141.66H139.94l4.78-4.77Z"/></svg>
\ No newline at end of file
packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png
Binary files /dev/null and b/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png differ
packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg new
+7
@@ -0,0 +1,7 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
2 + <g fill="#61DAFB">
3 + <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
4 + <circle cx="420.9" cy="296.5" r="45.7"/>
5 + <path d="M520.5 78.1z"/>
6 + </g>
7 +</svg>
packages/react-devtools-scheduling-profiler/src/constants.js new
+11
@@ -0,0 +1,11 @@
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 +// App constants
10 +
11 +export const REACT_TOTAL_NUM_LANES = 31;
packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js new
+378
@@ -0,0 +1,378 @@
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 + Flamechart,
12 + FlamechartStackFrame,
13 + FlamechartStackLayer,
14 +} from '../types';
15 +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
16 +
17 +import {
18 + ColorView,
19 + Surface,
20 + View,
21 + layeredLayout,
22 + rectContainsPoint,
23 + rectEqualToRect,
24 + intersectionOfRects,
25 + rectIntersectsRect,
26 + verticallyStackedLayout,
27 +} from '../view-base';
28 +import {
29 + durationToWidth,
30 + positioningScaleFactor,
31 + timestampToPosition,
32 +} from './utils/positioning';
33 +import {
34 + COLORS,
35 + FLAMECHART_FONT_SIZE,
36 + FLAMECHART_FRAME_HEIGHT,
37 + FLAMECHART_TEXT_PADDING,
38 + COLOR_HOVER_DIM_DELTA,
39 + BORDER_SIZE,
40 +} from './constants';
41 +import {ColorGenerator, dimmedColor, hslaColorToString} from './utils/colors';
42 +
43 +// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399
44 +const colorGenerator = new ColorGenerator(
45 + {min: 30, max: 330},
46 + {min: 50, max: 80, count: 3},
47 + 85,
48 +);
49 +colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100});
50 +
51 +function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) {
52 + return colorGenerator.colorForID(scriptUrl ?? '');
53 +}
54 +
55 +function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string {
56 + const color = defaultHslaColorForStackFrame(stackFrame);
57 + return hslaColorToString(color);
58 +}
59 +
60 +function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
61 + const color = dimmedColor(
62 + defaultHslaColorForStackFrame(stackFrame),
63 + COLOR_HOVER_DIM_DELTA,
64 + );
65 + return hslaColorToString(color);
66 +}
67 +
68 +const cachedFlamechartTextWidths = new Map();
69 +const trimFlamechartText = (
70 + context: CanvasRenderingContext2D,
71 + text: string,
72 + width: number,
73 +) => {
74 + for (let i = text.length - 1; i >= 0; i--) {
75 + const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
76 +
77 + let measuredWidth = cachedFlamechartTextWidths.get(trimmedText);
78 + if (measuredWidth == null) {
79 + measuredWidth = context.measureText(trimmedText).width;
80 + cachedFlamechartTextWidths.set(trimmedText, measuredWidth);
81 + }
82 +
83 + if (measuredWidth <= width) {
84 + return trimmedText;
85 + }
86 + }
87 +
88 + return null;
89 +};
90 +
91 +class FlamechartStackLayerView extends View {
92 + /** Layer to display */
93 + _stackLayer: FlamechartStackLayer;
94 +
95 + /** A set of `stackLayer`'s frames, for efficient lookup. */
96 + _stackFrameSet: Set<FlamechartStackFrame>;
97 +
98 + _intrinsicSize: Size;
99 +
100 + _hoveredStackFrame: FlamechartStackFrame | null = null;
101 + _onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
102 +
103 + constructor(
104 + surface: Surface,
105 + frame: Rect,
106 + stackLayer: FlamechartStackLayer,
107 + duration: number,
108 + ) {
109 + super(surface, frame);
110 + this._stackLayer = stackLayer;
111 + this._stackFrameSet = new Set(stackLayer);
112 + this._intrinsicSize = {
113 + width: duration,
114 + height: FLAMECHART_FRAME_HEIGHT,
115 + };
116 + }
117 +
118 + desiredSize() {
119 + return this._intrinsicSize;
120 + }
121 +
122 + setHoveredFlamechartStackFrame(
123 + hoveredStackFrame: FlamechartStackFrame | null,
124 + ) {
125 + if (this._hoveredStackFrame === hoveredStackFrame) {
126 + return; // We're already hovering over this frame
127 + }
128 +
129 + // Only care about frames displayed by this view.
130 + const stackFrameToSet =
131 + hoveredStackFrame && this._stackFrameSet.has(hoveredStackFrame)
132 + ? hoveredStackFrame
133 + : null;
134 + if (this._hoveredStackFrame === stackFrameToSet) {
135 + return; // Resulting state is unchanged
136 + }
137 + this._hoveredStackFrame = stackFrameToSet;
138 + this.setNeedsDisplay();
139 + }
140 +
141 + draw(context: CanvasRenderingContext2D) {
142 + const {
143 + frame,
144 + _stackLayer,
145 + _hoveredStackFrame,
146 + _intrinsicSize,
147 + visibleArea,
148 + } = this;
149 +
150 + context.fillStyle = COLORS.BACKGROUND;
151 + context.fillRect(
152 + visibleArea.origin.x,
153 + visibleArea.origin.y,
154 + visibleArea.size.width,
155 + visibleArea.size.height,
156 + );
157 +
158 + context.textAlign = 'left';
159 + context.textBaseline = 'middle';
160 + context.font = `${FLAMECHART_FONT_SIZE}px sans-serif`;
161 +
162 + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
163 +
164 + for (let i = 0; i < _stackLayer.length; i++) {
165 + const stackFrame = _stackLayer[i];
166 + const {name, timestamp, duration} = stackFrame;
167 +
168 + const width = durationToWidth(duration, scaleFactor);
169 + if (width < 1) {
170 + continue; // Too small to render at this zoom level
171 + }
172 +
173 + const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
174 + const nodeRect: Rect = {
175 + origin: {x, y: frame.origin.y},
176 + size: {
177 + width: Math.floor(width - BORDER_SIZE),
178 + height: Math.floor(FLAMECHART_FRAME_HEIGHT - BORDER_SIZE),
179 + },
180 + };
181 + if (!rectIntersectsRect(nodeRect, visibleArea)) {
182 + continue; // Not in view
183 + }
184 +
185 + const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];
186 + context.fillStyle = showHoverHighlight
187 + ? hoverColorForStackFrame(stackFrame)
188 + : defaultColorForStackFrame(stackFrame);
189 +
190 + const drawableRect = intersectionOfRects(nodeRect, visibleArea);
191 + context.fillRect(
192 + drawableRect.origin.x,
193 + drawableRect.origin.y,
194 + drawableRect.size.width,
195 + drawableRect.size.height,
196 + );
197 +
198 + if (width > FLAMECHART_TEXT_PADDING * 2) {
199 + const trimmedName = trimFlamechartText(
200 + context,
201 + name,
202 + width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0),
203 + );
204 +
205 + if (trimmedName !== null) {
206 + context.fillStyle = COLORS.PRIORITY_LABEL;
207 +
208 + // Prevent text from being drawn outside `viewableArea`
209 + const textOverflowsViewableArea = !rectEqualToRect(
210 + drawableRect,
211 + nodeRect,
212 + );
213 + if (textOverflowsViewableArea) {
214 + context.save();
215 + context.beginPath();
216 + context.rect(
217 + drawableRect.origin.x,
218 + drawableRect.origin.y,
219 + drawableRect.size.width,
220 + drawableRect.size.height,
221 + );
222 + context.closePath();
223 + context.clip();
224 + }
225 +
226 + context.fillText(
227 + trimmedName,
228 + nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0),
229 + nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2,
230 + );
231 +
232 + if (textOverflowsViewableArea) {
233 + context.restore();
234 + }
235 + }
236 + }
237 + }
238 + }
239 +
240 + /**
241 + * @private
242 + */
243 + _handleMouseMove(interaction: MouseMoveInteraction) {
244 + const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this;
245 + const {location} = interaction.payload;
246 + if (!_onHover || !rectContainsPoint(location, visibleArea)) {
247 + return;
248 + }
249 +
250 + // Find the node being hovered over.
251 + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
252 + let startIndex = 0;
253 + let stopIndex = _stackLayer.length - 1;
254 + while (startIndex <= stopIndex) {
255 + const currentIndex = Math.floor((startIndex + stopIndex) / 2);
256 + const flamechartStackFrame = _stackLayer[currentIndex];
257 + const {timestamp, duration} = flamechartStackFrame;
258 +
259 + const width = durationToWidth(duration, scaleFactor);
260 + const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
261 + if (x <= location.x && x + width >= location.x) {
262 + _onHover(flamechartStackFrame);
263 + return;
264 + }
265 +
266 + if (x > location.x) {
267 + stopIndex = currentIndex - 1;
268 + } else {
269 + startIndex = currentIndex + 1;
270 + }
271 + }
272 +
273 + _onHover(null);
274 + }
275 +
276 + handleInteraction(interaction: Interaction) {
277 + switch (interaction.type) {
278 + case 'mousemove':
279 + this._handleMouseMove(interaction);
280 + break;
281 + }
282 + }
283 +}
284 +
285 +export class FlamechartView extends View {
286 + _flamechartRowViews: FlamechartStackLayerView[] = [];
287 +
288 + /** Container view that vertically stacks flamechart rows */
289 + _verticalStackView: View;
290 +
291 + _hoveredStackFrame: FlamechartStackFrame | null = null;
292 + _onHover: ((node: FlamechartStackFrame | null) => void) | null = null;
293 +
294 + constructor(
295 + surface: Surface,
296 + frame: Rect,
297 + flamechart: Flamechart,
298 + duration: number,
299 + ) {
300 + super(surface, frame, layeredLayout);
301 + this.setDataAndUpdateSubviews(flamechart, duration);
302 + }
303 +
304 + setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) {
305 + const {surface, frame, _onHover, _hoveredStackFrame} = this;
306 +
307 + // Clear existing rows on data update
308 + if (this._verticalStackView) {
309 + this.removeAllSubviews();
310 + this._flamechartRowViews = [];
311 + }
312 +
313 + this._verticalStackView = new View(surface, frame, verticallyStackedLayout);
314 + this._flamechartRowViews = flamechart.map(stackLayer => {
315 + const rowView = new FlamechartStackLayerView(
316 + surface,
317 + frame,
318 + stackLayer,
319 + duration,
320 + );
321 + this._verticalStackView.addSubview(rowView);
322 +
323 + // Update states
324 + rowView._onHover = _onHover;
325 + rowView.setHoveredFlamechartStackFrame(_hoveredStackFrame);
326 + return rowView;
327 + });
328 +
329 + // Add a plain background view to prevent gaps from appearing between
330 + // flamechartRowViews.
331 + const colorView = new ColorView(surface, frame, COLORS.BACKGROUND);
332 + this.addSubview(colorView);
333 + this.addSubview(this._verticalStackView);
334 + }
335 +
336 + setHoveredFlamechartStackFrame(
337 + hoveredStackFrame: FlamechartStackFrame | null,
338 + ) {
339 + this._hoveredStackFrame = hoveredStackFrame;
340 + this._flamechartRowViews.forEach(rowView =>
341 + rowView.setHoveredFlamechartStackFrame(hoveredStackFrame),
342 + );
343 + }
344 +
345 + setOnHover(onHover: (node: FlamechartStackFrame | null) => void) {
346 + this._onHover = onHover;
347 + this._flamechartRowViews.forEach(rowView => (rowView._onHover = onHover));
348 + }
349 +
350 + desiredSize() {
351 + // Ignore the wishes of the background color view
352 + return this._verticalStackView.desiredSize();
353 + }
354 +
355 + /**
356 + * @private
357 + */
358 + _handleMouseMove(interaction: MouseMoveInteraction) {
359 + const {_onHover, visibleArea} = this;
360 + if (!_onHover) {
361 + return;
362 + }
363 +
364 + const {location} = interaction.payload;
365 + if (!rectContainsPoint(location, visibleArea)) {
366 + // Clear out any hovered flamechart stack frame
367 + _onHover(null);
368 + }
369 + }
370 +
371 + handleInteraction(interaction: Interaction) {
372 + switch (interaction.type) {
373 + case 'mousemove':
374 + this._handleMouseMove(interaction);
375 + break;
376 + }
377 + }
378 +}
packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js new
+276
@@ -0,0 +1,276 @@
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 {ReactEvent, ReactProfilerData} from '../types';
11 +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
12 +
13 +import {
14 + positioningScaleFactor,
15 + timestampToPosition,
16 + positionToTimestamp,
17 + widthToDuration,
18 +} from './utils/positioning';
19 +import {
20 + View,
21 + Surface,
22 + rectContainsPoint,
23 + rectIntersectsRect,
24 + intersectionOfRects,
25 +} from '../view-base';
26 +import {
27 + COLORS,
28 + EVENT_ROW_PADDING,
29 + EVENT_DIAMETER,
30 + BORDER_SIZE,
31 +} from './constants';
32 +
33 +const EVENT_ROW_HEIGHT_FIXED =
34 + EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
35 +
36 +function isSuspenseEvent(event: ReactEvent): boolean %checks {
37 + return (
38 + event.type === 'suspense-suspend' ||
39 + event.type === 'suspense-resolved' ||
40 + event.type === 'suspense-rejected'
41 + );
42 +}
43 +
44 +export class ReactEventsView extends View {
45 + _profilerData: ReactProfilerData;
46 + _intrinsicSize: Size;
47 +
48 + _hoveredEvent: ReactEvent | null = null;
49 + onHover: ((event: ReactEvent | null) => void) | null = null;
50 +
51 + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
52 + super(surface, frame);
53 + this._profilerData = profilerData;
54 +
55 + this._intrinsicSize = {
56 + width: this._profilerData.duration,
57 + height: EVENT_ROW_HEIGHT_FIXED,
58 + };
59 + }
60 +
61 + desiredSize() {
62 + return this._intrinsicSize;
63 + }
64 +
65 + setHoveredEvent(hoveredEvent: ReactEvent | null) {
66 + if (this._hoveredEvent === hoveredEvent) {
67 + return;
68 + }
69 + this._hoveredEvent = hoveredEvent;
70 + this.setNeedsDisplay();
71 + }
72 +
73 + /**
74 + * Draw a single `ReactEvent` as a circle in the canvas.
75 + */
76 + _drawSingleReactEvent(
77 + context: CanvasRenderingContext2D,
78 + rect: Rect,
79 + event: ReactEvent,
80 + baseY: number,
81 + scaleFactor: number,
82 + showHoverHighlight: boolean,
83 + ) {
84 + const {frame} = this;
85 + const {timestamp, type} = event;
86 +
87 + const x = timestampToPosition(timestamp, scaleFactor, frame);
88 + const radius = EVENT_DIAMETER / 2;
89 + const eventRect: Rect = {
90 + origin: {
91 + x: x - radius,
92 + y: baseY,
93 + },
94 + size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER},
95 + };
96 + if (!rectIntersectsRect(eventRect, rect)) {
97 + return; // Not in view
98 + }
99 +
100 + let fillStyle = null;
101 +
102 + switch (type) {
103 + case 'schedule-render':
104 + case 'schedule-state-update':
105 + case 'schedule-force-update':
106 + if (event.isCascading) {
107 + fillStyle = showHoverHighlight
108 + ? COLORS.REACT_SCHEDULE_CASCADING_HOVER
109 + : COLORS.REACT_SCHEDULE_CASCADING;
110 + } else {
111 + fillStyle = showHoverHighlight
112 + ? COLORS.REACT_SCHEDULE_HOVER
113 + : COLORS.REACT_SCHEDULE;
114 + }
115 + break;
116 + case 'suspense-suspend':
117 + case 'suspense-resolved':
118 + case 'suspense-rejected':
119 + fillStyle = showHoverHighlight
120 + ? COLORS.REACT_SUSPEND_HOVER
121 + : COLORS.REACT_SUSPEND;
122 + break;
123 + default:
124 + if (__DEV__) {
125 + console.warn('Unexpected event type "%s"', type);
126 + }
127 + break;
128 + }
129 +
130 + if (fillStyle !== null) {
131 + const y = eventRect.origin.y + radius;
132 +
133 + context.beginPath();
134 + context.fillStyle = fillStyle;
135 + context.arc(x, y, radius, 0, 2 * Math.PI);
136 + context.fill();
137 + }
138 + }
139 +
140 + draw(context: CanvasRenderingContext2D) {
141 + const {
142 + frame,
143 + _profilerData: {events},
144 + _hoveredEvent,
145 + visibleArea,
146 + } = this;
147 +
148 + context.fillStyle = COLORS.BACKGROUND;
149 + context.fillRect(
150 + visibleArea.origin.x,
151 + visibleArea.origin.y,
152 + visibleArea.size.width,
153 + visibleArea.size.height,
154 + );
155 +
156 + // Draw events
157 + const baseY = frame.origin.y + EVENT_ROW_PADDING;
158 + const scaleFactor = positioningScaleFactor(
159 + this._intrinsicSize.width,
160 + frame,
161 + );
162 +
163 + const highlightedEvents: ReactEvent[] = [];
164 +
165 + events.forEach(event => {
166 + if (
167 + event === _hoveredEvent ||
168 + (_hoveredEvent &&
169 + isSuspenseEvent(event) &&
170 + isSuspenseEvent(_hoveredEvent) &&
171 + event.id === _hoveredEvent.id)
172 + ) {
173 + highlightedEvents.push(event);
174 + return;
175 + }
176 + this._drawSingleReactEvent(
177 + context,
178 + visibleArea,
179 + event,
180 + baseY,
181 + scaleFactor,
182 + false,
183 + );
184 + });
185 +
186 + // Draw the highlighted items on top so they stand out.
187 + // This is helpful if there are multiple (overlapping) items close to each other.
188 + highlightedEvents.forEach(event => {
189 + this._drawSingleReactEvent(
190 + context,
191 + visibleArea,
192 + event,
193 + baseY,
194 + scaleFactor,
195 + true,
196 + );
197 + });
198 +
199 + // Render bottom border.
200 + // Propose border rect, check if intersects with `rect`, draw intersection.
201 + const borderFrame: Rect = {
202 + origin: {
203 + x: frame.origin.x,
204 + y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE,
205 + },
206 + size: {
207 + width: frame.size.width,
208 + height: BORDER_SIZE,
209 + },
210 + };
211 + if (rectIntersectsRect(borderFrame, visibleArea)) {
212 + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
213 + context.fillStyle = COLORS.PRIORITY_BORDER;
214 + context.fillRect(
215 + borderDrawableRect.origin.x,
216 + borderDrawableRect.origin.y,
217 + borderDrawableRect.size.width,
218 + borderDrawableRect.size.height,
219 + );
220 + }
221 + }
222 +
223 + /**
224 + * @private
225 + */
226 + _handleMouseMove(interaction: MouseMoveInteraction) {
227 + const {frame, onHover, visibleArea} = this;
228 + if (!onHover) {
229 + return;
230 + }
231 +
232 + const {location} = interaction.payload;
233 + if (!rectContainsPoint(location, visibleArea)) {
234 + onHover(null);
235 + return;
236 + }
237 +
238 + const {
239 + _profilerData: {events},
240 + } = this;
241 + const scaleFactor = positioningScaleFactor(
242 + this._intrinsicSize.width,
243 + frame,
244 + );
245 + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
246 + const eventTimestampAllowance = widthToDuration(
247 + EVENT_DIAMETER / 2,
248 + scaleFactor,
249 + );
250 +
251 + // Because data ranges may overlap, we want to find the last intersecting item.
252 + // This will always be the one on "top" (the one the user is hovering over).
253 + for (let index = events.length - 1; index >= 0; index--) {
254 + const event = events[index];
255 + const {timestamp} = event;
256 +
257 + if (
258 + timestamp - eventTimestampAllowance <= hoverTimestamp &&
259 + hoverTimestamp <= timestamp + eventTimestampAllowance
260 + ) {
261 + onHover(event);
262 + return;
263 + }
264 + }
265 +
266 + onHover(null);
267 + }
268 +
269 + handleInteraction(interaction: Interaction) {
270 + switch (interaction.type) {
271 + case 'mousemove':
272 + this._handleMouseMove(interaction);
273 + break;
274 + }
275 + }
276 +}
packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js new
+318
@@ -0,0 +1,318 @@
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 {ReactLane, ReactMeasure, ReactProfilerData} from '../types';
11 +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
12 +
13 +import {
14 + durationToWidth,
15 + positioningScaleFactor,
16 + positionToTimestamp,
17 + timestampToPosition,
18 +} from './utils/positioning';
19 +import {
20 + View,
21 + Surface,
22 + rectContainsPoint,
23 + rectIntersectsRect,
24 + intersectionOfRects,
25 +} from '../view-base';
26 +
27 +import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
28 +import {REACT_TOTAL_NUM_LANES} from '../constants';
29 +
30 +const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
31 +
32 +function getMeasuresForLane(
33 + allMeasures: ReactMeasure[],
34 + lane: ReactLane,
35 +): ReactMeasure[] {
36 + return allMeasures.filter(measure => measure.lanes.includes(lane));
37 +}
38 +
39 +export class ReactMeasuresView extends View {
40 + _profilerData: ReactProfilerData;
41 + _intrinsicSize: Size;
42 +
43 + _lanesToRender: ReactLane[];
44 + _laneToMeasures: Map<ReactLane, ReactMeasure[]>;
45 +
46 + _hoveredMeasure: ReactMeasure | null = null;
47 + onHover: ((measure: ReactMeasure | null) => void) | null = null;
48 +
49 + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
50 + super(surface, frame);
51 + this._profilerData = profilerData;
52 + this._performPreflightComputations();
53 + }
54 +
55 + _performPreflightComputations() {
56 + this._lanesToRender = [];
57 + this._laneToMeasures = new Map<ReactLane, ReactMeasure[]>();
58 +
59 + for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
60 + const measuresForLane = getMeasuresForLane(
61 + this._profilerData.measures,
62 + lane,
63 + );
64 + // Only show lanes with measures
65 + if (measuresForLane.length) {
66 + this._lanesToRender.push(lane);
67 + this._laneToMeasures.set(lane, measuresForLane);
68 + }
69 + }
70 +
71 + this._intrinsicSize = {
72 + width: this._profilerData.duration,
73 + height: this._lanesToRender.length * REACT_LANE_HEIGHT,
74 + };
75 + }
76 +
77 + desiredSize() {
78 + return this._intrinsicSize;
79 + }
80 +
81 + setHoveredMeasure(hoveredMeasure: ReactMeasure | null) {
82 + if (this._hoveredMeasure === hoveredMeasure) {
83 + return;
84 + }
85 + this._hoveredMeasure = hoveredMeasure;
86 + this.setNeedsDisplay();
87 + }
88 +
89 + /**
90 + * Draw a single `ReactMeasure` as a bar in the canvas.
91 + */
92 + _drawSingleReactMeasure(
93 + context: CanvasRenderingContext2D,
94 + rect: Rect,
95 + measure: ReactMeasure,
96 + baseY: number,
97 + scaleFactor: number,
98 + showGroupHighlight: boolean,
99 + showHoverHighlight: boolean,
100 + ) {
101 + const {frame} = this;
102 + const {timestamp, type, duration} = measure;
103 +
104 + let fillStyle = null;
105 + let hoveredFillStyle = null;
106 + let groupSelectedFillStyle = null;
107 +
108 + // We could change the max to 0 and just skip over rendering anything that small,
109 + // but this has the effect of making the chart look very empty when zoomed out.
110 + // So long as perf is okay- it might be best to err on the side of showing things.
111 + const width = durationToWidth(duration, scaleFactor);
112 + if (width <= 0) {
113 + return; // Too small to render at this zoom level
114 + }
115 +
116 + const x = timestampToPosition(timestamp, scaleFactor, frame);
117 + const measureRect: Rect = {
118 + origin: {x, y: baseY},
119 + size: {width, height: REACT_MEASURE_HEIGHT},
120 + };
121 + if (!rectIntersectsRect(measureRect, rect)) {
122 + return; // Not in view
123 + }
124 +
125 + switch (type) {
126 + case 'commit':
127 + fillStyle = COLORS.REACT_COMMIT;
128 + hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
129 + groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED;
130 + break;
131 + case 'render-idle':
132 + // We could render idle time as diagonal hashes.
133 + // This looks nicer when zoomed in, but not so nice when zoomed out.
134 + // color = context.createPattern(getIdlePattern(), 'repeat');
135 + fillStyle = COLORS.REACT_IDLE;
136 + hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
137 + groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED;
138 + break;
139 + case 'render':
140 + fillStyle = COLORS.REACT_RENDER;
141 + hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
142 + groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED;
143 + break;
144 + case 'layout-effects':
145 + fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
146 + hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
147 + groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED;
148 + break;
149 + case 'passive-effects':
150 + fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
151 + hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
152 + groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED;
153 + break;
154 + default:
155 + throw new Error(`Unexpected measure type "${type}"`);
156 + }
157 +
158 + const drawableRect = intersectionOfRects(measureRect, rect);
159 + context.fillStyle = showHoverHighlight
160 + ? hoveredFillStyle
161 + : showGroupHighlight
162 + ? groupSelectedFillStyle
163 + : fillStyle;
164 + context.fillRect(
165 + drawableRect.origin.x,
166 + drawableRect.origin.y,
167 + drawableRect.size.width,
168 + drawableRect.size.height,
169 + );
170 + }
171 +
172 + draw(context: CanvasRenderingContext2D) {
173 + const {
174 + frame,
175 + _hoveredMeasure,
176 + _lanesToRender,
177 + _laneToMeasures,
178 + visibleArea,
179 + } = this;
180 +
181 + context.fillStyle = COLORS.PRIORITY_BACKGROUND;
182 + context.fillRect(
183 + visibleArea.origin.x,
184 + visibleArea.origin.y,
185 + visibleArea.size.width,
186 + visibleArea.size.height,
187 + );
188 +
189 + const scaleFactor = positioningScaleFactor(
190 + this._intrinsicSize.width,
191 + frame,
192 + );
193 +
194 + for (let i = 0; i < _lanesToRender.length; i++) {
195 + const lane = _lanesToRender[i];
196 + const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
197 + const measuresForLane = _laneToMeasures.get(lane);
198 +
199 + if (!measuresForLane) {
200 + throw new Error(
201 + 'No measures found for a React lane! This is a bug in this profiler tool. Please file an issue.',
202 + );
203 + }
204 +
205 + // Draw measures
206 + for (let j = 0; j < measuresForLane.length; j++) {
207 + const measure = measuresForLane[j];
208 + const showHoverHighlight = _hoveredMeasure === measure;
209 + const showGroupHighlight =
210 + !!_hoveredMeasure && _hoveredMeasure.batchUID === measure.batchUID;
211 +
212 + this._drawSingleReactMeasure(
213 + context,
214 + visibleArea,
215 + measure,
216 + baseY,
217 + scaleFactor,
218 + showGroupHighlight,
219 + showHoverHighlight,
220 + );
221 + }
222 +
223 + // Render bottom border
224 + const borderFrame: Rect = {
225 + origin: {
226 + x: frame.origin.x,
227 + y: frame.origin.y + (i + 1) * REACT_LANE_HEIGHT - BORDER_SIZE,
228 + },
229 + size: {
230 + width: frame.size.width,
231 + height: BORDER_SIZE,
232 + },
233 + };
234 + if (rectIntersectsRect(borderFrame, visibleArea)) {
235 + const borderDrawableRect = intersectionOfRects(
236 + borderFrame,
237 + visibleArea,
238 + );
239 + context.fillStyle = COLORS.PRIORITY_BORDER;
240 + context.fillRect(
241 + borderDrawableRect.origin.x,
242 + borderDrawableRect.origin.y,
243 + borderDrawableRect.size.width,
244 + borderDrawableRect.size.height,
245 + );
246 + }
247 + }
248 + }
249 +
250 + /**
251 + * @private
252 + */
253 + _handleMouseMove(interaction: MouseMoveInteraction) {
254 + const {
255 + frame,
256 + _intrinsicSize,
257 + _lanesToRender,
258 + _laneToMeasures,
259 + onHover,
260 + visibleArea,
261 + } = this;
262 + if (!onHover) {
263 + return;
264 + }
265 +
266 + const {location} = interaction.payload;
267 + if (!rectContainsPoint(location, visibleArea)) {
268 + onHover(null);
269 + return;
270 + }
271 +
272 + // Identify the lane being hovered over
273 + const adjustedCanvasMouseY = location.y - frame.origin.y;
274 + const renderedLaneIndex = Math.floor(
275 + adjustedCanvasMouseY / REACT_LANE_HEIGHT,
276 + );
277 + if (renderedLaneIndex < 0 || renderedLaneIndex >= _lanesToRender.length) {
278 + onHover(null);
279 + return;
280 + }
281 + const lane = _lanesToRender[renderedLaneIndex];
282 +
283 + // Find the measure in `lane` being hovered over.
284 + //
285 + // Because data ranges may overlap, we want to find the last intersecting item.
286 + // This will always be the one on "top" (the one the user is hovering over).
287 + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
288 + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
289 + const measures = _laneToMeasures.get(lane);
290 + if (!measures) {
291 + onHover(null);
292 + return;
293 + }
294 +
295 + for (let index = measures.length - 1; index >= 0; index--) {
296 + const measure = measures[index];
297 + const {duration, timestamp} = measure;
298 +
299 + if (
300 + hoverTimestamp >= timestamp &&
301 + hoverTimestamp <= timestamp + duration
302 + ) {
303 + onHover(measure);
304 + return;
305 + }
306 + }
307 +
308 + onHover(null);
309 + }
310 +
311 + handleInteraction(interaction: Interaction) {
312 + switch (interaction.type) {
313 + case 'mousemove':
314 + this._handleMouseMove(interaction);
315 + break;
316 + }
317 + }
318 +}
packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js new
+163
@@ -0,0 +1,163 @@
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, Size} from '../view-base';
11 +
12 +import {
13 + durationToWidth,
14 + positioningScaleFactor,
15 + positionToTimestamp,
16 + timestampToPosition,
17 +} from './utils/positioning';
18 +import {
19 + View,
20 + Surface,
21 + rectIntersectsRect,
22 + intersectionOfRects,
23 +} from '../view-base';
24 +import {
25 + COLORS,
26 + INTERVAL_TIMES,
27 + LABEL_SIZE,
28 + MARKER_FONT_SIZE,
29 + MARKER_HEIGHT,
30 + MARKER_TEXT_PADDING,
31 + MARKER_TICK_HEIGHT,
32 + MIN_INTERVAL_SIZE_PX,
33 + BORDER_SIZE,
34 +} from './constants';
35 +
36 +const HEADER_HEIGHT_FIXED = MARKER_HEIGHT + BORDER_SIZE;
37 +const LABEL_FIXED_WIDTH = LABEL_SIZE + BORDER_SIZE;
38 +
39 +export class TimeAxisMarkersView extends View {
40 + _totalDuration: number;
41 + _intrinsicSize: Size;
42 +
43 + constructor(surface: Surface, frame: Rect, totalDuration: number) {
44 + super(surface, frame);
45 + this._totalDuration = totalDuration;
46 + this._intrinsicSize = {
47 + width: this._totalDuration,
48 + height: HEADER_HEIGHT_FIXED,
49 + };
50 + }
51 +
52 + desiredSize() {
53 + return this._intrinsicSize;
54 + }
55 +
56 + // Time mark intervals vary based on the current zoom range and the time it represents.
57 + // In Chrome, these seem to range from 70-140 pixels wide.
58 + // Time wise, they represent intervals of e.g. 1s, 500ms, 200ms, 100ms, 50ms, 20ms.
59 + // Based on zoom, we should determine which amount to actually show.
60 + _getTimeTickInterval(scaleFactor: number): number {
61 + for (let i = 0; i < INTERVAL_TIMES.length; i++) {
62 + const currentInterval = INTERVAL_TIMES[i];
63 + const intervalWidth = durationToWidth(currentInterval, scaleFactor);
64 + if (intervalWidth > MIN_INTERVAL_SIZE_PX) {
65 + return currentInterval;
66 + }
67 + }
68 + return INTERVAL_TIMES[0];
69 + }
70 +
71 + draw(context: CanvasRenderingContext2D) {
72 + const {frame, _intrinsicSize, visibleArea} = this;
73 + const clippedFrame = {
74 + origin: frame.origin,
75 + size: {
76 + width: frame.size.width,
77 + height: _intrinsicSize.height,
78 + },
79 + };
80 + const drawableRect = intersectionOfRects(clippedFrame, visibleArea);
81 +
82 + // Clear background
83 + context.fillStyle = COLORS.BACKGROUND;
84 + context.fillRect(
85 + drawableRect.origin.x,
86 + drawableRect.origin.y,
87 + drawableRect.size.width,
88 + drawableRect.size.height,
89 + );
90 +
91 + const scaleFactor = positioningScaleFactor(
92 + _intrinsicSize.width,
93 + clippedFrame,
94 + );
95 + const interval = this._getTimeTickInterval(scaleFactor);
96 + const firstIntervalTimestamp =
97 + Math.ceil(
98 + positionToTimestamp(
99 + drawableRect.origin.x - LABEL_FIXED_WIDTH,
100 + scaleFactor,
101 + clippedFrame,
102 + ) / interval,
103 + ) * interval;
104 +
105 + for (
106 + let markerTimestamp = firstIntervalTimestamp;
107 + true;
108 + markerTimestamp += interval
109 + ) {
110 + if (markerTimestamp <= 0) {
111 + continue; // Timestamps < are probably a bug; markers at 0 are ugly.
112 + }
113 +
114 + const x = timestampToPosition(markerTimestamp, scaleFactor, clippedFrame);
115 + if (x > drawableRect.origin.x + drawableRect.size.width) {
116 + break; // Not in view
117 + }
118 +
119 + const markerLabel = Math.round(markerTimestamp);
120 +
121 + context.fillStyle = COLORS.PRIORITY_BORDER;
122 + context.fillRect(
123 + x,
124 + drawableRect.origin.y + MARKER_HEIGHT - MARKER_TICK_HEIGHT,
125 + BORDER_SIZE,
126 + MARKER_TICK_HEIGHT,
127 + );
128 +
129 + context.fillStyle = COLORS.TIME_MARKER_LABEL;
130 + context.textAlign = 'right';
131 + context.textBaseline = 'middle';
132 + context.font = `${MARKER_FONT_SIZE}px sans-serif`;
133 + context.fillText(
134 + `${markerLabel}ms`,
135 + x - MARKER_TEXT_PADDING,
136 + MARKER_HEIGHT / 2,
137 + );
138 + }
139 +
140 + // Render bottom border.
141 + // Propose border rect, check if intersects with `rect`, draw intersection.
142 + const borderFrame: Rect = {
143 + origin: {
144 + x: clippedFrame.origin.x,
145 + y: clippedFrame.origin.y + clippedFrame.size.height - BORDER_SIZE,
146 + },
147 + size: {
148 + width: clippedFrame.size.width,
149 + height: BORDER_SIZE,
150 + },
151 + };
152 + if (rectIntersectsRect(borderFrame, visibleArea)) {
153 + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
154 + context.fillStyle = COLORS.PRIORITY_BORDER;
155 + context.fillRect(
156 + borderDrawableRect.origin.x,
157 + borderDrawableRect.origin.y,
158 + borderDrawableRect.size.width,
159 + borderDrawableRect.size.height,
160 + );
161 + }
162 + }
163 +}
packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js new
+230
@@ -0,0 +1,230 @@
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 {UserTimingMark} from '../types';
11 +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
12 +
13 +import {
14 + positioningScaleFactor,
15 + timestampToPosition,
16 + positionToTimestamp,
17 + widthToDuration,
18 +} from './utils/positioning';
19 +import {
20 + View,
21 + Surface,
22 + rectContainsPoint,
23 + rectIntersectsRect,
24 + intersectionOfRects,
25 +} from '../view-base';
26 +import {
27 + COLORS,
28 + EVENT_ROW_PADDING,
29 + EVENT_DIAMETER,
30 + BORDER_SIZE,
31 +} from './constants';
32 +
33 +const ROW_HEIGHT_FIXED = EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING;
34 +
35 +export class UserTimingMarksView extends View {
36 + _marks: UserTimingMark[];
37 + _intrinsicSize: Size;
38 +
39 + _hoveredMark: UserTimingMark | null = null;
40 + onHover: ((mark: UserTimingMark | null) => void) | null = null;
41 +
42 + constructor(
43 + surface: Surface,
44 + frame: Rect,
45 + marks: UserTimingMark[],
46 + duration: number,
47 + ) {
48 + super(surface, frame);
49 + this._marks = marks;
50 +
51 + this._intrinsicSize = {
52 + width: duration,
53 + height: ROW_HEIGHT_FIXED,
54 + };
55 + }
56 +
57 + desiredSize() {
58 + return this._intrinsicSize;
59 + }
60 +
61 + setHoveredMark(hoveredMark: UserTimingMark | null) {
62 + if (this._hoveredMark === hoveredMark) {
63 + return;
64 + }
65 + this._hoveredMark = hoveredMark;
66 + this.setNeedsDisplay();
67 + }
68 +
69 + /**
70 + * Draw a single `UserTimingMark` as a circle in the canvas.
71 + */
72 + _drawSingleMark(
73 + context: CanvasRenderingContext2D,
74 + rect: Rect,
75 + mark: UserTimingMark,
76 + baseY: number,
77 + scaleFactor: number,
78 + showHoverHighlight: boolean,
79 + ) {
80 + const {frame} = this;
81 + const {timestamp} = mark;
82 +
83 + const x = timestampToPosition(timestamp, scaleFactor, frame);
84 + const radius = EVENT_DIAMETER / 2;
85 + const markRect: Rect = {
86 + origin: {
87 + x: x - radius,
88 + y: baseY,
89 + },
90 + size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER},
91 + };
92 + if (!rectIntersectsRect(markRect, rect)) {
93 + return; // Not in view
94 + }
95 +
96 + const fillStyle = showHoverHighlight
97 + ? COLORS.USER_TIMING_HOVER
98 + : COLORS.USER_TIMING;
99 +
100 + if (fillStyle !== null) {
101 + const y = markRect.origin.y + radius;
102 +
103 + context.beginPath();
104 + context.fillStyle = fillStyle;
105 + context.arc(x, y, radius, 0, 2 * Math.PI);
106 + context.fill();
107 + }
108 + }
109 +
110 + draw(context: CanvasRenderingContext2D) {
111 + const {frame, _marks, _hoveredMark, visibleArea} = this;
112 +
113 + context.fillStyle = COLORS.BACKGROUND;
114 + context.fillRect(
115 + visibleArea.origin.x,
116 + visibleArea.origin.y,
117 + visibleArea.size.width,
118 + visibleArea.size.height,
119 + );
120 +
121 + // Draw marks
122 + const baseY = frame.origin.y + EVENT_ROW_PADDING;
123 + const scaleFactor = positioningScaleFactor(
124 + this._intrinsicSize.width,
125 + frame,
126 + );
127 +
128 + _marks.forEach(mark => {
129 + if (mark === _hoveredMark) {
130 + return;
131 + }
132 + this._drawSingleMark(
133 + context,
134 + visibleArea,
135 + mark,
136 + baseY,
137 + scaleFactor,
138 + false,
139 + );
140 + });
141 +
142 + // Draw the hovered and/or selected items on top so they stand out.
143 + // This is helpful if there are multiple (overlapping) items close to each other.
144 + if (_hoveredMark !== null) {
145 + this._drawSingleMark(
146 + context,
147 + visibleArea,
148 + _hoveredMark,
149 + baseY,
150 + scaleFactor,
151 + true,
152 + );
153 + }
154 +
155 + // Render bottom border.
156 + // Propose border rect, check if intersects with `rect`, draw intersection.
157 + const borderFrame: Rect = {
158 + origin: {
159 + x: frame.origin.x,
160 + y: frame.origin.y + ROW_HEIGHT_FIXED - BORDER_SIZE,
161 + },
162 + size: {
163 + width: frame.size.width,
164 + height: BORDER_SIZE,
165 + },
166 + };
167 + if (rectIntersectsRect(borderFrame, visibleArea)) {
168 + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
169 + context.fillStyle = COLORS.PRIORITY_BORDER;
170 + context.fillRect(
171 + borderDrawableRect.origin.x,
172 + borderDrawableRect.origin.y,
173 + borderDrawableRect.size.width,
174 + borderDrawableRect.size.height,
175 + );
176 + }
177 + }
178 +
179 + /**
180 + * @private
181 + */
182 + _handleMouseMove(interaction: MouseMoveInteraction) {
183 + const {frame, onHover, visibleArea} = this;
184 + if (!onHover) {
185 + return;
186 + }
187 +
188 + const {location} = interaction.payload;
189 + if (!rectContainsPoint(location, visibleArea)) {
190 + onHover(null);
191 + return;
192 + }
193 +
194 + const {_marks} = this;
195 + const scaleFactor = positioningScaleFactor(
196 + this._intrinsicSize.width,
197 + frame,
198 + );
199 + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
200 + const markTimestampAllowance = widthToDuration(
201 + EVENT_DIAMETER / 2,
202 + scaleFactor,
203 + );
204 +
205 + // Because data ranges may overlap, we want to find the last intersecting item.
206 + // This will always be the one on "top" (the one the user is hovering over).
207 + for (let index = _marks.length - 1; index >= 0; index--) {
208 + const mark = _marks[index];
209 + const {timestamp} = mark;
210 +
211 + if (
212 + timestamp - markTimestampAllowance <= hoverTimestamp &&
213 + hoverTimestamp <= timestamp + markTimestampAllowance
214 + ) {
215 + onHover(mark);
216 + return;
217 + }
218 + }
219 +
220 + onHover(null);
221 + }
222 +
223 + handleInteraction(interaction: Interaction) {
224 + switch (interaction.type) {
225 + case 'mousemove':
226 + this._handleMouseMove(interaction);
227 + break;
228 + }
229 + }
230 +}
packages/react-devtools-scheduling-profiler/src/content-views/constants.js new
+73
@@ -0,0 +1,73 @@
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 +export const LABEL_SIZE = 80;
11 +export const LABEL_FONT_SIZE = 11;
12 +export const MARKER_HEIGHT = 20;
13 +export const MARKER_TICK_HEIGHT = 8;
14 +export const MARKER_FONT_SIZE = 10;
15 +export const MARKER_TEXT_PADDING = 8;
16 +export const COLOR_HOVER_DIM_DELTA = 5;
17 +
18 +export const INTERVAL_TIMES = [
19 + 1,
20 + 2,
21 + 5,
22 + 10,
23 + 20,
24 + 50,
25 + 100,
26 + 200,
27 + 500,
28 + 1000,
29 + 2000,
30 + 5000,
31 +];
32 +export const MIN_INTERVAL_SIZE_PX = 70;
33 +
34 +export const EVENT_ROW_PADDING = 4;
35 +export const EVENT_DIAMETER = 6;
36 +export const REACT_MEASURE_HEIGHT = 9;
37 +export const BORDER_SIZE = 1;
38 +
39 +export const FLAMECHART_FONT_SIZE = 10;
40 +export const FLAMECHART_FRAME_HEIGHT = 16;
41 +export const FLAMECHART_TEXT_PADDING = 3;
42 +
43 +export const COLORS = Object.freeze({
44 + BACKGROUND: '#ffffff',
45 + PRIORITY_BACKGROUND: '#ededf0',
46 + PRIORITY_BORDER: '#d7d7db',
47 + PRIORITY_LABEL: '#272727',
48 + USER_TIMING: '#c9cacd',
49 + USER_TIMING_HOVER: '#93959a',
50 + REACT_IDLE: '#edf6ff',
51 + REACT_IDLE_SELECTED: '#EDF6FF',
52 + REACT_IDLE_HOVER: '#EDF6FF',
53 + REACT_RENDER: '#9fc3f3',
54 + REACT_RENDER_SELECTED: '#64A9F5',
55 + REACT_RENDER_HOVER: '#2683E2',
56 + REACT_COMMIT: '#ff718e',
57 + REACT_COMMIT_SELECTED: '#FF5277',
58 + REACT_COMMIT_HOVER: '#ed0030',
59 + REACT_LAYOUT_EFFECTS: '#c88ff0',
60 + REACT_LAYOUT_EFFECTS_SELECTED: '#934FC1',
61 + REACT_LAYOUT_EFFECTS_HOVER: '#601593',
62 + REACT_PASSIVE_EFFECTS: '#c88ff0',
63 + REACT_PASSIVE_EFFECTS_SELECTED: '#934FC1',
64 + REACT_PASSIVE_EFFECTS_HOVER: '#601593',
65 + REACT_SCHEDULE: '#9fc3f3',
66 + REACT_SCHEDULE_HOVER: '#2683E2',
67 + REACT_SCHEDULE_CASCADING: '#ff718e',
68 + REACT_SCHEDULE_CASCADING_HOVER: '#ed0030',
69 + REACT_SUSPEND: '#a6e59f',
70 + REACT_SUSPEND_HOVER: '#13bc00',
71 + REACT_WORK_BORDER: '#ffffff',
72 + TIME_MARKER_LABEL: '#18212b',
73 +});
packages/react-devtools-scheduling-profiler/src/content-views/index.js new
+14
@@ -0,0 +1,14 @@
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 +export * from './FlamechartView';
11 +export * from './ReactEventsView';
12 +export * from './ReactMeasuresView';
13 +export * from './TimeAxisMarkersView';
14 +export * from './UserTimingMarksView';
packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/colors-test.js new
+93
@@ -0,0 +1,93 @@
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 {hslaColorToString, dimmedColor, ColorGenerator} from '../colors';
11 +
12 +describe(hslaColorToString, () => {
13 + it('should transform colors to strings', () => {
14 + expect(hslaColorToString({h: 1, s: 2, l: 3, a: 4})).toEqual(
15 + 'hsl(1deg 2% 3% / 4)',
16 + );
17 + expect(hslaColorToString({h: 3.14, s: 6.28, l: 1.68, a: 100})).toEqual(
18 + 'hsl(3.14deg 6.28% 1.68% / 100)',
19 + );
20 + });
21 +});
22 +
23 +describe(dimmedColor, () => {
24 + it('should dim luminosity using delta', () => {
25 + expect(dimmedColor({h: 1, s: 2, l: 3, a: 4}, 3)).toEqual({
26 + h: 1,
27 + s: 2,
28 + l: 0,
29 + a: 4,
30 + });
31 + expect(dimmedColor({h: 1, s: 2, l: 3, a: 4}, -3)).toEqual({
32 + h: 1,
33 + s: 2,
34 + l: 6,
35 + a: 4,
36 + });
37 + });
38 +});
39 +
40 +describe(ColorGenerator, () => {
41 + describe(ColorGenerator.prototype.colorForID, () => {
42 + it('should generate a color for an ID', () => {
43 + expect(new ColorGenerator().colorForID('123')).toMatchInlineSnapshot(`
44 + Object {
45 + "a": 1,
46 + "h": 190,
47 + "l": 80,
48 + "s": 67,
49 + }
50 + `);
51 + });
52 +
53 + it('should generate colors deterministically given an ID', () => {
54 + expect(new ColorGenerator().colorForID('id1')).toEqual(
55 + new ColorGenerator().colorForID('id1'),
56 + );
57 + expect(new ColorGenerator().colorForID('id2')).toEqual(
58 + new ColorGenerator().colorForID('id2'),
59 + );
60 + });
61 +
62 + it('should generate different colors for different IDs', () => {
63 + expect(new ColorGenerator().colorForID('id1')).not.toEqual(
64 + new ColorGenerator().colorForID('id2'),
65 + );
66 + });
67 +
68 + it('should return colors that have been set manually', () => {
69 + const generator = new ColorGenerator();
70 + const manualColor = {h: 1, s: 2, l: 3, a: 4};
71 + generator.setColorForID('id with set color', manualColor);
72 + expect(generator.colorForID('id with set color')).toEqual(manualColor);
73 + expect(generator.colorForID('some other id')).not.toEqual(manualColor);
74 + });
75 +
76 + it('should generate colors from fixed color spaces', () => {
77 + const generator = new ColorGenerator(1, 2, 3, 4);
78 + expect(generator.colorForID('123')).toEqual({h: 1, s: 2, l: 3, a: 4});
79 + expect(generator.colorForID('234')).toEqual({h: 1, s: 2, l: 3, a: 4});
80 + });
81 +
82 + it('should generate colors from range color spaces', () => {
83 + const generator = new ColorGenerator(
84 + {min: 0, max: 360, count: 2},
85 + 2,
86 + 3,
87 + 4,
88 + );
89 + expect(generator.colorForID('123')).toEqual({h: 0, s: 2, l: 3, a: 4});
90 + expect(generator.colorForID('234')).toEqual({h: 360, s: 2, l: 3, a: 4});
91 + });
92 + });
93 +});
packages/react-devtools-scheduling-profiler/src/content-views/utils/colors.js new
+113
@@ -0,0 +1,113 @@
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 +type ColorSpace = number | {|min: number, max: number, count?: number|};
11 +
12 +// Docstrings from https://www.w3schools.com/css/css_colors_hsl.asp
13 +type HslaColor = $ReadOnly<{|
14 + /** Hue is a degree on the color wheel from 0 to 360. 0 is red, 120 is green, and 240 is blue. */
15 + h: number,
16 + /** Saturation is a percentage value, 0% means a shade of gray, and 100% is the full color. */
17 + s: number,
18 + /** Lightness is a percentage, 0% is black, 50% is neither light or dark, 100% is white. */
19 + l: number,
20 + /** Alpha is a percentage, 0% is fully transparent, and 100 is not transparent at all. */
21 + a: number,
22 +|}>;
23 +
24 +export function hslaColorToString({h, s, l, a}: HslaColor): string {
25 + return `hsl(${h}deg ${s}% ${l}% / ${a})`;
26 +}
27 +
28 +export function dimmedColor(color: HslaColor, dimDelta: number): HslaColor {
29 + return {
30 + ...color,
31 + l: color.l - dimDelta,
32 + };
33 +}
34 +
35 +// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/platform/utilities.js;l=120
36 +function hashCode(string: string): number {
37 + // Hash algorithm for substrings is described in "Über die Komplexität der Multiplikation in
38 + // eingeschränkten Branchingprogrammmodellen" by Woelfe.
39 + // http://opendatastructures.org/versions/edition-0.1d/ods-java/node33.html#SECTION00832000000000000000
40 + const p = (1 << 30) * 4 - 5; // prime: 2^32 - 5
41 + const z = 0x5033d967; // 32 bits from random.org
42 + const z2 = 0x59d2f15d; // random odd 32 bit number
43 + let s = 0;
44 + let zi = 1;
45 + for (let i = 0; i < string.length; i++) {
46 + const xi = string.charCodeAt(i) * z2;
47 + s = (s + zi * xi) % p;
48 + zi = (zi * z) % p;
49 + }
50 + s = (s + zi * (p - 1)) % p;
51 + return Math.abs(s | 0);
52 +}
53 +
54 +function indexToValueInSpace(index: number, space: ColorSpace): number {
55 + if (typeof space === 'number') {
56 + return space;
57 + }
58 + const count = space.count || space.max - space.min;
59 + index %= count;
60 + return (
61 + space.min + Math.floor((index / (count - 1)) * (space.max - space.min))
62 + );
63 +}
64 +
65 +/**
66 + * Deterministic color generator.
67 + *
68 + * Adapted from: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/common/Color.js
69 + */
70 +export class ColorGenerator {
71 + _hueSpace: ColorSpace;
72 + _satSpace: ColorSpace;
73 + _lightnessSpace: ColorSpace;
74 + _alphaSpace: ColorSpace;
75 + _colors: Map<string, HslaColor>;
76 +
77 + constructor(
78 + hueSpace?: ColorSpace,
79 + satSpace?: ColorSpace,
80 + lightnessSpace?: ColorSpace,
81 + alphaSpace?: ColorSpace,
82 + ) {
83 + this._hueSpace = hueSpace || {min: 0, max: 360};
84 + this._satSpace = satSpace || 67;
85 + this._lightnessSpace = lightnessSpace || 80;
86 + this._alphaSpace = alphaSpace || 1;
87 + this._colors = new Map();
88 + }
89 +
90 + setColorForID(id: string, color: HslaColor) {
91 + this._colors.set(id, color);
92 + }
93 +
94 + colorForID(id: string): HslaColor {
95 + const cachedColor = this._colors.get(id);
96 + if (cachedColor) {
97 + return cachedColor;
98 + }
99 + const color = this._generateColorForID(id);
100 + this._colors.set(id, color);
101 + return color;
102 + }
103 +
104 + _generateColorForID(id: string): HslaColor {
105 + const hash = hashCode(id);
106 + return {
107 + h: indexToValueInSpace(hash, this._hueSpace),
108 + s: indexToValueInSpace(hash >> 8, this._satSpace),
109 + l: indexToValueInSpace(hash >> 16, this._lightnessSpace),
110 + a: indexToValueInSpace(hash >> 24, this._alphaSpace),
111 + };
112 + }
113 +}
packages/react-devtools-scheduling-profiler/src/content-views/utils/positioning.js new
+41
@@ -0,0 +1,41 @@
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 +export function positioningScaleFactor(
13 + intrinsicWidth: number,
14 + frame: Rect,
15 +): number {
16 + return frame.size.width / intrinsicWidth;
17 +}
18 +
19 +export function timestampToPosition(
20 + timestamp: number,
21 + scaleFactor: number,
22 + frame: Rect,
23 +): number {
24 + return frame.origin.x + timestamp * scaleFactor;
25 +}
26 +
27 +export function positionToTimestamp(
28 + position: number,
29 + scaleFactor: number,
30 + frame: Rect,
31 +): number {
32 + return (position - frame.origin.x) / scaleFactor;
33 +}
34 +
35 +export function durationToWidth(duration: number, scaleFactor: number): number {
36 + return duration * scaleFactor;
37 +}
38 +
39 +export function widthToDuration(width: number, scaleFactor: number): number {
40 + return width / scaleFactor;
41 +}
packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css new
+10
@@ -0,0 +1,10 @@
1 +.ContextMenu {
2 + position: absolute;
3 + border-radius: 0.125rem;
4 + background-color: #ffffff;
5 + border: 1px solid #ccc;
6 + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
7 + font-size: 11px;
8 + overflow: hidden;
9 + z-index: 10000002;
10 +}
packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js new
+143
@@ -0,0 +1,143 @@
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 {RegistryContextType} from './Contexts';
11 +
12 +import * as React from 'react';
13 +import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react';
14 +import {createPortal} from 'react-dom';
15 +import {RegistryContext} from './Contexts';
16 +
17 +import styles from './ContextMenu.css';
18 +
19 +function repositionToFit(element: HTMLElement, pageX: number, pageY: number) {
20 + const ownerWindow = element.ownerDocument.defaultView;
21 + if (element !== null) {
22 + if (pageY + element.offsetHeight >= ownerWindow.innerHeight) {
23 + if (pageY - element.offsetHeight > 0) {
24 + element.style.top = `${pageY - element.offsetHeight}px`;
25 + } else {
26 + element.style.top = '0px';
27 + }
28 + } else {
29 + element.style.top = `${pageY}px`;
30 + }
31 +
32 + if (pageX + element.offsetWidth >= ownerWindow.innerWidth) {
33 + if (pageX - element.offsetWidth > 0) {
34 + element.style.left = `${pageX - element.offsetWidth}px`;
35 + } else {
36 + element.style.left = '0px';
37 + }
38 + } else {
39 + element.style.left = `${pageX}px`;
40 + }
41 + }
42 +}
43 +
44 +const HIDDEN_STATE = {
45 + data: null,
46 + isVisible: false,
47 + pageX: 0,
48 + pageY: 0,
49 +};
50 +
51 +type Props = {|
52 + children: (data: Object) => React$Node,
53 + id: string,
54 +|};
55 +
56 +export default function ContextMenu({children, id}: Props) {
57 + const {hideMenu, registerMenu} = useContext<RegistryContextType>(
58 + RegistryContext,
59 + );
60 +
61 + const [state, setState] = useState(HIDDEN_STATE);
62 +
63 + const bodyAccessorRef = useRef<HTMLDivElement | null>(null);
64 + const containerRef = useRef<HTMLDivElement | null>(null);
65 + const menuRef = useRef<HTMLDivElement | null>(null);
66 +
67 + useEffect(() => {
68 + if (!bodyAccessorRef.current) {
69 + return;
70 + }
71 + const ownerDocument = bodyAccessorRef.current.ownerDocument;
72 + containerRef.current = ownerDocument.createElement('div');
73 + if (ownerDocument.body) {
74 + ownerDocument.body.appendChild(containerRef.current);
75 + }
76 + return () => {
77 + if (ownerDocument.body && containerRef.current) {
78 + ownerDocument.body.removeChild(containerRef.current);
79 + }
80 + };
81 + }, [bodyAccessorRef, containerRef]);
82 +
83 + useEffect(() => {
84 + const showMenuFn = ({data, pageX, pageY}) => {
85 + setState({data, isVisible: true, pageX, pageY});
86 + };
87 + const hideMenuFn = () => setState(HIDDEN_STATE);
88 + return registerMenu(id, showMenuFn, hideMenuFn);
89 + }, [id]);
90 +
91 + useLayoutEffect(() => {
92 + if (!state.isVisible || !containerRef.current) {
93 + return;
94 + }
95 +
96 + const menu = menuRef.current;
97 + if (!menu) {
98 + return;
99 + }
100 +
101 + const hideUnlessContains: MouseEventHandler &
102 + TouchEventHandler &
103 + KeyboardEventHandler = event => {
104 + if (event.target instanceof HTMLElement && !menu.contains(event.target)) {
105 + hideMenu();
106 + }
107 + };
108 +
109 + const ownerDocument = containerRef.current.ownerDocument;
110 + ownerDocument.addEventListener('mousedown', hideUnlessContains);
111 + ownerDocument.addEventListener('touchstart', hideUnlessContains);
112 + ownerDocument.addEventListener('keydown', hideUnlessContains);
113 +
114 + const ownerWindow = ownerDocument.defaultView;
115 + ownerWindow.addEventListener('resize', hideMenu);
116 +
117 + repositionToFit(menu, state.pageX, state.pageY);
118 +
119 + return () => {
120 + ownerDocument.removeEventListener('mousedown', hideUnlessContains);
121 + ownerDocument.removeEventListener('touchstart', hideUnlessContains);
122 + ownerDocument.removeEventListener('keydown', hideUnlessContains);
123 +
124 + ownerWindow.removeEventListener('resize', hideMenu);
125 + };
126 + }, [state]);
127 +
128 + if (!state.isVisible) {
129 + return <div ref={bodyAccessorRef} />;
130 + } else {
131 + const container = containerRef.current;
132 + if (container !== null) {
133 + return createPortal(
134 + <div ref={menuRef} className={styles.ContextMenu}>
135 + {children(state.data)}
136 + </div>,
137 + container,
138 + );
139 + } else {
140 + return null;
141 + }
142 + }
143 +}
packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css new
+20
@@ -0,0 +1,20 @@
1 +.ContextMenuItem {
2 + display: flex;
3 + align-items: center;
4 + color: #333;
5 + padding: 0.5rem 0.75rem;
6 + cursor: default;
7 + border-top: 1px solid #ccc;
8 +}
9 +.ContextMenuItem:first-of-type {
10 + border-top: none;
11 +}
12 +.ContextMenuItem:hover,
13 +.ContextMenuItem:focus {
14 + outline: 0;
15 + background-color: rgba(0, 136, 250, 0.1);
16 +}
17 +.ContextMenuItem:active {
18 + background-color: #0088fa;
19 + color: #fff;
20 +}
packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js new
+40
@@ -0,0 +1,40 @@
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 {RegistryContextType} from './Contexts';
11 +
12 +import * as React from 'react';
13 +import {useContext} from 'react';
14 +import {RegistryContext} from './Contexts';
15 +
16 +import styles from './ContextMenuItem.css';
17 +
18 +type Props = {|
19 + children: React$Node,
20 + onClick: () => void,
21 + title: string,
22 +|};
23 +
24 +export default function ContextMenuItem({children, onClick, title}: Props) {
25 + const {hideMenu} = useContext<RegistryContextType>(RegistryContext);
26 +
27 + const handleClick: MouseEventHandler = event => {
28 + onClick();
29 + hideMenu();
30 + };
31 +
32 + return (
33 + <div
34 + className={styles.ContextMenuItem}
35 + onClick={handleClick}
36 + onTouchEnd={handleClick}>
37 + {children}
38 + </div>
39 + );
40 +}
packages/react-devtools-scheduling-profiler/src/context/Contexts.js new
+87
@@ -0,0 +1,87 @@
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 {createContext} from 'react';
11 +
12 +export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void;
13 +export type HideFn = () => void;
14 +export type OnChangeFn = boolean => void;
15 +
16 +const idToShowFnMap = new Map<string, ShowFn>();
17 +const idToHideFnMap = new Map<string, HideFn>();
18 +
19 +let currentHideFn: ?HideFn = null;
20 +let currentOnChange: ?OnChangeFn = null;
21 +
22 +function hideMenu() {
23 + if (typeof currentHideFn === 'function') {
24 + currentHideFn();
25 +
26 + if (typeof currentOnChange === 'function') {
27 + currentOnChange(false);
28 + }
29 + }
30 +
31 + currentHideFn = null;
32 + currentOnChange = null;
33 +}
34 +
35 +function showMenu({
36 + data,
37 + id,
38 + onChange,
39 + pageX,
40 + pageY,
41 +}: {|
42 + data: Object,
43 + id: string,
44 + onChange?: OnChangeFn,
45 + pageX: number,
46 + pageY: number,
47 +|}) {
48 + const showFn = idToShowFnMap.get(id);
49 + if (typeof showFn === 'function') {
50 + // Prevent open menus from being left hanging.
51 + hideMenu();
52 +
53 + currentHideFn = idToHideFnMap.get(id);
54 + showFn({data, pageX, pageY});
55 +
56 + if (typeof onChange === 'function') {
57 + currentOnChange = onChange;
58 + onChange(true);
59 + }
60 + }
61 +}
62 +
63 +function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) {
64 + if (idToShowFnMap.has(id)) {
65 + throw Error(`Context menu with id "${id}" already registered.`);
66 + }
67 +
68 + idToShowFnMap.set(id, showFn);
69 + idToHideFnMap.set(id, hideFn);
70 +
71 + return function unregisterMenu() {
72 + idToShowFnMap.delete(id);
73 + idToHideFnMap.delete(id);
74 + };
75 +}
76 +
77 +export type RegistryContextType = {|
78 + hideMenu: typeof hideMenu,
79 + showMenu: typeof showMenu,
80 + registerMenu: typeof registerMenu,
81 +|};
82 +
83 +export const RegistryContext = createContext<RegistryContextType>({
84 + hideMenu,
85 + showMenu,
86 + registerMenu,
87 +});
packages/react-devtools-scheduling-profiler/src/context/index.js new
+15
@@ -0,0 +1,15 @@
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 {RegistryContext} from './Contexts';
11 +import ContextMenu from './ContextMenu';
12 +import ContextMenuItem from './ContextMenuItem';
13 +import useContextMenu from './useContextMenu';
14 +
15 +export {RegistryContext, ContextMenu, ContextMenuItem, useContextMenu};
packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js new
+52
@@ -0,0 +1,52 @@
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 {OnChangeFn, RegistryContextType} from './Contexts';
11 +
12 +import {useContext, useEffect} from 'react';
13 +import {RegistryContext} from './Contexts';
14 +
15 +export default function useContextMenu<T>({
16 + data,
17 + id,
18 + onChange,
19 + ref,
20 +}: {|
21 + data: T,
22 + id: string,
23 + onChange: OnChangeFn,
24 + ref: {+current: HTMLElement | null},
25 +|}) {
26 + const {showMenu} = useContext<RegistryContextType>(RegistryContext);
27 +
28 + useEffect(() => {
29 + if (ref.current !== null) {
30 + const handleContextMenu = (event: MouseEvent | TouchEvent) => {
31 + event.preventDefault();
32 + event.stopPropagation();
33 +
34 + const pageX =
35 + (event: any).pageX ||
36 + (event.touches && (event: any).touches[0].pageX);
37 + const pageY =
38 + (event: any).pageY ||
39 + (event.touches && (event: any).touches[0].pageY);
40 +
41 + showMenu({data, id, onChange, pageX, pageY});
42 + };
43 +
44 + const trigger = ref.current;
45 + trigger.addEventListener('contextmenu', handleContextMenu);
46 +
47 + return () => {
48 + trigger.removeEventListener('contextmenu', handleContextMenu);
49 + };
50 + }
51 + }, [data, id, showMenu]);
52 +}
packages/react-devtools-scheduling-profiler/src/index.css new
+13
@@ -0,0 +1,13 @@
1 +body {
2 + margin: 0;
3 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 + sans-serif;
6 + -webkit-font-smoothing: antialiased;
7 + -moz-osx-font-smoothing: grayscale;
8 +}
9 +
10 +code {
11 + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 + monospace;
13 +}
packages/react-devtools-scheduling-profiler/src/index.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 'regenerator-runtime/runtime';
11 +
12 +import * as React from 'react';
13 +// $FlowFixMe Flow does not yet know about createRoot()
14 +import {unstable_createRoot as createRoot} from 'react-dom';
15 +import nullthrows from 'nullthrows';
16 +import App from './App';
17 +
18 +import './index.css';
19 +
20 +const container = document.createElement('div');
21 +container.id = 'root';
22 +
23 +const body = nullthrows(document.body, 'Expect document.body to exist');
24 +body.appendChild(container);
25 +
26 +createRoot(container).render(
27 + <React.StrictMode>
28 + <App />
29 + </React.StrictMode>,
30 +);
packages/react-devtools-scheduling-profiler/src/types.js new
+135
@@ -0,0 +1,135 @@
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 +// Type utilities
11 +
12 +// Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798
13 +// eslint-disable-next-line no-unused-vars
14 +type Return_<R, F: (...args: Array<any>) => R> = R;
15 +/** Get return type of a function. */
16 +export type Return<T> = Return_<*, T>;
17 +
18 +// Project types
19 +
20 +export type Milliseconds = number;
21 +
22 +export type ReactLane = number;
23 +
24 +type BaseReactEvent = {|
25 + +componentName?: string,
26 + +componentStack?: string,
27 + +timestamp: Milliseconds,
28 +|};
29 +
30 +type BaseReactScheduleEvent = {|
31 + ...BaseReactEvent,
32 + +lanes: ReactLane[],
33 +|};
34 +export type ReactScheduleRenderEvent = {|
35 + ...BaseReactScheduleEvent,
36 + type: 'schedule-render',
37 +|};
38 +export type ReactScheduleStateUpdateEvent = {|
39 + ...BaseReactScheduleEvent,
40 + type: 'schedule-state-update',
41 + isCascading: boolean,
42 +|};
43 +export type ReactScheduleForceUpdateEvent = {|
44 + ...BaseReactScheduleEvent,
45 + type: 'schedule-force-update',
46 + isCascading: boolean,
47 +|};
48 +
49 +type BaseReactSuspenseEvent = {|
50 + ...BaseReactEvent,
51 + id: string,
52 +|};
53 +export type ReactSuspenseSuspendEvent = {|
54 + ...BaseReactSuspenseEvent,
55 + type: 'suspense-suspend',
56 +|};
57 +export type ReactSuspenseResolvedEvent = {|
58 + ...BaseReactSuspenseEvent,
59 + type: 'suspense-resolved',
60 +|};
61 +export type ReactSuspenseRejectedEvent = {|
62 + ...BaseReactSuspenseEvent,
63 + type: 'suspense-rejected',
64 +|};
65 +
66 +export type ReactEvent =
67 + | ReactScheduleRenderEvent
68 + | ReactScheduleStateUpdateEvent
69 + | ReactScheduleForceUpdateEvent
70 + | ReactSuspenseSuspendEvent
71 + | ReactSuspenseResolvedEvent
72 + | ReactSuspenseRejectedEvent;
73 +export type ReactEventType = $PropertyType<ReactEvent, 'type'>;
74 +
75 +export type ReactMeasureType =
76 + | 'commit'
77 + // render-idle: A measure spanning the time when a render starts, through all
78 + // yields and restarts, and ends when commit stops OR render is cancelled.
79 + | 'render-idle'
80 + | 'render'
81 + | 'layout-effects'
82 + | 'passive-effects';
83 +
84 +export type BatchUID = number;
85 +
86 +export type ReactMeasure = {|
87 + +type: ReactMeasureType,
88 + +lanes: ReactLane[],
89 + +timestamp: Milliseconds,
90 + +duration: Milliseconds,
91 + +batchUID: BatchUID,
92 + +depth: number,
93 +|};
94 +
95 +/**
96 + * A flamechart stack frame belonging to a stack trace.
97 + */
98 +export type FlamechartStackFrame = {|
99 + name: string,
100 + timestamp: Milliseconds,
101 + duration: Milliseconds,
102 + scriptUrl?: string,
103 + locationLine?: number,
104 + locationColumn?: number,
105 +|};
106 +
107 +export type UserTimingMark = {|
108 + name: string,
109 + timestamp: Milliseconds,
110 +|};
111 +
112 +/**
113 + * A "layer" of stack frames in the profiler UI, i.e. all stack frames of the
114 + * same depth across all stack traces. Displayed as a flamechart row in the UI.
115 + */
116 +export type FlamechartStackLayer = FlamechartStackFrame[];
117 +
118 +export type Flamechart = FlamechartStackLayer[];
119 +
120 +export type ReactProfilerData = {|
121 + startTime: number,
122 + duration: number,
123 + events: ReactEvent[],
124 + measures: ReactMeasure[],
125 + flamechart: Flamechart,
126 + otherUserTimingMarks: UserTimingMark[],
127 +|};
128 +
129 +export type ReactHoverContextInfo = {|
130 + event: ReactEvent | null,
131 + measure: ReactMeasure | null,
132 + data: $ReadOnly<ReactProfilerData> | null,
133 + flamechartStackFrame: FlamechartStackFrame | null,
134 + userTimingMark: UserTimingMark | null,
135 +|};
packages/react-devtools-scheduling-profiler/src/utils/__tests__/__snapshots__/preprocessData-test.js.snap new
+1380
@@ -0,0 +1,1380 @@
1 +// Jest Snapshot v1, https://goo.gl/fbAQLP
2 +
3 +exports[`preprocessData should process complete set of events (page load sample data) 1`] = `
4 +Object {
5 + "duration": 2586.895,
6 + "events": Array [
7 + Object {
8 + "componentStack": "",
9 + "lanes": Array [
10 + 9,
11 + ],
12 + "timestamp": 278.073,
13 + "type": "schedule-render",
14 + },
15 + Object {
16 + "componentName": "ResourceButton",
17 + "componentStack": "
18 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
19 + at Suspense
20 + at div
21 + at div
22 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
23 + at App",
24 + "id": "0",
25 + "timestamp": 658.345,
26 + "type": "suspense-suspend",
27 + },
28 + Object {
29 + "componentName": "ResourceButton",
30 + "componentStack": "
31 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
32 + at Suspense
33 + at div
34 + at div
35 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
36 + at App",
37 + "id": "1",
38 + "timestamp": 660.739,
39 + "type": "suspense-suspend",
40 + },
41 + Object {
42 + "componentName": "ResourceButton",
43 + "componentStack": "
44 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
45 + at Suspense
46 + at div
47 + at div
48 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
49 + at App",
50 + "id": "2",
51 + "timestamp": 662.554,
52 + "type": "suspense-suspend",
53 + },
54 + Object {
55 + "componentName": "ResourceButton",
56 + "componentStack": "
57 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
58 + at Suspense
59 + at div
60 + at div
61 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
62 + at App",
63 + "id": "3",
64 + "timestamp": 663.455,
65 + "type": "suspense-suspend",
66 + },
67 + Object {
68 + "componentName": "ResourceButton",
69 + "componentStack": "
70 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
71 + at Suspense
72 + at div
73 + at div
74 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
75 + at App",
76 + "id": "4",
77 + "timestamp": 664.202,
78 + "type": "suspense-suspend",
79 + },
80 + Object {
81 + "componentName": "ResourceButton",
82 + "componentStack": "
83 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
84 + at Suspense
85 + at div
86 + at div
87 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
88 + at App",
89 + "id": "0",
90 + "timestamp": 666.68,
91 + "type": "suspense-suspend",
92 + },
93 + Object {
94 + "componentName": "ResourceButton",
95 + "componentStack": "
96 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
97 + at Suspense
98 + at div
99 + at div
100 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
101 + at App",
102 + "id": "1",
103 + "timestamp": 667.201,
104 + "type": "suspense-suspend",
105 + },
106 + Object {
107 + "componentName": "ResourceButton",
108 + "componentStack": "
109 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
110 + at Suspense
111 + at div
112 + at div
113 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
114 + at App",
115 + "id": "2",
116 + "timestamp": 667.74,
117 + "type": "suspense-suspend",
118 + },
119 + Object {
120 + "componentName": "ResourceButton",
121 + "componentStack": "
122 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
123 + at Suspense
124 + at div
125 + at div
126 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
127 + at App",
128 + "id": "3",
129 + "timestamp": 668.282,
130 + "type": "suspense-suspend",
131 + },
132 + Object {
133 + "componentName": "ResourceButton",
134 + "componentStack": "
135 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
136 + at Suspense
137 + at div
138 + at div
139 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
140 + at App",
141 + "id": "4",
142 + "timestamp": 668.848,
143 + "type": "suspense-suspend",
144 + },
145 + Object {
146 + "componentName": "ResourceButton",
147 + "componentStack": "
148 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
149 + at Suspense
150 + at SuspenseList
151 + at div
152 + at div
153 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
154 + at App",
155 + "id": "0",
156 + "timestamp": 671.335,
157 + "type": "suspense-suspend",
158 + },
159 + Object {
160 + "componentName": "ForceUpdateDemo_ForceUpdateDemo",
161 + "componentStack": "
162 + at ForceUpdateDemo_ForceUpdateDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:18:98)
163 + at App",
164 + "isCascading": true,
165 + "lanes": Array [
166 + 0,
167 + ],
168 + "timestamp": 679.438,
169 + "type": "schedule-state-update",
170 + },
171 + Object {
172 + "componentName": "ResourceButton",
173 + "componentStack": "
174 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
175 + at Suspense
176 + at div
177 + at div
178 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
179 + at App",
180 + "id": "1",
181 + "timestamp": 1794.922,
182 + "type": "suspense-resolved",
183 + },
184 + Object {
185 + "componentName": "ResourceButton",
186 + "componentStack": "
187 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
188 + at Suspense
189 + at div
190 + at div
191 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
192 + at App",
193 + "id": "1",
194 + "timestamp": 1795.374,
195 + "type": "suspense-resolved",
196 + },
197 + Object {
198 + "componentName": "ResourceButton",
199 + "componentStack": "
200 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
201 + at Suspense
202 + at div
203 + at div
204 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
205 + at App",
206 + "id": "0",
207 + "timestamp": 1797.166,
208 + "type": "suspense-suspend",
209 + },
210 + Object {
211 + "componentName": "ResourceButton",
212 + "componentStack": "
213 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
214 + at Suspense
215 + at div
216 + at div
217 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
218 + at App",
219 + "id": "2",
220 + "timestamp": 1797.811,
221 + "type": "suspense-suspend",
222 + },
223 + Object {
224 + "componentName": "ResourceButton",
225 + "componentStack": "
226 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
227 + at Suspense
228 + at div
229 + at div
230 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
231 + at App",
232 + "id": "3",
233 + "timestamp": 1798.163,
234 + "type": "suspense-suspend",
235 + },
236 + Object {
237 + "componentName": "ResourceButton",
238 + "componentStack": "
239 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
240 + at Suspense
241 + at div
242 + at div
243 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
244 + at App",
245 + "id": "4",
246 + "timestamp": 1798.505,
247 + "type": "suspense-suspend",
248 + },
249 + Object {
250 + "componentName": "ResourceButton",
251 + "componentStack": "
252 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
253 + at Suspense
254 + at div
255 + at div
256 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
257 + at App",
258 + "id": "2",
259 + "timestamp": 1866.249,
260 + "type": "suspense-resolved",
261 + },
262 + Object {
263 + "componentName": "ResourceButton",
264 + "componentStack": "
265 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
266 + at Suspense
267 + at div
268 + at div
269 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
270 + at App",
271 + "id": "2",
272 + "timestamp": 1866.542,
273 + "type": "suspense-resolved",
274 + },
275 + Object {
276 + "componentName": "ResourceButton",
277 + "componentStack": "
278 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
279 + at Suspense
280 + at div
281 + at div
282 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
283 + at App",
284 + "id": "2",
285 + "timestamp": 1866.858,
286 + "type": "suspense-resolved",
287 + },
288 + Object {
289 + "componentName": "ResourceButton",
290 + "componentStack": "
291 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
292 + at Suspense
293 + at div
294 + at div
295 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
296 + at App",
297 + "id": "0",
298 + "timestamp": 1867.816,
299 + "type": "suspense-suspend",
300 + },
301 + Object {
302 + "componentName": "ResourceButton",
303 + "componentStack": "
304 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
305 + at Suspense
306 + at div
307 + at div
308 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
309 + at App",
310 + "id": "3",
311 + "timestamp": 1868.638,
312 + "type": "suspense-suspend",
313 + },
314 + Object {
315 + "componentName": "ResourceButton",
316 + "componentStack": "
317 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
318 + at Suspense
319 + at div
320 + at div
321 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
322 + at App",
323 + "id": "4",
324 + "timestamp": 1868.966,
325 + "type": "suspense-suspend",
326 + },
327 + Object {
328 + "componentName": "ResourceButton",
329 + "componentStack": "
330 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
331 + at Suspense
332 + at div
333 + at div
334 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
335 + at App",
336 + "id": "3",
337 + "timestamp": 1980.273,
338 + "type": "suspense-resolved",
339 + },
340 + Object {
341 + "componentName": "ResourceButton",
342 + "componentStack": "
343 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
344 + at Suspense
345 + at div
346 + at div
347 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
348 + at App",
349 + "id": "3",
350 + "timestamp": 1980.522,
351 + "type": "suspense-resolved",
352 + },
353 + Object {
354 + "componentName": "ResourceButton",
355 + "componentStack": "
356 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
357 + at Suspense
358 + at div
359 + at div
360 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
361 + at App",
362 + "id": "3",
363 + "timestamp": 1980.946,
364 + "type": "suspense-resolved",
365 + },
366 + Object {
367 + "componentName": "ResourceButton",
368 + "componentStack": "
369 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
370 + at Suspense
371 + at div
372 + at div
373 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
374 + at App",
375 + "id": "3",
376 + "timestamp": 1981.164,
377 + "type": "suspense-resolved",
378 + },
379 + Object {
380 + "componentName": "ResourceButton",
381 + "componentStack": "
382 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
383 + at Suspense
384 + at div
385 + at div
386 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
387 + at App",
388 + "id": "0",
389 + "timestamp": 1982.079,
390 + "type": "suspense-suspend",
391 + },
392 + Object {
393 + "componentName": "ResourceButton",
394 + "componentStack": "
395 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
396 + at Suspense
397 + at div
398 + at div
399 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
400 + at App",
401 + "id": "4",
402 + "timestamp": 1983.17,
403 + "type": "suspense-suspend",
404 + },
405 + Object {
406 + "componentName": "ResourceButton",
407 + "componentStack": "
408 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
409 + at Suspense
410 + at div
411 + at div
412 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
413 + at App",
414 + "id": "4",
415 + "timestamp": 2379.587,
416 + "type": "suspense-resolved",
417 + },
418 + Object {
419 + "componentName": "ResourceButton",
420 + "componentStack": "
421 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
422 + at Suspense
423 + at div
424 + at div
425 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
426 + at App",
427 + "id": "4",
428 + "timestamp": 2379.895,
429 + "type": "suspense-resolved",
430 + },
431 + Object {
432 + "componentName": "ResourceButton",
433 + "componentStack": "
434 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
435 + at Suspense
436 + at div
437 + at div
438 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
439 + at App",
440 + "id": "4",
441 + "timestamp": 2380.242,
442 + "type": "suspense-resolved",
443 + },
444 + Object {
445 + "componentName": "ResourceButton",
446 + "componentStack": "
447 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
448 + at Suspense
449 + at div
450 + at div
451 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
452 + at App",
453 + "id": "4",
454 + "timestamp": 2380.487,
455 + "type": "suspense-resolved",
456 + },
457 + Object {
458 + "componentName": "ResourceButton",
459 + "componentStack": "
460 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
461 + at Suspense
462 + at div
463 + at div
464 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
465 + at App",
466 + "id": "4",
467 + "timestamp": 2380.735,
468 + "type": "suspense-resolved",
469 + },
470 + Object {
471 + "componentName": "ResourceButton",
472 + "componentStack": "
473 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
474 + at Suspense
475 + at div
476 + at div
477 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
478 + at App",
479 + "id": "0",
480 + "timestamp": 2381.772,
481 + "type": "suspense-suspend",
482 + },
483 + Object {
484 + "componentName": "ResourceButton",
485 + "componentStack": "
486 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
487 + at Suspense
488 + at div
489 + at div
490 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
491 + at App",
492 + "id": "0",
493 + "timestamp": 2579.186,
494 + "type": "suspense-resolved",
495 + },
496 + Object {
497 + "componentName": "ResourceButton",
498 + "componentStack": "
499 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
500 + at Suspense
501 + at div
502 + at div
503 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
504 + at App",
505 + "id": "0",
506 + "timestamp": 2579.487,
507 + "type": "suspense-resolved",
508 + },
509 + Object {
510 + "componentName": "ResourceButton",
511 + "componentStack": "
512 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
513 + at Suspense
514 + at SuspenseList
515 + at div
516 + at div
517 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
518 + at App",
519 + "id": "0",
520 + "timestamp": 2579.735,
521 + "type": "suspense-resolved",
522 + },
523 + Object {
524 + "componentName": "ResourceButton",
525 + "componentStack": "
526 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
527 + at Suspense
528 + at div
529 + at div
530 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
531 + at App",
532 + "id": "0",
533 + "timestamp": 2580.042,
534 + "type": "suspense-resolved",
535 + },
536 + Object {
537 + "componentName": "ResourceButton",
538 + "componentStack": "
539 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
540 + at Suspense
541 + at div
542 + at div
543 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
544 + at App",
545 + "id": "0",
546 + "timestamp": 2580.29,
547 + "type": "suspense-resolved",
548 + },
549 + Object {
550 + "componentName": "ResourceButton",
551 + "componentStack": "
552 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
553 + at Suspense
554 + at div
555 + at div
556 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
557 + at App",
558 + "id": "0",
559 + "timestamp": 2580.546,
560 + "type": "suspense-resolved",
561 + },
562 + Object {
563 + "componentName": "ResourceButton",
564 + "componentStack": "
565 + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)
566 + at Suspense
567 + at div
568 + at div
569 + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)
570 + at App",
571 + "id": "0",
572 + "timestamp": 2580.8,
573 + "type": "suspense-resolved",
574 + },
575 + ],
576 + "flamechart": Array [],
577 + "measures": Array [
578 + Object {
579 + "batchUID": 0,
580 + "depth": 0,
581 + "duration": 402.58299999999997,
582 + "lanes": Array [
583 + 9,
584 + ],
585 + "timestamp": 285.293,
586 + "type": "render-idle",
587 + },
588 + Object {
589 + "batchUID": 0,
590 + "depth": 0,
591 + "duration": 5.235000000000014,
592 + "lanes": Array [
593 + 9,
594 + ],
595 + "timestamp": 285.293,
596 + "type": "render",
597 + },
598 + Object {
599 + "batchUID": 0,
600 + "depth": 0,
601 + "duration": 8.16799999999995,
602 + "lanes": Array [
603 + 9,
604 + ],
605 + "timestamp": 297.708,
606 + "type": "render",
607 + },
608 + Object {
609 + "batchUID": 0,
610 + "depth": 0,
611 + "duration": 4.989000000000033,
612 + "lanes": Array [
613 + 9,
614 + ],
615 + "timestamp": 310.457,
616 + "type": "render",
617 + },
618 + Object {
619 + "batchUID": 0,
620 + "depth": 0,
621 + "duration": 6.470000000000027,
622 + "lanes": Array [
623 + 9,
624 + ],
625 + "timestamp": 357.767,
626 + "type": "render",
627 + },
628 + Object {
629 + "batchUID": 0,
630 + "depth": 0,
631 + "duration": 6.633000000000038,
632 + "lanes": Array [
633 + 9,
634 + ],
635 + "timestamp": 371.486,
636 + "type": "render",
637 + },
638 + Object {
639 + "batchUID": 0,
640 + "depth": 0,
641 + "duration": 9.889999999999986,
642 + "lanes": Array [
643 + 9,
644 + ],
645 + "timestamp": 385.05,
646 + "type": "render",
647 + },
648 + Object {
649 + "batchUID": 0,
650 + "depth": 0,
651 + "duration": 7.8589999999999804,
652 + "lanes": Array [
653 + 9,
654 + ],
655 + "timestamp": 398.06,
656 + "type": "render",
657 + },
658 + Object {
659 + "batchUID": 0,
660 + "depth": 0,
661 + "duration": 7.261000000000024,
662 + "lanes": Array [
663 + 9,
664 + ],
665 + "timestamp": 406.666,
666 + "type": "render",
667 + },
668 + Object {
669 + "batchUID": 0,
670 + "depth": 0,
671 + "duration": 7.700999999999965,
672 + "lanes": Array [
673 + 9,
674 + ],
675 + "timestamp": 414.218,
676 + "type": "render",
677 + },
678 + Object {
679 + "batchUID": 0,
680 + "depth": 0,
681 + "duration": 7.793000000000006,
682 + "lanes": Array [
683 + 9,
684 + ],
685 + "timestamp": 422.141,
686 + "type": "render",
687 + },
688 + Object {
689 + "batchUID": 0,
690 + "depth": 0,
691 + "duration": 7.706999999999994,
692 + "lanes": Array [
693 + 9,
694 + ],
695 + "timestamp": 430.185,
696 + "type": "render",
697 + },
698 + Object {
699 + "batchUID": 0,
700 + "depth": 0,
701 + "duration": 7.406999999999982,
702 + "lanes": Array [
703 + 9,
704 + ],
705 + "timestamp": 439.552,
706 + "type": "render",
707 + },
708 + Object {
709 + "batchUID": 0,
710 + "depth": 0,
711 + "duration": 7.649000000000001,
712 + "lanes": Array [
713 + 9,
714 + ],
715 + "timestamp": 447.294,
716 + "type": "render",
717 + },
718 + Object {
719 + "batchUID": 0,
720 + "depth": 0,
721 + "duration": 7.736000000000047,
722 + "lanes": Array [
723 + 9,
724 + ],
725 + "timestamp": 455.215,
726 + "type": "render",
727 + },
728 + Object {
729 + "batchUID": 0,
730 + "depth": 0,
731 + "duration": 7.6959999999999695,
732 + "lanes": Array [
733 + 9,
734 + ],
735 + "timestamp": 463.259,
736 + "type": "render",
737 + },
738 + Object {
739 + "batchUID": 0,
740 + "depth": 0,
741 + "duration": 7.700999999999965,
742 + "lanes": Array [
743 + 9,
744 + ],
745 + "timestamp": 471.247,
746 + "type": "render",
747 + },
748 + Object {
749 + "batchUID": 0,
750 + "depth": 0,
751 + "duration": 7.55499999999995,
752 + "lanes": Array [
753 + 9,
754 + ],
755 + "timestamp": 479.362,
756 + "type": "render",
757 + },
758 + Object {
759 + "batchUID": 0,
760 + "depth": 0,
761 + "duration": 7.7900000000000205,
762 + "lanes": Array [
763 + 9,
764 + ],
765 + "timestamp": 487.195,
766 + "type": "render",
767 + },
768 + Object {
769 + "batchUID": 0,
770 + "depth": 0,
771 + "duration": 7.617000000000019,
772 + "lanes": Array [
773 + 9,
774 + ],
775 + "timestamp": 495.354,
776 + "type": "render",
777 + },
778 + Object {
779 + "batchUID": 0,
780 + "depth": 0,
781 + "duration": 7.241999999999962,
782 + "lanes": Array [
783 + 9,
784 + ],
785 + "timestamp": 503.706,
786 + "type": "render",
787 + },
788 + Object {
789 + "batchUID": 0,
790 + "depth": 0,
791 + "duration": 7.81899999999996,
792 + "lanes": Array [
793 + 9,
794 + ],
795 + "timestamp": 513.106,
796 + "type": "render",
797 + },
798 + Object {
799 + "batchUID": 0,
800 + "depth": 0,
801 + "duration": 9.269999999999982,
802 + "lanes": Array [
803 + 9,
804 + ],
805 + "timestamp": 521.667,
806 + "type": "render",
807 + },
808 + Object {
809 + "batchUID": 0,
810 + "depth": 0,
811 + "duration": 7.758000000000038,
812 + "lanes": Array [
813 + 9,
814 + ],
815 + "timestamp": 531.185,
816 + "type": "render",
817 + },
818 + Object {
819 + "batchUID": 0,
820 + "depth": 0,
821 + "duration": 7.735000000000014,
822 + "lanes": Array [
823 + 9,
824 + ],
825 + "timestamp": 539.213,
826 + "type": "render",
827 + },
828 + Object {
829 + "batchUID": 0,
830 + "depth": 0,
831 + "duration": 7.730000000000018,
832 + "lanes": Array [
833 + 9,
834 + ],
835 + "timestamp": 547.223,
836 + "type": "render",
837 + },
838 + Object {
839 + "batchUID": 0,
840 + "depth": 0,
841 + "duration": 7.697000000000003,
842 + "lanes": Array [
843 + 9,
844 + ],
845 + "timestamp": 555.234,
846 + "type": "render",
847 + },
848 + Object {
849 + "batchUID": 0,
850 + "depth": 0,
851 + "duration": 7.715000000000032,
852 + "lanes": Array [
853 + 9,
854 + ],
855 + "timestamp": 563.232,
856 + "type": "render",
857 + },
858 + Object {
859 + "batchUID": 0,
860 + "depth": 0,
861 + "duration": 7.760999999999967,
862 + "lanes": Array [
863 + 9,
864 + ],
865 + "timestamp": 571.212,
866 + "type": "render",
867 + },
868 + Object {
869 + "batchUID": 0,
870 + "depth": 0,
871 + "duration": 7.618000000000052,
872 + "lanes": Array [
873 + 9,
874 + ],
875 + "timestamp": 579.324,
876 + "type": "render",
877 + },
878 + Object {
879 + "batchUID": 0,
880 + "depth": 0,
881 + "duration": 7.737999999999943,
882 + "lanes": Array [
883 + 9,
884 + ],
885 + "timestamp": 587.201,
886 + "type": "render",
887 + },
888 + Object {
889 + "batchUID": 0,
890 + "depth": 0,
891 + "duration": 7.739000000000033,
892 + "lanes": Array [
893 + 9,
894 + ],
895 + "timestamp": 595.209,
896 + "type": "render",
897 + },
898 + Object {
899 + "batchUID": 0,
900 + "depth": 0,
901 + "duration": 7.732000000000085,
902 + "lanes": Array [
903 + 9,
904 + ],
905 + "timestamp": 603.194,
906 + "type": "render",
907 + },
908 + Object {
909 + "batchUID": 0,
910 + "depth": 0,
911 + "duration": 7.712999999999965,
912 + "lanes": Array [
913 + 9,
914 + ],
915 + "timestamp": 611.212,
916 + "type": "render",
917 + },
918 + Object {
919 + "batchUID": 0,
920 + "depth": 0,
921 + "duration": 7.3700000000000045,
922 + "lanes": Array [
923 + 9,
924 + ],
925 + "timestamp": 619.574,
926 + "type": "render",
927 + },
928 + Object {
929 + "batchUID": 0,
930 + "depth": 0,
931 + "duration": 7.73599999999999,
932 + "lanes": Array [
933 + 9,
934 + ],
935 + "timestamp": 627.188,
936 + "type": "render",
937 + },
938 + Object {
939 + "batchUID": 0,
940 + "depth": 0,
941 + "duration": 7.729000000000042,
942 + "lanes": Array [
943 + 9,
944 + ],
945 + "timestamp": 635.193,
946 + "type": "render",
947 + },
948 + Object {
949 + "batchUID": 0,
950 + "depth": 0,
951 + "duration": 7.908999999999992,
952 + "lanes": Array [
953 + 9,
954 + ],
955 + "timestamp": 643.206,
956 + "type": "render",
957 + },
958 + Object {
959 + "batchUID": 0,
960 + "depth": 0,
961 + "duration": 7.6159999999999854,
962 + "lanes": Array [
963 + 9,
964 + ],
965 + "timestamp": 651.339,
966 + "type": "render",
967 + },
968 + Object {
969 + "batchUID": 0,
970 + "depth": 0,
971 + "duration": 5.334999999999923,
972 + "lanes": Array [
973 + 9,
974 + ],
975 + "timestamp": 659.445,
976 + "type": "render",
977 + },
978 + Object {
979 + "batchUID": 0,
980 + "depth": 0,
981 + "duration": 5.5890000000000555,
982 + "lanes": Array [
983 + 9,
984 + ],
985 + "timestamp": 664.952,
986 + "type": "render",
987 + },
988 + Object {
989 + "batchUID": 0,
990 + "depth": 0,
991 + "duration": 1.58400000000006,
992 + "lanes": Array [
993 + 9,
994 + ],
995 + "timestamp": 670.784,
996 + "type": "render",
997 + },
998 + Object {
999 + "batchUID": 0,
1000 + "depth": 0,
1001 + "duration": 14.552000000000021,
1002 + "lanes": Array [
1003 + 9,
1004 + ],
1005 + "timestamp": 673.324,
1006 + "type": "commit",
1007 + },
1008 + Object {
1009 + "batchUID": 0,
1010 + "depth": 1,
1011 + "duration": 2.6889999999999645,
1012 + "lanes": Array [
1013 + 9,
1014 + ],
1015 + "timestamp": 677.236,
1016 + "type": "layout-effects",
1017 + },
1018 + Object {
1019 + "batchUID": 1,
1020 + "depth": 1,
1021 + "duration": 3.5559999999999263,
1022 + "lanes": Array [
1023 + 0,
1024 + ],
1025 + "timestamp": 684.104,
1026 + "type": "render-idle",
1027 + },
1028 + Object {
1029 + "batchUID": 1,
1030 + "depth": 1,
1031 + "duration": 2.2169999999999845,
1032 + "lanes": Array [
1033 + 0,
1034 + ],
1035 + "timestamp": 684.104,
1036 + "type": "render",
1037 + },
1038 + Object {
1039 + "batchUID": 1,
1040 + "depth": 1,
1041 + "duration": 1.3120000000000118,
1042 + "lanes": Array [
1043 + 0,
1044 + ],
1045 + "timestamp": 686.348,
1046 + "type": "commit",
1047 + },
1048 + Object {
1049 + "batchUID": 1,
1050 + "depth": 2,
1051 + "duration": 0.020999999999958163,
1052 + "lanes": Array [
1053 + 0,
1054 + ],
1055 + "timestamp": 687.267,
1056 + "type": "layout-effects",
1057 + },
1058 + Object {
1059 + "batchUID": 2,
1060 + "depth": 0,
1061 + "duration": 6.119000000000142,
1062 + "lanes": Array [
1063 + 10,
1064 + ],
1065 + "timestamp": 1796.042,
1066 + "type": "render-idle",
1067 + },
1068 + Object {
1069 + "batchUID": 2,
1070 + "depth": 0,
1071 + "duration": 3.4329999999999927,
1072 + "lanes": Array [
1073 + 10,
1074 + ],
1075 + "timestamp": 1796.042,
1076 + "type": "render",
1077 + },
1078 + Object {
1079 + "batchUID": 2,
1080 + "depth": 0,
1081 + "duration": 2.589000000000169,
1082 + "lanes": Array [
1083 + 10,
1084 + ],
1085 + "timestamp": 1799.572,
1086 + "type": "commit",
1087 + },
1088 + Object {
1089 + "batchUID": 2,
1090 + "depth": 1,
1091 + "duration": 0.020999999999958163,
1092 + "lanes": Array [
1093 + 10,
1094 + ],
1095 + "timestamp": 1801.605,
1096 + "type": "layout-effects",
1097 + },
1098 + Object {
1099 + "batchUID": 3,
1100 + "depth": 0,
1101 + "duration": 4.116999999999962,
1102 + "lanes": Array [
1103 + 11,
1104 + ],
1105 + "timestamp": 1866.998,
1106 + "type": "render-idle",
1107 + },
1108 + Object {
1109 + "batchUID": 3,
1110 + "depth": 0,
1111 + "duration": 2.6679999999998927,
1112 + "lanes": Array [
1113 + 11,
1114 + ],
1115 + "timestamp": 1866.998,
1116 + "type": "render",
1117 + },
1118 + Object {
1119 + "batchUID": 3,
1120 + "depth": 0,
1121 + "duration": 1.4200000000000728,
1122 + "lanes": Array [
1123 + 11,
1124 + ],
1125 + "timestamp": 1869.695,
1126 + "type": "commit",
1127 + },
1128 + Object {
1129 + "batchUID": 3,
1130 + "depth": 1,
1131 + "duration": 0.020000000000209184,
1132 + "lanes": Array [
1133 + 11,
1134 + ],
1135 + "timestamp": 1870.764,
1136 + "type": "layout-effects",
1137 + },
1138 + Object {
1139 + "batchUID": 4,
1140 + "depth": 0,
1141 + "duration": 3.7679999999998017,
1142 + "lanes": Array [
1143 + 12,
1144 + ],
1145 + "timestamp": 1981.361,
1146 + "type": "render-idle",
1147 + },
1148 + Object {
1149 + "batchUID": 4,
1150 + "depth": 0,
1151 + "duration": 2.438999999999851,
1152 + "lanes": Array [
1153 + 12,
1154 + ],
1155 + "timestamp": 1981.361,
1156 + "type": "render",
1157 + },
1158 + Object {
1159 + "batchUID": 4,
1160 + "depth": 0,
1161 + "duration": 1.2579999999998108,
1162 + "lanes": Array [
1163 + 12,
1164 + ],
1165 + "timestamp": 1983.871,
1166 + "type": "commit",
1167 + },
1168 + Object {
1169 + "batchUID": 4,
1170 + "depth": 1,
1171 + "duration": 0.027000000000043656,
1172 + "lanes": Array [
1173 + 12,
1174 + ],
1175 + "timestamp": 1984.931,
1176 + "type": "layout-effects",
1177 + },
1178 + Object {
1179 + "batchUID": 5,
1180 + "depth": 0,
1181 + "duration": 3.8899999999998727,
1182 + "lanes": Array [
1183 + 13,
1184 + ],
1185 + "timestamp": 2380.969,
1186 + "type": "render-idle",
1187 + },
1188 + Object {
1189 + "batchUID": 5,
1190 + "depth": 0,
1191 + "duration": 2.3099999999999454,
1192 + "lanes": Array [
1193 + 13,
1194 + ],
1195 + "timestamp": 2380.969,
1196 + "type": "render",
1197 + },
1198 + Object {
1199 + "batchUID": 5,
1200 + "depth": 0,
1201 + "duration": 1.5209999999997308,
1202 + "lanes": Array [
1203 + 13,
1204 + ],
1205 + "timestamp": 2383.338,
1206 + "type": "commit",
1207 + },
1208 + Object {
1209 + "batchUID": 5,
1210 + "depth": 1,
1211 + "duration": 0.01999999999998181,
1212 + "lanes": Array [
1213 + 13,
1214 + ],
1215 + "timestamp": 2384.535,
1216 + "type": "layout-effects",
1217 + },
1218 + Object {
1219 + "batchUID": 6,
1220 + "depth": 0,
1221 + "duration": 5.942999999999756,
1222 + "lanes": Array [
1223 + 14,
1224 + ],
1225 + "timestamp": 2580.952,
1226 + "type": "render-idle",
1227 + },
1228 + Object {
1229 + "batchUID": 6,
1230 + "depth": 0,
1231 + "duration": 3.424999999999727,
1232 + "lanes": Array [
1233 + 14,
1234 + ],
1235 + "timestamp": 2580.952,
1236 + "type": "render",
1237 + },
1238 + Object {
1239 + "batchUID": 6,
1240 + "depth": 0,
1241 + "duration": 2.4470000000001164,
1242 + "lanes": Array [
1243 + 14,
1244 + ],
1245 + "timestamp": 2584.448,
1246 + "type": "commit",
1247 + },
1248 + Object {
1249 + "batchUID": 6,
1250 + "depth": 1,
1251 + "duration": 0.027000000000043656,
1252 + "lanes": Array [
1253 + 14,
1254 + ],
1255 + "timestamp": 2586.122,
1256 + "type": "layout-effects",
1257 + },
1258 + ],
1259 + "otherUserTimingMarks": Array [
1260 + Object {
1261 + "name": "navigationStart",
1262 + "timestamp": -29.357,
1263 + },
1264 + Object {
1265 + "name": "fetchStart",
1266 + "timestamp": -26.92,
1267 + },
1268 + Object {
1269 + "name": "requestStart",
1270 + "timestamp": -21.171,
1271 + },
1272 + Object {
1273 + "name": "responseEnd",
1274 + "timestamp": -15.655,
1275 + },
1276 + Object {
1277 + "name": "unloadEventStart",
1278 + "timestamp": -0.74,
1279 + },
1280 + Object {
1281 + "name": "unloadEventEnd",
1282 + "timestamp": 39.608,
1283 + },
1284 + Object {
1285 + "name": "requestStart",
1286 + "timestamp": 107.649,
1287 + },
1288 + Object {
1289 + "name": "requestStart",
1290 + "timestamp": 108.385,
1291 + },
1292 + Object {
1293 + "name": "loadEventStart",
1294 + "timestamp": 307.056,
1295 + },
1296 + Object {
1297 + "name": "loadEventEnd",
1298 + "timestamp": 308.242,
1299 + },
1300 + Object {
1301 + "name": "requestStart",
1302 + "timestamp": 341.329,
1303 + },
1304 + Object {
1305 + "name": "requestStart",
1306 + "timestamp": 344.02,
1307 + },
1308 + Object {
1309 + "name": "requestStart",
1310 + "timestamp": 387.585,
1311 + },
1312 + ],
1313 + "startTime": 8993778496,
1314 +}
1315 +`;
1316 +
1317 +exports[`preprocessData should process forced update event 1`] = `
1318 +Object {
1319 + "duration": 67.461,
1320 + "events": Array [
1321 + Object {
1322 + "componentName": "ForceUpdateDemo_ForceUpdateDemo",
1323 + "componentStack": "
1324 + at ForceUpdateDemo_ForceUpdateDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:18:98)
1325 + at App",
1326 + "isCascading": false,
1327 + "lanes": Array [
1328 + 4,
1329 + ],
1330 + "timestamp": 63.355,
1331 + "type": "schedule-force-update",
1332 + },
1333 + ],
1334 + "flamechart": Array [],
1335 + "measures": Array [
1336 + Object {
1337 + "batchUID": 0,
1338 + "depth": 0,
1339 + "duration": 2.1910000000000025,
1340 + "lanes": Array [
1341 + 4,
1342 + ],
1343 + "timestamp": 65.27,
1344 + "type": "render-idle",
1345 + },
1346 + Object {
1347 + "batchUID": 0,
1348 + "depth": 0,
1349 + "duration": 0.9770000000000039,
1350 + "lanes": Array [
1351 + 4,
1352 + ],
1353 + "timestamp": 65.27,
1354 + "type": "render",
1355 + },
1356 + Object {
1357 + "batchUID": 0,
1358 + "depth": 0,
1359 + "duration": 1.1670000000000016,
1360 + "lanes": Array [
1361 + 4,
1362 + ],
1363 + "timestamp": 66.294,
1364 + "type": "commit",
1365 + },
1366 + Object {
1367 + "batchUID": 0,
1368 + "depth": 1,
1369 + "duration": 0.018000000000000682,
1370 + "lanes": Array [
1371 + 4,
1372 + ],
1373 + "timestamp": 67.325,
1374 + "type": "layout-effects",
1375 + },
1376 + ],
1377 + "otherUserTimingMarks": Array [],
1378 + "startTime": 40806924876,
1379 +}
1380 +`;
packages/react-devtools-scheduling-profiler/src/utils/__tests__/preprocessData-test.js new
+352
@@ -0,0 +1,352 @@
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 +
8 +'use strict';
9 +
10 +import preprocessData, {
11 + getLanesFromTransportDecimalBitmask,
12 +} from '../preprocessData';
13 +import {REACT_TOTAL_NUM_LANES} from '../../constants';
14 +
15 +// Disable quotes rule in the whole file as we paste raw JSON as test inputs and
16 +// Prettier will already format the remaining quotes.
17 +/* eslint-disable quotes */
18 +
19 +describe(getLanesFromTransportDecimalBitmask, () => {
20 + it('should return array of lane numbers from bitmask string', () => {
21 + expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);
22 + expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]);
23 + expect(getLanesFromTransportDecimalBitmask('3')).toEqual([0, 1]);
24 + expect(getLanesFromTransportDecimalBitmask('1234')).toEqual([
25 + 1,
26 + 4,
27 + 6,
28 + 7,
29 + 10,
30 + ]); // 2 + 16 + 64 + 128 + 1024
31 + expect(
32 + getLanesFromTransportDecimalBitmask('1073741824'), // 0b1000000000000000000000000000000
33 + ).toEqual([30]);
34 + expect(
35 + getLanesFromTransportDecimalBitmask('2147483647'), // 0b1111111111111111111111111111111
36 + ).toEqual(Array.from(Array(31).keys()));
37 + });
38 +
39 + it('should return empty array if laneBitmaskString is not a bitmask', () => {
40 + expect(getLanesFromTransportDecimalBitmask('')).toEqual([]);
41 + expect(getLanesFromTransportDecimalBitmask('hello')).toEqual([]);
42 + expect(getLanesFromTransportDecimalBitmask('-1')).toEqual([]);
43 + expect(getLanesFromTransportDecimalBitmask('-0')).toEqual([]);
44 + });
45 +
46 + it('should ignore lanes outside REACT_TOTAL_NUM_LANES', () => {
47 + // Sanity check; this test may need to be updated when the no. of fiber
48 + // lanes are changed.
49 + expect(REACT_TOTAL_NUM_LANES).toBe(31);
50 +
51 + expect(
52 + getLanesFromTransportDecimalBitmask(
53 + '4294967297', // 2^32 + 1
54 + ),
55 + ).toEqual([0]);
56 + });
57 +});
58 +
59 +describe(preprocessData, () => {
60 + it('should throw given an empty timeline', () => {
61 + expect(() => preprocessData([])).toThrow();
62 + });
63 +
64 + it('should throw given a timeline with no Profile event', () => {
65 + expect(() =>
66 + // prettier-ignore
67 + preprocessData([
68 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--schedule-render-512-","ph":"R","pid":9312,"tid":10252,"ts":8994056569,"tts":1816966},
69 + ]),
70 + ).toThrow();
71 + });
72 +
73 + it('should return empty data given a timeline with no React scheduling profiling marks', () => {
74 + expect(
75 + // prettier-ignore
76 + preprocessData([
77 + {"args":{"data":{"startTime":8993778496}},"cat":"disabled-by-default-v8.cpu_profiler","id":"0x1","name":"Profile","ph":"P","pid":9312,"tid":10252,"ts":8993778520,"tts":1614266},
78 + {"pid":57632,"tid":38659,"ts":874860756135,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":18,"tdur":19,"tts":8700284918,"args":{}},
79 + {"pid":57632,"tid":38659,"ts":874860756158,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":30,"tdur":30,"tts":8700284941,"args":{}},
80 + {"pid":57632,"tid":38659,"ts":874860756192,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":21,"tdur":20,"tts":8700284976,"args":{}},
81 + {"pid":57632,"tid":38659,"ts":874860756216,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":6,"tdur":5,"tts":8700285000,"args":{}},
82 + {"pid":57632,"tid":38659,"ts":874860756224,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":7,"tdur":6,"tts":8700285008,"args":{}},
83 + {"pid":57632,"tid":38659,"ts":874860756233,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":5,"tdur":4,"tts":8700285017,"args":{}},
84 + ]),
85 + ).toEqual({
86 + startTime: 8993778496,
87 + duration: 865866977.737,
88 + events: [],
89 + measures: [],
90 + flamechart: [],
91 + otherUserTimingMarks: [],
92 + });
93 + });
94 +
95 + it('should error if events and measures are incomplete', () => {
96 + const error = spyOnDevAndProd(console, 'error');
97 + // prettier-ignore
98 + preprocessData([
99 + {"args":{"data":{"startTime":8993778496}},"cat":"disabled-by-default-v8.cpu_profiler","id":"0x1","name":"Profile","ph":"P","pid":9312,"tid":10252,"ts":8993778520,"tts":1614266},
100 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--render-start-8","ph":"R","pid":1852,"tid":12484,"ts":42351664678,"tts":1512475},
101 + ]);
102 + expect(error).toHaveBeenCalled();
103 + });
104 +
105 + it('should error if work is completed without being started', () => {
106 + const error = spyOnDevAndProd(console, 'error');
107 + // prettier-ignore
108 + preprocessData([
109 + {"args":{"data":{"startTime":8993778496}},"cat":"disabled-by-default-v8.cpu_profiler","id":"0x1","name":"Profile","ph":"P","pid":9312,"tid":10252,"ts":8993778520,"tts":1614266},
110 + {"args":{"data":{"navigationId":"E082C30FBDA3ACEE0E7B5FD75F8B7F0D"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":17232,"tid":13628,"ts":264686513020,"tts":4082554},
111 + ]);
112 + expect(error).toHaveBeenCalled();
113 + });
114 +
115 + it('should process complete set of events (page load sample data)', () => {
116 + expect(
117 + // prettier-ignore
118 + preprocessData([
119 + {"args":{"data":{"documentLoaderURL":"https://concurrent-demo.now.sh/","isLoadingMainFrame":true,"navigationId":"43BC238A4FB7548146D3CD739C9C9434"},"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"navigationStart","ph":"R","pid":9312,"tid":10252,"ts":8993749139,"tts":1646191},
120 + {"args":{"data":{"startTime":8993778496}},"cat":"disabled-by-default-v8.cpu_profiler","id":"0x1","name":"Profile","ph":"P","pid":9312,"tid":10252,"ts":8993778520,"tts":1614266},
121 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"fetchStart","ph":"R","pid":9312,"tid":10252,"ts":8993751576,"tts":1646197},
122 + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8993757325,"tts":1612760},
123 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"responseEnd","ph":"R","pid":9312,"tid":10252,"ts":8993762841,"tts":1652151},
124 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"unloadEventStart","ph":"R","pid":9312,"tid":10252,"ts":8993777756,"tts":1646416},
125 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"unloadEventEnd","ph":"R","pid":9312,"tid":10252,"ts":8993818104,"tts":1646419},
126 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domLoading","ph":"R","pid":9312,"tid":10252,"ts":8993820215,"tts":1647488},
127 + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8993886145,"tts":1771277},
128 + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8993886881,"tts":1778953},
129 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--schedule-render-512-","ph":"R","pid":9312,"tid":10252,"ts":8994056569,"tts":1816966},
130 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domInteractive","ph":"R","pid":9312,"tid":10252,"ts":8994058638,"tts":1818851},
131 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domContentLoadedEventStart","ph":"R","pid":9312,"tid":10252,"ts":8994058898,"tts":1819078},
132 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domContentLoadedEventEnd","ph":"R","pid":9312,"tid":10252,"ts":8994060045,"tts":1820100},
133 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994063789,"tts":1823183},
134 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994069024,"tts":1826507},
135 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994076204,"tts":1830657},
136 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994084372,"tts":1837590},
137 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domComplete","ph":"R","pid":9312,"tid":10252,"ts":8994085517,"tts":1838615},
138 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"loadEventStart","ph":"R","pid":9312,"tid":10252,"ts":8994085552,"tts":1838649},
139 + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"loadEventEnd","ph":"R","pid":9312,"tid":10252,"ts":8994086738,"tts":1839690},
140 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994088953,"tts":1840749},
141 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994093942,"tts":1844455},
142 + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8994119825,"tts":1877483},
143 + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8994122516,"tts":1886344},
144 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994136263,"tts":1871212},
145 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994142733,"tts":1875838},
146 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994149982,"tts":1880208},
147 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994156615,"tts":1885309},
148 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994163546,"tts":1887453},
149 + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8994166081,"tts":1895751},
150 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994173436,"tts":1894442},
151 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994176556,"tts":1897176},
152 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994184415,"tts":1904263},
153 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994185162,"tts":1904938},
154 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994192423,"tts":1910624},
155 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994192714,"tts":1910872},
156 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994200415,"tts":1917859},
157 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994200637,"tts":1918062},
158 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994208430,"tts":1924894},
159 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994208681,"tts":1925124},
160 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994216388,"tts":1932117},
161 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994218048,"tts":1933622},
162 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994225455,"tts":1940076},
163 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994225790,"tts":1940391},
164 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994233439,"tts":1947224},
165 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994233711,"tts":1947473},
166 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994241447,"tts":1954160},
167 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994241755,"tts":1954426},
168 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994249451,"tts":1961213},
169 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994249743,"tts":1961494},
170 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994257444,"tts":1968141},
171 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994257858,"tts":1968525},
172 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994265413,"tts":1975172},
173 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994265691,"tts":1975416},
174 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994273481,"tts":1981826},
175 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994273850,"tts":1982112},
176 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994281467,"tts":1988537},
177 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994282202,"tts":1988894},
178 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994289444,"tts":1994103},
179 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994291602,"tts":1995165},
180 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994299421,"tts":2000342},
181 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994300163,"tts":2001009},
182 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994309433,"tts":2005662},
183 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994309681,"tts":2005897},
184 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994317439,"tts":2012641},
185 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994317709,"tts":2012890},
186 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994325444,"tts":2019235},
187 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994325719,"tts":2019477},
188 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994333449,"tts":2026367},
189 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994333730,"tts":2026617},
190 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994341427,"tts":2033147},
191 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994341728,"tts":2033411},
192 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994349443,"tts":2040163},
193 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994349708,"tts":2040409},
194 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994357469,"tts":2047136},
195 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994357820,"tts":2047465},
196 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994365438,"tts":2054203},
197 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994365697,"tts":2054434},
198 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994373435,"tts":2061259},
199 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994373705,"tts":2061502},
200 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994381444,"tts":2068300},
201 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994381690,"tts":2068529},
202 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994389422,"tts":2075272},
203 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994389708,"tts":2075518},
204 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994397421,"tts":2082156},
205 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994398070,"tts":2082726},
206 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994405440,"tts":2088860},
207 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994405684,"tts":2089091},
208 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994413420,"tts":2095780},
209 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994413689,"tts":2096021},
210 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994421418,"tts":2102791},
211 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994421702,"tts":2103047},
212 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994429611,"tts":2110142},
213 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994429835,"tts":2110349},
214 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994436841,"tts":2115594},
215 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994437451,"tts":2116087},
216 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994437941,"tts":2116287},
217 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-1-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994439235,"tts":2117153},
218 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-2-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994441050,"tts":2118088},
219 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994441951,"tts":2118783},
220 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994442698,"tts":2119371},
221 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994443276,"tts":2119875},
222 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994443448,"tts":2120040},
223 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994445176,"tts":2121499},
224 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-1-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994445697,"tts":2121968},
225 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-2-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994446236,"tts":2122460},
226 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994446778,"tts":2122951},
227 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994447344,"tts":2123444},
228 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994449037,"tts":2124925},
229 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994449280,"tts":2125142},
230 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at SuspenseList\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994449831,"tts":2125639},
231 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8994450864,"tts":2126555},
232 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994451820,"tts":2127417},
233 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994455732,"tts":2130777},
234 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--schedule-state-update-1-ForceUpdateDemo_ForceUpdateDemo-\n at ForceUpdateDemo_ForceUpdateDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:18:98)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8994457934,"tts":2132671},
235 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8994458421,"tts":2133089},
236 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-1","ph":"R","pid":9312,"tid":10252,"ts":8994462600,"tts":2136847},
237 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8994464817,"tts":2138817},
238 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-1","ph":"R","pid":9312,"tid":10252,"ts":8994464844,"tts":2138843},
239 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-1","ph":"R","pid":9312,"tid":10252,"ts":8994465763,"tts":2139664},
240 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8994465784,"tts":2139686},
241 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8994466156,"tts":2140023},
242 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8994466372,"tts":2140208},
243 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-1-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995573418,"tts":2205582},
244 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-1-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995573870,"tts":2205980},
245 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-1024","ph":"R","pid":9312,"tid":10252,"ts":8995574538,"tts":2206568},
246 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995575662,"tts":2207534},
247 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-2-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995576307,"tts":2208142},
248 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995576659,"tts":2208445},
249 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995577001,"tts":2208736},
250 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8995577971,"tts":2209602},
251 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-1024","ph":"R","pid":9312,"tid":10252,"ts":8995578068,"tts":2209689},
252 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-1024","ph":"R","pid":9312,"tid":10252,"ts":8995580101,"tts":2211495},
253 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8995580122,"tts":2211515},
254 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8995580657,"tts":2211995},
255 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-2-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995644745,"tts":2217336},
256 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-2-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995645038,"tts":2217571},
257 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-2-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995645354,"tts":2217861},
258 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-2048","ph":"R","pid":9312,"tid":10252,"ts":8995645494,"tts":2217999},
259 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995646312,"tts":2218721},
260 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995647134,"tts":2219450},
261 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995647462,"tts":2219740},
262 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8995648162,"tts":2220335},
263 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-2048","ph":"R","pid":9312,"tid":10252,"ts":8995648191,"tts":2220363},
264 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-2048","ph":"R","pid":9312,"tid":10252,"ts":8995649260,"tts":2221320},
265 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8995649280,"tts":2221340},
266 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8995649611,"tts":2221636},
267 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995758769,"tts":2228839},
268 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995759018,"tts":2229064},
269 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995759442,"tts":2229424},
270 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-3-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995759660,"tts":2229627},
271 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-4096","ph":"R","pid":9312,"tid":10252,"ts":8995759857,"tts":2229803},
272 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995760575,"tts":2230456},
273 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8995761666,"tts":2231399},
274 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8995762296,"tts":2231965},
275 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-4096","ph":"R","pid":9312,"tid":10252,"ts":8995762367,"tts":2232017},
276 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-4096","ph":"R","pid":9312,"tid":10252,"ts":8995763427,"tts":2232966},
277 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8995763454,"tts":2232993},
278 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8995763625,"tts":2233154},
279 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996158083,"tts":2252466},
280 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996158391,"tts":2252730},
281 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996158738,"tts":2253038},
282 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996158983,"tts":2253239},
283 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-4-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996159231,"tts":2253447},
284 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-8192","ph":"R","pid":9312,"tid":10252,"ts":8996159465,"tts":2253624},
285 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-suspend-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996160268,"tts":2254312},
286 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8996161775,"tts":2255670},
287 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-8192","ph":"R","pid":9312,"tid":10252,"ts":8996161834,"tts":2255716},
288 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-8192","ph":"R","pid":9312,"tid":10252,"ts":8996163031,"tts":2256754},
289 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8996163051,"tts":2256773},
290 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8996163355,"tts":2257045},
291 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996357682,"tts":2267920},
292 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996357983,"tts":2268179},
293 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at SuspenseList\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996358231,"tts":2268389},
294 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996358538,"tts":2268679},
295 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996358786,"tts":2268867},
296 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996359042,"tts":2269066},
297 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--suspense-resolved-0-ResourceButton-\n at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295)\n at Suspense\n at div\n at div\n at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713)\n at App","ph":"R","pid":9312,"tid":10252,"ts":8996359296,"tts":2269264},
298 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-16384","ph":"R","pid":9312,"tid":10252,"ts":8996359448,"tts":2269412},
299 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8996362873,"tts":2272363},
300 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-16384","ph":"R","pid":9312,"tid":10252,"ts":8996362944,"tts":2272420},
301 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-16384","ph":"R","pid":9312,"tid":10252,"ts":8996364618,"tts":2273869},
302 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8996364645,"tts":2273895},
303 + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8996365391,"tts":2274547},
304 + ]),
305 + ).toMatchSnapshot();
306 + });
307 +
308 + it('should process forced update event', () => {
309 + expect(
310 + // prettier-ignore
311 + preprocessData([
312 + {"args":{"data":{"startTime":40806924876}},"cat":"disabled-by-default-v8.cpu_profiler","id":"0x2","name":"Profile","ph":"P","pid":1852,"tid":12484,"ts":40806924880,"tts":996658},
313 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--schedule-forced-update-16-ForceUpdateDemo_ForceUpdateDemo-\n at ForceUpdateDemo_ForceUpdateDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:18:98)\n at App","ph":"R","pid":1852,"tid":12484,"ts":40806988231,"tts":1037762},
314 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--render-start-16","ph":"R","pid":1852,"tid":12484,"ts":40806990146,"tts":1038890},
315 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":1852,"tid":12484,"ts":40806991123,"tts":1039401},
316 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--commit-start-16","ph":"R","pid":1852,"tid":12484,"ts":40806991170,"tts":1039447},
317 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--layout-effects-start-16","ph":"R","pid":1852,"tid":12484,"ts":40806992201,"tts":1040023},
318 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":1852,"tid":12484,"ts":40806992219,"tts":1040041},
319 + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":1852,"tid":12484,"ts":40806992337,"tts":1040149},
320 + ]),
321 + ).toMatchSnapshot();
322 + });
323 +
324 + it('should populate other user timing marks', () => {
325 + expect(
326 + // prettier-ignore
327 + preprocessData([
328 + {"args":{},"cat":"blink.user_timing","id":"0xcdf75f7c","name":"VCWithoutImage: root","ph":"n","pid":55132,"scope":"blink.user_timing","tid":775,"ts":458734963394},
329 + {"args":{"data":{"startTime":458738069897}},"cat":"disabled-by-default-v8.cpu_profiler","id":"0x4","name":"Profile","ph":"P","pid":55132,"tid":775,"ts":458738069898,"tts":27896428},
330 + {"args":{"data":{"navigationId":"B8774C733A75946C099FE21F8A0E8D38"}},"cat":"blink.user_timing","name":"--a-mark-that-looks-like-one-of-ours","ph":"R","pid":55132,"tid":775,"ts":458738256356,"tts":28082555},
331 + {"args":{"data":{"navigationId":"B8774C733A75946C099FE21F8A0E8D38"}},"cat":"blink.user_timing","name":"Some other mark","ph":"R","pid":55132,"tid":775,"ts":458738261491,"tts":28087691},
332 + ]).otherUserTimingMarks,
333 + ).toMatchInlineSnapshot(`
334 + Array [
335 + Object {
336 + "name": "VCWithoutImage: root",
337 + "timestamp": -3106.503,
338 + },
339 + Object {
340 + "name": "--a-mark-that-looks-like-one-of-ours",
341 + "timestamp": 186.459,
342 + },
343 + Object {
344 + "name": "Some other mark",
345 + "timestamp": 191.594,
346 + },
347 + ]
348 + `);
349 + });
350 +
351 + // TODO: Add test for flamechart parsing
352 +});
packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js new
+44
@@ -0,0 +1,44 @@
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 memoize from 'memoize-one';
11 +
12 +import type {BatchUID, Milliseconds, ReactProfilerData} from '../types';
13 +
14 +function unmemoizedGetBatchRange(
15 + batchUID: BatchUID,
16 + data: ReactProfilerData,
17 +): [Milliseconds, Milliseconds] {
18 + const {measures} = data;
19 +
20 + let startTime = 0;
21 + let stopTime = Infinity;
22 +
23 + let i = 0;
24 +
25 + for (i; i < measures.length; i++) {
26 + const measure = measures[i];
27 + if (measure.batchUID === batchUID) {
28 + startTime = measure.timestamp;
29 + break;
30 + }
31 + }
32 +
33 + for (i; i < measures.length; i++) {
34 + const measure = measures[i];
35 + stopTime = measure.timestamp;
36 + if (measure.batchUID !== batchUID) {
37 + break;
38 + }
39 + }
40 +
41 + return [startTime, stopTime];
42 +}
43 +
44 +export const getBatchRange = memoize(unmemoizedGetBatchRange);
packages/react-devtools-scheduling-profiler/src/utils/preprocessData.js new
+461
@@ -0,0 +1,461 @@
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 {
11 + importFromChromeTimeline,
12 + Flamechart as SpeedscopeFlamechart,
13 +} from '@elg/speedscope';
14 +import type {TimelineEvent} from '@elg/speedscope';
15 +import type {
16 + Milliseconds,
17 + BatchUID,
18 + Flamechart,
19 + ReactLane,
20 + ReactMeasureType,
21 + ReactProfilerData,
22 +} from '../types';
23 +
24 +import {REACT_TOTAL_NUM_LANES} from '../constants';
25 +
26 +type MeasureStackElement = {|
27 + type: ReactMeasureType,
28 + depth: number,
29 + index: number,
30 + startTime: Milliseconds,
31 + stopTime?: Milliseconds,
32 +|};
33 +
34 +type ProcessorState = {|
35 + nextRenderShouldGenerateNewBatchID: boolean,
36 + batchUID: BatchUID,
37 + uidCounter: BatchUID,
38 + measureStack: MeasureStackElement[],
39 +|};
40 +
41 +// Exported for tests
42 +export function getLanesFromTransportDecimalBitmask(
43 + laneBitmaskString: string,
44 +): ReactLane[] {
45 + const laneBitmask = parseInt(laneBitmaskString, 10);
46 +
47 + // As negative numbers are stored in two's complement format, our bitmask
48 + // checks will be thrown off by them.
49 + if (laneBitmask < 0) {
50 + return [];
51 + }
52 +
53 + const lanes = [];
54 + let powersOfTwo = 0;
55 + while (powersOfTwo <= REACT_TOTAL_NUM_LANES) {
56 + if ((1 << powersOfTwo) & laneBitmask) {
57 + lanes.push(powersOfTwo);
58 + }
59 + powersOfTwo++;
60 + }
61 + return lanes;
62 +}
63 +
64 +function getLastType(stack: $PropertyType<ProcessorState, 'measureStack'>) {
65 + if (stack.length > 0) {
66 + const {type} = stack[stack.length - 1];
67 + return type;
68 + }
69 + return null;
70 +}
71 +
72 +function getDepth(stack: $PropertyType<ProcessorState, 'measureStack'>) {
73 + if (stack.length > 0) {
74 + const {depth, type} = stack[stack.length - 1];
75 + return type === 'render-idle' ? depth : depth + 1;
76 + }
77 + return 0;
78 +}
79 +
80 +function markWorkStarted(
81 + type: ReactMeasureType,
82 + startTime: Milliseconds,
83 + lanes: ReactLane[],
84 + currentProfilerData: ReactProfilerData,
85 + state: ProcessorState,
86 +) {
87 + const {batchUID, measureStack} = state;
88 + const index = currentProfilerData.measures.length;
89 + const depth = getDepth(measureStack);
90 +
91 + state.measureStack.push({depth, index, startTime, type});
92 +
93 + currentProfilerData.measures.push({
94 + type,
95 + batchUID,
96 + depth,
97 + lanes,
98 + timestamp: startTime,
99 + duration: 0,
100 + });
101 +}
102 +
103 +function markWorkCompleted(
104 + type: ReactMeasureType,
105 + stopTime: Milliseconds,
106 + currentProfilerData: ReactProfilerData,
107 + stack: $PropertyType<ProcessorState, 'measureStack'>,
108 +) {
109 + if (stack.length === 0) {
110 + console.error(
111 + 'Unexpected type "%s" completed at %sms while stack is empty.',
112 + type,
113 + stopTime,
114 + );
115 + // Ignore work "completion" user timing mark that doesn't complete anything
116 + return;
117 + }
118 +
119 + const last = stack[stack.length - 1];
120 + if (last.type !== type) {
121 + console.error(
122 + 'Unexpected type "%s" completed at %sms before "%s" completed.',
123 + type,
124 + stopTime,
125 + last.type,
126 + );
127 + }
128 +
129 + const {index, startTime} = stack.pop();
130 + const measure = currentProfilerData.measures[index];
131 + if (!measure) {
132 + console.error('Could not find matching measure for type "%s".', type);
133 + }
134 +
135 + // $FlowFixMe This property should not be writable outside of this function.
136 + measure.duration = stopTime - startTime;
137 +}
138 +
139 +function throwIfIncomplete(
140 + type: ReactMeasureType,
141 + stack: $PropertyType<ProcessorState, 'measureStack'>,
142 +) {
143 + const lastIndex = stack.length - 1;
144 + if (lastIndex >= 0) {
145 + const last = stack[lastIndex];
146 + if (last.stopTime === undefined && last.type === type) {
147 + throw new Error(
148 + `Unexpected type "${type}" started before "${last.type}" completed.`,
149 + );
150 + }
151 + }
152 +}
153 +
154 +function processTimelineEvent(
155 + event: TimelineEvent,
156 + /** Finalized profiler data up to `event`. May be mutated. */
157 + currentProfilerData: ReactProfilerData,
158 + /** Intermediate processor state. May be mutated. */
159 + state: ProcessorState,
160 +) {
161 + const {cat, name, ts, ph} = event;
162 + if (cat !== 'blink.user_timing') {
163 + return;
164 + }
165 +
166 + const startTime = (ts - currentProfilerData.startTime) / 1000;
167 +
168 + // React Events - schedule
169 + if (name.startsWith('--schedule-render-')) {
170 + const [laneBitmaskString, ...splitComponentStack] = name
171 + .substr(18)
172 + .split('-');
173 + currentProfilerData.events.push({
174 + type: 'schedule-render',
175 + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
176 + componentStack: splitComponentStack.join('-'),
177 + timestamp: startTime,
178 + });
179 + } else if (name.startsWith('--schedule-forced-update-')) {
180 + const [
181 + laneBitmaskString,
182 + componentName,
183 + ...splitComponentStack
184 + ] = name.substr(25).split('-');
185 + const isCascading = !!state.measureStack.find(
186 + ({type}) => type === 'commit',
187 + );
188 + currentProfilerData.events.push({
189 + type: 'schedule-force-update',
190 + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
191 + componentName,
192 + componentStack: splitComponentStack.join('-'),
193 + timestamp: startTime,
194 + isCascading,
195 + });
196 + } else if (name.startsWith('--schedule-state-update-')) {
197 + const [
198 + laneBitmaskString,
199 + componentName,
200 + ...splitComponentStack
201 + ] = name.substr(24).split('-');
202 + const isCascading = !!state.measureStack.find(
203 + ({type}) => type === 'commit',
204 + );
205 + currentProfilerData.events.push({
206 + type: 'schedule-state-update',
207 + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString),
208 + componentName,
209 + componentStack: splitComponentStack.join('-'),
210 + timestamp: startTime,
211 + isCascading,
212 + });
213 + } // eslint-disable-line brace-style
214 +
215 + // React Events - suspense
216 + else if (name.startsWith('--suspense-suspend-')) {
217 + const [id, componentName, ...splitComponentStack] = name
218 + .substr(19)
219 + .split('-');
220 + currentProfilerData.events.push({
221 + type: 'suspense-suspend',
222 + id,
223 + componentName,
224 + componentStack: splitComponentStack.join('-'),
225 + timestamp: startTime,
226 + });
227 + } else if (name.startsWith('--suspense-resolved-')) {
228 + const [id, componentName, ...splitComponentStack] = name
229 + .substr(20)
230 + .split('-');
231 + currentProfilerData.events.push({
232 + type: 'suspense-resolved',
233 + id,
234 + componentName,
235 + componentStack: splitComponentStack.join('-'),
236 + timestamp: startTime,
237 + });
238 + } else if (name.startsWith('--suspense-rejected-')) {
239 + const [id, componentName, ...splitComponentStack] = name
240 + .substr(20)
241 + .split('-');
242 + currentProfilerData.events.push({
243 + type: 'suspense-rejected',
244 + id,
245 + componentName,
246 + componentStack: splitComponentStack.join('-'),
247 + timestamp: startTime,
248 + });
249 + } // eslint-disable-line brace-style
250 +
251 + // React Measures - render
252 + else if (name.startsWith('--render-start-')) {
253 + if (state.nextRenderShouldGenerateNewBatchID) {
254 + state.nextRenderShouldGenerateNewBatchID = false;
255 + state.batchUID = ((state.uidCounter++: any): BatchUID);
256 + }
257 + const laneBitmaskString = name.substr(15);
258 + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
259 + throwIfIncomplete('render', state.measureStack);
260 + if (getLastType(state.measureStack) !== 'render-idle') {
261 + markWorkStarted(
262 + 'render-idle',
263 + startTime,
264 + lanes,
265 + currentProfilerData,
266 + state,
267 + );
268 + }
269 + markWorkStarted('render', startTime, lanes, currentProfilerData, state);
270 + } else if (
271 + name.startsWith('--render-stop') ||
272 + name.startsWith('--render-yield')
273 + ) {
274 + markWorkCompleted(
275 + 'render',
276 + startTime,
277 + currentProfilerData,
278 + state.measureStack,
279 + );
280 + } else if (name.startsWith('--render-cancel')) {
281 + state.nextRenderShouldGenerateNewBatchID = true;
282 + markWorkCompleted(
283 + 'render',
284 + startTime,
285 + currentProfilerData,
286 + state.measureStack,
287 + );
288 + markWorkCompleted(
289 + 'render-idle',
290 + startTime,
291 + currentProfilerData,
292 + state.measureStack,
293 + );
294 + } // eslint-disable-line brace-style
295 +
296 + // React Measures - commits
297 + else if (name.startsWith('--commit-start-')) {
298 + state.nextRenderShouldGenerateNewBatchID = true;
299 + const laneBitmaskString = name.substr(15);
300 + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
301 + markWorkStarted('commit', startTime, lanes, currentProfilerData, state);
302 + } else if (name.startsWith('--commit-stop')) {
303 + markWorkCompleted(
304 + 'commit',
305 + startTime,
306 + currentProfilerData,
307 + state.measureStack,
308 + );
309 + markWorkCompleted(
310 + 'render-idle',
311 + startTime,
312 + currentProfilerData,
313 + state.measureStack,
314 + );
315 + } // eslint-disable-line brace-style
316 +
317 + // React Measures - layout effects
318 + else if (name.startsWith('--layout-effects-start-')) {
319 + const laneBitmaskString = name.substr(23);
320 + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
321 + markWorkStarted(
322 + 'layout-effects',
323 + startTime,
324 + lanes,
325 + currentProfilerData,
326 + state,
327 + );
328 + } else if (name.startsWith('--layout-effects-stop')) {
329 + markWorkCompleted(
330 + 'layout-effects',
331 + startTime,
332 + currentProfilerData,
333 + state.measureStack,
334 + );
335 + } // eslint-disable-line brace-style
336 +
337 + // React Measures - passive effects
338 + else if (name.startsWith('--passive-effects-start-')) {
339 + const laneBitmaskString = name.substr(24);
340 + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
341 + markWorkStarted(
342 + 'passive-effects',
343 + startTime,
344 + lanes,
345 + currentProfilerData,
346 + state,
347 + );
348 + } else if (name.startsWith('--passive-effects-stop')) {
349 + markWorkCompleted(
350 + 'passive-effects',
351 + startTime,
352 + currentProfilerData,
353 + state.measureStack,
354 + );
355 + } // eslint-disable-line brace-style
356 +
357 + // Other user timing marks/measures
358 + else if (ph === 'R' || ph === 'n') {
359 + // User Timing mark
360 + currentProfilerData.otherUserTimingMarks.push({
361 + name,
362 + timestamp: startTime,
363 + });
364 + } else if (ph === 'b') {
365 + // TODO: Begin user timing measure
366 + } else if (ph === 'e') {
367 + // TODO: End user timing measure
368 + } // eslint-disable-line brace-style
369 +
370 + // Unrecognized event
371 + else {
372 + throw new Error(
373 + `Unrecognized event ${JSON.stringify(
374 + event,
375 + )}! This is likely a bug in this profiler tool.`,
376 + );
377 + }
378 +}
379 +
380 +function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
381 + const parsedData = importFromChromeTimeline(rawData, 'react-devtools');
382 + const profile = parsedData.profiles[0]; // TODO: Choose the main CPU thread only
383 +
384 + const speedscopeFlamechart = new SpeedscopeFlamechart({
385 + getTotalWeight: profile.getTotalWeight.bind(profile),
386 + forEachCall: profile.forEachCall.bind(profile),
387 + formatValue: profile.formatValue.bind(profile),
388 + getColorBucketForFrame: () => 0,
389 + });
390 +
391 + const flamechart: Flamechart = speedscopeFlamechart.getLayers().map(layer =>
392 + layer.map(({start, end, node: {frame: {name, file, line, col}}}) => ({
393 + name,
394 + timestamp: start / 1000,
395 + duration: (end - start) / 1000,
396 + scriptUrl: file,
397 + locationLine: line,
398 + locationColumn: col,
399 + })),
400 + );
401 +
402 + return flamechart;
403 +}
404 +
405 +export default function preprocessData(
406 + timeline: TimelineEvent[],
407 +): ReactProfilerData {
408 + const flamechart = preprocessFlamechart(timeline);
409 +
410 + const profilerData: ReactProfilerData = {
411 + startTime: 0,
412 + duration: 0,
413 + events: [],
414 + measures: [],
415 + flamechart,
416 + otherUserTimingMarks: [],
417 + };
418 +
419 + // Sort `timeline`. JSON Array Format trace events need not be ordered. See:
420 + // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.f2f0yd51wi15
421 + timeline = timeline.filter(Boolean).sort((a, b) => (a.ts > b.ts ? 1 : -1));
422 +
423 + // Events displayed in flamechart have timestamps relative to the profile
424 + // event's startTime. Source: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1486
425 + //
426 + // We'll thus expect there to be a 'Profile' event; if there is not one, we
427 + // can deduce that there are no flame chart events. As we expect React
428 + // scheduling profiling user timing marks to be recorded together with browser
429 + // flame chart events, we can futher deduce that the data is invalid and we
430 + // don't bother finding React events.
431 + const indexOfProfileEvent = timeline.findIndex(
432 + event => event.name === 'Profile',
433 + );
434 + if (indexOfProfileEvent === -1) {
435 + return profilerData;
436 + }
437 +
438 + // Use Profile event's `startTime` as the start time to align with flame chart.
439 + // TODO: Remove assumption that there'll only be 1 'Profile' event. If this
440 + // assumption does not hold, the chart may start at the wrong time.
441 + profilerData.startTime = timeline[indexOfProfileEvent].args.data.startTime;
442 + profilerData.duration =
443 + (timeline[timeline.length - 1].ts - profilerData.startTime) / 1000;
444 +
445 + const state: ProcessorState = {
446 + batchUID: 0,
447 + uidCounter: 0,
448 + nextRenderShouldGenerateNewBatchID: true,
449 + measureStack: [],
450 + };
451 +
452 + timeline.forEach(event => processTimelineEvent(event, profilerData, state));
453 +
454 + // Validate that all events and measures are complete
455 + const {measureStack} = state;
456 + if (measureStack.length > 0) {
457 + console.error('Incomplete events or measures', measureStack);
458 + }
459 +
460 + return profilerData;
461 +}
packages/react-devtools-scheduling-profiler/src/utils/readInputData.js new
+36
@@ -0,0 +1,36 @@
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 nullthrows from 'nullthrows';
11 +
12 +export const readInputData = (file: File): Promise<string> => {
13 + if (!file.name.endsWith('.json')) {
14 + return Promise.reject(
15 + new Error(
16 + 'Invalid file type. Only JSON performance profiles are supported',
17 + ),
18 + );
19 + }
20 +
21 + const fileReader = new FileReader();
22 +
23 + return new Promise((resolve, reject) => {
24 + fileReader.onload = () => {
25 + const result = nullthrows(fileReader.result);
26 + if (typeof result === 'string') {
27 + resolve(result);
28 + }
29 + reject(new Error('Input file was not read as a string'));
30 + };
31 +
32 + fileReader.onerror = () => reject(fileReader.error);
33 +
34 + fileReader.readAsText(file);
35 + });
36 +};
packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js new
+70
@@ -0,0 +1,70 @@
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 {useLayoutEffect, useRef} from 'react';
11 +
12 +const TOOLTIP_OFFSET = 4;
13 +
14 +export default function useSmartTooltip({
15 + mouseX,
16 + mouseY,
17 +}: {
18 + mouseX: number,
19 + mouseY: number,
20 +}) {
21 + const ref = useRef<HTMLElement | null>(null);
22 +
23 + useLayoutEffect(() => {
24 + const element = ref.current;
25 + if (element !== null) {
26 + // Let's check the vertical position.
27 + if (
28 + mouseY + TOOLTIP_OFFSET + element.offsetHeight >=
29 + window.innerHeight
30 + ) {
31 + // The tooltip doesn't fit below the mouse cursor (which is our
32 + // default strategy). Therefore we try to position it either above the
33 + // mouse cursor or finally aligned with the window's top edge.
34 + if (mouseY - TOOLTIP_OFFSET - element.offsetHeight > 0) {
35 + // We position the tooltip above the mouse cursor if it fits there.
36 + element.style.top = `${mouseY -
37 + element.offsetHeight -
38 + TOOLTIP_OFFSET}px`;
39 + } else {
40 + // Otherwise we align the tooltip with the window's top edge.
41 + element.style.top = '0px';
42 + }
43 + } else {
44 + element.style.top = `${mouseY + TOOLTIP_OFFSET}px`;
45 + }
46 +
47 + // Now let's check the horizontal position.
48 + if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= window.innerWidth) {
49 + // The tooltip doesn't fit at the right of the mouse cursor (which is
50 + // our default strategy). Therefore we try to position it either at the
51 + // left of the mouse cursor or finally aligned with the window's left
52 + // edge.
53 + if (mouseX - TOOLTIP_OFFSET - element.offsetWidth > 0) {
54 + // We position the tooltip at the left of the mouse cursor if it fits
55 + // there.
56 + element.style.left = `${mouseX -
57 + element.offsetWidth -
58 + TOOLTIP_OFFSET}px`;
59 + } else {
60 + // Otherwise, align the tooltip with the window's left edge.
61 + element.style.left = '0px';
62 + }
63 + } else {
64 + element.style.left = `${mouseX + TOOLTIP_OFFSET}px`;
65 + }
66 + }
67 + }, [mouseX, mouseY, ref]);
68 +
69 + return ref;
70 +}
packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js new
+44
@@ -0,0 +1,44 @@
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 './geometry';
11 +
12 +import {Surface} from './Surface';
13 +import {View} from './View';
14 +
15 +/**
16 + * View that fills its visible area with a CSS color.
17 + */
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 +
34 + draw(context: CanvasRenderingContext2D) {
35 + const {_color, visibleArea} = this;
36 + context.fillStyle = _color;
37 + context.fillRect(
38 + visibleArea.origin.x,
39 + visibleArea.origin.y,
40 + visibleArea.size.width,
41 + visibleArea.size.height,
42 + );
43 + }
44 +}
packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js new
+342
@@ -0,0 +1,342 @@
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 + WheelPlainInteraction,
16 + WheelWithShiftInteraction,
17 + WheelWithControlInteraction,
18 + WheelWithMetaInteraction,
19 +} from './useCanvasInteraction';
20 +import type {Rect} from './geometry';
21 +
22 +import {Surface} from './Surface';
23 +import {View} from './View';
24 +import {rectContainsPoint} from './geometry';
25 +import {clamp} from './utils/clamp';
26 +import {
27 + MIN_ZOOM_LEVEL,
28 + MAX_ZOOM_LEVEL,
29 + MOVE_WHEEL_DELTA_THRESHOLD,
30 +} from './constants';
31 +
32 +type HorizontalPanAndZoomState = $ReadOnly<{|
33 + /** Horizontal offset; positive in the left direction */
34 + offsetX: number,
35 + zoomLevel: number,
36 +|}>;
37 +
38 +export type HorizontalPanAndZoomViewOnChangeCallback = (
39 + state: HorizontalPanAndZoomState,
40 + view: HorizontalPanAndZoomView,
41 +) => void;
42 +
43 +function panAndZoomStatesAreEqual(
44 + state1: HorizontalPanAndZoomState,
45 + state2: HorizontalPanAndZoomState,
46 +): boolean {
47 + return (
48 + state1.offsetX === state2.offsetX && state1.zoomLevel === state2.zoomLevel
49 + );
50 +}
51 +
52 +function zoomLevelAndIntrinsicWidthToFrameWidth(
53 + zoomLevel: number,
54 + intrinsicWidth: number,
55 +): number {
56 + return intrinsicWidth * zoomLevel;
57 +}
58 +
59 +export class HorizontalPanAndZoomView extends View {
60 + _intrinsicContentWidth: number;
61 +
62 + _panAndZoomState: HorizontalPanAndZoomState = {
63 + offsetX: 0,
64 + zoomLevel: 0.25,
65 + };
66 +
67 + _isPanning = false;
68 +
69 + _onStateChange: HorizontalPanAndZoomViewOnChangeCallback = () => {};
70 +
71 + constructor(
72 + surface: Surface,
73 + frame: Rect,
74 + contentView: View,
75 + intrinsicContentWidth: number,
76 + onStateChange?: HorizontalPanAndZoomViewOnChangeCallback,
77 + ) {
78 + super(surface, frame);
79 + this.addSubview(contentView);
80 + this._intrinsicContentWidth = intrinsicContentWidth;
81 + if (onStateChange) this._onStateChange = onStateChange;
82 + }
83 +
84 + setFrame(newFrame: Rect) {
85 + super.setFrame(newFrame);
86 +
87 + // Revalidate panAndZoomState
88 + this._setStateAndInformCallbacksIfChanged(this._panAndZoomState);
89 + }
90 +
91 + setPanAndZoomState(proposedState: HorizontalPanAndZoomState) {
92 + this._setPanAndZoomState(proposedState);
93 + }
94 +
95 + /**
96 + * Just sets pan and zoom state. Use `_setStateAndInformCallbacksIfChanged`
97 + * if this view's callbacks should also be called.
98 + *
99 + * @returns Whether state was changed
100 + * @private
101 + */
102 + _setPanAndZoomState(proposedState: HorizontalPanAndZoomState): boolean {
103 + const clampedState = this._clampedProposedState(proposedState);
104 + if (panAndZoomStatesAreEqual(clampedState, this._panAndZoomState)) {
105 + return false;
106 + }
107 + this._panAndZoomState = clampedState;
108 + this.setNeedsDisplay();
109 + return true;
110 + }
111 +
112 + /**
113 + * @private
114 + */
115 + _setStateAndInformCallbacksIfChanged(
116 + proposedState: HorizontalPanAndZoomState,
117 + ) {
118 + if (this._setPanAndZoomState(proposedState)) {
119 + this._onStateChange(this._panAndZoomState, this);
120 + }
121 + }
122 +
123 + desiredSize() {
124 + return this._contentView.desiredSize();
125 + }
126 +
127 + /**
128 + * Reference to the content view. This view is also the only view in
129 + * `this.subviews`.
130 + */
131 + get _contentView() {
132 + return this.subviews[0];
133 + }
134 +
135 + layoutSubviews() {
136 + const {offsetX, zoomLevel} = this._panAndZoomState;
137 + const proposedFrame = {
138 + origin: {
139 + x: this.frame.origin.x + offsetX,
140 + y: this.frame.origin.y,
141 + },
142 + size: {
143 + width: zoomLevelAndIntrinsicWidthToFrameWidth(
144 + zoomLevel,
145 + this._intrinsicContentWidth,
146 + ),
147 + height: this.frame.size.height,
148 + },
149 + };
150 + this._contentView.setFrame(proposedFrame);
151 + super.layoutSubviews();
152 + }
153 +
154 + /**
155 + * Zoom to a specific range of the content specified as a range of the
156 + * content view's intrinsic content size.
157 + *
158 + * Does not inform callbacks of state change since this is a public API.
159 + */
160 + zoomToRange(startX: number, endX: number) {
161 + // Zoom and offset must be done separately, so that if the zoom level is
162 + // clamped the offset will still be correct (unless it gets clamped too).
163 + const zoomClampedState = this._clampedProposedStateZoomLevel({
164 + ...this._panAndZoomState,
165 + // Let:
166 + // I = intrinsic content width, i = zoom range = (endX - startX).
167 + // W = contentView's final zoomed width, w = this view's width
168 + // Goal: we want the visible width w to only contain the requested range i.
169 + // Derivation:
170 + // (1) i/I = w/W (by intuitive definition of variables)
171 + // (2) W = zoomLevel * I (definition of zoomLevel)
172 + // => zoomLevel = W/I (algebraic manipulation)
173 + // = w/i (rearranging (1))
174 + zoomLevel: this.frame.size.width / (endX - startX),
175 + });
176 + const offsetAdjustedState = this._clampedProposedStateOffsetX({
177 + ...zoomClampedState,
178 + offsetX: -startX * zoomClampedState.zoomLevel,
179 + });
180 + this._setPanAndZoomState(offsetAdjustedState);
181 + }
182 +
183 + _handleMouseDown(interaction: MouseDownInteraction) {
184 + if (rectContainsPoint(interaction.payload.location, this.frame)) {
185 + this._isPanning = true;
186 + }
187 + }
188 +
189 + _handleMouseMove(interaction: MouseMoveInteraction) {
190 + if (!this._isPanning) {
191 + return;
192 + }
193 + const {offsetX} = this._panAndZoomState;
194 + const {movementX} = interaction.payload.event;
195 + this._setStateAndInformCallbacksIfChanged({
196 + ...this._panAndZoomState,
197 + offsetX: offsetX + movementX,
198 + });
199 + }
200 +
201 + _handleMouseUp(interaction: MouseUpInteraction) {
202 + if (this._isPanning) {
203 + this._isPanning = false;
204 + }
205 + }
206 +
207 + _handleWheelPlain(interaction: WheelPlainInteraction) {
208 + const {
209 + location,
210 + delta: {deltaX, deltaY},
211 + } = interaction.payload;
212 + if (!rectContainsPoint(location, this.frame)) {
213 + return; // Not scrolling on view
214 + }
215 +
216 + const absDeltaX = Math.abs(deltaX);
217 + const absDeltaY = Math.abs(deltaY);
218 + if (absDeltaY > absDeltaX) {
219 + return; // Scrolling vertically
220 + }
221 +
222 + if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
223 + return;
224 + }
225 +
226 + this._setStateAndInformCallbacksIfChanged({
227 + ...this._panAndZoomState,
228 + offsetX: this._panAndZoomState.offsetX - deltaX,
229 + });
230 + }
231 +
232 + _handleWheelZoom(
233 + interaction:
234 + | WheelWithShiftInteraction
235 + | WheelWithControlInteraction
236 + | WheelWithMetaInteraction,
237 + ) {
238 + const {
239 + location,
240 + delta: {deltaY},
241 + } = interaction.payload;
242 + if (!rectContainsPoint(location, this.frame)) {
243 + return; // Not scrolling on view
244 + }
245 +
246 + const absDeltaY = Math.abs(deltaY);
247 + if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
248 + return;
249 + }
250 +
251 + const zoomClampedState = this._clampedProposedStateZoomLevel({
252 + ...this._panAndZoomState,
253 + zoomLevel: this._panAndZoomState.zoomLevel * (1 + 0.005 * -deltaY),
254 + });
255 +
256 + // Determine where the mouse is, and adjust the offset so that point stays
257 + // centered after zooming.
258 + const oldMouseXInFrame = location.x - zoomClampedState.offsetX;
259 + const fractionalMouseX =
260 + oldMouseXInFrame / this._contentView.frame.size.width;
261 + const newContentWidth = zoomLevelAndIntrinsicWidthToFrameWidth(
262 + zoomClampedState.zoomLevel,
263 + this._intrinsicContentWidth,
264 + );
265 + const newMouseXInFrame = fractionalMouseX * newContentWidth;
266 +
267 + const offsetAdjustedState = this._clampedProposedStateOffsetX({
268 + ...zoomClampedState,
269 + offsetX: location.x - newMouseXInFrame,
270 + });
271 +
272 + this._setStateAndInformCallbacksIfChanged(offsetAdjustedState);
273 + }
274 +
275 + handleInteraction(interaction: Interaction) {
276 + switch (interaction.type) {
277 + case 'mousedown':
278 + this._handleMouseDown(interaction);
279 + break;
280 + case 'mousemove':
281 + this._handleMouseMove(interaction);
282 + break;
283 + case 'mouseup':
284 + this._handleMouseUp(interaction);
285 + break;
286 + case 'wheel-plain':
287 + this._handleWheelPlain(interaction);
288 + break;
289 + case 'wheel-shift':
290 + case 'wheel-control':
291 + case 'wheel-meta':
292 + this._handleWheelZoom(interaction);
293 + break;
294 + }
295 + }
296 +
297 + /**
298 + * @private
299 + */
300 + _clampedProposedStateZoomLevel(
301 + proposedState: HorizontalPanAndZoomState,
302 + ): HorizontalPanAndZoomState {
303 + // Content-based min zoom level to ensure that contentView's width >= our width.
304 + const minContentBasedZoomLevel =
305 + this.frame.size.width / this._intrinsicContentWidth;
306 + const minZoomLevel = Math.max(MIN_ZOOM_LEVEL, minContentBasedZoomLevel);
307 + return {
308 + ...proposedState,
309 + zoomLevel: clamp(minZoomLevel, MAX_ZOOM_LEVEL, proposedState.zoomLevel),
310 + };
311 + }
312 +
313 + /**
314 + * @private
315 + */
316 + _clampedProposedStateOffsetX(
317 + proposedState: HorizontalPanAndZoomState,
318 + ): HorizontalPanAndZoomState {
319 + const newContentWidth = zoomLevelAndIntrinsicWidthToFrameWidth(
320 + proposedState.zoomLevel,
321 + this._intrinsicContentWidth,
322 + );
323 + return {
324 + ...proposedState,
325 + offsetX: clamp(
326 + -(newContentWidth - this.frame.size.width),
327 + 0,
328 + proposedState.offsetX,
329 + ),
330 + };
331 + }
332 +
333 + /**
334 + * @private
335 + */
336 + _clampedProposedState(
337 + proposedState: HorizontalPanAndZoomState,
338 + ): HorizontalPanAndZoomState {
339 + const zoomClampedState = this._clampedProposedStateZoomLevel(proposedState);
340 + return this._clampedProposedStateOffsetX(zoomClampedState);
341 + }
342 +}
packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js new
+311
@@ -0,0 +1,311 @@
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 +
18 +import nullthrows from 'nullthrows';
19 +import {Surface} from './Surface';
20 +import {View} from './View';
21 +import {rectContainsPoint} from './geometry';
22 +import {layeredLayout, noopLayout} from './layouter';
23 +import {ColorView} from './ColorView';
24 +import {clamp} from './utils/clamp';
25 +
26 +type ResizeBarState = 'normal' | 'hovered' | 'dragging';
27 +
28 +type ResizingState = $ReadOnly<{|
29 + /** Distance between top of resize bar and mouseY */
30 + cursorOffsetInBarFrame: number,
31 + /** Mouse's vertical coordinates relative to canvas */
32 + mouseY: number,
33 +|}>;
34 +
35 +type LayoutState = $ReadOnly<{|
36 + /** Resize bar's vertical position relative to resize view's frame.origin.y */
37 + barOffsetY: number,
38 +|}>;
39 +
40 +function getColorForBarState(state: ResizeBarState): string {
41 + // Colors obtained from Firefox Profiler
42 + switch (state) {
43 + case 'normal':
44 + return '#ccc';
45 + case 'hovered':
46 + return '#bbb';
47 + case 'dragging':
48 + return '#aaa';
49 + }
50 + throw new Error(`Unknown resize bar state ${state}`);
51 +}
52 +
53 +class ResizeBar extends View {
54 + _intrinsicContentSize: Size = {
55 + width: 0,
56 + height: 5,
57 + };
58 +
59 + _interactionState: ResizeBarState = 'normal';
60 +
61 + constructor(surface: Surface, frame: Rect) {
62 + super(surface, frame, layeredLayout);
63 + this.addSubview(new ColorView(surface, frame, ''));
64 + this._updateColor();
65 + }
66 +
67 + desiredSize() {
68 + return this._intrinsicContentSize;
69 + }
70 +
71 + _getColorView(): ColorView {
72 + return (this.subviews[0]: any);
73 + }
74 +
75 + _updateColor() {
76 + this._getColorView().setColor(getColorForBarState(this._interactionState));
77 + }
78 +
79 + _setInteractionState(state: ResizeBarState) {
80 + if (this._interactionState === state) {
81 + return;
82 + }
83 + this._interactionState = state;
84 + this._updateColor();
85 + }
86 +
87 + _handleMouseDown(interaction: MouseDownInteraction) {
88 + const cursorInView = rectContainsPoint(
89 + interaction.payload.location,
90 + this.frame,
91 + );
92 + if (cursorInView) {
93 + this._setInteractionState('dragging');
94 + }
95 + }
96 +
97 + _handleMouseMove(interaction: MouseMoveInteraction) {
98 + const cursorInView = rectContainsPoint(
99 + interaction.payload.location,
100 + this.frame,
101 + );
102 + if (this._interactionState === 'dragging') {
103 + return;
104 + }
105 + this._setInteractionState(cursorInView ? 'hovered' : 'normal');
106 + }
107 +
108 + _handleMouseUp(interaction: MouseUpInteraction) {
109 + const cursorInView = rectContainsPoint(
110 + interaction.payload.location,
111 + this.frame,
112 + );
113 + if (this._interactionState === 'dragging') {
114 + this._setInteractionState(cursorInView ? 'hovered' : 'normal');
115 + }
116 + }
117 +
118 + handleInteraction(interaction: Interaction) {
119 + switch (interaction.type) {
120 + case 'mousedown':
121 + this._handleMouseDown(interaction);
122 + return;
123 + case 'mousemove':
124 + this._handleMouseMove(interaction);
125 + return;
126 + case 'mouseup':
127 + this._handleMouseUp(interaction);
128 + return;
129 + }
130 + }
131 +}
132 +
133 +export class ResizableSplitView extends View {
134 + _resizingState: ResizingState | null = null;
135 + _layoutState: LayoutState;
136 +
137 + constructor(
138 + surface: Surface,
139 + frame: Rect,
140 + topSubview: View,
141 + bottomSubview: View,
142 + ) {
143 + super(surface, frame, noopLayout);
144 +
145 + this.addSubview(topSubview);
146 + this.addSubview(new ResizeBar(surface, frame));
147 + this.addSubview(bottomSubview);
148 +
149 + const topSubviewDesiredSize = topSubview.desiredSize();
150 + this._layoutState = {
151 + barOffsetY: topSubviewDesiredSize ? topSubviewDesiredSize.height : 0,
152 + };
153 + }
154 +
155 + _getTopSubview(): View {
156 + return this.subviews[0];
157 + }
158 +
159 + _getResizeBar(): View {
160 + return this.subviews[1];
161 + }
162 +
163 + _getBottomSubview(): View {
164 + return this.subviews[2];
165 + }
166 +
167 + _getResizeBarDesiredSize(): Size {
168 + return nullthrows(
169 + this._getResizeBar().desiredSize(),
170 + 'Resize bar must have desired size',
171 + );
172 + }
173 +
174 + desiredSize() {
175 + const topSubviewDesiredSize = this._getTopSubview().desiredSize();
176 + const resizeBarDesiredSize = this._getResizeBarDesiredSize();
177 + const bottomSubviewDesiredSize = this._getBottomSubview().desiredSize();
178 +
179 + const topSubviewDesiredWidth = topSubviewDesiredSize
180 + ? topSubviewDesiredSize.width
181 + : 0;
182 + const bottomSubviewDesiredWidth = bottomSubviewDesiredSize
183 + ? bottomSubviewDesiredSize.width
184 + : 0;
185 +
186 + const topSubviewDesiredHeight = topSubviewDesiredSize
187 + ? topSubviewDesiredSize.height
188 + : 0;
189 + const bottomSubviewDesiredHeight = bottomSubviewDesiredSize
190 + ? bottomSubviewDesiredSize.height
191 + : 0;
192 +
193 + return {
194 + width: Math.max(
195 + topSubviewDesiredWidth,
196 + resizeBarDesiredSize.width,
197 + bottomSubviewDesiredWidth,
198 + ),
199 + height:
200 + topSubviewDesiredHeight +
201 + resizeBarDesiredSize.height +
202 + bottomSubviewDesiredHeight,
203 + };
204 + }
205 +
206 + layoutSubviews() {
207 + this._updateLayoutState();
208 + this._updateSubviewFrames();
209 + super.layoutSubviews();
210 + }
211 +
212 + _updateLayoutState() {
213 + const {frame, visibleArea, _resizingState} = this;
214 +
215 + const resizeBarDesiredSize = this._getResizeBarDesiredSize();
216 + // Allow bar to travel to bottom of the visible area of this view but no further
217 + const maxPossibleBarOffset =
218 + visibleArea.size.height - resizeBarDesiredSize.height;
219 + const topSubviewDesiredSize = this._getTopSubview().desiredSize();
220 + const maxBarOffset = topSubviewDesiredSize
221 + ? Math.min(maxPossibleBarOffset, topSubviewDesiredSize.height)
222 + : maxPossibleBarOffset;
223 +
224 + let proposedBarOffsetY = this._layoutState.barOffsetY;
225 + // Update bar offset if dragging bar
226 + if (_resizingState) {
227 + const {mouseY, cursorOffsetInBarFrame} = _resizingState;
228 + proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame;
229 + }
230 +
231 + this._layoutState = {
232 + ...this._layoutState,
233 + barOffsetY: clamp(0, maxBarOffset, proposedBarOffsetY),
234 + };
235 + }
236 +
237 + _updateSubviewFrames() {
238 + const {
239 + frame: {
240 + origin: {x, y},
241 + size: {width, height},
242 + },
243 + _layoutState: {barOffsetY},
244 + } = this;
245 +
246 + const resizeBarDesiredSize = this._getResizeBarDesiredSize();
247 +
248 + let currentY = y;
249 +
250 + this._getTopSubview().setFrame({
251 + origin: {x, y: currentY},
252 + size: {width, height: barOffsetY},
253 + });
254 + currentY += this._getTopSubview().frame.size.height;
255 +
256 + this._getResizeBar().setFrame({
257 + origin: {x, y: currentY},
258 + size: {width, height: resizeBarDesiredSize.height},
259 + });
260 + currentY += this._getResizeBar().frame.size.height;
261 +
262 + this._getBottomSubview().setFrame({
263 + origin: {x, y: currentY},
264 + // Fill remaining height
265 + size: {width, height: height + y - currentY},
266 + });
267 + }
268 +
269 + _handleMouseDown(interaction: MouseDownInteraction) {
270 + const cursorLocation = interaction.payload.location;
271 + const resizeBarFrame = this._getResizeBar().frame;
272 + if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
273 + const mouseY = cursorLocation.y;
274 + this._resizingState = {
275 + cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
276 + mouseY,
277 + };
278 + }
279 + }
280 +
281 + _handleMouseMove(interaction: MouseMoveInteraction) {
282 + const {_resizingState} = this;
283 + if (_resizingState) {
284 + this._resizingState = {
285 + ..._resizingState,
286 + mouseY: interaction.payload.location.y,
287 + };
288 + this.setNeedsDisplay();
289 + }
290 + }
291 +
292 + _handleMouseUp(interaction: MouseUpInteraction) {
293 + if (this._resizingState) {
294 + this._resizingState = null;
295 + }
296 + }
297 +
298 + handleInteraction(interaction: Interaction) {
299 + switch (interaction.type) {
300 + case 'mousedown':
301 + this._handleMouseDown(interaction);
302 + return;
303 + case 'mousemove':
304 + this._handleMouseMove(interaction);
305 + return;
306 + case 'mouseup':
307 + this._handleMouseUp(interaction);
308 + return;
309 + }
310 + }
311 +}
packages/react-devtools-scheduling-profiler/src/view-base/Surface.js new
+89
@@ -0,0 +1,89 @@
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 {Interaction} from './useCanvasInteraction';
11 +import type {Size} from './geometry';
12 +
13 +import memoize from 'memoize-one';
14 +
15 +import {View} from './View';
16 +import {zeroPoint} from './geometry';
17 +
18 +// hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
19 +function configureRetinaCanvas(canvas, height, width) {
20 + const dpr: number = window.devicePixelRatio || 1;
21 + canvas.width = width * dpr;
22 + canvas.height = height * dpr;
23 + canvas.style.width = `${width}px`;
24 + canvas.style.height = `${height}px`;
25 + return dpr;
26 +}
27 +
28 +const getCanvasContext = memoize(
29 + (
30 + canvas: HTMLCanvasElement,
31 + height: number,
32 + width: number,
33 + scaleCanvas: boolean = true,
34 + ): CanvasRenderingContext2D => {
35 + const context = canvas.getContext('2d', {alpha: false});
36 + if (scaleCanvas) {
37 + const dpr = configureRetinaCanvas(canvas, height, width);
38 + // Scale all drawing operations by the dpr, so you don't have to worry about the difference.
39 + context.scale(dpr, dpr);
40 + }
41 + return context;
42 + },
43 +);
44 +
45 +/**
46 + * Represents the canvas surface and a view heirarchy. A surface is also the
47 + * place where all interactions enter the view heirarchy.
48 + */
49 +export class Surface {
50 + rootView: ?View;
51 + _context: ?CanvasRenderingContext2D;
52 + _canvasSize: ?Size;
53 +
54 + setCanvas(canvas: HTMLCanvasElement, canvasSize: Size) {
55 + this._context = getCanvasContext(
56 + canvas,
57 + canvasSize.height,
58 + canvasSize.width,
59 + );
60 + this._canvasSize = canvasSize;
61 +
62 + if (this.rootView) {
63 + this.rootView.setNeedsDisplay();
64 + }
65 + }
66 +
67 + displayIfNeeded() {
68 + const {rootView, _canvasSize, _context} = this;
69 + if (!rootView || !_context || !_canvasSize) {
70 + return;
71 + }
72 + rootView.setFrame({
73 + origin: zeroPoint,
74 + size: _canvasSize,
75 + });
76 + rootView.setVisibleArea({
77 + origin: zeroPoint,
78 + size: _canvasSize,
79 + });
80 + rootView.displayIfNeeded(_context);
81 + }
82 +
83 + handleInteraction(interaction: Interaction) {
84 + if (!this.rootView) {
85 + return;
86 + }
87 + this.rootView.handleInteractionAndPropagateToSubviews(interaction);
88 + }
89 +}
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js new
+181
@@ -0,0 +1,181 @@
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 + WheelPlainInteraction,
16 +} from './useCanvasInteraction';
17 +import type {Rect} from './geometry';
18 +
19 +import {Surface} from './Surface';
20 +import {View} from './View';
21 +import {rectContainsPoint} from './geometry';
22 +import {clamp} from './utils/clamp';
23 +import {MOVE_WHEEL_DELTA_THRESHOLD} from './constants';
24 +
25 +type VerticalScrollState = $ReadOnly<{|
26 + offsetY: number,
27 +|}>;
28 +
29 +function scrollStatesAreEqual(
30 + state1: VerticalScrollState,
31 + state2: VerticalScrollState,
32 +): boolean {
33 + return state1.offsetY === state2.offsetY;
34 +}
35 +
36 +export class VerticalScrollView extends View {
37 + _scrollState: VerticalScrollState = {
38 + offsetY: 0,
39 + };
40 +
41 + _isPanning = false;
42 +
43 + constructor(surface: Surface, frame: Rect, contentView: View) {
44 + super(surface, frame);
45 + this.addSubview(contentView);
46 + }
47 +
48 + setFrame(newFrame: Rect) {
49 + super.setFrame(newFrame);
50 +
51 + // Revalidate scrollState
52 + this._updateState(this._scrollState);
53 + }
54 +
55 + desiredSize() {
56 + return this._contentView.desiredSize();
57 + }
58 +
59 + /**
60 + * Reference to the content view. This view is also the only view in
61 + * `this.subviews`.
62 + */
63 + get _contentView() {
64 + return this.subviews[0];
65 + }
66 +
67 + layoutSubviews() {
68 + const {offsetY} = this._scrollState;
69 + const desiredSize = this._contentView.desiredSize();
70 +
71 + const minimumHeight = this.frame.size.height;
72 + const desiredHeight = desiredSize ? desiredSize.height : 0;
73 + // Force view to take up at least all remaining vertical space.
74 + const height = Math.max(desiredHeight, minimumHeight);
75 +
76 + const proposedFrame = {
77 + origin: {
78 + x: this.frame.origin.x,
79 + y: this.frame.origin.y + offsetY,
80 + },
81 + size: {
82 + width: this.frame.size.width,
83 + height,
84 + },
85 + };
86 + this._contentView.setFrame(proposedFrame);
87 + super.layoutSubviews();
88 + }
89 +
90 + _handleMouseDown(interaction: MouseDownInteraction) {
91 + if (rectContainsPoint(interaction.payload.location, this.frame)) {
92 + this._isPanning = true;
93 + }
94 + }
95 +
96 + _handleMouseMove(interaction: MouseMoveInteraction) {
97 + if (!this._isPanning) {
98 + return;
99 + }
100 + const {offsetY} = this._scrollState;
101 + const {movementY} = interaction.payload.event;
102 + this._updateState({
103 + ...this._scrollState,
104 + offsetY: offsetY + movementY,
105 + });
106 + }
107 +
108 + _handleMouseUp(interaction: MouseUpInteraction) {
109 + if (this._isPanning) {
110 + this._isPanning = false;
111 + }
112 + }
113 +
114 + _handleWheelPlain(interaction: WheelPlainInteraction) {
115 + const {
116 + location,
117 + delta: {deltaX, deltaY},
118 + } = interaction.payload;
119 + if (!rectContainsPoint(location, this.frame)) {
120 + return; // Not scrolling on view
121 + }
122 +
123 + const absDeltaX = Math.abs(deltaX);
124 + const absDeltaY = Math.abs(deltaY);
125 + if (absDeltaX > absDeltaY) {
126 + return; // Scrolling horizontally
127 + }
128 +
129 + if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
130 + return;
131 + }
132 +
133 + this._updateState({
134 + ...this._scrollState,
135 + offsetY: this._scrollState.offsetY - deltaY,
136 + });
137 + }
138 +
139 + handleInteraction(interaction: Interaction) {
140 + switch (interaction.type) {
141 + case 'mousedown':
142 + this._handleMouseDown(interaction);
143 + break;
144 + case 'mousemove':
145 + this._handleMouseMove(interaction);
146 + break;
147 + case 'mouseup':
148 + this._handleMouseUp(interaction);
149 + break;
150 + case 'wheel-plain':
151 + this._handleWheelPlain(interaction);
152 + break;
153 + }
154 + }
155 +
156 + /**
157 + * @private
158 + */
159 + _updateState(proposedState: VerticalScrollState) {
160 + const clampedState = this._clampedProposedState(proposedState);
161 + if (!scrollStatesAreEqual(clampedState, this._scrollState)) {
162 + this._scrollState = clampedState;
163 + this.setNeedsDisplay();
164 + }
165 + }
166 +
167 + /**
168 + * @private
169 + */
170 + _clampedProposedState(
171 + proposedState: VerticalScrollState,
172 + ): VerticalScrollState {
173 + return {
174 + offsetY: clamp(
175 + -(this._contentView.frame.size.height - this.frame.size.height),
176 + 0,
177 + proposedState.offsetY,
178 + ),
179 + };
180 + }
181 +}
packages/react-devtools-scheduling-profiler/src/view-base/View.js new
+274
@@ -0,0 +1,274 @@
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 {Interaction} from './useCanvasInteraction';
11 +import type {Rect, Size} from './geometry';
12 +import type {Layouter} from './layouter';
13 +
14 +import {Surface} from './Surface';
15 +import {
16 + rectEqualToRect,
17 + intersectionOfRects,
18 + rectIntersectsRect,
19 + sizeIsEmpty,
20 + sizeIsValid,
21 + unionOfRects,
22 + zeroRect,
23 +} from './geometry';
24 +import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
25 +
26 +/**
27 + * Base view class that can be subclassed to draw custom content or manage
28 + * subclasses.
29 + */
30 +export class View {
31 + surface: Surface;
32 +
33 + frame: Rect;
34 + visibleArea: Rect;
35 +
36 + superview: ?View;
37 + subviews: View[] = [];
38 +
39 + /**
40 + * An injected function that lays out our subviews.
41 + * @private
42 + */
43 + _layouter: Layouter;
44 +
45 + /**
46 + * Whether this view needs to be drawn.
47 + *
48 + * NOTE: Do not set directly! Use `setNeedsDisplay`.
49 + *
50 + * @see setNeedsDisplay
51 + * @private
52 + */
53 + _needsDisplay = true;
54 +
55 + /**
56 + * Whether the heirarchy below this view has subviews that need display.
57 + *
58 + * NOTE: Do not set directly! Use `setSubviewsNeedDisplay`.
59 + *
60 + * @see setSubviewsNeedDisplay
61 + * @private
62 + */
63 + _subviewsNeedDisplay = false;
64 +
65 + constructor(
66 + surface: Surface,
67 + frame: Rect,
68 + layouter: Layouter = noopLayout,
69 + visibleArea: Rect = frame,
70 + ) {
71 + this.surface = surface;
72 + this.frame = frame;
73 + this._layouter = layouter;
74 + this.visibleArea = visibleArea;
75 + }
76 +
77 + /**
78 + * Invalidates view's contents.
79 + *
80 + * Downward propagating; once called, all subviews of this view should also
81 + * be invalidated.
82 + */
83 + setNeedsDisplay() {
84 + this._needsDisplay = true;
85 + if (this.superview) {
86 + this.superview._setSubviewsNeedDisplay();
87 + }
88 + this.subviews.forEach(subview => subview.setNeedsDisplay());
89 + }
90 +
91 + /**
92 + * Informs superview that it has subviews that need to be drawn.
93 + *
94 + * Upward propagating; once called, all superviews of this view should also
95 + * have `subviewsNeedDisplay` = true.
96 + *
97 + * @private
98 + */
99 + _setSubviewsNeedDisplay() {
100 + this._subviewsNeedDisplay = true;
101 + if (this.superview) {
102 + this.superview._setSubviewsNeedDisplay();
103 + }
104 + }
105 +
106 + setFrame(newFrame: Rect) {
107 + if (!rectEqualToRect(this.frame, newFrame)) {
108 + this.frame = newFrame;
109 + if (sizeIsValid(newFrame.size)) {
110 + this.frame = newFrame;
111 + } else {
112 + this.frame = zeroRect;
113 + }
114 + this.setNeedsDisplay();
115 + }
116 + }
117 +
118 + setVisibleArea(newVisibleArea: Rect) {
119 + if (!rectEqualToRect(this.visibleArea, newVisibleArea)) {
120 + if (sizeIsValid(newVisibleArea.size)) {
121 + this.visibleArea = newVisibleArea;
122 + } else {
123 + this.visibleArea = zeroRect;
124 + }
125 + this.setNeedsDisplay();
126 + }
127 + }
128 +
129 + /**
130 + * A size that can be used as a hint by layout functions.
131 + *
132 + * Implementations should typically return the intrinsic content size or a
133 + * size that fits all the view's content.
134 + *
135 + * The default implementation returns a size that fits all the view's
136 + * subviews.
137 + *
138 + * Can be overridden by subclasses.
139 + */
140 + desiredSize(): ?Size {
141 + if (this._needsDisplay) {
142 + this.layoutSubviews();
143 + }
144 + const frames = this.subviews.map(subview => subview.frame);
145 + return unionOfRects(...frames).size;
146 + }
147 +
148 + /**
149 + * Appends `view` to the list of this view's `subviews`.
150 + */
151 + addSubview(view: View) {
152 + if (this.subviews.includes(view)) {
153 + return;
154 + }
155 + this.subviews.push(view);
156 + view.superview = this;
157 + }
158 +
159 + /**
160 + * Breaks the subview-superview relationship between `view` and this view, if
161 + * `view` is a subview of this view.
162 + */
163 + removeSubview(view: View) {
164 + const subviewIndex = this.subviews.indexOf(view);
165 + if (subviewIndex === -1) {
166 + return;
167 + }
168 + view.superview = undefined;
169 + this.subviews.splice(subviewIndex, 1);
170 + }
171 +
172 + /**
173 + * Removes all subviews from this view.
174 + */
175 + removeAllSubviews() {
176 + this.subviews.forEach(subview => (subview.superview = undefined));
177 + this.subviews = [];
178 + }
179 +
180 + /**
181 + * Executes the display flow if this view needs to be drawn.
182 + *
183 + * 1. Lays out subviews with `layoutSubviews`.
184 + * 2. Draws content with `draw`.
185 + */
186 + displayIfNeeded(context: CanvasRenderingContext2D) {
187 + if (
188 + (this._needsDisplay || this._subviewsNeedDisplay) &&
189 + rectIntersectsRect(this.frame, this.visibleArea) &&
190 + !sizeIsEmpty(this.visibleArea.size)
191 + ) {
192 + this.layoutSubviews();
193 + if (this._needsDisplay) this._needsDisplay = false;
194 + if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
195 + this.draw(context);
196 + }
197 + }
198 +
199 + /**
200 + * Layout self and subviews.
201 + *
202 + * Implementations should call `setNeedsDisplay` if a draw is required.
203 + *
204 + * The default implementation uses `this.layouter` to lay out subviews.
205 + *
206 + * Can be overwritten by subclasses that wish to manually manage their
207 + * subviews' layout.
208 + *
209 + * NOTE: Do not call directly! Use `displayIfNeeded`.
210 + *
211 + * @see displayIfNeeded
212 + */
213 + layoutSubviews() {
214 + const {frame, _layouter, subviews, visibleArea} = this;
215 + const existingLayout = viewsToLayout(subviews);
216 + const newLayout = _layouter(existingLayout, frame);
217 + collapseLayoutIntoViews(newLayout);
218 +
219 + subviews.forEach((subview, subviewIndex) => {
220 + if (rectIntersectsRect(visibleArea, subview.frame)) {
221 + subview.setVisibleArea(intersectionOfRects(visibleArea, subview.frame));
222 + } else {
223 + subview.setVisibleArea(zeroRect);
224 + }
225 + });
226 + }
227 +
228 + /**
229 + * Draw the contents of this view in the given canvas `context`.
230 + *
231 + * Defaults to drawing this view's `subviews`.
232 + *
233 + * To be overwritten by subclasses that wish to draw custom content.
234 + *
235 + * NOTE: Do not call directly! Use `displayIfNeeded`.
236 + *
237 + * @see displayIfNeeded
238 + */
239 + draw(context: CanvasRenderingContext2D) {
240 + const {subviews, visibleArea} = this;
241 + subviews.forEach(subview => {
242 + if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
243 + subview.displayIfNeeded(context);
244 + }
245 + });
246 + }
247 +
248 + /**
249 + * Handle an `interaction`.
250 + *
251 + * To be overwritten by subclasses that wish to handle interactions.
252 + */
253 + // Internal note: Do not call directly! Use
254 + // `handleInteractionAndPropagateToSubviews` so that interactions are
255 + // propagated to subviews.
256 + handleInteraction(interaction: Interaction) {}
257 +
258 + /**
259 + * Handle an `interaction` and propagates it to all of this view's
260 + * `subviews`.
261 + *
262 + * NOTE: Should not be overridden! Subclasses should override
263 + * `handleInteraction` instead.
264 + *
265 + * @see handleInteraction
266 + * @protected
267 + */
268 + handleInteractionAndPropagateToSubviews(interaction: Interaction) {
269 + this.handleInteraction(interaction);
270 + this.subviews.forEach(subview =>
271 + subview.handleInteractionAndPropagateToSubviews(interaction),
272 + );
273 + }
274 +}
packages/react-devtools-scheduling-profiler/src/view-base/__tests__/geometry-test.js new
+272
@@ -0,0 +1,272 @@
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 {
11 + pointEqualToPoint,
12 + sizeEqualToSize,
13 + rectEqualToRect,
14 + sizeIsValid,
15 + sizeIsEmpty,
16 + rectIntersectsRect,
17 + intersectionOfRects,
18 + rectContainsPoint,
19 + unionOfRects,
20 +} from '../geometry';
21 +
22 +describe(pointEqualToPoint, () => {
23 + it('should return true when 2 points have the same values', () => {
24 + expect(pointEqualToPoint({x: 1, y: 1}, {x: 1, y: 1})).toBe(true);
25 + expect(pointEqualToPoint({x: -1, y: 2}, {x: -1, y: 2})).toBe(true);
26 + expect(
27 + pointEqualToPoint({x: 3.14159, y: 0.26535}, {x: 3.14159, y: 0.26535}),
28 + ).toBe(true);
29 + });
30 +
31 + it('should return false when 2 points have different values', () => {
32 + expect(pointEqualToPoint({x: 1, y: 1}, {x: 1, y: 0})).toBe(false);
33 + expect(pointEqualToPoint({x: -1, y: 2}, {x: 0, y: 1})).toBe(false);
34 + expect(
35 + pointEqualToPoint({x: 3.1416, y: 0.26534}, {x: 3.14159, y: 0.26535}),
36 + ).toBe(false);
37 + });
38 +});
39 +
40 +describe(sizeEqualToSize, () => {
41 + it('should return true when 2 sizes have the same values', () => {
42 + expect(sizeEqualToSize({width: 1, height: 1}, {width: 1, height: 1})).toBe(
43 + true,
44 + );
45 + expect(
46 + sizeEqualToSize({width: -1, height: 2}, {width: -1, height: 2}),
47 + ).toBe(true);
48 + expect(
49 + sizeEqualToSize(
50 + {width: 3.14159, height: 0.26535},
51 + {width: 3.14159, height: 0.26535},
52 + ),
53 + ).toBe(true);
54 + });
55 +
56 + it('should return false when 2 sizes have different values', () => {
57 + expect(sizeEqualToSize({width: 1, height: 1}, {width: 1, height: 0})).toBe(
58 + false,
59 + );
60 + expect(sizeEqualToSize({width: -1, height: 2}, {width: 0, height: 1})).toBe(
61 + false,
62 + );
63 + expect(
64 + sizeEqualToSize(
65 + {width: 3.1416, height: 0.26534},
66 + {width: 3.14159, height: 0.26535},
67 + ),
68 + ).toBe(false);
69 + });
70 +});
71 +
72 +describe(rectEqualToRect, () => {
73 + it('should return true when 2 rects have the same values', () => {
74 + expect(
75 + rectEqualToRect(
76 + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}},
77 + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}},
78 + ),
79 + ).toBe(true);
80 + expect(
81 + rectEqualToRect(
82 + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}},
83 + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}},
84 + ),
85 + ).toBe(true);
86 + });
87 +
88 + it('should return false when 2 rects have different values', () => {
89 + expect(
90 + rectEqualToRect(
91 + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}},
92 + {origin: {x: 0, y: 1}, size: {width: 1, height: 1}},
93 + ),
94 + ).toBe(false);
95 + expect(
96 + rectEqualToRect(
97 + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}},
98 + {origin: {x: 1, y: 2}, size: {width: 3.15, height: 4}},
99 + ),
100 + ).toBe(false);
101 + });
102 +});
103 +
104 +describe(sizeIsValid, () => {
105 + it('should return true when the size has non-negative width and height', () => {
106 + expect(sizeIsValid({width: 1, height: 1})).toBe(true);
107 + expect(sizeIsValid({width: 0, height: 0})).toBe(true);
108 + });
109 +
110 + it('should return false when the size has negative width or height', () => {
111 + expect(sizeIsValid({width: 0, height: -1})).toBe(false);
112 + expect(sizeIsValid({width: -1, height: 0})).toBe(false);
113 + expect(sizeIsValid({width: -1, height: -1})).toBe(false);
114 + });
115 +});
116 +
117 +describe(sizeIsEmpty, () => {
118 + it('should return true when the size has negative area', () => {
119 + expect(sizeIsEmpty({width: 1, height: -1})).toBe(true);
120 + expect(sizeIsEmpty({width: -1, height: -1})).toBe(true);
121 + });
122 +
123 + it('should return true when the size has zero area', () => {
124 + expect(sizeIsEmpty({width: 0, height: 0})).toBe(true);
125 + expect(sizeIsEmpty({width: 0, height: 1})).toBe(true);
126 + expect(sizeIsEmpty({width: 1, height: 0})).toBe(true);
127 + });
128 +
129 + it('should return false when the size has positive area', () => {
130 + expect(sizeIsEmpty({width: 1, height: 1})).toBe(false);
131 + expect(sizeIsEmpty({width: 2, height: 1})).toBe(false);
132 + });
133 +});
134 +
135 +describe(rectIntersectsRect, () => {
136 + it('should return true when 2 rects intersect', () => {
137 + // Rects touch
138 + expect(
139 + rectIntersectsRect(
140 + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}},
141 + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}},
142 + ),
143 + ).toEqual(true);
144 +
145 + // Rects overlap
146 + expect(
147 + rectIntersectsRect(
148 + {origin: {x: 0, y: 0}, size: {width: 2, height: 1}},
149 + {origin: {x: 1, y: -2}, size: {width: 0.5, height: 5}},
150 + ),
151 + ).toEqual(true);
152 +
153 + // Rects are equal
154 + expect(
155 + rectIntersectsRect(
156 + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}},
157 + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}},
158 + ),
159 + ).toEqual(true);
160 + });
161 +
162 + it('should return false when 2 rects do not intersect', () => {
163 + expect(
164 + rectIntersectsRect(
165 + {origin: {x: 0, y: 1}, size: {width: 1, height: 1}},
166 + {origin: {x: 0, y: 10}, size: {width: 1, height: 1}},
167 + ),
168 + ).toBe(false);
169 + expect(
170 + rectIntersectsRect(
171 + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}},
172 + {origin: {x: -4, y: 2}, size: {width: 3.15, height: 4}},
173 + ),
174 + ).toBe(false);
175 + });
176 +});
177 +
178 +describe(intersectionOfRects, () => {
179 + // NOTE: Undefined behavior if rects do not intersect
180 +
181 + it('should return intersection when 2 rects intersect', () => {
182 + // Rects touch
183 + expect(
184 + intersectionOfRects(
185 + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}},
186 + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}},
187 + ),
188 + ).toEqual({origin: {x: 1, y: 1}, size: {width: 0, height: 0}});
189 +
190 + // Rects overlap
191 + expect(
192 + intersectionOfRects(
193 + {origin: {x: 0, y: 0}, size: {width: 2, height: 1}},
194 + {origin: {x: 1, y: -2}, size: {width: 0.5, height: 5}},
195 + ),
196 + ).toEqual({origin: {x: 1, y: 0}, size: {width: 0.5, height: 1}});
197 +
198 + // Rects are equal
199 + expect(
200 + intersectionOfRects(
201 + {origin: {x: 1, y: 2}, size: {width: 9.24, height: 4}},
202 + {origin: {x: 1, y: 2}, size: {width: 9.24, height: 4}},
203 + ),
204 + ).toEqual({origin: {x: 1, y: 2}, size: {width: 9.24, height: 4}});
205 + });
206 +});
207 +
208 +describe(rectContainsPoint, () => {
209 + it("should return true if point is on the rect's edge", () => {
210 + expect(
211 + rectContainsPoint(
212 + {x: 0, y: 0},
213 + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}},
214 + ),
215 + ).toBe(true);
216 + expect(
217 + rectContainsPoint(
218 + {x: 5, y: 0},
219 + {origin: {x: 0, y: 0}, size: {width: 10, height: 1}},
220 + ),
221 + ).toBe(true);
222 + expect(
223 + rectContainsPoint(
224 + {x: 1, y: 1},
225 + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}},
226 + ),
227 + ).toBe(true);
228 + });
229 +
230 + it('should return true if point is in rect', () => {
231 + expect(
232 + rectContainsPoint(
233 + {x: 5, y: 50},
234 + {origin: {x: 0, y: 0}, size: {width: 10, height: 100}},
235 + ),
236 + ).toBe(true);
237 + });
238 +
239 + it('should return false if point is not in rect', () => {
240 + expect(
241 + rectContainsPoint(
242 + {x: -1, y: 0},
243 + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}},
244 + ),
245 + ).toBe(false);
246 + });
247 +});
248 +
249 +describe(unionOfRects, () => {
250 + it('should return zero rect if no rects are provided', () => {
251 + expect(unionOfRects()).toEqual({
252 + origin: {x: 0, y: 0},
253 + size: {width: 0, height: 0},
254 + });
255 + });
256 +
257 + it('should return rect if 1 rect is provided', () => {
258 + expect(
259 + unionOfRects({origin: {x: 1, y: 2}, size: {width: 3, height: 4}}),
260 + ).toEqual({origin: {x: 1, y: 2}, size: {width: 3, height: 4}});
261 + });
262 +
263 + it('should return union of rects if more than one rect is provided', () => {
264 + expect(
265 + unionOfRects(
266 + {origin: {x: 1, y: 2}, size: {width: 3, height: 4}},
267 + {origin: {x: 100, y: 200}, size: {width: 3, height: 4}},
268 + {origin: {x: -10, y: -20}, size: {width: 50, height: 60}},
269 + ),
270 + ).toEqual({origin: {x: -10, y: -20}, size: {width: 113, height: 224}});
271 + });
272 +});
packages/react-devtools-scheduling-profiler/src/view-base/constants.js new
+13
@@ -0,0 +1,13 @@
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 +export const MOVE_WHEEL_DELTA_THRESHOLD = 1;
11 +export const ZOOM_WHEEL_DELTA_THRESHOLD = 1;
12 +export const MIN_ZOOM_LEVEL = 0.25;
13 +export const MAX_ZOOM_LEVEL = 1000;
packages/react-devtools-scheduling-profiler/src/view-base/geometry.js new
+128
@@ -0,0 +1,128 @@
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 +export type Point = $ReadOnly<{|x: number, y: number|}>;
11 +export type Size = $ReadOnly<{|width: number, height: number|}>;
12 +export type Rect = $ReadOnly<{|origin: Point, size: Size|}>;
13 +
14 +/**
15 + * Alternative representation of `Rect`.
16 + * A tuple of (`top`, `right`, `bottom`, `left`) coordinates.
17 + */
18 +type Box = [number, number, number, number];
19 +
20 +export const zeroPoint: Point = Object.freeze({x: 0, y: 0});
21 +export const zeroSize: Size = Object.freeze({width: 0, height: 0});
22 +export const zeroRect: Rect = Object.freeze({
23 + origin: zeroPoint,
24 + size: zeroSize,
25 +});
26 +
27 +export function pointEqualToPoint(point1: Point, point2: Point): boolean {
28 + return point1.x === point2.x && point1.y === point2.y;
29 +}
30 +
31 +export function sizeEqualToSize(size1: Size, size2: Size): boolean {
32 + return size1.width === size2.width && size1.height === size2.height;
33 +}
34 +
35 +export function rectEqualToRect(rect1: Rect, rect2: Rect): boolean {
36 + return (
37 + pointEqualToPoint(rect1.origin, rect2.origin) &&
38 + sizeEqualToSize(rect1.size, rect2.size)
39 + );
40 +}
41 +
42 +export function sizeIsValid({width, height}: Size): boolean {
43 + return width >= 0 && height >= 0;
44 +}
45 +
46 +export function sizeIsEmpty({width, height}: Size): boolean {
47 + return width <= 0 || height <= 0;
48 +}
49 +
50 +function rectToBox(rect: Rect): Box {
51 + const top = rect.origin.y;
52 + const right = rect.origin.x + rect.size.width;
53 + const bottom = rect.origin.y + rect.size.height;
54 + const left = rect.origin.x;
55 + return [top, right, bottom, left];
56 +}
57 +
58 +function boxToRect(box: Box): Rect {
59 + const [top, right, bottom, left] = box;
60 + return {
61 + origin: {
62 + x: left,
63 + y: top,
64 + },
65 + size: {
66 + width: right - left,
67 + height: bottom - top,
68 + },
69 + };
70 +}
71 +
72 +export function rectIntersectsRect(rect1: Rect, rect2: Rect): boolean {
73 + const [top1, right1, bottom1, left1] = rectToBox(rect1);
74 + const [top2, right2, bottom2, left2] = rectToBox(rect2);
75 + return !(
76 + right1 < left2 ||
77 + right2 < left1 ||
78 + bottom1 < top2 ||
79 + bottom2 < top1
80 + );
81 +}
82 +
83 +/**
84 + * Returns the intersection of the 2 rectangles.
85 + *
86 + * Prerequisite: `rect1` must intersect with `rect2`.
87 + */
88 +export function intersectionOfRects(rect1: Rect, rect2: Rect): Rect {
89 + const [top1, right1, bottom1, left1] = rectToBox(rect1);
90 + const [top2, right2, bottom2, left2] = rectToBox(rect2);
91 + return boxToRect([
92 + Math.max(top1, top2),
93 + Math.min(right1, right2),
94 + Math.min(bottom1, bottom2),
95 + Math.max(left1, left2),
96 + ]);
97 +}
98 +
99 +export function rectContainsPoint({x, y}: Point, rect: Rect): boolean {
100 + const [top, right, bottom, left] = rectToBox(rect);
101 + return left <= x && x <= right && top <= y && y <= bottom;
102 +}
103 +
104 +/**
105 + * Returns the smallest rectangle that contains all provided rects.
106 + *
107 + * @returns Union of `rects`. If `rects` is empty, returns `zeroRect`.
108 + */
109 +export function unionOfRects(...rects: Rect[]): Rect {
110 + if (rects.length === 0) {
111 + return zeroRect;
112 + }
113 +
114 + const [firstRect, ...remainingRects] = rects;
115 + const boxUnion = remainingRects
116 + .map(rectToBox)
117 + .reduce((intermediateUnion, nextBox): Box => {
118 + const [unionTop, unionRight, unionBottom, unionLeft] = intermediateUnion;
119 + const [nextTop, nextRight, nextBottom, nextLeft] = nextBox;
120 + return [
121 + Math.min(unionTop, nextTop),
122 + Math.max(unionRight, nextRight),
123 + Math.max(unionBottom, nextBottom),
124 + Math.min(unionLeft, nextLeft),
125 + ];
126 + }, rectToBox(firstRect));
127 + return boxToRect(boxUnion);
128 +}
packages/react-devtools-scheduling-profiler/src/view-base/index.js new
+18
@@ -0,0 +1,18 @@
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 +export * from './ColorView';
11 +export * from './HorizontalPanAndZoomView';
12 +export * from './ResizableSplitView';
13 +export * from './Surface';
14 +export * from './VerticalScrollView';
15 +export * from './View';
16 +export * from './geometry';
17 +export * from './layouter';
18 +export * from './useCanvasInteraction';
packages/react-devtools-scheduling-profiler/src/view-base/layouter.js new
+269
@@ -0,0 +1,269 @@
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 './geometry';
11 +import type {View} from './View';
12 +
13 +export type LayoutInfo = {|view: View, frame: Rect|};
14 +export type Layout = LayoutInfo[];
15 +
16 +/**
17 + * A function that takes a list of subviews, currently laid out in
18 + * `existingLayout`, and lays them out into `containingFrame`.
19 + */
20 +export type Layouter = (
21 + existingLayout: Layout,
22 + containingFrame: Rect,
23 +) => Layout;
24 +
25 +function viewToLayoutInfo(view: View): LayoutInfo {
26 + return {view, frame: view.frame};
27 +}
28 +
29 +export function viewsToLayout(views: View[]): Layout {
30 + return views.map(viewToLayoutInfo);
31 +}
32 +
33 +/**
34 + * Applies `layout`'s `frame`s to its corresponding `view`.
35 + */
36 +export function collapseLayoutIntoViews(layout: Layout) {
37 + layout.forEach(({view, frame}) => view.setFrame(frame));
38 +}
39 +
40 +/**
41 + * A no-operation layout; does not modify the layout.
42 + */
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`.
48 + *
49 + * Equivalent to composing:
50 + * - `alignToContainerXLayout`,
51 + * - `alignToContainerYLayout`,
52 + * - `containerWidthLayout`, and
53 + * - `containerHeightLayout`.
54 + */
55 +export const layeredLayout: Layouter = (layout, containerFrame) =>
56 + layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame}));
57 +
58 +/**
59 + * Stacks `views` vertically in `frame`. All views in `views` will have their
60 + * widths set to the frame's width.
61 + */
62 +export const verticallyStackedLayout: Layouter = (layout, containerFrame) => {
63 + let currentY = containerFrame.origin.y;
64 + return layout.map(layoutInfo => {
65 + const desiredSize = layoutInfo.view.desiredSize();
66 + const height = desiredSize
67 + ? desiredSize.height
68 + : containerFrame.origin.y + containerFrame.size.height - currentY;
69 + const proposedFrame = {
70 + origin: {x: containerFrame.origin.x, y: currentY},
71 + size: {width: containerFrame.size.width, height},
72 + };
73 + currentY += height;
74 + return {
75 + ...layoutInfo,
76 + frame: proposedFrame,
77 + };
78 + });
79 +};
80 +
81 +/**
82 + * A layouter that aligns all frames' lefts to the container frame's left.
83 + */
84 +export const alignToContainerXLayout: Layouter = (layout, containerFrame) => {
85 + return layout.map(layoutInfo => ({
86 + ...layoutInfo,
87 + frame: {
88 + origin: {
89 + x: containerFrame.origin.x,
90 + y: layoutInfo.frame.origin.y,
91 + },
92 + size: layoutInfo.frame.size,
93 + },
94 + }));
95 +};
96 +
97 +/**
98 + * A layouter that aligns all frames' tops to the container frame's top.
99 + */
100 +export const alignToContainerYLayout: Layouter = (layout, containerFrame) => {
101 + return layout.map(layoutInfo => ({
102 + ...layoutInfo,
103 + frame: {
104 + origin: {
105 + x: layoutInfo.frame.origin.x,
106 + y: containerFrame.origin.y,
107 + },
108 + size: layoutInfo.frame.size,
109 + },
110 + }));
111 +};
112 +
113 +/**
114 + * A layouter that sets all frames' widths to `containerFrame.size.width`.
115 + */
116 +export const containerWidthLayout: Layouter = (layout, containerFrame) => {
117 + return layout.map(layoutInfo => ({
118 + ...layoutInfo,
119 + frame: {
120 + origin: layoutInfo.frame.origin,
121 + size: {
122 + width: containerFrame.size.width,
123 + height: layoutInfo.frame.size.height,
124 + },
125 + },
126 + }));
127 +};
128 +
129 +/**
130 + * A layouter that sets all frames' heights to `containerFrame.size.height`.
131 + */
132 +export const containerHeightLayout: Layouter = (layout, containerFrame) => {
133 + return layout.map(layoutInfo => ({
134 + ...layoutInfo,
135 + frame: {
136 + origin: layoutInfo.frame.origin,
137 + size: {
138 + width: layoutInfo.frame.size.width,
139 + height: containerFrame.size.height,
140 + },
141 + },
142 + }));
143 +};
144 +
145 +/**
146 + * A layouter that sets all frames' heights to the desired height of its view.
147 + * If the view has no desired size, the frame's height is set to 0.
148 + */
149 +export const desiredHeightLayout: Layouter = layout => {
150 + return layout.map(layoutInfo => {
151 + const desiredSize = layoutInfo.view.desiredSize();
152 + const height = desiredSize ? desiredSize.height : 0;
153 + return {
154 + ...layoutInfo,
155 + frame: {
156 + origin: layoutInfo.frame.origin,
157 + size: {
158 + width: layoutInfo.frame.size.width,
159 + height,
160 + },
161 + },
162 + };
163 + });
164 +};
165 +
166 +/**
167 + * A layouter that sets all frames' heights to the height of the tallest frame.
168 + */
169 +export const uniformMaxSubviewHeightLayout: Layouter = layout => {
170 + const maxHeight = Math.max(
171 + ...layout.map(layoutInfo => layoutInfo.frame.size.height),
172 + );
173 + return layout.map(layoutInfo => ({
174 + ...layoutInfo,
175 + frame: {
176 + origin: layoutInfo.frame.origin,
177 + size: {
178 + width: layoutInfo.frame.size.width,
179 + height: maxHeight,
180 + },
181 + },
182 + }));
183 +};
184 +
185 +/**
186 + * A layouter that sets heights in this fashion:
187 + * - If a frame's height >= `containerFrame.size.height`, the frame is left unchanged.
188 + * - Otherwise, sets the frame's height to `containerFrame.size.height`.
189 + */
190 +export const atLeastContainerHeightLayout: Layouter = (
191 + layout,
192 + containerFrame,
193 +) => {
194 + return layout.map(layoutInfo => ({
195 + ...layoutInfo,
196 + frame: {
197 + origin: layoutInfo.frame.origin,
198 + size: {
199 + width: layoutInfo.frame.size.width,
200 + height: Math.max(
201 + containerFrame.size.height,
202 + layoutInfo.frame.size.height,
203 + ),
204 + },
205 + },
206 + }));
207 +};
208 +
209 +/**
210 + * Forces last view to take up the space below the second-last view.
211 + * Intended to be used with a vertical stack layout.
212 + */
213 +export const lastViewTakesUpRemainingSpaceLayout: Layouter = (
214 + layout,
215 + containerFrame,
216 +) => {
217 + if (layout.length === 0) {
218 + // Nothing to do
219 + return layout;
220 + }
221 +
222 + if (layout.length === 1) {
223 + // No second-last view; the view should just take up the container height
224 + return containerHeightLayout(layout, containerFrame);
225 + }
226 +
227 + const layoutInfoToPassThrough = layout.slice(0, layout.length - 1);
228 + const secondLastLayoutInfo =
229 + layoutInfoToPassThrough[layoutInfoToPassThrough.length - 1];
230 +
231 + const remainingHeight =
232 + containerFrame.size.height -
233 + secondLastLayoutInfo.frame.origin.y -
234 + secondLastLayoutInfo.frame.size.height;
235 + const height = Math.max(remainingHeight, 0); // Prevent negative heights
236 +
237 + const lastLayoutInfo = layout[layout.length - 1];
238 + return [
239 + ...layoutInfoToPassThrough,
240 + {
241 + ...lastLayoutInfo,
242 + frame: {
243 + origin: lastLayoutInfo.frame.origin,
244 + size: {
245 + width: lastLayoutInfo.frame.size.width,
246 + height,
247 + },
248 + },
249 + },
250 + ];
251 +};
252 +
253 +/**
254 + * Create a layouter that applies each layouter in `layouters` in sequence.
255 + */
256 +export function createComposedLayout(...layouters: Layouter[]): Layouter {
257 + if (layouters.length === 0) {
258 + return noopLayout;
259 + }
260 +
261 + const composedLayout: Layouter = (layout, containerFrame) => {
262 + return layouters.reduce(
263 + (intermediateLayout, layouter) =>
264 + layouter(intermediateLayout, containerFrame),
265 + layout,
266 + );
267 + };
268 + return composedLayout;
269 +}
packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js new
+192
@@ -0,0 +1,192 @@
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 {NormalizedWheelDelta} from './utils/normalizeWheel';
11 +import type {Point} from './geometry';
12 +
13 +import {useEffect} from 'react';
14 +import {normalizeWheel} from './utils/normalizeWheel';
15 +
16 +export type MouseDownInteraction = {|
17 + type: 'mousedown',
18 + payload: {|
19 + event: MouseEvent,
20 + location: Point,
21 + |},
22 +|};
23 +export type MouseMoveInteraction = {|
24 + type: 'mousemove',
25 + payload: {|
26 + event: MouseEvent,
27 + location: Point,
28 + |},
29 +|};
30 +export type MouseUpInteraction = {|
31 + type: 'mouseup',
32 + payload: {|
33 + event: MouseEvent,
34 + location: Point,
35 + |},
36 +|};
37 +export type WheelPlainInteraction = {|
38 + type: 'wheel-plain',
39 + payload: {|
40 + event: WheelEvent,
41 + location: Point,
42 + delta: NormalizedWheelDelta,
43 + |},
44 +|};
45 +export type WheelWithShiftInteraction = {|
46 + type: 'wheel-shift',
47 + payload: {|
48 + event: WheelEvent,
49 + location: Point,
50 + delta: NormalizedWheelDelta,
51 + |},
52 +|};
53 +export type WheelWithControlInteraction = {|
54 + type: 'wheel-control',
55 + payload: {|
56 + event: WheelEvent,
57 + location: Point,
58 + delta: NormalizedWheelDelta,
59 + |},
60 +|};
61 +export type WheelWithMetaInteraction = {|
62 + type: 'wheel-meta',
63 + payload: {|
64 + event: WheelEvent,
65 + location: Point,
66 + delta: NormalizedWheelDelta,
67 + |},
68 +|};
69 +
70 +export type Interaction =
71 + | MouseDownInteraction
72 + | MouseMoveInteraction
73 + | MouseUpInteraction
74 + | WheelPlainInteraction
75 + | WheelWithShiftInteraction
76 + | WheelWithControlInteraction
77 + | WheelWithMetaInteraction;
78 +
79 +let canvasBoundingRectCache = null;
80 +function cacheFirstGetCanvasBoundingRect(
81 + canvas: HTMLCanvasElement,
82 +): ClientRect {
83 + if (
84 + canvasBoundingRectCache &&
85 + canvas.width === canvasBoundingRectCache.width &&
86 + canvas.height === canvasBoundingRectCache.height
87 + ) {
88 + return canvasBoundingRectCache.rect;
89 + }
90 + canvasBoundingRectCache = {
91 + width: canvas.width,
92 + height: canvas.height,
93 + rect: canvas.getBoundingClientRect(),
94 + };
95 + return canvasBoundingRectCache.rect;
96 +}
97 +
98 +export function useCanvasInteraction(
99 + canvasRef: {|current: HTMLCanvasElement | null|},
100 + interactor: (interaction: Interaction) => void,
101 +) {
102 + useEffect(() => {
103 + const canvas = canvasRef.current;
104 + if (!canvas) {
105 + return;
106 + }
107 +
108 + function localToCanvasCoordinates(localCoordinates: Point): Point {
109 + const canvasRect = cacheFirstGetCanvasBoundingRect(canvas);
110 + return {
111 + x: localCoordinates.x - canvasRect.left,
112 + y: localCoordinates.y - canvasRect.top,
113 + };
114 + }
115 +
116 + const onCanvasMouseDown: MouseEventHandler = event => {
117 + interactor({
118 + type: 'mousedown',
119 + payload: {
120 + event,
121 + location: localToCanvasCoordinates({x: event.x, y: event.y}),
122 + },
123 + });
124 + };
125 +
126 + const onDocumentMouseMove: MouseEventHandler = event => {
127 + interactor({
128 + type: 'mousemove',
129 + payload: {
130 + event,
131 + location: localToCanvasCoordinates({x: event.x, y: event.y}),
132 + },
133 + });
134 + };
135 +
136 + const onDocumentMouseUp: MouseEventHandler = event => {
137 + interactor({
138 + type: 'mouseup',
139 + payload: {
140 + event,
141 + location: localToCanvasCoordinates({x: event.x, y: event.y}),
142 + },
143 + });
144 + };
145 +
146 + const onCanvasWheel: WheelEventHandler = event => {
147 + event.preventDefault();
148 + event.stopPropagation();
149 +
150 + const location = localToCanvasCoordinates({x: event.x, y: event.y});
151 + const delta = normalizeWheel(event);
152 +
153 + if (event.shiftKey) {
154 + interactor({
155 + type: 'wheel-shift',
156 + payload: {event, location, delta},
157 + });
158 + } else if (event.ctrlKey) {
159 + interactor({
160 + type: 'wheel-control',
161 + payload: {event, location, delta},
162 + });
163 + } else if (event.metaKey) {
164 + interactor({
165 + type: 'wheel-meta',
166 + payload: {event, location, delta},
167 + });
168 + } else {
169 + interactor({
170 + type: 'wheel-plain',
171 + payload: {event, location, delta},
172 + });
173 + }
174 +
175 + return false;
176 + };
177 +
178 + document.addEventListener('mousemove', onDocumentMouseMove);
179 + document.addEventListener('mouseup', onDocumentMouseUp);
180 +
181 + canvas.addEventListener('mousedown', onCanvasMouseDown);
182 + canvas.addEventListener('wheel', onCanvasWheel);
183 +
184 + return () => {
185 + document.removeEventListener('mousemove', onDocumentMouseMove);
186 + document.removeEventListener('mouseup', onDocumentMouseUp);
187 +
188 + canvas.removeEventListener('mousedown', onCanvasMouseDown);
189 + canvas.removeEventListener('wheel', onCanvasWheel);
190 + };
191 + }, [canvasRef, interactor]);
192 +}
packages/react-devtools-scheduling-profiler/src/view-base/utils/clamp.js new
+17
@@ -0,0 +1,17 @@
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 +export function clamp(min: number, max: number, value: number): number {
11 + if (Number.isNaN(min) || Number.isNaN(max) || Number.isNaN(value)) {
12 + throw new Error(
13 + `Clamp was called with NaN. Args: min: ${min}, max: ${max}, value: ${value}.`,
14 + );
15 + }
16 + return Math.min(max, Math.max(min, value));
17 +}
packages/react-devtools-scheduling-profiler/src/view-base/utils/normalizeWheel.js new
+91
@@ -0,0 +1,91 @@
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 +// Adapted from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
11 +
12 +export type NormalizedWheelDelta = {|
13 + deltaX: number,
14 + deltaY: number,
15 +|};
16 +
17 +// Reasonable defaults
18 +const LINE_HEIGHT = 40;
19 +const PAGE_HEIGHT = 800;
20 +
21 +/**
22 + * Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
23 + * complicated, thus this doc is long and (hopefully) detailed enough to answer
24 + * your questions.
25 + *
26 + * If you need to react to the mouse wheel in a predictable way, this code is
27 + * like your bestest friend. * hugs *
28 + *
29 + * In your event callback, use this code to get sane interpretation of the
30 + * deltas. This code will return an object with properties:
31 + *
32 + * - deltaX -- normalized distance (to pixels) - x plane
33 + * - deltaY -- " - y plane
34 + *
35 + * Wheel values are provided by the browser assuming you are using the wheel to
36 + * scroll a web page by a number of lines or pixels (or pages). Values can vary
37 + * significantly on different platforms and browsers, forgetting that you can
38 + * scroll at different speeds. Some devices (like trackpads) emit more events
39 + * at smaller increments with fine granularity, and some emit massive jumps with
40 + * linear speed or acceleration.
41 + *
42 + * This code does its best to normalize the deltas for you:
43 + *
44 + * - delta* is normalizing the desired scroll delta in pixel units.
45 + *
46 + * - positive value indicates scrolling DOWN/RIGHT, negative UP/LEFT. This
47 + * should translate to positive value zooming IN, negative zooming OUT.
48 + * This matches the 'wheel' event.
49 + *
50 + * Implementation info:
51 + *
52 + * The basics of the standard 'wheel' event is that it includes a unit,
53 + * deltaMode (pixels, lines, pages), and deltaX, deltaY and deltaZ.
54 + * See: http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
55 + *
56 + * Examples of 'wheel' event if you scroll slowly (down) by one step with an
57 + * average mouse:
58 + *
59 + * OS X + Chrome (mouse) - 4 pixel delta (wheelDelta -120)
60 + * OS X + Safari (mouse) - N/A pixel delta (wheelDelta -12)
61 + * OS X + Firefox (mouse) - 0.1 line delta (wheelDelta N/A)
62 + * Win8 + Chrome (mouse) - 100 pixel delta (wheelDelta -120)
63 + * Win8 + Firefox (mouse) - 3 line delta (wheelDelta -120)
64 + *
65 + * On the trackpad:
66 + *
67 + * OS X + Chrome (trackpad) - 2 pixel delta (wheelDelta -6)
68 + * OS X + Firefox (trackpad) - 1 pixel delta (wheelDelta N/A)
69 + */
70 +export function normalizeWheel(event: WheelEvent): NormalizedWheelDelta {
71 + let deltaX = event.deltaX;
72 + let deltaY = event.deltaY;
73 +
74 + if (
75 + // $FlowFixMe DOM_DELTA_LINE missing from WheelEvent
76 + event.deltaMode === WheelEvent.DOM_DELTA_LINE
77 + ) {
78 + // delta in LINE units
79 + deltaX *= LINE_HEIGHT;
80 + deltaY *= LINE_HEIGHT;
81 + } else if (
82 + // $FlowFixMe DOM_DELTA_PAGE missing from WheelEvent
83 + event.deltaMode === WheelEvent.DOM_DELTA_PAGE
84 + ) {
85 + // delta in PAGE units
86 + deltaX *= PAGE_HEIGHT;
87 + deltaY *= PAGE_HEIGHT;
88 + }
89 +
90 + return {deltaX, deltaY};
91 +}
packages/react-devtools-scheduling-profiler/webpack.config.js new
+104
@@ -0,0 +1,104 @@
1 +'use strict';
2 +
3 +const {resolve} = require('path');
4 +const {DefinePlugin} = require('webpack');
5 +const HtmlWebpackPlugin = require('html-webpack-plugin');
6 +
7 +const NODE_ENV = process.env.NODE_ENV;
8 +if (!NODE_ENV) {
9 + console.error('NODE_ENV not set');
10 + process.exit(1);
11 +}
12 +
13 +const TARGET = process.env.TARGET;
14 +if (!TARGET) {
15 + console.error('TARGET not set');
16 + process.exit(1);
17 +}
18 +
19 +const builtModulesDir = resolve(__dirname, '..', '..', 'build', 'node_modules');
20 +
21 +const __DEV__ = NODE_ENV === 'development';
22 +
23 +const imageInlineSizeLimit = 10000;
24 +
25 +const config = {
26 + mode: __DEV__ ? 'development' : 'production',
27 + devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
28 + entry: {
29 + app: './src/index.js',
30 + },
31 + resolve: {
32 + alias: {
33 + react: resolve(builtModulesDir, 'react'),
34 + 'react-dom': resolve(builtModulesDir, 'react-dom'),
35 + },
36 + },
37 + plugins: [
38 + new DefinePlugin({
39 + __DEV__,
40 + __PROFILE__: false,
41 + __EXPERIMENTAL__: true,
42 + }),
43 + new HtmlWebpackPlugin({
44 + title: 'React Concurrent Mode Profiler',
45 + }),
46 + ],
47 + module: {
48 + rules: [
49 + {
50 + test: /\.js$/,
51 + loader: 'babel-loader',
52 + options: {
53 + configFile: resolve(
54 + __dirname,
55 + '..',
56 + 'react-devtools-shared',
57 + 'babel.config.js',
58 + ),
59 + },
60 + },
61 + {
62 + test: /\.css$/,
63 + use: [
64 + {
65 + loader: 'style-loader',
66 + },
67 + {
68 + loader: 'css-loader',
69 + options: {
70 + sourceMap: true,
71 + modules: {
72 + localIdentName: '[local]___[hash:base64:5]',
73 + },
74 + },
75 + },
76 + ],
77 + },
78 + {
79 + test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
80 + loader: 'url-loader',
81 + options: {
82 + limit: imageInlineSizeLimit,
83 + name: 'static/media/[name].[hash:8].[ext]',
84 + },
85 + },
86 + ],
87 + },
88 +};
89 +
90 +if (TARGET === 'local') {
91 + config.devServer = {
92 + hot: true,
93 + port: 8080,
94 + clientLogLevel: 'warning',
95 + stats: 'errors-only',
96 + };
97 +} else {
98 + config.output = {
99 + path: resolve(__dirname, 'dist'),
100 + filename: '[name].js',
101 + };
102 +}
103 +
104 +module.exports = config;
yarn.lock
+744 -16
@@ -1572,6 +1572,14 @@
1572 global-agent "^2.0.2"
1573 global-tunnel-ng "^2.7.1"
1574
1575 +"@elg/speedscope@1.9.0-a6f84db":
1576 + version "1.9.0-a6f84db"
1577 + resolved "https://registry.yarnpkg.com/@elg/speedscope/-/speedscope-1.9.0-a6f84db.tgz#36079096390b9b396dfec5aeac12e971feb46084"
1578 + integrity sha512-AoGBS1H5s8jrqIh51P8tZnO9i02w7jBJsd0W6+nrF2tLce502zb2rOyot4iv4fnvhmjnM6VFPBLjE2DfElg8Dw==
1579 + dependencies:
1580 + opn "5.3.0"
1581 + react "^16.13.1"
1582 +
1583 "@istanbuljs/load-nyc-config@^1.0.0":
1584 version "1.0.0"
1585 resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
@@ -1903,6 +1911,11 @@
1911 dependencies:
1912 defer-to-connect "^1.0.1"
1913
1914 +"@types/anymatch@*":
1915 + version "1.3.1"
1916 + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
1917 + integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
1918 +
1919 "@types/babel__core@^7.1.0":
1920 version "7.1.2"
1921 resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
@@ -1965,6 +1978,11 @@
1978 "@types/minimatch" "*"
1979 "@types/node" "*"
1980
1981 +"@types/html-minifier-terser@^5.0.0":
1982 + version "5.1.0"
1983 + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"
1984 + integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA==
1985 +
1986 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
1987 version "2.0.1"
1988 resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -1990,6 +2008,11 @@
2008 resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
2009 integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
2010
2011 +"@types/json-schema@^7.0.4":
2012 + version "7.0.5"
2013 + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
2014 + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
2015 +
2016 "@types/minimatch@*", "@types/minimatch@^3.0.3":
2017 version "3.0.3"
2018 resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -2010,11 +2033,49 @@
2033 resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
2034 integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
2035
2036 +"@types/source-list-map@*":
2037 + version "0.1.2"
2038 + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
2039 + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
2040 +
2041 "@types/stack-utils@^1.0.1":
2042 version "1.0.1"
2043 resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
2044 integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
2045
2046 +"@types/tapable@*", "@types/tapable@^1.0.5":
2047 + version "1.0.6"
2048 + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74"
2049 + integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
2050 +
2051 +"@types/uglify-js@*":
2052 + version "3.9.3"
2053 + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b"
2054 + integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w==
2055 + dependencies:
2056 + source-map "^0.6.1"
2057 +
2058 +"@types/webpack-sources@*":
2059 + version "1.4.2"
2060 + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.2.tgz#5d3d4dea04008a779a90135ff96fb5c0c9e6292c"
2061 + integrity sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw==
2062 + dependencies:
2063 + "@types/node" "*"
2064 + "@types/source-list-map" "*"
2065 + source-map "^0.7.3"
2066 +
2067 +"@types/webpack@^4.41.8":
2068 + version "4.41.21"
2069 + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.21.tgz#cc685b332c33f153bb2f5fc1fa3ac8adeb592dee"
2070 + integrity sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA==
2071 + dependencies:
2072 + "@types/anymatch" "*"
2073 + "@types/node" "*"
2074 + "@types/tapable" "*"
2075 + "@types/uglify-js" "*"
2076 + "@types/webpack-sources" "*"
2077 + source-map "^0.6.0"
2078 +
2079 "@types/yargs-parser@*":
2080 version "13.0.0"
2081 resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0"
@@ -2469,6 +2530,16 @@ ajv@^6.10.0, ajv@^6.5.5, ajv@^6.9.1:
2530 json-schema-traverse "^0.4.1"
2531 uri-js "^4.2.2"
2532
2533 +ajv@^6.12.2:
2534 + version "6.12.4"
2535 + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
2536 + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==
2537 + dependencies:
2538 + fast-deep-equal "^3.1.1"
2539 + fast-json-stable-stringify "^2.0.0"
2540 + json-schema-traverse "^0.4.1"
2541 + uri-js "^4.2.2"
2542 +
2543 ansi-align@^2.0.0:
2544 version "2.0.0"
2545 resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
@@ -2586,7 +2657,7 @@ anymatch@^2.0.0:
2657 micromatch "^3.1.4"
2658 normalize-path "^2.1.1"
2659
2589 -anymatch@^3.0.3:
2660 +anymatch@^3.0.3, anymatch@~3.1.1:
2661 version "3.1.1"
2662 resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
2663 integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
@@ -2967,6 +3038,17 @@ babel-loader@^8.0.4:
3038 mkdirp "^0.5.1"
3039 pify "^4.0.1"
3040
3041 +babel-loader@^8.1.0:
3042 + version "8.1.0"
3043 + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
3044 + integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
3045 + dependencies:
3046 + find-cache-dir "^2.1.0"
3047 + loader-utils "^1.4.0"
3048 + mkdirp "^0.5.3"
3049 + pify "^4.0.1"
3050 + schema-utils "^2.6.5"
3051 +
3052 babel-plugin-dynamic-import-node@^2.3.0:
3053 version "2.3.0"
3054 resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
@@ -3106,6 +3188,11 @@ binary-extensions@^1.0.0:
3188 resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
3189 integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
3190
3191 +binary-extensions@^2.0.0:
3192 + version "2.1.0"
3193 + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
3194 + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==
3195 +
3196 bindings@^1.5.0:
3197 version "1.5.0"
3198 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@@ -3245,7 +3332,7 @@ braces@^2.3.1, braces@^2.3.2:
3332 split-string "^3.0.2"
3333 to-regex "^3.0.1"
3334
3248 -braces@^3.0.1:
3335 +braces@^3.0.1, braces@~3.0.2:
3336 version "3.0.2"
3337 resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
3338 integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -3554,6 +3641,14 @@ callsites@^3.0.0:
3641 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
3642 integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
3643
3644 +camel-case@^4.1.1:
3645 + version "4.1.1"
3646 + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.1.tgz#1fc41c854f00e2f7d0139dfeba1542d6896fe547"
3647 + integrity sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==
3648 + dependencies:
3649 + pascal-case "^3.1.1"
3650 + tslib "^1.10.0"
3651 +
3652 camelcase@5.3.1, camelcase@^5.0.0, camelcase@^5.3.1:
3653 version "5.3.1"
3654 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -3564,6 +3659,11 @@ camelcase@^4.0.0:
3659 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
3660 integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
3661
3662 +camelcase@^6.0.0:
3663 + version "6.0.0"
3664 + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
3665 + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
3666 +
3667 caniuse-lite@^1.0.30001022:
3668 version "1.0.30001022"
3669 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6"
@@ -3680,6 +3780,21 @@ chokidar@^2.1.8:
3780 optionalDependencies:
3781 fsevents "^1.2.7"
3782
3783 +chokidar@^3.4.1:
3784 + version "3.4.2"
3785 + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d"
3786 + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==
3787 + dependencies:
3788 + anymatch "~3.1.1"
3789 + braces "~3.0.2"
3790 + glob-parent "~5.1.0"
3791 + is-binary-path "~2.1.0"
3792 + is-glob "~4.0.1"
3793 + normalize-path "~3.0.0"
3794 + readdirp "~3.4.0"
3795 + optionalDependencies:
3796 + fsevents "~2.1.2"
3797 +
3798 chownr@^1.0.1, chownr@^1.1.2, chownr@^1.1.3:
3799 version "1.1.3"
3800 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
@@ -3755,6 +3870,13 @@ class-utils@^0.3.5:
3870 isobject "^3.0.0"
3871 static-extend "^0.1.1"
3872
3873 +clean-css@^4.2.3:
3874 + version "4.2.3"
3875 + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
3876 + integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
3877 + dependencies:
3878 + source-map "~0.6.0"
3879 +
3880 clean-stack@^2.0.0:
3881 version "2.2.0"
3882 resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@@ -3966,6 +4088,11 @@ commander@^4.0.1:
4088 resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83"
4089 integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==
4090
4091 +commander@^4.1.1:
4092 + version "4.1.1"
4093 + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
4094 + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
4095 +
4096 common-tags@1.8.0:
4097 version "1.8.0"
4098 resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
@@ -4384,7 +4511,26 @@ css-loader@^1.0.1:
4511 postcss-value-parser "^3.3.0"
4512 source-list-map "^2.0.0"
4513
4387 -css-select@~1.2.0:
4514 +css-loader@^4.2.1:
4515 + version "4.2.1"
4516 + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.1.tgz#9f48fd7eae1219d629a3f085ba9a9102ca1141a7"
4517 + integrity sha512-MoqmF1if7Z0pZIEXA4ZF9PgtCXxWbfzfJM+3p+OYfhcrwcqhaCRb74DSnfzRl7e024xEiCRn5hCvfUbTf2sgFA==
4518 + dependencies:
4519 + camelcase "^6.0.0"
4520 + cssesc "^3.0.0"
4521 + icss-utils "^4.1.1"
4522 + loader-utils "^2.0.0"
4523 + normalize-path "^3.0.0"
4524 + postcss "^7.0.32"
4525 + postcss-modules-extract-imports "^2.0.0"
4526 + postcss-modules-local-by-default "^3.0.3"
4527 + postcss-modules-scope "^2.2.0"
4528 + postcss-modules-values "^3.0.0"
4529 + postcss-value-parser "^4.1.0"
4530 + schema-utils "^2.7.0"
4531 + semver "^7.3.2"
4532 +
4533 +css-select@^1.1.0, css-select@~1.2.0:
4534 version "1.2.0"
4535 resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
4536 integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
@@ -4413,6 +4559,11 @@ cssesc@^0.1.0:
4559 resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
4560 integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=
4561
4562 +cssesc@^3.0.0:
4563 + version "3.0.0"
4564 + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
4565 + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
4566 +
4567 cssom@^0.4.1, cssom@^0.4.4:
4568 version "0.4.4"
4569 resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
@@ -4795,6 +4946,13 @@ doctrine@^3.0.0:
4946 dependencies:
4947 esutils "^2.0.2"
4948
4949 +dom-converter@^0.2:
4950 + version "0.2.0"
4951 + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
4952 + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
4953 + dependencies:
4954 + utila "~0.4"
4955 +
4956 dom-serializer@0:
4957 version "0.2.1"
4958 resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb"
@@ -4863,6 +5021,14 @@ domutils@^1.5.1:
5021 dom-serializer "0"
5022 domelementtype "1"
5023
5024 +dot-case@^3.0.3:
5025 + version "3.0.3"
5026 + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa"
5027 + integrity sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==
5028 + dependencies:
5029 + no-case "^3.0.3"
5030 + tslib "^1.10.0"
5031 +
5032 dot-prop@^4.1.0:
5033 version "4.2.0"
5034 resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@@ -5026,6 +5192,15 @@ enhanced-resolve@^4.1.0:
5192 memory-fs "^0.5.0"
5193 tapable "^1.0.0"
5194
5195 +enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0:
5196 + version "4.3.0"
5197 + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126"
5198 + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==
5199 + dependencies:
5200 + graceful-fs "^4.1.2"
5201 + memory-fs "^0.5.0"
5202 + tapable "^1.0.0"
5203 +
5204 entities@^1.1.1, entities@~1.1.1:
5205 version "1.1.2"
5206 resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
@@ -5079,6 +5254,23 @@ es-abstract@^1.13.0:
5254 is-regex "^1.0.4"
5255 object-keys "^1.0.12"
5256
5257 +es-abstract@^1.17.0-next.1, es-abstract@^1.17.5:
5258 + version "1.17.6"
5259 + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
5260 + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
5261 + dependencies:
5262 + es-to-primitive "^1.2.1"
5263 + function-bind "^1.1.1"
5264 + has "^1.0.3"
5265 + has-symbols "^1.0.1"
5266 + is-callable "^1.2.0"
5267 + is-regex "^1.1.0"
5268 + object-inspect "^1.7.0"
5269 + object-keys "^1.1.1"
5270 + object.assign "^4.1.0"
5271 + string.prototype.trimend "^1.0.1"
5272 + string.prototype.trimstart "^1.0.1"
5273 +
5274 es-to-primitive@^1.2.0:
5275 version "1.2.0"
5276 resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
@@ -5088,6 +5280,15 @@ es-to-primitive@^1.2.0:
5280 is-date-object "^1.0.1"
5281 is-symbol "^1.0.2"
5282
5283 +es-to-primitive@^1.2.1:
5284 + version "1.2.1"
5285 + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
5286 + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
5287 + dependencies:
5288 + is-callable "^1.1.4"
5289 + is-date-object "^1.0.1"
5290 + is-symbol "^1.0.2"
5291 +
5292 es6-error@4.1.1, es6-error@^4.1.1:
5293 version "4.1.1"
5294 resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
@@ -5875,6 +6076,14 @@ file-entry-cache@^5.0.1:
6076 dependencies:
6077 flat-cache "^2.0.1"
6078
6079 +file-loader@^6.0.0:
6080 + version "6.0.0"
6081 + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f"
6082 + integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==
6083 + dependencies:
6084 + loader-utils "^2.0.0"
6085 + schema-utils "^2.6.5"
6086 +
6087 file-uri-to-path@1.0.0:
6088 version "1.0.0"
6089 resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -5955,7 +6164,7 @@ find-up@^4.0.0, find-up@^4.1.0:
6164 locate-path "^5.0.0"
6165 path-exists "^4.0.0"
6166
5958 -findup-sync@3.0.0:
6167 +findup-sync@3.0.0, findup-sync@^3.0.0:
6168 version "3.0.0"
6169 resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
6170 integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
@@ -6167,7 +6376,7 @@ fs.realpath@^1.0.0:
6376 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
6377 integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
6378
6170 -fsevents@2.1.3:
6379 +fsevents@2.1.3, fsevents@~2.1.2:
6380 version "2.1.3"
6381 resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
6382 integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
@@ -6319,6 +6528,13 @@ glob-parent@^5.0.0:
6528 dependencies:
6529 is-glob "^4.0.1"
6530
6531 +glob-parent@~5.1.0:
6532 + version "5.1.1"
6533 + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
6534 + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
6535 + dependencies:
6536 + is-glob "^4.0.1"
6537 +
6538 glob-stream@^6.1.0:
6539 version "6.1.0"
6540 resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
@@ -6385,7 +6601,7 @@ global-dirs@^2.0.1:
6601 dependencies:
6602 ini "^1.3.5"
6603
6388 -global-modules@2.0.0:
6604 +global-modules@2.0.0, global-modules@^2.0.0:
6605 version "2.0.0"
6606 resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
6607 integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
@@ -6614,6 +6830,11 @@ has-symbols@^1.0.0:
6830 resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
6831 integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
6832
6833 +has-symbols@^1.0.1:
6834 + version "1.0.1"
6835 + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
6836 + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
6837 +
6838 has-value@^0.3.1:
6839 version "0.3.1"
6840 resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -6679,6 +6900,11 @@ hasurl@^1.0.0:
6900 resolved "https://registry.yarnpkg.com/hasurl/-/hasurl-1.0.0.tgz#e4c619097ae1e8fc906bee904ce47e94f5e1ea37"
6901 integrity sha512-43ypUd3DbwyCT01UYpA99AEZxZ4aKtRxWGBHEIbjcOsUghd9YUON0C+JF6isNjaiwC/UF5neaUudy6JS9jZPZQ==
6902
6903 +he@^1.2.0:
6904 + version "1.2.0"
6905 + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
6906 + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
6907 +
6908 hmac-drbg@^1.0.0:
6909 version "1.0.1"
6910 resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -6731,12 +6957,45 @@ html-entities@^1.2.1:
6957 resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
6958 integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
6959
6960 +html-entities@^1.3.1:
6961 + version "1.3.1"
6962 + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44"
6963 + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==
6964 +
6965 html-escaper@^2.0.0:
6966 version "2.0.0"
6967 resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491"
6968 integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==
6969
6739 -htmlparser2@^3.9.1:
6970 +html-minifier-terser@^5.0.1:
6971 + version "5.1.1"
6972 + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054"
6973 + integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==
6974 + dependencies:
6975 + camel-case "^4.1.1"
6976 + clean-css "^4.2.3"
6977 + commander "^4.1.1"
6978 + he "^1.2.0"
6979 + param-case "^3.0.3"
6980 + relateurl "^0.2.7"
6981 + terser "^4.6.3"
6982 +
6983 +html-webpack-plugin@^4.3.0:
6984 + version "4.3.0"
6985 + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz#53bf8f6d696c4637d5b656d3d9863d89ce8174fd"
6986 + integrity sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w==
6987 + dependencies:
6988 + "@types/html-minifier-terser" "^5.0.0"
6989 + "@types/tapable" "^1.0.5"
6990 + "@types/webpack" "^4.41.8"
6991 + html-minifier-terser "^5.0.1"
6992 + loader-utils "^1.2.3"
6993 + lodash "^4.17.15"
6994 + pretty-error "^2.1.1"
6995 + tapable "^1.1.3"
6996 + util.promisify "1.0.0"
6997 +
6998 +htmlparser2@^3.3.0, htmlparser2@^3.9.1:
6999 version "3.10.1"
7000 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
7001 integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@@ -6919,6 +7178,13 @@ icss-utils@^2.1.0:
7178 dependencies:
7179 postcss "^6.0.1"
7180
7181 +icss-utils@^4.0.0, icss-utils@^4.1.1:
7182 + version "4.1.1"
7183 + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
7184 + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
7185 + dependencies:
7186 + postcss "^7.0.14"
7187 +
7188 ieee754@^1.1.12, ieee754@^1.1.4:
7189 version "1.1.13"
7190 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@@ -6993,6 +7259,11 @@ indent-string@^4.0.0:
7259 resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
7260 integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
7261
7262 +indexes-of@^1.0.1:
7263 + version "1.0.1"
7264 + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
7265 + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
7266 +
7267 indexof@0.0.1:
7268 version "0.0.1"
7269 resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
@@ -7089,6 +7360,11 @@ interpret@1.2.0, interpret@^1.0.0:
7360 resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
7361 integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
7362
7363 +interpret@^1.4.0:
7364 + version "1.4.0"
7365 + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
7366 + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
7367 +
7368 invariant@^2.2.2, invariant@^2.2.4:
7369 version "2.2.4"
7370 resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -7172,6 +7448,13 @@ is-binary-path@^1.0.0:
7448 dependencies:
7449 binary-extensions "^1.0.0"
7450
7451 +is-binary-path@~2.1.0:
7452 + version "2.1.0"
7453 + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
7454 + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
7455 + dependencies:
7456 + binary-extensions "^2.0.0"
7457 +
7458 is-buffer@^1.1.5:
7459 version "1.1.6"
7460 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -7182,6 +7465,11 @@ is-callable@^1.1.4:
7465 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
7466 integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
7467
7468 +is-callable@^1.2.0:
7469 + version "1.2.0"
7470 + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
7471 + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
7472 +
7473 is-ci@^1.0.10:
7474 version "1.2.1"
7475 resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
@@ -7308,7 +7596,7 @@ is-glob@^3.1.0:
7596 dependencies:
7597 is-extglob "^2.1.0"
7598
7311 -is-glob@^4.0.0, is-glob@^4.0.1:
7599 +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
7600 version "4.0.1"
7601 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
7602 integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
@@ -7462,6 +7750,13 @@ is-regex@^1.0.4:
7750 dependencies:
7751 has "^1.0.1"
7752
7753 +is-regex@^1.1.0:
7754 + version "1.1.1"
7755 + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
7756 + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
7757 + dependencies:
7758 + has-symbols "^1.0.1"
7759 +
7760 is-relative@^0.1.0:
7761 version "0.1.3"
7762 resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.1.3.tgz#905fee8ae86f45b3ec614bc3c15c869df0876e82"
@@ -8461,7 +8756,7 @@ loader-utils@1.2.3, loader-utils@^1.0.2, loader-utils@^1.1.0:
8756 emojis-list "^2.0.0"
8757 json5 "^1.0.1"
8758
8464 -loader-utils@^1.2.3:
8759 +loader-utils@^1.2.3, loader-utils@^1.4.0:
8760 version "1.4.0"
8761 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
8762 integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@@ -8470,6 +8765,15 @@ loader-utils@^1.2.3:
8765 emojis-list "^3.0.0"
8766 json5 "^1.0.1"
8767
8768 +loader-utils@^2.0.0:
8769 + version "2.0.0"
8770 + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
8771 + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
8772 + dependencies:
8773 + big.js "^5.2.2"
8774 + emojis-list "^3.0.0"
8775 + json5 "^2.1.2"
8776 +
8777 local-storage-fallback@^4.1.1:
8778 version "4.1.1"
8779 resolved "https://registry.yarnpkg.com/local-storage-fallback/-/local-storage-fallback-4.1.1.tgz#6dc964635c8e9ab6d522d7d88538f465b62bd161"
@@ -8637,7 +8941,7 @@ log-driver@^1.2.7:
8941 resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8"
8942 integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==
8943
8640 -loglevel@^1.6.6:
8944 +loglevel@^1.6.6, loglevel@^1.6.8:
8945 version "1.6.8"
8946 resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
8947 integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
@@ -8656,6 +8960,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4
8960 dependencies:
8961 js-tokens "^3.0.0 || ^4.0.0"
8962
8963 +lower-case@^2.0.1:
8964 + version "2.0.1"
8965 + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7"
8966 + integrity sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==
8967 + dependencies:
8968 + tslib "^1.10.0"
8969 +
8970 lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
8971 version "1.0.1"
8972 resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
@@ -8837,6 +9148,11 @@ memoize-one@^3.1.1:
9148 resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
9149 integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA==
9150
9151 +memoize-one@^5.1.1:
9152 + version "5.1.1"
9153 + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
9154 + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
9155 +
9156 memory-fs@^0.4.0, memory-fs@^0.4.1:
9157 version "0.4.1"
9158 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -8927,6 +9243,11 @@ mime-db@1.40.0, "mime-db@>= 1.40.0 < 2":
9243 resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
9244 integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
9245
9246 +mime-db@1.44.0:
9247 + version "1.44.0"
9248 + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
9249 + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
9250 +
9251 mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
9252 version "2.1.24"
9253 resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
@@ -8934,6 +9255,13 @@ mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
9255 dependencies:
9256 mime-db "1.40.0"
9257
9258 +mime-types@^2.1.26:
9259 + version "2.1.27"
9260 + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
9261 + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
9262 + dependencies:
9263 + mime-db "1.44.0"
9264 +
9265 mime@1.6.0:
9266 version "1.6.0"
9267 resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
@@ -9085,7 +9413,7 @@ mkdirp@1.0.4:
9413 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
9414 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
9415
9088 -mkdirp@^0.5.0, mkdirp@^0.5.3:
9416 +mkdirp@^0.5.0, mkdirp@^0.5.3, mkdirp@^0.5.5:
9417 version "0.5.5"
9418 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
9419 integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -9233,6 +9561,14 @@ nice-try@^1.0.4:
9561 resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
9562 integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
9563
9564 +no-case@^3.0.3:
9565 + version "3.0.3"
9566 + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.3.tgz#c21b434c1ffe48b39087e86cfb4d2582e9df18f8"
9567 + integrity sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==
9568 + dependencies:
9569 + lower-case "^2.0.1"
9570 + tslib "^1.10.0"
9571 +
9572 node-cleanup@^2.1.2:
9573 version "2.1.2"
9574 resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
@@ -9376,7 +9712,7 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
9712 dependencies:
9713 remove-trailing-separator "^1.0.1"
9714
9379 -normalize-path@^3.0.0:
9715 +normalize-path@^3.0.0, normalize-path@~3.0.0:
9716 version "3.0.0"
9717 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
9718 integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -9473,7 +9809,7 @@ nth-check@~1.0.1:
9809 dependencies:
9810 boolbase "~1.0.0"
9811
9476 -nullthrows@^1.0.0:
9812 +nullthrows@^1.0.0, nullthrows@^1.1.1:
9813 version "1.1.1"
9814 resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
9815 integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
@@ -9507,7 +9843,12 @@ object-copy@^0.1.0:
9843 define-property "^0.2.5"
9844 kind-of "^3.0.3"
9845
9510 -object-keys@^1.0.11, object-keys@^1.0.12:
9846 +object-inspect@^1.7.0:
9847 + version "1.8.0"
9848 + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
9849 + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
9850 +
9851 +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
9852 version "1.1.1"
9853 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
9854 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -9529,6 +9870,14 @@ object.assign@^4.0.4, object.assign@^4.1.0:
9870 has-symbols "^1.0.0"
9871 object-keys "^1.0.11"
9872
9873 +object.getownpropertydescriptors@^2.0.3:
9874 + version "2.1.0"
9875 + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
9876 + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
9877 + dependencies:
9878 + define-properties "^1.1.3"
9879 + es-abstract "^1.17.0-next.1"
9880 +
9881 object.omit@^2.0.0:
9882 version "2.0.1"
9883 resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -9602,6 +9951,13 @@ open@^7.0.2:
9951 is-docker "^2.0.0"
9952 is-wsl "^2.1.1"
9953
9954 +opn@5.3.0:
9955 + version "5.3.0"
9956 + resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
9957 + integrity sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==
9958 + dependencies:
9959 + is-wsl "^1.1.0"
9960 +
9961 opn@^5.5.0:
9962 version "5.5.0"
9963 resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
@@ -9869,6 +10225,14 @@ parallel-transform@^1.1.0:
10225 inherits "^2.0.3"
10226 readable-stream "^2.1.5"
10227
10228 +param-case@^3.0.3:
10229 + version "3.0.3"
10230 + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.3.tgz#4be41f8399eff621c56eebb829a5e451d9801238"
10231 + integrity sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==
10232 + dependencies:
10233 + dot-case "^3.0.3"
10234 + tslib "^1.10.0"
10235 +
10236 parent-module@^1.0.0:
10237 version "1.0.1"
10238 resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -9934,6 +10298,11 @@ parse-link-header@^1.0.1:
10298 dependencies:
10299 xtend "~4.0.1"
10300
10301 +parse-ms@^2.1.0:
10302 + version "2.1.0"
10303 + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
10304 + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
10305 +
10306 parse-node-version@^1.0.0:
10307 version "1.0.1"
10308 resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
@@ -9966,6 +10335,14 @@ parseurl@~1.3.2, parseurl@~1.3.3:
10335 resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
10336 integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
10337
10338 +pascal-case@^3.1.1:
10339 + version "3.1.1"
10340 + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.1.tgz#5ac1975133ed619281e88920973d2cd1f279de5f"
10341 + integrity sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==
10342 + dependencies:
10343 + no-case "^3.0.3"
10344 + tslib "^1.10.0"
10345 +
10346 pascalcase@^0.1.1:
10347 version "0.1.1"
10348 resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
@@ -10065,6 +10442,11 @@ picomatch@^2.0.4, picomatch@^2.0.5:
10442 resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
10443 integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
10444
10445 +picomatch@^2.2.1:
10446 + version "2.2.2"
10447 + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
10448 + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
10449 +
10450 pify@^2.0.0:
10451 version "2.3.0"
10452 resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -10172,6 +10554,15 @@ portfinder@^1.0.25:
10554 debug "^3.1.1"
10555 mkdirp "^0.5.1"
10556
10557 +portfinder@^1.0.26:
10558 + version "1.0.28"
10559 + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
10560 + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
10561 + dependencies:
10562 + async "^2.6.2"
10563 + debug "^3.1.1"
10564 + mkdirp "^0.5.5"
10565 +
10566 posix-character-classes@^0.1.0:
10567 version "0.1.1"
10568 resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -10184,6 +10575,13 @@ postcss-modules-extract-imports@^1.2.0:
10575 dependencies:
10576 postcss "^6.0.1"
10577
10578 +postcss-modules-extract-imports@^2.0.0:
10579 + version "2.0.0"
10580 + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
10581 + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
10582 + dependencies:
10583 + postcss "^7.0.5"
10584 +
10585 postcss-modules-local-by-default@^1.2.0:
10586 version "1.2.0"
10587 resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
@@ -10192,6 +10590,16 @@ postcss-modules-local-by-default@^1.2.0:
10590 css-selector-tokenizer "^0.7.0"
10591 postcss "^6.0.1"
10592
10593 +postcss-modules-local-by-default@^3.0.3:
10594 + version "3.0.3"
10595 + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
10596 + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
10597 + dependencies:
10598 + icss-utils "^4.1.1"
10599 + postcss "^7.0.32"
10600 + postcss-selector-parser "^6.0.2"
10601 + postcss-value-parser "^4.1.0"
10602 +
10603 postcss-modules-scope@^1.1.0:
10604 version "1.1.0"
10605 resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
@@ -10200,6 +10608,14 @@ postcss-modules-scope@^1.1.0:
10608 css-selector-tokenizer "^0.7.0"
10609 postcss "^6.0.1"
10610
10611 +postcss-modules-scope@^2.2.0:
10612 + version "2.2.0"
10613 + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
10614 + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
10615 + dependencies:
10616 + postcss "^7.0.6"
10617 + postcss-selector-parser "^6.0.0"
10618 +
10619 postcss-modules-values@^1.3.0:
10620 version "1.3.0"
10621 resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
@@ -10208,12 +10624,34 @@ postcss-modules-values@^1.3.0:
10624 icss-replace-symbols "^1.1.0"
10625 postcss "^6.0.1"
10626
10627 +postcss-modules-values@^3.0.0:
10628 + version "3.0.0"
10629 + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
10630 + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
10631 + dependencies:
10632 + icss-utils "^4.0.0"
10633 + postcss "^7.0.6"
10634 +
10635 +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
10636 + version "6.0.2"
10637 + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
10638 + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
10639 + dependencies:
10640 + cssesc "^3.0.0"
10641 + indexes-of "^1.0.1"
10642 + uniq "^1.0.1"
10643 +
10644 postcss-value-parser@^3.3.0:
10645 version "3.3.1"
10646 resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
10647 integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
10648
10216 -postcss@7.0.32:
10649 +postcss-value-parser@^4.1.0:
10650 + version "4.1.0"
10651 + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
10652 + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
10653 +
10654 +postcss@7.0.32, postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
10655 version "7.0.32"
10656 resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
10657 integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
@@ -10261,6 +10699,14 @@ prettier@1.19.1, prettier@^1.0.0:
10699 resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
10700 integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
10701
10702 +pretty-error@^2.1.1:
10703 + version "2.1.1"
10704 + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
10705 + integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=
10706 + dependencies:
10707 + renderkid "^2.0.1"
10708 + utila "~0.4"
10709 +
10710 pretty-format@^25.2.6:
10711 version "25.2.6"
10712 resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.6.tgz#542a1c418d019bbf1cca2e3620443bc1323cb8d7"
@@ -10271,6 +10717,13 @@ pretty-format@^25.2.6:
10717 ansi-styles "^4.0.0"
10718 react-is "^16.12.0"
10719
10720 +pretty-ms@^7.0.0:
10721 + version "7.0.0"
10722 + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.0.tgz#45781273110caf35f55cab21a8a9bd403a233dc0"
10723 + integrity sha512-J3aPWiC5e9ZeZFuSeBraGxSkGMOvulSWsxDByOcbD1Pr75YL3LSNIKIb52WXbCLE1sS5s4inBBbryjF4Y05Ceg==
10724 + dependencies:
10725 + parse-ms "^2.1.0"
10726 +
10727 prettyjson@^1.2.1:
10728 version "1.2.1"
10729 resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289"
@@ -10620,6 +11073,15 @@ react-virtualized-auto-sizer@^1.0.2:
11073 resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
11074 integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
11075
11076 +react@^16.13.1:
11077 + version "16.13.1"
11078 + resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
11079 + integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
11080 + dependencies:
11081 + loose-envify "^1.1.0"
11082 + object-assign "^4.1.1"
11083 + prop-types "^15.6.2"
11084 +
11085 read-package-json-fast@^1.1.1:
11086 version "1.1.3"
11087 resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-1.1.3.tgz#3b78464ea8f3c4447f3358635390b6946dc0737e"
@@ -10710,6 +11172,13 @@ readdirp@^2.2.1:
11172 micromatch "^3.1.10"
11173 readable-stream "^2.0.2"
11174
11175 +readdirp@~3.4.0:
11176 + version "3.4.0"
11177 + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
11178 + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
11179 + dependencies:
11180 + picomatch "^2.2.1"
11181 +
11182 readline-sync@^1.4.9:
11183 version "1.4.10"
11184 resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
@@ -10749,6 +11218,11 @@ regenerator-runtime@^0.13.4:
11218 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
11219 integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
11220
11221 +regenerator-runtime@^0.13.7:
11222 + version "0.13.7"
11223 + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
11224 + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
11225 +
11226 regenerator-transform@^0.14.2:
11227 version "0.14.5"
11228 resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
@@ -10856,6 +11330,11 @@ regjsparser@^0.6.4:
11330 dependencies:
11331 jsesc "~0.5.0"
11332
11333 +relateurl@^0.2.7:
11334 + version "0.2.7"
11335 + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
11336 + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
11337 +
11338 relaxed-json@1.0.3:
11339 version "1.0.3"
11340 resolved "https://registry.yarnpkg.com/relaxed-json/-/relaxed-json-1.0.3.tgz#eb2101ae0ee60e82267d95ed0ddf19a3604b8c1e"
@@ -10869,6 +11348,17 @@ remove-trailing-separator@^1.0.1:
11348 resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
11349 integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
11350
11351 +renderkid@^2.0.1:
11352 + version "2.0.3"
11353 + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
11354 + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==
11355 + dependencies:
11356 + css-select "^1.1.0"
11357 + dom-converter "^0.2"
11358 + htmlparser2 "^3.3.0"
11359 + strip-ansi "^3.0.0"
11360 + utila "^0.4.0"
11361 +
11362 repeat-element@^1.1.2:
11363 version "1.1.3"
11364 resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
@@ -11354,6 +11844,15 @@ schema-utils@^2.0.1:
11844 ajv "^6.1.0"
11845 ajv-keywords "^3.1.0"
11846
11847 +schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0:
11848 + version "2.7.0"
11849 + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
11850 + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
11851 + dependencies:
11852 + "@types/json-schema" "^7.0.4"
11853 + ajv "^6.12.2"
11854 + ajv-keywords "^3.4.1"
11855 +
11856 select-hose@^2.0.0:
11857 version "2.0.0"
11858 resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -11675,6 +12174,15 @@ sockjs@0.3.19:
12174 faye-websocket "^0.10.0"
12175 uuid "^3.0.1"
12176
12177 +sockjs@0.3.20:
12178 + version "0.3.20"
12179 + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855"
12180 + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==
12181 + dependencies:
12182 + faye-websocket "^0.10.0"
12183 + uuid "^3.4.0"
12184 + websocket-driver "0.6.5"
12185 +
12186 socks-proxy-agent@^4.0.0:
12187 version "4.0.2"
12188 resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
@@ -11749,7 +12257,7 @@ source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6:
12257 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
12258 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
12259
11752 -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
12260 +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
12261 version "0.6.1"
12262 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
12263 integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -11795,6 +12303,17 @@ spdy@^4.0.1:
12303 select-hose "^2.0.0"
12304 spdy-transport "^3.0.0"
12305
12306 +spdy@^4.0.2:
12307 + version "4.0.2"
12308 + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b"
12309 + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==
12310 + dependencies:
12311 + debug "^4.1.0"
12312 + handle-thing "^2.0.0"
12313 + http-deceiver "^1.2.7"
12314 + select-hose "^2.0.0"
12315 + spdy-transport "^3.0.0"
12316 +
12317 split-on-first@^1.0.0:
12318 version "1.1.0"
12319 resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
@@ -11993,6 +12512,22 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
12512 is-fullwidth-code-point "^3.0.0"
12513 strip-ansi "^6.0.0"
12514
12515 +string.prototype.trimend@^1.0.1:
12516 + version "1.0.1"
12517 + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
12518 + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
12519 + dependencies:
12520 + define-properties "^1.1.3"
12521 + es-abstract "^1.17.5"
12522 +
12523 +string.prototype.trimstart@^1.0.1:
12524 + version "1.0.1"
12525 + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
12526 + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
12527 + dependencies:
12528 + define-properties "^1.1.3"
12529 + es-abstract "^1.17.5"
12530 +
12531 string_decoder@^1.0.0, string_decoder@^1.1.1:
12532 version "1.3.0"
12533 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -12103,6 +12638,14 @@ style-loader@^0.23.1:
12638 loader-utils "^1.1.0"
12639 schema-utils "^1.0.0"
12640
12641 +style-loader@^1.2.1:
12642 + version "1.2.1"
12643 + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a"
12644 + integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg==
12645 + dependencies:
12646 + loader-utils "^2.0.0"
12647 + schema-utils "^2.6.6"
12648 +
12649 sumchecker@^3.0.1:
12650 version "3.0.1"
12651 resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
@@ -12274,6 +12817,15 @@ terser@^4.1.2:
12817 source-map "~0.6.1"
12818 source-map-support "~0.5.12"
12819
12820 +terser@^4.6.3:
12821 + version "4.8.0"
12822 + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
12823 + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
12824 + dependencies:
12825 + commander "^2.20.0"
12826 + source-map "~0.6.1"
12827 + source-map-support "~0.5.12"
12828 +
12829 test-exclude@^6.0.0:
12830 version "6.0.0"
12831 resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
@@ -12518,6 +13070,11 @@ truncate-utf8-bytes@^1.0.0:
13070 dependencies:
13071 utf8-byte-length "^1.0.1"
13072
13073 +tslib@^1.10.0:
13074 + version "1.13.0"
13075 + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
13076 + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
13077 +
13078 tslib@^1.8.1:
13079 version "1.11.1"
13080 resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
@@ -12659,6 +13216,11 @@ union-value@^1.0.0:
13216 is-extendable "^0.1.1"
13217 set-value "^2.0.1"
13218
13219 +uniq@^1.0.1:
13220 + version "1.0.1"
13221 + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
13222 + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
13223 +
13224 unique-filename@^1.1.1:
13225 version "1.1.1"
13226 resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
@@ -12784,6 +13346,15 @@ urix@^0.1.0:
13346 resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
13347 integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
13348
13349 +url-loader@^4.1.0:
13350 + version "4.1.0"
13351 + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.0.tgz#c7d6b0d6b0fccd51ab3ffc58a78d32b8d89a7be2"
13352 + integrity sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw==
13353 + dependencies:
13354 + loader-utils "^2.0.0"
13355 + mime-types "^2.1.26"
13356 + schema-utils "^2.6.5"
13357 +
13358 url-parse-lax@^1.0.0:
13359 version "1.0.0"
13360 resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
@@ -12847,6 +13418,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
13418 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
13419 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
13420
13421 +util.promisify@1.0.0:
13422 + version "1.0.0"
13423 + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
13424 + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
13425 + dependencies:
13426 + define-properties "^1.1.2"
13427 + object.getownpropertydescriptors "^2.0.3"
13428 +
13429 util@0.10.3:
13430 version "0.10.3"
13431 resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
@@ -12868,6 +13447,11 @@ util@~0.10.3:
13447 dependencies:
13448 inherits "2.0.3"
13449
13450 +utila@^0.4.0, utila@~0.4:
13451 + version "0.4.0"
13452 + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
13453 + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
13454 +
13455 utils-merge@1.0.1:
13456 version "1.0.1"
13457 resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
@@ -12878,6 +13462,11 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2:
13462 resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
13463 integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
13464
13465 +uuid@^3.4.0:
13466 + version "3.4.0"
13467 + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
13468 + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
13469 +
13470 v8-compile-cache@2.0.3:
13471 version "2.0.3"
13472 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
@@ -12888,6 +13477,11 @@ v8-compile-cache@^2.0.3:
13477 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
13478 integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
13479
13480 +v8-compile-cache@^2.1.1:
13481 + version "2.1.1"
13482 + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
13483 + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
13484 +
13485 v8-to-istanbul@^4.0.1:
13486 version "4.0.1"
13487 resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.0.1.tgz#d6a2a3823b8ff49bdf2167ff2a45d82dff81d02f"
@@ -12998,6 +13592,13 @@ warning@^4.0.2:
13592 dependencies:
13593 loose-envify "^1.0.0"
13594
13595 +watchpack-chokidar2@^2.0.0:
13596 + version "2.0.0"
13597 + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
13598 + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
13599 + dependencies:
13600 + chokidar "^2.1.8"
13601 +
13602 watchpack@1.6.1, watchpack@^1.6.1:
13603 version "1.6.1"
13604 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2"
@@ -13007,6 +13608,17 @@ watchpack@1.6.1, watchpack@^1.6.1:
13608 graceful-fs "^4.1.2"
13609 neo-async "^2.5.0"
13610
13611 +watchpack@^1.7.4:
13612 + version "1.7.4"
13613 + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
13614 + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
13615 + dependencies:
13616 + graceful-fs "^4.1.2"
13617 + neo-async "^2.5.0"
13618 + optionalDependencies:
13619 + chokidar "^3.4.1"
13620 + watchpack-chokidar2 "^2.0.0"
13621 +
13622 wbuf@^1.1.0, wbuf@^1.7.3:
13623 version "1.7.3"
13624 resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
@@ -13093,6 +13705,23 @@ webpack-cli@^3.3.11:
13705 v8-compile-cache "2.0.3"
13706 yargs "13.2.4"
13707
13708 +webpack-cli@^3.3.12:
13709 + version "3.3.12"
13710 + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"
13711 + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==
13712 + dependencies:
13713 + chalk "^2.4.2"
13714 + cross-spawn "^6.0.5"
13715 + enhanced-resolve "^4.1.1"
13716 + findup-sync "^3.0.0"
13717 + global-modules "^2.0.0"
13718 + import-local "^2.0.0"
13719 + interpret "^1.4.0"
13720 + loader-utils "^1.4.0"
13721 + supports-color "^6.1.0"
13722 + v8-compile-cache "^2.1.1"
13723 + yargs "^13.3.2"
13724 +
13725 webpack-dev-middleware@^3.7.2:
13726 version "3.7.2"
13727 resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3"
@@ -13143,6 +13772,45 @@ webpack-dev-server@^3.10.3:
13772 ws "^6.2.1"
13773 yargs "12.0.5"
13774
13775 +webpack-dev-server@^3.11.0:
13776 + version "3.11.0"
13777 + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c"
13778 + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==
13779 + dependencies:
13780 + ansi-html "0.0.7"
13781 + bonjour "^3.5.0"
13782 + chokidar "^2.1.8"
13783 + compression "^1.7.4"
13784 + connect-history-api-fallback "^1.6.0"
13785 + debug "^4.1.1"
13786 + del "^4.1.1"
13787 + express "^4.17.1"
13788 + html-entities "^1.3.1"
13789 + http-proxy-middleware "0.19.1"
13790 + import-local "^2.0.0"
13791 + internal-ip "^4.3.0"
13792 + ip "^1.1.5"
13793 + is-absolute-url "^3.0.3"
13794 + killable "^1.0.1"
13795 + loglevel "^1.6.8"
13796 + opn "^5.5.0"
13797 + p-retry "^3.0.1"
13798 + portfinder "^1.0.26"
13799 + schema-utils "^1.0.0"
13800 + selfsigned "^1.10.7"
13801 + semver "^6.3.0"
13802 + serve-index "^1.9.1"
13803 + sockjs "0.3.20"
13804 + sockjs-client "1.4.0"
13805 + spdy "^4.0.2"
13806 + strip-ansi "^3.0.1"
13807 + supports-color "^6.1.0"
13808 + url "^0.11.0"
13809 + webpack-dev-middleware "^3.7.2"
13810 + webpack-log "^2.0.0"
13811 + ws "^6.2.1"
13812 + yargs "^13.3.2"
13813 +
13814 webpack-log@^2.0.0:
13815 version "2.0.0"
13816 resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f"
@@ -13188,6 +13856,42 @@ webpack@^4.41.2, webpack@^4.43.0:
13856 watchpack "^1.6.1"
13857 webpack-sources "^1.4.1"
13858
13859 +webpack@^4.44.1:
13860 + version "4.44.1"
13861 + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21"
13862 + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ==
13863 + dependencies:
13864 + "@webassemblyjs/ast" "1.9.0"
13865 + "@webassemblyjs/helper-module-context" "1.9.0"
13866 + "@webassemblyjs/wasm-edit" "1.9.0"
13867 + "@webassemblyjs/wasm-parser" "1.9.0"
13868 + acorn "^6.4.1"
13869 + ajv "^6.10.2"
13870 + ajv-keywords "^3.4.1"
13871 + chrome-trace-event "^1.0.2"
13872 + enhanced-resolve "^4.3.0"
13873 + eslint-scope "^4.0.3"
13874 + json-parse-better-errors "^1.0.2"
13875 + loader-runner "^2.4.0"
13876 + loader-utils "^1.2.3"
13877 + memory-fs "^0.4.1"
13878 + micromatch "^3.1.10"
13879 + mkdirp "^0.5.3"
13880 + neo-async "^2.6.1"
13881 + node-libs-browser "^2.2.1"
13882 + schema-utils "^1.0.0"
13883 + tapable "^1.1.3"
13884 + terser-webpack-plugin "^1.4.3"
13885 + watchpack "^1.7.4"
13886 + webpack-sources "^1.4.1"
13887 +
13888 +websocket-driver@0.6.5:
13889 + version "0.6.5"
13890 + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
13891 + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=
13892 + dependencies:
13893 + websocket-extensions ">=0.1.1"
13894 +
13895 websocket-driver@>=0.5.1:
13896 version "0.7.3"
13897 resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"
@@ -13467,6 +14171,14 @@ yargs-parser@^13.1.0:
14171 camelcase "^5.0.0"
14172 decamelize "^1.2.0"
14173
14174 +yargs-parser@^13.1.2:
14175 + version "13.1.2"
14176 + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
14177 + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
14178 + dependencies:
14179 + camelcase "^5.0.0"
14180 + decamelize "^1.2.0"
14181 +
14182 yargs-parser@^18.1.1:
14183 version "18.1.2"
14184 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1"
@@ -13527,6 +14239,22 @@ yargs@15.3.1, yargs@^15.3.1, yargs@~15.3.0:
14239 y18n "^4.0.0"
14240 yargs-parser "^18.1.1"
14241
14242 +yargs@^13.3.2:
14243 + version "13.3.2"
14244 + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
14245 + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
14246 + dependencies:
14247 + cliui "^5.0.0"
14248 + find-up "^3.0.0"
14249 + get-caller-file "^2.0.1"
14250 + require-directory "^2.1.1"
14251 + require-main-filename "^2.0.0"
14252 + set-blocking "^2.0.0"
14253 + string-width "^3.0.0"
14254 + which-module "^2.0.0"
14255 + y18n "^4.0.0"
14256 + yargs-parser "^13.1.2"
14257 +
14258 yauzl@2.10.0:
14259 version "2.10.0"
14260 resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"