Dashboard performance analytics system with Scenes integration (#112137)

* Dashboard performance profiling architecture improvements

- Create shared performanceUtils.ts with type-safe performance.memory access
- Add standardized grouped logging utilities for structured console output
- Convert observer methods to arrow functions eliminating constructor bindings
- Implement DashboardAnalyticsAggregator for comprehensive panel metrics
- Add ScenePerformanceLogger for performance marks and measurements
- Create DashboardAnalyticsInitializerBehavior for automatic profiling setup
- Update dashboard scene integration to use improved profiling system
- Add numeric duration logging for better programmatic analysis
- Fix localStorage usage to use @grafana/data store for consistency
- Consolidate performance tracking logic into shared utilities

* canary scenes

* tests/lint

* docs

* performanceUtils namespace

* Review and sync scenes

* Only enable dashboard profiling when needed

* docs update

* update scenes

---------

Co-authored-by: Victor Marin <victor.marin@grafana.com>
This commit is contained in:
Dominik Prokop
2025-10-30 06:25:55 +00:00
committed by GitHub
co-authored by Victor Marin
parent 84edc45dee
commit 209aa13ff7
16 changed files with 1483 additions and 277 deletions
+2 -2
View File
@@ -296,8 +296,8 @@
"@grafana/plugin-ui": "^0.10.10",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^6.40.1",
"@grafana/scenes-react": "^6.40.1",
"@grafana/scenes": "^6.41.0",
"@grafana/scenes-react": "^6.41.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -0,0 +1,37 @@
import { writePerformanceLog } from '@grafana/scenes';
import { getDashboardAnalyticsAggregator } from '../../dashboard/services/DashboardAnalyticsAggregator';
import { DashboardScene } from '../scene/DashboardScene';
/**
* Scene behavior function that manages the dashboard-specific initialization
* of the global analytics aggregator for each dashboard session.
*
* Note: Both ScenePerformanceLogger and DashboardAnalyticsAggregator are now
* initialized globally to avoid timing issues. This behavior only sets
* dashboard-specific context.
*/
export function dashboardAnalyticsInitializer(dashboard: DashboardScene) {
const { uid, title } = dashboard.state;
if (!uid) {
console.warn('dashboardAnalyticsInitializer: Dashboard UID is missing');
return;
}
writePerformanceLog('DAI', 'Setting dashboard context for analytics aggregator');
// Set dashboard context on the global aggregator (observer already registered)
const aggregator = getDashboardAnalyticsAggregator();
aggregator.initialize(uid, title || 'Untitled Dashboard');
writePerformanceLog('DAI', 'Dashboard analytics aggregator context set:', { uid, title });
// Return cleanup function
return () => {
// Only clear dashboard state, keep observer registered for next dashboard
aggregator.destroy();
writePerformanceLog('DAI', 'Dashboard analytics aggregator context cleared');
};
}
@@ -17,8 +17,11 @@ import {
import { ensureV2Response, transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers';
import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { isDashboardV2Resource, isDashboardV2Spec, isV2StoredVersion } from 'app/features/dashboard/api/utils';
import { initializeDashboardAnalyticsAggregator } from 'app/features/dashboard/services/DashboardAnalyticsAggregator';
import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { initializeScenePerformanceLogger } from 'app/features/dashboard/services/ScenePerformanceLogger';
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
import { trackDashboardSceneLoaded } from 'app/features/dashboard-scene/utils/tracking';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
@@ -41,6 +44,14 @@ import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSession
import { processQueryParamsForDashboardLoad, updateNavModel } from './utils';
/**
* Initialize both performance services to ensure they're ready before profiling starts
*/
function initializeDashboardPerformanceServices(): void {
initializeScenePerformanceLogger();
initializeDashboardAnalyticsAggregator();
}
export interface LoadError {
status?: number;
messageId?: string;
@@ -296,6 +307,16 @@ abstract class DashboardScenePageStateManagerBase<T>
const queryController = sceneGraph.getQueryController(dashboard);
trackDashboardSceneLoaded(dashboard, measure?.duration);
const enableProfiling =
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === options.uid) !== -1;
if (enableProfiling) {
// Initialize both performance services before starting profiling to ensure observers are registered
initializeDashboardPerformanceServices();
}
// Start dashboard_view profiling (both services are now guaranteed to be listening)
queryController?.startProfile('dashboard_view');
if (options.route !== DashboardRoutes.New) {
@@ -409,6 +430,11 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
fromCache.state.version === rsp?.dashboard.version &&
fromCache.state.meta.created === rsp?.meta.created
) {
const profiler = getDashboardSceneProfiler();
profiler.setMetadata({
dashboardUID: fromCache.state.uid,
dashboardTitle: fromCache.state.title,
});
return fromCache;
}
@@ -696,6 +722,11 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
const fromCache = this.getSceneFromCache(options.uid);
if (fromCache && fromCache.state.version === rsp?.metadata.generation) {
const profiler = getDashboardSceneProfiler();
profiler.setMetadata({
dashboardUID: fromCache.state.uid,
dashboardTitle: fromCache.state.title,
});
return fromCache;
}
@@ -25,6 +25,7 @@ import store from 'app/core/store';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types';
import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel, ScopeMeta } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@@ -625,7 +626,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
}
public onCreateNewPanel(): VizPanel {
const profiler = getDashboardSceneProfiler();
const vizPanel = getDefaultVizPanel();
profiler.attachProfilerToPanel(vizPanel);
this.addPanel(vizPanel);
return vizPanel;
}
@@ -56,13 +56,14 @@ import {
} from 'app/features/apiserver/types';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import {
getDashboardSceneProfilerWithMetadata,
enablePanelProfilingForDashboard,
getDashboardComponentInteractionCallback,
getDashboardInteractionCallback,
getDashboardSceneProfiler,
} from 'app/features/dashboard/services/DashboardProfiler';
import { DashboardMeta } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
@@ -168,22 +169,24 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
//createLayoutManager(dashboard);
// Create profiler once and reuse to avoid duplicate metadata setting
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(metadata.name, dashboard.title);
const enableProfiling =
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1;
const queryController = new behaviors.SceneQueryController(
{
enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
onProfileComplete: getDashboardInteractionCallback(metadata.name, dashboard.title),
enableProfiling,
},
getDashboardSceneProfiler()
dashboardProfiler
);
const interactionTracker = new behaviors.SceneInteractionTracker(
{
enableInteractionTracking:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
enableInteractionTracking: enableProfiling,
onInteractionComplete: getDashboardComponentInteractionCallback(metadata.name, dashboard.title),
},
getDashboardSceneProfiler()
dashboardProfiler
);
const dashboardScene = new DashboardScene(
@@ -223,6 +226,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && false,
uid: dashboardId?.toString(),
}),
...(enableProfiling ? [dashboardAnalyticsInitializer] : []),
],
$data: new DashboardDataLayerSet({
annotationLayers,
@@ -245,6 +249,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
dashboardScene.setInitialSaveModel(dto.spec, dto.metadata, apiVersion);
// Enable panel profiling for this dashboard using the composed SceneRenderProfiler
enablePanelProfilingForDashboard(dashboardScene, metadata.name);
return dashboardScene;
}
@@ -23,20 +23,22 @@ import {
import { isWeekStart } from '@grafana/ui';
import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1';
import {
getDashboardSceneProfilerWithMetadata,
enablePanelProfilingForDashboard,
getDashboardComponentInteractionCallback,
getDashboardInteractionCallback,
getDashboardSceneProfiler,
} from 'app/features/dashboard/services/DashboardProfiler';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { registerDashboardMacro } from '../scene/DashboardMacro';
// DashboardPanelProfilingBehavior removed - now using composed SceneRenderProfiler
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
@@ -296,22 +298,24 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
}
: undefined;
// Create profiler once and reuse to avoid duplicate metadata setting
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(oldModel.uid, oldModel.title);
const enableProfiling =
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1;
const queryController = new behaviors.SceneQueryController(
{
enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
enableProfiling,
},
getDashboardSceneProfiler()
dashboardProfiler
);
const interactionTracker = new behaviors.SceneInteractionTracker(
{
enableInteractionTracking:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
enableInteractionTracking: enableProfiling,
onInteractionComplete: getDashboardComponentInteractionCallback(oldModel.uid, oldModel.title),
},
getDashboardSceneProfiler()
dashboardProfiler
);
const behaviorList: SceneObjectState['$behaviors'] = [
@@ -330,6 +334,12 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
}),
];
if (enableProfiling) {
// Analytics aggregator lifecycle management (initialization, observer registration, cleanup)
behaviorList.push(dashboardAnalyticsInitializer);
}
// Will be enabled in the dashboard creation below
let body: DashboardLayoutManager;
if (config.featureToggles.dashboardNewLayouts && oldModel.panels.some((p) => p.type === 'row')) {
@@ -385,6 +395,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
serializerVersion
);
// Enable panel profiling for this dashboard using the composed SceneRenderProfiler
enablePanelProfilingForDashboard(dashboardScene, uid);
return dashboardScene;
}
@@ -51,6 +51,9 @@ describe('PublicDashboardPageProxy', () => {
beforeEach(() => {
config.featureToggles.publicDashboardsScene = false;
// Mock console methods to avoid jest-fail-on-console issues
jest.spyOn(console, 'warn').mockImplementation();
// Mock the dashboard UID response so we don't get any refused connection errors
// from this test (as the fetch polyfill means this logic would actually try and call the API)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -0,0 +1,373 @@
import { logMeasurement, reportInteraction } from '@grafana/runtime';
import { performanceUtils } from '@grafana/scenes';
import { SLOW_OPERATION_THRESHOLD_MS } from './performanceConstants';
import {
registerPerformanceObserver,
getPerformanceMemory,
writePerformanceGroupStart,
writePerformanceGroupLog,
writePerformanceGroupEnd,
} from './performanceUtils';
/**
* Panel metrics structure for analytics
*/
interface PanelAnalyticsMetrics {
panelId: string;
panelKey: string;
pluginId: string;
pluginVersion?: string;
totalQueryTime: number;
totalFieldConfigTime: number;
totalTransformationTime: number;
totalRenderTime: number;
pluginLoadTime: number;
queryOperations: Array<{
duration: number;
timestamp: number;
queryType?: string;
seriesCount?: number;
dataPointsCount?: number;
}>;
fieldConfigOperations: Array<{
duration: number;
timestamp: number;
}>;
transformationOperations: Array<{
duration: number;
timestamp: number;
transformationId?: string;
success?: boolean;
outputSeriesCount?: number;
}>;
renderOperations: Array<{
duration: number;
timestamp: number;
}>;
}
/**
* Aggregates Scene performance events into analytics-ready panel metrics
*/
export class DashboardAnalyticsAggregator implements performanceUtils.ScenePerformanceObserver {
private panelMetrics = new Map<string, PanelAnalyticsMetrics>();
private dashboardUID = '';
private dashboardTitle = '';
public initialize(uid: string, title: string) {
// Clear previous dashboard data and set new context
this.panelMetrics.clear();
this.dashboardUID = uid;
this.dashboardTitle = title;
}
public destroy() {
// Clear dashboard context
this.panelMetrics.clear();
this.dashboardUID = '';
this.dashboardTitle = '';
}
/**
* Clear all collected metrics (called on dashboard interaction start)
*/
public clearMetrics() {
this.panelMetrics.clear();
}
/**
* Get aggregated panel metrics for analytics
*/
public getPanelMetrics(): PanelAnalyticsMetrics[] {
return Array.from(this.panelMetrics.values());
}
// 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
this.clearMetrics();
};
onDashboardInteractionMilestone = (_data: performanceUtils.DashboardInteractionMilestoneData): void => {
// No action needed for milestones in analytics
};
onDashboardInteractionComplete = (data: performanceUtils.DashboardInteractionCompleteData): void => {
// Send analytics report for dashboard interaction completion
this.sendAnalyticsReport(data);
};
// Panel-level events
onPanelOperationStart = (data: performanceUtils.PanelPerformanceData): void => {
// 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 => {
// Aggregate panel metrics without verbose logging (handled by ScenePerformanceLogger)
const panel = this.panelMetrics.get(data.panelKey);
if (!panel) {
console.warn('Panel not found for operation completion:', data.panelKey);
return;
}
const duration = data.duration || 0;
switch (data.operation) {
case 'fieldConfig':
panel.totalFieldConfigTime += duration;
panel.fieldConfigOperations.push({
duration,
timestamp: data.timestamp,
});
break;
case 'transform':
panel.totalTransformationTime += duration;
panel.transformationOperations.push({
duration,
timestamp: data.timestamp,
transformationId: data.metadata.transformationId,
success: data.metadata.success,
});
break;
case 'query':
panel.totalQueryTime += duration;
panel.queryOperations.push({
duration,
timestamp: data.timestamp,
queryType: data.metadata.queryType,
});
break;
case 'render':
panel.totalRenderTime += duration;
panel.renderOperations.push({
duration,
timestamp: data.timestamp,
});
break;
case 'plugin-load':
panel.pluginLoadTime += duration;
break;
}
};
// Query-level events
onQueryStart = (_data: performanceUtils.QueryPerformanceData): void => {
// no-op
};
onQueryComplete = (_data: performanceUtils.QueryPerformanceData): void => {
// no-op
};
/**
* Ensure a panel exists in our tracking map
*/
private ensurePanelExists(
panelKey: string,
panelId: string,
pluginId: string,
pluginVersion?: string
): PanelAnalyticsMetrics {
let panel = this.panelMetrics.get(panelKey);
if (!panel) {
panel = {
panelId,
panelKey,
pluginId,
pluginVersion,
totalQueryTime: 0,
totalFieldConfigTime: 0,
totalTransformationTime: 0,
totalRenderTime: 0,
pluginLoadTime: 0,
queryOperations: [],
fieldConfigOperations: [],
transformationOperations: [],
renderOperations: [],
};
this.panelMetrics.set(panelKey, panel);
}
return panel;
}
/**
* Send analytics report for dashboard interactions
*/
private sendAnalyticsReport(data: performanceUtils.DashboardInteractionCompleteData): void {
const payload = {
duration: data.duration || 0,
networkDuration: data.networkDuration || 0,
startTs: data.timestamp,
endTs: data.timestamp + (data.duration || 0),
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
longFramesCount: data.longFramesCount,
longFramesTotalTime: data.longFramesTotalTime,
...getPerformanceMemory(),
};
const panelMetrics = this.getPanelMetrics();
this.logDashboardAnalyticsEvent(data, payload, panelMetrics);
reportInteraction('dashboard_render', {
interactionType: data.interactionType,
uid: this.dashboardUID,
...payload,
});
logMeasurement('dashboard_render', payload, {
interactionType: data.interactionType,
dashboard: this.dashboardUID,
title: this.dashboardTitle,
});
}
/**
* Log dashboard analytics event with panel metrics and performance insights
*/
private logDashboardAnalyticsEvent(
data: performanceUtils.DashboardInteractionCompleteData,
payload: Record<string, unknown>,
panelMetrics: PanelAnalyticsMetrics[] | null
): void {
const panelCount = panelMetrics?.length || 0;
const panelSummary = panelCount ? `${panelCount} panels analyzed` : 'No panel metrics';
// Main analytics summary
const slowPanelCount =
panelMetrics?.filter(
(p) =>
p.totalQueryTime + p.totalTransformationTime + p.totalRenderTime + p.totalFieldConfigTime + p.pluginLoadTime >
SLOW_OPERATION_THRESHOLD_MS
).length || 0;
writePerformanceGroupStart(
'DAA',
`[ANALYTICS] ${data.interactionType} | ${panelSummary}${slowPanelCount > 0 ? ` | ${slowPanelCount} slow panels ⚠️` : ''}`
);
// Dashboard overview
writePerformanceGroupLog('DAA', '📊 Dashboard (ms):', {
duration: Math.round((data.duration || 0) * 10) / 10,
network: Math.round((data.networkDuration || 0) * 10) / 10,
interactionType: data.interactionType,
slowPanels: slowPanelCount,
});
// Analytics payload
writePerformanceGroupLog('DAA', '📈 Analytics payload:', payload);
// Individual collapsible panel logs with detailed breakdown
if (panelMetrics && panelMetrics.length > 0) {
panelMetrics.forEach((panel) => {
const totalPanelTime =
panel.totalQueryTime +
panel.totalTransformationTime +
panel.totalRenderTime +
panel.totalFieldConfigTime +
panel.pluginLoadTime;
const isSlowPanel = totalPanelTime > SLOW_OPERATION_THRESHOLD_MS;
const slowWarning = isSlowPanel ? ' ⚠️ SLOW' : '';
writePerformanceGroupStart(
'DAA',
`🎨 Panel ${panel.pluginId}-${panel.panelId}: ${totalPanelTime.toFixed(1)}ms total${slowWarning}`
);
writePerformanceGroupLog('DAA', '🔧 Plugin:', {
id: panel.pluginId,
version: panel.pluginVersion || 'unknown',
panelId: panel.panelId,
panelKey: panel.panelKey,
});
writePerformanceGroupLog('DAA', '⚡ Performance (ms):', {
totalTime: Math.round(totalPanelTime * 10) / 10, // Round to 1 decimal
isSlowPanel: isSlowPanel,
breakdown: {
query: Math.round(panel.totalQueryTime * 10) / 10,
transform: Math.round(panel.totalTransformationTime * 10) / 10,
render: Math.round(panel.totalRenderTime * 10) / 10,
fieldConfig: Math.round(panel.totalFieldConfigTime * 10) / 10,
pluginLoad: Math.round(panel.pluginLoadTime * 10) / 10,
},
});
if (panel.queryOperations.length > 0) {
writePerformanceGroupLog('DAA', '📊 Queries:', {
count: panel.queryOperations.length,
details: panel.queryOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
queryType: op.queryType || 'unknown',
})),
});
}
if (panel.transformationOperations.length > 0) {
writePerformanceGroupLog('DAA', '🔄 Transformations:', {
count: panel.transformationOperations.length,
details: panel.transformationOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
transformationId: op.transformationId || 'unknown',
success: op.success !== false,
})),
});
}
if (panel.renderOperations.length > 0) {
writePerformanceGroupLog('DAA', '🎨 Renders:', {
count: panel.renderOperations.length,
details: panel.renderOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
})),
});
}
if (panel.fieldConfigOperations.length > 0) {
writePerformanceGroupLog('DAA', '⚙️ FieldConfigs:', {
count: panel.fieldConfigOperations.length,
details: panel.fieldConfigOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
})),
});
}
writePerformanceGroupEnd();
});
}
writePerformanceGroupEnd();
}
}
// Global singleton instance with lazy initialization
let dashboardAnalyticsAggregator: DashboardAnalyticsAggregator | null = null;
export function initializeDashboardAnalyticsAggregator(): DashboardAnalyticsAggregator {
if (!dashboardAnalyticsAggregator) {
dashboardAnalyticsAggregator = new DashboardAnalyticsAggregator();
// Register as global performance observer
registerPerformanceObserver(dashboardAnalyticsAggregator, 'DAA');
}
return dashboardAnalyticsAggregator;
}
export function getDashboardAnalyticsAggregator(): DashboardAnalyticsAggregator {
return initializeDashboardAnalyticsAggregator();
}
@@ -1,11 +1,24 @@
import { logMeasurement, reportInteraction } from '@grafana/runtime';
import { SceneInteractionProfileEvent, SceneRenderProfiler } from '@grafana/scenes';
import { logMeasurement, reportInteraction, config } from '@grafana/runtime';
import { performanceUtils, type SceneObject } from '@grafana/scenes';
let dashboardSceneProfiler: SceneRenderProfiler | undefined;
interface SceneInteractionProfileEvent {
origin: string;
duration: number;
networkDuration: number;
startTs: number;
endTs: number;
}
let dashboardSceneProfiler: performanceUtils.SceneRenderProfiler | undefined;
export function getDashboardSceneProfiler() {
if (!dashboardSceneProfiler) {
dashboardSceneProfiler = new SceneRenderProfiler();
// Create panel profiling configuration
const panelProfilingConfig = {
watchStateKey: 'body', // Watch dashboard body changes for panel structure changes
};
dashboardSceneProfiler = new performanceUtils.SceneRenderProfiler(panelProfilingConfig);
}
return dashboardSceneProfiler;
}
@@ -30,28 +43,31 @@ export function getDashboardComponentInteractionCallback(uid: string, title: str
};
}
export function getDashboardInteractionCallback(uid: string, title: string) {
return (e: SceneInteractionProfileEvent) => {
const payload = {
duration: e.duration,
networkDuration: e.networkDuration,
processingTime: e.duration - e.networkDuration,
startTs: e.startTs,
endTs: e.endTs,
totalJSHeapSize: e.totalJSHeapSize,
usedJSHeapSize: e.usedJSHeapSize,
jsHeapSizeLimit: e.jsHeapSizeLimit,
longFramesCount: e.longFramesCount,
longFramesTotalTime: e.longFramesTotalTime,
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
};
// Enhanced function to create profiler with dashboard metadata
export function getDashboardSceneProfilerWithMetadata(uid: string, title: string) {
const profiler = getDashboardSceneProfiler();
reportInteraction('dashboard_render', {
interactionType: e.origin,
uid,
...payload,
});
// Set metadata for observer notifications
profiler.setMetadata({
dashboardUID: uid,
dashboardTitle: title,
});
logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title });
};
// Note: Analytics aggregator initialization and observer registration
// is now handled by DashboardAnalyticsInitializerBehavior
return profiler;
}
// 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
const shouldEnablePanelProfiling =
config.dashboardPerformanceMetrics.findIndex((configUid) => configUid === '*' || configUid === uid) !== -1;
if (shouldEnablePanelProfiling) {
const profiler = getDashboardSceneProfiler();
// Attach panel profiling to this dashboard
profiler.attachPanelProfiling(dashboard);
}
}
@@ -0,0 +1,220 @@
import { performanceUtils, writePerformanceLog } from '@grafana/scenes';
import { PERFORMANCE_MARKS, PERFORMANCE_MEASURES, SLOW_OPERATION_THRESHOLD_MS } from './performanceConstants';
import { registerPerformanceObserver, createPerformanceMark, createPerformanceMeasure } from './performanceUtils';
/**
* Grafana logger that subscribes to Scene performance events
* and logs them to console with Chrome DevTools performance marks and measurements for debugging.
*/
export class ScenePerformanceLogger implements performanceUtils.ScenePerformanceObserver {
private panelGroupsOpen = new Set<string>(); // Track which panels we've seen
public initialize() {
writePerformanceLog('SPL', 'Performance logger ready');
}
public destroy() {
this.panelGroupsOpen.clear();
writePerformanceLog('SPL', 'Performance logger state cleared');
}
// Dashboard-level events
onDashboardInteractionStart = (data: performanceUtils.DashboardInteractionStartData): void => {
const dashboardStartMark = PERFORMANCE_MARKS.DASHBOARD_INTERACTION_START(data.operationId);
createPerformanceMark(dashboardStartMark, data.timestamp);
const title = data.metadata?.dashboardTitle || 'Unknown Dashboard';
writePerformanceLog('SPL', `[DASHBOARD] ${data.interactionType} started: ${title}`);
};
onDashboardInteractionMilestone = (data: performanceUtils.DashboardInteractionMilestoneData): void => {
const milestone = data.milestone || 'unknown';
const dashboardMilestoneMark = PERFORMANCE_MARKS.DASHBOARD_MILESTONE(data.operationId, milestone);
createPerformanceMark(dashboardMilestoneMark, data.timestamp);
};
onDashboardInteractionComplete = (data: performanceUtils.DashboardInteractionCompleteData): void => {
const dashboardEndMark = PERFORMANCE_MARKS.DASHBOARD_INTERACTION_END(data.operationId);
const dashboardStartMark = PERFORMANCE_MARKS.DASHBOARD_INTERACTION_START(data.operationId);
const dashboardMeasureName = PERFORMANCE_MEASURES.DASHBOARD_INTERACTION(data.operationId);
createPerformanceMark(dashboardEndMark, data.timestamp);
createPerformanceMeasure(dashboardMeasureName, dashboardStartMark, dashboardEndMark);
this.panelGroupsOpen.clear();
};
onPanelOperationStart = (data: performanceUtils.PanelPerformanceData): void => {
this.createStandardizedPanelMark(data, 'start');
// Track panel for summary logging later
this.panelGroupsOpen.add(data.panelKey);
};
onPanelOperationComplete = (data: performanceUtils.PanelPerformanceData): void => {
this.createStandardizedPanelMark(data, 'end');
this.createStandardizedPanelMeasure(data);
const duration = (data.duration || 0).toFixed(1);
const slowWarning = (data.duration || 0) > SLOW_OPERATION_THRESHOLD_MS ? ' ⚠️ SLOW' : '';
// For query operations, include the queryId for correlation
let operationDisplay: string = data.operation;
if (data.operation === 'query') {
operationDisplay = `${data.operation} [${data.metadata.queryId}]`;
}
writePerformanceLog(
'SPL',
`[PANEL] ${data.pluginId}-${data.panelId} ${operationDisplay}: ${duration}ms${slowWarning}`
);
};
// Query-level events
onQueryStart = (data: performanceUtils.QueryPerformanceData): void => {
const queryStartMark = PERFORMANCE_MARKS.QUERY_START(data.origin, data.queryId);
createPerformanceMark(queryStartMark, data.timestamp);
};
onQueryComplete = (data: performanceUtils.QueryPerformanceData): void => {
const queryEndMark = PERFORMANCE_MARKS.QUERY_END(data.origin, data.queryId);
const queryStartMark = PERFORMANCE_MARKS.QUERY_START(data.origin, data.queryId);
const queryMeasureName = PERFORMANCE_MEASURES.QUERY(data.origin, data.queryId);
createPerformanceMark(queryEndMark, data.timestamp);
createPerformanceMeasure(queryMeasureName, queryStartMark, queryEndMark);
const duration = (data.duration || 0).toFixed(1);
const slowWarning = (data.duration || 0) > SLOW_OPERATION_THRESHOLD_MS ? ' ⚠️ SLOW' : '';
const queryType = data.queryType.replace(/^(getDataSource\/|AnnotationsDataLayer\/)/, ''); // Remove prefixes
writePerformanceLog('SPL', `[QUERY ${data.origin}] ${queryType} [${data.queryId}]: ${duration}ms${slowWarning}`);
};
private createStandardizedPanelMark(data: performanceUtils.PanelPerformanceData, phase: 'start' | 'end'): void {
const { operation, panelKey, operationId } = data;
switch (operation) {
case 'query':
const markName =
phase === 'start'
? PERFORMANCE_MARKS.PANEL_QUERY_START(panelKey, operationId)
: PERFORMANCE_MARKS.PANEL_QUERY_END(panelKey, operationId);
createPerformanceMark(markName, data.timestamp);
break;
case 'plugin-load':
const pluginMarkName =
phase === 'start'
? PERFORMANCE_MARKS.PANEL_PLUGIN_LOAD_START(panelKey, operationId)
: PERFORMANCE_MARKS.PANEL_PLUGIN_LOAD_END(panelKey, operationId);
createPerformanceMark(pluginMarkName, data.timestamp);
break;
case 'fieldConfig':
const fieldConfigMarkName =
phase === 'start'
? PERFORMANCE_MARKS.PANEL_FIELD_CONFIG_START(panelKey, operationId)
: PERFORMANCE_MARKS.PANEL_FIELD_CONFIG_END(panelKey, operationId);
createPerformanceMark(fieldConfigMarkName, data.timestamp);
break;
case 'render':
const renderMarkName =
phase === 'start'
? PERFORMANCE_MARKS.PANEL_RENDER_START(panelKey, operationId)
: PERFORMANCE_MARKS.PANEL_RENDER_END(panelKey, operationId);
createPerformanceMark(renderMarkName, data.timestamp);
break;
case 'transform':
const transformationId = data.metadata.transformationId;
if (phase === 'start') {
createPerformanceMark(
PERFORMANCE_MARKS.PANEL_TRANSFORM_START(panelKey, transformationId, operationId),
data.timestamp
);
} else {
const isError = data.metadata.error || data.metadata.success === false;
const transformEndMarkName = isError
? PERFORMANCE_MARKS.PANEL_TRANSFORM_ERROR(panelKey, transformationId, operationId)
: PERFORMANCE_MARKS.PANEL_TRANSFORM_END(panelKey, transformationId, operationId);
createPerformanceMark(transformEndMarkName, data.timestamp);
}
break;
default:
break;
}
}
private createStandardizedPanelMeasure(data: performanceUtils.PanelPerformanceData): void {
const { operation, panelKey, operationId } = data;
switch (operation) {
case 'query':
const startMark = PERFORMANCE_MARKS.PANEL_QUERY_START(panelKey, operationId);
const endMark = PERFORMANCE_MARKS.PANEL_QUERY_END(panelKey, operationId);
const measureName = PERFORMANCE_MEASURES.PANEL_QUERY(panelKey, operationId);
createPerformanceMeasure(measureName, startMark, endMark);
break;
case 'plugin-load':
const pluginStartMark = PERFORMANCE_MARKS.PANEL_PLUGIN_LOAD_START(panelKey, operationId);
const pluginEndMark = PERFORMANCE_MARKS.PANEL_PLUGIN_LOAD_END(panelKey, operationId);
const pluginMeasureName = PERFORMANCE_MEASURES.PANEL_PLUGIN_LOAD(panelKey, operationId);
createPerformanceMeasure(pluginMeasureName, pluginStartMark, pluginEndMark);
break;
case 'fieldConfig':
const fieldConfigStartMark = PERFORMANCE_MARKS.PANEL_FIELD_CONFIG_START(panelKey, operationId);
const fieldConfigEndMark = PERFORMANCE_MARKS.PANEL_FIELD_CONFIG_END(panelKey, operationId);
const fieldConfigMeasureName = PERFORMANCE_MEASURES.PANEL_FIELD_CONFIG(panelKey, operationId);
createPerformanceMeasure(fieldConfigMeasureName, fieldConfigStartMark, fieldConfigEndMark);
break;
case 'render':
const renderStartMark = PERFORMANCE_MARKS.PANEL_RENDER_START(panelKey, operationId);
const renderEndMark = PERFORMANCE_MARKS.PANEL_RENDER_END(panelKey, operationId);
const renderMeasureName = PERFORMANCE_MEASURES.PANEL_RENDER(panelKey, operationId);
createPerformanceMeasure(renderMeasureName, renderStartMark, renderEndMark);
break;
case 'transform':
const transformationId = data.metadata.transformationId;
const transformStartMark = PERFORMANCE_MARKS.PANEL_TRANSFORM_START(panelKey, transformationId, operationId);
const isError = data.metadata.error || data.metadata.success === false;
const transformEndMark = isError
? PERFORMANCE_MARKS.PANEL_TRANSFORM_ERROR(panelKey, transformationId, operationId)
: PERFORMANCE_MARKS.PANEL_TRANSFORM_END(panelKey, transformationId, operationId);
const transformMeasureName = PERFORMANCE_MEASURES.PANEL_TRANSFORM(panelKey, transformationId, operationId);
createPerformanceMeasure(transformMeasureName, transformStartMark, transformEndMark);
break;
default:
break;
}
}
}
// Global singleton instance with lazy initialization
let scenePerformanceLogger: ScenePerformanceLogger | null = null;
export function initializeScenePerformanceLogger(): ScenePerformanceLogger {
if (!scenePerformanceLogger) {
scenePerformanceLogger = new ScenePerformanceLogger();
scenePerformanceLogger.initialize();
// Register as global performance observer
registerPerformanceObserver(scenePerformanceLogger, 'SPL');
}
return scenePerformanceLogger;
}
export function getScenePerformanceLogger(): ScenePerformanceLogger {
return initializeScenePerformanceLogger();
}
@@ -10,25 +10,34 @@ This documentation describes the dashboard render performance metrics exposed fr
- [Tracked Interactions](#tracked-interactions)
- [Core Performance-Tracked Interactions](#core-performance-tracked-interactions)
- [Interaction Origin Mapping](#interaction-origin-mapping)
- [Panel-Level Performance Attribution](#panel-level-performance-attribution)
- [Overview](#panel-level-overview)
- [Panel Operations Tracked](#panel-operations-tracked)
- [Performance Observer Architecture](#performance-observer-architecture)
- [Profiling Implementation](#profiling-implementation)
- [Profile Data Structure](#profile-data-structure)
- [Collected Metrics](#collected-metrics)
- [Debugging and Development](#debugging-and-development)
- [Enable Profiler Debug Logging](#enable-profiler-debug-logging)
- [Enable Echo Service Debug Logging](#enable-echo-service-debug-logging)
- [Browser Performance Profiler](#browser-performance-profiler)
- [Analytics Integration](#analytics-integration)
- [Interaction Reporting](#interaction-reporting)
- [Data Collection](#data-collection)
- [Implementation Details](#implementation-details)
- [Long Frame Detection](#long-frame-detection)
- [Analytics Integration](#analytics-integration)
- [Analytics Components](#analytics-components)
- [Chrome DevTools Integration](#chrome-devtools-integration)
- [Data Collection](#data-collection)
- [Debugging and Development](#debugging-and-development)
- [Enable Performance Debug Logging](#enable-performance-debug-logging)
- [Console Output Examples](#console-output-examples)
- [Browser Performance Profiler](#browser-performance-profiler)
- [Implementation Details](#implementation-details)
- [Architecture Overview](#architecture-overview)
- [Tab Inactivity Handling](#tab-inactivity-handling)
- [Profile Isolation](#profile-isolation)
- [Related Documentation](#related-documentation)
## Overview
The exposed dashboard performance metrics feature provides comprehensive tracking and profiling of dashboard interactions, allowing administrators and developers to analyze dashboard render performance, user interactions, and identify performance bottlenecks.
The system includes **panel-level performance attribution** through an observer pattern architecture, providing visibility into individual panel operations within dashboard interactions. This enables identification of performance bottlenecks at both dashboard and panel levels, with comprehensive analytics reporting and Chrome DevTools integration.
## Configuration
### Enabling Performance Metrics
@@ -110,6 +119,102 @@ logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboa
The profiling system uses profiler event's `origin` directly as the `interactionType`, providing direct mapping between user actions and performance measurements.
## Panel-Level Performance Attribution
### Panel-Level Overview
The panel-level performance attribution system uses a observer pattern architecture built around `ScenePerformanceTracker` to provide comprehensive visibility into individual panel operations. When dashboard profiling is enabled, `VizPanelRenderProfiler` instances are automatically attached to all panels, providing granular tracking of panel lifecycle operations.
**Key Features:**
- **Complete lifecycle tracking**: Monitors plugin load, query execution, data transformation, field configuration, and rendering phases
- **Sub-millisecond precision timing**: Chrome DevTools integration via performance marks and measurements
- **Operation ID correlation**: UUID-based operation IDs with crypto fallback for cross-environment compatibility
- **Observer pattern architecture**: Clean separation between performance tracking and business logic with extensible observer support
- **Real-time analytics aggregation**: Structured data format ready for analytics reporting
- **Conditional profiling**: Analytics aggregator only initialized when profiling is enabled
- **Type-safe interfaces**: Comprehensive TypeScript support with event-specific interfaces
### Panel Operations Tracked
The system tracks the following panel operations:
| Operation | Description | When Tracked |
| ------------- | --------------------- | ----------------------------------------------- |
| `plugin-load` | Plugin initialization | When panel plugin is loaded |
| `query` | Data source queries | When panel executes queries |
| `transform` | Data transformations | When data is transformed (SceneDataTransformer) |
| `fieldConfig` | Field configuration | When field configurations are applied |
| `render` | Panel rendering | When panel is rendered |
Each operation is tracked with:
- **Operation ID**: UUID-based unique identifier for correlating start/complete events (e.g., `query-a1b2c3d4-e5f6-7890-abcd-ef1234567890`)
- **Timing**: High-precision start and end timestamps with sub-millisecond duration calculation
- **Metadata**: Operation-specific data (query types, transformation IDs, plugin information, etc.)
### Operation ID Format
The system generates unique operation IDs using a standardized format:
```
<operation-type>-<uuid>
```
**Examples:**
- `plugin-load-550e8400-e29b-41d4-a716-446655440000`
- `query-a1b2c3d4-e5f6-7890-abcd-ef1234567890`
- `transform-b2c3d4e5-f6g7-8901-bcde-f23456789012`
- `fieldConfig-c3d4e5f6-g7h8-9012-cdef-345678901234`
- `render-d4e5f6g7-h8i9-0123-def0-456789012345`
**Benefits:**
- **Global Uniqueness**: UUIDs prevent ID collisions across dashboard sessions
- **Cross-Environment Compatibility**: Crypto fallback ensures operation in all environments
- **Operation Correlation**: Enables precise tracking of start/complete event pairs
- **Debugging**: Human-readable prefixes make log analysis easier
### Performance Observer Architecture
The system uses `ScenePerformanceTracker` as a centralized coordinator that manages performance observers through an event-driven architecture. The performance utilities are organized under the `performanceUtils` namespace.
```typescript
// Import performance utilities from scenes
import { performanceUtils } from '@grafana/scenes';
// Observer interface implemented by analytics components
interface ScenePerformanceObserver {
onDashboardInteractionStart?(data: performanceUtils.DashboardInteractionStartData): void;
onDashboardInteractionMilestone?(data: performanceUtils.DashboardInteractionMilestoneData): void;
onDashboardInteractionComplete?(data: performanceUtils.DashboardInteractionCompleteData): void;
onPanelOperationStart?(data: performanceUtils.PanelPerformanceData): void;
onPanelOperationComplete?(data: performanceUtils.PanelPerformanceData): void;
onQueryStart?(data: performanceUtils.QueryPerformanceData): void;
onQueryComplete?(data: performanceUtils.QueryPerformanceData): void;
}
// Register observers with the performance tracker
const tracker = performanceUtils.getScenePerformanceTracker();
tracker.addObserver(myObserver);
```
**Operation ID Generation:**
The system generates unique operation IDs for correlating start/complete events using UUID with fallback support:
```typescript
// Uses crypto.randomUUID() when available, Math.random() fallback for compatibility
const operationId = performanceUtils.generateOperationId('panel-query');
// Result: "panel-query-550e8400-e29b-41d4-a716-446655440000"
```
**Registered Observers:**
- **`DashboardAnalyticsAggregator`**: Aggregates panel metrics for analytics reporting (conditionally initialized)
- **`ScenePerformanceLogger`**: Creates Chrome DevTools performance marks and console logs
## Profiling Implementation
### Profile Data Structure
@@ -153,42 +258,274 @@ The performance metrics provide detailed insights into where time is spent durin
- **Total Duration (`duration`)**: Complete time from interaction start to completion
- **Network Time (`networkDuration`)**: Time spent waiting for server responses (data source queries, API calls)
- **Processing Time (`processingTime`)**: Time spent on client-side operations (rendering, computations, DOM updates)
- **Long Frames (`longFramesCount` & `longFramesTotalTime`)**: Frames exceeding 50ms threshold indicate potential UI jank or performance issues. These metrics help identify interactions causing poor user experience:
- `longFramesCount`: The number of frames that exceeded the 50ms threshold
- `longFramesTotalTime`: The total accumulated time of all long frames, indicating the severity of performance issues
- **Detection Method**: Automatically uses Long Animation Frame API when available (Chrome 123+), falls back to manual tracking for broader browser support
- **Long Frames (`longFramesCount` & `longFramesTotalTime`)**: Frames exceeding 50ms threshold indicate potential UI jank or performance issues
### Long Frame Detection
The profiler includes sophisticated long frame detection using the Long Animation Frame (LoAF) API when available, with automatic fallback to manual frame tracking:
#### Detection Methods
1. **Long Animation Frame API (Primary)**
- **Browser Support**: Chrome 123+ (automatically detected)
- **Threshold**: 50ms (standard LoAF threshold)
- **Benefits**: Browser-level accuracy, script attribution, automatic buffering control
2. **Manual Frame Tracking (Fallback)**
- **Browser Support**: All browsers
- **Threshold**: 50ms (same as LoAF)
- **Implementation**: Uses requestAnimationFrame for frame monitoring
#### Metrics Collected
- **`longFramesCount`**: Number of frames exceeding the 50ms threshold
- **`longFramesTotalTime`**: Cumulative duration of all long frames during interaction
This helps identify:
- Rendering performance issues impacting user experience
- Interactions causing UI jank or frame drops
- Performance optimization opportunities
## Analytics Integration
### Analytics Components
The performance tracking system integrates with Grafana's analytics through two main components:
#### DashboardAnalyticsAggregator
Aggregates panel-level performance metrics for analytics reporting:
- Collects and aggregates metrics for all panel operations
- Tracks operation counts and total time spent per panel
- Sends comprehensive analytics reports via `reportInteraction` and `logMeasurement`
- Provides detailed panel breakdowns including slow panel detection
#### ScenePerformanceLogger
Creates Chrome DevTools performance marks and measurements for debugging:
- Generates performance marks for all dashboard and panel operations
- Creates performance measurements for timing visualization
- Provides console logging for real-time debugging
- Integrates with Chrome DevTools Performance timeline
### Chrome DevTools Integration
Performance operations are recorded as marks and measurements in the Chrome DevTools Performance timeline:
**Dashboard-level marks:**
```
Dashboard Interaction Start: <operationId>
Dashboard Interaction End: <operationId>
Dashboard Milestone: <operationId>:<milestone>
```
**Panel-level marks:**
```
Panel Query Start: <panelKey>:<operationId>
Panel Query End: <panelKey>:<operationId>
Panel Render Start: <panelKey>:<operationId>
Panel Render End: <panelKey>:<operationId>
```
### Data Collection
The system collects and reports data at two levels:
#### Dashboard Interaction Data
Reported for each interaction via `reportInteraction` and `logMeasurement`:
```typescript
{
interactionType: string, // Type of interaction
uid: string, // Dashboard UID
duration: number, // Total duration
networkDuration: number, // Network time
processingTime: number, // Client-side processing time
startTs: number, // Profile start timestamp
endTs: number, // Profile end timestamp
longFramesCount: number, // Number of long frames
longFramesTotalTime: number, // Total time of long frames
totalJSHeapSize: number, // Memory metrics
usedJSHeapSize: number,
jsHeapSizeLimit: number,
timeSinceBoot: number // Time since frontend boot
}
```
#### Panel-Level Metrics
Aggregated by `DashboardAnalyticsAggregator` for each panel with detailed operation tracking:
```typescript
{
panelId: string, // Panel identifier
pluginId: string, // Plugin type (e.g., 'timeseries', 'stat')
pluginVersion?: string, // Plugin version
totalQueryTime: number, // Total time spent in queries
totalTransformationTime: number, // Total time in transformations
totalRenderTime: number, // Total render time
totalFieldConfigTime: number, // Total field config time
pluginLoadTime: number, // Plugin initialization time
// Individual operations with UUID-based operation IDs
pluginLoadOperations: Array<{
operationId: string, // e.g., "plugin-load-550e8400-e29b-41d4-a716-446655440000"
duration: number,
timestamp: number
}>,
queryOperations: Array<{ // Individual query operations
operationId: string, // e.g., "query-a1b2c3d4-e5f6-7890-abcd-ef1234567890"
duration: number,
timestamp: number,
queryType?: string
}>,
transformationOperations: Array<{
duration: number,
timestamp: number,
transformationType?: string
}>,
fieldConfigOperations: Array<{
duration: number,
timestamp: number
}>,
renderOperations: Array<{ // Individual render operations
duration: number,
timestamp: number
}>,
// Performance analysis
isSlowPanel: boolean, // true if total time > SLOW_OPERATION_THRESHOLD_MS (500ms)
slowOperationThreshold: number, // Current threshold value (500ms)
totalPanelTime: number // Sum of all operation times
}
```
## Debugging and Development
### Enable Profiler Debug Logging
### Enable Performance Debug Logging
To observe profiling events in the browser console:
To observe performance profiling events in the browser console:
```javascript
localStorage.setItem('grafana.debug.scenes', 'true');
// Enable performance debug logging
localStorage.setItem('grafana.debug.sceneProfiling', 'true');
```
#### Console Output
### Performance Threshold Configuration
When debug logging is enabled, you'll see console logs for each profiling event:
The system uses a const threshold to identify slow operations:
- **Default Threshold**: `SLOW_OPERATION_THRESHOLD_MS = 500` milliseconds
- **Applies to**: Individual panel operations and total panel performance
- **Slow Panel Detection**: Panels exceeding threshold display ⚠️ warnings in logs
- **Analytics Integration**: Slow panel count included in dashboard analytics reports
**Example Slow Operation Warning:**
```javascript
SPL: [PANEL] timeseries-panel-1 query [query-abc123]: 125.3ms ⚠️ SLOW
DAA: 🎨 Panel timeseries-panel-1: 125.3ms total ⚠️ SLOW
```
### Console Output Examples
With debug logging enabled, you'll see detailed performance logs:
#### Dashboard Interaction Logs
```
SceneRenderProfiler: Profile started[clean]
├─ Origin: dashboard_view
└─ Timestamp: 1072.5ms
LongFrameDetector: Started tracking with LoAF API method, threshold: 50ms
... // intermediate steps adding profile crumbs
LongFrameDetector: Long frame detected (LoAF): 67.4ms at 1071.5ms
LongFrameDetector: Long frame detected (LoAF): 76.3ms at 1139.8ms
... // more long frame detections
SceneRenderProfiler: Profile completed
├─ Timestamp: 3530.6ms
├─ Total time: 156.8ms
├─ Slow frames: 16.3ms (1 frames)
└─ Long frames: 143.7ms (2 frames)
SceneRenderProfiler: Stopped long frame detection - profile complete at 3530.6ms
SRP: [PROFILER] dashboard_view started (clean)
LFD: Started tracking with LoAF API method, threshold: 50ms
SPL: [DASHBOARD] dashboard_view started: My Dashboard
SRP: [PROFILER] dashboard_view completed
├─ Duration: 156.8ms
├─ Long frames: 143.7ms (2 frames)
└─ Network time: 45.2ms
```
#### Panel Operation Logs
```
SPL: [PANEL] timeseries-panel-1 plugin-load: 39.0ms
SPL: [PANEL] timeseries-panel-1 query [query-a1b2c3d4-e5f6-7890-abcd]: 45.2ms
SPL: [PANEL] timeseries-panel-1 transform: 12.3ms
SPL: [PANEL] timeseries-panel-1 fieldConfig: 5.0ms
SPL: [PANEL] timeseries-panel-1 render: 23.8ms ⚠️ SLOW
```
#### VizPanelRenderProfiler Logs
The `VizPanelRenderProfiler` provides lifecycle and error logging (only visible with scenes debug logging enabled):
```
VizPanelRenderProfiler [My Dashboard Panel]: Plugin changed to timeseries
VizPanelRenderProfiler [My Dashboard Panel]: Cleaned up
VizPanelRenderProfiler: Not attached to a VizPanel
VizPanelRenderProfiler: Panel has no key, skipping tracking
```
#### Analytics Aggregator Summary
The `DashboardAnalyticsAggregator` creates structured **collapsible console groups** for detailed analysis. Each panel gets its own expandable group in the browser console:
```
DAA: [ANALYTICS] dashboard_view | 4 panels analyzed | 1 slow panels ⚠️
DAA: 📊 Dashboard (ms): {
duration: 156.8,
network: 45.2,
interactionType: "dashboard_view",
slowPanels: 1
}
DAA: 📈 Analytics payload: { /* comprehensive analytics data */ }
// Per-panel detailed breakdown (console group for each panel)
DAA: 🎨 Panel timeseries-panel-1: 125.3ms total ⚠️ SLOW
DAA: 🔧 Plugin: {
id: "timeseries",
version: "10.0.0",
panelId: "panel-1",
panelKey: "panel-1"
}
DAA: ⚡ Performance (ms): {
totalTime: 125.3,
isSlowPanel: true,
breakdown: {
query: 45.2,
transform: 12.3,
render: 23.8,
fieldConfig: 5.0,
pluginLoad: 39.0
}
}
DAA: 📊 Queries: {
count: 2,
details: [
{ operation: 1, duration: 25.1, timestamp: 1729692845100.123 },
{ operation: 2, duration: 20.1, timestamp: 1729692845125.456 }
]
}
DAA: 🔄 Transformations: {
count: 1,
details: [
{ operation: 1, duration: 12.3, timestamp: 1729692845150.789 }
]
}
DAA: 🎨 Renders: {
count: 1,
details: [
{ operation: 1, duration: 23.8, timestamp: 1729692845163.012 }
]
}
```
**Note**: The indentation shows the **console group hierarchy**. In the browser console, each panel creates a collapsible group that can be expanded to see detailed operation breakdowns. The main dashboard analytics group contains nested panel groups for organized analysis.
### Enable Echo Service Debug Logging
To observe Echo events in the browser console:
@@ -207,99 +544,79 @@ When Echo debug logging is enabled, you'll see console logs for each profiling e
### Browser Performance Profiler
Dashboard interactions can be recorded in the browser's performance profiler, where they appear as:
Dashboard and panel operations are recorded in the Chrome DevTools Performance timeline with detailed marks and measurements:
**Dashboard Marks:**
```
Dashboard Interaction <NAME_OF_INTERACTION>
Dashboard Interaction Start: dashboard-550e8400-e29b-41d4-a716-446655440000
Dashboard Interaction End: dashboard-550e8400-e29b-41d4-a716-446655440000
Dashboard Milestone: dashboard-550e8400-e29b-41d4-a716-446655440000:queries_complete
Dashboard Milestone: dashboard-550e8400-e29b-41d4-a716-446655440000:actual_interaction_complete
```
## Analytics Integration
**Panel Marks:**
### Interaction Reporting
Performance data is integrated with Grafana's analytics system through:
- **`reportInteraction`**: Reports interaction events to Echo service with performance data
- **`logMeasurement`**: Records Faro's performance measurements with metadata
### Data Collection
The system reports the following data for each interaction:
```typescript
{
interactionType: string, // Type of interaction
uid: string, // Dashboard UID
duration: number, // Total duration
networkDuration: number, // Network time
processingTime: number, // Client-side processing time (duration - networkDuration)
startTs: number, // Profile start timestamp
endTs: number, // Profile end timestamp
totalJSHeapSize: number, // Memory metrics
usedJSHeapSize: number,
jsHeapSizeLimit: number,
longFramesCount: number, // Number of long frames (>50ms threshold)
longFramesTotalTime: number, // Total time of all long frames
timeSinceBoot: number // Time since frontend boot
}
```
Panel Plugin Load Start: panel-1:plugin-load-a1b2c3d4-e5f6-7890-abcd-ef1234567890
Panel Plugin Load End: panel-1:plugin-load-a1b2c3d4-e5f6-7890-abcd-ef1234567890
Panel Query Start: panel-1:query-b2c3d4e5-f6g7-8901-bcde-f23456789012
Panel Query End: panel-1:query-b2c3d4e5-f6g7-8901-bcde-f23456789012
Panel Transform Start: panel-1:transform-c3d4e5f6-g7h8-9012-cdef-345678901234
Panel Transform End: panel-1:transform-c3d4e5f6-g7h8-9012-cdef-345678901234
Panel Field Config Start: panel-1:fieldConfig-d4e5f6g7-h8i9-0123-def0-456789012345
Panel Field Config End: panel-1:fieldConfig-d4e5f6g7-h8i9-0123-def0-456789012345
Panel Render Start: panel-1:render-e5f6g7h8-i9j0-1234-ef01-567890123456
Panel Render End: panel-1:render-e5f6g7h8-i9j0-1234-ef01-567890123456
```
These marks enable visual timeline analysis of:
- Overall dashboard interaction timing
- Individual panel operation performance
- Parallel vs sequential operations
- Performance bottlenecks
## Implementation Details
The profiler is integrated into dashboard creation paths and uses a singleton pattern to share profiler instances across dashboard reloads. The performance tracking is implemented using the `SceneRenderProfiler` from the `@grafana/scenes` library.
### Architecture Overview
### Long Frame Detection
The performance tracking system consists of multiple integrated components with observer pattern architecture:
The profiler uses the Long Animation Frame (LoAF) API when available to monitor frame rendering performance during dashboard interactions:
1. **SceneRenderProfiler** (Scenes library - `performanceUtils` namespace)
- Singleton profiler instance shared across dashboard reloads
- Tracks dashboard interactions and manages long frame detection
- Integrates with VizPanelRenderProfiler for comprehensive panel-level tracking
#### Primary Method: Long Animation Frame API
2. **ScenePerformanceTracker** (Scenes library - `performanceUtils` namespace)
- Central coordinator implementing observer pattern architecture
- Distributes performance events to registered observers without coupling
- Provides type-safe interfaces for different event types
- Supports extensible observer registration with clean separation of concerns
- **Browser Support**: Chrome 123+ (automatically detected)
- **Threshold**: 50ms (standard LoAF threshold)
- **Benefits**:
- Browser-level accuracy and performance
- Standards-based implementation
- More efficient than manual tracking
- Automatic buffering control for real-time detection
3. **VizPanelRenderProfiler** (Scenes library - `performanceUtils` namespace)
- Automatically attached to individual panels when profiling is enabled
- Tracks complete panel lifecycle: plugin-load, query, transform, fieldConfig, render
- Uses UUID-based operation IDs with crypto fallback for cross-environment compatibility
- Reports structured performance data to ScenePerformanceTracker
#### Fallback Method: Manual Frame Tracking
4. **DashboardAnalyticsAggregator** (Grafana)
- **Conditionally initialized**: Only activated when `enableProfiling` is true
- Aggregates panel metrics for analytics reporting with slow panel detection
- Uses configurable threshold (SLOW_OPERATION_THRESHOLD_MS = 100ms)
- Sends comprehensive reports via reportInteraction and logMeasurement
- **Browser Support**: All browsers
- **Threshold**: 50ms (same as LoAF threshold)
- **Used when**: LoAF API is not available
- **Implementation**: Uses requestAnimationFrame for frame monitoring
Both methods track:
- **Count**: Number of frames exceeding the threshold
- **Total Time**: Cumulative duration of all long frames
#### Debug Output
With LoAF API:
```
LongFrameDetector: Long frame detected (LoAF): 67.4ms at 1234.5ms
```
With manual fallback:
```
LongFrameDetector: Long frame detected (manual): 38.2ms (threshold: 50ms)
```
This metric is particularly valuable for:
- Detecting rendering performance issues that impact user experience
- Identifying when interactions cause UI jank or frame drops
- Measuring the impact of performance optimizations on frame rendering
- Comparing performance across different browsers and environments
5. **ScenePerformanceLogger** (Grafana)
- Creates Chrome DevTools performance marks and measurements
- Provides structured console logging for debugging with localStorage controls
- Maps operations to standardized performance mark names
- Integrates with browser Performance Timeline API
### Tab Inactivity Handling
To prevent meaningless profiling data when users switch browser tabs, the `SceneRenderProfiler` implements dual protection mechanisms:
#### Primary Protection: Page Visibility API
#### Page Visibility API
The profiler automatically cancels active profiling sessions when the browser tab becomes inactive:
@@ -313,109 +630,71 @@ document.addEventListener('visibilitychange', () => {
This provides immediate response to tab switches using the browser's native visibility change events.
#### Fallback Protection: Frame Length Detection
#### Frame Length Measurement for Performance Analysis
As a backup mechanism, the profiler detects tab inactivity by monitoring frame duration:
The profiler measures frame lengths during the post-interaction recording window for performance analysis:
```javascript
if (frameLength > TAB_INACTIVE_THRESHOLD) {
// 1000ms
this.cancelProfile();
return;
}
const frameLength = currentFrameTime - lastFrameTime;
this.#recordedTrailingSpans.push(frameLength);
```
This fallback catches cases where visibility events might be missed and prevents recording of artificially long frame times (hours instead of milliseconds) that occur when `requestAnimationFrame` callbacks resume after tab reactivation.
**Note**: Frame length measurement is used for performance analytics only. The profiler does **not** use frame length thresholds for tab inactivity detection. Tab inactivity protection relies exclusively on the Page Visibility API for accurate and immediate response to tab changes.
### Profile Isolation and Overlapping Interactions
### Profile Isolation
To ensure accurate performance measurements, the `SceneRenderProfiler` implements profile isolation to handle rapid user interactions:
To ensure accurate performance measurements, the profiler implements automatic profile cancellation when handling rapid user interactions:
#### Understanding Trailing Frame Recording
**Trailing Frame Recording**: After an interaction completes, the profiler continues recording for 2 seconds (POST_STORM_WINDOW) to capture delayed rendering effects.
After the main interaction completes, the profiler continues to record "trailing frames" for 2 seconds (POST_STORM_WINDOW) to capture any delayed rendering effects. This ensures complete performance measurement including:
- Delayed DOM updates
- Asynchronous rendering operations
- Secondary effects from the initial interaction
#### Problem: Mixed Performance Data
When users perform rapid interactions during this 2-second trailing frame window (e.g., quickly changing time ranges or triggering a refresh), the performance data from multiple actions could be mixed into a single profile. This led to:
- Inaccurate performance measurements
- Profile events that never completed
- Crumbs from different interactions being combined
- Trailing frames from one interaction being attributed to another
#### Solution: Automatic Profile Cancellation
Starting with `@grafana/scenes` v6.30.4, the profiler automatically cancels the current profile when a new interaction begins while trailing frames are still being recorded:
**Automatic Cancellation**: When a new interaction begins during trailing frame recording, the current profile is cancelled to prevent mixing performance data:
```javascript
// When new profile is requested while still recording trailing frames
if (this.#trailAnimationFrameId) {
this.cancelProfile();
this._startNewProfile(name, true); // true = forced profile
} else {
this.addCrumb(name);
this._startNewProfile(name, true); // forced profile
}
```
This ensures:
**Profile Types**:
- Each interaction gets its own isolated measurement
- No mixing of performance data between different user actions
- Clean separation of interaction metrics
- **Clean Start**: No active profile when starting
- **Forced Start**: Previous profile cancelled for new interaction
#### Profile Start Types
The profiler now distinguishes between two types of profile starts:
1. **Clean Start**: Profile started when no other profile is active
2. **Forced Start (Interrupted)**: Profile started by cancelling a previous active profile
This information is logged in debug mode:
```
SceneRenderProfiler: Profile started[forced]: {origin: "refresh", crumbs: []} <timestamp>
SceneRenderProfiler: Profile started[clean]: {origin: "dashboard_view", crumbs: []} <timestamp>
```
Additionally, when a profile is cancelled due to overlapping interactions:
```
SceneRenderProfiler: Cancelled recording frames, new profile started
```
#### Example Scenario
1. User changes time range (profile starts)
2. Dashboard finishes loading after 500ms (main profile complete)
3. Profiler continues recording trailing frames to capture delayed effects
4. At 1 second, user clicks refresh button
5. Without this fix: Refresh would be added as a crumb to the time range profile
6. With this fix: Time range profile is cancelled, new refresh profile starts cleanly
This fix is particularly important for dashboards with:
- Auto-refresh enabled
- Slow API responses
- Rapid user interactions
Without profile isolation, these scenarios could result in profiles that never complete and mix data from multiple unrelated interactions.
This ensures each interaction gets isolated measurements, preventing data contamination from overlapping operations.
## Related Documentation
- [PR #858 - Add SceneRenderProfiler to scenes](https://github.com/grafana/scenes/pull/858)
- [PR #99629 - Dashboard render performance metrics](https://github.com/grafana/grafana/pull/99629)
- [PR #108658 - Dashboard: Tweak interaction tracking](https://github.com/grafana/grafana/pull/108658)
- [PR #1195 - Enhance SceneRenderProfiler with additional interaction tracking](https://github.com/grafana/scenes/pull/1195)
- [PR #1198 - Make SceneRenderProfiler optional and injectable](https://github.com/grafana/scenes/pull/1198)
- [PR #1199 - SceneRenderProfiler: add start and end timestamps to profile events](https://github.com/grafana/scenes/pull/1199)
- [PR #1205 - SceneRenderProfiler: Handle tab inactivity](https://github.com/grafana/scenes/pull/1205)
- [PR #1209 - SceneRenderProfiler: Only capture network requests within measurement window](https://github.com/grafana/scenes/pull/1209)
- [PR #1211 - SceneRenderProfiler: Improve profiler accuracy by adding cancellation and skipping inactive tabs](https://github.com/grafana/scenes/pull/1211)
- [PR #1212 - SceneQueryController: Fix profiler query controller registration on scene re-activation](https://github.com/grafana/scenes/pull/1212)
- [PR #1225 - SceneRenderProfiler: Handle overlapping profiles by cancelling previous profile](https://github.com/grafana/scenes/pull/1225)
- [PR #1235 - Implement long frame detection with LoAF API and manual fallback](https://github.com/grafana/scenes/pull/1235)
### Foundational Performance System
- [PR #858 - Add SceneRenderProfiler to scenes](https://github.com/grafana/scenes/pull/858) ✅ Merged
- [PR #99629 - Dashboard render performance metrics](https://github.com/grafana/grafana/pull/99629) ✅ Merged
- [PR #108658 - Dashboard: Tweak interaction tracking](https://github.com/grafana/grafana/pull/108658) ✅ Merged
### Enhanced Profiling Features
- [PR #1195 - Enhance SceneRenderProfiler with additional interaction tracking](https://github.com/grafana/scenes/pull/1195) ✅ Merged
- [PR #1198 - Make SceneRenderProfiler optional and injectable](https://github.com/grafana/scenes/pull/1198) ✅ Merged
- [PR #1199 - SceneRenderProfiler: add start and end timestamps to profile events](https://github.com/grafana/scenes/pull/1199) ✅ Merged
- [PR #1205 - SceneRenderProfiler: Handle tab inactivity](https://github.com/grafana/scenes/pull/1205) ✅ Merged
- [PR #1209 - SceneRenderProfiler: Only capture network requests within measurement window](https://github.com/grafana/scenes/pull/1209) ✅ Merged
- [PR #1211 - SceneRenderProfiler: Improve profiler accuracy by adding cancellation and skipping inactive tabs](https://github.com/grafana/scenes/pull/1211) ✅ Merged
- [PR #1212 - SceneQueryController: Fix profiler query controller registration on scene re-activation](https://github.com/grafana/scenes/pull/1212) ✅ Merged
- [PR #1225 - SceneRenderProfiler: Handle overlapping profiles by cancelling previous profile](https://github.com/grafana/scenes/pull/1225) ✅ Merged
- [PR #1235 - Implement long frame detection with LoAF API and manual fallback](https://github.com/grafana/scenes/pull/1235) ✅ Merged
### Panel-Level Performance Attribution System
- [PR #1265 - Panel-level performance attribution system](https://github.com/grafana/scenes/pull/1265) 🔄 **In Review**
- Modern observer pattern architecture with ScenePerformanceTracker
- Complete panel lifecycle tracking (plugin-load, query, transform, fieldConfig, render)
- UUID-based operation IDs with crypto fallback for cross-environment compatibility
- performanceUtils namespace organization for clean API separation
- Type-safe performance interfaces with comprehensive TypeScript support
- Chrome DevTools integration via performance marks and measurements
- [PR #112137 - Dashboard performance analytics system with Scenes integration](https://github.com/grafana/grafana/pull/112137) 🔄 **In Review**
- DashboardAnalyticsAggregator with conditional initialization
- ScenePerformanceLogger for debugging and Chrome DevTools integration
- Configurable performance thresholds (SLOW_OPERATION_THRESHOLD_MS)
- Comprehensive analytics reporting via reportInteraction and logMeasurement
- Integration with the panel-level performance attribution system from PR #1265
@@ -0,0 +1,82 @@
// Standardized performance mark names for Scene operations
export const PERFORMANCE_MARKS = {
// Panel operations
PANEL_QUERY_START: (panelKey: string, operationId?: string) =>
operationId ? `scenes.panel.query.start.${panelKey}.${operationId}` : `scenes.panel.query.start.${panelKey}`,
PANEL_QUERY_END: (panelKey: string, operationId?: string) =>
operationId ? `scenes.panel.query.end.${panelKey}.${operationId}` : `scenes.panel.query.end.${panelKey}`,
PANEL_PLUGIN_LOAD_START: (panelKey: string, operationId?: string) =>
operationId
? `scenes.panel.pluginLoad.start.${panelKey}.${operationId}`
: `scenes.panel.pluginLoad.start.${panelKey}`,
PANEL_PLUGIN_LOAD_END: (panelKey: string, operationId?: string) =>
operationId ? `scenes.panel.pluginLoad.end.${panelKey}.${operationId}` : `scenes.panel.pluginLoad.end.${panelKey}`,
PANEL_FIELD_CONFIG_START: (panelKey: string, operationId?: string) =>
operationId
? `scenes.panel.fieldConfig.start.${panelKey}.${operationId}`
: `scenes.panel.fieldConfig.start.${panelKey}`,
PANEL_FIELD_CONFIG_END: (panelKey: string, operationId?: string) =>
operationId
? `scenes.panel.fieldConfig.end.${panelKey}.${operationId}`
: `scenes.panel.fieldConfig.end.${panelKey}`,
PANEL_RENDER_START: (panelKey: string, operationId?: string) =>
operationId ? `scenes.panel.render.start.${panelKey}.${operationId}` : `scenes.panel.render.start.${panelKey}`,
PANEL_RENDER_END: (panelKey: string, operationId?: string) =>
operationId ? `scenes.panel.render.end.${panelKey}.${operationId}` : `scenes.panel.render.end.${panelKey}`,
PANEL_TRANSFORM_START: (panelKey: string, transformationId: string, operationId?: string) =>
operationId
? `scenes.panel.transform.start.${panelKey}.${transformationId}.${operationId}`
: `scenes.panel.transform.start.${panelKey}.${transformationId}`,
PANEL_TRANSFORM_END: (panelKey: string, transformationId: string, operationId?: string) =>
operationId
? `scenes.panel.transform.end.${panelKey}.${transformationId}.${operationId}`
: `scenes.panel.transform.end.${panelKey}.${transformationId}`,
PANEL_TRANSFORM_ERROR: (panelKey: string, transformationId: string, operationId?: string) =>
operationId
? `scenes.panel.transform.error.${panelKey}.${transformationId}.${operationId}`
: `scenes.panel.transform.error.${panelKey}.${transformationId}`,
// Dashboard operations
DASHBOARD_INTERACTION_START: (operationId: string) => `scenes.dashboard.interaction.start.${operationId}`,
DASHBOARD_INTERACTION_END: (operationId: string) => `scenes.dashboard.interaction.end.${operationId}`,
DASHBOARD_MILESTONE: (operationId: string, milestone: string) =>
`scenes.dashboard.milestone.${milestone}.${operationId}`,
// Query operations
QUERY_START: (panelId: string, queryId: string) => `scenes.query.start.${panelId}.${queryId}`,
QUERY_END: (panelId: string, queryId: string) => `scenes.query.end.${panelId}.${queryId}`,
};
// Standardized performance measure names
export const PERFORMANCE_MEASURES = {
// Panel operations
PANEL_QUERY: (panelKey: string, operationId?: string) =>
operationId ? `scenes.panel.query.duration.${panelKey}.${operationId}` : `scenes.panel.query.duration.${panelKey}`,
PANEL_PLUGIN_LOAD: (panelKey: string, operationId?: string) =>
operationId
? `scenes.panel.pluginLoad.duration.${panelKey}.${operationId}`
: `scenes.panel.pluginLoad.duration.${panelKey}`,
PANEL_FIELD_CONFIG: (panelKey: string, operationId?: string) =>
operationId
? `scenes.panel.fieldConfig.duration.${panelKey}.${operationId}`
: `scenes.panel.fieldConfig.duration.${panelKey}`,
PANEL_RENDER: (panelKey: string, operationId?: string) =>
operationId
? `scenes.panel.render.duration.${panelKey}.${operationId}`
: `scenes.panel.render.duration.${panelKey}`,
PANEL_TRANSFORM: (panelKey: string, transformationId: string, operationId?: string) =>
operationId
? `scenes.panel.transform.duration.${panelKey}.${transformationId}.${operationId}`
: `scenes.panel.transform.duration.${panelKey}.${transformationId}`,
// Dashboard operations
DASHBOARD_INTERACTION: (operationId: string) => `scenes.dashboard.interaction.duration.${operationId}`,
// Query operations
QUERY: (panelId: string, queryId: string) => `scenes.query.duration.${panelId}.${queryId}`,
};
/**
* Threshold in milliseconds for determining slow operations (panels, queries, transformations, etc.)
*/
export const SLOW_OPERATION_THRESHOLD_MS = 500;
@@ -0,0 +1,139 @@
import { store } from '@grafana/data';
import { performanceUtils, writePerformanceLog } from '@grafana/scenes';
/**
* Utility function to register a performance observer with the global tracker
* Reduces duplication between ScenePerformanceLogger and DashboardAnalyticsAggregator
*/
export function registerPerformanceObserver(
observer: performanceUtils.ScenePerformanceObserver,
loggerName: string
): void {
const tracker = performanceUtils.getScenePerformanceTracker();
tracker.addObserver(observer);
writePerformanceLog(loggerName, 'Initialized globally and registered as performance observer');
}
/**
* Chrome-specific performance.memory interface (non-standard)
*/
export interface PerformanceMemory {
totalJSHeapSize: number;
usedJSHeapSize: number;
jsHeapSizeLimit: number;
}
/**
* Extended Performance interface with Chrome's memory property
*/
export interface PerformanceWithMemory extends Performance {
memory?: PerformanceMemory;
}
/**
* Type guard to check if performance has memory property (Chrome-specific)
*/
function hasPerformanceMemory(perf: Performance): perf is PerformanceWithMemory {
return 'memory' in perf;
}
/**
* Safely get performance memory metrics (Chrome-specific, non-standard)
* Returns zero values for browsers without performance.memory support
*/
export function getPerformanceMemory(): PerformanceMemory {
if (hasPerformanceMemory(performance)) {
return {
totalJSHeapSize: performance.memory?.totalJSHeapSize || 0,
usedJSHeapSize: performance.memory?.usedJSHeapSize || 0,
jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit || 0,
};
}
// Fallback for browsers without performance.memory
return {
totalJSHeapSize: 0,
usedJSHeapSize: 0,
jsHeapSizeLimit: 0,
};
}
/**
* Check if performance logging is enabled via localStorage
*/
function isPerformanceLoggingEnabled(): boolean {
if (typeof window !== 'undefined') {
return store.get('grafana.debug.sceneProfiling') === 'true';
}
return false;
}
/**
* Write a collapsible performance log group (follows writePerformanceLog pattern)
*/
export function writePerformanceGroupStart(logger: string, message: string): void {
if (isPerformanceLoggingEnabled()) {
// eslint-disable-next-line no-console
console.groupCollapsed(`${logger}: ${message}`);
}
}
/**
* Write a performance log within a group (follows writePerformanceLog pattern)
*/
export function writePerformanceGroupLog(logger: string, message: string, data?: unknown): void {
if (isPerformanceLoggingEnabled()) {
if (data) {
// eslint-disable-next-line no-console
console.log(message, data);
} else {
// eslint-disable-next-line no-console
console.log(message);
}
}
}
/**
* End a performance log group (follows writePerformanceLog pattern)
*/
export function writePerformanceGroupEnd(): void {
if (isPerformanceLoggingEnabled()) {
// eslint-disable-next-line no-console
console.groupEnd();
}
}
/**
* Safely creates a performance mark, ignoring errors if the Performance API is not available.
*/
export function createPerformanceMark(name: string, timestamp?: number): void {
try {
if (typeof performance !== 'undefined' && performance.mark) {
if (timestamp !== undefined) {
performance.mark(name, { startTime: timestamp });
} else {
performance.mark(name);
}
}
} catch (error) {
console.error(`❌ Failed to create performance mark: ${name}`, { timestamp, error });
}
}
/**
* Safely creates a performance measure, ignoring errors if the Performance API is not available.
*/
export function createPerformanceMeasure(name: string, startMark: string, endMark?: string): void {
try {
if (typeof performance !== 'undefined' && performance.measure) {
if (endMark) {
performance.measure(name, startMark, endMark);
} else {
performance.measure(name, startMark);
}
}
} catch (error) {
console.error(`❌ Failed to create performance measure: ${name}`, { startMark, endMark, error });
}
}
@@ -1,6 +1,6 @@
import { Scope, ScopeNode, store as storeImpl } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import { SceneRenderProfiler } from '@grafana/scenes';
import { performanceUtils } from '@grafana/scenes';
import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler';
import { ScopesApiClient } from '../ScopesApiClient';
@@ -54,7 +54,8 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
private apiClient: ScopesApiClient,
private dashboardsService: ScopesDashboardsService,
private store = storeImpl,
private interactionProfiler: SceneRenderProfiler | undefined = config.dashboardPerformanceMetrics.length
private interactionProfiler: performanceUtils.SceneRenderProfiler | undefined = config.dashboardPerformanceMetrics
.length
? getDashboardSceneProfiler()
: undefined
) {
@@ -19,6 +19,7 @@ describe('Dashboard reload', () => {
let dashboardReloadSpy: jest.SpyInstance;
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(console, 'warn').mockImplementation();
});
beforeAll(() => {
config.featureToggles.scopeFilters = true;
+40 -40
View File
@@ -2324,15 +2324,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:2.3.6":
version: 2.3.6
resolution: "@formatjs/ecma402-abstract@npm:2.3.6"
"@formatjs/ecma402-abstract@npm:2.3.4":
version: 2.3.4
resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
dependencies:
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc
checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2
languageName: node
linkType: hard
@@ -2376,13 +2376,13 @@ __metadata:
linkType: hard
"@formatjs/intl-durationformat@npm:^0.7.0":
version: 0.7.6
resolution: "@formatjs/intl-durationformat@npm:0.7.6"
version: 0.7.4
resolution: "@formatjs/intl-durationformat@npm:0.7.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
tslib: "npm:^2.8.0"
checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b
checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15
languageName: node
linkType: hard
@@ -2395,12 +2395,12 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.6.2":
version: 0.6.2
resolution: "@formatjs/intl-localematcher@npm:0.6.2"
"@formatjs/intl-localematcher@npm:0.6.1":
version: 0.6.1
resolution: "@formatjs/intl-localematcher@npm:0.6.1"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064
checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998
languageName: node
linkType: hard
@@ -3555,11 +3555,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:^6.40.1":
version: 6.40.1
resolution: "@grafana/scenes-react@npm:6.40.1"
"@grafana/scenes-react@npm:^6.41.0":
version: 6.42.0
resolution: "@grafana/scenes-react@npm:6.42.0"
dependencies:
"@grafana/scenes": "npm:6.40.1"
"@grafana/scenes": "npm:6.42.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3571,7 +3571,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/4515d53609d7b49e234bbc5c3da8131406c20b8ac0e54b1a5ca3ce3d1d42220ff08db65d31c83c9796188ef9d76e9a24fef170badb793a56b44bbabd31302575
checksum: 10/05db719566e8499b2f9f46ac7da83b2c669bc29d3169b42cb800ad0bb099ad7d5dead4109975d5c65bb711a98965f4c8eb20ea3e60d8f46c958be0508346f84e
languageName: node
linkType: hard
@@ -3601,9 +3601,9 @@ __metadata:
languageName: node
linkType: hard
"@grafana/scenes@npm:6.40.1, @grafana/scenes@npm:^6.40.1":
version: 6.40.1
resolution: "@grafana/scenes@npm:6.40.1"
"@grafana/scenes@npm:6.42.0, @grafana/scenes@npm:^6.41.0":
version: 6.42.0
resolution: "@grafana/scenes@npm:6.42.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3623,7 +3623,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/f3f33d58bca9d05cf8d9527564e2f6bd9f164b44531382972c2b1ed0a54f54e29a6130fcab5797cd5dbba673e5d678ae5d1ef9c01ce6fe7895ee0f0c9408bc10
checksum: 10/790afb142b6aa78a8a4f1359ec363e5a6c6c198873eafd39398b125d65881fb37514cf0c2f424e020dedca53a11a378f1dc87bab8ebc69fa76f5d10d59837e9f
languageName: node
linkType: hard
@@ -6029,9 +6029,9 @@ __metadata:
linkType: hard
"@openfeature/core@npm:^1.9.0":
version: 1.9.1
resolution: "@openfeature/core@npm:1.9.1"
checksum: 10/6099e16b1b4cd6e3c45c05ab4acd44c9cb4ab501b676ab6f3e77f0be1b56abc7506c5629187381333a02975d315d831e0905582435b356d9676255e72a465899
version: 1.9.0
resolution: "@openfeature/core@npm:1.9.0"
checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094
languageName: node
linkType: hard
@@ -6056,11 +6056,11 @@ __metadata:
linkType: hard
"@openfeature/web-sdk@npm:^1.6.1":
version: 1.6.2
resolution: "@openfeature/web-sdk@npm:1.6.2"
version: 1.6.1
resolution: "@openfeature/web-sdk@npm:1.6.1"
peerDependencies:
"@openfeature/core": ^1.9.0
checksum: 10/0fcc0ef76ff51d4725a00ff07755b21941b647f775d1ff04fc3d973143e78c909ccba485f582fcbc7f6201f42c795c85bcb103c43d64924fedf13ea517f625ac
checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d
languageName: node
linkType: hard
@@ -18832,8 +18832,8 @@ __metadata:
"@grafana/plugin-ui": "npm:^0.10.10"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:^6.40.1"
"@grafana/scenes-react": "npm:^6.40.1"
"@grafana/scenes": "npm:^6.41.0"
"@grafana/scenes-react": "npm:^6.41.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"
@@ -23060,9 +23060,9 @@ __metadata:
linkType: hard
"lossless-json@npm:^4.1.1":
version: 4.3.0
resolution: "lossless-json@npm:4.3.0"
checksum: 10/a984a882c79b6e62a917d0202518472c17587bdee002f1427d7ec61115f9fbdeb5f0baa9f0e9fff8d9dbacd17da6b6c910012b2dcab69dd33d151cb7c13d5a37
version: 4.2.0
resolution: "lossless-json@npm:4.2.0"
checksum: 10/b29cbf9a90b8f3108219ff410223a90a3a6ad14cf902eed4038571d9421c69ade7077fb59737e1ca20f3f404ddd68fad69be11365b0d79b4b61421b054759551
languageName: node
linkType: hard
@@ -23530,9 +23530,9 @@ __metadata:
linkType: hard
"micro-memoize@npm:^4.1.2":
version: 4.2.0
resolution: "micro-memoize@npm:4.2.0"
checksum: 10/260e27a5c15809f7dc435a89a424d93e49ccca62b743fa96ee72f254264df58c95edacbd847a2899a6d7f8ad0daef188548a8b12add5ccff83e88869a50a762c
version: 4.1.3
resolution: "micro-memoize@npm:4.1.3"
checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402
languageName: node
linkType: hard
@@ -24230,11 +24230,11 @@ __metadata:
linkType: hard
"nanoid@npm:^5.0.9":
version: 5.1.6
resolution: "nanoid@npm:5.1.6"
version: 5.1.5
resolution: "nanoid@npm:5.1.5"
bin:
nanoid: bin/nanoid.js
checksum: 10/4109dbcf596d7f297a9b42f459b8f01694a03ebbdd2f41408d963ad54e5ec7234cbe7b4acad137751f31add11bb4fb3415a3e688082516745812811f05570014
checksum: 10/6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5
languageName: node
linkType: hard