Skip to content

Commit

Permalink
[DevTools] Add screenshots to Scheduling Profiler (facebook#22088)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn authored and zhengjitf committed Apr 15, 2022
1 parent 5b0eef5 commit 4b7e76f
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 41 deletions.
44 changes: 44 additions & 0 deletions packages/react-devtools-scheduling-profiler/src/CanvasPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
NativeEventsView,
ReactMeasuresView,
SchedulingEventsView,
SnapshotsView,
SuspenseEventsView,
TimeAxisMarkersView,
UserTimingMarksView,
Expand Down Expand Up @@ -157,6 +158,7 @@ function AutoSizedCanvas({
const componentMeasuresViewRef = useRef(null);
const reactMeasuresViewRef = useRef(null);
const flamechartViewRef = useRef(null);
const snapshotsViewRef = useRef(null);

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

Expand Down Expand Up @@ -304,6 +306,18 @@ function AutoSizedCanvas({
);
}

let snapshotsViewWrapper = null;
if (data.snapshots.length > 0) {
const snapshotsView = new SnapshotsView(surface, defaultFrame, data);
snapshotsViewRef.current = snapshotsView;
snapshotsViewWrapper = createViewHelper(
snapshotsView,
'snapshots',
true,
true,
);
}

const flamechartView = new FlamechartView(
surface,
defaultFrame,
Expand Down Expand Up @@ -340,6 +354,9 @@ function AutoSizedCanvas({
if (componentMeasuresViewWrapper !== null) {
rootView.addSubview(componentMeasuresViewWrapper);
}
if (snapshotsViewWrapper !== null) {
rootView.addSubview(snapshotsViewWrapper);
}
rootView.addSubview(flamechartViewWrapper);

const verticalScrollOverflowView = new VerticalScrollOverflowView(
Expand Down Expand Up @@ -389,6 +406,7 @@ function AutoSizedCanvas({
measure: null,
nativeEvent: null,
schedulingEvent: null,
snapshot: null,
suspenseEvent: null,
userTimingMark: null,
};
Expand Down Expand Up @@ -447,6 +465,7 @@ function AutoSizedCanvas({
measure: null,
nativeEvent: null,
schedulingEvent: null,
snapshot: null,
suspenseEvent: null,
userTimingMark,
});
Expand All @@ -465,6 +484,7 @@ function AutoSizedCanvas({
measure: null,
nativeEvent,
schedulingEvent: null,
snapshot: null,
suspenseEvent: null,
userTimingMark: null,
});
Expand All @@ -483,6 +503,7 @@ function AutoSizedCanvas({
measure: null,
nativeEvent: null,
schedulingEvent,
snapshot: null,
suspenseEvent: null,
userTimingMark: null,
});
Expand All @@ -501,6 +522,7 @@ function AutoSizedCanvas({
measure: null,
nativeEvent: null,
schedulingEvent: null,
snapshot: null,
suspenseEvent,
userTimingMark: null,
});
Expand All @@ -519,6 +541,7 @@ function AutoSizedCanvas({
measure,
nativeEvent: null,
schedulingEvent: null,
snapshot: null,
suspenseEvent: null,
userTimingMark: null,
});
Expand All @@ -540,6 +563,26 @@ function AutoSizedCanvas({
measure: null,
nativeEvent: null,
schedulingEvent: null,
snapshot: null,
suspenseEvent: null,
userTimingMark: null,
});
}
};
}

const {current: snapshotsView} = snapshotsViewRef;
if (snapshotsView) {
snapshotsView.onHover = snapshot => {
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
setHoveredEvent({
componentMeasure: null,
data,
flamechartStackFrame: null,
measure: null,
nativeEvent: null,
schedulingEvent: null,
snapshot,
suspenseEvent: null,
userTimingMark: null,
});
Expand All @@ -561,6 +604,7 @@ function AutoSizedCanvas({
measure: null,
nativeEvent: null,
schedulingEvent: null,
snapshot: null,
suspenseEvent: null,
userTimingMark: null,
});
Expand Down
21 changes: 21 additions & 0 deletions packages/react-devtools-scheduling-profiler/src/EventTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
ReactProfilerData,
Return,
SchedulingEvent,
Snapshot,
SuspenseEvent,
UserTimingMark,
} from './types';
Expand Down Expand Up @@ -87,6 +88,7 @@ export default function EventTooltip({
measure,
nativeEvent,
schedulingEvent,
snapshot,
suspenseEvent,
userTimingMark,
} = hoveredEvent;
Expand All @@ -110,6 +112,8 @@ export default function EventTooltip({
tooltipRef={tooltipRef}
/>
);
} else if (snapshot !== null) {
return <TooltipSnapshot snapshot={snapshot} tooltipRef={tooltipRef} />;
} else if (suspenseEvent !== null) {
return (
<TooltipSuspenseEvent
Expand Down Expand Up @@ -301,6 +305,23 @@ const TooltipSchedulingEvent = ({
);
};

const TooltipSnapshot = ({
snapshot,
tooltipRef,
}: {
snapshot: Snapshot,
tooltipRef: Return<typeof useRef>,
}) => {
return (
<div className={styles.Tooltip} ref={tooltipRef}>
<img
src={snapshot.imageSource}
style={{width: snapshot.width / 2, height: snapshot.height / 2}}
/>
</div>
);
};

const TooltipSuspenseEvent = ({
suspenseEvent,
tooltipRef,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {Snapshot, ReactProfilerData} from '../types';
import type {
Interaction,
MouseMoveInteraction,
Rect,
Size,
Surface,
ViewRefs,
} from '../view-base';

import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
import {
intersectionOfRects,
rectContainsPoint,
rectEqualToRect,
View,
} from '../view-base';
import {BORDER_SIZE, COLORS, SNAPSHOT_HEIGHT} from './constants';

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

export class SnapshotsView extends View {
_intrinsicSize: Size;
_profilerData: ReactProfilerData;

onHover: OnHover | null = null;

constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
super(surface, frame);

this._intrinsicSize = {
width: profilerData.duration,
height: SNAPSHOT_HEIGHT,
};
this._profilerData = profilerData;
}

desiredSize() {
return this._intrinsicSize;
}

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

context.fillStyle = COLORS.BACKGROUND;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y,
visibleArea.size.width,
visibleArea.size.height,
);

const y = visibleArea.origin.y;

let x = visibleArea.origin.x;

// Rather than drawing each snapshot where it occured,
// draw them at fixed intervals and just show the nearest one.
while (x < visibleArea.origin.x + visibleArea.size.width) {
const snapshot = this._findClosestSnapshot(x);

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

const imageRect: Rect = {
origin: {
x,
y,
},
size: {width: scaledWidth, height: scaledHeight},
};

// Lazily create and cache Image objects as we render a snapsho for the first time.
if (snapshot.image === null) {
const img = (snapshot.image = new Image());
img.onload = () => {
this._drawSnapshotImage(context, snapshot, imageRect);
};
img.src = snapshot.imageSource;
} else {
this._drawSnapshotImage(context, snapshot, imageRect);
}

x += scaledWidth + BORDER_SIZE;
}
}

handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
switch (interaction.type) {
case 'mousemove':
this._handleMouseMove(interaction, viewRefs);
break;
}
}

_drawSnapshotImage(
context: CanvasRenderingContext2D,
snapshot: Snapshot,
imageRect: Rect,
) {
const visibleArea = this.visibleArea;

// Prevent snapshot from visibly overflowing its container when clipped.
const shouldClip = !rectEqualToRect(imageRect, visibleArea);
if (shouldClip) {
const clippedRect = intersectionOfRects(imageRect, visibleArea);
context.save();
context.beginPath();
context.rect(
clippedRect.origin.x,
clippedRect.origin.y,
clippedRect.size.width,
clippedRect.size.height,
);
context.closePath();
context.clip();
}

// $FlowFixMe Flow doesn't know about the 9 argument variant of drawImage()
context.drawImage(
snapshot.image,

// Image coordinates
0,
0,

// Native image size
snapshot.width,
snapshot.height,

// Canvas coordinates
imageRect.origin.x,
imageRect.origin.y,

// Scaled image size
imageRect.size.width,
imageRect.size.height,
);

if (shouldClip) {
context.restore();
}
}

_findClosestSnapshot(x: number): Snapshot {
const frame = this.frame;
const scaleFactor = positioningScaleFactor(
this._intrinsicSize.width,
frame,
);

const snapshots = this._profilerData.snapshots;

let startIndex = 0;
let stopIndex = snapshots.length - 1;
while (startIndex <= stopIndex) {
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
const snapshot = snapshots[currentIndex];
const {timestamp} = snapshot;

const snapshotX = Math.floor(
timestampToPosition(timestamp, scaleFactor, frame),
);

if (x < snapshotX) {
stopIndex = currentIndex - 1;
} else {
startIndex = currentIndex + 1;
}
}

return snapshots[stopIndex];
}

/**
* @private
*/
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
const {onHover, visibleArea} = this;
if (!onHover) {
return;
}

const {location} = interaction.payload;
if (!rectContainsPoint(location, visibleArea)) {
onHover(null);
return;
}

const snapshot = this._findClosestSnapshot(location.x);
if (snapshot) {
this.currentCursor = 'context-menu';
viewRefs.hoveredView = this;
onHover(snapshot);
} else {
onHover(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const REACT_MEASURE_HEIGHT = 14;
export const BORDER_SIZE = 1;
export const FLAMECHART_FRAME_HEIGHT = 14;
export const TEXT_PADDING = 3;
export const SNAPSHOT_HEIGHT = 50;

export const INTERVAL_TIMES = [
1,
Expand Down
Loading

0 comments on commit 4b7e76f

Please sign in to comment.