diff --git a/.eslintignore b/.eslintignore index 62ca593965..2af8176bc3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,4 +18,6 @@ packages/react-devtools-extensions/chrome/build packages/react-devtools-extensions/firefox/build packages/react-devtools-extensions/shared/build packages/react-devtools-inline/dist -packages/react-devtools-shell/dist \ No newline at end of file +packages/react-devtools-shell/dist +packages/react-devtools-scheduling-profiler/dist +packages/react-devtools-scheduling-profiler/static \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2a70572385..5a913ac6e5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ packages/react-devtools-extensions/firefox/*.pem packages/react-devtools-extensions/shared/build packages/react-devtools-extensions/.tempUserDataDir packages/react-devtools-inline/dist -packages/react-devtools-shell/dist \ No newline at end of file +packages/react-devtools-shell/dist +packages/react-devtools-scheduling-profiler/dist \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index bea24210ec..80650567ef 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,4 +3,6 @@ packages/react-devtools-extensions/chrome/build packages/react-devtools-extensions/firefox/build packages/react-devtools-extensions/shared/build packages/react-devtools-inline/dist -packages/react-devtools-shell/dist \ No newline at end of file +packages/react-devtools-shell/dist +packages/react-devtools-scheduling-profiler/dist +packages/react-devtools-scheduling-profiler/static \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/README.md b/packages/react-devtools-scheduling-profiler/README.md new file mode 100644 index 0000000000..b3e0b07b05 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/README.md @@ -0,0 +1,3 @@ +# Experimental React Concurrent Mode Profiler + +- Deployed at: https://react-scheduling-profiler.vercel.app diff --git a/packages/react-devtools-scheduling-profiler/package.json b/packages/react-devtools-scheduling-profiler/package.json new file mode 100644 index 0000000000..2923f10ff7 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/package.json @@ -0,0 +1,30 @@ +{ + "private": true, + "name": "react-devtools-scheduling-profiler", + "version": "0.0.1", + "license": "MIT", + "scripts": { + "build": "cross-env NODE_ENV=production cross-env TARGET=remote webpack --config webpack.config.js", + "start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open" + }, + "dependencies": { + "@elg/speedscope": "1.9.0-a6f84db", + "clipboard-js": "^0.3.6", + "memoize-one": "^5.1.1", + "nullthrows": "^1.1.1", + "pretty-ms": "^7.0.0", + "react-virtualized-auto-sizer": "^1.0.2", + "regenerator-runtime": "^0.13.7" + }, + "devDependencies": { + "babel-loader": "^8.1.0", + "css-loader": "^4.2.1", + "file-loader": "^6.0.0", + "html-webpack-plugin": "^4.3.0", + "style-loader": "^1.2.1", + "url-loader": "^4.1.0", + "webpack": "^4.44.1", + "webpack-cli": "^3.3.12", + "webpack-dev-server": "^3.11.0" + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/App.js b/packages/react-devtools-scheduling-profiler/src/App.js new file mode 100644 index 0000000000..93f66ac09b --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/App.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactProfilerData} from './types'; + +import * as React from 'react'; +import {useState} from 'react'; + +import ImportPage from './ImportPage'; +import CanvasPage from './CanvasPage'; + +export default function App() { + const [profilerData, setProfilerData] = useState( + null, + ); + + if (profilerData) { + return ; + } else { + return ; + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.css b/packages/react-devtools-scheduling-profiler/src/CanvasPage.css new file mode 100644 index 0000000000..e5d238a0d9 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.css @@ -0,0 +1,7 @@ +.CanvasPage { + position: absolute; + top: 0.5rem; + bottom: 0.5rem; + left: 0.5rem; + right: 0.5rem; +} diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js new file mode 100644 index 0000000000..9d93005f06 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -0,0 +1,480 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Point, + HorizontalPanAndZoomViewOnChangeCallback, +} from './view-base'; +import type { + ReactHoverContextInfo, + ReactProfilerData, + ReactMeasure, +} from './types'; + +import * as React from 'react'; +import { + Fragment, + useEffect, + useLayoutEffect, + useRef, + useState, + useCallback, +} from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import {copy} from 'clipboard-js'; +import prettyMilliseconds from 'pretty-ms'; + +import { + HorizontalPanAndZoomView, + ResizableSplitView, + Surface, + VerticalScrollView, + View, + createComposedLayout, + lastViewTakesUpRemainingSpaceLayout, + useCanvasInteraction, + verticallyStackedLayout, + zeroPoint, +} from './view-base'; +import { + FlamechartView, + ReactEventsView, + ReactMeasuresView, + TimeAxisMarkersView, + UserTimingMarksView, +} from './content-views'; +import {COLORS} from './content-views/constants'; + +import EventTooltip from './EventTooltip'; +import {ContextMenu, ContextMenuItem, useContextMenu} from './context'; +import {getBatchRange} from './utils/getBatchRange'; + +import styles from './CanvasPage.css'; + +const CONTEXT_MENU_ID = 'canvas'; + +type ContextMenuContextData = {| + data: ReactProfilerData, + hoveredEvent: ReactHoverContextInfo | null, +|}; + +type Props = {| + profilerData: ReactProfilerData, +|}; + +function CanvasPage({profilerData}: Props) { + return ( +
+ + {({height, width}: {height: number, width: number}) => ( + + )} + +
+ ); +} + +const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => { + const {batchUID, duration, timestamp, type} = measure; + + const [startTime, stopTime] = getBatchRange(batchUID, data); + + copy( + JSON.stringify({ + type, + timestamp: prettyMilliseconds(timestamp), + duration: prettyMilliseconds(duration), + batchDuration: prettyMilliseconds(stopTime - startTime), + }), + ); +}; + +const zoomToBatch = ( + data: ReactProfilerData, + measure: ReactMeasure, + syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[], +) => { + const {batchUID} = measure; + const [startTime, stopTime] = getBatchRange(batchUID, data); + syncedHorizontalPanAndZoomViews.forEach(syncedView => + // Using time as range works because the views' intrinsic content size is + // based on time. + syncedView.zoomToRange(startTime, stopTime), + ); +}; + +type AutoSizedCanvasProps = {| + data: ReactProfilerData, + height: number, + width: number, +|}; + +function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { + const canvasRef = useRef(null); + + const [isContextMenuShown, setIsContextMenuShown] = useState(false); + const [mouseLocation, setMouseLocation] = useState(zeroPoint); // DOM coordinates + const [ + hoveredEvent, + setHoveredEvent, + ] = useState(null); + + const surfaceRef = useRef(new Surface()); + const userTimingMarksViewRef = useRef(null); + const reactEventsViewRef = useRef(null); + const reactMeasuresViewRef = useRef(null); + const flamechartViewRef = useRef(null); + const syncedHorizontalPanAndZoomViewsRef = useRef( + [], + ); + + useLayoutEffect(() => { + const surface = surfaceRef.current; + const defaultFrame = {origin: zeroPoint, size: {width, height}}; + + // Clear synced views + syncedHorizontalPanAndZoomViewsRef.current = []; + + const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = ( + newState, + triggeringView?: HorizontalPanAndZoomView, + ) => { + syncedHorizontalPanAndZoomViewsRef.current.forEach( + syncedView => + triggeringView !== syncedView && + syncedView.setPanAndZoomState(newState), + ); + }; + + // Top content + + const topContentStack = new View( + surface, + defaultFrame, + verticallyStackedLayout, + ); + + const axisMarkersView = new TimeAxisMarkersView( + surface, + defaultFrame, + data.duration, + ); + topContentStack.addSubview(axisMarkersView); + + if (data.otherUserTimingMarks.length > 0) { + const userTimingMarksView = new UserTimingMarksView( + surface, + defaultFrame, + data.otherUserTimingMarks, + data.duration, + ); + userTimingMarksViewRef.current = userTimingMarksView; + topContentStack.addSubview(userTimingMarksView); + } + + const reactEventsView = new ReactEventsView(surface, defaultFrame, data); + reactEventsViewRef.current = reactEventsView; + topContentStack.addSubview(reactEventsView); + + const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView( + surface, + defaultFrame, + topContentStack, + data.duration, + syncAllHorizontalPanAndZoomViewStates, + ); + syncedHorizontalPanAndZoomViewsRef.current.push( + topContentHorizontalPanAndZoomView, + ); + + // Resizable content + + const reactMeasuresView = new ReactMeasuresView( + surface, + defaultFrame, + data, + ); + reactMeasuresViewRef.current = reactMeasuresView; + const reactMeasuresVerticalScrollView = new VerticalScrollView( + surface, + defaultFrame, + reactMeasuresView, + ); + const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView( + surface, + defaultFrame, + reactMeasuresVerticalScrollView, + data.duration, + syncAllHorizontalPanAndZoomViewStates, + ); + syncedHorizontalPanAndZoomViewsRef.current.push( + reactMeasuresHorizontalPanAndZoomView, + ); + + const flamechartView = new FlamechartView( + surface, + defaultFrame, + data.flamechart, + data.duration, + ); + flamechartViewRef.current = flamechartView; + const flamechartVerticalScrollView = new VerticalScrollView( + surface, + defaultFrame, + flamechartView, + ); + const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView( + surface, + defaultFrame, + flamechartVerticalScrollView, + data.duration, + syncAllHorizontalPanAndZoomViewStates, + ); + syncedHorizontalPanAndZoomViewsRef.current.push( + flamechartHorizontalPanAndZoomView, + ); + + const resizableContentStack = new ResizableSplitView( + surface, + defaultFrame, + reactMeasuresHorizontalPanAndZoomView, + flamechartHorizontalPanAndZoomView, + ); + + const rootView = new View( + surface, + defaultFrame, + createComposedLayout( + verticallyStackedLayout, + lastViewTakesUpRemainingSpaceLayout, + ), + ); + rootView.addSubview(topContentHorizontalPanAndZoomView); + rootView.addSubview(resizableContentStack); + + surfaceRef.current.rootView = rootView; + }, [data]); + + useLayoutEffect(() => { + if (canvasRef.current) { + surfaceRef.current.setCanvas(canvasRef.current, {width, height}); + } + }, [width, height]); + + const interactor = useCallback(interaction => { + if (canvasRef.current === null) { + return; + } + surfaceRef.current.handleInteraction(interaction); + // Defer drawing to canvas until React's commit phase, to avoid drawing + // twice and to ensure that both the canvas and DOM elements managed by + // React are in sync. + setMouseLocation({ + x: interaction.payload.event.x, + y: interaction.payload.event.y, + }); + }, []); + + useCanvasInteraction(canvasRef, interactor); + + useContextMenu({ + data: { + data, + hoveredEvent, + }, + id: CONTEXT_MENU_ID, + onChange: setIsContextMenuShown, + ref: canvasRef, + }); + + useEffect(() => { + const {current: userTimingMarksView} = userTimingMarksViewRef; + if (userTimingMarksView) { + userTimingMarksView.onHover = userTimingMark => { + if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) { + setHoveredEvent({ + userTimingMark, + event: null, + flamechartStackFrame: null, + measure: null, + data, + }); + } + }; + } + + const {current: reactEventsView} = reactEventsViewRef; + if (reactEventsView) { + reactEventsView.onHover = event => { + if (!hoveredEvent || hoveredEvent.event !== event) { + setHoveredEvent({ + userTimingMark: null, + event, + flamechartStackFrame: null, + measure: null, + data, + }); + } + }; + } + + const {current: reactMeasuresView} = reactMeasuresViewRef; + if (reactMeasuresView) { + reactMeasuresView.onHover = measure => { + if (!hoveredEvent || hoveredEvent.measure !== measure) { + setHoveredEvent({ + userTimingMark: null, + event: null, + flamechartStackFrame: null, + measure, + data, + }); + } + }; + } + + const {current: flamechartView} = flamechartViewRef; + if (flamechartView) { + flamechartView.setOnHover(flamechartStackFrame => { + if ( + !hoveredEvent || + hoveredEvent.flamechartStackFrame !== flamechartStackFrame + ) { + setHoveredEvent({ + userTimingMark: null, + event: null, + flamechartStackFrame, + measure: null, + data, + }); + } + }); + } + }, [hoveredEvent]); + + useLayoutEffect(() => { + const {current: userTimingMarksView} = userTimingMarksViewRef; + if (userTimingMarksView) { + userTimingMarksView.setHoveredMark( + hoveredEvent ? hoveredEvent.userTimingMark : null, + ); + } + + const {current: reactEventsView} = reactEventsViewRef; + if (reactEventsView) { + reactEventsView.setHoveredEvent(hoveredEvent ? hoveredEvent.event : null); + } + + const {current: reactMeasuresView} = reactMeasuresViewRef; + if (reactMeasuresView) { + reactMeasuresView.setHoveredMeasure( + hoveredEvent ? hoveredEvent.measure : null, + ); + } + + const {current: flamechartView} = flamechartViewRef; + if (flamechartView) { + flamechartView.setHoveredFlamechartStackFrame( + hoveredEvent ? hoveredEvent.flamechartStackFrame : null, + ); + } + }, [hoveredEvent]); + + // Draw to canvas in React's commit phase + useLayoutEffect(() => { + surfaceRef.current.displayIfNeeded(); + }); + + return ( + + + + {(contextData: ContextMenuContextData) => { + if (contextData.hoveredEvent == null) { + return null; + } + const { + event, + flamechartStackFrame, + measure, + } = contextData.hoveredEvent; + return ( + + {event !== null && ( + copy(event.componentName)} + title="Copy component name"> + Copy component name + + )} + {event !== null && ( + copy(event.componentStack)} + title="Copy component stack"> + Copy component stack + + )} + {measure !== null && ( + + zoomToBatch( + contextData.data, + measure, + syncedHorizontalPanAndZoomViewsRef.current, + ) + } + title="Zoom to batch"> + Zoom to batch + + )} + {measure !== null && ( + copySummary(contextData.data, measure)} + title="Copy summary"> + Copy summary + + )} + {flamechartStackFrame !== null && ( + copy(flamechartStackFrame.scriptUrl)} + title="Copy file path"> + Copy file path + + )} + {flamechartStackFrame !== null && ( + + copy( + `line ${flamechartStackFrame.locationLine ?? + ''}, column ${flamechartStackFrame.locationColumn ?? + ''}`, + ) + } + title="Copy location"> + Copy location + + )} + + ); + }} + + {!isContextMenuShown && ( + + )} + + ); +} + +export default CanvasPage; diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css new file mode 100644 index 0000000000..f721295b2f --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -0,0 +1,78 @@ +.Tooltip { + position: fixed; + display: inline-block; + border-radius: 0.125rem; + max-width: 300px; + padding: 0.25rem; + user-select: none; + pointer-events: none; + background-color: #ffffff; + border: 1px solid #ccc; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); + font-size: 11px; +} + +.Divider { + height: 1px; + background-color: #aaa; + margin: 0.5rem 0; +} + +.DetailsGrid { + display: grid; + padding-top: 5px; + grid-gap: 2px 5px; + grid-template-columns: min-content auto; +} + +.DetailsGridLabel { + color: #666; + text-align: right; +} + +.DetailsGridURL { + word-break: break-all; + max-height: 50vh; + overflow: hidden; +} + +.FlamechartStackFrameName { + word-break: break-word; + margin-left: 0.4rem; +} + +.ComponentName { + font-weight: bold; + word-break: break-word; + margin-right: 0.4rem; +} + +.ComponentStack { + overflow: hidden; + max-width: 35em; + max-height: 10em; + margin: 0; + font-size: 0.9em; + line-height: 1.5; + -webkit-mask-image: linear-gradient( + 180deg, + #fff, + #fff 5em, + transparent + ); + mask-image: linear-gradient( + 180deg, + #fff, + #fff 5em, + transparent + ); + white-space: pre; +} + +.ReactMeasureLabel { + margin-left: 0.4rem; +} + +.UserTimingLabel { + word-break: break-word; +} diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js new file mode 100644 index 0000000000..52fb66d981 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -0,0 +1,292 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Point} from './view-base'; +import type { + FlamechartStackFrame, + ReactEvent, + ReactHoverContextInfo, + ReactMeasure, + ReactProfilerData, + Return, + UserTimingMark, +} from './types'; + +import * as React from 'react'; +import {Fragment, useRef} from 'react'; +import prettyMilliseconds from 'pretty-ms'; +import {COLORS} from './content-views/constants'; +import {getBatchRange} from './utils/getBatchRange'; +import useSmartTooltip from './utils/useSmartTooltip'; +import styles from './EventTooltip.css'; + +type Props = {| + data: ReactProfilerData, + hoveredEvent: ReactHoverContextInfo | null, + origin: Point, +|}; + +function formatTimestamp(ms) { + return ms.toLocaleString(undefined, {minimumFractionDigits: 2}) + 'ms'; +} + +function formatDuration(ms) { + return prettyMilliseconds(ms, {millisecondsDecimalDigits: 3}); +} + +function trimmedString(string: string, length: number): string { + if (string.length > length) { + return `${string.substr(0, length - 1)}…`; + } + return string; +} + +function getReactEventLabel(type): string | null { + switch (type) { + case 'schedule-render': + return 'render scheduled'; + case 'schedule-state-update': + return 'state update scheduled'; + case 'schedule-force-update': + return 'force update scheduled'; + case 'suspense-suspend': + return 'suspended'; + case 'suspense-resolved': + return 'suspense resolved'; + case 'suspense-rejected': + return 'suspense rejected'; + default: + return null; + } +} + +function getReactEventColor(event: ReactEvent): string | null { + switch (event.type) { + case 'schedule-render': + return COLORS.REACT_SCHEDULE_HOVER; + case 'schedule-state-update': + case 'schedule-force-update': + return event.isCascading + ? COLORS.REACT_SCHEDULE_CASCADING_HOVER + : COLORS.REACT_SCHEDULE_HOVER; + case 'suspense-suspend': + case 'suspense-resolved': + case 'suspense-rejected': + return COLORS.REACT_SUSPEND_HOVER; + default: + return null; + } +} + +function getReactMeasureLabel(type): string | null { + switch (type) { + case 'commit': + return 'commit'; + case 'render-idle': + return 'idle'; + case 'render': + return 'render'; + case 'layout-effects': + return 'layout effects'; + case 'passive-effects': + return 'passive effects'; + default: + return null; + } +} + +export default function EventTooltip({data, hoveredEvent, origin}: Props) { + const tooltipRef = useSmartTooltip({ + mouseX: origin.x, + mouseY: origin.y, + }); + + if (hoveredEvent === null) { + return null; + } + + const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent; + + if (event !== null) { + return ; + } else if (measure !== null) { + return ( + + ); + } else if (flamechartStackFrame !== null) { + return ( + + ); + } else if (userTimingMark !== null) { + return ( + + ); + } + return null; +} + +function formatComponentStack(componentStack: string): string { + const lines = componentStack.split('\n').map(line => line.trim()); + lines.shift(); + + if (lines.length > 5) { + return lines.slice(0, 5).join('\n') + '\n...'; + } + return lines.join('\n'); +} + +const TooltipFlamechartNode = ({ + stackFrame, + tooltipRef, +}: { + stackFrame: FlamechartStackFrame, + tooltipRef: Return, +}) => { + const { + name, + timestamp, + duration, + scriptUrl, + locationLine, + locationColumn, + } = stackFrame; + return ( +
+ {formatDuration(duration)} + {name} +
+
Timestamp:
+
{formatTimestamp(timestamp)}
+ {scriptUrl && ( + <> +
Script URL:
+
{scriptUrl}
+ + )} + {(locationLine !== undefined || locationColumn !== undefined) && ( + <> +
Location:
+
+ line {locationLine}, column {locationColumn} +
+ + )} +
+
+ ); +}; + +const TooltipReactEvent = ({ + event, + tooltipRef, +}: { + event: ReactEvent, + tooltipRef: Return, +}) => { + const label = getReactEventLabel(event.type); + const color = getReactEventColor(event); + if (!label || !color) { + if (__DEV__) { + console.warn('Unexpected event type "%s"', event.type); + } + return null; + } + + const {componentName, componentStack, timestamp} = event; + + return ( +
+ {componentName && ( + + {trimmedString(componentName, 768)} + + )} + {label} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+ {componentStack && ( + +
Component stack:
+
+              {formatComponentStack(componentStack)}
+            
+
+ )} +
+
+ ); +}; + +const TooltipReactMeasure = ({ + data, + measure, + tooltipRef, +}: { + data: ReactProfilerData, + measure: ReactMeasure, + tooltipRef: Return, +}) => { + const label = getReactMeasureLabel(measure.type); + if (!label) { + if (__DEV__) { + console.warn('Unexpected measure type "%s"', measure.type); + } + return null; + } + + const {batchUID, duration, timestamp, lanes} = measure; + const [startTime, stopTime] = getBatchRange(batchUID, data); + + return ( +
+ {formatDuration(duration)} + {label} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
Batch duration:
+
{formatDuration(stopTime - startTime)}
+
+ Lane{lanes.length === 1 ? '' : 's'}: +
+
{lanes.join(', ')}
+
+
+ ); +}; + +const TooltipUserTimingMark = ({ + mark, + tooltipRef, +}: { + mark: UserTimingMark, + tooltipRef: Return, +}) => { + const {name, timestamp} = mark; + return ( +
+ {name} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
+
+ ); +}; diff --git a/packages/react-devtools-scheduling-profiler/src/ImportPage.css b/packages/react-devtools-scheduling-profiler/src/ImportPage.css new file mode 100644 index 0000000000..0bfcd43892 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/ImportPage.css @@ -0,0 +1,206 @@ +.App { + background-color: #282c34; + text-align: center; +} + +.container { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + font-size: calc(10px + 1.5vmin); + min-height: 100vh; +} + +.link { + color: #282c34; + transition: 0.2s; + font-size: calc(10px + 1.5vmin); +} +.link:hover { + color: #61dafb; + transition: 0.2s; + font-size: calc(10px + 1.5vmin); +} + +kbd { + display: inline-block; + padding: 0 0.5em; + border: 1px solid #d7dfe4; + margin: 0 0.2em; + background-color: #f6f6f6; + border-radius: 0.2em; +} + +/* Landing Graphic */ +.browserScreenshot { + width: 35rem; + max-width: inherit; + align-self: center; + justify-content: center; + border: 1px solid #d7dfe4; + border-radius: 0.4em; + box-shadow: 0 2px 4px #ddd; +} + +.legendKey { + font-size: calc(8px + 1.5vmin); + margin: 0; + margin-bottom: 1rem; +} + +.legendKey > svg { + padding-left: 2rem; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +hr { + margin-top: 0px; + margin-left: 4px; + width: 80%; +} + +.row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; +} + +.column { + padding: 1rem; + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; +} + +.columncontent { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + text-align: left; +} + +/* Card Styling */ +.card { + background-color: white; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); + border-radius: 5px; + transition: 0.3s; + width: 80%; + color: black; +} + +.card:hover { + box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); +} + +.cardcontainer { + padding: 40px 16px; +} + +.inputbtn { + display: none; +} + +.buttongrp { + float: left; /* Float the buttons side by side */ +} + +/* Import Button Styling */ +.ImportButton { + background-color: #61dafb; + border: none; + color: #ffffff; + text-align: center; + font-size: 28px; + padding: 20px; + width: 200px; + transition: all 0.3s; + cursor: pointer; + margin: 5px; +} + +.ImportButton span { + cursor: pointer; + display: inline-block; + position: relative; + transition: 0.3s; +} + +.ImportButton span:after { + content: '\00bb'; + position: absolute; + opacity: 0; + top: 0; + right: -20px; + transition: 0.3s; +} + +.ImportButton:hover { + background-color: white; + color: black; +} + +.ImportButton:hover span { + padding-right: 25px; +} + +.ImportButton:hover span:after { + opacity: 1; + right: 0; +} + +/* ViewSource Button styling */ +.ViewSourceButton { + background-color: transparent; + color: black; + border: none; + text-align: center; + font-size: 28px; + padding: 20px; + width: 200px; + transition: all 0.3s; + cursor: pointer; + margin: 5px; +} + +.ViewSourceButton span { + cursor: pointer; + display: inline-block; + position: relative; + transition: 0.3s; +} + +.ViewSourceButton span:after { + content: '\00bb'; + position: absolute; + opacity: 0; + top: 0; + right: -20px; + transition: 0.3s; +} + +.ViewSourceButton:hover { + background-color: white; + color: black; +} + +.ViewSourceButton:hover span { + padding-right: 25px; +} + +.ViewSourceButton:hover span:after { + opacity: 1; + right: 0; +} diff --git a/packages/react-devtools-scheduling-profiler/src/ImportPage.js b/packages/react-devtools-scheduling-profiler/src/ImportPage.js new file mode 100644 index 0000000000..93a02ba5e3 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/ImportPage.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {TimelineEvent} from '@elg/speedscope'; +import type {ReactProfilerData} from './types'; + +import * as React from 'react'; +import {useCallback, useRef} from 'react'; + +import profilerBrowser from './assets/profilerBrowser.png'; +import style from './ImportPage.css'; + +import preprocessData from './utils/preprocessData'; +import {readInputData} from './utils/readInputData'; + +type Props = {| + onDataImported: (profilerData: ReactProfilerData) => void, +|}; + +export default function ImportPage({onDataImported}: Props) { + const processTimeline = useCallback( + (events: TimelineEvent[]) => { + if (events.length > 0) { + onDataImported(preprocessData(events)); + } + }, + [onDataImported], + ); + + const handleProfilerInput = useCallback( + async (event: SyntheticInputEvent) => { + const readFile = await readInputData(event.target.files[0]); + processTimeline(JSON.parse(readFile)); + }, + [processTimeline], + ); + + const upload = useRef(null); + + return ( +
+
+
+
+
+
+ logo +
+
+

React Concurrent Mode Profiler

+
+

+ Import a captured{' '} + + performance profile + {' '} + from Chrome Devtools. +
+ To zoom, scroll while holding down Ctrl or{' '} + Shift +

+

+ + + + State Update Scheduled +
+ + + + State Update Scheduled +
+ + + + Suspended +

+ +
+ + + + +
+
+
+
+
+
+
+ ); +} diff --git a/packages/react-devtools-scheduling-profiler/src/assets/logo.svg b/packages/react-devtools-scheduling-profiler/src/assets/logo.svg new file mode 100644 index 0000000000..2e5df0d3ab --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png b/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png new file mode 100644 index 0000000000..b0282be2f6 Binary files /dev/null and b/packages/react-devtools-scheduling-profiler/src/assets/profilerBrowser.png differ diff --git a/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg b/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg new file mode 100644 index 0000000000..6b60c1042f --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/assets/reactlogo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/react-devtools-scheduling-profiler/src/constants.js b/packages/react-devtools-scheduling-profiler/src/constants.js new file mode 100644 index 0000000000..77f8964e82 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/constants.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +// App constants + +export const REACT_TOTAL_NUM_LANES = 31; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js new file mode 100644 index 0000000000..00b022899f --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -0,0 +1,378 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Flamechart, + FlamechartStackFrame, + FlamechartStackLayer, +} from '../types'; +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; + +import { + ColorView, + Surface, + View, + layeredLayout, + rectContainsPoint, + rectEqualToRect, + intersectionOfRects, + rectIntersectsRect, + verticallyStackedLayout, +} from '../view-base'; +import { + durationToWidth, + positioningScaleFactor, + timestampToPosition, +} from './utils/positioning'; +import { + COLORS, + FLAMECHART_FONT_SIZE, + FLAMECHART_FRAME_HEIGHT, + FLAMECHART_TEXT_PADDING, + COLOR_HOVER_DIM_DELTA, + BORDER_SIZE, +} from './constants'; +import {ColorGenerator, dimmedColor, hslaColorToString} from './utils/colors'; + +// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399 +const colorGenerator = new ColorGenerator( + {min: 30, max: 330}, + {min: 50, max: 80, count: 3}, + 85, +); +colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100}); + +function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) { + return colorGenerator.colorForID(scriptUrl ?? ''); +} + +function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string { + const color = defaultHslaColorForStackFrame(stackFrame); + return hslaColorToString(color); +} + +function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string { + const color = dimmedColor( + defaultHslaColorForStackFrame(stackFrame), + COLOR_HOVER_DIM_DELTA, + ); + return hslaColorToString(color); +} + +const cachedFlamechartTextWidths = new Map(); +const trimFlamechartText = ( + context: CanvasRenderingContext2D, + text: string, + width: number, +) => { + for (let i = text.length - 1; i >= 0; i--) { + const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…'; + + let measuredWidth = cachedFlamechartTextWidths.get(trimmedText); + if (measuredWidth == null) { + measuredWidth = context.measureText(trimmedText).width; + cachedFlamechartTextWidths.set(trimmedText, measuredWidth); + } + + if (measuredWidth <= width) { + return trimmedText; + } + } + + return null; +}; + +class FlamechartStackLayerView extends View { + /** Layer to display */ + _stackLayer: FlamechartStackLayer; + + /** A set of `stackLayer`'s frames, for efficient lookup. */ + _stackFrameSet: Set; + + _intrinsicSize: Size; + + _hoveredStackFrame: FlamechartStackFrame | null = null; + _onHover: ((node: FlamechartStackFrame | null) => void) | null = null; + + constructor( + surface: Surface, + frame: Rect, + stackLayer: FlamechartStackLayer, + duration: number, + ) { + super(surface, frame); + this._stackLayer = stackLayer; + this._stackFrameSet = new Set(stackLayer); + this._intrinsicSize = { + width: duration, + height: FLAMECHART_FRAME_HEIGHT, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredFlamechartStackFrame( + hoveredStackFrame: FlamechartStackFrame | null, + ) { + if (this._hoveredStackFrame === hoveredStackFrame) { + return; // We're already hovering over this frame + } + + // Only care about frames displayed by this view. + const stackFrameToSet = + hoveredStackFrame && this._stackFrameSet.has(hoveredStackFrame) + ? hoveredStackFrame + : null; + if (this._hoveredStackFrame === stackFrameToSet) { + return; // Resulting state is unchanged + } + this._hoveredStackFrame = stackFrameToSet; + this.setNeedsDisplay(); + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _stackLayer, + _hoveredStackFrame, + _intrinsicSize, + visibleArea, + } = this; + + context.fillStyle = COLORS.BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + context.textAlign = 'left'; + context.textBaseline = 'middle'; + context.font = `${FLAMECHART_FONT_SIZE}px sans-serif`; + + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); + + for (let i = 0; i < _stackLayer.length; i++) { + const stackFrame = _stackLayer[i]; + const {name, timestamp, duration} = stackFrame; + + const width = durationToWidth(duration, scaleFactor); + if (width < 1) { + continue; // Too small to render at this zoom level + } + + const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); + const nodeRect: Rect = { + origin: {x, y: frame.origin.y}, + size: { + width: Math.floor(width - BORDER_SIZE), + height: Math.floor(FLAMECHART_FRAME_HEIGHT - BORDER_SIZE), + }, + }; + if (!rectIntersectsRect(nodeRect, visibleArea)) { + continue; // Not in view + } + + const showHoverHighlight = _hoveredStackFrame === _stackLayer[i]; + context.fillStyle = showHoverHighlight + ? hoverColorForStackFrame(stackFrame) + : defaultColorForStackFrame(stackFrame); + + const drawableRect = intersectionOfRects(nodeRect, visibleArea); + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + + if (width > FLAMECHART_TEXT_PADDING * 2) { + const trimmedName = trimFlamechartText( + context, + name, + width - FLAMECHART_TEXT_PADDING * 2 + (x < 0 ? x : 0), + ); + + if (trimmedName !== null) { + context.fillStyle = COLORS.PRIORITY_LABEL; + + // Prevent text from being drawn outside `viewableArea` + const textOverflowsViewableArea = !rectEqualToRect( + drawableRect, + nodeRect, + ); + if (textOverflowsViewableArea) { + context.save(); + context.beginPath(); + context.rect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + context.closePath(); + context.clip(); + } + + context.fillText( + trimmedName, + nodeRect.origin.x + FLAMECHART_TEXT_PADDING - (x < 0 ? x : 0), + nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2, + ); + + if (textOverflowsViewableArea) { + context.restore(); + } + } + } + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction) { + const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this; + const {location} = interaction.payload; + if (!_onHover || !rectContainsPoint(location, visibleArea)) { + return; + } + + // Find the node being hovered over. + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); + let startIndex = 0; + let stopIndex = _stackLayer.length - 1; + while (startIndex <= stopIndex) { + const currentIndex = Math.floor((startIndex + stopIndex) / 2); + const flamechartStackFrame = _stackLayer[currentIndex]; + const {timestamp, duration} = flamechartStackFrame; + + const width = durationToWidth(duration, scaleFactor); + const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); + if (x <= location.x && x + width >= location.x) { + _onHover(flamechartStackFrame); + return; + } + + if (x > location.x) { + stopIndex = currentIndex - 1; + } else { + startIndex = currentIndex + 1; + } + } + + _onHover(null); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction); + break; + } + } +} + +export class FlamechartView extends View { + _flamechartRowViews: FlamechartStackLayerView[] = []; + + /** Container view that vertically stacks flamechart rows */ + _verticalStackView: View; + + _hoveredStackFrame: FlamechartStackFrame | null = null; + _onHover: ((node: FlamechartStackFrame | null) => void) | null = null; + + constructor( + surface: Surface, + frame: Rect, + flamechart: Flamechart, + duration: number, + ) { + super(surface, frame, layeredLayout); + this.setDataAndUpdateSubviews(flamechart, duration); + } + + setDataAndUpdateSubviews(flamechart: Flamechart, duration: number) { + const {surface, frame, _onHover, _hoveredStackFrame} = this; + + // Clear existing rows on data update + if (this._verticalStackView) { + this.removeAllSubviews(); + this._flamechartRowViews = []; + } + + this._verticalStackView = new View(surface, frame, verticallyStackedLayout); + this._flamechartRowViews = flamechart.map(stackLayer => { + const rowView = new FlamechartStackLayerView( + surface, + frame, + stackLayer, + duration, + ); + this._verticalStackView.addSubview(rowView); + + // Update states + rowView._onHover = _onHover; + rowView.setHoveredFlamechartStackFrame(_hoveredStackFrame); + return rowView; + }); + + // Add a plain background view to prevent gaps from appearing between + // flamechartRowViews. + const colorView = new ColorView(surface, frame, COLORS.BACKGROUND); + this.addSubview(colorView); + this.addSubview(this._verticalStackView); + } + + setHoveredFlamechartStackFrame( + hoveredStackFrame: FlamechartStackFrame | null, + ) { + this._hoveredStackFrame = hoveredStackFrame; + this._flamechartRowViews.forEach(rowView => + rowView.setHoveredFlamechartStackFrame(hoveredStackFrame), + ); + } + + setOnHover(onHover: (node: FlamechartStackFrame | null) => void) { + this._onHover = onHover; + this._flamechartRowViews.forEach(rowView => (rowView._onHover = onHover)); + } + + desiredSize() { + // Ignore the wishes of the background color view + return this._verticalStackView.desiredSize(); + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction) { + const {_onHover, visibleArea} = this; + if (!_onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + // Clear out any hovered flamechart stack frame + _onHover(null); + } + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js new file mode 100644 index 0000000000..fb60e54eb4 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js @@ -0,0 +1,276 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactEvent, ReactProfilerData} from '../types'; +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; + +import { + positioningScaleFactor, + timestampToPosition, + positionToTimestamp, + widthToDuration, +} from './utils/positioning'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + COLORS, + EVENT_ROW_PADDING, + EVENT_DIAMETER, + BORDER_SIZE, +} from './constants'; + +const EVENT_ROW_HEIGHT_FIXED = + EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING; + +function isSuspenseEvent(event: ReactEvent): boolean %checks { + return ( + event.type === 'suspense-suspend' || + event.type === 'suspense-resolved' || + event.type === 'suspense-rejected' + ); +} + +export class ReactEventsView extends View { + _profilerData: ReactProfilerData; + _intrinsicSize: Size; + + _hoveredEvent: ReactEvent | null = null; + onHover: ((event: ReactEvent | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + this._profilerData = profilerData; + + this._intrinsicSize = { + width: this._profilerData.duration, + height: EVENT_ROW_HEIGHT_FIXED, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredEvent(hoveredEvent: ReactEvent | null) { + if (this._hoveredEvent === hoveredEvent) { + return; + } + this._hoveredEvent = hoveredEvent; + this.setNeedsDisplay(); + } + + /** + * Draw a single `ReactEvent` as a circle in the canvas. + */ + _drawSingleReactEvent( + context: CanvasRenderingContext2D, + rect: Rect, + event: ReactEvent, + baseY: number, + scaleFactor: number, + showHoverHighlight: boolean, + ) { + const {frame} = this; + const {timestamp, type} = event; + + const x = timestampToPosition(timestamp, scaleFactor, frame); + const radius = EVENT_DIAMETER / 2; + const eventRect: Rect = { + origin: { + x: x - radius, + y: baseY, + }, + size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER}, + }; + if (!rectIntersectsRect(eventRect, rect)) { + return; // Not in view + } + + let fillStyle = null; + + switch (type) { + case 'schedule-render': + case 'schedule-state-update': + case 'schedule-force-update': + if (event.isCascading) { + fillStyle = showHoverHighlight + ? COLORS.REACT_SCHEDULE_CASCADING_HOVER + : COLORS.REACT_SCHEDULE_CASCADING; + } else { + fillStyle = showHoverHighlight + ? COLORS.REACT_SCHEDULE_HOVER + : COLORS.REACT_SCHEDULE; + } + break; + case 'suspense-suspend': + case 'suspense-resolved': + case 'suspense-rejected': + fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPEND_HOVER + : COLORS.REACT_SUSPEND; + break; + default: + if (__DEV__) { + console.warn('Unexpected event type "%s"', type); + } + break; + } + + if (fillStyle !== null) { + const y = eventRect.origin.y + radius; + + context.beginPath(); + context.fillStyle = fillStyle; + context.arc(x, y, radius, 0, 2 * Math.PI); + context.fill(); + } + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {events}, + _hoveredEvent, + visibleArea, + } = this; + + context.fillStyle = COLORS.BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + // Draw events + const baseY = frame.origin.y + EVENT_ROW_PADDING; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + const highlightedEvents: ReactEvent[] = []; + + events.forEach(event => { + if ( + event === _hoveredEvent || + (_hoveredEvent && + isSuspenseEvent(event) && + isSuspenseEvent(_hoveredEvent) && + event.id === _hoveredEvent.id) + ) { + highlightedEvents.push(event); + return; + } + this._drawSingleReactEvent( + context, + visibleArea, + event, + baseY, + scaleFactor, + false, + ); + }); + + // Draw the highlighted items on top so they stand out. + // This is helpful if there are multiple (overlapping) items close to each other. + highlightedEvents.forEach(event => { + this._drawSingleReactEvent( + context, + visibleArea, + event, + baseY, + scaleFactor, + true, + ); + }); + + // Render bottom border. + // Propose border rect, check if intersects with `rect`, draw intersection. + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE, + }, + size: { + width: frame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea); + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction) { + const {frame, onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + const { + _profilerData: {events}, + } = this; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + const eventTimestampAllowance = widthToDuration( + EVENT_DIAMETER / 2, + scaleFactor, + ); + + // Because data ranges may overlap, we want to find the last intersecting item. + // This will always be the one on "top" (the one the user is hovering over). + for (let index = events.length - 1; index >= 0; index--) { + const event = events[index]; + const {timestamp} = event; + + if ( + timestamp - eventTimestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + eventTimestampAllowance + ) { + onHover(event); + return; + } + } + + onHover(null); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js new file mode 100644 index 0000000000..46fbe51bb8 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -0,0 +1,318 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactLane, ReactMeasure, ReactProfilerData} from '../types'; +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; + +import { + durationToWidth, + positioningScaleFactor, + positionToTimestamp, + timestampToPosition, +} from './utils/positioning'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; + +import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants'; +import {REACT_TOTAL_NUM_LANES} from '../constants'; + +const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE; + +function getMeasuresForLane( + allMeasures: ReactMeasure[], + lane: ReactLane, +): ReactMeasure[] { + return allMeasures.filter(measure => measure.lanes.includes(lane)); +} + +export class ReactMeasuresView extends View { + _profilerData: ReactProfilerData; + _intrinsicSize: Size; + + _lanesToRender: ReactLane[]; + _laneToMeasures: Map; + + _hoveredMeasure: ReactMeasure | null = null; + onHover: ((measure: ReactMeasure | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + this._profilerData = profilerData; + this._performPreflightComputations(); + } + + _performPreflightComputations() { + this._lanesToRender = []; + this._laneToMeasures = new Map(); + + for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) { + const measuresForLane = getMeasuresForLane( + this._profilerData.measures, + lane, + ); + // Only show lanes with measures + if (measuresForLane.length) { + this._lanesToRender.push(lane); + this._laneToMeasures.set(lane, measuresForLane); + } + } + + this._intrinsicSize = { + width: this._profilerData.duration, + height: this._lanesToRender.length * REACT_LANE_HEIGHT, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredMeasure(hoveredMeasure: ReactMeasure | null) { + if (this._hoveredMeasure === hoveredMeasure) { + return; + } + this._hoveredMeasure = hoveredMeasure; + this.setNeedsDisplay(); + } + + /** + * Draw a single `ReactMeasure` as a bar in the canvas. + */ + _drawSingleReactMeasure( + context: CanvasRenderingContext2D, + rect: Rect, + measure: ReactMeasure, + baseY: number, + scaleFactor: number, + showGroupHighlight: boolean, + showHoverHighlight: boolean, + ) { + const {frame} = this; + const {timestamp, type, duration} = measure; + + let fillStyle = null; + let hoveredFillStyle = null; + let groupSelectedFillStyle = null; + + // We could change the max to 0 and just skip over rendering anything that small, + // but this has the effect of making the chart look very empty when zoomed out. + // So long as perf is okay- it might be best to err on the side of showing things. + const width = durationToWidth(duration, scaleFactor); + if (width <= 0) { + return; // Too small to render at this zoom level + } + + const x = timestampToPosition(timestamp, scaleFactor, frame); + const measureRect: Rect = { + origin: {x, y: baseY}, + size: {width, height: REACT_MEASURE_HEIGHT}, + }; + if (!rectIntersectsRect(measureRect, rect)) { + return; // Not in view + } + + switch (type) { + case 'commit': + fillStyle = COLORS.REACT_COMMIT; + hoveredFillStyle = COLORS.REACT_COMMIT_HOVER; + groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED; + break; + case 'render-idle': + // We could render idle time as diagonal hashes. + // This looks nicer when zoomed in, but not so nice when zoomed out. + // color = context.createPattern(getIdlePattern(), 'repeat'); + fillStyle = COLORS.REACT_IDLE; + hoveredFillStyle = COLORS.REACT_IDLE_HOVER; + groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED; + break; + case 'render': + fillStyle = COLORS.REACT_RENDER; + hoveredFillStyle = COLORS.REACT_RENDER_HOVER; + groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED; + break; + case 'layout-effects': + fillStyle = COLORS.REACT_LAYOUT_EFFECTS; + hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER; + groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED; + break; + case 'passive-effects': + fillStyle = COLORS.REACT_PASSIVE_EFFECTS; + hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER; + groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED; + break; + default: + throw new Error(`Unexpected measure type "${type}"`); + } + + const drawableRect = intersectionOfRects(measureRect, rect); + context.fillStyle = showHoverHighlight + ? hoveredFillStyle + : showGroupHighlight + ? groupSelectedFillStyle + : fillStyle; + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _hoveredMeasure, + _lanesToRender, + _laneToMeasures, + visibleArea, + } = this; + + context.fillStyle = COLORS.PRIORITY_BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + for (let i = 0; i < _lanesToRender.length; i++) { + const lane = _lanesToRender[i]; + const baseY = frame.origin.y + i * REACT_LANE_HEIGHT; + const measuresForLane = _laneToMeasures.get(lane); + + if (!measuresForLane) { + throw new Error( + 'No measures found for a React lane! This is a bug in this profiler tool. Please file an issue.', + ); + } + + // Draw measures + for (let j = 0; j < measuresForLane.length; j++) { + const measure = measuresForLane[j]; + const showHoverHighlight = _hoveredMeasure === measure; + const showGroupHighlight = + !!_hoveredMeasure && _hoveredMeasure.batchUID === measure.batchUID; + + this._drawSingleReactMeasure( + context, + visibleArea, + measure, + baseY, + scaleFactor, + showGroupHighlight, + showHoverHighlight, + ); + } + + // Render bottom border + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + (i + 1) * REACT_LANE_HEIGHT - BORDER_SIZE, + }, + size: { + width: frame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects( + borderFrame, + visibleArea, + ); + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction) { + const { + frame, + _intrinsicSize, + _lanesToRender, + _laneToMeasures, + onHover, + visibleArea, + } = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + // Identify the lane being hovered over + const adjustedCanvasMouseY = location.y - frame.origin.y; + const renderedLaneIndex = Math.floor( + adjustedCanvasMouseY / REACT_LANE_HEIGHT, + ); + if (renderedLaneIndex < 0 || renderedLaneIndex >= _lanesToRender.length) { + onHover(null); + return; + } + const lane = _lanesToRender[renderedLaneIndex]; + + // Find the measure in `lane` being hovered over. + // + // Because data ranges may overlap, we want to find the last intersecting item. + // This will always be the one on "top" (the one the user is hovering over). + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + const measures = _laneToMeasures.get(lane); + if (!measures) { + onHover(null); + return; + } + + for (let index = measures.length - 1; index >= 0; index--) { + const measure = measures[index]; + const {duration, timestamp} = measure; + + if ( + hoverTimestamp >= timestamp && + hoverTimestamp <= timestamp + duration + ) { + onHover(measure); + return; + } + } + + onHover(null); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js b/packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js new file mode 100644 index 0000000000..4f55494282 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/TimeAxisMarkersView.js @@ -0,0 +1,163 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Rect, Size} from '../view-base'; + +import { + durationToWidth, + positioningScaleFactor, + positionToTimestamp, + timestampToPosition, +} from './utils/positioning'; +import { + View, + Surface, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + COLORS, + INTERVAL_TIMES, + LABEL_SIZE, + MARKER_FONT_SIZE, + MARKER_HEIGHT, + MARKER_TEXT_PADDING, + MARKER_TICK_HEIGHT, + MIN_INTERVAL_SIZE_PX, + BORDER_SIZE, +} from './constants'; + +const HEADER_HEIGHT_FIXED = MARKER_HEIGHT + BORDER_SIZE; +const LABEL_FIXED_WIDTH = LABEL_SIZE + BORDER_SIZE; + +export class TimeAxisMarkersView extends View { + _totalDuration: number; + _intrinsicSize: Size; + + constructor(surface: Surface, frame: Rect, totalDuration: number) { + super(surface, frame); + this._totalDuration = totalDuration; + this._intrinsicSize = { + width: this._totalDuration, + height: HEADER_HEIGHT_FIXED, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + // Time mark intervals vary based on the current zoom range and the time it represents. + // In Chrome, these seem to range from 70-140 pixels wide. + // Time wise, they represent intervals of e.g. 1s, 500ms, 200ms, 100ms, 50ms, 20ms. + // Based on zoom, we should determine which amount to actually show. + _getTimeTickInterval(scaleFactor: number): number { + for (let i = 0; i < INTERVAL_TIMES.length; i++) { + const currentInterval = INTERVAL_TIMES[i]; + const intervalWidth = durationToWidth(currentInterval, scaleFactor); + if (intervalWidth > MIN_INTERVAL_SIZE_PX) { + return currentInterval; + } + } + return INTERVAL_TIMES[0]; + } + + draw(context: CanvasRenderingContext2D) { + const {frame, _intrinsicSize, visibleArea} = this; + const clippedFrame = { + origin: frame.origin, + size: { + width: frame.size.width, + height: _intrinsicSize.height, + }, + }; + const drawableRect = intersectionOfRects(clippedFrame, visibleArea); + + // Clear background + context.fillStyle = COLORS.BACKGROUND; + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + + const scaleFactor = positioningScaleFactor( + _intrinsicSize.width, + clippedFrame, + ); + const interval = this._getTimeTickInterval(scaleFactor); + const firstIntervalTimestamp = + Math.ceil( + positionToTimestamp( + drawableRect.origin.x - LABEL_FIXED_WIDTH, + scaleFactor, + clippedFrame, + ) / interval, + ) * interval; + + for ( + let markerTimestamp = firstIntervalTimestamp; + true; + markerTimestamp += interval + ) { + if (markerTimestamp <= 0) { + continue; // Timestamps < are probably a bug; markers at 0 are ugly. + } + + const x = timestampToPosition(markerTimestamp, scaleFactor, clippedFrame); + if (x > drawableRect.origin.x + drawableRect.size.width) { + break; // Not in view + } + + const markerLabel = Math.round(markerTimestamp); + + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + x, + drawableRect.origin.y + MARKER_HEIGHT - MARKER_TICK_HEIGHT, + BORDER_SIZE, + MARKER_TICK_HEIGHT, + ); + + context.fillStyle = COLORS.TIME_MARKER_LABEL; + context.textAlign = 'right'; + context.textBaseline = 'middle'; + context.font = `${MARKER_FONT_SIZE}px sans-serif`; + context.fillText( + `${markerLabel}ms`, + x - MARKER_TEXT_PADDING, + MARKER_HEIGHT / 2, + ); + } + + // Render bottom border. + // Propose border rect, check if intersects with `rect`, draw intersection. + const borderFrame: Rect = { + origin: { + x: clippedFrame.origin.x, + y: clippedFrame.origin.y + clippedFrame.size.height - BORDER_SIZE, + }, + size: { + width: clippedFrame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea); + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js new file mode 100644 index 0000000000..1078ecc26b --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js @@ -0,0 +1,230 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {UserTimingMark} from '../types'; +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; + +import { + positioningScaleFactor, + timestampToPosition, + positionToTimestamp, + widthToDuration, +} from './utils/positioning'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + COLORS, + EVENT_ROW_PADDING, + EVENT_DIAMETER, + BORDER_SIZE, +} from './constants'; + +const ROW_HEIGHT_FIXED = EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING; + +export class UserTimingMarksView extends View { + _marks: UserTimingMark[]; + _intrinsicSize: Size; + + _hoveredMark: UserTimingMark | null = null; + onHover: ((mark: UserTimingMark | null) => void) | null = null; + + constructor( + surface: Surface, + frame: Rect, + marks: UserTimingMark[], + duration: number, + ) { + super(surface, frame); + this._marks = marks; + + this._intrinsicSize = { + width: duration, + height: ROW_HEIGHT_FIXED, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredMark(hoveredMark: UserTimingMark | null) { + if (this._hoveredMark === hoveredMark) { + return; + } + this._hoveredMark = hoveredMark; + this.setNeedsDisplay(); + } + + /** + * Draw a single `UserTimingMark` as a circle in the canvas. + */ + _drawSingleMark( + context: CanvasRenderingContext2D, + rect: Rect, + mark: UserTimingMark, + baseY: number, + scaleFactor: number, + showHoverHighlight: boolean, + ) { + const {frame} = this; + const {timestamp} = mark; + + const x = timestampToPosition(timestamp, scaleFactor, frame); + const radius = EVENT_DIAMETER / 2; + const markRect: Rect = { + origin: { + x: x - radius, + y: baseY, + }, + size: {width: EVENT_DIAMETER, height: EVENT_DIAMETER}, + }; + if (!rectIntersectsRect(markRect, rect)) { + return; // Not in view + } + + const fillStyle = showHoverHighlight + ? COLORS.USER_TIMING_HOVER + : COLORS.USER_TIMING; + + if (fillStyle !== null) { + const y = markRect.origin.y + radius; + + context.beginPath(); + context.fillStyle = fillStyle; + context.arc(x, y, radius, 0, 2 * Math.PI); + context.fill(); + } + } + + draw(context: CanvasRenderingContext2D) { + const {frame, _marks, _hoveredMark, visibleArea} = this; + + context.fillStyle = COLORS.BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + // Draw marks + const baseY = frame.origin.y + EVENT_ROW_PADDING; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + _marks.forEach(mark => { + if (mark === _hoveredMark) { + return; + } + this._drawSingleMark( + context, + visibleArea, + mark, + baseY, + scaleFactor, + false, + ); + }); + + // Draw the hovered and/or selected items on top so they stand out. + // This is helpful if there are multiple (overlapping) items close to each other. + if (_hoveredMark !== null) { + this._drawSingleMark( + context, + visibleArea, + _hoveredMark, + baseY, + scaleFactor, + true, + ); + } + + // Render bottom border. + // Propose border rect, check if intersects with `rect`, draw intersection. + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + ROW_HEIGHT_FIXED - BORDER_SIZE, + }, + size: { + width: frame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea); + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction) { + const {frame, onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + const {_marks} = this; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + const markTimestampAllowance = widthToDuration( + EVENT_DIAMETER / 2, + scaleFactor, + ); + + // Because data ranges may overlap, we want to find the last intersecting item. + // This will always be the one on "top" (the one the user is hovering over). + for (let index = _marks.length - 1; index >= 0; index--) { + const mark = _marks[index]; + const {timestamp} = mark; + + if ( + timestamp - markTimestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + markTimestampAllowance + ) { + onHover(mark); + return; + } + } + + onHover(null); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js new file mode 100644 index 0000000000..468f223be2 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export const LABEL_SIZE = 80; +export const LABEL_FONT_SIZE = 11; +export const MARKER_HEIGHT = 20; +export const MARKER_TICK_HEIGHT = 8; +export const MARKER_FONT_SIZE = 10; +export const MARKER_TEXT_PADDING = 8; +export const COLOR_HOVER_DIM_DELTA = 5; + +export const INTERVAL_TIMES = [ + 1, + 2, + 5, + 10, + 20, + 50, + 100, + 200, + 500, + 1000, + 2000, + 5000, +]; +export const MIN_INTERVAL_SIZE_PX = 70; + +export const EVENT_ROW_PADDING = 4; +export const EVENT_DIAMETER = 6; +export const REACT_MEASURE_HEIGHT = 9; +export const BORDER_SIZE = 1; + +export const FLAMECHART_FONT_SIZE = 10; +export const FLAMECHART_FRAME_HEIGHT = 16; +export const FLAMECHART_TEXT_PADDING = 3; + +export const COLORS = Object.freeze({ + BACKGROUND: '#ffffff', + PRIORITY_BACKGROUND: '#ededf0', + PRIORITY_BORDER: '#d7d7db', + PRIORITY_LABEL: '#272727', + USER_TIMING: '#c9cacd', + USER_TIMING_HOVER: '#93959a', + REACT_IDLE: '#edf6ff', + REACT_IDLE_SELECTED: '#EDF6FF', + REACT_IDLE_HOVER: '#EDF6FF', + REACT_RENDER: '#9fc3f3', + REACT_RENDER_SELECTED: '#64A9F5', + REACT_RENDER_HOVER: '#2683E2', + REACT_COMMIT: '#ff718e', + REACT_COMMIT_SELECTED: '#FF5277', + REACT_COMMIT_HOVER: '#ed0030', + REACT_LAYOUT_EFFECTS: '#c88ff0', + REACT_LAYOUT_EFFECTS_SELECTED: '#934FC1', + REACT_LAYOUT_EFFECTS_HOVER: '#601593', + REACT_PASSIVE_EFFECTS: '#c88ff0', + REACT_PASSIVE_EFFECTS_SELECTED: '#934FC1', + REACT_PASSIVE_EFFECTS_HOVER: '#601593', + REACT_SCHEDULE: '#9fc3f3', + REACT_SCHEDULE_HOVER: '#2683E2', + REACT_SCHEDULE_CASCADING: '#ff718e', + REACT_SCHEDULE_CASCADING_HOVER: '#ed0030', + REACT_SUSPEND: '#a6e59f', + REACT_SUSPEND_HOVER: '#13bc00', + REACT_WORK_BORDER: '#ffffff', + TIME_MARKER_LABEL: '#18212b', +}); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/index.js b/packages/react-devtools-scheduling-profiler/src/content-views/index.js new file mode 100644 index 0000000000..e017b95e20 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/index.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './FlamechartView'; +export * from './ReactEventsView'; +export * from './ReactMeasuresView'; +export * from './TimeAxisMarkersView'; +export * from './UserTimingMarksView'; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/colors-test.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/colors-test.js new file mode 100644 index 0000000000..dcbe900f98 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/__tests__/colors-test.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {hslaColorToString, dimmedColor, ColorGenerator} from '../colors'; + +describe(hslaColorToString, () => { + it('should transform colors to strings', () => { + expect(hslaColorToString({h: 1, s: 2, l: 3, a: 4})).toEqual( + 'hsl(1deg 2% 3% / 4)', + ); + expect(hslaColorToString({h: 3.14, s: 6.28, l: 1.68, a: 100})).toEqual( + 'hsl(3.14deg 6.28% 1.68% / 100)', + ); + }); +}); + +describe(dimmedColor, () => { + it('should dim luminosity using delta', () => { + expect(dimmedColor({h: 1, s: 2, l: 3, a: 4}, 3)).toEqual({ + h: 1, + s: 2, + l: 0, + a: 4, + }); + expect(dimmedColor({h: 1, s: 2, l: 3, a: 4}, -3)).toEqual({ + h: 1, + s: 2, + l: 6, + a: 4, + }); + }); +}); + +describe(ColorGenerator, () => { + describe(ColorGenerator.prototype.colorForID, () => { + it('should generate a color for an ID', () => { + expect(new ColorGenerator().colorForID('123')).toMatchInlineSnapshot(` + Object { + "a": 1, + "h": 190, + "l": 80, + "s": 67, + } + `); + }); + + it('should generate colors deterministically given an ID', () => { + expect(new ColorGenerator().colorForID('id1')).toEqual( + new ColorGenerator().colorForID('id1'), + ); + expect(new ColorGenerator().colorForID('id2')).toEqual( + new ColorGenerator().colorForID('id2'), + ); + }); + + it('should generate different colors for different IDs', () => { + expect(new ColorGenerator().colorForID('id1')).not.toEqual( + new ColorGenerator().colorForID('id2'), + ); + }); + + it('should return colors that have been set manually', () => { + const generator = new ColorGenerator(); + const manualColor = {h: 1, s: 2, l: 3, a: 4}; + generator.setColorForID('id with set color', manualColor); + expect(generator.colorForID('id with set color')).toEqual(manualColor); + expect(generator.colorForID('some other id')).not.toEqual(manualColor); + }); + + it('should generate colors from fixed color spaces', () => { + const generator = new ColorGenerator(1, 2, 3, 4); + expect(generator.colorForID('123')).toEqual({h: 1, s: 2, l: 3, a: 4}); + expect(generator.colorForID('234')).toEqual({h: 1, s: 2, l: 3, a: 4}); + }); + + it('should generate colors from range color spaces', () => { + const generator = new ColorGenerator( + {min: 0, max: 360, count: 2}, + 2, + 3, + 4, + ); + expect(generator.colorForID('123')).toEqual({h: 0, s: 2, l: 3, a: 4}); + expect(generator.colorForID('234')).toEqual({h: 360, s: 2, l: 3, a: 4}); + }); + }); +}); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/colors.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/colors.js new file mode 100644 index 0000000000..a1ad49b588 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/colors.js @@ -0,0 +1,113 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +type ColorSpace = number | {|min: number, max: number, count?: number|}; + +// Docstrings from https://www.w3schools.com/css/css_colors_hsl.asp +type HslaColor = $ReadOnly<{| + /** Hue is a degree on the color wheel from 0 to 360. 0 is red, 120 is green, and 240 is blue. */ + h: number, + /** Saturation is a percentage value, 0% means a shade of gray, and 100% is the full color. */ + s: number, + /** Lightness is a percentage, 0% is black, 50% is neither light or dark, 100% is white. */ + l: number, + /** Alpha is a percentage, 0% is fully transparent, and 100 is not transparent at all. */ + a: number, +|}>; + +export function hslaColorToString({h, s, l, a}: HslaColor): string { + return `hsl(${h}deg ${s}% ${l}% / ${a})`; +} + +export function dimmedColor(color: HslaColor, dimDelta: number): HslaColor { + return { + ...color, + l: color.l - dimDelta, + }; +} + +// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/platform/utilities.js;l=120 +function hashCode(string: string): number { + // Hash algorithm for substrings is described in "Über die Komplexität der Multiplikation in + // eingeschränkten Branchingprogrammmodellen" by Woelfe. + // http://opendatastructures.org/versions/edition-0.1d/ods-java/node33.html#SECTION00832000000000000000 + const p = (1 << 30) * 4 - 5; // prime: 2^32 - 5 + const z = 0x5033d967; // 32 bits from random.org + const z2 = 0x59d2f15d; // random odd 32 bit number + let s = 0; + let zi = 1; + for (let i = 0; i < string.length; i++) { + const xi = string.charCodeAt(i) * z2; + s = (s + zi * xi) % p; + zi = (zi * z) % p; + } + s = (s + zi * (p - 1)) % p; + return Math.abs(s | 0); +} + +function indexToValueInSpace(index: number, space: ColorSpace): number { + if (typeof space === 'number') { + return space; + } + const count = space.count || space.max - space.min; + index %= count; + return ( + space.min + Math.floor((index / (count - 1)) * (space.max - space.min)) + ); +} + +/** + * Deterministic color generator. + * + * Adapted from: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/common/Color.js + */ +export class ColorGenerator { + _hueSpace: ColorSpace; + _satSpace: ColorSpace; + _lightnessSpace: ColorSpace; + _alphaSpace: ColorSpace; + _colors: Map; + + constructor( + hueSpace?: ColorSpace, + satSpace?: ColorSpace, + lightnessSpace?: ColorSpace, + alphaSpace?: ColorSpace, + ) { + this._hueSpace = hueSpace || {min: 0, max: 360}; + this._satSpace = satSpace || 67; + this._lightnessSpace = lightnessSpace || 80; + this._alphaSpace = alphaSpace || 1; + this._colors = new Map(); + } + + setColorForID(id: string, color: HslaColor) { + this._colors.set(id, color); + } + + colorForID(id: string): HslaColor { + const cachedColor = this._colors.get(id); + if (cachedColor) { + return cachedColor; + } + const color = this._generateColorForID(id); + this._colors.set(id, color); + return color; + } + + _generateColorForID(id: string): HslaColor { + const hash = hashCode(id); + return { + h: indexToValueInSpace(hash, this._hueSpace), + s: indexToValueInSpace(hash >> 8, this._satSpace), + l: indexToValueInSpace(hash >> 16, this._lightnessSpace), + a: indexToValueInSpace(hash >> 24, this._alphaSpace), + }; + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/positioning.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/positioning.js new file mode 100644 index 0000000000..8cea48ced9 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/positioning.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Rect} from '../../view-base'; + +export function positioningScaleFactor( + intrinsicWidth: number, + frame: Rect, +): number { + return frame.size.width / intrinsicWidth; +} + +export function timestampToPosition( + timestamp: number, + scaleFactor: number, + frame: Rect, +): number { + return frame.origin.x + timestamp * scaleFactor; +} + +export function positionToTimestamp( + position: number, + scaleFactor: number, + frame: Rect, +): number { + return (position - frame.origin.x) / scaleFactor; +} + +export function durationToWidth(duration: number, scaleFactor: number): number { + return duration * scaleFactor; +} + +export function widthToDuration(width: number, scaleFactor: number): number { + return width / scaleFactor; +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css new file mode 100644 index 0000000000..60848641f4 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.css @@ -0,0 +1,10 @@ +.ContextMenu { + position: absolute; + border-radius: 0.125rem; + background-color: #ffffff; + border: 1px solid #ccc; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); + font-size: 11px; + overflow: hidden; + z-index: 10000002; +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js new file mode 100644 index 0000000000..8b09ef1510 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/ContextMenu.js @@ -0,0 +1,143 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {RegistryContextType} from './Contexts'; + +import * as React from 'react'; +import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react'; +import {createPortal} from 'react-dom'; +import {RegistryContext} from './Contexts'; + +import styles from './ContextMenu.css'; + +function repositionToFit(element: HTMLElement, pageX: number, pageY: number) { + const ownerWindow = element.ownerDocument.defaultView; + if (element !== null) { + if (pageY + element.offsetHeight >= ownerWindow.innerHeight) { + if (pageY - element.offsetHeight > 0) { + element.style.top = `${pageY - element.offsetHeight}px`; + } else { + element.style.top = '0px'; + } + } else { + element.style.top = `${pageY}px`; + } + + if (pageX + element.offsetWidth >= ownerWindow.innerWidth) { + if (pageX - element.offsetWidth > 0) { + element.style.left = `${pageX - element.offsetWidth}px`; + } else { + element.style.left = '0px'; + } + } else { + element.style.left = `${pageX}px`; + } + } +} + +const HIDDEN_STATE = { + data: null, + isVisible: false, + pageX: 0, + pageY: 0, +}; + +type Props = {| + children: (data: Object) => React$Node, + id: string, +|}; + +export default function ContextMenu({children, id}: Props) { + const {hideMenu, registerMenu} = useContext( + RegistryContext, + ); + + const [state, setState] = useState(HIDDEN_STATE); + + const bodyAccessorRef = useRef(null); + const containerRef = useRef(null); + const menuRef = useRef(null); + + useEffect(() => { + if (!bodyAccessorRef.current) { + return; + } + const ownerDocument = bodyAccessorRef.current.ownerDocument; + containerRef.current = ownerDocument.createElement('div'); + if (ownerDocument.body) { + ownerDocument.body.appendChild(containerRef.current); + } + return () => { + if (ownerDocument.body && containerRef.current) { + ownerDocument.body.removeChild(containerRef.current); + } + }; + }, [bodyAccessorRef, containerRef]); + + useEffect(() => { + const showMenuFn = ({data, pageX, pageY}) => { + setState({data, isVisible: true, pageX, pageY}); + }; + const hideMenuFn = () => setState(HIDDEN_STATE); + return registerMenu(id, showMenuFn, hideMenuFn); + }, [id]); + + useLayoutEffect(() => { + if (!state.isVisible || !containerRef.current) { + return; + } + + const menu = menuRef.current; + if (!menu) { + return; + } + + const hideUnlessContains: MouseEventHandler & + TouchEventHandler & + KeyboardEventHandler = event => { + if (event.target instanceof HTMLElement && !menu.contains(event.target)) { + hideMenu(); + } + }; + + const ownerDocument = containerRef.current.ownerDocument; + ownerDocument.addEventListener('mousedown', hideUnlessContains); + ownerDocument.addEventListener('touchstart', hideUnlessContains); + ownerDocument.addEventListener('keydown', hideUnlessContains); + + const ownerWindow = ownerDocument.defaultView; + ownerWindow.addEventListener('resize', hideMenu); + + repositionToFit(menu, state.pageX, state.pageY); + + return () => { + ownerDocument.removeEventListener('mousedown', hideUnlessContains); + ownerDocument.removeEventListener('touchstart', hideUnlessContains); + ownerDocument.removeEventListener('keydown', hideUnlessContains); + + ownerWindow.removeEventListener('resize', hideMenu); + }; + }, [state]); + + if (!state.isVisible) { + return
; + } else { + const container = containerRef.current; + if (container !== null) { + return createPortal( +
+ {children(state.data)} +
, + container, + ); + } else { + return null; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css new file mode 100644 index 0000000000..19fd8284a4 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.css @@ -0,0 +1,20 @@ +.ContextMenuItem { + display: flex; + align-items: center; + color: #333; + padding: 0.5rem 0.75rem; + cursor: default; + border-top: 1px solid #ccc; +} +.ContextMenuItem:first-of-type { + border-top: none; +} +.ContextMenuItem:hover, +.ContextMenuItem:focus { + outline: 0; + background-color: rgba(0, 136, 250, 0.1); +} +.ContextMenuItem:active { + background-color: #0088fa; + color: #fff; +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js new file mode 100644 index 0000000000..5750bd90cd --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/ContextMenuItem.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {RegistryContextType} from './Contexts'; + +import * as React from 'react'; +import {useContext} from 'react'; +import {RegistryContext} from './Contexts'; + +import styles from './ContextMenuItem.css'; + +type Props = {| + children: React$Node, + onClick: () => void, + title: string, +|}; + +export default function ContextMenuItem({children, onClick, title}: Props) { + const {hideMenu} = useContext(RegistryContext); + + const handleClick: MouseEventHandler = event => { + onClick(); + hideMenu(); + }; + + return ( +
+ {children} +
+ ); +} diff --git a/packages/react-devtools-scheduling-profiler/src/context/Contexts.js b/packages/react-devtools-scheduling-profiler/src/context/Contexts.js new file mode 100644 index 0000000000..46c742e06d --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/Contexts.js @@ -0,0 +1,87 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {createContext} from 'react'; + +export type ShowFn = ({|data: Object, pageX: number, pageY: number|}) => void; +export type HideFn = () => void; +export type OnChangeFn = boolean => void; + +const idToShowFnMap = new Map(); +const idToHideFnMap = new Map(); + +let currentHideFn: ?HideFn = null; +let currentOnChange: ?OnChangeFn = null; + +function hideMenu() { + if (typeof currentHideFn === 'function') { + currentHideFn(); + + if (typeof currentOnChange === 'function') { + currentOnChange(false); + } + } + + currentHideFn = null; + currentOnChange = null; +} + +function showMenu({ + data, + id, + onChange, + pageX, + pageY, +}: {| + data: Object, + id: string, + onChange?: OnChangeFn, + pageX: number, + pageY: number, +|}) { + const showFn = idToShowFnMap.get(id); + if (typeof showFn === 'function') { + // Prevent open menus from being left hanging. + hideMenu(); + + currentHideFn = idToHideFnMap.get(id); + showFn({data, pageX, pageY}); + + if (typeof onChange === 'function') { + currentOnChange = onChange; + onChange(true); + } + } +} + +function registerMenu(id: string, showFn: ShowFn, hideFn: HideFn) { + if (idToShowFnMap.has(id)) { + throw Error(`Context menu with id "${id}" already registered.`); + } + + idToShowFnMap.set(id, showFn); + idToHideFnMap.set(id, hideFn); + + return function unregisterMenu() { + idToShowFnMap.delete(id); + idToHideFnMap.delete(id); + }; +} + +export type RegistryContextType = {| + hideMenu: typeof hideMenu, + showMenu: typeof showMenu, + registerMenu: typeof registerMenu, +|}; + +export const RegistryContext = createContext({ + hideMenu, + showMenu, + registerMenu, +}); diff --git a/packages/react-devtools-scheduling-profiler/src/context/index.js b/packages/react-devtools-scheduling-profiler/src/context/index.js new file mode 100644 index 0000000000..c903d4f886 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/index.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {RegistryContext} from './Contexts'; +import ContextMenu from './ContextMenu'; +import ContextMenuItem from './ContextMenuItem'; +import useContextMenu from './useContextMenu'; + +export {RegistryContext, ContextMenu, ContextMenuItem, useContextMenu}; diff --git a/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js b/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js new file mode 100644 index 0000000000..467c138f62 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/context/useContextMenu.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {OnChangeFn, RegistryContextType} from './Contexts'; + +import {useContext, useEffect} from 'react'; +import {RegistryContext} from './Contexts'; + +export default function useContextMenu({ + data, + id, + onChange, + ref, +}: {| + data: T, + id: string, + onChange: OnChangeFn, + ref: {+current: HTMLElement | null}, +|}) { + const {showMenu} = useContext(RegistryContext); + + useEffect(() => { + if (ref.current !== null) { + const handleContextMenu = (event: MouseEvent | TouchEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const pageX = + (event: any).pageX || + (event.touches && (event: any).touches[0].pageX); + const pageY = + (event: any).pageY || + (event.touches && (event: any).touches[0].pageY); + + showMenu({data, id, onChange, pageX, pageY}); + }; + + const trigger = ref.current; + trigger.addEventListener('contextmenu', handleContextMenu); + + return () => { + trigger.removeEventListener('contextmenu', handleContextMenu); + }; + } + }, [data, id, showMenu]); +} diff --git a/packages/react-devtools-scheduling-profiler/src/index.css b/packages/react-devtools-scheduling-profiler/src/index.css new file mode 100644 index 0000000000..ec2585e8c0 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/packages/react-devtools-scheduling-profiler/src/index.js b/packages/react-devtools-scheduling-profiler/src/index.js new file mode 100644 index 0000000000..ffb1a8d501 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/index.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import 'regenerator-runtime/runtime'; + +import * as React from 'react'; +// $FlowFixMe Flow does not yet know about createRoot() +import {unstable_createRoot as createRoot} from 'react-dom'; +import nullthrows from 'nullthrows'; +import App from './App'; + +import './index.css'; + +const container = document.createElement('div'); +container.id = 'root'; + +const body = nullthrows(document.body, 'Expect document.body to exist'); +body.appendChild(container); + +createRoot(container).render( + + + , +); diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js new file mode 100644 index 0000000000..a5a1c0d43b --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -0,0 +1,135 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Type utilities + +// Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798 +// eslint-disable-next-line no-unused-vars +type Return_) => R> = R; +/** Get return type of a function. */ +export type Return = Return_<*, T>; + +// Project types + +export type Milliseconds = number; + +export type ReactLane = number; + +type BaseReactEvent = {| + +componentName?: string, + +componentStack?: string, + +timestamp: Milliseconds, +|}; + +type BaseReactScheduleEvent = {| + ...BaseReactEvent, + +lanes: ReactLane[], +|}; +export type ReactScheduleRenderEvent = {| + ...BaseReactScheduleEvent, + type: 'schedule-render', +|}; +export type ReactScheduleStateUpdateEvent = {| + ...BaseReactScheduleEvent, + type: 'schedule-state-update', + isCascading: boolean, +|}; +export type ReactScheduleForceUpdateEvent = {| + ...BaseReactScheduleEvent, + type: 'schedule-force-update', + isCascading: boolean, +|}; + +type BaseReactSuspenseEvent = {| + ...BaseReactEvent, + id: string, +|}; +export type ReactSuspenseSuspendEvent = {| + ...BaseReactSuspenseEvent, + type: 'suspense-suspend', +|}; +export type ReactSuspenseResolvedEvent = {| + ...BaseReactSuspenseEvent, + type: 'suspense-resolved', +|}; +export type ReactSuspenseRejectedEvent = {| + ...BaseReactSuspenseEvent, + type: 'suspense-rejected', +|}; + +export type ReactEvent = + | ReactScheduleRenderEvent + | ReactScheduleStateUpdateEvent + | ReactScheduleForceUpdateEvent + | ReactSuspenseSuspendEvent + | ReactSuspenseResolvedEvent + | ReactSuspenseRejectedEvent; +export type ReactEventType = $PropertyType; + +export type ReactMeasureType = + | 'commit' + // render-idle: A measure spanning the time when a render starts, through all + // yields and restarts, and ends when commit stops OR render is cancelled. + | 'render-idle' + | 'render' + | 'layout-effects' + | 'passive-effects'; + +export type BatchUID = number; + +export type ReactMeasure = {| + +type: ReactMeasureType, + +lanes: ReactLane[], + +timestamp: Milliseconds, + +duration: Milliseconds, + +batchUID: BatchUID, + +depth: number, +|}; + +/** + * A flamechart stack frame belonging to a stack trace. + */ +export type FlamechartStackFrame = {| + name: string, + timestamp: Milliseconds, + duration: Milliseconds, + scriptUrl?: string, + locationLine?: number, + locationColumn?: number, +|}; + +export type UserTimingMark = {| + name: string, + timestamp: Milliseconds, +|}; + +/** + * A "layer" of stack frames in the profiler UI, i.e. all stack frames of the + * same depth across all stack traces. Displayed as a flamechart row in the UI. + */ +export type FlamechartStackLayer = FlamechartStackFrame[]; + +export type Flamechart = FlamechartStackLayer[]; + +export type ReactProfilerData = {| + startTime: number, + duration: number, + events: ReactEvent[], + measures: ReactMeasure[], + flamechart: Flamechart, + otherUserTimingMarks: UserTimingMark[], +|}; + +export type ReactHoverContextInfo = {| + event: ReactEvent | null, + measure: ReactMeasure | null, + data: $ReadOnly | null, + flamechartStackFrame: FlamechartStackFrame | null, + userTimingMark: UserTimingMark | null, +|}; diff --git a/packages/react-devtools-scheduling-profiler/src/utils/__tests__/__snapshots__/preprocessData-test.js.snap b/packages/react-devtools-scheduling-profiler/src/utils/__tests__/__snapshots__/preprocessData-test.js.snap new file mode 100644 index 0000000000..4055b6d45e --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/__tests__/__snapshots__/preprocessData-test.js.snap @@ -0,0 +1,1380 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`preprocessData should process complete set of events (page load sample data) 1`] = ` +Object { + "duration": 2586.895, + "events": Array [ + Object { + "componentStack": "", + "lanes": Array [ + 9, + ], + "timestamp": 278.073, + "type": "schedule-render", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 658.345, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "1", + "timestamp": 660.739, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "2", + "timestamp": 662.554, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 663.455, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 664.202, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 666.68, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "1", + "timestamp": 667.201, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "2", + "timestamp": 667.74, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 668.282, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 668.848, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at SuspenseList + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 671.335, + "type": "suspense-suspend", + }, + Object { + "componentName": "ForceUpdateDemo_ForceUpdateDemo", + "componentStack": " + at ForceUpdateDemo_ForceUpdateDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:18:98) + at App", + "isCascading": true, + "lanes": Array [ + 0, + ], + "timestamp": 679.438, + "type": "schedule-state-update", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "1", + "timestamp": 1794.922, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "1", + "timestamp": 1795.374, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 1797.166, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "2", + "timestamp": 1797.811, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 1798.163, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 1798.505, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "2", + "timestamp": 1866.249, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "2", + "timestamp": 1866.542, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "2", + "timestamp": 1866.858, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 1867.816, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 1868.638, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 1868.966, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 1980.273, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 1980.522, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 1980.946, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "3", + "timestamp": 1981.164, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 1982.079, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 1983.17, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 2379.587, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 2379.895, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 2380.242, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 2380.487, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "4", + "timestamp": 2380.735, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2381.772, + "type": "suspense-suspend", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2579.186, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2579.487, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at SuspenseList + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2579.735, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2580.042, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2580.29, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2580.546, + "type": "suspense-resolved", + }, + Object { + "componentName": "ResourceButton", + "componentStack": " + at ResourceButton (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:295) + at Suspense + at div + at div + at SuspenseDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:36:713) + at App", + "id": "0", + "timestamp": 2580.8, + "type": "suspense-resolved", + }, + ], + "flamechart": Array [], + "measures": Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 402.58299999999997, + "lanes": Array [ + 9, + ], + "timestamp": 285.293, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 5.235000000000014, + "lanes": Array [ + 9, + ], + "timestamp": 285.293, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 8.16799999999995, + "lanes": Array [ + 9, + ], + "timestamp": 297.708, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 4.989000000000033, + "lanes": Array [ + 9, + ], + "timestamp": 310.457, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 6.470000000000027, + "lanes": Array [ + 9, + ], + "timestamp": 357.767, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 6.633000000000038, + "lanes": Array [ + 9, + ], + "timestamp": 371.486, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 9.889999999999986, + "lanes": Array [ + 9, + ], + "timestamp": 385.05, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.8589999999999804, + "lanes": Array [ + 9, + ], + "timestamp": 398.06, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.261000000000024, + "lanes": Array [ + 9, + ], + "timestamp": 406.666, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.700999999999965, + "lanes": Array [ + 9, + ], + "timestamp": 414.218, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.793000000000006, + "lanes": Array [ + 9, + ], + "timestamp": 422.141, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.706999999999994, + "lanes": Array [ + 9, + ], + "timestamp": 430.185, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.406999999999982, + "lanes": Array [ + 9, + ], + "timestamp": 439.552, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.649000000000001, + "lanes": Array [ + 9, + ], + "timestamp": 447.294, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.736000000000047, + "lanes": Array [ + 9, + ], + "timestamp": 455.215, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.6959999999999695, + "lanes": Array [ + 9, + ], + "timestamp": 463.259, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.700999999999965, + "lanes": Array [ + 9, + ], + "timestamp": 471.247, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.55499999999995, + "lanes": Array [ + 9, + ], + "timestamp": 479.362, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.7900000000000205, + "lanes": Array [ + 9, + ], + "timestamp": 487.195, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.617000000000019, + "lanes": Array [ + 9, + ], + "timestamp": 495.354, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.241999999999962, + "lanes": Array [ + 9, + ], + "timestamp": 503.706, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.81899999999996, + "lanes": Array [ + 9, + ], + "timestamp": 513.106, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 9.269999999999982, + "lanes": Array [ + 9, + ], + "timestamp": 521.667, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.758000000000038, + "lanes": Array [ + 9, + ], + "timestamp": 531.185, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.735000000000014, + "lanes": Array [ + 9, + ], + "timestamp": 539.213, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.730000000000018, + "lanes": Array [ + 9, + ], + "timestamp": 547.223, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.697000000000003, + "lanes": Array [ + 9, + ], + "timestamp": 555.234, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.715000000000032, + "lanes": Array [ + 9, + ], + "timestamp": 563.232, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.760999999999967, + "lanes": Array [ + 9, + ], + "timestamp": 571.212, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.618000000000052, + "lanes": Array [ + 9, + ], + "timestamp": 579.324, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.737999999999943, + "lanes": Array [ + 9, + ], + "timestamp": 587.201, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.739000000000033, + "lanes": Array [ + 9, + ], + "timestamp": 595.209, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.732000000000085, + "lanes": Array [ + 9, + ], + "timestamp": 603.194, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.712999999999965, + "lanes": Array [ + 9, + ], + "timestamp": 611.212, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.3700000000000045, + "lanes": Array [ + 9, + ], + "timestamp": 619.574, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.73599999999999, + "lanes": Array [ + 9, + ], + "timestamp": 627.188, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.729000000000042, + "lanes": Array [ + 9, + ], + "timestamp": 635.193, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.908999999999992, + "lanes": Array [ + 9, + ], + "timestamp": 643.206, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 7.6159999999999854, + "lanes": Array [ + 9, + ], + "timestamp": 651.339, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 5.334999999999923, + "lanes": Array [ + 9, + ], + "timestamp": 659.445, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 5.5890000000000555, + "lanes": Array [ + 9, + ], + "timestamp": 664.952, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 1.58400000000006, + "lanes": Array [ + 9, + ], + "timestamp": 670.784, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 14.552000000000021, + "lanes": Array [ + 9, + ], + "timestamp": 673.324, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 2.6889999999999645, + "lanes": Array [ + 9, + ], + "timestamp": 677.236, + "type": "layout-effects", + }, + Object { + "batchUID": 1, + "depth": 1, + "duration": 3.5559999999999263, + "lanes": Array [ + 0, + ], + "timestamp": 684.104, + "type": "render-idle", + }, + Object { + "batchUID": 1, + "depth": 1, + "duration": 2.2169999999999845, + "lanes": Array [ + 0, + ], + "timestamp": 684.104, + "type": "render", + }, + Object { + "batchUID": 1, + "depth": 1, + "duration": 1.3120000000000118, + "lanes": Array [ + 0, + ], + "timestamp": 686.348, + "type": "commit", + }, + Object { + "batchUID": 1, + "depth": 2, + "duration": 0.020999999999958163, + "lanes": Array [ + 0, + ], + "timestamp": 687.267, + "type": "layout-effects", + }, + Object { + "batchUID": 2, + "depth": 0, + "duration": 6.119000000000142, + "lanes": Array [ + 10, + ], + "timestamp": 1796.042, + "type": "render-idle", + }, + Object { + "batchUID": 2, + "depth": 0, + "duration": 3.4329999999999927, + "lanes": Array [ + 10, + ], + "timestamp": 1796.042, + "type": "render", + }, + Object { + "batchUID": 2, + "depth": 0, + "duration": 2.589000000000169, + "lanes": Array [ + 10, + ], + "timestamp": 1799.572, + "type": "commit", + }, + Object { + "batchUID": 2, + "depth": 1, + "duration": 0.020999999999958163, + "lanes": Array [ + 10, + ], + "timestamp": 1801.605, + "type": "layout-effects", + }, + Object { + "batchUID": 3, + "depth": 0, + "duration": 4.116999999999962, + "lanes": Array [ + 11, + ], + "timestamp": 1866.998, + "type": "render-idle", + }, + Object { + "batchUID": 3, + "depth": 0, + "duration": 2.6679999999998927, + "lanes": Array [ + 11, + ], + "timestamp": 1866.998, + "type": "render", + }, + Object { + "batchUID": 3, + "depth": 0, + "duration": 1.4200000000000728, + "lanes": Array [ + 11, + ], + "timestamp": 1869.695, + "type": "commit", + }, + Object { + "batchUID": 3, + "depth": 1, + "duration": 0.020000000000209184, + "lanes": Array [ + 11, + ], + "timestamp": 1870.764, + "type": "layout-effects", + }, + Object { + "batchUID": 4, + "depth": 0, + "duration": 3.7679999999998017, + "lanes": Array [ + 12, + ], + "timestamp": 1981.361, + "type": "render-idle", + }, + Object { + "batchUID": 4, + "depth": 0, + "duration": 2.438999999999851, + "lanes": Array [ + 12, + ], + "timestamp": 1981.361, + "type": "render", + }, + Object { + "batchUID": 4, + "depth": 0, + "duration": 1.2579999999998108, + "lanes": Array [ + 12, + ], + "timestamp": 1983.871, + "type": "commit", + }, + Object { + "batchUID": 4, + "depth": 1, + "duration": 0.027000000000043656, + "lanes": Array [ + 12, + ], + "timestamp": 1984.931, + "type": "layout-effects", + }, + Object { + "batchUID": 5, + "depth": 0, + "duration": 3.8899999999998727, + "lanes": Array [ + 13, + ], + "timestamp": 2380.969, + "type": "render-idle", + }, + Object { + "batchUID": 5, + "depth": 0, + "duration": 2.3099999999999454, + "lanes": Array [ + 13, + ], + "timestamp": 2380.969, + "type": "render", + }, + Object { + "batchUID": 5, + "depth": 0, + "duration": 1.5209999999997308, + "lanes": Array [ + 13, + ], + "timestamp": 2383.338, + "type": "commit", + }, + Object { + "batchUID": 5, + "depth": 1, + "duration": 0.01999999999998181, + "lanes": Array [ + 13, + ], + "timestamp": 2384.535, + "type": "layout-effects", + }, + Object { + "batchUID": 6, + "depth": 0, + "duration": 5.942999999999756, + "lanes": Array [ + 14, + ], + "timestamp": 2580.952, + "type": "render-idle", + }, + Object { + "batchUID": 6, + "depth": 0, + "duration": 3.424999999999727, + "lanes": Array [ + 14, + ], + "timestamp": 2580.952, + "type": "render", + }, + Object { + "batchUID": 6, + "depth": 0, + "duration": 2.4470000000001164, + "lanes": Array [ + 14, + ], + "timestamp": 2584.448, + "type": "commit", + }, + Object { + "batchUID": 6, + "depth": 1, + "duration": 0.027000000000043656, + "lanes": Array [ + 14, + ], + "timestamp": 2586.122, + "type": "layout-effects", + }, + ], + "otherUserTimingMarks": Array [ + Object { + "name": "navigationStart", + "timestamp": -29.357, + }, + Object { + "name": "fetchStart", + "timestamp": -26.92, + }, + Object { + "name": "requestStart", + "timestamp": -21.171, + }, + Object { + "name": "responseEnd", + "timestamp": -15.655, + }, + Object { + "name": "unloadEventStart", + "timestamp": -0.74, + }, + Object { + "name": "unloadEventEnd", + "timestamp": 39.608, + }, + Object { + "name": "requestStart", + "timestamp": 107.649, + }, + Object { + "name": "requestStart", + "timestamp": 108.385, + }, + Object { + "name": "loadEventStart", + "timestamp": 307.056, + }, + Object { + "name": "loadEventEnd", + "timestamp": 308.242, + }, + Object { + "name": "requestStart", + "timestamp": 341.329, + }, + Object { + "name": "requestStart", + "timestamp": 344.02, + }, + Object { + "name": "requestStart", + "timestamp": 387.585, + }, + ], + "startTime": 8993778496, +} +`; + +exports[`preprocessData should process forced update event 1`] = ` +Object { + "duration": 67.461, + "events": Array [ + Object { + "componentName": "ForceUpdateDemo_ForceUpdateDemo", + "componentStack": " + at ForceUpdateDemo_ForceUpdateDemo (https://concurrent-demo.now.sh/static/js/main.c9f122eb.chunk.js:18:98) + at App", + "isCascading": false, + "lanes": Array [ + 4, + ], + "timestamp": 63.355, + "type": "schedule-force-update", + }, + ], + "flamechart": Array [], + "measures": Array [ + Object { + "batchUID": 0, + "depth": 0, + "duration": 2.1910000000000025, + "lanes": Array [ + 4, + ], + "timestamp": 65.27, + "type": "render-idle", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 0.9770000000000039, + "lanes": Array [ + 4, + ], + "timestamp": 65.27, + "type": "render", + }, + Object { + "batchUID": 0, + "depth": 0, + "duration": 1.1670000000000016, + "lanes": Array [ + 4, + ], + "timestamp": 66.294, + "type": "commit", + }, + Object { + "batchUID": 0, + "depth": 1, + "duration": 0.018000000000000682, + "lanes": Array [ + 4, + ], + "timestamp": 67.325, + "type": "layout-effects", + }, + ], + "otherUserTimingMarks": Array [], + "startTime": 40806924876, +} +`; diff --git a/packages/react-devtools-scheduling-profiler/src/utils/__tests__/preprocessData-test.js b/packages/react-devtools-scheduling-profiler/src/utils/__tests__/preprocessData-test.js new file mode 100644 index 0000000000..8626252cee --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/__tests__/preprocessData-test.js @@ -0,0 +1,352 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +import preprocessData, { + getLanesFromTransportDecimalBitmask, +} from '../preprocessData'; +import {REACT_TOTAL_NUM_LANES} from '../../constants'; + +// Disable quotes rule in the whole file as we paste raw JSON as test inputs and +// Prettier will already format the remaining quotes. +/* eslint-disable quotes */ + +describe(getLanesFromTransportDecimalBitmask, () => { + it('should return array of lane numbers from bitmask string', () => { + expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]); + expect(getLanesFromTransportDecimalBitmask('512')).toEqual([9]); + expect(getLanesFromTransportDecimalBitmask('3')).toEqual([0, 1]); + expect(getLanesFromTransportDecimalBitmask('1234')).toEqual([ + 1, + 4, + 6, + 7, + 10, + ]); // 2 + 16 + 64 + 128 + 1024 + expect( + getLanesFromTransportDecimalBitmask('1073741824'), // 0b1000000000000000000000000000000 + ).toEqual([30]); + expect( + getLanesFromTransportDecimalBitmask('2147483647'), // 0b1111111111111111111111111111111 + ).toEqual(Array.from(Array(31).keys())); + }); + + it('should return empty array if laneBitmaskString is not a bitmask', () => { + expect(getLanesFromTransportDecimalBitmask('')).toEqual([]); + expect(getLanesFromTransportDecimalBitmask('hello')).toEqual([]); + expect(getLanesFromTransportDecimalBitmask('-1')).toEqual([]); + expect(getLanesFromTransportDecimalBitmask('-0')).toEqual([]); + }); + + it('should ignore lanes outside REACT_TOTAL_NUM_LANES', () => { + // Sanity check; this test may need to be updated when the no. of fiber + // lanes are changed. + expect(REACT_TOTAL_NUM_LANES).toBe(31); + + expect( + getLanesFromTransportDecimalBitmask( + '4294967297', // 2^32 + 1 + ), + ).toEqual([0]); + }); +}); + +describe(preprocessData, () => { + it('should throw given an empty timeline', () => { + expect(() => preprocessData([])).toThrow(); + }); + + it('should throw given a timeline with no Profile event', () => { + expect(() => + // prettier-ignore + preprocessData([ + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--schedule-render-512-","ph":"R","pid":9312,"tid":10252,"ts":8994056569,"tts":1816966}, + ]), + ).toThrow(); + }); + + it('should return empty data given a timeline with no React scheduling profiling marks', () => { + expect( + // prettier-ignore + preprocessData([ + {"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}, + {"pid":57632,"tid":38659,"ts":874860756135,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":18,"tdur":19,"tts":8700284918,"args":{}}, + {"pid":57632,"tid":38659,"ts":874860756158,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":30,"tdur":30,"tts":8700284941,"args":{}}, + {"pid":57632,"tid":38659,"ts":874860756192,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":21,"tdur":20,"tts":8700284976,"args":{}}, + {"pid":57632,"tid":38659,"ts":874860756216,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":6,"tdur":5,"tts":8700285000,"args":{}}, + {"pid":57632,"tid":38659,"ts":874860756224,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":7,"tdur":6,"tts":8700285008,"args":{}}, + {"pid":57632,"tid":38659,"ts":874860756233,"ph":"X","cat":"disabled-by-default-devtools.timeline","name":"RunTask","dur":5,"tdur":4,"tts":8700285017,"args":{}}, + ]), + ).toEqual({ + startTime: 8993778496, + duration: 865866977.737, + events: [], + measures: [], + flamechart: [], + otherUserTimingMarks: [], + }); + }); + + it('should error if events and measures are incomplete', () => { + const error = spyOnDevAndProd(console, 'error'); + // prettier-ignore + preprocessData([ + {"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}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--render-start-8","ph":"R","pid":1852,"tid":12484,"ts":42351664678,"tts":1512475}, + ]); + expect(error).toHaveBeenCalled(); + }); + + it('should error if work is completed without being started', () => { + const error = spyOnDevAndProd(console, 'error'); + // prettier-ignore + preprocessData([ + {"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}, + {"args":{"data":{"navigationId":"E082C30FBDA3ACEE0E7B5FD75F8B7F0D"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":17232,"tid":13628,"ts":264686513020,"tts":4082554}, + ]); + expect(error).toHaveBeenCalled(); + }); + + it('should process complete set of events (page load sample data)', () => { + expect( + // prettier-ignore + preprocessData([ + {"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}, + {"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}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"fetchStart","ph":"R","pid":9312,"tid":10252,"ts":8993751576,"tts":1646197}, + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8993757325,"tts":1612760}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"responseEnd","ph":"R","pid":9312,"tid":10252,"ts":8993762841,"tts":1652151}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"unloadEventStart","ph":"R","pid":9312,"tid":10252,"ts":8993777756,"tts":1646416}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"unloadEventEnd","ph":"R","pid":9312,"tid":10252,"ts":8993818104,"tts":1646419}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domLoading","ph":"R","pid":9312,"tid":10252,"ts":8993820215,"tts":1647488}, + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8993886145,"tts":1771277}, + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8993886881,"tts":1778953}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--schedule-render-512-","ph":"R","pid":9312,"tid":10252,"ts":8994056569,"tts":1816966}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domInteractive","ph":"R","pid":9312,"tid":10252,"ts":8994058638,"tts":1818851}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domContentLoadedEventStart","ph":"R","pid":9312,"tid":10252,"ts":8994058898,"tts":1819078}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domContentLoadedEventEnd","ph":"R","pid":9312,"tid":10252,"ts":8994060045,"tts":1820100}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994063789,"tts":1823183}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994069024,"tts":1826507}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994076204,"tts":1830657}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994084372,"tts":1837590}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing,rail","name":"domComplete","ph":"R","pid":9312,"tid":10252,"ts":8994085517,"tts":1838615}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"loadEventStart","ph":"R","pid":9312,"tid":10252,"ts":8994085552,"tts":1838649}, + {"args":{"frame":"FD65D9AFD04B1295CEA36B883F0FA82F"},"cat":"blink.user_timing","name":"loadEventEnd","ph":"R","pid":9312,"tid":10252,"ts":8994086738,"tts":1839690}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994088953,"tts":1840749}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994093942,"tts":1844455}, + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8994119825,"tts":1877483}, + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8994122516,"tts":1886344}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994136263,"tts":1871212}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994142733,"tts":1875838}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994149982,"tts":1880208}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994156615,"tts":1885309}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994163546,"tts":1887453}, + {"args":{},"cat":"blink.user_timing","name":"requestStart","ph":"R","pid":9312,"tid":10252,"ts":8994166081,"tts":1895751}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994173436,"tts":1894442}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994176556,"tts":1897176}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994184415,"tts":1904263}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994185162,"tts":1904938}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994192423,"tts":1910624}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994192714,"tts":1910872}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994200415,"tts":1917859}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994200637,"tts":1918062}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994208430,"tts":1924894}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994208681,"tts":1925124}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994216388,"tts":1932117}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994218048,"tts":1933622}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994225455,"tts":1940076}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994225790,"tts":1940391}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994233439,"tts":1947224}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994233711,"tts":1947473}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994241447,"tts":1954160}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994241755,"tts":1954426}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994249451,"tts":1961213}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994249743,"tts":1961494}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994257444,"tts":1968141}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994257858,"tts":1968525}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994265413,"tts":1975172}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994265691,"tts":1975416}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994273481,"tts":1981826}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994273850,"tts":1982112}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994281467,"tts":1988537}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994282202,"tts":1988894}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994289444,"tts":1994103}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994291602,"tts":1995165}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994299421,"tts":2000342}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994300163,"tts":2001009}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994309433,"tts":2005662}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994309681,"tts":2005897}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994317439,"tts":2012641}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994317709,"tts":2012890}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994325444,"tts":2019235}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994325719,"tts":2019477}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994333449,"tts":2026367}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994333730,"tts":2026617}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994341427,"tts":2033147}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994341728,"tts":2033411}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994349443,"tts":2040163}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994349708,"tts":2040409}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994357469,"tts":2047136}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994357820,"tts":2047465}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994365438,"tts":2054203}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994365697,"tts":2054434}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994373435,"tts":2061259}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994373705,"tts":2061502}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994381444,"tts":2068300}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994381690,"tts":2068529}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994389422,"tts":2075272}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994389708,"tts":2075518}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994397421,"tts":2082156}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994398070,"tts":2082726}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994405440,"tts":2088860}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994405684,"tts":2089091}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994413420,"tts":2095780}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994413689,"tts":2096021}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994421418,"tts":2102791}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994421702,"tts":2103047}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994429611,"tts":2110142}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994429835,"tts":2110349}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994437451,"tts":2116087}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994437941,"tts":2116287}, + {"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}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994443276,"tts":2119875}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994443448,"tts":2120040}, + {"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}, + {"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}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-yield","ph":"R","pid":9312,"tid":10252,"ts":8994449037,"tts":2124925}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994449280,"tts":2125142}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8994450864,"tts":2126555}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994451820,"tts":2127417}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-512","ph":"R","pid":9312,"tid":10252,"ts":8994455732,"tts":2130777}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8994458421,"tts":2133089}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-1","ph":"R","pid":9312,"tid":10252,"ts":8994462600,"tts":2136847}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8994464817,"tts":2138817}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-1","ph":"R","pid":9312,"tid":10252,"ts":8994464844,"tts":2138843}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-1","ph":"R","pid":9312,"tid":10252,"ts":8994465763,"tts":2139664}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8994465784,"tts":2139686}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8994466156,"tts":2140023}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8994466372,"tts":2140208}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-1024","ph":"R","pid":9312,"tid":10252,"ts":8995574538,"tts":2206568}, + {"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}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8995577971,"tts":2209602}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-1024","ph":"R","pid":9312,"tid":10252,"ts":8995578068,"tts":2209689}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-1024","ph":"R","pid":9312,"tid":10252,"ts":8995580101,"tts":2211495}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8995580122,"tts":2211515}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8995580657,"tts":2211995}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-2048","ph":"R","pid":9312,"tid":10252,"ts":8995645494,"tts":2217999}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8995648162,"tts":2220335}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-2048","ph":"R","pid":9312,"tid":10252,"ts":8995648191,"tts":2220363}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-2048","ph":"R","pid":9312,"tid":10252,"ts":8995649260,"tts":2221320}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8995649280,"tts":2221340}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8995649611,"tts":2221636}, + {"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}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-4096","ph":"R","pid":9312,"tid":10252,"ts":8995759857,"tts":2229803}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8995762296,"tts":2231965}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-4096","ph":"R","pid":9312,"tid":10252,"ts":8995762367,"tts":2232017}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-4096","ph":"R","pid":9312,"tid":10252,"ts":8995763427,"tts":2232966}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8995763454,"tts":2232993}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8995763625,"tts":2233154}, + {"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}, + {"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}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-8192","ph":"R","pid":9312,"tid":10252,"ts":8996159465,"tts":2253624}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8996161775,"tts":2255670}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-8192","ph":"R","pid":9312,"tid":10252,"ts":8996161834,"tts":2255716}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-8192","ph":"R","pid":9312,"tid":10252,"ts":8996163031,"tts":2256754}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8996163051,"tts":2256773}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8996163355,"tts":2257045}, + {"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}, + {"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}, + {"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}, + {"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}, + {"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}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-start-16384","ph":"R","pid":9312,"tid":10252,"ts":8996359448,"tts":2269412}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":9312,"tid":10252,"ts":8996362873,"tts":2272363}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-start-16384","ph":"R","pid":9312,"tid":10252,"ts":8996362944,"tts":2272420}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-start-16384","ph":"R","pid":9312,"tid":10252,"ts":8996364618,"tts":2273869}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":9312,"tid":10252,"ts":8996364645,"tts":2273895}, + {"args":{"data":{"navigationId":"43BC238A4FB7548146D3CD739C9C9434"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":9312,"tid":10252,"ts":8996365391,"tts":2274547}, + ]), + ).toMatchSnapshot(); + }); + + it('should process forced update event', () => { + expect( + // prettier-ignore + preprocessData([ + {"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}, + {"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}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--render-start-16","ph":"R","pid":1852,"tid":12484,"ts":40806990146,"tts":1038890}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--render-stop","ph":"R","pid":1852,"tid":12484,"ts":40806991123,"tts":1039401}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--commit-start-16","ph":"R","pid":1852,"tid":12484,"ts":40806991170,"tts":1039447}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--layout-effects-start-16","ph":"R","pid":1852,"tid":12484,"ts":40806992201,"tts":1040023}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--layout-effects-stop","ph":"R","pid":1852,"tid":12484,"ts":40806992219,"tts":1040041}, + {"args":{"data":{"navigationId":"1065756F5FDAD64BE45CA86B0BBC1F8B"}},"cat":"blink.user_timing","name":"--commit-stop","ph":"R","pid":1852,"tid":12484,"ts":40806992337,"tts":1040149}, + ]), + ).toMatchSnapshot(); + }); + + it('should populate other user timing marks', () => { + expect( + // prettier-ignore + preprocessData([ + {"args":{},"cat":"blink.user_timing","id":"0xcdf75f7c","name":"VCWithoutImage: root","ph":"n","pid":55132,"scope":"blink.user_timing","tid":775,"ts":458734963394}, + {"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}, + {"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}, + {"args":{"data":{"navigationId":"B8774C733A75946C099FE21F8A0E8D38"}},"cat":"blink.user_timing","name":"Some other mark","ph":"R","pid":55132,"tid":775,"ts":458738261491,"tts":28087691}, + ]).otherUserTimingMarks, + ).toMatchInlineSnapshot(` + Array [ + Object { + "name": "VCWithoutImage: root", + "timestamp": -3106.503, + }, + Object { + "name": "--a-mark-that-looks-like-one-of-ours", + "timestamp": 186.459, + }, + Object { + "name": "Some other mark", + "timestamp": 191.594, + }, + ] + `); + }); + + // TODO: Add test for flamechart parsing +}); diff --git a/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js b/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js new file mode 100644 index 0000000000..cba61a4716 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/getBatchRange.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import memoize from 'memoize-one'; + +import type {BatchUID, Milliseconds, ReactProfilerData} from '../types'; + +function unmemoizedGetBatchRange( + batchUID: BatchUID, + data: ReactProfilerData, +): [Milliseconds, Milliseconds] { + const {measures} = data; + + let startTime = 0; + let stopTime = Infinity; + + let i = 0; + + for (i; i < measures.length; i++) { + const measure = measures[i]; + if (measure.batchUID === batchUID) { + startTime = measure.timestamp; + break; + } + } + + for (i; i < measures.length; i++) { + const measure = measures[i]; + stopTime = measure.timestamp; + if (measure.batchUID !== batchUID) { + break; + } + } + + return [startTime, stopTime]; +} + +export const getBatchRange = memoize(unmemoizedGetBatchRange); diff --git a/packages/react-devtools-scheduling-profiler/src/utils/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/utils/preprocessData.js new file mode 100644 index 0000000000..402ff9ea15 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/preprocessData.js @@ -0,0 +1,461 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import { + importFromChromeTimeline, + Flamechart as SpeedscopeFlamechart, +} from '@elg/speedscope'; +import type {TimelineEvent} from '@elg/speedscope'; +import type { + Milliseconds, + BatchUID, + Flamechart, + ReactLane, + ReactMeasureType, + ReactProfilerData, +} from '../types'; + +import {REACT_TOTAL_NUM_LANES} from '../constants'; + +type MeasureStackElement = {| + type: ReactMeasureType, + depth: number, + index: number, + startTime: Milliseconds, + stopTime?: Milliseconds, +|}; + +type ProcessorState = {| + nextRenderShouldGenerateNewBatchID: boolean, + batchUID: BatchUID, + uidCounter: BatchUID, + measureStack: MeasureStackElement[], +|}; + +// Exported for tests +export function getLanesFromTransportDecimalBitmask( + laneBitmaskString: string, +): ReactLane[] { + const laneBitmask = parseInt(laneBitmaskString, 10); + + // As negative numbers are stored in two's complement format, our bitmask + // checks will be thrown off by them. + if (laneBitmask < 0) { + return []; + } + + const lanes = []; + let powersOfTwo = 0; + while (powersOfTwo <= REACT_TOTAL_NUM_LANES) { + if ((1 << powersOfTwo) & laneBitmask) { + lanes.push(powersOfTwo); + } + powersOfTwo++; + } + return lanes; +} + +function getLastType(stack: $PropertyType) { + if (stack.length > 0) { + const {type} = stack[stack.length - 1]; + return type; + } + return null; +} + +function getDepth(stack: $PropertyType) { + if (stack.length > 0) { + const {depth, type} = stack[stack.length - 1]; + return type === 'render-idle' ? depth : depth + 1; + } + return 0; +} + +function markWorkStarted( + type: ReactMeasureType, + startTime: Milliseconds, + lanes: ReactLane[], + currentProfilerData: ReactProfilerData, + state: ProcessorState, +) { + const {batchUID, measureStack} = state; + const index = currentProfilerData.measures.length; + const depth = getDepth(measureStack); + + state.measureStack.push({depth, index, startTime, type}); + + currentProfilerData.measures.push({ + type, + batchUID, + depth, + lanes, + timestamp: startTime, + duration: 0, + }); +} + +function markWorkCompleted( + type: ReactMeasureType, + stopTime: Milliseconds, + currentProfilerData: ReactProfilerData, + stack: $PropertyType, +) { + if (stack.length === 0) { + console.error( + 'Unexpected type "%s" completed at %sms while stack is empty.', + type, + stopTime, + ); + // Ignore work "completion" user timing mark that doesn't complete anything + return; + } + + const last = stack[stack.length - 1]; + if (last.type !== type) { + console.error( + 'Unexpected type "%s" completed at %sms before "%s" completed.', + type, + stopTime, + last.type, + ); + } + + const {index, startTime} = stack.pop(); + const measure = currentProfilerData.measures[index]; + if (!measure) { + console.error('Could not find matching measure for type "%s".', type); + } + + // $FlowFixMe This property should not be writable outside of this function. + measure.duration = stopTime - startTime; +} + +function throwIfIncomplete( + type: ReactMeasureType, + stack: $PropertyType, +) { + const lastIndex = stack.length - 1; + if (lastIndex >= 0) { + const last = stack[lastIndex]; + if (last.stopTime === undefined && last.type === type) { + throw new Error( + `Unexpected type "${type}" started before "${last.type}" completed.`, + ); + } + } +} + +function processTimelineEvent( + event: TimelineEvent, + /** Finalized profiler data up to `event`. May be mutated. */ + currentProfilerData: ReactProfilerData, + /** Intermediate processor state. May be mutated. */ + state: ProcessorState, +) { + const {cat, name, ts, ph} = event; + if (cat !== 'blink.user_timing') { + return; + } + + const startTime = (ts - currentProfilerData.startTime) / 1000; + + // React Events - schedule + if (name.startsWith('--schedule-render-')) { + const [laneBitmaskString, ...splitComponentStack] = name + .substr(18) + .split('-'); + currentProfilerData.events.push({ + type: 'schedule-render', + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } else if (name.startsWith('--schedule-forced-update-')) { + const [ + laneBitmaskString, + componentName, + ...splitComponentStack + ] = name.substr(25).split('-'); + const isCascading = !!state.measureStack.find( + ({type}) => type === 'commit', + ); + currentProfilerData.events.push({ + type: 'schedule-force-update', + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + isCascading, + }); + } else if (name.startsWith('--schedule-state-update-')) { + const [ + laneBitmaskString, + componentName, + ...splitComponentStack + ] = name.substr(24).split('-'); + const isCascading = !!state.measureStack.find( + ({type}) => type === 'commit', + ); + currentProfilerData.events.push({ + type: 'schedule-state-update', + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + isCascading, + }); + } // eslint-disable-line brace-style + + // React Events - suspense + else if (name.startsWith('--suspense-suspend-')) { + const [id, componentName, ...splitComponentStack] = name + .substr(19) + .split('-'); + currentProfilerData.events.push({ + type: 'suspense-suspend', + id, + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } else if (name.startsWith('--suspense-resolved-')) { + const [id, componentName, ...splitComponentStack] = name + .substr(20) + .split('-'); + currentProfilerData.events.push({ + type: 'suspense-resolved', + id, + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } else if (name.startsWith('--suspense-rejected-')) { + const [id, componentName, ...splitComponentStack] = name + .substr(20) + .split('-'); + currentProfilerData.events.push({ + type: 'suspense-rejected', + id, + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } // eslint-disable-line brace-style + + // React Measures - render + else if (name.startsWith('--render-start-')) { + if (state.nextRenderShouldGenerateNewBatchID) { + state.nextRenderShouldGenerateNewBatchID = false; + state.batchUID = ((state.uidCounter++: any): BatchUID); + } + const laneBitmaskString = name.substr(15); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + throwIfIncomplete('render', state.measureStack); + if (getLastType(state.measureStack) !== 'render-idle') { + markWorkStarted( + 'render-idle', + startTime, + lanes, + currentProfilerData, + state, + ); + } + markWorkStarted('render', startTime, lanes, currentProfilerData, state); + } else if ( + name.startsWith('--render-stop') || + name.startsWith('--render-yield') + ) { + markWorkCompleted( + 'render', + startTime, + currentProfilerData, + state.measureStack, + ); + } else if (name.startsWith('--render-cancel')) { + state.nextRenderShouldGenerateNewBatchID = true; + markWorkCompleted( + 'render', + startTime, + currentProfilerData, + state.measureStack, + ); + markWorkCompleted( + 'render-idle', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // React Measures - commits + else if (name.startsWith('--commit-start-')) { + state.nextRenderShouldGenerateNewBatchID = true; + const laneBitmaskString = name.substr(15); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + markWorkStarted('commit', startTime, lanes, currentProfilerData, state); + } else if (name.startsWith('--commit-stop')) { + markWorkCompleted( + 'commit', + startTime, + currentProfilerData, + state.measureStack, + ); + markWorkCompleted( + 'render-idle', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // React Measures - layout effects + else if (name.startsWith('--layout-effects-start-')) { + const laneBitmaskString = name.substr(23); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + markWorkStarted( + 'layout-effects', + startTime, + lanes, + currentProfilerData, + state, + ); + } else if (name.startsWith('--layout-effects-stop')) { + markWorkCompleted( + 'layout-effects', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // React Measures - passive effects + else if (name.startsWith('--passive-effects-start-')) { + const laneBitmaskString = name.substr(24); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + markWorkStarted( + 'passive-effects', + startTime, + lanes, + currentProfilerData, + state, + ); + } else if (name.startsWith('--passive-effects-stop')) { + markWorkCompleted( + 'passive-effects', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // Other user timing marks/measures + else if (ph === 'R' || ph === 'n') { + // User Timing mark + currentProfilerData.otherUserTimingMarks.push({ + name, + timestamp: startTime, + }); + } else if (ph === 'b') { + // TODO: Begin user timing measure + } else if (ph === 'e') { + // TODO: End user timing measure + } // eslint-disable-line brace-style + + // Unrecognized event + else { + throw new Error( + `Unrecognized event ${JSON.stringify( + event, + )}! This is likely a bug in this profiler tool.`, + ); + } +} + +function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart { + const parsedData = importFromChromeTimeline(rawData, 'react-devtools'); + const profile = parsedData.profiles[0]; // TODO: Choose the main CPU thread only + + const speedscopeFlamechart = new SpeedscopeFlamechart({ + getTotalWeight: profile.getTotalWeight.bind(profile), + forEachCall: profile.forEachCall.bind(profile), + formatValue: profile.formatValue.bind(profile), + getColorBucketForFrame: () => 0, + }); + + const flamechart: Flamechart = speedscopeFlamechart.getLayers().map(layer => + layer.map(({start, end, node: {frame: {name, file, line, col}}}) => ({ + name, + timestamp: start / 1000, + duration: (end - start) / 1000, + scriptUrl: file, + locationLine: line, + locationColumn: col, + })), + ); + + return flamechart; +} + +export default function preprocessData( + timeline: TimelineEvent[], +): ReactProfilerData { + const flamechart = preprocessFlamechart(timeline); + + const profilerData: ReactProfilerData = { + startTime: 0, + duration: 0, + events: [], + measures: [], + flamechart, + otherUserTimingMarks: [], + }; + + // Sort `timeline`. JSON Array Format trace events need not be ordered. See: + // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.f2f0yd51wi15 + timeline = timeline.filter(Boolean).sort((a, b) => (a.ts > b.ts ? 1 : -1)); + + // Events displayed in flamechart have timestamps relative to the profile + // event's startTime. Source: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1486 + // + // We'll thus expect there to be a 'Profile' event; if there is not one, we + // can deduce that there are no flame chart events. As we expect React + // scheduling profiling user timing marks to be recorded together with browser + // flame chart events, we can futher deduce that the data is invalid and we + // don't bother finding React events. + const indexOfProfileEvent = timeline.findIndex( + event => event.name === 'Profile', + ); + if (indexOfProfileEvent === -1) { + return profilerData; + } + + // Use Profile event's `startTime` as the start time to align with flame chart. + // TODO: Remove assumption that there'll only be 1 'Profile' event. If this + // assumption does not hold, the chart may start at the wrong time. + profilerData.startTime = timeline[indexOfProfileEvent].args.data.startTime; + profilerData.duration = + (timeline[timeline.length - 1].ts - profilerData.startTime) / 1000; + + const state: ProcessorState = { + batchUID: 0, + uidCounter: 0, + nextRenderShouldGenerateNewBatchID: true, + measureStack: [], + }; + + timeline.forEach(event => processTimelineEvent(event, profilerData, state)); + + // Validate that all events and measures are complete + const {measureStack} = state; + if (measureStack.length > 0) { + console.error('Incomplete events or measures', measureStack); + } + + return profilerData; +} diff --git a/packages/react-devtools-scheduling-profiler/src/utils/readInputData.js b/packages/react-devtools-scheduling-profiler/src/utils/readInputData.js new file mode 100644 index 0000000000..fe46337f0f --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/readInputData.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import nullthrows from 'nullthrows'; + +export const readInputData = (file: File): Promise => { + if (!file.name.endsWith('.json')) { + return Promise.reject( + new Error( + 'Invalid file type. Only JSON performance profiles are supported', + ), + ); + } + + const fileReader = new FileReader(); + + return new Promise((resolve, reject) => { + fileReader.onload = () => { + const result = nullthrows(fileReader.result); + if (typeof result === 'string') { + resolve(result); + } + reject(new Error('Input file was not read as a string')); + }; + + fileReader.onerror = () => reject(fileReader.error); + + fileReader.readAsText(file); + }); +}; diff --git a/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js b/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js new file mode 100644 index 0000000000..8acfd882cb --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {useLayoutEffect, useRef} from 'react'; + +const TOOLTIP_OFFSET = 4; + +export default function useSmartTooltip({ + mouseX, + mouseY, +}: { + mouseX: number, + mouseY: number, +}) { + const ref = useRef(null); + + useLayoutEffect(() => { + const element = ref.current; + if (element !== null) { + // Let's check the vertical position. + if ( + mouseY + TOOLTIP_OFFSET + element.offsetHeight >= + window.innerHeight + ) { + // The tooltip doesn't fit below the mouse cursor (which is our + // default strategy). Therefore we try to position it either above the + // mouse cursor or finally aligned with the window's top edge. + if (mouseY - TOOLTIP_OFFSET - element.offsetHeight > 0) { + // We position the tooltip above the mouse cursor if it fits there. + element.style.top = `${mouseY - + element.offsetHeight - + TOOLTIP_OFFSET}px`; + } else { + // Otherwise we align the tooltip with the window's top edge. + element.style.top = '0px'; + } + } else { + element.style.top = `${mouseY + TOOLTIP_OFFSET}px`; + } + + // Now let's check the horizontal position. + if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= window.innerWidth) { + // The tooltip doesn't fit at the right of the mouse cursor (which is + // our default strategy). Therefore we try to position it either at the + // left of the mouse cursor or finally aligned with the window's left + // edge. + if (mouseX - TOOLTIP_OFFSET - element.offsetWidth > 0) { + // We position the tooltip at the left of the mouse cursor if it fits + // there. + element.style.left = `${mouseX - + element.offsetWidth - + TOOLTIP_OFFSET}px`; + } else { + // Otherwise, align the tooltip with the window's left edge. + element.style.left = '0px'; + } + } else { + element.style.left = `${mouseX + TOOLTIP_OFFSET}px`; + } + } + }, [mouseX, mouseY, ref]); + + return ref; +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js new file mode 100644 index 0000000000..551814eab6 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Rect} from './geometry'; + +import {Surface} from './Surface'; +import {View} from './View'; + +/** + * View that fills its visible area with a CSS color. + */ +export class ColorView extends View { + _color: string; + + constructor(surface: Surface, frame: Rect, color: string) { + super(surface, frame); + this._color = color; + } + + setColor(color: string) { + if (this._color === color) { + return; + } + this._color = color; + this.setNeedsDisplay(); + } + + draw(context: CanvasRenderingContext2D) { + const {_color, visibleArea} = this; + context.fillStyle = _color; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js new file mode 100644 index 0000000000..3bed528ef6 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js @@ -0,0 +1,342 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Interaction, + MouseDownInteraction, + MouseMoveInteraction, + MouseUpInteraction, + WheelPlainInteraction, + WheelWithShiftInteraction, + WheelWithControlInteraction, + WheelWithMetaInteraction, +} from './useCanvasInteraction'; +import type {Rect} from './geometry'; + +import {Surface} from './Surface'; +import {View} from './View'; +import {rectContainsPoint} from './geometry'; +import {clamp} from './utils/clamp'; +import { + MIN_ZOOM_LEVEL, + MAX_ZOOM_LEVEL, + MOVE_WHEEL_DELTA_THRESHOLD, +} from './constants'; + +type HorizontalPanAndZoomState = $ReadOnly<{| + /** Horizontal offset; positive in the left direction */ + offsetX: number, + zoomLevel: number, +|}>; + +export type HorizontalPanAndZoomViewOnChangeCallback = ( + state: HorizontalPanAndZoomState, + view: HorizontalPanAndZoomView, +) => void; + +function panAndZoomStatesAreEqual( + state1: HorizontalPanAndZoomState, + state2: HorizontalPanAndZoomState, +): boolean { + return ( + state1.offsetX === state2.offsetX && state1.zoomLevel === state2.zoomLevel + ); +} + +function zoomLevelAndIntrinsicWidthToFrameWidth( + zoomLevel: number, + intrinsicWidth: number, +): number { + return intrinsicWidth * zoomLevel; +} + +export class HorizontalPanAndZoomView extends View { + _intrinsicContentWidth: number; + + _panAndZoomState: HorizontalPanAndZoomState = { + offsetX: 0, + zoomLevel: 0.25, + }; + + _isPanning = false; + + _onStateChange: HorizontalPanAndZoomViewOnChangeCallback = () => {}; + + constructor( + surface: Surface, + frame: Rect, + contentView: View, + intrinsicContentWidth: number, + onStateChange?: HorizontalPanAndZoomViewOnChangeCallback, + ) { + super(surface, frame); + this.addSubview(contentView); + this._intrinsicContentWidth = intrinsicContentWidth; + if (onStateChange) this._onStateChange = onStateChange; + } + + setFrame(newFrame: Rect) { + super.setFrame(newFrame); + + // Revalidate panAndZoomState + this._setStateAndInformCallbacksIfChanged(this._panAndZoomState); + } + + setPanAndZoomState(proposedState: HorizontalPanAndZoomState) { + this._setPanAndZoomState(proposedState); + } + + /** + * Just sets pan and zoom state. Use `_setStateAndInformCallbacksIfChanged` + * if this view's callbacks should also be called. + * + * @returns Whether state was changed + * @private + */ + _setPanAndZoomState(proposedState: HorizontalPanAndZoomState): boolean { + const clampedState = this._clampedProposedState(proposedState); + if (panAndZoomStatesAreEqual(clampedState, this._panAndZoomState)) { + return false; + } + this._panAndZoomState = clampedState; + this.setNeedsDisplay(); + return true; + } + + /** + * @private + */ + _setStateAndInformCallbacksIfChanged( + proposedState: HorizontalPanAndZoomState, + ) { + if (this._setPanAndZoomState(proposedState)) { + this._onStateChange(this._panAndZoomState, this); + } + } + + desiredSize() { + return this._contentView.desiredSize(); + } + + /** + * Reference to the content view. This view is also the only view in + * `this.subviews`. + */ + get _contentView() { + return this.subviews[0]; + } + + layoutSubviews() { + const {offsetX, zoomLevel} = this._panAndZoomState; + const proposedFrame = { + origin: { + x: this.frame.origin.x + offsetX, + y: this.frame.origin.y, + }, + size: { + width: zoomLevelAndIntrinsicWidthToFrameWidth( + zoomLevel, + this._intrinsicContentWidth, + ), + height: this.frame.size.height, + }, + }; + this._contentView.setFrame(proposedFrame); + super.layoutSubviews(); + } + + /** + * Zoom to a specific range of the content specified as a range of the + * content view's intrinsic content size. + * + * Does not inform callbacks of state change since this is a public API. + */ + zoomToRange(startX: number, endX: number) { + // Zoom and offset must be done separately, so that if the zoom level is + // clamped the offset will still be correct (unless it gets clamped too). + const zoomClampedState = this._clampedProposedStateZoomLevel({ + ...this._panAndZoomState, + // Let: + // I = intrinsic content width, i = zoom range = (endX - startX). + // W = contentView's final zoomed width, w = this view's width + // Goal: we want the visible width w to only contain the requested range i. + // Derivation: + // (1) i/I = w/W (by intuitive definition of variables) + // (2) W = zoomLevel * I (definition of zoomLevel) + // => zoomLevel = W/I (algebraic manipulation) + // = w/i (rearranging (1)) + zoomLevel: this.frame.size.width / (endX - startX), + }); + const offsetAdjustedState = this._clampedProposedStateOffsetX({ + ...zoomClampedState, + offsetX: -startX * zoomClampedState.zoomLevel, + }); + this._setPanAndZoomState(offsetAdjustedState); + } + + _handleMouseDown(interaction: MouseDownInteraction) { + if (rectContainsPoint(interaction.payload.location, this.frame)) { + this._isPanning = true; + } + } + + _handleMouseMove(interaction: MouseMoveInteraction) { + if (!this._isPanning) { + return; + } + const {offsetX} = this._panAndZoomState; + const {movementX} = interaction.payload.event; + this._setStateAndInformCallbacksIfChanged({ + ...this._panAndZoomState, + offsetX: offsetX + movementX, + }); + } + + _handleMouseUp(interaction: MouseUpInteraction) { + if (this._isPanning) { + this._isPanning = false; + } + } + + _handleWheelPlain(interaction: WheelPlainInteraction) { + const { + location, + delta: {deltaX, deltaY}, + } = interaction.payload; + if (!rectContainsPoint(location, this.frame)) { + return; // Not scrolling on view + } + + const absDeltaX = Math.abs(deltaX); + const absDeltaY = Math.abs(deltaY); + if (absDeltaY > absDeltaX) { + return; // Scrolling vertically + } + + if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) { + return; + } + + this._setStateAndInformCallbacksIfChanged({ + ...this._panAndZoomState, + offsetX: this._panAndZoomState.offsetX - deltaX, + }); + } + + _handleWheelZoom( + interaction: + | WheelWithShiftInteraction + | WheelWithControlInteraction + | WheelWithMetaInteraction, + ) { + const { + location, + delta: {deltaY}, + } = interaction.payload; + if (!rectContainsPoint(location, this.frame)) { + return; // Not scrolling on view + } + + const absDeltaY = Math.abs(deltaY); + if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) { + return; + } + + const zoomClampedState = this._clampedProposedStateZoomLevel({ + ...this._panAndZoomState, + zoomLevel: this._panAndZoomState.zoomLevel * (1 + 0.005 * -deltaY), + }); + + // Determine where the mouse is, and adjust the offset so that point stays + // centered after zooming. + const oldMouseXInFrame = location.x - zoomClampedState.offsetX; + const fractionalMouseX = + oldMouseXInFrame / this._contentView.frame.size.width; + const newContentWidth = zoomLevelAndIntrinsicWidthToFrameWidth( + zoomClampedState.zoomLevel, + this._intrinsicContentWidth, + ); + const newMouseXInFrame = fractionalMouseX * newContentWidth; + + const offsetAdjustedState = this._clampedProposedStateOffsetX({ + ...zoomClampedState, + offsetX: location.x - newMouseXInFrame, + }); + + this._setStateAndInformCallbacksIfChanged(offsetAdjustedState); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousedown': + this._handleMouseDown(interaction); + break; + case 'mousemove': + this._handleMouseMove(interaction); + break; + case 'mouseup': + this._handleMouseUp(interaction); + break; + case 'wheel-plain': + this._handleWheelPlain(interaction); + break; + case 'wheel-shift': + case 'wheel-control': + case 'wheel-meta': + this._handleWheelZoom(interaction); + break; + } + } + + /** + * @private + */ + _clampedProposedStateZoomLevel( + proposedState: HorizontalPanAndZoomState, + ): HorizontalPanAndZoomState { + // Content-based min zoom level to ensure that contentView's width >= our width. + const minContentBasedZoomLevel = + this.frame.size.width / this._intrinsicContentWidth; + const minZoomLevel = Math.max(MIN_ZOOM_LEVEL, minContentBasedZoomLevel); + return { + ...proposedState, + zoomLevel: clamp(minZoomLevel, MAX_ZOOM_LEVEL, proposedState.zoomLevel), + }; + } + + /** + * @private + */ + _clampedProposedStateOffsetX( + proposedState: HorizontalPanAndZoomState, + ): HorizontalPanAndZoomState { + const newContentWidth = zoomLevelAndIntrinsicWidthToFrameWidth( + proposedState.zoomLevel, + this._intrinsicContentWidth, + ); + return { + ...proposedState, + offsetX: clamp( + -(newContentWidth - this.frame.size.width), + 0, + proposedState.offsetX, + ), + }; + } + + /** + * @private + */ + _clampedProposedState( + proposedState: HorizontalPanAndZoomState, + ): HorizontalPanAndZoomState { + const zoomClampedState = this._clampedProposedStateZoomLevel(proposedState); + return this._clampedProposedStateOffsetX(zoomClampedState); + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js new file mode 100644 index 0000000000..314be68e28 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js @@ -0,0 +1,311 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Interaction, + MouseDownInteraction, + MouseMoveInteraction, + MouseUpInteraction, +} from './useCanvasInteraction'; +import type {Rect, Size} from './geometry'; + +import nullthrows from 'nullthrows'; +import {Surface} from './Surface'; +import {View} from './View'; +import {rectContainsPoint} from './geometry'; +import {layeredLayout, noopLayout} from './layouter'; +import {ColorView} from './ColorView'; +import {clamp} from './utils/clamp'; + +type ResizeBarState = 'normal' | 'hovered' | 'dragging'; + +type ResizingState = $ReadOnly<{| + /** Distance between top of resize bar and mouseY */ + cursorOffsetInBarFrame: number, + /** Mouse's vertical coordinates relative to canvas */ + mouseY: number, +|}>; + +type LayoutState = $ReadOnly<{| + /** Resize bar's vertical position relative to resize view's frame.origin.y */ + barOffsetY: number, +|}>; + +function getColorForBarState(state: ResizeBarState): string { + // Colors obtained from Firefox Profiler + switch (state) { + case 'normal': + return '#ccc'; + case 'hovered': + return '#bbb'; + case 'dragging': + return '#aaa'; + } + throw new Error(`Unknown resize bar state ${state}`); +} + +class ResizeBar extends View { + _intrinsicContentSize: Size = { + width: 0, + height: 5, + }; + + _interactionState: ResizeBarState = 'normal'; + + constructor(surface: Surface, frame: Rect) { + super(surface, frame, layeredLayout); + this.addSubview(new ColorView(surface, frame, '')); + this._updateColor(); + } + + desiredSize() { + return this._intrinsicContentSize; + } + + _getColorView(): ColorView { + return (this.subviews[0]: any); + } + + _updateColor() { + this._getColorView().setColor(getColorForBarState(this._interactionState)); + } + + _setInteractionState(state: ResizeBarState) { + if (this._interactionState === state) { + return; + } + this._interactionState = state; + this._updateColor(); + } + + _handleMouseDown(interaction: MouseDownInteraction) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (cursorInView) { + this._setInteractionState('dragging'); + } + } + + _handleMouseMove(interaction: MouseMoveInteraction) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (this._interactionState === 'dragging') { + return; + } + this._setInteractionState(cursorInView ? 'hovered' : 'normal'); + } + + _handleMouseUp(interaction: MouseUpInteraction) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (this._interactionState === 'dragging') { + this._setInteractionState(cursorInView ? 'hovered' : 'normal'); + } + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousedown': + this._handleMouseDown(interaction); + return; + case 'mousemove': + this._handleMouseMove(interaction); + return; + case 'mouseup': + this._handleMouseUp(interaction); + return; + } + } +} + +export class ResizableSplitView extends View { + _resizingState: ResizingState | null = null; + _layoutState: LayoutState; + + constructor( + surface: Surface, + frame: Rect, + topSubview: View, + bottomSubview: View, + ) { + super(surface, frame, noopLayout); + + this.addSubview(topSubview); + this.addSubview(new ResizeBar(surface, frame)); + this.addSubview(bottomSubview); + + const topSubviewDesiredSize = topSubview.desiredSize(); + this._layoutState = { + barOffsetY: topSubviewDesiredSize ? topSubviewDesiredSize.height : 0, + }; + } + + _getTopSubview(): View { + return this.subviews[0]; + } + + _getResizeBar(): View { + return this.subviews[1]; + } + + _getBottomSubview(): View { + return this.subviews[2]; + } + + _getResizeBarDesiredSize(): Size { + return nullthrows( + this._getResizeBar().desiredSize(), + 'Resize bar must have desired size', + ); + } + + desiredSize() { + const topSubviewDesiredSize = this._getTopSubview().desiredSize(); + const resizeBarDesiredSize = this._getResizeBarDesiredSize(); + const bottomSubviewDesiredSize = this._getBottomSubview().desiredSize(); + + const topSubviewDesiredWidth = topSubviewDesiredSize + ? topSubviewDesiredSize.width + : 0; + const bottomSubviewDesiredWidth = bottomSubviewDesiredSize + ? bottomSubviewDesiredSize.width + : 0; + + const topSubviewDesiredHeight = topSubviewDesiredSize + ? topSubviewDesiredSize.height + : 0; + const bottomSubviewDesiredHeight = bottomSubviewDesiredSize + ? bottomSubviewDesiredSize.height + : 0; + + return { + width: Math.max( + topSubviewDesiredWidth, + resizeBarDesiredSize.width, + bottomSubviewDesiredWidth, + ), + height: + topSubviewDesiredHeight + + resizeBarDesiredSize.height + + bottomSubviewDesiredHeight, + }; + } + + layoutSubviews() { + this._updateLayoutState(); + this._updateSubviewFrames(); + super.layoutSubviews(); + } + + _updateLayoutState() { + const {frame, visibleArea, _resizingState} = this; + + const resizeBarDesiredSize = this._getResizeBarDesiredSize(); + // Allow bar to travel to bottom of the visible area of this view but no further + const maxPossibleBarOffset = + visibleArea.size.height - resizeBarDesiredSize.height; + const topSubviewDesiredSize = this._getTopSubview().desiredSize(); + const maxBarOffset = topSubviewDesiredSize + ? Math.min(maxPossibleBarOffset, topSubviewDesiredSize.height) + : maxPossibleBarOffset; + + let proposedBarOffsetY = this._layoutState.barOffsetY; + // Update bar offset if dragging bar + if (_resizingState) { + const {mouseY, cursorOffsetInBarFrame} = _resizingState; + proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame; + } + + this._layoutState = { + ...this._layoutState, + barOffsetY: clamp(0, maxBarOffset, proposedBarOffsetY), + }; + } + + _updateSubviewFrames() { + const { + frame: { + origin: {x, y}, + size: {width, height}, + }, + _layoutState: {barOffsetY}, + } = this; + + const resizeBarDesiredSize = this._getResizeBarDesiredSize(); + + let currentY = y; + + this._getTopSubview().setFrame({ + origin: {x, y: currentY}, + size: {width, height: barOffsetY}, + }); + currentY += this._getTopSubview().frame.size.height; + + this._getResizeBar().setFrame({ + origin: {x, y: currentY}, + size: {width, height: resizeBarDesiredSize.height}, + }); + currentY += this._getResizeBar().frame.size.height; + + this._getBottomSubview().setFrame({ + origin: {x, y: currentY}, + // Fill remaining height + size: {width, height: height + y - currentY}, + }); + } + + _handleMouseDown(interaction: MouseDownInteraction) { + const cursorLocation = interaction.payload.location; + const resizeBarFrame = this._getResizeBar().frame; + if (rectContainsPoint(cursorLocation, resizeBarFrame)) { + const mouseY = cursorLocation.y; + this._resizingState = { + cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y, + mouseY, + }; + } + } + + _handleMouseMove(interaction: MouseMoveInteraction) { + const {_resizingState} = this; + if (_resizingState) { + this._resizingState = { + ..._resizingState, + mouseY: interaction.payload.location.y, + }; + this.setNeedsDisplay(); + } + } + + _handleMouseUp(interaction: MouseUpInteraction) { + if (this._resizingState) { + this._resizingState = null; + } + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousedown': + this._handleMouseDown(interaction); + return; + case 'mousemove': + this._handleMouseMove(interaction); + return; + case 'mouseup': + this._handleMouseUp(interaction); + return; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js new file mode 100644 index 0000000000..eb4285da7b --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Interaction} from './useCanvasInteraction'; +import type {Size} from './geometry'; + +import memoize from 'memoize-one'; + +import {View} from './View'; +import {zeroPoint} from './geometry'; + +// hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/ +function configureRetinaCanvas(canvas, height, width) { + const dpr: number = window.devicePixelRatio || 1; + canvas.width = width * dpr; + canvas.height = height * dpr; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + return dpr; +} + +const getCanvasContext = memoize( + ( + canvas: HTMLCanvasElement, + height: number, + width: number, + scaleCanvas: boolean = true, + ): CanvasRenderingContext2D => { + const context = canvas.getContext('2d', {alpha: false}); + if (scaleCanvas) { + const dpr = configureRetinaCanvas(canvas, height, width); + // Scale all drawing operations by the dpr, so you don't have to worry about the difference. + context.scale(dpr, dpr); + } + return context; + }, +); + +/** + * Represents the canvas surface and a view heirarchy. A surface is also the + * place where all interactions enter the view heirarchy. + */ +export class Surface { + rootView: ?View; + _context: ?CanvasRenderingContext2D; + _canvasSize: ?Size; + + setCanvas(canvas: HTMLCanvasElement, canvasSize: Size) { + this._context = getCanvasContext( + canvas, + canvasSize.height, + canvasSize.width, + ); + this._canvasSize = canvasSize; + + if (this.rootView) { + this.rootView.setNeedsDisplay(); + } + } + + displayIfNeeded() { + const {rootView, _canvasSize, _context} = this; + if (!rootView || !_context || !_canvasSize) { + return; + } + rootView.setFrame({ + origin: zeroPoint, + size: _canvasSize, + }); + rootView.setVisibleArea({ + origin: zeroPoint, + size: _canvasSize, + }); + rootView.displayIfNeeded(_context); + } + + handleInteraction(interaction: Interaction) { + if (!this.rootView) { + return; + } + this.rootView.handleInteractionAndPropagateToSubviews(interaction); + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js new file mode 100644 index 0000000000..24a0612fc9 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -0,0 +1,181 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Interaction, + MouseDownInteraction, + MouseMoveInteraction, + MouseUpInteraction, + WheelPlainInteraction, +} from './useCanvasInteraction'; +import type {Rect} from './geometry'; + +import {Surface} from './Surface'; +import {View} from './View'; +import {rectContainsPoint} from './geometry'; +import {clamp} from './utils/clamp'; +import {MOVE_WHEEL_DELTA_THRESHOLD} from './constants'; + +type VerticalScrollState = $ReadOnly<{| + offsetY: number, +|}>; + +function scrollStatesAreEqual( + state1: VerticalScrollState, + state2: VerticalScrollState, +): boolean { + return state1.offsetY === state2.offsetY; +} + +export class VerticalScrollView extends View { + _scrollState: VerticalScrollState = { + offsetY: 0, + }; + + _isPanning = false; + + constructor(surface: Surface, frame: Rect, contentView: View) { + super(surface, frame); + this.addSubview(contentView); + } + + setFrame(newFrame: Rect) { + super.setFrame(newFrame); + + // Revalidate scrollState + this._updateState(this._scrollState); + } + + desiredSize() { + return this._contentView.desiredSize(); + } + + /** + * Reference to the content view. This view is also the only view in + * `this.subviews`. + */ + get _contentView() { + return this.subviews[0]; + } + + layoutSubviews() { + const {offsetY} = this._scrollState; + const desiredSize = this._contentView.desiredSize(); + + const minimumHeight = this.frame.size.height; + const desiredHeight = desiredSize ? desiredSize.height : 0; + // Force view to take up at least all remaining vertical space. + const height = Math.max(desiredHeight, minimumHeight); + + const proposedFrame = { + origin: { + x: this.frame.origin.x, + y: this.frame.origin.y + offsetY, + }, + size: { + width: this.frame.size.width, + height, + }, + }; + this._contentView.setFrame(proposedFrame); + super.layoutSubviews(); + } + + _handleMouseDown(interaction: MouseDownInteraction) { + if (rectContainsPoint(interaction.payload.location, this.frame)) { + this._isPanning = true; + } + } + + _handleMouseMove(interaction: MouseMoveInteraction) { + if (!this._isPanning) { + return; + } + const {offsetY} = this._scrollState; + const {movementY} = interaction.payload.event; + this._updateState({ + ...this._scrollState, + offsetY: offsetY + movementY, + }); + } + + _handleMouseUp(interaction: MouseUpInteraction) { + if (this._isPanning) { + this._isPanning = false; + } + } + + _handleWheelPlain(interaction: WheelPlainInteraction) { + const { + location, + delta: {deltaX, deltaY}, + } = interaction.payload; + if (!rectContainsPoint(location, this.frame)) { + return; // Not scrolling on view + } + + const absDeltaX = Math.abs(deltaX); + const absDeltaY = Math.abs(deltaY); + if (absDeltaX > absDeltaY) { + return; // Scrolling horizontally + } + + if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) { + return; + } + + this._updateState({ + ...this._scrollState, + offsetY: this._scrollState.offsetY - deltaY, + }); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousedown': + this._handleMouseDown(interaction); + break; + case 'mousemove': + this._handleMouseMove(interaction); + break; + case 'mouseup': + this._handleMouseUp(interaction); + break; + case 'wheel-plain': + this._handleWheelPlain(interaction); + break; + } + } + + /** + * @private + */ + _updateState(proposedState: VerticalScrollState) { + const clampedState = this._clampedProposedState(proposedState); + if (!scrollStatesAreEqual(clampedState, this._scrollState)) { + this._scrollState = clampedState; + this.setNeedsDisplay(); + } + } + + /** + * @private + */ + _clampedProposedState( + proposedState: VerticalScrollState, + ): VerticalScrollState { + return { + offsetY: clamp( + -(this._contentView.frame.size.height - this.frame.size.height), + 0, + proposedState.offsetY, + ), + }; + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/View.js b/packages/react-devtools-scheduling-profiler/src/view-base/View.js new file mode 100644 index 0000000000..d92a55b6fe --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/View.js @@ -0,0 +1,274 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Interaction} from './useCanvasInteraction'; +import type {Rect, Size} from './geometry'; +import type {Layouter} from './layouter'; + +import {Surface} from './Surface'; +import { + rectEqualToRect, + intersectionOfRects, + rectIntersectsRect, + sizeIsEmpty, + sizeIsValid, + unionOfRects, + zeroRect, +} from './geometry'; +import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter'; + +/** + * Base view class that can be subclassed to draw custom content or manage + * subclasses. + */ +export class View { + surface: Surface; + + frame: Rect; + visibleArea: Rect; + + superview: ?View; + subviews: View[] = []; + + /** + * An injected function that lays out our subviews. + * @private + */ + _layouter: Layouter; + + /** + * Whether this view needs to be drawn. + * + * NOTE: Do not set directly! Use `setNeedsDisplay`. + * + * @see setNeedsDisplay + * @private + */ + _needsDisplay = true; + + /** + * Whether the heirarchy below this view has subviews that need display. + * + * NOTE: Do not set directly! Use `setSubviewsNeedDisplay`. + * + * @see setSubviewsNeedDisplay + * @private + */ + _subviewsNeedDisplay = false; + + constructor( + surface: Surface, + frame: Rect, + layouter: Layouter = noopLayout, + visibleArea: Rect = frame, + ) { + this.surface = surface; + this.frame = frame; + this._layouter = layouter; + this.visibleArea = visibleArea; + } + + /** + * Invalidates view's contents. + * + * Downward propagating; once called, all subviews of this view should also + * be invalidated. + */ + setNeedsDisplay() { + this._needsDisplay = true; + if (this.superview) { + this.superview._setSubviewsNeedDisplay(); + } + this.subviews.forEach(subview => subview.setNeedsDisplay()); + } + + /** + * Informs superview that it has subviews that need to be drawn. + * + * Upward propagating; once called, all superviews of this view should also + * have `subviewsNeedDisplay` = true. + * + * @private + */ + _setSubviewsNeedDisplay() { + this._subviewsNeedDisplay = true; + if (this.superview) { + this.superview._setSubviewsNeedDisplay(); + } + } + + setFrame(newFrame: Rect) { + if (!rectEqualToRect(this.frame, newFrame)) { + this.frame = newFrame; + if (sizeIsValid(newFrame.size)) { + this.frame = newFrame; + } else { + this.frame = zeroRect; + } + this.setNeedsDisplay(); + } + } + + setVisibleArea(newVisibleArea: Rect) { + if (!rectEqualToRect(this.visibleArea, newVisibleArea)) { + if (sizeIsValid(newVisibleArea.size)) { + this.visibleArea = newVisibleArea; + } else { + this.visibleArea = zeroRect; + } + this.setNeedsDisplay(); + } + } + + /** + * A size that can be used as a hint by layout functions. + * + * Implementations should typically return the intrinsic content size or a + * size that fits all the view's content. + * + * The default implementation returns a size that fits all the view's + * subviews. + * + * Can be overridden by subclasses. + */ + desiredSize(): ?Size { + if (this._needsDisplay) { + this.layoutSubviews(); + } + const frames = this.subviews.map(subview => subview.frame); + return unionOfRects(...frames).size; + } + + /** + * Appends `view` to the list of this view's `subviews`. + */ + addSubview(view: View) { + if (this.subviews.includes(view)) { + return; + } + this.subviews.push(view); + view.superview = this; + } + + /** + * Breaks the subview-superview relationship between `view` and this view, if + * `view` is a subview of this view. + */ + removeSubview(view: View) { + const subviewIndex = this.subviews.indexOf(view); + if (subviewIndex === -1) { + return; + } + view.superview = undefined; + this.subviews.splice(subviewIndex, 1); + } + + /** + * Removes all subviews from this view. + */ + removeAllSubviews() { + this.subviews.forEach(subview => (subview.superview = undefined)); + this.subviews = []; + } + + /** + * Executes the display flow if this view needs to be drawn. + * + * 1. Lays out subviews with `layoutSubviews`. + * 2. Draws content with `draw`. + */ + displayIfNeeded(context: CanvasRenderingContext2D) { + if ( + (this._needsDisplay || this._subviewsNeedDisplay) && + rectIntersectsRect(this.frame, this.visibleArea) && + !sizeIsEmpty(this.visibleArea.size) + ) { + this.layoutSubviews(); + if (this._needsDisplay) this._needsDisplay = false; + if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false; + this.draw(context); + } + } + + /** + * Layout self and subviews. + * + * Implementations should call `setNeedsDisplay` if a draw is required. + * + * The default implementation uses `this.layouter` to lay out subviews. + * + * Can be overwritten by subclasses that wish to manually manage their + * subviews' layout. + * + * NOTE: Do not call directly! Use `displayIfNeeded`. + * + * @see displayIfNeeded + */ + layoutSubviews() { + const {frame, _layouter, subviews, visibleArea} = this; + const existingLayout = viewsToLayout(subviews); + const newLayout = _layouter(existingLayout, frame); + collapseLayoutIntoViews(newLayout); + + subviews.forEach((subview, subviewIndex) => { + if (rectIntersectsRect(visibleArea, subview.frame)) { + subview.setVisibleArea(intersectionOfRects(visibleArea, subview.frame)); + } else { + subview.setVisibleArea(zeroRect); + } + }); + } + + /** + * Draw the contents of this view in the given canvas `context`. + * + * Defaults to drawing this view's `subviews`. + * + * To be overwritten by subclasses that wish to draw custom content. + * + * NOTE: Do not call directly! Use `displayIfNeeded`. + * + * @see displayIfNeeded + */ + draw(context: CanvasRenderingContext2D) { + const {subviews, visibleArea} = this; + subviews.forEach(subview => { + if (rectIntersectsRect(visibleArea, subview.visibleArea)) { + subview.displayIfNeeded(context); + } + }); + } + + /** + * Handle an `interaction`. + * + * To be overwritten by subclasses that wish to handle interactions. + */ + // Internal note: Do not call directly! Use + // `handleInteractionAndPropagateToSubviews` so that interactions are + // propagated to subviews. + handleInteraction(interaction: Interaction) {} + + /** + * Handle an `interaction` and propagates it to all of this view's + * `subviews`. + * + * NOTE: Should not be overridden! Subclasses should override + * `handleInteraction` instead. + * + * @see handleInteraction + * @protected + */ + handleInteractionAndPropagateToSubviews(interaction: Interaction) { + this.handleInteraction(interaction); + this.subviews.forEach(subview => + subview.handleInteractionAndPropagateToSubviews(interaction), + ); + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/__tests__/geometry-test.js b/packages/react-devtools-scheduling-profiler/src/view-base/__tests__/geometry-test.js new file mode 100644 index 0000000000..2042651b69 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/__tests__/geometry-test.js @@ -0,0 +1,272 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import { + pointEqualToPoint, + sizeEqualToSize, + rectEqualToRect, + sizeIsValid, + sizeIsEmpty, + rectIntersectsRect, + intersectionOfRects, + rectContainsPoint, + unionOfRects, +} from '../geometry'; + +describe(pointEqualToPoint, () => { + it('should return true when 2 points have the same values', () => { + expect(pointEqualToPoint({x: 1, y: 1}, {x: 1, y: 1})).toBe(true); + expect(pointEqualToPoint({x: -1, y: 2}, {x: -1, y: 2})).toBe(true); + expect( + pointEqualToPoint({x: 3.14159, y: 0.26535}, {x: 3.14159, y: 0.26535}), + ).toBe(true); + }); + + it('should return false when 2 points have different values', () => { + expect(pointEqualToPoint({x: 1, y: 1}, {x: 1, y: 0})).toBe(false); + expect(pointEqualToPoint({x: -1, y: 2}, {x: 0, y: 1})).toBe(false); + expect( + pointEqualToPoint({x: 3.1416, y: 0.26534}, {x: 3.14159, y: 0.26535}), + ).toBe(false); + }); +}); + +describe(sizeEqualToSize, () => { + it('should return true when 2 sizes have the same values', () => { + expect(sizeEqualToSize({width: 1, height: 1}, {width: 1, height: 1})).toBe( + true, + ); + expect( + sizeEqualToSize({width: -1, height: 2}, {width: -1, height: 2}), + ).toBe(true); + expect( + sizeEqualToSize( + {width: 3.14159, height: 0.26535}, + {width: 3.14159, height: 0.26535}, + ), + ).toBe(true); + }); + + it('should return false when 2 sizes have different values', () => { + expect(sizeEqualToSize({width: 1, height: 1}, {width: 1, height: 0})).toBe( + false, + ); + expect(sizeEqualToSize({width: -1, height: 2}, {width: 0, height: 1})).toBe( + false, + ); + expect( + sizeEqualToSize( + {width: 3.1416, height: 0.26534}, + {width: 3.14159, height: 0.26535}, + ), + ).toBe(false); + }); +}); + +describe(rectEqualToRect, () => { + it('should return true when 2 rects have the same values', () => { + expect( + rectEqualToRect( + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}}, + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}}, + ), + ).toBe(true); + expect( + rectEqualToRect( + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}}, + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}}, + ), + ).toBe(true); + }); + + it('should return false when 2 rects have different values', () => { + expect( + rectEqualToRect( + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}}, + {origin: {x: 0, y: 1}, size: {width: 1, height: 1}}, + ), + ).toBe(false); + expect( + rectEqualToRect( + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}}, + {origin: {x: 1, y: 2}, size: {width: 3.15, height: 4}}, + ), + ).toBe(false); + }); +}); + +describe(sizeIsValid, () => { + it('should return true when the size has non-negative width and height', () => { + expect(sizeIsValid({width: 1, height: 1})).toBe(true); + expect(sizeIsValid({width: 0, height: 0})).toBe(true); + }); + + it('should return false when the size has negative width or height', () => { + expect(sizeIsValid({width: 0, height: -1})).toBe(false); + expect(sizeIsValid({width: -1, height: 0})).toBe(false); + expect(sizeIsValid({width: -1, height: -1})).toBe(false); + }); +}); + +describe(sizeIsEmpty, () => { + it('should return true when the size has negative area', () => { + expect(sizeIsEmpty({width: 1, height: -1})).toBe(true); + expect(sizeIsEmpty({width: -1, height: -1})).toBe(true); + }); + + it('should return true when the size has zero area', () => { + expect(sizeIsEmpty({width: 0, height: 0})).toBe(true); + expect(sizeIsEmpty({width: 0, height: 1})).toBe(true); + expect(sizeIsEmpty({width: 1, height: 0})).toBe(true); + }); + + it('should return false when the size has positive area', () => { + expect(sizeIsEmpty({width: 1, height: 1})).toBe(false); + expect(sizeIsEmpty({width: 2, height: 1})).toBe(false); + }); +}); + +describe(rectIntersectsRect, () => { + it('should return true when 2 rects intersect', () => { + // Rects touch + expect( + rectIntersectsRect( + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}}, + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}}, + ), + ).toEqual(true); + + // Rects overlap + expect( + rectIntersectsRect( + {origin: {x: 0, y: 0}, size: {width: 2, height: 1}}, + {origin: {x: 1, y: -2}, size: {width: 0.5, height: 5}}, + ), + ).toEqual(true); + + // Rects are equal + expect( + rectIntersectsRect( + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}}, + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}}, + ), + ).toEqual(true); + }); + + it('should return false when 2 rects do not intersect', () => { + expect( + rectIntersectsRect( + {origin: {x: 0, y: 1}, size: {width: 1, height: 1}}, + {origin: {x: 0, y: 10}, size: {width: 1, height: 1}}, + ), + ).toBe(false); + expect( + rectIntersectsRect( + {origin: {x: 1, y: 2}, size: {width: 3.14, height: 4}}, + {origin: {x: -4, y: 2}, size: {width: 3.15, height: 4}}, + ), + ).toBe(false); + }); +}); + +describe(intersectionOfRects, () => { + // NOTE: Undefined behavior if rects do not intersect + + it('should return intersection when 2 rects intersect', () => { + // Rects touch + expect( + intersectionOfRects( + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}}, + {origin: {x: 1, y: 1}, size: {width: 1, height: 1}}, + ), + ).toEqual({origin: {x: 1, y: 1}, size: {width: 0, height: 0}}); + + // Rects overlap + expect( + intersectionOfRects( + {origin: {x: 0, y: 0}, size: {width: 2, height: 1}}, + {origin: {x: 1, y: -2}, size: {width: 0.5, height: 5}}, + ), + ).toEqual({origin: {x: 1, y: 0}, size: {width: 0.5, height: 1}}); + + // Rects are equal + expect( + intersectionOfRects( + {origin: {x: 1, y: 2}, size: {width: 9.24, height: 4}}, + {origin: {x: 1, y: 2}, size: {width: 9.24, height: 4}}, + ), + ).toEqual({origin: {x: 1, y: 2}, size: {width: 9.24, height: 4}}); + }); +}); + +describe(rectContainsPoint, () => { + it("should return true if point is on the rect's edge", () => { + expect( + rectContainsPoint( + {x: 0, y: 0}, + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}}, + ), + ).toBe(true); + expect( + rectContainsPoint( + {x: 5, y: 0}, + {origin: {x: 0, y: 0}, size: {width: 10, height: 1}}, + ), + ).toBe(true); + expect( + rectContainsPoint( + {x: 1, y: 1}, + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}}, + ), + ).toBe(true); + }); + + it('should return true if point is in rect', () => { + expect( + rectContainsPoint( + {x: 5, y: 50}, + {origin: {x: 0, y: 0}, size: {width: 10, height: 100}}, + ), + ).toBe(true); + }); + + it('should return false if point is not in rect', () => { + expect( + rectContainsPoint( + {x: -1, y: 0}, + {origin: {x: 0, y: 0}, size: {width: 1, height: 1}}, + ), + ).toBe(false); + }); +}); + +describe(unionOfRects, () => { + it('should return zero rect if no rects are provided', () => { + expect(unionOfRects()).toEqual({ + origin: {x: 0, y: 0}, + size: {width: 0, height: 0}, + }); + }); + + it('should return rect if 1 rect is provided', () => { + expect( + unionOfRects({origin: {x: 1, y: 2}, size: {width: 3, height: 4}}), + ).toEqual({origin: {x: 1, y: 2}, size: {width: 3, height: 4}}); + }); + + it('should return union of rects if more than one rect is provided', () => { + expect( + unionOfRects( + {origin: {x: 1, y: 2}, size: {width: 3, height: 4}}, + {origin: {x: 100, y: 200}, size: {width: 3, height: 4}}, + {origin: {x: -10, y: -20}, size: {width: 50, height: 60}}, + ), + ).toEqual({origin: {x: -10, y: -20}, size: {width: 113, height: 224}}); + }); +}); diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/constants.js b/packages/react-devtools-scheduling-profiler/src/view-base/constants.js new file mode 100644 index 0000000000..0886dc024a --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/constants.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export const MOVE_WHEEL_DELTA_THRESHOLD = 1; +export const ZOOM_WHEEL_DELTA_THRESHOLD = 1; +export const MIN_ZOOM_LEVEL = 0.25; +export const MAX_ZOOM_LEVEL = 1000; diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js b/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js new file mode 100644 index 0000000000..b027b708f0 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type Point = $ReadOnly<{|x: number, y: number|}>; +export type Size = $ReadOnly<{|width: number, height: number|}>; +export type Rect = $ReadOnly<{|origin: Point, size: Size|}>; + +/** + * Alternative representation of `Rect`. + * A tuple of (`top`, `right`, `bottom`, `left`) coordinates. + */ +type Box = [number, number, number, number]; + +export const zeroPoint: Point = Object.freeze({x: 0, y: 0}); +export const zeroSize: Size = Object.freeze({width: 0, height: 0}); +export const zeroRect: Rect = Object.freeze({ + origin: zeroPoint, + size: zeroSize, +}); + +export function pointEqualToPoint(point1: Point, point2: Point): boolean { + return point1.x === point2.x && point1.y === point2.y; +} + +export function sizeEqualToSize(size1: Size, size2: Size): boolean { + return size1.width === size2.width && size1.height === size2.height; +} + +export function rectEqualToRect(rect1: Rect, rect2: Rect): boolean { + return ( + pointEqualToPoint(rect1.origin, rect2.origin) && + sizeEqualToSize(rect1.size, rect2.size) + ); +} + +export function sizeIsValid({width, height}: Size): boolean { + return width >= 0 && height >= 0; +} + +export function sizeIsEmpty({width, height}: Size): boolean { + return width <= 0 || height <= 0; +} + +function rectToBox(rect: Rect): Box { + const top = rect.origin.y; + const right = rect.origin.x + rect.size.width; + const bottom = rect.origin.y + rect.size.height; + const left = rect.origin.x; + return [top, right, bottom, left]; +} + +function boxToRect(box: Box): Rect { + const [top, right, bottom, left] = box; + return { + origin: { + x: left, + y: top, + }, + size: { + width: right - left, + height: bottom - top, + }, + }; +} + +export function rectIntersectsRect(rect1: Rect, rect2: Rect): boolean { + const [top1, right1, bottom1, left1] = rectToBox(rect1); + const [top2, right2, bottom2, left2] = rectToBox(rect2); + return !( + right1 < left2 || + right2 < left1 || + bottom1 < top2 || + bottom2 < top1 + ); +} + +/** + * Returns the intersection of the 2 rectangles. + * + * Prerequisite: `rect1` must intersect with `rect2`. + */ +export function intersectionOfRects(rect1: Rect, rect2: Rect): Rect { + const [top1, right1, bottom1, left1] = rectToBox(rect1); + const [top2, right2, bottom2, left2] = rectToBox(rect2); + return boxToRect([ + Math.max(top1, top2), + Math.min(right1, right2), + Math.min(bottom1, bottom2), + Math.max(left1, left2), + ]); +} + +export function rectContainsPoint({x, y}: Point, rect: Rect): boolean { + const [top, right, bottom, left] = rectToBox(rect); + return left <= x && x <= right && top <= y && y <= bottom; +} + +/** + * Returns the smallest rectangle that contains all provided rects. + * + * @returns Union of `rects`. If `rects` is empty, returns `zeroRect`. + */ +export function unionOfRects(...rects: Rect[]): Rect { + if (rects.length === 0) { + return zeroRect; + } + + const [firstRect, ...remainingRects] = rects; + const boxUnion = remainingRects + .map(rectToBox) + .reduce((intermediateUnion, nextBox): Box => { + const [unionTop, unionRight, unionBottom, unionLeft] = intermediateUnion; + const [nextTop, nextRight, nextBottom, nextLeft] = nextBox; + return [ + Math.min(unionTop, nextTop), + Math.max(unionRight, nextRight), + Math.max(unionBottom, nextBottom), + Math.min(unionLeft, nextLeft), + ]; + }, rectToBox(firstRect)); + return boxToRect(boxUnion); +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/index.js b/packages/react-devtools-scheduling-profiler/src/view-base/index.js new file mode 100644 index 0000000000..9d432bed57 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/index.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './ColorView'; +export * from './HorizontalPanAndZoomView'; +export * from './ResizableSplitView'; +export * from './Surface'; +export * from './VerticalScrollView'; +export * from './View'; +export * from './geometry'; +export * from './layouter'; +export * from './useCanvasInteraction'; diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js new file mode 100644 index 0000000000..ca5ba1ebfd --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js @@ -0,0 +1,269 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Rect} from './geometry'; +import type {View} from './View'; + +export type LayoutInfo = {|view: View, frame: Rect|}; +export type Layout = LayoutInfo[]; + +/** + * A function that takes a list of subviews, currently laid out in + * `existingLayout`, and lays them out into `containingFrame`. + */ +export type Layouter = ( + existingLayout: Layout, + containingFrame: Rect, +) => Layout; + +function viewToLayoutInfo(view: View): LayoutInfo { + return {view, frame: view.frame}; +} + +export function viewsToLayout(views: View[]): Layout { + return views.map(viewToLayoutInfo); +} + +/** + * Applies `layout`'s `frame`s to its corresponding `view`. + */ +export function collapseLayoutIntoViews(layout: Layout) { + layout.forEach(({view, frame}) => view.setFrame(frame)); +} + +/** + * A no-operation layout; does not modify the layout. + */ +export const noopLayout: Layouter = layout => layout; + +/** + * Layer views on top of each other. All views' frames will be set to + * `containerFrame`. + * + * Equivalent to composing: + * - `alignToContainerXLayout`, + * - `alignToContainerYLayout`, + * - `containerWidthLayout`, and + * - `containerHeightLayout`. + */ +export const layeredLayout: Layouter = (layout, containerFrame) => + layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame})); + +/** + * Stacks `views` vertically in `frame`. All views in `views` will have their + * widths set to the frame's width. + */ +export const verticallyStackedLayout: Layouter = (layout, containerFrame) => { + let currentY = containerFrame.origin.y; + return layout.map(layoutInfo => { + const desiredSize = layoutInfo.view.desiredSize(); + const height = desiredSize + ? desiredSize.height + : containerFrame.origin.y + containerFrame.size.height - currentY; + const proposedFrame = { + origin: {x: containerFrame.origin.x, y: currentY}, + size: {width: containerFrame.size.width, height}, + }; + currentY += height; + return { + ...layoutInfo, + frame: proposedFrame, + }; + }); +}; + +/** + * A layouter that aligns all frames' lefts to the container frame's left. + */ +export const alignToContainerXLayout: Layouter = (layout, containerFrame) => { + return layout.map(layoutInfo => ({ + ...layoutInfo, + frame: { + origin: { + x: containerFrame.origin.x, + y: layoutInfo.frame.origin.y, + }, + size: layoutInfo.frame.size, + }, + })); +}; + +/** + * A layouter that aligns all frames' tops to the container frame's top. + */ +export const alignToContainerYLayout: Layouter = (layout, containerFrame) => { + return layout.map(layoutInfo => ({ + ...layoutInfo, + frame: { + origin: { + x: layoutInfo.frame.origin.x, + y: containerFrame.origin.y, + }, + size: layoutInfo.frame.size, + }, + })); +}; + +/** + * A layouter that sets all frames' widths to `containerFrame.size.width`. + */ +export const containerWidthLayout: Layouter = (layout, containerFrame) => { + return layout.map(layoutInfo => ({ + ...layoutInfo, + frame: { + origin: layoutInfo.frame.origin, + size: { + width: containerFrame.size.width, + height: layoutInfo.frame.size.height, + }, + }, + })); +}; + +/** + * A layouter that sets all frames' heights to `containerFrame.size.height`. + */ +export const containerHeightLayout: Layouter = (layout, containerFrame) => { + return layout.map(layoutInfo => ({ + ...layoutInfo, + frame: { + origin: layoutInfo.frame.origin, + size: { + width: layoutInfo.frame.size.width, + height: containerFrame.size.height, + }, + }, + })); +}; + +/** + * A layouter that sets all frames' heights to the desired height of its view. + * If the view has no desired size, the frame's height is set to 0. + */ +export const desiredHeightLayout: Layouter = layout => { + return layout.map(layoutInfo => { + const desiredSize = layoutInfo.view.desiredSize(); + const height = desiredSize ? desiredSize.height : 0; + return { + ...layoutInfo, + frame: { + origin: layoutInfo.frame.origin, + size: { + width: layoutInfo.frame.size.width, + height, + }, + }, + }; + }); +}; + +/** + * A layouter that sets all frames' heights to the height of the tallest frame. + */ +export const uniformMaxSubviewHeightLayout: Layouter = layout => { + const maxHeight = Math.max( + ...layout.map(layoutInfo => layoutInfo.frame.size.height), + ); + return layout.map(layoutInfo => ({ + ...layoutInfo, + frame: { + origin: layoutInfo.frame.origin, + size: { + width: layoutInfo.frame.size.width, + height: maxHeight, + }, + }, + })); +}; + +/** + * A layouter that sets heights in this fashion: + * - If a frame's height >= `containerFrame.size.height`, the frame is left unchanged. + * - Otherwise, sets the frame's height to `containerFrame.size.height`. + */ +export const atLeastContainerHeightLayout: Layouter = ( + layout, + containerFrame, +) => { + return layout.map(layoutInfo => ({ + ...layoutInfo, + frame: { + origin: layoutInfo.frame.origin, + size: { + width: layoutInfo.frame.size.width, + height: Math.max( + containerFrame.size.height, + layoutInfo.frame.size.height, + ), + }, + }, + })); +}; + +/** + * Forces last view to take up the space below the second-last view. + * Intended to be used with a vertical stack layout. + */ +export const lastViewTakesUpRemainingSpaceLayout: Layouter = ( + layout, + containerFrame, +) => { + if (layout.length === 0) { + // Nothing to do + return layout; + } + + if (layout.length === 1) { + // No second-last view; the view should just take up the container height + return containerHeightLayout(layout, containerFrame); + } + + const layoutInfoToPassThrough = layout.slice(0, layout.length - 1); + const secondLastLayoutInfo = + layoutInfoToPassThrough[layoutInfoToPassThrough.length - 1]; + + const remainingHeight = + containerFrame.size.height - + secondLastLayoutInfo.frame.origin.y - + secondLastLayoutInfo.frame.size.height; + const height = Math.max(remainingHeight, 0); // Prevent negative heights + + const lastLayoutInfo = layout[layout.length - 1]; + return [ + ...layoutInfoToPassThrough, + { + ...lastLayoutInfo, + frame: { + origin: lastLayoutInfo.frame.origin, + size: { + width: lastLayoutInfo.frame.size.width, + height, + }, + }, + }, + ]; +}; + +/** + * Create a layouter that applies each layouter in `layouters` in sequence. + */ +export function createComposedLayout(...layouters: Layouter[]): Layouter { + if (layouters.length === 0) { + return noopLayout; + } + + const composedLayout: Layouter = (layout, containerFrame) => { + return layouters.reduce( + (intermediateLayout, layouter) => + layouter(intermediateLayout, containerFrame), + layout, + ); + }; + return composedLayout; +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js new file mode 100644 index 0000000000..b08d9bbbbb --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js @@ -0,0 +1,192 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {NormalizedWheelDelta} from './utils/normalizeWheel'; +import type {Point} from './geometry'; + +import {useEffect} from 'react'; +import {normalizeWheel} from './utils/normalizeWheel'; + +export type MouseDownInteraction = {| + type: 'mousedown', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; +export type MouseMoveInteraction = {| + type: 'mousemove', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; +export type MouseUpInteraction = {| + type: 'mouseup', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; +export type WheelPlainInteraction = {| + type: 'wheel-plain', + payload: {| + event: WheelEvent, + location: Point, + delta: NormalizedWheelDelta, + |}, +|}; +export type WheelWithShiftInteraction = {| + type: 'wheel-shift', + payload: {| + event: WheelEvent, + location: Point, + delta: NormalizedWheelDelta, + |}, +|}; +export type WheelWithControlInteraction = {| + type: 'wheel-control', + payload: {| + event: WheelEvent, + location: Point, + delta: NormalizedWheelDelta, + |}, +|}; +export type WheelWithMetaInteraction = {| + type: 'wheel-meta', + payload: {| + event: WheelEvent, + location: Point, + delta: NormalizedWheelDelta, + |}, +|}; + +export type Interaction = + | MouseDownInteraction + | MouseMoveInteraction + | MouseUpInteraction + | WheelPlainInteraction + | WheelWithShiftInteraction + | WheelWithControlInteraction + | WheelWithMetaInteraction; + +let canvasBoundingRectCache = null; +function cacheFirstGetCanvasBoundingRect( + canvas: HTMLCanvasElement, +): ClientRect { + if ( + canvasBoundingRectCache && + canvas.width === canvasBoundingRectCache.width && + canvas.height === canvasBoundingRectCache.height + ) { + return canvasBoundingRectCache.rect; + } + canvasBoundingRectCache = { + width: canvas.width, + height: canvas.height, + rect: canvas.getBoundingClientRect(), + }; + return canvasBoundingRectCache.rect; +} + +export function useCanvasInteraction( + canvasRef: {|current: HTMLCanvasElement | null|}, + interactor: (interaction: Interaction) => void, +) { + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + function localToCanvasCoordinates(localCoordinates: Point): Point { + const canvasRect = cacheFirstGetCanvasBoundingRect(canvas); + return { + x: localCoordinates.x - canvasRect.left, + y: localCoordinates.y - canvasRect.top, + }; + } + + const onCanvasMouseDown: MouseEventHandler = event => { + interactor({ + type: 'mousedown', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + + const onDocumentMouseMove: MouseEventHandler = event => { + interactor({ + type: 'mousemove', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + + const onDocumentMouseUp: MouseEventHandler = event => { + interactor({ + type: 'mouseup', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + + const onCanvasWheel: WheelEventHandler = event => { + event.preventDefault(); + event.stopPropagation(); + + const location = localToCanvasCoordinates({x: event.x, y: event.y}); + const delta = normalizeWheel(event); + + if (event.shiftKey) { + interactor({ + type: 'wheel-shift', + payload: {event, location, delta}, + }); + } else if (event.ctrlKey) { + interactor({ + type: 'wheel-control', + payload: {event, location, delta}, + }); + } else if (event.metaKey) { + interactor({ + type: 'wheel-meta', + payload: {event, location, delta}, + }); + } else { + interactor({ + type: 'wheel-plain', + payload: {event, location, delta}, + }); + } + + return false; + }; + + document.addEventListener('mousemove', onDocumentMouseMove); + document.addEventListener('mouseup', onDocumentMouseUp); + + canvas.addEventListener('mousedown', onCanvasMouseDown); + canvas.addEventListener('wheel', onCanvasWheel); + + return () => { + document.removeEventListener('mousemove', onDocumentMouseMove); + document.removeEventListener('mouseup', onDocumentMouseUp); + + canvas.removeEventListener('mousedown', onCanvasMouseDown); + canvas.removeEventListener('wheel', onCanvasWheel); + }; + }, [canvasRef, interactor]); +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/clamp.js b/packages/react-devtools-scheduling-profiler/src/view-base/utils/clamp.js new file mode 100644 index 0000000000..1e915d3ccb --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/utils/clamp.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export function clamp(min: number, max: number, value: number): number { + if (Number.isNaN(min) || Number.isNaN(max) || Number.isNaN(value)) { + throw new Error( + `Clamp was called with NaN. Args: min: ${min}, max: ${max}, value: ${value}.`, + ); + } + return Math.min(max, Math.max(min, value)); +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/normalizeWheel.js b/packages/react-devtools-scheduling-profiler/src/view-base/utils/normalizeWheel.js new file mode 100644 index 0000000000..6e783a0fc0 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/utils/normalizeWheel.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Adapted from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js + +export type NormalizedWheelDelta = {| + deltaX: number, + deltaY: number, +|}; + +// Reasonable defaults +const LINE_HEIGHT = 40; +const PAGE_HEIGHT = 800; + +/** + * Mouse wheel (and 2-finger trackpad) support on the web sucks. It is + * complicated, thus this doc is long and (hopefully) detailed enough to answer + * your questions. + * + * If you need to react to the mouse wheel in a predictable way, this code is + * like your bestest friend. * hugs * + * + * In your event callback, use this code to get sane interpretation of the + * deltas. This code will return an object with properties: + * + * - deltaX -- normalized distance (to pixels) - x plane + * - deltaY -- " - y plane + * + * Wheel values are provided by the browser assuming you are using the wheel to + * scroll a web page by a number of lines or pixels (or pages). Values can vary + * significantly on different platforms and browsers, forgetting that you can + * scroll at different speeds. Some devices (like trackpads) emit more events + * at smaller increments with fine granularity, and some emit massive jumps with + * linear speed or acceleration. + * + * This code does its best to normalize the deltas for you: + * + * - delta* is normalizing the desired scroll delta in pixel units. + * + * - positive value indicates scrolling DOWN/RIGHT, negative UP/LEFT. This + * should translate to positive value zooming IN, negative zooming OUT. + * This matches the 'wheel' event. + * + * Implementation info: + * + * The basics of the standard 'wheel' event is that it includes a unit, + * deltaMode (pixels, lines, pages), and deltaX, deltaY and deltaZ. + * See: http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents + * + * Examples of 'wheel' event if you scroll slowly (down) by one step with an + * average mouse: + * + * OS X + Chrome (mouse) - 4 pixel delta (wheelDelta -120) + * OS X + Safari (mouse) - N/A pixel delta (wheelDelta -12) + * OS X + Firefox (mouse) - 0.1 line delta (wheelDelta N/A) + * Win8 + Chrome (mouse) - 100 pixel delta (wheelDelta -120) + * Win8 + Firefox (mouse) - 3 line delta (wheelDelta -120) + * + * On the trackpad: + * + * OS X + Chrome (trackpad) - 2 pixel delta (wheelDelta -6) + * OS X + Firefox (trackpad) - 1 pixel delta (wheelDelta N/A) + */ +export function normalizeWheel(event: WheelEvent): NormalizedWheelDelta { + let deltaX = event.deltaX; + let deltaY = event.deltaY; + + if ( + // $FlowFixMe DOM_DELTA_LINE missing from WheelEvent + event.deltaMode === WheelEvent.DOM_DELTA_LINE + ) { + // delta in LINE units + deltaX *= LINE_HEIGHT; + deltaY *= LINE_HEIGHT; + } else if ( + // $FlowFixMe DOM_DELTA_PAGE missing from WheelEvent + event.deltaMode === WheelEvent.DOM_DELTA_PAGE + ) { + // delta in PAGE units + deltaX *= PAGE_HEIGHT; + deltaY *= PAGE_HEIGHT; + } + + return {deltaX, deltaY}; +} diff --git a/packages/react-devtools-scheduling-profiler/webpack.config.js b/packages/react-devtools-scheduling-profiler/webpack.config.js new file mode 100644 index 0000000000..91d6a141c2 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/webpack.config.js @@ -0,0 +1,104 @@ +'use strict'; + +const {resolve} = require('path'); +const {DefinePlugin} = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + console.error('NODE_ENV not set'); + process.exit(1); +} + +const TARGET = process.env.TARGET; +if (!TARGET) { + console.error('TARGET not set'); + process.exit(1); +} + +const builtModulesDir = resolve(__dirname, '..', '..', 'build', 'node_modules'); + +const __DEV__ = NODE_ENV === 'development'; + +const imageInlineSizeLimit = 10000; + +const config = { + mode: __DEV__ ? 'development' : 'production', + devtool: __DEV__ ? 'cheap-module-eval-source-map' : false, + entry: { + app: './src/index.js', + }, + resolve: { + alias: { + react: resolve(builtModulesDir, 'react'), + 'react-dom': resolve(builtModulesDir, 'react-dom'), + }, + }, + plugins: [ + new DefinePlugin({ + __DEV__, + __PROFILE__: false, + __EXPERIMENTAL__: true, + }), + new HtmlWebpackPlugin({ + title: 'React Concurrent Mode Profiler', + }), + ], + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + options: { + configFile: resolve( + __dirname, + '..', + 'react-devtools-shared', + 'babel.config.js', + ), + }, + }, + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: true, + modules: { + localIdentName: '[local]___[hash:base64:5]', + }, + }, + }, + ], + }, + { + test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], + loader: 'url-loader', + options: { + limit: imageInlineSizeLimit, + name: 'static/media/[name].[hash:8].[ext]', + }, + }, + ], + }, +}; + +if (TARGET === 'local') { + config.devServer = { + hot: true, + port: 8080, + clientLogLevel: 'warning', + stats: 'errors-only', + }; +} else { + config.output = { + path: resolve(__dirname, 'dist'), + filename: '[name].js', + }; +} + +module.exports = config; diff --git a/yarn.lock b/yarn.lock index c164ef3f18..90b4219bda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1572,6 +1572,14 @@ global-agent "^2.0.2" global-tunnel-ng "^2.7.1" +"@elg/speedscope@1.9.0-a6f84db": + version "1.9.0-a6f84db" + resolved "https://registry.yarnpkg.com/@elg/speedscope/-/speedscope-1.9.0-a6f84db.tgz#36079096390b9b396dfec5aeac12e971feb46084" + integrity sha512-AoGBS1H5s8jrqIh51P8tZnO9i02w7jBJsd0W6+nrF2tLce502zb2rOyot4iv4fnvhmjnM6VFPBLjE2DfElg8Dw== + dependencies: + opn "5.3.0" + react "^16.13.1" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -1903,6 +1911,11 @@ dependencies: defer-to-connect "^1.0.1" +"@types/anymatch@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" + integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== + "@types/babel__core@^7.1.0": version "7.1.2" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" @@ -1965,6 +1978,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/html-minifier-terser@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" + integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1990,6 +2008,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== +"@types/json-schema@^7.0.4": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + "@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -2010,11 +2033,49 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ== +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/tapable@*", "@types/tapable@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" + integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== + +"@types/uglify-js@*": + version "3.9.3" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b" + integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w== + dependencies: + source-map "^0.6.1" + +"@types/webpack-sources@*": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.2.tgz#5d3d4dea04008a779a90135ff96fb5c0c9e6292c" + integrity sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.7.3" + +"@types/webpack@^4.41.8": + version "4.41.21" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.21.tgz#cc685b332c33f153bb2f5fc1fa3ac8adeb592dee" + integrity sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + "@types/webpack-sources" "*" + source-map "^0.6.0" + "@types/yargs-parser@*": version "13.0.0" 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: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.2: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -2586,7 +2657,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.3: +anymatch@^3.0.3, anymatch@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== @@ -2967,6 +3038,17 @@ babel-loader@^8.0.4: mkdirp "^0.5.1" pify "^4.0.1" +babel-loader@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" + integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw== + dependencies: + find-cache-dir "^2.1.0" + loader-utils "^1.4.0" + mkdirp "^0.5.3" + pify "^4.0.1" + schema-utils "^2.6.5" + babel-plugin-dynamic-import-node@^2.3.0: version "2.3.0" 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: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -3245,7 +3332,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -3554,6 +3641,14 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.1.tgz#1fc41c854f00e2f7d0139dfeba1542d6896fe547" + integrity sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q== + dependencies: + pascal-case "^3.1.1" + tslib "^1.10.0" + camelcase@5.3.1, camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -3564,6 +3659,11 @@ camelcase@^4.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= +camelcase@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== + caniuse-lite@^1.0.30001022: version "1.0.30001022" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6" @@ -3680,6 +3780,21 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.0.1, chownr@^1.1.2, chownr@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" @@ -3755,6 +3870,13 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-css@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" + integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== + dependencies: + source-map "~0.6.0" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -3966,6 +4088,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== +commander@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + common-tags@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -4384,7 +4511,26 @@ css-loader@^1.0.1: postcss-value-parser "^3.3.0" source-list-map "^2.0.0" -css-select@~1.2.0: +css-loader@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.1.tgz#9f48fd7eae1219d629a3f085ba9a9102ca1141a7" + integrity sha512-MoqmF1if7Z0pZIEXA4ZF9PgtCXxWbfzfJM+3p+OYfhcrwcqhaCRb74DSnfzRl7e024xEiCRn5hCvfUbTf2sgFA== + dependencies: + camelcase "^6.0.0" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.3" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" + semver "^7.3.2" + +css-select@^1.1.0, css-select@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= @@ -4413,6 +4559,11 @@ cssesc@^0.1.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q= +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + cssom@^0.4.1, cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -4795,6 +4946,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-converter@^0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + dom-serializer@0: version "0.2.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb" @@ -4863,6 +5021,14 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" +dot-case@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa" + integrity sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA== + dependencies: + no-case "^3.0.3" + tslib "^1.10.0" + dot-prop@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -5026,6 +5192,15 @@ enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" +enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + entities@^1.1.1, entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -5079,6 +5254,23 @@ es-abstract@^1.13.0: is-regex "^1.0.4" object-keys "^1.0.12" +es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-regex "^1.1.0" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + es-to-primitive@^1.2.0: version "1.2.0" 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: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es6-error@4.1.1, es6-error@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" @@ -5875,6 +6076,14 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-loader@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f" + integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.5" + file-uri-to-path@1.0.0: version "1.0.0" 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: locate-path "^5.0.0" path-exists "^4.0.0" -findup-sync@3.0.0: +findup-sync@3.0.0, findup-sync@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== @@ -6167,7 +6376,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@2.1.3: +fsevents@2.1.3, fsevents@~2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== @@ -6319,6 +6528,13 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob-stream@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" @@ -6385,7 +6601,7 @@ global-dirs@^2.0.1: dependencies: ini "^1.3.5" -global-modules@2.0.0: +global-modules@2.0.0, global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== @@ -6614,6 +6830,11 @@ has-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -6679,6 +6900,11 @@ hasurl@^1.0.0: resolved "https://registry.yarnpkg.com/hasurl/-/hasurl-1.0.0.tgz#e4c619097ae1e8fc906bee904ce47e94f5e1ea37" integrity sha512-43ypUd3DbwyCT01UYpA99AEZxZ4aKtRxWGBHEIbjcOsUghd9YUON0C+JF6isNjaiwC/UF5neaUudy6JS9jZPZQ== +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6731,12 +6957,45 @@ html-entities@^1.2.1: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= +html-entities@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== + html-escaper@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== -htmlparser2@^3.9.1: +html-minifier-terser@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" + integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== + dependencies: + camel-case "^4.1.1" + clean-css "^4.2.3" + commander "^4.1.1" + he "^1.2.0" + param-case "^3.0.3" + relateurl "^0.2.7" + terser "^4.6.3" + +html-webpack-plugin@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz#53bf8f6d696c4637d5b656d3d9863d89ce8174fd" + integrity sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w== + dependencies: + "@types/html-minifier-terser" "^5.0.0" + "@types/tapable" "^1.0.5" + "@types/webpack" "^4.41.8" + html-minifier-terser "^5.0.1" + loader-utils "^1.2.3" + lodash "^4.17.15" + pretty-error "^2.1.1" + tapable "^1.1.3" + util.promisify "1.0.0" + +htmlparser2@^3.3.0, htmlparser2@^3.9.1: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -6919,6 +7178,13 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + ieee754@^1.1.12, ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -6993,6 +7259,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -7089,6 +7360,11 @@ interpret@1.2.0, interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== +interpret@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -7172,6 +7448,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -7182,6 +7465,11 @@ is-callable@^1.1.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== +is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" @@ -7308,7 +7596,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -7462,6 +7750,13 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + dependencies: + has-symbols "^1.0.1" + is-relative@^0.1.0: version "0.1.3" 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: emojis-list "^2.0.0" json5 "^1.0.1" -loader-utils@^1.2.3: +loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== @@ -8470,6 +8765,15 @@ loader-utils@^1.2.3: emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + local-storage-fallback@^4.1.1: version "4.1.1" 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: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== -loglevel@^1.6.6: +loglevel@^1.6.6, loglevel@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" 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 dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7" + integrity sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ== + dependencies: + tslib "^1.10.0" + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -8837,6 +9148,11 @@ memoize-one@^3.1.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== +memoize-one@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + memory-fs@^0.4.0, memory-fs@^0.4.1: version "0.4.1" 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": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.24" 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: dependencies: mime-db "1.40.0" +mime-types@^2.1.26: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -9085,7 +9413,7 @@ mkdirp@1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.0, mkdirp@^0.5.3: +mkdirp@^0.5.0, mkdirp@^0.5.3, mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -9233,6 +9561,14 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +no-case@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.3.tgz#c21b434c1ffe48b39087e86cfb4d2582e9df18f8" + integrity sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw== + dependencies: + lower-case "^2.0.1" + tslib "^1.10.0" + node-cleanup@^2.1.2: version "2.1.2" 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: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -9473,7 +9809,7 @@ nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -nullthrows@^1.0.0: +nullthrows@^1.0.0, nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== @@ -9507,7 +9843,12 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-keys@^1.0.11, object-keys@^1.0.12: +object-inspect@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -9529,6 +9870,14 @@ object.assign@^4.0.4, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.getownpropertydescriptors@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -9602,6 +9951,13 @@ open@^7.0.2: is-docker "^2.0.0" is-wsl "^2.1.1" +opn@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" + integrity sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g== + dependencies: + is-wsl "^1.1.0" + opn@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -9869,6 +10225,14 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" +param-case@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.3.tgz#4be41f8399eff621c56eebb829a5e451d9801238" + integrity sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA== + dependencies: + dot-case "^3.0.3" + tslib "^1.10.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -9934,6 +10298,11 @@ parse-link-header@^1.0.1: dependencies: xtend "~4.0.1" +parse-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== + parse-node-version@^1.0.0: version "1.0.1" 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: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascal-case@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.1.tgz#5ac1975133ed619281e88920973d2cd1f279de5f" + integrity sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA== + dependencies: + no-case "^3.0.3" + tslib "^1.10.0" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -10065,6 +10442,11 @@ picomatch@^2.0.4, picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== +picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -10172,6 +10554,15 @@ portfinder@^1.0.25: debug "^3.1.1" mkdirp "^0.5.1" +portfinder@^1.0.26: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + posix-character-classes@^0.1.0: version "0.1.1" 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: dependencies: postcss "^6.0.1" +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + postcss-modules-local-by-default@^1.2.0: version "1.2.0" 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: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" +postcss-modules-local-by-default@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + postcss-modules-scope@^1.1.0: version "1.1.0" 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: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + postcss-modules-values@^1.3.0: version "1.3.0" 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: icss-replace-symbols "^1.1.0" postcss "^6.0.1" +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-value-parser@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss@7.0.32: +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss@7.0.32, postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -10261,6 +10699,14 @@ prettier@1.19.1, prettier@^1.0.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +pretty-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" + integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + dependencies: + renderkid "^2.0.1" + utila "~0.4" + pretty-format@^25.2.6: version "25.2.6" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.6.tgz#542a1c418d019bbf1cca2e3620443bc1323cb8d7" @@ -10271,6 +10717,13 @@ pretty-format@^25.2.6: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-ms@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.0.tgz#45781273110caf35f55cab21a8a9bd403a233dc0" + integrity sha512-J3aPWiC5e9ZeZFuSeBraGxSkGMOvulSWsxDByOcbD1Pr75YL3LSNIKIb52WXbCLE1sS5s4inBBbryjF4Y05Ceg== + dependencies: + parse-ms "^2.1.0" + prettyjson@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" @@ -10620,6 +11073,15 @@ react-virtualized-auto-sizer@^1.0.2: resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg== +react@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" + integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + read-package-json-fast@^1.1.1: version "1.1.3" 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: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + readline-sync@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" @@ -10749,6 +11218,11 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== +regenerator-runtime@^0.13.7: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -10856,6 +11330,11 @@ regjsparser@^0.6.4: dependencies: jsesc "~0.5.0" +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + relaxed-json@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/relaxed-json/-/relaxed-json-1.0.3.tgz#eb2101ae0ee60e82267d95ed0ddf19a3604b8c1e" @@ -10869,6 +11348,17 @@ remove-trailing-separator@^1.0.1: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +renderkid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== + dependencies: + css-select "^1.1.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" + strip-ansi "^3.0.0" + utila "^0.4.0" + repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" @@ -11354,6 +11844,15 @@ schema-utils@^2.0.1: ajv "^6.1.0" ajv-keywords "^3.1.0" +schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -11675,6 +12174,15 @@ sockjs@0.3.19: faye-websocket "^0.10.0" uuid "^3.0.1" +sockjs@0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.4.0" + websocket-driver "0.6.5" + socks-proxy-agent@^4.0.0: version "4.0.2" 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: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -11795,6 +12303,17 @@ spdy@^4.0.1: select-hose "^2.0.0" spdy-transport "^3.0.0" +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + split-on-first@^1.0.0: version "1.1.0" 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: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -12103,6 +12638,14 @@ style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" +style-loader@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" + integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.6" + sumchecker@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" @@ -12274,6 +12817,15 @@ terser@^4.1.2: source-map "~0.6.1" source-map-support "~0.5.12" +terser@^4.6.3: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -12518,6 +13070,11 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" +tslib@^1.10.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + tslib@^1.8.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -12659,6 +13216,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -12784,6 +13346,15 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-loader@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.0.tgz#c7d6b0d6b0fccd51ab3ffc58a78d32b8d89a7be2" + integrity sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.26" + schema-utils "^2.6.5" + url-parse-lax@^1.0.0: version "1.0.0" 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: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util.promisify@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + util@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" @@ -12868,6 +13447,11 @@ util@~0.10.3: dependencies: inherits "2.0.3" +utila@^0.4.0, utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + utils-merge@1.0.1: version "1.0.1" 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: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== +uuid@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + v8-compile-cache@2.0.3: version "2.0.3" 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: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== +v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + v8-to-istanbul@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.0.1.tgz#d6a2a3823b8ff49bdf2167ff2a45d82dff81d02f" @@ -12998,6 +13592,13 @@ warning@^4.0.2: dependencies: loose-envify "^1.0.0" +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== + dependencies: + chokidar "^2.1.8" + watchpack@1.6.1, watchpack@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" @@ -13007,6 +13608,17 @@ watchpack@1.6.1, watchpack@^1.6.1: graceful-fs "^4.1.2" neo-async "^2.5.0" +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== + dependencies: + graceful-fs "^4.1.2" + neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.0" + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -13093,6 +13705,23 @@ webpack-cli@^3.3.11: v8-compile-cache "2.0.3" yargs "13.2.4" +webpack-cli@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== + dependencies: + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" + webpack-dev-middleware@^3.7.2: version "3.7.2" 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: ws "^6.2.1" yargs "12.0.5" +webpack-dev-server@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.3.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.8" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.26" + schema-utils "^1.0.0" + selfsigned "^1.10.7" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "0.3.20" + sockjs-client "1.4.0" + spdy "^4.0.2" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "^13.3.2" + webpack-log@^2.0.0: version "2.0.0" 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: watchpack "^1.6.1" webpack-sources "^1.4.1" +webpack@^4.44.1: + version "4.44.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" @@ -13467,6 +14171,14 @@ yargs-parser@^13.1.0: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^18.1.1: version "18.1.2" 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: y18n "^4.0.0" yargs-parser "^18.1.1" +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + yauzl@2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"