Files
2024-02-15 17:01:50 +00:00

177 lines
6.9 KiB
TypeScript

import type {mat3} from 'gl-matrix';
import type {FlamegraphSearch} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphSearch';
import {
computeHighlightedBounds,
ELLIPSIS,
getContext,
lowerBound,
resizeCanvasToDisplaySize,
upperBound,
} from 'sentry/utils/profiling/gl/utils';
import {TextRenderer} from 'sentry/utils/profiling/renderers/textRenderer';
import type {Flamegraph} from '../flamegraph';
import type {FlamegraphTheme} from '../flamegraph/flamegraphTheme';
import type {FlamegraphFrame} from '../flamegraphFrame';
import {getFlamegraphFrameSearchId} from '../flamegraphFrame';
import type {Rect} from '../speedscope';
import {findRangeBinarySearch, trimTextCenter} from '../speedscope';
class FlamegraphTextRenderer extends TextRenderer {
flamegraph: Flamegraph;
constructor(canvas: HTMLCanvasElement, theme: FlamegraphTheme, flamegraph: Flamegraph) {
super(canvas, theme);
this.canvas = canvas;
this.theme = theme;
this.flamegraph = flamegraph;
this.textCache = {};
this.context = getContext(canvas, '2d');
resizeCanvasToDisplaySize(canvas);
}
draw(
configView: Rect,
configViewToPhysicalSpace: mat3,
flamegraphSearchResults?: FlamegraphSearch['results']['frames']
): void {
// Make sure we set font size before we measure text for the first draw
const FONT_SIZE = this.theme.SIZES.BAR_FONT_SIZE * window.devicePixelRatio;
this.context.font = `${FONT_SIZE}px ${this.theme.FONTS.FRAME_FONT}`;
this.context.textBaseline = 'alphabetic';
this.maybeInvalidateCache();
const MIN_WIDTH = this.measureAndCacheText(ELLIPSIS).width;
const SIDE_PADDING = 2 * this.theme.SIZES.BAR_PADDING * window.devicePixelRatio;
const HALF_SIDE_PADDING = SIDE_PADDING / 2;
const BASELINE_OFFSET =
(this.theme.SIZES.BAR_HEIGHT - this.theme.SIZES.BAR_FONT_SIZE / 2) *
window.devicePixelRatio;
const HIGHLIGHT_BACKGROUND_COLOR = `rgb(${this.theme.COLORS.HIGHLIGHTED_LABEL_COLOR.join(
', '
)})`;
const TOP_BOUNDARY = configView.top - 1;
const BOTTOM_BOUNDARY = configView.bottom + 1;
const HAS_SEARCH_RESULTS =
flamegraphSearchResults && flamegraphSearchResults.size > 0;
const TEXT_Y_POSITION = FONT_SIZE / 2 - BASELINE_OFFSET;
// We start by iterating over root frames, so we draw the call stacks top-down.
// This allows us to do a couple optimizations that improve our best case performance.
// 1. We can skip drawing the entire tree if the root frame is not visible
// 2. We can skip drawing and
// Find the upper and lower bounds of the frames we need to draw so we dont end up
// iterating over all of the root frames and avoid creating shallow copies if we dont need to.
const start = lowerBound(configView.left, this.flamegraph.root.children);
const end = upperBound(configView.right, this.flamegraph.root.children);
// Populate the initial set of frames to draw
const frames: FlamegraphFrame[] = this.flamegraph.root.children.slice(start, end);
while (frames.length > 0) {
const frame = frames.pop()!;
if (frame.depth > BOTTOM_BOUNDARY) {
continue;
}
// We pin the start and end of the frame, so scrolling around keeps text pinned to the left or right side of the viewport
const pinnedStart = Math.max(frame.start, configView.left);
const pinnedEnd = Math.min(frame.end, configView.right);
// Transform frame to physical space coordinates. This does the same operation as
// Rect.transformRect, but without allocating a new Rect object.
const frameWidth =
(pinnedEnd - pinnedStart) * configViewToPhysicalSpace[0] +
configViewToPhysicalSpace[3];
// Since the text is not exactly aligned to the left/right bounds of the frame, we need to subtract the padding
// from the total width, so that we can truncate the center of the text accurately.
const paddedRectangleWidth = frameWidth - SIDE_PADDING;
// Since children of a frame cannot be wider than the frame itself, we can exit early and discard the entire subtree
if (paddedRectangleWidth <= MIN_WIDTH) {
continue;
}
const endChild = upperBound(configView.right, frame.children);
for (let i = lowerBound(configView.left, frame.children); i < endChild; i++) {
frames.push(frame.children[i]);
}
// If a frame is lower than the top, we can skip drawing its text, however
// we can only do so after we have pushed it's children into the queue or else
// those children will never be drawn and the entire sub-tree will be skipped.
if (frame.depth < TOP_BOUNDARY) {
continue;
}
// Transform frame to physical space coordinates. This does the same operation as
// Rect.transformRect, but without allocating a new Rect object.
const frameHeight =
(pinnedEnd - pinnedStart) * configViewToPhysicalSpace[1] +
configViewToPhysicalSpace[4];
const frameX =
pinnedStart * configViewToPhysicalSpace[0] +
frame.depth * configViewToPhysicalSpace[3] +
configViewToPhysicalSpace[6];
const frameY =
pinnedStart * configViewToPhysicalSpace[1] +
frame.depth * configViewToPhysicalSpace[4] +
configViewToPhysicalSpace[7];
// We want to draw the text in the vertical center of the frame, so we substract half the height of the text.
// Since the origin of the rect in the inverted view is also inverted, we need to add the height.
const y = frameY + (frameHeight < 0 ? frameHeight : 0) + BASELINE_OFFSET;
const x = frameX + (frameWidth < 0 ? frameWidth : 0) + HALF_SIDE_PADDING;
const trim = trimTextCenter(
frame.frame.name,
findRangeBinarySearch(
{low: 0, high: paddedRectangleWidth},
n => this.measureAndCacheText(frame.frame.name.substring(0, n)).width,
paddedRectangleWidth
)[0]
);
if (HAS_SEARCH_RESULTS) {
const frameId = getFlamegraphFrameSearchId(frame);
const frameResults = flamegraphSearchResults.get(frameId);
if (frameResults) {
this.context.fillStyle = HIGHLIGHT_BACKGROUND_COLOR;
for (let i = 0; i < frameResults.match.length; i++) {
const highlightedBounds = computeHighlightedBounds(
frameResults.match[i],
trim
);
const frontMatter = trim.text.slice(0, highlightedBounds[0]);
const highlightWidth = this.measureAndCacheText(
trim.text.substring(highlightedBounds[0], highlightedBounds[1])
).width;
this.context.fillRect(
x + this.measureAndCacheText(frontMatter).width,
y + TEXT_Y_POSITION,
highlightWidth,
FONT_SIZE
);
}
}
}
this.context.fillStyle = this.theme.COLORS.LABEL_FONT_COLOR;
this.context.fillText(trim.text, x, y);
}
}
}
export {FlamegraphTextRenderer};