Skip to content

Commit 99a73ac

Browse files
author
Brian Vaughn
authored
Timeline: Improved snapshot view (#22706)
1 parent 327d5c4 commit 99a73ac

File tree

9 files changed

+113
-10
lines changed

9 files changed

+113
-10
lines changed

packages/react-devtools-timeline/src/CanvasPage.js

+2
Original file line numberDiff line numberDiff line change
@@ -753,8 +753,10 @@ function AutoSizedCanvas({
753753
<EventTooltip
754754
canvasRef={canvasRef}
755755
data={data}
756+
height={height}
756757
hoveredEvent={hoveredEvent}
757758
origin={mouseLocation}
759+
width={width}
758760
/>
759761
)}
760762
</Fragment>

packages/react-devtools-timeline/src/EventTooltip.js

+31-3
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ const MAX_TOOLTIP_TEXT_LENGTH = 60;
3434
type Props = {|
3535
canvasRef: {|current: HTMLCanvasElement | null|},
3636
data: ReactProfilerData,
37+
height: number,
3738
hoveredEvent: ReactHoverContextInfo | null,
3839
origin: Point,
40+
width: number,
3941
|};
4042

4143
function getSchedulingEventLabel(event: SchedulingEvent): string | null {
@@ -71,8 +73,10 @@ function getReactMeasureLabel(type): string | null {
7173
export default function EventTooltip({
7274
canvasRef,
7375
data,
76+
height,
7477
hoveredEvent,
7578
origin,
79+
width,
7680
}: Props) {
7781
const ref = useSmartTooltip({
7882
canvasRef,
@@ -111,7 +115,9 @@ export default function EventTooltip({
111115
<TooltipSchedulingEvent data={data} schedulingEvent={schedulingEvent} />
112116
);
113117
} else if (snapshot !== null) {
114-
content = <TooltipSnapshot snapshot={snapshot} />;
118+
content = (
119+
<TooltipSnapshot height={height} snapshot={snapshot} width={width} />
120+
);
115121
} else if (suspenseEvent !== null) {
116122
content = <TooltipSuspenseEvent suspenseEvent={suspenseEvent} />;
117123
} else if (measure !== null) {
@@ -333,12 +339,34 @@ const TooltipSchedulingEvent = ({
333339
);
334340
};
335341

336-
const TooltipSnapshot = ({snapshot}: {|snapshot: Snapshot|}) => {
342+
const TooltipSnapshot = ({
343+
height,
344+
snapshot,
345+
width,
346+
}: {|
347+
height: number,
348+
snapshot: Snapshot,
349+
width: number,
350+
|}) => {
351+
const aspectRatio = snapshot.width / snapshot.height;
352+
353+
// Zoomed in view should not be any bigger than the DevTools viewport.
354+
let safeWidth = snapshot.width;
355+
let safeHeight = snapshot.height;
356+
if (safeWidth > width) {
357+
safeWidth = width;
358+
safeHeight = safeWidth / aspectRatio;
359+
}
360+
if (safeHeight > height) {
361+
safeHeight = height;
362+
safeWidth = safeHeight * aspectRatio;
363+
}
364+
337365
return (
338366
<img
339367
className={styles.Image}
340368
src={snapshot.imageSource}
341-
style={{width: snapshot.width / 2, height: snapshot.height / 2}}
369+
style={{height: safeHeight, width: safeWidth}}
342370
/>
343371
);
344372
};

packages/react-devtools-timeline/src/constants.js

+2
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ export const REACT_TOTAL_NUM_LANES = 31;
1616

1717
// Increment this number any time a backwards breaking change is made to the profiler metadata.
1818
export const SCHEDULING_PROFILER_VERSION = 1;
19+
20+
export const SNAPSHOT_MAX_HEIGHT = 60;

packages/react-devtools-timeline/src/content-views/SnapshotsView.js

+42-4
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import {
2424
rectEqualToRect,
2525
View,
2626
} from '../view-base';
27-
import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';
27+
import {BORDER_SIZE, COLORS, SNAPSHOT_SCRUBBER_SIZE} from './constants';
2828

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

3131
export class SnapshotsView extends View {
32+
_hoverLocation: Point | null = null;
3233
_intrinsicSize: Size;
3334
_profilerData: ReactProfilerData;
3435

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

4041
this._intrinsicSize = {
4142
width: profilerData.duration,
42-
height: SNAPSHOT_HEIGHT,
43+
height: profilerData.snapshotHeight,
4344
};
4445
this._profilerData = profilerData;
4546
}
@@ -49,6 +50,7 @@ export class SnapshotsView extends View {
4950
}
5051

5152
draw(context: CanvasRenderingContext2D) {
53+
const snapshotHeight = this._profilerData.snapshotHeight;
5254
const {visibleArea} = this;
5355

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

75-
const scaledHeight = SNAPSHOT_HEIGHT;
76-
const scaledWidth = (snapshot.width * SNAPSHOT_HEIGHT) / snapshot.height;
77+
const scaledHeight = snapshotHeight;
78+
const scaledWidth = (snapshot.width * snapshotHeight) / snapshot.height;
7779

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

9799
x += scaledWidth + BORDER_SIZE;
98100
}
101+
102+
const hoverLocation = this._hoverLocation;
103+
if (hoverLocation !== null) {
104+
const scrubberWidth = SNAPSHOT_SCRUBBER_SIZE + BORDER_SIZE * 2;
105+
const scrubberOffset = scrubberWidth / 2;
106+
107+
context.fillStyle = COLORS.SCRUBBER_BORDER;
108+
context.fillRect(
109+
hoverLocation.x - scrubberOffset,
110+
visibleArea.origin.y,
111+
scrubberWidth,
112+
visibleArea.size.height,
113+
);
114+
115+
context.fillStyle = COLORS.SCRUBBER_BACKGROUND;
116+
context.fillRect(
117+
hoverLocation.x - scrubberOffset + BORDER_SIZE,
118+
visibleArea.origin.y,
119+
SNAPSHOT_SCRUBBER_SIZE,
120+
visibleArea.size.height,
121+
);
122+
}
99123
}
100124

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

210234
if (!rectContainsPoint(location, visibleArea)) {
235+
if (this._hoverLocation !== null) {
236+
this._hoverLocation = null;
237+
238+
this.setNeedsDisplay();
239+
}
240+
211241
onHover(null);
212242
return;
213243
}
214244

215245
const snapshot = this._findClosestSnapshot(location.x);
216246
if (snapshot !== null) {
247+
this._hoverLocation = location;
248+
217249
onHover(snapshot);
218250
} else {
251+
this._hoverLocation = null;
252+
219253
onHover(null);
220254
}
255+
256+
// Any time the mouse moves within the boundaries of this view, we need to re-render.
257+
// This is because we draw a scrubbing bar that shows the location corresponding to the current tooltip.
258+
this.setNeedsDisplay();
221259
}
222260
}

packages/react-devtools-timeline/src/content-views/constants.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const REACT_MEASURE_HEIGHT = 14;
2424
export const BORDER_SIZE = 1 / DPR;
2525
export const FLAMECHART_FRAME_HEIGHT = 14;
2626
export const TEXT_PADDING = 3;
27-
export const SNAPSHOT_HEIGHT = 35;
27+
export const SNAPSHOT_SCRUBBER_SIZE = 3;
2828

2929
export const INTERVAL_TIMES = [
3030
1,
@@ -89,6 +89,8 @@ export let COLORS = {
8989
REACT_THROWN_ERROR_HOVER: '',
9090
REACT_WORK_BORDER: '',
9191
SCROLL_CARET: '',
92+
SCRUBBER_BACKGROUND: '',
93+
SCRUBBER_BORDER: '',
9294
TEXT_COLOR: '',
9395
TEXT_DIM_COLOR: '',
9496
TIME_MARKER_LABEL: '',
@@ -230,6 +232,12 @@ export function updateColorsToMatchTheme(element: Element): boolean {
230232
'--color-timeline-react-work-border',
231233
),
232234
SCROLL_CARET: computedStyle.getPropertyValue('--color-scroll-caret'),
235+
SCRUBBER_BACKGROUND: computedStyle.getPropertyValue(
236+
'--color-timeline-react-suspense-rejected',
237+
),
238+
SCRUBBER_BORDER: computedStyle.getPropertyValue(
239+
'--color-timeline-text-color',
240+
),
233241
TEXT_COLOR: computedStyle.getPropertyValue('--color-timeline-text-color'),
234242
TEXT_DIM_COLOR: computedStyle.getPropertyValue(
235243
'--color-timeline-text-dim-color',

packages/react-devtools-timeline/src/import-worker/__tests__/preprocessData-test.internal.js

+4
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ describe('preprocessData', () => {
354354
"otherUserTimingMarks": Array [],
355355
"reactVersion": "17.0.3",
356356
"schedulingEvents": Array [],
357+
"snapshotHeight": 0,
357358
"snapshots": Array [],
358359
"startTime": 1,
359360
"suspenseEvents": Array [],
@@ -572,6 +573,7 @@ describe('preprocessData', () => {
572573
"warning": null,
573574
},
574575
],
576+
"snapshotHeight": 0,
575577
"snapshots": Array [],
576578
"startTime": 1,
577579
"suspenseEvents": Array [],
@@ -765,6 +767,7 @@ describe('preprocessData', () => {
765767
"warning": null,
766768
},
767769
],
770+
"snapshotHeight": 0,
768771
"snapshots": Array [],
769772
"startTime": 4,
770773
"suspenseEvents": Array [],
@@ -1129,6 +1132,7 @@ describe('preprocessData', () => {
11291132
"warning": null,
11301133
},
11311134
],
1135+
"snapshotHeight": 0,
11321136
"snapshots": Array [],
11331137
"startTime": 4,
11341138
"suspenseEvents": Array [],

packages/react-devtools-timeline/src/import-worker/preprocessData.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import type {
2929
SchedulingEvent,
3030
SuspenseEvent,
3131
} from '../types';
32-
import {REACT_TOTAL_NUM_LANES, SCHEDULING_PROFILER_VERSION} from '../constants';
32+
import {
33+
REACT_TOTAL_NUM_LANES,
34+
SCHEDULING_PROFILER_VERSION,
35+
SNAPSHOT_MAX_HEIGHT,
36+
} from '../constants';
3337
import InvalidProfileError from './InvalidProfileError';
3438
import {getBatchRange} from '../utils/getBatchRange';
3539
import ErrorStackParser from 'error-stack-parser';
@@ -1066,6 +1070,7 @@ export default async function preprocessData(
10661070
reactVersion: null,
10671071
schedulingEvents: [],
10681072
snapshots: [],
1073+
snapshotHeight: 0,
10691074
startTime: 0,
10701075
suspenseEvents: [],
10711076
thrownErrors: [],
@@ -1189,5 +1194,18 @@ export default async function preprocessData(
11891194
// Since processing is done in a worker, async work must complete before data is serialized and returned.
11901195
await Promise.all(state.asyncProcessingPromises);
11911196

1197+
// Now that all images have been loaded, let's figure out the display size we're going to use for our thumbnails:
1198+
// both the ones rendered to the canvas and the ones shown on hover.
1199+
if (profilerData.snapshots.length > 0) {
1200+
// NOTE We assume a static window size here, which is not necessarily true but should be for most cases.
1201+
// Regardless, Chrome also sets a single size/ratio and stick with it- so we'll do the same.
1202+
const snapshot = profilerData.snapshots[0];
1203+
1204+
profilerData.snapshotHeight = Math.min(
1205+
snapshot.height,
1206+
SNAPSHOT_MAX_HEIGHT,
1207+
);
1208+
}
1209+
11921210
return profilerData;
11931211
}

packages/react-devtools-timeline/src/types.js

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export type ReactProfilerData = {|
202202
reactVersion: string | null,
203203
schedulingEvents: SchedulingEvent[],
204204
snapshots: Snapshot[],
205+
snapshotHeight: number,
205206
startTime: number,
206207
suspenseEvents: SuspenseEvent[],
207208
thrownErrors: ThrownError[],

packages/react-devtools-timeline/src/view-base/View.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ export class View {
197197
!sizeIsEmpty(this.visibleArea.size)
198198
) {
199199
this.layoutSubviews();
200-
if (this._needsDisplay) this._needsDisplay = false;
200+
if (this._needsDisplay) {
201+
this._needsDisplay = false;
202+
}
201203
if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
202204

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

0 commit comments

Comments
 (0)