[release-12.0.6] FlameGraph: Ensure total is only counted once for recursive function calls (#111604)
Actionlint / Lint GitHub Actions files (push) Waiting to run Details
Backend Code Checks / Validate Backend Configs (push) Waiting to run Details
Backend Unit Tests / Detect whether code changed (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (1/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (2/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (3/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (4/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (5/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (6/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (7/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (8/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (1/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (2/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (3/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (4/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (5/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (6/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (7/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (8/8) (push) Blocked by required conditions Details
Backend Unit Tests / All backend unit tests complete (push) Blocked by required conditions Details
CodeQL checks / Analyze (actions) (push) Waiting to run Details
CodeQL checks / Analyze (go) (push) Waiting to run Details
CodeQL checks / Analyze (javascript) (push) Waiting to run Details
Lint Frontend / Detect whether code changed (push) Waiting to run Details
Lint Frontend / Lint (push) Blocked by required conditions Details
Lint Frontend / Typecheck (push) Blocked by required conditions Details
Lint Frontend / Betterer (push) Blocked by required conditions Details
Verify i18n / verify-i18n (push) Waiting to run Details
End-to-end tests / Detect whether code changed (push) Waiting to run Details
End-to-end tests / Build & Package Grafana (push) Blocked by required conditions Details
End-to-end tests / Build E2E test runner (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env DISABLE_SCENES=true", e2e/old-arch/dashboards-suite, dashboards-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env DISABLE_SCENES=true", e2e/old-arch/panels-suite, panels-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env DISABLE_SCENES=true", e2e/old-arch/smoke-tests-suite, smoke-tests-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env DISABLE_SCENES=true", e2e/old-arch/various-suite, various-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/dashboards-suite, dashboards-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/panels-suite, panels-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/smoke-tests-suite, smoke-tests-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/various-suite, various-suite) (push) Blocked by required conditions Details
End-to-end tests / A11y test (push) Blocked by required conditions Details
End-to-end tests / All E2E tests complete (push) Blocked by required conditions Details
Frontend tests / Detect whether code changed (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (1) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (2) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (3) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (4) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (5) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (6) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (7) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (8) (push) Blocked by required conditions Details
Frontend tests / All frontend unit tests complete (push) Blocked by required conditions Details
Integration Tests / Sqlite (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / All backend integration tests complete (push) Blocked by required conditions Details
Reject GitHub secrets / reject-gh-secrets (push) Waiting to run Details
Run dashboard schema v2 e2e / dashboard-schema-v2-e2e (push) Waiting to run Details
Shellcheck / Shellcheck scripts (push) Waiting to run Details
Swagger generated code / Verify committed API specs match (push) Waiting to run Details
Dispatch sync to mirror / dispatch-job (push) Waiting to run Details

FlameGraph: Ensure total is only counted once for recursive function calls (#111548)

grafana-flamegraph: Ensure total is only counted once for recursive function calls

Example flamegraph: https://flamegraph.com/share/2bb59df3-9930-11f0-94ec-760777e76ccd

(cherry picked from commit c5f6318b7b)

Co-authored-by: Christian Simon <simon@swine.de>
This commit is contained in:
grafana-delivery-bot[bot] 2025-10-07 12:51:26 +02:00 committed by GitHub
parent d351ebe919
commit ef3e1b4731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 160 additions and 24 deletions

View File

@ -5,9 +5,10 @@ import { createDataFrame } from '@grafana/data';
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform'; import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
import { data } from '../FlameGraph/testData/dataNestedSet'; import { data } from '../FlameGraph/testData/dataNestedSet';
import { textToDataContainer } from '../FlameGraph/testHelpers';
import { ColorScheme } from '../types'; import { ColorScheme } from '../types';
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer'; import FlameGraphTopTableContainer, { buildFilteredTable } from './FlameGraphTopTableContainer';
describe('FlameGraphTopTableContainer', () => { describe('FlameGraphTopTableContainer', () => {
const setup = () => { const setup = () => {
@ -48,7 +49,10 @@ describe('FlameGraphTopTableContainer', () => {
expect(cells).toHaveLength(60); // 16 rows expect(cells).toHaveLength(60); // 16 rows
expect(cells[1].textContent).toEqual('net/http.HandlerFunc.ServeHTTP'); expect(cells[1].textContent).toEqual('net/http.HandlerFunc.ServeHTTP');
expect(cells[2].textContent).toEqual('31.7 K'); expect(cells[2].textContent).toEqual('31.7 K');
expect(cells[3].textContent).toEqual('31.7 Bil'); expect(cells[3].textContent).toEqual('5.58 Bil');
expect(cells[5].textContent).toEqual('total');
expect(cells[6].textContent).toEqual('16.5 K');
expect(cells[7].textContent).toEqual('16.5 Bil');
expect(cells[25].textContent).toEqual('net/http.(*conn).serve'); expect(cells[25].textContent).toEqual('net/http.(*conn).serve');
expect(cells[26].textContent).toEqual('5.63 K'); expect(cells[26].textContent).toEqual('5.63 K');
expect(cells[27].textContent).toEqual('5.63 Bil'); expect(cells[27].textContent).toEqual('5.63 Bil');
@ -74,3 +78,111 @@ describe('FlameGraphTopTableContainer', () => {
expect(mocks.onSandwich).toHaveBeenCalledWith('net/http.HandlerFunc.ServeHTTP'); expect(mocks.onSandwich).toHaveBeenCalledWith('net/http.HandlerFunc.ServeHTTP');
}); });
}); });
describe('buildFilteredTable', () => {
it('should group data by label and sum values', () => {
const container = textToDataContainer(`
[0////]
[1][2]
[3][4]
`);
const result = buildFilteredTable(container!);
expect(result).toEqual({
'0': { self: 1, total: 7, totalRight: 0 },
'1': { self: 0, total: 3, totalRight: 0 },
'2': { self: 0, total: 3, totalRight: 0 },
'3': { self: 3, total: 3, totalRight: 0 },
'4': { self: 3, total: 3, totalRight: 0 },
});
});
it('should sum values for duplicate labels', () => {
const container = textToDataContainer(`
[0///]
[1][1]
`);
const result = buildFilteredTable(container!);
expect(result).toEqual({
'0': { self: 0, total: 6, totalRight: 0 },
'1': { self: 6, total: 6, totalRight: 0 },
});
});
it('should filter by matchedLabels when provided', () => {
const container = textToDataContainer(`
[0////]
[1][2]
[3][4]
`);
const matchedLabels = new Set(['1', '3']);
const result = buildFilteredTable(container!, matchedLabels);
expect(result).toEqual({
'1': { self: 0, total: 3, totalRight: 0 },
'3': { self: 3, total: 3, totalRight: 0 },
});
});
it('should handle empty matchedLabels set', () => {
const container = textToDataContainer(`
[0////]
[1][2]
[3][4]
`);
const matchedLabels = new Set<string>();
const result = buildFilteredTable(container!, matchedLabels);
expect(result).toEqual({});
});
it('should handle data with no matches', () => {
const container = textToDataContainer(`
[0////]
[1][2]
[3][4]
`);
const matchedLabels = new Set(['9']);
const result = buildFilteredTable(container!, matchedLabels);
expect(result).toEqual({});
});
it('should work without matchedLabels filter', () => {
const container = textToDataContainer(`
[0]
[1]
`);
const result = buildFilteredTable(container!);
expect(result).toEqual({
'0': { self: 0, total: 3, totalRight: 0 },
'1': { self: 3, total: 3, totalRight: 0 },
});
});
it('should not inflate totals for recursive calls', () => {
const container = textToDataContainer(`
[0////]
[1][2]
[3][4]
[0]
`);
const result = buildFilteredTable(container!);
expect(result).toEqual({
'0': { self: 4, total: 7, totalRight: 0 },
'1': { self: 0, total: 3, totalRight: 0 },
'2': { self: 0, total: 3, totalRight: 0 },
'3': { self: 0, total: 3, totalRight: 0 },
'4': { self: 3, total: 3, totalRight: 0 },
});
});
});

View File

@ -53,28 +53,7 @@ const FlameGraphTopTableContainer = memo(
onTableSort, onTableSort,
colorScheme, colorScheme,
}: Props) => { }: Props) => {
const table = useMemo(() => { const table = useMemo(() => buildFilteredTable(data, matchedLabels), [data, matchedLabels]);
// Group the data by label, we show only one row per label and sum the values
// TODO: should be by filename + funcName + linenumber?
let filteredTable: { [key: string]: TableData } = Object.create(null);
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);
// If user is doing text search we filter out labels in the same way we highlight them in flame graph.
if (!matchedLabels || matchedLabels.has(label)) {
filteredTable[label] = filteredTable[label] || {};
filteredTable[label].self = filteredTable[label].self ? filteredTable[label].self + self : self;
filteredTable[label].total = filteredTable[label].total ? filteredTable[label].total + value : value;
filteredTable[label].totalRight = filteredTable[label].totalRight
? filteredTable[label].totalRight + valueRight
: valueRight;
}
}
return filteredTable;
}, [data, matchedLabels]);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const theme = useTheme2(); const theme = useTheme2();
@ -124,6 +103,49 @@ const FlameGraphTopTableContainer = memo(
FlameGraphTopTableContainer.displayName = 'FlameGraphTopTableContainer'; FlameGraphTopTableContainer.displayName = 'FlameGraphTopTableContainer';
function buildFilteredTable(data: FlameGraphDataContainer, matchedLabels?: Set<string>) {
// Group the data by label, we show only one row per label and sum the values
// TODO: should be by filename + funcName + linenumber?
let filteredTable: { [key: string]: TableData } = Object.create(null);
// Track call stack to detect recursive calls
const callStack: string[] = [];
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);
const level = data.getLevel(i);
// Maintain call stack based on level changes
while (callStack.length > level) {
callStack.pop();
}
// Check if this is a recursive call (same label already in call stack)
const isRecursive = callStack.some((entry) => entry === label);
// If user is doing text search we filter out labels in the same way we highlight them in flame graph.
if (!matchedLabels || matchedLabels.has(label)) {
filteredTable[label] = filteredTable[label] || {};
filteredTable[label].self = filteredTable[label].self ? filteredTable[label].self + self : self;
// Only add to total if this is not a recursive call
if (!isRecursive) {
filteredTable[label].total = filteredTable[label].total ? filteredTable[label].total + value : value;
filteredTable[label].totalRight = filteredTable[label].totalRight
? filteredTable[label].totalRight + valueRight
: valueRight;
}
}
// Add current call to the stack
callStack.push(label);
}
return filteredTable;
}
function buildTableDataFrame( function buildTableDataFrame(
data: FlameGraphDataContainer, data: FlameGraphDataContainer,
table: { [key: string]: TableData }, table: { [key: string]: TableData },
@ -365,4 +387,6 @@ const getStylesActionCell = () => {
}; };
}; };
export { buildFilteredTable };
export default FlameGraphTopTableContainer; export default FlameGraphTopTableContainer;