Flamegraph: Remove unused code after move to package (#75103)

pull/75110/head^2
Andrej Ocenas 2 years ago committed by GitHub
parent 24ac1da817
commit b622ac2a66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .betterer.results
  2. 1
      packages/grafana-flamegraph/src/index.ts
  3. 5
      public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx
  4. 98
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.test.tsx
  5. 315
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx
  6. 59
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphContextMenu.tsx
  7. 73
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.test.tsx
  8. 124
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.tsx
  9. 156
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.test.tsx
  10. 202
      public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx
  11. 37730
      public/app/plugins/panel/flamegraph/components/FlameGraph/__snapshots__/FlameGraph.test.tsx.snap
  12. 25
      public/app/plugins/panel/flamegraph/components/FlameGraph/colors.test.ts
  13. 133
      public/app/plugins/panel/flamegraph/components/FlameGraph/colors.ts
  14. 205
      public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts
  15. 288
      public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.ts
  16. 84
      public/app/plugins/panel/flamegraph/components/FlameGraph/murmur3.ts
  17. 88
      public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.test.ts
  18. 317
      public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.ts
  19. 572
      public/app/plugins/panel/flamegraph/components/FlameGraph/testData/dataFlamebearer.ts
  20. 2466
      public/app/plugins/panel/flamegraph/components/FlameGraph/testData/dataNestedSet.ts
  21. 50
      public/app/plugins/panel/flamegraph/components/FlameGraph/testHelpers.test.ts
  22. 104
      public/app/plugins/panel/flamegraph/components/FlameGraph/testHelpers.ts
  23. 162
      public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.test.ts
  24. 134
      public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.ts
  25. 91
      public/app/plugins/panel/flamegraph/components/FlameGraphContainer.test.tsx
  26. 195
      public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx
  27. 86
      public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx
  28. 295
      public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx
  29. 76
      public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.test.tsx
  30. 300
      public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx
  31. 57
      public/app/plugins/panel/flamegraph/components/types.ts
  32. 8
      public/app/plugins/panel/flamegraph/constants.ts
  33. 3
      public/app/plugins/panel/flamegraph/suggestions.ts

@ -4323,9 +4323,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/gauge/GaugeMigrations.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

@ -1 +1,2 @@
export { default as FlameGraph, type Props } from './FlameGraphContainer';
export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform';

@ -1,12 +1,9 @@
import React from 'react';
import { CoreApp, PanelProps } from '@grafana/data';
import { FlameGraph } from '@grafana/flamegraph';
import { FlameGraph, checkFields, getMessageCheckFieldsResult } from '@grafana/flamegraph';
import { PanelDataErrorView, reportInteraction, config } from '@grafana/runtime';
import { checkFields, getMessageCheckFieldsResult } from './components/FlameGraph/dataTransform';
// import FlameGraphContainer from './components/FlameGraphContainer';
function interaction(name: string, context: Record<string, string | number> = {}) {
reportInteraction(`grafana_flamegraph_${name}`, {
app: CoreApp.Unknown,

@ -1,98 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { createDataFrame } from '@grafana/data';
import { ColorScheme } from '../types';
import FlameGraph from './FlameGraph';
import { FlameGraphDataContainer } from './dataTransform';
import { data } from './testData/dataNestedSet';
import 'jest-canvas-mock';
jest.mock('react-use', () => {
const reactUse = jest.requireActual('react-use');
return {
...reactUse,
useMeasure: () => {
const ref = React.useRef();
return [ref, { width: 1600 }];
},
};
});
describe('FlameGraph', () => {
function setup() {
const flameGraphData = createDataFrame(data);
const container = new FlameGraphDataContainer(flameGraphData);
const setRangeMin = jest.fn();
const setRangeMax = jest.fn();
const onItemFocused = jest.fn();
const onSandwich = jest.fn();
const onFocusPillClick = jest.fn();
const onSandwichPillClick = jest.fn();
const renderResult = render(
<FlameGraph
data={container}
rangeMin={0}
rangeMax={1}
search={''}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
onItemFocused={onItemFocused}
textAlign={'left'}
onSandwich={onSandwich}
onFocusPillClick={onFocusPillClick}
onSandwichPillClick={onSandwichPillClick}
colorScheme={ColorScheme.ValueBased}
/>
);
return {
renderResult,
mocks: {
setRangeMax,
setRangeMin,
onItemFocused,
onSandwich,
onFocusPillClick,
onSandwichPillClick,
},
};
}
it('should render without error', async () => {
setup();
});
it('should render correctly', async () => {
setup();
const canvas = screen.getByTestId('flameGraph') as HTMLCanvasElement;
const ctx = canvas!.getContext('2d');
const calls = ctx!.__getDrawCalls();
expect(calls).toMatchSnapshot();
});
it('should render metadata', async () => {
setup();
expect(screen.getByText('16.5 Bil | 16.5 Bil samples (Count)')).toBeDefined();
});
it('should render context menu', async () => {
const event = new MouseEvent('click', { bubbles: true });
Object.defineProperty(event, 'offsetX', { get: () => 10 });
Object.defineProperty(event, 'offsetY', { get: () => 10 });
Object.defineProperty(HTMLCanvasElement.prototype, 'clientWidth', { configurable: true, value: 500 });
setup();
const canvas = screen.getByTestId('flameGraph') as HTMLCanvasElement;
expect(canvas).toBeInTheDocument();
expect(screen.queryByTestId('contextMenu')).not.toBeInTheDocument();
fireEvent(canvas, event);
expect(screen.getByTestId('contextMenu')).toBeInTheDocument();
});
});

@ -1,315 +0,0 @@
// This component is based on logic from the flamebearer project
// https://github.com/mapbox/flamebearer
// ISC License
// Copyright (c) 2018, Mapbox
// Permission to use, copy, modify, and/or distribute this software for any purpose
// with or without fee is hereby granted, provided that the above copyright notice
// and this permission notice appear in all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
// THIS SOFTWARE.
import { css } from '@emotion/css';
import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useMeasure } from 'react-use';
import { Icon, useStyles2 } from '@grafana/ui';
import { PIXELS_PER_LEVEL } from '../../constants';
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
import FlameGraphContextMenu from './FlameGraphContextMenu';
import FlameGraphMetadata from './FlameGraphMetadata';
import FlameGraphTooltip from './FlameGraphTooltip';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
import { getBarX, useFlameRender } from './rendering';
type Props = {
data: FlameGraphDataContainer;
rangeMin: number;
rangeMax: number;
search: string;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
style?: React.CSSProperties;
onItemFocused: (data: ClickedItemData) => void;
focusedItemData?: ClickedItemData;
textAlign: TextAlign;
sandwichItem?: string;
onSandwich: (label: string) => void;
onFocusPillClick: () => void;
onSandwichPillClick: () => void;
colorScheme: ColorScheme | ColorSchemeDiff;
};
const FlameGraph = ({
data,
rangeMin,
rangeMax,
search,
setRangeMin,
setRangeMax,
onItemFocused,
focusedItemData,
textAlign,
onSandwich,
sandwichItem,
onFocusPillClick,
onSandwichPillClick,
colorScheme,
}: Props) => {
const styles = useStyles2(getStyles);
const [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount] = useMemo(() => {
let levels = data.getLevels();
let totalProfileTicks = levels.length ? levels[0][0].value : 0;
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
let callersCount = 0;
let totalViewTicks = totalProfileTicks;
if (sandwichItem) {
const [callers, callees] = data.getSandwichLevels(sandwichItem);
levels = [...callers, [], ...callees];
// We need this separate as in case of diff profile we to compute diff colors based on the original ticks.
totalViewTicks = callees[0]?.[0]?.value ?? 0;
callersCount = callers.length;
}
return [levels, totalProfileTicks, totalProfileTicksRight, totalViewTicks, callersCount];
}, [data, sandwichItem]);
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
const graphRef = useRef<HTMLCanvasElement>(null);
const [tooltipItem, setTooltipItem] = useState<LevelItem>();
const [clickedItemData, setClickedItemData] = useState<ClickedItemData>();
useFlameRender({
canvasRef: graphRef,
colorScheme,
data,
focusedItemData,
levels,
rangeMax,
rangeMin,
search,
textAlign,
totalViewTicks,
// We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.
totalColorTicks: data.isDiffFlamegraph() ? totalProfileTicks : totalViewTicks,
totalTicksRight: totalProfileTicksRight,
wrapperWidth,
});
const onGraphClick = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement>) => {
setTooltipItem(undefined);
const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
levels,
pixelsPerTick,
totalViewTicks,
rangeMin
);
// if clicking on a block in the canvas
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
const item = levels[levelIndex][barIndex];
setClickedItemData({
posY: e.clientY,
posX: e.clientX,
item,
level: levelIndex,
label: data.getLabel(item.itemIndexes[0]),
});
} else {
// if clicking on the canvas but there is no block beneath the cursor
setClickedItemData(undefined);
}
},
[data, rangeMin, rangeMax, totalViewTicks, levels]
);
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>();
const onGraphMouseMove = useCallback(
(e: ReactMouseEvent<HTMLCanvasElement>) => {
if (clickedItemData === undefined) {
setTooltipItem(undefined);
setMousePosition(undefined);
const pixelsPerTick = graphRef.current!.clientWidth / totalViewTicks / (rangeMax - rangeMin);
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
levels,
pixelsPerTick,
totalViewTicks,
rangeMin
);
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
setMousePosition({ x: e.clientX, y: e.clientY });
setTooltipItem(levels[levelIndex][barIndex]);
}
}
},
[rangeMin, rangeMax, totalViewTicks, clickedItemData, levels, setMousePosition]
);
const onGraphMouseLeave = useCallback(() => {
setTooltipItem(undefined);
}, []);
// hide context menu if outside the flame graph canvas is clicked
useEffect(() => {
const handleOnClick = (e: MouseEvent) => {
if (
e.target instanceof HTMLElement &&
e.target.parentElement?.id !== 'flameGraphCanvasContainer_clickOutsideCheck'
) {
setClickedItemData(undefined);
}
};
window.addEventListener('click', handleOnClick);
return () => window.removeEventListener('click', handleOnClick);
}, [setClickedItemData]);
return (
<div className={styles.graph}>
<FlameGraphMetadata
data={data}
focusedItem={focusedItemData}
sandwichedLabel={sandwichItem}
totalTicks={totalViewTicks}
onFocusPillClick={onFocusPillClick}
onSandwichPillClick={onSandwichPillClick}
/>
<div className={styles.canvasContainer}>
{sandwichItem && (
<div>
<div
className={styles.sandwichMarker}
style={{ height: (callersCount * PIXELS_PER_LEVEL) / window.devicePixelRatio }}
>
Callers
<Icon className={styles.sandwichMarkerIcon} name={'arrow-down'} />
</div>
<div className={styles.sandwichMarker} style={{ marginTop: PIXELS_PER_LEVEL / window.devicePixelRatio }}>
<Icon className={styles.sandwichMarkerIcon} name={'arrow-up'} />
Callees
</div>
</div>
)}
<div className={styles.canvasWrapper} id="flameGraphCanvasContainer_clickOutsideCheck" ref={sizeRef}>
<canvas
ref={graphRef}
data-testid="flameGraph"
onClick={onGraphClick}
onMouseMove={onGraphMouseMove}
onMouseLeave={onGraphMouseLeave}
/>
</div>
</div>
<FlameGraphTooltip position={mousePosition} item={tooltipItem} data={data} totalTicks={totalViewTicks} />
{clickedItemData && (
<FlameGraphContextMenu
itemData={clickedItemData}
onMenuItemClick={() => {
setClickedItemData(undefined);
}}
onItemFocus={() => {
setRangeMin(clickedItemData.item.start / totalViewTicks);
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalViewTicks);
onItemFocused(clickedItemData);
}}
onSandwich={() => {
onSandwich(data.getLabel(clickedItemData.item.itemIndexes[0]));
}}
/>
)}
</div>
);
};
const getStyles = () => ({
graph: css`
overflow: scroll;
height: 100%;
flex-grow: 1;
flex-basis: 50%;
`,
canvasContainer: css`
label: canvasContainer;
display: flex;
`,
canvasWrapper: css`
label: canvasWrapper;
cursor: pointer;
flex: 1;
overflow: hidden;
`,
sandwichMarker: css`
writing-mode: vertical-lr;
transform: rotate(180deg);
overflow: hidden;
white-space: nowrap;
`,
sandwichMarkerIcon: css`
vertical-align: baseline;
`,
});
// Convert pixel coordinates to bar coordinates in the levels array so that we can add mouse events like clicks to
// the canvas.
const convertPixelCoordinatesToBarCoordinates = (
// position relative to the start of the graph
pos: { x: number; y: number },
levels: LevelItem[][],
pixelsPerTick: number,
totalTicks: number,
rangeMin: number
) => {
const levelIndex = Math.floor(pos.y / (PIXELS_PER_LEVEL / window.devicePixelRatio));
const barIndex = getBarIndex(pos.x, levels[levelIndex], pixelsPerTick, totalTicks, rangeMin);
return { levelIndex, barIndex };
};
/**
* Binary search for a bar in a level, based on the X pixel coordinate. Useful for detecting which bar did user click
* on.
*/
const getBarIndex = (x: number, level: LevelItem[], pixelsPerTick: number, totalTicks: number, rangeMin: number) => {
if (level) {
let start = 0;
let end = level.length - 1;
while (start <= end) {
const midIndex = (start + end) >> 1;
const startOfBar = getBarX(level[midIndex].start, totalTicks, rangeMin, pixelsPerTick);
const startOfNextBar = getBarX(
level[midIndex].start + level[midIndex].value,
totalTicks,
rangeMin,
pixelsPerTick
);
if (startOfBar <= x && startOfNextBar >= x) {
return midIndex;
}
if (startOfBar > x) {
end = midIndex - 1;
} else {
start = midIndex + 1;
}
}
}
return -1;
};
export default FlameGraph;

@ -1,59 +0,0 @@
import React from 'react';
import { MenuItem, ContextMenu } from '@grafana/ui';
import { ClickedItemData } from '../types';
type Props = {
itemData: ClickedItemData;
onMenuItemClick: () => void;
onItemFocus: () => void;
onSandwich: () => void;
};
const FlameGraphContextMenu = ({ itemData, onMenuItemClick, onItemFocus, onSandwich }: Props) => {
function renderItems() {
return (
<>
<MenuItem
label="Focus block"
icon={'eye'}
onClick={() => {
onItemFocus();
onMenuItemClick();
}}
/>
<MenuItem
label="Copy function name"
icon={'copy'}
onClick={() => {
navigator.clipboard.writeText(itemData.label).then(() => {
onMenuItemClick();
});
}}
/>
<MenuItem
label="Sandwich view"
icon={'gf-show-context'}
onClick={() => {
onSandwich();
onMenuItemClick();
}}
/>
</>
);
}
return (
<div data-testid="contextMenu">
<ContextMenu
renderMenuItems={renderItems}
x={itemData.posX + 10}
y={itemData.posY}
focusOnOpen={false}
></ContextMenu>
</div>
);
};
export default FlameGraphContextMenu;

@ -1,73 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import FlameGraphMetadata from './FlameGraphMetadata';
import { textToDataContainer } from './testHelpers';
function setup(props: Partial<React.ComponentProps<typeof FlameGraphMetadata>> = {}) {
const container = textToDataContainer(`
[1//////////////]
[2][4//][7///]
[3][5]
[6]
`)!;
const onFocusPillClick = jest.fn();
const onSandwichPillClick = jest.fn();
const renderResult = render(
<FlameGraphMetadata
data={container}
totalTicks={17}
onFocusPillClick={onFocusPillClick}
onSandwichPillClick={onSandwichPillClick}
{...props}
/>
);
return { renderResult, mocks: { onSandwichPillClick, onFocusPillClick } };
}
describe('FlameGraphMetadata', () => {
it('shows only default pill if not focus or sandwich', () => {
setup();
expect(screen.getByText(/17 | 17 samples (Count)/)).toBeInTheDocument();
expect(screen.queryByLabelText(/Remove focus/)).toBeNull();
expect(screen.queryByLabelText(/Remove sandwich/)).toBeNull();
});
it('shows focus pill', async () => {
const { mocks } = setup({
focusedItem: {
label: '4',
item: {
value: 5,
children: [],
itemIndexes: [3],
start: 3,
},
level: 0,
posX: 0,
posY: 0,
},
});
expect(screen.getByText(/17 | 17 samples (Count)/)).toBeInTheDocument();
expect(screen.getByText(/29.41% of total/)).toBeInTheDocument();
expect(screen.queryByLabelText(/Remove sandwich/)).toBeNull();
await userEvent.click(screen.getByLabelText(/Remove focus/));
expect(mocks.onFocusPillClick).toHaveBeenCalledTimes(1);
});
it('shows sandwich state', async () => {
const { mocks } = setup({
sandwichedLabel: 'some/random/func.go',
});
expect(screen.getByText(/17 | 17 samples (Count)/)).toBeInTheDocument();
expect(screen.queryByLabelText(/Remove focus/)).toBeNull();
expect(screen.getByText(/func.go/)).toBeInTheDocument();
await userEvent.click(screen.getByLabelText(/Remove sandwich/));
expect(mocks.onSandwichPillClick).toHaveBeenCalledTimes(1);
});
});

@ -1,124 +0,0 @@
import { css } from '@emotion/css';
import React, { ReactNode } from 'react';
import { getValueFormat, GrafanaTheme2 } from '@grafana/data/src';
import { Icon, IconButton, useStyles2 } from '@grafana/ui';
import { ClickedItemData } from '../types';
import { FlameGraphDataContainer } from './dataTransform';
type Props = {
data: FlameGraphDataContainer;
totalTicks: number;
onFocusPillClick: () => void;
onSandwichPillClick: () => void;
focusedItem?: ClickedItemData;
sandwichedLabel?: string;
};
const FlameGraphMetadata = React.memo(
({ data, focusedItem, totalTicks, sandwichedLabel, onFocusPillClick, onSandwichPillClick }: Props) => {
const styles = useStyles2(getStyles);
const parts: ReactNode[] = [];
const ticksVal = getValueFormat('short')(totalTicks);
const displayValue = data.valueDisplayProcessor(totalTicks);
let unitValue = displayValue.text + displayValue.suffix;
const unitTitle = data.getUnitTitle();
if (unitTitle === 'Count') {
if (!displayValue.suffix) {
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
unitValue = displayValue.text;
}
}
parts.push(
<div className={styles.metadataPill} key={'default'}>
{unitValue} | {ticksVal.text}
{ticksVal.suffix} samples ({unitTitle})
</div>
);
if (sandwichedLabel) {
parts.push(
<span key={'sandwich'}>
<Icon size={'sm'} name={'angle-right'} />
<div className={styles.metadataPill}>
<Icon size={'sm'} name={'gf-show-context'} />{' '}
<span className={styles.metadataPillName}>
{sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)}
</span>
<IconButton
className={styles.pillCloseButton}
name={'times'}
size={'sm'}
onClick={onSandwichPillClick}
tooltip={'Remove sandwich view'}
aria-label={'Remove sandwich view'}
/>
</div>
</span>
);
}
if (focusedItem) {
const percentValue = Math.round(10000 * (focusedItem.item.value / totalTicks)) / 100;
parts.push(
<span key={'focus'}>
<Icon size={'sm'} name={'angle-right'} />
<div className={styles.metadataPill}>
<Icon size={'sm'} name={'eye'} /> {percentValue}% of total
<IconButton
className={styles.pillCloseButton}
name={'times'}
size={'sm'}
onClick={onFocusPillClick}
tooltip={'Remove focus'}
aria-label={'Remove focus'}
/>
</div>
</span>
);
}
return <>{<div className={styles.metadata}>{parts}</div>}</>;
}
);
FlameGraphMetadata.displayName = 'FlameGraphMetadata';
const getStyles = (theme: GrafanaTheme2) => ({
metadataPill: css`
label: metadataPill;
display: inline-flex;
align-items: center;
background: ${theme.colors.background.secondary};
border-radius: ${theme.shape.borderRadius(8)};
padding: ${theme.spacing(0.5, 1)};
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
line-height: ${theme.typography.bodySmall.lineHeight};
color: ${theme.colors.text.secondary};
`,
pillCloseButton: css`
label: pillCloseButton;
vertical-align: text-bottom;
margin: ${theme.spacing(0, 0.5)};
`,
metadata: css`
margin: 8px 0;
text-align: center;
`,
metadataPillName: css`
label: metadataPillName;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: ${theme.spacing(0.5)};
`,
});
export default FlameGraphMetadata;

@ -1,156 +0,0 @@
import { Field, FieldType, createDataFrame } from '@grafana/data';
import { getDiffTooltipData, getTooltipData } from './FlameGraphTooltip';
import { FlameGraphDataContainer } from './dataTransform';
function setupData(unit?: string) {
const flameGraphData = createDataFrame({
fields: [
{ name: 'level', values: [0] },
unit ? makeField('value', unit, [8_624_078_250]) : { name: 'value', values: [8_624_078_250] },
{ name: 'self', values: [978_250] },
{ name: 'label', values: ['total'] },
],
});
return new FlameGraphDataContainer(flameGraphData);
}
function setupDiffData() {
const flameGraphData = createDataFrame({
fields: [
{ name: 'level', values: [0, 1] },
{ name: 'value', values: [200, 90] },
{ name: 'valueRight', values: [100, 40] },
{ name: 'self', values: [110, 90] },
{ name: 'selfRight', values: [60, 40] },
{ name: 'label', values: ['total', 'func1'] },
],
});
return new FlameGraphDataContainer(flameGraphData);
}
describe('FlameGraphTooltip', () => {
it('for bytes', () => {
const tooltipData = getTooltipData(
setupData('bytes'),
{ start: 0, itemIndexes: [0], value: 8_624_078_250, children: [] },
8_624_078_250
);
expect(tooltipData).toEqual({
percentSelf: 0.01,
percentValue: 100,
unitTitle: 'RAM',
unitSelf: '955 KiB',
unitValue: '8.03 GiB',
samples: '8,624,078,250',
});
});
it('with default unit', () => {
const tooltipData = getTooltipData(
setupData('none'),
{ start: 0, itemIndexes: [0], value: 8_624_078_250, children: [] },
8_624_078_250
);
expect(tooltipData).toEqual({
percentSelf: 0.01,
percentValue: 100,
unitSelf: '978250',
unitTitle: 'Count',
unitValue: '8624078250',
samples: '8,624,078,250',
});
});
it('without unit', () => {
const tooltipData = getTooltipData(
setupData('none'),
{ start: 0, itemIndexes: [0], value: 8_624_078_250, children: [] },
8_624_078_250
);
expect(tooltipData).toEqual({
percentSelf: 0.01,
percentValue: 100,
unitTitle: 'Count',
unitSelf: '978250',
unitValue: '8624078250',
samples: '8,624,078,250',
});
});
it('for objects', () => {
const tooltipData = getTooltipData(
setupData('short'),
{ start: 0, itemIndexes: [0], value: 8_624_078_250, children: [] },
8_624_078_250
);
expect(tooltipData).toEqual({
percentSelf: 0.01,
percentValue: 100,
unitTitle: 'Count',
unitSelf: '978 K',
unitValue: '8.62 Bil',
samples: '8,624,078,250',
});
});
it('for nanoseconds', () => {
const tooltipData = getTooltipData(
setupData('ns'),
{ start: 0, itemIndexes: [0], value: 8_624_078_250, children: [] },
8_624_078_250
);
expect(tooltipData).toEqual({
percentSelf: 0.01,
percentValue: 100,
unitTitle: 'Time',
unitSelf: '978 µs',
unitValue: '8.62 s',
samples: '8,624,078,250',
});
});
});
describe('getDiffTooltipData', () => {
it('works with diff data', () => {
const tooltipData = getDiffTooltipData(
setupDiffData(),
{ start: 0, itemIndexes: [1], value: 90, valueRight: 40, children: [] },
200
);
expect(tooltipData).toEqual([
{
rowId: '1',
label: '% of total',
baseline: '50%',
comparison: '40%',
diff: '-20%',
},
{
rowId: '2',
label: 'Value',
baseline: '50',
comparison: '40',
diff: '-10',
},
{
rowId: '3',
label: 'Samples',
baseline: '50',
comparison: '40',
diff: '-10',
},
]);
});
});
function makeField(name: string, unit: string, values: number[]): Field {
return {
name,
type: FieldType.number,
config: {
unit,
},
values: values,
};
}

@ -1,202 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { DisplayValue, getValueFormat, GrafanaTheme2 } from '@grafana/data';
import { InteractiveTable, Portal, useStyles2, VizTooltipContainer } from '@grafana/ui';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
type Props = {
data: FlameGraphDataContainer;
totalTicks: number;
position?: { x: number; y: number };
item?: LevelItem;
};
const FlameGraphTooltip = ({ data, item, totalTicks, position }: Props) => {
const styles = useStyles2(getStyles);
if (!(item && position)) {
return null;
}
let content;
if (data.isDiffFlamegraph()) {
const tableData = getDiffTooltipData(data, item, totalTicks);
content = (
<InteractiveTable
className={styles.tooltipTable}
columns={[
{ id: 'label', header: '' },
{ id: 'baseline', header: 'Baseline' },
{ id: 'comparison', header: 'Comparison' },
{ id: 'diff', header: 'Diff' },
]}
data={tableData}
getRowId={(originalRow) => originalRow.rowId}
/>
);
} else {
const tooltipData = getTooltipData(data, item, totalTicks);
content = (
<p className={styles.lastParagraph}>
{tooltipData.unitTitle}
<br />
Total: <b>{tooltipData.unitValue}</b> ({tooltipData.percentValue}%)
<br />
Self: <b>{tooltipData.unitSelf}</b> ({tooltipData.percentSelf}%)
<br />
Samples: <b>{tooltipData.samples}</b>
</p>
);
}
return (
<Portal>
<VizTooltipContainer className={styles.tooltipContainer} position={position} offset={{ x: 15, y: 0 }}>
<div className={styles.tooltipContent}>
<p className={styles.tooltipName}>{data.getLabel(item.itemIndexes[0])}</p>
{content}
</div>
</VizTooltipContainer>
</Portal>
);
};
type TooltipData = {
percentValue: number;
percentSelf: number;
unitTitle: string;
unitValue: string;
unitSelf: string;
samples: string;
};
export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, totalTicks: number): TooltipData => {
const displayValue = data.valueDisplayProcessor(item.value);
const displaySelf = data.getSelfDisplay(item.itemIndexes);
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
const percentSelf = Math.round(10000 * (displaySelf.numeric / totalTicks)) / 100;
let unitValue = displayValue.text + displayValue.suffix;
let unitSelf = displaySelf.text + displaySelf.suffix;
const unitTitle = data.getUnitTitle();
if (unitTitle === 'Count') {
if (!displayValue.suffix) {
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
unitValue = displayValue.text;
}
if (!displaySelf.suffix) {
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
unitSelf = displaySelf.text;
}
}
return {
percentValue,
percentSelf,
unitTitle,
unitValue,
unitSelf,
samples: displayValue.numeric.toLocaleString(),
};
};
type DiffTableData = {
rowId: string;
label: string;
baseline: string | number;
comparison: string | number;
diff: string | number;
};
export const getDiffTooltipData = (
data: FlameGraphDataContainer,
item: LevelItem,
totalTicks: number
): DiffTableData[] => {
const levels = data.getLevels();
const totalTicksRight = levels[0][0].valueRight!;
const totalTicksLeft = totalTicks - totalTicksRight;
const valueLeft = item.value - item.valueRight!;
const percentageLeft = Math.round((10000 * valueLeft) / totalTicksLeft) / 100;
const percentageRight = Math.round((10000 * item.valueRight!) / totalTicksRight) / 100;
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
const displayValueLeft = getValueWithUnit(data, data.valueDisplayProcessor(valueLeft));
const displayValueRight = getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight!));
const shortValFormat = getValueFormat('short');
return [
{
rowId: '1',
label: '% of total',
baseline: percentageLeft + '%',
comparison: percentageRight + '%',
diff: shortValFormat(diff).text + '%',
},
{
rowId: '2',
label: 'Value',
baseline: displayValueLeft,
comparison: displayValueRight,
diff: getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight! - valueLeft)),
},
{
rowId: '3',
label: 'Samples',
baseline: shortValFormat(valueLeft).text,
comparison: shortValFormat(item.valueRight!).text,
diff: shortValFormat(item.valueRight! - valueLeft).text,
},
];
};
function getValueWithUnit(data: FlameGraphDataContainer, displayValue: DisplayValue) {
let unitValue = displayValue.text + displayValue.suffix;
const unitTitle = data.getUnitTitle();
if (unitTitle === 'Count') {
if (!displayValue.suffix) {
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
unitValue = displayValue.text;
}
}
return unitValue;
}
const getStyles = (theme: GrafanaTheme2) => ({
tooltipContainer: css`
title: tooltipContainer;
overflow: hidden;
`,
tooltipContent: css`
title: tooltipContent;
font-size: ${theme.typography.bodySmall.fontSize};
width: 100%;
`,
tooltipName: css`
title: tooltipName;
word-break: break-all;
`,
lastParagraph: css`
title: lastParagraph;
margin-bottom: 0;
`,
name: css`
title: name;
margin-bottom: 10px;
`,
tooltipTable: css`
title: tooltipTable;
max-width: 300px;
`,
});
export default FlameGraphTooltip;

@ -1,25 +0,0 @@
import { createTheme } from '@grafana/data';
import { getBarColorByPackage, getBarColorByValue } from './colors';
describe('getBarColorByValue', () => {
it('converts value to color', () => {
expect(getBarColorByValue(1, 100, 0, 1).toHslString()).toBe('hsl(50, 100%, 65%)');
expect(getBarColorByValue(100, 100, 0, 1).toHslString()).toBe('hsl(0, 100%, 72%)');
expect(getBarColorByValue(10, 100, 0, 0.1).toHslString()).toBe('hsl(0, 100%, 72%)');
});
});
describe('getBarColorByPackage', () => {
it('converts package to color', () => {
const theme = createTheme();
const c = getBarColorByPackage('net/http.HandlerFunc.ServeHTTP', theme);
expect(c.toHslString()).toBe('hsl(246, 40%, 65%)');
// same package should have same color
expect(getBarColorByPackage('net/http.(*conn).serve', theme).toHslString()).toBe(c.toHslString());
expect(getBarColorByPackage('github.com/grafana/phlare/pkg/util.Log.Wrap.func1', theme).toHslString()).toBe(
'hsl(105, 40%, 76%)'
);
});
});

@ -1,133 +0,0 @@
import { scaleLinear } from 'd3';
import color from 'tinycolor2';
import { GrafanaTheme2 } from '@grafana/data';
import { ColorSchemeDiff } from '../types';
import murmurhash3_32_gc from './murmur3';
// Colors taken from pyroscope, they should be from Grafana originally, but I didn't find from where exactly.
const packageColors = [
color({ h: 24, s: 69, l: 60 }),
color({ h: 34, s: 65, l: 65 }),
color({ h: 194, s: 52, l: 61 }),
color({ h: 163, s: 45, l: 55 }),
color({ h: 211, s: 48, l: 60 }),
color({ h: 246, s: 40, l: 65 }),
color({ h: 305, s: 63, l: 79 }),
color({ h: 47, s: 100, l: 73 }),
color({ r: 183, g: 219, b: 171 }),
color({ r: 244, g: 213, b: 152 }),
color({ r: 78, g: 146, b: 249 }),
color({ r: 249, g: 186, b: 143 }),
color({ r: 242, g: 145, b: 145 }),
color({ r: 130, g: 181, b: 216 }),
color({ r: 229, g: 168, b: 226 }),
color({ r: 174, g: 162, b: 224 }),
color({ r: 154, g: 196, b: 138 }),
color({ r: 242, g: 201, b: 109 }),
color({ r: 101, g: 197, b: 219 }),
color({ r: 249, g: 147, b: 78 }),
color({ r: 234, g: 100, b: 96 }),
color({ r: 81, g: 149, b: 206 }),
color({ r: 214, g: 131, b: 206 }),
color({ r: 128, g: 110, b: 183 }),
];
const byValueMinColor = getBarColorByValue(1, 100, 0, 1);
const byValueMaxColor = getBarColorByValue(100, 100, 0, 1);
export const byValueGradient = `linear-gradient(90deg, ${byValueMinColor} 0%, ${byValueMaxColor} 100%)`;
// Handpicked some vaguely rainbow-ish colors
export const byPackageGradient = `linear-gradient(90deg, ${packageColors[0]} 0%, ${packageColors[2]} 30%, ${packageColors[6]} 50%, ${packageColors[7]} 70%, ${packageColors[8]} 100%)`;
export function getBarColorByValue(value: number, totalTicks: number, rangeMin: number, rangeMax: number) {
// / (rangeMax - rangeMin) here so when you click a bar it will adjust the top (clicked)bar to the most 'intense' color
const intensity = Math.min(1, value / totalTicks / (rangeMax - rangeMin));
const h = 50 - 50 * intensity;
const l = 65 + 7 * intensity;
return color({ h, s: 100, l });
}
export function getBarColorByPackage(label: string, theme: GrafanaTheme2) {
const packageName = getPackageName(label);
// TODO: similar thing happens in trace view with selecting colors of the spans, so maybe this could be unified.
const hash = murmurhash3_32_gc(packageName || '', 0);
const colorIndex = hash % packageColors.length;
let packageColor = packageColors[colorIndex];
if (theme.isLight) {
packageColor = packageColor.clone().brighten(15);
}
return packageColor;
}
// green to red
const diffDefaultColors = ['rgb(0, 170, 0)', 'rgb(148, 142, 142)', 'rgb(200, 0, 0)'];
export const diffDefaultGradient = `linear-gradient(90deg, ${diffDefaultColors[0]} 0%, ${diffDefaultColors[1]} 50%, ${diffDefaultColors[2]} 100%)`;
const diffColorBlindColors = ['rgb(26, 133, 255)', 'rgb(148, 142, 142)', 'rgb(220, 50, 32)'];
export const diffColorBlindGradient = `linear-gradient(90deg, ${diffColorBlindColors[0]} 0%, ${diffColorBlindColors[1]} 50%, ${diffColorBlindColors[2]} 100%)`;
export function getBarColorByDiff(
ticks: number,
ticksRight: number,
totalTicks: number,
totalTicksRight: number,
colorScheme: ColorSchemeDiff
) {
const ticksLeft = ticks - ticksRight;
const totalTicksLeft = totalTicks - totalTicksRight;
const percentageLeft = Math.round((10000 * ticksLeft) / totalTicksLeft) / 100;
const percentageRight = Math.round((10000 * ticksRight) / totalTicksRight) / 100;
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
const range = colorScheme === ColorSchemeDiff.Default ? diffDefaultColors : diffColorBlindColors;
const colorScale = scaleLinear()
.domain([-100, 0, 100])
// TODO types from DefinitelyTyped seem to mismatch
// @ts-ignore
.range(range);
// TODO types from DefinitelyTyped seem to mismatch
// @ts-ignore
const rgbString: string = colorScale(diff);
return color(rgbString);
}
// const getColors = memoizeOne((theme) => getFilteredColors(colors, theme));
// Different regexes to get the package name and function name from the label. We may at some point get an info about
// the language from the backend and use the right regex but right now we just try all of them from most to least
// specific.
const matchers = [
['phpspy', /^(?<packageName>(.*\/)*)(?<filename>.*\.php+)(?<line_info>.*)$/],
['pyspy', /^(?<packageName>(.*\/)*)(?<filename>.*\.py+)(?<line_info>.*)$/],
['rbspy', /^(?<packageName>(.*\/)*)(?<filename>.*\.rb+)(?<line_info>.*)$/],
[
'nodespy',
/^(\.\/node_modules\/)?(?<packageName>[^/]*)(?<filename>.*\.?(jsx?|tsx?)?):(?<functionName>.*):(?<line_info>.*)$/,
],
['gospy', /^(?<packageName>.*?\/.*?\.|.*?\.|.+)(?<functionName>.*)$/], // also 'scrape'
['javaspy', /^(?<packageName>.+\/)(?<filename>.+\.)(?<functionName>.+)$/],
['dotnetspy', /^(?<packageName>.+)\.(.+)\.(.+)\(.*\)$/],
['tracing', /^(?<packageName>.+?):.*$/],
['pyroscope-rs', /^(?<packageName>[^::]+)/],
['ebpfspy', /^(?<packageName>.+)$/],
['unknown', /^(?<packageName>.+)$/],
];
// Get the package name from the symbol. Try matchers from the list and return first one that matches.
function getPackageName(name: string): string | undefined {
for (const [_, matcher] of matchers) {
const match = name.match(matcher);
if (match) {
return match.groups?.packageName || '';
}
}
return undefined;
}

@ -1,205 +0,0 @@
import { createDataFrame, DataFrameDTO, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
describe('nestedSetToLevels', () => {
it('converts nested set data frame to levels', () => {
// [1------]
// [2---][6]
// [3][5][7]
// [4] [8]
// [9]
const frame = createDataFrame({
fields: [
{ name: 'level', values: [0, 1, 2, 3, 2, 1, 2, 3, 4] },
{ name: 'value', values: [10, 5, 3, 1, 1, 4, 3, 2, 1] },
{ name: 'label', values: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], type: FieldType.string },
{ name: 'self', values: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
],
});
const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
const n9: LevelItem = { itemIndexes: [8], start: 5, children: [], value: 1 };
const n8: LevelItem = { itemIndexes: [7], start: 5, children: [n9], value: 2 };
const n7: LevelItem = { itemIndexes: [6], start: 5, children: [n8], value: 3 };
const n6: LevelItem = { itemIndexes: [5], start: 5, children: [n7], value: 4 };
const n5: LevelItem = { itemIndexes: [4], start: 3, children: [], value: 1 };
const n4: LevelItem = { itemIndexes: [3], start: 0, children: [], value: 1 };
const n3: LevelItem = { itemIndexes: [2], start: 0, children: [n4], value: 3 };
const n2: LevelItem = { itemIndexes: [1], start: 0, children: [n3, n5], value: 5 };
const n1: LevelItem = { itemIndexes: [0], start: 0, children: [n2, n6], value: 10 };
n2.parents = [n1];
n6.parents = [n1];
n3.parents = [n2];
n5.parents = [n2];
n4.parents = [n3];
n7.parents = [n6];
n8.parents = [n7];
n9.parents = [n8];
expect(levels[0]).toEqual([n1]);
expect(levels[1]).toEqual([n2, n6]);
expect(levels[2]).toEqual([n3, n5, n7]);
expect(levels[3]).toEqual([n4, n8]);
expect(levels[4]).toEqual([n9]);
});
it('converts nested set data if multiple same level items', () => {
const frame = createDataFrame({
fields: [
{ name: 'level', values: [0, 1, 1, 1] },
{ name: 'value', values: [10, 5, 3, 1] },
{ name: 'label', values: ['1', '2', '3', '4'], type: FieldType.string },
{ name: 'self', values: [10, 5, 3, 1] },
],
});
const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
const n4: LevelItem = { itemIndexes: [3], start: 8, children: [], value: 1 };
const n3: LevelItem = { itemIndexes: [2], start: 5, children: [], value: 3 };
const n2: LevelItem = { itemIndexes: [1], start: 0, children: [], value: 5 };
const n1: LevelItem = { itemIndexes: [0], start: 0, children: [n2, n3, n4], value: 10 };
n2.parents = [n1];
n3.parents = [n1];
n4.parents = [n1];
expect(levels[0]).toEqual([n1]);
expect(levels[1]).toEqual([n2, n3, n4]);
});
it('handles diff data', () => {
const frame = createDataFrame({
fields: [
{ name: 'level', values: [0, 1, 1, 1] },
{ name: 'value', values: [10, 5, 3, 1] },
{ name: 'valueRight', values: [10, 4, 2, 1] },
{ name: 'label', values: ['1', '2', '3', '4'], type: FieldType.string },
{ name: 'self', values: [10, 5, 3, 1] },
{ name: 'selfRight', values: [10, 4, 2, 1] },
],
});
const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
expect(levels[1][0]).toMatchObject({ itemIndexes: [1], value: 9, valueRight: 4 });
expect(levels[1][1]).toMatchObject({ itemIndexes: [2], value: 5, valueRight: 2 });
expect(levels[1][2]).toMatchObject({ itemIndexes: [3], value: 2, valueRight: 1 });
});
});
describe('diffFlamebearerToDataFrameDTO', () => {
it('works', function () {
// The main point of this test is to have some easy way to convert flamebearer data to data frame, so it can be used
// for example in test data source. Reason is in grafana we don't have a way to produce diff frames and so we have
// to use pyro app which gives you flamebearer format. So if you need to create a diff data frame to save somewhere
// just log the frame and copy the values.
const levels = [
[0, 378, 0, 0, 316, 0, 0],
[0, 12, 0, 0, 16, 0, 1],
];
const names = ['total', 'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize'];
const frame = diffFlamebearerToDataFrameDTO(levels, names);
// console.log(JSON.stringify(frame));
expect(frame).toMatchObject({
name: 'response',
meta: { preferredVisualisationType: 'flamegraph' },
fields: [
{ name: 'level', values: [0, 1] },
{ name: 'label', values: ['total', 'System.Threading!ThreadPoolWorkQueueThreadLocals.Finalize'] },
{ name: 'self', values: [0, 0] },
{ name: 'value', values: [378, 12] },
{ name: 'selfRight', values: [0, 0] },
{ name: 'valueRight', values: [316, 16] },
],
});
});
});
function getNodes(level: number[], names: string[]) {
const nodes = [];
for (let i = 0; i < level.length; i += 7) {
nodes.push({
level: 0,
label: names[level[i + 6]],
self: level[i + 2],
val: level[i + 1],
selfRight: level[i + 5],
valRight: level[i + 4],
valTotal: level[i + 1] + level[i + 4],
offset: level[i],
offsetRight: level[i + 3],
offsetTotal: level[i] + level[i + 3],
children: [],
});
}
return nodes;
}
function diffFlamebearerToDataFrameDTO(levels: number[][], names: string[]) {
const nodeLevels: any[][] = [];
for (let i = 0; i < levels.length; i++) {
nodeLevels[i] = [];
for (const node of getNodes(levels[i], names)) {
node.level = i;
nodeLevels[i].push(node);
if (i > 0) {
const prevNodesInLevel = nodeLevels[i].slice(0, -1);
const currentNodeStart =
prevNodesInLevel.reduce((acc, n) => n.offsetTotal + n.valTotal + acc, 0) + node.offsetTotal;
const prevLevel = nodeLevels[i - 1];
let prevLevelOffset = 0;
for (const prevLevelNode of prevLevel) {
const parentNodeStart = prevLevelOffset + prevLevelNode.offsetTotal;
const parentNodeEnd = parentNodeStart + prevLevelNode.valTotal;
if (parentNodeStart <= currentNodeStart && parentNodeEnd > currentNodeStart) {
prevLevelNode.children.push(node);
break;
} else {
prevLevelOffset += prevLevelNode.offsetTotal + prevLevelNode.valTotal;
}
}
}
}
}
const root = nodeLevels[0][0];
const stack = [root];
const labelValues = [];
const levelValues = [];
const selfValues = [];
const valueValues = [];
const selfRightValues = [];
const valueRightValues = [];
while (stack.length) {
const node = stack.shift();
labelValues.push(node.label);
levelValues.push(node.level);
selfValues.push(node.self);
valueValues.push(node.val);
selfRightValues.push(node.selfRight);
valueRightValues.push(node.valRight);
stack.unshift(...node.children);
}
const frame: DataFrameDTO = {
name: 'response',
meta: { preferredVisualisationType: 'flamegraph' },
fields: [
{ name: 'level', values: levelValues },
{ name: 'label', values: labelValues, type: FieldType.string },
{ name: 'self', values: selfValues },
{ name: 'value', values: valueValues },
{ name: 'selfRight', values: selfRightValues },
{ name: 'valueRight', values: valueRightValues },
],
};
return frame;
}

@ -1,288 +0,0 @@
import {
createTheme,
DataFrame,
DisplayProcessor,
Field,
FieldType,
getDisplayProcessor,
GrafanaTheme2,
} from '@grafana/data';
import { SampleUnit } from '../types';
import { mergeParentSubtrees, mergeSubtrees } from './treeTransforms';
export type LevelItem = {
// Offset from the start of the level.
start: number;
// Value here can be different from a value of items in the data frame as for callers tree in sandwich view we have
// to trim the value to correspond only to the part used by the children in the subtree.
// In case of diff profile this is actually left + right value.
value: number;
// Only exists for diff profiles.
valueRight?: number;
// Index into the data frame. It is an array because for sandwich views we may be merging multiple items into single
// node.
itemIndexes: number[];
children: LevelItem[];
parents?: LevelItem[];
};
/**
* Convert data frame with nested set format into array of level. This is mainly done for compatibility with current
* rendering code.
*/
export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelItem[][], Record<string, LevelItem[]>] {
const levels: LevelItem[][] = [];
let offset = 0;
let parent: LevelItem | undefined = undefined;
const uniqueLabels: Record<string, LevelItem[]> = {};
for (let i = 0; i < container.data.length; i++) {
const currentLevel = container.getLevel(i);
const prevLevel = i > 0 ? container.getLevel(i - 1) : undefined;
levels[currentLevel] = levels[currentLevel] || [];
if (prevLevel && prevLevel >= currentLevel) {
// We are going down a level or staying at the same level, so we are adding a sibling to the last item in a level.
// So we have to compute the correct offset based on the last sibling.
const lastSibling = levels[currentLevel][levels[currentLevel].length - 1];
offset =
lastSibling.start +
container.getValue(lastSibling.itemIndexes[0]) +
container.getValueRight(lastSibling.itemIndexes[0]);
// we assume there is always a single root node so lastSibling should always have a parent.
// Also it has to have the same parent because of how the items are ordered.
parent = lastSibling.parents![0];
}
const newItem: LevelItem = {
itemIndexes: [i],
value: container.getValue(i) + container.getValueRight(i),
valueRight: container.isDiffFlamegraph() ? container.getValueRight(i) : undefined,
start: offset,
parents: parent && [parent],
children: [],
};
if (uniqueLabels[container.getLabel(i)]) {
uniqueLabels[container.getLabel(i)].push(newItem);
} else {
uniqueLabels[container.getLabel(i)] = [newItem];
}
if (parent) {
parent.children.push(newItem);
}
parent = newItem;
levels[currentLevel].push(newItem);
}
return [levels, uniqueLabels];
}
export function getMessageCheckFieldsResult(wrongFields: CheckFieldsResult) {
if (wrongFields.missingFields.length) {
return `Data is missing fields: ${wrongFields.missingFields.join(', ')}`;
}
if (wrongFields.wrongTypeFields.length) {
return `Data has fields of wrong type: ${wrongFields.wrongTypeFields
.map((f) => `${f.name} has type ${f.type} but should be ${f.expectedTypes.join(' or ')}`)
.join(', ')}`;
}
return '';
}
export type CheckFieldsResult = {
wrongTypeFields: Array<{ name: string; expectedTypes: FieldType[]; type: FieldType }>;
missingFields: string[];
};
export function checkFields(data: DataFrame): CheckFieldsResult | undefined {
const fields: Array<[string, FieldType[]]> = [
['label', [FieldType.string, FieldType.enum]],
['level', [FieldType.number]],
['value', [FieldType.number]],
['self', [FieldType.number]],
];
const missingFields = [];
const wrongTypeFields = [];
for (const field of fields) {
const [name, types] = field;
const frameField = data.fields.find((f) => f.name === name);
if (!frameField) {
missingFields.push(name);
continue;
}
if (!types.includes(frameField.type)) {
wrongTypeFields.push({ name, expectedTypes: types, type: frameField.type });
}
}
if (missingFields.length > 0 || wrongTypeFields.length > 0) {
return {
wrongTypeFields,
missingFields,
};
}
return undefined;
}
export class FlameGraphDataContainer {
data: DataFrame;
labelField: Field;
levelField: Field;
valueField: Field;
selfField: Field;
// Optional fields for diff view
valueRightField?: Field;
selfRightField?: Field;
labelDisplayProcessor: DisplayProcessor;
valueDisplayProcessor: DisplayProcessor;
uniqueLabels: string[];
private levels: LevelItem[][] | undefined;
private uniqueLabelsMap: Record<string, LevelItem[]> | undefined;
constructor(data: DataFrame, theme: GrafanaTheme2 = createTheme()) {
this.data = data;
const wrongFields = checkFields(data);
if (wrongFields) {
throw new Error(getMessageCheckFieldsResult(wrongFields));
}
this.labelField = data.fields.find((f) => f.name === 'label')!;
this.levelField = data.fields.find((f) => f.name === 'level')!;
this.valueField = data.fields.find((f) => f.name === 'value')!;
this.selfField = data.fields.find((f) => f.name === 'self')!;
this.valueRightField = data.fields.find((f) => f.name === 'valueRight')!;
this.selfRightField = data.fields.find((f) => f.name === 'selfRight')!;
if ((this.valueField || this.selfField) && !(this.valueField && this.selfField)) {
throw new Error(
'Malformed dataFrame: both valueRight and selfRight has to be present if one of them is present.'
);
}
const enumConfig = this.labelField?.config?.type?.enum;
// Label can actually be an enum field so depending on that we have to access it through display processor. This is
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
// users to use this panel with correct query from data sources that do not return profiles natively.
if (enumConfig) {
this.labelDisplayProcessor = getDisplayProcessor({ field: this.labelField, theme });
this.uniqueLabels = enumConfig.text || [];
} else {
this.labelDisplayProcessor = (value) => ({
text: value + '',
numeric: 0,
});
this.uniqueLabels = [...new Set<string>(this.labelField.values)];
}
this.valueDisplayProcessor = getDisplayProcessor({
field: this.valueField,
theme,
});
}
isDiffFlamegraph() {
return this.valueRightField && this.selfRightField;
}
getLabel(index: number) {
return this.labelDisplayProcessor(this.labelField.values[index]).text;
}
getLevel(index: number) {
return this.levelField.values[index];
}
getValue(index: number | number[]) {
return fieldAccessor(this.valueField, index);
}
getValueRight(index: number | number[]) {
return fieldAccessor(this.valueRightField, index);
}
getSelf(index: number | number[]) {
return fieldAccessor(this.selfField, index);
}
getSelfRight(index: number | number[]) {
return fieldAccessor(this.selfRightField, index);
}
getSelfDisplay(index: number | number[]) {
return this.valueDisplayProcessor(this.getSelf(index));
}
getUniqueLabels() {
return this.uniqueLabels;
}
getUnitTitle() {
switch (this.valueField.config.unit) {
case SampleUnit.Bytes:
return 'RAM';
case SampleUnit.Nanoseconds:
return 'Time';
}
return 'Count';
}
getLevels() {
this.initLevels();
return this.levels!;
}
getSandwichLevels(label: string): [LevelItem[][], LevelItem[][]] {
const nodes = this.getNodesWithLabel(label);
if (!nodes?.length) {
return [[], []];
}
const callers = mergeParentSubtrees(nodes, this);
const callees = mergeSubtrees(nodes, this);
return [callers, callees];
}
getNodesWithLabel(label: string) {
this.initLevels();
return this.uniqueLabelsMap![label];
}
private initLevels() {
if (!this.levels) {
const [levels, uniqueLabelsMap] = nestedSetToLevels(this);
this.levels = levels;
this.uniqueLabelsMap = uniqueLabelsMap;
}
}
}
// Access field value with either single index or array of indexes. This is needed as we sometimes merge multiple
// into one, and we want to access aggregated values.
function fieldAccessor(field: Field | undefined, index: number | number[]) {
if (!field) {
return 0;
}
let indexArray: number[] = typeof index === 'number' ? [index] : index;
return indexArray.reduce((acc, index) => {
return acc + field.values[index];
}, 0);
}

@ -1,84 +0,0 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/*
Copyright (c) 2011 Gary Court
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* eslint-disable no-plusplus */
/* eslint-disable prefer-const */
/* eslint-disable no-bitwise */
/* eslint-disable camelcase */
export default function murmurhash3_32_gc(key: string, seed = 0) {
let remainder;
let bytes;
let h1;
let h1b;
let c1;
let c2;
let k1;
let i;
remainder = key.length & 3; // key.length % 4
bytes = key.length - remainder;
h1 = seed;
c1 = 0xcc9e2d51;
c2 = 0x1b873593;
i = 0;
while (i < bytes) {
k1 =
(key.charCodeAt(i) & 0xff) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
++i;
k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
}
k1 = 0;
switch (remainder) {
case 3:
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
// fall through
case 2:
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
// fall through
case 1:
k1 ^= key.charCodeAt(i) & 0xff;
// fall through
default:
k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
}
h1 ^= key.length;
h1 ^= h1 >>> 16;
h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
h1 ^= h1 >>> 13;
h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff;
h1 ^= h1 >>> 16;
return h1 >>> 0;
}

@ -1,88 +0,0 @@
import { createDataFrame, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
import { getRectDimensionsForLevel } from './rendering';
function makeDataFrame(fields: Record<string, Array<number | string>>) {
return createDataFrame({
fields: Object.keys(fields).map((key) => ({
name: key,
values: fields[key],
type: typeof fields[key][0] === 'string' ? FieldType.string : FieldType.number,
})),
});
}
describe('getRectDimensionsForLevel', () => {
it('should render a single item', () => {
const level: LevelItem[] = [{ start: 0, itemIndexes: [0], children: [], value: 100 }];
const container = new FlameGraphDataContainer(makeDataFrame({ value: [100], level: [1], label: ['1'], self: [0] }));
const result = getRectDimensionsForLevel(container, level, 1, 100, 0, 10);
expect(result).toEqual([
{
width: 999,
height: 22,
itemIndex: 0,
x: 0,
y: 22,
collapsed: false,
ticks: 100,
label: '1',
unitLabel: '100',
},
]);
});
it('should render a multiple items', () => {
const level: LevelItem[] = [
{ start: 0, itemIndexes: [0], children: [], value: 100 },
{ start: 100, itemIndexes: [1], children: [], value: 50 },
{ start: 150, itemIndexes: [2], children: [], value: 50 },
];
const container = new FlameGraphDataContainer(
makeDataFrame({ value: [100, 50, 50], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
);
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 10);
expect(result).toEqual([
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100', itemIndex: 0 },
{
width: 499,
height: 22,
x: 1000,
y: 44,
collapsed: false,
ticks: 50,
label: '2',
unitLabel: '50',
itemIndex: 1,
},
{
width: 499,
height: 22,
x: 1500,
y: 44,
collapsed: false,
ticks: 50,
label: '3',
unitLabel: '50',
itemIndex: 2,
},
]);
});
it('should render a collapsed items', () => {
const level: LevelItem[] = [
{ start: 0, itemIndexes: [0], children: [], value: 100 },
{ start: 100, itemIndexes: [1], children: [], value: 2 },
{ start: 102, itemIndexes: [2], children: [], value: 1 },
];
const container = new FlameGraphDataContainer(
makeDataFrame({ value: [100, 2, 1], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
);
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 1);
expect(result).toEqual([
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100', itemIndex: 0 },
{ width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2', unitLabel: '2', itemIndex: 1 },
]);
});
});

@ -1,317 +0,0 @@
import uFuzzy from '@leeoniya/ufuzzy';
import { RefObject, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { colors, useTheme2 } from '@grafana/ui';
import {
BAR_BORDER_WIDTH,
BAR_TEXT_PADDING_LEFT,
COLLAPSE_THRESHOLD,
HIDE_THRESHOLD,
LABEL_THRESHOLD,
PIXELS_PER_LEVEL,
} from '../../constants';
import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../types';
import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './colors';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
const ufuzzy = new uFuzzy();
type RenderOptions = {
canvasRef: RefObject<HTMLCanvasElement>;
data: FlameGraphDataContainer;
levels: LevelItem[][];
wrapperWidth: number;
// If we are rendering only zoomed in part of the graph.
rangeMin: number;
rangeMax: number;
search: string;
textAlign: TextAlign;
// Total ticks that will be used for sizing
totalViewTicks: number;
// Total ticks that will be used for computing colors as some color scheme (like in diff view) should not be affected
// by sandwich or focus view.
totalColorTicks: number;
// Total ticks used to compute the diff colors
totalTicksRight: number | undefined;
colorScheme: ColorScheme | ColorSchemeDiff;
focusedItemData?: ClickedItemData;
};
export function useFlameRender(options: RenderOptions) {
const {
canvasRef,
data,
levels,
wrapperWidth,
rangeMin,
rangeMax,
search,
textAlign,
totalViewTicks,
totalColorTicks,
totalTicksRight,
colorScheme,
focusedItemData,
} = options;
const foundLabels = useMemo(() => {
if (search) {
const foundLabels = new Set<string>();
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
if (idxs) {
for (let idx of idxs) {
foundLabels.add(data.getUniqueLabels()[idx]);
}
}
return foundLabels;
}
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
return undefined;
}, [search, data]);
const ctx = useSetupCanvas(canvasRef, wrapperWidth, levels.length);
const theme = useTheme2();
useEffect(() => {
if (!ctx) {
return;
}
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalViewTicks / (rangeMax - rangeMin);
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
const level = levels[levelIndex];
// Get all the dimensions of the rectangles for the level. We do this by level instead of per rectangle, because
// sometimes we collapse multiple bars into single rect.
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalViewTicks, rangeMin, pixelsPerTick);
for (const rect of dimensions) {
const focusedLevel = focusedItemData ? focusedItemData.level : 0;
// Render each rectangle based on the computed dimensions
renderRect(
ctx,
rect,
totalColorTicks,
totalTicksRight,
rangeMin,
rangeMax,
levelIndex,
focusedLevel,
foundLabels,
textAlign,
colorScheme,
theme
);
}
}
}, [
ctx,
data,
levels,
wrapperWidth,
rangeMin,
rangeMax,
search,
focusedItemData,
foundLabels,
textAlign,
totalViewTicks,
totalColorTicks,
totalTicksRight,
colorScheme,
theme,
]);
}
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement>, wrapperWidth: number, numberOfLevels: number) {
const [ctx, setCtx] = useState<CanvasRenderingContext2D>();
useEffect(() => {
if (!(numberOfLevels && canvasRef.current)) {
return;
}
const ctx = canvasRef.current.getContext('2d')!;
const height = PIXELS_PER_LEVEL * numberOfLevels;
canvasRef.current.width = Math.round(wrapperWidth * window.devicePixelRatio);
canvasRef.current.height = Math.round(height);
canvasRef.current.style.width = `${wrapperWidth}px`;
canvasRef.current.style.height = `${height / window.devicePixelRatio}px`;
ctx.textBaseline = 'middle';
ctx.font = 12 * window.devicePixelRatio + 'px monospace';
ctx.strokeStyle = 'white';
setCtx(ctx);
}, [canvasRef, setCtx, wrapperWidth, numberOfLevels]);
return ctx;
}
type RectData = {
width: number;
height: number;
x: number;
y: number;
collapsed: boolean;
ticks: number;
ticksRight?: number;
label: string;
unitLabel: string;
itemIndex: number;
};
/**
* Compute the pixel coordinates for each bar in a level. We need full level of bars so that we can collapse small bars
* into bigger rects.
*/
export function getRectDimensionsForLevel(
data: FlameGraphDataContainer,
level: LevelItem[],
levelIndex: number,
totalTicks: number,
rangeMin: number,
pixelsPerTick: number
): RectData[] {
const coordinatesLevel = [];
for (let barIndex = 0; barIndex < level.length; barIndex += 1) {
const item = level[barIndex];
const barX = getBarX(item.start, totalTicks, rangeMin, pixelsPerTick);
let curBarTicks = item.value;
// merge very small blocks into big "collapsed" ones for performance
const collapsed = curBarTicks * pixelsPerTick <= COLLAPSE_THRESHOLD;
if (collapsed) {
while (
barIndex < level.length - 1 &&
item.start + curBarTicks === level[barIndex + 1].start &&
level[barIndex + 1].value * pixelsPerTick <= COLLAPSE_THRESHOLD
) {
barIndex += 1;
curBarTicks += level[barIndex].value;
}
}
const displayValue = data.valueDisplayProcessor(item.value);
let unit = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text;
const width = curBarTicks * pixelsPerTick - (collapsed ? 0 : BAR_BORDER_WIDTH * 2);
coordinatesLevel.push({
width,
height: PIXELS_PER_LEVEL,
x: barX,
y: levelIndex * PIXELS_PER_LEVEL,
collapsed,
ticks: curBarTicks,
// When collapsed this does not make that much sense but then we don't really use it anyway.
ticksRight: item.valueRight,
label: data.getLabel(item.itemIndexes[0]),
unitLabel: unit,
itemIndex: item.itemIndexes[0],
});
}
return coordinatesLevel;
}
export function renderRect(
ctx: CanvasRenderingContext2D,
rect: RectData,
totalTicks: number,
totalTicksRight: number | undefined,
rangeMin: number,
rangeMax: number,
levelIndex: number,
topLevelIndex: number,
foundNames: Set<string> | undefined,
textAlign: TextAlign,
colorScheme: ColorScheme | ColorSchemeDiff,
theme: GrafanaTheme2
) {
if (rect.width < HIDE_THRESHOLD) {
return;
}
ctx.beginPath();
ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height);
const color =
rect.ticksRight !== undefined &&
(colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind)
? getBarColorByDiff(rect.ticks, rect.ticksRight, totalTicks, totalTicksRight!, colorScheme)
: colorScheme === ColorScheme.ValueBased
? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax)
: getBarColorByPackage(rect.label, theme);
if (foundNames) {
// Means we are searching, we use color for matches and gray the rest
ctx.fillStyle = foundNames.has(rect.label) ? color.toHslString() : colors[55];
} else {
// No search
if (rect.collapsed) {
// Collapsed are always grayed
ctx.fillStyle = colors[55];
} else {
// Mute if we are above the focused symbol
ctx.fillStyle = levelIndex > topLevelIndex - 1 ? color.toHslString() : color.lighten(15).toHslString();
}
}
if (rect.collapsed) {
// Only fill the collapsed rects
ctx.fill();
return;
}
ctx.stroke();
ctx.fill();
if (rect.width >= LABEL_THRESHOLD) {
renderLabel(ctx, rect.label, rect, textAlign);
}
}
// Renders a text inside the node rectangle. It allows setting alignment of the text left or right which takes effect
// when text is too long to fit in the rectangle.
function renderLabel(ctx: CanvasRenderingContext2D, name: string, rect: RectData, textAlign: TextAlign) {
ctx.save();
ctx.clip(); // so text does not overflow
ctx.fillStyle = '#222';
// We only measure name here instead of full label because of how we deal with the units and aligning later.
const measure = ctx.measureText(name);
const spaceForTextInRect = rect.width - BAR_TEXT_PADDING_LEFT;
let label = `${name} (${rect.unitLabel})`;
let labelX = Math.max(rect.x, 0) + BAR_TEXT_PADDING_LEFT;
// We use the desired alignment only if there is not enough space for the text, otherwise we keep left alignment as
// that will already show full text.
if (measure.width > spaceForTextInRect) {
ctx.textAlign = textAlign;
// If aligned to the right we don't want to take the space with the unit label as the assumption is user wants to
// mainly see the name. This also reflects how pyro/flamegraph works.
if (textAlign === 'right') {
label = name;
labelX = rect.x + rect.width - BAR_TEXT_PADDING_LEFT;
}
}
ctx.fillText(label, labelX, rect.y + PIXELS_PER_LEVEL / 2);
ctx.restore();
}
/**
* Returns the X position of the bar. totalTicks * rangeMin is to adjust for any current zoom. So if we zoom to a
* section of the graph we align and shift the X coordinates accordingly.
* @param offset
* @param totalTicks
* @param rangeMin
* @param pixelsPerTick
*/
export function getBarX(offset: number, totalTicks: number, rangeMin: number, pixelsPerTick: number) {
return (offset - totalTicks * rangeMin) * pixelsPerTick;
}

@ -1,572 +0,0 @@
export const data = {
version: 1,
flamebearer: {
names: [
'total',
'runtime.mcall',
'runtime.park_m',
'runtime.schedule',
'runtime.resetspinning',
'runtime.wakep',
'runtime.startm',
'runtime.newm',
'runtime.allocm',
'github.com/bufbuild/connect-go.(*duplexHTTPCall).makeRequest',
'net/http.(*Client).Do',
'net/http.(*Client).do',
'net/http.(*Client).send',
'net/http.send',
'test/pkg/util.RoundTripperFunc.RoundTrip',
'test/pkg/util.WrapWithInstrumentedHTTPTransport.func1',
'github.com/opentracing-contrib/go-stdlib/nethttp.(*Transport).RoundTrip',
'github.com/opentracing-contrib/go-stdlib/nethttp.(*Tracer).start',
'github.com/uber/jaeger-client-go.(*Tracer).StartSpan',
'github.com/uber/jaeger-client-go.(*Tracer).startSpanWithOptions',
'github.com/uber/jaeger-client-go.(*Tracer).randomID',
'github.com/uber/jaeger-client-go.NewTracer.func2',
'sync.(*Pool).Get',
'sync.(*Pool).pin',
'sync.(*Pool).pinSlow',
'runtime.mstart',
'runtime.mstart0',
'runtime.mstart1',
'golang.org/x/net/http2.(*serverConn).writeFrameAsync',
'golang.org/x/net/http2.(*writeResHeaders).writeFrame',
'golang.org/x/net/http2.splitHeaderBlock',
'golang.org/x/net/http2.(*writeResHeaders).writeHeaderBlock',
'golang.org/x/net/http2.(*Framer).WriteHeaders',
'golang.org/x/net/http2.(*Framer).endWrite',
'golang.org/x/net/http2.(*bufferedWriter).Write',
'golang.org/x/net/http2.glob..func8',
'bufio.NewWriterSize',
'regexp/syntax.(*compiler).compile',
'regexp/syntax.(*compiler).rune',
'regexp/syntax.(*compiler).inst',
'runtime.systemstack',
'runtime.newproc.func1',
'runtime.newproc1',
'runtime.malg',
'google.golang.org/grpc/internal/transport.newHTTP2Client.func3',
'google.golang.org/grpc/internal/transport.(*loopyWriter).run',
'google.golang.org/grpc/internal/transport.(*loopyWriter).handle',
'google.golang.org/grpc/internal/transport.(*loopyWriter).headerHandler',
'google.golang.org/grpc/internal/transport.(*loopyWriter).originateStream',
'google.golang.org/grpc/internal/transport.(*loopyWriter).writeHeader',
'golang.org/x/net/http2/hpack.(*Encoder).WriteField',
'golang.org/x/net/http2/hpack.(*dynamicTable).add',
'golang.org/x/net/http2/hpack.(*headerFieldTable).addEntry',
'net/http.(*persistConn).readLoop',
'net/http.(*persistConn).readResponse',
'net/http.ReadResponse',
'net/http.serverHandler.ServeHTTP',
'net/http.HandlerFunc.ServeHTTP',
'test/pkg/util.glob..func1.1',
'golang.org/x/net/http2/h2c.h2cHandler.ServeHTTP',
'test/pkg/create.(*create).initServer.func2.1',
'github.com/opentracing-contrib/go-stdlib/nethttp.MiddlewareFunc.func5',
'github.com/weaveworks/common/middleware.Log.Wrap.func1',
'github.com/weaveworks/common/middleware.Instrument.Wrap.func1',
'github.com/felixge/httpsnoop.CaptureMetricsFn',
'github.com/felixge/httpsnoop.(*Metrics).CaptureMetrics',
'github.com/weaveworks/common/middleware.Instrument.Wrap.func1.2',
'github.com/gorilla/mux.(*Router).ServeHTTP',
'net/http.(*ServeMux).ServeHTTP',
'net/http/pprof.Index',
'net/http/pprof.handler.ServeHTTP',
'runtime/pprof.(*Profile).WriteTo',
'runtime/pprof.writeGoroutine',
'runtime/pprof.writeRuntimeProfile',
'runtime/pprof.printCountProfile',
'runtime/pprof.(*profileBuilder).appendLocsForStack',
'runtime/pprof.(*profileBuilder).emitLocation',
'runtime/pprof.(*profileBuilder).flush',
'compress/gzip.(*Writer).Write',
'compress/flate.NewWriter',
'compress/flate.(*compressor).init',
'compress/flate.newHuffmanBitWriter',
'compress/flate.newHuffmanEncoder',
'test/pkg/create.(*create).Run.func3',
'github.com/weaveworks/common/signals.(*Handler).Loop',
'runtime.gcBgMarkWorker',
'runtime.gcMarkDone',
'runtime.semacquire',
'runtime.semacquire1',
'runtime.acquireSudog',
'test/dskit/services.(*BasicService).main',
'test/dskit/ring.(*Ring).loop',
'test/dskit/kv.metrics.WatchKey',
'github.com/weaveworks/common/instrument.CollectedRequest',
'test/dskit/kv.metrics.WatchKey.func1',
'test/dskit/kv.(*prefixedKVClient).WatchKey',
'test/dskit/kv/memberlist.(*Client).WatchKey',
'test/dskit/kv/memberlist.(*KV).WatchKey',
'test/dskit/kv/memberlist.(*KV).get',
'test/dskit/kv/memberlist.ValueDesc.Clone',
'test/dskit/ring.(*Desc).Clone',
'github.com/gogo/protobuf/proto.Clone',
'github.com/gogo/protobuf/proto.Merge',
'test/dskit/ring.(*Desc).XXX_Merge',
'github.com/gogo/protobuf/proto.(*InternalMessageInfo).Merge',
'github.com/gogo/protobuf/proto.(*mergeInfo).merge',
'github.com/gogo/protobuf/proto.(*mergeInfo).computeMergeInfo.func31',
'reflect.Value.SetMapIndex',
'reflect.mapassign_faststr',
'runtime/pprof.profileWriter',
'runtime/pprof.(*profileBuilder).addCPUData',
'runtime/pprof.(*profMap).lookup',
'runtime/pprof.newProfileBuilder',
'compress/gzip.NewWriterLevel',
'runtime/pprof.(*profileBuilder).build',
'compress/flate.newDeflateFast',
'github.com/hashicorp/memberlist.(*Memberlist).triggerFunc',
'github.com/hashicorp/memberlist.(*Memberlist).gossip',
'github.com/armon/go-metrics.MeasureSince',
'github.com/armon/go-metrics.(*Metrics).MeasureSince',
'github.com/armon/go-metrics.(*Metrics).MeasureSinceWithLabels',
'github.com/armon/go-metrics/prometheus.(*PrometheusSink).AddSampleWithLabels',
'github.com/armon/go-metrics/prometheus.flattenKey',
'regexp.(*Regexp).ReplaceAllString',
'regexp.(*Regexp).replaceAll',
'regexp.(*Regexp).doExecute',
'regexp.(*Regexp).backtrack',
'regexp.(*bitState).reset',
'runtime.main',
'main.main',
'test/pkg/create.New',
'github.com/prometheus/common/config.NewClientFromConfig',
'github.com/prometheus/common/config.NewRoundTripperFromConfig',
'github.com/mwitkow/go-conntrack.NewDialContextFunc',
'github.com/mwitkow/go-conntrack.PreRegisterDialerMetrics',
'github.com/prometheus/client_golang/prometheus.(*CounterVec).WithLabelValues',
'github.com/prometheus/client_golang/prometheus.(*CounterVec).GetMetricWithLabelValues',
'github.com/prometheus/client_golang/prometheus.(*MetricVec).GetMetricWithLabelValues',
'github.com/prometheus/client_golang/prometheus.(*metricMap).getOrCreateMetricWithLabelValues',
'test/pkg/create.(*create).Run',
'test/dskit/modules.(*Manager).InitModuleServices',
'test/dskit/modules.(*Manager).initModule',
'test/pkg/create.(*create).initcreate',
'test/pkg/create.New',
'test/pkg/create.(*create).initHead',
'test/pkg/create.NewHead',
'test/pkg/create.(*deduplicatingSlice[...]).Init',
'github.com/segmentio/parquet-go.NewWriter',
'github.com/segmentio/parquet-go.(*Writer).configure',
'github.com/segmentio/parquet-go.newWriter',
'runtime.doInit',
'test/dskit/ring.init',
'html/template.(*Template).Parse',
'text/template.(*Template).Parse',
'text/template/parse.Parse',
'text/template/parse.(*Tree).Parse',
'text/template/parse.(*Tree).parse',
'text/template/parse.(*Tree).textOrAction',
'text/template/parse.(*Tree).action',
'text/template/parse.(*Tree).rangeControl',
'text/template/parse.(*Tree).parseControl',
'text/template/parse.(*Tree).itemList',
'text/template/parse.(*Tree).pipeline',
'text/template/parse.(*PipeNode).append',
'text/template/parse.(*Tree).newPipeline',
'google.golang.org/protobuf/types/known/structpb.init',
'github.com/prometheus/prometheus/scrape.init',
'fmt.Errorf',
'github.com/prometheus/prometheus/discovery/consul.init',
'github.com/prometheus/client_golang/prometheus.(*SummaryVec).WithLabelValues',
'github.com/prometheus/client_golang/prometheus.(*SummaryVec).GetMetricWithLabelValues',
'github.com/prometheus/client_golang/prometheus.NewSummaryVec.func1',
'github.com/prometheus/client_golang/prometheus.newSummary',
'github.com/prometheus/client_golang/prometheus.(*summary).newStream',
'github.com/beorn7/perks/quantile.NewTargeted',
'github.com/beorn7/perks/quantile.newStream',
'encoding/gob.init',
'encoding/gob.mustGetTypeInfo',
'encoding/gob.getTypeInfo',
'encoding/gob.buildTypeInfo',
'encoding/gob.getBaseType',
'encoding/gob.getType',
'encoding/gob.newTypeObject',
'encoding/gob.userType',
'encoding/gob.validUserType',
'sync.(*Map).LoadOrStore',
'sync.(*Map).dirtyLocked',
'go.opentelemetry.io/otel/trace.init',
'regexp.MustCompile',
'regexp.Compile',
'regexp.compile',
'regexp.compileOnePass',
'regexp.onePassCopy',
'cloud.google.com/go/storage.init',
'regexp/syntax.Compile',
'github.com/aws/aws-sdk-go/aws/endpoints.init',
'github.com/asaskevich/govalidator.init',
'regexp/syntax.(*Regexp).CapNames',
'github.com/goccy/go-json/internal/decoder.init.0',
'k8s.io/api/flowcontrol/v1beta2.init',
'k8s.io/kube-openapi/pkg/handler3.init.0',
'mime.AddExtensionType',
'sync.(*Once).Do',
'sync.(*Once).doSlow',
'mime.initMime',
'mime.initMimeUnix',
'mime.loadMimeFile',
'mime.setExtensionType',
'sync.(*Map).Store',
'google.golang.org/genproto/googleapis/rpc/errdetails.init.0',
'google.golang.org/genproto/googleapis/rpc/errdetails.file_google_rpc_error_details_proto_init',
'google.golang.org/protobuf/internal/filetype.Builder.Build',
'google.golang.org/protobuf/internal/filedesc.Builder.Build',
'google.golang.org/protobuf/internal/filedesc.newRawFile',
'google.golang.org/protobuf/internal/filedesc.(*File).unmarshalSeed',
'google.golang.org/protobuf/internal/filedesc.(*Message).unmarshalSeed',
'google.golang.org/protobuf/internal/filedesc.appendFullName',
'google.golang.org/protobuf/internal/strs.(*Builder).AppendFullName',
'google.golang.org/genproto/googleapis/type/color.init.0',
'google.golang.org/genproto/googleapis/type/color.file_google_type_color_proto_init',
'google.golang.org/protobuf/reflect/protoregistry.(*Files).RegisterFile',
'google.golang.org/protobuf/reflect/protoregistry.rangeTopLevelDescriptors',
'google.golang.org/protobuf/reflect/protoregistry.(*Files).RegisterFile.func2',
'github.com/goccy/go-json/internal/encoder.init.0',
'google.golang.org/protobuf/types/descriptorpb.init.0',
'google.golang.org/protobuf/types/descriptorpb.file_google_protobuf_descriptor_proto_init',
'google.golang.org/protobuf/internal/filedesc.(*File).initDecls',
'golang.org/x/net/http2.(*serverConn).runHandler',
'github.com/weaveworks/common/middleware.Tracer.Wrap.func1',
'github.com/weaveworks/common/middleware.getRouteName',
'github.com/gorilla/mux.(*Router).Match',
'github.com/gorilla/mux.(*Route).Match',
'github.com/gorilla/mux.(*routeRegexp).Match',
'regexp.(*Regexp).MatchString',
'regexp.(*Regexp).doMatch',
'test/pkg/agent.(*Target).start.func1',
'test/pkg/agent.(*Target).scrape',
'github.com/prometheus/prometheus/util/pool.(*Pool).Get',
'test/pkg/agent.glob..func1',
'test/pkg/agent.(*Target).fetchProfile',
'io/ioutil.ReadAll',
'io.ReadAll',
'test/pkg/distributor.(*Distributor).Push',
'compress/flate.(*compressor).initDeflate',
'compress/gzip.(*Reader).Read',
'compress/flate.(*decompressor).Read',
'compress/flate.(*decompressor).nextBlock',
'compress/flate.(*decompressor).readHuffman',
'compress/flate.(*huffmanDecoder).init',
'compress/gzip.NewReader',
'compress/gzip.(*Reader).Reset',
'compress/gzip.(*Reader).readHeader',
'compress/flate.NewReader',
'compress/flate.(*dictDecoder).init',
'test/pkg/gen/google/v1.(*Profile).UnmarshalVT',
'test/pkg/gen/google/v1.(*Location).UnmarshalVT',
'test/pkg/gen/google/v1.(*Sample).UnmarshalVT',
'test/pkg/distributor.sanitizeProfile',
'github.com/samber/lo.Reject[...]',
'net/http.(*conn).serve',
'net/http.(*response).finishRequest',
'net/http.putBufioWriter',
'sync.(*Pool).Put',
'net/http.(*conn).readRequest',
'net/http.newBufioWriterSize',
'net/http.readRequest',
'net/textproto.(*Reader).ReadMIMEHeader',
'net/http.newTextprotoReader',
'github.com/uber/jaeger-client-go.NewTracer.func1',
'math/rand.NewSource',
'fmt.Sprintf',
'fmt.newPrinter',
'fmt.glob..func1',
'regexp.newBitState',
'github.com/felixge/httpsnoop.Wrap',
'github.com/bufbuild/connect-go.(*Handler).ServeHTTP',
'net/http.(*Request).WithContext',
'github.com/bufbuild/connect-go.NewUnaryHandler[...].func1',
'github.com/bufbuild/connect-go.(*errorTranslatingSender).Send',
'github.com/bufbuild/connect-go.(*connectUnaryHandlerSender).Send',
'github.com/bufbuild/connect-go.(*connectUnaryMarshaler).Marshal',
'github.com/bufbuild/connect-go.(*bufferPool).Put',
'sync.(*poolChain).pushHead',
'github.com/bufbuild/connect-go.(*compressionPool).Compress',
'github.com/bufbuild/connect-go.(*compressionPool).putCompressor',
'compress/gzip.(*Writer).Close',
'io.Copy',
'io.copyBuffer',
'bytes.(*Buffer).WriteTo',
'github.com/bufbuild/connect-go.(*protoBinaryCodec).Marshal',
'google.golang.org/protobuf/proto.Marshal',
'google.golang.org/protobuf/proto.MarshalOptions.marshal',
'github.com/bufbuild/connect-go.NewUnaryHandler[...].func1.1',
'test/pkg/ingester.(*Ingester).Push',
'github.com/klauspost/compress/gzip.NewReader',
'github.com/klauspost/compress/gzip.(*Reader).Reset',
'github.com/klauspost/compress/gzip.(*Reader).readHeader',
'github.com/klauspost/compress/flate.NewReader',
'github.com/klauspost/compress/flate.(*dictDecoder).init',
'test/pkg/create.(*Head).Ingest',
'test/pkg/create.(*deduplicatingSlice[...]).ingest',
'test/pkg/model.(*LabelsBuilder).Set',
'test/pkg/create.(*Head).convertSamples',
'github.com/bufbuild/connect-go.receiveUnaryRequest[...]',
'github.com/bufbuild/connect-go.(*errorTranslatingReceiver).Receive',
'github.com/bufbuild/connect-go.(*connectUnaryHandlerReceiver).Receive',
'github.com/bufbuild/connect-go.(*connectUnaryUnmarshaler).Unmarshal',
'github.com/bufbuild/connect-go.(*connectUnaryUnmarshaler).UnmarshalFunc',
'bytes.(*Buffer).ReadFrom',
'bytes.(*Buffer).grow',
'bytes.makeSlice',
'github.com/bufbuild/connect-go.(*bufferPool).Get',
'net/http/pprof.Profile',
'runtime/pprof.StartCPUProfile',
'runtime/pprof.writeMutex',
'runtime/pprof.writeProfileInternal',
'runtime/pprof.printCountCycleProfile',
'runtime/pprof.writeBlock',
'runtime/pprof.writeAlloc',
'runtime/pprof.writeHeapInternal',
'runtime/pprof.writeHeapProto',
'runtime/pprof.(*protobuf).strings',
'runtime/pprof.(*protobuf).string',
'runtime/pprof.(*profileBuilder).stringIndex',
'runtime/pprof.(*protobuf).uint64Opt',
'runtime/pprof.(*protobuf).uint64',
'runtime/pprof.(*protobuf).varint',
'runtime/pprof.allFrames',
'runtime/pprof.(*profileBuilder).pbSample',
'runtime/pprof.printCountProfile.func1',
'bytes.(*Buffer).String',
'net/http.(*persistConn).writeLoop',
'net/http.(*Request).write',
'net/http.(*transferWriter).writeBody',
'net/http.(*transferWriter).doBodyCopy',
'test/pkg/distributor.(*Distributor).Push.func1',
'test/pkg/distributor.(*Distributor).sendProfiles',
'test/pkg/distributor.(*Distributor).sendProfilesErr',
'test/pkg/gen/ingester/v1/ingesterv1connect.(*ingesterServiceClient).Push',
'github.com/bufbuild/connect-go.(*Client[...]).CallUnary',
'github.com/bufbuild/connect-go.NewClient[...].func2',
'github.com/bufbuild/connect-go.NewClient[...].func1',
'github.com/bufbuild/connect-go.(*connectClientSender).Send',
'github.com/bufbuild/connect-go.(*errorTranslatingReceiver).Close',
'github.com/bufbuild/connect-go.(*connectUnaryClientReceiver).Close',
'github.com/bufbuild/connect-go.(*duplexHTTPCall).CloseRead',
'github.com/bufbuild/connect-go.discard',
'io.discard.ReadFrom',
'io.glob..func1',
'github.com/bufbuild/connect-go.receiveUnaryResponse[...]',
'github.com/bufbuild/connect-go.(*connectUnaryClientReceiver).Receive',
'github.com/bufbuild/connect-go.(*compressionPool).Decompress',
'github.com/bufbuild/connect-go.(*compressionPool).getDecompressor',
],
levels: [
[0, 8624078250, 0, 0],
[
0, 60011939, 0, 335, 0, 1081684, 0, 331, 0, 2765065247, 0, 259, 0, 144858662, 0, 235, 0, 1081684, 0, 227, 0,
4523250662, 0, 128, 0, 9691644, 0, 116, 0, 8663322, 0, 109, 0, 1574208, 0, 90, 0, 132657008, 0, 85, 0,
304386696, 0, 83, 0, 1049728, 0, 56, 0, 524360, 0, 53, 0, 2624640, 0, 44, 0, 132697488, 0, 40, 0, 545034, 0, 37,
0, 1052676, 0, 28, 0, 398371776, 0, 25, 0, 2099200, 0, 9, 0, 132790592, 0, 1,
],
[
0, 60011939, 0, 336, 0, 1081684, 0, 332, 0, 2756669265, 0, 56, 0, 6821582, 0, 263, 0, 1574400, 0, 260, 0,
144858662, 0, 236, 0, 1081684, 0, 57, 0, 4255866888, 0, 150, 0, 267383774, 0, 129, 0, 9691644, 0, 117, 0,
4444206, 0, 114, 0, 1048752, 0, 112, 0, 3170364, 0, 110, 0, 1574208, 0, 91, 0, 132657008, 0, 86, 0, 304386696,
304386696, 84, 0, 1049728, 0, 57, 0, 524360, 0, 54, 0, 2624640, 0, 45, 0, 132697488, 0, 41, 0, 545034, 0, 37, 0,
1052676, 0, 29, 0, 398371776, 0, 26, 0, 2099200, 0, 10, 0, 132790592, 0, 2,
],
[
0, 60011939, 0, 337, 0, 1081684, 0, 333, 0, 2756669265, 0, 57, 0, 6296270, 0, 265, 0, 525312, 0, 264, 0,
1574400, 0, 261, 0, 135394175, 0, 242, 0, 526980, 0, 239, 0, 8937507, 0, 237, 0, 1081684, 0, 60, 0, 4255866888,
0, 150, 0, 135751342, 0, 139, 0, 131632432, 0, 130, 0, 9691644, 0, 118, 0, 4444206, 0, 78, 0, 1048752, 1048752,
113, 0, 3170364, 3170364, 111, 0, 1574208, 0, 92, 0, 132657008, 0, 87, 304386696, 1049728, 0, 58, 0, 524360,
524360, 55, 0, 2624640, 0, 46, 0, 132697488, 0, 42, 0, 545034, 0, 37, 0, 1052676, 0, 30, 0, 398371776, 0, 27, 0,
2099200, 0, 11, 0, 132790592, 0, 3,
],
[
0, 60011939, 0, 338, 0, 1081684, 0, 334, 0, 2756669265, 0, 58, 0, 1049600, 0, 267, 0, 5246670, 5246670, 266, 0,
525312, 525312, 36, 0, 1574400, 0, 262, 0, 4248808, 0, 257, 0, 35145141, 29377349, 254, 0, 5380182, 0, 249, 0,
5283874, 0, 240, 0, 85336170, 0, 78, 0, 526980, 0, 240, 0, 8937507, 8937507, 238, 0, 1081684, 0, 57, 0,
4255866888, 0, 150, 0, 135751342, 0, 140, 0, 131632432, 0, 131, 0, 9691644, 0, 119, 0, 4444206, 1848496, 79,
4219116, 1574208, 0, 93, 0, 132657008, 0, 88, 304386696, 1049728, 0, 59, 524360, 2624640, 0, 47, 0, 132697488,
132697488, 43, 0, 545034, 0, 37, 0, 1052676, 0, 31, 0, 398371776, 0, 3, 0, 2099200, 0, 12, 0, 132790592, 0, 4,
],
[
0, 60011939, 0, 339, 0, 1081684, 0, 286, 0, 2756669265, 0, 59, 0, 1049600, 0, 22, 5771982, 1574400, 0, 23, 0,
4248808, 4248808, 258, 29377349, 4194832, 4194832, 256, 0, 1572960, 1572960, 255, 0, 5380182, 0, 250, 0,
5283874, 2137058, 241, 0, 85336170, 67470104, 79, 0, 526980, 526980, 241, 8937507, 1081684, 0, 61, 0,
3990564004, 0, 150, 0, 265302884, 0, 151, 0, 135751342, 0, 141, 0, 131632432, 0, 132, 0, 9691644, 0, 120,
1848496, 2595710, 1998711, 80, 4219116, 1574208, 0, 94, 0, 132657008, 132657008, 89, 304386696, 1049728, 0, 57,
524360, 2624640, 0, 48, 132697488, 545034, 0, 37, 0, 1052676, 0, 32, 0, 398371776, 0, 4, 0, 2099200, 0, 13, 0,
132790592, 0, 5,
],
[
0, 60011939, 0, 340, 0, 1081684, 1081684, 287, 0, 2756669265, 0, 57, 0, 1049600, 0, 23, 5771982, 1574400,
1574400, 24, 39393949, 5380182, 0, 251, 2137058, 3146816, 0, 244, 67470104, 17866066, 0, 80, 9464487, 1081684,
0, 228, 0, 3725614404, 0, 150, 0, 132126624, 0, 166, 0, 132822976, 132822976, 165, 0, 265302884, 0, 152, 0,
135751342, 0, 142, 0, 131632432, 0, 133, 0, 9691644, 0, 121, 3847207, 596999, 596999, 115, 4219116, 1574208, 0,
95, 437043704, 1049728, 0, 60, 524360, 2624640, 0, 49, 132697488, 545034, 0, 37, 0, 1052676, 0, 33, 0,
398371776, 0, 5, 0, 2099200, 0, 14, 0, 132790592, 0, 6,
],
[
0, 60011939, 0, 341, 1081684, 2756669265, 0, 60, 0, 1049600, 1049600, 24, 46740331, 5380182, 1053446, 252,
2137058, 3146816, 0, 245, 67470104, 524864, 524864, 81, 0, 17341202, 17341202, 243, 9464487, 1081684, 0, 229, 0,
3725614404, 0, 150, 0, 132126624, 132126624, 167, 132822976, 265302884, 0, 153, 0, 135751342, 0, 143, 0,
131632432, 0, 134, 0, 9691644, 0, 122, 8663322, 1574208, 0, 96, 437043704, 1049728, 0, 57, 524360, 2624640, 0,
50, 132697488, 545034, 0, 37, 0, 1052676, 0, 34, 0, 398371776, 0, 6, 0, 2099200, 0, 15, 0, 132790592, 0, 7,
],
[
0, 35515758, 0, 349, 0, 7925912, 0, 343, 0, 16570269, 0, 278, 1081684, 2756669265, 0, 57, 48843377, 4326736,
4326736, 253, 2137058, 3146816, 0, 246, 94800657, 1081684, 0, 230, 0, 3457203921, 0, 150, 0, 268410483, 0, 168,
264949600, 265302884, 0, 154, 0, 135751342, 0, 144, 0, 131632432, 0, 135, 0, 9691644, 0, 123, 8663322, 1574208,
0, 97, 437043704, 1049728, 0, 61, 524360, 2624640, 0, 51, 132697488, 545034, 0, 37, 0, 1052676, 0, 22, 0,
398371776, 0, 7, 0, 2099200, 0, 16, 0, 132790592, 132790592, 8,
],
[
0, 35515758, 0, 304, 0, 7925912, 0, 344, 0, 16570269, 0, 342, 1081684, 2756669265, 0, 61, 55307171, 3146816, 0,
247, 94800657, 1081684, 0, 231, 0, 3324445713, 0, 150, 0, 132758208, 0, 176, 0, 268410483, 0, 169, 264949600,
265302884, 0, 155, 0, 135751342, 0, 145, 0, 131632432, 0, 136, 0, 9691644, 0, 124, 8663322, 1574208, 0, 98,
437043704, 1049728, 0, 57, 524360, 2624640, 2624640, 52, 132697488, 545034, 0, 37, 0, 1052676, 0, 35, 0,
398371776, 398371776, 8, 0, 2099200, 0, 17,
],
[
0, 35515758, 0, 350, 0, 7925912, 0, 345, 0, 16570269, 0, 280, 1081684, 2740098251, 0, 57, 0, 13936114, 0, 228,
0, 2634900, 0, 18, 55307171, 3146816, 3146816, 248, 94800657, 1081684, 0, 232, 0, 133423345, 0, 224, 0,
2527422102, 0, 150, 0, 264582108, 0, 196, 0, 265453672, 265453672, 195, 0, 132985149, 0, 193, 0, 579337, 0, 187,
0, 132758208, 0, 177, 0, 268410483, 0, 170, 264949600, 265302884, 0, 156, 0, 135751342, 0, 146, 0, 131632432, 0,
137, 0, 9691644, 0, 125, 8663322, 1574208, 0, 99, 437043704, 1049728, 0, 62, 135846488, 545034, 0, 37, 0,
1052676, 1052676, 36, 398371776, 2099200, 0, 18,
],
[
0, 35515758, 0, 306, 0, 7925912, 0, 346, 0, 16045469, 0, 289, 0, 524800, 0, 281, 1081684, 2740098251, 0, 62, 0,
11838546, 0, 229, 0, 2097568, 0, 270, 0, 2634900, 0, 19, 153254644, 1081684, 0, 233, 0, 133423345, 0, 225, 0,
663692876, 663692876, 223, 0, 550717750, 0, 150, 0, 1313011476, 1313011476, 198, 0, 264582108, 0, 188,
265453672, 132985149, 0, 188, 0, 579337, 0, 188, 0, 132758208, 0, 178, 0, 268410483, 0, 137, 264949600,
265302884, 0, 157, 0, 135751342, 0, 147, 0, 131632432, 131632432, 138, 0, 9691644, 0, 126, 8663322, 1574208, 0,
100, 437043704, 1049728, 0, 57, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 19,
],
[
0, 35515758, 0, 307, 0, 7925912, 0, 286, 0, 16045469, 0, 290, 0, 524800, 0, 262, 1081684, 2740098251, 0, 57, 0,
11838546, 0, 230, 0, 2097568, 0, 271, 0, 2634900, 0, 20, 153254644, 1081684, 0, 234, 0, 133423345, 0, 211,
663692876, 143277442, 0, 218, 0, 134728066, 0, 150, 0, 140030946, 0, 200, 0, 132681296, 132681296, 199,
1313011476, 264582108, 0, 189, 265453672, 132985149, 0, 189, 0, 579337, 0, 189, 0, 132758208, 0, 179, 0,
268410483, 0, 138, 264949600, 265302884, 0, 158, 0, 135751342, 0, 148, 131632432, 9691644, 9691644, 127,
8663322, 1574208, 0, 101, 437043704, 1049728, 0, 63, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 20,
],
[
0, 35515758, 0, 351, 0, 7925912, 0, 287, 0, 16045469, 16045469, 291, 0, 524800, 0, 23, 1081684, 2740098251, 0,
63, 0, 11838546, 0, 231, 0, 2097568, 0, 22, 0, 2634900, 0, 21, 153254644, 1081684, 0, 125, 0, 133423345, 0, 212,
663692876, 143277442, 0, 219, 0, 134728066, 0, 209, 0, 140030946, 0, 201, 1445692772, 264582108, 0, 190,
265453672, 132985149, 0, 190, 0, 579337, 0, 190, 0, 132758208, 0, 180, 0, 268410483, 0, 171, 264949600,
265302884, 0, 159, 0, 135751342, 0, 149, 149987398, 1574208, 0, 102, 437043704, 1049728, 0, 64, 135846488,
545034, 0, 37, 399424452, 2099200, 0, 21,
],
[
0, 34466158, 0, 352, 0, 1049600, 0, 308, 0, 7925912, 0, 347, 16045469, 524800, 524800, 24, 1081684, 2740098251,
0, 64, 0, 11838546, 0, 232, 0, 2097568, 2097568, 272, 0, 2634900, 0, 22, 153254644, 1081684, 0, 126, 0,
133423345, 0, 213, 663692876, 143277442, 0, 211, 0, 134728066, 0, 210, 0, 140030946, 0, 202, 1445692772,
132122592, 132122592, 197, 0, 132459516, 0, 194, 265453672, 132985149, 0, 194, 0, 579337, 0, 191, 0, 132758208,
0, 181, 0, 268410483, 0, 172, 264949600, 265302884, 0, 160, 0, 135751342, 135751342, 36, 149987398, 1574208, 0,
103, 437043704, 1049728, 0, 65, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 22,
],
[
0, 34466158, 0, 250, 0, 1049600, 0, 309, 0, 7925912, 0, 22, 17651953, 2740098251, 0, 65, 0, 11838546, 0, 233,
2097568, 2634900, 0, 268, 153254644, 1081684, 1081684, 127, 0, 133423345, 133423345, 226, 663692876, 143277442,
0, 212, 0, 134728066, 0, 211, 0, 140030946, 0, 203, 1577815364, 132459516, 0, 37, 265453672, 132985149, 0, 37,
0, 579337, 579337, 192, 0, 132758208, 0, 182, 0, 268410483, 0, 173, 264949600, 265302884, 0, 161, 285738740,
1574208, 0, 104, 437043704, 1049728, 0, 66, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 23,
],
[
0, 34466158, 0, 251, 0, 1049600, 1049600, 310, 0, 7925912, 7925912, 348, 17651953, 2739573923, 0, 66, 0, 524328,
524328, 274, 0, 11838546, 0, 234, 2097568, 2634900, 2634900, 269, 951452549, 143277442, 0, 220, 0, 134728066, 0,
212, 0, 140030946, 0, 204, 1577815364, 132459516, 0, 37, 265453672, 132985149, 0, 37, 579337, 132758208, 0, 183,
0, 268410483, 0, 174, 264949600, 265302884, 0, 157, 285738740, 1574208, 0, 105, 437043704, 1049728, 0, 67,
135846488, 545034, 0, 37, 399424452, 2099200, 2099200, 24,
],
[
0, 34466158, 5260690, 252, 26627465, 2739573923, 0, 67, 524328, 11838546, 0, 125, 956185017, 143277442, 0, 221,
0, 134728066, 0, 213, 0, 140030946, 0, 205, 1577815364, 132459516, 0, 37, 265453672, 132985149, 0, 38, 579337,
132758208, 0, 184, 0, 268410483, 268410483, 175, 264949600, 265302884, 0, 158, 285738740, 1574208, 0, 106,
437043704, 1049728, 0, 68, 135846488, 545034, 0, 37,
],
[
5260690, 29205468, 29205468, 253, 26627465, 409934141, 0, 68, 0, 2329639782, 0, 275, 524328, 11838546, 0, 126,
956185017, 143277442, 143277442, 222, 0, 134728066, 0, 214, 0, 140030946, 0, 206, 1577815364, 132459516, 0, 38,
265453672, 132985149, 132985149, 39, 579337, 132758208, 0, 185, 533360083, 265302884, 0, 162, 285738740,
1574208, 0, 107, 437043704, 1049728, 0, 57, 135846488, 545034, 0, 37,
],
[
61093623, 409934141, 0, 57, 0, 2329115366, 0, 277, 0, 524416, 524416, 276, 524328, 524376, 524376, 273, 0,
11314170, 11314170, 127, 1099462459, 134728066, 0, 215, 0, 140030946, 0, 207, 1577815364, 132459516, 132459516,
39, 399018158, 132758208, 132758208, 186, 533360083, 132657008, 132657008, 164, 0, 132645876, 132645876, 163,
285738740, 1574208, 1574208, 108, 437043704, 1049728, 0, 69, 135846488, 545034, 0, 37,
],
[
61093623, 178075376, 0, 69, 0, 231858765, 0, 312, 0, 14574244, 0, 303, 0, 1624251841, 0, 292, 0, 690289281, 0,
278, 1112349749, 134728066, 0, 216, 0, 140030946, 140030946, 208, 3765070865, 1049728, 0, 70, 135846488, 545034,
0, 37,
],
[
61093623, 178075376, 0, 70, 0, 231858765, 231858765, 313, 0, 14574244, 0, 304, 0, 1624251841, 0, 293, 0,
690289281, 0, 279, 1112349749, 134728066, 134728066, 217, 3905101811, 1049728, 0, 71, 135846488, 545034, 0, 37,
],
[
61093623, 178075376, 0, 71, 231858765, 14574244, 0, 305, 0, 1595244443, 0, 299, 0, 3274238, 3274238, 241, 0,
24651476, 23602660, 254, 0, 1081684, 0, 294, 0, 690289281, 0, 280, 5152179626, 1049728, 0, 72, 135846488,
545034, 0, 37,
],
[
61093623, 37385630, 0, 72, 0, 95081897, 0, 318, 0, 18338066, 0, 317, 0, 27269783, 0, 314, 231858765, 14574244,
0, 306, 0, 1566824927, 1566824927, 302, 0, 1048656, 1048656, 301, 0, 27370860, 27370860, 300, 26876898, 1048816,
1048816, 256, 0, 1081684, 0, 295, 0, 624126, 0, 289, 0, 689140843, 0, 283, 0, 524312, 0, 281, 5152179626,
1049728, 0, 73, 135846488, 545034, 0, 37,
],
[
61093623, 37385630, 0, 73, 0, 95081897, 6643414, 319, 0, 18338066, 0, 315, 0, 27269783, 0, 315, 231858765,
14574244, 0, 307, 1623170157, 1081684, 0, 296, 0, 624126, 0, 290, 0, 12739870, 0, 286, 0, 676400973, 0, 284, 0,
524312, 0, 262, 5152179626, 1049728, 0, 74, 135846488, 545034, 0, 37,
],
[
61093623, 37385630, 0, 74, 6643414, 88438483, 0, 320, 0, 18338066, 0, 316, 0, 27269783, 0, 316, 231858765,
524800, 0, 311, 0, 14049444, 0, 308, 1623170157, 1081684, 0, 297, 0, 624126, 624126, 291, 0, 12739870, 0, 287,
0, 676400973, 0, 285, 0, 524312, 524312, 282, 5152179626, 1049728, 0, 75, 135846488, 545034, 0, 37,
],
[
61093623, 27334886, 0, 75, 0, 9526424, 0, 328, 0, 524320, 0, 329, 6643414, 20792845, 0, 328, 0, 65465889, 0, 75,
0, 2179749, 0, 114, 0, 18338066, 0, 114, 0, 27269783, 0, 114, 231858765, 524800, 0, 22, 0, 14049444, 0, 309,
1623170157, 1081684, 1081684, 298, 624126, 12739870, 0, 288, 0, 676400973, 0, 78, 5152703938, 1049728, 0, 76,
135846488, 545034, 0, 37,
],
[
61093623, 26810494, 1585182, 76, 0, 524392, 524392, 327, 0, 9526424, 0, 77, 0, 524320, 524320, 330, 6643414,
20792845, 0, 77, 0, 8389952, 8389952, 327, 0, 57075937, 2171836, 76, 0, 2179749, 0, 321, 0, 18338066, 0, 78, 0,
27269783, 0, 78, 231858765, 524800, 0, 23, 0, 14049444, 14049444, 310, 1624875967, 12739870, 0, 78, 0,
676400973, 557321544, 79, 5152703938, 1049728, 0, 77, 135846488, 545034, 0, 37,
],
[
62678805, 25225312, 0, 77, 524392, 9526424, 0, 78, 7167734, 20792845, 0, 78, 10561788, 49063106, 0, 77, 0,
1050624, 0, 324, 0, 4790371, 4790371, 323, 0, 2179749, 2179749, 322, 0, 18338066, 9242480, 79, 0, 27269783,
18484960, 79, 231858765, 524800, 524800, 24, 1638925411, 12739870, 11090976, 79, 557321544, 119079429, 0, 80,
5152703938, 1049728, 0, 78, 135846488, 545034, 0, 37,
],
[
62678805, 25225312, 0, 78, 524392, 9526424, 4621240, 79, 7167734, 20792845, 11090976, 79, 10561788, 49063106, 0,
78, 0, 1050624, 0, 325, 16212600, 9095586, 5670636, 80, 18484960, 8784823, 8227085, 80, 1882399952, 1648894, 0,
80, 557321544, 2097312, 0, 81, 0, 116982117, 116982117, 243, 5152703938, 1049728, 0, 79, 135846488, 545034, 0,
37,
],
[
62678805, 25225312, 14787968, 79, 5145632, 4905184, 1998711, 80, 18258710, 9701869, 6119875, 80, 10561788,
49063106, 25878944, 79, 0, 1050624, 1050624, 326, 21883236, 3424950, 3424950, 115, 26712045, 557738, 557738,
115, 1882399952, 524864, 524864, 81, 0, 1124030, 1124030, 243, 557321544, 2097312, 2097312, 82, 5269686055,
1049728, 0, 80, 135846488, 545034, 0, 37,
],
[
77466773, 10437344, 6336873, 80, 7144343, 2906473, 2906473, 115, 24378585, 3581994, 3581994, 115, 36440732,
23184162, 14346960, 80, 7766782350, 1049728, 0, 81, 135846488, 545034, 0, 38,
],
[
83803646, 4100471, 4100471, 115, 88799087, 8837202, 8837202, 115, 7766782350, 1049728, 1049728, 82, 135846488,
545034, 545034, 39,
],
],
numTicks: 8624078250,
},
timeline: null,
};

@ -1,50 +0,0 @@
import { LevelItem } from './dataTransform';
import { levelsToString, textToDataContainer, trimLevelsString } from './testHelpers';
describe('textToDataContainer', () => {
it('converts text to correct data container', () => {
const container = textToDataContainer(`
[1//////////////]
[2][4//][7///]
[3][5]
[6]
`)!;
const n6: LevelItem = { itemIndexes: [5], start: 3, children: [], value: 3 };
const n5: LevelItem = { itemIndexes: [4], start: 3, children: [n6], value: 3 };
const n3: LevelItem = { itemIndexes: [2], start: 0, children: [], value: 3 };
const n7: LevelItem = { itemIndexes: [6], start: 8, children: [], value: 6 };
const n4: LevelItem = { itemIndexes: [3], start: 3, children: [n5], value: 5 };
const n2: LevelItem = { itemIndexes: [1], start: 0, children: [n3], value: 3 };
const n1: LevelItem = { itemIndexes: [0], start: 0, children: [n2, n4, n7], value: 17 };
n2.parents = [n1];
n4.parents = [n1];
n7.parents = [n1];
n3.parents = [n2];
n5.parents = [n4];
n6.parents = [n5];
const levels = container.getLevels();
expect(levels[0][0]).toEqual(n1);
});
});
describe('levelsToString', () => {
it('converts data container to correct string', () => {
const stringGraph = trimLevelsString(`
[1//////////////]
[2][4//][7///]
[3][5]
[6]
`);
const container = textToDataContainer(stringGraph)!;
expect(levelsToString(container.getLevels(), container)).toEqual(stringGraph);
});
});

@ -1,104 +0,0 @@
import { arrayToDataFrame, FieldType } from '@grafana/data';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
// Convert text to a FlameGraphDataContainer for testing. The format representing the flamegraph for example:
// [0///////]
// [1//][4//]
// [2//][5]
// [3] [6]
// [7]
// Each node starts with [ ends with ], single digit is used for label and the length of a node is it's value.
export function textToDataContainer(text: string) {
const levels = text.split('\n');
if (levels.length === 0) {
return undefined;
}
if (levels[0] === '') {
levels.shift();
}
const dfValues: Array<{ level: number; value: number; label: string; self: number }> = [];
const dfSorted: Array<{ level: number; value: number; label: string; self: number }> = [];
const leftMargin = levels[0].indexOf('[');
let itemLevels: LevelItem[][] = [];
const re = /\[(\d)[^\[]*]/g;
let match;
for (let i = 0; i < levels.length; i++) {
while ((match = re.exec(levels[i])) !== null) {
const currentNodeValue = match[0].length;
dfValues.push({
value: match[0].length,
label: match[1],
self: match[0].length,
level: i,
});
const node: LevelItem = {
value: match[0].length,
itemIndexes: [dfValues.length - 1],
start: match.index - leftMargin,
children: [],
};
itemLevels[i] = itemLevels[i] || [];
itemLevels[i].push(node);
const prevLevel = itemLevels[i - 1];
if (prevLevel) {
for (const n of prevLevel) {
const nRow = dfValues[n.itemIndexes[0]];
const value = nRow.value;
if (n.start + value > node.start) {
n.children.push(node);
nRow.self = nRow.self - currentNodeValue;
break;
}
}
}
}
}
const root = itemLevels[0][0];
const stack = [root];
while (stack.length) {
const node = stack.shift()!;
const index = node.itemIndexes[0];
dfSorted.push(dfValues[index]);
node.itemIndexes = [dfSorted.length - 1];
if (node.children) {
stack.unshift(...node.children);
}
}
const df = arrayToDataFrame(dfSorted);
const labelField = df.fields.find((f) => f.name === 'label')!;
labelField.type = FieldType.string;
return new FlameGraphDataContainer(df);
}
export function trimLevelsString(s: string) {
const lines = s.split('\n').filter((l) => !l.match(/^\s*$/));
const offset = Math.min(lines[0].indexOf('['), lines[lines.length - 1].indexOf('['));
return lines.map((l) => l.substring(offset)).join('\n');
}
// Convert levels array to a string representation that can be visually compared. Mainly useful together with
// textToDataContainer to create more visual tests.
export function levelsToString(levels: LevelItem[][], data: FlameGraphDataContainer) {
let sLevels = [];
for (const level of levels) {
let sLevel = ' '.repeat(level[0].start);
for (const node of level) {
sLevel += ' '.repeat(node.start - sLevel.length);
sLevel += `[${data.getLabel(node.itemIndexes[0])}${'/'.repeat(node.value - 3)}]`;
}
sLevels.push(sLevel);
}
return sLevels.join('\n');
}

@ -1,162 +0,0 @@
import { levelsToString, textToDataContainer, trimLevelsString } from './testHelpers';
import { mergeParentSubtrees, mergeSubtrees } from './treeTransforms';
describe('mergeSubtrees', () => {
it('correctly merges trees', () => {
const container = textToDataContainer(`
[0////////////]
[1//][4/////]
[2] [1////]
[3] [2][7/]
[8]
`)!;
const levels = container.getLevels()!;
const node1 = levels[1][0];
const node2 = levels[2][1];
const merged = mergeSubtrees([node1, node2], container);
expect(merged[0][0]).toMatchObject({ itemIndexes: [1, 5], start: 0 });
expect(merged[1][0]).toMatchObject({ itemIndexes: [2, 6], start: 0 });
expect(merged[1][1]).toMatchObject({ itemIndexes: [7], start: 6 });
expect(merged[2][0]).toMatchObject({ itemIndexes: [3], start: 0 });
expect(merged[2][1]).toMatchObject({ itemIndexes: [8], start: 6 });
expect(levelsToString(merged, container)).toEqual(
trimLevelsString(`
[1/////////]
[2///][7/]
[3] [8]
`)
);
});
it('normalizes the tree offset for single node', () => {
const container = textToDataContainer(`
[0////////////]
[1//][4/////]
[2] [5////]
[3] [6][7/]
[8]
`)!;
const levels = container.getLevels()!;
const node = levels[1][1];
const merged = mergeSubtrees([node], container);
expect(merged[0][0]).toMatchObject({ itemIndexes: [4], start: 0 });
expect(merged[1][0]).toMatchObject({ itemIndexes: [5], start: 0 });
expect(merged[2][0]).toMatchObject({ itemIndexes: [6], start: 0 });
expect(merged[2][1]).toMatchObject({ itemIndexes: [7], start: 3 });
expect(merged[3][0]).toMatchObject({ itemIndexes: [8], start: 3 });
expect(levelsToString(merged, container)).toEqual(
trimLevelsString(`
[4/////]
[5////]
[6][7/]
[8]
`)
);
});
it('handles repeating items', () => {
const container = textToDataContainer(`
[0]
[0]
[0]
[0]
`)!;
const levels = container.getLevels()!;
const merged = mergeSubtrees([levels[0][0]], container);
expect(levelsToString(merged, container)).toEqual(
trimLevelsString(`
[0]
[0]
[0]
[0]
`)
);
});
});
describe('mergeParentSubtrees', () => {
it('correctly merges trees', () => {
const container = textToDataContainer(`
[0/////////////]
[1//][4/////][6]
[2] [5/////]
[6] [6/][8/]
[7]
`)!;
const levels = container.getLevels()!;
const merged = mergeParentSubtrees([levels[3][0], levels[3][1], levels[1][2]], container);
expect(merged[0][0]).toMatchObject({ itemIndexes: [0], start: 3, value: 3 });
expect(merged[0][1]).toMatchObject({ itemIndexes: [0], start: 6, value: 4 });
expect(merged[1][0]).toMatchObject({ itemIndexes: [1], start: 3, value: 3 });
expect(merged[1][1]).toMatchObject({ itemIndexes: [4], start: 6, value: 4 });
expect(merged[2][0]).toMatchObject({ itemIndexes: [0], start: 0, value: 3 });
expect(merged[2][1]).toMatchObject({ itemIndexes: [2], start: 3, value: 3 });
expect(merged[2][2]).toMatchObject({ itemIndexes: [5], start: 6, value: 4 });
expect(merged[3][0]).toMatchObject({ itemIndexes: [3, 6, 9], start: 0, value: 10 });
expect(levelsToString(merged, container)).toEqual(
trimLevelsString(`
[0][0/]
[1][4/]
[0][2][5/]
[6///////]
`)
);
});
it('handles repeating nodes in single parent tree', () => {
const container = textToDataContainer(`
[0]
[1]
[2]
[1]
[4]
`)!;
const levels = container.getLevels()!;
const merged = mergeParentSubtrees([levels[1][0], levels[3][0]], container);
expect(levelsToString(merged, container)).toEqual(
trimLevelsString(`
[0]
[1]
[0][2]
[1///]
`)
);
});
it('handles single node', () => {
const container = textToDataContainer(`[0]`)!;
const levels = container.getLevels()!;
const merged = mergeParentSubtrees([levels[0][0]], container);
expect(levelsToString(merged, container)).toEqual(trimLevelsString(`[0]`));
});
it('handles multiple same nodes', () => {
const container = textToDataContainer(`
[0]
[0]
[0]
[0]
[0]
`)!;
const levels = container.getLevels()!;
const merged = mergeParentSubtrees([levels[4][0]], container);
expect(levelsToString(merged, container)).toEqual(
trimLevelsString(`
[0]
[0]
[0]
[0]
[0]
`)
);
});
});

@ -1,134 +0,0 @@
import { groupBy } from 'lodash';
import { LevelItem } from './dataTransform';
type DataInterface = {
getLabel: (index: number) => string;
};
// Merge parent subtree of the roots for the callers tree in the sandwich view of the flame graph.
export function mergeParentSubtrees(roots: LevelItem[], data: DataInterface): LevelItem[][] {
const newRoots = getParentSubtrees(roots);
return mergeSubtrees(newRoots, data, 'parents');
}
// Returns a subtrees per root that will have the parents resized to the same value as the root. When doing callers
// tree we need to keep proper sizes of the parents, before we merge them, so we correctly attribute to the parents
// only the value it contributed to the root.
// So if we have something like:
// [0/////////////]
// [1//][4/////][6]
// [2] [5/////]
// [6] [6/][8/]
// [7]
// Taking all the node with '6' will create:
// [0][0/]
// [1][4/]
// [2][5/][0]
// [6][6/][6]
// Which we can later merge.
function getParentSubtrees(roots: LevelItem[]) {
return roots.map((r) => {
if (!r.parents?.length) {
return r;
}
const newRoot = {
...r,
children: [],
};
const stack: Array<{ child: undefined | LevelItem; parent: LevelItem }> = [
{ child: newRoot, parent: r.parents[0] },
];
while (stack.length) {
const args = stack.shift()!;
const newNode = {
...args.parent,
children: args.child ? [args.child] : [],
parents: [],
};
if (args.child) {
newNode.value = args.child.value;
newNode.valueRight = args.child.valueRight;
args.child.parents = [newNode];
}
if (args.parent.parents?.length) {
stack.push({ child: newNode, parent: args.parent.parents[0] });
}
}
return newRoot;
});
}
// Merge subtrees into a single tree. Returns an array of levels for easy rendering. It assumes roots are mergeable,
// meaning they represent the same unit of work (same label). Then we walk the tree in a specified direction,
// merging nodes with the same label and same parent/child into single bigger node. This copies the tree (and all nodes)
// as we are creating new merged nodes and modifying the parents/children.
export function mergeSubtrees(
roots: LevelItem[],
data: DataInterface,
direction: 'parents' | 'children' = 'children'
): LevelItem[][] {
const oppositeDirection = direction === 'parents' ? 'children' : 'parents';
const levels: LevelItem[][] = [];
// Loop instead of recursion to be sure we don't blow stack size limit and save some memory. Each stack item is
// basically a list of arrays you would pass to each level of recursion.
const stack: Array<{ previous: undefined | LevelItem; items: LevelItem[]; level: number }> = [
{ previous: undefined, items: roots, level: 0 },
];
while (stack.length) {
const args = stack.shift()!;
const indexes = args.items.flatMap((i) => i.itemIndexes);
const newItem: LevelItem = {
// We use the items value instead of value from the data frame, cause we could have changed it in the process
value: args.items.reduce((acc, i) => acc + i.value, 0),
// valueRight may not exist at all if this is not a diff profile
valueRight: args.items.reduce<number | undefined>((acc, i) => {
if (i.valueRight !== undefined) {
return (acc ?? 0) + i.valueRight;
} else {
return acc;
}
}, undefined),
itemIndexes: indexes,
// these will change later
children: [],
parents: [],
start: 0,
};
levels[args.level] = levels[args.level] || [];
levels[args.level].push(newItem);
if (args.previous) {
// Not the first level, so we need to make sure we update previous items to keep the child/parent relationships
// and compute correct new start offset for the item.
newItem[oppositeDirection] = [args.previous];
const prevSiblingsVal =
args.previous[direction]?.reduce((acc, node) => {
return acc + node.value;
}, 0) || 0;
newItem.start = args.previous.start + prevSiblingsVal;
args.previous[direction]!.push(newItem);
}
const nextItems = args.items.flatMap((i) => i[direction] || []);
// Group by label which for now is the only identifier by which we decide if node represents the same unit of work.
const nextGroups = groupBy(nextItems, (c) => data.getLabel(c.itemIndexes[0]));
for (const g of Object.values(nextGroups)) {
stack.push({ previous: newItem, items: g, level: args.level + 1 });
}
}
// Reverse the levels if we are doing callers tree, so we return levels in the correct order.
if (direction === 'parents') {
levels.reverse();
}
return levels;
}

@ -1,91 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CoreApp, createDataFrame } from '@grafana/data';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import { data } from './FlameGraph/testData/dataNestedSet';
import FlameGraphContainer from './FlameGraphContainer';
jest.mock('react-use', () => ({
useMeasure: () => {
const ref = React.useRef();
return [ref, { width: 1600 }];
},
}));
describe('FlameGraphContainer', () => {
// Needed for AutoSizer to work in test
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { value: 500 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 500 });
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { value: 500 });
const FlameGraphContainerWithProps = () => {
const flameGraphData = createDataFrame(data);
flameGraphData.meta = {
custom: {
ProfileTypeID: 'cpu:foo:bar',
},
};
return <FlameGraphContainer data={flameGraphData} app={CoreApp.Explore} />;
};
it('should render without error', async () => {
expect(() => render(<FlameGraphContainerWithProps />)).not.toThrow();
});
it('should update search when row selected in top table', async () => {
render(<FlameGraphContainerWithProps />);
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
expect(screen.getByDisplayValue('net/http.HandlerFunc.ServeHTTP')).toBeInTheDocument();
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]);
expect(screen.getByDisplayValue('total')).toBeInTheDocument();
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]);
expect(screen.queryByDisplayValue('total')).not.toBeInTheDocument();
});
it('should render options', async () => {
render(<FlameGraphContainerWithProps />);
expect(screen.getByText(/Top Table/)).toBeDefined();
expect(screen.getByText(/Flame Graph/)).toBeDefined();
expect(screen.getByText(/Both/)).toBeDefined();
});
it('should update selected view', async () => {
render(<FlameGraphContainerWithProps />);
expect(screen.getByTestId('flameGraph')).toBeDefined();
expect(screen.getByTestId('topTable')).toBeDefined();
await userEvent.click(screen.getByText(/Top Table/));
expect(screen.queryByTestId('flameGraph')).toBeNull();
expect(screen.getByTestId('topTable')).toBeDefined();
await userEvent.click(screen.getByText(/Flame Graph/));
expect(screen.getByTestId('flameGraph')).toBeDefined();
expect(screen.queryByTestId('topTable')).toBeNull();
await userEvent.click(screen.getByText(/Both/));
expect(screen.getByTestId('flameGraph')).toBeDefined();
expect(screen.getByTestId('topTable')).toBeDefined();
});
it('should render both option if screen width >= threshold', async () => {
global.innerWidth = MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH;
global.dispatchEvent(new Event('resize')); // Trigger the window resize event
render(<FlameGraphContainerWithProps />);
expect(screen.getByText(/Both/)).toBeDefined();
});
it('should not render both option if screen width < threshold', async () => {
global.innerWidth = MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH - 1;
global.dispatchEvent(new Event('resize'));
render(<FlameGraphContainerWithProps />);
expect(screen.queryByTestId(/Both/)).toBeNull();
});
});

@ -1,195 +0,0 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DataFrame, CoreApp, GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { useStyles2, useTheme2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import FlameGraph from './FlameGraph/FlameGraph';
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
import FlameGraphHeader from './FlameGraphHeader';
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
type Props = {
data?: DataFrame;
app: CoreApp;
};
const FlameGraphContainer = (props: Props) => {
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
const [rangeMin, setRangeMin] = useState(0);
const [rangeMax, setRangeMax] = useState(1);
const [search, setSearch] = useState('');
const [selectedView, setSelectedView] = useState(SelectedView.Both);
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
const [textAlign, setTextAlign] = useState<TextAlign>('left');
// This is a label of the item because in sandwich view we group all items by label and present a merged graph
const [sandwichItem, setSandwichItem] = useState<string>();
const theme = useTheme2();
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
if (!props.data) {
return;
}
return new FlameGraphDataContainer(props.data, theme);
}, [props.data, theme]);
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
const styles = useStyles2(getStyles);
// If user resizes window with both as the selected view
useEffect(() => {
if (
containerWidth > 0 &&
containerWidth < MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH &&
selectedView === SelectedView.Both
) {
setSelectedView(SelectedView.FlameGraph);
}
}, [selectedView, setSelectedView, containerWidth]);
const resetFocus = useCallback(() => {
setFocusedItemData(undefined);
setRangeMin(0);
setRangeMax(1);
}, [setFocusedItemData, setRangeMax, setRangeMin]);
function resetSandwich() {
setSandwichItem(undefined);
}
useEffect(() => {
resetFocus();
resetSandwich();
}, [props.data, resetFocus]);
const onSymbolClick = useCallback(
(symbol: string) => {
if (search === symbol) {
setSearch('');
} else {
reportInteraction('grafana_flamegraph_table_item_selected', {
app: props.app,
grafana_version: config.buildInfo.version,
});
setSearch(symbol);
resetFocus();
}
},
[setSearch, resetFocus, props.app, search]
);
return (
<>
{dataContainer && (
<div ref={sizeRef} className={styles.container}>
<FlameGraphHeader
app={props.app}
search={search}
setSearch={setSearch}
selectedView={selectedView}
setSelectedView={setSelectedView}
containerWidth={containerWidth}
onReset={() => {
resetFocus();
resetSandwich();
}}
textAlign={textAlign}
onTextAlignChange={setTextAlign}
showResetButton={Boolean(focusedItemData || sandwichItem)}
colorScheme={colorScheme}
onColorSchemeChange={setColorScheme}
isDiffMode={Boolean(dataContainer.isDiffFlamegraph())}
/>
<div className={styles.body}>
{selectedView !== SelectedView.FlameGraph && (
<FlameGraphTopTableContainer
data={dataContainer}
app={props.app}
onSymbolClick={onSymbolClick}
height={selectedView === SelectedView.TopTable ? 600 : undefined}
search={search}
sandwichItem={sandwichItem}
onSandwich={setSandwichItem}
onSearch={setSearch}
/>
)}
{selectedView !== SelectedView.TopTable && (
<FlameGraph
data={dataContainer}
rangeMin={rangeMin}
rangeMax={rangeMax}
search={search}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
onItemFocused={(data) => setFocusedItemData(data)}
focusedItemData={focusedItemData}
textAlign={textAlign}
sandwichItem={sandwichItem}
onSandwich={(label: string) => {
resetFocus();
setSandwichItem(label);
}}
onFocusPillClick={resetFocus}
onSandwichPillClick={resetSandwich}
colorScheme={colorScheme}
/>
)}
</div>
</div>
)}
</>
);
};
function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
const [colorScheme, setColorScheme] = useState<ColorScheme | ColorSchemeDiff>(
dataContainer?.isDiffFlamegraph() ? ColorSchemeDiff.Default : ColorScheme.ValueBased
);
useEffect(() => {
if (
dataContainer?.isDiffFlamegraph() &&
(colorScheme === ColorScheme.ValueBased || colorScheme === ColorScheme.PackageBased)
) {
setColorScheme(ColorSchemeDiff.Default);
}
if (
!dataContainer?.isDiffFlamegraph() &&
(colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind)
) {
setColorScheme(ColorScheme.ValueBased);
}
}, [dataContainer, colorScheme]);
return [colorScheme, setColorScheme] as const;
}
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
height: '100%',
display: 'flex',
flex: '1 1 0',
flexDirection: 'column',
minHeight: 0,
gap: theme.spacing(1),
}),
body: css({
display: 'flex',
flexGrow: 1,
minHeight: 0,
}),
};
}
export default FlameGraphContainer;

@ -1,86 +0,0 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CoreApp } from '@grafana/data';
import FlameGraphHeader from './FlameGraphHeader';
import { ColorScheme, SelectedView } from './types';
describe('FlameGraphHeader', () => {
function setup(props: Partial<React.ComponentProps<typeof FlameGraphHeader>> = {}) {
const setSearch = jest.fn();
const setSelectedView = jest.fn();
const onReset = jest.fn();
const onSchemeChange = jest.fn();
const renderResult = render(
<FlameGraphHeader
app={CoreApp.Explore}
search={''}
setSearch={setSearch}
selectedView={SelectedView.Both}
setSelectedView={setSelectedView}
containerWidth={1600}
onReset={onReset}
onTextAlignChange={jest.fn()}
textAlign={'left'}
showResetButton={true}
colorScheme={ColorScheme.ValueBased}
onColorSchemeChange={onSchemeChange}
isDiffMode={false}
{...props}
/>
);
return {
renderResult,
handlers: {
setSearch,
setSelectedView,
onReset,
onSchemeChange,
},
};
}
it('show reset button when needed', async () => {
setup({ showResetButton: false });
expect(screen.queryByLabelText(/Reset focus/)).toBeNull();
setup();
expect(screen.getByLabelText(/Reset focus/)).toBeInTheDocument();
});
it('calls on reset when reset button is clicked', async () => {
const { handlers } = setup();
const resetButton = screen.getByLabelText(/Reset focus/);
expect(resetButton).toBeInTheDocument();
await userEvent.click(resetButton);
expect(handlers.onReset).toHaveBeenCalledTimes(1);
});
it('calls on color scheme change when clicked', async () => {
const { handlers } = setup();
const changeButton = screen.getByLabelText(/Change color scheme/);
expect(changeButton).toBeInTheDocument();
await userEvent.click(changeButton);
const byPackageButton = screen.getByText(/By package name/);
expect(byPackageButton).toBeInTheDocument();
await userEvent.click(byPackageButton);
expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
});
it('shows diff color scheme switch when diff', async () => {
setup({ isDiffMode: true });
const changeButton = screen.getByLabelText(/Change color scheme/);
expect(changeButton).toBeInTheDocument();
await userEvent.click(changeButton);
expect(screen.getByText(/Default/)).toBeInTheDocument();
expect(screen.getByText(/Color blind/)).toBeInTheDocument();
});
});

@ -1,295 +0,0 @@
import { css, cx } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import usePrevious from 'react-use/lib/usePrevious';
import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { reportInteraction, config } from '@grafana/runtime';
import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
type Props = {
app: CoreApp;
search: string;
setSearch: (search: string) => void;
selectedView: SelectedView;
setSelectedView: (view: SelectedView) => void;
containerWidth: number;
onReset: () => void;
textAlign: TextAlign;
onTextAlignChange: (align: TextAlign) => void;
showResetButton: boolean;
colorScheme: ColorScheme | ColorSchemeDiff;
onColorSchemeChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
isDiffMode: boolean;
};
const FlameGraphHeader = ({
app,
search,
setSearch,
selectedView,
setSelectedView,
containerWidth,
onReset,
textAlign,
onTextAlignChange,
showResetButton,
colorScheme,
onColorSchemeChange,
isDiffMode,
}: Props) => {
const styles = useStyles2((theme) => getStyles(theme, app));
function interaction(name: string, context: Record<string, string | number>) {
reportInteraction(`grafana_flamegraph_${name}`, {
app,
grafana_version: config.buildInfo.version,
...context,
});
}
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
const suffix =
localSearch !== '' ? (
<Button
icon="times"
fill="text"
size="sm"
onClick={() => {
// We could set only one and wait them to sync but there is no need to debounce this.
setSearch('');
setLocalSearch('');
}}
>
Clear
</Button>
) : null;
return (
<div className={styles.header}>
<div className={styles.inputContainer}>
<Input
value={localSearch || ''}
onChange={(v) => {
setLocalSearch(v.currentTarget.value);
}}
placeholder={'Search..'}
width={44}
suffix={suffix}
/>
</div>
<div className={styles.rightContainer}>
{showResetButton && (
<Button
variant={'secondary'}
fill={'outline'}
size={'sm'}
icon={'history-alt'}
tooltip={'Reset focus and sandwich state'}
onClick={() => {
onReset();
}}
className={styles.buttonSpacing}
aria-label={'Reset focus and sandwich state'}
/>
)}
<ColorSchemeButton app={app} value={colorScheme} onChange={onColorSchemeChange} isDiffMode={isDiffMode} />
<RadioButtonGroup<TextAlign>
size="sm"
disabled={selectedView === SelectedView.TopTable}
options={alignOptions}
value={textAlign}
onChange={(val) => {
interaction('text_align_selected', { align: val });
onTextAlignChange(val);
}}
className={styles.buttonSpacing}
/>
<RadioButtonGroup<SelectedView>
size="sm"
options={getViewOptions(containerWidth)}
value={selectedView}
onChange={(view) => {
interaction('view_selected', { view });
setSelectedView(view);
}}
/>
</div>
</div>
);
};
type ColorSchemeButtonProps = {
app: CoreApp;
value: ColorScheme | ColorSchemeDiff;
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
isDiffMode: boolean;
};
function ColorSchemeButton(props: ColorSchemeButtonProps) {
const styles = useStyles2((theme) => getStyles(theme, props.app));
let menu = (
<Menu>
<Menu.Item label="By value" onClick={() => props.onChange(ColorScheme.ValueBased)} />
<Menu.Item label="By package name" onClick={() => props.onChange(ColorScheme.PackageBased)} />
</Menu>
);
if (props.isDiffMode) {
menu = (
<Menu>
<Menu.Item label="Default (green to red)" onClick={() => props.onChange(ColorSchemeDiff.Default)} />
<Menu.Item label="Color blind (blue to red)" onClick={() => props.onChange(ColorSchemeDiff.DiffColorBlind)} />
</Menu>
);
}
// Show a bit different gradient as a way to indicate selected value
const colorDotStyle =
{
[ColorScheme.ValueBased]: styles.colorDotByValue,
[ColorScheme.PackageBased]: styles.colorDotByPackage,
[ColorSchemeDiff.DiffColorBlind]: styles.colorDotDiffColorBlind,
[ColorSchemeDiff.Default]: styles.colorDotDiffDefault,
}[props.value] || styles.colorDotByValue;
return (
<Dropdown overlay={menu}>
<Button
variant={'secondary'}
fill={'outline'}
size={'sm'}
tooltip={'Change color scheme'}
onClick={() => {}}
className={styles.buttonSpacing}
aria-label={'Change color scheme'}
>
<span className={cx(styles.colorDot, colorDotStyle)} />
</Button>
</Dropdown>
);
}
const alignOptions: Array<SelectableValue<TextAlign>> = [
{ value: 'left', description: 'Align text left', icon: 'align-left' },
{ value: 'right', description: 'Align text right', icon: 'align-right' },
];
function getViewOptions(width: number): Array<SelectableValue<SelectedView>> {
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
];
if (width >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH) {
viewOptions.push({
value: SelectedView.Both,
label: 'Both',
description: 'Show both the top table and flame graph',
});
}
return viewOptions;
}
function useSearchInput(
search: string,
setSearch: (search: string) => void
): [string | undefined, (search: string) => void] {
const [localSearchState, setLocalSearchState] = useState(search);
const prevSearch = usePrevious(search);
// Debouncing cause changing parent search triggers rerender on both the flamegraph and table
useDebounce(
() => {
setSearch(localSearchState);
},
250,
[localSearchState]
);
// Make sure we still handle updates from parent (from clicking on a table item for example). We check if the parent
// search value changed to something that isn't our local value.
useEffect(() => {
if (prevSearch !== search && search !== localSearchState) {
setLocalSearchState(search);
}
}, [search, prevSearch, localSearchState]);
return [localSearchState, setLocalSearchState];
}
const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
header: css`
label: header;
display: flex;
justify-content: space-between;
width: 100%;
background: ${theme.colors.background.primary};
top: 0;
z-index: ${theme.zIndex.navbarFixed};
${app === CoreApp.Explore
? css`
position: sticky;
padding-bottom: ${theme.spacing(1)};
padding-top: ${theme.spacing(1)};
`
: ''};
`,
inputContainer: css`
label: inputContainer;
margin-right: 20px;
`,
rightContainer: css`
label: rightContainer;
display: flex;
align-items: flex-start;
`,
buttonSpacing: css`
label: buttonSpacing;
margin-right: ${theme.spacing(1)};
`,
resetButton: css`
label: resetButton;
display: flex;
margin-right: ${theme.spacing(2)};
`,
resetButtonIconWrapper: css`
label: resetButtonIcon;
padding: 0 5px;
color: ${theme.colors.text.disabled};
`,
colorDot: css`
label: colorDot;
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
`,
colorDotByValue: css`
label: colorDotByValue;
background: ${byValueGradient};
`,
colorDotByPackage: css`
label: colorDotByPackage;
background: ${byPackageGradient};
`,
colorDotDiffDefault: css`
label: colorDotDiffDefault;
background: ${diffDefaultGradient};
`,
colorDotDiffColorBlind: css`
label: colorDotDiffColorBlind;
background: ${diffColorBlindGradient};
`,
});
export default FlameGraphHeader;

@ -1,76 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvents from '@testing-library/user-event';
import React from 'react';
import { CoreApp, createDataFrame } from '@grafana/data';
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
import { data } from '../FlameGraph/testData/dataNestedSet';
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer';
describe('FlameGraphTopTableContainer', () => {
const setup = () => {
const flameGraphData = createDataFrame(data);
const container = new FlameGraphDataContainer(flameGraphData);
const onSearch = jest.fn();
const onSandwich = jest.fn();
const renderResult = render(
<FlameGraphTopTableContainer
data={container}
app={CoreApp.Explore}
onSymbolClick={jest.fn()}
onSearch={onSearch}
onSandwich={onSandwich}
/>
);
return { renderResult, mocks: { onSearch, onSandwich } };
};
it('should render correctly', async () => {
// Needed for AutoSizer to work in test
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 });
setup();
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(16);
const columnHeaders = screen.getAllByRole('columnheader');
expect(columnHeaders).toHaveLength(4);
expect(columnHeaders[1].textContent).toEqual('Symbol');
expect(columnHeaders[2].textContent).toEqual('Self');
expect(columnHeaders[3].textContent).toEqual('Total');
const cells = screen.getAllByRole('cell');
expect(cells).toHaveLength(60); // 16 rows
expect(cells[1].textContent).toEqual('net/http.HandlerFunc.ServeHTTP');
expect(cells[2].textContent).toEqual('31.7 K');
expect(cells[3].textContent).toEqual('31.7 Bil');
expect(cells[25].textContent).toEqual('net/http.(*conn).serve');
expect(cells[26].textContent).toEqual('5.63 K');
expect(cells[27].textContent).toEqual('5.63 Bil');
});
it('should render search and sandwich buttons', async () => {
// Needed for AutoSizer to work in test
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 });
const { mocks } = setup();
const searchButtons = screen.getAllByLabelText(/Search for symbol/);
expect(searchButtons.length > 0).toBeTruthy();
await userEvents.click(searchButtons[0]);
expect(mocks.onSearch).toHaveBeenCalledWith('net/http.HandlerFunc.ServeHTTP');
const sandwichButtons = screen.getAllByLabelText(/Show in sandwich view/);
expect(sandwichButtons.length > 0).toBeTruthy();
await userEvents.click(sandwichButtons[0]);
expect(mocks.onSandwich).toHaveBeenCalledWith('net/http.HandlerFunc.ServeHTTP');
});
});

@ -1,300 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { applyFieldOverrides, CoreApp, DataFrame, DataLinkClickEvent, Field, FieldType } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import {
IconButton,
Table,
TableCellDisplayMode,
TableCustomCellOptions,
TableFieldOptions,
TableSortByFieldState,
useStyles2,
} from '@grafana/ui';
import { TOP_TABLE_COLUMN_WIDTH } from '../../constants';
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
import { TableData } from '../types';
type Props = {
data: FlameGraphDataContainer;
app: CoreApp;
onSymbolClick: (symbol: string) => void;
height?: number;
search?: string;
sandwichItem?: string;
onSearch: (str: string) => void;
onSandwich: (str?: string) => void;
};
const FlameGraphTopTableContainer = React.memo(
({ data, app, onSymbolClick, height, search, onSearch, sandwichItem, onSandwich }: Props) => {
const styles = useStyles2(getStyles);
const [sort, setSort] = useState<TableSortByFieldState[]>([{ displayName: 'Self', desc: true }]);
return (
<div className={styles.topTableContainer} data-testid="topTable">
<AutoSizer style={{ width: '100%', height }}>
{({ width, height }) => {
if (width < 3 || height < 3) {
return null;
}
const frame = buildTableDataFrame(data, width, onSymbolClick, onSearch, onSandwich, search, sandwichItem);
return (
<Table
initialSortBy={sort}
onSortByChange={(s) => {
if (s && s.length) {
reportInteraction('grafana_flamegraph_table_sort_selected', {
app,
grafana_version: config.buildInfo.version,
sort: s[0].displayName + '_' + (s[0].desc ? 'desc' : 'asc'),
});
}
setSort(s);
}}
data={frame}
width={width}
height={height}
/>
);
}}
</AutoSizer>
</div>
);
}
);
FlameGraphTopTableContainer.displayName = 'FlameGraphTopTableContainer';
function buildTableDataFrame(
data: FlameGraphDataContainer,
width: number,
onSymbolClick: (str: string) => void,
onSearch: (str: string) => void,
onSandwich: (str?: string) => void,
search?: string,
sandwichItem?: string
): DataFrame {
// Group the data by label
// TODO: should be by filename + funcName + linenumber?
let table: { [key: string]: TableData } = {};
for (let i = 0; i < data.data.length; i++) {
const value = data.getValue(i);
const valueRight = data.getValueRight(i);
const self = data.getSelf(i);
const label = data.getLabel(i);
table[label] = table[label] || {};
table[label].self = table[label].self ? table[label].self + self : self;
table[label].total = table[label].total ? table[label].total + value : value;
table[label].totalRight = table[label].totalRight ? table[label].totalRight + valueRight : valueRight;
}
const actionField: Field = createActionField(onSandwich, onSearch, search, sandwichItem);
const symbolField: Field = {
type: FieldType.string,
name: 'Symbol',
values: [],
config: {
custom: { width: width - actionColumnWidth - TOP_TABLE_COLUMN_WIDTH * 2 },
links: [
{
title: 'Highlight symbol',
url: '',
onClick: (e: DataLinkClickEvent) => {
const field: Field = e.origin.field;
const value = field.values[e.origin.rowIndex];
onSymbolClick(value);
},
},
],
},
};
let frame;
if (data.isDiffFlamegraph()) {
symbolField.config.custom.width = width - actionColumnWidth - TOP_TABLE_COLUMN_WIDTH * 3;
const baselineField = createNumberField('Baseline', 'percent');
const comparisonField = createNumberField('Comparison', 'percent');
const diffField = createNumberField('Diff', 'percent');
// For this we don't really consider sandwich view even though you can switch it on.
const levels = data.getLevels();
const totalTicks = levels.length ? levels[0][0].value : 0;
const totalTicksRight = levels.length ? levels[0][0].valueRight : undefined;
for (let key in table) {
actionField.values.push(null);
symbolField.values.push(key);
const ticksLeft = table[key].total;
const ticksRight = table[key].totalRight;
// We are iterating over table of the data so totalTicksRight needs to be defined
const totalTicksLeft = totalTicks - totalTicksRight!;
const percentageLeft = Math.round((10000 * ticksLeft) / totalTicksLeft) / 100;
const percentageRight = Math.round((10000 * ticksRight) / totalTicksRight!) / 100;
const diff = ((percentageRight - percentageLeft) / percentageLeft) * 100;
baselineField.values.push(percentageLeft);
comparisonField.values.push(percentageRight);
diffField.values.push(diff);
}
frame = {
fields: [actionField, symbolField, baselineField, comparisonField, diffField],
length: symbolField.values.length,
};
} else {
const selfField = createNumberField('Self', data.selfField.config.unit);
const totalField = createNumberField('Total', data.valueField.config.unit);
for (let key in table) {
actionField.values.push(null);
symbolField.values.push(key);
selfField.values.push(table[key].self);
totalField.values.push(table[key].total);
}
frame = { fields: [actionField, symbolField, selfField, totalField], length: symbolField.values.length };
}
const dataFrames = applyFieldOverrides({
data: [frame],
fieldConfig: {
defaults: {},
overrides: [],
},
replaceVariables: (value: string) => value,
theme: config.theme2,
});
return dataFrames[0];
}
function createNumberField(name: string, unit?: string): Field {
return {
type: FieldType.number,
name,
values: [],
config: { unit, custom: { width: TOP_TABLE_COLUMN_WIDTH } },
};
}
const actionColumnWidth = 61;
function createActionField(
onSandwich: (str?: string) => void,
onSearch: (str: string) => void,
search?: string,
sandwichItem?: string
): Field {
const options: TableCustomCellOptions = {
type: TableCellDisplayMode.Custom,
cellComponent: (props) => {
return (
<ActionCell
frame={props.frame}
onSandwich={onSandwich}
onSearch={onSearch}
search={search}
sandwichItem={sandwichItem}
rowIndex={props.rowIndex}
/>
);
},
};
const actionFieldTableConfig: TableFieldOptions = {
filterable: false,
width: actionColumnWidth,
hideHeader: true,
inspect: false,
align: 'auto',
cellOptions: options,
};
return {
type: FieldType.number,
name: 'actions',
values: [],
config: {
custom: actionFieldTableConfig,
},
};
}
type ActionCellProps = {
frame: DataFrame;
rowIndex: number;
search?: string;
sandwichItem?: string;
onSearch: (symbol: string) => void;
onSandwich: (symbol: string) => void;
};
function ActionCell(props: ActionCellProps) {
const styles = useStyles2(getStyles);
const symbol = props.frame.fields.find((f: Field) => f.name === 'Symbol')?.values[props.rowIndex];
const isSearched = props.search === symbol;
const isSandwiched = props.sandwichItem === symbol;
return (
<div className={styles.actionCellWrapper}>
<IconButton
className={styles.actionCellButton}
name={'search'}
variant={isSearched ? 'primary' : 'secondary'}
tooltip={isSearched ? 'Clear from search' : 'Search for symbol'}
aria-label={isSearched ? 'Clear from search' : 'Search for symbol'}
onClick={() => {
props.onSearch(isSearched ? '' : symbol);
}}
/>
<IconButton
className={styles.actionCellButton}
name={'gf-show-context'}
tooltip={isSandwiched ? 'Remove from sandwich view' : 'Show in sandwich view'}
variant={isSandwiched ? 'primary' : 'secondary'}
aria-label={isSandwiched ? 'Remove from sandwich view' : 'Show in sandwich view'}
onClick={() => {
props.onSandwich(isSandwiched ? undefined : symbol);
}}
/>
</div>
);
}
const getStyles = () => {
return {
topTableContainer: css`
label: topTableContainer;
flex-grow: 1;
flex-basis: 50%;
overflow: hidden;
`,
actionCellWrapper: css`
label: actionCellWrapper;
display: flex;
height: 24px;
`,
actionCellButton: css`
label: actionCellButton;
margin-right: 0;
width: 24px;
`,
};
};
export default FlameGraphTopTableContainer;

@ -1,57 +0,0 @@
import { LevelItem } from './FlameGraph/dataTransform';
export type ClickedItemData = {
posX: number;
posY: number;
label: string;
item: LevelItem;
level: number;
};
export enum SampleUnit {
Bytes = 'bytes',
Short = 'short',
Nanoseconds = 'ns',
}
export enum ColumnTypes {
Symbol = 'Symbol',
Self = 'Self',
Total = 'Total',
}
export enum SelectedView {
TopTable = 'topTable',
FlameGraph = 'flameGraph',
Both = 'both',
}
export interface TableData {
self: number;
total: number;
// For diff view
totalRight: number;
}
export interface TopTableData {
symbol: string;
self: TopTableValue;
total: TopTableValue;
}
export type TopTableValue = {
value: number;
unitValue: string;
};
export enum ColorScheme {
ValueBased = 'valueBased',
PackageBased = 'packageBased',
}
export enum ColorSchemeDiff {
Default = 'default',
DiffColorBlind = 'diffColorBlind',
}
export type TextAlign = 'left' | 'right';

@ -1,8 +0,0 @@
export const PIXELS_PER_LEVEL = 22 * window.devicePixelRatio;
export const COLLAPSE_THRESHOLD = 10 * window.devicePixelRatio;
export const HIDE_THRESHOLD = 0.5 * window.devicePixelRatio;
export const LABEL_THRESHOLD = 20 * window.devicePixelRatio;
export const BAR_BORDER_WIDTH = 0.5 * window.devicePixelRatio;
export const BAR_TEXT_PADDING_LEFT = 4 * window.devicePixelRatio;
export const MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH = 800;
export const TOP_TABLE_COLUMN_WIDTH = 120;

@ -1,8 +1,7 @@
import { VisualizationSuggestionsBuilder } from '@grafana/data';
import { checkFields } from '@grafana/flamegraph';
import { SuggestionName } from 'app/types/suggestions';
import { checkFields } from './components/FlameGraph/dataTransform';
export class FlameGraphSuggestionsSupplier {
getListWithDefaults(builder: VisualizationSuggestionsBuilder) {
return builder.getListAppender<{}, {}>({

Loading…
Cancel
Save