diff --git a/.betterer.results b/.betterer.results index 265b61e70ac..4db1152f170 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5264,6 +5264,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], + "public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/plugins/panel/gauge/GaugeMigrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 3559bb4464e..9d7b628917a 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -107,6 +107,7 @@ Alpha features might be changed or removed without prior notice. | `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | | `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | | `unifiedRequestLog` | Writes error logs to the request logger | +| `pyroscopeFlameGraph` | Changes flame graph to pyroscope one | ## Development feature toggles diff --git a/package.json b/package.json index 44bfbb1211b..1e8cf53df28 100644 --- a/package.json +++ b/package.json @@ -279,6 +279,7 @@ "@opentelemetry/semantic-conventions": "1.9.1", "@popperjs/core": "2.11.6", "@prometheus-io/lezer-promql": "^0.37.0-rc.1", + "@pyroscope/flamegraph": "^0.35.5", "@react-aria/button": "3.6.1", "@react-aria/dialog": "3.3.1", "@react-aria/focus": "3.8.0", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0c84cb2fb0b..e62f5f0f32d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -94,4 +94,5 @@ export interface FeatureToggles { alertStateHistoryLokiOnly?: boolean; unifiedRequestLog?: boolean; renderAuthJWT?: boolean; + pyroscopeFlameGraph?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 11bf3812990..2143c9d4ad3 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -505,5 +505,11 @@ var ( State: FeatureStateBeta, Owner: grafanaAsCodeSquad, }, + { + Name: "pyroscopeFlameGraph", + Description: "Changes flame graph to pyroscope one", + State: FeatureStateAlpha, + Owner: grafanaObservabilityTracesAndProfilingSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d7bbe3f1e55..0286f61d16e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -75,3 +75,4 @@ alertStateHistoryLokiPrimary,alpha,@grafana/alerting-squad,false,false,false,fal alertStateHistoryLokiOnly,alpha,@grafana/alerting-squad,false,false,false,false unifiedRequestLog,alpha,@grafana/backend-platform,false,false,false,false renderAuthJWT,beta,@grafana/grafana-as-code,false,false,false,false +pyroscopeFlameGraph,alpha,@grafana/observability-traces-and-profiling,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4595ec528e1..a648556ad6f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -310,4 +310,8 @@ const ( // FlagRenderAuthJWT // Uses JWT-based auth for rendering instead of relying on remote cache FlagRenderAuthJWT = "renderAuthJWT" + + // FlagPyroscopeFlameGraph + // Changes flame graph to pyroscope one + FlagPyroscopeFlameGraph = "pyroscopeFlameGraph" ) diff --git a/public/app/features/explore/FlameGraphExploreContainer.tsx b/public/app/features/explore/FlameGraphExploreContainer.tsx index ed66d58b236..9e64f393c45 100644 --- a/public/app/features/explore/FlameGraphExploreContainer.tsx +++ b/public/app/features/explore/FlameGraphExploreContainer.tsx @@ -3,8 +3,7 @@ import React from 'react'; import { DataFrame, GrafanaTheme2, CoreApp } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; - -import FlameGraphContainer from '../../plugins/panel/flamegraph/components/FlameGraphContainer'; +import { FlameGraphTopWrapper } from 'app/plugins/panel/flamegraph/components/FlameGraphTopWrapper'; interface Props { dataFrames: DataFrame[]; @@ -15,7 +14,7 @@ export const FlameGraphExploreContainer = (props: Props) => { return (
- +
); }; diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx new file mode 100644 index 00000000000..c52c882e704 --- /dev/null +++ b/public/app/plugins/panel/flamegraph/components/FlameGraphTopWrapper.tsx @@ -0,0 +1,126 @@ +import { FlamegraphRenderer } from '@pyroscope/flamegraph'; +import React from 'react'; +import '@pyroscope/flamegraph/dist/index.css'; + +import { CoreApp, DataFrame, DataFrameView } from '@grafana/data'; +import { config } from '@grafana/runtime'; + +import FlameGraphContainer from './FlameGraphContainer'; + +type Props = { + data?: DataFrame; + app: CoreApp; + // Height for flame graph when not used in explore. + // This needs to be different to explore flame graph height as we + // use panels with user adjustable heights in dashboards etc. + flameGraphHeight?: number; +}; + +export const FlameGraphTopWrapper = (props: Props) => { + if (config.featureToggles.pyroscopeFlameGraph) { + const profile = props.data ? dataFrameToFlameBearer(props.data) : undefined; + return ; + } + + return ; +}; + +type Row = { + level: number; + label: string; + value: number; + self: number; +}; + +/** + * Converts a nested set format from a DataFrame to a Flamebearer format needed by the pyroscope flamegraph. + * @param data + */ +function dataFrameToFlameBearer(data: DataFrame) { + // Unfortunately we cannot use @pyroscope/models for now as they publish ts files which then get type checked and + // they do not pass our with our tsconfig + const profile: any = { + version: 1, + flamebearer: { + names: [], + levels: [], + numTicks: 0, + maxSelf: 0, + }, + metadata: { + format: 'single' as const, + sampleRate: 100, + spyName: 'gospy' as const, + units: 'samples' as const, + }, + }; + const view = new DataFrameView(data); + const labelField = data.fields.find((f) => f.name === 'label'); + + if (labelField?.config?.type?.enum?.text) { + profile.flamebearer.names = labelField.config.type.enum.text; + } + + const labelMap: Record = {}; + + // Handle both cases where label is a string or a number pointing to enum config text array. + const getLabel = (label: string | number) => { + if (typeof label === 'number') { + return label; + } else { + if (labelMap[label] === undefined) { + labelMap[label] = profile.flamebearer.names.length; + profile.flamebearer.names.push(label); + } + + return labelMap[label]; + } + }; + + // Absolute offset where we are currently at. + let offset = 0; + + for (let i = 0; i < data.length; i++) { + // view.get() changes the underlying object, so we have to call this first get the value and then call get() for + // current row. + const prevLevel = i > 0 ? view.get(i - 1).level : undefined; + const row = view.get(i); + const currentLevel = row.level; + const level = profile.flamebearer.levels[currentLevel]; + + // First row is the root and always the total number of ticks. + if (i === 0) { + profile.flamebearer.numTicks = row.value; + } + profile.flamebearer.maxSelf = Math.max(profile.flamebearer.maxSelf, row.self); + + if (prevLevel && prevLevel >= currentLevel) { + // we are going back to the previous level and adding sibling we have to figure out new offset + offset = levelWidth(level); + } + + if (!level) { + // Starting a new level. Offset is what ever current absolute offset is as there are no siblings yet. + profile.flamebearer.levels[row.level] = [offset, row.value, row.self, getLabel(row.label)]; + } else { + // We actually need offset relative to sibling while offset variable contains absolute offset. + const width = levelWidth(level); + level.push(offset - width, row.value, row.self, getLabel(row.label)); + } + } + return profile; +} + +/** + * Get a width of a level. As offsets are relative to siblings we need to sum all the offsets and values in a level. + * @param level + */ +function levelWidth(level: number[]) { + let length = 0; + for (let i = 0; i < level.length; i += 4) { + const start = level[i]; + const value = level[i + 1]; + length += start + value; + } + return length; +} diff --git a/yarn.lock b/yarn.lock index ace327afbb0..405468006b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6271,6 +6271,18 @@ __metadata: languageName: node linkType: hard +"@pyroscope/flamegraph@npm:^0.35.5": + version: 0.35.5 + resolution: "@pyroscope/flamegraph@npm:0.35.5" + peerDependencies: + graphviz-react: ^1.2.5 + react: ">=16.14.0" + react-dom: ">=16.14.0" + true-myth: ^5.1.2 + checksum: e63580683f5d2333202415b827ebe78986294d1fe49de978b62400e5cd070afd1f48d071ff7e5c22bc1ace6c52e9767e85650b34444c6ad0c9ecb23788d83ffd + languageName: node + linkType: hard + "@radix-ui/react-compose-refs@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-compose-refs@npm:1.0.0" @@ -20126,6 +20138,7 @@ __metadata: "@pmmmwh/react-refresh-webpack-plugin": 0.5.10 "@popperjs/core": 2.11.6 "@prometheus-io/lezer-promql": ^0.37.0-rc.1 + "@pyroscope/flamegraph": ^0.35.5 "@react-aria/button": 3.6.1 "@react-aria/dialog": 3.3.1 "@react-aria/focus": 3.8.0