Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DevTools] Add screenshots to Scheduling Profiler #22088

Merged
merged 1 commit into from Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/react-devtools-scheduling-profiler/src/CanvasPage.js
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
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
@@ -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);
}
}
}
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