Scheduling Profiler: Add network measures (#22112)

This commit is contained in:
Brian Vaughn
2021-08-18 12:17:59 -04:00
committed by GitHub
parent 9eb2aaaf81
commit f51579fffe
14 changed files with 872 additions and 136 deletions
+90 -16
View File
@@ -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,
@@ -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;
}
}
}
@@ -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';
@@ -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 = {|
@@ -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",
@@ -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
View File
@@ -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,
@@ -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,
);
}
}
@@ -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;
}
}
+8
View File
@@ -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',