Flamegraph: Move to package (#73113)

This commit is contained in:
Andrej Ocenas 2023-09-12 12:28:48 +02:00 committed by GitHub
parent 02f617a20d
commit e4f26a5e4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 46192 additions and 344 deletions

1
.github/CODEOWNERS vendored
View File

@ -323,6 +323,7 @@
/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling
/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling
/plugins-bundled/ @grafana/plugins-platform-frontend

View File

@ -248,6 +248,7 @@
"@grafana/experimental": "1.7.0",
"@grafana/faro-core": "1.1.2",
"@grafana/faro-web-sdk": "1.1.2",
"@grafana/flamegraph": "workspace:*",
"@grafana/google-sdk": "0.1.1",
"@grafana/lezer-logql": "0.1.11",
"@grafana/lezer-traceql": "0.0.5",

View File

@ -0,0 +1,19 @@
{
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": ["@grafana/runtime", "@grafana/e2e", "@grafana/e2e-selectors/*"]
}
]
},
"overrides": [
{
"files": ["**/*.{test,story}.{ts,tsx}"],
"rules": {
"no-restricted-imports": "off",
"react/prop-types": "off"
}
}
]
}

View File

@ -0,0 +1,3 @@
# 10.2.0 (2023-09-06)
First public release

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Grafana Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,53 @@
# Grafana Flamegraph component
> **@grafana/flamegraph is currently in BETA**.
This is a Flamegraph component that is used in Grafana and Pyroscope web app to display profiles.
## Usage
Currently this library exposes single component `Flamegraph` that renders whole visualization used for profiling which contains a header, a table representation of the data and a flamegraph.
```tsx
import { Flamegraph } from '@grafana/flamegraph';
<FlameGraph
getTheme={() => createTheme({ colors: { mode: 'dark' } })}
data={dataFrame}
extraHeaderElements={
<Button onClick={() => {}} variant="secondary">
Download
/>
}
stickyHeader
vertical
/>
```
#### Props
| Name | Type | Description |
| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| data | DataFrame | DataFrame with the profile data. Optional, if missing or empty the flamegraph is not rendered |
| stickyHeader | boolean | Whether the header should be sticky and be always visible on the top when scrolling. |
| getTheme | () => GrafanaTheme2 | Provides a theme for the visualization on which colors and some sizes are based. |
| onTableSymbolClick | (symbol: string) => void | Interaction hook that can be used to report on the interaction. Fires when user click on a name in the table. |
| onViewSelected | (view: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the view to show (table/graph/both) |
| onTextAlignSelected | (align: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the text align. |
| onTableSort | (sort: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the teble sorting. |
| extraHeaderElements | React.ReactNode | Elements that will be shown in the header on the right side of the header buttons. Useful for additional functionality. |
| vertical | boolean | If true the flamegraph will be rendered on top of the table. |
##### DataFrame schema
The dataFrame needs to have the following fields:
| Name | Type | Description |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
| name | string | The name of the node. |
| labels | string[] | The labels of the node. |
| level | number | The nesting level of the node. |
| value | number | The total value of the node. |
| self | number | The self value of the node. |
| valueRight | number | The total value of the node in the right profile. Optional, if present will show a diff version of the flamegraph. |
| selfRight | number | The self value of the node in the right profile. Optional, if present will show a diff version of the flamegraph. |

View File

@ -0,0 +1,88 @@
{
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/flamegraph",
"version": "10.2.0-pre",
"description": "Grafana flamegraph visualization component",
"keywords": [
"grafana",
"flamegraph",
"profiling",
"pyroscope"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git",
"directory": "packages/grafana-flamegraph"
},
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"access": "public"
},
"files": [
"./dist",
"./README.md",
"./CHANGELOG.md",
"./LICENSE_APACHE2"
],
"scripts": {
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts",
"bundle": "rollup -c rollup.config.ts",
"clean": "rimraf ./dist ./compiled ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
"postpack": "mv package.json.bak package.json"
},
"browserslist": [
"defaults",
"not IE 11"
],
"dependencies": {
"@emotion/css": "11.11.2",
"@grafana/data": "10.2.0-pre",
"@grafana/ui": "10.2.0-pre",
"@leeoniya/ufuzzy": "1.0.8",
"d3": "^7.8.5",
"lodash": "4.17.21",
"react": "18.2.0",
"react-use": "17.4.0",
"react-virtualized-auto-sizer": "1.0.7",
"tinycolor2": "1.6.0",
"tslib": "2.6.0"
},
"devDependencies": {
"@babel/core": "7.22.9",
"@babel/preset-env": "7.22.9",
"@babel/preset-react": "7.22.5",
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-node-resolve": "15.1.0",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/d3": "^7",
"@types/jest": "^29.5.4",
"@types/lodash": "4.14.195",
"@types/react": "18.2.15",
"@types/react-virtualized-auto-sizer": "1.0.1",
"@types/tinycolor2": "1.4.3",
"babel-jest": "29.3.1",
"jest": "^29.6.4",
"jest-canvas-mock": "2.5.2",
"rollup": "2.79.1",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-esbuild": "5.0.0",
"rollup-plugin-node-externals": "^5.0.0",
"ts-jest": "29.1.1",
"ts-node": "10.9.1",
"typescript": "4.8.4"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
}
}

View File

@ -0,0 +1,37 @@
import resolve from '@rollup/plugin-node-resolve';
import path from 'path';
import dts from 'rollup-plugin-dts';
import esbuild from 'rollup-plugin-esbuild';
import { externals } from 'rollup-plugin-node-externals';
const pkg = require('./package.json');
export default [
{
input: 'src/index.ts',
plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()],
output: [
{
format: 'cjs',
sourcemap: true,
dir: path.dirname(pkg.publishConfig.main),
},
{
format: 'esm',
sourcemap: true,
dir: path.dirname(pkg.publishConfig.module),
preserveModules: true,
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`),
},
],
},
{
input: './compiled/index.d.ts',
plugins: [dts()],
output: {
file: pkg.publishConfig.types,
format: 'es',
},
},
];

View File

@ -0,0 +1,99 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { createDataFrame, createTheme } 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}
getTheme={() => createTheme({ colors: { mode: 'dark' } })}
/>
);
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();
});
});

View File

@ -0,0 +1,329 @@
// 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 { GrafanaTheme2 } from '@grafana/data';
import { Icon } 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;
getTheme: () => GrafanaTheme2;
};
const FlameGraph = ({
data,
rangeMin,
rangeMax,
search,
setRangeMin,
setRangeMax,
onItemFocused,
focusedItemData,
textAlign,
onSandwich,
sandwichItem,
onFocusPillClick,
onSandwichPillClick,
colorScheme,
getTheme,
}: Props) => {
const styles = 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,
getTheme,
});
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
getTheme={getTheme}
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
getTheme={getTheme}
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`
label: graph;
overflow: auto;
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`
label: sandwichMarker;
writing-mode: vertical-lr;
transform: rotate(180deg);
overflow: hidden;
white-space: nowrap;
`,
sandwichMarkerIcon: css`
label: sandwichMarkerIcon;
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;

View File

@ -0,0 +1,59 @@
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;

View File

@ -0,0 +1,76 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { createTheme } from '@grafana/data';
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}
getTheme={() => createTheme({ colors: { mode: 'dark' } })}
{...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);
});
});

View File

@ -0,0 +1,125 @@
import { css } from '@emotion/css';
import React, { ReactNode } from 'react';
import { getValueFormat, GrafanaTheme2 } from '@grafana/data';
import { Icon, IconButton } 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;
getTheme: () => GrafanaTheme2;
};
const FlameGraphMetadata = React.memo(
({ data, focusedItem, totalTicks, sandwichedLabel, onFocusPillClick, onSandwichPillClick, getTheme }: Props) => {
const styles = getStyles(getTheme());
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;

View File

@ -0,0 +1,156 @@
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,
};
}

View File

@ -0,0 +1,203 @@
import { css } from '@emotion/css';
import React from 'react';
import { DisplayValue, getValueFormat, GrafanaTheme2 } from '@grafana/data';
import { InteractiveTable, Portal, VizTooltipContainer } from '@grafana/ui';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
type Props = {
data: FlameGraphDataContainer;
totalTicks: number;
position?: { x: number; y: number };
item?: LevelItem;
getTheme: () => GrafanaTheme2;
};
const FlameGraphTooltip = ({ data, item, totalTicks, position, getTheme }: Props) => {
const styles = getStyles(getTheme());
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;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
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%)'
);
});
});

View File

@ -0,0 +1,133 @@
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;
}

View File

@ -0,0 +1,71 @@
import { createDataFrame, 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]);
});
});

View File

@ -0,0 +1,288 @@
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);
}

View File

@ -0,0 +1,84 @@
// 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;
}

View File

@ -0,0 +1,88 @@
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 },
]);
});
});

View File

@ -0,0 +1,319 @@
import uFuzzy from '@leeoniya/ufuzzy';
import { RefObject, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { colors } 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;
getTheme: () => GrafanaTheme2;
};
export function useFlameRender(options: RenderOptions) {
const {
canvasRef,
data,
levels,
wrapperWidth,
rangeMin,
rangeMax,
search,
textAlign,
totalViewTicks,
totalColorTicks,
totalTicksRight,
colorScheme,
focusedItemData,
getTheme,
} = 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 = getTheme();
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;
}

View File

@ -0,0 +1,572 @@
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,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
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);
});
});

View File

@ -0,0 +1,104 @@
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');
}

View File

@ -0,0 +1,162 @@
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]
`)
);
});
});

View File

@ -0,0 +1,125 @@
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;
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),
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;
}

View File

@ -0,0 +1,90 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { createDataFrame, createTheme } from '@grafana/data';
import { data } from './FlameGraph/testData/dataNestedSet';
import FlameGraphContainer from './FlameGraphContainer';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
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} getTheme={() => createTheme({ colors: { mode: 'dark' } })} />;
};
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();
});
});

View File

@ -0,0 +1,252 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
import FlameGraph from './FlameGraph/FlameGraph';
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
import FlameGraphHeader from './FlameGraphHeader';
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
export type Props = {
/**
* DataFrame with the profile data. The dataFrame needs to have the following fields:
* label: string - the label of the node
* level: number - the nesting level of the node
* value: number - the total value of the node
* self: number - the self value of the node
* Optionally if it represents diff of 2 different profiles it can also have fields:
* valueRight: number - the total value of the node in the right profile
* selfRight: number - the self value of the node in the right profile
*/
data?: DataFrame;
/**
* Whether the header should be sticky and be always visible on the top when scrolling.
*/
stickyHeader?: boolean;
/**
* Provides a theme for the visualization on which colors and some sizes are based.
*/
getTheme: () => GrafanaTheme2;
/**
* Various interaction hooks that can be used to report on the interaction.
*/
onTableSymbolClick?: (symbol: string) => void;
onViewSelected?: (view: string) => void;
onTextAlignSelected?: (align: string) => void;
onTableSort?: (sort: string) => void;
/**
* Elements that will be shown in the header on the right side of the header buttons. Useful for additional
* functionality.
*/
extraHeaderElements?: React.ReactNode;
/**
* If true the flamegraph will be rendered on top of the table.
*/
vertical?: boolean;
};
const FlameGraphContainer = ({
data,
onTableSymbolClick,
onViewSelected,
onTextAlignSelected,
onTableSort,
getTheme,
stickyHeader,
extraHeaderElements,
vertical,
}: 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 = getTheme();
const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
if (!data) {
return;
}
return new FlameGraphDataContainer(data, theme);
}, [data, theme]);
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
const styles = getStyles(theme, vertical);
// 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 &&
!vertical
) {
setSelectedView(SelectedView.FlameGraph);
}
}, [selectedView, setSelectedView, containerWidth, vertical]);
const resetFocus = useCallback(() => {
setFocusedItemData(undefined);
setRangeMin(0);
setRangeMax(1);
}, [setFocusedItemData, setRangeMax, setRangeMin]);
function resetSandwich() {
setSandwichItem(undefined);
}
useEffect(() => {
resetFocus();
resetSandwich();
}, [data, resetFocus]);
const onSymbolClick = useCallback(
(symbol: string) => {
if (search === symbol) {
setSearch('');
} else {
onTableSymbolClick?.(symbol);
setSearch(symbol);
resetFocus();
}
},
[setSearch, resetFocus, onTableSymbolClick, search]
);
if (!dataContainer) {
return null;
}
return (
<div ref={sizeRef} className={styles.container}>
<FlameGraphHeader
search={search}
setSearch={setSearch}
selectedView={selectedView}
setSelectedView={(view) => {
setSelectedView(view);
onViewSelected?.(view);
}}
containerWidth={containerWidth}
onReset={() => {
resetFocus();
resetSandwich();
}}
textAlign={textAlign}
onTextAlignChange={(align) => {
setTextAlign(align);
onTextAlignSelected?.(align);
}}
showResetButton={Boolean(focusedItemData || sandwichItem)}
colorScheme={colorScheme}
onColorSchemeChange={setColorScheme}
stickyHeader={Boolean(stickyHeader)}
extraHeaderElements={extraHeaderElements}
vertical={vertical}
isDiffMode={Boolean(dataContainer.isDiffFlamegraph())}
getTheme={getTheme}
/>
<div className={styles.body}>
{selectedView !== SelectedView.FlameGraph && (
<FlameGraphTopTableContainer
data={dataContainer}
onSymbolClick={onSymbolClick}
height={selectedView === SelectedView.TopTable ? 600 : undefined}
search={search}
sandwichItem={sandwichItem}
onSandwich={setSandwichItem}
onSearch={setSearch}
onTableSort={onTableSort}
getTheme={getTheme}
vertical={vertical}
/>
)}
{selectedView !== SelectedView.TopTable && (
<FlameGraph
getTheme={getTheme}
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, vertical?: boolean) {
return {
container: css({
label: 'container',
height: '100%',
display: 'flex',
flex: '1 1 0',
flexDirection: 'column',
minHeight: 0,
gap: theme.spacing(1),
}),
body: css({
label: 'body',
display: 'flex',
flexGrow: 1,
minHeight: 0,
flexDirection: vertical ? 'column-reverse' : 'row',
}),
};
}
export default FlameGraphContainer;

View File

@ -0,0 +1,87 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { createTheme } 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
search={''}
setSearch={setSearch}
selectedView={SelectedView.Both}
setSelectedView={setSelectedView}
containerWidth={1600}
onReset={onReset}
onTextAlignChange={jest.fn()}
textAlign={'left'}
showResetButton={true}
colorScheme={ColorScheme.ValueBased}
onColorSchemeChange={onSchemeChange}
getTheme={() => createTheme({ colors: { mode: 'dark' } })}
stickyHeader={false}
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();
});
});

View File

@ -0,0 +1,299 @@
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 { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Button, Dropdown, Input, Menu, RadioButtonGroup } from '@grafana/ui';
import { byPackageGradient, byValueGradient, diffColorBlindGradient, diffDefaultGradient } from './FlameGraph/colors';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
import { ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
type Props = {
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;
stickyHeader: boolean;
vertical?: boolean;
isDiffMode: boolean;
getTheme: () => GrafanaTheme2;
extraHeaderElements?: React.ReactNode;
};
const FlameGraphHeader = ({
search,
setSearch,
selectedView,
setSelectedView,
containerWidth,
onReset,
textAlign,
onTextAlignChange,
showResetButton,
colorScheme,
onColorSchemeChange,
stickyHeader,
extraHeaderElements,
vertical,
isDiffMode,
getTheme,
}: Props) => {
const styles = getStyles(getTheme(), stickyHeader);
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..'}
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
value={colorScheme}
onChange={onColorSchemeChange}
isDiffMode={isDiffMode}
getTheme={getTheme}
/>
<RadioButtonGroup<TextAlign>
size="sm"
disabled={selectedView === SelectedView.TopTable}
options={alignOptions}
value={textAlign}
onChange={onTextAlignChange}
className={styles.buttonSpacing}
/>
<RadioButtonGroup<SelectedView>
size="sm"
options={getViewOptions(containerWidth, vertical)}
value={selectedView}
onChange={setSelectedView}
/>
{extraHeaderElements && <div className={styles.extraElements}>{extraHeaderElements}</div>}
</div>
</div>
);
};
type ColorSchemeButtonProps = {
value: ColorScheme | ColorSchemeDiff;
onChange: (colorScheme: ColorScheme | ColorSchemeDiff) => void;
getTheme: () => GrafanaTheme2;
isDiffMode: boolean;
};
function ColorSchemeButton(props: ColorSchemeButtonProps) {
// TODO: probably create separate getStyles
const styles = getStyles(props.getTheme(), false);
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, vertical?: boolean): 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 || vertical) {
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, sticky?: boolean) => ({
header: css`
label: header;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
top: 0;
z-index: ${theme.zIndex.navbarFixed};
${sticky
? css`
position: sticky;
padding-bottom: ${theme.spacing(1)};
padding-top: ${theme.spacing(1)};
background: ${theme.colors.background.primary};
`
: ''};
`,
inputContainer: css`
label: inputContainer;
margin-right: 20px;
flex-grow: 1;
min-width: 150px;
max-width: 350px;
`,
rightContainer: css`
label: rightContainer;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
`,
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};
`,
extraElements: css`
label: extraElements;
margin-left: ${theme.spacing(1)};
`,
});
export default FlameGraphHeader;

View File

@ -0,0 +1,76 @@
import { render, screen } from '@testing-library/react';
import userEvents from '@testing-library/user-event';
import React from 'react';
import { createDataFrame, createTheme } 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}
onSymbolClick={jest.fn()}
onSearch={onSearch}
onSandwich={onSandwich}
getTheme={() => createTheme({ colors: { mode: 'dark' } })}
/>
);
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');
});
});

View File

@ -0,0 +1,362 @@
import { css } from '@emotion/css';
import React, { useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import {
applyFieldOverrides,
DataFrame,
DataLinkClickEvent,
Field,
FieldType,
GrafanaTheme2,
MappingType,
} from '@grafana/data';
import {
IconButton,
Table,
TableCellDisplayMode,
TableCustomCellOptions,
TableFieldOptions,
TableSortByFieldState,
} from '@grafana/ui';
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
import { TOP_TABLE_COLUMN_WIDTH } from '../constants';
import { TableData } from '../types';
type Props = {
data: FlameGraphDataContainer;
onSymbolClick: (symbol: string) => void;
height?: number;
search?: string;
sandwichItem?: string;
onSearch: (str: string) => void;
onSandwich: (str?: string) => void;
onTableSort?: (sort: string) => void;
getTheme: () => GrafanaTheme2;
vertical?: boolean;
};
const FlameGraphTopTableContainer = React.memo(
({
data,
onSymbolClick,
height,
search,
onSearch,
sandwichItem,
onSandwich,
onTableSort,
getTheme,
vertical,
}: Props) => {
const table = useMemo(() => {
// Group the data by label, we show only one row per label and sum the values
// 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;
}
return table;
}, [data]);
const rowHeight = 35;
// When we use normal layout we size the table to have the same height as the flamegraph to look good side by side.
// In vertical layout we don't need that so this is a bit arbitrary. We want some max limit
// so we don't show potentially thousands of rows at once which can hinder performance (the table is virtualized
// so with some max height it handles it fine)
const tableHeight = vertical ? Math.min(Object.keys(table).length * rowHeight, 800) : 0;
const styles = getStyles(tableHeight);
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,
table,
width,
onSymbolClick,
onSearch,
onSandwich,
getTheme,
search,
sandwichItem
);
return (
<Table
initialSortBy={sort}
onSortByChange={(s) => {
if (s && s.length) {
onTableSort?.(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,
table: { [key: string]: TableData },
width: number,
onSymbolClick: (str: string) => void,
onSearch: (str: string) => void,
onSandwich: (str?: string) => void,
getTheme: () => GrafanaTheme2,
search?: string,
sandwichItem?: string
): DataFrame {
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');
diffField.config.custom.cellOptions.type = TableCellDisplayMode.ColorText;
diffField.config.mappings = [
{ type: MappingType.ValueToText, options: { [Infinity]: { text: 'new', color: 'red' } } },
{ type: MappingType.ValueToText, options: { [-100]: { text: 'removed', color: 'green' } } },
{ type: MappingType.RangeToText, options: { from: 0, to: Infinity, result: { color: 'red' } } },
{ type: MappingType.RangeToText, options: { from: -Infinity, to: 0, result: { color: 'green' } } },
];
// 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;
diffField.values.push(diff);
baselineField.values.push(percentageLeft);
comparisonField.values.push(percentageRight);
}
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: getTheme(),
});
return dataFrames[0];
}
function createNumberField(name: string, unit?: string): Field {
const tableFieldOptions: TableFieldOptions = {
width: TOP_TABLE_COLUMN_WIDTH,
align: 'auto',
inspect: false,
cellOptions: { type: TableCellDisplayMode.Auto },
};
return {
type: FieldType.number,
name,
values: [],
config: {
unit,
custom: tableFieldOptions,
},
};
}
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 = getStylesActionCell();
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 = (height: number) => {
return {
topTableContainer: css`
label: topTableContainer;
flex-grow: 1;
flex-basis: 50%;
overflow: hidden;
${height
? css`
min-height: ${height}px;
`
: ''}
`,
};
};
const getStylesActionCell = () => {
return {
actionCellWrapper: css`
label: actionCellWrapper;
display: flex;
height: 24px;
`,
actionCellButton: css`
label: actionCellButton;
margin-right: 0;
width: 24px;
`,
};
};
export default FlameGraphTopTableContainer;

View File

@ -0,0 +1,8 @@
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;

View File

@ -0,0 +1 @@
export { default as FlameGraph, type Props } from './FlameGraphContainer';

View File

@ -0,0 +1,57 @@
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';

View File

@ -0,0 +1,4 @@
// This stub is required due to Storybook 6.x reliance on @emotion/core
// which causes conflicts with emotion 11 types resulting in bundled
// components throwing ts error `Property 'css' is missing in type...`
export {};

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"paths": {
"@emotion/core": ["./src/types/emotion-core-stub.d.ts"]
}
},
"exclude": ["**/*.test.ts*"],
"extends": "./tsconfig.json"
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": "./",
"declarationDir": "./compiled",
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."]
},
"exclude": ["dist/**/*"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"]
}

View File

@ -1,6 +1,8 @@
[
"unicons/at",
"unicons/adjust-circle",
"unicons/align-left",
"unicons/align-right",
"unicons/angle-double-down",
"unicons/angle-double-right",
"unicons/angle-down",
@ -75,6 +77,7 @@
"unicons/forward",
"unicons/graph-bar",
"unicons/history",
"unicons/history-alt",
"unicons/home-alt",
"unicons/import",
"unicons/info",
@ -150,6 +153,7 @@
"custom/gf-landscape",
"custom/gf-layout-simple",
"custom/gf-portrait",
"custom/gf-show-context",
"custom/gf-bar-alignment-after",
"custom/gf-bar-alignment-before",
"custom/gf-bar-alignment-center",

View File

@ -9,172 +9,176 @@ import { cacheStore } from 'react-inlinesvg';
// the list of icons live here: @grafana/ui/components/Icon/cached.json
import u1000 from '../../../../../public/img/icons/unicons/at.svg';
import u1001 from '../../../../../public/img/icons/unicons/adjust-circle.svg';
import u1002 from '../../../../../public/img/icons/unicons/angle-double-down.svg';
import u1003 from '../../../../../public/img/icons/unicons/angle-double-right.svg';
import u1004 from '../../../../../public/img/icons/unicons/angle-down.svg';
import u1005 from '../../../../../public/img/icons/unicons/angle-left.svg';
import u1006 from '../../../../../public/img/icons/unicons/angle-right.svg';
import u1007 from '../../../../../public/img/icons/unicons/angle-up.svg';
import u1008 from '../../../../../public/img/icons/unicons/apps.svg';
import u1009 from '../../../../../public/img/icons/unicons/arrow.svg';
import u1010 from '../../../../../public/img/icons/unicons/arrow-down.svg';
import u1011 from '../../../../../public/img/icons/unicons/arrow-from-right.svg';
import u1012 from '../../../../../public/img/icons/unicons/arrow-left.svg';
import u1013 from '../../../../../public/img/icons/unicons/arrow-random.svg';
import u1014 from '../../../../../public/img/icons/unicons/arrow-right.svg';
import u1015 from '../../../../../public/img/icons/unicons/arrow-to-right.svg';
import u1016 from '../../../../../public/img/icons/unicons/arrow-up.svg';
import u1017 from '../../../../../public/img/icons/unicons/arrows-h.svg';
import u1018 from '../../../../../public/img/icons/unicons/backward.svg';
import u1019 from '../../../../../public/img/icons/unicons/bars.svg';
import u1020 from '../../../../../public/img/icons/unicons/bell.svg';
import u1021 from '../../../../../public/img/icons/unicons/bell-slash.svg';
import u1022 from '../../../../../public/img/icons/unicons/bolt.svg';
import u1023 from '../../../../../public/img/icons/unicons/book.svg';
import u1024 from '../../../../../public/img/icons/unicons/book-open.svg';
import u1025 from '../../../../../public/img/icons/unicons/brackets-curly.svg';
import u1026 from '../../../../../public/img/icons/unicons/bug.svg';
import u1027 from '../../../../../public/img/icons/unicons/building.svg';
import u1028 from '../../../../../public/img/icons/unicons/calculator-alt.svg';
import u1029 from '../../../../../public/img/icons/unicons/calendar-alt.svg';
import u1030 from '../../../../../public/img/icons/unicons/calendar-slash.svg';
import u1031 from '../../../../../public/img/icons/unicons/camera.svg';
import u1032 from '../../../../../public/img/icons/unicons/channel-add.svg';
import u1033 from '../../../../../public/img/icons/unicons/chart-line.svg';
import u1034 from '../../../../../public/img/icons/unicons/check.svg';
import u1035 from '../../../../../public/img/icons/unicons/check-circle.svg';
import u1036 from '../../../../../public/img/icons/unicons/circle.svg';
import u1037 from '../../../../../public/img/icons/unicons/clipboard-alt.svg';
import u1038 from '../../../../../public/img/icons/unicons/clock-nine.svg';
import u1039 from '../../../../../public/img/icons/unicons/cloud.svg';
import u1040 from '../../../../../public/img/icons/unicons/cloud-download.svg';
import u1041 from '../../../../../public/img/icons/unicons/code-branch.svg';
import u1042 from '../../../../../public/img/icons/unicons/cog.svg';
import u1043 from '../../../../../public/img/icons/unicons/columns.svg';
import u1044 from '../../../../../public/img/icons/unicons/comment-alt.svg';
import u1045 from '../../../../../public/img/icons/unicons/comment-alt-share.svg';
import u1046 from '../../../../../public/img/icons/unicons/comments-alt.svg';
import u1047 from '../../../../../public/img/icons/unicons/compass.svg';
import u1048 from '../../../../../public/img/icons/unicons/copy.svg';
import u1049 from '../../../../../public/img/icons/unicons/corner-down-right-alt.svg';
import u1050 from '../../../../../public/img/icons/unicons/cube.svg';
import u1051 from '../../../../../public/img/icons/unicons/dashboard.svg';
import u1052 from '../../../../../public/img/icons/unicons/database.svg';
import u1053 from '../../../../../public/img/icons/unicons/document-info.svg';
import u1054 from '../../../../../public/img/icons/unicons/download-alt.svg';
import u1055 from '../../../../../public/img/icons/unicons/draggabledots.svg';
import u1056 from '../../../../../public/img/icons/unicons/edit.svg';
import u1057 from '../../../../../public/img/icons/unicons/ellipsis-v.svg';
import u1058 from '../../../../../public/img/icons/unicons/ellipsis-h.svg';
import u1059 from '../../../../../public/img/icons/unicons/envelope.svg';
import u1060 from '../../../../../public/img/icons/unicons/exchange-alt.svg';
import u1061 from '../../../../../public/img/icons/unicons/exclamation-circle.svg';
import u1062 from '../../../../../public/img/icons/unicons/exclamation-triangle.svg';
import u1063 from '../../../../../public/img/icons/unicons/external-link-alt.svg';
import u1064 from '../../../../../public/img/icons/unicons/eye.svg';
import u1065 from '../../../../../public/img/icons/unicons/eye-slash.svg';
import u1066 from '../../../../../public/img/icons/unicons/file-alt.svg';
import u1067 from '../../../../../public/img/icons/unicons/file-blank.svg';
import u1068 from '../../../../../public/img/icons/unicons/filter.svg';
import u1069 from '../../../../../public/img/icons/unicons/folder.svg';
import u1070 from '../../../../../public/img/icons/unicons/folder-open.svg';
import u1071 from '../../../../../public/img/icons/unicons/folder-plus.svg';
import u1072 from '../../../../../public/img/icons/unicons/folder-upload.svg';
import u1073 from '../../../../../public/img/icons/unicons/forward.svg';
import u1074 from '../../../../../public/img/icons/unicons/graph-bar.svg';
import u1075 from '../../../../../public/img/icons/unicons/history.svg';
import u1076 from '../../../../../public/img/icons/unicons/home-alt.svg';
import u1077 from '../../../../../public/img/icons/unicons/import.svg';
import u1078 from '../../../../../public/img/icons/unicons/info.svg';
import u1079 from '../../../../../public/img/icons/unicons/info-circle.svg';
import u1080 from '../../../../../public/img/icons/unicons/k6.svg';
import u1081 from '../../../../../public/img/icons/unicons/key-skeleton-alt.svg';
import u1082 from '../../../../../public/img/icons/unicons/keyboard.svg';
import u1083 from '../../../../../public/img/icons/unicons/link.svg';
import u1084 from '../../../../../public/img/icons/unicons/list-ul.svg';
import u1085 from '../../../../../public/img/icons/unicons/lock.svg';
import u1086 from '../../../../../public/img/icons/unicons/minus.svg';
import u1087 from '../../../../../public/img/icons/unicons/minus-circle.svg';
import u1088 from '../../../../../public/img/icons/unicons/mobile-android.svg';
import u1089 from '../../../../../public/img/icons/unicons/monitor.svg';
import u1090 from '../../../../../public/img/icons/unicons/pause.svg';
import u1091 from '../../../../../public/img/icons/unicons/pen.svg';
import u1092 from '../../../../../public/img/icons/unicons/play.svg';
import u1093 from '../../../../../public/img/icons/unicons/plug.svg';
import u1094 from '../../../../../public/img/icons/unicons/plus.svg';
import u1095 from '../../../../../public/img/icons/unicons/plus-circle.svg';
import u1096 from '../../../../../public/img/icons/unicons/power.svg';
import u1097 from '../../../../../public/img/icons/unicons/presentation-play.svg';
import u1098 from '../../../../../public/img/icons/unicons/process.svg';
import u1099 from '../../../../../public/img/icons/unicons/question-circle.svg';
import u1100 from '../../../../../public/img/icons/unicons/repeat.svg';
import u1101 from '../../../../../public/img/icons/unicons/rocket.svg';
import u1102 from '../../../../../public/img/icons/unicons/rss.svg';
import u1103 from '../../../../../public/img/icons/unicons/save.svg';
import u1104 from '../../../../../public/img/icons/unicons/search.svg';
import u1105 from '../../../../../public/img/icons/unicons/search-minus.svg';
import u1106 from '../../../../../public/img/icons/unicons/search-plus.svg';
import u1107 from '../../../../../public/img/icons/unicons/share-alt.svg';
import u1108 from '../../../../../public/img/icons/unicons/shield.svg';
import u1109 from '../../../../../public/img/icons/unicons/signal.svg';
import u1110 from '../../../../../public/img/icons/unicons/signin.svg';
import u1111 from '../../../../../public/img/icons/unicons/signout.svg';
import u1112 from '../../../../../public/img/icons/unicons/sitemap.svg';
import u1113 from '../../../../../public/img/icons/unicons/slack.svg';
import u1114 from '../../../../../public/img/icons/unicons/sliders-v-alt.svg';
import u1115 from '../../../../../public/img/icons/unicons/sort-amount-down.svg';
import u1116 from '../../../../../public/img/icons/unicons/sort-amount-up.svg';
import u1117 from '../../../../../public/img/icons/unicons/square-shape.svg';
import u1118 from '../../../../../public/img/icons/unicons/star.svg';
import u1119 from '../../../../../public/img/icons/unicons/step-backward.svg';
import u1120 from '../../../../../public/img/icons/unicons/sync.svg';
import u1121 from '../../../../../public/img/icons/unicons/stopwatch.svg';
import u1122 from '../../../../../public/img/icons/unicons/table.svg';
import u1123 from '../../../../../public/img/icons/unicons/tag-alt.svg';
import u1124 from '../../../../../public/img/icons/unicons/times.svg';
import u1125 from '../../../../../public/img/icons/unicons/trash-alt.svg';
import u1126 from '../../../../../public/img/icons/unicons/unlock.svg';
import u1127 from '../../../../../public/img/icons/unicons/upload.svg';
import u1128 from '../../../../../public/img/icons/unicons/user.svg';
import u1129 from '../../../../../public/img/icons/unicons/users-alt.svg';
import u1130 from '../../../../../public/img/icons/unicons/wrap-text.svg';
import u1131 from '../../../../../public/img/icons/unicons/cloud-upload.svg';
import u1132 from '../../../../../public/img/icons/unicons/credit-card.svg';
import u1133 from '../../../../../public/img/icons/unicons/file-copy-alt.svg';
import u1134 from '../../../../../public/img/icons/unicons/fire.svg';
import u1135 from '../../../../../public/img/icons/unicons/hourglass.svg';
import u1136 from '../../../../../public/img/icons/unicons/layer-group.svg';
import u1137 from '../../../../../public/img/icons/unicons/layers-alt.svg';
import u1138 from '../../../../../public/img/icons/unicons/line-alt.svg';
import u1139 from '../../../../../public/img/icons/unicons/list-ui-alt.svg';
import u1140 from '../../../../../public/img/icons/unicons/message.svg';
import u1141 from '../../../../../public/img/icons/unicons/palette.svg';
import u1142 from '../../../../../public/img/icons/unicons/percentage.svg';
import u1143 from '../../../../../public/img/icons/unicons/shield-exclamation.svg';
import u1144 from '../../../../../public/img/icons/unicons/plus-square.svg';
import u1145 from '../../../../../public/img/icons/unicons/x.svg';
import u1146 from '../../../../../public/img/icons/unicons/capture.svg';
import u1147 from '../../../../../public/img/icons/custom/gf-grid.svg';
import u1148 from '../../../../../public/img/icons/custom/gf-landscape.svg';
import u1149 from '../../../../../public/img/icons/custom/gf-layout-simple.svg';
import u1150 from '../../../../../public/img/icons/custom/gf-portrait.svg';
import u1151 from '../../../../../public/img/icons/custom/gf-bar-alignment-after.svg';
import u1152 from '../../../../../public/img/icons/custom/gf-bar-alignment-before.svg';
import u1153 from '../../../../../public/img/icons/custom/gf-bar-alignment-center.svg';
import u1154 from '../../../../../public/img/icons/custom/gf-interpolation-linear.svg';
import u1155 from '../../../../../public/img/icons/custom/gf-interpolation-smooth.svg';
import u1156 from '../../../../../public/img/icons/custom/gf-interpolation-step-after.svg';
import u1157 from '../../../../../public/img/icons/custom/gf-interpolation-step-before.svg';
import u1158 from '../../../../../public/img/icons/custom/gf-logs.svg';
import u1159 from '../../../../../public/img/icons/custom/gf-movepane-left.svg';
import u1160 from '../../../../../public/img/icons/custom/gf-movepane-right.svg';
import u1161 from '../../../../../public/img/icons/mono/favorite.svg';
import u1162 from '../../../../../public/img/icons/mono/grafana.svg';
import u1163 from '../../../../../public/img/icons/mono/heart.svg';
import u1164 from '../../../../../public/img/icons/mono/heart-break.svg';
import u1165 from '../../../../../public/img/icons/mono/panel-add.svg';
import u1166 from '../../../../../public/img/icons/mono/library-panel.svg';
import u1167 from '../../../../../public/img/icons/unicons/record-audio.svg';
import u1002 from '../../../../../public/img/icons/unicons/align-left.svg';
import u1003 from '../../../../../public/img/icons/unicons/align-right.svg';
import u1004 from '../../../../../public/img/icons/unicons/angle-double-down.svg';
import u1005 from '../../../../../public/img/icons/unicons/angle-double-right.svg';
import u1006 from '../../../../../public/img/icons/unicons/angle-down.svg';
import u1007 from '../../../../../public/img/icons/unicons/angle-left.svg';
import u1008 from '../../../../../public/img/icons/unicons/angle-right.svg';
import u1009 from '../../../../../public/img/icons/unicons/angle-up.svg';
import u1010 from '../../../../../public/img/icons/unicons/apps.svg';
import u1011 from '../../../../../public/img/icons/unicons/arrow.svg';
import u1012 from '../../../../../public/img/icons/unicons/arrow-down.svg';
import u1013 from '../../../../../public/img/icons/unicons/arrow-from-right.svg';
import u1014 from '../../../../../public/img/icons/unicons/arrow-left.svg';
import u1015 from '../../../../../public/img/icons/unicons/arrow-random.svg';
import u1016 from '../../../../../public/img/icons/unicons/arrow-right.svg';
import u1017 from '../../../../../public/img/icons/unicons/arrow-to-right.svg';
import u1018 from '../../../../../public/img/icons/unicons/arrow-up.svg';
import u1019 from '../../../../../public/img/icons/unicons/arrows-h.svg';
import u1020 from '../../../../../public/img/icons/unicons/backward.svg';
import u1021 from '../../../../../public/img/icons/unicons/bars.svg';
import u1022 from '../../../../../public/img/icons/unicons/bell.svg';
import u1023 from '../../../../../public/img/icons/unicons/bell-slash.svg';
import u1024 from '../../../../../public/img/icons/unicons/bolt.svg';
import u1025 from '../../../../../public/img/icons/unicons/book.svg';
import u1026 from '../../../../../public/img/icons/unicons/book-open.svg';
import u1027 from '../../../../../public/img/icons/unicons/brackets-curly.svg';
import u1028 from '../../../../../public/img/icons/unicons/bug.svg';
import u1029 from '../../../../../public/img/icons/unicons/building.svg';
import u1030 from '../../../../../public/img/icons/unicons/calculator-alt.svg';
import u1031 from '../../../../../public/img/icons/unicons/calendar-alt.svg';
import u1032 from '../../../../../public/img/icons/unicons/calendar-slash.svg';
import u1033 from '../../../../../public/img/icons/unicons/camera.svg';
import u1034 from '../../../../../public/img/icons/unicons/channel-add.svg';
import u1035 from '../../../../../public/img/icons/unicons/chart-line.svg';
import u1036 from '../../../../../public/img/icons/unicons/check.svg';
import u1037 from '../../../../../public/img/icons/unicons/check-circle.svg';
import u1038 from '../../../../../public/img/icons/unicons/circle.svg';
import u1039 from '../../../../../public/img/icons/unicons/clipboard-alt.svg';
import u1040 from '../../../../../public/img/icons/unicons/clock-nine.svg';
import u1041 from '../../../../../public/img/icons/unicons/cloud.svg';
import u1042 from '../../../../../public/img/icons/unicons/cloud-download.svg';
import u1043 from '../../../../../public/img/icons/unicons/code-branch.svg';
import u1044 from '../../../../../public/img/icons/unicons/cog.svg';
import u1045 from '../../../../../public/img/icons/unicons/columns.svg';
import u1046 from '../../../../../public/img/icons/unicons/comment-alt.svg';
import u1047 from '../../../../../public/img/icons/unicons/comment-alt-share.svg';
import u1048 from '../../../../../public/img/icons/unicons/comments-alt.svg';
import u1049 from '../../../../../public/img/icons/unicons/compass.svg';
import u1050 from '../../../../../public/img/icons/unicons/copy.svg';
import u1051 from '../../../../../public/img/icons/unicons/corner-down-right-alt.svg';
import u1052 from '../../../../../public/img/icons/unicons/cube.svg';
import u1053 from '../../../../../public/img/icons/unicons/dashboard.svg';
import u1054 from '../../../../../public/img/icons/unicons/database.svg';
import u1055 from '../../../../../public/img/icons/unicons/document-info.svg';
import u1056 from '../../../../../public/img/icons/unicons/download-alt.svg';
import u1057 from '../../../../../public/img/icons/unicons/draggabledots.svg';
import u1058 from '../../../../../public/img/icons/unicons/edit.svg';
import u1059 from '../../../../../public/img/icons/unicons/ellipsis-v.svg';
import u1060 from '../../../../../public/img/icons/unicons/ellipsis-h.svg';
import u1061 from '../../../../../public/img/icons/unicons/envelope.svg';
import u1062 from '../../../../../public/img/icons/unicons/exchange-alt.svg';
import u1063 from '../../../../../public/img/icons/unicons/exclamation-circle.svg';
import u1064 from '../../../../../public/img/icons/unicons/exclamation-triangle.svg';
import u1065 from '../../../../../public/img/icons/unicons/external-link-alt.svg';
import u1066 from '../../../../../public/img/icons/unicons/eye.svg';
import u1067 from '../../../../../public/img/icons/unicons/eye-slash.svg';
import u1068 from '../../../../../public/img/icons/unicons/file-alt.svg';
import u1069 from '../../../../../public/img/icons/unicons/file-blank.svg';
import u1070 from '../../../../../public/img/icons/unicons/filter.svg';
import u1071 from '../../../../../public/img/icons/unicons/folder.svg';
import u1072 from '../../../../../public/img/icons/unicons/folder-open.svg';
import u1073 from '../../../../../public/img/icons/unicons/folder-plus.svg';
import u1074 from '../../../../../public/img/icons/unicons/folder-upload.svg';
import u1075 from '../../../../../public/img/icons/unicons/forward.svg';
import u1076 from '../../../../../public/img/icons/unicons/graph-bar.svg';
import u1077 from '../../../../../public/img/icons/unicons/history.svg';
import u1078 from '../../../../../public/img/icons/unicons/history-alt.svg';
import u1079 from '../../../../../public/img/icons/unicons/home-alt.svg';
import u1080 from '../../../../../public/img/icons/unicons/import.svg';
import u1081 from '../../../../../public/img/icons/unicons/info.svg';
import u1082 from '../../../../../public/img/icons/unicons/info-circle.svg';
import u1083 from '../../../../../public/img/icons/unicons/k6.svg';
import u1084 from '../../../../../public/img/icons/unicons/key-skeleton-alt.svg';
import u1085 from '../../../../../public/img/icons/unicons/keyboard.svg';
import u1086 from '../../../../../public/img/icons/unicons/link.svg';
import u1087 from '../../../../../public/img/icons/unicons/list-ul.svg';
import u1088 from '../../../../../public/img/icons/unicons/lock.svg';
import u1089 from '../../../../../public/img/icons/unicons/minus.svg';
import u1090 from '../../../../../public/img/icons/unicons/minus-circle.svg';
import u1091 from '../../../../../public/img/icons/unicons/mobile-android.svg';
import u1092 from '../../../../../public/img/icons/unicons/monitor.svg';
import u1093 from '../../../../../public/img/icons/unicons/pause.svg';
import u1094 from '../../../../../public/img/icons/unicons/pen.svg';
import u1095 from '../../../../../public/img/icons/unicons/play.svg';
import u1096 from '../../../../../public/img/icons/unicons/plug.svg';
import u1097 from '../../../../../public/img/icons/unicons/plus.svg';
import u1098 from '../../../../../public/img/icons/unicons/plus-circle.svg';
import u1099 from '../../../../../public/img/icons/unicons/power.svg';
import u1100 from '../../../../../public/img/icons/unicons/presentation-play.svg';
import u1101 from '../../../../../public/img/icons/unicons/process.svg';
import u1102 from '../../../../../public/img/icons/unicons/question-circle.svg';
import u1103 from '../../../../../public/img/icons/unicons/repeat.svg';
import u1104 from '../../../../../public/img/icons/unicons/rocket.svg';
import u1105 from '../../../../../public/img/icons/unicons/rss.svg';
import u1106 from '../../../../../public/img/icons/unicons/save.svg';
import u1107 from '../../../../../public/img/icons/unicons/search.svg';
import u1108 from '../../../../../public/img/icons/unicons/search-minus.svg';
import u1109 from '../../../../../public/img/icons/unicons/search-plus.svg';
import u1110 from '../../../../../public/img/icons/unicons/share-alt.svg';
import u1111 from '../../../../../public/img/icons/unicons/shield.svg';
import u1112 from '../../../../../public/img/icons/unicons/signal.svg';
import u1113 from '../../../../../public/img/icons/unicons/signin.svg';
import u1114 from '../../../../../public/img/icons/unicons/signout.svg';
import u1115 from '../../../../../public/img/icons/unicons/sitemap.svg';
import u1116 from '../../../../../public/img/icons/unicons/slack.svg';
import u1117 from '../../../../../public/img/icons/unicons/sliders-v-alt.svg';
import u1118 from '../../../../../public/img/icons/unicons/sort-amount-down.svg';
import u1119 from '../../../../../public/img/icons/unicons/sort-amount-up.svg';
import u1120 from '../../../../../public/img/icons/unicons/square-shape.svg';
import u1121 from '../../../../../public/img/icons/unicons/star.svg';
import u1122 from '../../../../../public/img/icons/unicons/step-backward.svg';
import u1123 from '../../../../../public/img/icons/unicons/sync.svg';
import u1124 from '../../../../../public/img/icons/unicons/stopwatch.svg';
import u1125 from '../../../../../public/img/icons/unicons/table.svg';
import u1126 from '../../../../../public/img/icons/unicons/tag-alt.svg';
import u1127 from '../../../../../public/img/icons/unicons/times.svg';
import u1128 from '../../../../../public/img/icons/unicons/trash-alt.svg';
import u1129 from '../../../../../public/img/icons/unicons/unlock.svg';
import u1130 from '../../../../../public/img/icons/unicons/upload.svg';
import u1131 from '../../../../../public/img/icons/unicons/user.svg';
import u1132 from '../../../../../public/img/icons/unicons/users-alt.svg';
import u1133 from '../../../../../public/img/icons/unicons/wrap-text.svg';
import u1134 from '../../../../../public/img/icons/unicons/cloud-upload.svg';
import u1135 from '../../../../../public/img/icons/unicons/credit-card.svg';
import u1136 from '../../../../../public/img/icons/unicons/file-copy-alt.svg';
import u1137 from '../../../../../public/img/icons/unicons/fire.svg';
import u1138 from '../../../../../public/img/icons/unicons/hourglass.svg';
import u1139 from '../../../../../public/img/icons/unicons/layer-group.svg';
import u1140 from '../../../../../public/img/icons/unicons/layers-alt.svg';
import u1141 from '../../../../../public/img/icons/unicons/line-alt.svg';
import u1142 from '../../../../../public/img/icons/unicons/list-ui-alt.svg';
import u1143 from '../../../../../public/img/icons/unicons/message.svg';
import u1144 from '../../../../../public/img/icons/unicons/palette.svg';
import u1145 from '../../../../../public/img/icons/unicons/percentage.svg';
import u1146 from '../../../../../public/img/icons/unicons/shield-exclamation.svg';
import u1147 from '../../../../../public/img/icons/unicons/plus-square.svg';
import u1148 from '../../../../../public/img/icons/unicons/x.svg';
import u1149 from '../../../../../public/img/icons/unicons/capture.svg';
import u1150 from '../../../../../public/img/icons/custom/gf-grid.svg';
import u1151 from '../../../../../public/img/icons/custom/gf-landscape.svg';
import u1152 from '../../../../../public/img/icons/custom/gf-layout-simple.svg';
import u1153 from '../../../../../public/img/icons/custom/gf-portrait.svg';
import u1154 from '../../../../../public/img/icons/custom/gf-show-context.svg';
import u1155 from '../../../../../public/img/icons/custom/gf-bar-alignment-after.svg';
import u1156 from '../../../../../public/img/icons/custom/gf-bar-alignment-before.svg';
import u1157 from '../../../../../public/img/icons/custom/gf-bar-alignment-center.svg';
import u1158 from '../../../../../public/img/icons/custom/gf-interpolation-linear.svg';
import u1159 from '../../../../../public/img/icons/custom/gf-interpolation-smooth.svg';
import u1160 from '../../../../../public/img/icons/custom/gf-interpolation-step-after.svg';
import u1161 from '../../../../../public/img/icons/custom/gf-interpolation-step-before.svg';
import u1162 from '../../../../../public/img/icons/custom/gf-logs.svg';
import u1163 from '../../../../../public/img/icons/custom/gf-movepane-left.svg';
import u1164 from '../../../../../public/img/icons/custom/gf-movepane-right.svg';
import u1165 from '../../../../../public/img/icons/mono/favorite.svg';
import u1166 from '../../../../../public/img/icons/mono/grafana.svg';
import u1167 from '../../../../../public/img/icons/mono/heart.svg';
import u1168 from '../../../../../public/img/icons/mono/heart-break.svg';
import u1169 from '../../../../../public/img/icons/mono/panel-add.svg';
import u1170 from '../../../../../public/img/icons/mono/library-panel.svg';
import u1171 from '../../../../../public/img/icons/unicons/record-audio.svg';
// do not edit this list directly
// the list of icons live here: @grafana/ui/components/Icon/cached.json
@ -199,172 +203,176 @@ export function initIconCache() {
// the list of icons live here: @grafana/ui/components/Icon/cached.json
cacheItem(u1000, 'unicons/at.svg');
cacheItem(u1001, 'unicons/adjust-circle.svg');
cacheItem(u1002, 'unicons/angle-double-down.svg');
cacheItem(u1003, 'unicons/angle-double-right.svg');
cacheItem(u1004, 'unicons/angle-down.svg');
cacheItem(u1005, 'unicons/angle-left.svg');
cacheItem(u1006, 'unicons/angle-right.svg');
cacheItem(u1007, 'unicons/angle-up.svg');
cacheItem(u1008, 'unicons/apps.svg');
cacheItem(u1009, 'unicons/arrow.svg');
cacheItem(u1010, 'unicons/arrow-down.svg');
cacheItem(u1011, 'unicons/arrow-from-right.svg');
cacheItem(u1012, 'unicons/arrow-left.svg');
cacheItem(u1013, 'unicons/arrow-random.svg');
cacheItem(u1014, 'unicons/arrow-right.svg');
cacheItem(u1015, 'unicons/arrow-to-right.svg');
cacheItem(u1016, 'unicons/arrow-up.svg');
cacheItem(u1017, 'unicons/arrows-h.svg');
cacheItem(u1018, 'unicons/backward.svg');
cacheItem(u1019, 'unicons/bars.svg');
cacheItem(u1020, 'unicons/bell.svg');
cacheItem(u1021, 'unicons/bell-slash.svg');
cacheItem(u1022, 'unicons/bolt.svg');
cacheItem(u1023, 'unicons/book.svg');
cacheItem(u1024, 'unicons/book-open.svg');
cacheItem(u1025, 'unicons/brackets-curly.svg');
cacheItem(u1026, 'unicons/bug.svg');
cacheItem(u1027, 'unicons/building.svg');
cacheItem(u1028, 'unicons/calculator-alt.svg');
cacheItem(u1029, 'unicons/calendar-alt.svg');
cacheItem(u1030, 'unicons/calendar-slash.svg');
cacheItem(u1031, 'unicons/camera.svg');
cacheItem(u1032, 'unicons/channel-add.svg');
cacheItem(u1033, 'unicons/chart-line.svg');
cacheItem(u1034, 'unicons/check.svg');
cacheItem(u1035, 'unicons/check-circle.svg');
cacheItem(u1036, 'unicons/circle.svg');
cacheItem(u1037, 'unicons/clipboard-alt.svg');
cacheItem(u1038, 'unicons/clock-nine.svg');
cacheItem(u1039, 'unicons/cloud.svg');
cacheItem(u1040, 'unicons/cloud-download.svg');
cacheItem(u1041, 'unicons/code-branch.svg');
cacheItem(u1042, 'unicons/cog.svg');
cacheItem(u1043, 'unicons/columns.svg');
cacheItem(u1044, 'unicons/comment-alt.svg');
cacheItem(u1045, 'unicons/comment-alt-share.svg');
cacheItem(u1046, 'unicons/comments-alt.svg');
cacheItem(u1047, 'unicons/compass.svg');
cacheItem(u1048, 'unicons/copy.svg');
cacheItem(u1049, 'unicons/corner-down-right-alt.svg');
cacheItem(u1050, 'unicons/cube.svg');
cacheItem(u1051, 'unicons/dashboard.svg');
cacheItem(u1052, 'unicons/database.svg');
cacheItem(u1053, 'unicons/document-info.svg');
cacheItem(u1054, 'unicons/download-alt.svg');
cacheItem(u1055, 'unicons/draggabledots.svg');
cacheItem(u1056, 'unicons/edit.svg');
cacheItem(u1057, 'unicons/ellipsis-v.svg');
cacheItem(u1058, 'unicons/ellipsis-h.svg');
cacheItem(u1059, 'unicons/envelope.svg');
cacheItem(u1060, 'unicons/exchange-alt.svg');
cacheItem(u1061, 'unicons/exclamation-circle.svg');
cacheItem(u1062, 'unicons/exclamation-triangle.svg');
cacheItem(u1063, 'unicons/external-link-alt.svg');
cacheItem(u1064, 'unicons/eye.svg');
cacheItem(u1065, 'unicons/eye-slash.svg');
cacheItem(u1066, 'unicons/file-alt.svg');
cacheItem(u1067, 'unicons/file-blank.svg');
cacheItem(u1068, 'unicons/filter.svg');
cacheItem(u1069, 'unicons/folder.svg');
cacheItem(u1070, 'unicons/folder-open.svg');
cacheItem(u1071, 'unicons/folder-plus.svg');
cacheItem(u1072, 'unicons/folder-upload.svg');
cacheItem(u1073, 'unicons/forward.svg');
cacheItem(u1074, 'unicons/graph-bar.svg');
cacheItem(u1075, 'unicons/history.svg');
cacheItem(u1076, 'unicons/home-alt.svg');
cacheItem(u1077, 'unicons/import.svg');
cacheItem(u1078, 'unicons/info.svg');
cacheItem(u1079, 'unicons/info-circle.svg');
cacheItem(u1080, 'unicons/k6.svg');
cacheItem(u1081, 'unicons/key-skeleton-alt.svg');
cacheItem(u1082, 'unicons/keyboard.svg');
cacheItem(u1083, 'unicons/link.svg');
cacheItem(u1084, 'unicons/list-ul.svg');
cacheItem(u1085, 'unicons/lock.svg');
cacheItem(u1086, 'unicons/minus.svg');
cacheItem(u1087, 'unicons/minus-circle.svg');
cacheItem(u1088, 'unicons/mobile-android.svg');
cacheItem(u1089, 'unicons/monitor.svg');
cacheItem(u1090, 'unicons/pause.svg');
cacheItem(u1091, 'unicons/pen.svg');
cacheItem(u1092, 'unicons/play.svg');
cacheItem(u1093, 'unicons/plug.svg');
cacheItem(u1094, 'unicons/plus.svg');
cacheItem(u1095, 'unicons/plus-circle.svg');
cacheItem(u1096, 'unicons/power.svg');
cacheItem(u1097, 'unicons/presentation-play.svg');
cacheItem(u1098, 'unicons/process.svg');
cacheItem(u1099, 'unicons/question-circle.svg');
cacheItem(u1100, 'unicons/repeat.svg');
cacheItem(u1101, 'unicons/rocket.svg');
cacheItem(u1102, 'unicons/rss.svg');
cacheItem(u1103, 'unicons/save.svg');
cacheItem(u1104, 'unicons/search.svg');
cacheItem(u1105, 'unicons/search-minus.svg');
cacheItem(u1106, 'unicons/search-plus.svg');
cacheItem(u1107, 'unicons/share-alt.svg');
cacheItem(u1108, 'unicons/shield.svg');
cacheItem(u1109, 'unicons/signal.svg');
cacheItem(u1110, 'unicons/signin.svg');
cacheItem(u1111, 'unicons/signout.svg');
cacheItem(u1112, 'unicons/sitemap.svg');
cacheItem(u1113, 'unicons/slack.svg');
cacheItem(u1114, 'unicons/sliders-v-alt.svg');
cacheItem(u1115, 'unicons/sort-amount-down.svg');
cacheItem(u1116, 'unicons/sort-amount-up.svg');
cacheItem(u1117, 'unicons/square-shape.svg');
cacheItem(u1118, 'unicons/star.svg');
cacheItem(u1119, 'unicons/step-backward.svg');
cacheItem(u1120, 'unicons/sync.svg');
cacheItem(u1121, 'unicons/stopwatch.svg');
cacheItem(u1122, 'unicons/table.svg');
cacheItem(u1123, 'unicons/tag-alt.svg');
cacheItem(u1124, 'unicons/times.svg');
cacheItem(u1125, 'unicons/trash-alt.svg');
cacheItem(u1126, 'unicons/unlock.svg');
cacheItem(u1127, 'unicons/upload.svg');
cacheItem(u1128, 'unicons/user.svg');
cacheItem(u1129, 'unicons/users-alt.svg');
cacheItem(u1130, 'unicons/wrap-text.svg');
cacheItem(u1131, 'unicons/cloud-upload.svg');
cacheItem(u1132, 'unicons/credit-card.svg');
cacheItem(u1133, 'unicons/file-copy-alt.svg');
cacheItem(u1134, 'unicons/fire.svg');
cacheItem(u1135, 'unicons/hourglass.svg');
cacheItem(u1136, 'unicons/layer-group.svg');
cacheItem(u1137, 'unicons/layers-alt.svg');
cacheItem(u1138, 'unicons/line-alt.svg');
cacheItem(u1139, 'unicons/list-ui-alt.svg');
cacheItem(u1140, 'unicons/message.svg');
cacheItem(u1141, 'unicons/palette.svg');
cacheItem(u1142, 'unicons/percentage.svg');
cacheItem(u1143, 'unicons/shield-exclamation.svg');
cacheItem(u1144, 'unicons/plus-square.svg');
cacheItem(u1145, 'unicons/x.svg');
cacheItem(u1146, 'unicons/capture.svg');
cacheItem(u1147, 'custom/gf-grid.svg');
cacheItem(u1148, 'custom/gf-landscape.svg');
cacheItem(u1149, 'custom/gf-layout-simple.svg');
cacheItem(u1150, 'custom/gf-portrait.svg');
cacheItem(u1151, 'custom/gf-bar-alignment-after.svg');
cacheItem(u1152, 'custom/gf-bar-alignment-before.svg');
cacheItem(u1153, 'custom/gf-bar-alignment-center.svg');
cacheItem(u1154, 'custom/gf-interpolation-linear.svg');
cacheItem(u1155, 'custom/gf-interpolation-smooth.svg');
cacheItem(u1156, 'custom/gf-interpolation-step-after.svg');
cacheItem(u1157, 'custom/gf-interpolation-step-before.svg');
cacheItem(u1158, 'custom/gf-logs.svg');
cacheItem(u1159, 'custom/gf-movepane-left.svg');
cacheItem(u1160, 'custom/gf-movepane-right.svg');
cacheItem(u1161, 'mono/favorite.svg');
cacheItem(u1162, 'mono/grafana.svg');
cacheItem(u1163, 'mono/heart.svg');
cacheItem(u1164, 'mono/heart-break.svg');
cacheItem(u1165, 'mono/panel-add.svg');
cacheItem(u1166, 'mono/library-panel.svg');
cacheItem(u1167, 'unicons/record-audio.svg');
cacheItem(u1002, 'unicons/align-left.svg');
cacheItem(u1003, 'unicons/align-right.svg');
cacheItem(u1004, 'unicons/angle-double-down.svg');
cacheItem(u1005, 'unicons/angle-double-right.svg');
cacheItem(u1006, 'unicons/angle-down.svg');
cacheItem(u1007, 'unicons/angle-left.svg');
cacheItem(u1008, 'unicons/angle-right.svg');
cacheItem(u1009, 'unicons/angle-up.svg');
cacheItem(u1010, 'unicons/apps.svg');
cacheItem(u1011, 'unicons/arrow.svg');
cacheItem(u1012, 'unicons/arrow-down.svg');
cacheItem(u1013, 'unicons/arrow-from-right.svg');
cacheItem(u1014, 'unicons/arrow-left.svg');
cacheItem(u1015, 'unicons/arrow-random.svg');
cacheItem(u1016, 'unicons/arrow-right.svg');
cacheItem(u1017, 'unicons/arrow-to-right.svg');
cacheItem(u1018, 'unicons/arrow-up.svg');
cacheItem(u1019, 'unicons/arrows-h.svg');
cacheItem(u1020, 'unicons/backward.svg');
cacheItem(u1021, 'unicons/bars.svg');
cacheItem(u1022, 'unicons/bell.svg');
cacheItem(u1023, 'unicons/bell-slash.svg');
cacheItem(u1024, 'unicons/bolt.svg');
cacheItem(u1025, 'unicons/book.svg');
cacheItem(u1026, 'unicons/book-open.svg');
cacheItem(u1027, 'unicons/brackets-curly.svg');
cacheItem(u1028, 'unicons/bug.svg');
cacheItem(u1029, 'unicons/building.svg');
cacheItem(u1030, 'unicons/calculator-alt.svg');
cacheItem(u1031, 'unicons/calendar-alt.svg');
cacheItem(u1032, 'unicons/calendar-slash.svg');
cacheItem(u1033, 'unicons/camera.svg');
cacheItem(u1034, 'unicons/channel-add.svg');
cacheItem(u1035, 'unicons/chart-line.svg');
cacheItem(u1036, 'unicons/check.svg');
cacheItem(u1037, 'unicons/check-circle.svg');
cacheItem(u1038, 'unicons/circle.svg');
cacheItem(u1039, 'unicons/clipboard-alt.svg');
cacheItem(u1040, 'unicons/clock-nine.svg');
cacheItem(u1041, 'unicons/cloud.svg');
cacheItem(u1042, 'unicons/cloud-download.svg');
cacheItem(u1043, 'unicons/code-branch.svg');
cacheItem(u1044, 'unicons/cog.svg');
cacheItem(u1045, 'unicons/columns.svg');
cacheItem(u1046, 'unicons/comment-alt.svg');
cacheItem(u1047, 'unicons/comment-alt-share.svg');
cacheItem(u1048, 'unicons/comments-alt.svg');
cacheItem(u1049, 'unicons/compass.svg');
cacheItem(u1050, 'unicons/copy.svg');
cacheItem(u1051, 'unicons/corner-down-right-alt.svg');
cacheItem(u1052, 'unicons/cube.svg');
cacheItem(u1053, 'unicons/dashboard.svg');
cacheItem(u1054, 'unicons/database.svg');
cacheItem(u1055, 'unicons/document-info.svg');
cacheItem(u1056, 'unicons/download-alt.svg');
cacheItem(u1057, 'unicons/draggabledots.svg');
cacheItem(u1058, 'unicons/edit.svg');
cacheItem(u1059, 'unicons/ellipsis-v.svg');
cacheItem(u1060, 'unicons/ellipsis-h.svg');
cacheItem(u1061, 'unicons/envelope.svg');
cacheItem(u1062, 'unicons/exchange-alt.svg');
cacheItem(u1063, 'unicons/exclamation-circle.svg');
cacheItem(u1064, 'unicons/exclamation-triangle.svg');
cacheItem(u1065, 'unicons/external-link-alt.svg');
cacheItem(u1066, 'unicons/eye.svg');
cacheItem(u1067, 'unicons/eye-slash.svg');
cacheItem(u1068, 'unicons/file-alt.svg');
cacheItem(u1069, 'unicons/file-blank.svg');
cacheItem(u1070, 'unicons/filter.svg');
cacheItem(u1071, 'unicons/folder.svg');
cacheItem(u1072, 'unicons/folder-open.svg');
cacheItem(u1073, 'unicons/folder-plus.svg');
cacheItem(u1074, 'unicons/folder-upload.svg');
cacheItem(u1075, 'unicons/forward.svg');
cacheItem(u1076, 'unicons/graph-bar.svg');
cacheItem(u1077, 'unicons/history.svg');
cacheItem(u1078, 'unicons/history-alt.svg');
cacheItem(u1079, 'unicons/home-alt.svg');
cacheItem(u1080, 'unicons/import.svg');
cacheItem(u1081, 'unicons/info.svg');
cacheItem(u1082, 'unicons/info-circle.svg');
cacheItem(u1083, 'unicons/k6.svg');
cacheItem(u1084, 'unicons/key-skeleton-alt.svg');
cacheItem(u1085, 'unicons/keyboard.svg');
cacheItem(u1086, 'unicons/link.svg');
cacheItem(u1087, 'unicons/list-ul.svg');
cacheItem(u1088, 'unicons/lock.svg');
cacheItem(u1089, 'unicons/minus.svg');
cacheItem(u1090, 'unicons/minus-circle.svg');
cacheItem(u1091, 'unicons/mobile-android.svg');
cacheItem(u1092, 'unicons/monitor.svg');
cacheItem(u1093, 'unicons/pause.svg');
cacheItem(u1094, 'unicons/pen.svg');
cacheItem(u1095, 'unicons/play.svg');
cacheItem(u1096, 'unicons/plug.svg');
cacheItem(u1097, 'unicons/plus.svg');
cacheItem(u1098, 'unicons/plus-circle.svg');
cacheItem(u1099, 'unicons/power.svg');
cacheItem(u1100, 'unicons/presentation-play.svg');
cacheItem(u1101, 'unicons/process.svg');
cacheItem(u1102, 'unicons/question-circle.svg');
cacheItem(u1103, 'unicons/repeat.svg');
cacheItem(u1104, 'unicons/rocket.svg');
cacheItem(u1105, 'unicons/rss.svg');
cacheItem(u1106, 'unicons/save.svg');
cacheItem(u1107, 'unicons/search.svg');
cacheItem(u1108, 'unicons/search-minus.svg');
cacheItem(u1109, 'unicons/search-plus.svg');
cacheItem(u1110, 'unicons/share-alt.svg');
cacheItem(u1111, 'unicons/shield.svg');
cacheItem(u1112, 'unicons/signal.svg');
cacheItem(u1113, 'unicons/signin.svg');
cacheItem(u1114, 'unicons/signout.svg');
cacheItem(u1115, 'unicons/sitemap.svg');
cacheItem(u1116, 'unicons/slack.svg');
cacheItem(u1117, 'unicons/sliders-v-alt.svg');
cacheItem(u1118, 'unicons/sort-amount-down.svg');
cacheItem(u1119, 'unicons/sort-amount-up.svg');
cacheItem(u1120, 'unicons/square-shape.svg');
cacheItem(u1121, 'unicons/star.svg');
cacheItem(u1122, 'unicons/step-backward.svg');
cacheItem(u1123, 'unicons/sync.svg');
cacheItem(u1124, 'unicons/stopwatch.svg');
cacheItem(u1125, 'unicons/table.svg');
cacheItem(u1126, 'unicons/tag-alt.svg');
cacheItem(u1127, 'unicons/times.svg');
cacheItem(u1128, 'unicons/trash-alt.svg');
cacheItem(u1129, 'unicons/unlock.svg');
cacheItem(u1130, 'unicons/upload.svg');
cacheItem(u1131, 'unicons/user.svg');
cacheItem(u1132, 'unicons/users-alt.svg');
cacheItem(u1133, 'unicons/wrap-text.svg');
cacheItem(u1134, 'unicons/cloud-upload.svg');
cacheItem(u1135, 'unicons/credit-card.svg');
cacheItem(u1136, 'unicons/file-copy-alt.svg');
cacheItem(u1137, 'unicons/fire.svg');
cacheItem(u1138, 'unicons/hourglass.svg');
cacheItem(u1139, 'unicons/layer-group.svg');
cacheItem(u1140, 'unicons/layers-alt.svg');
cacheItem(u1141, 'unicons/line-alt.svg');
cacheItem(u1142, 'unicons/list-ui-alt.svg');
cacheItem(u1143, 'unicons/message.svg');
cacheItem(u1144, 'unicons/palette.svg');
cacheItem(u1145, 'unicons/percentage.svg');
cacheItem(u1146, 'unicons/shield-exclamation.svg');
cacheItem(u1147, 'unicons/plus-square.svg');
cacheItem(u1148, 'unicons/x.svg');
cacheItem(u1149, 'unicons/capture.svg');
cacheItem(u1150, 'custom/gf-grid.svg');
cacheItem(u1151, 'custom/gf-landscape.svg');
cacheItem(u1152, 'custom/gf-layout-simple.svg');
cacheItem(u1153, 'custom/gf-portrait.svg');
cacheItem(u1154, 'custom/gf-show-context.svg');
cacheItem(u1155, 'custom/gf-bar-alignment-after.svg');
cacheItem(u1156, 'custom/gf-bar-alignment-before.svg');
cacheItem(u1157, 'custom/gf-bar-alignment-center.svg');
cacheItem(u1158, 'custom/gf-interpolation-linear.svg');
cacheItem(u1159, 'custom/gf-interpolation-smooth.svg');
cacheItem(u1160, 'custom/gf-interpolation-step-after.svg');
cacheItem(u1161, 'custom/gf-interpolation-step-before.svg');
cacheItem(u1162, 'custom/gf-logs.svg');
cacheItem(u1163, 'custom/gf-movepane-left.svg');
cacheItem(u1164, 'custom/gf-movepane-right.svg');
cacheItem(u1165, 'mono/favorite.svg');
cacheItem(u1166, 'mono/grafana.svg');
cacheItem(u1167, 'mono/heart.svg');
cacheItem(u1168, 'mono/heart-break.svg');
cacheItem(u1169, 'mono/panel-add.svg');
cacheItem(u1170, 'mono/library-panel.svg');
cacheItem(u1171, 'unicons/record-audio.svg');
// do not edit this list directly
// the list of icons live here: @grafana/ui/components/Icon/cached.json
}

View File

@ -7,7 +7,18 @@ import * as schema from '@grafana/schema';
import { TableStyles } from './styles';
export { type FieldTextAlignment, TableCellBackgroundDisplayMode, TableCellDisplayMode } from '@grafana/schema';
export {
type FieldTextAlignment,
TableCellBackgroundDisplayMode,
TableCellDisplayMode,
type TableAutoCellOptions,
type TableSparklineCellOptions,
type TableBarGaugeCellOptions,
type TableColoredBackgroundCellOptions,
type TableColorTextCellOptions,
type TableImageCellOptions,
type TableJsonViewCellOptions,
} from '@grafana/schema';
export interface TableRow {
[x: string]: any;

View File

@ -90,6 +90,13 @@ export {
type TableSortByFieldState,
type TableFooterCalc,
type AdHocFilterItem,
type TableAutoCellOptions,
type TableSparklineCellOptions,
type TableBarGaugeCellOptions,
type TableColoredBackgroundCellOptions,
type TableColorTextCellOptions,
type TableImageCellOptions,
type TableJsonViewCellOptions,
} from './Table/types';
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
export { TabsBar } from './Tabs/TabsBar';

View File

@ -25,6 +25,7 @@ var packages = []string{
"@grafana/e2e",
"@grafana/e2e-selectors",
"@grafana/schema",
"@grafana/flamegraph",
}
// PublishNpmPackages will publish local NPM packages to NPM registry.

View File

@ -2,19 +2,36 @@ import { css } from '@emotion/css';
import React from 'react';
import { DataFrame, GrafanaTheme2, CoreApp } from '@grafana/data';
import { FlameGraph } from '@grafana/flamegraph';
import { reportInteraction, config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import FlameGraphContainer from 'app/plugins/panel/flamegraph/components/FlameGraphContainer';
interface Props {
dataFrames: DataFrame[];
}
function interaction(name: string, context: Record<string, string | number> = {}) {
reportInteraction(`grafana_flamegraph_${name}`, {
app: CoreApp.Unknown,
grafana_version: config.buildInfo.version,
...context,
});
}
export const FlameGraphExploreContainer = (props: Props) => {
const styles = useStyles2((theme) => getStyles(theme));
return (
<div className={styles.container}>
<FlameGraphContainer data={props.dataFrames[0]} app={CoreApp.Explore} />
<FlameGraph
data={props.dataFrames[0]}
stickyHeader={true}
getTheme={() => config.theme2}
onTableSymbolClick={() => interaction('table_item_selected')}
onViewSelected={(view: string) => interaction('view_selected', { view })}
onTextAlignSelected={(align: string) => interaction('text_align_selected', { align })}
onTableSort={(sort: string) => interaction('table_sort_selected', { sort })}
/>
</div>
);
};

View File

@ -1,10 +1,19 @@
import React from 'react';
import { CoreApp, PanelProps } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { FlameGraph } from '@grafana/flamegraph';
import { PanelDataErrorView, reportInteraction, config } from '@grafana/runtime';
import { checkFields, getMessageCheckFieldsResult } from './components/FlameGraph/dataTransform';
import FlameGraphContainer from './components/FlameGraphContainer';
// import FlameGraphContainer from './components/FlameGraphContainer';
function interaction(name: string, context: Record<string, string | number> = {}) {
reportInteraction(`grafana_flamegraph_${name}`, {
app: CoreApp.Unknown,
grafana_version: config.buildInfo.version,
...context,
});
}
export const FlameGraphPanel = (props: PanelProps) => {
const wrongFields = checkFields(props.data.series[0]);
@ -13,5 +22,16 @@ export const FlameGraphPanel = (props: PanelProps) => {
<PanelDataErrorView panelId={props.id} data={props.data} message={getMessageCheckFieldsResult(wrongFields)} />
);
}
return <FlameGraphContainer data={props.data.series[0]} app={CoreApp.Unknown} />;
return (
<FlameGraph
data={props.data.series[0]}
stickyHeader={false}
getTheme={() => config.theme2}
onTableSymbolClick={() => interaction('table_item_selected')}
onViewSelected={(view: string) => interaction('view_selected', { view })}
onTextAlignSelected={(align: string) => interaction('text_align_selected', { align })}
onTableSort={(sort: string) => interaction('table_sort_selected', { sort })}
/>
);
};

View File

@ -244,7 +244,7 @@ type ActionCellProps = {
function ActionCell(props: ActionCellProps) {
const styles = useStyles2(getStyles);
const symbol = props.frame.fields.find((f: Field) => f.name === 'Symbol')?.values.get(props.rowIndex);
const symbol = props.frame.fields.find((f: Field) => f.name === 'Symbol')?.values[props.rowIndex];
const isSearched = props.search === symbol;
const isSandwiched = props.sandwichItem === symbol;

865
yarn.lock

File diff suppressed because it is too large Load Diff