DevTools scheduling profiler: Add React component measures (#22013)

This commit is contained in:
Brian Vaughn 2021-08-03 13:03:29 -04:00 committed by GitHub
parent b54f36f2b6
commit e3049bb850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 609 additions and 45 deletions

View File

@ -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<HorizontalPanAndZoomView[]>(
@ -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 (
<Fragment>
{componentMeasure !== null && (
<ContextMenuItem
onClick={() => copy(componentMeasure.componentName)}
title="Copy component name">
Copy component name
</ContextMenuItem>
)}
{schedulingEvent !== null && (
<ContextMenuItem
onClick={() => copy(schedulingEvent.componentName)}

View File

@ -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 (
<TooltipReactComponentMeasure
componentMeasure={componentMeasure}
tooltipRef={tooltipRef}
/>
);
} else if (nativeEvent !== null) {
return (
<TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
);
@ -130,6 +139,38 @@ export default function EventTooltip({
return null;
}
const TooltipReactComponentMeasure = ({
componentMeasure,
tooltipRef,
}: {
componentMeasure: ReactComponentMeasure,
tooltipRef: Return<typeof useRef>,
}) => {
const {componentName, duration, timestamp, warning} = componentMeasure;
const label = `${componentName} rendered`;
return (
<div className={styles.Tooltip} ref={tooltipRef}>
<div className={styles.TooltipSection}>
{trimString(label, 768)}
<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>
{warning !== null && (
<div className={styles.TooltipWarningSection}>
<div className={styles.WarningText}>{warning}</div>
</div>
)}
</div>
);
};
const TooltipFlamechartNode = ({
stackFrame,
tooltipRef,

View File

@ -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<DataResource | null>(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)

View File

@ -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;
}
}
}

View File

@ -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,
);
}
}

View File

@ -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) {

View File

@ -231,7 +231,7 @@ export class SuspenseEventsView extends View {
label += ` - ${formatDuration(duration)}`;
}
drawText(label, context, eventRect, drawableRect, width);
drawText(label, context, eventRect, drawableRect);
}
}

View File

@ -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',

View File

@ -7,6 +7,7 @@
* @flow
*/
export * from './ComponentMeasuresView';
export * from './FlamechartView';
export * from './NativeEventsView';
export * from './ReactMeasuresView';

View File

@ -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);
}

View File

@ -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,27 +467,18 @@ 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,
laneLabels: ['Default'],
lanes: [4],
timestamp: 0.015,
type: 'render',
},
{
batchUID: 1,
depth: 0,
@ -478,6 +486,15 @@ describe(preprocessData, () => {
laneLabels: ['Default'],
lanes: [4],
timestamp: 0.017,
type: 'render',
},
{
batchUID: 1,
depth: 0,
duration: 0.002999999999999999,
laneLabels: ['Default'],
lanes: [4],
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,
},

View File

@ -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,

View File

@ -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<ReactProfilerData> | null,
flamechartStackFrame: FlamechartStackFrame | null,
measure: ReactMeasure | null,

View File

@ -110,7 +110,6 @@ class ResizeBar extends View {
context,
labelRect,
drawableRect,
visibleArea.size.width,
'center',
COLORS.REACT_RESIZE_BAR_DOT,
);

View File

@ -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',

View File

@ -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 (
<Button
disabled={isProfiling || !didRecordCommits}
disabled={isProfiling || !doesHaveData}
onClick={clear}
title="Clear profiling data">
<ButtonIcon type="clear" />

View File

@ -103,8 +103,16 @@ function Profiler(_: {||}) {
<div className={styles.Profiler}>
<div className={styles.LeftColumn}>
<div className={styles.Toolbar}>
<RecordToggle disabled={!supportsProfiling} />
<ReloadAndProfileButton />
<RecordToggle
disabled={
!supportsProfiling || selectedTabID === 'scheduling-profiler'
}
/>
<ReloadAndProfileButton
disabled={
selectedTabID === 'scheduling-profiler' || !supportsProfiling
}
/>
<ClearProfilingDataButton />
<ProfilingImportExportButtons />
<div className={styles.VRule} />

View File

@ -19,7 +19,11 @@ type SubscriptionData = {|
supportsReloadAndProfile: boolean,
|};
export default function ReloadAndProfileButton() {
export default function ReloadAndProfileButton({
disabled,
}: {|
disabled: boolean,
|}) {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@ -61,7 +65,7 @@ export default function ReloadAndProfileButton() {
return (
<Button
disabled={!store.supportsProfiling}
disabled={disabled}
onClick={reloadAndProfile}
title="Reload and start profiling">
<ButtonIcon type="reload" />

View File

@ -31,7 +31,10 @@ import type {
import type {UpdateQueue} from './ReactUpdateQueue.new';
import checkPropTypes from 'shared/checkPropTypes';
import {
markComponentRenderStarted,
markComponentRenderStopped,
} from './SchedulingProfiler';
import {
IndeterminateComponent,
FunctionComponent,
@ -85,6 +88,7 @@ import {
enableCache,
enableLazyContextPropagation,
enableSuspenseLayoutEffectSemantics,
enableSchedulingProfiler,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import isArray from 'shared/isArray';
@ -357,6 +361,9 @@ function updateForwardRef(
// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setIsRendering(true);
@ -397,6 +404,9 @@ function updateForwardRef(
renderLanes,
);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
@ -958,6 +968,9 @@ function updateFunctionComponent(
let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setIsRendering(true);
@ -998,6 +1011,9 @@ function updateFunctionComponent(
renderLanes,
);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
@ -1177,6 +1193,9 @@ function finishClassComponent(
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
setIsRendering(true);
nextChildren = instance.render();
@ -1195,6 +1214,9 @@ function finishClassComponent(
} else {
nextChildren = instance.render();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
}
// React DevTools reads this flag.
@ -1567,6 +1589,9 @@ function mountIndeterminateComponent(
prepareToReadContext(workInProgress, renderLanes);
let value;
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
if (
Component.prototype &&
@ -1610,6 +1635,10 @@ function mountIndeterminateComponent(
renderLanes,
);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
@ -3195,6 +3224,9 @@ function updateContextConsumer(
prepareToReadContext(workInProgress, renderLanes);
const newValue = readContext(context);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
let newChildren;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
@ -3204,6 +3236,9 @@ function updateContextConsumer(
} else {
newChildren = render(newValue);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;

View File

@ -31,7 +31,10 @@ import type {
import type {UpdateQueue} from './ReactUpdateQueue.old';
import checkPropTypes from 'shared/checkPropTypes';
import {
markComponentRenderStarted,
markComponentRenderStopped,
} from './SchedulingProfiler';
import {
IndeterminateComponent,
FunctionComponent,
@ -85,6 +88,7 @@ import {
enableCache,
enableLazyContextPropagation,
enableSuspenseLayoutEffectSemantics,
enableSchedulingProfiler,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import isArray from 'shared/isArray';
@ -357,6 +361,9 @@ function updateForwardRef(
// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setIsRendering(true);
@ -397,6 +404,9 @@ function updateForwardRef(
renderLanes,
);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
@ -958,6 +968,9 @@ function updateFunctionComponent(
let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setIsRendering(true);
@ -998,6 +1011,9 @@ function updateFunctionComponent(
renderLanes,
);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
@ -1177,6 +1193,9 @@ function finishClassComponent(
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
setIsRendering(true);
nextChildren = instance.render();
@ -1195,6 +1214,9 @@ function finishClassComponent(
} else {
nextChildren = instance.render();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
}
// React DevTools reads this flag.
@ -1567,6 +1589,9 @@ function mountIndeterminateComponent(
prepareToReadContext(workInProgress, renderLanes);
let value;
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
if (
Component.prototype &&
@ -1610,6 +1635,10 @@ function mountIndeterminateComponent(
renderLanes,
);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
@ -3195,6 +3224,9 @@ function updateContextConsumer(
prepareToReadContext(workInProgress, renderLanes);
const newValue = readContext(context);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
let newChildren;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
@ -3204,6 +3236,9 @@ function updateContextConsumer(
} else {
newChildren = render(newValue);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;

View File

@ -70,7 +70,10 @@ import {
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
import {logCapturedError} from './ReactFiberErrorLogger';
import {logComponentSuspended} from './DebugTracing';
import {markComponentSuspended} from './SchedulingProfiler';
import {
markComponentRenderStopped,
markComponentSuspended,
} from './SchedulingProfiler';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
import {
SyncLane,
@ -244,6 +247,7 @@ function throwException(
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
}

View File

@ -70,7 +70,10 @@ import {
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
import {logCapturedError} from './ReactFiberErrorLogger';
import {logComponentSuspended} from './DebugTracing';
import {markComponentSuspended} from './SchedulingProfiler';
import {
markComponentRenderStopped,
markComponentSuspended,
} from './SchedulingProfiler';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
import {
SyncLane,
@ -244,6 +247,7 @@ function throwException(
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
}

View File

@ -97,6 +97,23 @@ export function markCommitStopped(): void {
}
}
export function markComponentRenderStarted(fiber: Fiber): void {
if (enableSchedulingProfiler) {
if (supportsUserTimingV3) {
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
markAndClear(`--component-render-start-${componentName}`);
}
}
}
export function markComponentRenderStopped(): void {
if (enableSchedulingProfiler) {
if (supportsUserTimingV3) {
markAndClear('--component-render-stop');
}
}
}
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps

View File

@ -174,6 +174,8 @@ describe('SchedulingProfiler', () => {
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.TransitionLane1)}`,
`--render-start-${formatLanes(ReactFiberLane.TransitionLane1)}`,
'--component-render-start-Foo',
'--component-render-stop',
'--render-yield',
]);
} else {
@ -208,6 +210,8 @@ describe('SchedulingProfiler', () => {
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--suspense-suspend-0-Example-mount-1-Sync',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
@ -239,6 +243,8 @@ describe('SchedulingProfiler', () => {
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.SyncLane)}`,
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--suspense-suspend-0-Example-mount-1-Sync',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
@ -278,6 +284,8 @@ describe('SchedulingProfiler', () => {
expectMarksToEqual([
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--suspense-suspend-0-Example-mount-16-Default',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@ -317,6 +325,8 @@ describe('SchedulingProfiler', () => {
expectMarksToEqual([
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--suspense-suspend-0-Example-mount-16-Default',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@ -356,12 +366,16 @@ describe('SchedulingProfiler', () => {
expectMarksToEqual([
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--schedule-state-update-${formatLanes(ReactFiberLane.SyncLane)}-Example`,
'--layout-effects-stop',
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--commit-stop',
@ -393,6 +407,8 @@ describe('SchedulingProfiler', () => {
expectMarksToEqual([
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@ -401,6 +417,8 @@ describe('SchedulingProfiler', () => {
)}-Example`,
'--layout-effects-stop',
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--commit-stop',
@ -495,12 +513,16 @@ describe('SchedulingProfiler', () => {
expectMarksToEqual([
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--schedule-state-update-${formatLanes(ReactFiberLane.SyncLane)}-Example`,
'--layout-effects-stop',
`--render-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.SyncLane)}`,
'--commit-stop',
@ -528,6 +550,8 @@ describe('SchedulingProfiler', () => {
`--react-init-${ReactVersion}`,
`--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
`--layout-effects-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
@ -539,6 +563,8 @@ describe('SchedulingProfiler', () => {
)}-Example`,
'--passive-effects-stop',
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--component-render-start-Example',
'--component-render-stop',
'--render-stop',
`--commit-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
'--commit-stop',