diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 05700d6431d01..e699959013b7d 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -44,6 +44,7 @@ import { zeroPoint, } from './view-base'; import { + ComponentMeasuresView, FlamechartView, NativeEventsView, ReactMeasuresView, @@ -132,6 +133,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { const nativeEventsViewRef = useRef(null); const schedulingEventsViewRef = useRef(null); const suspenseEventsViewRef = useRef(null); + const componentMeasuresViewRef = useRef(null); const reactMeasuresViewRef = useRef(null); const flamechartViewRef = useRef(null); const syncedHorizontalPanAndZoomViewsRef = useRef( @@ -259,6 +261,17 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { true, ); + let componentMeasuresViewWrapper = null; + if (data.componentMeasures.length > 0) { + const componentMeasuresView = new ComponentMeasuresView( + surface, + defaultFrame, + data, + ); + componentMeasuresViewRef.current = componentMeasuresView; + componentMeasuresViewWrapper = createViewHelper(componentMeasuresView); + } + const flamechartView = new FlamechartView( surface, defaultFrame, @@ -293,6 +306,9 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { rootView.addSubview(suspenseEventsViewWrapper); } rootView.addSubview(reactMeasuresViewWrapper); + if (componentMeasuresViewWrapper !== null) { + rootView.addSubview(componentMeasuresViewWrapper); + } rootView.addSubview(flamechartViewWrapper); // If subviews are less than the available height, fill remaining height with a solid color. @@ -323,6 +339,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { if (prevHoverEvent === null) { return prevHoverEvent; } else if ( + prevHoverEvent.componentMeasure !== null || prevHoverEvent.flamechartStackFrame !== null || prevHoverEvent.measure !== null || prevHoverEvent.nativeEvent !== null || @@ -331,6 +348,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { prevHoverEvent.userTimingMark !== null ) { return { + componentMeasure: null, data: prevHoverEvent.data, flamechartStackFrame: null, measure: null, @@ -378,6 +396,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { userTimingMarksView.onHover = userTimingMark => { if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) { setHoveredEvent({ + componentMeasure: null, data, flamechartStackFrame: null, measure: null, @@ -395,6 +414,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { nativeEventsView.onHover = nativeEvent => { if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) { setHoveredEvent({ + componentMeasure: null, data, flamechartStackFrame: null, measure: null, @@ -412,6 +432,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { schedulingEventsView.onHover = schedulingEvent => { if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) { setHoveredEvent({ + componentMeasure: null, data, flamechartStackFrame: null, measure: null, @@ -429,6 +450,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { suspenseEventsView.onHover = suspenseEvent => { if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) { setHoveredEvent({ + componentMeasure: null, data, flamechartStackFrame: null, measure: null, @@ -446,6 +468,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { reactMeasuresView.onHover = measure => { if (!hoveredEvent || hoveredEvent.measure !== measure) { setHoveredEvent({ + componentMeasure: null, data, flamechartStackFrame: null, measure, @@ -458,6 +481,27 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { }; } + const {current: componentMeasuresView} = componentMeasuresViewRef; + if (componentMeasuresView) { + componentMeasuresView.onHover = componentMeasure => { + if ( + !hoveredEvent || + hoveredEvent.componentMeasure !== componentMeasure + ) { + setHoveredEvent({ + componentMeasure, + data, + flamechartStackFrame: null, + measure: null, + nativeEvent: null, + schedulingEvent: null, + suspenseEvent: null, + userTimingMark: null, + }); + } + }; + } + const {current: flamechartView} = flamechartViewRef; if (flamechartView) { flamechartView.setOnHover(flamechartStackFrame => { @@ -466,6 +510,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { hoveredEvent.flamechartStackFrame !== flamechartStackFrame ) { setHoveredEvent({ + componentMeasure: null, data, flamechartStackFrame, measure: null, @@ -540,6 +585,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { return null; } const { + componentMeasure, flamechartStackFrame, measure, schedulingEvent, @@ -547,6 +593,13 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { } = contextData.hoveredEvent; return ( + {componentMeasure !== null && ( + copy(componentMeasure.componentName)} + title="Copy component name"> + Copy component name + + )} {schedulingEvent !== null && ( copy(schedulingEvent.componentName)} diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index d6019cb254434..80935bd69be6a 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, + ReactComponentMeasure, ReactHoverContextInfo, ReactMeasure, ReactProfilerData, @@ -81,6 +82,7 @@ export default function EventTooltip({ } const { + componentMeasure, flamechartStackFrame, measure, nativeEvent, @@ -89,7 +91,14 @@ export default function EventTooltip({ userTimingMark, } = hoveredEvent; - if (nativeEvent !== null) { + if (componentMeasure !== null) { + return ( + + ); + } else if (nativeEvent !== null) { return ( ); @@ -130,6 +139,38 @@ export default function EventTooltip({ return null; } +const TooltipReactComponentMeasure = ({ + componentMeasure, + tooltipRef, +}: { + componentMeasure: ReactComponentMeasure, + tooltipRef: Return, +}) => { + const {componentName, duration, timestamp, warning} = componentMeasure; + + const label = `${componentName} rendered`; + + return ( +
+
+ {trimString(label, 768)} +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
Duration:
+
{formatDuration(duration)}
+
+
+ {warning !== null && ( +
+
{warning}
+
+ )} +
+ ); +}; + const TooltipFlamechartNode = ({ stackFrame, tooltipRef, diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js index e6638ce728d82..dc95fc31a9da6 100644 --- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js +++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js @@ -14,6 +14,7 @@ import createDataResourceFromImportedFile from './createDataResourceFromImported import type {DataResource} from './createDataResourceFromImportedFile'; export type Context = {| + clearSchedulingProfilerData: () => void, importSchedulingProfilerData: (file: File) => void, schedulingProfilerData: DataResource | null, |}; @@ -33,6 +34,10 @@ function SchedulingProfilerContextController({children}: Props) { setSchedulingProfilerData, ] = useState(null); + const clearSchedulingProfilerData = useCallback(() => { + setSchedulingProfilerData(null); + }, []); + const importSchedulingProfilerData = useCallback((file: File) => { setSchedulingProfilerData(createDataResourceFromImportedFile(file)); }, []); @@ -41,11 +46,13 @@ function SchedulingProfilerContextController({children}: Props) { const value = useMemo( () => ({ + clearSchedulingProfilerData, importSchedulingProfilerData, schedulingProfilerData, // TODO (scheduling profiler) }), [ + clearSchedulingProfilerData, importSchedulingProfilerData, schedulingProfilerData, // TODO (scheduling profiler) diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js new file mode 100644 index 0000000000000..4d9ecfa5d17ee --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js @@ -0,0 +1,222 @@ +/** + * 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 {ReactComponentMeasure, 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 {COLORS, NATIVE_EVENT_HEIGHT, BORDER_SIZE} from './constants'; + +const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE; + +export class ComponentMeasuresView extends View { + _hoveredComponentMeasure: ReactComponentMeasure | null = null; + _intrinsicSize: IntrinsicSize; + _profilerData: ReactProfilerData; + + onHover: ((event: ReactComponentMeasure | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + + this._profilerData = profilerData; + + this._intrinsicSize = { + width: profilerData.duration, + height: ROW_WITH_BORDER_HEIGHT, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredEvent(hoveredEvent: ReactComponentMeasure | null) { + if (this._hoveredComponentMeasure === hoveredEvent) { + return; + } + this._hoveredComponentMeasure = hoveredEvent; + this.setNeedsDisplay(); + } + + /** + * Draw a single `ReactComponentMeasure` as a box/span with text inside of it. + */ + _drawSingleReactComponentMeasure( + context: CanvasRenderingContext2D, + rect: Rect, + componentMeasure: ReactComponentMeasure, + scaleFactor: number, + showHoverHighlight: boolean, + ): boolean { + const {frame} = this; + const {componentName, duration, timestamp, warning} = componentMeasure; + + const xStart = timestampToPosition(timestamp, scaleFactor, frame); + const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame); + const componentMeasureRect: Rect = { + origin: { + x: xStart, + y: frame.origin.y, + }, + size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT}, + }; + if (!rectIntersectsRect(componentMeasureRect, rect)) { + return false; // Not in view + } + + const width = durationToWidth(duration, scaleFactor); + if (width < 1) { + return false; // Too small to render at this zoom level + } + + const drawableRect = intersectionOfRects(componentMeasureRect, rect); + context.beginPath(); + if (warning !== null) { + context.fillStyle = showHoverHighlight + ? COLORS.WARNING_BACKGROUND_HOVER + : COLORS.WARNING_BACKGROUND; + } else { + context.fillStyle = showHoverHighlight + ? COLORS.REACT_COMPONENT_MEASURE_HOVER + : COLORS.REACT_COMPONENT_MEASURE; + } + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + + const label = `${componentName} rendered - ${formatDuration(duration)}`; + + drawText(label, context, componentMeasureRect, drawableRect); + + return true; + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {componentMeasures}, + _hoveredComponentMeasure, + 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, + ); + + let didDrawMeasure = false; + componentMeasures.forEach(componentMeasure => { + didDrawMeasure = + this._drawSingleReactComponentMeasure( + context, + visibleArea, + componentMeasure, + scaleFactor, + componentMeasure === _hoveredComponentMeasure, + ) || didDrawMeasure; + }); + + if (!didDrawMeasure) { + drawText( + '(zoom or pan to see React components)', + context, + visibleArea, + visibleArea, + 'center', + COLORS.TEXT_DIM_COLOR, + ); + } + + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y + ROW_WITH_BORDER_HEIGHT - BORDER_SIZE, + visibleArea.size.width, + BORDER_SIZE, + ); + } + + /** + * @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 componentMeasures = this._profilerData.componentMeasures; + for (let index = componentMeasures.length - 1; index >= 0; index--) { + const componentMeasure = componentMeasures[index]; + const {duration, timestamp} = componentMeasure; + + if ( + hoverTimestamp >= timestamp && + hoverTimestamp <= timestamp + duration + ) { + this.currentCursor = 'context-menu'; + viewRefs.hoveredView = this; + onHover(componentMeasure); + 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/FlamechartView.js b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js index 57cd8ae1f30c4..86f411a5b6e24 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js @@ -128,7 +128,7 @@ class FlamechartStackLayerView extends View { visibleArea, } = this; - context.fillStyle = COLORS.BACKGROUND; + context.fillStyle = COLORS.PRIORITY_BACKGROUND; context.fillRect( visibleArea.origin.x, visibleArea.origin.y, @@ -172,7 +172,29 @@ class FlamechartStackLayerView extends View { drawableRect.size.height, ); - drawText(name, context, nodeRect, drawableRect, width); + drawText(name, context, nodeRect, drawableRect); + } + + // Render bottom border. + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + FLAMECHART_FRAME_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, + ); } } 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 34424e7b5b81d..29e2535de6ab9 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -143,7 +143,7 @@ export class NativeEventsView extends View { const label = `${type} - ${formatDuration(duration)}`; - drawText(label, context, eventRect, drawableRect, width); + drawText(label, context, eventRect, drawableRect); } 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 5b0d0f2d51c38..db234e4cebecf 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js @@ -231,7 +231,7 @@ export class SuspenseEventsView extends View { label += ` - ${formatDuration(duration)}`; } - drawText(label, context, eventRect, drawableRect, width); + drawText(label, context, eventRect, drawableRect); } } 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 214cf26d7bfac..ec8fec8d1e461 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -50,6 +50,8 @@ export let COLORS = { PRIORITY_LABEL: '', USER_TIMING: '', USER_TIMING_HOVER: '', + REACT_COMPONENT_MEASURE: '', + REACT_COMPONENT_MEASURE_HOVER: '', REACT_IDLE: '', REACT_IDLE_HOVER: '', REACT_RENDER: '', @@ -75,6 +77,7 @@ export let COLORS = { REACT_WORK_BORDER: '', SCROLL_CARET: '', TEXT_COLOR: '', + TEXT_DIM_COLOR: '', TIME_MARKER_LABEL: '', WARNING_BACKGROUND: '', WARNING_BACKGROUND_HOVER: '', @@ -111,6 +114,12 @@ export function updateColorsToMatchTheme(element: Element): boolean { USER_TIMING_HOVER: computedStyle.getPropertyValue( '--color-scheduling-profiler-user-timing-hover', ), + REACT_COMPONENT_MEASURE: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render', + ), + REACT_COMPONENT_MEASURE_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-react-render-hover', + ), REACT_IDLE: computedStyle.getPropertyValue( '--color-scheduling-profiler-react-idle', ), @@ -182,6 +191,9 @@ export function updateColorsToMatchTheme(element: Element): boolean { TEXT_COLOR: computedStyle.getPropertyValue( '--color-scheduling-profiler-text-color', ), + TEXT_DIM_COLOR: computedStyle.getPropertyValue( + '--color-scheduling-profiler-text-dim-color', + ), TIME_MARKER_LABEL: computedStyle.getPropertyValue('--color-text'), WARNING_BACKGROUND: computedStyle.getPropertyValue( '--color-warning-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 588f3e5d7bd90..fc1f4eabd4229 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/index.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/index.js @@ -7,6 +7,7 @@ * @flow */ +export * from './ComponentMeasuresView'; export * from './FlamechartView'; export * from './NativeEventsView'; export * from './ReactMeasuresView'; 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 da14733bce62c..229b9f50f8dcd 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 @@ -41,11 +41,10 @@ export function drawText( context: CanvasRenderingContext2D, fullRect: Rect, drawableRect: Rect, - availableWidth: number, textAlign: 'left' | 'center' = 'left', fillStyle: string = COLORS.TEXT_COLOR, ): void { - if (availableWidth > TEXT_PADDING * 2) { + if (fullRect.size.width > TEXT_PADDING * 2) { context.textAlign = textAlign; context.textBaseline = 'middle'; context.font = `${FONT_SIZE}px sans-serif`; @@ -55,7 +54,7 @@ export function drawText( const trimmedName = trimText( context, text, - availableWidth - TEXT_PADDING * 2 + (x < 0 ? x : 0), + fullRect.size.width - TEXT_PADDING * 2 + (x < 0 ? x : 0), ); if (trimmedName !== null) { @@ -81,7 +80,7 @@ export function drawText( let textX; if (textAlign === 'center') { - textX = x + availableWidth / 2 + TEXT_PADDING - (x < 0 ? x : 0); + textX = x + fullRect.size.width / 2 + TEXT_PADDING - (x < 0 ? x : 0); } else { textX = x + TEXT_PADDING - (x < 0 ? x : 0); } 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 bb10bca4d8552..f47200c6f0e21 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 @@ -203,6 +203,7 @@ describe(preprocessData, () => { }); expect(preprocessData([cpuProfilerSample, randomSample])).toStrictEqual({ + componentMeasures: [], duration: 0.002, flamechart: [], measures: [], @@ -259,6 +260,7 @@ describe(preprocessData, () => { }), ]), ).toStrictEqual({ + componentMeasures: [], duration: 0.008, flamechart: [], measures: [ @@ -323,6 +325,7 @@ describe(preprocessData, () => { const userTimingData = createUserTimingData(clearedMarks); expect(preprocessData(userTimingData)).toStrictEqual({ + componentMeasures: [], duration: 0.011, flamechart: [], measures: [ @@ -405,13 +408,27 @@ describe(preprocessData, () => { const userTimingData = createUserTimingData(clearedMarks); expect(preprocessData(userTimingData)).toStrictEqual({ - duration: 0.022, + componentMeasures: [ + { + componentName: 'App', + duration: 0.001, + timestamp: 0.007, + warning: null, + }, + { + componentName: 'App', + duration: 0.0010000000000000009, + timestamp: 0.018, + warning: null, + }, + ], + duration: 0.026, flamechart: [], measures: [ { batchUID: 0, depth: 0, - duration: 0.004999999999999999, + duration: 0.006999999999999999, laneLabels: ['Default'], lanes: [4], timestamp: 0.006, @@ -420,7 +437,7 @@ describe(preprocessData, () => { { batchUID: 0, depth: 0, - duration: 0.001, + duration: 0.002999999999999999, laneLabels: ['Default'], lanes: [4], timestamp: 0.006, @@ -432,7 +449,7 @@ describe(preprocessData, () => { duration: 0.002999999999999999, laneLabels: ['Default'], lanes: [4], - timestamp: 0.008, + timestamp: 0.01, type: 'commit', }, { @@ -441,7 +458,7 @@ describe(preprocessData, () => { duration: 0.0010000000000000009, laneLabels: ['Default'], lanes: [4], - timestamp: 0.009, + timestamp: 0.011, type: 'layout-effects', }, { @@ -450,25 +467,25 @@ describe(preprocessData, () => { duration: 0.002, laneLabels: ['Default'], lanes: [4], - timestamp: 0.012, + timestamp: 0.014, type: 'passive-effects', }, { batchUID: 1, depth: 0, - duration: 0.005000000000000001, + duration: 0.006999999999999999, laneLabels: ['Default'], lanes: [4], - timestamp: 0.015, + timestamp: 0.017, type: 'render-idle', }, { batchUID: 1, depth: 0, - duration: 0.0010000000000000009, + duration: 0.002999999999999999, laneLabels: ['Default'], lanes: [4], - timestamp: 0.015, + timestamp: 0.017, type: 'render', }, { @@ -477,7 +494,7 @@ describe(preprocessData, () => { duration: 0.002999999999999999, laneLabels: ['Default'], lanes: [4], - timestamp: 0.017, + timestamp: 0.021, type: 'commit', }, { @@ -486,7 +503,7 @@ describe(preprocessData, () => { duration: 0.0010000000000000009, laneLabels: ['Default'], lanes: [4], - timestamp: 0.018, + timestamp: 0.022, type: 'layout-effects', }, { @@ -495,7 +512,7 @@ describe(preprocessData, () => { duration: 0.0009999999999999974, laneLabels: ['Default'], lanes: [4], - timestamp: 0.021, + timestamp: 0.025, type: 'passive-effects', }, ], @@ -522,7 +539,7 @@ describe(preprocessData, () => { componentName: 'App', laneLabels: ['Default'], lanes: [4], - timestamp: 0.013, + timestamp: 0.015, type: 'schedule-state-update', warning: null, }, 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 a365be120897c..d3d47e111bcaa 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -18,6 +18,7 @@ import type { Flamechart, NativeEvent, ReactLane, + ReactComponentMeasure, ReactMeasureType, ReactProfilerData, SuspenseEvent, @@ -36,6 +37,7 @@ type MeasureStackElement = {| type ProcessorState = {| batchUID: BatchUID, + currentReactComponentMeasure: ReactComponentMeasure | null, measureStack: MeasureStackElement[], nativeEventStack: NativeEvent[], nextRenderShouldGenerateNewBatchID: boolean, @@ -240,8 +242,32 @@ function processTimelineEvent( case 'blink.user_timing': const startTime = (ts - currentProfilerData.startTime) / 1000; - // React Events - schedule - if (name.startsWith('--schedule-render-')) { + if (name.startsWith('--component-render-start-')) { + const [componentName] = name.substr(25).split('-'); + + if (state.currentReactComponentMeasure !== null) { + console.error( + 'Render started while another render in progress:', + state.currentReactComponentMeasure, + ); + } + + state.currentReactComponentMeasure = { + componentName, + timestamp: startTime, + duration: 0, + warning: null, + }; + } else if (name === '--component-render-stop') { + if (state.currentReactComponentMeasure !== null) { + const componentMeasure = state.currentReactComponentMeasure; + componentMeasure.duration = startTime - componentMeasure.timestamp; + + state.currentReactComponentMeasure = null; + + currentProfilerData.componentMeasures.push(componentMeasure); + } + } else if (name.startsWith('--schedule-render-')) { const [laneBitmaskString, laneLabels] = name.substr(18).split('-'); currentProfilerData.schedulingEvents.push({ type: 'schedule-render', @@ -581,6 +607,7 @@ export default function preprocessData( const flamechart = preprocessFlamechart(timeline); const profilerData: ReactProfilerData = { + componentMeasures: [], duration: 0, flamechart, measures: [], @@ -619,6 +646,7 @@ export default function preprocessData( const state: ProcessorState = { batchUID: 0, + currentReactComponentMeasure: null, measureStack: [], nativeEventStack: [], nextRenderShouldGenerateNewBatchID: true, diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index 674dabdc8e93a..106e4048363a5 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -91,6 +91,13 @@ export type ReactMeasure = {| +depth: number, |}; +export type ReactComponentMeasure = {| + +componentName: string, + duration: Milliseconds, + +timestamp: Milliseconds, + warning: string | null, +|}; + /** * A flamechart stack frame belonging to a stack trace. */ @@ -117,6 +124,7 @@ export type FlamechartStackLayer = FlamechartStackFrame[]; export type Flamechart = FlamechartStackLayer[]; export type ReactProfilerData = {| + componentMeasures: ReactComponentMeasure[], duration: number, flamechart: Flamechart, measures: ReactMeasure[], @@ -128,6 +136,7 @@ export type ReactProfilerData = {| |}; export type ReactHoverContextInfo = {| + componentMeasure: ReactComponentMeasure | null, data: $ReadOnly | null, flamechartStackFrame: FlamechartStackFrame | null, measure: ReactMeasure | null, 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 80527a193cb59..1b99d20a54c3a 100644 --- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js +++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js @@ -110,7 +110,6 @@ class ResizeBar extends View { context, labelRect, drawableRect, - visibleArea.size.width, 'center', COLORS.REACT_RESIZE_BAR_DOT, ); diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index a4b742a3fb64a..8afcf85d41519 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -164,6 +164,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd', '--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a', '--color-scheduling-profiler-text-color': '#000000', + '--color-scheduling-profiler-text-dim-color': '#ccc', '--color-scheduling-profiler-react-work-border': '#ffffff', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', @@ -293,7 +294,8 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = { '--color-scheduling-profiler-react-suspense-resolved-hover': '#89d281', '--color-scheduling-profiler-react-suspense-unresolved': '#c9cacd', '--color-scheduling-profiler-react-suspense-unresolved-hover': '#93959a', - '--color-scheduling-profiler-text-color': '#000000', + '--color-scheduling-profiler-text-color': '#282c34', + '--color-scheduling-profiler-text-dim-color': '#555b66', '--color-scheduling-profiler-react-work-border': '#ffffff', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js index f07d4e7d778c8..a6abd38712f0e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ClearProfilingDataButton.js @@ -8,22 +8,41 @@ */ import * as React from 'react'; -import {useCallback, useContext} from 'react'; +import {useContext} from 'react'; import {ProfilerContext} from './ProfilerContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import {StoreContext} from '../context'; +import {SchedulingProfilerContext} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; export default function ClearProfilingDataButton() { const store = useContext(StoreContext); - const {didRecordCommits, isProfiling} = useContext(ProfilerContext); + const {didRecordCommits, isProfiling, selectedTabID} = useContext( + ProfilerContext, + ); + const {clearSchedulingProfilerData, schedulingProfilerData} = useContext( + SchedulingProfilerContext, + ); const {profilerStore} = store; - const clear = useCallback(() => profilerStore.clear(), [profilerStore]); + let doesHaveData = false; + if (selectedTabID === 'scheduling-profiler') { + doesHaveData = schedulingProfilerData !== null; + } else { + doesHaveData = didRecordCommits; + } + + const clear = () => { + if (selectedTabID === 'scheduling-profiler') { + clearSchedulingProfilerData(); + } else { + profilerStore.clear(); + } + }; return (