Skip to content

Commit 4b7e76f

Browse files
Brian Vaughnzhengjitf
Brian Vaughn
authored andcommitted
[DevTools] Add screenshots to Scheduling Profiler (facebook#22088)
1 parent 5b0eef5 commit 4b7e76f

File tree

9 files changed

+375
-41
lines changed

9 files changed

+375
-41
lines changed

packages/react-devtools-scheduling-profiler/src/CanvasPage.js

+44
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
NativeEventsView,
4747
ReactMeasuresView,
4848
SchedulingEventsView,
49+
SnapshotsView,
4950
SuspenseEventsView,
5051
TimeAxisMarkersView,
5152
UserTimingMarksView,
@@ -157,6 +158,7 @@ function AutoSizedCanvas({
157158
const componentMeasuresViewRef = useRef(null);
158159
const reactMeasuresViewRef = useRef(null);
159160
const flamechartViewRef = useRef(null);
161+
const snapshotsViewRef = useRef(null);
160162

161163
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
162164

@@ -304,6 +306,18 @@ function AutoSizedCanvas({
304306
);
305307
}
306308

309+
let snapshotsViewWrapper = null;
310+
if (data.snapshots.length > 0) {
311+
const snapshotsView = new SnapshotsView(surface, defaultFrame, data);
312+
snapshotsViewRef.current = snapshotsView;
313+
snapshotsViewWrapper = createViewHelper(
314+
snapshotsView,
315+
'snapshots',
316+
true,
317+
true,
318+
);
319+
}
320+
307321
const flamechartView = new FlamechartView(
308322
surface,
309323
defaultFrame,
@@ -340,6 +354,9 @@ function AutoSizedCanvas({
340354
if (componentMeasuresViewWrapper !== null) {
341355
rootView.addSubview(componentMeasuresViewWrapper);
342356
}
357+
if (snapshotsViewWrapper !== null) {
358+
rootView.addSubview(snapshotsViewWrapper);
359+
}
343360
rootView.addSubview(flamechartViewWrapper);
344361

345362
const verticalScrollOverflowView = new VerticalScrollOverflowView(
@@ -389,6 +406,7 @@ function AutoSizedCanvas({
389406
measure: null,
390407
nativeEvent: null,
391408
schedulingEvent: null,
409+
snapshot: null,
392410
suspenseEvent: null,
393411
userTimingMark: null,
394412
};
@@ -447,6 +465,7 @@ function AutoSizedCanvas({
447465
measure: null,
448466
nativeEvent: null,
449467
schedulingEvent: null,
468+
snapshot: null,
450469
suspenseEvent: null,
451470
userTimingMark,
452471
});
@@ -465,6 +484,7 @@ function AutoSizedCanvas({
465484
measure: null,
466485
nativeEvent,
467486
schedulingEvent: null,
487+
snapshot: null,
468488
suspenseEvent: null,
469489
userTimingMark: null,
470490
});
@@ -483,6 +503,7 @@ function AutoSizedCanvas({
483503
measure: null,
484504
nativeEvent: null,
485505
schedulingEvent,
506+
snapshot: null,
486507
suspenseEvent: null,
487508
userTimingMark: null,
488509
});
@@ -501,6 +522,7 @@ function AutoSizedCanvas({
501522
measure: null,
502523
nativeEvent: null,
503524
schedulingEvent: null,
525+
snapshot: null,
504526
suspenseEvent,
505527
userTimingMark: null,
506528
});
@@ -519,6 +541,7 @@ function AutoSizedCanvas({
519541
measure,
520542
nativeEvent: null,
521543
schedulingEvent: null,
544+
snapshot: null,
522545
suspenseEvent: null,
523546
userTimingMark: null,
524547
});
@@ -540,6 +563,26 @@ function AutoSizedCanvas({
540563
measure: null,
541564
nativeEvent: null,
542565
schedulingEvent: null,
566+
snapshot: null,
567+
suspenseEvent: null,
568+
userTimingMark: null,
569+
});
570+
}
571+
};
572+
}
573+
574+
const {current: snapshotsView} = snapshotsViewRef;
575+
if (snapshotsView) {
576+
snapshotsView.onHover = snapshot => {
577+
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
578+
setHoveredEvent({
579+
componentMeasure: null,
580+
data,
581+
flamechartStackFrame: null,
582+
measure: null,
583+
nativeEvent: null,
584+
schedulingEvent: null,
585+
snapshot,
543586
suspenseEvent: null,
544587
userTimingMark: null,
545588
});
@@ -561,6 +604,7 @@ function AutoSizedCanvas({
561604
measure: null,
562605
nativeEvent: null,
563606
schedulingEvent: null,
607+
snapshot: null,
564608
suspenseEvent: null,
565609
userTimingMark: null,
566610
});

packages/react-devtools-scheduling-profiler/src/EventTooltip.js

+21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
ReactProfilerData,
1818
Return,
1919
SchedulingEvent,
20+
Snapshot,
2021
SuspenseEvent,
2122
UserTimingMark,
2223
} from './types';
@@ -87,6 +88,7 @@ export default function EventTooltip({
8788
measure,
8889
nativeEvent,
8990
schedulingEvent,
91+
snapshot,
9092
suspenseEvent,
9193
userTimingMark,
9294
} = hoveredEvent;
@@ -110,6 +112,8 @@ export default function EventTooltip({
110112
tooltipRef={tooltipRef}
111113
/>
112114
);
115+
} else if (snapshot !== null) {
116+
return <TooltipSnapshot snapshot={snapshot} tooltipRef={tooltipRef} />;
113117
} else if (suspenseEvent !== null) {
114118
return (
115119
<TooltipSuspenseEvent
@@ -301,6 +305,23 @@ const TooltipSchedulingEvent = ({
301305
);
302306
};
303307

308+
const TooltipSnapshot = ({
309+
snapshot,
310+
tooltipRef,
311+
}: {
312+
snapshot: Snapshot,
313+
tooltipRef: Return<typeof useRef>,
314+
}) => {
315+
return (
316+
<div className={styles.Tooltip} ref={tooltipRef}>
317+
<img
318+
src={snapshot.imageSource}
319+
style={{width: snapshot.width / 2, height: snapshot.height / 2}}
320+
/>
321+
</div>
322+
);
323+
};
324+
304325
const TooltipSuspenseEvent = ({
305326
suspenseEvent,
306327
tooltipRef,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Snapshot, ReactProfilerData} from '../types';
11+
import type {
12+
Interaction,
13+
MouseMoveInteraction,
14+
Rect,
15+
Size,
16+
Surface,
17+
ViewRefs,
18+
} from '../view-base';
19+
20+
import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
21+
import {
22+
intersectionOfRects,
23+
rectContainsPoint,
24+
rectEqualToRect,
25+
View,
26+
} from '../view-base';
27+
import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';
28+
29+
type OnHover = (node: Snapshot | null) => void;
30+
31+
export class SnapshotsView extends View {
32+
_intrinsicSize: Size;
33+
_profilerData: ReactProfilerData;
34+
35+
onHover: OnHover | null = null;
36+
37+
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
38+
super(surface, frame);
39+
40+
this._intrinsicSize = {
41+
width: profilerData.duration,
42+
height: SNAPSHOT_HEIGHT,
43+
};
44+
this._profilerData = profilerData;
45+
}
46+
47+
desiredSize() {
48+
return this._intrinsicSize;
49+
}
50+
51+
draw(context: CanvasRenderingContext2D) {
52+
const {visibleArea} = this;
53+
54+
context.fillStyle = COLORS.BACKGROUND;
55+
context.fillRect(
56+
visibleArea.origin.x,
57+
visibleArea.origin.y,
58+
visibleArea.size.width,
59+
visibleArea.size.height,
60+
);
61+
62+
const y = visibleArea.origin.y;
63+
64+
let x = visibleArea.origin.x;
65+
66+
// Rather than drawing each snapshot where it occured,
67+
// draw them at fixed intervals and just show the nearest one.
68+
while (x < visibleArea.origin.x + visibleArea.size.width) {
69+
const snapshot = this._findClosestSnapshot(x);
70+
71+
const scaledHeight = SNAPSHOT_HEIGHT;
72+
const scaledWidth = (snapshot.width * SNAPSHOT_HEIGHT) / snapshot.height;
73+
74+
const imageRect: Rect = {
75+
origin: {
76+
x,
77+
y,
78+
},
79+
size: {width: scaledWidth, height: scaledHeight},
80+
};
81+
82+
// Lazily create and cache Image objects as we render a snapsho for the first time.
83+
if (snapshot.image === null) {
84+
const img = (snapshot.image = new Image());
85+
img.onload = () => {
86+
this._drawSnapshotImage(context, snapshot, imageRect);
87+
};
88+
img.src = snapshot.imageSource;
89+
} else {
90+
this._drawSnapshotImage(context, snapshot, imageRect);
91+
}
92+
93+
x += scaledWidth + BORDER_SIZE;
94+
}
95+
}
96+
97+
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
98+
switch (interaction.type) {
99+
case 'mousemove':
100+
this._handleMouseMove(interaction, viewRefs);
101+
break;
102+
}
103+
}
104+
105+
_drawSnapshotImage(
106+
context: CanvasRenderingContext2D,
107+
snapshot: Snapshot,
108+
imageRect: Rect,
109+
) {
110+
const visibleArea = this.visibleArea;
111+
112+
// Prevent snapshot from visibly overflowing its container when clipped.
113+
const shouldClip = !rectEqualToRect(imageRect, visibleArea);
114+
if (shouldClip) {
115+
const clippedRect = intersectionOfRects(imageRect, visibleArea);
116+
context.save();
117+
context.beginPath();
118+
context.rect(
119+
clippedRect.origin.x,
120+
clippedRect.origin.y,
121+
clippedRect.size.width,
122+
clippedRect.size.height,
123+
);
124+
context.closePath();
125+
context.clip();
126+
}
127+
128+
// $FlowFixMe Flow doesn't know about the 9 argument variant of drawImage()
129+
context.drawImage(
130+
snapshot.image,
131+
132+
// Image coordinates
133+
0,
134+
0,
135+
136+
// Native image size
137+
snapshot.width,
138+
snapshot.height,
139+
140+
// Canvas coordinates
141+
imageRect.origin.x,
142+
imageRect.origin.y,
143+
144+
// Scaled image size
145+
imageRect.size.width,
146+
imageRect.size.height,
147+
);
148+
149+
if (shouldClip) {
150+
context.restore();
151+
}
152+
}
153+
154+
_findClosestSnapshot(x: number): Snapshot {
155+
const frame = this.frame;
156+
const scaleFactor = positioningScaleFactor(
157+
this._intrinsicSize.width,
158+
frame,
159+
);
160+
161+
const snapshots = this._profilerData.snapshots;
162+
163+
let startIndex = 0;
164+
let stopIndex = snapshots.length - 1;
165+
while (startIndex <= stopIndex) {
166+
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
167+
const snapshot = snapshots[currentIndex];
168+
const {timestamp} = snapshot;
169+
170+
const snapshotX = Math.floor(
171+
timestampToPosition(timestamp, scaleFactor, frame),
172+
);
173+
174+
if (x < snapshotX) {
175+
stopIndex = currentIndex - 1;
176+
} else {
177+
startIndex = currentIndex + 1;
178+
}
179+
}
180+
181+
return snapshots[stopIndex];
182+
}
183+
184+
/**
185+
* @private
186+
*/
187+
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
188+
const {onHover, visibleArea} = this;
189+
if (!onHover) {
190+
return;
191+
}
192+
193+
const {location} = interaction.payload;
194+
if (!rectContainsPoint(location, visibleArea)) {
195+
onHover(null);
196+
return;
197+
}
198+
199+
const snapshot = this._findClosestSnapshot(location.x);
200+
if (snapshot) {
201+
this.currentCursor = 'context-menu';
202+
viewRefs.hoveredView = this;
203+
onHover(snapshot);
204+
} else {
205+
onHover(null);
206+
}
207+
}
208+
}

packages/react-devtools-scheduling-profiler/src/content-views/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const REACT_MEASURE_HEIGHT = 14;
2323
export const BORDER_SIZE = 1;
2424
export const FLAMECHART_FRAME_HEIGHT = 14;
2525
export const TEXT_PADDING = 3;
26+
export const SNAPSHOT_HEIGHT = 50;
2627

2728
export const INTERVAL_TIMES = [
2829
1,

0 commit comments

Comments
 (0)