Feature: Add panel performance metrics to dashboard
This commit adds panel performance metrics to the dashboard.
This commit is contained in:
@@ -13,6 +13,7 @@ import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
|
||||
import { panelLinksBehavior } from './PanelMenuBehavior';
|
||||
import { PanelNotices } from './PanelNotices';
|
||||
import { PanelPerformanceMetrics } from './PanelPerformanceMetrics';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
import { PanelTimeRange } from './panel-timerange/PanelTimeRange';
|
||||
|
||||
@@ -64,6 +65,7 @@ export class LibraryPanelBehavior extends SceneObjectBase<LibraryPanelBehaviorSt
|
||||
})
|
||||
);
|
||||
titleItems.push(new PanelNotices());
|
||||
titleItems.push(new PanelPerformanceMetrics());
|
||||
|
||||
let title;
|
||||
if (config.featureToggles.preferLibraryPanelTitle) {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { Icon, PanelChrome, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
getDashboardAnalyticsAggregator,
|
||||
PanelAnalyticsMetrics,
|
||||
} from '../../dashboard/services/DashboardAnalyticsAggregator';
|
||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
interface PanelPerformanceMetricsState extends SceneObjectState {
|
||||
metrics?: PanelAnalyticsMetrics;
|
||||
}
|
||||
|
||||
export class PanelPerformanceMetrics extends SceneObjectBase<PanelPerformanceMetricsState> {
|
||||
static Component = PanelPerformanceMetricsRenderer;
|
||||
|
||||
constructor() {
|
||||
super({});
|
||||
this.addActivationHandler(this.onActivate);
|
||||
}
|
||||
|
||||
private onActivate = () => {
|
||||
const panel = this.parent;
|
||||
if (!panel || !(panel instanceof VizPanel)) {
|
||||
throw new Error('PanelPerformanceMetrics can be used only as title items for VizPanel');
|
||||
}
|
||||
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
if (!panelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const aggregator = getDashboardAnalyticsAggregator();
|
||||
const panelIdStr = String(panelId);
|
||||
|
||||
// Subscribe to metrics updates - callback will be called with initial metrics if available
|
||||
this._subs.add(
|
||||
aggregator.subscribeToPanelMetrics(panelIdStr, (updatedMetrics) => {
|
||||
this.setState({ metrics: updatedMetrics });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
public getPanel() {
|
||||
const panel = this.parent;
|
||||
|
||||
if (panel && panel instanceof VizPanel) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
function PanelPerformanceMetricsRenderer({ model }: SceneComponentProps<PanelPerformanceMetrics>) {
|
||||
const panel = model.getPanel();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { metrics } = model.useState();
|
||||
const [fakeQueryTime, setFakeQueryTime] = useState(0);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
|
||||
// Get last operation times (most recent operation in each array)
|
||||
const lastQueryTime =
|
||||
metrics && metrics.queryOperations.length > 0
|
||||
? metrics.queryOperations[metrics.queryOperations.length - 1].duration
|
||||
: 0;
|
||||
const lastRenderTime =
|
||||
metrics && metrics.renderOperations.length > 0
|
||||
? metrics.renderOperations[metrics.renderOperations.length - 1].duration
|
||||
: 0;
|
||||
const lastTransformTime =
|
||||
metrics && metrics.transformationOperations.length > 0
|
||||
? metrics.transformationOperations[metrics.transformationOperations.length - 1].duration
|
||||
: 0;
|
||||
|
||||
// Manage fake timer for query time
|
||||
useEffect(() => {
|
||||
// If we have a real query time, stop the fake timer
|
||||
if (lastQueryTime > 0) {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setFakeQueryTime(0);
|
||||
startTimeRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no query operations yet, start the fake timer
|
||||
if (metrics && metrics.queryOperations.length === 0) {
|
||||
// Start timer if not already running
|
||||
if (!intervalRef.current) {
|
||||
startTimeRef.current = Date.now();
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (startTimeRef.current) {
|
||||
setFakeQueryTime(Date.now() - startTimeRef.current);
|
||||
}
|
||||
}, 166); // Update every 166ms for smooth counting
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [lastQueryTime, metrics]);
|
||||
|
||||
// Use fake query time if real one is 0
|
||||
const displayQueryTime = lastQueryTime > 0 ? lastQueryTime : fakeQueryTime;
|
||||
const lastTotalTime = displayQueryTime + lastRenderTime + lastTransformTime;
|
||||
|
||||
// Don't render if panel or metrics are not available (and no fake timer running)
|
||||
if (!panel || (!metrics && fakeQueryTime === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show component if we have any metrics or fake query time is running
|
||||
if (lastTotalTime === 0 && fakeQueryTime === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderMetricRow = (label: string, current: number) => {
|
||||
return (
|
||||
<div>
|
||||
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
|
||||
<strong>{label}:</strong> {formatDuration(current)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tooltipContent = (
|
||||
<div>
|
||||
{renderMetricRow('Query', displayQueryTime)}
|
||||
{renderMetricRow('Render', lastRenderTime)}
|
||||
{renderMetricRow('Transform', lastTransformTime)}
|
||||
<div style={{ marginTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '8px' }}>
|
||||
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
|
||||
<strong>Total:</strong> {formatDuration(lastTotalTime)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const metricsText = [
|
||||
displayQueryTime > 0 && `Q:${formatDuration(displayQueryTime)}`,
|
||||
lastRenderTime > 0 && `R:${formatDuration(lastRenderTime)}`,
|
||||
lastTransformTime > 0 && `T:${formatDuration(lastTransformTime)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltipContent}>
|
||||
<PanelChrome.TitleItem className={styles.metrics}>
|
||||
<Stack gap={1} alignItems={'center'}>
|
||||
<Icon name="tachometer-fast" size="sm" />
|
||||
<div>{metricsText}</div>
|
||||
</Stack>
|
||||
</PanelChrome.TitleItem>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
metrics: css({
|
||||
color: theme.colors.text.link,
|
||||
gap: theme.spacing(0.5),
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.emphasize(theme.colors.text.link, 0.03),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
|
||||
import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../../scene/PanelNotices';
|
||||
import { PanelPerformanceMetrics } from '../../scene/PanelPerformanceMetrics';
|
||||
import { VizPanelHeaderActions } from '../../scene/VizPanelHeaderActions';
|
||||
import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader';
|
||||
import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem';
|
||||
@@ -54,6 +55,7 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel {
|
||||
);
|
||||
|
||||
titleItems.push(new PanelNotices());
|
||||
titleItems.push(new PanelPerformanceMetrics());
|
||||
|
||||
const queryOptions = panel.spec.data.spec.queryOptions;
|
||||
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
|
||||
@@ -110,6 +112,7 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane
|
||||
);
|
||||
|
||||
titleItems.push(new PanelNotices());
|
||||
titleItems.push(new PanelPerformanceMetrics());
|
||||
|
||||
const vizPanelState: VizPanelState = {
|
||||
key: getVizPanelKeyForPanelId(id ?? panel.spec.id),
|
||||
|
||||
@@ -46,6 +46,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../scene/PanelNotices';
|
||||
import { PanelPerformanceMetrics } from '../scene/PanelPerformanceMetrics';
|
||||
import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions';
|
||||
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem';
|
||||
@@ -312,8 +313,9 @@ export function createDashboardSceneFromDashboardModel(
|
||||
// Create profiler once and reuse to avoid duplicate metadata setting
|
||||
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(oldModel.uid, oldModel.title);
|
||||
|
||||
// HACK always on
|
||||
const enableProfiling =
|
||||
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1;
|
||||
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1 || true;
|
||||
const queryController = new behaviors.SceneQueryController(
|
||||
{
|
||||
enableProfiling,
|
||||
@@ -430,6 +432,7 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
|
||||
);
|
||||
|
||||
titleItems.push(new PanelNotices());
|
||||
titleItems.push(new PanelPerformanceMetrics());
|
||||
|
||||
const timeOverrideShown = (panel.timeFrom || panel.timeShift) && !panel.hideTimeOverride;
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
import { PanelNotices } from '../scene/PanelNotices';
|
||||
import { PanelPerformanceMetrics } from '../scene/PanelPerformanceMetrics';
|
||||
import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
|
||||
import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions';
|
||||
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
|
||||
@@ -274,7 +276,11 @@ export function getDefaultVizPanel(): VizPanel {
|
||||
title: newPanelTitle,
|
||||
pluginId: defaultPluginId,
|
||||
seriesLimit: config.panelSeriesLimit,
|
||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
|
||||
titleItems: [
|
||||
new VizPanelLinks({ menu: new VizPanelLinksMenu({}) }),
|
||||
new PanelNotices(),
|
||||
new PanelPerformanceMetrics(),
|
||||
],
|
||||
hoverHeaderOffset: 0,
|
||||
$behaviors: [],
|
||||
subHeader: new VizPanelSubHeader({
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
||||
import { logMeasurement, reportInteraction } from '@grafana/runtime';
|
||||
import { performanceUtils } from '@grafana/scenes';
|
||||
|
||||
@@ -13,7 +15,7 @@ import {
|
||||
/**
|
||||
* Panel metrics structure for analytics
|
||||
*/
|
||||
interface PanelAnalyticsMetrics {
|
||||
export interface PanelAnalyticsMetrics {
|
||||
panelId: string;
|
||||
panelKey: string;
|
||||
pluginId: string;
|
||||
@@ -54,6 +56,7 @@ export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerfo
|
||||
private panelMetrics = new Map<string, PanelAnalyticsMetrics>();
|
||||
private dashboardUID = '';
|
||||
private dashboardTitle = '';
|
||||
private panelMetricsSubject = new Subject<{ panelId: string; metrics: PanelAnalyticsMetrics }>();
|
||||
|
||||
public initialize(uid: string, title: string) {
|
||||
// Clear previous dashboard data and set new context
|
||||
@@ -67,6 +70,8 @@ export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerfo
|
||||
this.panelMetrics.clear();
|
||||
this.dashboardUID = '';
|
||||
this.dashboardTitle = '';
|
||||
// Complete the subject to clean up subscriptions
|
||||
this.panelMetricsSubject.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +79,7 @@ export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerfo
|
||||
*/
|
||||
public clearMetrics() {
|
||||
this.panelMetrics.clear();
|
||||
// Note: We don't emit clear events as subscribers should handle empty metrics gracefully
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +89,37 @@ export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerfo
|
||||
return Array.from(this.panelMetrics.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get panel metrics by panel ID
|
||||
*/
|
||||
public getPanelMetricsByPanelId(panelId: string): PanelAnalyticsMetrics | undefined {
|
||||
for (const metrics of this.panelMetrics.values()) {
|
||||
if (metrics.panelId === panelId) {
|
||||
return metrics;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to panel metrics updates for a specific panel ID
|
||||
* Returns a subscription that emits when metrics for the given panel are updated
|
||||
*/
|
||||
public subscribeToPanelMetrics(panelId: string, callback: (metrics: PanelAnalyticsMetrics) => void): Subscription {
|
||||
// Get initial metrics if available
|
||||
const initialMetrics = this.getPanelMetricsByPanelId(panelId);
|
||||
if (initialMetrics) {
|
||||
callback(initialMetrics);
|
||||
}
|
||||
|
||||
// Subscribe to future updates
|
||||
return this.panelMetricsSubject.subscribe(({ panelId: updatedPanelId, metrics }) => {
|
||||
if (updatedPanelId === panelId) {
|
||||
callback(metrics);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard-level events (we don't need to track these for panel analytics)
|
||||
onDashboardInteractionStart = (data: performanceUtils.DashboardInteractionStartData): void => {
|
||||
// Clear metrics when new dashboard interaction starts
|
||||
@@ -100,13 +137,17 @@ export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerfo
|
||||
|
||||
// Panel-level events
|
||||
onPanelOperationStart = (data: performanceUtils.PanelPerformanceData): void => {
|
||||
console.log('onPanelOperationStart', data.operation);
|
||||
// Start events don't need aggregation, just ensure panel exists
|
||||
this.ensurePanelExists(data.panelKey, data.panelId, data.pluginId, data.pluginVersion);
|
||||
};
|
||||
|
||||
onPanelOperationComplete = (data: performanceUtils.PanelPerformanceData): void => {
|
||||
console.log('onPanelOperationComplete', data.operation);
|
||||
// Aggregate panel metrics without verbose logging (handled by ScenePerformanceLogger)
|
||||
const panel = this.panelMetrics.get(data.panelKey);
|
||||
// Ensure panel exists - it may not have been created by onPanelOperationStart if the panel
|
||||
// was loaded from saved state or if start events were missed
|
||||
let panel = this.panelMetrics.get(data.panelKey);
|
||||
if (!panel) {
|
||||
console.warn('Panel not found for operation completion:', data.panelKey);
|
||||
return;
|
||||
@@ -154,6 +195,8 @@ export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerfo
|
||||
panel.pluginLoadTime += duration;
|
||||
break;
|
||||
}
|
||||
|
||||
this.panelMetricsSubject.next({ panelId: data.panelId, metrics: panel });
|
||||
};
|
||||
|
||||
// Query-level events
|
||||
|
||||
@@ -62,8 +62,9 @@ export function getDashboardSceneProfilerWithMetadata(uid: string, title: string
|
||||
// Function to enable panel profiling for a specific dashboard
|
||||
export function enablePanelProfilingForDashboard(dashboard: SceneObject, uid: string) {
|
||||
// Check if panel profiling should be enabled for this dashboard
|
||||
// HACK always on
|
||||
const shouldEnablePanelProfiling =
|
||||
config.dashboardPerformanceMetrics.findIndex((configUid) => configUid === '*' || configUid === uid) !== -1;
|
||||
config.dashboardPerformanceMetrics.findIndex((configUid) => configUid === '*' || configUid === uid) !== -1 || true; // HACK always on
|
||||
|
||||
if (shouldEnablePanelProfiling) {
|
||||
const profiler = getDashboardSceneProfiler();
|
||||
|
||||
Reference in New Issue
Block a user