mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Introduce new Flame graph panel (#56376)
* Flamegraph * Updated flame graph width/height values * Fix top table rendering issue * Add feature toggle for flamegraph in explore * Update tests * Hide flamegraph from dash panel viz list if feature toggle not enabled * Show table if no flameGraphFrames * Add flame graph to testdata ds * Minor improvement
This commit is contained in:
parent
a18a3d7628
commit
74c809f544
@ -7758,6 +7758,9 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/debug/RenderInfoViewer.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/gauge/GaugeMigrations.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -21,7 +21,7 @@ export enum LoadingState {
|
||||
}
|
||||
|
||||
// Should be kept in sync with grafana-plugin-sdk-go/data/frame_meta.go
|
||||
export const preferredVisualizationTypes = ['graph', 'table', 'logs', 'trace', 'nodeGraph'] as const;
|
||||
export const preferredVisualizationTypes = ['graph', 'table', 'logs', 'trace', 'nodeGraph', 'flamegraph'] as const;
|
||||
export type PreferredVisualisationType = typeof preferredVisualizationTypes[number];
|
||||
|
||||
/**
|
||||
|
@ -68,6 +68,7 @@ export interface FeatureToggles {
|
||||
grpcServer?: boolean;
|
||||
objectStore?: boolean;
|
||||
traceqlEditor?: boolean;
|
||||
flameGraph?: boolean;
|
||||
redshiftAsyncQueryDataSupport?: boolean;
|
||||
athenaAsyncQueryDataSupport?: boolean;
|
||||
increaseInMemDatabaseQueryCache?: boolean;
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||
@ -62,6 +63,11 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
continue
|
||||
}
|
||||
|
||||
hideFromList := panel.HideFromList
|
||||
if panel.ID == "flamegraph" {
|
||||
hideFromList = !hs.Features.IsEnabled(featuremgmt.FlagFlameGraph)
|
||||
}
|
||||
|
||||
panels[panel.ID] = plugins.PanelDTO{
|
||||
ID: panel.ID,
|
||||
Name: panel.Name,
|
||||
@ -69,7 +75,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
Module: panel.Module,
|
||||
BaseURL: panel.BaseURL,
|
||||
SkipDataQuery: panel.SkipDataQuery,
|
||||
HideFromList: panel.HideFromList,
|
||||
HideFromList: hideFromList,
|
||||
ReleaseState: string(panel.State),
|
||||
Signature: string(panel.Signature),
|
||||
Sort: getPanelSort(panel.ID),
|
||||
|
@ -164,6 +164,7 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *store.Serv
|
||||
"candlestick": {},
|
||||
"news": {},
|
||||
"nodeGraph": {},
|
||||
"flamegraph": {},
|
||||
"traces": {},
|
||||
"piechart": {},
|
||||
"stat": {},
|
||||
|
@ -65,6 +65,7 @@ func coreTreeList(lib thema.Library) pfs.TreeList {
|
||||
makeTreeOrPanic("public/app/plugins/panel/bargauge", "bargauge", lib),
|
||||
makeTreeOrPanic("public/app/plugins/panel/dashlist", "dashlist", lib),
|
||||
makeTreeOrPanic("public/app/plugins/panel/debug", "debug", lib),
|
||||
makeTreeOrPanic("public/app/plugins/panel/flamegraph", "flamegraph", lib),
|
||||
makeTreeOrPanic("public/app/plugins/panel/gauge", "gauge", lib),
|
||||
makeTreeOrPanic("public/app/plugins/panel/geomap", "geomap", lib),
|
||||
makeTreeOrPanic("public/app/plugins/panel/gettingstarted", "gettingstarted", lib),
|
||||
|
@ -289,6 +289,11 @@ var (
|
||||
Description: "Show the TraceQL editor in the explore page",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "flameGraph",
|
||||
Description: "Show the flame graph",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "redshiftAsyncQueryDataSupport",
|
||||
Description: "Enable async query data support for Redshift",
|
||||
|
@ -215,6 +215,10 @@ const (
|
||||
// Show the TraceQL editor in the explore page
|
||||
FlagTraceqlEditor = "traceqlEditor"
|
||||
|
||||
// FlagFlameGraph
|
||||
// Show the flame graph
|
||||
FlagFlameGraph = "flameGraph"
|
||||
|
||||
// FlagRedshiftAsyncQueryDataSupport
|
||||
// Enable async query data support for Redshift
|
||||
FlagRedshiftAsyncQueryDataSupport = "redshiftAsyncQueryDataSupport"
|
||||
|
@ -427,6 +427,42 @@
|
||||
"signatureType": "",
|
||||
"signatureOrg": ""
|
||||
},
|
||||
{
|
||||
"name": "Flame Graph",
|
||||
"type": "panel",
|
||||
"id": "flamegraph",
|
||||
"enabled": true,
|
||||
"pinned": false,
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"description": "",
|
||||
"links": null,
|
||||
"logos": {
|
||||
"small": "public/img/icn-panel.svg",
|
||||
"large": "public/img/icn-panel.svg"
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": ""
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
"defaultNavUrl": "/plugins/flamegraph/",
|
||||
"category": "",
|
||||
"state": "beta",
|
||||
"signature": "internal",
|
||||
"signatureType": "",
|
||||
"signatureOrg": ""
|
||||
},
|
||||
{
|
||||
"name": "Gauge",
|
||||
"type": "panel",
|
||||
@ -1702,4 +1738,4 @@
|
||||
"signatureType": "",
|
||||
"signatureOrg": ""
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -39,6 +39,7 @@ const (
|
||||
serverError500Query queryType = "server_error_500"
|
||||
logsQuery queryType = "logs"
|
||||
nodeGraphQuery queryType = "node_graph"
|
||||
flameGraphQuery queryType = "flame_graph"
|
||||
rawFrameQuery queryType = "raw_frame"
|
||||
csvFileQueryType queryType = "csv_file"
|
||||
csvContentQueryType queryType = "csv_content"
|
||||
@ -195,6 +196,11 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means
|
||||
Name: "Node Graph",
|
||||
})
|
||||
|
||||
s.registerScenario(&Scenario{
|
||||
ID: string(flameGraphQuery),
|
||||
Name: "Flame Graph",
|
||||
})
|
||||
|
||||
s.registerScenario(&Scenario{
|
||||
ID: string(rawFrameQuery),
|
||||
Name: "Raw Frames",
|
||||
|
@ -80,6 +80,7 @@ const dummyProps: Props = {
|
||||
showTable: true,
|
||||
showTrace: true,
|
||||
showNodeGraph: true,
|
||||
showFlameGraph: true,
|
||||
splitOpen: (() => {}) as any,
|
||||
changeGraphStyle: () => {},
|
||||
graphStyle: 'lines',
|
||||
|
@ -8,6 +8,7 @@ import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { AbsoluteTimeRange, DataQuery, GrafanaTheme2, LoadingState, QueryFixAction, RawTimeRange } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2, PanelContainer } from '@grafana/ui';
|
||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@ -23,6 +24,7 @@ import { ExploreGraph } from './ExploreGraph';
|
||||
import { ExploreGraphLabel } from './ExploreGraphLabel';
|
||||
import ExploreQueryInspector from './ExploreQueryInspector';
|
||||
import { ExploreToolbar } from './ExploreToolbar';
|
||||
import { FlameGraphExploreContainer } from './FlameGraphExploreContainer';
|
||||
import LogsContainer from './LogsContainer';
|
||||
import { NoData } from './NoData';
|
||||
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
||||
@ -228,15 +230,26 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
}
|
||||
|
||||
renderGraphPanel(width: number) {
|
||||
const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading, theme, graphStyle } = this.props;
|
||||
const {
|
||||
graphResult,
|
||||
absoluteRange,
|
||||
timeZone,
|
||||
splitOpen,
|
||||
queryResponse,
|
||||
loading,
|
||||
theme,
|
||||
graphStyle,
|
||||
showFlameGraph,
|
||||
} = this.props;
|
||||
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
||||
const label = <ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={this.onChangeGraphStyle} />;
|
||||
|
||||
return (
|
||||
<Collapse label={label} loading={loading} isOpen>
|
||||
<ExploreGraph
|
||||
graphStyle={graphStyle}
|
||||
data={graphResult!}
|
||||
height={400}
|
||||
height={showFlameGraph ? 180 : 400}
|
||||
width={width - spacing}
|
||||
absoluteRange={absoluteRange}
|
||||
onChangeTime={this.onUpdateTimeRange}
|
||||
@ -297,6 +310,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
|
||||
memoizedGetNodeGraphDataFrames = memoizeOne(getNodeGraphDataFrames);
|
||||
|
||||
renderFlameGraphPanel() {
|
||||
const { queryResponse } = this.props;
|
||||
return <FlameGraphExploreContainer dataFrames={queryResponse.flameGraphFrames} />;
|
||||
}
|
||||
|
||||
renderTraceViewPanel() {
|
||||
const { queryResponse, splitOpen, exploreId } = this.props;
|
||||
const dataFrames = queryResponse.series.filter((series) => series.meta?.preferredVisualisationType === 'trace');
|
||||
@ -330,6 +348,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
showLogs,
|
||||
showTrace,
|
||||
showNodeGraph,
|
||||
showFlameGraph,
|
||||
timeZone,
|
||||
} = this.props;
|
||||
const { openDrawer } = this.state;
|
||||
@ -344,6 +363,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
queryResponse.logsFrames,
|
||||
queryResponse.graphFrames,
|
||||
queryResponse.nodeGraphFrames,
|
||||
queryResponse.flameGraphFrames,
|
||||
queryResponse.tableFrames,
|
||||
queryResponse.traceFrames,
|
||||
].every((e) => e.length === 0);
|
||||
@ -391,6 +411,9 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
|
||||
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
|
||||
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
|
||||
{showFlameGraph && config.featureToggles.flameGraph && (
|
||||
<ErrorBoundaryAlert>{this.renderFlameGraphPanel()}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
|
||||
{showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
|
||||
</>
|
||||
@ -441,6 +464,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
||||
absoluteRange,
|
||||
queryResponse,
|
||||
showNodeGraph,
|
||||
showFlameGraph,
|
||||
loading,
|
||||
graphStyle,
|
||||
} = item;
|
||||
@ -461,6 +485,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
||||
showTable,
|
||||
showTrace,
|
||||
showNodeGraph,
|
||||
showFlameGraph,
|
||||
loading,
|
||||
graphStyle,
|
||||
};
|
||||
|
@ -50,6 +50,7 @@ const setup = (propOverrides = {}) => {
|
||||
tableFrames: [],
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
|
31
public/app/features/explore/FlameGraphExploreContainer.tsx
Normal file
31
public/app/features/explore/FlameGraphExploreContainer.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import FlameGraphContainer from '../../plugins/panel/flamegraph/components/FlameGraphContainer';
|
||||
|
||||
interface Props {
|
||||
dataFrames: DataFrame[];
|
||||
}
|
||||
|
||||
export const FlameGraphExploreContainer = (props: Props) => {
|
||||
const styles = useStyles2((theme) => getStyles(theme));
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<FlameGraphContainer data={props.dataFrames[0]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
background: ${theme.colors.background.primary};
|
||||
display: flow-root;
|
||||
padding: ${theme.spacing(1)};
|
||||
border: 1px solid ${theme.components.panel.borderColor};
|
||||
border-radius: ${theme.shape.borderRadius(1)};
|
||||
`,
|
||||
});
|
@ -56,6 +56,7 @@ function setup(error: DataQueryError) {
|
||||
tableFrames: [],
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
|
@ -919,6 +919,7 @@ export const processQueryResponse = (
|
||||
tableResult,
|
||||
traceFrames,
|
||||
nodeGraphFrames,
|
||||
flameGraphFrames,
|
||||
} = response;
|
||||
|
||||
if (error) {
|
||||
@ -962,5 +963,6 @@ export const processQueryResponse = (
|
||||
showTable: !!tableResult,
|
||||
showTrace: !!traceFrames.length,
|
||||
showNodeGraph: !!nodeGraphFrames.length,
|
||||
showFlameGraph: !!flameGraphFrames.length,
|
||||
};
|
||||
};
|
||||
|
@ -96,6 +96,7 @@ export const createEmptyQueryResponse = (): ExplorePanelData => ({
|
||||
logsFrames: [],
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
tableFrames: [],
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
TimeRange,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime/src/config';
|
||||
import { GraphDrawStyle, StackingMode } from '@grafana/schema';
|
||||
import TableModel from 'app/core/TableModel';
|
||||
import { ExplorePanelData } from 'app/types';
|
||||
@ -67,7 +68,19 @@ const getTestContext = () => {
|
||||
meta: { preferredVisualisationType: 'logs' },
|
||||
});
|
||||
|
||||
return { emptyTable, timeSeries, logs, table };
|
||||
const flameGraph = toDataFrame({
|
||||
name: 'flameGraph-res',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'level', type: FieldType.number, values: [4, 5, 6] },
|
||||
{ name: 'value', type: FieldType.number, values: [100, 100, 100] },
|
||||
{ name: 'self', type: FieldType.number, values: [10, 10, 10] },
|
||||
{ name: 'label', type: FieldType.string, values: ['this is a message', 'second message', 'third'] },
|
||||
],
|
||||
meta: { preferredVisualisationType: 'flamegraph' },
|
||||
});
|
||||
|
||||
return { emptyTable, timeSeries, logs, table, flameGraph };
|
||||
};
|
||||
|
||||
const createExplorePanelData = (args: Partial<ExplorePanelData>): ExplorePanelData => {
|
||||
@ -83,20 +96,23 @@ const createExplorePanelData = (args: Partial<ExplorePanelData>): ExplorePanelDa
|
||||
tableResult: undefined as unknown as null,
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
};
|
||||
|
||||
return { ...defaults, ...args };
|
||||
};
|
||||
|
||||
describe('decorateWithGraphLogsTraceAndTable', () => {
|
||||
describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => {
|
||||
it('should correctly classify the dataFrames', () => {
|
||||
const { table, logs, timeSeries, emptyTable } = getTestContext();
|
||||
const series = [table, logs, timeSeries, emptyTable];
|
||||
const { table, logs, timeSeries, emptyTable, flameGraph } = getTestContext();
|
||||
const series = [table, logs, timeSeries, emptyTable, flameGraph];
|
||||
const panelData: PanelData = {
|
||||
series,
|
||||
state: LoadingState.Done,
|
||||
timeRange: {} as unknown as TimeRange,
|
||||
};
|
||||
// Needed so flamegraph does not fallback to table, will be removed when feature flag no longer necessary
|
||||
config.featureToggles.flameGraph = true;
|
||||
|
||||
expect(decorateWithFrameTypeMetadata(panelData)).toEqual({
|
||||
series,
|
||||
@ -107,6 +123,7 @@ describe('decorateWithGraphLogsTraceAndTable', () => {
|
||||
logsFrames: [logs],
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [flameGraph],
|
||||
graphResult: null,
|
||||
tableResult: null,
|
||||
logsResult: null,
|
||||
@ -130,6 +147,7 @@ describe('decorateWithGraphLogsTraceAndTable', () => {
|
||||
logsFrames: [],
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
graphResult: null,
|
||||
tableResult: null,
|
||||
logsResult: null,
|
||||
@ -156,6 +174,7 @@ describe('decorateWithGraphLogsTraceAndTable', () => {
|
||||
logsFrames: [logs],
|
||||
traceFrames: [],
|
||||
nodeGraphFrames: [],
|
||||
flameGraphFrames: [],
|
||||
graphResult: null,
|
||||
tableResult: null,
|
||||
logsResult: null,
|
||||
|
@ -30,6 +30,7 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData
|
||||
const logsFrames: DataFrame[] = [];
|
||||
const traceFrames: DataFrame[] = [];
|
||||
const nodeGraphFrames: DataFrame[] = [];
|
||||
const flameGraphFrames: DataFrame[] = [];
|
||||
|
||||
for (const frame of data.series) {
|
||||
switch (frame.meta?.preferredVisualisationType) {
|
||||
@ -48,6 +49,9 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData
|
||||
case 'nodeGraph':
|
||||
nodeGraphFrames.push(frame);
|
||||
break;
|
||||
case 'flamegraph':
|
||||
config.featureToggles.flameGraph ? flameGraphFrames.push(frame) : tableFrames.push(frame);
|
||||
break;
|
||||
default:
|
||||
if (isTimeSeries(frame)) {
|
||||
graphFrames.push(frame);
|
||||
@ -66,6 +70,7 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData
|
||||
logsFrames,
|
||||
traceFrames,
|
||||
nodeGraphFrames,
|
||||
flameGraphFrames,
|
||||
graphResult: null,
|
||||
tableResult: null,
|
||||
logsResult: null,
|
||||
|
@ -49,6 +49,7 @@ import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
|
||||
import * as candlestickPanel from 'app/plugins/panel/candlestick/module';
|
||||
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
||||
import * as debugPanel from 'app/plugins/panel/debug/module';
|
||||
import * as flamegraphPanel from 'app/plugins/panel/flamegraph/module';
|
||||
import * as gaugePanel from 'app/plugins/panel/gauge/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as histogramPanel from 'app/plugins/panel/histogram/module';
|
||||
@ -133,6 +134,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/panel/live/module': livePanel,
|
||||
'app/plugins/panel/stat/module': statPanel,
|
||||
'app/plugins/panel/debug/module': debugPanel,
|
||||
'app/plugins/panel/flamegraph/module': flamegraphPanel,
|
||||
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
|
||||
'app/plugins/panel/gauge/module': gaugePanel,
|
||||
'app/plugins/panel/piechart/module': pieChartPanel,
|
||||
|
@ -114,4 +114,10 @@ export const scenarios = [
|
||||
name: 'Table Static',
|
||||
stringInput: '',
|
||||
},
|
||||
{
|
||||
description: '',
|
||||
id: 'flame_graph',
|
||||
name: 'Flame Graph',
|
||||
stringInput: '',
|
||||
},
|
||||
];
|
||||
|
@ -21,6 +21,7 @@ import { getSearchFilterScopedVar } from 'app/features/variables/utils';
|
||||
import { queryMetricTree } from './metricTree';
|
||||
import { generateRandomNodes, savedNodesResponse } from './nodeGraphUtils';
|
||||
import { runStream } from './runStreams';
|
||||
import { flameGraphData } from './testData/flameGraphResponse';
|
||||
import { Scenario, TestDataQuery } from './types';
|
||||
import { TestDataVariableSupport } from './variables';
|
||||
|
||||
@ -66,6 +67,9 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
|
||||
case 'node_graph':
|
||||
streams.push(this.nodesQuery(target, options));
|
||||
break;
|
||||
case 'flame_graph':
|
||||
streams.push(this.flameGraphQuery());
|
||||
break;
|
||||
case 'raw_frame':
|
||||
streams.push(this.rawFrameQuery(target, options));
|
||||
break;
|
||||
@ -213,6 +217,10 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
|
||||
return of({ data: frames }).pipe(delay(100));
|
||||
}
|
||||
|
||||
flameGraphQuery(): Observable<DataQueryResponse> {
|
||||
return of({ data: [flameGraphData] }).pipe(delay(100));
|
||||
}
|
||||
|
||||
rawFrameQuery(target: TestDataQuery, options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
|
||||
try {
|
||||
const data = JSON.parse(target.rawFrameContent ?? '[]').map((v: any) => {
|
||||
|
1319
public/app/plugins/datasource/testdata/testData/flameGraphResponse.ts
vendored
Normal file
1319
public/app/plugins/datasource/testdata/testData/flameGraphResponse.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
|
||||
import { screen } from '@testing-library/dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataFrameView, MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { SelectedView } from '../types';
|
||||
|
||||
import FlameGraph from './FlameGraph';
|
||||
import { Item, nestedSetToLevels } from './dataTransform';
|
||||
import { data } from './testData/dataNestedSet';
|
||||
import 'jest-canvas-mock';
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useMeasure: () => {
|
||||
const ref = React.useRef();
|
||||
return [ref, { width: 1600 }];
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FlameGraph', () => {
|
||||
const FlameGraphWithProps = () => {
|
||||
const [topLevelIndex, setTopLevelIndex] = useState(0);
|
||||
const [rangeMin, setRangeMin] = useState(0);
|
||||
const [rangeMax, setRangeMax] = useState(1);
|
||||
const [search] = useState('');
|
||||
const [selectedView, _] = useState(SelectedView.Both);
|
||||
|
||||
const flameGraphData = new MutableDataFrame(data);
|
||||
const dataView = new DataFrameView<Item>(flameGraphData);
|
||||
const levels = nestedSetToLevels(dataView);
|
||||
|
||||
return (
|
||||
<FlameGraph
|
||||
data={flameGraphData}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
search={search}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('should render without error', async () => {
|
||||
expect(() => render(<FlameGraphWithProps />)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should render correctly', async () => {
|
||||
render(<FlameGraphWithProps />);
|
||||
|
||||
const canvas = screen.getByTestId('flameGraph') as HTMLCanvasElement;
|
||||
const ctx = canvas!.getContext('2d');
|
||||
const calls = ctx!.__getDrawCalls();
|
||||
expect(calls).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,223 @@
|
||||
// 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, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { DataFrame, FieldType } from '@grafana/data';
|
||||
|
||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||
import { TooltipData, SelectedView } from '../types';
|
||||
|
||||
import FlameGraphTooltip, { getTooltipData } from './FlameGraphTooltip';
|
||||
import { ItemWithStart } from './dataTransform';
|
||||
import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
|
||||
|
||||
type Props = {
|
||||
data: DataFrame;
|
||||
levels: ItemWithStart[][];
|
||||
topLevelIndex: number;
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
search: string;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
selectedView: SelectedView;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
const FlameGraph = ({
|
||||
data,
|
||||
levels,
|
||||
topLevelIndex,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
setTopLevelIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
}: Props) => {
|
||||
const styles = getStyles(selectedView);
|
||||
const totalTicks = data.fields[1].values.get(0);
|
||||
const valueField =
|
||||
data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number);
|
||||
|
||||
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
||||
const graphRef = useRef<HTMLCanvasElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [tooltipData, setTooltipData] = useState<TooltipData>();
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
// Convert pixel coordinates to bar coordinates in the levels array so that we can add mouse events like clicks to
|
||||
// the canvas.
|
||||
const convertPixelCoordinatesToBarCoordinates = useCallback(
|
||||
(x: number, y: number, pixelsPerTick: number) => {
|
||||
const levelIndex = Math.floor(y / (PIXELS_PER_LEVEL / window.devicePixelRatio));
|
||||
const barIndex = getBarIndex(x, levels[levelIndex], pixelsPerTick, totalTicks, rangeMin);
|
||||
return { levelIndex, barIndex };
|
||||
},
|
||||
[levels, totalTicks, rangeMin]
|
||||
);
|
||||
|
||||
const render = useCallback(
|
||||
(pixelsPerTick: number) => {
|
||||
if (!levels.length) {
|
||||
return;
|
||||
}
|
||||
const ctx = graphRef.current?.getContext('2d')!;
|
||||
const graph = graphRef.current!;
|
||||
|
||||
const height = PIXELS_PER_LEVEL * levels.length;
|
||||
graph.width = Math.round(wrapperWidth * window.devicePixelRatio);
|
||||
graph.height = Math.round(height * window.devicePixelRatio);
|
||||
graph.style.width = `${wrapperWidth}px`;
|
||||
graph.style.height = `${height}px`;
|
||||
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = 12 * window.devicePixelRatio + 'px monospace';
|
||||
ctx.strokeStyle = 'white';
|
||||
|
||||
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(level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
||||
for (const rect of dimensions) {
|
||||
// Render each rectangle based on the computed dimensions
|
||||
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, topLevelIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
[levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (graphRef.current) {
|
||||
const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalTicks / (rangeMax - rangeMin);
|
||||
render(pixelsPerTick);
|
||||
|
||||
// Clicking allows user to "zoom" into the flamegraph. Zooming means the x axis gets smaller so that the clicked
|
||||
// bar takes 100% of the x axis.
|
||||
graphRef.current.onclick = (e) => {
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(e.offsetX, e.offsetY, pixelsPerTick);
|
||||
|
||||
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
|
||||
setTopLevelIndex(levelIndex);
|
||||
setRangeMin(levels[levelIndex][barIndex].start / totalTicks);
|
||||
setRangeMax((levels[levelIndex][barIndex].start + levels[levelIndex][barIndex].value) / totalTicks);
|
||||
}
|
||||
};
|
||||
|
||||
graphRef.current!.onmousemove = (e) => {
|
||||
if (tooltipRef.current) {
|
||||
setShowTooltip(false);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(e.offsetX, e.offsetY, pixelsPerTick);
|
||||
|
||||
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
|
||||
tooltipRef.current.style.left = e.clientX + 10 + 'px';
|
||||
tooltipRef.current.style.top = e.clientY + 40 + 'px';
|
||||
|
||||
const bar = levels[levelIndex][barIndex];
|
||||
const tooltipData = getTooltipData(valueField!, bar.label, bar.value, totalTicks);
|
||||
setTooltipData(tooltipData);
|
||||
setShowTooltip(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
graphRef.current!.onmouseleave = () => {
|
||||
setShowTooltip(false);
|
||||
};
|
||||
}
|
||||
}, [
|
||||
render,
|
||||
convertPixelCoordinatesToBarCoordinates,
|
||||
levels,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
topLevelIndex,
|
||||
totalTicks,
|
||||
wrapperWidth,
|
||||
setTopLevelIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
valueField,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.graph} ref={sizeRef}>
|
||||
<canvas ref={graphRef} data-testid="flameGraph" />
|
||||
<FlameGraphTooltip tooltipRef={tooltipRef} tooltipData={tooltipData!} showTooltip={showTooltip} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (selectedView: SelectedView) => ({
|
||||
graph: css`
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
width: ${selectedView === SelectedView.FlameGraph ? '100%' : '50%'};
|
||||
`,
|
||||
});
|
||||
|
||||
/**
|
||||
* 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: ItemWithStart[],
|
||||
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,86 @@
|
||||
import { ArrayVector, Field, FieldType } from '@grafana/data';
|
||||
|
||||
import { getTooltipData } from './FlameGraphTooltip';
|
||||
|
||||
describe('should get tooltip data correctly', () => {
|
||||
it('for bytes', () => {
|
||||
const tooltipData = getTooltipData(makeField('bytes'), 'total', 8_624_078_250, 8_624_078_250);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentTitle: '% of total',
|
||||
percentValue: 100,
|
||||
unitTitle: 'RAM',
|
||||
unitValue: '8.03 GiB',
|
||||
samples: '8,624,078,250',
|
||||
});
|
||||
});
|
||||
|
||||
it('with none unit', () => {
|
||||
const tooltipData = getTooltipData(makeField('none'), 'total', 8_624_078_250, 8_624_078_250);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentTitle: '% of total',
|
||||
percentValue: 100,
|
||||
unitTitle: 'Count',
|
||||
unitValue: '8624078250',
|
||||
samples: '8,624,078,250',
|
||||
});
|
||||
});
|
||||
|
||||
it('without unit', () => {
|
||||
const tooltipData = getTooltipData(
|
||||
{
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(),
|
||||
config: {},
|
||||
},
|
||||
'total',
|
||||
8_624_078_250,
|
||||
8_624_078_250
|
||||
);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentTitle: '% of total',
|
||||
percentValue: 100,
|
||||
unitTitle: 'Count',
|
||||
unitValue: '8624078250',
|
||||
samples: '8,624,078,250',
|
||||
});
|
||||
});
|
||||
|
||||
it('for objects', () => {
|
||||
const tooltipData = getTooltipData(makeField('short'), 'total', 8_624_078_250, 8_624_078_250);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentTitle: '% of total',
|
||||
percentValue: 100,
|
||||
unitTitle: 'Count',
|
||||
unitValue: '8.62 Bil',
|
||||
samples: '8,624,078,250',
|
||||
});
|
||||
});
|
||||
|
||||
it('for nanoseconds', () => {
|
||||
const tooltipData = getTooltipData(makeField('ns'), 'total', 8_624_078_250, 8_624_078_250);
|
||||
expect(tooltipData).toEqual({
|
||||
name: 'total',
|
||||
percentTitle: '% of total time',
|
||||
percentValue: 100,
|
||||
unitTitle: 'Time',
|
||||
unitValue: '8.62 s',
|
||||
samples: '8,624,078,250',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeField(unit: string): Field {
|
||||
return {
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
config: {
|
||||
unit,
|
||||
},
|
||||
values: new ArrayVector(),
|
||||
};
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { LegacyRef } from 'react';
|
||||
|
||||
import { createTheme, Field, getDisplayProcessor } from '@grafana/data';
|
||||
import { useStyles, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { TooltipData, SampleUnit } from '../types';
|
||||
|
||||
type Props = {
|
||||
tooltipRef: LegacyRef<HTMLDivElement>;
|
||||
tooltipData: TooltipData;
|
||||
showTooltip: boolean;
|
||||
};
|
||||
|
||||
const FlameGraphTooltip = ({ tooltipRef, tooltipData, showTooltip }: Props) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
return (
|
||||
<div ref={tooltipRef} className={styles.tooltip}>
|
||||
{tooltipData && showTooltip && (
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
<div className={styles.name}>{tooltipData.name}</div>
|
||||
<div>
|
||||
{tooltipData.percentTitle}: <b>{tooltipData.percentValue}%</b>
|
||||
</div>
|
||||
<div>
|
||||
{tooltipData.unitTitle}: <b>{tooltipData.unitValue}</b>
|
||||
</div>
|
||||
<div>
|
||||
Samples: <b>{tooltipData.samples}</b>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
placement={'right'}
|
||||
show={true}
|
||||
>
|
||||
<span></span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getTooltipData = (field: Field, label: string, value: number, totalTicks: number): TooltipData => {
|
||||
let samples = value;
|
||||
let percentTitle = '';
|
||||
let unitTitle = '';
|
||||
|
||||
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
|
||||
const displayValue = processor(value);
|
||||
const percent = Math.round(10000 * (samples / totalTicks)) / 100;
|
||||
let unitValue = displayValue.text + displayValue.suffix;
|
||||
|
||||
switch (field.config.unit) {
|
||||
case SampleUnit.Bytes:
|
||||
percentTitle = '% of total';
|
||||
unitTitle = 'RAM';
|
||||
break;
|
||||
case SampleUnit.Nanoseconds:
|
||||
percentTitle = '% of total time';
|
||||
unitTitle = 'Time';
|
||||
break;
|
||||
default:
|
||||
percentTitle = '% of total';
|
||||
unitTitle = 'Count';
|
||||
if (!displayValue.suffix) {
|
||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||
unitValue = displayValue.text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
name: label,
|
||||
percentTitle: percentTitle,
|
||||
percentValue: percent,
|
||||
unitTitle: unitTitle,
|
||||
unitValue,
|
||||
samples: samples.toLocaleString(),
|
||||
};
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
tooltip: css`
|
||||
position: fixed;
|
||||
`,
|
||||
name: css`
|
||||
margin-bottom: 10px;
|
||||
`,
|
||||
});
|
||||
|
||||
export default FlameGraphTooltip;
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
||||
import { DataFrameView, MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { Item, nestedSetToLevels } from './dataTransform';
|
||||
|
||||
describe('nestedSetToLevels', () => {
|
||||
it('converts nested set data frame to levels', () => {
|
||||
const frame = new MutableDataFrame({
|
||||
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'] },
|
||||
],
|
||||
});
|
||||
const levels = nestedSetToLevels(new DataFrameView<Item>(frame));
|
||||
expect(levels).toEqual([
|
||||
[{ level: 0, value: 10, start: 0, label: '1' }],
|
||||
[
|
||||
{ level: 1, value: 5, start: 0, label: '2' },
|
||||
{ level: 1, value: 4, start: 5, label: '6' },
|
||||
],
|
||||
[
|
||||
{ level: 2, value: 3, start: 0, label: '3' },
|
||||
{ level: 2, value: 1, start: 3, label: '5' },
|
||||
{ level: 2, value: 3, start: 5, label: '7' },
|
||||
],
|
||||
[
|
||||
{ level: 3, value: 1, start: 0, label: '4' },
|
||||
{ level: 3, value: 2, start: 5, label: '8' },
|
||||
],
|
||||
[{ level: 4, value: 1, start: 5, label: '9' }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('converts nested set data if multiple same level items', () => {
|
||||
const frame = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'level', values: [0, 1, 1, 1] },
|
||||
{ name: 'value', values: [10, 5, 3, 1] },
|
||||
{ name: 'label', values: ['1', '2', '3', '4'] },
|
||||
],
|
||||
});
|
||||
const levels = nestedSetToLevels(new DataFrameView<Item>(frame));
|
||||
expect(levels).toEqual([
|
||||
[{ level: 0, value: 10, start: 0, label: '1' }],
|
||||
[
|
||||
{ level: 1, value: 5, start: 0, label: '2' },
|
||||
{ level: 1, value: 3, start: 5, label: '3' },
|
||||
{ level: 1, value: 1, start: 8, label: '4' },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
import { DataFrameView } from '@grafana/data';
|
||||
|
||||
export type Item = { level: number; value: number; label: string; self: number };
|
||||
export type ItemWithStart = Item & { start: number };
|
||||
|
||||
/**
|
||||
* Convert data frame with nested set format into array of level. This is mainly done for compatibility with current
|
||||
* rendering code.
|
||||
* @param dataView
|
||||
*/
|
||||
export function nestedSetToLevels(dataView: DataFrameView<Item>): ItemWithStart[][] {
|
||||
const levels: ItemWithStart[][] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < dataView.length; i++) {
|
||||
// We have to clone the items as .get(i) returns a changing pointer not the data themselves.
|
||||
const item = { ...dataView.get(i) };
|
||||
const prevItem = i > 0 ? { ...dataView.get(i - 1) } : undefined;
|
||||
|
||||
levels[item.level] = levels[item.level] || [];
|
||||
if (prevItem && prevItem.level >= item.level) {
|
||||
// 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 lastItem = levels[item.level][levels[item.level].length - 1];
|
||||
offset = lastItem.start + lastItem.value;
|
||||
}
|
||||
const newItem: ItemWithStart = {
|
||||
...item,
|
||||
start: offset,
|
||||
};
|
||||
|
||||
levels[item.level].push(newItem);
|
||||
}
|
||||
return levels;
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { ItemWithStart } from './dataTransform';
|
||||
import { getRectDimensionsForLevel } from './rendering';
|
||||
|
||||
describe('getRectDimensionsForLevel', () => {
|
||||
it('should render a single item', () => {
|
||||
const level: ItemWithStart[] = [{ level: 1, start: 0, value: 100, label: '1', self: 0 }];
|
||||
const result = getRectDimensionsForLevel(level, 1, 100, 0, 10);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
width: 999,
|
||||
height: 22,
|
||||
x: 0,
|
||||
y: 22,
|
||||
collapsed: false,
|
||||
ticks: 100,
|
||||
label: '1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a multiple items', () => {
|
||||
const level: ItemWithStart[] = [
|
||||
{ level: 2, start: 0, value: 100, label: '1', self: 0 },
|
||||
{ level: 2, start: 100, value: 50, label: '2', self: 0 },
|
||||
{ level: 2, start: 150, value: 50, label: '3', self: 0 },
|
||||
];
|
||||
const result = getRectDimensionsForLevel(level, 2, 100, 0, 10);
|
||||
expect(result).toEqual([
|
||||
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1' },
|
||||
{ width: 499, height: 22, x: 1000, y: 44, collapsed: false, ticks: 50, label: '2' },
|
||||
{ width: 499, height: 22, x: 1500, y: 44, collapsed: false, ticks: 50, label: '3' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a collapsed items', () => {
|
||||
const level: ItemWithStart[] = [
|
||||
{ level: 2, start: 0, value: 100, label: '1', self: 0 },
|
||||
{ level: 2, start: 100, value: 2, label: '2', self: 0 },
|
||||
{ level: 2, start: 102, value: 1, label: '3', self: 0 },
|
||||
];
|
||||
const result = getRectDimensionsForLevel(level, 2, 100, 0, 1);
|
||||
expect(result).toEqual([
|
||||
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1' },
|
||||
{ width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2' },
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,134 @@
|
||||
import { colors, fuzzyMatch } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
BAR_BORDER_WIDTH,
|
||||
BAR_TEXT_PADDING_LEFT,
|
||||
COLLAPSE_THRESHOLD,
|
||||
HIDE_THRESHOLD,
|
||||
LABEL_THRESHOLD,
|
||||
PIXELS_PER_LEVEL,
|
||||
} from '../../constants';
|
||||
|
||||
import { ItemWithStart } from './dataTransform';
|
||||
|
||||
type RectData = {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
collapsed: boolean;
|
||||
ticks: number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param level
|
||||
* @param levelIndex
|
||||
* @param totalTicks
|
||||
* @param rangeMin
|
||||
* @param pixelsPerTick
|
||||
*/
|
||||
export function getRectDimensionsForLevel(
|
||||
level: ItemWithStart[],
|
||||
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 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,
|
||||
label: item.label,
|
||||
});
|
||||
}
|
||||
return coordinatesLevel;
|
||||
}
|
||||
|
||||
export function renderRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
rect: RectData,
|
||||
totalTicks: number,
|
||||
rangeMin: number,
|
||||
rangeMax: number,
|
||||
query: string,
|
||||
levelIndex: number,
|
||||
topLevelIndex: number
|
||||
) {
|
||||
if (rect.width < HIDE_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height);
|
||||
|
||||
// / (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, rect.ticks / totalTicks / (rangeMax - rangeMin));
|
||||
const h = 50 - 50 * intensity;
|
||||
const l = 65 + 7 * intensity;
|
||||
|
||||
const name = rect.label;
|
||||
const queryResult = query && fuzzyMatch(name.toLowerCase(), query.toLowerCase()).found;
|
||||
|
||||
if (!rect.collapsed) {
|
||||
ctx.stroke();
|
||||
|
||||
if (query) {
|
||||
ctx.fillStyle = queryResult ? getBarColor(h, l) : colors[55];
|
||||
} else {
|
||||
ctx.fillStyle = levelIndex > topLevelIndex - 1 ? getBarColor(h, l) : getBarColor(h, l + 15);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = queryResult ? getBarColor(h, l) : colors[55];
|
||||
}
|
||||
ctx.fill();
|
||||
|
||||
if (!rect.collapsed && rect.width >= LABEL_THRESHOLD) {
|
||||
ctx.save();
|
||||
ctx.clip(); // so text does not overflow
|
||||
ctx.fillStyle = '#222';
|
||||
ctx.fillText(`${name}`, Math.max(rect.x, 0) + BAR_TEXT_PADDING_LEFT, 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;
|
||||
}
|
||||
|
||||
function getBarColor(h: number, l: number) {
|
||||
return `hsl(${h}, 100%, ${l}%)`;
|
||||
}
|
@ -0,0 +1,572 @@
|
||||
export const data = {
|
||||
version: 1,
|
||||
flamebearer: {
|
||||
names: [
|
||||
'total',
|
||||
'runtime.mcall',
|
||||
'runtime.park_m',
|
||||
'runtime.schedule',
|
||||
'runtime.resetspinning',
|
||||
'runtime.wakep',
|
||||
'runtime.startm',
|
||||
'runtime.newm',
|
||||
'runtime.allocm',
|
||||
'github.com/bufbuild/connect-go.(*duplexHTTPCall).makeRequest',
|
||||
'net/http.(*Client).Do',
|
||||
'net/http.(*Client).do',
|
||||
'net/http.(*Client).send',
|
||||
'net/http.send',
|
||||
'test/pkg/util.RoundTripperFunc.RoundTrip',
|
||||
'test/pkg/util.WrapWithInstrumentedHTTPTransport.func1',
|
||||
'github.com/opentracing-contrib/go-stdlib/nethttp.(*Transport).RoundTrip',
|
||||
'github.com/opentracing-contrib/go-stdlib/nethttp.(*Tracer).start',
|
||||
'github.com/uber/jaeger-client-go.(*Tracer).StartSpan',
|
||||
'github.com/uber/jaeger-client-go.(*Tracer).startSpanWithOptions',
|
||||
'github.com/uber/jaeger-client-go.(*Tracer).randomID',
|
||||
'github.com/uber/jaeger-client-go.NewTracer.func2',
|
||||
'sync.(*Pool).Get',
|
||||
'sync.(*Pool).pin',
|
||||
'sync.(*Pool).pinSlow',
|
||||
'runtime.mstart',
|
||||
'runtime.mstart0',
|
||||
'runtime.mstart1',
|
||||
'golang.org/x/net/http2.(*serverConn).writeFrameAsync',
|
||||
'golang.org/x/net/http2.(*writeResHeaders).writeFrame',
|
||||
'golang.org/x/net/http2.splitHeaderBlock',
|
||||
'golang.org/x/net/http2.(*writeResHeaders).writeHeaderBlock',
|
||||
'golang.org/x/net/http2.(*Framer).WriteHeaders',
|
||||
'golang.org/x/net/http2.(*Framer).endWrite',
|
||||
'golang.org/x/net/http2.(*bufferedWriter).Write',
|
||||
'golang.org/x/net/http2.glob..func8',
|
||||
'bufio.NewWriterSize',
|
||||
'regexp/syntax.(*compiler).compile',
|
||||
'regexp/syntax.(*compiler).rune',
|
||||
'regexp/syntax.(*compiler).inst',
|
||||
'runtime.systemstack',
|
||||
'runtime.newproc.func1',
|
||||
'runtime.newproc1',
|
||||
'runtime.malg',
|
||||
'google.golang.org/grpc/internal/transport.newHTTP2Client.func3',
|
||||
'google.golang.org/grpc/internal/transport.(*loopyWriter).run',
|
||||
'google.golang.org/grpc/internal/transport.(*loopyWriter).handle',
|
||||
'google.golang.org/grpc/internal/transport.(*loopyWriter).headerHandler',
|
||||
'google.golang.org/grpc/internal/transport.(*loopyWriter).originateStream',
|
||||
'google.golang.org/grpc/internal/transport.(*loopyWriter).writeHeader',
|
||||
'golang.org/x/net/http2/hpack.(*Encoder).WriteField',
|
||||
'golang.org/x/net/http2/hpack.(*dynamicTable).add',
|
||||
'golang.org/x/net/http2/hpack.(*headerFieldTable).addEntry',
|
||||
'net/http.(*persistConn).readLoop',
|
||||
'net/http.(*persistConn).readResponse',
|
||||
'net/http.ReadResponse',
|
||||
'net/http.serverHandler.ServeHTTP',
|
||||
'net/http.HandlerFunc.ServeHTTP',
|
||||
'test/pkg/util.glob..func1.1',
|
||||
'golang.org/x/net/http2/h2c.h2cHandler.ServeHTTP',
|
||||
'test/pkg/create.(*create).initServer.func2.1',
|
||||
'github.com/opentracing-contrib/go-stdlib/nethttp.MiddlewareFunc.func5',
|
||||
'github.com/weaveworks/common/middleware.Log.Wrap.func1',
|
||||
'github.com/weaveworks/common/middleware.Instrument.Wrap.func1',
|
||||
'github.com/felixge/httpsnoop.CaptureMetricsFn',
|
||||
'github.com/felixge/httpsnoop.(*Metrics).CaptureMetrics',
|
||||
'github.com/weaveworks/common/middleware.Instrument.Wrap.func1.2',
|
||||
'github.com/gorilla/mux.(*Router).ServeHTTP',
|
||||
'net/http.(*ServeMux).ServeHTTP',
|
||||
'net/http/pprof.Index',
|
||||
'net/http/pprof.handler.ServeHTTP',
|
||||
'runtime/pprof.(*Profile).WriteTo',
|
||||
'runtime/pprof.writeGoroutine',
|
||||
'runtime/pprof.writeRuntimeProfile',
|
||||
'runtime/pprof.printCountProfile',
|
||||
'runtime/pprof.(*profileBuilder).appendLocsForStack',
|
||||
'runtime/pprof.(*profileBuilder).emitLocation',
|
||||
'runtime/pprof.(*profileBuilder).flush',
|
||||
'compress/gzip.(*Writer).Write',
|
||||
'compress/flate.NewWriter',
|
||||
'compress/flate.(*compressor).init',
|
||||
'compress/flate.newHuffmanBitWriter',
|
||||
'compress/flate.newHuffmanEncoder',
|
||||
'test/pkg/create.(*create).Run.func3',
|
||||
'github.com/weaveworks/common/signals.(*Handler).Loop',
|
||||
'runtime.gcBgMarkWorker',
|
||||
'runtime.gcMarkDone',
|
||||
'runtime.semacquire',
|
||||
'runtime.semacquire1',
|
||||
'runtime.acquireSudog',
|
||||
'test/dskit/services.(*BasicService).main',
|
||||
'test/dskit/ring.(*Ring).loop',
|
||||
'test/dskit/kv.metrics.WatchKey',
|
||||
'github.com/weaveworks/common/instrument.CollectedRequest',
|
||||
'test/dskit/kv.metrics.WatchKey.func1',
|
||||
'test/dskit/kv.(*prefixedKVClient).WatchKey',
|
||||
'test/dskit/kv/memberlist.(*Client).WatchKey',
|
||||
'test/dskit/kv/memberlist.(*KV).WatchKey',
|
||||
'test/dskit/kv/memberlist.(*KV).get',
|
||||
'test/dskit/kv/memberlist.ValueDesc.Clone',
|
||||
'test/dskit/ring.(*Desc).Clone',
|
||||
'github.com/gogo/protobuf/proto.Clone',
|
||||
'github.com/gogo/protobuf/proto.Merge',
|
||||
'test/dskit/ring.(*Desc).XXX_Merge',
|
||||
'github.com/gogo/protobuf/proto.(*InternalMessageInfo).Merge',
|
||||
'github.com/gogo/protobuf/proto.(*mergeInfo).merge',
|
||||
'github.com/gogo/protobuf/proto.(*mergeInfo).computeMergeInfo.func31',
|
||||
'reflect.Value.SetMapIndex',
|
||||
'reflect.mapassign_faststr',
|
||||
'runtime/pprof.profileWriter',
|
||||
'runtime/pprof.(*profileBuilder).addCPUData',
|
||||
'runtime/pprof.(*profMap).lookup',
|
||||
'runtime/pprof.newProfileBuilder',
|
||||
'compress/gzip.NewWriterLevel',
|
||||
'runtime/pprof.(*profileBuilder).build',
|
||||
'compress/flate.newDeflateFast',
|
||||
'github.com/hashicorp/memberlist.(*Memberlist).triggerFunc',
|
||||
'github.com/hashicorp/memberlist.(*Memberlist).gossip',
|
||||
'github.com/armon/go-metrics.MeasureSince',
|
||||
'github.com/armon/go-metrics.(*Metrics).MeasureSince',
|
||||
'github.com/armon/go-metrics.(*Metrics).MeasureSinceWithLabels',
|
||||
'github.com/armon/go-metrics/prometheus.(*PrometheusSink).AddSampleWithLabels',
|
||||
'github.com/armon/go-metrics/prometheus.flattenKey',
|
||||
'regexp.(*Regexp).ReplaceAllString',
|
||||
'regexp.(*Regexp).replaceAll',
|
||||
'regexp.(*Regexp).doExecute',
|
||||
'regexp.(*Regexp).backtrack',
|
||||
'regexp.(*bitState).reset',
|
||||
'runtime.main',
|
||||
'main.main',
|
||||
'test/pkg/create.New',
|
||||
'github.com/prometheus/common/config.NewClientFromConfig',
|
||||
'github.com/prometheus/common/config.NewRoundTripperFromConfig',
|
||||
'github.com/mwitkow/go-conntrack.NewDialContextFunc',
|
||||
'github.com/mwitkow/go-conntrack.PreRegisterDialerMetrics',
|
||||
'github.com/prometheus/client_golang/prometheus.(*CounterVec).WithLabelValues',
|
||||
'github.com/prometheus/client_golang/prometheus.(*CounterVec).GetMetricWithLabelValues',
|
||||
'github.com/prometheus/client_golang/prometheus.(*MetricVec).GetMetricWithLabelValues',
|
||||
'github.com/prometheus/client_golang/prometheus.(*metricMap).getOrCreateMetricWithLabelValues',
|
||||
'test/pkg/create.(*create).Run',
|
||||
'test/dskit/modules.(*Manager).InitModuleServices',
|
||||
'test/dskit/modules.(*Manager).initModule',
|
||||
'test/pkg/create.(*create).initcreate',
|
||||
'test/pkg/create.New',
|
||||
'test/pkg/create.(*create).initHead',
|
||||
'test/pkg/create.NewHead',
|
||||
'test/pkg/create.(*deduplicatingSlice[...]).Init',
|
||||
'github.com/segmentio/parquet-go.NewWriter',
|
||||
'github.com/segmentio/parquet-go.(*Writer).configure',
|
||||
'github.com/segmentio/parquet-go.newWriter',
|
||||
'runtime.doInit',
|
||||
'test/dskit/ring.init',
|
||||
'html/template.(*Template).Parse',
|
||||
'text/template.(*Template).Parse',
|
||||
'text/template/parse.Parse',
|
||||
'text/template/parse.(*Tree).Parse',
|
||||
'text/template/parse.(*Tree).parse',
|
||||
'text/template/parse.(*Tree).textOrAction',
|
||||
'text/template/parse.(*Tree).action',
|
||||
'text/template/parse.(*Tree).rangeControl',
|
||||
'text/template/parse.(*Tree).parseControl',
|
||||
'text/template/parse.(*Tree).itemList',
|
||||
'text/template/parse.(*Tree).pipeline',
|
||||
'text/template/parse.(*PipeNode).append',
|
||||
'text/template/parse.(*Tree).newPipeline',
|
||||
'google.golang.org/protobuf/types/known/structpb.init',
|
||||
'github.com/prometheus/prometheus/scrape.init',
|
||||
'fmt.Errorf',
|
||||
'github.com/prometheus/prometheus/discovery/consul.init',
|
||||
'github.com/prometheus/client_golang/prometheus.(*SummaryVec).WithLabelValues',
|
||||
'github.com/prometheus/client_golang/prometheus.(*SummaryVec).GetMetricWithLabelValues',
|
||||
'github.com/prometheus/client_golang/prometheus.NewSummaryVec.func1',
|
||||
'github.com/prometheus/client_golang/prometheus.newSummary',
|
||||
'github.com/prometheus/client_golang/prometheus.(*summary).newStream',
|
||||
'github.com/beorn7/perks/quantile.NewTargeted',
|
||||
'github.com/beorn7/perks/quantile.newStream',
|
||||
'encoding/gob.init',
|
||||
'encoding/gob.mustGetTypeInfo',
|
||||
'encoding/gob.getTypeInfo',
|
||||
'encoding/gob.buildTypeInfo',
|
||||
'encoding/gob.getBaseType',
|
||||
'encoding/gob.getType',
|
||||
'encoding/gob.newTypeObject',
|
||||
'encoding/gob.userType',
|
||||
'encoding/gob.validUserType',
|
||||
'sync.(*Map).LoadOrStore',
|
||||
'sync.(*Map).dirtyLocked',
|
||||
'go.opentelemetry.io/otel/trace.init',
|
||||
'regexp.MustCompile',
|
||||
'regexp.Compile',
|
||||
'regexp.compile',
|
||||
'regexp.compileOnePass',
|
||||
'regexp.onePassCopy',
|
||||
'cloud.google.com/go/storage.init',
|
||||
'regexp/syntax.Compile',
|
||||
'github.com/aws/aws-sdk-go/aws/endpoints.init',
|
||||
'github.com/asaskevich/govalidator.init',
|
||||
'regexp/syntax.(*Regexp).CapNames',
|
||||
'github.com/goccy/go-json/internal/decoder.init.0',
|
||||
'k8s.io/api/flowcontrol/v1beta2.init',
|
||||
'k8s.io/kube-openapi/pkg/handler3.init.0',
|
||||
'mime.AddExtensionType',
|
||||
'sync.(*Once).Do',
|
||||
'sync.(*Once).doSlow',
|
||||
'mime.initMime',
|
||||
'mime.initMimeUnix',
|
||||
'mime.loadMimeFile',
|
||||
'mime.setExtensionType',
|
||||
'sync.(*Map).Store',
|
||||
'google.golang.org/genproto/googleapis/rpc/errdetails.init.0',
|
||||
'google.golang.org/genproto/googleapis/rpc/errdetails.file_google_rpc_error_details_proto_init',
|
||||
'google.golang.org/protobuf/internal/filetype.Builder.Build',
|
||||
'google.golang.org/protobuf/internal/filedesc.Builder.Build',
|
||||
'google.golang.org/protobuf/internal/filedesc.newRawFile',
|
||||
'google.golang.org/protobuf/internal/filedesc.(*File).unmarshalSeed',
|
||||
'google.golang.org/protobuf/internal/filedesc.(*Message).unmarshalSeed',
|
||||
'google.golang.org/protobuf/internal/filedesc.appendFullName',
|
||||
'google.golang.org/protobuf/internal/strs.(*Builder).AppendFullName',
|
||||
'google.golang.org/genproto/googleapis/type/color.init.0',
|
||||
'google.golang.org/genproto/googleapis/type/color.file_google_type_color_proto_init',
|
||||
'google.golang.org/protobuf/reflect/protoregistry.(*Files).RegisterFile',
|
||||
'google.golang.org/protobuf/reflect/protoregistry.rangeTopLevelDescriptors',
|
||||
'google.golang.org/protobuf/reflect/protoregistry.(*Files).RegisterFile.func2',
|
||||
'github.com/goccy/go-json/internal/encoder.init.0',
|
||||
'google.golang.org/protobuf/types/descriptorpb.init.0',
|
||||
'google.golang.org/protobuf/types/descriptorpb.file_google_protobuf_descriptor_proto_init',
|
||||
'google.golang.org/protobuf/internal/filedesc.(*File).initDecls',
|
||||
'golang.org/x/net/http2.(*serverConn).runHandler',
|
||||
'github.com/weaveworks/common/middleware.Tracer.Wrap.func1',
|
||||
'github.com/weaveworks/common/middleware.getRouteName',
|
||||
'github.com/gorilla/mux.(*Router).Match',
|
||||
'github.com/gorilla/mux.(*Route).Match',
|
||||
'github.com/gorilla/mux.(*routeRegexp).Match',
|
||||
'regexp.(*Regexp).MatchString',
|
||||
'regexp.(*Regexp).doMatch',
|
||||
'test/pkg/agent.(*Target).start.func1',
|
||||
'test/pkg/agent.(*Target).scrape',
|
||||
'github.com/prometheus/prometheus/util/pool.(*Pool).Get',
|
||||
'test/pkg/agent.glob..func1',
|
||||
'test/pkg/agent.(*Target).fetchProfile',
|
||||
'io/ioutil.ReadAll',
|
||||
'io.ReadAll',
|
||||
'test/pkg/distributor.(*Distributor).Push',
|
||||
'compress/flate.(*compressor).initDeflate',
|
||||
'compress/gzip.(*Reader).Read',
|
||||
'compress/flate.(*decompressor).Read',
|
||||
'compress/flate.(*decompressor).nextBlock',
|
||||
'compress/flate.(*decompressor).readHuffman',
|
||||
'compress/flate.(*huffmanDecoder).init',
|
||||
'compress/gzip.NewReader',
|
||||
'compress/gzip.(*Reader).Reset',
|
||||
'compress/gzip.(*Reader).readHeader',
|
||||
'compress/flate.NewReader',
|
||||
'compress/flate.(*dictDecoder).init',
|
||||
'test/pkg/gen/google/v1.(*Profile).UnmarshalVT',
|
||||
'test/pkg/gen/google/v1.(*Location).UnmarshalVT',
|
||||
'test/pkg/gen/google/v1.(*Sample).UnmarshalVT',
|
||||
'test/pkg/distributor.sanitizeProfile',
|
||||
'github.com/samber/lo.Reject[...]',
|
||||
'net/http.(*conn).serve',
|
||||
'net/http.(*response).finishRequest',
|
||||
'net/http.putBufioWriter',
|
||||
'sync.(*Pool).Put',
|
||||
'net/http.(*conn).readRequest',
|
||||
'net/http.newBufioWriterSize',
|
||||
'net/http.readRequest',
|
||||
'net/textproto.(*Reader).ReadMIMEHeader',
|
||||
'net/http.newTextprotoReader',
|
||||
'github.com/uber/jaeger-client-go.NewTracer.func1',
|
||||
'math/rand.NewSource',
|
||||
'fmt.Sprintf',
|
||||
'fmt.newPrinter',
|
||||
'fmt.glob..func1',
|
||||
'regexp.newBitState',
|
||||
'github.com/felixge/httpsnoop.Wrap',
|
||||
'github.com/bufbuild/connect-go.(*Handler).ServeHTTP',
|
||||
'net/http.(*Request).WithContext',
|
||||
'github.com/bufbuild/connect-go.NewUnaryHandler[...].func1',
|
||||
'github.com/bufbuild/connect-go.(*errorTranslatingSender).Send',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryHandlerSender).Send',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryMarshaler).Marshal',
|
||||
'github.com/bufbuild/connect-go.(*bufferPool).Put',
|
||||
'sync.(*poolChain).pushHead',
|
||||
'github.com/bufbuild/connect-go.(*compressionPool).Compress',
|
||||
'github.com/bufbuild/connect-go.(*compressionPool).putCompressor',
|
||||
'compress/gzip.(*Writer).Close',
|
||||
'io.Copy',
|
||||
'io.copyBuffer',
|
||||
'bytes.(*Buffer).WriteTo',
|
||||
'github.com/bufbuild/connect-go.(*protoBinaryCodec).Marshal',
|
||||
'google.golang.org/protobuf/proto.Marshal',
|
||||
'google.golang.org/protobuf/proto.MarshalOptions.marshal',
|
||||
'github.com/bufbuild/connect-go.NewUnaryHandler[...].func1.1',
|
||||
'test/pkg/ingester.(*Ingester).Push',
|
||||
'github.com/klauspost/compress/gzip.NewReader',
|
||||
'github.com/klauspost/compress/gzip.(*Reader).Reset',
|
||||
'github.com/klauspost/compress/gzip.(*Reader).readHeader',
|
||||
'github.com/klauspost/compress/flate.NewReader',
|
||||
'github.com/klauspost/compress/flate.(*dictDecoder).init',
|
||||
'test/pkg/create.(*Head).Ingest',
|
||||
'test/pkg/create.(*deduplicatingSlice[...]).ingest',
|
||||
'test/pkg/model.(*LabelsBuilder).Set',
|
||||
'test/pkg/create.(*Head).convertSamples',
|
||||
'github.com/bufbuild/connect-go.receiveUnaryRequest[...]',
|
||||
'github.com/bufbuild/connect-go.(*errorTranslatingReceiver).Receive',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryHandlerReceiver).Receive',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryUnmarshaler).Unmarshal',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryUnmarshaler).UnmarshalFunc',
|
||||
'bytes.(*Buffer).ReadFrom',
|
||||
'bytes.(*Buffer).grow',
|
||||
'bytes.makeSlice',
|
||||
'github.com/bufbuild/connect-go.(*bufferPool).Get',
|
||||
'net/http/pprof.Profile',
|
||||
'runtime/pprof.StartCPUProfile',
|
||||
'runtime/pprof.writeMutex',
|
||||
'runtime/pprof.writeProfileInternal',
|
||||
'runtime/pprof.printCountCycleProfile',
|
||||
'runtime/pprof.writeBlock',
|
||||
'runtime/pprof.writeAlloc',
|
||||
'runtime/pprof.writeHeapInternal',
|
||||
'runtime/pprof.writeHeapProto',
|
||||
'runtime/pprof.(*protobuf).strings',
|
||||
'runtime/pprof.(*protobuf).string',
|
||||
'runtime/pprof.(*profileBuilder).stringIndex',
|
||||
'runtime/pprof.(*protobuf).uint64Opt',
|
||||
'runtime/pprof.(*protobuf).uint64',
|
||||
'runtime/pprof.(*protobuf).varint',
|
||||
'runtime/pprof.allFrames',
|
||||
'runtime/pprof.(*profileBuilder).pbSample',
|
||||
'runtime/pprof.printCountProfile.func1',
|
||||
'bytes.(*Buffer).String',
|
||||
'net/http.(*persistConn).writeLoop',
|
||||
'net/http.(*Request).write',
|
||||
'net/http.(*transferWriter).writeBody',
|
||||
'net/http.(*transferWriter).doBodyCopy',
|
||||
'test/pkg/distributor.(*Distributor).Push.func1',
|
||||
'test/pkg/distributor.(*Distributor).sendProfiles',
|
||||
'test/pkg/distributor.(*Distributor).sendProfilesErr',
|
||||
'test/pkg/gen/ingester/v1/ingesterv1connect.(*ingesterServiceClient).Push',
|
||||
'github.com/bufbuild/connect-go.(*Client[...]).CallUnary',
|
||||
'github.com/bufbuild/connect-go.NewClient[...].func2',
|
||||
'github.com/bufbuild/connect-go.NewClient[...].func1',
|
||||
'github.com/bufbuild/connect-go.(*connectClientSender).Send',
|
||||
'github.com/bufbuild/connect-go.(*errorTranslatingReceiver).Close',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryClientReceiver).Close',
|
||||
'github.com/bufbuild/connect-go.(*duplexHTTPCall).CloseRead',
|
||||
'github.com/bufbuild/connect-go.discard',
|
||||
'io.discard.ReadFrom',
|
||||
'io.glob..func1',
|
||||
'github.com/bufbuild/connect-go.receiveUnaryResponse[...]',
|
||||
'github.com/bufbuild/connect-go.(*connectUnaryClientReceiver).Receive',
|
||||
'github.com/bufbuild/connect-go.(*compressionPool).Decompress',
|
||||
'github.com/bufbuild/connect-go.(*compressionPool).getDecompressor',
|
||||
],
|
||||
levels: [
|
||||
[0, 8624078250, 0, 0],
|
||||
[
|
||||
0, 60011939, 0, 335, 0, 1081684, 0, 331, 0, 2765065247, 0, 259, 0, 144858662, 0, 235, 0, 1081684, 0, 227, 0,
|
||||
4523250662, 0, 128, 0, 9691644, 0, 116, 0, 8663322, 0, 109, 0, 1574208, 0, 90, 0, 132657008, 0, 85, 0,
|
||||
304386696, 0, 83, 0, 1049728, 0, 56, 0, 524360, 0, 53, 0, 2624640, 0, 44, 0, 132697488, 0, 40, 0, 545034, 0, 37,
|
||||
0, 1052676, 0, 28, 0, 398371776, 0, 25, 0, 2099200, 0, 9, 0, 132790592, 0, 1,
|
||||
],
|
||||
[
|
||||
0, 60011939, 0, 336, 0, 1081684, 0, 332, 0, 2756669265, 0, 56, 0, 6821582, 0, 263, 0, 1574400, 0, 260, 0,
|
||||
144858662, 0, 236, 0, 1081684, 0, 57, 0, 4255866888, 0, 150, 0, 267383774, 0, 129, 0, 9691644, 0, 117, 0,
|
||||
4444206, 0, 114, 0, 1048752, 0, 112, 0, 3170364, 0, 110, 0, 1574208, 0, 91, 0, 132657008, 0, 86, 0, 304386696,
|
||||
304386696, 84, 0, 1049728, 0, 57, 0, 524360, 0, 54, 0, 2624640, 0, 45, 0, 132697488, 0, 41, 0, 545034, 0, 37, 0,
|
||||
1052676, 0, 29, 0, 398371776, 0, 26, 0, 2099200, 0, 10, 0, 132790592, 0, 2,
|
||||
],
|
||||
[
|
||||
0, 60011939, 0, 337, 0, 1081684, 0, 333, 0, 2756669265, 0, 57, 0, 6296270, 0, 265, 0, 525312, 0, 264, 0,
|
||||
1574400, 0, 261, 0, 135394175, 0, 242, 0, 526980, 0, 239, 0, 8937507, 0, 237, 0, 1081684, 0, 60, 0, 4255866888,
|
||||
0, 150, 0, 135751342, 0, 139, 0, 131632432, 0, 130, 0, 9691644, 0, 118, 0, 4444206, 0, 78, 0, 1048752, 1048752,
|
||||
113, 0, 3170364, 3170364, 111, 0, 1574208, 0, 92, 0, 132657008, 0, 87, 304386696, 1049728, 0, 58, 0, 524360,
|
||||
524360, 55, 0, 2624640, 0, 46, 0, 132697488, 0, 42, 0, 545034, 0, 37, 0, 1052676, 0, 30, 0, 398371776, 0, 27, 0,
|
||||
2099200, 0, 11, 0, 132790592, 0, 3,
|
||||
],
|
||||
[
|
||||
0, 60011939, 0, 338, 0, 1081684, 0, 334, 0, 2756669265, 0, 58, 0, 1049600, 0, 267, 0, 5246670, 5246670, 266, 0,
|
||||
525312, 525312, 36, 0, 1574400, 0, 262, 0, 4248808, 0, 257, 0, 35145141, 29377349, 254, 0, 5380182, 0, 249, 0,
|
||||
5283874, 0, 240, 0, 85336170, 0, 78, 0, 526980, 0, 240, 0, 8937507, 8937507, 238, 0, 1081684, 0, 57, 0,
|
||||
4255866888, 0, 150, 0, 135751342, 0, 140, 0, 131632432, 0, 131, 0, 9691644, 0, 119, 0, 4444206, 1848496, 79,
|
||||
4219116, 1574208, 0, 93, 0, 132657008, 0, 88, 304386696, 1049728, 0, 59, 524360, 2624640, 0, 47, 0, 132697488,
|
||||
132697488, 43, 0, 545034, 0, 37, 0, 1052676, 0, 31, 0, 398371776, 0, 3, 0, 2099200, 0, 12, 0, 132790592, 0, 4,
|
||||
],
|
||||
[
|
||||
0, 60011939, 0, 339, 0, 1081684, 0, 286, 0, 2756669265, 0, 59, 0, 1049600, 0, 22, 5771982, 1574400, 0, 23, 0,
|
||||
4248808, 4248808, 258, 29377349, 4194832, 4194832, 256, 0, 1572960, 1572960, 255, 0, 5380182, 0, 250, 0,
|
||||
5283874, 2137058, 241, 0, 85336170, 67470104, 79, 0, 526980, 526980, 241, 8937507, 1081684, 0, 61, 0,
|
||||
3990564004, 0, 150, 0, 265302884, 0, 151, 0, 135751342, 0, 141, 0, 131632432, 0, 132, 0, 9691644, 0, 120,
|
||||
1848496, 2595710, 1998711, 80, 4219116, 1574208, 0, 94, 0, 132657008, 132657008, 89, 304386696, 1049728, 0, 57,
|
||||
524360, 2624640, 0, 48, 132697488, 545034, 0, 37, 0, 1052676, 0, 32, 0, 398371776, 0, 4, 0, 2099200, 0, 13, 0,
|
||||
132790592, 0, 5,
|
||||
],
|
||||
[
|
||||
0, 60011939, 0, 340, 0, 1081684, 1081684, 287, 0, 2756669265, 0, 57, 0, 1049600, 0, 23, 5771982, 1574400,
|
||||
1574400, 24, 39393949, 5380182, 0, 251, 2137058, 3146816, 0, 244, 67470104, 17866066, 0, 80, 9464487, 1081684,
|
||||
0, 228, 0, 3725614404, 0, 150, 0, 132126624, 0, 166, 0, 132822976, 132822976, 165, 0, 265302884, 0, 152, 0,
|
||||
135751342, 0, 142, 0, 131632432, 0, 133, 0, 9691644, 0, 121, 3847207, 596999, 596999, 115, 4219116, 1574208, 0,
|
||||
95, 437043704, 1049728, 0, 60, 524360, 2624640, 0, 49, 132697488, 545034, 0, 37, 0, 1052676, 0, 33, 0,
|
||||
398371776, 0, 5, 0, 2099200, 0, 14, 0, 132790592, 0, 6,
|
||||
],
|
||||
[
|
||||
0, 60011939, 0, 341, 1081684, 2756669265, 0, 60, 0, 1049600, 1049600, 24, 46740331, 5380182, 1053446, 252,
|
||||
2137058, 3146816, 0, 245, 67470104, 524864, 524864, 81, 0, 17341202, 17341202, 243, 9464487, 1081684, 0, 229, 0,
|
||||
3725614404, 0, 150, 0, 132126624, 132126624, 167, 132822976, 265302884, 0, 153, 0, 135751342, 0, 143, 0,
|
||||
131632432, 0, 134, 0, 9691644, 0, 122, 8663322, 1574208, 0, 96, 437043704, 1049728, 0, 57, 524360, 2624640, 0,
|
||||
50, 132697488, 545034, 0, 37, 0, 1052676, 0, 34, 0, 398371776, 0, 6, 0, 2099200, 0, 15, 0, 132790592, 0, 7,
|
||||
],
|
||||
[
|
||||
0, 35515758, 0, 349, 0, 7925912, 0, 343, 0, 16570269, 0, 278, 1081684, 2756669265, 0, 57, 48843377, 4326736,
|
||||
4326736, 253, 2137058, 3146816, 0, 246, 94800657, 1081684, 0, 230, 0, 3457203921, 0, 150, 0, 268410483, 0, 168,
|
||||
264949600, 265302884, 0, 154, 0, 135751342, 0, 144, 0, 131632432, 0, 135, 0, 9691644, 0, 123, 8663322, 1574208,
|
||||
0, 97, 437043704, 1049728, 0, 61, 524360, 2624640, 0, 51, 132697488, 545034, 0, 37, 0, 1052676, 0, 22, 0,
|
||||
398371776, 0, 7, 0, 2099200, 0, 16, 0, 132790592, 132790592, 8,
|
||||
],
|
||||
[
|
||||
0, 35515758, 0, 304, 0, 7925912, 0, 344, 0, 16570269, 0, 342, 1081684, 2756669265, 0, 61, 55307171, 3146816, 0,
|
||||
247, 94800657, 1081684, 0, 231, 0, 3324445713, 0, 150, 0, 132758208, 0, 176, 0, 268410483, 0, 169, 264949600,
|
||||
265302884, 0, 155, 0, 135751342, 0, 145, 0, 131632432, 0, 136, 0, 9691644, 0, 124, 8663322, 1574208, 0, 98,
|
||||
437043704, 1049728, 0, 57, 524360, 2624640, 2624640, 52, 132697488, 545034, 0, 37, 0, 1052676, 0, 35, 0,
|
||||
398371776, 398371776, 8, 0, 2099200, 0, 17,
|
||||
],
|
||||
[
|
||||
0, 35515758, 0, 350, 0, 7925912, 0, 345, 0, 16570269, 0, 280, 1081684, 2740098251, 0, 57, 0, 13936114, 0, 228,
|
||||
0, 2634900, 0, 18, 55307171, 3146816, 3146816, 248, 94800657, 1081684, 0, 232, 0, 133423345, 0, 224, 0,
|
||||
2527422102, 0, 150, 0, 264582108, 0, 196, 0, 265453672, 265453672, 195, 0, 132985149, 0, 193, 0, 579337, 0, 187,
|
||||
0, 132758208, 0, 177, 0, 268410483, 0, 170, 264949600, 265302884, 0, 156, 0, 135751342, 0, 146, 0, 131632432, 0,
|
||||
137, 0, 9691644, 0, 125, 8663322, 1574208, 0, 99, 437043704, 1049728, 0, 62, 135846488, 545034, 0, 37, 0,
|
||||
1052676, 1052676, 36, 398371776, 2099200, 0, 18,
|
||||
],
|
||||
[
|
||||
0, 35515758, 0, 306, 0, 7925912, 0, 346, 0, 16045469, 0, 289, 0, 524800, 0, 281, 1081684, 2740098251, 0, 62, 0,
|
||||
11838546, 0, 229, 0, 2097568, 0, 270, 0, 2634900, 0, 19, 153254644, 1081684, 0, 233, 0, 133423345, 0, 225, 0,
|
||||
663692876, 663692876, 223, 0, 550717750, 0, 150, 0, 1313011476, 1313011476, 198, 0, 264582108, 0, 188,
|
||||
265453672, 132985149, 0, 188, 0, 579337, 0, 188, 0, 132758208, 0, 178, 0, 268410483, 0, 137, 264949600,
|
||||
265302884, 0, 157, 0, 135751342, 0, 147, 0, 131632432, 131632432, 138, 0, 9691644, 0, 126, 8663322, 1574208, 0,
|
||||
100, 437043704, 1049728, 0, 57, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 19,
|
||||
],
|
||||
[
|
||||
0, 35515758, 0, 307, 0, 7925912, 0, 286, 0, 16045469, 0, 290, 0, 524800, 0, 262, 1081684, 2740098251, 0, 57, 0,
|
||||
11838546, 0, 230, 0, 2097568, 0, 271, 0, 2634900, 0, 20, 153254644, 1081684, 0, 234, 0, 133423345, 0, 211,
|
||||
663692876, 143277442, 0, 218, 0, 134728066, 0, 150, 0, 140030946, 0, 200, 0, 132681296, 132681296, 199,
|
||||
1313011476, 264582108, 0, 189, 265453672, 132985149, 0, 189, 0, 579337, 0, 189, 0, 132758208, 0, 179, 0,
|
||||
268410483, 0, 138, 264949600, 265302884, 0, 158, 0, 135751342, 0, 148, 131632432, 9691644, 9691644, 127,
|
||||
8663322, 1574208, 0, 101, 437043704, 1049728, 0, 63, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 20,
|
||||
],
|
||||
[
|
||||
0, 35515758, 0, 351, 0, 7925912, 0, 287, 0, 16045469, 16045469, 291, 0, 524800, 0, 23, 1081684, 2740098251, 0,
|
||||
63, 0, 11838546, 0, 231, 0, 2097568, 0, 22, 0, 2634900, 0, 21, 153254644, 1081684, 0, 125, 0, 133423345, 0, 212,
|
||||
663692876, 143277442, 0, 219, 0, 134728066, 0, 209, 0, 140030946, 0, 201, 1445692772, 264582108, 0, 190,
|
||||
265453672, 132985149, 0, 190, 0, 579337, 0, 190, 0, 132758208, 0, 180, 0, 268410483, 0, 171, 264949600,
|
||||
265302884, 0, 159, 0, 135751342, 0, 149, 149987398, 1574208, 0, 102, 437043704, 1049728, 0, 64, 135846488,
|
||||
545034, 0, 37, 399424452, 2099200, 0, 21,
|
||||
],
|
||||
[
|
||||
0, 34466158, 0, 352, 0, 1049600, 0, 308, 0, 7925912, 0, 347, 16045469, 524800, 524800, 24, 1081684, 2740098251,
|
||||
0, 64, 0, 11838546, 0, 232, 0, 2097568, 2097568, 272, 0, 2634900, 0, 22, 153254644, 1081684, 0, 126, 0,
|
||||
133423345, 0, 213, 663692876, 143277442, 0, 211, 0, 134728066, 0, 210, 0, 140030946, 0, 202, 1445692772,
|
||||
132122592, 132122592, 197, 0, 132459516, 0, 194, 265453672, 132985149, 0, 194, 0, 579337, 0, 191, 0, 132758208,
|
||||
0, 181, 0, 268410483, 0, 172, 264949600, 265302884, 0, 160, 0, 135751342, 135751342, 36, 149987398, 1574208, 0,
|
||||
103, 437043704, 1049728, 0, 65, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 22,
|
||||
],
|
||||
[
|
||||
0, 34466158, 0, 250, 0, 1049600, 0, 309, 0, 7925912, 0, 22, 17651953, 2740098251, 0, 65, 0, 11838546, 0, 233,
|
||||
2097568, 2634900, 0, 268, 153254644, 1081684, 1081684, 127, 0, 133423345, 133423345, 226, 663692876, 143277442,
|
||||
0, 212, 0, 134728066, 0, 211, 0, 140030946, 0, 203, 1577815364, 132459516, 0, 37, 265453672, 132985149, 0, 37,
|
||||
0, 579337, 579337, 192, 0, 132758208, 0, 182, 0, 268410483, 0, 173, 264949600, 265302884, 0, 161, 285738740,
|
||||
1574208, 0, 104, 437043704, 1049728, 0, 66, 135846488, 545034, 0, 37, 399424452, 2099200, 0, 23,
|
||||
],
|
||||
[
|
||||
0, 34466158, 0, 251, 0, 1049600, 1049600, 310, 0, 7925912, 7925912, 348, 17651953, 2739573923, 0, 66, 0, 524328,
|
||||
524328, 274, 0, 11838546, 0, 234, 2097568, 2634900, 2634900, 269, 951452549, 143277442, 0, 220, 0, 134728066, 0,
|
||||
212, 0, 140030946, 0, 204, 1577815364, 132459516, 0, 37, 265453672, 132985149, 0, 37, 579337, 132758208, 0, 183,
|
||||
0, 268410483, 0, 174, 264949600, 265302884, 0, 157, 285738740, 1574208, 0, 105, 437043704, 1049728, 0, 67,
|
||||
135846488, 545034, 0, 37, 399424452, 2099200, 2099200, 24,
|
||||
],
|
||||
[
|
||||
0, 34466158, 5260690, 252, 26627465, 2739573923, 0, 67, 524328, 11838546, 0, 125, 956185017, 143277442, 0, 221,
|
||||
0, 134728066, 0, 213, 0, 140030946, 0, 205, 1577815364, 132459516, 0, 37, 265453672, 132985149, 0, 38, 579337,
|
||||
132758208, 0, 184, 0, 268410483, 268410483, 175, 264949600, 265302884, 0, 158, 285738740, 1574208, 0, 106,
|
||||
437043704, 1049728, 0, 68, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
5260690, 29205468, 29205468, 253, 26627465, 409934141, 0, 68, 0, 2329639782, 0, 275, 524328, 11838546, 0, 126,
|
||||
956185017, 143277442, 143277442, 222, 0, 134728066, 0, 214, 0, 140030946, 0, 206, 1577815364, 132459516, 0, 38,
|
||||
265453672, 132985149, 132985149, 39, 579337, 132758208, 0, 185, 533360083, 265302884, 0, 162, 285738740,
|
||||
1574208, 0, 107, 437043704, 1049728, 0, 57, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 409934141, 0, 57, 0, 2329115366, 0, 277, 0, 524416, 524416, 276, 524328, 524376, 524376, 273, 0,
|
||||
11314170, 11314170, 127, 1099462459, 134728066, 0, 215, 0, 140030946, 0, 207, 1577815364, 132459516, 132459516,
|
||||
39, 399018158, 132758208, 132758208, 186, 533360083, 132657008, 132657008, 164, 0, 132645876, 132645876, 163,
|
||||
285738740, 1574208, 1574208, 108, 437043704, 1049728, 0, 69, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 178075376, 0, 69, 0, 231858765, 0, 312, 0, 14574244, 0, 303, 0, 1624251841, 0, 292, 0, 690289281, 0,
|
||||
278, 1112349749, 134728066, 0, 216, 0, 140030946, 140030946, 208, 3765070865, 1049728, 0, 70, 135846488, 545034,
|
||||
0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 178075376, 0, 70, 0, 231858765, 231858765, 313, 0, 14574244, 0, 304, 0, 1624251841, 0, 293, 0,
|
||||
690289281, 0, 279, 1112349749, 134728066, 134728066, 217, 3905101811, 1049728, 0, 71, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 178075376, 0, 71, 231858765, 14574244, 0, 305, 0, 1595244443, 0, 299, 0, 3274238, 3274238, 241, 0,
|
||||
24651476, 23602660, 254, 0, 1081684, 0, 294, 0, 690289281, 0, 280, 5152179626, 1049728, 0, 72, 135846488,
|
||||
545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 37385630, 0, 72, 0, 95081897, 0, 318, 0, 18338066, 0, 317, 0, 27269783, 0, 314, 231858765, 14574244,
|
||||
0, 306, 0, 1566824927, 1566824927, 302, 0, 1048656, 1048656, 301, 0, 27370860, 27370860, 300, 26876898, 1048816,
|
||||
1048816, 256, 0, 1081684, 0, 295, 0, 624126, 0, 289, 0, 689140843, 0, 283, 0, 524312, 0, 281, 5152179626,
|
||||
1049728, 0, 73, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 37385630, 0, 73, 0, 95081897, 6643414, 319, 0, 18338066, 0, 315, 0, 27269783, 0, 315, 231858765,
|
||||
14574244, 0, 307, 1623170157, 1081684, 0, 296, 0, 624126, 0, 290, 0, 12739870, 0, 286, 0, 676400973, 0, 284, 0,
|
||||
524312, 0, 262, 5152179626, 1049728, 0, 74, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 37385630, 0, 74, 6643414, 88438483, 0, 320, 0, 18338066, 0, 316, 0, 27269783, 0, 316, 231858765,
|
||||
524800, 0, 311, 0, 14049444, 0, 308, 1623170157, 1081684, 0, 297, 0, 624126, 624126, 291, 0, 12739870, 0, 287,
|
||||
0, 676400973, 0, 285, 0, 524312, 524312, 282, 5152179626, 1049728, 0, 75, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 27334886, 0, 75, 0, 9526424, 0, 328, 0, 524320, 0, 329, 6643414, 20792845, 0, 328, 0, 65465889, 0, 75,
|
||||
0, 2179749, 0, 114, 0, 18338066, 0, 114, 0, 27269783, 0, 114, 231858765, 524800, 0, 22, 0, 14049444, 0, 309,
|
||||
1623170157, 1081684, 1081684, 298, 624126, 12739870, 0, 288, 0, 676400973, 0, 78, 5152703938, 1049728, 0, 76,
|
||||
135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
61093623, 26810494, 1585182, 76, 0, 524392, 524392, 327, 0, 9526424, 0, 77, 0, 524320, 524320, 330, 6643414,
|
||||
20792845, 0, 77, 0, 8389952, 8389952, 327, 0, 57075937, 2171836, 76, 0, 2179749, 0, 321, 0, 18338066, 0, 78, 0,
|
||||
27269783, 0, 78, 231858765, 524800, 0, 23, 0, 14049444, 14049444, 310, 1624875967, 12739870, 0, 78, 0,
|
||||
676400973, 557321544, 79, 5152703938, 1049728, 0, 77, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
62678805, 25225312, 0, 77, 524392, 9526424, 0, 78, 7167734, 20792845, 0, 78, 10561788, 49063106, 0, 77, 0,
|
||||
1050624, 0, 324, 0, 4790371, 4790371, 323, 0, 2179749, 2179749, 322, 0, 18338066, 9242480, 79, 0, 27269783,
|
||||
18484960, 79, 231858765, 524800, 524800, 24, 1638925411, 12739870, 11090976, 79, 557321544, 119079429, 0, 80,
|
||||
5152703938, 1049728, 0, 78, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
62678805, 25225312, 0, 78, 524392, 9526424, 4621240, 79, 7167734, 20792845, 11090976, 79, 10561788, 49063106, 0,
|
||||
78, 0, 1050624, 0, 325, 16212600, 9095586, 5670636, 80, 18484960, 8784823, 8227085, 80, 1882399952, 1648894, 0,
|
||||
80, 557321544, 2097312, 0, 81, 0, 116982117, 116982117, 243, 5152703938, 1049728, 0, 79, 135846488, 545034, 0,
|
||||
37,
|
||||
],
|
||||
[
|
||||
62678805, 25225312, 14787968, 79, 5145632, 4905184, 1998711, 80, 18258710, 9701869, 6119875, 80, 10561788,
|
||||
49063106, 25878944, 79, 0, 1050624, 1050624, 326, 21883236, 3424950, 3424950, 115, 26712045, 557738, 557738,
|
||||
115, 1882399952, 524864, 524864, 81, 0, 1124030, 1124030, 243, 557321544, 2097312, 2097312, 82, 5269686055,
|
||||
1049728, 0, 80, 135846488, 545034, 0, 37,
|
||||
],
|
||||
[
|
||||
77466773, 10437344, 6336873, 80, 7144343, 2906473, 2906473, 115, 24378585, 3581994, 3581994, 115, 36440732,
|
||||
23184162, 14346960, 80, 7766782350, 1049728, 0, 81, 135846488, 545034, 0, 38,
|
||||
],
|
||||
[
|
||||
83803646, 4100471, 4100471, 115, 88799087, 8837202, 8837202, 115, 7766782350, 1049728, 1049728, 82, 135846488,
|
||||
545034, 545034, 39,
|
||||
],
|
||||
],
|
||||
numTicks: 8624078250,
|
||||
},
|
||||
timeline: null,
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,90 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
||||
|
||||
import { data } from './FlameGraph/testData/dataNestedSet';
|
||||
import FlameGraphContainer from './FlameGraphContainer';
|
||||
|
||||
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 });
|
||||
|
||||
const FlameGraphContainerWithProps = () => {
|
||||
const flameGraphData = new MutableDataFrame(data);
|
||||
flameGraphData.meta = {
|
||||
custom: {
|
||||
ProfileTypeID: 'cpu:foo:bar',
|
||||
},
|
||||
};
|
||||
|
||||
return <FlameGraphContainer data={flameGraphData} />;
|
||||
};
|
||||
|
||||
it('should render without error', async () => {
|
||||
expect(() => render(<FlameGraphContainerWithProps />)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should update search when row selected in top table', async () => {
|
||||
render(<FlameGraphContainerWithProps />);
|
||||
screen.getAllByRole('row')[1].click();
|
||||
expect(screen.getByDisplayValue('net/http.HandlerFunc.ServeHTTP')).toBeInTheDocument();
|
||||
screen.getAllByRole('row')[2].click();
|
||||
expect(screen.getByDisplayValue('total')).toBeInTheDocument();
|
||||
screen.getAllByRole('row')[2].click();
|
||||
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();
|
||||
|
||||
screen.getByText(/Top Table/).click();
|
||||
expect(screen.queryByTestId('flameGraph')).toBeNull();
|
||||
expect(screen.getByTestId('topTable')).toBeDefined();
|
||||
|
||||
screen.getByText(/Flame Graph/).click();
|
||||
expect(screen.getByTestId('flameGraph')).toBeDefined();
|
||||
expect(screen.queryByTestId('topTable')).toBeNull();
|
||||
|
||||
screen.getByText(/Both/).click();
|
||||
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();
|
||||
});
|
||||
});
|
@ -0,0 +1,92 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { DataFrame, DataFrameView } from '@grafana/data';
|
||||
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
||||
|
||||
import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { Item, nestedSetToLevels } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import { SelectedView } from './types';
|
||||
|
||||
type Props = {
|
||||
data: DataFrame;
|
||||
};
|
||||
|
||||
const FlameGraphContainer = (props: Props) => {
|
||||
const [topLevelIndex, setTopLevelIndex] = useState(0);
|
||||
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>();
|
||||
|
||||
// Transform dataFrame with nested set format to array of levels. Each level contains all the bars for a particular
|
||||
// level of the flame graph. We do this temporary as in the end we should be able to render directly by iterating
|
||||
// over the dataFrame rows.
|
||||
const levels = useMemo(() => {
|
||||
if (!props.data) {
|
||||
return [];
|
||||
}
|
||||
const dataView = new DataFrameView<Item>(props.data);
|
||||
return nestedSetToLevels(dataView);
|
||||
}, [props.data]);
|
||||
|
||||
// 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
|
||||
) {
|
||||
setSelectedView(SelectedView.FlameGraph);
|
||||
}
|
||||
}, [selectedView, setSelectedView, containerWidth]);
|
||||
|
||||
return (
|
||||
<div ref={sizeRef}>
|
||||
<FlameGraphHeader
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
selectedView={selectedView}
|
||||
setSelectedView={setSelectedView}
|
||||
containerWidth={containerWidth}
|
||||
/>
|
||||
|
||||
{selectedView !== SelectedView.FlameGraph && (
|
||||
<FlameGraphTopTableContainer
|
||||
data={props.data}
|
||||
totalLevels={levels.length}
|
||||
selectedView={selectedView}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView !== SelectedView.TopTable && (
|
||||
<FlameGraph
|
||||
data={props.data}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
search={search}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlameGraphContainer;
|
@ -0,0 +1,35 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import { SelectedView } from './types';
|
||||
|
||||
describe('FlameGraphHeader', () => {
|
||||
const FlameGraphHeaderWithProps = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
||||
|
||||
return (
|
||||
<FlameGraphHeader
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
selectedView={selectedView}
|
||||
setSelectedView={setSelectedView}
|
||||
containerWidth={1600}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('reset button should remove search text', async () => {
|
||||
render(<FlameGraphHeaderWithProps />);
|
||||
await userEvent.type(screen.getByPlaceholderText('Search..'), 'abc');
|
||||
expect(screen.getByDisplayValue('abc')).toBeInTheDocument();
|
||||
screen.getByRole('button', { name: /Reset/i }).click();
|
||||
expect(screen.queryByDisplayValue('abc')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,104 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Input, useStyles, RadioButtonGroup } from '@grafana/ui';
|
||||
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
||||
|
||||
import { SelectedView } from './types';
|
||||
|
||||
type Props = {
|
||||
search: string;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
setSearch: (search: string) => void;
|
||||
selectedView: SelectedView;
|
||||
setSelectedView: (view: SelectedView) => void;
|
||||
containerWidth: number;
|
||||
};
|
||||
|
||||
const FlameGraphHeader = ({
|
||||
search,
|
||||
setTopLevelIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
setSearch,
|
||||
selectedView,
|
||||
setSelectedView,
|
||||
containerWidth,
|
||||
}: Props) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
let viewOptions: Array<{ value: string; 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 (containerWidth >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH) {
|
||||
viewOptions.push({
|
||||
value: SelectedView.Both,
|
||||
label: 'Both',
|
||||
description: 'Show both the top table and flame graph',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.leftContainer}>
|
||||
<div className={styles.inputContainer}>
|
||||
<Input
|
||||
value={search || ''}
|
||||
onChange={(v) => {
|
||||
setSearch(v.currentTarget.value);
|
||||
}}
|
||||
placeholder={'Search..'}
|
||||
width={24}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type={'button'}
|
||||
variant={'secondary'}
|
||||
size={'md'}
|
||||
onClick={() => {
|
||||
setTopLevelIndex(0);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
setSearch('');
|
||||
}}
|
||||
>
|
||||
Reset View
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightContainer}>
|
||||
<RadioButtonGroup
|
||||
options={viewOptions}
|
||||
value={selectedView}
|
||||
onChange={(view) => {
|
||||
setSelectedView(view as SelectedView);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
header: css`
|
||||
display: flow-root;
|
||||
padding: 0 0 20px 0;
|
||||
width: 100%;
|
||||
`,
|
||||
inputContainer: css`
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
`,
|
||||
leftContainer: css`
|
||||
float: left;
|
||||
`,
|
||||
rightContainer: css`
|
||||
float: right;
|
||||
`,
|
||||
});
|
||||
|
||||
export default FlameGraphHeader;
|
@ -0,0 +1,257 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { SortByFn, useSortBy, useAbsoluteLayout, useTable, CellProps } from 'react-table';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, useStyles2, CustomScrollbar } from '@grafana/ui';
|
||||
|
||||
import { TOP_TABLE_COLUMN_WIDTH } from '../../constants';
|
||||
import { ColumnTypes, TopTableData, TopTableValue } from '../types';
|
||||
|
||||
type Props = {
|
||||
width: number;
|
||||
height: number;
|
||||
data: TopTableData[];
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
};
|
||||
|
||||
const FlameGraphTopTable = ({
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
search,
|
||||
setSearch,
|
||||
setTopLevelIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
}: Props) => {
|
||||
const styles = useStyles2((theme) => getStyles(theme));
|
||||
|
||||
const sortSymbols: SortByFn<object> = (a, b, column) => {
|
||||
return a.values[column].localeCompare(b.values[column]);
|
||||
};
|
||||
|
||||
const sortUnits: SortByFn<object> = (a, b, column) => {
|
||||
return a.values[column].value.toString().localeCompare(b.values[column].value.toString(), 'en', { numeric: true });
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessor: ColumnTypes.Symbol.toLowerCase(),
|
||||
header: ColumnTypes.Symbol,
|
||||
cell: SymbolCell,
|
||||
sortType: sortSymbols,
|
||||
width: width - TOP_TABLE_COLUMN_WIDTH * 2,
|
||||
},
|
||||
{
|
||||
accessor: ColumnTypes.Self.toLowerCase(),
|
||||
header: ColumnTypes.Self,
|
||||
cell: UnitCell,
|
||||
sortType: sortUnits,
|
||||
width: TOP_TABLE_COLUMN_WIDTH,
|
||||
},
|
||||
{
|
||||
accessor: ColumnTypes.Total.toLowerCase(),
|
||||
header: ColumnTypes.Total,
|
||||
cell: UnitCell,
|
||||
sortType: sortUnits,
|
||||
width: TOP_TABLE_COLUMN_WIDTH,
|
||||
},
|
||||
],
|
||||
[width]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
columns,
|
||||
data,
|
||||
initialState: {
|
||||
sortBy: [
|
||||
{
|
||||
id: ColumnTypes.Self.toLowerCase(),
|
||||
desc: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[columns, data]
|
||||
);
|
||||
|
||||
const rowClicked = useCallback(
|
||||
(row: string) => {
|
||||
if (search === row) {
|
||||
setSearch('');
|
||||
} else {
|
||||
setSearch(row);
|
||||
// Reset selected level in flamegraph when selecting row in top table
|
||||
setTopLevelIndex(0);
|
||||
setRangeMin(0);
|
||||
setRangeMax(1);
|
||||
}
|
||||
},
|
||||
[search, setRangeMax, setRangeMin, setSearch, setTopLevelIndex]
|
||||
);
|
||||
|
||||
const { headerGroups, rows, prepareRow } = useTable(options, useSortBy, useAbsoluteLayout);
|
||||
|
||||
const renderRow = React.useCallback(
|
||||
({ index, style }) => {
|
||||
let row = rows[index];
|
||||
prepareRow(row);
|
||||
|
||||
const rowValue = row.values[ColumnTypes.Symbol.toLowerCase()];
|
||||
const classNames = cx(rowValue === search && styles.matchedRow, styles.row);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...row.getRowProps({ style })}
|
||||
className={classNames}
|
||||
onClick={() => {
|
||||
rowClicked(rowValue);
|
||||
}}
|
||||
>
|
||||
{row.cells.map((cell) => {
|
||||
const { key, ...cellProps } = cell.getCellProps();
|
||||
if (cellProps.style) {
|
||||
cellProps.style.minWidth = cellProps.style.width;
|
||||
}
|
||||
return (
|
||||
<div key={key} className={styles.cell} {...cellProps}>
|
||||
{cell.render('cell')}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[rows, prepareRow, search, styles.matchedRow, styles.row, styles.cell, rowClicked]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.table(height)} data-testid="topTable">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<div key={key} className={styles.header} {...headerGroupProps}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key, ...headerProps } = column.getHeaderProps(
|
||||
column.canSort ? column.getSortByToggleProps() : undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={key} className={styles.headerCell} {...headerProps}>
|
||||
{column.render('header')}
|
||||
{column.isSorted && <Icon name={column.isSortedDesc ? 'arrow-down' : 'arrow-up'} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{rows.length > 0 ? (
|
||||
<CustomScrollbar hideVerticalTrack={true}>
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
itemCount={rows.length}
|
||||
itemSize={38}
|
||||
width={'100%'}
|
||||
style={{ overflow: 'hidden auto' }}
|
||||
>
|
||||
{renderRow}
|
||||
</FixedSizeList>
|
||||
</CustomScrollbar>
|
||||
) : (
|
||||
<div style={{ height: height }} className={styles.noData}>
|
||||
No data
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SymbolCell = ({ cell: { value } }: CellProps<TopTableValue, TopTableValue>) => {
|
||||
return <div>{value}</div>;
|
||||
};
|
||||
|
||||
const UnitCell = ({ cell: { value } }: CellProps<TopTableValue, TopTableValue>) => {
|
||||
return <div>{value.unitValue}</div>;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: (height: number) => {
|
||||
return css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
height: ${height}px;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
},
|
||||
header: css`
|
||||
height: 38px;
|
||||
|
||||
& > :nth-child(2),
|
||||
& > :nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
`,
|
||||
headerCell: css`
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
color: ${theme.colors.primary.text};
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
matchedRow: css`
|
||||
& > :nth-child(1),
|
||||
& > :nth-child(2),
|
||||
& > :nth-child(3) {
|
||||
background-color: ${theme.colors.background.secondary} !important;
|
||||
}
|
||||
`,
|
||||
row: css`
|
||||
border-top: 1px solid ${theme.components.panel.borderColor};
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
||||
}
|
||||
& > :nth-child(2),
|
||||
& > :nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
& > :nth-child(3) {
|
||||
border-right: none;
|
||||
}
|
||||
`,
|
||||
cell: css`
|
||||
border-right: 1px solid ${theme.components.panel.borderColor};
|
||||
padding: ${theme.spacing(1)};
|
||||
|
||||
div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&:hover {
|
||||
overflow: visible;
|
||||
width: auto !important;
|
||||
box-shadow: 0 0 2px ${theme.colors.primary.main};
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
||||
z-index: 1;
|
||||
}
|
||||
`,
|
||||
noData: css`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`,
|
||||
});
|
||||
|
||||
export default FlameGraphTopTable;
|
@ -0,0 +1,63 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataFrameView, MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { Item, nestedSetToLevels } from '../FlameGraph/dataTransform';
|
||||
import { data } from '../FlameGraph/testData/dataNestedSet';
|
||||
import { SelectedView } from '../types';
|
||||
|
||||
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer';
|
||||
|
||||
describe('FlameGraphTopTableContainer', () => {
|
||||
const FlameGraphTopTableContainerWithProps = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, _] = useState(SelectedView.Both);
|
||||
|
||||
const flameGraphData = new MutableDataFrame(data);
|
||||
const dataView = new DataFrameView<Item>(flameGraphData);
|
||||
const levels = nestedSetToLevels(dataView);
|
||||
|
||||
return (
|
||||
<FlameGraphTopTableContainer
|
||||
data={flameGraphData}
|
||||
totalLevels={levels.length}
|
||||
selectedView={selectedView}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('should render without error', async () => {
|
||||
expect(() => render(<FlameGraphTopTableContainerWithProps />)).not.toThrow();
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
render(<FlameGraphTopTableContainerWithProps />);
|
||||
const rows = screen.getAllByRole('row');
|
||||
expect(rows).toHaveLength(17); // + 1 for the columnHeaders
|
||||
|
||||
const columnHeaders = screen.getAllByRole('columnheader');
|
||||
expect(columnHeaders).toHaveLength(3);
|
||||
expect(columnHeaders[0].textContent).toEqual('Symbol');
|
||||
expect(columnHeaders[1].textContent).toEqual('Self');
|
||||
expect(columnHeaders[2].textContent).toEqual('Total');
|
||||
|
||||
const cells = screen.getAllByRole('cell');
|
||||
expect(cells).toHaveLength(48); // 16 rows
|
||||
expect(cells[0].textContent).toEqual('net/http.HandlerFunc.ServeHTTP');
|
||||
expect(cells[1].textContent).toEqual('31.7 K');
|
||||
expect(cells[2].textContent).toEqual('31.7 Bil');
|
||||
expect(cells[24].textContent).toEqual('test/pkg/create.(*create).initServer.func2.1');
|
||||
expect(cells[25].textContent).toEqual('5.58 K');
|
||||
expect(cells[26].textContent).toEqual('5.58 Bil');
|
||||
});
|
||||
});
|
@ -0,0 +1,138 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { createTheme, DataFrame, Field, FieldType, getDisplayProcessor } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||
import { SampleUnit, SelectedView, TableData, TopTableData } from '../types';
|
||||
|
||||
import FlameGraphTopTable from './FlameGraphTopTable';
|
||||
|
||||
type Props = {
|
||||
data: DataFrame;
|
||||
totalLevels: number;
|
||||
selectedView: SelectedView;
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
setTopLevelIndex: (level: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
};
|
||||
|
||||
const FlameGraphTopTableContainer = ({
|
||||
data,
|
||||
totalLevels,
|
||||
selectedView,
|
||||
search,
|
||||
setSearch,
|
||||
setTopLevelIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(() => getStyles(selectedView));
|
||||
const [topTable, setTopTable] = useState<TopTableData[]>();
|
||||
const valueField =
|
||||
data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number);
|
||||
const selfField = data.fields.find((f) => f.name === 'self') ?? data.fields.find((f) => f.type === FieldType.number);
|
||||
|
||||
const sortLevelsIntoTable = useCallback(() => {
|
||||
let label, self, value;
|
||||
let table: { [key: string]: TableData } = {};
|
||||
|
||||
if (data.fields.length === 4) {
|
||||
const valueValues = data.fields[1].values;
|
||||
const selfValues = data.fields[2].values;
|
||||
const labelValues = data.fields[3].values;
|
||||
|
||||
for (let i = 0; i < valueValues.length; i++) {
|
||||
value = valueValues.get(i);
|
||||
self = selfValues.get(i);
|
||||
label = labelValues.get(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;
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}, [data.fields]);
|
||||
|
||||
const getTopTableData = (field: Field, value: number) => {
|
||||
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
|
||||
const displayValue = processor(value);
|
||||
let unitValue = displayValue.text + displayValue.suffix;
|
||||
|
||||
switch (field.config.unit) {
|
||||
case SampleUnit.Bytes:
|
||||
break;
|
||||
case SampleUnit.Nanoseconds:
|
||||
break;
|
||||
default:
|
||||
if (!displayValue.suffix) {
|
||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||
unitValue = displayValue.text;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return unitValue;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const table = sortLevelsIntoTable();
|
||||
|
||||
let topTable: TopTableData[] = [];
|
||||
for (let key in table) {
|
||||
const selfUnit = getTopTableData(selfField!, table[key].self);
|
||||
const valueUnit = getTopTableData(valueField!, table[key].total);
|
||||
|
||||
topTable.push({
|
||||
symbol: key,
|
||||
self: { value: table[key].self, unitValue: selfUnit },
|
||||
total: { value: table[key].total, unitValue: valueUnit },
|
||||
});
|
||||
}
|
||||
|
||||
setTopTable(topTable);
|
||||
}, [data.fields, selfField, sortLevelsIntoTable, valueField]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{topTable && (
|
||||
<div className={styles.topTableContainer}>
|
||||
<AutoSizer style={{ width: '100%', height: PIXELS_PER_LEVEL * totalLevels + 'px' }}>
|
||||
{({ width, height }) => (
|
||||
<FlameGraphTopTable
|
||||
width={width}
|
||||
height={height}
|
||||
data={topTable}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={setTopLevelIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (selectedView: SelectedView) => {
|
||||
const marginRight = '20px';
|
||||
|
||||
return {
|
||||
topTableContainer: css`
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
margin-right: ${marginRight};
|
||||
width: ${selectedView === SelectedView.TopTable ? '100%' : `calc(50% - ${marginRight})`};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export default FlameGraphTopTableContainer;
|
42
public/app/plugins/panel/flamegraph/components/types.ts
Normal file
42
public/app/plugins/panel/flamegraph/components/types.ts
Normal file
@ -0,0 +1,42 @@
|
||||
export type TooltipData = {
|
||||
name: string;
|
||||
percentTitle: string;
|
||||
percentValue: number;
|
||||
unitTitle: string;
|
||||
unitValue: string;
|
||||
samples: string;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface TopTableData {
|
||||
symbol: string;
|
||||
self: TopTableValue;
|
||||
total: TopTableValue;
|
||||
}
|
||||
|
||||
export type TopTableValue = {
|
||||
value: number;
|
||||
unitValue: string;
|
||||
};
|
8
public/app/plugins/panel/flamegraph/constants.ts
Normal file
8
public/app/plugins/panel/flamegraph/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;
|
11
public/app/plugins/panel/flamegraph/module.tsx
Normal file
11
public/app/plugins/panel/flamegraph/module.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PanelPlugin, PanelProps } from '@grafana/data';
|
||||
|
||||
import FlameGraphContainer from './components/FlameGraphContainer';
|
||||
|
||||
export const FlameGraphPanel: React.FunctionComponent<PanelProps> = (props) => {
|
||||
return <FlameGraphContainer data={props.data.series[0]} />;
|
||||
};
|
||||
|
||||
export const plugin = new PanelPlugin(FlameGraphPanel);
|
17
public/app/plugins/panel/flamegraph/plugin.json
Normal file
17
public/app/plugins/panel/flamegraph/plugin.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Flame Graph",
|
||||
"id": "flamegraph",
|
||||
"state": "beta",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "public/img/icn-panel.svg",
|
||||
"large": "public/img/icn-panel.svg"
|
||||
}
|
||||
}
|
||||
}
|
@ -160,6 +160,7 @@ export interface ExploreItemState {
|
||||
showTable?: boolean;
|
||||
showTrace?: boolean;
|
||||
showNodeGraph?: boolean;
|
||||
showFlameGraph?: boolean;
|
||||
|
||||
/**
|
||||
* History of all queries
|
||||
@ -227,6 +228,7 @@ export interface ExplorePanelData extends PanelData {
|
||||
logsFrames: DataFrame[];
|
||||
traceFrames: DataFrame[];
|
||||
nodeGraphFrames: DataFrame[];
|
||||
flameGraphFrames: DataFrame[];
|
||||
graphResult: DataFrame[] | null;
|
||||
tableResult: DataFrame | null;
|
||||
logsResult: LogsModel | null;
|
||||
|
Loading…
Reference in New Issue
Block a user