Skip to content

[DevTools] Add native events to the scheduling profiler #21947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 26, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions packages/react-devtools-scheduling-profiler/src/CanvasPage.js
Original file line number Diff line number Diff line change
@@ -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 (
<Fragment>
{event !== null && (
{reactEvent !== null && (
<ContextMenuItem
onClick={() => copy(event.componentName)}
onClick={() => copy(reactEvent.componentName)}
title="Copy component name">
Copy component name
</ContextMenuItem>
)}
{event !== null && event.componentStack && (
{reactEvent !== null && reactEvent.componentStack && (
<ContextMenuItem
onClick={() => copy(event.componentStack)}
onClick={() => copy(reactEvent.componentStack)}
title="Copy component stack">
Copy component stack
</ContextMenuItem>
Original file line number Diff line number Diff line change
@@ -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 {
59 changes: 48 additions & 11 deletions packages/react-devtools-scheduling-profiler/src/EventTooltip.js
Original file line number Diff line number Diff line change
@@ -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 <TooltipReactEvent event={event} tooltipRef={tooltipRef} />;
if (nativeEvent !== null) {
return (
<TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
);
} else if (reactEvent !== null) {
return (
<TooltipReactEvent reactEvent={reactEvent} tooltipRef={tooltipRef} />
);
} else if (measure !== null) {
return (
<TooltipReactMeasure
@@ -189,23 +202,47 @@ const TooltipFlamechartNode = ({
);
};

const TooltipNativeEvent = ({
nativeEvent,
tooltipRef,
}: {
nativeEvent: NativeEvent,
tooltipRef: Return<typeof useRef>,
}) => {
const {duration, timestamp, type} = nativeEvent;

return (
<div className={styles.Tooltip} ref={tooltipRef}>
<span className={styles.ComponentName}>{trimmedString(type, 768)}</span>
event
<div className={styles.Divider} />
<div className={styles.DetailsGrid}>
<div className={styles.DetailsGridLabel}>Timestamp:</div>
<div>{formatTimestamp(timestamp)}</div>
<div className={styles.DetailsGridLabel}>Duration:</div>
<div>{formatDuration(duration)}</div>
</div>
</div>
);
};

const TooltipReactEvent = ({
event,
reactEvent,
tooltipRef,
}: {
event: ReactEvent,
reactEvent: ReactEvent,
tooltipRef: Return<typeof useRef>,
}) => {
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 (
<div className={styles.Tooltip} ref={tooltipRef}>
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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 (
Original file line number Diff line number Diff line change
@@ -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,16 +83,22 @@ 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',
),
PRIORITY_BORDER: computedStyle.getPropertyValue(
'--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',
),
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
*/

export * from './FlamechartView';
export * from './NativeEventsView';
export * from './ReactEventsView';
export * from './ReactMeasuresView';
export * from './TimeAxisMarkersView';
Original file line number Diff line number Diff line change
@@ -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,
});
});

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions packages/react-devtools-scheduling-profiler/src/types.js
Original file line number Diff line number Diff line change
@@ -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<ReactProfilerData> | null,
flamechartStackFrame: FlamechartStackFrame | null,
Original file line number Diff line number Diff line change
@@ -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',
8 changes: 6 additions & 2 deletions packages/react-devtools-shared/src/devtools/views/root.css
Original file line number Diff line number Diff line change
@@ -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;