mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Scheduling Profiler: Add network measures (#22112)
This commit is contained in:
+90
-16
@@ -44,6 +44,7 @@ import {
|
||||
ComponentMeasuresView,
|
||||
FlamechartView,
|
||||
NativeEventsView,
|
||||
NetworkMeasuresView,
|
||||
ReactMeasuresView,
|
||||
SchedulingEventsView,
|
||||
SnapshotsView,
|
||||
@@ -128,6 +129,18 @@ const zoomToBatch = (
|
||||
viewState.updateHorizontalScrollState(scrollState);
|
||||
};
|
||||
|
||||
const EMPTY_CONTEXT_INFO: ReactHoverContextInfo = {
|
||||
componentMeasure: null,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
};
|
||||
|
||||
type AutoSizedCanvasProps = {|
|
||||
data: ReactProfilerData,
|
||||
height: number,
|
||||
@@ -150,7 +163,12 @@ function AutoSizedCanvas({
|
||||
setHoveredEvent,
|
||||
] = useState<ReactHoverContextInfo | null>(null);
|
||||
|
||||
const surfaceRef = useRef(new Surface());
|
||||
const resetHoveredEvent = useCallback(
|
||||
() => setHoveredEvent(EMPTY_CONTEXT_INFO),
|
||||
[],
|
||||
);
|
||||
|
||||
const surfaceRef = useRef(new Surface(resetHoveredEvent));
|
||||
const userTimingMarksViewRef = useRef(null);
|
||||
const nativeEventsViewRef = useRef(null);
|
||||
const schedulingEventsViewRef = useRef(null);
|
||||
@@ -158,6 +176,7 @@ function AutoSizedCanvas({
|
||||
const componentMeasuresViewRef = useRef(null);
|
||||
const reactMeasuresViewRef = useRef(null);
|
||||
const flamechartViewRef = useRef(null);
|
||||
const networkMeasuresViewRef = useRef(null);
|
||||
const snapshotsViewRef = useRef(null);
|
||||
|
||||
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
|
||||
@@ -318,6 +337,22 @@ function AutoSizedCanvas({
|
||||
);
|
||||
}
|
||||
|
||||
let networkMeasuresViewWrapper = null;
|
||||
if (data.snapshots.length > 0) {
|
||||
const networkMeasuresView = new NetworkMeasuresView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
data,
|
||||
);
|
||||
networkMeasuresViewRef.current = networkMeasuresView;
|
||||
networkMeasuresViewWrapper = createViewHelper(
|
||||
networkMeasuresView,
|
||||
'network',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const flamechartView = new FlamechartView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
@@ -357,6 +392,9 @@ function AutoSizedCanvas({
|
||||
if (snapshotsViewWrapper !== null) {
|
||||
rootView.addSubview(snapshotsViewWrapper);
|
||||
}
|
||||
if (networkMeasuresViewWrapper !== null) {
|
||||
rootView.addSubview(networkMeasuresViewWrapper);
|
||||
}
|
||||
rootView.addSubview(flamechartViewWrapper);
|
||||
|
||||
const verticalScrollOverflowView = new VerticalScrollOverflowView(
|
||||
@@ -395,16 +433,18 @@ function AutoSizedCanvas({
|
||||
prevHoverEvent.flamechartStackFrame !== null ||
|
||||
prevHoverEvent.measure !== null ||
|
||||
prevHoverEvent.nativeEvent !== null ||
|
||||
prevHoverEvent.networkMeasure !== null ||
|
||||
prevHoverEvent.schedulingEvent !== null ||
|
||||
prevHoverEvent.snapshot !== null ||
|
||||
prevHoverEvent.suspenseEvent !== null ||
|
||||
prevHoverEvent.userTimingMark !== null
|
||||
) {
|
||||
return {
|
||||
componentMeasure: null,
|
||||
data: prevHoverEvent.data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -460,10 +500,10 @@ function AutoSizedCanvas({
|
||||
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -479,10 +519,10 @@ function AutoSizedCanvas({
|
||||
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -498,10 +538,10 @@ function AutoSizedCanvas({
|
||||
if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -517,10 +557,10 @@ function AutoSizedCanvas({
|
||||
if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent,
|
||||
@@ -536,10 +576,10 @@ function AutoSizedCanvas({
|
||||
if (!hoveredEvent || hoveredEvent.measure !== measure) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -558,10 +598,10 @@ function AutoSizedCanvas({
|
||||
) {
|
||||
setHoveredEvent({
|
||||
componentMeasure,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -577,10 +617,10 @@ function AutoSizedCanvas({
|
||||
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot,
|
||||
suspenseEvent: null,
|
||||
@@ -599,10 +639,10 @@ function AutoSizedCanvas({
|
||||
) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
data,
|
||||
flamechartStackFrame,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure: null,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
@@ -611,53 +651,79 @@ function AutoSizedCanvas({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const {current: networkMeasuresView} = networkMeasuresViewRef;
|
||||
if (networkMeasuresView) {
|
||||
networkMeasuresView.onHover = networkMeasure => {
|
||||
if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) {
|
||||
setHoveredEvent({
|
||||
componentMeasure: null,
|
||||
flamechartStackFrame: null,
|
||||
measure: null,
|
||||
nativeEvent: null,
|
||||
networkMeasure,
|
||||
schedulingEvent: null,
|
||||
snapshot: null,
|
||||
suspenseEvent: null,
|
||||
userTimingMark: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [
|
||||
hoveredEvent,
|
||||
data, // Attach onHover callbacks when views are re-created on data change
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const {current: userTimingMarksView} = userTimingMarksViewRef;
|
||||
const userTimingMarksView = userTimingMarksViewRef.current;
|
||||
if (userTimingMarksView) {
|
||||
userTimingMarksView.setHoveredMark(
|
||||
hoveredEvent ? hoveredEvent.userTimingMark : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: nativeEventsView} = nativeEventsViewRef;
|
||||
const nativeEventsView = nativeEventsViewRef.current;
|
||||
if (nativeEventsView) {
|
||||
nativeEventsView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.nativeEvent : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: schedulingEventsView} = schedulingEventsViewRef;
|
||||
const schedulingEventsView = schedulingEventsViewRef.current;
|
||||
if (schedulingEventsView) {
|
||||
schedulingEventsView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.schedulingEvent : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: suspenseEventsView} = suspenseEventsViewRef;
|
||||
const suspenseEventsView = suspenseEventsViewRef.current;
|
||||
if (suspenseEventsView) {
|
||||
suspenseEventsView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.suspenseEvent : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: reactMeasuresView} = reactMeasuresViewRef;
|
||||
const reactMeasuresView = reactMeasuresViewRef.current;
|
||||
if (reactMeasuresView) {
|
||||
reactMeasuresView.setHoveredMeasure(
|
||||
hoveredEvent ? hoveredEvent.measure : null,
|
||||
);
|
||||
}
|
||||
|
||||
const {current: flamechartView} = flamechartViewRef;
|
||||
const flamechartView = flamechartViewRef.current;
|
||||
if (flamechartView) {
|
||||
flamechartView.setHoveredFlamechartStackFrame(
|
||||
hoveredEvent ? hoveredEvent.flamechartStackFrame : null,
|
||||
);
|
||||
}
|
||||
|
||||
const networkMeasuresView = networkMeasuresViewRef.current;
|
||||
if (networkMeasuresView) {
|
||||
networkMeasuresView.setHoveredEvent(
|
||||
hoveredEvent ? hoveredEvent.networkMeasure : null,
|
||||
);
|
||||
}
|
||||
}, [hoveredEvent]);
|
||||
|
||||
// Draw to canvas in React's commit phase
|
||||
@@ -677,6 +743,7 @@ function AutoSizedCanvas({
|
||||
componentMeasure,
|
||||
flamechartStackFrame,
|
||||
measure,
|
||||
networkMeasure,
|
||||
schedulingEvent,
|
||||
suspenseEvent,
|
||||
} = contextData.hoveredEvent;
|
||||
@@ -689,6 +756,13 @@ function AutoSizedCanvas({
|
||||
Copy component name
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{networkMeasure !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(networkMeasure.url)}
|
||||
title="Copy URL">
|
||||
Copy URL
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{schedulingEvent !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() => copy(schedulingEvent.componentName)}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
}
|
||||
|
||||
.TooltipSection,
|
||||
.TooltipWarningSection {
|
||||
.TooltipWarningSection,
|
||||
.SingleLineTextSection {
|
||||
display: block;
|
||||
border-radius: 0.125rem;
|
||||
max-width: 300px;
|
||||
padding: 0.25rem;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
@@ -19,6 +19,13 @@
|
||||
margin-top: 0.25rem;
|
||||
background-color: var(--color-warning-background);
|
||||
}
|
||||
.TooltipSection,
|
||||
.TooltipWarningSection {
|
||||
max-width: 300px;
|
||||
}
|
||||
.SingleLineTextSection {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Divider {
|
||||
height: 1px;
|
||||
@@ -75,4 +82,8 @@
|
||||
|
||||
.Image {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.DimText {
|
||||
color: var(--color-dim);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type {Point} from './view-base';
|
||||
import type {
|
||||
FlamechartStackFrame,
|
||||
NativeEvent,
|
||||
NetworkMeasure,
|
||||
ReactComponentMeasure,
|
||||
ReactHoverContextInfo,
|
||||
ReactMeasure,
|
||||
@@ -29,6 +30,8 @@ import {getBatchRange} from './utils/getBatchRange';
|
||||
import useSmartTooltip from './utils/useSmartTooltip';
|
||||
import styles from './EventTooltip.css';
|
||||
|
||||
const MAX_TOOLTIP_TEXT_LENGTH = 60;
|
||||
|
||||
type Props = {|
|
||||
canvasRef: {|current: HTMLCanvasElement | null|},
|
||||
data: ReactProfilerData,
|
||||
@@ -87,6 +90,7 @@ export default function EventTooltip({
|
||||
flamechartStackFrame,
|
||||
measure,
|
||||
nativeEvent,
|
||||
networkMeasure,
|
||||
schedulingEvent,
|
||||
snapshot,
|
||||
suspenseEvent,
|
||||
@@ -104,6 +108,13 @@ export default function EventTooltip({
|
||||
return (
|
||||
<TooltipNativeEvent nativeEvent={nativeEvent} tooltipRef={tooltipRef} />
|
||||
);
|
||||
} else if (networkMeasure !== null) {
|
||||
return (
|
||||
<TooltipNetworkMeasure
|
||||
networkMeasure={networkMeasure}
|
||||
tooltipRef={tooltipRef}
|
||||
/>
|
||||
);
|
||||
} else if (schedulingEvent !== null) {
|
||||
return (
|
||||
<TooltipSchedulingEvent
|
||||
@@ -238,6 +249,44 @@ const TooltipNativeEvent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipNetworkMeasure = ({
|
||||
networkMeasure,
|
||||
tooltipRef,
|
||||
}: {
|
||||
networkMeasure: NetworkMeasure,
|
||||
tooltipRef: Return<typeof useRef>,
|
||||
}) => {
|
||||
const {
|
||||
finishTimestamp,
|
||||
lastReceivedDataTimestamp,
|
||||
priority,
|
||||
sendRequestTimestamp,
|
||||
url,
|
||||
} = networkMeasure;
|
||||
|
||||
let urlToDisplay = url;
|
||||
if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
|
||||
const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
|
||||
urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half);
|
||||
}
|
||||
|
||||
const timestampBegin = sendRequestTimestamp;
|
||||
const timestampEnd = finishTimestamp || lastReceivedDataTimestamp;
|
||||
const duration =
|
||||
timestampEnd > 0
|
||||
? formatDuration(finishTimestamp - timestampBegin)
|
||||
: '(incomplete)';
|
||||
|
||||
return (
|
||||
<div className={styles.Tooltip} ref={tooltipRef}>
|
||||
<div className={styles.SingleLineTextSection}>
|
||||
{duration} <span className={styles.DimText}>{priority}</span>{' '}
|
||||
{urlToDisplay}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipSchedulingEvent = ({
|
||||
data,
|
||||
schedulingEvent,
|
||||
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 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 {NetworkMeasure, ReactProfilerData} from '../types';
|
||||
import type {
|
||||
Interaction,
|
||||
IntrinsicSize,
|
||||
MouseMoveInteraction,
|
||||
Rect,
|
||||
ViewRefs,
|
||||
} from '../view-base';
|
||||
|
||||
import {
|
||||
durationToWidth,
|
||||
positioningScaleFactor,
|
||||
positionToTimestamp,
|
||||
timestampToPosition,
|
||||
} from './utils/positioning';
|
||||
import {drawText} from './utils/text';
|
||||
import {formatDuration} from '../utils/formatting';
|
||||
import {
|
||||
View,
|
||||
Surface,
|
||||
rectContainsPoint,
|
||||
rectIntersectsRect,
|
||||
intersectionOfRects,
|
||||
} from '../view-base';
|
||||
import {BORDER_SIZE, COLORS, SUSPENSE_EVENT_HEIGHT} from './constants';
|
||||
|
||||
const HEIGHT = SUSPENSE_EVENT_HEIGHT; // TODO Constant name
|
||||
const ROW_WITH_BORDER_HEIGHT = HEIGHT + BORDER_SIZE;
|
||||
|
||||
const BASE_URL_REGEX = /([^:]+:\/\/[^\/]+)/;
|
||||
|
||||
export class NetworkMeasuresView extends View {
|
||||
_depthToNetworkMeasure: Map<number, NetworkMeasure[]>;
|
||||
_hoveredNetworkMeasure: NetworkMeasure | null = null;
|
||||
_intrinsicSize: IntrinsicSize;
|
||||
_maxDepth: number = 0;
|
||||
_profilerData: ReactProfilerData;
|
||||
|
||||
onHover: ((event: NetworkMeasure | null) => void) | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
super(surface, frame);
|
||||
|
||||
this._profilerData = profilerData;
|
||||
|
||||
this._performPreflightComputations();
|
||||
}
|
||||
|
||||
_performPreflightComputations() {
|
||||
this._depthToNetworkMeasure = new Map();
|
||||
|
||||
const {duration, networkMeasures} = this._profilerData;
|
||||
|
||||
networkMeasures.forEach(event => {
|
||||
const depth = event.depth;
|
||||
|
||||
this._maxDepth = Math.max(this._maxDepth, depth);
|
||||
|
||||
if (!this._depthToNetworkMeasure.has(depth)) {
|
||||
this._depthToNetworkMeasure.set(depth, [event]);
|
||||
} else {
|
||||
// $FlowFixMe This is unnecessary.
|
||||
this._depthToNetworkMeasure.get(depth).push(event);
|
||||
}
|
||||
});
|
||||
|
||||
this._intrinsicSize = {
|
||||
width: duration,
|
||||
height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
|
||||
// Collapsed by default
|
||||
maxInitialHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._intrinsicSize;
|
||||
}
|
||||
|
||||
setHoveredEvent(networkMeasure: NetworkMeasure | null) {
|
||||
if (this._hoveredNetworkMeasure === networkMeasure) {
|
||||
return;
|
||||
}
|
||||
this._hoveredNetworkMeasure = networkMeasure;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single `NetworkMeasure` as a box/span with text inside of it.
|
||||
*/
|
||||
_drawSingleNetworkMeasure(
|
||||
context: CanvasRenderingContext2D,
|
||||
networkMeasure: NetworkMeasure,
|
||||
baseY: number,
|
||||
scaleFactor: number,
|
||||
showHoverHighlight: boolean,
|
||||
) {
|
||||
const {frame, visibleArea} = this;
|
||||
const {
|
||||
depth,
|
||||
finishTimestamp,
|
||||
firstReceivedDataTimestamp,
|
||||
lastReceivedDataTimestamp,
|
||||
receiveResponseTimestamp,
|
||||
sendRequestTimestamp,
|
||||
url,
|
||||
} = networkMeasure;
|
||||
|
||||
// Account for requests that did not complete while we were profiling.
|
||||
// As well as requests that did not receive data before finish (cached?).
|
||||
const duration = this._profilerData.duration;
|
||||
const timestampBegin = sendRequestTimestamp;
|
||||
const timestampEnd =
|
||||
finishTimestamp || lastReceivedDataTimestamp || duration;
|
||||
const timestampMiddle =
|
||||
receiveResponseTimestamp || firstReceivedDataTimestamp || timestampEnd;
|
||||
|
||||
// Convert all timestamps to x coordinates.
|
||||
const xStart = timestampToPosition(timestampBegin, scaleFactor, frame);
|
||||
const xMiddle = timestampToPosition(timestampMiddle, scaleFactor, frame);
|
||||
const xStop = timestampToPosition(timestampEnd, scaleFactor, frame);
|
||||
|
||||
const width = durationToWidth(xStop - xStart, scaleFactor);
|
||||
if (width < 1) {
|
||||
return; // Too small to render at this zoom level
|
||||
}
|
||||
|
||||
baseY += depth * ROW_WITH_BORDER_HEIGHT;
|
||||
|
||||
const outerRect: Rect = {
|
||||
origin: {
|
||||
x: xStart,
|
||||
y: baseY,
|
||||
},
|
||||
size: {
|
||||
width: xStop - xStart,
|
||||
height: HEIGHT,
|
||||
},
|
||||
};
|
||||
if (!rectIntersectsRect(outerRect, visibleArea)) {
|
||||
return; // Not in view
|
||||
}
|
||||
|
||||
// Draw the secondary rect first (since it also shows as a thin border around the primary rect).
|
||||
let rect = {
|
||||
origin: {
|
||||
x: xStart,
|
||||
y: baseY,
|
||||
},
|
||||
size: {
|
||||
width: xStop - xStart,
|
||||
height: HEIGHT,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(rect, visibleArea)) {
|
||||
context.beginPath();
|
||||
context.fillStyle =
|
||||
this._hoveredNetworkMeasure === networkMeasure
|
||||
? COLORS.NETWORK_SECONDARY_HOVER
|
||||
: COLORS.NETWORK_SECONDARY;
|
||||
context.fillRect(
|
||||
rect.origin.x,
|
||||
rect.origin.y,
|
||||
rect.size.width,
|
||||
rect.size.height,
|
||||
);
|
||||
}
|
||||
|
||||
rect = {
|
||||
origin: {
|
||||
x: xStart + BORDER_SIZE,
|
||||
y: baseY + BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: xMiddle - xStart - BORDER_SIZE,
|
||||
height: HEIGHT - BORDER_SIZE * 2,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(rect, visibleArea)) {
|
||||
context.beginPath();
|
||||
context.fillStyle =
|
||||
this._hoveredNetworkMeasure === networkMeasure
|
||||
? COLORS.NETWORK_PRIMARY_HOVER
|
||||
: COLORS.NETWORK_PRIMARY;
|
||||
context.fillRect(
|
||||
rect.origin.x,
|
||||
rect.origin.y,
|
||||
rect.size.width,
|
||||
rect.size.height,
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = url.match(BASE_URL_REGEX);
|
||||
const displayUrl = baseUrl !== null ? baseUrl[1] : url;
|
||||
|
||||
const durationLabel =
|
||||
finishTimestamp !== 0
|
||||
? `${formatDuration(finishTimestamp - sendRequestTimestamp)} - `
|
||||
: '';
|
||||
|
||||
const label = durationLabel + displayUrl;
|
||||
|
||||
drawText(label, context, outerRect, visibleArea);
|
||||
}
|
||||
|
||||
draw(context: CanvasRenderingContext2D) {
|
||||
const {
|
||||
frame,
|
||||
_profilerData: {networkMeasures},
|
||||
_hoveredNetworkMeasure,
|
||||
visibleArea,
|
||||
} = this;
|
||||
|
||||
context.fillStyle = COLORS.PRIORITY_BACKGROUND;
|
||||
context.fillRect(
|
||||
visibleArea.origin.x,
|
||||
visibleArea.origin.y,
|
||||
visibleArea.size.width,
|
||||
visibleArea.size.height,
|
||||
);
|
||||
|
||||
const scaleFactor = positioningScaleFactor(
|
||||
this._intrinsicSize.width,
|
||||
frame,
|
||||
);
|
||||
|
||||
networkMeasures.forEach(networkMeasure => {
|
||||
this._drawSingleNetworkMeasure(
|
||||
context,
|
||||
networkMeasure,
|
||||
frame.origin.y,
|
||||
scaleFactor,
|
||||
networkMeasure === _hoveredNetworkMeasure,
|
||||
);
|
||||
});
|
||||
|
||||
// Render bottom borders.
|
||||
for (let i = 0; i <= this._maxDepth; i++) {
|
||||
const borderFrame: Rect = {
|
||||
origin: {
|
||||
x: frame.origin.x,
|
||||
y: frame.origin.y + (i + 1) * ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
|
||||
},
|
||||
size: {
|
||||
width: frame.size.width,
|
||||
height: BORDER_SIZE,
|
||||
},
|
||||
};
|
||||
if (rectIntersectsRect(borderFrame, visibleArea)) {
|
||||
const borderDrawableRect = intersectionOfRects(
|
||||
borderFrame,
|
||||
visibleArea,
|
||||
);
|
||||
context.fillStyle = COLORS.PRIORITY_BORDER;
|
||||
context.fillRect(
|
||||
borderDrawableRect.origin.x,
|
||||
borderDrawableRect.origin.y,
|
||||
borderDrawableRect.size.width,
|
||||
borderDrawableRect.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
|
||||
const {frame, _intrinsicSize, onHover, visibleArea} = this;
|
||||
if (!onHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {location} = interaction.payload;
|
||||
if (!rectContainsPoint(location, visibleArea)) {
|
||||
onHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
|
||||
const adjustedCanvasMouseY = location.y - frame.origin.y;
|
||||
const depth = Math.floor(adjustedCanvasMouseY / ROW_WITH_BORDER_HEIGHT);
|
||||
const networkMeasuresAtDepth = this._depthToNetworkMeasure.get(depth);
|
||||
|
||||
const duration = this._profilerData.duration;
|
||||
|
||||
if (networkMeasuresAtDepth) {
|
||||
// Find the event being hovered over.
|
||||
for (let index = networkMeasuresAtDepth.length - 1; index >= 0; index--) {
|
||||
const networkMeasure = networkMeasuresAtDepth[index];
|
||||
const {
|
||||
finishTimestamp,
|
||||
lastReceivedDataTimestamp,
|
||||
sendRequestTimestamp,
|
||||
} = networkMeasure;
|
||||
|
||||
const timestampBegin = sendRequestTimestamp;
|
||||
const timestampEnd =
|
||||
finishTimestamp || lastReceivedDataTimestamp || duration;
|
||||
|
||||
if (
|
||||
hoverTimestamp >= timestampBegin &&
|
||||
hoverTimestamp <= timestampEnd
|
||||
) {
|
||||
this.currentCursor = 'context-menu';
|
||||
viewRefs.hoveredView = this;
|
||||
onHover(networkMeasure);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewRefs.hoveredView === this) {
|
||||
viewRefs.hoveredView = null;
|
||||
}
|
||||
|
||||
onHover(null);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -7,6 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export const DPR: number = window.devicePixelRatio || 1;
|
||||
export const LABEL_SIZE = 80;
|
||||
export const MARKER_HEIGHT = 20;
|
||||
export const MARKER_TICK_HEIGHT = 8;
|
||||
@@ -20,7 +21,7 @@ export const PENDING_SUSPENSE_EVENT_SIZE = 8;
|
||||
export const REACT_EVENT_DIAMETER = 6;
|
||||
export const USER_TIMING_MARK_SIZE = 8;
|
||||
export const REACT_MEASURE_HEIGHT = 14;
|
||||
export const BORDER_SIZE = 1;
|
||||
export const BORDER_SIZE = 1 / DPR;
|
||||
export const FLAMECHART_FRAME_HEIGHT = 14;
|
||||
export const TEXT_PADDING = 3;
|
||||
export const SNAPSHOT_HEIGHT = 35;
|
||||
@@ -46,6 +47,10 @@ export let COLORS = {
|
||||
BACKGROUND: '',
|
||||
NATIVE_EVENT: '',
|
||||
NATIVE_EVENT_HOVER: '',
|
||||
NETWORK_PRIMARY: '',
|
||||
NETWORK_PRIMARY_HOVER: '',
|
||||
NETWORK_SECONDARY: '',
|
||||
NETWORK_SECONDARY_HOVER: '',
|
||||
PRIORITY_BACKGROUND: '',
|
||||
PRIORITY_BORDER: '',
|
||||
PRIORITY_LABEL: '',
|
||||
@@ -106,6 +111,18 @@ export function updateColorsToMatchTheme(element: Element): boolean {
|
||||
NATIVE_EVENT_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-native-event-hover',
|
||||
),
|
||||
NETWORK_PRIMARY: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-network-primary',
|
||||
),
|
||||
NETWORK_PRIMARY_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-network-primary-hover',
|
||||
),
|
||||
NETWORK_SECONDARY: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-network-secondary',
|
||||
),
|
||||
NETWORK_SECONDARY_HOVER: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-network-secondary-hover',
|
||||
),
|
||||
PRIORITY_BACKGROUND: computedStyle.getPropertyValue(
|
||||
'--color-scheduling-profiler-priority-background',
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export * from './ComponentMeasuresView';
|
||||
export * from './FlamechartView';
|
||||
export * from './NativeEventsView';
|
||||
export * from './NetworkMeasuresView';
|
||||
export * from './ReactMeasuresView';
|
||||
export * from './SchedulingEventsView';
|
||||
export * from './SnapshotsView';
|
||||
|
||||
+24
-4
@@ -32,14 +32,34 @@ export function trimText(
|
||||
text: string,
|
||||
width: number,
|
||||
): string | null {
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
const trimmedText = i === text.length - 1 ? text : text.substr(0, i) + '…';
|
||||
const maxIndex = text.length - 1;
|
||||
|
||||
let startIndex = 0;
|
||||
let stopIndex = maxIndex;
|
||||
|
||||
let longestValidIndex = 0;
|
||||
let longestValidText = null;
|
||||
|
||||
// Trimming long text could be really slow if we decrease only 1 character at a time.
|
||||
// Trimming with more of a binary search approach is faster in the worst cases.
|
||||
while (startIndex <= stopIndex) {
|
||||
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
|
||||
const trimmedText =
|
||||
currentIndex === maxIndex ? text : text.substr(0, currentIndex) + '…';
|
||||
|
||||
if (getTextWidth(context, trimmedText) <= width) {
|
||||
return trimmedText;
|
||||
if (longestValidIndex < currentIndex) {
|
||||
longestValidIndex = currentIndex;
|
||||
longestValidText = trimmedText;
|
||||
}
|
||||
|
||||
startIndex = currentIndex + 1;
|
||||
} else {
|
||||
stopIndex = currentIndex - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return longestValidText;
|
||||
}
|
||||
|
||||
type TextConfig = {|
|
||||
|
||||
+4
@@ -324,6 +324,7 @@ describe('preprocessData', () => {
|
||||
30 => Array [],
|
||||
},
|
||||
"nativeEvents": Array [],
|
||||
"networkMeasures": Array [],
|
||||
"otherUserTimingMarks": Array [],
|
||||
"reactVersion": "17.0.3",
|
||||
"schedulingEvents": Array [],
|
||||
@@ -530,6 +531,7 @@ describe('preprocessData', () => {
|
||||
30 => Array [],
|
||||
},
|
||||
"nativeEvents": Array [],
|
||||
"networkMeasures": Array [],
|
||||
"otherUserTimingMarks": Array [],
|
||||
"reactVersion": "17.0.3",
|
||||
"schedulingEvents": Array [
|
||||
@@ -715,6 +717,7 @@ describe('preprocessData', () => {
|
||||
30 => Array [],
|
||||
},
|
||||
"nativeEvents": Array [],
|
||||
"networkMeasures": Array [],
|
||||
"otherUserTimingMarks": Array [
|
||||
Object {
|
||||
"name": "__v3",
|
||||
@@ -1051,6 +1054,7 @@ describe('preprocessData', () => {
|
||||
30 => Array [],
|
||||
},
|
||||
"nativeEvents": Array [],
|
||||
"networkMeasures": Array [],
|
||||
"otherUserTimingMarks": Array [
|
||||
Object {
|
||||
"name": "__v3",
|
||||
|
||||
+239
-93
@@ -17,6 +17,7 @@ import type {
|
||||
Flamechart,
|
||||
Milliseconds,
|
||||
NativeEvent,
|
||||
NetworkMeasure,
|
||||
Phase,
|
||||
ReactLane,
|
||||
ReactComponentMeasure,
|
||||
@@ -51,6 +52,7 @@ type ProcessorState = {|
|
||||
potentialSuspenseEventsOutsideOfTransition: Array<
|
||||
[SuspenseEvent, ReactLane[]],
|
||||
>,
|
||||
requestIdToNetworkMeasureMap: Map<string, NetworkMeasure>,
|
||||
uidCounter: BatchUID,
|
||||
unresolvedSuspenseEvents: Map<string, SuspenseEvent>,
|
||||
|};
|
||||
@@ -215,6 +217,205 @@ function throwIfIncomplete(
|
||||
}
|
||||
}
|
||||
|
||||
function processEventDispatch(
|
||||
event: TimelineEvent,
|
||||
timestamp: Milliseconds,
|
||||
profilerData: ReactProfilerData,
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const data = event.args.data;
|
||||
const type = data.type;
|
||||
|
||||
if (type.startsWith('react-')) {
|
||||
const stackTrace = data.stackTrace;
|
||||
if (stackTrace) {
|
||||
const topFrame = stackTrace[stackTrace.length - 1];
|
||||
if (topFrame.url.includes('/react-dom.')) {
|
||||
// Filter out fake React events dispatched by invokeGuardedCallbackDev.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
|
||||
if (
|
||||
type === 'blur' ||
|
||||
type === 'click' ||
|
||||
type === 'input' ||
|
||||
type.startsWith('focus') ||
|
||||
type.startsWith('key') ||
|
||||
type.startsWith('mouse') ||
|
||||
type.startsWith('pointer')
|
||||
) {
|
||||
const duration = event.dur / 1000;
|
||||
|
||||
let depth = 0;
|
||||
|
||||
while (state.nativeEventStack.length > 0) {
|
||||
const prevNativeEvent =
|
||||
state.nativeEventStack[state.nativeEventStack.length - 1];
|
||||
const prevStopTime = prevNativeEvent.timestamp + prevNativeEvent.duration;
|
||||
|
||||
if (timestamp < prevStopTime) {
|
||||
depth = prevNativeEvent.depth + 1;
|
||||
break;
|
||||
} else {
|
||||
state.nativeEventStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const nativeEvent = {
|
||||
depth,
|
||||
duration,
|
||||
timestamp,
|
||||
type,
|
||||
warning: null,
|
||||
};
|
||||
|
||||
profilerData.nativeEvents.push(nativeEvent);
|
||||
|
||||
// Keep track of curent event in case future ones overlap.
|
||||
// We separate them into different vertical lanes in this case.
|
||||
state.nativeEventStack.push(nativeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function processResourceFinish(
|
||||
event: TimelineEvent,
|
||||
timestamp: Milliseconds,
|
||||
profilerData: ReactProfilerData,
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const requestId = event.args.data.requestId;
|
||||
const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
|
||||
if (networkMeasure != null) {
|
||||
networkMeasure.finishTimestamp = timestamp;
|
||||
if (networkMeasure.firstReceivedDataTimestamp === 0) {
|
||||
networkMeasure.firstReceivedDataTimestamp = timestamp;
|
||||
}
|
||||
if (networkMeasure.lastReceivedDataTimestamp === 0) {
|
||||
networkMeasure.lastReceivedDataTimestamp = timestamp;
|
||||
}
|
||||
|
||||
// Clean up now that the resource is done.
|
||||
state.requestIdToNetworkMeasureMap.delete(event.args.data.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
function processResourceReceivedData(
|
||||
event: TimelineEvent,
|
||||
timestamp: Milliseconds,
|
||||
profilerData: ReactProfilerData,
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const requestId = event.args.data.requestId;
|
||||
const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
|
||||
if (networkMeasure != null) {
|
||||
if (networkMeasure.firstReceivedDataTimestamp === 0) {
|
||||
networkMeasure.firstReceivedDataTimestamp = timestamp;
|
||||
}
|
||||
networkMeasure.lastReceivedDataTimestamp = timestamp;
|
||||
networkMeasure.finishTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
function processResourceReceiveResponse(
|
||||
event: TimelineEvent,
|
||||
timestamp: Milliseconds,
|
||||
profilerData: ReactProfilerData,
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const requestId = event.args.data.requestId;
|
||||
const networkMeasure = state.requestIdToNetworkMeasureMap.get(requestId);
|
||||
if (networkMeasure != null) {
|
||||
networkMeasure.receiveResponseTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
function processScreenshot(
|
||||
event: TimelineEvent,
|
||||
timestamp: Milliseconds,
|
||||
profilerData: ReactProfilerData,
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const encodedSnapshot = event.args.snapshot; // Base 64 encoded
|
||||
|
||||
const snapshot = {
|
||||
height: 0,
|
||||
image: null,
|
||||
imageSource: `data:image/png;base64,${encodedSnapshot}`,
|
||||
timestamp,
|
||||
width: 0,
|
||||
};
|
||||
|
||||
// Delay processing until we've extracted snapshot dimensions.
|
||||
let resolveFn = ((null: any): Function);
|
||||
state.asyncProcessingPromises.push(
|
||||
new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
// Parse the Base64 image data to determine native size.
|
||||
// This will be used later to scale for display within the thumbnail strip.
|
||||
fetch(snapshot.imageSource)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
// $FlowFixMe createImageBitmap
|
||||
createImageBitmap(blob).then(bitmap => {
|
||||
snapshot.height = bitmap.height;
|
||||
snapshot.width = bitmap.width;
|
||||
|
||||
resolveFn();
|
||||
});
|
||||
});
|
||||
|
||||
profilerData.snapshots.push(snapshot);
|
||||
}
|
||||
|
||||
function processResourceSendRequest(
|
||||
event: TimelineEvent,
|
||||
timestamp: Milliseconds,
|
||||
profilerData: ReactProfilerData,
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const data = event.args.data;
|
||||
const requestId = data.requestId;
|
||||
|
||||
const availableDepths = new Array(
|
||||
state.requestIdToNetworkMeasureMap.size + 1,
|
||||
).fill(true);
|
||||
state.requestIdToNetworkMeasureMap.forEach(({depth}) => {
|
||||
availableDepths[depth] = false;
|
||||
});
|
||||
|
||||
let depth = 0;
|
||||
for (let i = 0; i < availableDepths.length; i++) {
|
||||
if (availableDepths[i]) {
|
||||
depth = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const networkMeasure: NetworkMeasure = {
|
||||
depth,
|
||||
finishTimestamp: 0,
|
||||
firstReceivedDataTimestamp: 0,
|
||||
lastReceivedDataTimestamp: 0,
|
||||
requestId,
|
||||
requestMethod: data.requestMethod,
|
||||
priority: data.priority,
|
||||
sendRequestTimestamp: timestamp,
|
||||
receiveResponseTimestamp: 0,
|
||||
url: data.url,
|
||||
};
|
||||
|
||||
state.requestIdToNetworkMeasureMap.set(requestId, networkMeasure);
|
||||
|
||||
profilerData.networkMeasures.push(networkMeasure);
|
||||
networkMeasure.sendRequestTimestamp = timestamp;
|
||||
}
|
||||
|
||||
function processTimelineEvent(
|
||||
event: TimelineEvent,
|
||||
/** Finalized profiler data up to `event`. May be mutated. */
|
||||
@@ -222,106 +423,49 @@ function processTimelineEvent(
|
||||
/** Intermediate processor state. May be mutated. */
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const {args, cat, name, ts, ph} = event;
|
||||
const {cat, name, ts, ph} = event;
|
||||
|
||||
const startTime = (ts - currentProfilerData.startTime) / 1000;
|
||||
|
||||
switch (cat) {
|
||||
case 'disabled-by-default-devtools.screenshot':
|
||||
const encodedSnapshot = args.snapshot; // Base 64 encoded
|
||||
|
||||
const snapshot = {
|
||||
height: 0,
|
||||
image: null,
|
||||
imageSource: `data:image/png;base64,${encodedSnapshot}`,
|
||||
timestamp: (ts - currentProfilerData.startTime) / 1000,
|
||||
width: 0,
|
||||
};
|
||||
|
||||
// Delay processing until we've extracted snapshot dimensions.
|
||||
let resolveFn = ((null: any): Function);
|
||||
state.asyncProcessingPromises.push(
|
||||
new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
// Parse the Base64 image data to determine native size.
|
||||
// This will be used later to scale for display within the thumbnail strip.
|
||||
fetch(snapshot.imageSource)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
// $FlowFixMe createImageBitmap
|
||||
createImageBitmap(blob).then(bitmap => {
|
||||
snapshot.height = bitmap.height;
|
||||
snapshot.width = bitmap.width;
|
||||
|
||||
resolveFn();
|
||||
});
|
||||
});
|
||||
|
||||
currentProfilerData.snapshots.push(snapshot);
|
||||
processScreenshot(event, startTime, currentProfilerData, state);
|
||||
break;
|
||||
case 'devtools.timeline':
|
||||
if (name === 'EventDispatch') {
|
||||
const type = args.data.type;
|
||||
|
||||
if (type.startsWith('react-')) {
|
||||
const stackTrace = args.data.stackTrace;
|
||||
if (stackTrace) {
|
||||
const topFrame = stackTrace[stackTrace.length - 1];
|
||||
if (topFrame.url.includes('/react-dom.')) {
|
||||
// Filter out fake React events dispatched by invokeGuardedCallbackDev.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
|
||||
if (
|
||||
type === 'blur' ||
|
||||
type === 'click' ||
|
||||
type === 'input' ||
|
||||
type.startsWith('focus') ||
|
||||
type.startsWith('key') ||
|
||||
type.startsWith('mouse') ||
|
||||
type.startsWith('pointer')
|
||||
) {
|
||||
const timestamp = (ts - currentProfilerData.startTime) / 1000;
|
||||
const duration = event.dur / 1000;
|
||||
|
||||
let depth = 0;
|
||||
|
||||
while (state.nativeEventStack.length > 0) {
|
||||
const prevNativeEvent =
|
||||
state.nativeEventStack[state.nativeEventStack.length - 1];
|
||||
const prevStopTime =
|
||||
prevNativeEvent.timestamp + prevNativeEvent.duration;
|
||||
|
||||
if (timestamp < prevStopTime) {
|
||||
depth = prevNativeEvent.depth + 1;
|
||||
break;
|
||||
} else {
|
||||
state.nativeEventStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const nativeEvent = {
|
||||
depth,
|
||||
duration,
|
||||
timestamp,
|
||||
type,
|
||||
warning: null,
|
||||
};
|
||||
|
||||
currentProfilerData.nativeEvents.push(nativeEvent);
|
||||
|
||||
// Keep track of curent event in case future ones overlap.
|
||||
// We separate them into different vertical lanes in this case.
|
||||
state.nativeEventStack.push(nativeEvent);
|
||||
}
|
||||
switch (name) {
|
||||
case 'EventDispatch':
|
||||
processEventDispatch(event, startTime, currentProfilerData, state);
|
||||
break;
|
||||
case 'ResourceFinish':
|
||||
processResourceFinish(event, startTime, currentProfilerData, state);
|
||||
break;
|
||||
case 'ResourceReceivedData':
|
||||
processResourceReceivedData(
|
||||
event,
|
||||
startTime,
|
||||
currentProfilerData,
|
||||
state,
|
||||
);
|
||||
break;
|
||||
case 'ResourceReceiveResponse':
|
||||
processResourceReceiveResponse(
|
||||
event,
|
||||
startTime,
|
||||
currentProfilerData,
|
||||
state,
|
||||
);
|
||||
break;
|
||||
case 'ResourceSendRequest':
|
||||
processResourceSendRequest(
|
||||
event,
|
||||
startTime,
|
||||
currentProfilerData,
|
||||
state,
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'blink.user_timing':
|
||||
const startTime = (ts - currentProfilerData.startTime) / 1000;
|
||||
|
||||
if (name.startsWith('--react-version-')) {
|
||||
const [reactVersion] = name.substr(16).split('-');
|
||||
currentProfilerData.reactVersion = reactVersion;
|
||||
@@ -714,6 +858,7 @@ export default async function preprocessData(
|
||||
laneToLabelMap: new Map(),
|
||||
laneToReactMeasureMap,
|
||||
nativeEvents: [],
|
||||
networkMeasures: [],
|
||||
otherUserTimingMarks: [],
|
||||
reactVersion: null,
|
||||
schedulingEvents: [],
|
||||
@@ -759,6 +904,7 @@ export default async function preprocessData(
|
||||
potentialLongNestedUpdate: null,
|
||||
potentialLongNestedUpdates: [],
|
||||
potentialSuspenseEventsOutsideOfTransition: [],
|
||||
requestIdToNetworkMeasureMap: new Map(),
|
||||
uidCounter: 0,
|
||||
unresolvedSuspenseEvents: new Map(),
|
||||
};
|
||||
|
||||
+15
-1
@@ -91,6 +91,19 @@ export type ReactMeasure = {|
|
||||
+depth: number,
|
||||
|};
|
||||
|
||||
export type NetworkMeasure = {|
|
||||
+depth: number,
|
||||
finishTimestamp: Milliseconds,
|
||||
firstReceivedDataTimestamp: Milliseconds,
|
||||
lastReceivedDataTimestamp: Milliseconds,
|
||||
priority: string,
|
||||
receiveResponseTimestamp: Milliseconds,
|
||||
+requestId: string,
|
||||
requestMethod: string,
|
||||
sendRequestTimestamp: Milliseconds,
|
||||
url: string,
|
||||
|};
|
||||
|
||||
export type ReactComponentMeasure = {|
|
||||
+componentName: string,
|
||||
duration: Milliseconds,
|
||||
@@ -155,6 +168,7 @@ export type ReactProfilerData = {|
|
||||
laneToLabelMap: Map<ReactLane, string>,
|
||||
laneToReactMeasureMap: Map<ReactLane, ReactMeasure[]>,
|
||||
nativeEvents: NativeEvent[],
|
||||
networkMeasures: NetworkMeasure[],
|
||||
otherUserTimingMarks: UserTimingMark[],
|
||||
reactVersion: string | null,
|
||||
schedulingEvents: SchedulingEvent[],
|
||||
@@ -165,10 +179,10 @@ export type ReactProfilerData = {|
|
||||
|
||||
export type ReactHoverContextInfo = {|
|
||||
componentMeasure: ReactComponentMeasure | null,
|
||||
data: $ReadOnly<ReactProfilerData> | null,
|
||||
flamechartStackFrame: FlamechartStackFrame | null,
|
||||
measure: ReactMeasure | null,
|
||||
nativeEvent: NativeEvent | null,
|
||||
networkMeasure: NetworkMeasure | null,
|
||||
schedulingEvent: SchedulingEvent | null,
|
||||
suspenseEvent: SuspenseEvent | null,
|
||||
snapshot: Snapshot | null,
|
||||
|
||||
+9
-2
@@ -156,7 +156,7 @@ export class HorizontalPanAndZoomView extends View {
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
);
|
||||
if (isHovered) {
|
||||
if (isHovered && viewRefs.hoveredView === null) {
|
||||
viewRefs.hoveredView = this;
|
||||
}
|
||||
|
||||
@@ -169,9 +169,16 @@ export class HorizontalPanAndZoomView extends View {
|
||||
if (!this._isPanning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't prevent mouse-move events from bubbling if they are vertical drags.
|
||||
const {movementX, movementY} = interaction.payload.event;
|
||||
if (Math.abs(movementX) < Math.abs(movementY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = translateState({
|
||||
state: this._viewState.horizontalScrollState,
|
||||
delta: interaction.payload.event.movementX,
|
||||
delta: movementX,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._viewState.updateHorizontalScrollState(newState);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactHoverContextInfo} from '../types';
|
||||
import type {Interaction} from './useCanvasInteraction';
|
||||
import type {Size} from './geometry';
|
||||
|
||||
@@ -14,6 +15,7 @@ import memoize from 'memoize-one';
|
||||
|
||||
import {View} from './View';
|
||||
import {zeroPoint} from './geometry';
|
||||
import {DPR} from '../content-views/constants';
|
||||
|
||||
export type ViewRefs = {|
|
||||
activeView: View | null,
|
||||
@@ -22,12 +24,10 @@ export type ViewRefs = {|
|
||||
|
||||
// hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
|
||||
function configureRetinaCanvas(canvas, height, width) {
|
||||
const dpr: number = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
canvas.width = width * DPR;
|
||||
canvas.height = height * DPR;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
return dpr;
|
||||
}
|
||||
|
||||
const getCanvasContext = memoize(
|
||||
@@ -39,14 +39,19 @@ const getCanvasContext = memoize(
|
||||
): CanvasRenderingContext2D => {
|
||||
const context = canvas.getContext('2d', {alpha: false});
|
||||
if (scaleCanvas) {
|
||||
const dpr = configureRetinaCanvas(canvas, height, width);
|
||||
configureRetinaCanvas(canvas, height, width);
|
||||
|
||||
// Scale all drawing operations by the dpr, so you don't have to worry about the difference.
|
||||
context.scale(dpr, dpr);
|
||||
context.scale(DPR, DPR);
|
||||
}
|
||||
return context;
|
||||
},
|
||||
);
|
||||
|
||||
type ResetHoveredEventFn = (
|
||||
partialState: $Shape<ReactHoverContextInfo>,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Represents the canvas surface and a view heirarchy. A surface is also the
|
||||
* place where all interactions enter the view heirarchy.
|
||||
@@ -57,11 +62,17 @@ export class Surface {
|
||||
_context: ?CanvasRenderingContext2D;
|
||||
_canvasSize: ?Size;
|
||||
|
||||
_resetHoveredEvent: ResetHoveredEventFn;
|
||||
|
||||
_viewRefs: ViewRefs = {
|
||||
activeView: null,
|
||||
hoveredView: null,
|
||||
};
|
||||
|
||||
constructor(resetHoveredEvent: ResetHoveredEventFn) {
|
||||
this._resetHoveredEvent = resetHoveredEvent;
|
||||
}
|
||||
|
||||
hasActiveView(): boolean {
|
||||
return this._viewRefs.activeView !== null;
|
||||
}
|
||||
@@ -107,12 +118,32 @@ export class Surface {
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
if (!this.rootView) {
|
||||
return;
|
||||
const rootView = this.rootView;
|
||||
if (rootView != null) {
|
||||
const viewRefs = this._viewRefs;
|
||||
switch (interaction.type) {
|
||||
case 'mousemove':
|
||||
// Clean out the hovered view before processing a new mouse move interaction.
|
||||
const hoveredView = viewRefs.hoveredView;
|
||||
viewRefs.hoveredView = null;
|
||||
|
||||
rootView.handleInteractionAndPropagateToSubviews(
|
||||
interaction,
|
||||
viewRefs,
|
||||
);
|
||||
|
||||
// If a previously hovered view is no longer hovered, update the outer state.
|
||||
if (hoveredView !== null && viewRefs.hoveredView === null) {
|
||||
this._resetHoveredEvent({});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
rootView.handleInteractionAndPropagateToSubviews(
|
||||
interaction,
|
||||
viewRefs,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.rootView.handleInteractionAndPropagateToSubviews(
|
||||
interaction,
|
||||
this._viewRefs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-5
@@ -199,12 +199,20 @@ export class VerticalScrollView extends View {
|
||||
if (!this._isPanning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't prevent mouse-move events from bubbling if they are horizontal drags.
|
||||
const {movementX, movementY} = interaction.payload.event;
|
||||
if (Math.abs(movementX) > Math.abs(movementY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
delta: interaction.payload.event.movementY,
|
||||
containerLength: this.frame.size.height,
|
||||
});
|
||||
this._setScrollState(newState);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -255,12 +263,14 @@ export class VerticalScrollView extends View {
|
||||
}
|
||||
|
||||
_setScrollState(proposedState: ScrollState): boolean {
|
||||
const height = this._contentView.frame.size.height;
|
||||
const contentHeight = this._contentView.frame.size.height;
|
||||
const containerHeight = this.frame.size.height;
|
||||
|
||||
const clampedState = clampState({
|
||||
state: proposedState,
|
||||
minContentLength: height,
|
||||
maxContentLength: height,
|
||||
containerLength: this.frame.size.height,
|
||||
minContentLength: contentHeight,
|
||||
maxContentLength: contentHeight,
|
||||
containerLength: containerHeight,
|
||||
});
|
||||
if (!areScrollStatesEqual(clampedState, this._scrollState)) {
|
||||
this._scrollState.offset = clampedState.offset;
|
||||
@@ -275,6 +285,13 @@ export class VerticalScrollView extends View {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Don't allow wheel events to bubble past this view even if we've scrolled to the edge.
|
||||
// It just feels bad to have the scrolling jump unexpectedly from in a container to the outer page.
|
||||
// The only exception is when the container fitst the contnet (no scrolling).
|
||||
if (contentHeight === containerHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
|
||||
'--color-resize-bar-dot': '#333333',
|
||||
'--color-scheduling-profiler-native-event': '#ccc',
|
||||
'--color-scheduling-profiler-native-event-hover': '#aaa',
|
||||
'--color-scheduling-profiler-network-primary': '#fcf3dc',
|
||||
'--color-scheduling-profiler-network-primary-hover': '#f0e7d1',
|
||||
'--color-scheduling-profiler-network-secondary': '#efc457',
|
||||
'--color-scheduling-profiler-network-secondary-hover': '#e3ba52',
|
||||
'--color-scheduling-profiler-priority-background': '#f6f6f6',
|
||||
'--color-scheduling-profiler-priority-border': '#eeeeee',
|
||||
'--color-scheduling-profiler-user-timing': '#c9cacd',
|
||||
@@ -276,6 +280,10 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any} = {
|
||||
'--color-resize-bar-dot': '#cfd1d5',
|
||||
'--color-scheduling-profiler-native-event': '#b2b2b2',
|
||||
'--color-scheduling-profiler-native-event-hover': '#949494',
|
||||
'--color-scheduling-profiler-network-primary': '#fcf3dc',
|
||||
'--color-scheduling-profiler-network-primary-hover': '#e3dbc5',
|
||||
'--color-scheduling-profiler-network-secondary': '#efc457',
|
||||
'--color-scheduling-profiler-network-secondary-hover': '#d6af4d',
|
||||
'--color-scheduling-profiler-priority-background': '#1d2129',
|
||||
'--color-scheduling-profiler-priority-border': '#282c34',
|
||||
'--color-scheduling-profiler-user-timing': '#c9cacd',
|
||||
|
||||
Reference in New Issue
Block a user