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"