close
The Wayback Machine - https://web.archive.org/web/20211108220814/https://github.com/facebook/react/commit/99a73ac670ce8d71ae8a690e9795d35cbdfa20b6
Skip to content
Permalink
Browse files
Timeline: Improved snapshot view (#22706)
  • Loading branch information
bvaughn committed Nov 8, 2021
1 parent 327d5c4 commit 99a73ac670ce8d71ae8a690e9795d35cbdfa20b6
@@ -753,8 +753,10 @@ function AutoSizedCanvas({
<EventTooltip
canvasRef={canvasRef}
data={data}
height={height}
hoveredEvent={hoveredEvent}
origin={mouseLocation}
width={width}
/>
)}
</Fragment>
@@ -34,8 +34,10 @@ const MAX_TOOLTIP_TEXT_LENGTH = 60;
type Props = {|
canvasRef: {|current: HTMLCanvasElement | null|},
data: ReactProfilerData,
height: number,
hoveredEvent: ReactHoverContextInfo | null,
origin: Point,
width: number,
|};

function getSchedulingEventLabel(event: SchedulingEvent): string | null {
@@ -71,8 +73,10 @@ function getReactMeasureLabel(type): string | null {
export default function EventTooltip({
canvasRef,
data,
height,
hoveredEvent,
origin,
width,
}: Props) {
const ref = useSmartTooltip({
canvasRef,
@@ -111,7 +115,9 @@ export default function EventTooltip({
<TooltipSchedulingEvent data={data} schedulingEvent={schedulingEvent} />
);
} else if (snapshot !== null) {
content = <TooltipSnapshot snapshot={snapshot} />;
content = (
<TooltipSnapshot height={height} snapshot={snapshot} width={width} />
);
} else if (suspenseEvent !== null) {
content = <TooltipSuspenseEvent suspenseEvent={suspenseEvent} />;
} else if (measure !== null) {
@@ -333,12 +339,34 @@ const TooltipSchedulingEvent = ({
);
};

const TooltipSnapshot = ({snapshot}: {|snapshot: Snapshot|}) => {
const TooltipSnapshot = ({
height,
snapshot,
width,
}: {|
height: number,
snapshot: Snapshot,
width: number,
|}) => {
const aspectRatio = snapshot.width / snapshot.height;

// Zoomed in view should not be any bigger than the DevTools viewport.
let safeWidth = snapshot.width;
let safeHeight = snapshot.height;
if (safeWidth > width) {
safeWidth = width;
safeHeight = safeWidth / aspectRatio;
}
if (safeHeight > height) {
safeHeight = height;
safeWidth = safeHeight * aspectRatio;
}

return (
<img
className={styles.Image}
src={snapshot.imageSource}
style={{width: snapshot.width / 2, height: snapshot.height / 2}}
style={{height: safeHeight, width: safeWidth}}
/>
);
};
@@ -16,3 +16,5 @@ export const REACT_TOTAL_NUM_LANES = 31;

// Increment this number any time a backwards breaking change is made to the profiler metadata.
export const SCHEDULING_PROFILER_VERSION = 1;

export const SNAPSHOT_MAX_HEIGHT = 60;
@@ -24,11 +24,12 @@ import {
rectEqualToRect,
View,
} from '../view-base';
import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';
import {BORDER_SIZE, COLORS, SNAPSHOT_SCRUBBER_SIZE} from './constants';

type OnHover = (node: Snapshot | null) => void;

export class SnapshotsView extends View {
_hoverLocation: Point | null = null;
_intrinsicSize: Size;
_profilerData: ReactProfilerData;

@@ -39,7 +40,7 @@ export class SnapshotsView extends View {

this._intrinsicSize = {
width: profilerData.duration,
height: SNAPSHOT_HEIGHT,
height: profilerData.snapshotHeight,
};
this._profilerData = profilerData;
}
@@ -49,6 +50,7 @@ export class SnapshotsView extends View {
}

draw(context: CanvasRenderingContext2D) {
const snapshotHeight = this._profilerData.snapshotHeight;
const {visibleArea} = this;

context.fillStyle = COLORS.BACKGROUND;
@@ -72,8 +74,8 @@ export class SnapshotsView extends View {
break;
}

const scaledHeight = SNAPSHOT_HEIGHT;
const scaledWidth = (snapshot.width * SNAPSHOT_HEIGHT) / snapshot.height;
const scaledHeight = snapshotHeight;
const scaledWidth = (snapshot.width * snapshotHeight) / snapshot.height;

const imageRect: Rect = {
origin: {
@@ -96,6 +98,28 @@ export class SnapshotsView extends View {

x += scaledWidth + BORDER_SIZE;
}

const hoverLocation = this._hoverLocation;
if (hoverLocation !== null) {
const scrubberWidth = SNAPSHOT_SCRUBBER_SIZE + BORDER_SIZE * 2;
const scrubberOffset = scrubberWidth / 2;

context.fillStyle = COLORS.SCRUBBER_BORDER;
context.fillRect(
hoverLocation.x - scrubberOffset,
visibleArea.origin.y,
scrubberWidth,
visibleArea.size.height,
);

context.fillStyle = COLORS.SCRUBBER_BACKGROUND;
context.fillRect(
hoverLocation.x - scrubberOffset + BORDER_SIZE,
visibleArea.origin.y,
SNAPSHOT_SCRUBBER_SIZE,
visibleArea.size.height,
);
}
}

handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
@@ -208,15 +232,29 @@ export class SnapshotsView extends View {
}

if (!rectContainsPoint(location, visibleArea)) {
if (this._hoverLocation !== null) {
this._hoverLocation = null;

this.setNeedsDisplay();
}

onHover(null);
return;
}

const snapshot = this._findClosestSnapshot(location.x);
if (snapshot !== null) {
this._hoverLocation = location;

onHover(snapshot);
} else {
this._hoverLocation = null;

onHover(null);
}

// Any time the mouse moves within the boundaries of this view, we need to re-render.
// This is because we draw a scrubbing bar that shows the location corresponding to the current tooltip.
this.setNeedsDisplay();
}
}
@@ -24,7 +24,7 @@ export const REACT_MEASURE_HEIGHT = 14;
export const BORDER_SIZE = 1 / DPR;
export const FLAMECHART_FRAME_HEIGHT = 14;
export const TEXT_PADDING = 3;
export const SNAPSHOT_HEIGHT = 35;
export const SNAPSHOT_SCRUBBER_SIZE = 3;

export const INTERVAL_TIMES = [
1,
@@ -89,6 +89,8 @@ export let COLORS = {
REACT_THROWN_ERROR_HOVER: '',
REACT_WORK_BORDER: '',
SCROLL_CARET: '',
SCRUBBER_BACKGROUND: '',
SCRUBBER_BORDER: '',
TEXT_COLOR: '',
TEXT_DIM_COLOR: '',
TIME_MARKER_LABEL: '',
@@ -230,6 +232,12 @@ export function updateColorsToMatchTheme(element: Element): boolean {
'--color-timeline-react-work-border',
),
SCROLL_CARET: computedStyle.getPropertyValue('--color-scroll-caret'),
SCRUBBER_BACKGROUND: computedStyle.getPropertyValue(
'--color-timeline-react-suspense-rejected',
),
SCRUBBER_BORDER: computedStyle.getPropertyValue(
'--color-timeline-text-color',
),
TEXT_COLOR: computedStyle.getPropertyValue('--color-timeline-text-color'),
TEXT_DIM_COLOR: computedStyle.getPropertyValue(
'--color-timeline-text-dim-color',
@@ -354,6 +354,7 @@ describe('preprocessData', () => {
"otherUserTimingMarks": Array [],
"reactVersion": "17.0.3",
"schedulingEvents": Array [],
"snapshotHeight": 0,
"snapshots": Array [],
"startTime": 1,
"suspenseEvents": Array [],
@@ -572,6 +573,7 @@ describe('preprocessData', () => {
"warning": null,
},
],
"snapshotHeight": 0,
"snapshots": Array [],
"startTime": 1,
"suspenseEvents": Array [],
@@ -765,6 +767,7 @@ describe('preprocessData', () => {
"warning": null,
},
],
"snapshotHeight": 0,
"snapshots": Array [],
"startTime": 4,
"suspenseEvents": Array [],
@@ -1129,6 +1132,7 @@ describe('preprocessData', () => {
"warning": null,
},
],
"snapshotHeight": 0,
"snapshots": Array [],
"startTime": 4,
"suspenseEvents": Array [],
@@ -29,7 +29,11 @@ import type {
SchedulingEvent,
SuspenseEvent,
} from '../types';
import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants';
import {
REACT_TOTAL_NUM_LANES,
SCHEDULING_PROFILER_VERSION,
SNAPSHOT_MAX_HEIGHT,
} from '../constants';
import InvalidProfileError from './InvalidProfileError';
import {getBatchRange} from '../utils/getBatchRange';
import ErrorStackParser from 'error-stack-parser';
@@ -1066,6 +1070,7 @@ export default async function preprocessData(
reactVersion: null,
schedulingEvents: [],
snapshots: [],
snapshotHeight: 0,
startTime: 0,
suspenseEvents: [],
thrownErrors: [],
@@ -1189,5 +1194,18 @@ export default async function preprocessData(
// Since processing is done in a worker, async work must complete before data is serialized and returned.
await Promise.all(state.asyncProcessingPromises);

// Now that all images have been loaded, let's figure out the display size we're going to use for our thumbnails:
// both the ones rendered to the canvas and the ones shown on hover.
if (profilerData.snapshots.length > 0) {
// NOTE We assume a static window size here, which is not necessarily true but should be for most cases.
// Regardless, Chrome also sets a single size/ratio and stick with it- so we'll do the same.
const snapshot = profilerData.snapshots[0];

profilerData.snapshotHeight = Math.min(
snapshot.height,
SNAPSHOT_MAX_HEIGHT,
);
}

return profilerData;
}
@@ -202,6 +202,7 @@ export type ReactProfilerData = {|
reactVersion: string | null,
schedulingEvents: SchedulingEvent[],
snapshots: Snapshot[],
snapshotHeight: number,
startTime: number,
suspenseEvents: SuspenseEvent[],
thrownErrors: ThrownError[],
@@ -197,7 +197,9 @@ export class View {
!sizeIsEmpty(this.visibleArea.size)
) {
this.layoutSubviews();
if (this._needsDisplay) this._needsDisplay = false;
if (this._needsDisplay) {
this._needsDisplay = false;
}
if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;

// Clip anything drawn by the view to prevent it from overflowing its visible area.

0 comments on commit 99a73ac

Please sign in to comment.