diff --git a/package.json b/package.json index 373fca80b00..a68dd4130c0 100644 --- a/package.json +++ b/package.json @@ -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:*", diff --git a/public/app/features/dashboard-scene/behaviors/DashboardAnalyticsInitializerBehavior.ts b/public/app/features/dashboard-scene/behaviors/DashboardAnalyticsInitializerBehavior.ts new file mode 100644 index 00000000000..2cc1e1138e0 --- /dev/null +++ b/public/app/features/dashboard-scene/behaviors/DashboardAnalyticsInitializerBehavior.ts @@ -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'); + }; +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 54dd386c619..e17f7e36cff 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -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 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; } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 748d3e7986e..087cd505fcd 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -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 impleme } public onCreateNewPanel(): VizPanel { + const profiler = getDashboardSceneProfiler(); const vizPanel = getDefaultVizPanel(); + profiler.attachProfilerToPanel(vizPanel); + this.addPanel(vizPanel); return vizPanel; } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index ad94f404e68..7d57a2b087f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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 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 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; } diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index b8ca09b1b4e..0bb44738440 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -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 diff --git a/public/app/features/dashboard/services/DashboardAnalyticsAggregator.ts b/public/app/features/dashboard/services/DashboardAnalyticsAggregator.ts new file mode 100644 index 00000000000..8671c2b13e5 --- /dev/null +++ b/public/app/features/dashboard/services/DashboardAnalyticsAggregator.ts @@ -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(); + 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, + 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(); +} diff --git a/public/app/features/dashboard/services/DashboardProfiler.ts b/public/app/features/dashboard/services/DashboardProfiler.ts index 1be6cfd1f1c..01fe67eb451 100644 --- a/public/app/features/dashboard/services/DashboardProfiler.ts +++ b/public/app/features/dashboard/services/DashboardProfiler.ts @@ -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); + } } diff --git a/public/app/features/dashboard/services/ScenePerformanceLogger.ts b/public/app/features/dashboard/services/ScenePerformanceLogger.ts new file mode 100644 index 00000000000..79b0bceb402 --- /dev/null +++ b/public/app/features/dashboard/services/ScenePerformanceLogger.ts @@ -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(); // 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(); +} diff --git a/public/app/features/dashboard/services/dashboard-render-performance-profiling.md b/public/app/features/dashboard/services/dashboard-render-performance-profiling.md index 80f6fc522f6..cbef48d7411 100644 --- a/public/app/features/dashboard/services/dashboard-render-performance-profiling.md +++ b/public/app/features/dashboard/services/dashboard-render-performance-profiling.md @@ -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: + +``` +- +``` + +**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: +Dashboard Interaction End: +Dashboard Milestone: : +``` + +**Panel-level marks:** + +``` +Panel Query Start: : +Panel Query End: : +Panel Render Start: : +Panel Render End: : +``` + +### 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 +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: []} -SceneRenderProfiler: Profile started[clean]: {origin: "dashboard_view", crumbs: []} -``` - -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 diff --git a/public/app/features/dashboard/services/performanceConstants.ts b/public/app/features/dashboard/services/performanceConstants.ts new file mode 100644 index 00000000000..c5ae3474d33 --- /dev/null +++ b/public/app/features/dashboard/services/performanceConstants.ts @@ -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; diff --git a/public/app/features/dashboard/services/performanceUtils.ts b/public/app/features/dashboard/services/performanceUtils.ts new file mode 100644 index 00000000000..a09096b513c --- /dev/null +++ b/public/app/features/dashboard/services/performanceUtils.ts @@ -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 }); + } +} diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 370258d4785..b7c502de5bd 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -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 { let dashboardReloadSpy: jest.SpyInstance; beforeEach(() => { jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'warn').mockImplementation(); }); beforeAll(() => { config.featureToggles.scopeFilters = true; diff --git a/yarn.lock b/yarn.lock index 792fd086e36..12c677dba68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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