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