mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
b7e7f1a3fa
Mostly just changes in ternary formatting.
371 lines
11 KiB
JavaScript
371 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {ReactLane, ReactMeasure, TimelineData} from '../types';
|
|
import type {
|
|
Interaction,
|
|
IntrinsicSize,
|
|
MouseMoveInteraction,
|
|
Rect,
|
|
ViewRefs,
|
|
} from '../view-base';
|
|
|
|
import {formatDuration} from '../utils/formatting';
|
|
import {drawText} from './utils/text';
|
|
import {
|
|
durationToWidth,
|
|
positioningScaleFactor,
|
|
positionToTimestamp,
|
|
timestampToPosition,
|
|
} from './utils/positioning';
|
|
import {
|
|
View,
|
|
Surface,
|
|
rectContainsPoint,
|
|
rectIntersectsRect,
|
|
intersectionOfRects,
|
|
} from '../view-base';
|
|
|
|
import {COLORS, BORDER_SIZE, REACT_MEASURE_HEIGHT} from './constants';
|
|
|
|
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
|
|
const MAX_ROWS_TO_SHOW_INITIALLY = 5;
|
|
|
|
export class ReactMeasuresView extends View {
|
|
_intrinsicSize: IntrinsicSize;
|
|
_lanesToRender: ReactLane[];
|
|
_profilerData: TimelineData;
|
|
_hoveredMeasure: ReactMeasure | null = null;
|
|
|
|
onHover: ((measure: ReactMeasure | null) => void) | null = null;
|
|
|
|
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
|
|
super(surface, frame);
|
|
this._profilerData = profilerData;
|
|
this._performPreflightComputations();
|
|
}
|
|
|
|
_performPreflightComputations() {
|
|
this._lanesToRender = [];
|
|
|
|
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
|
for (const [lane, measuresForLane] of this._profilerData
|
|
.laneToReactMeasureMap) {
|
|
// Only show lanes with measures
|
|
if (measuresForLane.length > 0) {
|
|
this._lanesToRender.push(lane);
|
|
}
|
|
}
|
|
|
|
this._intrinsicSize = {
|
|
width: this._profilerData.duration,
|
|
height: this._lanesToRender.length * REACT_LANE_HEIGHT,
|
|
hideScrollBarIfLessThanHeight: REACT_LANE_HEIGHT,
|
|
maxInitialHeight: MAX_ROWS_TO_SHOW_INITIALLY * REACT_LANE_HEIGHT,
|
|
};
|
|
}
|
|
|
|
desiredSize(): IntrinsicSize {
|
|
return this._intrinsicSize;
|
|
}
|
|
|
|
setHoveredMeasure(hoveredMeasure: ReactMeasure | null) {
|
|
if (this._hoveredMeasure === hoveredMeasure) {
|
|
return;
|
|
}
|
|
this._hoveredMeasure = hoveredMeasure;
|
|
this.setNeedsDisplay();
|
|
}
|
|
|
|
/**
|
|
* Draw a single `ReactMeasure` as a bar in the canvas.
|
|
*/
|
|
_drawSingleReactMeasure(
|
|
context: CanvasRenderingContext2D,
|
|
rect: Rect,
|
|
measure: ReactMeasure,
|
|
nextMeasure: ReactMeasure | null,
|
|
baseY: number,
|
|
scaleFactor: number,
|
|
showGroupHighlight: boolean,
|
|
showHoverHighlight: boolean,
|
|
): void {
|
|
const {frame, visibleArea} = this;
|
|
const {timestamp, type, duration} = measure;
|
|
|
|
let fillStyle = null;
|
|
let hoveredFillStyle = null;
|
|
let groupSelectedFillStyle = null;
|
|
let textFillStyle = null;
|
|
|
|
// We could change the max to 0 and just skip over rendering anything that small,
|
|
// but this has the effect of making the chart look very empty when zoomed out.
|
|
// So long as perf is okay- it might be best to err on the side of showing things.
|
|
const width = durationToWidth(duration, scaleFactor);
|
|
if (width <= 0) {
|
|
return; // Too small to render at this zoom level
|
|
}
|
|
|
|
const x = timestampToPosition(timestamp, scaleFactor, frame);
|
|
const measureRect: Rect = {
|
|
origin: {x, y: baseY},
|
|
size: {width, height: REACT_MEASURE_HEIGHT},
|
|
};
|
|
if (!rectIntersectsRect(measureRect, rect)) {
|
|
return; // Not in view
|
|
}
|
|
|
|
const drawableRect = intersectionOfRects(measureRect, rect);
|
|
let textRect = measureRect;
|
|
|
|
switch (type) {
|
|
case 'commit':
|
|
fillStyle = COLORS.REACT_COMMIT;
|
|
hoveredFillStyle = COLORS.REACT_COMMIT_HOVER;
|
|
groupSelectedFillStyle = COLORS.REACT_COMMIT_HOVER;
|
|
textFillStyle = COLORS.REACT_COMMIT_TEXT;
|
|
|
|
// Commit phase rects are overlapped by layout and passive rects,
|
|
// and it looks bad if text flows underneath/behind these overlayed rects.
|
|
if (nextMeasure != null) {
|
|
// This clipping shouldn't apply for measures that don't overlap though,
|
|
// like passive effects that are processed after a delay,
|
|
// or if there are now layout or passive effects and the next measure is render or idle.
|
|
if (nextMeasure.timestamp < measure.timestamp + measure.duration) {
|
|
textRect = {
|
|
...measureRect,
|
|
size: {
|
|
width:
|
|
timestampToPosition(
|
|
nextMeasure.timestamp,
|
|
scaleFactor,
|
|
frame,
|
|
) - x,
|
|
height: REACT_MEASURE_HEIGHT,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
break;
|
|
case 'render-idle':
|
|
// We could render idle time as diagonal hashes.
|
|
// This looks nicer when zoomed in, but not so nice when zoomed out.
|
|
// color = context.createPattern(getIdlePattern(), 'repeat');
|
|
fillStyle = COLORS.REACT_IDLE;
|
|
hoveredFillStyle = COLORS.REACT_IDLE_HOVER;
|
|
groupSelectedFillStyle = COLORS.REACT_IDLE_HOVER;
|
|
break;
|
|
case 'render':
|
|
fillStyle = COLORS.REACT_RENDER;
|
|
hoveredFillStyle = COLORS.REACT_RENDER_HOVER;
|
|
groupSelectedFillStyle = COLORS.REACT_RENDER_HOVER;
|
|
textFillStyle = COLORS.REACT_RENDER_TEXT;
|
|
break;
|
|
case 'layout-effects':
|
|
fillStyle = COLORS.REACT_LAYOUT_EFFECTS;
|
|
hoveredFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
|
|
groupSelectedFillStyle = COLORS.REACT_LAYOUT_EFFECTS_HOVER;
|
|
textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
|
|
break;
|
|
case 'passive-effects':
|
|
fillStyle = COLORS.REACT_PASSIVE_EFFECTS;
|
|
hoveredFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
|
|
groupSelectedFillStyle = COLORS.REACT_PASSIVE_EFFECTS_HOVER;
|
|
textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
|
|
break;
|
|
default:
|
|
throw new Error(`Unexpected measure type "${type}"`);
|
|
}
|
|
|
|
context.fillStyle = showHoverHighlight
|
|
? hoveredFillStyle
|
|
: showGroupHighlight
|
|
? groupSelectedFillStyle
|
|
: fillStyle;
|
|
context.fillRect(
|
|
drawableRect.origin.x,
|
|
drawableRect.origin.y,
|
|
drawableRect.size.width,
|
|
drawableRect.size.height,
|
|
);
|
|
|
|
if (textFillStyle !== null) {
|
|
drawText(formatDuration(duration), context, textRect, visibleArea, {
|
|
fillStyle: textFillStyle,
|
|
});
|
|
}
|
|
}
|
|
|
|
draw(context: CanvasRenderingContext2D): void {
|
|
const {frame, _hoveredMeasure, _lanesToRender, _profilerData, visibleArea} =
|
|
this;
|
|
|
|
context.fillStyle = COLORS.PRIORITY_BACKGROUND;
|
|
context.fillRect(
|
|
visibleArea.origin.x,
|
|
visibleArea.origin.y,
|
|
visibleArea.size.width,
|
|
visibleArea.size.height,
|
|
);
|
|
|
|
const scaleFactor = positioningScaleFactor(
|
|
this._intrinsicSize.width,
|
|
frame,
|
|
);
|
|
|
|
for (let i = 0; i < _lanesToRender.length; i++) {
|
|
const lane = _lanesToRender[i];
|
|
const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
|
|
const measuresForLane = _profilerData.laneToReactMeasureMap.get(lane);
|
|
|
|
if (!measuresForLane) {
|
|
throw new Error(
|
|
'No measures found for a React lane! This is a bug in this profiler tool. Please file an issue.',
|
|
);
|
|
}
|
|
|
|
// Render lane labels
|
|
const label = _profilerData.laneToLabelMap.get(lane);
|
|
if (label == null) {
|
|
console.warn(`Could not find label for lane ${lane}.`);
|
|
} else {
|
|
const labelRect = {
|
|
origin: {
|
|
x: visibleArea.origin.x,
|
|
y: baseY,
|
|
},
|
|
size: {
|
|
width: visibleArea.size.width,
|
|
height: REACT_LANE_HEIGHT,
|
|
},
|
|
};
|
|
|
|
drawText(label, context, labelRect, visibleArea, {
|
|
fillStyle: COLORS.TEXT_DIM_COLOR,
|
|
});
|
|
}
|
|
|
|
// Draw measures
|
|
for (let j = 0; j < measuresForLane.length; j++) {
|
|
const measure = measuresForLane[j];
|
|
const showHoverHighlight = _hoveredMeasure === measure;
|
|
const showGroupHighlight =
|
|
!!_hoveredMeasure && _hoveredMeasure.batchUID === measure.batchUID;
|
|
|
|
this._drawSingleReactMeasure(
|
|
context,
|
|
visibleArea,
|
|
measure,
|
|
measuresForLane[j + 1] || null,
|
|
baseY,
|
|
scaleFactor,
|
|
showGroupHighlight,
|
|
showHoverHighlight,
|
|
);
|
|
}
|
|
|
|
// Render bottom border
|
|
const borderFrame: Rect = {
|
|
origin: {
|
|
x: frame.origin.x,
|
|
y: frame.origin.y + (i + 1) * REACT_LANE_HEIGHT - BORDER_SIZE,
|
|
},
|
|
size: {
|
|
width: frame.size.width,
|
|
height: BORDER_SIZE,
|
|
},
|
|
};
|
|
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
|
const borderDrawableRect = intersectionOfRects(
|
|
borderFrame,
|
|
visibleArea,
|
|
);
|
|
context.fillStyle = COLORS.PRIORITY_BORDER;
|
|
context.fillRect(
|
|
borderDrawableRect.origin.x,
|
|
borderDrawableRect.origin.y,
|
|
borderDrawableRect.size.width,
|
|
borderDrawableRect.size.height,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
|
const {
|
|
frame,
|
|
_intrinsicSize,
|
|
_lanesToRender,
|
|
onHover,
|
|
_profilerData,
|
|
visibleArea,
|
|
} = this;
|
|
if (!onHover) {
|
|
return;
|
|
}
|
|
|
|
const {location} = interaction.payload;
|
|
if (!rectContainsPoint(location, visibleArea)) {
|
|
onHover(null);
|
|
return;
|
|
}
|
|
|
|
// Identify the lane being hovered over
|
|
const adjustedCanvasMouseY = location.y - frame.origin.y;
|
|
const renderedLaneIndex = Math.floor(
|
|
adjustedCanvasMouseY / REACT_LANE_HEIGHT,
|
|
);
|
|
if (renderedLaneIndex < 0 || renderedLaneIndex >= _lanesToRender.length) {
|
|
onHover(null);
|
|
return;
|
|
}
|
|
const lane = _lanesToRender[renderedLaneIndex];
|
|
|
|
// Find the measure in `lane` 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).
|
|
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
|
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
|
const measures = _profilerData.laneToReactMeasureMap.get(lane);
|
|
if (!measures) {
|
|
onHover(null);
|
|
return;
|
|
}
|
|
|
|
for (let index = measures.length - 1; index >= 0; index--) {
|
|
const measure = measures[index];
|
|
const {duration, timestamp} = measure;
|
|
|
|
if (
|
|
hoverTimestamp >= timestamp &&
|
|
hoverTimestamp <= timestamp + duration
|
|
) {
|
|
this.currentCursor = 'context-menu';
|
|
viewRefs.hoveredView = this;
|
|
onHover(measure);
|
|
return;
|
|
}
|
|
}
|
|
|
|
onHover(null);
|
|
}
|
|
|
|
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
|
switch (interaction.type) {
|
|
case 'mousemove':
|
|
this._handleMouseMove(interaction, viewRefs);
|
|
break;
|
|
}
|
|
}
|
|
}
|