Performance logging architecture improvements and service consolidation

- Rename ScenePerformanceService to ScenePerformanceLogger for clarity
- Consolidate performance service initialization into dashboardPerformanceInitializer
- Remove global singleton pattern in favor of per-dashboard lifecycle management
- Add comprehensive collapsible analytics logging with detailed panel breakdowns
- Remove duplicate completion logs between services
- Implement proper cleanup for both ScenePerformanceLogger and DashboardAnalyticsAggregator
- Update all imports and references to use new naming conventions
This commit is contained in:
Dominik Prokop 2025-09-29 13:58:44 +02:00
parent c13cf18cf0
commit d6d071e7c0
6 changed files with 196 additions and 188 deletions

View File

@ -1,44 +1,55 @@
import { getScenePerformanceTracker, writePerformanceLog } from '@grafana/scenes';
import { getDashboardAnalyticsAggregator } from '../../dashboard/services/DashboardAnalyticsAggregator';
import { getScenePerformanceLogger } from '../../dashboard/services/ScenePerformanceLogger';
import { DashboardScene } from '../scene/DashboardScene';
/**
* Scene behavior function that manages the initialization and lifecycle of
* DashboardAnalyticsAggregator for each dashboard session.
* dashboard performance services for each dashboard session.
*
* Initializes the aggregator with dashboard metadata and registers it as a
* performance observer. Returns a cleanup function for deactivation.
* Initializes both the analytics aggregator and performance logger, registering
* them as performance observers. Returns a cleanup function for deactivation.
*/
export function dashboardAnalyticsInitializer(dashboard: DashboardScene) {
export function dashboardPerformanceInitializer(dashboard: DashboardScene) {
const { uid, title } = dashboard.state;
if (!uid) {
console.warn('dashboardAnalyticsInitializer: Dashboard UID is missing');
console.warn('dashboardPerformanceInitializer: Dashboard UID is missing');
return;
}
// writePerformanceLog('DashboardAnalyticsInitializer', 'Initializing dashboard analytics behavior');
writePerformanceLog('DAI', 'Initializing dashboard performance services');
// Initialize analytics aggregator
const aggregator = getDashboardAnalyticsAggregator();
aggregator.initialize(uid, title || 'Untitled Dashboard');
// Register as performance observer
const tracker = getScenePerformanceTracker();
const unsubscribe = tracker.addObserver(aggregator);
// Initialize performance logger
const logger = getScenePerformanceLogger();
logger.initialize();
writePerformanceLog('DashboardAnalyticsInitializer', 'DashboardAnalyticsAggregator initialized:', { uid, title });
// Register both as performance observers
const tracker = getScenePerformanceTracker();
const unsubscribeAggregator = tracker.addObserver(aggregator);
const unsubscribeLogger = tracker.addObserver(logger);
writePerformanceLog('DAI', 'Dashboard performance services initialized:', { uid, title });
// Return cleanup function
return () => {
// Unsubscribe from performance tracker
if (unsubscribe) {
unsubscribe();
if (unsubscribeAggregator) {
unsubscribeAggregator();
}
if (unsubscribeLogger) {
unsubscribeLogger();
}
// Clean up aggregator state
// Clean up service states
aggregator.destroy();
logger.destroy();
writePerformanceLog('DashboardAnalyticsInitializer', 'DashboardAnalyticsAggregator cleaned up');
writePerformanceLog('DAI', 'Dashboard performance services cleaned up');
};
}

View File

@ -60,7 +60,7 @@ import {
import { DashboardMeta } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { dashboardPerformanceInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
@ -228,7 +228,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
uid: dashboardId?.toString(),
}),
// Analytics aggregator lifecycle management (initialization, observer registration, cleanup)
dashboardAnalyticsInitializer,
dashboardPerformanceInitializer,
// Panel profiling is now handled by composed SceneRenderProfiler
],
$data: new DashboardDataLayerSet({

View File

@ -32,7 +32,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { dashboardPerformanceInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
@ -331,7 +331,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
uid,
}),
// Analytics aggregator lifecycle management (initialization, observer registration, cleanup)
dashboardAnalyticsInitializer,
dashboardPerformanceInitializer,
];
// Panel profiling is now handled by composed SceneRenderProfiler

View File

@ -6,7 +6,6 @@ import {
type DashboardInteractionCompleteData,
type PanelPerformanceData,
type QueryPerformanceData,
writePerformanceLog,
} from '@grafana/scenes';
/**
@ -120,8 +119,7 @@ export class DashboardAnalyticsAggregator implements ScenePerformanceObserver {
}
onPanelOperationComplete(data: PanelPerformanceData): void {
writePerformanceLog('DashboardAnalyticsAggregator', '🔍 onPanelOperationComplete called with:', data);
// 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);
@ -176,27 +174,14 @@ export class DashboardAnalyticsAggregator implements ScenePerformanceObserver {
onQueryStart(data: QueryPerformanceData): void {
// Non-panel queries (annotations, variables, plugins, datasources) don't need aggregation
// These are infrastructure queries that don't belong to specific panels
writePerformanceLog('DashboardAnalyticsAggregator', '📊 Non-Panel Query Started:', {
queryId: data.queryId,
queryType: data.queryType,
querySource: data.querySource,
origin: data.origin,
});
// Logging handled by ScenePerformanceLogger to avoid duplication
}
onQueryComplete(data: QueryPerformanceData): void {
// Non-panel queries (annotations, variables, plugins, datasources) don't need panel aggregation
// These are infrastructure queries that don't belong to specific panels
writePerformanceLog('DashboardAnalyticsAggregator', '📊 Non-Panel Query Complete:', {
queryId: data.queryId,
queryType: data.queryType,
querySource: data.querySource,
origin: data.origin,
duration: data.duration,
});
// Logging handled by ScenePerformanceLogger to avoid duplication
// Could track infrastructure query metrics separately in the future if needed
// For now, just log for observability
}
/**
@ -252,7 +237,7 @@ export class DashboardAnalyticsAggregator implements ScenePerformanceObserver {
// S4.0/S5.0: Log complete analytics event including panel metrics
this.logDashboardAnalyticsEvent(data, payload, panelMetrics);
writePerformanceLog('DashboardAnalyticsAggregator', 'Analytics payload:', payload);
// Analytics payload logged separately if needed for debugging
// Send the same analytics as before
reportInteraction('dashboard_render', {
@ -276,66 +261,124 @@ export class DashboardAnalyticsAggregator implements ScenePerformanceObserver {
payload: Record<string, unknown>,
panelMetrics: PanelAnalyticsMetrics[] | null
): void {
// Calculate performance insights if panel metrics are available
let performanceInsights = null;
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 >
100
).length || 0;
// eslint-disable-next-line no-console
console.groupCollapsed(
`DAA: [ANALYTICS] ${data.interactionType} | ${panelSummary}${slowPanelCount > 0 ? ` | ${slowPanelCount} slow panels ⚠️` : ''}`
);
// Dashboard overview
console.log('📊 Dashboard:', {
duration: `${(data.duration || 0).toFixed(1)}ms`,
network: `${(data.networkDuration || 0).toFixed(1)}ms`,
interactionType: data.interactionType,
slowPanels: slowPanelCount,
});
// Analytics payload
console.log('📈 Analytics payload:', payload);
// Individual collapsible panel logs with detailed breakdown
if (panelMetrics && panelMetrics.length > 0) {
const totalPanelTime = panelMetrics.reduce((sum: number, panel) => {
const panelTotal =
panelMetrics.forEach((panel) => {
const totalPanelTime =
panel.totalQueryTime +
panel.totalFieldConfigTime +
panel.totalTransformationTime +
panel.totalRenderTime +
panel.pluginLoadTime;
return sum + panelTotal;
}, 0);
const avgPanelTime = totalPanelTime / panelMetrics.length;
const slowestPanel = panelMetrics.reduce((slowest, panel) => {
const panelTotal =
panel.totalQueryTime +
panel.totalFieldConfigTime +
panel.totalTransformationTime +
panel.totalRenderTime +
panel.pluginLoadTime;
const slowestTotal =
slowest.totalQueryTime +
slowest.totalFieldConfigTime +
slowest.totalTransformationTime +
slowest.totalRenderTime +
slowest.pluginLoadTime;
return panelTotal > slowestTotal ? panel : slowest;
const isSlowPanel = totalPanelTime > 100;
const slowWarning = isSlowPanel ? ' ⚠️ SLOW' : '';
// eslint-disable-next-line no-console
console.groupCollapsed(
`🎨 Panel ${panel.pluginId}-${panel.panelId}: ${totalPanelTime.toFixed(1)}ms total${slowWarning}`
);
console.log('🔧 Plugin:', {
id: panel.pluginId,
version: panel.pluginVersion || 'unknown',
panelId: panel.panelId,
panelKey: panel.panelKey,
});
console.log('⚡ Performance:', {
totalTime: `${totalPanelTime.toFixed(1)}ms`,
isSlowPanel: isSlowPanel,
breakdown: {
query: `${panel.totalQueryTime.toFixed(1)}ms`,
transform: `${panel.totalTransformationTime.toFixed(1)}ms`,
render: `${panel.totalRenderTime.toFixed(1)}ms`,
fieldConfig: `${panel.totalFieldConfigTime.toFixed(1)}ms`,
pluginLoad: `${panel.pluginLoadTime.toFixed(1)}ms`,
},
});
if (panel.queryOperations.length > 0) {
console.log('📊 Queries:', {
count: panel.queryOperations.length,
details: panel.queryOperations.map((op, index) => ({
operation: index + 1,
duration: `${op.duration.toFixed(1)}ms`,
timestamp: op.timestamp,
queryType: op.queryType || 'unknown',
})),
});
}
if (panel.transformationOperations.length > 0) {
console.log('🔄 Transformations:', {
count: panel.transformationOperations.length,
details: panel.transformationOperations.map((op, index) => ({
operation: index + 1,
duration: `${op.duration.toFixed(1)}ms`,
timestamp: op.timestamp,
transformationId: op.transformationId || 'unknown',
success: op.success !== false,
})),
});
}
if (panel.renderOperations.length > 0) {
console.log('🎨 Renders:', {
count: panel.renderOperations.length,
details: panel.renderOperations.map((op, index) => ({
operation: index + 1,
duration: `${op.duration.toFixed(1)}ms`,
timestamp: op.timestamp,
})),
});
}
if (panel.fieldConfigOperations.length > 0) {
console.log('⚙️ FieldConfigs:', {
count: panel.fieldConfigOperations.length,
details: panel.fieldConfigOperations.map((op, index) => ({
operation: index + 1,
duration: `${op.duration.toFixed(1)}ms`,
timestamp: op.timestamp,
})),
});
}
// eslint-disable-next-line no-console
console.groupEnd();
});
const slowestPanelTime =
slowestPanel.totalQueryTime +
slowestPanel.totalFieldConfigTime +
slowestPanel.totalTransformationTime +
slowestPanel.totalRenderTime +
slowestPanel.pluginLoadTime;
performanceInsights = {
totalPanelTime: `${totalPanelTime.toFixed(2)}ms`,
averagePanelTime: `${avgPanelTime.toFixed(2)}ms`,
slowestPanel: {
panelId: slowestPanel.panelId,
pluginId: slowestPanel.pluginId,
time: `${slowestPanelTime.toFixed(2)}ms`,
},
};
}
writePerformanceLog('DashboardAnalyticsAggregator', '🎯 Dashboard Analytics Event:', {
uid: this.dashboardUID,
title: this.dashboardTitle,
interactionType: data.interactionType,
dashboardMetrics: payload,
panelMetricsMapSize: this.panelMetrics.size,
panelMetricsArrayLength: panelMetrics?.length || 0,
panelMetrics: (panelMetrics?.length ?? 0) > 0 ? panelMetrics : 'No panel metrics found',
performanceInsights: performanceInsights || 'No performance insights available',
});
// eslint-disable-next-line no-console
console.groupEnd();
}
}

View File

@ -1,7 +1,7 @@
import { logMeasurement, reportInteraction, config } from '@grafana/runtime';
import { SceneRenderProfiler, type SceneObject } from '@grafana/scenes';
import { initializeScenePerformanceService } from './ScenePerformanceService';
import { initializeScenePerformanceLogger } from './ScenePerformanceLogger';
interface SceneInteractionProfileEvent {
origin: string;
@ -26,9 +26,6 @@ export function getDashboardSceneProfiler() {
};
dashboardSceneProfiler = new SceneRenderProfiler(panelProfilingConfig);
// Initialize the Scene performance service to start listening to events
initializeScenePerformanceService();
}
return dashboardSceneProfiler;
}

View File

@ -17,13 +17,13 @@ import {
} from './performanceConstants';
/**
* Grafana service that subscribes to Scene performance events
* and integrates them with Grafana's observability systems.
* Also creates Chrome DevTools performance marks and measurements for debugging.
* Grafana logger that subscribes to Scene performance events
* and logs them to console with Chrome DevTools performance marks and measurements for debugging.
*/
export class ScenePerformanceService implements ScenePerformanceObserver {
export class ScenePerformanceLogger implements ScenePerformanceObserver {
private isInitialized = false;
private unsubscribe: (() => void) | null = null;
private panelGroupsOpen = new Set<string>(); // Track which panels we've seen
constructor() {
// Bind all observer methods to preserve 'this' context when called by ScenePerformanceTracker
@ -48,7 +48,7 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
// Note: Analytics aggregator will be initialized separately with dashboard context
this.isInitialized = true;
writePerformanceLog('ScenePerformanceService', 'Initialized and subscribed to Scene performance events');
writePerformanceLog('SPL', 'Initialized and subscribed to Scene performance events');
}
public destroy() {
@ -63,7 +63,7 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
}
this.isInitialized = false;
writePerformanceLog('ScenePerformanceService', 'Destroyed and unsubscribed from Scene performance events');
writePerformanceLog('SPL', 'Destroyed and unsubscribed from Scene performance events');
}
// Dashboard-level events
@ -72,14 +72,10 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
const dashboardStartMark = PERFORMANCE_MARKS.DASHBOARD_INTERACTION_START(data.operationId);
createPerformanceMark(dashboardStartMark, data.timestamp);
writePerformanceLog('ScenePerformanceService', '🎯 Dashboard Interaction Started:', {
type: data.interactionType,
uid: data.metadata?.dashboardUID,
title: data.metadata?.dashboardTitle,
panelCount: data.metadata?.panelCount,
timestamp: data.timestamp,
operationId: data.operationId,
});
const title = data.metadata?.dashboardTitle || 'Unknown Dashboard';
const panelCount = data.metadata?.panelCount || 0;
writePerformanceLog('SPL', `[DASHBOARD] ${data.interactionType} started: ${title} (${panelCount} panels)`);
}
onDashboardInteractionMilestone(data: DashboardInteractionMilestoneData): void {
@ -88,13 +84,8 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
const dashboardMilestoneMark = PERFORMANCE_MARKS.DASHBOARD_MILESTONE(data.operationId, milestone);
createPerformanceMark(dashboardMilestoneMark, data.timestamp);
writePerformanceLog('ScenePerformanceService', '🔄 Dashboard Milestone:', {
type: data.interactionType,
uid: data.metadata?.dashboardUID,
milestone: data.milestone,
timestamp: data.timestamp,
operationId: data.operationId,
});
// Log milestones quietly - only when verbose debugging is needed
// Most milestones are covered by the start/complete query logs
}
onDashboardInteractionComplete(data: DashboardInteractionCompleteData): void {
@ -106,15 +97,11 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
createPerformanceMark(dashboardEndMark, data.timestamp);
createPerformanceMeasure(dashboardMeasureName, dashboardStartMark, dashboardEndMark);
writePerformanceLog('ScenePerformanceService', '✅ Dashboard Interaction Complete:', {
type: data.interactionType,
uid: data.metadata?.dashboardUID,
title: data.metadata?.dashboardTitle,
duration: data.duration,
networkDuration: data.networkDuration,
timestamp: data.timestamp,
operationId: data.operationId,
});
// Clear tracking state
this.panelGroupsOpen.clear();
// Dashboard completion logging is handled comprehensively by SceneRenderProfiler
// This observer focuses on creating DevTools performance marks/measures
}
// Panel-level events
@ -122,16 +109,10 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
// Create standardized performance marks based on operation type
this.createStandardizedPanelMark(data, 'start');
const operationIcon = this.getOperationIcon(data.operation);
writePerformanceLog('ScenePerformanceService', `${operationIcon} Panel Operation Started [${data.operation}]:`, {
panelId: data.panelId,
panelKey: data.panelKey,
pluginId: data.pluginId,
operation: data.operation,
timestamp: data.timestamp,
metadata: data.metadata,
operationId: data.operationId,
});
// Track panel for summary logging later
this.panelGroupsOpen.add(data.panelKey);
// Don't log start events - they're noise. Only log completions with timing.
}
onPanelOperationComplete(data: PanelPerformanceData): void {
@ -139,17 +120,19 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
this.createStandardizedPanelMark(data, 'end');
this.createStandardizedPanelMeasure(data);
const operationIcon = this.getOperationIcon(data.operation);
writePerformanceLog('ScenePerformanceService', `${operationIcon} Panel Operation Complete [${data.operation}]:`, {
panelId: data.panelId,
panelKey: data.panelKey,
pluginId: data.pluginId,
operation: data.operation,
duration: data.duration,
timestamp: data.timestamp,
metadata: data.metadata,
operationId: data.operationId,
});
const duration = (data.duration || 0).toFixed(1);
const slowWarning = (data.duration || 0) > 100 ? ' ⚠️ 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
@ -158,14 +141,9 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
const queryStartMark = PERFORMANCE_MARKS.QUERY_START(data.origin, data.queryId);
createPerformanceMark(queryStartMark, data.timestamp);
writePerformanceLog('ScenePerformanceService', '📊 Non-Panel Query Started:', {
queryId: data.queryId,
queryType: data.queryType,
querySource: data.querySource,
origin: data.origin,
timestamp: data.timestamp,
operationId: data.operationId,
});
// Mark that we're processing infrastructure queries
// Don't log start events - they're noise. Only log completions with timing.
}
onQueryComplete(data: QueryPerformanceData): void {
@ -177,15 +155,12 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
createPerformanceMark(queryEndMark, data.timestamp);
createPerformanceMeasure(queryMeasureName, queryStartMark, queryEndMark);
writePerformanceLog('ScenePerformanceService', '📊 Non-Panel Query Complete:', {
queryId: data.queryId,
queryType: data.queryType,
querySource: data.querySource,
origin: data.origin,
duration: data.duration,
timestamp: data.timestamp,
operationId: data.operationId,
});
const duration = (data.duration || 0).toFixed(1);
const slowWarning = (data.duration || 0) > 100 ? ' ⚠️ SLOW' : '';
// Simple, clean format
const queryType = data.queryType.replace(/^(getDataSource\/|AnnotationsDataLayer\/)/, ''); // Remove prefixes
writePerformanceLog('SPL', `[QUERY ${data.origin}] ${queryType} [${data.queryId}]: ${duration}ms${slowWarning}`);
}
// Standardized performance mark creation methods - now with full type safety!
@ -302,44 +277,26 @@ export class ScenePerformanceService implements ScenePerformanceObserver {
}
}
// Helper method to get appropriate icon for operation type
private getOperationIcon(operation: string): string {
switch (operation) {
case 'query':
return '🔍';
case 'transform':
return '🔄';
case 'fieldConfig':
return '🔧';
case 'render':
return '🎨';
case 'plugin-load':
return '⚡';
default:
return '⚡';
}
}
// All performance marks now use standardized functions from performanceConstants.ts
}
// Singleton instance
let scenePerformanceService: ScenePerformanceService | null = null;
let scenePerformanceLogger: ScenePerformanceLogger | null = null;
export function getScenePerformanceService(): ScenePerformanceService {
if (!scenePerformanceService) {
scenePerformanceService = new ScenePerformanceService();
export function getScenePerformanceLogger(): ScenePerformanceLogger {
if (!scenePerformanceLogger) {
scenePerformanceLogger = new ScenePerformanceLogger();
}
return scenePerformanceService;
return scenePerformanceLogger;
}
export function initializeScenePerformanceService(): void {
getScenePerformanceService().initialize();
export function initializeScenePerformanceLogger(): void {
getScenePerformanceLogger().initialize();
}
export function destroyScenePerformanceService(): void {
if (scenePerformanceService) {
scenePerformanceService.destroy();
scenePerformanceService = null;
export function destroyScenePerformanceLogger(): void {
if (scenePerformanceLogger) {
scenePerformanceLogger.destroy();
scenePerformanceLogger = null;
}
}