mirror of https://github.com/grafana/grafana.git
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:
parent
9d60d03d11
commit
b07e514cf0
|
@ -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');
|
||||
};
|
||||
}
|
|
@ -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,11 @@ abstract class DashboardScenePageStateManagerBase<T>
|
|||
const queryController = sceneGraph.getQueryController(dashboard);
|
||||
|
||||
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');
|
||||
|
||||
if (options.route !== DashboardRoutes.New) {
|
||||
|
@ -409,6 +425,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;
|
||||
}
|
||||
|
||||
|
@ -635,6 +656,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -624,7 +625,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
|||
}
|
||||
|
||||
public onCreateNewPanel(): VizPanel {
|
||||
const profiler = getDashboardSceneProfiler();
|
||||
const vizPanel = getDefaultVizPanel();
|
||||
profiler.attachProfilerToPanel(vizPanel);
|
||||
|
||||
this.addPanel(vizPanel);
|
||||
return vizPanel;
|
||||
}
|
||||
|
|
|
@ -53,13 +53,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';
|
||||
|
@ -164,13 +165,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
|||
|
||||
//createLayoutManager(dashboard);
|
||||
|
||||
// Create profiler once and reuse to avoid duplicate metadata setting
|
||||
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(metadata.name, dashboard.title);
|
||||
|
||||
const queryController = new behaviors.SceneQueryController(
|
||||
{
|
||||
enableProfiling:
|
||||
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
|
||||
onProfileComplete: getDashboardInteractionCallback(metadata.name, dashboard.title),
|
||||
},
|
||||
getDashboardSceneProfiler()
|
||||
dashboardProfiler
|
||||
);
|
||||
|
||||
const interactionTracker = new behaviors.SceneInteractionTracker(
|
||||
|
@ -179,7 +182,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
|||
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
|
||||
onInteractionComplete: getDashboardComponentInteractionCallback(metadata.name, dashboard.title),
|
||||
},
|
||||
getDashboardSceneProfiler()
|
||||
dashboardProfiler
|
||||
);
|
||||
|
||||
const dashboardScene = new DashboardScene(
|
||||
|
@ -219,6 +222,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
|||
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && false,
|
||||
uid: dashboardId?.toString(),
|
||||
}),
|
||||
// Analytics aggregator lifecycle management (initialization, observer registration, cleanup)
|
||||
dashboardAnalyticsInitializer,
|
||||
// Panel profiling is now handled by composed SceneRenderProfiler
|
||||
],
|
||||
$data: new DashboardDataLayerSet({
|
||||
annotationLayers,
|
||||
|
@ -241,6 +247,9 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
|||
|
||||
dashboardScene.setInitialSaveModel(dto.spec, dto.metadata, apiVersion);
|
||||
|
||||
// Enable panel profiling for this dashboard using the composed SceneRenderProfiler
|
||||
enablePanelProfilingForDashboard(dashboardScene, metadata.name);
|
||||
|
||||
return dashboardScene;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import {
|
|||
import { isWeekStart } from '@grafana/ui';
|
||||
import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1';
|
||||
import {
|
||||
getDashboardSceneProfilerWithMetadata,
|
||||
enablePanelProfilingForDashboard,
|
||||
getDashboardComponentInteractionCallback,
|
||||
getDashboardInteractionCallback,
|
||||
getDashboardSceneProfiler,
|
||||
|
@ -32,12 +34,14 @@ 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 { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||
import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { registerDashboardMacro } from '../scene/DashboardMacro';
|
||||
// DashboardPanelProfilingBehavior removed - now using composed SceneRenderProfiler
|
||||
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
|
@ -297,13 +301,15 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
|||
}
|
||||
: undefined;
|
||||
|
||||
// Create profiler once and reuse to avoid duplicate metadata setting
|
||||
const dashboardProfiler = getDashboardSceneProfilerWithMetadata(oldModel.uid, oldModel.title);
|
||||
|
||||
const queryController = new behaviors.SceneQueryController(
|
||||
{
|
||||
enableProfiling:
|
||||
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
|
||||
onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
|
||||
},
|
||||
getDashboardSceneProfiler()
|
||||
dashboardProfiler
|
||||
);
|
||||
|
||||
const interactionTracker = new behaviors.SceneInteractionTracker(
|
||||
|
@ -312,7 +318,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
|||
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
|
||||
onInteractionComplete: getDashboardComponentInteractionCallback(oldModel.uid, oldModel.title),
|
||||
},
|
||||
getDashboardSceneProfiler()
|
||||
dashboardProfiler
|
||||
);
|
||||
|
||||
const behaviorList: SceneObjectState['$behaviors'] = [
|
||||
|
@ -329,8 +335,13 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
|||
reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
|
||||
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;
|
||||
|
||||
if (config.featureToggles.dashboardNewLayouts && oldModel.panels.some((p) => p.type === 'row')) {
|
||||
|
@ -386,6 +397,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
|||
serializerVersion
|
||||
);
|
||||
|
||||
// Enable panel profiling for this dashboard using the composed SceneRenderProfiler
|
||||
enablePanelProfilingForDashboard(dashboardScene, uid);
|
||||
|
||||
return dashboardScene;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -1,11 +1,24 @@
|
|||
import { logMeasurement, reportInteraction } from '@grafana/runtime';
|
||||
import { SceneInteractionProfileEvent, SceneRenderProfiler } from '@grafana/scenes';
|
||||
import { logMeasurement, reportInteraction, config } from '@grafana/runtime';
|
||||
import { SceneRenderProfiler, type SceneObject } from '@grafana/scenes';
|
||||
|
||||
interface SceneInteractionProfileEvent {
|
||||
origin: string;
|
||||
duration: number;
|
||||
networkDuration: number;
|
||||
startTs: number;
|
||||
endTs: number;
|
||||
}
|
||||
|
||||
let dashboardSceneProfiler: 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 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue