From 5971eb724f68302bfd7f7ccba377b298a888e245 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 28 Jul 2021 16:26:39 -0400 Subject: [PATCH 01/22] Refactored scheduling profiler colors and tooltip text/info --- .../src/EventTooltip.css | 25 +- .../src/EventTooltip.js | 241 +++++++++--------- .../src/content-views/NativeEventsView.js | 15 +- .../src/content-views/ReactEventsView.js | 18 +- .../src/content-views/ReactMeasuresView.js | 10 +- .../src/content-views/constants.js | 62 ++--- .../src/import-worker/preprocessData.js | 67 +++-- .../src/types.js | 5 +- .../src/utils/formatting.js | 30 +++ .../src/utils/useSmartTooltip.js | 19 +- .../views/Settings/SettingsContext.js | 56 +--- .../src/devtools/views/root.css | 90 +++---- 12 files changed, 323 insertions(+), 315 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/utils/formatting.js diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css index 91e60bf13cfd2..2eb61830f834e 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 { @@ -72,9 +79,19 @@ } .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..c1f377d6e1bc0 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -20,9 +20,8 @@ import type { } 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,21 +33,6 @@ 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 { switch (event.type) { case 'schedule-render': @@ -68,36 +52,18 @@ function getReactEventLabel(event: ReactEvent): string | null { } } -function getReactEventColor(event: ReactEvent): string | null { - switch (event.type) { - case 'schedule-render': - return COLORS.REACT_SCHEDULE_HOVER; - case 'schedule-state-update': - case 'schedule-force-update': - return event.isCascading - ? COLORS.REACT_SCHEDULE_CASCADING_HOVER - : COLORS.REACT_SCHEDULE_HOVER; - case 'suspense-suspend': - case 'suspense-resolved': - case 'suspense-rejected': - return COLORS.REACT_SUSPEND_HOVER; - default: - return null; - } -} - function getReactMeasureLabel(type): string | null { switch (type) { case 'commit': - return 'commit'; + 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; } @@ -185,25 +151,28 @@ const TooltipFlamechartNode = ({ } = 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)}
+ {scriptUrl && ( + <> +
Script URL:
+
{scriptUrl}
+ + )} + {(locationLine !== undefined || locationColumn !== undefined) && ( + <> +
Location:
+
+ line {locationLine}, column {locationColumn} +
+ + )} +
); @@ -216,32 +185,26 @@ 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}
+
+ )}
); }; @@ -254,37 +217,62 @@ const TooltipReactEvent = ({ tooltipRef: Return, }) => { const label = getReactEventLabel(reactEvent); - const color = getReactEventColor(reactEvent); - if (!label || !color) { + if (!label) { if (__DEV__) { console.warn('Unexpected reactEvent type "%s"', reactEvent.type); } return null; } - const {componentName, componentStack, timestamp} = reactEvent; + let laneLabels = null; + let lanes = null; + switch (reactEvent.type) { + case 'schedule-render': + case 'schedule-state-update': + case 'schedule-force-update': + laneLabels = reactEvent.laneLabels; + lanes = reactEvent.lanes; + break; + } + + const {componentName, componentStack, timestamp, warning} = reactEvent; return (
- {componentName && ( - - {trimmedString(componentName, 768)} - - )} - {label} -
-
-
Timestamp:
-
{formatTimestamp(timestamp)}
- {componentStack && ( - -
Component stack:
-
-              {formatComponentStack(componentStack)}
-            
-
+
+ {componentName && ( + + {trimString(componentName, 100)} + )} + {label} +
+
+ {laneLabels !== null && lanes !== null && ( + <> +
Lanes:
+
+ {laneLabels.join(', ')} ({lanes.join(', ')}) +
+ + )} +
Timestamp:
+
{formatTimestamp(timestamp)}
+ {componentStack && ( + <> +
Component stack:
+
+                {formatComponentStack(componentStack)}
+              
+ + )} +
+ {warning !== null && ( +
+
{warning}
+
+ )}
); }; @@ -311,21 +299,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 +337,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/content-views/NativeEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js index 82371d2f2fce9..bf7e5eebcd946 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,7 @@ import { positionToTimestamp, timestampToPosition, } from './utils/positioning'; +import {formatDuration} from '../utils/formatting'; import { View, Surface, @@ -128,7 +129,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 +153,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 @@ -177,15 +178,13 @@ export class NativeEventsView extends View { const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); const trimmedName = trimFlamechartText( context, - type, + `${type} - ${formatDuration(duration)}`, width - TEXT_PADDING * 2 + (x < 0 ? x : 0), ); if (trimmedName !== null) { context.fillStyle = - warnings !== null - ? COLORS.NATIVE_EVENT_WARNING_TEXT - : COLORS.TEXT_COLOR; + warning !== null ? COLORS.WARNING_TEXT : COLORS.TEXT_COLOR; context.fillText( trimmedName, diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js index 6d35a5f1ca644..d26b8321208ef 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js @@ -111,10 +111,10 @@ export class ReactEventsView extends View { case 'schedule-render': case 'schedule-state-update': case 'schedule-force-update': - if (event.isCascading) { + if (event.warning !== null) { fillStyle = showHoverHighlight - ? COLORS.REACT_SCHEDULE_CASCADING_HOVER - : COLORS.REACT_SCHEDULE_CASCADING; + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; } else { fillStyle = showHoverHighlight ? COLORS.REACT_SCHEDULE_HOVER @@ -122,12 +122,20 @@ export class ReactEventsView extends View { } break; case 'suspense-suspend': - case 'suspense-resolved': - case 'suspense-rejected': fillStyle = showHoverHighlight ? COLORS.REACT_SUSPEND_HOVER : COLORS.REACT_SUSPEND; break; + case 'suspense-resolved': + fillStyle = showHoverHighlight + ? COLORS.REACT_RESOLVE_HOVER + : COLORS.REACT_RESOLVE; + break; + case 'suspense-rejected': + fillStyle = showHoverHighlight + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; + break; default: if (__DEV__) { console.warn('Unexpected event type "%s"', type); 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..98dc538bce5bf 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -132,7 +132,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 +140,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}"`); 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..2f3d516511b69 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -43,39 +43,35 @@ 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_RESOLVE: '', + REACT_RESOLVE_HOVER: '', REACT_SCHEDULE: '', REACT_SCHEDULE_HOVER: '', - REACT_SCHEDULE_CASCADING: '', - REACT_SCHEDULE_CASCADING_HOVER: '', REACT_SUSPEND: '', REACT_SUSPEND_HOVER: '', REACT_WORK_BORDER: '', TEXT_COLOR: '', TIME_MARKER_LABEL: '', + WARNING_BACKGROUND: '', + WARNING_BACKGROUND_HOVER: '', + WARNING_TEXT: '', + WARNING_TEXT_INVERED: '', }; export function updateColorsToMatchTheme(): void { @@ -89,15 +85,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,61 +101,46 @@ 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_RESOLVE: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-resolve', + ), + REACT_RESOLVE_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-resolve-hover', + ), 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_SCHEDULE_CASCADING_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-schedule-cascading-hover', - ), REACT_SUSPEND: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-suspend', ), @@ -182,5 +154,15 @@ export function updateColorsToMatchTheme(): void { '--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', + ), }; } 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..b4cbdea4ec72b 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -38,9 +38,17 @@ type ProcessorState = {| batchUID: BatchUID, uidCounter: BatchUID, measureStack: MeasureStackElement[], + nativeEventStack: NativeEvent[], |}; -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 startTransition 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.', +}; // Exported for tests export function getLanesFromTransportDecimalBitmask( @@ -186,8 +194,9 @@ function processTimelineEvent( let depth = 0; - while (nativeEventStack.length > 0) { - const prevNativeEvent = nativeEventStack[nativeEventStack.length - 1]; + while (state.nativeEventStack.length > 0) { + const prevNativeEvent = + state.nativeEventStack[state.nativeEventStack.length - 1]; const prevStopTime = prevNativeEvent.timestamp + prevNativeEvent.duration; @@ -195,7 +204,7 @@ function processTimelineEvent( depth = prevNativeEvent.depth + 1; break; } else { - nativeEventStack.pop(); + state.nativeEventStack.pop(); } } @@ -204,14 +213,14 @@ function processTimelineEvent( duration, timestamp, type, - warnings: null, + warning: null, }; 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); + state.nativeEventStack.push(nativeEvent); } break; case 'blink.user_timing': @@ -230,6 +239,7 @@ function processTimelineEvent( laneLabels: laneLabels ? laneLabels.split(',') : [], componentStack: splitComponentStack.join('-'), timestamp: startTime, + warning: null, }); } else if (name.startsWith('--schedule-forced-update-')) { const [ @@ -238,9 +248,12 @@ function processTimelineEvent( componentName, ...splitComponentStack ] = name.substr(25).split('-'); - const isCascading = !!state.measureStack.find( - ({type}) => type === 'commit', - ); + + let warning = null; + if (state.measureStack.find(({type}) => type === 'commit')) { + warning = WARNING_STRINGS.NESTED_UPDATE; + } + currentProfilerData.reactEvents.push({ type: 'schedule-force-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), @@ -248,7 +261,7 @@ function processTimelineEvent( componentName, componentStack: splitComponentStack.join('-'), timestamp: startTime, - isCascading, + warning, }); } else if (name.startsWith('--schedule-state-update-')) { const [ @@ -257,9 +270,12 @@ function processTimelineEvent( componentName, ...splitComponentStack ] = name.substr(24).split('-'); - const isCascading = !!state.measureStack.find( - ({type}) => type === 'commit', - ); + + let warning = null; + if (state.measureStack.find(({type}) => type === 'commit')) { + warning = WARNING_STRINGS.NESTED_UPDATE; + } + currentProfilerData.reactEvents.push({ type: 'schedule-state-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), @@ -267,7 +283,7 @@ function processTimelineEvent( componentName, componentStack: splitComponentStack.join('-'), timestamp: startTime, - isCascading, + warning, }); } // eslint-disable-line brace-style @@ -282,6 +298,7 @@ function processTimelineEvent( componentName, componentStack: splitComponentStack.join('-'), timestamp: startTime, + warning: null, }); } else if (name.startsWith('--suspense-resolved-')) { const [id, componentName, ...splitComponentStack] = name @@ -293,6 +310,7 @@ function processTimelineEvent( componentName, componentStack: splitComponentStack.join('-'), timestamp: startTime, + warning: null, }); } else if (name.startsWith('--suspense-rejected-')) { const [id, componentName, ...splitComponentStack] = name @@ -304,6 +322,7 @@ function processTimelineEvent( componentName, componentStack: splitComponentStack.join('-'), timestamp: startTime, + warning: null, }); } // eslint-disable-line brace-style @@ -335,17 +354,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,8 +525,6 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart { export default function preprocessData( timeline: TimelineEvent[], ): ReactProfilerData { - nativeEventStack = []; - const flamechart = preprocessFlamechart(timeline); const profilerData: ReactProfilerData = { @@ -554,6 +568,7 @@ export default function preprocessData( uidCounter: 0, nextRenderShouldGenerateNewBatchID: true, measureStack: [], + nativeEventStack: [], }; 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..b38cc76d6b512 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -26,13 +26,14 @@ 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 = {| @@ -47,12 +48,10 @@ export type ReactScheduleRenderEvent = {| export type ReactScheduleStateUpdateEvent = {| ...BaseReactScheduleEvent, type: 'schedule-state-update', - isCascading: boolean, |}; export type ReactScheduleForceUpdateEvent = {| ...BaseReactScheduleEvent, type: 'schedule-force-update', - isCascading: boolean, |}; type BaseReactSuspenseEvent = {| 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-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index e3fbec773ddcd..758c8c1ca542a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -431,21 +431,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 +466,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 +476,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 +486,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', @@ -526,11 +496,6 @@ export function updateThemeVariables( 'color-scheduling-profiler-react-layout-effects', documentElements, ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-react-layout-effects-selected', - documentElements, - ); updateStyleHelper( theme, 'color-scheduling-profiler-react-layout-effects-hover', @@ -541,11 +506,6 @@ export function updateThemeVariables( 'color-scheduling-profiler-react-passive-effects', documentElements, ); - updateStyleHelper( - theme, - 'color-scheduling-profiler-react-passive-effects-selected', - documentElements, - ); updateStyleHelper( theme, 'color-scheduling-profiler-react-passive-effects-hover', @@ -553,22 +513,22 @@ export function updateThemeVariables( ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule', + 'color-scheduling-profiler-react-resolve', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-hover', + 'color-scheduling-profiler-react-resolve-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-cascading', + 'color-scheduling-profiler-react-schedule', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-cascading-hover', + 'color-scheduling-profiler-react-schedule-hover', documentElements, ); updateStyleHelper( @@ -597,6 +557,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/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index 7d69b4536a708..073ccbb2d8643 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -80,36 +80,28 @@ --light-color-resize-bar: #cccccc; --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: #b069e2; + --light-color-scheduling-profiler-react-layout-effects: #fb3655; + --light-color-scheduling-profiler-react-layout-effects-hover: #f82849; + --light-color-scheduling-profiler-react-passive-effects: #f1cc14; + --light-color-scheduling-profiler-react-passive-effects-hover: #e7c20a; + --light-color-scheduling-profiler-react-resolve: #a6e59f; + --light-color-scheduling-profiler-react-resolve-hover: #13bc00; --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-suspend: #f1cc14; + --light-color-scheduling-profiler-react-suspend-hover: #ffdf37; --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; @@ -127,6 +119,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; @@ -205,36 +201,28 @@ --dark-color-resize-bar: #3d424a; --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: #601593; + --dark-color-scheduling-profiler-react-layout-effects: #ee1638; + --dark-color-scheduling-profiler-react-layout-effects-hover: #da1030; + --dark-color-scheduling-profiler-react-passive-effects: #f1cc14; + --dark-color-scheduling-profiler-react-passive-effects-hover: #e4c00f; + --dark-color-scheduling-profiler-react-resolve: #13bc00; + --dark-color-scheduling-profiler-react-resolve-hover: #11a601; + --dark-color-scheduling-profiler-react-schedule: #2683E2; + --dark-color-scheduling-profiler-react-schedule-hover: #1a76d4; + --dark-color-scheduling-profiler-react-suspend: #f1cc14; + --dark-color-scheduling-profiler-react-suspend-hover: #e4c00f; --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; @@ -252,6 +240,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; From 6dd1a62ebe4d798e2957e76383121dc319308a62 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 28 Jul 2021 16:26:58 -0400 Subject: [PATCH 02/22] Removed a console log statement from hook names Suspense cache --- packages/react-devtools-shared/src/hookNamesCache.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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; From f5870195f0b86d13072a1f81d8036bfdbcf3482a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 29 Jul 2021 14:42:34 -0400 Subject: [PATCH 03/22] Separate suspense events into their own rows, and display as ranges --- .../src/CanvasPage.js | 122 ++++--- .../src/EventTooltip.css | 22 -- .../src/EventTooltip.js | 118 +++++-- .../src/content-views/FlamechartView.js | 7 +- .../src/content-views/NativeEventsView.js | 8 +- ...tEventsView.js => SchedulingEventsView.js} | 99 ++---- .../src/content-views/SuspenseEventsView.js | 322 ++++++++++++++++++ .../src/content-views/constants.js | 37 +- .../src/content-views/index.js | 3 +- .../src/import-worker/preprocessData.js | 227 +++++++----- .../src/types.js | 54 ++- .../views/Settings/SettingsContext.js | 22 +- .../src/devtools/views/root.css | 20 +- .../src/ReactFiberThrow.new.js | 2 +- .../src/ReactFiberThrow.old.js | 2 +- .../src/SchedulingProfiler.js | 16 +- 16 files changed, 754 insertions(+), 327 deletions(-) rename packages/react-devtools-scheduling-profiler/src/content-views/{ReactEventsView.js => SchedulingEventsView.js} (71%) create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index eb6b746dec0c4..83305e5cce72e 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -45,8 +45,9 @@ import { import { FlamechartView, NativeEventsView, - ReactEventsView, ReactMeasuresView, + SchedulingEventsView, + SuspenseEventsView, TimeAxisMarkersView, UserTimingMarksView, } from './content-views'; @@ -128,7 +129,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( @@ -182,9 +184,21 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { nativeEventsViewRef.current = nativeEventsView; topContentStack.addSubview(nativeEventsView); - const reactEventsView = new ReactEventsView(surface, defaultFrame, data); - reactEventsViewRef.current = reactEventsView; - topContentStack.addSubview(reactEventsView); + const schedulingEventsView = new SchedulingEventsView( + surface, + defaultFrame, + data, + ); + schedulingEventsViewRef.current = schedulingEventsView; + topContentStack.addSubview(schedulingEventsView); + + const suspenseEventsView = new SuspenseEventsView( + surface, + defaultFrame, + data, + ); + suspenseEventsViewRef.current = suspenseEventsView; + topContentStack.addSubview(suspenseEventsView); const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView( surface, @@ -310,12 +324,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 +341,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 +392,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 +412,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 +443,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 +486,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 2eb61830f834e..114d3fe7bbe15 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -56,28 +56,6 @@ 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 { } diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index c1f377d6e1bc0..6af9a63733448 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -11,11 +11,12 @@ import type {Point} from './view-base'; import type { FlamechartStackFrame, NativeEvent, - ReactEvent, ReactHoverContextInfo, ReactMeasure, ReactProfilerData, Return, + SchedulingEvent, + SuspenseEvent, UserTimingMark, } from './types'; @@ -33,7 +34,7 @@ type Props = {| origin: Point, |}; -function getReactEventLabel(event: ReactEvent): string | null { +function getSchedulingEventLabel(event: SchedulingEvent): string | null { switch (event.type) { case 'schedule-render': return 'render scheduled'; @@ -41,12 +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; } @@ -86,10 +81,11 @@ export default function EventTooltip({ } const { - nativeEvent, - reactEvent, - measure, flamechartStackFrame, + measure, + nativeEvent, + schedulingEvent, + suspenseEvent, userTimingMark, } = hoveredEvent; @@ -97,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 ( @@ -124,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, @@ -209,33 +205,36 @@ const TooltipNativeEvent = ({ ); }; -const TooltipReactEvent = ({ - reactEvent, +const TooltipSchedulingEvent = ({ + schedulingEvent, tooltipRef, }: { - reactEvent: ReactEvent, + schedulingEvent: SchedulingEvent, tooltipRef: Return, }) => { - const label = getReactEventLabel(reactEvent); + 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; } let laneLabels = null; let lanes = null; - switch (reactEvent.type) { + switch (schedulingEvent.type) { case 'schedule-render': case 'schedule-state-update': case 'schedule-force-update': - laneLabels = reactEvent.laneLabels; - lanes = reactEvent.lanes; + laneLabels = schedulingEvent.laneLabels; + lanes = schedulingEvent.lanes; break; } - const {componentName, componentStack, timestamp, warning} = reactEvent; + const {componentName, timestamp, warning} = schedulingEvent; return (
@@ -258,12 +257,57 @@ const TooltipReactEvent = ({ )}
Timestamp:
{formatTimestamp(timestamp)}
- {componentStack && ( +
+
+ {warning !== null && ( +
+
{warning}
+
+ )} +
+ ); +}; + +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 && ( <> -
Component stack:
-
-                {formatComponentStack(componentStack)}
-              
+
Duration:
+
{formatDuration(duration)}
)}
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..f253747e873be 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -71,7 +71,8 @@ function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string { return hslaColorToString(color); } -const cachedFlamechartTextWidths = new Map(); +// TODO (scheduling profiler) Make this a reusable util +const cachedTextWidths = new Map(); const trimFlamechartText = ( context: CanvasRenderingContext2D, text: string, @@ -80,10 +81,10 @@ const trimFlamechartText = ( 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); + let measuredWidth = cachedTextWidths.get(trimmedText); if (measuredWidth == null) { measuredWidth = context.measureText(trimmedText).width; - cachedFlamechartTextWidths.set(trimmedText, measuredWidth); + cachedTextWidths.set(trimmedText, measuredWidth); } if (measuredWidth <= width) { 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 bf7e5eebcd946..9f484c0a786fa 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -41,7 +41,7 @@ import { const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE; // TODO (scheduling profiler) Make this a reusable util -const cachedFlamechartTextWidths = new Map(); +const cachedTextWidths = new Map(); const trimFlamechartText = ( context: CanvasRenderingContext2D, text: string, @@ -50,10 +50,10 @@ const trimFlamechartText = ( 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); + let measuredWidth = cachedTextWidths.get(trimmedText); if (measuredWidth == null) { measuredWidth = context.measureText(trimmedText).width; - cachedFlamechartTextWidths.set(trimmedText, measuredWidth); + cachedTextWidths.set(trimmedText, measuredWidth); } if (measuredWidth <= width) { @@ -118,7 +118,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, 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 71% 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 d26b8321208ef..c53bd8ead6bf4 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,42 +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.warning !== null) { - fillStyle = showHoverHighlight - ? COLORS.WARNING_BACKGROUND_HOVER - : COLORS.WARNING_BACKGROUND; - } 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': - fillStyle = showHoverHighlight - ? COLORS.REACT_SUSPEND_HOVER - : COLORS.REACT_SUSPEND; - break; - case 'suspense-resolved': - fillStyle = showHoverHighlight - ? COLORS.REACT_RESOLVE_HOVER - : COLORS.REACT_RESOLVE; - break; - case 'suspense-rejected': - fillStyle = showHoverHighlight - ? COLORS.WARNING_BACKGROUND_HOVER - : COLORS.WARNING_BACKGROUND; - 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) { @@ -156,7 +131,7 @@ export class ReactEventsView extends View { draw(context: CanvasRenderingContext2D) { const { frame, - _profilerData: {reactEvents}, + _profilerData: {schedulingEvents}, _hoveredEvent, visibleArea, } = this; @@ -176,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, @@ -202,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, @@ -252,7 +221,7 @@ export class ReactEventsView extends View { } const { - _profilerData: {reactEvents}, + _profilerData: {schedulingEvents}, } = this; const scaleFactor = positioningScaleFactor( this._intrinsicSize.width, @@ -266,8 +235,8 @@ 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 ( 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..ed9e7e0461071 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -0,0 +1,322 @@ +/** + * 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, + Size, + ViewRefs, +} from '../view-base'; + +import { + durationToWidth, + positioningScaleFactor, + positionToTimestamp, + timestampToPosition, +} from './utils/positioning'; +import {formatDuration} from '../utils/formatting'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + COLORS, + TEXT_PADDING, + SUSPENSE_EVENT_HEIGHT, + FONT_SIZE, + BORDER_SIZE, +} from './constants'; + +const ROW_WITH_BORDER_HEIGHT = SUSPENSE_EVENT_HEIGHT + BORDER_SIZE; + +// TODO (scheduling profiler) Make this a reusable util +const cachedTextWidths = 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 = cachedTextWidths.get(trimmedText); + if (measuredWidth == null) { + measuredWidth = context.measureText(trimmedText).width; + cachedTextWidths.set(trimmedText, measuredWidth); + } + + if (measuredWidth <= width) { + return trimmedText; + } + } + + return null; +}; + +// TODO (scheduling profiler) Make this a resizable view. +export class SuspenseEventsView extends View { + _depthToSuspenseEvent: Map; + _hoveredEvent: SuspenseEvent | null = null; + _intrinsicSize: Size; + _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, + }; + } + + 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; + + const xStart = timestampToPosition(timestamp, scaleFactor, frame); + 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 + } + + if (duration === null) { + // TODO (scheduling profiler) + return; // For now, don't show unresolved. + } + + const width = durationToWidth(duration, scaleFactor); + if (width < 1) { + return; // Too small to render at this zoom level + } + + const drawableRect = intersectionOfRects(eventRect, rect); + context.beginPath(); + if (warning !== null) { + context.fillStyle = showHoverHighlight + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; + } else { + switch (resolution) { + case 'pending': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPENSE_PENDING_EVENT_HOVER + : COLORS.REACT_SUSPENSE_PENDING_EVENT; + break; + case 'rejected': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPENSE_REJECTED_EVENT_HOVER + : COLORS.REACT_SUSPENSE_REJECTED_EVENT; + break; + case 'resolved': + context.fillStyle = showHoverHighlight + ? COLORS.REACT_SUSPENSE_RESOLVED_EVENT_HOVER + : COLORS.REACT_SUSPENSE_RESOLVED_EVENT; + break; + } + } + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + 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)); + + let label = 'suspended'; + if (componentName != null) { + label = `${componentName} ${label}`; + } + if (phase !== null) { + label += ` during ${phase}`; + } + label += ` - ${formatDuration(duration)}`; + + const trimmedName = trimFlamechartText( + context, + label, + width - TEXT_PADDING * 2 + (x < 0 ? x : 0), + ); + + if (trimmedName !== null) { + context.fillStyle = + warning !== null ? COLORS.WARNING_TEXT : COLORS.TEXT_COLOR; + + context.fillText( + trimmedName, + eventRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0), + eventRect.origin.y + SUSPENSE_EVENT_HEIGHT / 2, + ); + } + } + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {suspenseEvents}, + _hoveredEvent, + visibleArea, + } = this; + + context.fillStyle = COLORS.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, + ); + }); + } + + /** + * @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 ( + hoverTimestamp >= timestamp && + hoverTimestamp <= timestamp + duration + ) { + this.currentCursor = 'pointer'; + + 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/constants.js b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js index 2f3d516511b69..56cb783196822 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,7 @@ 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 REACT_EVENT_DIAMETER = 6; export const USER_TIMING_MARK_SIZE = 8; export const REACT_MEASURE_HEIGHT = 9; @@ -59,12 +60,14 @@ export let COLORS = { REACT_PASSIVE_EFFECTS: '', REACT_PASSIVE_EFFECTS_HOVER: '', REACT_RESIZE_BAR: '', - REACT_RESOLVE: '', - REACT_RESOLVE_HOVER: '', REACT_SCHEDULE: '', REACT_SCHEDULE_HOVER: '', - REACT_SUSPEND: '', - REACT_SUSPEND_HOVER: '', + REACT_SUSPENSE_PENDING_EVENT: '', + REACT_SUSPENSE_PENDING_EVENT_HOVER: '', + REACT_SUSPENSE_REJECTED_EVENT: '', + REACT_SUSPENSE_REJECTED_EVENT_HOVER: '', + REACT_SUSPENSE_RESOLVED_EVENT: '', + REACT_SUSPENSE_RESOLVED_EVENT_HOVER: '', REACT_WORK_BORDER: '', TEXT_COLOR: '', TIME_MARKER_LABEL: '', @@ -129,23 +132,29 @@ export function updateColorsToMatchTheme(): void { '--color-scheduling-profiler-react-passive-effects-hover', ), REACT_RESIZE_BAR: computedStyle.getPropertyValue('--color-resize-bar'), - REACT_RESOLVE: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-resolve', - ), - REACT_RESOLVE_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-resolve-hover', - ), REACT_SCHEDULE: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-schedule', ), REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-schedule-hover', ), - REACT_SUSPEND: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspend', + REACT_SUSPENSE_PENDING_EVENT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-pending', + ), + REACT_SUSPENSE_PENDING_EVENT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-pending-hover', + ), + 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_SUSPEND_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspend-hover', + REACT_SUSPENSE_RESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-resolved-hover', ), REACT_WORK_BORDER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-work-border', 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/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js index b4cbdea4ec72b..1c591321ca098 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,20 +35,24 @@ type MeasureStackElement = {| |}; type ProcessorState = {| - nextRenderShouldGenerateNewBatchID: boolean, batchUID: BatchUID, - uidCounter: BatchUID, measureStack: MeasureStackElement[], nativeEventStack: NativeEvent[], + nextRenderShouldGenerateNewBatchID: boolean, + uidCounter: BatchUID, + unresolvedSuspenseEvents: Map, |}; const NATIVE_EVENT_DURATION_THRESHOLD = 20; const WARNING_STRINGS = { LONG_EVENT_HANDLER: - 'An event handler scheduled a big update with React. Consider using the startTransition API to defer some of this work.', + '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 @@ -182,45 +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 (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(); + // 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, - warning: 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. - state.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': @@ -228,60 +242,49 @@ 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 [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 updat is longer than some threshold. warning = WARNING_STRINGS.NESTED_UPDATE; } - currentProfilerData.reactEvents.push({ + currentProfilerData.schedulingEvents.push({ type: 'schedule-force-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), laneLabels: laneLabels ? laneLabels.split(',') : [], componentName, - componentStack: splitComponentStack.join('-'), timestamp: startTime, warning, }); } else if (name.startsWith('--schedule-state-update-')) { - const [ - laneBitmaskString, - laneLabels, - componentName, - ...splitComponentStack - ] = name.substr(24).split('-'); + 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 updat is longer than some threshold. warning = WARNING_STRINGS.NESTED_UPDATE; } - currentProfilerData.reactEvents.push({ + currentProfilerData.schedulingEvents.push({ type: 'schedule-state-update', lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), laneLabels: laneLabels ? laneLabels.split(',') : [], componentName, - componentStack: splitComponentStack.join('-'), timestamp: startTime, warning, }); @@ -289,41 +292,79 @@ function processTimelineEvent( // 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, - warning: null, + 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; + } + } + } + + let depth = 0; + state.unresolvedSuspenseEvents.forEach(unresolvedSuspenseEvent => { + depth = Math.max(depth, unresolvedSuspenseEvent.depth + 1); }); - } else if (name.startsWith('--suspense-resolved-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(20) - .split('-'); - currentProfilerData.reactEvents.push({ - type: 'suspense-resolved', - id, + + // TODO (scheduling profiler) + // Maybe default duration to be the end of the profiler data (for unresolved suspense?) + // Or should we just draw these are diamonds where they started instead? + const suspenseEvent = { componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - warning: null, - }); - } 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: 'pending', + resuspendTimestamps: null, timestamp: startTime, - warning: null, - }); + 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 @@ -528,13 +569,14 @@ export default function preprocessData( 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: @@ -565,10 +607,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 b38cc76d6b512..d340afd64f065 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -31,7 +31,6 @@ export type NativeEvent = {| type BaseReactEvent = {| +componentName?: string, - +componentStack?: string, +timestamp: Milliseconds, warning: string | null, |}; @@ -43,42 +42,33 @@ type BaseReactScheduleEvent = {| |}; export type ReactScheduleRenderEvent = {| ...BaseReactScheduleEvent, - type: 'schedule-render', + +type: 'schedule-render', |}; export type ReactScheduleStateUpdateEvent = {| ...BaseReactScheduleEvent, - type: 'schedule-state-update', + +type: 'schedule-state-update', |}; export type ReactScheduleForceUpdateEvent = {| ...BaseReactScheduleEvent, - type: 'schedule-force-update', + +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: 'pending' | 'resolved' | 'rejected', + 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' @@ -127,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-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 758c8c1ca542a..d878a62e5f06b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -513,32 +513,42 @@ export function updateThemeVariables( ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-resolve', + 'color-scheduling-profiler-react-schedule', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-resolve-hover', + 'color-scheduling-profiler-react-schedule-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule', + 'color-scheduling-profiler-react-suspense-pending', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-schedule-hover', + 'color-scheduling-profiler-react-suspense-pending-hover', + documentElements, + ); + updateStyleHelper( + theme, + 'color-scheduling-profiler-react-suspense-rejected-event', + documentElements, + ); + updateStyleHelper( + theme, + 'color-scheduling-profiler-react-suspense-rejected-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspend', + 'color-scheduling-profiler-react-suspense-resolved', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspend-hover', + 'color-scheduling-profiler-react-suspense-resolved-hover', documentElements, ); updateStyleHelper( diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index 073ccbb2d8643..bfb3baa621e17 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -94,12 +94,14 @@ --light-color-scheduling-profiler-react-layout-effects-hover: #f82849; --light-color-scheduling-profiler-react-passive-effects: #f1cc14; --light-color-scheduling-profiler-react-passive-effects-hover: #e7c20a; - --light-color-scheduling-profiler-react-resolve: #a6e59f; - --light-color-scheduling-profiler-react-resolve-hover: #13bc00; --light-color-scheduling-profiler-react-schedule: #9fc3f3; --light-color-scheduling-profiler-react-schedule-hover: #2683E2; - --light-color-scheduling-profiler-react-suspend: #f1cc14; - --light-color-scheduling-profiler-react-suspend-hover: #ffdf37; + --light-color-scheduling-profiler-react-suspense-pending: #c9cacd; + --light-color-scheduling-profiler-react-suspense-pending-hover: #93959a; + --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-text-color: #000000; --light-color-scheduling-profiler-react-work-border: #ffffff; --light-color-scroll-thumb: #c2c2c2; @@ -215,12 +217,14 @@ --dark-color-scheduling-profiler-react-layout-effects-hover: #da1030; --dark-color-scheduling-profiler-react-passive-effects: #f1cc14; --dark-color-scheduling-profiler-react-passive-effects-hover: #e4c00f; - --dark-color-scheduling-profiler-react-resolve: #13bc00; - --dark-color-scheduling-profiler-react-resolve-hover: #11a601; --dark-color-scheduling-profiler-react-schedule: #2683E2; --dark-color-scheduling-profiler-react-schedule-hover: #1a76d4; - --dark-color-scheduling-profiler-react-suspend: #f1cc14; - --dark-color-scheduling-profiler-react-suspend-hover: #e4c00f; + --dark-color-scheduling-profiler-react-suspense-pending: #c9cacd; + --dark-color-scheduling-profiler-react-suspense-pending-hover: #93959a; + --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-text-color: #000000; --dark-color-scheduling-profiler-react-work-border: #ffffff; --dark-color-scroll-thumb: #afb3b9; 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}`), From 78be7e746e7d671a9009897b3787305df89df72c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 10:04:17 -0400 Subject: [PATCH 04/22] Refactored resizable view and made several views resizable Also refactored text logic into a drawText helper. --- .../src/CanvasPage.js | 126 +++++++++--------- .../src/content-views/FlamechartView.js | 72 +--------- .../src/content-views/NativeEventsView.js | 58 +------- .../src/content-views/SuspenseEventsView.js | 79 ++--------- .../src/content-views/utils/text.js | 91 +++++++++++++ ...ResizableSplitView.js => ResizableView.js} | 110 +++++---------- .../src/view-base/VerticalScrollView.js | 1 + .../src/view-base/View.js | 2 +- .../src/view-base/index.js | 2 +- 9 files changed, 213 insertions(+), 328 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js rename packages/react-devtools-scheduling-profiler/src/view-base/{ResizableSplitView.js => ResizableView.js} (71%) diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 83305e5cce72e..587356a01a019 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -32,7 +32,7 @@ import prettyMilliseconds from 'pretty-ms'; import { HorizontalPanAndZoomView, - ResizableSplitView, + ResizableView, Surface, VerticalScrollView, View, @@ -154,21 +154,51 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { ); }; - // Top content + function createViewHelper( + view: View, + 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, + ); + } + + 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, @@ -177,12 +207,16 @@ 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, + true, + true, + ); const schedulingEventsView = new SchedulingEventsView( surface, @@ -190,7 +224,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data, ); schedulingEventsViewRef.current = schedulingEventsView; - topContentStack.addSubview(schedulingEventsView); + const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView); const suspenseEventsView = new SuspenseEventsView( surface, @@ -198,20 +232,11 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data, ); suspenseEventsViewRef.current = suspenseEventsView; - topContentStack.addSubview(suspenseEventsView); - - const topContentHorizontalPanAndZoomView = new HorizontalPanAndZoomView( - surface, - defaultFrame, - topContentStack, - data.duration, - syncAllHorizontalPanAndZoomViewStates, + const suspenseEventsViewWrapper = createViewHelper( + suspenseEventsView, + true, + true, ); - syncedHorizontalPanAndZoomViewsRef.current.push( - topContentHorizontalPanAndZoomView, - ); - - // Resizable content const reactMeasuresView = new ReactMeasuresView( surface, @@ -219,20 +244,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { 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, + true, + true, ); const flamechartView = new FlamechartView( @@ -242,30 +257,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data.duration, ); flamechartViewRef.current = flamechartView; - const flamechartVerticalScrollView = new VerticalScrollView( - surface, - defaultFrame, - flamechartView, - ); - const flamechartHorizontalPanAndZoomView = new HorizontalPanAndZoomView( - surface, - defaultFrame, - flamechartVerticalScrollView, - data.duration, - syncAllHorizontalPanAndZoomViewStates, - ); - syncedHorizontalPanAndZoomViewsRef.current.push( - flamechartHorizontalPanAndZoomView, - ); - - const resizableContentStack = new ResizableSplitView( - surface, - defaultFrame, - reactMeasuresHorizontalPanAndZoomView, - flamechartHorizontalPanAndZoomView, - canvasRef, - ); + const flamechartViewWrapper = createViewHelper(flamechartView, 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, @@ -274,8 +269,15 @@ 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); surfaceRef.current.rootView = rootView; }, [data]); 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 f253747e873be..edf50eec0163a 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -26,7 +26,6 @@ import { 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,30 +69,6 @@ function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string { return hslaColorToString(color); } -// TODO (scheduling profiler) Make this a reusable util -const cachedTextWidths = 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 = cachedTextWidths.get(trimmedText); - if (measuredWidth == null) { - measuredWidth = context.measureText(trimmedText).width; - cachedTextWidths.set(trimmedText, measuredWidth); - } - - if (measuredWidth <= width) { - return trimmedText; - } - } - - return null; -}; - class FlamechartStackLayerView extends View { /** Layer to display */ _stackLayer: FlamechartStackLayer; @@ -162,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++) { @@ -202,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); } } 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 9f484c0a786fa..b05f7eec9b3a8 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,7 @@ import { positionToTimestamp, timestampToPosition, } from './utils/positioning'; +import {drawText} from './utils/text'; import {formatDuration} from '../utils/formatting'; import { View, @@ -30,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 cachedTextWidths = 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 = cachedTextWidths.get(trimmedText); - if (measuredWidth == null) { - measuredWidth = context.measureText(trimmedText).width; - cachedTextWidths.set(trimmedText, measuredWidth); - } - - if (measuredWidth <= width) { - return trimmedText; - } - } - - return null; -}; - export class NativeEventsView extends View { _depthToNativeEvent: Map; _hoveredEvent: NativeEvent | null = null; @@ -169,30 +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`; + const label = `${type} - ${formatDuration(duration)}`; - if (width > TEXT_PADDING * 2) { - const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); - const trimmedName = trimFlamechartText( - context, - `${type} - ${formatDuration(duration)}`, - width - TEXT_PADDING * 2 + (x < 0 ? x : 0), - ); - - if (trimmedName !== null) { - context.fillStyle = - warning !== null ? COLORS.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) { diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index ed9e7e0461071..909dfb649ae4e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -22,6 +22,7 @@ import { positionToTimestamp, timestampToPosition, } from './utils/positioning'; +import {drawText} from './utils/text'; import {formatDuration} from '../utils/formatting'; import { View, @@ -30,41 +31,10 @@ import { rectIntersectsRect, intersectionOfRects, } from '../view-base'; -import { - COLORS, - TEXT_PADDING, - SUSPENSE_EVENT_HEIGHT, - FONT_SIZE, - BORDER_SIZE, -} from './constants'; +import {COLORS, SUSPENSE_EVENT_HEIGHT, BORDER_SIZE} from './constants'; const ROW_WITH_BORDER_HEIGHT = SUSPENSE_EVENT_HEIGHT + BORDER_SIZE; -// TODO (scheduling profiler) Make this a reusable util -const cachedTextWidths = 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 = cachedTextWidths.get(trimmedText); - if (measuredWidth == null) { - measuredWidth = context.measureText(trimmedText).width; - cachedTextWidths.set(trimmedText, measuredWidth); - } - - if (measuredWidth <= width) { - return trimmedText; - } - } - - return null; -}; - -// TODO (scheduling profiler) Make this a resizable view. export class SuspenseEventsView extends View { _depthToSuspenseEvent: Map; _hoveredEvent: SuspenseEvent | null = null; @@ -156,7 +126,8 @@ export class SuspenseEventsView extends View { } if (duration === null) { - // TODO (scheduling profiler) + // TODO (scheduling profiler) We should probably draw a different representation for incomplete suspense measures. + // Maybe a dot? Maybe a gray measure? return; // For now, don't show unresolved. } @@ -197,40 +168,16 @@ export class SuspenseEventsView 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)); - - let label = 'suspended'; - if (componentName != null) { - label = `${componentName} ${label}`; - } - if (phase !== null) { - label += ` during ${phase}`; - } - label += ` - ${formatDuration(duration)}`; - - const trimmedName = trimFlamechartText( - context, - label, - width - TEXT_PADDING * 2 + (x < 0 ? x : 0), - ); - - if (trimmedName !== null) { - context.fillStyle = - warning !== null ? COLORS.WARNING_TEXT : COLORS.TEXT_COLOR; - - context.fillText( - trimmedName, - eventRect.origin.x + TEXT_PADDING - (x < 0 ? x : 0), - eventRect.origin.y + SUSPENSE_EVENT_HEIGHT / 2, - ); - } + let label = 'suspended'; + if (componentName != null) { + label = `${componentName} ${label}`; } + if (phase !== null) { + label += ` during ${phase}`; + } + label += ` - ${formatDuration(duration)}`; + + drawText(label, context, eventRect, drawableRect, width); } draw(context: CanvasRenderingContext2D) { 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..8ece94a37e84f --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +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, +): void { + if (availableWidth > TEXT_PADDING * 2) { + context.textAlign = 'left'; + 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 = COLORS.TEXT_COLOR; + + // 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(); + } + + context.fillText( + trimmedName, + x + TEXT_PADDING - (x < 0 ? x : 0), + y + fullRect.size.height / 2, + ); + + if (textOverflowsViewableArea) { + context.restore(); + } + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js similarity index 71% rename from packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js rename to packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 177fe5f2c76d1..b34f4844913e6 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -17,7 +17,6 @@ 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'; @@ -142,100 +141,69 @@ class ResizeBar extends View { } } -export class ResizableSplitView extends View { +// TODO (ResizableView) Rename this to ResizableView +// TODO (ResizableView) Clip sub-view somehow so text doesn't overflow. +export class ResizableView extends View { _canvasRef: {current: HTMLCanvasElement | null}; _resizingState: ResizingState | null = null; _layoutState: LayoutState; + _resizeBar: ResizeBar; + _subview: View; constructor( surface: Surface, frame: Rect, - topSubview: View, - bottomSubview: View, + subview: View, canvasRef: {current: HTMLCanvasElement | null}, ) { super(surface, frame, noopLayout); this._canvasRef = canvasRef; - this.addSubview(topSubview); - this.addSubview(new ResizeBar(surface, frame)); - this.addSubview(bottomSubview); + this._subview = subview; + this._resizeBar = new ResizeBar(surface, frame); - const topSubviewDesiredSize = topSubview.desiredSize(); + this.addSubview(this._subview); + this.addSubview(this._resizeBar); + + // TODO (ResizableView) Allow subviews to specify default sizes. + // Maybe that or set some % based default so all panels are visible to begin with. + const subviewDesiredSize = subview.desiredSize(); this._layoutState = { - barOffsetY: topSubviewDesiredSize ? topSubviewDesiredSize.height : 0, + barOffsetY: subviewDesiredSize ? subviewDesiredSize.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 resizeBarDesiredSize = this._resizeBar.desiredSize(); + const subviewDesiredSize = this._subview.desiredSize(); - const topSubviewDesiredWidth = topSubviewDesiredSize - ? topSubviewDesiredSize.width - : 0; - const bottomSubviewDesiredWidth = bottomSubviewDesiredSize - ? bottomSubviewDesiredSize.width - : 0; - - const topSubviewDesiredHeight = topSubviewDesiredSize - ? topSubviewDesiredSize.height - : 0; - const bottomSubviewDesiredHeight = bottomSubviewDesiredSize - ? bottomSubviewDesiredSize.height + const subviewDesiredWidth = subviewDesiredSize + ? subviewDesiredSize.width : 0; return { - width: Math.max( - topSubviewDesiredWidth, - resizeBarDesiredSize.width, - bottomSubviewDesiredWidth, - ), - height: - topSubviewDesiredHeight + - resizeBarDesiredSize.height + - bottomSubviewDesiredHeight, + width: Math.max(subviewDesiredWidth, resizeBarDesiredSize.width), + height: this._layoutState.barOffsetY + resizeBarDesiredSize.height, }; } layoutSubviews() { this._updateLayoutState(); this._updateSubviewFrames(); + super.layoutSubviews(); } + // TODO (ResizableView) Change ResizeBar view style slightly when fully collapsed. + // TODO (ResizableView) Double click on ResizeBar to collapse/toggle. _updateLayoutState() { - const {frame, visibleArea, _resizingState} = this; + const {frame, _resizingState} = this; - const resizeBarDesiredSize = this._getResizeBarDesiredSize(); + // TODO (ResizableView) Allow subviews to specify min size too. // 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; + const subviewDesiredSize = this._subview.desiredSize(); + const maxBarOffset = subviewDesiredSize.height; let proposedBarOffsetY = this._layoutState.barOffsetY; // Update bar offset if dragging bar @@ -254,37 +222,31 @@ export class ResizableSplitView extends View { const { frame: { origin: {x, y}, - size: {width, height}, + size: {width}, }, _layoutState: {barOffsetY}, } = this; - const resizeBarDesiredSize = this._getResizeBarDesiredSize(); + const resizeBarDesiredSize = this._resizeBar.desiredSize(); let currentY = y; - this._getTopSubview().setFrame({ + this._subview.setFrame({ origin: {x, y: currentY}, size: {width, height: barOffsetY}, }); - currentY += this._getTopSubview().frame.size.height; + currentY += this._subview.frame.size.height; - this._getResizeBar().setFrame({ + this._resizeBar.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}, - }); + currentY += this._resizeBar.frame.size.height; } _handleMouseDown(interaction: MouseDownInteraction) { const cursorLocation = interaction.payload.location; - const resizeBarFrame = this._getResizeBar().frame; + const resizeBarFrame = this._resizeBar.frame; if (rectContainsPoint(cursorLocation, resizeBarFrame)) { const mouseY = cursorLocation.y; this._resizingState = { @@ -315,7 +277,7 @@ export class ResizableSplitView extends View { getCursorActiveSubView(interaction: Interaction): View | null { const cursorLocation = interaction.payload.location; - const resizeBarFrame = this._getResizeBar().frame; + const resizeBarFrame = this._resizeBar.frame; if (rectContainsPoint(cursorLocation, resizeBarFrame)) { return this; } else { 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..f91a2640282bf 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -27,6 +27,7 @@ import { } from './utils/scrollState'; import {MOVE_WHEEL_DELTA_THRESHOLD} from './constants'; +// TODO VerticalScrollView Draw caret over top+center and/or bottom+center to indicate scrollable content. export class VerticalScrollView extends View { _scrollState: ScrollState = {offset: 0, length: 0}; _isPanning = false; 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..10d925fd63f09 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/View.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/View.js @@ -140,7 +140,7 @@ export class View { * * Can be overridden by subclasses. */ - desiredSize(): ?Size { + desiredSize(): Size { if (this._needsDisplay) { this.layoutSubviews(); } 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..1df9721cd18b2 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/index.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/index.js @@ -9,7 +9,7 @@ export * from './ColorView'; export * from './HorizontalPanAndZoomView'; -export * from './ResizableSplitView'; +export * from './ResizableView'; export * from './Surface'; export * from './VerticalScrollView'; export * from './View'; From 0f705837b6df83d9ff0b83e45914c2f1373d5588 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 10:18:34 -0400 Subject: [PATCH 05/22] Hide snapshot selector if Scheduling Profiler is active --- .../src/devtools/views/Profiler/Profiler.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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}
+ )}
From f44c87f4166abe421097a622156cf6963e72c978 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 11:58:27 -0400 Subject: [PATCH 06/22] Improved ResizeView and ResizeBar UX Double click on a resize view to toggle expand/collapse. Resize bar has border and ellipsis like Chrome bar. --- .../src/CanvasPage.js | 2 +- .../src/content-views/NativeEventsView.js | 12 +- .../src/content-views/SuspenseEventsView.js | 29 ++++- .../src/content-views/constants.js | 12 ++ .../src/view-base/ResizableView.js | 112 +++++++++++++----- .../src/view-base/Surface.js | 2 +- .../src/view-base/View.js | 13 +- .../src/view-base/useCanvasInteraction.js | 20 ++++ .../views/Settings/SettingsContext.js | 3 + .../src/devtools/views/root.css | 10 +- 10 files changed, 171 insertions(+), 44 deletions(-) diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 587356a01a019..b08e01b9fe37b 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -257,7 +257,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data.duration, ); flamechartViewRef.current = flamechartView; - const flamechartViewWrapper = createViewHelper(flamechartView, true, true); + const flamechartViewWrapper = createViewHelper(flamechartView, true); // Root view contains all of the sub views defined above. // The order we add them below determines their vertical position. 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 b05f7eec9b3a8..bf1ac816776a3 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -191,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, ); } } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index 909dfb649ae4e..21a7a1a916e5f 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -188,7 +188,7 @@ export class SuspenseEventsView extends View { visibleArea, } = this; - context.fillStyle = COLORS.BACKGROUND; + context.fillStyle = COLORS.PRIORITY_BACKGROUND; context.fillRect( visibleArea.origin.x, visibleArea.origin.y, @@ -212,6 +212,33 @@ export class SuspenseEventsView extends View { 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 + SUSPENSE_EVENT_HEIGHT, + }, + 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, + ); + } + } } /** 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 56cb783196822..c4e4ecafdd643 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -60,6 +60,9 @@ export let COLORS = { REACT_PASSIVE_EFFECTS: '', 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_SUSPENSE_PENDING_EVENT: '', @@ -132,6 +135,15 @@ export function updateColorsToMatchTheme(): void { '--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', ), diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index b34f4844913e6..4e841db688a88 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -8,6 +8,7 @@ */ import type { + DoubleClickInteraction, Interaction, MouseDownInteraction, MouseMoveInteraction, @@ -16,12 +17,11 @@ import type { import type {Rect, Size} from './geometry'; import type {ViewRefs} from './Surface'; -import {COLORS} from '../content-views/constants'; +import {BORDER_SIZE, COLORS} from '../content-views/constants'; import {Surface} from './Surface'; import {View} from './View'; import {rectContainsPoint} from './geometry'; -import {layeredLayout, noopLayout} from './layouter'; -import {ColorView} from './ColorView'; +import {noopLayout} from './layouter'; import {clamp} from './utils/clamp'; type ResizeBarState = 'normal' | 'hovered' | 'dragging'; @@ -38,40 +38,69 @@ type LayoutState = $ReadOnly<{| 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}`); -} +const RESIZE_BAR_SIZE = 8; +const RESIZE_BAR_DOT_RADIUS = 1; +const RESIZE_BAR_DOT_SPACING = 4; +// TODO (ResizableView) Draw borders on top and bottom in case two bars are collapsed next to each other. class ResizeBar extends View { _intrinsicContentSize: Size = { width: 0, - height: 5, + height: RESIZE_BAR_SIZE, }; _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)); + 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; + + // Draw 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) { @@ -79,7 +108,7 @@ class ResizeBar extends View { return; } this._interactionState = state; - this._updateColor(); + this.setNeedsDisplay(); } _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) { @@ -141,8 +170,6 @@ class ResizeBar extends View { } } -// TODO (ResizableView) Rename this to ResizableView -// TODO (ResizableView) Clip sub-view somehow so text doesn't overflow. export class ResizableView extends View { _canvasRef: {current: HTMLCanvasElement | null}; _resizingState: ResizingState | null = null; @@ -244,6 +271,32 @@ export class ResizableView extends View { currentY += this._resizeBar.frame.size.height; } + _handleDoubleClick(interaction: DoubleClickInteraction) { + const cursorInView = rectContainsPoint( + interaction.payload.location, + this.frame, + ); + if (cursorInView) { + if (this._layoutState.barOffsetY === 0) { + // TODO (ResizableView) Allow subviews to specify min size too. + // 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; + + this._layoutState = { + ...this._layoutState, + barOffsetY: maxBarOffset, + }; + } else { + this._layoutState = { + ...this._layoutState, + barOffsetY: 0, + }; + } + this.setNeedsDisplay(); + } + } + _handleMouseDown(interaction: MouseDownInteraction) { const cursorLocation = interaction.payload.location; const resizeBarFrame = this._resizeBar.frame; @@ -287,6 +340,9 @@ export class ResizableView extends View { handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { switch (interaction.type) { + case 'double-click': + this._handleDoubleClick(interaction); + return; case 'mousedown': this._handleMouseDown(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/View.js b/packages/react-devtools-scheduling-profiler/src/view-base/View.js index 10d925fd63f09..b72532d75e314 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/View.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/View.js @@ -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) {} /** 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..147188ad27835 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js @@ -13,6 +13,13 @@ import type {Point} from './geometry'; import {useEffect} from 'react'; import {normalizeWheel} from './utils/normalizeWheel'; +export type DoubleClickInteraction = {| + type: 'double-click', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; export type MouseDownInteraction = {| type: 'mousedown', payload: {| @@ -68,6 +75,7 @@ export type WheelWithMetaInteraction = {| |}; export type Interaction = + | DoubleClickInteraction | MouseDownInteraction | MouseMoveInteraction | MouseUpInteraction @@ -113,6 +121,16 @@ export function useCanvasInteraction( }; } + const onCanvasDoubleClick: MouseEventHandler = event => { + interactor({ + type: 'double-click', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + const onCanvasMouseDown: MouseEventHandler = event => { interactor({ type: 'mousedown', @@ -179,6 +197,7 @@ export function useCanvasInteraction( ownerDocument.addEventListener('mousemove', onDocumentMouseMove); ownerDocument.addEventListener('mouseup', onDocumentMouseUp); + canvas.addEventListener('dblclick', onCanvasDoubleClick); canvas.addEventListener('mousedown', onCanvasMouseDown); canvas.addEventListener('wheel', onCanvasWheel); @@ -186,6 +205,7 @@ export function useCanvasInteraction( ownerDocument.removeEventListener('mousemove', onDocumentMouseMove); ownerDocument.removeEventListener('mouseup', onDocumentMouseUp); + canvas.removeEventListener('dblclick', onCanvasDoubleClick); canvas.removeEventListener('mousedown', onCanvasMouseDown); canvas.removeEventListener('wheel', onCanvasWheel); }; 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 d878a62e5f06b..690b149f6239c 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); diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index bfb3baa621e17..bf29c87886397 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -77,7 +77,10 @@ --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-priority-background: #f6f6f6; @@ -200,7 +203,10 @@ --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-priority-background: #1d2129; From 0592a283e272c064e5d5673ceaac8194e21fb882 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 15:39:24 -0400 Subject: [PATCH 07/22] Show unresolved suspends as gray diamond marks --- .../src/CanvasPage.js | 8 +- .../src/EventTooltip.js | 15 +- .../src/content-views/SuspenseEventsView.js | 159 ++++++++++++------ .../src/content-views/UserTimingMarksView.js | 6 +- .../src/content-views/constants.js | 17 +- .../src/import-worker/preprocessData.js | 30 +++- .../src/types.js | 2 +- .../src/view-base/ResizableView.js | 5 - .../view-base/VerticalScrollOverflowView.js | 3 + .../src/view-base/layouter.js | 7 +- .../views/Settings/SettingsContext.js | 12 +- .../src/devtools/views/root.css | 8 +- 12 files changed, 166 insertions(+), 106 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index b08e01b9fe37b..9aa4b5230af23 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -31,6 +31,7 @@ import {copy} from 'clipboard-js'; import prettyMilliseconds from 'pretty-ms'; import { + ColorView, HorizontalPanAndZoomView, ResizableView, Surface, @@ -257,7 +258,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data.duration, ); flamechartViewRef.current = flamechartView; - const flamechartViewWrapper = createViewHelper(flamechartView, true); + const flamechartViewWrapper = createViewHelper(flamechartView, true, true); // Root view contains all of the sub views defined above. // The order we add them below determines their vertical position. @@ -279,6 +280,11 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { rootView.addSubview(reactMeasuresViewWrapper); rootView.addSubview(flamechartViewWrapper); + // If subviews are less than the available height, fill remaining height with a solid color. + rootView.addSubview( + new ColorView(surface, defaultFrame, COLORS.BACKGROUND), + ); + surfaceRef.current.rootView = rootView; }, [data]); diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index 6af9a63733448..d6019cb254434 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -137,14 +137,7 @@ const TooltipFlamechartNode = ({ stackFrame: FlamechartStackFrame, tooltipRef: Return, }) => { - const { - name, - timestamp, - duration, - scriptUrl, - locationLine, - locationColumn, - } = stackFrame; + const {name, timestamp, duration, locationLine, locationColumn} = stackFrame; return (
@@ -154,12 +147,6 @@ const TooltipFlamechartNode = ({
{formatTimestamp(timestamp)}
Duration:
{formatDuration(duration)}
- {scriptUrl && ( - <> -
Script URL:
-
{scriptUrl}
- - )} {(locationLine !== undefined || locationColumn !== undefined) && ( <>
Location:
diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index 21a7a1a916e5f..89fa602d4111a 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -21,6 +21,7 @@ import { positioningScaleFactor, positionToTimestamp, timestampToPosition, + widthToDuration, } from './utils/positioning'; import {drawText} from './utils/text'; import {formatDuration} from '../utils/formatting'; @@ -31,7 +32,12 @@ import { rectIntersectsRect, intersectionOfRects, } from '../view-base'; -import {COLORS, SUSPENSE_EVENT_HEIGHT, BORDER_SIZE} from './constants'; +import { + BORDER_SIZE, + COLORS, + PENDING_SUSPENSE_EVENT_SIZE, + SUSPENSE_EVENT_HEIGHT, +} from './constants'; const ROW_WITH_BORDER_HEIGHT = SUSPENSE_EVENT_HEIGHT + BORDER_SIZE; @@ -112,72 +118,106 @@ export class SuspenseEventsView extends View { baseY += depth * ROW_WITH_BORDER_HEIGHT; - const xStart = timestampToPosition(timestamp, scaleFactor, frame); - 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 - } - - if (duration === null) { - // TODO (scheduling profiler) We should probably draw a different representation for incomplete suspense measures. - // Maybe a dot? Maybe a gray measure? - return; // For now, don't show unresolved. - } - - const width = durationToWidth(duration, scaleFactor); - if (width < 1) { - return; // Too small to render at this zoom level - } - - const drawableRect = intersectionOfRects(eventRect, rect); - context.beginPath(); + let fillStyle = ((null: any): string); if (warning !== null) { - context.fillStyle = showHoverHighlight + fillStyle = showHoverHighlight ? COLORS.WARNING_BACKGROUND_HOVER : COLORS.WARNING_BACKGROUND; } else { switch (resolution) { - case 'pending': - context.fillStyle = showHoverHighlight - ? COLORS.REACT_SUSPENSE_PENDING_EVENT_HOVER - : COLORS.REACT_SUSPENSE_PENDING_EVENT; - break; case 'rejected': - context.fillStyle = showHoverHighlight + fillStyle = showHoverHighlight ? COLORS.REACT_SUSPENSE_REJECTED_EVENT_HOVER : COLORS.REACT_SUSPENSE_REJECTED_EVENT; break; case 'resolved': - context.fillStyle = showHoverHighlight + 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; } } - 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}`; - } - label += ` - ${formatDuration(duration)}`; + 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 + } + + 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(); + } 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 + } - drawText(label, context, eventRect, drawableRect, width); + 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) { @@ -269,7 +309,24 @@ export class SuspenseEventsView extends View { const suspenseEvent = suspenseEventsAtDepth[index]; const {duration, timestamp} = suspenseEvent; - if ( + if (duration === null) { + const timestampAllowance = widthToDuration( + PENDING_SUSPENSE_EVENT_SIZE / 2, + scaleFactor, + ); + + if ( + timestamp - timestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + timestampAllowance + ) { + this.currentCursor = 'pointer'; + + viewRefs.hoveredView = this; + + onHover(suspenseEvent); + return; + } + } else if ( hoverTimestamp >= timestamp && hoverTimestamp <= timestamp + duration ) { 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..a0640923ebc63 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,8 +221,8 @@ export class UserTimingMarksView extends View { const {timestamp} = mark; if ( - timestamp - markTimestampAllowance <= hoverTimestamp && - hoverTimestamp <= timestamp + markTimestampAllowance + timestamp - timestampAllowance <= hoverTimestamp && + hoverTimestamp <= timestamp + timestampAllowance ) { this.currentCursor = 'pointer'; viewRefs.hoveredView = this; 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 c4e4ecafdd643..5a241c97742f4 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -16,6 +16,7 @@ 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; @@ -65,12 +66,12 @@ export let COLORS = { REACT_RESIZE_BAR_DOT: '', REACT_SCHEDULE: '', REACT_SCHEDULE_HOVER: '', - REACT_SUSPENSE_PENDING_EVENT: '', - REACT_SUSPENSE_PENDING_EVENT_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: '', TEXT_COLOR: '', TIME_MARKER_LABEL: '', @@ -150,12 +151,6 @@ export function updateColorsToMatchTheme(): void { REACT_SCHEDULE_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-schedule-hover', ), - REACT_SUSPENSE_PENDING_EVENT: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-pending', - ), - REACT_SUSPENSE_PENDING_EVENT_HOVER: computedStyle.getPropertyValue( - '--color-scheduling-profiler-react-suspense-pending-hover', - ), REACT_SUSPENSE_REJECTED_EVENT: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-suspense-rejected', ), @@ -168,6 +163,12 @@ export function updateColorsToMatchTheme(): void { REACT_SUSPENSE_RESOLVED_EVENT_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-suspense-resolved-hover', ), + REACT_SUSPENSE_UNRESOLVED_EVENT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-suspense-unresolved', + ), + 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', ), 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 1c591321ca098..a365be120897c 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -257,7 +257,7 @@ function processTimelineEvent( let warning = null; if (state.measureStack.find(({type}) => type === 'commit')) { - // TODO (scheduling profiler) Only warn if the subsequent updat is longer than some threshold. + // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold. warning = WARNING_STRINGS.NESTED_UPDATE; } @@ -276,7 +276,7 @@ function processTimelineEvent( let warning = null; if (state.measureStack.find(({type}) => type === 'commit')) { - // TODO (scheduling profiler) Only warn if the subsequent updat is longer than some threshold. + // TODO (scheduling profiler) Only warn if the subsequent update is longer than some threshold. warning = WARNING_STRINGS.NESTED_UPDATE; } @@ -314,21 +314,33 @@ function processTimelineEvent( } } - let depth = 0; - state.unresolvedSuspenseEvents.forEach(unresolvedSuspenseEvent => { - depth = Math.max(depth, unresolvedSuspenseEvent.depth + 1); + const availableDepths = new Array( + state.unresolvedSuspenseEvents.size + 1, + ).fill(true); + state.unresolvedSuspenseEvents.forEach(({depth}) => { + availableDepths[depth] = false; }); - // TODO (scheduling profiler) - // Maybe default duration to be the end of the profiler data (for unresolved suspense?) - // Or should we just draw these are diamonds where they started instead? + 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, depth, duration: null, id, phase, - resolution: 'pending', + resolution: 'unresolved', resuspendTimestamps: null, timestamp: startTime, type: 'suspense', diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index d340afd64f065..674dabdc8e93a 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -59,7 +59,7 @@ export type SuspenseEvent = {| duration: number | null, +id: string, +phase: 'mount' | 'update' | null, - resolution: 'pending' | 'resolved' | 'rejected', + resolution: 'rejected' | 'resolved' | 'unresolved', resuspendTimestamps: Array | null, +type: 'suspense', |}; diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 4e841db688a88..18cc70c745897 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -42,7 +42,6 @@ const RESIZE_BAR_SIZE = 8; const RESIZE_BAR_DOT_RADIUS = 1; const RESIZE_BAR_DOT_SPACING = 4; -// TODO (ResizableView) Draw borders on top and bottom in case two bars are collapsed next to each other. class ResizeBar extends View { _intrinsicContentSize: Size = { width: 0, @@ -222,12 +221,9 @@ export class ResizableView extends View { super.layoutSubviews(); } - // TODO (ResizableView) Change ResizeBar view style slightly when fully collapsed. - // TODO (ResizableView) Double click on ResizeBar to collapse/toggle. _updateLayoutState() { const {frame, _resizingState} = this; - // TODO (ResizableView) Allow subviews to specify min size too. // 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; @@ -278,7 +274,6 @@ export class ResizableView extends View { ); if (cursorInView) { if (this._layoutState.barOffsetY === 0) { - // TODO (ResizableView) Allow subviews to specify min size too. // 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; 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/layouter.js b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js index ca5ba1ebfdf47..2cad9659be066 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`, @@ -56,8 +55,8 @@ export const layeredLayout: Layouter = (layout, containerFrame) => layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame})); /** - * Stacks `views` vertically in `frame`. All views in `views` will have their - * widths set to the frame's width. + * 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-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 690b149f6239c..29b1b12be0289 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -526,32 +526,32 @@ export function updateThemeVariables( ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspense-pending', + 'color-scheduling-profiler-react-suspense-rejected-event', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspense-pending-hover', + 'color-scheduling-profiler-react-suspense-rejected-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspense-rejected-event', + 'color-scheduling-profiler-react-suspense-resolved', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspense-rejected-hover', + 'color-scheduling-profiler-react-suspense-resolved-hover', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspense-resolved', + 'color-scheduling-profiler-react-suspense-unresolved', documentElements, ); updateStyleHelper( theme, - 'color-scheduling-profiler-react-suspense-resolved-hover', + 'color-scheduling-profiler-react-suspense-unresolved-hover', documentElements, ); updateStyleHelper( diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index bf29c87886397..10b68fb01e901 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -99,12 +99,12 @@ --light-color-scheduling-profiler-react-passive-effects-hover: #e7c20a; --light-color-scheduling-profiler-react-schedule: #9fc3f3; --light-color-scheduling-profiler-react-schedule-hover: #2683E2; - --light-color-scheduling-profiler-react-suspense-pending: #c9cacd; - --light-color-scheduling-profiler-react-suspense-pending-hover: #93959a; --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-scroll-thumb: #c2c2c2; @@ -225,12 +225,12 @@ --dark-color-scheduling-profiler-react-passive-effects-hover: #e4c00f; --dark-color-scheduling-profiler-react-schedule: #2683E2; --dark-color-scheduling-profiler-react-schedule-hover: #1a76d4; - --dark-color-scheduling-profiler-react-suspense-pending: #c9cacd; - --dark-color-scheduling-profiler-react-suspense-pending-hover: #93959a; --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-scroll-thumb: #afb3b9; From 16088f298fc52512f9dfa042cf190426334c1a62 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 16:19:04 -0400 Subject: [PATCH 08/22] Scroll view shows up/down caret overlay indicating content is above or below the fold --- .../src/content-views/constants.js | 2 + .../src/view-base/VerticalScrollView.js | 54 +++++++++++++++++++ .../views/Settings/SettingsContext.js | 1 + .../src/devtools/views/root.css | 2 + 4 files changed, 59 insertions(+) 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 5a241c97742f4..2a3b6cda7d9e5 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -73,6 +73,7 @@ export let COLORS = { REACT_SUSPENSE_UNRESOLVED_EVENT: '', REACT_SUSPENSE_UNRESOLVED_EVENT_HOVER: '', REACT_WORK_BORDER: '', + SCROLL_CARET: '', TEXT_COLOR: '', TIME_MARKER_LABEL: '', WARNING_BACKGROUND: '', @@ -172,6 +173,7 @@ export function updateColorsToMatchTheme(): void { 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', ), 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 f91a2640282bf..3d54c797ad076 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -16,6 +16,7 @@ import type { } 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; // TODO VerticalScrollView Draw caret over top+center and/or bottom+center to indicate scrollable content. export class VerticalScrollView extends View { @@ -49,6 +55,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`. 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 29b1b12be0289..ce4e68628f715 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -559,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); diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index 10b68fb01e901..e65c2f02d0faf 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -113,6 +113,7 @@ --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-scroll-caret: #d1d1d1; --light-color-shadow: rgba(0, 0, 0, 0.25); --light-color-tab-selected-border: #0088fa; --light-color-text: #000000; @@ -239,6 +240,7 @@ --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; From 216f4cdea81c0cd49d618c0d474f1151edfdf93c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 18:04:11 -0400 Subject: [PATCH 09/22] Resize bars show labels now when collapsed --- .../src/CanvasPage.js | 12 +- .../src/content-views/utils/text.js | 21 +- .../src/view-base/ResizableView.js | 187 ++++++++++++------ .../src/view-base/VerticalScrollView.js | 1 - .../src/view-base/useCanvasInteraction.js | 20 ++ 5 files changed, 175 insertions(+), 66 deletions(-) diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 9aa4b5230af23..569728e57ab4f 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -157,6 +157,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { function createViewHelper( view: View, + resizeLabel: string = '', shouldScrollVertically: boolean = false, shouldResizeVertically: boolean = false, ): View { @@ -186,6 +187,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { defaultFrame, horizontalPanAndZoomView, canvasRef, + resizeLabel, ); } @@ -215,6 +217,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { nativeEventsViewRef.current = nativeEventsView; const nativeEventsViewWrapper = createViewHelper( nativeEventsView, + 'events', true, true, ); @@ -235,6 +238,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { suspenseEventsViewRef.current = suspenseEventsView; const suspenseEventsViewWrapper = createViewHelper( suspenseEventsView, + 'suspense', true, true, ); @@ -247,6 +251,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { reactMeasuresViewRef.current = reactMeasuresView; const reactMeasuresViewWrapper = createViewHelper( reactMeasuresView, + 'react', true, true, ); @@ -258,7 +263,12 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { data.duration, ); flamechartViewRef.current = flamechartView; - const flamechartViewWrapper = createViewHelper(flamechartView, true, true); + const flamechartViewWrapper = createViewHelper( + flamechartView, + 'flamechart', + true, + true, + ); // Root view contains all of the sub views defined above. // The order we add them below determines their vertical position. 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 index 8ece94a37e84f..da14733bce62c 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/utils/text.js @@ -42,9 +42,11 @@ export function drawText( fullRect: Rect, drawableRect: Rect, availableWidth: number, + textAlign: 'left' | 'center' = 'left', + fillStyle: string = COLORS.TEXT_COLOR, ): void { if (availableWidth > TEXT_PADDING * 2) { - context.textAlign = 'left'; + context.textAlign = textAlign; context.textBaseline = 'middle'; context.font = `${FONT_SIZE}px sans-serif`; @@ -57,7 +59,7 @@ export function drawText( ); if (trimmedName !== null) { - context.fillStyle = COLORS.TEXT_COLOR; + context.fillStyle = fillStyle; // Prevent text from visibly overflowing its container when clipped. const textOverflowsViewableArea = !rectEqualToRect( @@ -77,11 +79,16 @@ export function drawText( context.clip(); } - context.fillText( - trimmedName, - x + TEXT_PADDING - (x < 0 ? x : 0), - y + fullRect.size.height / 2, - ); + 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/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 18cc70c745897..27e7716d52c88 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -8,19 +8,21 @@ */ import type { + ClickInteraction, DoubleClickInteraction, Interaction, MouseDownInteraction, MouseMoveInteraction, MouseUpInteraction, } from './useCanvasInteraction'; -import type {Rect, Size} from './geometry'; +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 {rectContainsPoint} from './geometry'; +import {intersectionOfRects, rectContainsPoint} from './geometry'; import {noopLayout} from './layouter'; import {clamp} from './utils/clamp'; @@ -38,20 +40,27 @@ type LayoutState = $ReadOnly<{| barOffsetY: number, |}>; -const RESIZE_BAR_SIZE = 8; const RESIZE_BAR_DOT_RADIUS = 1; const RESIZE_BAR_DOT_SPACING = 4; +const RESIZE_BAR_HEIGHT = 8; +const RESIZE_BAR_WITH_LABEL_HEIGHT = 16; class ResizeBar extends View { - _intrinsicContentSize: Size = { - width: 0, - height: RESIZE_BAR_SIZE, - }; - _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._intrinsicContentSize; + return this.showLabel + ? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0} + : {height: RESIZE_BAR_HEIGHT, width: 0}; } draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) { @@ -75,31 +84,58 @@ class ResizeBar extends View { const horizontalCenter = x + width / 2; const verticalCenter = y + height / 2; - // Draw 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(); + 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) { @@ -127,9 +163,18 @@ class ResizeBar extends View { this.frame, ); - if (cursorInView || viewRefs.activeView === this) { + 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; } @@ -171,9 +216,10 @@ class ResizeBar extends View { export class ResizableView extends View { _canvasRef: {current: HTMLCanvasElement | null}; - _resizingState: ResizingState | null = null; + _didDrag: boolean = false; _layoutState: LayoutState; _resizeBar: ResizeBar; + _resizingState: ResizingState | null = null; _subview: View; constructor( @@ -181,13 +227,14 @@ export class ResizableView extends View { 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); + this._resizeBar = new ResizeBar(surface, frame, label); this.addSubview(this._subview); this.addSubview(this._resizeBar); @@ -195,9 +242,9 @@ export class ResizableView extends View { // TODO (ResizableView) Allow subviews to specify default sizes. // Maybe that or set some % based default so all panels are visible to begin with. const subviewDesiredSize = subview.desiredSize(); - this._layoutState = { - barOffsetY: subviewDesiredSize ? subviewDesiredSize.height : 0, - }; + this._updateLayoutStateAndResizeBar( + subviewDesiredSize ? subviewDesiredSize.height : 0, + ); } desiredSize() { @@ -221,6 +268,19 @@ export class ResizableView extends View { 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; @@ -235,10 +295,9 @@ export class ResizableView extends View { proposedBarOffsetY = mouseY - frame.origin.y - cursorOffsetInBarFrame; } - this._layoutState = { - ...this._layoutState, - barOffsetY: clamp(0, maxBarOffset, proposedBarOffsetY), - }; + this._updateLayoutStateAndResizeBar( + clamp(0, maxBarOffset, proposedBarOffsetY), + ); } _updateSubviewFrames() { @@ -267,28 +326,37 @@ export class ResizableView extends View { currentY += this._resizeBar.frame.size.height; } - _handleDoubleClick(interaction: DoubleClickInteraction) { + _handleClick(interaction: ClickInteraction) { + if (this._didDrag) { + // Ignore click events that come after drag-to-resize. + return; + } + const cursorInView = rectContainsPoint( interaction.payload.location, this.frame, ); if (cursorInView) { if (this._layoutState.barOffsetY === 0) { - // Allow bar to travel to bottom of the visible area of this view but no further + // Clicking on the collapsed label should expand. const subviewDesiredSize = this._subview.desiredSize(); - const maxBarOffset = subviewDesiredSize.height; + this._updateLayoutStateAndResizeBar(subviewDesiredSize.height); + this.setNeedsDisplay(); + } + } + } - this._layoutState = { - ...this._layoutState, - barOffsetY: maxBarOffset, - }; - } else { - this._layoutState = { - ...this._layoutState, - barOffsetY: 0, - }; + _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(); } - this.setNeedsDisplay(); } } @@ -296,6 +364,7 @@ export class ResizableView extends View { const cursorLocation = interaction.payload.location; const resizeBarFrame = this._resizeBar.frame; if (rectContainsPoint(cursorLocation, resizeBarFrame)) { + this._didDrag = false; const mouseY = cursorLocation.y; this._resizingState = { cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y, @@ -307,6 +376,7 @@ export class ResizableView extends View { _handleMouseMove(interaction: MouseMoveInteraction) { const {_resizingState} = this; if (_resizingState) { + this._didDrag = true; this._resizingState = { ..._resizingState, mouseY: interaction.payload.location.y, @@ -335,6 +405,9 @@ export class ResizableView extends View { handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { switch (interaction.type) { + case 'click': + this._handleClick(interaction); + return; case 'double-click': this._handleDoubleClick(interaction); return; 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 3d54c797ad076..b7912ed74c0cd 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -33,7 +33,6 @@ const CARET_MARGIN = 3; const CARET_WIDTH = 5; const CARET_HEIGHT = 3; -// TODO VerticalScrollView Draw caret over top+center and/or bottom+center to indicate scrollable content. export class VerticalScrollView extends View { _scrollState: ScrollState = {offset: 0, length: 0}; _isPanning = false; 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 147188ad27835..5aa8c28c667c6 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js @@ -13,6 +13,13 @@ import type {Point} from './geometry'; import {useEffect} from 'react'; import {normalizeWheel} from './utils/normalizeWheel'; +export type ClickInteraction = {| + type: 'click', + payload: {| + event: MouseEvent, + location: Point, + |}, +|}; export type DoubleClickInteraction = {| type: 'double-click', payload: {| @@ -75,6 +82,7 @@ export type WheelWithMetaInteraction = {| |}; export type Interaction = + | ClickInteraction | DoubleClickInteraction | MouseDownInteraction | MouseMoveInteraction @@ -121,6 +129,16 @@ export function useCanvasInteraction( }; } + const onCanvasClick: MouseEventHandler = event => { + interactor({ + type: 'click', + payload: { + event, + location: localToCanvasCoordinates({x: event.x, y: event.y}), + }, + }); + }; + const onCanvasDoubleClick: MouseEventHandler = event => { interactor({ type: 'double-click', @@ -197,6 +215,7 @@ 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); @@ -205,6 +224,7 @@ 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); From b7e66ec70076efcb752c87414e6b9774ffab2e54 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 19:33:24 -0400 Subject: [PATCH 10/22] Changed scroll wheel behavior Vertical wheel now zooms. Horizontal wheel pans. (Vertical wheel with shift zooms.) Wheel events of any kind clear the current tooltip as well. --- .../src/CanvasPage.js | 33 +++++++ .../src/view-base/HorizontalPanAndZoomView.js | 86 ++++++++----------- .../src/view-base/ResizableView.js | 8 -- .../src/view-base/VerticalScrollView.js | 8 +- .../src/view-base/useCanvasInteraction.js | 22 ++++- 5 files changed, 93 insertions(+), 64 deletions(-) diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 569728e57ab4f..5bb906e7f9398 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -310,6 +310,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); 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/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 27e7716d52c88..98cecc66c7cfa 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -216,7 +216,6 @@ class ResizeBar extends View { export class ResizableView extends View { _canvasRef: {current: HTMLCanvasElement | null}; - _didDrag: boolean = false; _layoutState: LayoutState; _resizeBar: ResizeBar; _resizingState: ResizingState | null = null; @@ -327,11 +326,6 @@ export class ResizableView extends View { } _handleClick(interaction: ClickInteraction) { - if (this._didDrag) { - // Ignore click events that come after drag-to-resize. - return; - } - const cursorInView = rectContainsPoint( interaction.payload.location, this.frame, @@ -364,7 +358,6 @@ export class ResizableView extends View { const cursorLocation = interaction.payload.location; const resizeBarFrame = this._resizeBar.frame; if (rectContainsPoint(cursorLocation, resizeBarFrame)) { - this._didDrag = false; const mouseY = cursorLocation.y; this._resizingState = { cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y, @@ -376,7 +369,6 @@ export class ResizableView extends View { _handleMouseMove(interaction: MouseMoveInteraction) { const {_resizingState} = this; if (_resizingState) { - this._didDrag = true; this._resizingState = { ..._resizingState, mouseY: interaction.payload.location.y, 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 b7912ed74c0cd..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,7 +12,7 @@ import type { MouseDownInteraction, MouseMoveInteraction, MouseUpInteraction, - WheelPlainInteraction, + WheelWithShiftInteraction, } from './useCanvasInteraction'; import type {Rect} from './geometry'; import type {ScrollState} from './utils/scrollState'; @@ -157,7 +157,7 @@ export class VerticalScrollView extends View { } } - _handleWheelPlain(interaction: WheelPlainInteraction) { + _handleWheelShift(interaction: WheelWithShiftInteraction) { const { location, delta: {deltaX, deltaY}, @@ -195,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/useCanvasInteraction.js b/packages/react-devtools-scheduling-profiler/src/view-base/useCanvasInteraction.js index 5aa8c28c667c6..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,7 +10,7 @@ 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 = {| @@ -115,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) { @@ -130,6 +133,10 @@ export function useCanvasInteraction( } const onCanvasClick: MouseEventHandler = event => { + if (didMouseMoveWhileDownRef.current) { + return; + } + interactor({ type: 'click', payload: { @@ -140,6 +147,10 @@ export function useCanvasInteraction( }; const onCanvasDoubleClick: MouseEventHandler = event => { + if (didMouseMoveWhileDownRef.current) { + return; + } + interactor({ type: 'double-click', payload: { @@ -150,6 +161,9 @@ export function useCanvasInteraction( }; const onCanvasMouseDown: MouseEventHandler = event => { + didMouseMoveWhileDownRef.current = false; + isMouseDownRef.current = true; + interactor({ type: 'mousedown', payload: { @@ -160,6 +174,10 @@ export function useCanvasInteraction( }; const onDocumentMouseMove: MouseEventHandler = event => { + if (isMouseDownRef.current) { + didMouseMoveWhileDownRef.current = true; + } + interactor({ type: 'mousemove', payload: { @@ -170,6 +188,8 @@ export function useCanvasInteraction( }; const onDocumentMouseUp: MouseEventHandler = event => { + isMouseDownRef.current = false; + interactor({ type: 'mouseup', payload: { From 9c656f21ddf36357cc8fd4860a8051c4a392c90c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 19:56:22 -0400 Subject: [PATCH 11/22] Clip Suspense diamonds so they don't overflow after being resized --- .../src/content-views/SuspenseEventsView.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index 89fa602d4111a..f3071d95ce9e3 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -166,6 +166,18 @@ export class SuspenseEventsView extends View { 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); @@ -173,6 +185,7 @@ export class SuspenseEventsView extends View { context.lineTo(xStart, y + halfSize); context.lineTo(xStart - halfSize, y); context.fill(); + context.restore(); } else { const xStop = timestampToPosition( timestamp + duration, From 1f476f3f1844d81243d2e00befe24c6dad0ce090 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 20:15:22 -0400 Subject: [PATCH 12/22] Set default height for resizable views --- .../src/content-views/FlamechartView.js | 7 ++++++- .../src/content-views/ReactMeasuresView.js | 6 ++++-- .../src/content-views/SuspenseEventsView.js | 6 ++++-- .../src/view-base/ResizableView.js | 7 ++++++- .../src/view-base/View.js | 4 ++-- .../src/view-base/geometry.js | 4 ++++ 6 files changed, 26 insertions(+), 8 deletions(-) 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 edf50eec0163a..fff6daa69d03e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -292,7 +292,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/ReactMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js index 98dc538bce5bf..b980441b5c96e 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, }; } diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index f3071d95ce9e3..1601a7bba753c 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -12,7 +12,7 @@ import type { Interaction, MouseMoveInteraction, Rect, - Size, + SizeWithMaxHeight, ViewRefs, } from '../view-base'; @@ -40,11 +40,12 @@ import { } 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: Size; + _intrinsicSize: SizeWithMaxHeight; _maxDepth: number = 0; _profilerData: ReactProfilerData; @@ -79,6 +80,7 @@ export class SuspenseEventsView extends View { this._intrinsicSize = { width: duration, height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT, + maxInitialHeight: ROW_WITH_BORDER_HEIGHT * MAX_ROWS_TO_SHOW_INITIALLY, }; } diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 98cecc66c7cfa..970371414a1dd 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -242,7 +242,12 @@ export class ResizableView extends View { // Maybe that or set some % based default so all panels are visible to begin with. const subviewDesiredSize = subview.desiredSize(); this._updateLayoutStateAndResizeBar( - subviewDesiredSize ? subviewDesiredSize.height : 0, + subviewDesiredSize.maxInitialHeight != null + ? Math.min( + subviewDesiredSize.maxInitialHeight, + subviewDesiredSize.height, + ) + : subviewDesiredSize.height, ); } 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 b72532d75e314..bd0cd64b03739 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(); } 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..df15c934738f1 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|}>; /** From 1610784e217c51ca2d237b0e38c81724e0f96dd0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 20:30:40 -0400 Subject: [PATCH 13/22] Tweaked layout and passive colors to align more with commit phase color They seemed to separate/bold before --- .../src/devtools/views/root.css | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index e65c2f02d0faf..009324450dafa 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -92,11 +92,11 @@ --light-color-scheduling-profiler-react-render: #9fc3f3; --light-color-scheduling-profiler-react-render-hover: #83afe9; --light-color-scheduling-profiler-react-commit: #c88ff0; - --light-color-scheduling-profiler-react-commit-hover: #b069e2; - --light-color-scheduling-profiler-react-layout-effects: #fb3655; - --light-color-scheduling-profiler-react-layout-effects-hover: #f82849; - --light-color-scheduling-profiler-react-passive-effects: #f1cc14; - --light-color-scheduling-profiler-react-passive-effects-hover: #e7c20a; + --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-suspense-rejected: #f1cc14; @@ -219,11 +219,11 @@ --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: #601593; - --dark-color-scheduling-profiler-react-layout-effects: #ee1638; - --dark-color-scheduling-profiler-react-layout-effects-hover: #da1030; - --dark-color-scheduling-profiler-react-passive-effects: #f1cc14; - --dark-color-scheduling-profiler-react-passive-effects-hover: #e4c00f; + --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; From 1399595017fb7bf3e60791f1057273c51e97e8d7 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 20:54:20 -0400 Subject: [PATCH 14/22] Added Suspense event row-border lines --- .../src/content-views/SuspenseEventsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js index 1601a7bba753c..82aeb1c248f71 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -273,7 +273,7 @@ export class SuspenseEventsView extends View { const borderFrame: Rect = { origin: { x: frame.origin.x, - y: frame.origin.y + SUSPENSE_EVENT_HEIGHT, + y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE, }, size: { width: frame.size.width, From 245d7c3bb92dbd863e86be8b1bf73aa8a5d29b37 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 30 Jul 2021 21:10:04 -0400 Subject: [PATCH 15/22] Updated profiler failing (snapshot) tests --- .../__tests__/preprocessData-test.internal.js | 21 +++++++++++-------- .../SchedulingProfiler-test.internal.js | 8 +++---- 2 files changed, 16 insertions(+), 13 deletions(-) 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-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)}`, From a9874287fd815c603a5c8879c2e5c4a63f65d598 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 31 Jul 2021 13:57:48 -0400 Subject: [PATCH 16/22] Tweaked caret colors to be more visible --- packages/react-devtools-shared/src/devtools/views/root.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index 009324450dafa..e35a1c6467878 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -113,8 +113,7 @@ --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-scroll-caret: #d1d1d1; - --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; From 772b2c309de8db354287dc6ec8e32c5e7dd4d4cd Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 31 Jul 2021 14:14:07 -0400 Subject: [PATCH 17/22] Profiler now remembers which (sub) tab was previously selected --- .../src/CanvasPage.js | 6 ++--- .../src/SchedulingProfiler.js | 14 ++++++++-- .../src/content-views/FlamechartView.js | 8 +++--- .../src/content-views/constants.js | 9 ++++++- .../{ColorView.js => BackgroundColorView.js} | 26 ++++--------------- .../src/view-base/index.js | 2 +- .../views/Profiler/ProfilerContext.js | 5 +++- 7 files changed, 35 insertions(+), 35 deletions(-) rename packages/react-devtools-scheduling-profiler/src/view-base/{ColorView.js => BackgroundColorView.js} (51%) diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 5bb906e7f9398..b35628a705d01 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -31,7 +31,7 @@ import {copy} from 'clipboard-js'; import prettyMilliseconds from 'pretty-ms'; import { - ColorView, + BackgroundColorView, HorizontalPanAndZoomView, ResizableView, Surface, @@ -291,9 +291,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { rootView.addSubview(flamechartViewWrapper); // If subviews are less than the available height, fill remaining height with a solid color. - rootView.addSubview( - new ColorView(surface, defaultFrame, COLORS.BACKGROUND), - ); + rootView.addSubview(new BackgroundColorView(surface, defaultFrame)); surfaceRef.current.rootView = rootView; }, [data]); 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 fff6daa69d03e..9316fa97bc914 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -21,7 +21,7 @@ import type { } from '../view-base'; import { - ColorView, + BackgroundColorView, Surface, View, layeredLayout, @@ -269,10 +269,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); } 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 2a3b6cda7d9e5..27ce99a05a11e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -82,9 +82,14 @@ export let COLORS = { 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( @@ -189,4 +194,6 @@ export function updateColorsToMatchTheme(): void { '--color-warning-text-color-inverted', ), }; + + return true; } 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/index.js b/packages/react-devtools-scheduling-profiler/src/view-base/index.js index 1df9721cd18b2..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,7 +7,7 @@ * @flow */ -export * from './ColorView'; +export * from './BackgroundColorView'; export * from './HorizontalPanAndZoomView'; export * from './ResizableView'; export * from './Surface'; 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(() => { From a28ef990893221c2a6895412f93e024e608002ff Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 1 Aug 2021 10:37:14 -0400 Subject: [PATCH 18/22] Updated cursor styles --- .../src/content-views/FlamechartView.js | 2 +- .../src/content-views/NativeEventsView.js | 2 -- .../src/content-views/ReactMeasuresView.js | 2 +- .../src/content-views/SchedulingEventsView.js | 2 +- .../src/content-views/SuspenseEventsView.js | 4 ++-- .../src/content-views/UserTimingMarksView.js | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) 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 9316fa97bc914..4c7901e6d28d5 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -198,7 +198,7 @@ class FlamechartStackLayerView extends View { const width = durationToWidth(duration, scaleFactor); const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame)); if (x <= location.x && x + width >= location.x) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; _onHover(flamechartStackFrame); return; 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 bf1ac816776a3..98e388c52a815 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -238,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 b980441b5c96e..96790b2a282e5 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js @@ -308,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/SchedulingEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js index c53bd8ead6bf4..37ff0832e0261 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SchedulingEventsView.js @@ -243,7 +243,7 @@ export class SchedulingEventsView extends View { 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 index 82aeb1c248f71..3d85ffd4e2dc0 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -334,7 +334,7 @@ export class SuspenseEventsView extends View { timestamp - timestampAllowance <= hoverTimestamp && hoverTimestamp <= timestamp + timestampAllowance ) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; @@ -345,7 +345,7 @@ export class SuspenseEventsView extends View { hoverTimestamp >= timestamp && hoverTimestamp <= timestamp + duration ) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; 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 a0640923ebc63..2249d3841ad7a 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js @@ -224,7 +224,7 @@ export class UserTimingMarksView extends View { timestamp - timestampAllowance <= hoverTimestamp && hoverTimestamp <= timestamp + timestampAllowance ) { - this.currentCursor = 'pointer'; + this.currentCursor = 'context-menu'; viewRefs.hoveredView = this; onHover(mark); return; From 586aefa27462710543e7ab5a3253e312633421c4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 1 Aug 2021 14:22:53 -0400 Subject: [PATCH 19/22] Don't show tooltips for flamegraph nodes that are too small to be visible --- .../src/content-views/FlamechartView.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 4c7901e6d28d5..57cd8ae1f30c4 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -195,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 = 'context-menu'; - 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) { From 17e3832d17fb088f750123895c14ddab11313b31 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 2 Aug 2021 08:32:07 -0400 Subject: [PATCH 20/22] Prevent hidden views from triggering cursor/tooltip interactions --- .../src/view-base/ResizableView.js | 34 ++++++++----------- .../src/view-base/View.js | 15 ++++++-- .../src/view-base/geometry.js | 9 +++++ .../src/view-base/layouter.js | 5 +-- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js index 970371414a1dd..54a3b05c1a9cf 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -45,6 +45,11 @@ 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; @@ -238,8 +243,6 @@ export class ResizableView extends View { this.addSubview(this._subview); this.addSubview(this._resizeBar); - // TODO (ResizableView) Allow subviews to specify default sizes. - // Maybe that or set some % based default so all panels are visible to begin with. const subviewDesiredSize = subview.desiredSize(); this._updateLayoutStateAndResizeBar( subviewDesiredSize.maxInitialHeight != null @@ -253,14 +256,9 @@ export class ResizableView extends View { desiredSize() { const resizeBarDesiredSize = this._resizeBar.desiredSize(); - const subviewDesiredSize = this._subview.desiredSize(); - - const subviewDesiredWidth = subviewDesiredSize - ? subviewDesiredSize.width - : 0; return { - width: Math.max(subviewDesiredWidth, resizeBarDesiredSize.width), + width: this.frame.size.width, height: this._layoutState.barOffsetY + resizeBarDesiredSize.height, }; } @@ -315,19 +313,19 @@ export class ResizableView extends View { const resizeBarDesiredSize = this._resizeBar.desiredSize(); - let currentY = y; - - this._subview.setFrame({ - origin: {x, y: currentY}, - size: {width, height: barOffsetY}, - }); - currentY += this._subview.frame.size.height; + 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: currentY}, + origin: {x, y: y + barOffsetY}, size: {width, height: resizeBarDesiredSize.height}, }); - currentY += this._resizeBar.frame.size.height; } _handleClick(interaction: ClickInteraction) { @@ -388,8 +386,6 @@ export class ResizableView extends View { } } - _didGrab: boolean = false; - getCursorActiveSubView(interaction: Interaction): View | null { const cursorLocation = interaction.payload.location; const resizeBarFrame = this._resizeBar.frame; 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 bd0cd64b03739..a43a733fd4762 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/View.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/View.js @@ -271,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 df15c934738f1..49c0d3981e721 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/geometry.js @@ -74,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/layouter.js b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js index 2cad9659be066..58adb026c2907 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/layouter.js @@ -51,8 +51,9 @@ 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`. From 89979f5e8194907f165b166c1960d6157a9a8693 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 2 Aug 2021 09:10:30 -0400 Subject: [PATCH 21/22] Fixed unrelated key warning in DevTools test shell --- packages/react-devtools-shared/src/devtools/views/TabBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From eaa8c2cd49a029d2fece07cecceca5ebaea88904 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 2 Aug 2021 09:19:29 -0400 Subject: [PATCH 22/22] STASHING --- .../src/CanvasPage.js | 9 +- .../view-base/VerticalScrollOverflowView.js | 319 +++++++++++++++++- .../src/view-base/VerticalScrollView.js | 1 + .../src/view-base/index.js | 1 + .../react-devtools-shell/src/app/index.js | 4 +- 5 files changed, 328 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index b35628a705d01..647849c732446 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -34,6 +34,7 @@ import { BackgroundColorView, HorizontalPanAndZoomView, ResizableView, + VerticalScrollOverflowView, Surface, VerticalScrollView, View, @@ -293,7 +294,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { // 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; + const rootViewWithVerticalScroll = new VerticalScrollOverflowView( + surface, + defaultFrame, + rootView, + ); + + surfaceRef.current.rootView = rootViewWithVerticalScroll; }, [data]); useLayoutEffect(() => { diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js index 7ddf6c1842691..e0bc73f37b39a 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js @@ -1,3 +1,316 @@ -// 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. +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Interaction} from './useCanvasInteraction'; +import type {Rect} from './geometry'; +import type {Surface, ViewRefs} from './Surface'; +import type { + ClickInteraction, + MouseDownInteraction, + MouseMoveInteraction, + MouseUpInteraction, +} from './useCanvasInteraction'; + +import { + intersectionOfRects, + rectContainsPoint, + rectIntersectsRect, +} from './geometry'; +import {View} from './View'; +import {BORDER_SIZE, COLORS} from '../content-views/constants'; + +const SCROLL_BAR_SIZE = 14; + +const HIDDEN_RECT = { + origin: { + x: 0, + y: 0, + }, + size: { + width: 0, + height: 0, + }, +}; + +// TODO How do we handle resizing +export class VerticalScrollOverflowView extends View { + _contentView: View; + _isScrolling: boolean = false; + _scrollOffset: number = 0; + _scrollBarView: VerticalScrollBarView; + + constructor(surface: Surface, frame: Rect, contentView: View) { + super(surface, frame); + + this._contentView = contentView; + this._scrollBarView = new VerticalScrollBarView(surface, frame, this); + + this.addSubview(contentView); + this.addSubview(this._scrollBarView); + } + + setScrollOffset(value: number) { + this._scrollOffset = value; + + const proposedFrame = { + ...this._contentView.frame, + origin: { + ...this._contentView.frame.origin, + y: value, + } + }; + + // TODO How do we pass this down to the sub views? + // It doesn't currently seem to be working. + // this._contentView.setVisibleArea(visibleArea); + + this._contentView.subviews.forEach((subview, subviewIndex) => { + if (rectIntersectsRect(proposedFrame, subview.frame)) { + subview.setFrame(intersectionOfRects(proposedFrame, subview.frame)); + } else { + subview.setFrame(HIDDEN_RECT); + } + }); + + this.setNeedsDisplay(); + } + + layoutSubviews() { + super.layoutSubviews(); + + const {frame} = this; + const {x, y} = frame.origin; + const {width, height} = frame.size; + + const contentHeight = this._contentView.desiredSize().height; + const shouldScroll = contentHeight > height; + + const scrollBarView = this._scrollBarView; + + this._contentView.setVisibleArea({ + origin: { + x, + y: y + this._scrollOffset, + }, + size: { + width: shouldScroll ? width - SCROLL_BAR_SIZE : width, + height, + }, + }); + + if (shouldScroll) { + const scrollBarX = x + width - SCROLL_BAR_SIZE; + + const proposedScrollBarFrame = { + origin: { + x: scrollBarX, + y, + }, + size: { + width: SCROLL_BAR_SIZE, + height, + }, + }; + + scrollBarView.setFrame(proposedScrollBarFrame); + scrollBarView.setContentHeight(contentHeight); + scrollBarView.setShouldScroll(true); + } else { + scrollBarView.setShouldScroll(false); + } + + this.setNeedsDisplay(); + } +} + +export class VerticalScrollBarView extends View { + _contentHeight: number = 0; + _isScrolling: boolean = false; + _scrollBarRect: Rect = HIDDEN_RECT; + _scrollThumbRect: Rect = HIDDEN_RECT; + _shouldScroll: boolean = false; + _verticalScrollOverflowView: VerticalScrollOverflowView; + + constructor( + surface: Surface, + frame: Rect, + verticalScrollOverflowView: VerticalScrollOverflowView, + ) { + super(surface, frame); + + this._verticalScrollOverflowView = verticalScrollOverflowView; + } + + get shouldScroll(): boolean { + return this._shouldScroll; + } + + setContentHeight(contentHeight: number) { + if (this._contentHeight !== contentHeight) { + this._contentHeight = contentHeight; + + const {height, width} = this.frame.size; + + this._scrollThumbRect = { + origin: { + x: this.frame.origin.x, + y: this._scrollThumbRect.origin.y, + }, + size: { + width, + height: height * (height / contentHeight), + }, + }; + + this.setNeedsDisplay(); + } + } + + setShouldScroll(shouldScroll: boolean) { + if (this._shouldScroll !== shouldScroll) { + this._shouldScroll = shouldScroll; + + this.setNeedsDisplay(); + } + } + + setScrollThumbY(value: number) { + const {height} = this.frame.size; + const scrollThumbRect = this._scrollThumbRect; + + const maxScrollThumbY = height - scrollThumbRect.size.height; + const newScrollThumbY = Math.max(0, Math.min(maxScrollThumbY, value)); + + this._scrollThumbRect = { + ...scrollThumbRect, + origin: { + x: this.frame.origin.x, + y: newScrollThumbY, + }, + }; + + this.setNeedsDisplay(); + + const maxContentOffset = this._contentHeight - height; + const contentScrollOffset = + (newScrollThumbY / maxScrollThumbY) * maxContentOffset * -1; + + this._verticalScrollOverflowView.setScrollOffset(contentScrollOffset); + } + + draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) { + if (this.shouldScroll) { + const {x, y} = this.frame.origin; + const {width, height} = this.frame.size; + + // TODO Use real color + context.fillStyle = COLORS.REACT_RESIZE_BAR; + context.fillRect(x, y, width, height); + + // TODO Use real color + context.fillStyle = COLORS.SCROLL_CARET; + context.fillRect( + this._scrollThumbRect.origin.x, + this._scrollThumbRect.origin.y, + this._scrollThumbRect.size.width, + this._scrollThumbRect.size.height, + ); + + // TODO Use real color + context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER; + context.fillRect(x, y, BORDER_SIZE, height); + } + } + + handleInteraction(interaction: Interaction, viewRefs: ViewRefs) { + if (!this.shouldScroll) { + // If content isn't scrollable, ignore. + return; + } + + switch (interaction.type) { + case 'click': + this._handleClick(interaction, viewRefs); + break; + case 'mousedown': + this._handleMouseDown(interaction, viewRefs); + break; + case 'mousemove': + this._handleMouseMove(interaction, viewRefs); + break; + case 'mouseup': + this._handleMouseUp(interaction, viewRefs); + break; + } + } + + _handleClick(interaction: ClickInteraction, viewRefs: ViewRefs) { + const {location} = interaction.payload; + if (rectContainsPoint(location, this.frame)) { + const currentScrollThumbY = this._scrollThumbRect.origin.y; + const y = location.y; + + if (rectContainsPoint(location, this._scrollThumbRect)) { + // Ignore clicks on the track thumb directly. + return; + } + + // Scroll up or down about one viewport worth of content: + // TODO This calculation is broken + const deltaY = this.frame.size.height * 0.8; + + this.setScrollThumbY( + y < currentScrollThumbY + ? currentScrollThumbY - deltaY + : currentScrollThumbY + deltaY, + ); + } + } + + _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) { + const {location} = interaction.payload; + if (!rectContainsPoint(location, this._scrollThumbRect)) { + return; + } + viewRefs.activeView = this; + + this.currentCursor = 'default'; + + this._isScrolling = true; + this.setNeedsDisplay(); + } + + _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) { + const {event, location} = interaction.payload; + if (rectContainsPoint(location, this.frame)) { + if (viewRefs.hoveredView !== this) { + viewRefs.hoveredView = this; + } + + this.currentCursor = 'default'; + } + + if (viewRefs.activeView === this) { + this.currentCursor = 'default'; + + this.setScrollThumbY(this._scrollThumbRect.origin.y + event.movementY); + } + } + + _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) { + if (viewRefs.activeView === this) { + viewRefs.activeView = null; + } + + if (this._isScrolling) { + this._isScrolling = false; + this.setNeedsDisplay(); + } + } +} 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 245b53aff7180..2fd41fae10814 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -135,6 +135,7 @@ export class VerticalScrollView extends View { _handleMouseDown(interaction: MouseDownInteraction) { if (rectContainsPoint(interaction.payload.location, this.frame)) { +console.log('VerticalScrollView()\n location:', interaction.payload.location, '\n frame:', JSON.stringify(this.frame).replace('"', ''), '\n visibleArea:', JSON.stringify(this.visibleArea).replace('"', '')); this._isPanning = true; } } 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 b5455ce249f3c..fc41a7504b4c3 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/index.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/index.js @@ -12,6 +12,7 @@ export * from './HorizontalPanAndZoomView'; export * from './ResizableView'; export * from './Surface'; export * from './VerticalScrollView'; +export * from './VerticalScrollOverflowView'; export * from './View'; export * from './geometry'; export * from './layouter'; diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js index d00ffde4a5ded..dd6fe9663c24e 100644 --- a/packages/react-devtools-shell/src/app/index.js +++ b/packages/react-devtools-shell/src/app/index.js @@ -12,7 +12,7 @@ import Iframe from './Iframe'; import EditableProps from './EditableProps'; import ElementTypes from './ElementTypes'; import Hydration from './Hydration'; -import InlineWarnings from './InlineWarnings'; +// import InlineWarnings from './InlineWarnings'; import InspectableElements from './InspectableElements'; import ReactNativeWeb from './ReactNativeWeb'; import ToDoList from './ToDoList'; @@ -52,7 +52,7 @@ function mountTestApp() { mountHelper(Hydration); mountHelper(ElementTypes); mountHelper(EditableProps); - mountHelper(InlineWarnings); + // mountHelper(InlineWarnings); mountHelper(ReactNativeWeb); mountHelper(Toggle); mountHelper(ErrorBoundaries);