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