Timeline: Improved snapshot view (#22706)
Brian Vaughn committed
Nov 8, 2021 at 12:28 UTC
99a73ac670ce8d71ae8a690e9795d35cbdfa20b6
9 files changed
+113
-10
packages/react-devtools-timeline/src/CanvasPage.js
+2
@@ -753,8 +753,10 @@ function AutoSizedCanvas({
753
<EventTooltip
754
canvasRef={canvasRef}
755
data={data}
756
+ height={height}
757
hoveredEvent={hoveredEvent}
758
origin={mouseLocation}
759
+ width={width}
760
/>
761
)}
762
</Fragment>
packages/react-devtools-timeline/src/EventTooltip.js
+31
-3
@@ -34,8 +34,10 @@ const MAX_TOOLTIP_TEXT_LENGTH = 60;
34
type Props = {|
35
canvasRef: {|current: HTMLCanvasElement | null|},
36
data: ReactProfilerData,
37
+ height: number,
38
hoveredEvent: ReactHoverContextInfo | null,
39
origin: Point,
40
+ width: number,
41
|};
42
43
function getSchedulingEventLabel(event: SchedulingEvent): string | null {
@@ -71,8 +73,10 @@ function getReactMeasureLabel(type): string | null {
73
export default function EventTooltip({
74
canvasRef,
75
data,
76
+ height,
77
hoveredEvent,
78
origin,
79
+ width,
80
}: Props) {
81
const ref = useSmartTooltip({
82
canvasRef,
@@ -111,7 +115,9 @@ export default function EventTooltip({
115
<TooltipSchedulingEvent data={data} schedulingEvent={schedulingEvent} />
116
);
117
} else if (snapshot !== null) {
114
- content = <TooltipSnapshot snapshot={snapshot} />;
118
+ content = (
119
+ <TooltipSnapshot height={height} snapshot={snapshot} width={width} />
120
+ );
121
} else if (suspenseEvent !== null) {
122
content = <TooltipSuspenseEvent suspenseEvent={suspenseEvent} />;
123
} else if (measure !== null) {
@@ -333,12 +339,34 @@ const TooltipSchedulingEvent = ({
339
);
340
};
341
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
+
365
return (
366
<img
367
className={styles.Image}
368
src={snapshot.imageSource}
341
- style={{width: snapshot.width / 2, height: snapshot.height / 2}}
369
+ style={{height: safeHeight, width: safeWidth}}
370
/>
371
);
372
};
packages/react-devtools-timeline/src/constants.js
+2
@@ -16,3 +16,5 @@ export const REACT_TOTAL_NUM_LANES = 31;
16
17
// Increment this number any time a backwards breaking change is made to the profiler metadata.
18
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
@@ -24,11 +24,12 @@ import {
24
rectEqualToRect,
25
View,
26
} from '../view-base';
27
-import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';
27
+import {BORDER_SIZE, COLORS, SNAPSHOT_SCRUBBER_SIZE} from './constants';
28
29
type OnHover = (node: Snapshot | null) => void;
30
31
export class SnapshotsView extends View {
32
+ _hoverLocation: Point | null = null;
33
_intrinsicSize: Size;
34
_profilerData: ReactProfilerData;
35
@@ -39,7 +40,7 @@ export class SnapshotsView extends View {
40
41
this._intrinsicSize = {
42
width: profilerData.duration,
42
- height: SNAPSHOT_HEIGHT,
43
+ height: profilerData.snapshotHeight,
44
};
45
this._profilerData = profilerData;
46
}
@@ -49,6 +50,7 @@ export class SnapshotsView extends View {
50
}
51
52
draw(context: CanvasRenderingContext2D) {
53
+ const snapshotHeight = this._profilerData.snapshotHeight;
54
const {visibleArea} = this;
55
56
context.fillStyle = COLORS.BACKGROUND;
@@ -72,8 +74,8 @@ export class SnapshotsView extends View {
74
break;
75
}
76
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;
79
80
const imageRect: Rect = {
81
origin: {
@@ -96,6 +98,28 @@ export class SnapshotsView extends View {
98
99
x += scaledWidth + BORDER_SIZE;
100
}
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
+ }
123
}
124
125
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
@@ -208,15 +232,29 @@ export class SnapshotsView extends View {
232
}
233
234
if (!rectContainsPoint(location, visibleArea)) {
235
+ if (this._hoverLocation !== null) {
236
+ this._hoverLocation = null;
237
+
238
+ this.setNeedsDisplay();
239
+ }
240
+
241
onHover(null);
242
return;
243
}
244
245
const snapshot = this._findClosestSnapshot(location.x);
246
if (snapshot !== null) {
247
+ this._hoverLocation = location;
248
+
249
onHover(snapshot);
250
} else {
251
+ this._hoverLocation = null;
252
+
253
onHover(null);
254
}
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();
259
}
260
}
packages/react-devtools-timeline/src/content-views/constants.js
+9
-1
@@ -24,7 +24,7 @@ export const REACT_MEASURE_HEIGHT = 14;
24
export const BORDER_SIZE = 1 / DPR;
25
export const FLAMECHART_FRAME_HEIGHT = 14;
26
export const TEXT_PADDING = 3;
27
-export const SNAPSHOT_HEIGHT = 35;
27
+export const SNAPSHOT_SCRUBBER_SIZE = 3;
28
29
export const INTERVAL_TIMES = [
30
1,
@@ -89,6 +89,8 @@ export let COLORS = {
89
REACT_THROWN_ERROR_HOVER: '',
90
REACT_WORK_BORDER: '',
91
SCROLL_CARET: '',
92
+ SCRUBBER_BACKGROUND: '',
93
+ SCRUBBER_BORDER: '',
94
TEXT_COLOR: '',
95
TEXT_DIM_COLOR: '',
96
TIME_MARKER_LABEL: '',
@@ -230,6 +232,12 @@ export function updateColorsToMatchTheme(element: Element): boolean {
232
'--color-timeline-react-work-border',
233
),
234
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
+ ),
241
TEXT_COLOR: computedStyle.getPropertyValue('--color-timeline-text-color'),
242
TEXT_DIM_COLOR: computedStyle.getPropertyValue(
243
'--color-timeline-text-dim-color',
packages/react-devtools-timeline/src/import-worker/__tests__/preprocessData-test.internal.js
+4
@@ -354,6 +354,7 @@ describe('preprocessData', () => {
354
"otherUserTimingMarks": Array [],
355
"reactVersion": "17.0.3",
356
"schedulingEvents": Array [],
357
+ "snapshotHeight": 0,
358
"snapshots": Array [],
359
"startTime": 1,
360
"suspenseEvents": Array [],
@@ -572,6 +573,7 @@ describe('preprocessData', () => {
573
"warning": null,
574
},
575
],
576
+ "snapshotHeight": 0,
577
"snapshots": Array [],
578
"startTime": 1,
579
"suspenseEvents": Array [],
@@ -765,6 +767,7 @@ describe('preprocessData', () => {
767
"warning": null,
768
},
769
],
770
+ "snapshotHeight": 0,
771
"snapshots": Array [],
772
"startTime": 4,
773
"suspenseEvents": Array [],
@@ -1129,6 +1132,7 @@ describe('preprocessData', () => {
1132
"warning": null,
1133
},
1134
],
1135
+ "snapshotHeight": 0,
1136
"snapshots": Array [],
1137
"startTime": 4,
1138
"suspenseEvents": Array [],
packages/react-devtools-timeline/src/import-worker/preprocessData.js
+19
-1
@@ -29,7 +29,11 @@ import type {
29
SchedulingEvent,
30
SuspenseEvent,
31
} 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';
37
import InvalidProfileError from './InvalidProfileError';
38
import {getBatchRange} from '../utils/getBatchRange';
39
import ErrorStackParser from 'error-stack-parser';
@@ -1066,6 +1070,7 @@ export default async function preprocessData(
1070
reactVersion: null,
1071
schedulingEvents: [],
1072
snapshots: [],
1073
+ snapshotHeight: 0,
1074
startTime: 0,
1075
suspenseEvents: [],
1076
thrownErrors: [],
@@ -1189,5 +1194,18 @@ export default async function preprocessData(
1194
// Since processing is done in a worker, async work must complete before data is serialized and returned.
1195
await Promise.all(state.asyncProcessingPromises);
1196
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
+
1210
return profilerData;
1211
}
packages/react-devtools-timeline/src/types.js
+1
@@ -202,6 +202,7 @@ export type ReactProfilerData = {|
202
reactVersion: string | null,
203
schedulingEvents: SchedulingEvent[],
204
snapshots: Snapshot[],
205
+ snapshotHeight: number,
206
startTime: number,
207
suspenseEvents: SuspenseEvent[],
208
thrownErrors: ThrownError[],
packages/react-devtools-timeline/src/view-base/View.js
+3
-1
@@ -197,7 +197,9 @@ export class View {
197
!sizeIsEmpty(this.visibleArea.size)
198
) {
199
this.layoutSubviews();
200
- if (this._needsDisplay) this._needsDisplay = false;
200
+ if (this._needsDisplay) {
201
+ this._needsDisplay = false;
202
+ }
203
if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
204
205
// Clip anything drawn by the view to prevent it from overflowing its visible area.