diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index eb6b746dec0c4..b35628a705d01 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -31,8 +31,9 @@ import {copy} from 'clipboard-js'; import prettyMilliseconds from 'pretty-ms'; import { + BackgroundColorView, HorizontalPanAndZoomView, - ResizableSplitView, + ResizableView, Surface, VerticalScrollView, View, @@ -45,8 +46,9 @@ import { import { FlamechartView, NativeEventsView, - ReactEventsView, ReactMeasuresView, + SchedulingEventsView, + SuspenseEventsView, TimeAxisMarkersView, UserTimingMarksView, } from './content-views'; @@ -128,7 +130,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { const surfaceRef = useRef(new Surface()); const userTimingMarksViewRef = useRef(null); const nativeEventsViewRef = useRef(null); - const reactEventsViewRef = useRef(null); + const schedulingEventsViewRef = useRef(null); + const suspenseEventsViewRef = useRef(null); const reactMeasuresViewRef = useRef(null); const flamechartViewRef = useRef(null); const syncedHorizontalPanAndZoomViewsRef = useRef( @@ -152,21 +155,53 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { ); }; - // Top content + function createViewHelper( + view: View, + resizeLabel: string = '', + shouldScrollVertically: boolean = false, + shouldResizeVertically: boolean = false, + ): View { + let verticalScrollView = null; + if (shouldScrollVertically) { + verticalScrollView = new VerticalScrollView( + surface, + defaultFrame, + view, + ); + } + + const horizontalPanAndZoomView = new HorizontalPanAndZoomView( + surface, + defaultFrame, + verticalScrollView !== null ? verticalScrollView : view, + data.duration, + syncAllHorizontalPanAndZoomViewStates, + ); - const topContentStack = new View( - surface, - defaultFrame, - verticallyStackedLayout, - ); + syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView); + + let viewToReturn = horizontalPanAndZoomView; + if (shouldResizeVertically) { + viewToReturn = new ResizableView( + surface, + defaultFrame, + horizontalPanAndZoomView, + canvasRef, + resizeLabel, + ); + } + + return viewToReturn; + } const axisMarkersView = new TimeAxisMarkersView( surface, defaultFrame, data.duration, ); - topContentStack.addSubview(axisMarkersView); + const axisMarkersViewWrapper = createViewHelper(axisMarkersView); + let userTimingMarksViewWrapper = null; if (data.otherUserTimingMarks.length > 0) { const userTimingMarksView = new UserTimingMarksView( surface, @@ -175,50 +210,50 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data.duration, ); userTimingMarksViewRef.current = userTimingMarksView; - topContentStack.addSubview(userTimingMarksView); + userTimingMarksViewWrapper = createViewHelper(userTimingMarksView); } const nativeEventsView = new NativeEventsView(surface, defaultFrame, data); nativeEventsViewRef.current = nativeEventsView; - topContentStack.addSubview(nativeEventsView); + const nativeEventsViewWrapper = createViewHelper( + nativeEventsView, + 'events', + true, + true, + ); - const reactEventsView = new ReactEventsView(surface, defaultFrame, data); - reactEventsViewRef.current = reactEventsView; - topContentStack.addSubview(reactEventsView); + const schedulingEventsView = new SchedulingEventsView( + surface, + defaultFrame, + data, + ); + schedulingEventsViewRef.current = schedulingEventsView; + const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView); - const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView( + const suspenseEventsView = new SuspenseEventsView( surface, defaultFrame, - topContentStack, - data.duration, - syncAllHorizontalPanAndZoomViewStates, + data, ); - syncedHorizontalPanAndZoomViewsRef.current.push( - topContentHorizontalPanAndZoomView, + suspenseEventsViewRef.current = suspenseEventsView; + const suspenseEventsViewWrapper = createViewHelper( + suspenseEventsView, + 'suspense', + true, + true, ); - // Resizable content - const reactMeasuresView = new ReactMeasuresView( surface, defaultFrame, data, ); reactMeasuresViewRef.current = reactMeasuresView; - const reactMeasuresVerticalScrollView = new VerticalScrollView( - surface, - defaultFrame, + const reactMeasuresViewWrapper = createViewHelper( reactMeasuresView, - ); - const reactMeasuresHorizontalPanAndZoomView = new HorizontalPanAndZoomView( - surface, - defaultFrame, - reactMeasuresVerticalScrollView, - data.duration, - syncAllHorizontalPanAndZoomViewStates, - ); - syncedHorizontalPanAndZoomViewsRef.current.push( - reactMeasuresHorizontalPanAndZoomView, + 'react', + true, + true, ); const flamechartView = new FlamechartView( @@ -228,30 +263,15 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data.duration, ); flamechartViewRef.current = flamechartView; - const flamechartVerticalScrollView = new VerticalScrollView( - surface, - defaultFrame, + const flamechartViewWrapper = createViewHelper( flamechartView, - ); - const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView( - surface, - defaultFrame, - flamechartVerticalScrollView, - data.duration, - syncAllHorizontalPanAndZoomViewStates, - ); - syncedHorizontalPanAndZoomViewsRef.current.push( - flamechartHorizontalPanAndZoomView, - ); - - const resizableContentStack = new ResizableSplitView( - surface, - defaultFrame, - reactMeasuresHorizontalPanAndZoomView, - flamechartHorizontalPanAndZoomView, - canvasRef, + 'flamechart', + true, + true, ); + // Root view contains all of the sub views defined above. + // The order we add them below determines their vertical position. const rootView = new View( surface, defaultFrame, @@ -260,8 +280,18 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { lastViewTakesUpRemainingSpaceLayout, ), ); - rootView.addSubview(topContentHorizontalPanAndZoomView); - rootView.addSubview(resizableContentStack); + rootView.addSubview(axisMarkersViewWrapper); + if (userTimingMarksViewWrapper !== null) { + rootView.addSubview(userTimingMarksViewWrapper); + } + rootView.addSubview(nativeEventsViewWrapper); + rootView.addSubview(schedulingEventsViewWrapper); + rootView.addSubview(suspenseEventsViewWrapper); + rootView.addSubview(reactMeasuresViewWrapper); + rootView.addSubview(flamechartViewWrapper); + + // If subviews are less than the available height, fill remaining height with a solid color. + rootView.addSubview(new BackgroundColorView(surface, defaultFrame)); surfaceRef.current.rootView = rootView; }, [data]); @@ -278,6 +308,39 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { return; } + // Wheel events should always hide the current toolltip. + switch (interaction.type) { + case 'wheel-control': + case 'wheel-meta': + case 'wheel-plain': + case 'wheel-shift': + setHoveredEvent(prevHoverEvent => { + if (prevHoverEvent === null) { + return prevHoverEvent; + } else if ( + prevHoverEvent.flamechartStackFrame !== null || + prevHoverEvent.measure !== null || + prevHoverEvent.nativeEvent !== null || + prevHoverEvent.schedulingEvent !== null || + prevHoverEvent.suspenseEvent !== null || + prevHoverEvent.userTimingMark !== null + ) { + return { + data: prevHoverEvent.data, + flamechartStackFrame: null, + measure: null, + nativeEvent: null, + schedulingEvent: null, + suspenseEvent: null, + userTimingMark: null, + }; + } else { + return prevHoverEvent; + } + }); + break; + } + const surface = surfaceRef.current; surface.handleInteraction(interaction); @@ -310,12 +373,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { userTimingMarksView.onHover = userTimingMark => { if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) { setHoveredEvent({ - userTimingMark, - nativeEvent: null, - reactEvent: null, + data, flamechartStackFrame: null, measure: null, - data, + nativeEvent: null, + schedulingEvent: null, + suspenseEvent: null, + userTimingMark, }); } }; @@ -326,28 +390,47 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { nativeEventsView.onHover = nativeEvent => { if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) { setHoveredEvent({ - userTimingMark: null, - nativeEvent, - reactEvent: null, + data, flamechartStackFrame: null, measure: null, - data, + nativeEvent, + schedulingEvent: null, + suspenseEvent: null, + userTimingMark: null, }); } }; } - const {current: reactEventsView} = reactEventsViewRef; - if (reactEventsView) { - reactEventsView.onHover = reactEvent => { - if (!hoveredEvent || hoveredEvent.reactEvent !== reactEvent) { + const {current: schedulingEventsView} = schedulingEventsViewRef; + if (schedulingEventsView) { + schedulingEventsView.onHover = schedulingEvent => { + if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) { setHoveredEvent({ - userTimingMark: null, - nativeEvent: null, - reactEvent, + data, flamechartStackFrame: null, measure: null, + nativeEvent: null, + schedulingEvent, + suspenseEvent: null, + userTimingMark: null, + }); + } + }; + } + + const {current: suspenseEventsView} = suspenseEventsViewRef; + if (suspenseEventsView) { + suspenseEventsView.onHover = suspenseEvent => { + if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) { + setHoveredEvent({ data, + flamechartStackFrame: null, + measure: null, + nativeEvent: null, + schedulingEvent: null, + suspenseEvent, + userTimingMark: null, }); } }; @@ -358,12 +441,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { reactMeasuresView.onHover = measure => { if (!hoveredEvent || hoveredEvent.measure !== measure) { setHoveredEvent({ - userTimingMark: null, - nativeEvent: null, - reactEvent: null, + data, flamechartStackFrame: null, measure, - data, + nativeEvent: null, + schedulingEvent: null, + suspenseEvent: null, + userTimingMark: null, }); } }; @@ -377,12 +461,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { hoveredEvent.flamechartStackFrame !== flamechartStackFrame ) { setHoveredEvent({ - userTimingMark: null, - nativeEvent: null, - reactEvent: null, + data, flamechartStackFrame, measure: null, - data, + nativeEvent: null, + schedulingEvent: null, + suspenseEvent: null, + userTimingMark: null, }); } }); @@ -407,10 +492,17 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { ); } - const {current: reactEventsView} = reactEventsViewRef; - if (reactEventsView) { - reactEventsView.setHoveredEvent( - hoveredEvent ? hoveredEvent.reactEvent : null, + const {current: schedulingEventsView} = schedulingEventsViewRef; + if (schedulingEventsView) { + schedulingEventsView.setHoveredEvent( + hoveredEvent ? hoveredEvent.schedulingEvent : null, + ); + } + + const {current: suspenseEventsView} = suspenseEventsViewRef; + if (suspenseEventsView) { + suspenseEventsView.setHoveredEvent( + hoveredEvent ? hoveredEvent.suspenseEvent : null, ); } @@ -443,24 +535,25 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { return null; } const { - reactEvent, flamechartStackFrame, measure, + schedulingEvent, + suspenseEvent, } = contextData.hoveredEvent; return ( - {reactEvent !== null && ( + {schedulingEvent !== null && ( copy(reactEvent.componentName)} + onClick={() => copy(schedulingEvent.componentName)} title="Copy component name"> Copy component name )} - {reactEvent !== null && reactEvent.componentStack && ( + {suspenseEvent !== null && ( copy(reactEvent.componentStack)} - title="Copy component stack"> - Copy component stack + onClick={() => copy(suspenseEvent.componentName)} + title="Copy component name"> + Copy component name )} {measure !== null && ( diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css index 91e60bf13cfd2..114d3fe7bbe15 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -1,6 +1,10 @@ .Tooltip { position: fixed; - display: inline-block; +} + +.TooltipSection, +.TooltipWarningSection { + display: block; border-radius: 0.125rem; max-width: 300px; padding: 0.25rem; @@ -12,11 +16,15 @@ color: var(--color-tooltip-text); font-size: 11px; } +.TooltipWarningSection { + margin-top: 0.25rem; + background-color: var(--color-warning-background); +} .Divider { height: 1px; background-color: #aaa; - margin: 0.5rem 0; + margin: 0.25rem 0; } .DetailsGrid { @@ -40,7 +48,6 @@ .FlamechartStackFrameName { word-break: break-word; - margin-left: 0.4rem; } .ComponentName { @@ -49,32 +56,20 @@ margin-right: 0.25rem; } -.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, - var(--color-tooltip-background), - var(--color-tooltip-background) 5em, - transparent - ); - mask-image: linear-gradient( - 180deg, - var(--color-tooltip-background), - var(--color-tooltip-background) 5em, - transparent - ); - white-space: pre; -} - .ReactMeasureLabel { - margin-left: 0.4rem; } .UserTimingLabel { word-break: break-word; } + +.NativeEventName { + font-weight: bold; + word-break: break-word; + margin-right: 0.25rem; +} + +.InfoText, +.WarningText { + color: var(--color-warning-text-color); +} \ No newline at end of file diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index 3e9f36e44acf9..d6019cb254434 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -11,18 +11,18 @@ import type {Point} from './view-base'; import type { FlamechartStackFrame, NativeEvent, - ReactEvent, ReactHoverContextInfo, ReactMeasure, ReactProfilerData, Return, + SchedulingEvent, + SuspenseEvent, 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 {useRef} from 'react'; +import {formatDuration, formatTimestamp, trimString} from './utils/formatting'; import {getBatchRange} from './utils/getBatchRange'; import useSmartTooltip from './utils/useSmartTooltip'; import styles from './EventTooltip.css'; @@ -34,22 +34,7 @@ type Props = {| 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(event: ReactEvent): string | null { +function getSchedulingEventLabel(event: SchedulingEvent): string | null { switch (event.type) { case 'schedule-render': return 'render scheduled'; @@ -57,30 +42,6 @@ function getReactEventLabel(event: ReactEvent): string | null { 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; } @@ -89,15 +50,15 @@ function getReactEventColor(event: ReactEvent): string | null { function getReactMeasureLabel(type): string | null { switch (type) { case 'commit': - return 'commit'; + return 'react commit'; case 'render-idle': - return 'idle'; + return 'react idle'; case 'render': - return 'render'; + return 'react render'; case 'layout-effects': - return 'layout effects'; + return 'react layout effects'; case 'passive-effects': - return 'passive effects'; + return 'react passive effects'; default: return null; } @@ -120,10 +81,11 @@ export default function EventTooltip({ } const { - nativeEvent, - reactEvent, - measure, flamechartStackFrame, + measure, + nativeEvent, + schedulingEvent, + suspenseEvent, userTimingMark, } = hoveredEvent; @@ -131,9 +93,19 @@ export default function EventTooltip({ return ( ); - } else if (reactEvent !== null) { + } else if (schedulingEvent !== null) { return ( - + + ); + } else if (suspenseEvent !== null) { + return ( + ); } else if (measure !== null) { return ( @@ -158,16 +130,6 @@ export default function EventTooltip({ 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, @@ -175,35 +137,25 @@ const TooltipFlamechartNode = ({ stackFrame: FlamechartStackFrame, tooltipRef: Return, }) => { - const { - name, - timestamp, - duration, - scriptUrl, - locationLine, - locationColumn, - } = stackFrame; + const {name, timestamp, duration, locationLine, locationColumn} = stackFrame; return (
- {formatDuration(duration)} - {name} -
-
Timestamp:
-
{formatTimestamp(timestamp)}
- {scriptUrl && ( - <> -
Script URL:
-
{scriptUrl}
- - )} - {(locationLine !== undefined || locationColumn !== undefined) && ( - <> -
Location:
-
- line {locationLine}, column {locationColumn} -
- - )} +
+ {name} +
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
Duration:
+
{formatDuration(duration)}
+ {(locationLine !== undefined || locationColumn !== undefined) && ( + <> +
Location:
+
+ line {locationLine}, column {locationColumn} +
+ + )} +
); @@ -216,75 +168,142 @@ const TooltipNativeEvent = ({ nativeEvent: NativeEvent, tooltipRef: Return, }) => { - const {duration, timestamp, type, warnings} = nativeEvent; - - const warningElements = []; - if (warnings !== null) { - warnings.forEach((warning, index) => { - warningElements.push( - -
Warning:
-
{warning}
-
, - ); - }); - } + const {duration, timestamp, type, warning} = nativeEvent; return (
- {trimmedString(type, 768)} - event -
-
-
Timestamp:
-
{formatTimestamp(timestamp)}
-
Duration:
-
{formatDuration(duration)}
- {warningElements} +
+ {trimString(type, 768)} + event +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
Duration:
+
{formatDuration(duration)}
+
+ {warning !== null && ( +
+
{warning}
+
+ )}
); }; -const TooltipReactEvent = ({ - reactEvent, +const TooltipSchedulingEvent = ({ + schedulingEvent, tooltipRef, }: { - reactEvent: ReactEvent, + schedulingEvent: SchedulingEvent, tooltipRef: Return, }) => { - const label = getReactEventLabel(reactEvent); - const color = getReactEventColor(reactEvent); - if (!label || !color) { + const label = getSchedulingEventLabel(schedulingEvent); + if (!label) { if (__DEV__) { - console.warn('Unexpected reactEvent type "%s"', reactEvent.type); + console.warn( + 'Unexpected schedulingEvent type "%s"', + schedulingEvent.type, + ); } return null; } - const {componentName, componentStack, timestamp} = reactEvent; + let laneLabels = null; + let lanes = null; + switch (schedulingEvent.type) { + case 'schedule-render': + case 'schedule-state-update': + case 'schedule-force-update': + laneLabels = schedulingEvent.laneLabels; + lanes = schedulingEvent.lanes; + break; + } + + const {componentName, timestamp, warning} = schedulingEvent; return (
- {componentName && ( - - {trimmedString(componentName, 768)} - +
+ {componentName && ( + + {trimString(componentName, 100)} + + )} + {label} +
+
+ {laneLabels !== null && lanes !== null && ( + <> +
Lanes:
+
+ {laneLabels.join(', ')} ({lanes.join(', ')}) +
+ + )} +
Timestamp:
+
{formatTimestamp(timestamp)}
+
+
+ {warning !== null && ( +
+
{warning}
+
)} - {label} -
-
-
Timestamp:
-
{formatTimestamp(timestamp)}
- {componentStack && ( - -
Component stack:
-
-              {formatComponentStack(componentStack)}
-            
-
+
+ ); +}; + +const TooltipSuspenseEvent = ({ + suspenseEvent, + tooltipRef, +}: { + suspenseEvent: SuspenseEvent, + tooltipRef: Return, +}) => { + const { + componentName, + duration, + phase, + resolution, + timestamp, + warning, + } = suspenseEvent; + + let label = 'suspended'; + if (phase !== null) { + label += ` during ${phase}`; + } + + return ( +
+
+ {componentName && ( + + {trimString(componentName, 100)} + )} + {label} +
+
+
Status:
+
{resolution}
+
Timestamp:
+
{formatTimestamp(timestamp)}
+ {duration !== null && ( + <> +
Duration:
+
{formatDuration(duration)}
+ + )} +
+ {warning !== null && ( +
+
{warning}
+
+ )}
); }; @@ -311,21 +330,28 @@ const TooltipReactMeasure = ({ return (
- {formatDuration(duration)} - {label} -
-
-
Timestamp:
-
{formatTimestamp(timestamp)}
-
Batch duration:
-
{formatDuration(stopTime - startTime)}
-
- Lane{lanes.length === 1 ? '' : 's'}: -
-
- {laneLabels.length > 0 - ? `${laneLabels.join(', ')} (${lanes.join(', ')})` - : lanes.join(', ')} +
+ {label} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+ {measure.type !== 'render-idle' && ( + <> +
Duration:
+
{formatDuration(duration)}
+ + )} +
Batch duration:
+
{formatDuration(stopTime - startTime)}
+
+ Lane{lanes.length === 1 ? '' : 's'}: +
+
+ {laneLabels.length > 0 + ? `${laneLabels.join(', ')} (${lanes.join(', ')})` + : lanes.join(', ')} +
@@ -342,11 +368,13 @@ const TooltipUserTimingMark = ({ const {name, timestamp} = mark; return (
- {name} -
-
-
Timestamp:
-
{formatTimestamp(timestamp)}
+
+ {name} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
); diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js index dff2398164fd8..c7a7783694fbd 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js @@ -41,8 +41,18 @@ export function SchedulingProfiler(_: {||}) { // The easiest way to guarangee this happens is to recreate the inner Canvas component. const [key, setKey] = useState(theme); useLayoutEffect(() => { - updateColorsToMatchTheme(); - setKey(deferredTheme); + const pollForTheme = () => { + if (updateColorsToMatchTheme()) { + clearInterval(intervalID); + setKey(deferredTheme); + } + }; + + const intervalID = setInterval(pollForTheme, 50); + + return () => { + clearInterval(intervalID); + }; }, [deferredTheme]); return ( diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js index d84a057dd2918..57cd8ae1f30c4 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -21,12 +21,11 @@ import type { } from '../view-base'; import { - ColorView, + BackgroundColorView, Surface, View, layeredLayout, rectContainsPoint, - rectEqualToRect, intersectionOfRects, rectIntersectsRect, verticallyStackedLayout, @@ -36,11 +35,10 @@ import { positioningScaleFactor, timestampToPosition, } from './utils/positioning'; +import {drawText} from './utils/text'; import { COLORS, - FONT_SIZE, FLAMECHART_FRAME_HEIGHT, - TEXT_PADDING, COLOR_HOVER_DIM_DELTA, BORDER_SIZE, } from './constants'; @@ -71,29 +69,6 @@ function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string { 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; @@ -161,10 +136,6 @@ class FlamechartStackLayerView extends View { visibleArea.size.height, ); - context.textAlign = 'left'; - context.textBaseline = 'middle'; - context.font = `${FONT_SIZE}px sans-serif`; - const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); for (let i = 0; i < _stackLayer.length; i++) { @@ -201,45 +172,7 @@ class FlamechartStackLayerView extends View { drawableRect.size.height, ); - if (width > TEXT_PADDING * 2) { - const trimmedName = trimFlamechartText( - context, - name, - width - TEXT_PADDING * 2 + (x < 0 ? x : 0), - ); - - if (trimmedName !== null) { - context.fillStyle = COLORS.TEXT_COLOR; - - // 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 + TEXT_PADDING - (x < 0 ? x : 0), - nodeRect.origin.y + FLAMECHART_FRAME_HEIGHT / 2, - ); - - if (textOverflowsViewableArea) { - context.restore(); - } - } - } + drawText(name, context, nodeRect, drawableRect, width); } } @@ -262,13 +195,17 @@ class FlamechartStackLayerView extends View { 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) { - this.currentCursor = 'pointer'; - viewRefs.hoveredView = this; - _onHover(flamechartStackFrame); - return; + const width = durationToWidth(duration, scaleFactor); + + // Don't show tooltips for nodes that are too small to render at this zoom level. + if (Math.floor(width - BORDER_SIZE) >= 1) { + if (x <= location.x && x + width >= location.x) { + this.currentCursor = 'context-menu'; + viewRefs.hoveredView = this; + _onHover(flamechartStackFrame); + return; + } } if (x > location.x) { @@ -336,10 +273,8 @@ export class FlamechartView extends View { 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); + // Add a plain background view to prevent gaps from appearing between flamechartRowViews. + this.addSubview(new BackgroundColorView(surface, frame)); this.addSubview(this._verticalStackView); } @@ -359,7 +294,12 @@ export class FlamechartView extends View { desiredSize() { // Ignore the wishes of the background color view - return this._verticalStackView.desiredSize(); + const intrinsicSize = this._verticalStackView.desiredSize(); + return { + ...intrinsicSize, + // Collapsed by default + maxInitialHeight: 0, + }; } /** diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js index 82371d2f2fce9..98e388c52a815 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -22,6 +22,8 @@ import { positionToTimestamp, timestampToPosition, } from './utils/positioning'; +import {drawText} from './utils/text'; +import {formatDuration} from '../utils/formatting'; import { View, Surface, @@ -29,40 +31,10 @@ import { rectIntersectsRect, intersectionOfRects, } from '../view-base'; -import { - COLORS, - TEXT_PADDING, - NATIVE_EVENT_HEIGHT, - FONT_SIZE, - BORDER_SIZE, -} from './constants'; +import {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants'; const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE; -// TODO (scheduling profiler) Make this a reusable util -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; -}; - export class NativeEventsView extends View { _depthToNativeEvent: Map; _hoveredEvent: NativeEvent | null = null; @@ -117,7 +89,7 @@ export class NativeEventsView extends View { } /** - * Draw a single `NativeEvent` as a circle in the canvas. + * Draw a single `NativeEvent` as a box/span with text inside of it. */ _drawSingleNativeEvent( context: CanvasRenderingContext2D, @@ -128,7 +100,7 @@ export class NativeEventsView extends View { showHoverHighlight: boolean, ) { const {frame} = this; - const {depth, duration, timestamp, type, warnings} = event; + const {depth, duration, timestamp, type, warning} = event; baseY += depth * ROW_WITH_BORDER_HEIGHT; @@ -152,10 +124,10 @@ export class NativeEventsView extends View { const drawableRect = intersectionOfRects(eventRect, rect); context.beginPath(); - if (warnings !== null) { + if (warning !== null) { context.fillStyle = showHoverHighlight - ? COLORS.NATIVE_EVENT_WARNING_HOVER - : COLORS.NATIVE_EVENT_WARNING; + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; } else { context.fillStyle = showHoverHighlight ? COLORS.NATIVE_EVENT_HOVER @@ -168,32 +140,9 @@ export class NativeEventsView extends View { drawableRect.size.height, ); - // Render event type label - context.textAlign = 'left'; - context.textBaseline = 'middle'; - context.font = `${FONT_SIZE}px sans-serif`; - - if (width > TEXT_PADDING * 2) { - const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); - const trimmedName = trimFlamechartText( - context, - type, - width - TEXT_PADDING * 2 + (x < 0 ? x : 0), - ); + const label = `${type} - ${formatDuration(duration)}`; - if (trimmedName !== null) { - context.fillStyle = - warnings !== null - ? COLORS.NATIVE_EVENT_WARNING_TEXT - : COLORS.TEXT_COLOR; - - context.fillText( - trimmedName, - eventRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0), - eventRect.origin.y + NATIVE_EVENT_HEIGHT / 2, - ); - } - } + drawText(label, context, eventRect, drawableRect, width); } draw(context: CanvasRenderingContext2D) { @@ -242,12 +191,16 @@ export class NativeEventsView extends View { }, }; if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects( + borderFrame, + visibleArea, + ); context.fillStyle = COLORS.PRIORITY_BORDER; context.fillRect( - visibleArea.origin.x, - frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE, - visibleArea.size.width, - BORDER_SIZE, + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, ); } } @@ -285,8 +238,6 @@ export class NativeEventsView extends View { hoverTimestamp >= timestamp && hoverTimestamp <= timestamp + duration ) { - this.currentCursor = 'pointer'; - viewRefs.hoveredView = this; onHover(nativeEvent); diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js index 9bd032b081396..96790b2a282e5 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -12,7 +12,7 @@ import type { Interaction, MouseMoveInteraction, Rect, - Size, + SizeWithMaxHeight, ViewRefs, } from '../view-base'; @@ -34,6 +34,7 @@ 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; +const MAX_ROWS_TO_SHOW_INITIALLY = 5; function getMeasuresForLane( allMeasures: ReactMeasure[], @@ -44,7 +45,7 @@ function getMeasuresForLane( export class ReactMeasuresView extends View { _profilerData: ReactProfilerData; - _intrinsicSize: Size; + _intrinsicSize: SizeWithMaxHeight; _lanesToRender: ReactLane[]; _laneToMeasures: Map; @@ -77,6 +78,7 @@ export class ReactMeasuresView extends View { this._intrinsicSize = { width: this._profilerData.duration, height: this._lanesToRender.length * REACT_LANE_HEIGHT, + maxInitialHeight: MAX_ROWS_TO_SHOW_INITIALLY * REACT_LANE_HEIGHT, }; } @@ -132,7 +134,7 @@ export class ReactMeasuresView extends View { case 'commit': fillStyle = COLORS.REACT_COMMIT; hoveredFillStyle = COLORS.REACT_COMMIT_HOVER; - groupSelectedFillStyle = COLORS.REACT_COMMIT_SELECTED; + groupSelectedFillStyle = COLORS.REACT_COMMIT_HOVER; break; case 'render-idle': // We could render idle time as diagonal hashes. @@ -140,22 +142,22 @@ export class ReactMeasuresView extends View { // color = context.createPattern(getIdlePattern(), 'repeat'); fillStyle = COLORS.REACT_IDLE; hoveredFillStyle = COLORS.REACT_IDLE_HOVER; - groupSelectedFillStyle = COLORS.REACT_IDLE_SELECTED; + groupSelectedFillStyle = COLORS.REACT_IDLE_HOVER; break; case 'render': fillStyle = COLORS.REACT_RENDER; hoveredFillStyle = COLORS.REACT_RENDER_HOVER; - groupSelectedFillStyle = COLORS.REACT_RENDER_SELECTED; + groupSelectedFillStyle = COLORS.REACT_RENDER_HOVER; break; case 'layout-effects': fillStyle = COLORS.REACT_LAYOUT_EFFECTS; hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER; - groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_SELECTED; + groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER; break; case 'passive-effects': fillStyle = COLORS.REACT_PASSIVE_EFFECTS; hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER; - groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_SELECTED; + groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER; break; default: throw new Error(`Unexpected measure type "${type}"`); @@ -306,7 +308,7 @@ export class ReactMeasuresView extends View { hoverTimestamp >= timestamp && hoverTimestamp <= timestamp + duration ) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; onHover(measure); return; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js similarity index 73% rename from packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js rename to packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js index 6d35a5f1ca644..37ff0832e0261 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactEvent, ReactProfilerData} from '../types'; +import type {SchedulingEvent, ReactProfilerData} from '../types'; import type { Interaction, MouseMoveInteraction, @@ -39,20 +39,12 @@ import { const EVENT_ROW_HEIGHT_FIXED = TOP_ROW_PADDING + REACT_EVENT_DIAMETER + TOP_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 { +export class SchedulingEventsView extends View { _profilerData: ReactProfilerData; _intrinsicSize: Size; - _hoveredEvent: ReactEvent | null = null; - onHover: ((event: ReactEvent | null) => void) | null = null; + _hoveredEvent: SchedulingEvent | null = null; + onHover: ((event: SchedulingEvent | null) => void) | null = null; constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { super(surface, frame); @@ -68,7 +60,7 @@ export class ReactEventsView extends View { return this._intrinsicSize; } - setHoveredEvent(hoveredEvent: ReactEvent | null) { + setHoveredEvent(hoveredEvent: SchedulingEvent | null) { if (this._hoveredEvent === hoveredEvent) { return; } @@ -77,18 +69,18 @@ export class ReactEventsView extends View { } /** - * Draw a single `ReactEvent` as a circle in the canvas. + * Draw a single `SchedulingEvent` as a circle in the canvas. */ - _drawSingleReactEvent( + _drawSingleSchedulingEvent( context: CanvasRenderingContext2D, rect: Rect, - event: ReactEvent, + event: SchedulingEvent, baseY: number, scaleFactor: number, showHoverHighlight: boolean, ) { const {frame} = this; - const {timestamp, type} = event; + const {timestamp, type, warning} = event; const x = timestampToPosition(timestamp, scaleFactor, frame); const radius = REACT_EVENT_DIAMETER / 2; @@ -105,34 +97,25 @@ export class ReactEventsView extends View { let fillStyle = null; - switch (type) { - case 'native-event': - return; - 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 { + if (warning !== null) { + fillStyle = showHoverHighlight + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; + } else { + switch (type) { + case 'schedule-render': + case 'schedule-state-update': + case 'schedule-force-update': 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; + break; + default: + if (__DEV__) { + console.warn('Unexpected event type "%s"', type); + } + break; + } } if (fillStyle !== null) { @@ -148,7 +131,7 @@ export class ReactEventsView extends View { draw(context: CanvasRenderingContext2D) { const { frame, - _profilerData: {reactEvents}, + _profilerData: {schedulingEvents}, _hoveredEvent, visibleArea, } = this; @@ -168,20 +151,14 @@ export class ReactEventsView extends View { frame, ); - const highlightedEvents: ReactEvent[] = []; + const highlightedEvents: SchedulingEvent[] = []; - reactEvents.forEach(event => { - if ( - event === _hoveredEvent || - (_hoveredEvent && - isSuspenseEvent(event) && - isSuspenseEvent(_hoveredEvent) && - event.id === _hoveredEvent.id) - ) { + schedulingEvents.forEach(event => { + if (event === _hoveredEvent) { highlightedEvents.push(event); return; } - this._drawSingleReactEvent( + this._drawSingleSchedulingEvent( context, visibleArea, event, @@ -194,7 +171,7 @@ export class ReactEventsView extends View { // 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( + this._drawSingleSchedulingEvent( context, visibleArea, event, @@ -244,7 +221,7 @@ export class ReactEventsView extends View { } const { - _profilerData: {reactEvents}, + _profilerData: {schedulingEvents}, } = this; const scaleFactor = positioningScaleFactor( this._intrinsicSize.width, @@ -258,15 +235,15 @@ export class ReactEventsView extends View { // 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 = reactEvents.length - 1; index >= 0; index--) { - const event = reactEvents[index]; + for (let index = schedulingEvents.length - 1; index >= 0; index--) { + const event = schedulingEvents[index]; const {timestamp} = event; if ( timestamp - eventTimestampAllowance <= hoverTimestamp && hoverTimestamp <= timestamp + eventTimestampAllowance ) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; onHover(event); return; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js new file mode 100644 index 0000000000000..3d85ffd4e2dc0 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -0,0 +1,368 @@ +/** + * 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 {SuspenseEvent, ReactProfilerData} from '../types'; +import type { + Interaction, + MouseMoveInteraction, + Rect, + SizeWithMaxHeight, + ViewRefs, +} from '../view-base'; + +import { + durationToWidth, + positioningScaleFactor, + positionToTimestamp, + timestampToPosition, + widthToDuration, +} from './utils/positioning'; +import {drawText} from './utils/text'; +import {formatDuration} from '../utils/formatting'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + BORDER_SIZE, + COLORS, + PENDING_SUSPENSE_EVENT_SIZE, + SUSPENSE_EVENT_HEIGHT, +} from './constants'; + +const ROW_WITH_BORDER_HEIGHT = SUSPENSE_EVENT_HEIGHT + BORDER_SIZE; +const MAX_ROWS_TO_SHOW_INITIALLY = 3; + +export class SuspenseEventsView extends View { + _depthToSuspenseEvent: Map; + _hoveredEvent: SuspenseEvent | null = null; + _intrinsicSize: SizeWithMaxHeight; + _maxDepth: number = 0; + _profilerData: ReactProfilerData; + + onHover: ((event: SuspenseEvent | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + + this._profilerData = profilerData; + + this._performPreflightComputations(); + } + + _performPreflightComputations() { + this._depthToSuspenseEvent = new Map(); + + const {duration, suspenseEvents} = this._profilerData; + + suspenseEvents.forEach(event => { + const depth = event.depth; + + this._maxDepth = Math.max(this._maxDepth, depth); + + if (!this._depthToSuspenseEvent.has(depth)) { + this._depthToSuspenseEvent.set(depth, [event]); + } else { + // $FlowFixMe This is unnecessary. + this._depthToSuspenseEvent.get(depth).push(event); + } + }); + + this._intrinsicSize = { + width: duration, + height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT, + maxInitialHeight: ROW_WITH_BORDER_HEIGHT * MAX_ROWS_TO_SHOW_INITIALLY, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredEvent(hoveredEvent: SuspenseEvent | null) { + if (this._hoveredEvent === hoveredEvent) { + return; + } + this._hoveredEvent = hoveredEvent; + this.setNeedsDisplay(); + } + + /** + * Draw a single `SuspenseEvent` as a box/span with text inside of it. + */ + _drawSingleSuspenseEvent( + context: CanvasRenderingContext2D, + rect: Rect, + event: SuspenseEvent, + baseY: number, + scaleFactor: number, + showHoverHighlight: boolean, + ) { + const {frame} = this; + const { + componentName, + depth, + duration, + phase, + resolution, + timestamp, + warning, + } = event; + + baseY += depth * ROW_WITH_BORDER_HEIGHT; + + let fillStyle = ((null: any): string); + if (warning !== null) { + fillStyle = showHoverHighlight + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; + } else { + switch (resolution) { + case 'rejected': + fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPENSE_REJECTED_EVENT_HOVER + : COLORS.REACT_SUSPENSE_REJECTED_EVENT; + break; + case 'resolved': + fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPENSE_RESOLVED_EVENT_HOVER + : COLORS.REACT_SUSPENSE_RESOLVED_EVENT; + break; + case 'unresolved': + fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER + : COLORS.REACT_SUSPENSE_UNRESOLVED_EVENT; + break; + } + } + + const xStart = timestampToPosition(timestamp, scaleFactor, frame); + + // Pending suspense events (ones that never resolved) won't have durations. + // So instead we draw them as diamonds. + if (duration === null) { + const size = PENDING_SUSPENSE_EVENT_SIZE; + const halfSize = size / 2; + + baseY += (SUSPENSE_EVENT_HEIGHT - PENDING_SUSPENSE_EVENT_SIZE) / 2; + + const y = baseY + halfSize; + + const suspenseRect: Rect = { + origin: { + x: xStart - halfSize, + y: baseY, + }, + size: {width: size, height: size}, + }; + if (!rectIntersectsRect(suspenseRect, rect)) { + return; // Not in view + } + + const drawableRect = intersectionOfRects(suspenseRect, rect); + + // Clip diamonds so they don't overflow if the view has been resized (smaller). + const region = new Path2D(); + region.rect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + context.save(); + context.clip(region); + context.beginPath(); + context.fillStyle = fillStyle; + context.moveTo(xStart, y - halfSize); + context.lineTo(xStart + halfSize, y); + context.lineTo(xStart, y + halfSize); + context.lineTo(xStart - halfSize, y); + context.fill(); + context.restore(); + } else { + const xStop = timestampToPosition( + timestamp + duration, + scaleFactor, + frame, + ); + const eventRect: Rect = { + origin: { + x: xStart, + y: baseY, + }, + size: {width: xStop - xStart, height: SUSPENSE_EVENT_HEIGHT}, + }; + if (!rectIntersectsRect(eventRect, rect)) { + return; // Not in view + } + + const width = durationToWidth(duration, scaleFactor); + if (width < 1) { + return; // Too small to render at this zoom level + } + + const drawableRect = intersectionOfRects(eventRect, rect); + context.beginPath(); + context.fillStyle = fillStyle; + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + + let label = 'suspended'; + if (componentName != null) { + label = `${componentName} ${label}`; + } + if (phase !== null) { + label += ` during ${phase}`; + } + if (resolution !== 'unresolved') { + label += ` - ${formatDuration(duration)}`; + } + + drawText(label, context, eventRect, drawableRect, width); + } + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {suspenseEvents}, + _hoveredEvent, + visibleArea, + } = this; + + context.fillStyle = COLORS.PRIORITY_BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + // Draw events + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + suspenseEvents.forEach(event => { + this._drawSingleSuspenseEvent( + context, + visibleArea, + event, + frame.origin.y, + scaleFactor, + event === _hoveredEvent, + ); + }); + + // Render bottom borders. + for (let i = 0; i <= this._maxDepth; i++) { + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_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, viewRefs: ViewRefs) { + const {frame, _intrinsicSize, onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + + const adjustedCanvasMouseY = location.y - frame.origin.y; + const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT); + const suspenseEventsAtDepth = this._depthToSuspenseEvent.get(depth); + + if (suspenseEventsAtDepth) { + // Find the event being hovered over. + for (let index = suspenseEventsAtDepth.length - 1; index >= 0; index--) { + const suspenseEvent = suspenseEventsAtDepth[index]; + const {duration, timestamp} = suspenseEvent; + + if (duration === null) { + const timestampAllowance = widthToDuration( + PENDING_SUSPENSE_EVENT_SIZE / 2, + scaleFactor, + ); + + if ( + timestamp - timestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + timestampAllowance + ) { + this.currentCursor = 'context-menu'; + + viewRefs.hoveredView = this; + + onHover(suspenseEvent); + return; + } + } else if ( + hoverTimestamp >= timestamp && + hoverTimestamp <= timestamp + duration + ) { + this.currentCursor = 'context-menu'; + + viewRefs.hoveredView = this; + + onHover(suspenseEvent); + return; + } + } + } + + onHover(null); + } + + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction, viewRefs); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js index 92c82c119d2ef..2249d3841ad7a 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js @@ -209,7 +209,7 @@ export class UserTimingMarksView extends View { frame, ); const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); - const markTimestampAllowance = widthToDuration( + const timestampAllowance = widthToDuration( USER_TIMING_MARK_SIZE / 2, scaleFactor, ); @@ -221,10 +221,10 @@ export class UserTimingMarksView extends View { const {timestamp} = mark; if ( - timestamp - markTimestampAllowance <= hoverTimestamp && - hoverTimestamp <= timestamp + markTimestampAllowance + timestamp - timestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + timestampAllowance ) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; onHover(mark); return; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index da3834892636d..27ce99a05a11e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -15,6 +15,8 @@ export const MARKER_TEXT_PADDING = 8; export const COLOR_HOVER_DIM_DELTA = 5; export const TOP_ROW_PADDING = 4; export const NATIVE_EVENT_HEIGHT = 14; +export const SUSPENSE_EVENT_HEIGHT = 14; +export const PENDING_SUSPENSE_EVENT_SIZE = 8; export const REACT_EVENT_DIAMETER = 6; export const USER_TIMING_MARK_SIZE = 8; export const REACT_MEASURE_HEIGHT = 9; @@ -43,44 +45,51 @@ export let COLORS = { BACKGROUND: '', NATIVE_EVENT: '', NATIVE_EVENT_HOVER: '', - NATIVE_EVENT_WARNING: '', - NATIVE_EVENT_WARNING_HOVER: '', - NATIVE_EVENT_WARNING_TEXT: '', PRIORITY_BACKGROUND: '', PRIORITY_BORDER: '', PRIORITY_LABEL: '', USER_TIMING: '', USER_TIMING_HOVER: '', REACT_IDLE: '', - REACT_IDLE_SELECTED: '', REACT_IDLE_HOVER: '', REACT_RENDER: '', - REACT_RENDER_SELECTED: '', REACT_RENDER_HOVER: '', REACT_COMMIT: '', - REACT_COMMIT_SELECTED: '', REACT_COMMIT_HOVER: '', REACT_LAYOUT_EFFECTS: '', - REACT_LAYOUT_EFFECTS_SELECTED: '', REACT_LAYOUT_EFFECTS_HOVER: '', REACT_PASSIVE_EFFECTS: '', - REACT_PASSIVE_EFFECTS_SELECTED: '', REACT_PASSIVE_EFFECTS_HOVER: '', REACT_RESIZE_BAR: '', + REACT_RESIZE_BAR_ACTIVE: '', + REACT_RESIZE_BAR_BORDER: '', + REACT_RESIZE_BAR_DOT: '', REACT_SCHEDULE: '', REACT_SCHEDULE_HOVER: '', - REACT_SCHEDULE_CASCADING: '', - REACT_SCHEDULE_CASCADING_HOVER: '', - REACT_SUSPEND: '', - REACT_SUSPEND_HOVER: '', + REACT_SUSPENSE_REJECTED_EVENT: '', + REACT_SUSPENSE_REJECTED_EVENT_HOVER: '', + REACT_SUSPENSE_RESOLVED_EVENT: '', + REACT_SUSPENSE_RESOLVED_EVENT_HOVER: '', + REACT_SUSPENSE_UNRESOLVED_EVENT: '', + REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: '', REACT_WORK_BORDER: '', + SCROLL_CARET: '', TEXT_COLOR: '', TIME_MARKER_LABEL: '', + WARNING_BACKGROUND: '', + WARNING_BACKGROUND_HOVER: '', + WARNING_TEXT: '', + WARNING_TEXT_INVERED: '', }; -export function updateColorsToMatchTheme(): void { +export function updateColorsToMatchTheme(): boolean { const computedStyle = getComputedStyle((document.body: any)); + // Check to see if styles have been initialized... + if (computedStyle.getPropertyValue('--color-background') == null) { + return false; + } + COLORS = { BACKGROUND: computedStyle.getPropertyValue('--color-background'), NATIVE_EVENT: computedStyle.getPropertyValue( @@ -89,15 +98,6 @@ export function updateColorsToMatchTheme(): void { NATIVE_EVENT_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-native-event-hover', ), - NATIVE_EVENT_WARNING: computedStyle.getPropertyValue( - '--color-scheduling-profiler-native-event-warning', - ), - NATIVE_EVENT_WARNING_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-native-event-warning-hover', - ), - NATIVE_EVENT_WARNING_TEXT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-native-event-warning-text', - ), PRIORITY_BACKGROUND: computedStyle.getPropertyValue( '--color-scheduling-profiler-priority-background', ), @@ -114,73 +114,86 @@ export function updateColorsToMatchTheme(): void { REACT_IDLE: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-idle', ), - REACT_IDLE_SELECTED: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-idle-selected', - ), REACT_IDLE_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-idle-hover', ), REACT_RENDER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-render', ), - REACT_RENDER_SELECTED: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-render-selected', - ), REACT_RENDER_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-render-hover', ), REACT_COMMIT: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-commit', ), - REACT_COMMIT_SELECTED: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-commit-selected', - ), REACT_COMMIT_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-commit-hover', ), REACT_LAYOUT_EFFECTS: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-layout-effects', ), - REACT_LAYOUT_EFFECTS_SELECTED: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-layout-effects-selected', - ), REACT_LAYOUT_EFFECTS_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-layout-effects-hover', ), REACT_PASSIVE_EFFECTS: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-passive-effects', ), - REACT_PASSIVE_EFFECTS_SELECTED: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-passive-effects-selected', - ), REACT_PASSIVE_EFFECTS_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-passive-effects-hover', ), REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'), + REACT_RESIZE_BAR_ACTIVE: computedStyle.getPropertyValue( + '--color-resize-bar-active', + ), + REACT_RESIZE_BAR_BORDER: computedStyle.getPropertyValue( + '--color-resize-bar-border', + ), + REACT_RESIZE_BAR_DOT: computedStyle.getPropertyValue( + '--color-resize-bar-dot', + ), REACT_SCHEDULE: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-schedule', ), REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-schedule-hover', ), - REACT_SCHEDULE_CASCADING: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-schedule-cascading', + REACT_SUSPENSE_REJECTED_EVENT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-rejected', + ), + REACT_SUSPENSE_REJECTED_EVENT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-rejected-hover', + ), + REACT_SUSPENSE_RESOLVED_EVENT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-resolved', ), - REACT_SCHEDULE_CASCADING_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-schedule-cascading-hover', + REACT_SUSPENSE_RESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-resolved-hover', ), - REACT_SUSPEND: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspend', + REACT_SUSPENSE_UNRESOLVED_EVENT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-unresolved', ), - REACT_SUSPEND_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspend-hover', + REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-unresolved-hover', ), REACT_WORK_BORDER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-work-border', ), + SCROLL_CARET: computedStyle.getPropertyValue('--color-scroll-caret'), TEXT_COLOR: computedStyle.getPropertyValue( '--color-scheduling-profiler-text-color', ), TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'), + WARNING_BACKGROUND: computedStyle.getPropertyValue( + '--color-warning-background', + ), + WARNING_BACKGROUND_HOVER: computedStyle.getPropertyValue( + '--color-warning-background-hover', + ), + WARNING_TEXT: computedStyle.getPropertyValue('--color-warning-text-color'), + WARNING_TEXT_INVERED: computedStyle.getPropertyValue( + '--color-warning-text-color-inverted', + ), }; + + return true; } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/index.js b/packages/react-devtools-scheduling-profiler/src/content-views/index.js index b50490b13ae92..588f3e5d7bd90 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/index.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/index.js @@ -9,7 +9,8 @@ export * from './FlamechartView'; export * from './NativeEventsView'; -export * from './ReactEventsView'; export * from './ReactMeasuresView'; +export * from './SchedulingEventsView'; +export * from './SuspenseEventsView'; export * from './TimeAxisMarkersView'; export * from './UserTimingMarksView'; diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js b/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js new file mode 100644 index 0000000000000..da14733bce62c --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js @@ -0,0 +1,98 @@ +/** + * 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'; + +import {rectEqualToRect} from '../../view-base'; +import {COLORS, FONT_SIZE, TEXT_PADDING} from '../constants'; + +const cachedTextWidths = new Map(); + +export function trimText( + context: CanvasRenderingContext2D, + text: string, + width: number, +): string | null { + for (let i = text.length - 1; i >= 0; i--) { + const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…'; + + let measuredWidth = cachedTextWidths.get(trimmedText); + if (measuredWidth == null) { + measuredWidth = context.measureText(trimmedText).width; + cachedTextWidths.set(trimmedText, measuredWidth); + } + + if (measuredWidth <= width) { + return trimmedText; + } + } + + return null; +} + +export function drawText( + text: string, + context: CanvasRenderingContext2D, + fullRect: Rect, + drawableRect: Rect, + availableWidth: number, + textAlign: 'left' | 'center' = 'left', + fillStyle: string = COLORS.TEXT_COLOR, +): void { + if (availableWidth > TEXT_PADDING * 2) { + context.textAlign = textAlign; + context.textBaseline = 'middle'; + context.font = `${FONT_SIZE}px sans-serif`; + + const {x, y} = fullRect.origin; + + const trimmedName = trimText( + context, + text, + availableWidth - TEXT_PADDING * 2 + (x < 0 ? x : 0), + ); + + if (trimmedName !== null) { + context.fillStyle = fillStyle; + + // Prevent text from visibly overflowing its container when clipped. + const textOverflowsViewableArea = !rectEqualToRect( + drawableRect, + fullRect, + ); + if (textOverflowsViewableArea) { + context.save(); + context.beginPath(); + context.rect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + context.closePath(); + context.clip(); + } + + let textX; + if (textAlign === 'center') { + textX = x + availableWidth / 2 + TEXT_PADDING - (x < 0 ? x : 0); + } else { + textX = x + TEXT_PADDING - (x < 0 ? x : 0); + } + + const textY = y + fullRect.size.height / 2; + + context.fillText(trimmedName, textX, textY); + + if (textOverflowsViewableArea) { + context.restore(); + } + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js index 649d90246993c..bb10bca4d8552 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js @@ -208,8 +208,9 @@ describe(preprocessData, () => { measures: [], nativeEvents: [], otherUserTimingMarks: [], - reactEvents: [], + schedulingEvents: [], startTime: 1, + suspenseEvents: [], }); }); @@ -300,16 +301,17 @@ describe(preprocessData, () => { ], nativeEvents: [], otherUserTimingMarks: [], - reactEvents: [ + schedulingEvents: [ { - componentStack: '', laneLabels: [], lanes: [9], timestamp: 0.002, type: 'schedule-render', + warning: null, }, ], startTime: 1, + suspenseEvents: [], }); }); @@ -372,16 +374,17 @@ describe(preprocessData, () => { timestamp: 0.004, }, ], - reactEvents: [ + schedulingEvents: [ { - componentStack: '', laneLabels: ['Sync'], lanes: [0], timestamp: 0.005, type: 'schedule-render', + warning: null, }, ], startTime: 1, + suspenseEvents: [], }); }); @@ -507,25 +510,25 @@ describe(preprocessData, () => { timestamp: 0.004, }, ], - reactEvents: [ + schedulingEvents: [ { - componentStack: '', laneLabels: ['Default'], lanes: [4], timestamp: 0.005, type: 'schedule-render', + warning: null, }, { componentName: 'App', - componentStack: '', - isCascading: false, laneLabels: ['Default'], lanes: [4], timestamp: 0.013, type: 'schedule-state-update', + warning: null, }, ], startTime: 1, + suspenseEvents: [], }); }); diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index 12a5bb644a330..a365be120897c 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -20,6 +20,7 @@ import type { ReactLane, ReactMeasureType, ReactProfilerData, + SuspenseEvent, } from '../types'; import {REACT_TOTAL_NUM_LANES} from '../constants'; @@ -34,13 +35,25 @@ type MeasureStackElement = {| |}; type ProcessorState = {| - nextRenderShouldGenerateNewBatchID: boolean, batchUID: BatchUID, - uidCounter: BatchUID, measureStack: MeasureStackElement[], + nativeEventStack: NativeEvent[], + nextRenderShouldGenerateNewBatchID: boolean, + uidCounter: BatchUID, + unresolvedSuspenseEvents: Map, |}; -let nativeEventStack: Array = []; +const NATIVE_EVENT_DURATION_THRESHOLD = 20; + +const WARNING_STRINGS = { + LONG_EVENT_HANDLER: + 'An event handler scheduled a big update with React. Consider using the Transition API to defer some of this work.', + NESTED_UPDATE: + 'A nested update was scheduled during layout. These updates require React to re-render synchronously before the browser can paint.', + SUSPENDD_DURING_UPATE: + 'A component suspended during an update which caused a fallback to be shown. ' + + "Consider using the Transition API to avoid hiding components after they've been mounted.", +}; // Exported for tests export function getLanesFromTransportDecimalBitmask( @@ -174,44 +187,54 @@ function processTimelineEvent( const stackTrace = args.data.stackTrace; if (stackTrace) { const topFrame = stackTrace[stackTrace.length - 1]; - if (topFrame.url.includes('node_modules/react-dom')) { + if (topFrame.url.includes('/react-dom.')) { // Filter out fake React events dispatched by invokeGuardedCallbackDev. return; } } } - const timestamp = (ts - currentProfilerData.startTime) / 1000; - const duration = event.dur / 1000; - - let depth = 0; - - while (nativeEventStack.length > 0) { - const prevNativeEvent = nativeEventStack[nativeEventStack.length - 1]; - const prevStopTime = - prevNativeEvent.timestamp + prevNativeEvent.duration; - - if (timestamp < prevStopTime) { - depth = prevNativeEvent.depth + 1; - break; - } else { - nativeEventStack.pop(); + // Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant + if ( + type.startsWith('blur') || + type.startsWith('click') || + type.startsWith('focus') || + type.startsWith('mouse') || + type.startsWith('pointer') + ) { + const timestamp = (ts - currentProfilerData.startTime) / 1000; + const duration = event.dur / 1000; + + let depth = 0; + + while (state.nativeEventStack.length > 0) { + const prevNativeEvent = + state.nativeEventStack[state.nativeEventStack.length - 1]; + const prevStopTime = + prevNativeEvent.timestamp + prevNativeEvent.duration; + + if (timestamp < prevStopTime) { + depth = prevNativeEvent.depth + 1; + break; + } else { + state.nativeEventStack.pop(); + } } - } - const nativeEvent = { - depth, - duration, - timestamp, - type, - warnings: null, - }; + const nativeEvent = { + depth, + duration, + timestamp, + type, + warning: null, + }; - currentProfilerData.nativeEvents.push(nativeEvent); + currentProfilerData.nativeEvents.push(nativeEvent); - // Keep track of curent event in case future ones overlap. - // We separate them into different vertical lanes in this case. - nativeEventStack.push(nativeEvent); + // Keep track of curent event in case future ones overlap. + // We separate them into different vertical lanes in this case. + state.nativeEventStack.push(nativeEvent); + } } break; case 'blink.user_timing': @@ -219,92 +242,141 @@ function processTimelineEvent( // React Events - schedule if (name.startsWith('--schedule-render-')) { - const [ - laneBitmaskString, - laneLabels, - ...splitComponentStack - ] = name.substr(18).split('-'); - currentProfilerData.reactEvents.push({ + const [laneBitmaskString, laneLabels] = name.substr(18).split('-'); + currentProfilerData.schedulingEvents.push({ type: 'schedule-render', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), laneLabels: laneLabels ? laneLabels.split(',') : [], - componentStack: splitComponentStack.join('-'), timestamp: startTime, + warning: null, }); } else if (name.startsWith('--schedule-forced-update-')) { - const [ - laneBitmaskString, - laneLabels, - componentName, - ...splitComponentStack - ] = name.substr(25).split('-'); - const isCascading = !!state.measureStack.find( - ({type}) => type === 'commit', - ); - currentProfilerData.reactEvents.push({ + const [laneBitmaskString, laneLabels, componentName] = name + .substr(25) + .split('-'); + + let warning = null; + if (state.measureStack.find(({type}) => type === 'commit')) { + // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold. + warning = WARNING_STRINGS.NESTED_UPDATE; + } + + currentProfilerData.schedulingEvents.push({ type: 'schedule-force-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), laneLabels: laneLabels ? laneLabels.split(',') : [], componentName, - componentStack: splitComponentStack.join('-'), timestamp: startTime, - isCascading, + warning, }); } else if (name.startsWith('--schedule-state-update-')) { - const [ - laneBitmaskString, - laneLabels, - componentName, - ...splitComponentStack - ] = name.substr(24).split('-'); - const isCascading = !!state.measureStack.find( - ({type}) => type === 'commit', - ); - currentProfilerData.reactEvents.push({ + const [laneBitmaskString, laneLabels, componentName] = name + .substr(24) + .split('-'); + + let warning = null; + if (state.measureStack.find(({type}) => type === 'commit')) { + // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold. + warning = WARNING_STRINGS.NESTED_UPDATE; + } + + currentProfilerData.schedulingEvents.push({ type: 'schedule-state-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), laneLabels: laneLabels ? laneLabels.split(',') : [], componentName, - componentStack: splitComponentStack.join('-'), timestamp: startTime, - isCascading, + warning, }); } // eslint-disable-line brace-style // React Events - suspense else if (name.startsWith('--suspense-suspend-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(19) - .split('-'); - currentProfilerData.reactEvents.push({ - type: 'suspense-suspend', - id, - componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, + const [id, componentName, ...rest] = name.substr(19).split('-'); + + // Older versions of the scheduling profiler data didn't contain phase or lane values. + let phase = null; + let warning = null; + if (rest.length === 3) { + switch (rest[0]) { + case 'mount': + case 'update': + phase = rest[0]; + break; + } + + if (phase === 'update') { + const laneLabels = rest[2]; + // HACK This is a bit gross but the numeric lane value might change between render versions. + if (!laneLabels.includes('Transition')) { + warning = WARNING_STRINGS.SUSPENDD_DURING_UPATE; + } + } + } + + const availableDepths = new Array( + state.unresolvedSuspenseEvents.size + 1, + ).fill(true); + state.unresolvedSuspenseEvents.forEach(({depth}) => { + availableDepths[depth] = false; }); - } else if (name.startsWith('--suspense-resolved-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(20) - .split('-'); - currentProfilerData.reactEvents.push({ - type: 'suspense-resolved', - id, + + let depth = 0; + for (let i = 0; i < availableDepths.length; i++) { + if (availableDepths[i]) { + depth = i; + break; + } + } + + // TODO (scheduling profiler) Maybe we should calculate depth in post, + // so unresolved Suspense requests don't take up space. + // We can't know if they'll be resolved or not at this point. + // We'll just give them a default (fake) duration width. + + const suspenseEvent = { componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - }); - } else if (name.startsWith('--suspense-rejected-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(20) - .split('-'); - currentProfilerData.reactEvents.push({ - type: 'suspense-rejected', + depth, + duration: null, id, - componentName, - componentStack: splitComponentStack.join('-'), + phase, + resolution: 'unresolved', + resuspendTimestamps: null, timestamp: startTime, - }); + type: 'suspense', + warning, + }; + + currentProfilerData.suspenseEvents.push(suspenseEvent); + state.unresolvedSuspenseEvents.set(id, suspenseEvent); + } else if (name.startsWith('--suspense-resuspend-')) { + const [id] = name.substr(21).split('-'); + const suspenseEvent = state.unresolvedSuspenseEvents.get(id); + if (suspenseEvent != null) { + if (suspenseEvent.resuspendTimestamps === null) { + suspenseEvent.resuspendTimestamps = [startTime]; + } else { + suspenseEvent.resuspendTimestamps.push(startTime); + } + } + } else if (name.startsWith('--suspense-resolved-')) { + const [id] = name.substr(20).split('-'); + const suspenseEvent = state.unresolvedSuspenseEvents.get(id); + if (suspenseEvent != null) { + state.unresolvedSuspenseEvents.delete(id); + + suspenseEvent.duration = startTime - suspenseEvent.timestamp; + suspenseEvent.resolution = 'resolved'; + } + } else if (name.startsWith('--suspense-rejected-')) { + const [id] = name.substr(20).split('-'); + const suspenseEvent = state.unresolvedSuspenseEvents.get(id); + if (suspenseEvent != null) { + state.unresolvedSuspenseEvents.delete(id); + + suspenseEvent.duration = startTime - suspenseEvent.timestamp; + suspenseEvent.resolution = 'rejected'; + } } // eslint-disable-line brace-style // React Measures - render @@ -335,17 +407,14 @@ function processTimelineEvent( state, ); - for (let i = 0; i < nativeEventStack.length; i++) { - const nativeEvent = nativeEventStack[i]; + for (let i = 0; i < state.nativeEventStack.length; i++) { + const nativeEvent = state.nativeEventStack[i]; const stopTime = nativeEvent.timestamp + nativeEvent.duration; - if (stopTime > startTime) { - const warning = - 'An event handler scheduled a synchronous update with React.'; - if (nativeEvent.warnings === null) { - nativeEvent.warnings = new Set([warning]); - } else { - nativeEvent.warnings.add(warning); - } + if ( + stopTime > startTime && + nativeEvent.duration > NATIVE_EVENT_DURATION_THRESHOLD + ) { + nativeEvent.warning = WARNING_STRINGS.LONG_EVENT_HANDLER; } } } else if ( @@ -509,18 +578,17 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart { export default function preprocessData( timeline: TimelineEvent[], ): ReactProfilerData { - nativeEventStack = []; - const flamechart = preprocessFlamechart(timeline); const profilerData: ReactProfilerData = { - startTime: 0, duration: 0, - nativeEvents: [], - reactEvents: [], - measures: [], flamechart, + measures: [], + nativeEvents: [], otherUserTimingMarks: [], + schedulingEvents: [], + startTime: 0, + suspenseEvents: [], }; // Sort `timeline`. JSON Array Format trace events need not be ordered. See: @@ -551,9 +619,11 @@ export default function preprocessData( const state: ProcessorState = { batchUID: 0, - uidCounter: 0, - nextRenderShouldGenerateNewBatchID: true, measureStack: [], + nativeEventStack: [], + nextRenderShouldGenerateNewBatchID: true, + uidCounter: 0, + unresolvedSuspenseEvents: new Map(), }; timeline.forEach(event => processTimelineEvent(event, profilerData, state)); diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index eb2ef87d407a9..674dabdc8e93a 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -26,13 +26,13 @@ export type NativeEvent = {| +duration: Milliseconds, +timestamp: Milliseconds, +type: string, - warnings: Set | null, + warning: string | null, |}; type BaseReactEvent = {| +componentName?: string, - +componentStack?: string, +timestamp: Milliseconds, + warning: string | null, |}; type BaseReactScheduleEvent = {| @@ -42,44 +42,33 @@ type BaseReactScheduleEvent = {| |}; export type ReactScheduleRenderEvent = {| ...BaseReactScheduleEvent, - type: 'schedule-render', + +type: 'schedule-render', |}; export type ReactScheduleStateUpdateEvent = {| ...BaseReactScheduleEvent, - type: 'schedule-state-update', - isCascading: boolean, + +type: 'schedule-state-update', |}; export type ReactScheduleForceUpdateEvent = {| ...BaseReactScheduleEvent, - type: 'schedule-force-update', - isCascading: boolean, + +type: 'schedule-force-update', |}; -type BaseReactSuspenseEvent = {| +export type SuspenseEvent = {| ...BaseReactEvent, - id: string, -|}; -export type ReactSuspenseSuspendEvent = {| - ...BaseReactSuspenseEvent, - type: 'suspense-suspend', -|}; -export type ReactSuspenseResolvedEvent = {| - ...BaseReactSuspenseEvent, - type: 'suspense-resolved', -|}; -export type ReactSuspenseRejectedEvent = {| - ...BaseReactSuspenseEvent, - type: 'suspense-rejected', + depth: number, + duration: number | null, + +id: string, + +phase: 'mount' | 'update' | null, + resolution: 'rejected' | 'resolved' | 'unresolved', + resuspendTimestamps: Array | null, + +type: 'suspense', |}; -export type ReactEvent = +export type SchedulingEvent = | ReactScheduleRenderEvent | ReactScheduleStateUpdateEvent - | ReactScheduleForceUpdateEvent - | ReactSuspenseSuspendEvent - | ReactSuspenseResolvedEvent - | ReactSuspenseRejectedEvent; -export type ReactEventType = $PropertyType; + | ReactScheduleForceUpdateEvent; +export type SchedulingEventType = $PropertyType; export type ReactMeasureType = | 'commit' @@ -128,20 +117,22 @@ export type FlamechartStackLayer = FlamechartStackFrame[]; export type Flamechart = FlamechartStackLayer[]; export type ReactProfilerData = {| - startTime: number, duration: number, - nativeEvents: NativeEvent[], - reactEvents: ReactEvent[], - measures: ReactMeasure[], flamechart: Flamechart, + measures: ReactMeasure[], + nativeEvents: NativeEvent[], otherUserTimingMarks: UserTimingMark[], + schedulingEvents: SchedulingEvent[], + startTime: number, + suspenseEvents: SuspenseEvent[], |}; export type ReactHoverContextInfo = {| - nativeEvent: NativeEvent | null, - reactEvent: ReactEvent | null, - measure: ReactMeasure | null, data: $ReadOnly | null, flamechartStackFrame: FlamechartStackFrame | null, + measure: ReactMeasure | null, + nativeEvent: NativeEvent | null, + schedulingEvent: SchedulingEvent | null, + suspenseEvent: SuspenseEvent | null, userTimingMark: UserTimingMark | null, |}; diff --git a/packages/react-devtools-scheduling-profiler/src/utils/formatting.js b/packages/react-devtools-scheduling-profiler/src/utils/formatting.js new file mode 100644 index 0000000000000..f5e589444236c --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/utils/formatting.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 prettyMilliseconds from 'pretty-ms'; + +export function formatTimestamp(ms: number) { + return ( + ms.toLocaleString(undefined, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }) + 'ms' + ); +} + +export function formatDuration(ms: number) { + return prettyMilliseconds(ms, {millisecondsDecimalDigits: 1}); +} + +export function trimString(string: string, length: number): string { + if (string.length > length) { + return `${string.substr(0, length - 1)}…`; + } + return string; +} diff --git a/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js b/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js index 4afe1fe99eadd..bbf36acb95c17 100644 --- a/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/utils/useSmartTooltip.js @@ -9,7 +9,8 @@ import {useLayoutEffect, useRef} from 'react'; -const TOOLTIP_OFFSET = 4; +const TOOLTIP_OFFSET_BOTTOM = 10; +const TOOLTIP_OFFSET_TOP = 5; export default function useSmartTooltip({ canvasRef, @@ -37,41 +38,41 @@ export default function useSmartTooltip({ const element = ref.current; if (element !== null) { // Let's check the vertical position. - if (mouseY + TOOLTIP_OFFSET + element.offsetHeight >= height) { + if (mouseY + TOOLTIP_OFFSET_BOTTOM + element.offsetHeight >= height) { // 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) { + if (mouseY - TOOLTIP_OFFSET_TOP - element.offsetHeight > 0) { // We position the tooltip above the mouse cursor if it fits there. element.style.top = `${mouseY - element.offsetHeight - - TOOLTIP_OFFSET}px`; + TOOLTIP_OFFSET_TOP}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`; + element.style.top = `${mouseY + TOOLTIP_OFFSET_BOTTOM}px`; } // Now let's check the horizontal position. - if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= width) { + if (mouseX + TOOLTIP_OFFSET_BOTTOM + element.offsetWidth >= width) { // 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) { + if (mouseX - TOOLTIP_OFFSET_TOP - 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`; + TOOLTIP_OFFSET_TOP}px`; } else { // Otherwise, align the tooltip with the window's left edge. element.style.left = '0px'; } } else { - element.style.left = `${mouseX + TOOLTIP_OFFSET}px`; + element.style.left = `${mouseX + TOOLTIP_OFFSET_BOTTOM}px`; } } }, [mouseX, mouseY, ref]); diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js b/packages/react-devtools-scheduling-profiler/src/view-base/BackgroundColorView.js similarity index 51% rename from packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js rename to packages/react-devtools-scheduling-profiler/src/view-base/BackgroundColorView.js index 551814eab6d36..bcacbd4526408 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ColorView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/BackgroundColorView.js @@ -7,33 +7,17 @@ * @flow */ -import type {Rect} from './geometry'; - -import {Surface} from './Surface'; import {View} from './View'; +import {COLORS} from '../content-views/constants'; /** * 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(); - } - +export class BackgroundColorView extends View { draw(context: CanvasRenderingContext2D) { - const {_color, visibleArea} = this; - context.fillStyle = _color; + const {visibleArea} = this; + + context.fillStyle = COLORS.BACKGROUND; context.fillRect( visibleArea.origin.x, visibleArea.origin.y, diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js index 8d8f427596ce3..78d90e2495da9 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js @@ -14,8 +14,6 @@ import type { MouseUpInteraction, WheelPlainInteraction, WheelWithShiftInteraction, - WheelWithControlInteraction, - WheelWithMetaInteraction, } from './useCanvasInteraction'; import type {Rect} from './geometry'; import type {ScrollState} from './utils/scrollState'; @@ -202,7 +200,7 @@ export class HorizontalPanAndZoomView extends View { } } - _handleWheelPlain(interaction: WheelPlainInteraction) { + _handleWheel(interaction: WheelPlainInteraction | WheelWithShiftInteraction) { const { location, delta: {deltaX, deltaY}, @@ -214,51 +212,41 @@ export class HorizontalPanAndZoomView extends View { const absDeltaX = Math.abs(deltaX); const absDeltaY = Math.abs(deltaY); - if (absDeltaY > absDeltaX) { - return; // Scrolling vertically - } - if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) { - return; - } - - const newState = translateState({ - state: this._scrollState, - delta: -deltaX, - containerLength: this.frame.size.width, - }); - this._setStateAndInformCallbacksIfChanged(newState); - } - - _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; + // Vertical scrolling zooms in and out (unless the SHIFT modifier is used). + // Horizontal scrolling pans. + if (absDeltaY > absDeltaX) { + if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) { + return; + } + + if (interaction.type === 'wheel-shift') { + // Shift modifier is for scrolling, not zooming. + return; + } + + const newState = zoomState({ + state: this._scrollState, + multiplier: 1 + 0.005 * -deltaY, + fixedPoint: location.x - this._scrollState.offset, + + minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL, + maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL, + containerLength: this.frame.size.width, + }); + this._setStateAndInformCallbacksIfChanged(newState); + } else { + if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) { + return; + } + + const newState = translateState({ + state: this._scrollState, + delta: -deltaX, + containerLength: this.frame.size.width, + }); + this._setStateAndInformCallbacksIfChanged(newState); } - - const newState = zoomState({ - state: this._scrollState, - multiplier: 1 + 0.005 * -deltaY, - fixedPoint: location.x - this._scrollState.offset, - - minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL, - maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL, - containerLength: this.frame.size.width, - }); - this._setStateAndInformCallbacksIfChanged(newState); } handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { @@ -273,12 +261,8 @@ export class HorizontalPanAndZoomView extends View { this._handleMouseUp(interaction, viewRefs); break; case 'wheel-plain': - this._handleWheelPlain(interaction); - break; case 'wheel-shift': - case 'wheel-control': - case 'wheel-meta': - this._handleWheelZoom(interaction); + this._handleWheel(interaction); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js deleted file mode 100644 index 177fe5f2c76d1..0000000000000 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js +++ /dev/null @@ -1,339 +0,0 @@ -/** - * 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 type {ViewRefs} from './Surface'; - -import {COLORS} from '../content-views/constants'; -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 { - switch (state) { - case 'normal': - case 'hovered': - case 'dragging': - return COLORS.REACT_RESIZE_BAR; - } - 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, viewRefs: ViewRefs) { - const cursorInView = rectContainsPoint( - interaction.payload.location, - this.frame, - ); - if (cursorInView) { - this._setInteractionState('dragging'); - viewRefs.activeView = this; - } - } - - _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) { - const cursorInView = rectContainsPoint( - interaction.payload.location, - this.frame, - ); - - if (cursorInView || viewRefs.activeView === this) { - this.currentCursor = 'ns-resize'; - } - if (cursorInView) { - viewRefs.hoveredView = this; - } - - if (this._interactionState === 'dragging') { - return; - } - this._setInteractionState(cursorInView ? 'hovered' : 'normal'); - } - - _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) { - const cursorInView = rectContainsPoint( - interaction.payload.location, - this.frame, - ); - if (this._interactionState === 'dragging') { - this._setInteractionState(cursorInView ? 'hovered' : 'normal'); - } - - if (viewRefs.activeView === this) { - viewRefs.activeView = null; - } - } - - handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { - switch (interaction.type) { - case 'mousedown': - this._handleMouseDown(interaction, viewRefs); - return; - case 'mousemove': - this._handleMouseMove(interaction, viewRefs); - return; - case 'mouseup': - this._handleMouseUp(interaction, viewRefs); - return; - } - } -} - -export class ResizableSplitView extends View { - _canvasRef: {current: HTMLCanvasElement | null}; - _resizingState: ResizingState | null = null; - _layoutState: LayoutState; - - constructor( - surface: Surface, - frame: Rect, - topSubview: View, - bottomSubview: View, - canvasRef: {current: HTMLCanvasElement | null}, - ) { - super(surface, frame, noopLayout); - - this._canvasRef = canvasRef; - - 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; - } - } - - _didGrab: boolean = false; - - getCursorActiveSubView(interaction: Interaction): View | null { - const cursorLocation = interaction.payload.location; - const resizeBarFrame = this._getResizeBar().frame; - if (rectContainsPoint(cursorLocation, resizeBarFrame)) { - return this; - } else { - return null; - } - } - - handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { - 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/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js new file mode 100644 index 0000000000000..54a3b05c1a9cf --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -0,0 +1,418 @@ +/** + * 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 { + ClickInteraction, + DoubleClickInteraction, + Interaction, + MouseDownInteraction, + MouseMoveInteraction, + MouseUpInteraction, +} from './useCanvasInteraction'; +import type {Rect} from './geometry'; +import type {ViewRefs} from './Surface'; + +import {BORDER_SIZE, COLORS} from '../content-views/constants'; +import {drawText} from '../content-views/utils/text'; +import {Surface} from './Surface'; +import {View} from './View'; +import {intersectionOfRects, rectContainsPoint} from './geometry'; +import {noopLayout} from './layouter'; +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, +|}>; + +const RESIZE_BAR_DOT_RADIUS = 1; +const RESIZE_BAR_DOT_SPACING = 4; +const RESIZE_BAR_HEIGHT = 8; +const RESIZE_BAR_WITH_LABEL_HEIGHT = 16; + +const HIDDEN_RECT = { + origin: {x: 0, y: 0}, + size: {width: 0, height: 0}, +}; + +class ResizeBar extends View { + _interactionState: ResizeBarState = 'normal'; + _label: string; + + showLabel: boolean = false; + + constructor(surface: Surface, frame: Rect, label: string) { + super(surface, frame, noopLayout); + + this._label = label; + } + + desiredSize() { + return this.showLabel + ? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0} + : {height: RESIZE_BAR_HEIGHT, width: 0}; + } + + draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) { + const {visibleArea} = this; + const {x, y} = visibleArea.origin; + const {width, height} = visibleArea.size; + + const isActive = + this._interactionState === 'dragging' || + (this._interactionState === 'hovered' && viewRefs.activeView === null); + + context.fillStyle = isActive + ? COLORS.REACT_RESIZE_BAR_ACTIVE + : COLORS.REACT_RESIZE_BAR; + context.fillRect(x, y, width, height); + + context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER; + context.fillRect(x, y, width, BORDER_SIZE); + context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE); + + const horizontalCenter = x + width / 2; + const verticalCenter = y + height / 2; + + if (this.showLabel) { + // When the resize view is collapsed entirely, + // rather than showing a resize bar– this view displays a label. + const labelRect: Rect = { + origin: { + x: 0, + y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT, + }, + size: { + width: visibleArea.size.width, + height: visibleArea.size.height, + }, + }; + + const drawableRect = intersectionOfRects(labelRect, this.visibleArea); + + drawText( + this._label, + context, + labelRect, + drawableRect, + visibleArea.size.width, + 'center', + COLORS.REACT_RESIZE_BAR_DOT, + ); + } else { + // Otherwise draw horizontally centered resize bar dots + context.beginPath(); + context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT; + context.arc( + horizontalCenter, + verticalCenter, + RESIZE_BAR_DOT_RADIUS, + 0, + 2 * Math.PI, + ); + context.arc( + horizontalCenter + RESIZE_BAR_DOT_SPACING, + verticalCenter, + RESIZE_BAR_DOT_RADIUS, + 0, + 2 * Math.PI, + ); + context.arc( + horizontalCenter - RESIZE_BAR_DOT_SPACING, + verticalCenter, + RESIZE_BAR_DOT_RADIUS, + 0, + 2 * Math.PI, + ); + context.fill(); + } + } + + _setInteractionState(state: ResizeBarState) { + if (this._interactionState === state) { + return; + } + this._interactionState = state; + this.setNeedsDisplay(); + } + + _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (cursorInView) { + this._setInteractionState('dragging'); + viewRefs.activeView = this; + } + } + + _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + + if (viewRefs.activeView === this) { + // If we're actively dragging this resize bar, + // show the cursor even if the pointer isn't hovering over this view. + this.currentCursor = 'ns-resize'; + } else if (cursorInView) { + if (this.showLabel) { + this.currentCursor = 'pointer'; + } else { + this.currentCursor = 'ns-resize'; + } + } + + if (cursorInView) { + viewRefs.hoveredView = this; + } + + if (this._interactionState === 'dragging') { + return; + } + this._setInteractionState(cursorInView ? 'hovered' : 'normal'); + } + + _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (this._interactionState === 'dragging') { + this._setInteractionState(cursorInView ? 'hovered' : 'normal'); + } + + if (viewRefs.activeView === this) { + viewRefs.activeView = null; + } + } + + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { + switch (interaction.type) { + case 'mousedown': + this._handleMouseDown(interaction, viewRefs); + return; + case 'mousemove': + this._handleMouseMove(interaction, viewRefs); + return; + case 'mouseup': + this._handleMouseUp(interaction, viewRefs); + return; + } + } +} + +export class ResizableView extends View { + _canvasRef: {current: HTMLCanvasElement | null}; + _layoutState: LayoutState; + _resizeBar: ResizeBar; + _resizingState: ResizingState | null = null; + _subview: View; + + constructor( + surface: Surface, + frame: Rect, + subview: View, + canvasRef: {current: HTMLCanvasElement | null}, + label: string, + ) { + super(surface, frame, noopLayout); + + this._canvasRef = canvasRef; + + this._subview = subview; + this._resizeBar = new ResizeBar(surface, frame, label); + + this.addSubview(this._subview); + this.addSubview(this._resizeBar); + + const subviewDesiredSize = subview.desiredSize(); + this._updateLayoutStateAndResizeBar( + subviewDesiredSize.maxInitialHeight != null + ? Math.min( + subviewDesiredSize.maxInitialHeight, + subviewDesiredSize.height, + ) + : subviewDesiredSize.height, + ); + } + + desiredSize() { + const resizeBarDesiredSize = this._resizeBar.desiredSize(); + + return { + width: this.frame.size.width, + height: this._layoutState.barOffsetY + resizeBarDesiredSize.height, + }; + } + + layoutSubviews() { + this._updateLayoutState(); + this._updateSubviewFrames(); + + super.layoutSubviews(); + } + + _updateLayoutStateAndResizeBar(barOffsetY: number) { + if (barOffsetY <= RESIZE_BAR_WITH_LABEL_HEIGHT - RESIZE_BAR_HEIGHT) { + barOffsetY = 0; + } + + this._layoutState = { + ...this._layoutState, + barOffsetY, + }; + + this._resizeBar.showLabel = barOffsetY === 0; + } + + _updateLayoutState() { + const {frame, _resizingState} = this; + + // Allow bar to travel to bottom of the visible area of this view but no further + const subviewDesiredSize = this._subview.desiredSize(); + const maxBarOffset = subviewDesiredSize.height; + + let proposedBarOffsetY = this._layoutState.barOffsetY; + // Update bar offset if dragging bar + if (_resizingState) { + const {mouseY, cursorOffsetInBarFrame} = _resizingState; + proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame; + } + + this._updateLayoutStateAndResizeBar( + clamp(0, maxBarOffset, proposedBarOffsetY), + ); + } + + _updateSubviewFrames() { + const { + frame: { + origin: {x, y}, + size: {width}, + }, + _layoutState: {barOffsetY}, + } = this; + + const resizeBarDesiredSize = this._resizeBar.desiredSize(); + + if (barOffsetY === 0) { + this._subview.setFrame(HIDDEN_RECT); + } else { + this._subview.setFrame({ + origin: {x, y}, + size: {width, height: barOffsetY}, + }); + } + + this._resizeBar.setFrame({ + origin: {x, y: y + barOffsetY}, + size: {width, height: resizeBarDesiredSize.height}, + }); + } + + _handleClick(interaction: ClickInteraction) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (cursorInView) { + if (this._layoutState.barOffsetY === 0) { + // Clicking on the collapsed label should expand. + const subviewDesiredSize = this._subview.desiredSize(); + this._updateLayoutStateAndResizeBar(subviewDesiredSize.height); + this.setNeedsDisplay(); + } + } + } + + _handleDoubleClick(interaction: DoubleClickInteraction) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (cursorInView) { + if (this._layoutState.barOffsetY > 0) { + // Double clicking on the expanded view should collapse. + this._updateLayoutStateAndResizeBar(0); + this.setNeedsDisplay(); + } + } + } + + _handleMouseDown(interaction: MouseDownInteraction) { + const cursorLocation = interaction.payload.location; + const resizeBarFrame = this._resizeBar.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; + } + } + + getCursorActiveSubView(interaction: Interaction): View | null { + const cursorLocation = interaction.payload.location; + const resizeBarFrame = this._resizeBar.frame; + if (rectContainsPoint(cursorLocation, resizeBarFrame)) { + return this; + } else { + return null; + } + } + + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { + switch (interaction.type) { + case 'click': + this._handleClick(interaction); + return; + case 'double-click': + this._handleDoubleClick(interaction); + return; + 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 index 074cc0296f937..6cdaff1c6e758 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js @@ -92,7 +92,7 @@ export class Surface { origin: zeroPoint, size: _canvasSize, }); - rootView.displayIfNeeded(_context); + rootView.displayIfNeeded(_context, this._viewRefs); } getCurrentCursor(): string | null { diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js new file mode 100644 index 0000000000000..7ddf6c1842691 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js @@ -0,0 +1,3 @@ +// TODO Vertically stack views (via verticallyStackedLayout). +// If stacked views are taller than the available height, a vertical scrollbar will be shown on the side, +// and width will be adjusted to subtract the width of the scrollbar. diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js index b01f33a9ca38b..245b53aff7180 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -12,10 +12,11 @@ import type { MouseDownInteraction, MouseMoveInteraction, MouseUpInteraction, - WheelPlainInteraction, + WheelWithShiftInteraction, } from './useCanvasInteraction'; import type {Rect} from './geometry'; import type {ScrollState} from './utils/scrollState'; +import type {ViewRefs} from './Surface'; import {Surface} from './Surface'; import {View} from './View'; @@ -26,6 +27,11 @@ import { translateState, } from './utils/scrollState'; import {MOVE_WHEEL_DELTA_THRESHOLD} from './constants'; +import {COLORS} from '../content-views/constants'; + +const CARET_MARGIN = 3; +const CARET_WIDTH = 5; +const CARET_HEIGHT = 3; export class VerticalScrollView extends View { _scrollState: ScrollState = {offset: 0, length: 0}; @@ -48,6 +54,54 @@ export class VerticalScrollView extends View { return this._contentView.desiredSize(); } + draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) { + super.draw(context, viewRefs); + + // Show carets if there's scroll overflow above or below the viewable area. + if (this.frame.size.height > CARET_HEIGHT * 2 + CARET_MARGIN * 3) { + const offset = this._scrollState.offset; + const desiredSize = this._contentView.desiredSize(); + + const above = offset; + const below = this.frame.size.height - desiredSize.height - offset; + + if (above < 0 || below < 0) { + const {visibleArea} = this; + const {x, y} = visibleArea.origin; + const {width, height} = visibleArea.size; + const horizontalCenter = x + width / 2; + + const halfWidth = CARET_WIDTH; + const left = horizontalCenter + halfWidth; + const right = horizontalCenter - halfWidth; + + if (above < 0) { + const topY = y + CARET_MARGIN; + + context.beginPath(); + context.moveTo(horizontalCenter, topY); + context.lineTo(left, topY + CARET_HEIGHT); + context.lineTo(right, topY + CARET_HEIGHT); + context.closePath(); + context.fillStyle = COLORS.SCROLL_CARET; + context.fill(); + } + + if (below < 0) { + const bottomY = y + height - CARET_MARGIN; + + context.beginPath(); + context.moveTo(horizontalCenter, bottomY); + context.lineTo(left, bottomY - CARET_HEIGHT); + context.lineTo(right, bottomY - CARET_HEIGHT); + context.closePath(); + context.fillStyle = COLORS.SCROLL_CARET; + context.fill(); + } + } + } + } + /** * Reference to the content view. This view is also the only view in * `this.subviews`. @@ -103,7 +157,7 @@ export class VerticalScrollView extends View { } } - _handleWheelPlain(interaction: WheelPlainInteraction) { + _handleWheelShift(interaction: WheelWithShiftInteraction) { const { location, delta: {deltaX, deltaY}, @@ -141,8 +195,8 @@ export class VerticalScrollView extends View { case 'mouseup': this._handleMouseUp(interaction); break; - case 'wheel-plain': - this._handleWheelPlain(interaction); + case 'wheel-shift': + this._handleWheelShift(interaction); break; } } diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/View.js b/packages/react-devtools-scheduling-profiler/src/view-base/View.js index e3461c10536df..a43a733fd4762 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/View.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/View.js @@ -8,7 +8,7 @@ */ import type {Interaction} from './useCanvasInteraction'; -import type {Rect, Size} from './geometry'; +import type {Rect, Size, SizeWithMaxHeight} from './geometry'; import type {Layouter} from './layouter'; import type {ViewRefs} from './Surface'; @@ -140,7 +140,7 @@ export class View { * * Can be overridden by subclasses. */ - desiredSize(): ?Size { + desiredSize(): Size | SizeWithMaxHeight { if (this._needsDisplay) { this.layoutSubviews(); } @@ -186,7 +186,7 @@ export class View { * 1. Lays out subviews with `layoutSubviews`. * 2. Draws content with `draw`. */ - displayIfNeeded(context: CanvasRenderingContext2D) { + displayIfNeeded(context: CanvasRenderingContext2D, viewRefs: ViewRefs) { if ( (this._needsDisplay || this._subviewsNeedDisplay) && rectIntersectsRect(this.frame, this.visibleArea) && @@ -195,7 +195,7 @@ export class View { this.layoutSubviews(); if (this._needsDisplay) this._needsDisplay = false; if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false; - this.draw(context); + this.draw(context, viewRefs); } } @@ -239,11 +239,11 @@ export class View { * * @see displayIfNeeded */ - draw(context: CanvasRenderingContext2D) { + draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) { const {subviews, visibleArea} = this; subviews.forEach(subview => { if (rectIntersectsRect(visibleArea, subview.visibleArea)) { - subview.displayIfNeeded(context); + subview.displayIfNeeded(context, viewRefs); } }); } @@ -252,10 +252,9 @@ export class View { * Handle an `interaction`. * * To be overwritten by subclasses that wish to handle interactions. + * + * NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews` */ - // Internal note: Do not call directly! Use - // `handleInteractionAndPropagateToSubviews` so that interactions are - // propagated to subviews. handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {} /** @@ -272,9 +271,18 @@ export class View { interaction: Interaction, viewRefs: ViewRefs, ) { + const {subviews, visibleArea} = this; + + if (visibleArea.size.height === 0) { + return; + } + this.handleInteraction(interaction, viewRefs); - this.subviews.forEach(subview => - subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs), - ); + + subviews.forEach(subview => { + if (rectIntersectsRect(visibleArea, subview.visibleArea)) { + subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs); + } + }); } } diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js b/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js index b027b708f0cfb..49c0d3981e721 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js @@ -9,6 +9,10 @@ export type Point = $ReadOnly<{|x: number, y: number|}>; export type Size = $ReadOnly<{|width: number, height: number|}>; +export type SizeWithMaxHeight = {| + ...Size, + maxInitialHeight?: number, +|}; export type Rect = $ReadOnly<{|origin: Point, size: Size|}>; /** @@ -70,6 +74,15 @@ function boxToRect(box: Box): Rect { } export function rectIntersectsRect(rect1: Rect, rect2: Rect): boolean { + if ( + rect1.size.width === 0 || + rect1.size.height === 0 || + rect2.size.width === 0 || + rect2.size.height === 0 + ) { + return false; + } + const [top1, right1, bottom1, left1] = rectToBox(rect1); const [top2, right2, bottom2, left2] = rectToBox(rect2); return !( diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/index.js b/packages/react-devtools-scheduling-profiler/src/view-base/index.js index 9d432bed57dac..b5455ce249f3c 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/index.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/index.js @@ -7,9 +7,9 @@ * @flow */ -export * from './ColorView'; +export * from './BackgroundColorView'; export * from './HorizontalPanAndZoomView'; -export * from './ResizableSplitView'; +export * from './ResizableView'; export * from './Surface'; export * from './VerticalScrollView'; export * from './View'; diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js index ca5ba1ebfdf47..58adb026c2907 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js @@ -43,8 +43,7 @@ export function collapseLayoutIntoViews(layout: Layout) { export const noopLayout: Layouter = layout => layout; /** - * Layer views on top of each other. All views' frames will be set to - * `containerFrame`. + * Layer views on top of each other. All views' frames will be set to `containerFrame`. * * Equivalent to composing: * - `alignToContainerXLayout`, @@ -52,12 +51,13 @@ export const noopLayout: Layouter = layout => layout; * - `containerWidthLayout`, and * - `containerHeightLayout`. */ -export const layeredLayout: Layouter = (layout, containerFrame) => - layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame})); +export const layeredLayout: Layouter = (layout, containerFrame) => { + return 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. + * 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; diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js index 3b374aebbee79..f22d8d7211f9b 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js @@ -10,9 +10,23 @@ import type {NormalizedWheelDelta} from './utils/normalizeWheel'; import type {Point} from './geometry'; -import {useEffect} from 'react'; +import {useEffect, useRef} from 'react'; import {normalizeWheel} from './utils/normalizeWheel'; +export type ClickInteraction = {| + type: 'click', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; +export type DoubleClickInteraction = {| + type: 'double-click', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; export type MouseDownInteraction = {| type: 'mousedown', payload: {| @@ -68,6 +82,8 @@ export type WheelWithMetaInteraction = {| |}; export type Interaction = + | ClickInteraction + | DoubleClickInteraction | MouseDownInteraction | MouseMoveInteraction | MouseUpInteraction @@ -99,6 +115,9 @@ export function useCanvasInteraction( canvasRef: {|current: HTMLCanvasElement | null|}, interactor: (interaction: Interaction) => void, ) { + const isMouseDownRef = useRef(false); + const didMouseMoveWhileDownRef = useRef(false); + useEffect(() => { const canvas = canvasRef.current; if (!canvas) { @@ -113,7 +132,38 @@ export function useCanvasInteraction( }; } + const onCanvasClick: MouseEventHandler = event => { + if (didMouseMoveWhileDownRef.current) { + return; + } + + interactor({ + type: 'click', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + + const onCanvasDoubleClick: MouseEventHandler = event => { + if (didMouseMoveWhileDownRef.current) { + return; + } + + interactor({ + type: 'double-click', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + const onCanvasMouseDown: MouseEventHandler = event => { + didMouseMoveWhileDownRef.current = false; + isMouseDownRef.current = true; + interactor({ type: 'mousedown', payload: { @@ -124,6 +174,10 @@ export function useCanvasInteraction( }; const onDocumentMouseMove: MouseEventHandler = event => { + if (isMouseDownRef.current) { + didMouseMoveWhileDownRef.current = true; + } + interactor({ type: 'mousemove', payload: { @@ -134,6 +188,8 @@ export function useCanvasInteraction( }; const onDocumentMouseUp: MouseEventHandler = event => { + isMouseDownRef.current = false; + interactor({ type: 'mouseup', payload: { @@ -179,6 +235,8 @@ export function useCanvasInteraction( ownerDocument.addEventListener('mousemove', onDocumentMouseMove); ownerDocument.addEventListener('mouseup', onDocumentMouseUp); + canvas.addEventListener('click', onCanvasClick); + canvas.addEventListener('dblclick', onCanvasDoubleClick); canvas.addEventListener('mousedown', onCanvasMouseDown); canvas.addEventListener('wheel', onCanvasWheel); @@ -186,6 +244,8 @@ export function useCanvasInteraction( ownerDocument.removeEventListener('mousemove', onDocumentMouseMove); ownerDocument.removeEventListener('mouseup', onDocumentMouseUp); + canvas.removeEventListener('click', onCanvasClick); + canvas.removeEventListener('dblclick', onCanvasDoubleClick); canvas.removeEventListener('mousedown', onCanvasMouseDown); canvas.removeEventListener('wheel', onCanvasWheel); }; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 17027f6a0c4ee..f7c69755207ca 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -45,20 +45,21 @@ function Profiler(_: {||}) { const {supportsSchedulingProfiler} = useContext(StoreContext); - let showRightColumn = true; + let isLegacyProfilerSelected = false; let view = null; if (didRecordCommits || selectedTabID === 'scheduling-profiler') { switch (selectedTabID) { case 'flame-chart': + isLegacyProfilerSelected = true; view = ; break; case 'ranked-chart': + isLegacyProfilerSelected = true; view = ; break; case 'scheduling-profiler': view = ; - showRightColumn = false; break; default: break; @@ -119,7 +120,7 @@ function Profiler(_: {||}) {
- {didRecordCommits && ( + {isLegacyProfilerSelected && didRecordCommits && (
@@ -131,7 +132,9 @@ function Profiler(_: {||}) {
- {showRightColumn &&
{sidebar}
} + {isLegacyProfilerSelected && ( +
{sidebar}
+ )}
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index 3206fcb28f74a..d5e43dc6a3d2a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -212,7 +212,10 @@ function ProfilerContextController({children}: Props) { const [selectedCommitIndex, selectCommitIndex] = useState( null, ); - const [selectedTabID, selectTab] = useState('flame-chart'); + const [selectedTabID, selectTab] = useLocalStorage( + 'React::DevTools::Profiler::defaultTab', + 'flame-chart', + ); if (isProfiling) { batchedUpdates(() => { diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index e3fbec773ddcd..ce4e68628f715 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -412,6 +412,9 @@ export function updateThemeVariables( updateStyleHelper(theme, 'color-record-hover', documentElements); updateStyleHelper(theme, 'color-record-inactive', documentElements); updateStyleHelper(theme, 'color-resize-bar', documentElements); + updateStyleHelper(theme, 'color-resize-bar-active', documentElements); + updateStyleHelper(theme, 'color-resize-bar-border', documentElements); + updateStyleHelper(theme, 'color-resize-bar-dot', documentElements); updateStyleHelper(theme, 'color-color-scroll-thumb', documentElements); updateStyleHelper(theme, 'color-color-scroll-track', documentElements); updateStyleHelper(theme, 'color-search-match', documentElements); @@ -431,21 +434,6 @@ export function updateThemeVariables( 'color-scheduling-profiler-native-event-hover', documentElements, ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-native-event-warning', - documentElements, - ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-native-event-warning-hover', - documentElements, - ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-native-event-warning-text', - documentElements, - ); updateStyleHelper( theme, 'color-selected-tree-highlight-active', @@ -481,11 +469,6 @@ export function updateThemeVariables( 'color-scheduling-profiler-react-idle', documentElements, ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-react-idle-selected', - documentElements, - ); updateStyleHelper( theme, 'color-scheduling-profiler-react-idle-hover', @@ -496,11 +479,6 @@ export function updateThemeVariables( 'color-scheduling-profiler-react-render', documentElements, ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-react-render-selected', - documentElements, - ); updateStyleHelper( theme, 'color-scheduling-profiler-react-render-hover', @@ -511,11 +489,6 @@ export function updateThemeVariables( 'color-scheduling-profiler-react-commit', documentElements, ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-react-commit-selected', - documentElements, - ); updateStyleHelper( theme, 'color-scheduling-profiler-react-commit-hover', @@ -528,57 +501,57 @@ export function updateThemeVariables( ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-layout-effects-selected', + 'color-scheduling-profiler-react-layout-effects-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-layout-effects-hover', + 'color-scheduling-profiler-react-passive-effects', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-passive-effects', + 'color-scheduling-profiler-react-passive-effects-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-passive-effects-selected', + 'color-scheduling-profiler-react-schedule', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-passive-effects-hover', + 'color-scheduling-profiler-react-schedule-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule', + 'color-scheduling-profiler-react-suspense-rejected-event', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-hover', + 'color-scheduling-profiler-react-suspense-rejected-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-cascading', + 'color-scheduling-profiler-react-suspense-resolved', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-cascading-hover', + 'color-scheduling-profiler-react-suspense-resolved-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspend', + 'color-scheduling-profiler-react-suspense-unresolved', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspend-hover', + 'color-scheduling-profiler-react-suspense-unresolved-hover', documentElements, ); updateStyleHelper( @@ -586,6 +559,7 @@ export function updateThemeVariables( 'color-scheduling-profiler-react-work-border', documentElements, ); + updateStyleHelper(theme, 'color-scroll-caret', documentElements); updateStyleHelper(theme, 'color-shadow', documentElements); updateStyleHelper(theme, 'color-tab-selected-border', documentElements); updateStyleHelper(theme, 'color-text', documentElements); @@ -597,6 +571,14 @@ export function updateThemeVariables( updateStyleHelper(theme, 'color-toggle-text', documentElements); updateStyleHelper(theme, 'color-tooltip-background', documentElements); updateStyleHelper(theme, 'color-tooltip-text', documentElements); + updateStyleHelper(theme, 'color-warning-background', documentElements); + updateStyleHelper(theme, 'color-warning-background-hover', documentElements); + updateStyleHelper(theme, 'color-warning-text-color', documentElements); + updateStyleHelper( + theme, + 'color-warning-text-color-inverted', + documentElements, + ); // Font smoothing varies based on the theme. updateStyleHelper(theme, 'font-smoothing', documentElements); diff --git a/packages/react-devtools-shared/src/devtools/views/TabBar.js b/packages/react-devtools-shared/src/devtools/views/TabBar.js index 608c660293a86..c195710a42b49 100644 --- a/packages/react-devtools-shared/src/devtools/views/TabBar.js +++ b/packages/react-devtools-shared/src/devtools/views/TabBar.js @@ -91,7 +91,7 @@ export default function TabBar({ {tabs.map(tab => { if (tab === null) { - return
; + return
; } const {icon, id, label, title} = tab; diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index 7d69b4536a708..e35a1c6467878 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -77,46 +77,43 @@ --light-color-record-active: #fc3a4b; --light-color-record-hover: #3578e5; --light-color-record-inactive: #0088fa; - --light-color-resize-bar: #cccccc; + --light-color-resize-bar: #eeeeee; + --light-color-resize-bar-active: #dcdcdc; + --light-color-resize-bar-border: #d1d1d1; + --light-color-resize-bar-dot: #333333; --light-color-scheduling-profiler-native-event: #ccc; --light-color-scheduling-profiler-native-event-hover: #aaa; - --light-color-scheduling-profiler-native-event-warning: #ee1638; - --light-color-scheduling-profiler-native-event-warning-hover: #da1030; - --light-color-scheduling-profiler-native-event-warning-text: #fff; --light-color-scheduling-profiler-priority-background: #f6f6f6; --light-color-scheduling-profiler-priority-border: #eeeeee; --light-color-scheduling-profiler-user-timing: #c9cacd; - --light-color-scheduling-profiler-user-timing-hover:#93959a; - --light-color-scheduling-profiler-react-idle: #edf6ff; - --light-color-scheduling-profiler-react-idle-selected:#EDF6FF; - --light-color-scheduling-profiler-react-idle-hover:#EDF6FF; + --light-color-scheduling-profiler-user-timing-hover: #93959a; + --light-color-scheduling-profiler-react-idle: #d3e5f6; + --light-color-scheduling-profiler-react-idle-hover: #c3d9ef; --light-color-scheduling-profiler-react-render: #9fc3f3; - --light-color-scheduling-profiler-react-render-selected:#64A9F5; - --light-color-scheduling-profiler-react-render-hover:#2683E2; - --light-color-scheduling-profiler-react-commit: #ff718e; - --light-color-scheduling-profiler-react-commit-selected:#FF5277; - --light-color-scheduling-profiler-react-commit-hover:#ed0030; - --light-color-scheduling-profiler-react-layout-effects:#c88ff0; - --light-color-scheduling-profiler-react-layout-effects-selected:#934FC1; - --light-color-scheduling-profiler-react-layout-effects-hover:#601593; - --light-color-scheduling-profiler-react-passive-effects:#c88ff0; - --light-color-scheduling-profiler-react-passive-effects-selected:#934FC1; - --light-color-scheduling-profiler-react-passive-effects-hover:#601593; + --light-color-scheduling-profiler-react-render-hover: #83afe9; + --light-color-scheduling-profiler-react-commit: #c88ff0; + --light-color-scheduling-profiler-react-commit-hover: #b281d6; + --light-color-scheduling-profiler-react-layout-effects: #b281d6; + --light-color-scheduling-profiler-react-layout-effects-hover: #9d71bd; + --light-color-scheduling-profiler-react-passive-effects: #b281d6; + --light-color-scheduling-profiler-react-passive-effects-hover: #9d71bd; --light-color-scheduling-profiler-react-schedule: #9fc3f3; - --light-color-scheduling-profiler-react-schedule-hover:#2683E2; - --light-color-scheduling-profiler-react-schedule-cascading:#ff718e; - --light-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030; - --light-color-scheduling-profiler-react-suspend: #a6e59f; - --light-color-scheduling-profiler-react-suspend-hover:#13bc00; + --light-color-scheduling-profiler-react-schedule-hover: #2683E2; + --light-color-scheduling-profiler-react-suspense-rejected: #f1cc14; + --light-color-scheduling-profiler-react-suspense-rejected-hover: #ffdf37; + --light-color-scheduling-profiler-react-suspense-resolved: #a6e59f; + --light-color-scheduling-profiler-react-suspense-resolved-hover: #89d281; + --light-color-scheduling-profiler-react-suspense-unresolved: #c9cacd; + --light-color-scheduling-profiler-react-suspense-unresolved-hover: #93959a; --light-color-scheduling-profiler-text-color: #000000; - --light-color-scheduling-profiler-react-work-border:#ffffff; + --light-color-scheduling-profiler-react-work-border: #ffffff; --light-color-scroll-thumb: #c2c2c2; --light-color-scroll-track: #fafafa; --light-color-search-match: yellow; --light-color-search-match-current: #f7923b; --light-color-selected-tree-highlight-active: rgba(0, 136, 250, 0.1); --light-color-selected-tree-highlight-inactive: rgba(0, 0, 0, 0.05); - --light-color-shadow: rgba(0, 0, 0, 0.25); + --light-color-scroll-caret: rgba(150, 150, 150, 0.5); --light-color-tab-selected-border: #0088fa; --light-color-text: #000000; --light-color-text-invalid: #ff0000; @@ -127,6 +124,10 @@ --light-color-toggle-text: #ffffff; --light-color-tooltip-background: rgba(0, 0, 0, 0.9); --light-color-tooltip-text: #ffffff; + --light-color-warning-background: #fb3655; + --light-color-warning-background-hover: #f82042; + --light-color-warning-text-color: #ffffff; + --light-color-warning-text-color-inverted: #fd4d69; /* Dark theme */ --dark-color-attribute-name: #9d87d2; @@ -202,45 +203,43 @@ --dark-color-record-active: #fc3a4b; --dark-color-record-hover: #a2e9fc; --dark-color-record-inactive: #61dafb; - --dark-color-resize-bar: #3d424a; + --dark-color-resize-bar: #282c34; + --dark-color-resize-bar-active: #31363f; + --dark-color-resize-bar-border: #3d424a; + --dark-color-resize-bar-dot: #cfd1d5; --dark-color-scheduling-profiler-native-event: #b2b2b2; --dark-color-scheduling-profiler-native-event-hover: #949494; - --dark-color-scheduling-profiler-native-event-warning: #ee1638; - --dark-color-scheduling-profiler-native-event-warning-hover: #da1030; - --dark-color-scheduling-profiler-native-event-warning-text: #fff; --dark-color-scheduling-profiler-priority-background: #1d2129; --dark-color-scheduling-profiler-priority-border: #282c34; --dark-color-scheduling-profiler-user-timing: #c9cacd; - --dark-color-scheduling-profiler-user-timing-hover:#93959a; + --dark-color-scheduling-profiler-user-timing-hover: #93959a; --dark-color-scheduling-profiler-react-idle: #3d485b; - --dark-color-scheduling-profiler-react-idle-selected:#465269; - --dark-color-scheduling-profiler-react-idle-hover:#465269; - --dark-color-scheduling-profiler-react-render: #9fc3f3; - --dark-color-scheduling-profiler-react-render-selected:#64A9F5; - --dark-color-scheduling-profiler-react-render-hover:#2683E2; - --dark-color-scheduling-profiler-react-commit: #ff718e; - --dark-color-scheduling-profiler-react-commit-selected:#FF5277; - --dark-color-scheduling-profiler-react-commit-hover:#ed0030; - --dark-color-scheduling-profiler-react-layout-effects:#c88ff0; - --dark-color-scheduling-profiler-react-layout-effects-selected:#934FC1; - --dark-color-scheduling-profiler-react-layout-effects-hover:#601593; - --dark-color-scheduling-profiler-react-passive-effects:#c88ff0; - --dark-color-scheduling-profiler-react-passive-effects-selected:#934FC1; - --dark-color-scheduling-profiler-react-passive-effects-hover:#601593; - --dark-color-scheduling-profiler-react-schedule: #9fc3f3; - --dark-color-scheduling-profiler-react-schedule-hover:#2683E2; - --dark-color-scheduling-profiler-react-schedule-cascading:#ff718e; - --dark-color-scheduling-profiler-react-schedule-cascading-hover:#ed0030; - --dark-color-scheduling-profiler-react-suspend: #a6e59f; - --dark-color-scheduling-profiler-react-suspend-hover:#13bc00; + --dark-color-scheduling-profiler-react-idle-hover: #465269; + --dark-color-scheduling-profiler-react-render: #2683E2; + --dark-color-scheduling-profiler-react-render-hover: #1a76d4; + --dark-color-scheduling-profiler-react-commit: #731fad; + --dark-color-scheduling-profiler-react-commit-hover: #611b94; + --dark-color-scheduling-profiler-react-layout-effects: #611b94; + --dark-color-scheduling-profiler-react-layout-effects-hover: #51167a; + --dark-color-scheduling-profiler-react-passive-effects: #611b94; + --dark-color-scheduling-profiler-react-passive-effects-hover: #51167a; + --dark-color-scheduling-profiler-react-schedule: #2683E2; + --dark-color-scheduling-profiler-react-schedule-hover: #1a76d4; + --dark-color-scheduling-profiler-react-suspense-rejected: #f1cc14; + --dark-color-scheduling-profiler-react-suspense-rejected-hover: #e4c00f; + --dark-color-scheduling-profiler-react-suspense-resolved: #a6e59f; + --dark-color-scheduling-profiler-react-suspense-resolved-hover: #89d281; + --dark-color-scheduling-profiler-react-suspense-unresolved: #c9cacd; + --dark-color-scheduling-profiler-react-suspense-unresolved-hover: #93959a; --dark-color-scheduling-profiler-text-color: #000000; - --dark-color-scheduling-profiler-react-work-border:#ffffff; + --dark-color-scheduling-profiler-react-work-border: #ffffff; --dark-color-scroll-thumb: #afb3b9; --dark-color-scroll-track: #313640; --dark-color-search-match: yellow; --dark-color-search-match-current: #f7923b; --dark-color-selected-tree-highlight-active: rgba(23, 143, 185, 0.15); --dark-color-selected-tree-highlight-inactive: rgba(255, 255, 255, 0.05); + --dark-color-scroll-caret: #4f5766; --dark-color-shadow: rgba(0, 0, 0, 0.5); --dark-color-tab-selected-border: #178fb9; --dark-color-text: #ffffff; @@ -252,6 +251,10 @@ --dark-color-toggle-text: #ffffff; --dark-color-tooltip-background: rgba(255, 255, 255, 0.95); --dark-color-tooltip-text: #000000; + --dark-color-warning-background: #ee1638; + --dark-color-warning-background-hover: #da1030; + --dark-color-warning-text-color: #ffffff; + --dark-color-warning-text-color-inverted: #ee1638; /* Font smoothing */ --light-font-smoothing: auto; diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js index 20d5d7b1cb0c2..070dafeb1d8eb 100644 --- a/packages/react-devtools-shared/src/hookNamesCache.js +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -103,14 +103,7 @@ export function loadHookNames( let didTimeout = false; - const response = loadHookNamesFunction(hooksTree); - console.log( - 'loadHookNamesFunction:', - loadHookNamesFunction, - '->', - response, - ); - response.then( + loadHookNamesFunction(hooksTree).then( function onSuccess(hookNames) { if (didTimeout) { return; diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index e7816b017e473..cfe900de65805 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -244,7 +244,7 @@ function throwException( } if (enableSchedulingProfiler) { - markComponentSuspended(sourceFiber, wakeable); + markComponentSuspended(sourceFiber, wakeable, rootRenderLanes); } // Reset the memoizedState to what it was before we attempted to render it. diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 70fda7bef55c1..d7f4803620ba4 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -244,7 +244,7 @@ function throwException( } if (enableSchedulingProfiler) { - markComponentSuspended(sourceFiber, wakeable); + markComponentSuspended(sourceFiber, wakeable, rootRenderLanes); } // Reset the memoizedState to what it was before we attempted to render it. diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index 67baa02ea2e48..2ab49efe0e134 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -109,13 +109,23 @@ function getWakeableID(wakeable: Wakeable): number { return ((wakeableIDs.get(wakeable): any): number); } -export function markComponentSuspended(fiber: Fiber, wakeable: Wakeable): void { +export function markComponentSuspended( + fiber: Fiber, + wakeable: Wakeable, + lanes: Lanes, +): void { if (enableSchedulingProfiler) { if (supportsUserTimingV3) { + const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend'; const id = getWakeableID(wakeable); const componentName = getComponentNameFromFiber(fiber) || 'Unknown'; - // TODO Add component stack id - markAndClear(`--suspense-suspend-${id}-${componentName}`); + const phase = fiber.alternate === null ? 'mount' : 'update'; + // TODO (scheduling profiler) Add component stack id if we re-add component stack info. + markAndClear( + `--suspense-${eventType}-${id}-${componentName}-${phase}-${formatLanes( + lanes, + )}`, + ); wakeable.then( () => markAndClear(`--suspense-resolved-${id}-${componentName}`), () => markAndClear(`--suspense-rejected-${id}-${componentName}`), diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js index b5d544d0a48d7..62da05e0e851b 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js @@ -208,7 +208,7 @@ describe('SchedulingProfiler', () => { `--react-init-${ReactVersion}`, `--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`, `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--suspense-suspend-0-Example', + '--suspense-suspend-0-Example-mount-1-Sync', '--render-stop', `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, `--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`, @@ -239,7 +239,7 @@ describe('SchedulingProfiler', () => { `--react-init-${ReactVersion}`, `--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`, `--render-start-${formatLanes(ReactFiberLane.SyncLane)}`, - '--suspense-suspend-0-Example', + '--suspense-suspend-0-Example-mount-1-Sync', '--render-stop', `--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`, `--layout-effects-start-${formatLanes(ReactFiberLane.SyncLane)}`, @@ -278,7 +278,7 @@ describe('SchedulingProfiler', () => { expectMarksToEqual([ `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--suspense-suspend-0-Example', + '--suspense-suspend-0-Example-mount-16-Default', '--render-stop', `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`, @@ -317,7 +317,7 @@ describe('SchedulingProfiler', () => { expectMarksToEqual([ `--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`, - '--suspense-suspend-0-Example', + '--suspense-suspend-0-Example-mount-16-Default', '--render-stop', `--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`, `--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,