/**
* Copyright (c) Meta Platforms, Inc. and 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, TimelineData} 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: TimelineData;
onHover: ((event: NetworkMeasure | null) => void) | null = null;
constructor(surface: Surface, frame: Rect, profilerData: TimelineData) {
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[incompatible-use] 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(): IntrinsicSize {
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;
}
}
}