@samitouri / QOS-React-2 / commits / 99a73ac670

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.