Dashboard: New measurements on variable timings (#110709)

* create new interaction profiler behaviour and also use it to measure scopes fetching times

* PR mods

* refactor

* add canary scenes

* refactor

* refactor

* canary version

* add config flag for scope measurements

* refactor

* fix
This commit is contained in:
Victor Marin
2025-09-19 14:41:14 +00:00
committed by GitHub
parent 3dc30b5acb
commit 0db140e697
4 changed files with 73 additions and 19 deletions
@@ -172,6 +172,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
getDashboardSceneProfiler()
);
const interactionTracker = new behaviors.SceneInteractionTracker(
{
enableInteractionTracking:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
onInteractionComplete: getDashboardInteractionCallback(metadata.name, dashboard.title),
},
getDashboardSceneProfiler()
);
const dashboardScene = new DashboardScene(
{
description: dashboard.description,
@@ -200,6 +209,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
sync: transformCursorSyncV2ToV1(dashboard.cursorSync),
}),
queryController,
interactionTracker,
registerDashboardMacro,
registerPanelInteractionsReporter,
new behaviors.LiveNowTimer({ enabled: dashboard.liveNow }),
@@ -305,11 +305,21 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
getDashboardSceneProfiler()
);
const interactionTracker = new behaviors.SceneInteractionTracker(
{
enableInteractionTracking:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
onInteractionComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
},
getDashboardSceneProfiler()
);
const behaviorList: SceneObjectState['$behaviors'] = [
new behaviors.CursorSync({
sync: oldModel.graphTooltip,
}),
queryController,
interactionTracker,
registerDashboardMacro,
registerPanelInteractionsReporter,
new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
@@ -10,6 +10,26 @@ export function getDashboardSceneProfiler() {
return dashboardSceneProfiler;
}
export function getDashboardComponentInteractionCallback(uid: string, title: string) {
return (e: SceneInteractionProfileEvent) => {
const payload = {
duration: e.duration,
networkDuration: e.networkDuration,
startTs: e.startTs,
endTs: e.endTs,
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
};
reportInteraction('dashboard_interaction', {
interactionType: e.origin,
uid,
...payload,
});
logMeasurement(`dashboard_interaction`, payload, { interactionType: e.origin, dashboard: uid, title: title });
};
}
export function getDashboardInteractionCallback(uid: string, title: string) {
return (e: SceneInteractionProfileEvent) => {
const payload = {
@@ -1,4 +1,7 @@
import { Scope, ScopeNode, store as storeImpl } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneRenderProfiler } from '@grafana/scenes';
import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler';
import { ScopesApiClient } from '../ScopesApiClient';
import { ScopesServiceBase } from '../ScopesServiceBase';
@@ -49,7 +52,10 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
constructor(
private apiClient: ScopesApiClient,
private dashboardsService: ScopesDashboardsService,
private store = storeImpl
private store = storeImpl,
private interactionProfiler: SceneRenderProfiler | undefined = config.dashboardPerformanceMetrics.length
? getDashboardSceneProfiler()
: undefined
) {
super({
loading: false,
@@ -94,32 +100,40 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
};
private expandOrFilterNode = async (scopeNodeId: string, query?: string) => {
this.interactionProfiler?.startInteraction('scopeNodeDiscovery');
const path = getPathOfNode(scopeNodeId, this.state.nodes);
const nodeToExpand = treeNodeAtPath(this.state.tree!, path);
if (!nodeToExpand) {
throw new Error(`Node ${scopeNodeId} not found in tree`);
}
try {
if (!nodeToExpand) {
throw new Error(`Node ${scopeNodeId} not found in tree`);
}
if (nodeToExpand.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToExpand.scopeNodeId])) {
throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`);
}
if (nodeToExpand.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToExpand.scopeNodeId])) {
throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`);
}
// Check if this is first expansion or filtering within existing children
const haveChildrenLoaded = nodeToExpand.children && Object.keys(nodeToExpand.children).length > 0;
// Check if this is first expansion or filtering within existing children
const haveChildrenLoaded = nodeToExpand.children && Object.keys(nodeToExpand.children).length > 0;
if (!nodeToExpand.expanded || nodeToExpand.query !== query || !haveChildrenLoaded) {
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = true;
// Reset query on first expansion, keep it only when filtering within existing children
treeNode.query = '';
});
this.updateState({ tree: newTree });
if (!nodeToExpand.expanded || nodeToExpand.query !== query || !haveChildrenLoaded) {
const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => {
treeNode.expanded = true;
// Reset query on first expansion, keep it only when filtering within existing children
treeNode.query = '';
});
this.updateState({ tree: newTree });
// For API call: only pass query if filtering within existing children
const queryForAPI = haveChildrenLoaded ? query : query === '' ? '' : undefined;
await this.loadNodeChildren(path, nodeToExpand, queryForAPI, haveChildrenLoaded);
// For API call: only pass query if filtering within existing children
const queryForAPI = haveChildrenLoaded ? query : query === '' ? '' : undefined;
await this.loadNodeChildren(path, nodeToExpand, queryForAPI, haveChildrenLoaded);
}
} catch (error) {
throw error;
} finally {
this.interactionProfiler?.stopInteraction();
}
};