mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Flamegraph: Move to package (#73113)
This commit is contained in:
parent
02f617a20d
commit
e4f26a5e4b
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
||||
|
||||
|
@ -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",
|
||||
|
19
packages/grafana-flamegraph/.eslintrc
Normal file
19
packages/grafana-flamegraph/.eslintrc
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
3
packages/grafana-flamegraph/CHANGELOG.md
Normal file
3
packages/grafana-flamegraph/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
# 10.2.0 (2023-09-06)
|
||||
|
||||
First public release
|
202
packages/grafana-flamegraph/LICENSE_APACHE2
Normal file
202
packages/grafana-flamegraph/LICENSE_APACHE2
Normal 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.
|
53
packages/grafana-flamegraph/README.md
Normal file
53
packages/grafana-flamegraph/README.md
Normal 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. |
|
88
packages/grafana-flamegraph/package.json
Normal file
88
packages/grafana-flamegraph/package.json
Normal 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"
|
||||
}
|
||||
}
|
37
packages/grafana-flamegraph/rollup.config.ts
Normal file
37
packages/grafana-flamegraph/rollup.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
@ -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();
|
||||
});
|
||||
});
|
329
packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx
Normal file
329
packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx
Normal 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;
|
@ -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;
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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,
|
||||
};
|
||||
}
|
203
packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx
Normal file
203
packages/grafana-flamegraph/src/FlameGraph/FlameGraphTooltip.tsx
Normal 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
25
packages/grafana-flamegraph/src/FlameGraph/colors.test.ts
Normal file
25
packages/grafana-flamegraph/src/FlameGraph/colors.test.ts
Normal 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%)'
|
||||
);
|
||||
});
|
||||
});
|
133
packages/grafana-flamegraph/src/FlameGraph/colors.ts
Normal file
133
packages/grafana-flamegraph/src/FlameGraph/colors.ts
Normal 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;
|
||||
}
|
@ -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]);
|
||||
});
|
||||
});
|
288
packages/grafana-flamegraph/src/FlameGraph/dataTransform.ts
Normal file
288
packages/grafana-flamegraph/src/FlameGraph/dataTransform.ts
Normal 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);
|
||||
}
|
84
packages/grafana-flamegraph/src/FlameGraph/murmur3.ts
Normal file
84
packages/grafana-flamegraph/src/FlameGraph/murmur3.ts
Normal 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;
|
||||
}
|
88
packages/grafana-flamegraph/src/FlameGraph/rendering.test.ts
Normal file
88
packages/grafana-flamegraph/src/FlameGraph/rendering.test.ts
Normal 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 },
|
||||
]);
|
||||
});
|
||||
});
|
319
packages/grafana-flamegraph/src/FlameGraph/rendering.ts
Normal file
319
packages/grafana-flamegraph/src/FlameGraph/rendering.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
};
|
2466
packages/grafana-flamegraph/src/FlameGraph/testData/dataNestedSet.ts
Normal file
2466
packages/grafana-flamegraph/src/FlameGraph/testData/dataNestedSet.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
});
|
||||
});
|
104
packages/grafana-flamegraph/src/FlameGraph/testHelpers.ts
Normal file
104
packages/grafana-flamegraph/src/FlameGraph/testHelpers.ts
Normal 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');
|
||||
}
|
@ -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]
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
125
packages/grafana-flamegraph/src/FlameGraph/treeTransforms.ts
Normal file
125
packages/grafana-flamegraph/src/FlameGraph/treeTransforms.ts
Normal 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;
|
||||
}
|
90
packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx
Normal file
90
packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
252
packages/grafana-flamegraph/src/FlameGraphContainer.tsx
Normal file
252
packages/grafana-flamegraph/src/FlameGraphContainer.tsx
Normal 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;
|
87
packages/grafana-flamegraph/src/FlameGraphHeader.test.tsx
Normal file
87
packages/grafana-flamegraph/src/FlameGraphHeader.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
299
packages/grafana-flamegraph/src/FlameGraphHeader.tsx
Normal file
299
packages/grafana-flamegraph/src/FlameGraphHeader.tsx
Normal 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;
|
@ -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');
|
||||
});
|
||||
});
|
@ -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;
|
8
packages/grafana-flamegraph/src/constants.ts
Normal file
8
packages/grafana-flamegraph/src/constants.ts
Normal 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;
|
1
packages/grafana-flamegraph/src/index.ts
Normal file
1
packages/grafana-flamegraph/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as FlameGraph, type Props } from './FlameGraphContainer';
|
57
packages/grafana-flamegraph/src/types.ts
Normal file
57
packages/grafana-flamegraph/src/types.ts
Normal 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';
|
4
packages/grafana-flamegraph/src/types/emotion-core-stub.d.ts
vendored
Normal file
4
packages/grafana-flamegraph/src/types/emotion-core-stub.d.ts
vendored
Normal 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 {};
|
9
packages/grafana-flamegraph/tsconfig.build.json
Normal file
9
packages/grafana-flamegraph/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@emotion/core": ["./src/types/emotion-core-stub.d.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
12
packages/grafana-flamegraph/tsconfig.json
Normal file
12
packages/grafana-flamegraph/tsconfig.json
Normal 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"]
|
||||
}
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user