From 95a1f38292be26a2dff9aa7303f6434fe2f41614 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 17 Aug 2021 10:40:46 -0700 Subject: [PATCH 1/5] Text utils (draw and measure) performance improvements Better handle the case of extemely long strings (which previously draw and measures by removing one character at a time) to instead use a binary search pattern of finding the biggest string that fits. --- .../src/content-views/utils/text.js | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) 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 4cd7d94821589..9351d1685ff7a 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 @@ -32,14 +32,34 @@ export function trimText( 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) + '…'; + const maxIndex = text.length - 1; + + let startIndex = 0; + let stopIndex = maxIndex; + + let longestValidIndex = 0; + let longestValidText = null; + + // Trimming long text could be really slow if we decrease only 1 character at a time. + // Trimming with more of a binary search approach is faster in the worst cases. + while (startIndex <= stopIndex) { + const currentIndex = Math.floor((startIndex + stopIndex) / 2); + const trimmedText = + currentIndex === maxIndex ? text : text.substr(0, currentIndex) + '…'; + if (getTextWidth(context, trimmedText) <= width) { - return trimmedText; + if (longestValidIndex < currentIndex) { + longestValidIndex = currentIndex; + longestValidText = trimmedText; + } + + startIndex = currentIndex + 1; + } else { + stopIndex = currentIndex - 1; } } - return null; + return longestValidText; } type TextConfig = {| From 8279dc8634c734e15fd2147955140e1bd26eb186 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 17 Aug 2021 15:43:13 -0700 Subject: [PATCH 2/5] Slightly tweaked scroll event bubbling behavior Don't allow wheel events to bubble past this view even if we've scrolled to the edge. It just feels bad to have the scrolling jump unexpectedly from in a container to the outer page. The only exception is when the container fitst the contnet (no scrolling). Also don't prevent mouse move events from bubbling if dragging horizontally within a vertical scroll view or vertically within a horizontal scroll view. --- .../src/view-base/HorizontalPanAndZoomView.js | 11 ++++++-- .../src/view-base/VerticalScrollView.js | 27 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) 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 5519599f12dcc..2a998493eda1b 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js @@ -156,7 +156,7 @@ export class HorizontalPanAndZoomView extends View { interaction.payload.location, this.frame, ); - if (isHovered) { + if (isHovered && viewRefs.hoveredView === null) { viewRefs.hoveredView = this; } @@ -169,9 +169,16 @@ export class HorizontalPanAndZoomView extends View { if (!this._isPanning) { return; } + + // Don't prevent mouse-move events from bubbling if they are vertical drags. + const {movementX, movementY} = interaction.payload.event; + if (Math.abs(movementX) < Math.abs(movementY)) { + return; + } + const newState = translateState({ state: this._viewState.horizontalScrollState, - delta: interaction.payload.event.movementX, + delta: movementX, containerLength: this.frame.size.width, }); this._viewState.updateHorizontalScrollState(newState); 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 a78e88425faa2..d54907da1a8db 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js @@ -199,12 +199,20 @@ export class VerticalScrollView extends View { if (!this._isPanning) { return; } + + // Don't prevent mouse-move events from bubbling if they are horizontal drags. + const {movementX, movementY} = interaction.payload.event; + if (Math.abs(movementX) > Math.abs(movementY)) { + return; + } + const newState = translateState({ state: this._scrollState, delta: interaction.payload.event.movementY, containerLength: this.frame.size.height, }); this._setScrollState(newState); + return true; } @@ -255,12 +263,14 @@ export class VerticalScrollView extends View { } _setScrollState(proposedState: ScrollState): boolean { - const height = this._contentView.frame.size.height; + const contentHeight = this._contentView.frame.size.height; + const containerHeight = this.frame.size.height; + const clampedState = clampState({ state: proposedState, - minContentLength: height, - maxContentLength: height, - containerLength: this.frame.size.height, + minContentLength: contentHeight, + maxContentLength: contentHeight, + containerLength: containerHeight, }); if (!areScrollStatesEqual(clampedState, this._scrollState)) { this._scrollState.offset = clampedState.offset; @@ -275,6 +285,13 @@ export class VerticalScrollView extends View { return true; } - return false; + // Don't allow wheel events to bubble past this view even if we've scrolled to the edge. + // It just feels bad to have the scrolling jump unexpectedly from in a container to the outer page. + // The only exception is when the container fitst the contnet (no scrolling). + if (contentHeight === containerHeight) { + return false; + } + + return true; } } From 62dffa6d00af34922dd7c263378e088ee1f1863c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 17 Aug 2021 16:02:21 -0700 Subject: [PATCH 3/5] Added Network measures to the Scheduling Profiler Also slightly refactored the preprocessData data function to split separate sections out into helper functions for readability. (We can do more of this for other mark types in a follow up commit.) --- .../src/CanvasPage.js | 106 +++++- .../src/EventTooltip.css | 15 +- .../src/EventTooltip.js | 49 +++ .../src/content-views/NetworkMeasuresView.js | 337 ++++++++++++++++++ .../src/content-views/constants.js | 16 + .../src/content-views/index.js | 1 + .../src/import-worker/preprocessData.js | 332 ++++++++++++----- .../src/types.js | 16 +- .../src/view-base/Surface.js | 43 ++- .../react-devtools-shared/src/constants.js | 8 + 10 files changed, 805 insertions(+), 118 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 83de9a2d2d598..db3d3cda4ee6e 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -44,6 +44,7 @@ import { ComponentMeasuresView, FlamechartView, NativeEventsView, + NetworkMeasuresView, ReactMeasuresView, SchedulingEventsView, SnapshotsView, @@ -128,6 +129,18 @@ const zoomToBatch = ( viewState.updateHorizontalScrollState(scrollState); }; +const EMPTY_CONTEXT_INFO: ReactHoverContextInfo = { + componentMeasure: null, + flamechartStackFrame: null, + measure: null, + nativeEvent: null, + networkMeasure: null, + schedulingEvent: null, + snapshot: null, + suspenseEvent: null, + userTimingMark: null, +}; + type AutoSizedCanvasProps = {| data: ReactProfilerData, height: number, @@ -150,7 +163,12 @@ function AutoSizedCanvas({ setHoveredEvent, ] = useState(null); - const surfaceRef = useRef(new Surface()); + const resetHoveredEvent = useCallback( + () => setHoveredEvent(EMPTY_CONTEXT_INFO), + [], + ); + + const surfaceRef = useRef(new Surface(resetHoveredEvent)); const userTimingMarksViewRef = useRef(null); const nativeEventsViewRef = useRef(null); const schedulingEventsViewRef = useRef(null); @@ -158,6 +176,7 @@ function AutoSizedCanvas({ const componentMeasuresViewRef = useRef(null); const reactMeasuresViewRef = useRef(null); const flamechartViewRef = useRef(null); + const networkMeasuresViewRef = useRef(null); const snapshotsViewRef = useRef(null); const {hideMenu: hideContextMenu} = useContext(RegistryContext); @@ -318,6 +337,22 @@ function AutoSizedCanvas({ ); } + let networkMeasuresViewWrapper = null; + if (data.snapshots.length > 0) { + const networkMeasuresView = new NetworkMeasuresView( + surface, + defaultFrame, + data, + ); + networkMeasuresViewRef.current = networkMeasuresView; + networkMeasuresViewWrapper = createViewHelper( + networkMeasuresView, + 'network', + true, + true, + ); + } + const flamechartView = new FlamechartView( surface, defaultFrame, @@ -357,6 +392,9 @@ function AutoSizedCanvas({ if (snapshotsViewWrapper !== null) { rootView.addSubview(snapshotsViewWrapper); } + if (networkMeasuresViewWrapper !== null) { + rootView.addSubview(networkMeasuresViewWrapper); + } rootView.addSubview(flamechartViewWrapper); const verticalScrollOverflowView = new VerticalScrollOverflowView( @@ -395,16 +433,18 @@ function AutoSizedCanvas({ prevHoverEvent.flamechartStackFrame !== null || prevHoverEvent.measure !== null || prevHoverEvent.nativeEvent !== null || + prevHoverEvent.networkMeasure !== null || prevHoverEvent.schedulingEvent !== null || + prevHoverEvent.snapshot !== null || prevHoverEvent.suspenseEvent !== null || prevHoverEvent.userTimingMark !== null ) { return { componentMeasure: null, - data: prevHoverEvent.data, flamechartStackFrame: null, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent: null, @@ -460,10 +500,10 @@ function AutoSizedCanvas({ if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame: null, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent: null, @@ -479,10 +519,10 @@ function AutoSizedCanvas({ if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame: null, measure: null, nativeEvent, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent: null, @@ -498,10 +538,10 @@ function AutoSizedCanvas({ if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame: null, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent, snapshot: null, suspenseEvent: null, @@ -517,10 +557,10 @@ function AutoSizedCanvas({ if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame: null, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent, @@ -536,10 +576,10 @@ function AutoSizedCanvas({ if (!hoveredEvent || hoveredEvent.measure !== measure) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame: null, measure, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent: null, @@ -558,10 +598,10 @@ function AutoSizedCanvas({ ) { setHoveredEvent({ componentMeasure, - data, flamechartStackFrame: null, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent: null, @@ -577,10 +617,10 @@ function AutoSizedCanvas({ if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame: null, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot, suspenseEvent: null, @@ -599,10 +639,10 @@ function AutoSizedCanvas({ ) { setHoveredEvent({ componentMeasure: null, - data, flamechartStackFrame, measure: null, nativeEvent: null, + networkMeasure: null, schedulingEvent: null, snapshot: null, suspenseEvent: null, @@ -611,53 +651,79 @@ function AutoSizedCanvas({ } }); } + + const {current: networkMeasuresView} = networkMeasuresViewRef; + if (networkMeasuresView) { + networkMeasuresView.onHover = networkMeasure => { + if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) { + setHoveredEvent({ + componentMeasure: null, + flamechartStackFrame: null, + measure: null, + nativeEvent: null, + networkMeasure, + schedulingEvent: null, + snapshot: null, + suspenseEvent: null, + userTimingMark: null, + }); + } + }; + } }, [ hoveredEvent, data, // Attach onHover callbacks when views are re-created on data change ]); useLayoutEffect(() => { - const {current: userTimingMarksView} = userTimingMarksViewRef; + const userTimingMarksView = userTimingMarksViewRef.current; if (userTimingMarksView) { userTimingMarksView.setHoveredMark( hoveredEvent ? hoveredEvent.userTimingMark : null, ); } - const {current: nativeEventsView} = nativeEventsViewRef; + const nativeEventsView = nativeEventsViewRef.current; if (nativeEventsView) { nativeEventsView.setHoveredEvent( hoveredEvent ? hoveredEvent.nativeEvent : null, ); } - const {current: schedulingEventsView} = schedulingEventsViewRef; + const schedulingEventsView = schedulingEventsViewRef.current; if (schedulingEventsView) { schedulingEventsView.setHoveredEvent( hoveredEvent ? hoveredEvent.schedulingEvent : null, ); } - const {current: suspenseEventsView} = suspenseEventsViewRef; + const suspenseEventsView = suspenseEventsViewRef.current; if (suspenseEventsView) { suspenseEventsView.setHoveredEvent( hoveredEvent ? hoveredEvent.suspenseEvent : null, ); } - const {current: reactMeasuresView} = reactMeasuresViewRef; + const reactMeasuresView = reactMeasuresViewRef.current; if (reactMeasuresView) { reactMeasuresView.setHoveredMeasure( hoveredEvent ? hoveredEvent.measure : null, ); } - const {current: flamechartView} = flamechartViewRef; + const flamechartView = flamechartViewRef.current; if (flamechartView) { flamechartView.setHoveredFlamechartStackFrame( hoveredEvent ? hoveredEvent.flamechartStackFrame : null, ); } + + const networkMeasuresView = networkMeasuresViewRef.current; + if (networkMeasuresView) { + networkMeasuresView.setHoveredEvent( + hoveredEvent ? hoveredEvent.networkMeasure : null, + ); + } }, [hoveredEvent]); // Draw to canvas in React's commit phase @@ -677,6 +743,7 @@ function AutoSizedCanvas({ componentMeasure, flamechartStackFrame, measure, + networkMeasure, schedulingEvent, suspenseEvent, } = contextData.hoveredEvent; @@ -689,6 +756,13 @@ function AutoSizedCanvas({ Copy component name )} + {networkMeasure !== null && ( + copy(networkMeasure.url)} + title="Copy URL"> + Copy URL + + )} {schedulingEvent !== null && ( copy(schedulingEvent.componentName)} diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css index af8c82e6308dc..5ed92c5462746 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -3,10 +3,10 @@ } .TooltipSection, -.TooltipWarningSection { +.TooltipWarningSection, +.SingleLineTextSection { display: block; border-radius: 0.125rem; - max-width: 300px; padding: 0.25rem; user-select: none; pointer-events: none; @@ -19,6 +19,13 @@ margin-top: 0.25rem; background-color: var(--color-warning-background); } +.TooltipSection, +.TooltipWarningSection { + max-width: 300px; +} +.SingleLineTextSection { + white-space: nowrap; +} .Divider { height: 1px; @@ -75,4 +82,8 @@ .Image { border: 1px solid var(--color-border); +} + +.DimText { + color: var(--color-dim); } \ 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 648bbcc56bb6c..84531e5b879c8 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -11,6 +11,7 @@ import type {Point} from './view-base'; import type { FlamechartStackFrame, NativeEvent, + NetworkMeasure, ReactComponentMeasure, ReactHoverContextInfo, ReactMeasure, @@ -29,6 +30,8 @@ import {getBatchRange} from './utils/getBatchRange'; import useSmartTooltip from './utils/useSmartTooltip'; import styles from './EventTooltip.css'; +const MAX_TOOLTIP_TEXT_LENGTH = 60; + type Props = {| canvasRef: {|current: HTMLCanvasElement | null|}, data: ReactProfilerData, @@ -87,6 +90,7 @@ export default function EventTooltip({ flamechartStackFrame, measure, nativeEvent, + networkMeasure, schedulingEvent, snapshot, suspenseEvent, @@ -104,6 +108,13 @@ export default function EventTooltip({ return ( ); + } else if (networkMeasure !== null) { + return ( + + ); } else if (schedulingEvent !== null) { return ( , +}) => { + const { + finishTimestamp, + lastReceivedDataTimestamp, + priority, + sendRequestTimestamp, + url, + } = networkMeasure; + + let urlToDisplay = url; + if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) { + const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2); + urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half); + } + + const timestampBegin = sendRequestTimestamp; + const timestampEnd = finishTimestamp || lastReceivedDataTimestamp; + const duration = + timestampEnd > 0 + ? formatDuration(finishTimestamp - timestampBegin) + : '(incomplete)'; + + return ( +
+
+ {duration} {priority}{' '} + {urlToDisplay} +
+
+ ); +}; + const TooltipSchedulingEvent = ({ data, schedulingEvent, diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js new file mode 100644 index 0000000000000..321bfcca1d5aa --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NetworkMeasuresView.js @@ -0,0 +1,337 @@ +/** + * 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 {NetworkMeasure, ReactProfilerData} from '../types'; +import type { + Interaction, + IntrinsicSize, + MouseMoveInteraction, + Rect, + ViewRefs, +} from '../view-base'; + +import { + durationToWidth, + positioningScaleFactor, + positionToTimestamp, + timestampToPosition, +} from './utils/positioning'; +import {drawText} from './utils/text'; +import {formatDuration} from '../utils/formatting'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import {BORDER_SIZE, COLORS, SUSPENSE_EVENT_HEIGHT} from './constants'; + +const HEIGHT = SUSPENSE_EVENT_HEIGHT; // TODO Constant name +const ROW_WITH_BORDER_HEIGHT = HEIGHT + BORDER_SIZE; + +const BASE_URL_REGEX = /([^:]+:\/\/[^\/]+)/; + +export class NetworkMeasuresView extends View { + _depthToNetworkMeasure: Map; + _hoveredNetworkMeasure: NetworkMeasure | null = null; + _intrinsicSize: IntrinsicSize; + _maxDepth: number = 0; + _profilerData: ReactProfilerData; + + onHover: ((event: NetworkMeasure | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + + this._profilerData = profilerData; + + this._performPreflightComputations(); + } + + _performPreflightComputations() { + this._depthToNetworkMeasure = new Map(); + + const {duration, networkMeasures} = this._profilerData; + + networkMeasures.forEach(event => { + const depth = event.depth; + + this._maxDepth = Math.max(this._maxDepth, depth); + + if (!this._depthToNetworkMeasure.has(depth)) { + this._depthToNetworkMeasure.set(depth, [event]); + } else { + // $FlowFixMe This is unnecessary. + this._depthToNetworkMeasure.get(depth).push(event); + } + }); + + this._intrinsicSize = { + width: duration, + height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT, + // Collapsed by default + maxInitialHeight: 0, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredEvent(networkMeasure: NetworkMeasure | null) { + if (this._hoveredNetworkMeasure === networkMeasure) { + return; + } + this._hoveredNetworkMeasure = networkMeasure; + this.setNeedsDisplay(); + } + + /** + * Draw a single `NetworkMeasure` as a box/span with text inside of it. + */ + _drawSingleNetworkMeasure( + context: CanvasRenderingContext2D, + networkMeasure: NetworkMeasure, + baseY: number, + scaleFactor: number, + showHoverHighlight: boolean, + ) { + const {frame, visibleArea} = this; + const { + depth, + finishTimestamp, + firstReceivedDataTimestamp, + lastReceivedDataTimestamp, + receiveResponseTimestamp, + sendRequestTimestamp, + url, + } = networkMeasure; + + // Account for requests that did not complete while we were profiling. + // As well as requests that did not receive data before finish (cached?). + const duration = this._profilerData.duration; + const timestampBegin = sendRequestTimestamp; + const timestampEnd = + finishTimestamp || lastReceivedDataTimestamp || duration; + const timestampMiddle = + receiveResponseTimestamp || firstReceivedDataTimestamp || timestampEnd; + + // Convert all timestamps to x coordinates. + const xStart = timestampToPosition(timestampBegin, scaleFactor, frame); + const xMiddle = timestampToPosition(timestampMiddle, scaleFactor, frame); + const xStop = timestampToPosition(timestampEnd, scaleFactor, frame); + + const width = durationToWidth(xStop - xStart, scaleFactor); + if (width < 1) { + return; // Too small to render at this zoom level + } + + baseY += depth * ROW_WITH_BORDER_HEIGHT; + + const outerRect: Rect = { + origin: { + x: xStart, + y: baseY, + }, + size: { + width: xStop - xStart, + height: HEIGHT, + }, + }; + if (!rectIntersectsRect(outerRect, visibleArea)) { + return; // Not in view + } + + // Draw the secondary rect first (since it also shows as a thin border around the primary rect). + let rect = { + origin: { + x: xStart, + y: baseY, + }, + size: { + width: xStop - xStart, + height: HEIGHT, + }, + }; + if (rectIntersectsRect(rect, visibleArea)) { + context.beginPath(); + context.fillStyle = + this._hoveredNetworkMeasure === networkMeasure + ? COLORS.NETWORK_SECONDARY_HOVER + : COLORS.NETWORK_SECONDARY; + context.fillRect( + rect.origin.x, + rect.origin.y, + rect.size.width, + rect.size.height, + ); + } + + rect = { + origin: { + x: xStart + BORDER_SIZE, + y: baseY + BORDER_SIZE, + }, + size: { + width: xMiddle - xStart - BORDER_SIZE, + height: HEIGHT - BORDER_SIZE * 2, + }, + }; + if (rectIntersectsRect(rect, visibleArea)) { + context.beginPath(); + context.fillStyle = + this._hoveredNetworkMeasure === networkMeasure + ? COLORS.NETWORK_PRIMARY_HOVER + : COLORS.NETWORK_PRIMARY; + context.fillRect( + rect.origin.x, + rect.origin.y, + rect.size.width, + rect.size.height, + ); + } + + const baseUrl = url.match(BASE_URL_REGEX); + const displayUrl = baseUrl !== null ? baseUrl[1] : url; + + const durationLabel = + finishTimestamp !== 0 + ? `${formatDuration(finishTimestamp - sendRequestTimestamp)} - ` + : ''; + + const label = durationLabel + displayUrl; + + drawText(label, context, outerRect, visibleArea); + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {networkMeasures}, + _hoveredNetworkMeasure, + visibleArea, + } = this; + + context.fillStyle = COLORS.PRIORITY_BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + networkMeasures.forEach(networkMeasure => { + this._drawSingleNetworkMeasure( + context, + networkMeasure, + frame.origin.y, + scaleFactor, + networkMeasure === _hoveredNetworkMeasure, + ); + }); + + // Render bottom borders. + for (let i = 0; i <= this._maxDepth; i++) { + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE, + }, + size: { + width: frame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects( + borderFrame, + visibleArea, + ); + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) { + const {frame, _intrinsicSize, onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + + const adjustedCanvasMouseY = location.y - frame.origin.y; + const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT); + const networkMeasuresAtDepth = this._depthToNetworkMeasure.get(depth); + + const duration = this._profilerData.duration; + + if (networkMeasuresAtDepth) { + // Find the event being hovered over. + for (let index = networkMeasuresAtDepth.length - 1; index >= 0; index--) { + const networkMeasure = networkMeasuresAtDepth[index]; + const { + finishTimestamp, + lastReceivedDataTimestamp, + sendRequestTimestamp, + } = networkMeasure; + + const timestampBegin = sendRequestTimestamp; + const timestampEnd = + finishTimestamp || lastReceivedDataTimestamp || duration; + + if ( + hoverTimestamp >= timestampBegin && + hoverTimestamp <= timestampEnd + ) { + this.currentCursor = 'context-menu'; + viewRefs.hoveredView = this; + onHover(networkMeasure); + return; + } + } + } + + if (viewRefs.hoveredView === this) { + viewRefs.hoveredView = null; + } + + 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 338ca1e5d163f..53b178d0a295e 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -46,6 +46,10 @@ export let COLORS = { BACKGROUND: '', NATIVE_EVENT: '', NATIVE_EVENT_HOVER: '', + NETWORK_PRIMARY: '', + NETWORK_PRIMARY_HOVER: '', + NETWORK_SECONDARY: '', + NETWORK_SECONDARY_HOVER: '', PRIORITY_BACKGROUND: '', PRIORITY_BORDER: '', PRIORITY_LABEL: '', @@ -106,6 +110,18 @@ export function updateColorsToMatchTheme(element: Element): boolean { NATIVE_EVENT_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-native-event-hover', ), + NETWORK_PRIMARY: computedStyle.getPropertyValue( + '--color-scheduling-profiler-network-primary', + ), + NETWORK_PRIMARY_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-network-primary-hover', + ), + NETWORK_SECONDARY: computedStyle.getPropertyValue( + '--color-scheduling-profiler-network-secondary', + ), + NETWORK_SECONDARY_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-network-secondary-hover', + ), PRIORITY_BACKGROUND: computedStyle.getPropertyValue( '--color-scheduling-profiler-priority-background', ), 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 91ab47bfd46ce..21c8cb570b35c 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/index.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/index.js @@ -10,6 +10,7 @@ export * from './ComponentMeasuresView'; export * from './FlamechartView'; export * from './NativeEventsView'; +export * from './NetworkMeasuresView'; export * from './ReactMeasuresView'; export * from './SchedulingEventsView'; export * from './SnapshotsView'; 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 9d6e56035ab88..a7545ef1b7a18 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -17,6 +17,7 @@ import type { Flamechart, Milliseconds, NativeEvent, + NetworkMeasure, Phase, ReactLane, ReactComponentMeasure, @@ -51,6 +52,7 @@ type ProcessorState = {| potentialSuspenseEventsOutsideOfTransition: Array< [SuspenseEvent, ReactLane[]], >, + requestIdToNetworkMeasureMap: Map, uidCounter: BatchUID, unresolvedSuspenseEvents: Map, |}; @@ -215,6 +217,205 @@ function throwIfIncomplete( } } +function processEventDispatch( + event: TimelineEvent, + timestamp: Milliseconds, + profilerData: ReactProfilerData, + state: ProcessorState, +) { + const data = event.args.data; + const type = data.type; + + if (type.startsWith('react-')) { + const stackTrace = data.stackTrace; + if (stackTrace) { + const topFrame = stackTrace[stackTrace.length - 1]; + if (topFrame.url.includes('/react-dom.')) { + // Filter out fake React events dispatched by invokeGuardedCallbackDev. + return; + } + } + } + + // Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant + if ( + type === 'blur' || + type === 'click' || + type === 'input' || + type.startsWith('focus') || + type.startsWith('key') || + type.startsWith('mouse') || + type.startsWith('pointer') + ) { + 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, + }; + + profilerData.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); + } +} + +function processResourceFinish( + event: TimelineEvent, + timestamp: Milliseconds, + profilerData: ReactProfilerData, + state: ProcessorState, +) { + const requestId = event.args.data.requestId; + const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId); + if (networkMeasure != null) { + networkMeasure.finishTimestamp = timestamp; + if (networkMeasure.firstReceivedDataTimestamp === 0) { + networkMeasure.firstReceivedDataTimestamp = timestamp; + } + if (networkMeasure.lastReceivedDataTimestamp === 0) { + networkMeasure.lastReceivedDataTimestamp = timestamp; + } + + // Clean up now that the resource is done. + state.requestIdToNetworkMeasureMap.delete(event.args.data.requestId); + } +} + +function processResourceReceivedData( + event: TimelineEvent, + timestamp: Milliseconds, + profilerData: ReactProfilerData, + state: ProcessorState, +) { + const requestId = event.args.data.requestId; + const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId); + if (networkMeasure != null) { + if (networkMeasure.firstReceivedDataTimestamp === 0) { + networkMeasure.firstReceivedDataTimestamp = timestamp; + } + networkMeasure.lastReceivedDataTimestamp = timestamp; + networkMeasure.finishTimestamp = timestamp; + } +} + +function processResourceReceiveResponse( + event: TimelineEvent, + timestamp: Milliseconds, + profilerData: ReactProfilerData, + state: ProcessorState, +) { + const requestId = event.args.data.requestId; + const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId); + if (networkMeasure != null) { + networkMeasure.receiveResponseTimestamp = timestamp; + } +} + +function processScreenshot( + event: TimelineEvent, + timestamp: Milliseconds, + profilerData: ReactProfilerData, + state: ProcessorState, +) { + const encodedSnapshot = event.args.snapshot; // Base 64 encoded + + const snapshot = { + height: 0, + image: null, + imageSource: `data:image/png;base64,${encodedSnapshot}`, + timestamp, + width: 0, + }; + + // Delay processing until we've extracted snapshot dimensions. + let resolveFn = ((null: any): Function); + state.asyncProcessingPromises.push( + new Promise(resolve => { + resolveFn = resolve; + }), + ); + + // Parse the Base64 image data to determine native size. + // This will be used later to scale for display within the thumbnail strip. + fetch(snapshot.imageSource) + .then(response => response.blob()) + .then(blob => { + // $FlowFixMe createImageBitmap + createImageBitmap(blob).then(bitmap => { + snapshot.height = bitmap.height; + snapshot.width = bitmap.width; + + resolveFn(); + }); + }); + + profilerData.snapshots.push(snapshot); +} + +function processResourceSendRequest( + event: TimelineEvent, + timestamp: Milliseconds, + profilerData: ReactProfilerData, + state: ProcessorState, +) { + const data = event.args.data; + const requestId = data.requestId; + + const availableDepths = new Array( + state.requestIdToNetworkMeasureMap.size + 1, + ).fill(true); + state.requestIdToNetworkMeasureMap.forEach(({depth}) => { + availableDepths[depth] = false; + }); + + let depth = 0; + for (let i = 0; i < availableDepths.length; i++) { + if (availableDepths[i]) { + depth = i; + break; + } + } + + const networkMeasure: NetworkMeasure = { + depth, + finishTimestamp: 0, + firstReceivedDataTimestamp: 0, + lastReceivedDataTimestamp: 0, + requestId, + requestMethod: data.requestMethod, + priority: data.priority, + sendRequestTimestamp: timestamp, + receiveResponseTimestamp: 0, + url: data.url, + }; + + state.requestIdToNetworkMeasureMap.set(requestId, networkMeasure); + + profilerData.networkMeasures.push(networkMeasure); + networkMeasure.sendRequestTimestamp = timestamp; +} + function processTimelineEvent( event: TimelineEvent, /** Finalized profiler data up to `event`. May be mutated. */ @@ -222,106 +423,49 @@ function processTimelineEvent( /** Intermediate processor state. May be mutated. */ state: ProcessorState, ) { - const {args, cat, name, ts, ph} = event; - switch (cat) { - case 'disabled-by-default-devtools.screenshot': - const encodedSnapshot = args.snapshot; // Base 64 encoded - - const snapshot = { - height: 0, - image: null, - imageSource: `data:image/png;base64,${encodedSnapshot}`, - timestamp: (ts - currentProfilerData.startTime) / 1000, - width: 0, - }; - - // Delay processing until we've extracted snapshot dimensions. - let resolveFn = ((null: any): Function); - state.asyncProcessingPromises.push( - new Promise(resolve => { - resolveFn = resolve; - }), - ); + const {cat, name, ts, ph} = event; - // Parse the Base64 image data to determine native size. - // This will be used later to scale for display within the thumbnail strip. - fetch(snapshot.imageSource) - .then(response => response.blob()) - .then(blob => { - // $FlowFixMe createImageBitmap - createImageBitmap(blob).then(bitmap => { - snapshot.height = bitmap.height; - snapshot.width = bitmap.width; - - resolveFn(); - }); - }); + const startTime = (ts - currentProfilerData.startTime) / 1000; - currentProfilerData.snapshots.push(snapshot); + switch (cat) { + case 'disabled-by-default-devtools.screenshot': + processScreenshot(event, startTime, currentProfilerData, state); break; case 'devtools.timeline': - if (name === 'EventDispatch') { - const type = args.data.type; - - if (type.startsWith('react-')) { - const stackTrace = args.data.stackTrace; - if (stackTrace) { - const topFrame = stackTrace[stackTrace.length - 1]; - if (topFrame.url.includes('/react-dom.')) { - // Filter out fake React events dispatched by invokeGuardedCallbackDev. - return; - } - } - } - - // Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant - if ( - type === 'blur' || - type === 'click' || - type === 'input' || - type.startsWith('focus') || - type.startsWith('key') || - 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, - }; - - 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); - } + switch (name) { + case 'EventDispatch': + processEventDispatch(event, startTime, currentProfilerData, state); + break; + case 'ResourceFinish': + processResourceFinish(event, startTime, currentProfilerData, state); + break; + case 'ResourceReceivedData': + processResourceReceivedData( + event, + startTime, + currentProfilerData, + state, + ); + break; + case 'ResourceReceiveResponse': + processResourceReceiveResponse( + event, + startTime, + currentProfilerData, + state, + ); + break; + case 'ResourceSendRequest': + processResourceSendRequest( + event, + startTime, + currentProfilerData, + state, + ); + break; } break; case 'blink.user_timing': - const startTime = (ts - currentProfilerData.startTime) / 1000; - if (name.startsWith('--react-version-')) { const [reactVersion] = name.substr(16).split('-'); currentProfilerData.reactVersion = reactVersion; @@ -714,6 +858,7 @@ export default async function preprocessData( laneToLabelMap: new Map(), laneToReactMeasureMap, nativeEvents: [], + networkMeasures: [], otherUserTimingMarks: [], reactVersion: null, schedulingEvents: [], @@ -759,6 +904,7 @@ export default async function preprocessData( potentialLongNestedUpdate: null, potentialLongNestedUpdates: [], potentialSuspenseEventsOutsideOfTransition: [], + requestIdToNetworkMeasureMap: new Map(), uidCounter: 0, unresolvedSuspenseEvents: new Map(), }; diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index 637d95714106f..073f4e1fbff10 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -91,6 +91,19 @@ export type ReactMeasure = {| +depth: number, |}; +export type NetworkMeasure = {| + +depth: number, + finishTimestamp: Milliseconds, + firstReceivedDataTimestamp: Milliseconds, + lastReceivedDataTimestamp: Milliseconds, + priority: string, + receiveResponseTimestamp: Milliseconds, + +requestId: string, + requestMethod: string, + sendRequestTimestamp: Milliseconds, + url: string, +|}; + export type ReactComponentMeasure = {| +componentName: string, duration: Milliseconds, @@ -155,6 +168,7 @@ export type ReactProfilerData = {| laneToLabelMap: Map, laneToReactMeasureMap: Map, nativeEvents: NativeEvent[], + networkMeasures: NetworkMeasure[], otherUserTimingMarks: UserTimingMark[], reactVersion: string | null, schedulingEvents: SchedulingEvent[], @@ -165,10 +179,10 @@ export type ReactProfilerData = {| export type ReactHoverContextInfo = {| componentMeasure: ReactComponentMeasure | null, - data: $ReadOnly | null, flamechartStackFrame: FlamechartStackFrame | null, measure: ReactMeasure | null, nativeEvent: NativeEvent | null, + networkMeasure: NetworkMeasure | null, schedulingEvent: SchedulingEvent | null, suspenseEvent: SuspenseEvent | null, snapshot: Snapshot | null, 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 6cdaff1c6e758..ccc9dc8c61aa0 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js @@ -7,6 +7,7 @@ * @flow */ +import type {ReactHoverContextInfo} from '../types'; import type {Interaction} from './useCanvasInteraction'; import type {Size} from './geometry'; @@ -47,6 +48,10 @@ const getCanvasContext = memoize( }, ); +type ResetHoveredEventFn = ( + partialState: $Shape, +) => void; + /** * Represents the canvas surface and a view heirarchy. A surface is also the * place where all interactions enter the view heirarchy. @@ -57,11 +62,17 @@ export class Surface { _context: ?CanvasRenderingContext2D; _canvasSize: ?Size; + _resetHoveredEvent: ResetHoveredEventFn; + _viewRefs: ViewRefs = { activeView: null, hoveredView: null, }; + constructor(resetHoveredEvent: ResetHoveredEventFn) { + this._resetHoveredEvent = resetHoveredEvent; + } + hasActiveView(): boolean { return this._viewRefs.activeView !== null; } @@ -107,12 +118,32 @@ export class Surface { } handleInteraction(interaction: Interaction) { - if (!this.rootView) { - return; + const rootView = this.rootView; + if (rootView != null) { + const viewRefs = this._viewRefs; + switch (interaction.type) { + case 'mousemove': + // Clean out the hovered view before processing a new mouse move interaction. + const hoveredView = viewRefs.hoveredView; + viewRefs.hoveredView = null; + + rootView.handleInteractionAndPropagateToSubviews( + interaction, + viewRefs, + ); + + // If a previously hovered view is no longer hovered, update the outer state. + if (hoveredView !== null && viewRefs.hoveredView === null) { + this._resetHoveredEvent({}); + } + break; + default: + rootView.handleInteractionAndPropagateToSubviews( + interaction, + viewRefs, + ); + break; + } } - this.rootView.handleInteractionAndPropagateToSubviews( - interaction, - this._viewRefs, - ); } } diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 2476ce32839d3..04962c204e287 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -141,6 +141,10 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-resize-bar-dot': '#333333', '--color-scheduling-profiler-native-event': '#ccc', '--color-scheduling-profiler-native-event-hover': '#aaa', + '--color-scheduling-profiler-network-primary': '#fcf3dc', + '--color-scheduling-profiler-network-primary-hover': '#f0e7d1', + '--color-scheduling-profiler-network-secondary': '#efc457', + '--color-scheduling-profiler-network-secondary-hover': '#e3ba52', '--color-scheduling-profiler-priority-background': '#f6f6f6', '--color-scheduling-profiler-priority-border': '#eeeeee', '--color-scheduling-profiler-user-timing': '#c9cacd', @@ -276,6 +280,10 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-resize-bar-dot': '#cfd1d5', '--color-scheduling-profiler-native-event': '#b2b2b2', '--color-scheduling-profiler-native-event-hover': '#949494', + '--color-scheduling-profiler-network-primary': '#fcf3dc', + '--color-scheduling-profiler-network-primary-hover': '#e3dbc5', + '--color-scheduling-profiler-network-secondary': '#efc457', + '--color-scheduling-profiler-network-secondary-hover': '#d6af4d', '--color-scheduling-profiler-priority-background': '#1d2129', '--color-scheduling-profiler-priority-border': '#282c34', '--color-scheduling-profiler-user-timing': '#c9cacd', From 033f932a763584e6d7e8b01b0927284d9c3c4cca Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 17 Aug 2021 16:02:24 -0700 Subject: [PATCH 4/5] Updated scheduling profiler test snapshots --- .../import-worker/__tests__/preprocessData-test.internal.js | 4 ++++ 1 file changed, 4 insertions(+) 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 f5267e8d06aa1..3481068f471b2 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 @@ -324,6 +324,7 @@ describe('preprocessData', () => { 30 => Array [], }, "nativeEvents": Array [], + "networkMeasures": Array [], "otherUserTimingMarks": Array [], "reactVersion": "17.0.3", "schedulingEvents": Array [], @@ -530,6 +531,7 @@ describe('preprocessData', () => { 30 => Array [], }, "nativeEvents": Array [], + "networkMeasures": Array [], "otherUserTimingMarks": Array [], "reactVersion": "17.0.3", "schedulingEvents": Array [ @@ -715,6 +717,7 @@ describe('preprocessData', () => { 30 => Array [], }, "nativeEvents": Array [], + "networkMeasures": Array [], "otherUserTimingMarks": Array [ Object { "name": "__v3", @@ -1051,6 +1054,7 @@ describe('preprocessData', () => { 30 => Array [], }, "nativeEvents": Array [], + "networkMeasures": Array [], "otherUserTimingMarks": Array [ Object { "name": "__v3", From 59a37bff01d8857b8344d7dea66395d9c5a14b1a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 17 Aug 2021 16:13:58 -0700 Subject: [PATCH 5/5] Adjust canvas borders to account for high DPI screens --- .../src/content-views/constants.js | 3 ++- .../src/view-base/Surface.js | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) 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 53b178d0a295e..48ba22aed30ae 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -7,6 +7,7 @@ * @flow */ +export const DPR: number = window.devicePixelRatio || 1; export const LABEL_SIZE = 80; export const MARKER_HEIGHT = 20; export const MARKER_TICK_HEIGHT = 8; @@ -20,7 +21,7 @@ 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 = 14; -export const BORDER_SIZE = 1; +export const BORDER_SIZE = 1 / DPR; export const FLAMECHART_FRAME_HEIGHT = 14; export const TEXT_PADDING = 3; export const SNAPSHOT_HEIGHT = 35; 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 ccc9dc8c61aa0..2a5774db3197b 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/Surface.js @@ -15,6 +15,7 @@ import memoize from 'memoize-one'; import {View} from './View'; import {zeroPoint} from './geometry'; +import {DPR} from '../content-views/constants'; export type ViewRefs = {| activeView: View | null, @@ -23,12 +24,10 @@ export type ViewRefs = {| // hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/ function configureRetinaCanvas(canvas, height, width) { - const dpr: number = window.devicePixelRatio || 1; - canvas.width = width * dpr; - canvas.height = height * dpr; + canvas.width = width * DPR; + canvas.height = height * DPR; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; - return dpr; } const getCanvasContext = memoize( @@ -40,9 +39,10 @@ const getCanvasContext = memoize( ): CanvasRenderingContext2D => { const context = canvas.getContext('2d', {alpha: false}); if (scaleCanvas) { - const dpr = configureRetinaCanvas(canvas, height, width); + configureRetinaCanvas(canvas, height, width); + // Scale all drawing operations by the dpr, so you don't have to worry about the difference. - context.scale(dpr, dpr); + context.scale(DPR, DPR); } return context; },