Dashboard performance profiling architecture improvements

- Create shared performanceUtils.ts with type-safe performance.memory access
- Add standardized grouped logging utilities for structured console output
- Convert observer methods to arrow functions eliminating constructor bindings
- Implement DashboardAnalyticsAggregator for comprehensive panel metrics
- Add ScenePerformanceLogger for performance marks and measurements
- Create DashboardAnalyticsInitializerBehavior for automatic profiling setup
- Update dashboard scene integration to use improved profiling system
- Add numeric duration logging for better programmatic analysis
- Fix localStorage usage to use @grafana/data store for consistency
- Consolidate performance tracking logic into shared utilities
This commit is contained in:
Dominik Prokop 2025-10-07 23:05:07 +02:00
parent 9d60d03d11
commit b07e514cf0
11 changed files with 1030 additions and 33 deletions

View File

@ -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');
};
}

View File

@ -17,8 +17,11 @@ import {
import { ensureV2Response, transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers'; import { ensureV2Response, transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers';
import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { isDashboardV2Resource, isDashboardV2Spec, isV2StoredVersion } from 'app/features/dashboard/api/utils'; 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 { 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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { initializeScenePerformanceLogger } from 'app/features/dashboard/services/ScenePerformanceLogger';
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor'; import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
import { trackDashboardSceneLoaded } from 'app/features/dashboard-scene/utils/tracking'; import { trackDashboardSceneLoaded } from 'app/features/dashboard-scene/utils/tracking';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
@ -41,6 +44,14 @@ import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSession
import { processQueryParamsForDashboardLoad, updateNavModel } from './utils'; 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 { export interface LoadError {
status?: number; status?: number;
messageId?: string; messageId?: string;
@ -296,6 +307,11 @@ abstract class DashboardScenePageStateManagerBase<T>
const queryController = sceneGraph.getQueryController(dashboard); const queryController = sceneGraph.getQueryController(dashboard);
trackDashboardSceneLoaded(dashboard, measure?.duration); trackDashboardSceneLoaded(dashboard, measure?.duration);
// 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'); queryController?.startProfile('dashboard_view');
if (options.route !== DashboardRoutes.New) { if (options.route !== DashboardRoutes.New) {
@ -409,6 +425,11 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
fromCache.state.version === rsp?.dashboard.version && fromCache.state.version === rsp?.dashboard.version &&
fromCache.state.meta.created === rsp?.meta.created fromCache.state.meta.created === rsp?.meta.created
) { ) {
const profiler = getDashboardSceneProfiler();
profiler.setMetadata({
dashboardUID: fromCache.state.uid,
dashboardTitle: fromCache.state.title,
});
return fromCache; return fromCache;
} }
@ -635,6 +656,11 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
const fromCache = this.getSceneFromCache(options.uid); const fromCache = this.getSceneFromCache(options.uid);
if (fromCache && fromCache.state.version === rsp?.metadata.generation) { if (fromCache && fromCache.state.version === rsp?.metadata.generation) {
const profiler = getDashboardSceneProfiler();
profiler.setMetadata({
dashboardUID: fromCache.state.uid,
dashboardTitle: fromCache.state.title,
});
return fromCache; return fromCache;
} }

View File

@ -25,6 +25,7 @@ import store from 'app/core/store';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel, ScopeMeta } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel, ScopeMeta } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@ -624,7 +625,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
} }
public onCreateNewPanel(): VizPanel { public onCreateNewPanel(): VizPanel {
const profiler = getDashboardSceneProfiler();
const vizPanel = getDefaultVizPanel(); const vizPanel = getDefaultVizPanel();
profiler.attachProfilerToPanel(vizPanel);
this.addPanel(vizPanel); this.addPanel(vizPanel);
return vizPanel; return vizPanel;
} }

View File

@ -53,13 +53,14 @@ import {
} from 'app/features/apiserver/types'; } from 'app/features/apiserver/types';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { import {
getDashboardSceneProfilerWithMetadata,
enablePanelProfilingForDashboard,
getDashboardComponentInteractionCallback, getDashboardComponentInteractionCallback,
getDashboardInteractionCallback,
getDashboardSceneProfiler,
} from 'app/features/dashboard/services/DashboardProfiler'; } from 'app/features/dashboard/services/DashboardProfiler';
import { DashboardMeta } from 'app/types/dashboard'; import { DashboardMeta } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls'; import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
@ -164,13 +165,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
//createLayoutManager(dashboard); //createLayoutManager(dashboard);
// Create profiler once and reuse to avoid duplicate metadata setting
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(metadata.name, dashboard.title);
const queryController = new behaviors.SceneQueryController( const queryController = new behaviors.SceneQueryController(
{ {
enableProfiling: enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1, config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
onProfileComplete: getDashboardInteractionCallback(metadata.name, dashboard.title),
}, },
getDashboardSceneProfiler() dashboardProfiler
); );
const interactionTracker = new behaviors.SceneInteractionTracker( const interactionTracker = new behaviors.SceneInteractionTracker(
@ -179,7 +182,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1, config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
onInteractionComplete: getDashboardComponentInteractionCallback(metadata.name, dashboard.title), onInteractionComplete: getDashboardComponentInteractionCallback(metadata.name, dashboard.title),
}, },
getDashboardSceneProfiler() dashboardProfiler
); );
const dashboardScene = new DashboardScene( const dashboardScene = new DashboardScene(
@ -219,6 +222,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && false, reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && false,
uid: dashboardId?.toString(), uid: dashboardId?.toString(),
}), }),
// Analytics aggregator lifecycle management (initialization, observer registration, cleanup)
dashboardAnalyticsInitializer,
// Panel profiling is now handled by composed SceneRenderProfiler
], ],
$data: new DashboardDataLayerSet({ $data: new DashboardDataLayerSet({
annotationLayers, annotationLayers,
@ -241,6 +247,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
dashboardScene.setInitialSaveModel(dto.spec, dto.metadata, apiVersion); dashboardScene.setInitialSaveModel(dto.spec, dto.metadata, apiVersion);
// Enable panel profiling for this dashboard using the composed SceneRenderProfiler
enablePanelProfilingForDashboard(dashboardScene, metadata.name);
return dashboardScene; return dashboardScene;
} }

View File

@ -23,6 +23,8 @@ import {
import { isWeekStart } from '@grafana/ui'; import { isWeekStart } from '@grafana/ui';
import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1'; import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1';
import { import {
getDashboardSceneProfilerWithMetadata,
enablePanelProfilingForDashboard,
getDashboardComponentInteractionCallback, getDashboardComponentInteractionCallback,
getDashboardInteractionCallback, getDashboardInteractionCallback,
getDashboardSceneProfiler, getDashboardSceneProfiler,
@ -32,12 +34,14 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard'; import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
import { dashboardAnalyticsInitializer } from '../behaviors/DashboardAnalyticsInitializerBehavior';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare'; import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls'; import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { registerDashboardMacro } from '../scene/DashboardMacro'; import { registerDashboardMacro } from '../scene/DashboardMacro';
// DashboardPanelProfilingBehavior removed - now using composed SceneRenderProfiler
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior'; import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
@ -297,13 +301,15 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
} }
: undefined; : undefined;
// Create profiler once and reuse to avoid duplicate metadata setting
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(oldModel.uid, oldModel.title);
const queryController = new behaviors.SceneQueryController( const queryController = new behaviors.SceneQueryController(
{ {
enableProfiling: enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
}, },
getDashboardSceneProfiler() dashboardProfiler
); );
const interactionTracker = new behaviors.SceneInteractionTracker( const interactionTracker = new behaviors.SceneInteractionTracker(
@ -312,7 +318,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1, config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
onInteractionComplete: getDashboardComponentInteractionCallback(oldModel.uid, oldModel.title), onInteractionComplete: getDashboardComponentInteractionCallback(oldModel.uid, oldModel.title),
}, },
getDashboardSceneProfiler() dashboardProfiler
); );
const behaviorList: SceneObjectState['$behaviors'] = [ const behaviorList: SceneObjectState['$behaviors'] = [
@ -329,8 +335,13 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange, reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
uid, uid,
}), }),
// Analytics aggregator lifecycle management (initialization, observer registration, cleanup)
dashboardAnalyticsInitializer,
]; ];
// Panel profiling is now handled by composed SceneRenderProfiler
// Will be enabled in the dashboard creation below
let body: DashboardLayoutManager; let body: DashboardLayoutManager;
if (config.featureToggles.dashboardNewLayouts && oldModel.panels.some((p) => p.type === 'row')) { if (config.featureToggles.dashboardNewLayouts && oldModel.panels.some((p) => p.type === 'row')) {
@ -386,6 +397,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
serializerVersion serializerVersion
); );
// Enable panel profiling for this dashboard using the composed SceneRenderProfiler
enablePanelProfilingForDashboard(dashboardScene, uid);
return dashboardScene; return dashboardScene;
} }

View File

@ -0,0 +1,383 @@
import { logMeasurement, reportInteraction } from '@grafana/runtime';
import {
type ScenePerformanceObserver,
type DashboardInteractionStartData,
type DashboardInteractionMilestoneData,
type DashboardInteractionCompleteData,
type PanelPerformanceData,
type QueryPerformanceData,
} from '@grafana/scenes';
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 ScenePerformanceObserver {
private panelMetrics = new Map<string, PanelAnalyticsMetrics>();
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: DashboardInteractionStartData): void => {
// Clear metrics when new dashboard interaction starts
this.clearMetrics();
};
onDashboardInteractionMilestone = (_data: DashboardInteractionMilestoneData): void => {
// No action needed for milestones in analytics
};
onDashboardInteractionComplete = (data: DashboardInteractionCompleteData): void => {
// Send analytics report for dashboard interaction completion
this.sendAnalyticsReport(data);
};
// Panel-level events
onPanelOperationStart = (data: PanelPerformanceData): void => {
// Start events don't need aggregation, just ensure panel exists
this.ensurePanelExists(data.panelKey, data.panelId, data.pluginId, data.pluginVersion);
};
onPanelOperationComplete = (data: 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: QueryPerformanceData): void => {
// Non-panel queries (annotations, variables, datasources) don't need aggregation
// These are infrastructure queries that don't belong to specific panels
// Logging handled by ScenePerformanceLogger to avoid duplication
};
onQueryComplete = (_data: QueryPerformanceData): void => {
// Non-panel queries (annotations, variables, datasources) don't need panel aggregation
// These are infrastructure queries that don't belong to specific panels
// Logging handled by ScenePerformanceLogger to avoid duplication
};
/**
* 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: 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: DashboardInteractionCompleteData,
payload: Record<string, unknown>,
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 >
100
).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 > 100;
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();
}

View File

@ -1,11 +1,24 @@
import { logMeasurement, reportInteraction } from '@grafana/runtime'; import { logMeasurement, reportInteraction, config } from '@grafana/runtime';
import { SceneInteractionProfileEvent, SceneRenderProfiler } from '@grafana/scenes'; import { SceneRenderProfiler, type SceneObject } from '@grafana/scenes';
interface SceneInteractionProfileEvent {
origin: string;
duration: number;
networkDuration: number;
startTs: number;
endTs: number;
}
let dashboardSceneProfiler: SceneRenderProfiler | undefined; let dashboardSceneProfiler: SceneRenderProfiler | undefined;
export function getDashboardSceneProfiler() { export function getDashboardSceneProfiler() {
if (!dashboardSceneProfiler) { if (!dashboardSceneProfiler) {
dashboardSceneProfiler = new SceneRenderProfiler(); // Create panel profiling configuration
const panelProfilingConfig = {
watchStateKey: 'body', // Watch dashboard body changes for panel structure changes
};
dashboardSceneProfiler = new SceneRenderProfiler(panelProfilingConfig);
} }
return dashboardSceneProfiler; return dashboardSceneProfiler;
} }
@ -30,28 +43,31 @@ export function getDashboardComponentInteractionCallback(uid: string, title: str
}; };
} }
export function getDashboardInteractionCallback(uid: string, title: string) { // Enhanced function to create profiler with dashboard metadata
return (e: SceneInteractionProfileEvent) => { export function getDashboardSceneProfilerWithMetadata(uid: string, title: string) {
const payload = { const profiler = getDashboardSceneProfiler();
duration: e.duration,
networkDuration: e.networkDuration,
processingTime: e.duration - e.networkDuration,
startTs: e.startTs,
endTs: e.endTs,
totalJSHeapSize: e.totalJSHeapSize,
usedJSHeapSize: e.usedJSHeapSize,
jsHeapSizeLimit: e.jsHeapSizeLimit,
longFramesCount: e.longFramesCount,
longFramesTotalTime: e.longFramesTotalTime,
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
};
reportInteraction('dashboard_render', { // Set metadata for observer notifications
interactionType: e.origin, profiler.setMetadata({
uid, dashboardUID: uid,
...payload, 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);
}
} }

View File

@ -0,0 +1,237 @@
import {
type ScenePerformanceObserver,
type DashboardInteractionStartData,
type DashboardInteractionMilestoneData,
type DashboardInteractionCompleteData,
type PanelPerformanceData,
type QueryPerformanceData,
writePerformanceLog,
} from '@grafana/scenes';
import {
PERFORMANCE_MARKS,
PERFORMANCE_MEASURES,
createPerformanceMark,
createPerformanceMeasure,
} from './performanceConstants';
import { registerPerformanceObserver } 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 ScenePerformanceObserver {
private panelGroupsOpen = new Set<string>(); // Track which panels we've seen
constructor() {
// Arrow methods automatically preserve 'this' context - no binding needed
}
public initialize() {
writePerformanceLog('SPL', 'Performance logger ready');
}
public destroy() {
this.panelGroupsOpen.clear();
writePerformanceLog('SPL', 'Performance logger state cleared');
}
// Dashboard-level events
onDashboardInteractionStart = (data: 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: DashboardInteractionMilestoneData): void => {
const milestone = data.milestone || 'unknown';
const dashboardMilestoneMark = PERFORMANCE_MARKS.DASHBOARD_MILESTONE(data.operationId, milestone);
createPerformanceMark(dashboardMilestoneMark, data.timestamp);
};
onDashboardInteractionComplete = (data: 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: PanelPerformanceData): void => {
this.createStandardizedPanelMark(data, 'start');
// Track panel for summary logging later
this.panelGroupsOpen.add(data.panelKey);
};
onPanelOperationComplete = (data: PanelPerformanceData): void => {
this.createStandardizedPanelMark(data, 'end');
this.createStandardizedPanelMeasure(data);
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
onQueryStart = (data: QueryPerformanceData): void => {
const queryStartMark = PERFORMANCE_MARKS.QUERY_START(data.origin, data.queryId);
createPerformanceMark(queryStartMark, data.timestamp);
};
onQueryComplete = (data: 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) > 100 ? ' ⚠️ SLOW' : '';
const queryType = data.queryType.replace(/^(getDataSource\/|AnnotationsDataLayer\/)/, ''); // Remove prefixes
writePerformanceLog('SPL', `[QUERY ${data.origin}] ${queryType} [${data.queryId}]: ${duration}ms${slowWarning}`);
};
private createStandardizedPanelMark(data: 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: 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();
}

View File

@ -0,0 +1,113 @@
// 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}`,
};
/**
* 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);
}
// writePerformanceLog('PerformanceConstants', `🎯 Created performance mark: ${name}`, { timestamp });
}
} 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);
}
// writePerformanceLog('PerformanceConstants', `✅ Created performance measure: ${name}`, { startMark, endMark });
}
} catch (error) {
console.error(`❌ Failed to create performance measure: ${name}`, { startMark, endMark, error });
}
}

View File

@ -0,0 +1,56 @@
import { getScenePerformanceTracker, writePerformanceLog, type ScenePerformanceObserver } from '@grafana/scenes';
/**
* Utility function to register a performance observer with the global tracker
* Reduces duplication between ScenePerformanceLogger and DashboardAnalyticsAggregator
*/
export function registerPerformanceObserver(observer: ScenePerformanceObserver, loggerName: string): void {
const tracker = 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,
};
}

View File

@ -0,0 +1,102 @@
import { store } from '@grafana/data';
import { getScenePerformanceTracker, writePerformanceLog, type ScenePerformanceObserver } from '@grafana/scenes';
/**
* Utility function to register a performance observer with the global tracker
* Reduces duplication between ScenePerformanceLogger and DashboardAnalyticsAggregator
*/
export function registerPerformanceObserver(observer: ScenePerformanceObserver, loggerName: string): void {
const tracker = 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();
}
}