public/app/features/dashboard/services/dashboard-render-performance-profiling.md
This documentation describes the dashboard render performance metrics exposed from Grafana's frontend.
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.
Dashboard performance metrics are configured in the Grafana configuration file (grafana.ini) under the [dashboards] section:
[dashboards]
# Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards
dashboard_performance_metrics = *
Configuration Options:
* - Enable profiling on all dashboards<comma-separated-list-of-dashboard-uid> - Enable profiling on specific dashboards only"" (empty) - Disable performance metrics (default)Examples:
# Enable for all dashboards
dashboard_performance_metrics = *
# Enable for specific dashboards
dashboard_performance_metrics = dashboard-uid-1,dashboard-uid-2,dashboard-uid-3
# Disable performance metrics
dashboard_performance_metrics =
The system tracks various dashboard interaction types automatically using the @grafana/scenes library. Each interaction is captured with a specific origin identifier that describes the type of user action performed. In Grafana, these interaction events are then reported as dashboard_render events with interaction type information included.
The following dashboard interaction types are tracked for dashboard render performance profiling:
| Interaction Type | Trigger | When Measured |
|---|---|---|
dashboard_view | Dashboard view | When user loads or navigates to a dashboard |
refresh | Manual/Auto refresh | When user clicks refresh button or auto-refresh triggers |
time_range_change | Time picker changes | When user changes time range in time picker |
filter_added | Ad-hoc filter addition | When user adds a new filter to the dashboard |
filter_removed | Ad-hoc filter removal | When user removes a filter from the dashboard |
filter_changed | Ad-hoc filter modification | When user changes filter values or operators |
filter_restored | Ad-hoc filter restoration | When user restores a previously applied filter |
variable_value_changed | Variable value changes | When user changes dashboard variable values |
scopes_changed | Scopes modifications | When user modifies dashboard scopes |
The interactions mentioned above are reported to Echo service as well as sent to Faro as dashboard_render measurements:
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,
};
reportInteraction('dashboard_render', {
interactionType: e.origin,
uid,
operationId: e.operationId, // Shared operationId for correlating with panel_render measurements
...payload,
});
logMeasurement('dashboard_render', payload, {
interactionType: e.origin,
dashboard: uid,
title: title,
operationId: e.operationId, // Shared operationId for correlating with panel_render measurements
});
The profiling system uses profiler event's origin directly as the interactionType, providing direct mapping between user actions and performance measurements.
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:
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:
query-a1b2c3d4-e5f6-7890-abcd-ef1234567890)The system generates unique operation IDs using a standardized format:
<operation-type>-<uuid>
Examples:
plugin-load-550e8400-e29b-41d4-a716-446655440000query-a1b2c3d4-e5f6-7890-abcd-ef1234567890transform-b2c3d4e5-f6g7-8901-bcde-f23456789012fieldConfig-c3d4e5f6-g7h8-9012-cdef-345678901234render-d4e5f6g7-h8i9-0123-def0-456789012345Benefits:
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.
// 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:
// 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 logsEach interaction profile event captures:
interface SceneInteractionProfileEvent {
origin: string; // Interaction type
duration: number; // Total interaction duration
networkDuration: number; // Network requests duration
totalJSHeapSize: number; // JavaScript heap size metrics
usedJSHeapSize: number; // Used JavaScript heap size
jsHeapSizeLimit: number; // JavaScript heap size limit
startTs: number; // Profile start timestamp
endTs: number; // Profile end timestamp
longFramesCount: number; // Number of long frames (>50ms threshold)
longFramesTotalTime: number; // Total time of long frames during interaction
}
For each tracked interaction, the system collects:
duration: Total interaction time from start to finishnetworkDuration: Time spent on network requests (API calls, data fetching)processingTime: Client-side processing time calculated as duration - networkDurationlongFramesCount: Number of frames that exceeded the 50ms thresholdlongFramesTotalTime: Cumulative time of all long frames during the interactionThe performance metrics provide detailed insights into where time is spent during dashboard interactions:
duration): Complete time from interaction start to completionnetworkDuration): Time spent waiting for server responses (data source queries, API calls)processingTime): Time spent on client-side operations (rendering, computations, DOM updates)longFramesCount & longFramesTotalTime): Frames exceeding 50ms threshold indicate potential UI jank or performance issuesThe profiler includes sophisticated long frame detection using the Long Animation Frame (LoAF) API when available, with automatic fallback to manual frame tracking:
Long Animation Frame API (Primary)
Manual Frame Tracking (Fallback)
longFramesCount: Number of frames exceeding the 50ms thresholdlongFramesTotalTime: Cumulative duration of all long frames during interactionThis helps identify:
The performance tracking system integrates with Grafana's analytics through two main components:
Aggregates panel-level performance metrics for analytics reporting:
reportInteraction and logMeasurementpanel_render measurements for each panel with aggregated metrics via logMeasurementCreates Chrome DevTools performance marks and measurements for debugging:
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>
The system collects and reports data at two levels:
Reported for each interaction via reportInteraction and logMeasurement:
{
interactionType: string, // Type of interaction
uid: string, // Dashboard UID
operationId: string, // Unique operationId for correlating with panel_render measurements
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
}
Correlation: The operationId field is shared between dashboard_render and all associated panel_render measurements, enabling correlation of panel metrics with their parent dashboard interaction.
Aggregated by DashboardAnalyticsAggregator for each panel with detailed operation tracking:
{
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
}
For each dashboard interaction, individual panel_render measurements are sent separately from dashboard_render via logMeasurement. These measurements are sent for all panels with collected metrics (even if all times are 0), providing granular panel-level analytics.
When sent: After dashboard_render interaction, one panel_render measurement per panel
Correlation: All panel_render measurements share the same operationId as their parent dashboard_render interaction, enabling correlation between dashboard and panel-level metrics.
Measurement values (via logMeasurement):
{
totalTime: number, // Sum of all operation times
queryCount: number, // Number of query operations
transformCount: number, // Number of transformation operations
renderCount: number, // Number of render operations
fieldConfigCount: number, // Number of field config operations
pluginLoadCount: number // Number of plugin load operations (0 or 1)
}
Measurement metadata (via logMeasurement context):
{
panelKey: string, // Panel key identifier
pluginId: string, // Panel plugin identifier
panelId: string, // Panel identifier
operationId: string // Shared operationId for correlating with dashboard_render
}
Note: dashboard, title, and interactionType are not included in panel_render metadata since they can be obtained by correlating with the parent dashboard_render interaction using operationId.
Correlating panel_render with dashboard_render:
All panel_render measurements share the same operationId as their parent dashboard_render interaction.
To observe performance profiling events in the browser console:
// Enable performance debug logging
localStorage.setItem('grafana.debug.sceneProfiling', 'true');
The system uses a const threshold to identify slow operations:
SLOW_OPERATION_THRESHOLD_MS = 500 millisecondsExample Slow Operation Warning:
SPL: [PANEL] timeseries-panel-1 query [query-abc123]: 125.3ms ⚠️ SLOW
DAA: 🎨 Panel timeseries-panel-1: 125.3ms total ⚠️ SLOW
With debug logging enabled, you'll see detailed performance logs:
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
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
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
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,
}
}
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.
After the detailed panel breakdown, a summary of all panel_render measurements sent is displayed:
DAA: 📤 Panel render interactions: 4 panels reported
DAA: 🎨 timeseries-panel-1: {
totalTime: 125.3,
operations: {
queries: 2,
transforms: 1,
renders: 1,
fieldConfigs: 1,
pluginLoads: 1
},
isSlowPanel: true,
warning: "SLOW"
}
DAA: 🎨 stat-panel-2: {
totalTime: 45.2,
operations: {
queries: 1,
transforms: 0,
renders: 1,
fieldConfigs: 1,
pluginLoads: 0
},
isSlowPanel: false
}
DAA: 🎨 table-panel-3: {
totalTime: 89.7,
operations: {
queries: 1,
transforms: 2,
renders: 1,
fieldConfigs: 1
},
isSlowPanel: false
}
DAA: 🎨 graph-panel-4: {
totalTime: 234.1,
operations: {
queries: 3,
transforms: 1,
renders: 2,
fieldConfigs: 1
},
isSlowPanel: false
}
This summary provides a quick overview of all panels that had panel_render measurements sent, showing total time, operation counts, and slow panel warnings.
To observe Echo events in the browser console:
_debug.echo.enable();
When Echo debug logging is enabled, you'll see console logs for each profiling event captured by Echo service:
[EchoSrv: interaction event]: {interactionName: 'dashboard_render', properties: {…}, meta: {…}}
Dashboard and panel operations are recorded in the Chrome DevTools Performance timeline with detailed marks and measurements:
Dashboard Marks:
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
Panel Marks:
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:
The performance tracking system consists of multiple integrated components with observer pattern architecture:
SceneRenderProfiler (Scenes library - performanceUtils namespace)
ScenePerformanceTracker (Scenes library - performanceUtils namespace)
VizPanelRenderProfiler (Scenes library - performanceUtils namespace)
DashboardAnalyticsAggregator (Grafana)
enableProfiling is trueScenePerformanceLogger (Grafana)
To prevent meaningless profiling data when users switch browser tabs, the SceneRenderProfiler implements dual protection mechanisms:
The profiler automatically cancels active profiling sessions when the browser tab becomes inactive:
document.addEventListener('visibilitychange', () => {
if (document.hidden && this.#profileInProgress) {
this.cancelProfile();
}
});
This provides immediate response to tab switches using the browser's native visibility change events.
The profiler measures frame lengths during the post-interaction recording window for performance analysis:
const frameLength = currentFrameTime - lastFrameTime;
this.#recordedTrailingSpans.push(frameLength);
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.
To ensure accurate performance measurements, the profiler implements automatic profile cancellation when handling rapid user interactions:
Trailing Frame Recording: After an interaction completes, the profiler continues recording for 2 seconds (POST_STORM_WINDOW) to capture delayed rendering effects.
Automatic Cancellation: When a new interaction begins during trailing frame recording, the current profile is cancelled to prevent mixing performance data:
if (this.#trailAnimationFrameId) {
this.cancelProfile();
this._startNewProfile(name, true); // forced profile
}
Profile Types:
This ensures each interaction gets isolated measurements, preventing data contamination from overlapping operations.