FlameGraph: Optimize rendering of muted regions (#78510)

Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
pull/78766/head
Leon Sorokin 2 years ago committed by GitHub
parent 9789c0cc79
commit 0daf0ad4b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18932
      packages/grafana-flamegraph/src/FlameGraph/__snapshots__/FlameGraph.test.tsx.snap
  2. 142
      packages/grafana-flamegraph/src/FlameGraph/rendering.ts

@ -1,5 +1,5 @@
import uFuzzy from '@leeoniya/ufuzzy'; import uFuzzy from '@leeoniya/ufuzzy';
import { RefObject, useEffect, useMemo, useState } from 'react'; import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import color from 'tinycolor2'; import color from 'tinycolor2';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
@ -79,16 +79,23 @@ export function useFlameRender(options: RenderOptions) {
// There is a bit of dependency injections here that does not add readability, mainly to prevent recomputing some // There is a bit of dependency injections here that does not add readability, mainly to prevent recomputing some
// common stuff for all the nodes in the graph when only once is enough. perf/readability tradeoff. // common stuff for all the nodes in the graph when only once is enough. perf/readability tradeoff.
const mutedColor = useMemo(() => {
const barMutedColor = color(theme.colors.background.secondary);
return theme.isLight ? barMutedColor.darken(10).toHexString() : barMutedColor.lighten(10).toHexString();
}, [theme]);
const getBarColor = useColorFunction( const getBarColor = useColorFunction(
totalColorTicks, totalColorTicks,
totalTicksRight, totalTicksRight,
colorScheme, colorScheme,
theme, theme,
mutedColor,
rangeMin, rangeMin,
rangeMax, rangeMax,
foundLabels, foundLabels,
focusedItemData ? focusedItemData.item.level : 0 focusedItemData ? focusedItemData.item.level : 0
); );
const renderFunc = useRenderFunc(ctx, data, getBarColor, textAlign, collapsedMap); const renderFunc = useRenderFunc(ctx, data, getBarColor, textAlign, collapsedMap);
useEffect(() => { useEffect(() => {
@ -97,73 +104,117 @@ export function useFlameRender(options: RenderOptions) {
} }
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
walkTree(root, direction, data, totalViewTicks, rangeMin, rangeMax, wrapperWidth, collapsedMap, renderFunc);
}, [ctx, data, root, wrapperWidth, rangeMin, rangeMax, totalViewTicks, direction, renderFunc, collapsedMap]); const mutedPath2D = new Path2D();
//
// Walk the tree and compute the dimensions for each item in the flamegraph.
//
walkTree(
root,
direction,
data,
totalViewTicks,
rangeMin,
rangeMax,
wrapperWidth,
collapsedMap,
(item, x, y, width, height, label, muted) => {
if (muted) {
// We do a bit of optimization for muted regions, and we render them all in single fill later on as they don't
// have labels and are the same color.
mutedPath2D.rect(x, y, width, height);
} else {
renderFunc(item, x, y, width, height, label);
}
}
);
// Only fill the muted rects
ctx.fillStyle = mutedColor;
ctx.fill(mutedPath2D);
}, [
ctx,
data,
root,
wrapperWidth,
rangeMin,
rangeMax,
totalViewTicks,
direction,
renderFunc,
collapsedMap,
mutedColor,
]);
} }
type RenderFunc = ( type RenderFunc = (item: LevelItem, x: number, y: number, width: number, height: number, label: string) => void;
type RenderFuncWrap = (
item: LevelItem, item: LevelItem,
x: number, x: number,
y: number, y: number,
width: number, width: number,
height: number, height: number,
label: string, label: string,
// muted means the width is too small, and we just show gray rectangle.
muted: boolean muted: boolean
) => void; ) => void;
/**
* Create a render function with some memoization to prevent excesive repainting of the canvas.
* @param ctx
* @param data
* @param getBarColor
* @param textAlign
* @param collapsedMap
*/
function useRenderFunc( function useRenderFunc(
ctx: CanvasRenderingContext2D | undefined, ctx: CanvasRenderingContext2D | undefined,
data: FlameGraphDataContainer, data: FlameGraphDataContainer,
getBarColor: (item: LevelItem, label: string, muted: boolean) => string, getBarColor: (item: LevelItem, label: string, muted: boolean) => string,
textAlign: TextAlign, textAlign: TextAlign,
collapsedMap: CollapsedMap collapsedMap: CollapsedMap
): RenderFunc { ) {
return useMemo(() => { return useMemo(() => {
if (!ctx) { if (!ctx) {
return () => {}; return () => {};
} }
return (item, x, y, width, height, label, muted) => { const renderFunc: RenderFunc = (item, x, y, width, height, label) => {
ctx.beginPath(); ctx.beginPath();
ctx.rect(x + (muted ? 0 : BAR_BORDER_WIDTH), y, width, height); ctx.rect(x + BAR_BORDER_WIDTH, y, width, height);
ctx.fillStyle = getBarColor(item, label, muted); ctx.fillStyle = getBarColor(item, label, false);
ctx.stroke();
ctx.fill();
const collapsedItemConfig = collapsedMap.get(item); const collapsedItemConfig = collapsedMap.get(item);
let finalLabel = label;
if (collapsedItemConfig && collapsedItemConfig.collapsed) { if (collapsedItemConfig && collapsedItemConfig.collapsed) {
const numberOfCollapsedItems = collapsedItemConfig.items.length; const numberOfCollapsedItems = collapsedItemConfig.items.length;
label = `(${numberOfCollapsedItems}) ` + label; finalLabel = `(${numberOfCollapsedItems}) ` + label;
} }
if (muted) { if (width >= LABEL_THRESHOLD) {
// Only fill the muted rects
ctx.fill();
} else {
ctx.stroke();
ctx.fill();
if (collapsedItemConfig) { if (collapsedItemConfig) {
if (width >= LABEL_THRESHOLD) { renderLabel(
renderLabel( ctx,
ctx, data,
data, finalLabel,
label, item,
item, width,
width, textAlign === 'left' ? x + GROUP_STRIP_MARGIN_LEFT + GROUP_TEXT_OFFSET : x,
textAlign === 'left' ? x + GROUP_STRIP_MARGIN_LEFT + GROUP_TEXT_OFFSET : x, y,
y, textAlign
textAlign );
);
renderGroupingStrip(ctx, x, y, height, item, collapsedItemConfig);
renderGroupingStrip(ctx, x, y, height, item, collapsedItemConfig);
}
} else { } else {
if (width >= LABEL_THRESHOLD) { renderLabel(ctx, data, finalLabel, item, width, x, y, textAlign);
renderLabel(ctx, data, label, item, width, x, y, textAlign);
}
} }
} }
}; };
return renderFunc;
}, [ctx, getBarColor, textAlign, data, collapsedMap]); }, [ctx, getBarColor, textAlign, data, collapsedMap]);
} }
@ -228,7 +279,7 @@ export function walkTree(
rangeMax: number, rangeMax: number,
wrapperWidth: number, wrapperWidth: number,
collapsedMap: CollapsedMap, collapsedMap: CollapsedMap,
renderFunc: RenderFunc renderFunc: RenderFuncWrap
) { ) {
// The levelOffset here is to keep track if items that we don't render because they are collapsed into single row. // The levelOffset here is to keep track if items that we don't render because they are collapsed into single row.
// That means we have to render next items with an offset of some rows up in the stack. // That means we have to render next items with an offset of some rows up in the stack.
@ -314,24 +365,18 @@ function useColorFunction(
totalTicksRight: number | undefined, totalTicksRight: number | undefined,
colorScheme: ColorScheme | ColorSchemeDiff, colorScheme: ColorScheme | ColorSchemeDiff,
theme: GrafanaTheme2, theme: GrafanaTheme2,
mutedColor: string,
rangeMin: number, rangeMin: number,
rangeMax: number, rangeMax: number,
foundNames: Set<string> | undefined, foundNames: Set<string> | undefined,
topLevel: number topLevel: number
) { ) {
return useMemo(() => { return useCallback(
// We use the same color for all muted bars so let's do it just once and reuse the result in the closure of the function getColor(item: LevelItem, label: string, muted: boolean) {
// returned function.
const barMutedColor = color(theme.colors.background.secondary);
const barMutedColorHex = theme.isLight
? barMutedColor.darken(10).toHexString()
: barMutedColor.lighten(10).toHexString();
return function getColor(item: LevelItem, label: string, muted: boolean) {
// If collapsed and no search we can quickly return the muted color // If collapsed and no search we can quickly return the muted color
if (muted && !foundNames) { if (muted && !foundNames) {
// Collapsed are always grayed // Collapsed are always grayed
return barMutedColorHex; return mutedColor;
} }
const barColor = const barColor =
@ -344,13 +389,14 @@ function useColorFunction(
if (foundNames) { if (foundNames) {
// Means we are searching, we use color for matches and gray the rest // Means we are searching, we use color for matches and gray the rest
return foundNames.has(label) ? barColor.toHslString() : barMutedColorHex; return foundNames.has(label) ? barColor.toHslString() : mutedColor;
} }
// Mute if we are above the focused symbol // Mute if we are above the focused symbol
return item.level > topLevel - 1 ? barColor.toHslString() : barColor.lighten(15).toHslString(); return item.level > topLevel - 1 ? barColor.toHslString() : barColor.lighten(15).toHslString();
}; },
}, [totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, foundNames, topLevel]); [totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, foundNames, topLevel, mutedColor]
);
} }
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement>, wrapperWidth: number, numberOfLevels: number) { function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement>, wrapperWidth: number, numberOfLevels: number) {

Loading…
Cancel
Save