From d49811cdd36dcc65268d00e4d442d038b6b852b9 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 23 Jul 2021 16:48:40 -0400 Subject: [PATCH 1/3] Add native events to the profiler --- .../src/CanvasPage.js | 59 +- .../src/EventTooltip.css | 3 +- .../src/EventTooltip.js | 59 +- .../src/content-views/NativeEventsView.js | 224 ++++++++ .../src/content-views/ReactEventsView.js | 12 +- .../src/content-views/constants.js | 16 +- .../src/content-views/index.js | 1 + .../src/import-worker/preprocessData.js | 507 +++++++++--------- .../src/types.js | 12 +- .../views/Settings/SettingsContext.js | 10 + .../src/devtools/views/root.css | 8 +- 11 files changed, 632 insertions(+), 279 deletions(-) create mode 100644 packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js index 74f1866c20dee..bbf98acb265d4 100644 --- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js +++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js @@ -44,6 +44,7 @@ import { } from './view-base'; import { FlamechartView, + NativeEventsView, ReactEventsView, ReactMeasuresView, TimeAxisMarkersView, @@ -126,6 +127,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { const surfaceRef = useRef(new Surface()); const userTimingMarksViewRef = useRef(null); + const nativeEventsViewRef = useRef(null); const reactEventsViewRef = useRef(null); const reactMeasuresViewRef = useRef(null); const flamechartViewRef = useRef(null); @@ -176,6 +178,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { topContentStack.addSubview(userTimingMarksView); } + const nativeEventsView = new NativeEventsView(surface, defaultFrame, data); + nativeEventsViewRef.current = nativeEventsView; + topContentStack.addSubview(nativeEventsView); + const reactEventsView = new ReactEventsView(surface, defaultFrame, data); reactEventsViewRef.current = reactEventsView; topContentStack.addSubview(reactEventsView); @@ -299,7 +305,24 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) { setHoveredEvent({ userTimingMark, - event: null, + nativeEvent: null, + reactEvent: null, + flamechartStackFrame: null, + measure: null, + data, + }); + } + }; + } + + const {current: nativeEventsView} = nativeEventsViewRef; + if (nativeEventsView) { + nativeEventsView.onHover = nativeEvent => { + if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) { + setHoveredEvent({ + userTimingMark: null, + nativeEvent, + reactEvent: null, flamechartStackFrame: null, measure: null, data, @@ -310,11 +333,12 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { const {current: reactEventsView} = reactEventsViewRef; if (reactEventsView) { - reactEventsView.onHover = event => { - if (!hoveredEvent || hoveredEvent.event !== event) { + reactEventsView.onHover = reactEvent => { + if (!hoveredEvent || hoveredEvent.reactEvent !== reactEvent) { setHoveredEvent({ userTimingMark: null, - event, + nativeEvent: null, + reactEvent, flamechartStackFrame: null, measure: null, data, @@ -329,7 +353,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { if (!hoveredEvent || hoveredEvent.measure !== measure) { setHoveredEvent({ userTimingMark: null, - event: null, + nativeEvent: null, + reactEvent: null, flamechartStackFrame: null, measure, data, @@ -347,7 +372,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { ) { setHoveredEvent({ userTimingMark: null, - event: null, + nativeEvent: null, + reactEvent: null, flamechartStackFrame, measure: null, data, @@ -368,9 +394,18 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { ); } + const {current: nativeEventsView} = nativeEventsViewRef; + if (nativeEventsView) { + nativeEventsView.setHoveredEvent( + hoveredEvent ? hoveredEvent.nativeEvent : null, + ); + } + const {current: reactEventsView} = reactEventsViewRef; if (reactEventsView) { - reactEventsView.setHoveredEvent(hoveredEvent ? hoveredEvent.event : null); + reactEventsView.setHoveredEvent( + hoveredEvent ? hoveredEvent.reactEvent : null, + ); } const {current: reactMeasuresView} = reactMeasuresViewRef; @@ -402,22 +437,22 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) { return null; } const { - event, + reactEvent, flamechartStackFrame, measure, } = contextData.hoveredEvent; return ( - {event !== null && ( + {reactEvent !== null && ( copy(event.componentName)} + onClick={() => copy(reactEvent.componentName)} title="Copy component name"> Copy component name )} - {event !== null && event.componentStack && ( + {reactEvent !== null && reactEvent.componentStack && ( copy(event.componentStack)} + onClick={() => copy(reactEvent.componentStack)} title="Copy component stack"> Copy component stack diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css index b6503b7338c35..91e60bf13cfd2 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css @@ -29,6 +29,7 @@ .DetailsGridLabel { color: var(--color-dim); text-align: right; + white-space: nowrap; } .DetailsGridURL { @@ -45,7 +46,7 @@ .ComponentName { font-weight: bold; word-break: break-word; - margin-right: 0.4rem; + margin-right: 0.25rem; } .ComponentStack { diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js index 608309c4a3caa..ee77c85cc8dad 100644 --- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js +++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js @@ -10,6 +10,7 @@ import type {Point} from './view-base'; import type { FlamechartStackFrame, + NativeEvent, ReactEvent, ReactHoverContextInfo, ReactMeasure, @@ -47,8 +48,8 @@ function trimmedString(string: string, length: number): string { return string; } -function getReactEventLabel(type): string | null { - switch (type) { +function getReactEventLabel(event: ReactEvent): string | null { + switch (event.type) { case 'schedule-render': return 'render scheduled'; case 'schedule-state-update': @@ -111,10 +112,22 @@ export default function EventTooltip({data, hoveredEvent, origin}: Props) { return null; } - const {event, measure, flamechartStackFrame, userTimingMark} = hoveredEvent; + const { + nativeEvent, + reactEvent, + measure, + flamechartStackFrame, + userTimingMark, + } = hoveredEvent; - if (event !== null) { - return ; + if (nativeEvent !== null) { + return ( + + ); + } else if (reactEvent !== null) { + return ( + + ); } else if (measure !== null) { return ( , +}) => { + const {duration, timestamp, type} = nativeEvent; + + return ( +
+ {trimmedString(type, 768)} + event +
+
+
Timestamp:
+
{formatTimestamp(timestamp)}
+
Duration:
+
{formatDuration(duration)}
+
+
+ ); +}; + const TooltipReactEvent = ({ - event, + reactEvent, tooltipRef, }: { - event: ReactEvent, + reactEvent: ReactEvent, tooltipRef: Return, }) => { - const label = getReactEventLabel(event.type); - const color = getReactEventColor(event); + const label = getReactEventLabel(reactEvent); + const color = getReactEventColor(reactEvent); if (!label || !color) { if (__DEV__) { - console.warn('Unexpected event type "%s"', event.type); + console.warn('Unexpected reactEvent type "%s"', reactEvent.type); } return null; } - const {componentName, componentStack, timestamp} = event; + const {componentName, componentStack, timestamp} = reactEvent; 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 new file mode 100644 index 0000000000000..97d4369753a16 --- /dev/null +++ b/packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js @@ -0,0 +1,224 @@ +/** + * 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 {NativeEvent, ReactProfilerData} from '../types'; +import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base'; + +import { + positioningScaleFactor, + timestampToPosition, + positionToTimestamp, +} from './utils/positioning'; +import { + View, + Surface, + rectContainsPoint, + rectIntersectsRect, + intersectionOfRects, +} from '../view-base'; +import { + COLORS, + EVENT_ROW_PADDING, + EVENT_DIAMETER, + BORDER_SIZE, +} from './constants'; + +const EVENT_ROW_HEIGHT_FIXED = + EVENT_ROW_PADDING + EVENT_DIAMETER + EVENT_ROW_PADDING; + +export class NativeEventsView extends View { + _profilerData: ReactProfilerData; + _intrinsicSize: Size; + + _hoveredEvent: NativeEvent | null = null; + onHover: ((event: NativeEvent | null) => void) | null = null; + + constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) { + super(surface, frame); + this._profilerData = profilerData; + + this._intrinsicSize = { + width: this._profilerData.duration, + height: EVENT_ROW_HEIGHT_FIXED, + }; + } + + desiredSize() { + return this._intrinsicSize; + } + + setHoveredEvent(hoveredEvent: NativeEvent | null) { + if (this._hoveredEvent === hoveredEvent) { + return; + } + this._hoveredEvent = hoveredEvent; + this.setNeedsDisplay(); + } + + /** + * Draw a single `NativeEvent` as a circle in the canvas. + */ + _drawSingleNativeEvent( + context: CanvasRenderingContext2D, + rect: Rect, + event: NativeEvent, + baseY: number, + scaleFactor: number, + showHoverHighlight: boolean, + ) { + const {frame} = this; + const {duration, timestamp} = event; + + 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: EVENT_DIAMETER}, + }; + if (!rectIntersectsRect(eventRect, rect)) { + return; // Not in view + } + + const fillStyle = showHoverHighlight + ? COLORS.NATIVE_EVENT_HOVER + : COLORS.NATIVE_EVENT; + + const drawableRect = intersectionOfRects(eventRect, rect); + context.beginPath(); + context.fillStyle = fillStyle; + context.fillRect( + drawableRect.origin.x, + drawableRect.origin.y, + drawableRect.size.width, + drawableRect.size.height, + ); + } + + draw(context: CanvasRenderingContext2D) { + const { + frame, + _profilerData: {nativeEvents}, + _hoveredEvent, + visibleArea, + } = this; + + context.fillStyle = COLORS.BACKGROUND; + context.fillRect( + visibleArea.origin.x, + visibleArea.origin.y, + visibleArea.size.width, + visibleArea.size.height, + ); + + // Draw events + const baseY = frame.origin.y + EVENT_ROW_PADDING; + const scaleFactor = positioningScaleFactor( + this._intrinsicSize.width, + frame, + ); + + nativeEvents.forEach(event => { + if (event === _hoveredEvent) { + // Draw the highlighted items on top so they stand out. + // This is helpful if there are multiple (overlapping) items close to each other. + this._drawSingleNativeEvent( + context, + visibleArea, + event, + baseY, + scaleFactor, + true, + ); + } else { + this._drawSingleNativeEvent( + context, + visibleArea, + event, + baseY, + scaleFactor, + false, + ); + } + }); + + // Render bottom border. + // Propose border rect, check if intersects with `rect`, draw intersection. + const borderFrame: Rect = { + origin: { + x: frame.origin.x, + y: frame.origin.y + EVENT_ROW_HEIGHT_FIXED - BORDER_SIZE, + }, + size: { + width: frame.size.width, + height: BORDER_SIZE, + }, + }; + if (rectIntersectsRect(borderFrame, visibleArea)) { + const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea); + context.fillStyle = COLORS.PRIORITY_BORDER; + context.fillRect( + borderDrawableRect.origin.x, + borderDrawableRect.origin.y, + borderDrawableRect.size.width, + borderDrawableRect.size.height, + ); + } + } + + /** + * @private + */ + _handleMouseMove(interaction: MouseMoveInteraction) { + const {frame, _intrinsicSize, onHover, visibleArea} = this; + if (!onHover) { + return; + } + + const {location} = interaction.payload; + if (!rectContainsPoint(location, visibleArea)) { + onHover(null); + return; + } + + const {nativeEvents} = this._profilerData; + + const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame); + const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame); + + // Find the event being hovered over. + // + // Because data ranges may overlap, we want to find the last intersecting item. + // This will always be the one on "top" (the one the user is hovering over). + for (let index = nativeEvents.length - 1; index >= 0; index--) { + const nativeEvent = nativeEvents[index]; + const {duration, timestamp} = nativeEvent; + + if ( + hoverTimestamp >= timestamp && + hoverTimestamp <= timestamp + duration + ) { + onHover(nativeEvent); + return; + } + } + + onHover(null); + } + + handleInteraction(interaction: Interaction) { + switch (interaction.type) { + case 'mousemove': + this._handleMouseMove(interaction); + break; + } + } +} diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js index fb60e54eb47cf..e67f590f6bf19 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js @@ -100,6 +100,8 @@ 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': @@ -140,7 +142,7 @@ export class ReactEventsView extends View { draw(context: CanvasRenderingContext2D) { const { frame, - _profilerData: {events}, + _profilerData: {reactEvents}, _hoveredEvent, visibleArea, } = this; @@ -162,7 +164,7 @@ export class ReactEventsView extends View { const highlightedEvents: ReactEvent[] = []; - events.forEach(event => { + reactEvents.forEach(event => { if ( event === _hoveredEvent || (_hoveredEvent && @@ -236,7 +238,7 @@ export class ReactEventsView extends View { } const { - _profilerData: {events}, + _profilerData: {reactEvents}, } = this; const scaleFactor = positioningScaleFactor( this._intrinsicSize.width, @@ -250,8 +252,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 = events.length - 1; index >= 0; index--) { - const event = events[index]; + for (let index = reactEvents.length - 1; index >= 0; index--) { + const event = reactEvents[index]; const {timestamp} = event; if ( 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 baaedebc64ca7..6581bed312c2c 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/constants.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/constants.js @@ -44,10 +44,12 @@ export const FLAMECHART_TEXT_PADDING = 3; // TODO Replace this with "export let" vars export let COLORS = { BACKGROUND: '', + FLAME_GRAPH_LABEL: '', + NATIVE_EVENT: '', + NATIVE_EVENT_HOVER: '', PRIORITY_BACKGROUND: '', PRIORITY_BORDER: '', PRIORITY_LABEL: '', - FLAME_GRAPH_LABEL: '', USER_TIMING: '', USER_TIMING_HOVER: '', REACT_IDLE: '', @@ -81,6 +83,15 @@ export function updateColorsToMatchTheme(): void { COLORS = { BACKGROUND: computedStyle.getPropertyValue('--color-background'), + FLAME_GRAPH_LABEL: computedStyle.getPropertyValue( + '--color-scheduling-profiler-flame-graph-label', + ), + NATIVE_EVENT: computedStyle.getPropertyValue( + '--color-scheduling-profiler-native-event', + ), + NATIVE_EVENT_HOVER: computedStyle.getPropertyValue( + '--color-scheduling-profiler-native-event-hover', + ), PRIORITY_BACKGROUND: computedStyle.getPropertyValue( '--color-scheduling-profiler-priority-background', ), @@ -88,9 +99,6 @@ export function updateColorsToMatchTheme(): void { '--color-scheduling-profiler-priority-border', ), PRIORITY_LABEL: computedStyle.getPropertyValue('--color-text'), - FLAME_GRAPH_LABEL: computedStyle.getPropertyValue( - '--color-scheduling-profiler-flame-graph-label', - ), USER_TIMING: computedStyle.getPropertyValue( '--color-scheduling-profiler-user-timing', ), 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 e017b95e20ec2..b50490b13ae92 100644 --- a/packages/react-devtools-scheduling-profiler/src/content-views/index.js +++ b/packages/react-devtools-scheduling-profiler/src/content-views/index.js @@ -8,6 +8,7 @@ */ export * from './FlamechartView'; +export * from './NativeEventsView'; export * from './ReactEventsView'; export * from './ReactMeasuresView'; export * from './TimeAxisMarkersView'; 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 da4153f677de3..52e60e08c931e 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -161,247 +161,269 @@ function processTimelineEvent( /** Intermediate processor state. May be mutated. */ state: ProcessorState, ) { - const {cat, name, ts, ph} = event; - if (cat !== 'blink.user_timing') { - return; - } - - const startTime = (ts - currentProfilerData.startTime) / 1000; - - // React Events - schedule - if (name.startsWith('--schedule-render-')) { - const [laneBitmaskString, laneLabels, ...splitComponentStack] = name - .substr(18) - .split('-'); - currentProfilerData.events.push({ - type: 'schedule-render', - lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), - laneLabels: laneLabels ? laneLabels.split(',') : [], - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - }); - } else if (name.startsWith('--schedule-forced-update-')) { - const [ - laneBitmaskString, - laneLabels, - componentName, - ...splitComponentStack - ] = name.substr(25).split('-'); - const isCascading = !!state.measureStack.find( - ({type}) => type === 'commit', - ); - currentProfilerData.events.push({ - type: 'schedule-force-update', - lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), - laneLabels: laneLabels ? laneLabels.split(',') : [], - componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - isCascading, - }); - } else if (name.startsWith('--schedule-state-update-')) { - const [ - laneBitmaskString, - laneLabels, - componentName, - ...splitComponentStack - ] = name.substr(24).split('-'); - const isCascading = !!state.measureStack.find( - ({type}) => type === 'commit', - ); - currentProfilerData.events.push({ - type: 'schedule-state-update', - lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), - laneLabels: laneLabels ? laneLabels.split(',') : [], - componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - isCascading, - }); - } // eslint-disable-line brace-style - - // React Events - suspense - else if (name.startsWith('--suspense-suspend-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(19) - .split('-'); - currentProfilerData.events.push({ - type: 'suspense-suspend', - id, - componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - }); - } else if (name.startsWith('--suspense-resolved-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(20) - .split('-'); - currentProfilerData.events.push({ - type: 'suspense-resolved', - id, - componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - }); - } else if (name.startsWith('--suspense-rejected-')) { - const [id, componentName, ...splitComponentStack] = name - .substr(20) - .split('-'); - currentProfilerData.events.push({ - type: 'suspense-rejected', - id, - componentName, - componentStack: splitComponentStack.join('-'), - timestamp: startTime, - }); - } // eslint-disable-line brace-style - - // React Measures - render - else if (name.startsWith('--render-start-')) { - if (state.nextRenderShouldGenerateNewBatchID) { - state.nextRenderShouldGenerateNewBatchID = false; - state.batchUID = ((state.uidCounter++: any): BatchUID); - } - const [laneBitmaskString, laneLabels] = name.substr(15).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - throwIfIncomplete('render', state.measureStack); - if (getLastType(state.measureStack) !== 'render-idle') { - markWorkStarted( - 'render-idle', - startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], - currentProfilerData, - state, - ); - } - markWorkStarted( - 'render', - startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], - currentProfilerData, - state, - ); - } else if ( - name.startsWith('--render-stop') || - name.startsWith('--render-yield') - ) { - markWorkCompleted( - 'render', - startTime, - currentProfilerData, - state.measureStack, - ); - } else if (name.startsWith('--render-cancel')) { - state.nextRenderShouldGenerateNewBatchID = true; - markWorkCompleted( - 'render', - startTime, - currentProfilerData, - state.measureStack, - ); - markWorkCompleted( - 'render-idle', - startTime, - currentProfilerData, - state.measureStack, - ); - } // eslint-disable-line brace-style - - // React Measures - commits - else if (name.startsWith('--commit-start-')) { - state.nextRenderShouldGenerateNewBatchID = true; - const [laneBitmaskString, laneLabels] = name.substr(15).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - markWorkStarted( - 'commit', - startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], - currentProfilerData, - state, - ); - } else if (name.startsWith('--commit-stop')) { - markWorkCompleted( - 'commit', - startTime, - currentProfilerData, - state.measureStack, - ); - markWorkCompleted( - 'render-idle', - startTime, - currentProfilerData, - state.measureStack, - ); - } // eslint-disable-line brace-style - - // React Measures - layout effects - else if (name.startsWith('--layout-effects-start-')) { - const [laneBitmaskString, laneLabels] = name.substr(23).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - markWorkStarted( - 'layout-effects', - startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], - currentProfilerData, - state, - ); - } else if (name.startsWith('--layout-effects-stop')) { - markWorkCompleted( - 'layout-effects', - startTime, - currentProfilerData, - state.measureStack, - ); - } // eslint-disable-line brace-style - - // React Measures - passive effects - else if (name.startsWith('--passive-effects-start-')) { - const [laneBitmaskString, laneLabels] = name.substr(24).split('-'); - const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - markWorkStarted( - 'passive-effects', - startTime, - lanes, - laneLabels ? laneLabels.split(',') : [], - currentProfilerData, - state, - ); - } else if (name.startsWith('--passive-effects-stop')) { - markWorkCompleted( - 'passive-effects', - startTime, - currentProfilerData, - state.measureStack, - ); - } // eslint-disable-line brace-style - - // Other user timing marks/measures - else if (ph === 'R' || ph === 'n') { - // User Timing mark - currentProfilerData.otherUserTimingMarks.push({ - name, - timestamp: startTime, - }); - } else if (ph === 'b') { - // TODO: Begin user timing measure - } else if (ph === 'e') { - // TODO: End user timing measure - } else if (ph === 'i' || ph === 'I') { - // Instant events. - // Note that the capital "I" is a deprecated value that exists in Chrome Canary traces. - } // eslint-disable-line brace-style - - // Unrecognized event - else { - throw new InvalidProfileError( - `Unrecognized event ${JSON.stringify( - event, - )}! This is likely a bug in this profiler tool.`, - ); + const {args, cat, name, ts, ph} = event; + switch (cat) { + case 'devtools.timeline': + if (name === 'EventDispatch') { + const type = args.data.type; + + if (type.startsWith('react-')) { + // Filter out fake React events dispatched by invokeGuardedCallbackDev. + return; + } + + const startTime = (ts - currentProfilerData.startTime) / 1000; + const duration = event.dur / 1000; + + // TODO (scheduling profiler) Should we filter out certain event types? + currentProfilerData.nativeEvents.push({ + duration, + timestamp: startTime, + type, + }); + } + break; + case 'blink.user_timing': + const startTime = (ts - currentProfilerData.startTime) / 1000; + + // React Events - schedule + if (name.startsWith('--schedule-render-')) { + const [ + laneBitmaskString, + laneLabels, + ...splitComponentStack + ] = name.substr(18).split('-'); + currentProfilerData.reactEvents.push({ + type: 'schedule-render', + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + laneLabels: laneLabels ? laneLabels.split(',') : [], + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } else if (name.startsWith('--schedule-forced-update-')) { + const [ + laneBitmaskString, + laneLabels, + componentName, + ...splitComponentStack + ] = name.substr(25).split('-'); + const isCascading = !!state.measureStack.find( + ({type}) => type === 'commit', + ); + currentProfilerData.reactEvents.push({ + type: 'schedule-force-update', + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + laneLabels: laneLabels ? laneLabels.split(',') : [], + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + isCascading, + }); + } else if (name.startsWith('--schedule-state-update-')) { + const [ + laneBitmaskString, + laneLabels, + componentName, + ...splitComponentStack + ] = name.substr(24).split('-'); + const isCascading = !!state.measureStack.find( + ({type}) => type === 'commit', + ); + currentProfilerData.reactEvents.push({ + type: 'schedule-state-update', + lanes: getLanesFromTransportDecimalBitmask(laneBitmaskString), + laneLabels: laneLabels ? laneLabels.split(',') : [], + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + isCascading, + }); + } // eslint-disable-line brace-style + + // React Events - suspense + else if (name.startsWith('--suspense-suspend-')) { + const [id, componentName, ...splitComponentStack] = name + .substr(19) + .split('-'); + currentProfilerData.reactEvents.push({ + type: 'suspense-suspend', + id, + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } else if (name.startsWith('--suspense-resolved-')) { + const [id, componentName, ...splitComponentStack] = name + .substr(20) + .split('-'); + currentProfilerData.reactEvents.push({ + type: 'suspense-resolved', + id, + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } else if (name.startsWith('--suspense-rejected-')) { + const [id, componentName, ...splitComponentStack] = name + .substr(20) + .split('-'); + currentProfilerData.reactEvents.push({ + type: 'suspense-rejected', + id, + componentName, + componentStack: splitComponentStack.join('-'), + timestamp: startTime, + }); + } // eslint-disable-line brace-style + + // React Measures - render + else if (name.startsWith('--render-start-')) { + if (state.nextRenderShouldGenerateNewBatchID) { + state.nextRenderShouldGenerateNewBatchID = false; + state.batchUID = ((state.uidCounter++: any): BatchUID); + } + const [laneBitmaskString, laneLabels] = name.substr(15).split('-'); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + throwIfIncomplete('render', state.measureStack); + if (getLastType(state.measureStack) !== 'render-idle') { + markWorkStarted( + 'render-idle', + startTime, + lanes, + laneLabels ? laneLabels.split(',') : [], + currentProfilerData, + state, + ); + } + markWorkStarted( + 'render', + startTime, + lanes, + laneLabels ? laneLabels.split(',') : [], + currentProfilerData, + state, + ); + } else if ( + name.startsWith('--render-stop') || + name.startsWith('--render-yield') + ) { + markWorkCompleted( + 'render', + startTime, + currentProfilerData, + state.measureStack, + ); + } else if (name.startsWith('--render-cancel')) { + state.nextRenderShouldGenerateNewBatchID = true; + markWorkCompleted( + 'render', + startTime, + currentProfilerData, + state.measureStack, + ); + markWorkCompleted( + 'render-idle', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // React Measures - commits + else if (name.startsWith('--commit-start-')) { + state.nextRenderShouldGenerateNewBatchID = true; + const [laneBitmaskString, laneLabels] = name.substr(15).split('-'); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + markWorkStarted( + 'commit', + startTime, + lanes, + laneLabels ? laneLabels.split(',') : [], + currentProfilerData, + state, + ); + } else if (name.startsWith('--commit-stop')) { + markWorkCompleted( + 'commit', + startTime, + currentProfilerData, + state.measureStack, + ); + markWorkCompleted( + 'render-idle', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // React Measures - layout effects + else if (name.startsWith('--layout-effects-start-')) { + const [laneBitmaskString, laneLabels] = name.substr(23).split('-'); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + markWorkStarted( + 'layout-effects', + startTime, + lanes, + laneLabels ? laneLabels.split(',') : [], + currentProfilerData, + state, + ); + } else if (name.startsWith('--layout-effects-stop')) { + markWorkCompleted( + 'layout-effects', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // React Measures - passive effects + else if (name.startsWith('--passive-effects-start-')) { + const [laneBitmaskString, laneLabels] = name.substr(24).split('-'); + const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); + markWorkStarted( + 'passive-effects', + startTime, + lanes, + laneLabels ? laneLabels.split(',') : [], + currentProfilerData, + state, + ); + } else if (name.startsWith('--passive-effects-stop')) { + markWorkCompleted( + 'passive-effects', + startTime, + currentProfilerData, + state.measureStack, + ); + } // eslint-disable-line brace-style + + // Other user timing marks/measures + else if (ph === 'R' || ph === 'n') { + // User Timing mark + currentProfilerData.otherUserTimingMarks.push({ + name, + timestamp: startTime, + }); + } else if (ph === 'b') { + // TODO: Begin user timing measure + } else if (ph === 'e') { + // TODO: End user timing measure + } else if (ph === 'i' || ph === 'I') { + // Instant events. + // Note that the capital "I" is a deprecated value that exists in Chrome Canary traces. + } // eslint-disable-line brace-style + + // Unrecognized event + else { + throw new InvalidProfileError( + `Unrecognized event ${JSON.stringify( + event, + )}! This is likely a bug in this profiler tool.`, + ); + } + break; } } @@ -447,7 +469,8 @@ export default function preprocessData( const profilerData: ReactProfilerData = { startTime: 0, duration: 0, - events: [], + nativeEvents: [], + reactEvents: [], measures: [], flamechart, otherUserTimingMarks: [], diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js index 833df582a5296..3047e7abae06e 100644 --- a/packages/react-devtools-scheduling-profiler/src/types.js +++ b/packages/react-devtools-scheduling-profiler/src/types.js @@ -21,6 +21,12 @@ export type Milliseconds = number; export type ReactLane = number; +export type NativeEvent = {| + +duration: Milliseconds, + +timestamp: Milliseconds, + +type: string, +|}; + type BaseReactEvent = {| +componentName?: string, +componentStack?: string, @@ -122,14 +128,16 @@ export type Flamechart = FlamechartStackLayer[]; export type ReactProfilerData = {| startTime: number, duration: number, - events: ReactEvent[], + nativeEvents: NativeEvent[], + reactEvents: ReactEvent[], measures: ReactMeasure[], flamechart: Flamechart, otherUserTimingMarks: UserTimingMark[], |}; export type ReactHoverContextInfo = {| - event: ReactEvent | null, + nativeEvent: NativeEvent | null, + reactEvent: ReactEvent | null, measure: ReactMeasure | null, data: $ReadOnly | null, flamechartStackFrame: FlamechartStackFrame | 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 a8336ba170eac..8967cca0f221c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -421,6 +421,16 @@ export function updateThemeVariables( 'color-scheduling-profiler-flame-graph-label', documentElements, ); + updateStyleHelper( + theme, + 'color-scheduling-profiler-native-event', + documentElements, + ); + updateStyleHelper( + theme, + 'color-scheduling-profiler-native-event-hover', + documentElements, + ); updateStyleHelper( theme, 'color-selected-tree-highlight-active', diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index 0c5b9a4f01d48..13f67daba2252 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -79,8 +79,10 @@ --light-color-record-inactive: #0088fa; --light-color-resize-bar: #cccccc; --light-color-scheduling-profiler-flame-graph-label: #000000; - --light-color-scheduling-profiler-priority-background: #ededf0; - --light-color-scheduling-profiler-priority-border: #d7d7db; + --light-color-scheduling-profiler-native-event: #aaaaaa; + --light-color-scheduling-profiler-native-event-hover: #888888; + --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; @@ -199,6 +201,8 @@ --dark-color-record-inactive: #61dafb; --dark-color-resize-bar: #3d424a; --dark-color-scheduling-profiler-flame-graph-label: #000000; + --dark-color-scheduling-profiler-native-event: #aaaaaa; + --dark-color-scheduling-profiler-native-event-hover: #888888; --dark-color-scheduling-profiler-priority-background: #1d2129; --dark-color-scheduling-profiler-priority-border: #282c34; --dark-color-scheduling-profiler-user-timing: #c9cacd; From 4409be891aa151a89d91c10d4fd0ea9426fd7b02 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 23 Jul 2021 17:01:11 -0400 Subject: [PATCH 2/3] Filter react-* events only if they come from react-dom URL --- .../src/import-worker/preprocessData.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 52e60e08c931e..2140c40ce490a 100644 --- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js +++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js @@ -168,8 +168,14 @@ function processTimelineEvent( const type = args.data.type; if (type.startsWith('react-')) { - // Filter out fake React events dispatched by invokeGuardedCallbackDev. - return; + const stackTrace = args.data.stackTrace; + if (stackTrace) { + const topFrame = stackTrace[stackTrace.length - 1]; + if (topFrame.url.includes('node_modules/react-dom')) { + // Filter out fake React events dispatched by invokeGuardedCallbackDev. + return; + } + } } const startTime = (ts - currentProfilerData.startTime) / 1000; From 8b113b87e52d03531c696a348a23e7663c9d3f14 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 26 Jul 2021 10:23:20 -0400 Subject: [PATCH 3/3] Update tests to account for new event data in snapshots --- .../__tests__/preprocessData-test.internal.js | 78 ++++++++++--------- 1 file changed, 41 insertions(+), 37 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 128fdfb0f1dcb..649d90246993c 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 @@ -204,10 +204,11 @@ describe(preprocessData, () => { expect(preprocessData([cpuProfilerSample, randomSample])).toStrictEqual({ duration: 0.002, - events: [], flamechart: [], measures: [], + nativeEvents: [], otherUserTimingMarks: [], + reactEvents: [], startTime: 1, }); }); @@ -258,15 +259,6 @@ describe(preprocessData, () => { ]), ).toStrictEqual({ duration: 0.008, - events: [ - { - componentStack: '', - laneLabels: [], - lanes: [9], - timestamp: 0.002, - type: 'schedule-render', - }, - ], flamechart: [], measures: [ { @@ -306,7 +298,17 @@ describe(preprocessData, () => { type: 'layout-effects', }, ], + nativeEvents: [], otherUserTimingMarks: [], + reactEvents: [ + { + componentStack: '', + laneLabels: [], + lanes: [9], + timestamp: 0.002, + type: 'schedule-render', + }, + ], startTime: 1, }); }); @@ -320,15 +322,6 @@ describe(preprocessData, () => { const userTimingData = createUserTimingData(clearedMarks); expect(preprocessData(userTimingData)).toStrictEqual({ duration: 0.011, - events: [ - { - componentStack: '', - laneLabels: ['Sync'], - lanes: [0], - timestamp: 0.005, - type: 'schedule-render', - }, - ], flamechart: [], measures: [ { @@ -368,6 +361,7 @@ describe(preprocessData, () => { type: 'layout-effects', }, ], + nativeEvents: [], otherUserTimingMarks: [ { name: '__v3', @@ -378,6 +372,15 @@ describe(preprocessData, () => { timestamp: 0.004, }, ], + reactEvents: [ + { + componentStack: '', + laneLabels: ['Sync'], + lanes: [0], + timestamp: 0.005, + type: 'schedule-render', + }, + ], startTime: 1, }); }); @@ -400,24 +403,6 @@ describe(preprocessData, () => { const userTimingData = createUserTimingData(clearedMarks); expect(preprocessData(userTimingData)).toStrictEqual({ duration: 0.022, - events: [ - { - componentStack: '', - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.005, - type: 'schedule-render', - }, - { - componentName: 'App', - componentStack: '', - isCascading: false, - laneLabels: ['Default'], - lanes: [4], - timestamp: 0.013, - type: 'schedule-state-update', - }, - ], flamechart: [], measures: [ { @@ -511,6 +496,7 @@ describe(preprocessData, () => { type: 'passive-effects', }, ], + nativeEvents: [], otherUserTimingMarks: [ { name: '__v3', @@ -521,6 +507,24 @@ describe(preprocessData, () => { timestamp: 0.004, }, ], + reactEvents: [ + { + componentStack: '', + laneLabels: ['Default'], + lanes: [4], + timestamp: 0.005, + type: 'schedule-render', + }, + { + componentName: 'App', + componentStack: '', + isCascading: false, + laneLabels: ['Default'], + lanes: [4], + timestamp: 0.013, + type: 'schedule-state-update', + }, + ], startTime: 1, }); });