From bb45dc88ac06f26e78daa98f1439cf9bf4d9cd66 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 30 Jul 2025 15:23:48 +0200 Subject: [PATCH] Dashboard: Tweak interaction performance tracking (#108658) * Dashboard: Tweak interaction tracking * Bump scenes to canary (https://github.com/grafana/scenes/pull/1195) * Update naming * Use latest canary * Update cenes, introduce singleton dashboard profiler, instrument v2 schema dashboard * Fix an oooooqpsiiii * Bmp scenes to 6.28.5 * Add documentation * Add event start/end time stamps, update documentation. Bump scenes * Docs * Move file * Typos --- package.json | 4 +- .../pages/DashboardScenePageStateManager.ts | 2 +- .../transformSaveModelSchemaV2ToScene.ts | 15 +- .../transformSaveModelToScene.ts | 59 ++---- .../dashboard/services/DashboardProfiler.ts | 34 +++ .../dashboard-render-performance-profiling.md | 199 ++++++++++++++++++ yarn.lock | 22 +- 7 files changed, 276 insertions(+), 59 deletions(-) create mode 100644 public/app/features/dashboard/services/DashboardProfiler.ts create mode 100644 public/app/features/dashboard/services/dashboard-render-performance-profiling.md diff --git a/package.json b/package.json index 278ffb6efcc..08f37ed064e 100644 --- a/package.json +++ b/package.json @@ -290,8 +290,8 @@ "@grafana/plugin-ui": "0.10.7", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^6.27.2", - "@grafana/scenes-react": "^6.27.2", + "@grafana/scenes": "6.28.6", + "@grafana/scenes-react": "6.28.6", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index d91cf0d2230..63f1aaa0bcd 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -267,7 +267,7 @@ abstract class DashboardScenePageStateManagerBase const queryController = sceneGraph.getQueryController(dashboard); trackDashboardSceneLoaded(dashboard, measure?.duration); - queryController?.startProfile('DashboardScene'); + queryController?.startProfile('dashboard_view'); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 986e0018329..2b6dc7d5864 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -51,6 +51,10 @@ import { DeprecatedInternalId, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { + getDashboardInteractionCallback, + getDashboardSceneProfiler, +} from 'app/features/dashboard/services/DashboardProfiler'; import { DashboardMeta } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; @@ -157,6 +161,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo uid === '*' || uid === metadata.name) !== -1, + onProfileComplete: getDashboardInteractionCallback(metadata.name, dashboard.title), + }, + getDashboardSceneProfiler() + ); + const dashboardScene = new DashboardScene( { description: dashboard.description, @@ -184,7 +197,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo uid === '*' || uid === oldModel.uid) !== -1, + onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), + }, + getDashboardSceneProfiler() + ); + const behaviorList: SceneObjectState['$behaviors'] = [ new behaviors.CursorSync({ sync: oldModel.graphTooltip, }), - new behaviors.SceneQueryController({ - enableProfiling: - config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, - onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), - }), + queryController, registerDashboardMacro, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), @@ -499,40 +507,3 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => { panel.snapshotData = []; } }; - -function getDashboardInteractionCallback(uid: string, title: string) { - return (e: SceneInteractionProfileEvent) => { - let interactionType = ''; - - if (e.origin === 'SceneTimeRange') { - interactionType = 'time-range-change'; - } else if (e.origin === 'SceneRefreshPicker') { - interactionType = 'refresh'; - } else if (e.origin === 'DashboardScene') { - interactionType = 'view'; - } else if (e.origin.indexOf('Variable') > -1) { - interactionType = 'variable-change'; - } - reportInteraction('dashboard-render', { - interactionType, - duration: e.duration, - networkDuration: e.networkDuration, - totalJSHeapSize: e.totalJSHeapSize, - usedJSHeapSize: e.usedJSHeapSize, - jsHeapSizeLimit: e.jsHeapSizeLimit, - }); - - logMeasurement( - `dashboard.${interactionType}`, - { - duration: e.duration, - networkDuration: e.networkDuration, - totalJSHeapSize: e.totalJSHeapSize, - usedJSHeapSize: e.usedJSHeapSize, - jsHeapSizeLimit: e.jsHeapSizeLimit, - timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, - }, - { dashboard: uid, title: title } - ); - }; -} diff --git a/public/app/features/dashboard/services/DashboardProfiler.ts b/public/app/features/dashboard/services/DashboardProfiler.ts new file mode 100644 index 00000000000..aa4ce4d2caa --- /dev/null +++ b/public/app/features/dashboard/services/DashboardProfiler.ts @@ -0,0 +1,34 @@ +import { logMeasurement, reportInteraction } from '@grafana/runtime'; +import { SceneInteractionProfileEvent, SceneRenderProfiler } from '@grafana/scenes'; + +let dashboardSceneProfiler: SceneRenderProfiler | undefined; + +export function getDashboardSceneProfiler() { + if (!dashboardSceneProfiler) { + dashboardSceneProfiler = new SceneRenderProfiler(); + } + return dashboardSceneProfiler; +} + +export function getDashboardInteractionCallback(uid: string, title: string) { + return (e: SceneInteractionProfileEvent) => { + const payload = { + duration: e.duration, + networkDuration: e.networkDuration, + startTs: e.startTs, + endTs: e.endTs, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, + }; + + reportInteraction('dashboard_render', { + interactionType: e.origin, + uid, + ...payload, + }); + + logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title }); + }; +} diff --git a/public/app/features/dashboard/services/dashboard-render-performance-profiling.md b/public/app/features/dashboard/services/dashboard-render-performance-profiling.md new file mode 100644 index 00000000000..574e4f7eb90 --- /dev/null +++ b/public/app/features/dashboard/services/dashboard-render-performance-profiling.md @@ -0,0 +1,199 @@ +# Grafana Dashboard Render Performance Metrics + +This documentation describes the dashboard render performance metrics exposed from Grafana's frontend. + +## 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. + +## Configuration + +### Enabling Performance Metrics + +Dashboard performance metrics are configured in the Grafana configuration file (`grafana.ini`) under the `[dashboards]` section: + +```ini +[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 +- **``** - Enable profiling on specific dashboards only +- **`""` (empty)** - Disable performance metrics (default) + +**Examples:** + +```ini +# 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 = +``` + +## Tracked Interactions + +The system tracks various dashboard interaction types automatically using the [`@grafana/scenes`](https://github.com/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. + +### Core Performance-Tracked Interactions + +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](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/) as `dashboard_render` measurements: + +```ts +const payload = { + duration: e.duration, + networkDuration: e.networkDuration, + startTs: e.startTs, + endTs: e.endTs, + totalJSHeapSize: e.totalJSHeapSize, + usedJSHeapSize: e.usedJSHeapSize, + jsHeapSizeLimit: e.jsHeapSizeLimit, + timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration, +}; + +reportInteraction('dashboard_render', { + interactionType: e.origin, + uid, + ...payload, +}); + +logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title }); +``` + +### Interaction Origin Mapping + +The profiling system uses profiler event's `origin` directly as the `interactionType`, providing direct mapping between user actions and performance measurements. + +## Profiling Implementation + +### Profile Data Structure + +Each interaction profile event captures: + +```typescript +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 +} +``` + +### Collected Metrics + +For each tracked interaction, the system collects: + +- **Dashboard Metadata**: UID, title +- **Performance Metrics**: Duration, network duration +- **Memory Metrics**: JavaScript heap usage statistics +- **Timing Information**: Time since boot, profile start and end timestamps +- **Interaction Context**: Type of user interaction + +## Debugging and Development + +### Enable Profiler Debug Logging + +To observe profiling events in the browser console: + +```javascript +localStorage.setItem('grafana.debug.scenes', 'true'); +``` + +#### Console Output + +When debug logging is enabled, you'll see console logs for each profiling event: + +``` +SceneRenderProfiler: Profile started: {origin: , crumbs: Array(0)} +... // intermediate steps adding profile crumbs +SceneRenderProfiler: Stopped recording, total measured time (network included): 2123 +``` + +### Enable Echo Service Debug Logging + +To observe Echo events in the browser console: + +```javascript +_debug.echo.enable(); +``` + +#### Console Output + +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: {…}} +``` + +### Browser Performance Profiler + +Dashboard interactions can be recorded in the browser's performance profiler, where they appear as: + +``` +Dashboard Interaction +``` + +## Analytics Integration + +### 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 + startTs: number, // Profile start timestamp + endTs: number, // Profile end timestamp + totalJSHeapSize: number, // Memory metrics + usedJSHeapSize: number, + jsHeapSizeLimit: number, + timeSinceBoot: number // Time since frontend boot +} +``` + +## 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. + +## 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) diff --git a/yarn.lock b/yarn.lock index 7d72c331da7..d8d007fda69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3600,11 +3600,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:^6.27.2": - version: 6.28.2 - resolution: "@grafana/scenes-react@npm:6.28.2" +"@grafana/scenes-react@npm:6.28.6": + version: 6.28.6 + resolution: "@grafana/scenes-react@npm:6.28.6" dependencies: - "@grafana/scenes": "npm:6.28.2" + "@grafana/scenes": "npm:6.28.6" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3616,13 +3616,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/c00730312828639f8a596c9fd9b0336ec57ca5cec624ac4f09a5fa0be93282d180451e3bcbf83c0209ff04486def25ff98d7b0ea6c370e91b8990112c3e903f2 + checksum: 10/55dfe20a6454f218b7c5ab2dc728678885d48e53656d046c125b555b201891e121e0960e108229372c2d70336cfe3dade9b392ed001fef90aec57cbcc13f76ca languageName: node linkType: hard -"@grafana/scenes@npm:6.28.2, @grafana/scenes@npm:^6.27.2": - version: 6.28.2 - resolution: "@grafana/scenes@npm:6.28.2" +"@grafana/scenes@npm:6.28.6": + version: 6.28.6 + resolution: "@grafana/scenes@npm:6.28.6" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3642,7 +3642,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/53370553ac4ac38d41ab1d782ee14cf05f483e35e5338406d619efb61e4c9881fd361c98ee116ae8bf40ab1a5fa2c82ca859fc519f6d532853c275e71949c654 + checksum: 10/fcfcf663e2eb63ad25fad27ec50af8590b1a6c9e802927e8e457bf2f81d04cf971c2f12f456a04a502c929ec7a1d50ef7118baee095134b133ecc92b269a6907 languageName: node linkType: hard @@ -18187,8 +18187,8 @@ __metadata: "@grafana/plugin-ui": "npm:0.10.7" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^6.27.2" - "@grafana/scenes-react": "npm:^6.27.2" + "@grafana/scenes": "npm:6.28.6" + "@grafana/scenes-react": "npm:6.28.6" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*"