From 056b5a7b0864081f6a81744448ac322709495865 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Tue, 28 Jan 2025 18:36:10 +0100 Subject: [PATCH] Dashboards: Monitor dashboard loading performance (#99629) * WIP benchmark dashboard rendering * Script * Benchmark with variable and a panel * Add one more benchmark * Explicitely enable profiling * Playwright tests * update scenes * Report measurement to faro when config set * Let user enable metrics reporting in UI * Fix logging * Change how performance metrics is enabled per dashboard, now in config file only * add benchmark run option * Fix benchmark runs * fix description for performance config * remove console.log * update codeowners * add back crashDetection init that was lost in merge * fix yarn.lock * restore custom.ini * fix import * Make sure we have the echoSrv * fix config type * Try to limit changes to e2e runs * remove benchmark * Fix lint issue * fix codeowners --------- Co-authored-by: Dominik Prokop Co-authored-by: Sergej-Vlasov --- .gitignore | 1 - conf/defaults.ini | 3 ++ package.json | 2 + packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + packages/grafana-runtime/src/index.ts | 2 +- packages/grafana-runtime/src/utils/logging.ts | 12 +++-- pkg/api/dtos/frontend_settings.go | 2 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 8 ++-- .../pages/DashboardScenePageStateManager.ts | 4 ++ .../transformSaveModelToScene.ts | 46 ++++++++++++++++++- 12 files changed, 71 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ac5ad6f5f7d..201697fce4d 100644 --- a/.gitignore +++ b/.gitignore @@ -176,7 +176,6 @@ compilation-stats.json /blob-report/ /playwright/.cache/ /playwright/.auth/ - # grafana server /scripts/grafana-server/server.log diff --git a/conf/defaults.ini b/conf/defaults.ini index 1ed0bb7a24f..55a89dfa205 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -452,6 +452,9 @@ min_refresh_interval = 5s # Path to the default home dashboard. If this value is empty, then Grafana uses StaticRootPath + "dashboards/home.json" default_home_dashboard_path = +# Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards +dashboard_performance_metrics = + ################################### Data sources ######################### [datasources] # Upper limit of data sources that Grafana will return. This limit is a temporary configuration and it will be deprecated when pagination will be introduced on the list data sources API. diff --git a/package.json b/package.json index ec79d22d1e5..12f8eb5769b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", + "build-benchmark": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js --env benchmark=1", + "e2e:playwright:benchmark": "yarn build-benchmark && ./e2e/plugin-e2e/start-and-benchmark", "e2e:playwright": "yarn playwright test", "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index b1f6ca4b28a..560f4ba672f 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -228,6 +228,7 @@ export interface GrafanaConfig { rudderstackConfigUrl: string | undefined; rudderstackIntegrationsUrl: string | undefined; analyticsConsoleReporting: boolean; + dashboardPerformanceMetrics: string[]; sqlConnectionLimits: SqlConnectionLimits; sharedWithMeFolderUID?: string; rootFolderUID?: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index f8b087b1bc6..6b29cdc0dee 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -185,6 +185,7 @@ export class GrafanaBootConfig implements GrafanaConfig { rudderstackConfigUrl: undefined; rudderstackIntegrationsUrl: undefined; analyticsConsoleReporting = false; + dashboardPerformanceMetrics: string[] = []; sqlConnectionLimits = { maxOpenConns: 100, maxIdleConns: 100, diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 0c64456428b..a28fab8050a 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -9,7 +9,7 @@ export * from './analytics/types'; export { loadPluginCss, type PluginCssOptions, setPluginImportUtils, getPluginImportUtils } from './utils/plugin'; export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils'; export { featureEnabled } from './utils/licensing'; -export { logInfo, logDebug, logWarning, logError, createMonitoringLogger } from './utils/logging'; +export { logInfo, logDebug, logWarning, logError, createMonitoringLogger, logMeasurement } from './utils/logging'; export { DataSourceWithBackend, HealthCheckError, diff --git a/packages/grafana-runtime/src/utils/logging.ts b/packages/grafana-runtime/src/utils/logging.ts index d7c94da53d8..98eb7241361 100644 --- a/packages/grafana-runtime/src/utils/logging.ts +++ b/packages/grafana-runtime/src/utils/logging.ts @@ -66,11 +66,13 @@ export function logError(err: Error, contexts?: LogContext) { export type MeasurementValues = Record; export function logMeasurement(type: string, values: MeasurementValues, context?: LogContext) { if (config.grafanaJavascriptAgent.enabled) { - faro.api.pushMeasurement({ - type, - values, - context, - }); + faro.api.pushMeasurement( + { + type, + values, + }, + { context: context } + ); } } diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index d7094de9d90..6827525be04 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -190,6 +190,8 @@ type FrontendSettingsDTO struct { AnalyticsConsoleReporting bool `json:"analyticsConsoleReporting"` + DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"` + FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"` ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"` ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index f11114b299a..8ec544337b0 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -215,6 +215,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL, RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL, AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting, + DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics, FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled, ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString, ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c78e455fa24..6bb86697f10 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -218,9 +218,10 @@ type Cfg struct { MetricsGrafanaEnvironmentInfo map[string]string // Dashboards - DashboardVersionsToKeep int - MinRefreshInterval string - DefaultHomeDashboardPath string + DashboardVersionsToKeep int + MinRefreshInterval string + DefaultHomeDashboardPath string + DashboardPerformanceMetrics []string // Auth LoginCookieName string @@ -1133,6 +1134,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) cfg.MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s") cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") + cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) if err := readUserSettings(iniFile, cfg); err != nil { return err diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index d5dcf3d533b..8989681da8d 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -2,6 +2,7 @@ import { isEqual } from 'lodash'; import { locationUtil, UrlQueryMap } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors'; @@ -135,7 +136,10 @@ abstract class DashboardScenePageStateManagerBase this.setState({ dashboard: dashboard, isLoading: false, options }); const measure = stopMeasure(LOAD_SCENE_MEASUREMENT); + const queryController = sceneGraph.getQueryController(dashboard); + trackDashboardSceneLoaded(dashboard, measure?.duration); + queryController?.startProfile(dashboard); if (options.route !== DashboardRoutes.New) { emitDashboardViewEvent({ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index c3b82b69b66..e483b3bfe19 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -1,7 +1,7 @@ import { uniqueId } from 'lodash'; import { DataFrameDTO, DataFrameJSON } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, logMeasurement, reportInteraction } from '@grafana/runtime'; import { VizPanel, SceneTimePicker, @@ -19,6 +19,7 @@ import { SceneDataLayerProvider, SceneDataLayerControls, UserActionEvent, + SceneInteractionProfileEvent, SceneObjectState, } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; @@ -229,7 +230,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, new behaviors.CursorSync({ sync: oldModel.graphTooltip, }), - new behaviors.SceneQueryController(), + new behaviors.SceneQueryController({ + enableProfiling: + config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, + onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title), + }), registerDashboardMacro, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), @@ -434,3 +439,40 @@ function trackIfEmpty(grid: SceneGridLayout) { sub.unsubscribe(); }; } + +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 } + ); + }; +}