DashboardScene: Measure and report scene load time (#86267)

* measure scene load time

* Fix tests that fail due to performance not being the proper global performance object in jest

* add isScene parameter to tracking test
This commit is contained in:
Oscar Kilhed 2024-04-24 11:45:32 +02:00 committed by GitHub
parent d46b163810
commit e7f40493e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 65 additions and 34 deletions

View File

@ -1,7 +1,7 @@
import { reportPerformance } from '../services/echo/EchoSrv';
export function startMeasure(eventName: string) {
if (!performance) {
if (!performance || !performance.mark) {
return;
}
@ -13,7 +13,7 @@ export function startMeasure(eventName: string) {
}
export function stopMeasure(eventName: string) {
if (!performance) {
if (!performance || !performance.mark) {
return;
}
@ -29,7 +29,9 @@ export function stopMeasure(eventName: string) {
performance.clearMarks(started);
performance.clearMarks(completed);
performance.clearMeasures(measured);
return measure;
} catch (error) {
console.error(`[Metrics] Failed to stopMeasure ${eventName}`, error);
return;
}
}

View File

@ -2,12 +2,14 @@ import { locationUtil } from '@grafana/data';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { default as localStorageStore } from 'app/core/store';
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import {
DASHBOARD_FROM_LS_KEY,
removeDashboardToFetchFromLocalStorage,
} from 'app/features/dashboard/state/initDashboard';
import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking';
import { DashboardDTO, DashboardRoutes } from 'app/types';
import { PanelEditor } from '../panel-edit/PanelEditor';
@ -26,6 +28,8 @@ export interface DashboardScenePageState {
export const DASHBOARD_CACHE_TTL = 500;
const LOAD_SCENE_MEASUREMENT = 'loadDashboardScene';
/** Only used by cache in loading home in DashboardPageProxy and initDashboard (Old arch), can remove this after old dashboard arch is gone */
export const HOME_DASHBOARD_CACHE_KEY = '__grafana_home_uid__';
@ -167,6 +171,7 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
public async loadDashboard(options: LoadDashboardOptions) {
try {
startMeasure(LOAD_SCENE_MEASUREMENT);
const dashboard = await this.loadScene(options);
if (!dashboard) {
return;
@ -177,6 +182,8 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
this.setState({ dashboard: dashboard, isLoading: false });
const measure = stopMeasure(LOAD_SCENE_MEASUREMENT);
trackDashboardSceneLoaded(dashboard, measure?.duration);
} catch (err) {
this.setState({ isLoading: false, loadError: String(err) });
}

View File

@ -30,7 +30,6 @@ import {
AdHocFiltersVariable,
} from '@grafana/scenes';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { trackDashboardLoaded } from 'app/features/dashboard/utils/tracking';
import { DashboardDTO } from 'app/types';
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
@ -284,7 +283,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
}),
new behaviors.SceneQueryController(),
registerDashboardMacro,
registerDashboardSceneTracking(oldModel),
registerPanelInteractionsReporter,
new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
],
@ -517,18 +515,6 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
});
}
function registerDashboardSceneTracking(model: DashboardModel) {
return () => {
const unsetDashboardInteractionsScenesContext = DashboardInteractions.setScenesContext();
trackDashboardLoaded(model, model.version);
return () => {
unsetDashboardInteractionsScenesContext();
};
};
}
function registerPanelInteractionsReporter(scene: DashboardScene) {
// Subscriptions set with subscribeToEvent are automatically unsubscribed when the scene deactivated
scene.subscribeToEvent(UserActionEvent, (e) => {

View File

@ -6,6 +6,7 @@ import { createErrorNotification } from 'app/core/copy/appNotification';
import { backendSrv } from 'app/core/services/backend_srv';
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
import store from 'app/core/store';
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -30,6 +31,8 @@ import { PanelModel } from './PanelModel';
import { emitDashboardViewEvent } from './analyticsProcessor';
import { dashboardInitCompleted, dashboardInitFailed, dashboardInitFetching, dashboardInitServices } from './reducers';
const INIT_DASHBOARD_MEASUREMENT = 'initDashboard';
export interface InitDashboardArgs {
urlUid?: string;
urlSlug?: string;
@ -174,7 +177,7 @@ const getQueriesByDatasource = (
*/
export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
return async (dispatch, getState) => {
const initStart = performance.now();
startMeasure(INIT_DASHBOARD_MEASUREMENT);
// set fetching state
dispatch(dashboardInitFetching());
@ -287,8 +290,8 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
})
);
const duration = performance.now() - initStart;
trackDashboardLoaded(dashboard, duration, versionBeforeMigration);
const measure = stopMeasure(INIT_DASHBOARD_MEASUREMENT);
trackDashboardLoaded(dashboard, measure?.duration, versionBeforeMigration);
// yay we are done
dispatch(dashboardInitCompleted(dashboard));

View File

@ -37,6 +37,7 @@ describe('trackDashboardLoaded', () => {
expect(reportInteractionSpy).toHaveBeenCalledWith('dashboards_init_dashboard_completed', {
duration: 200,
isScene: false,
uid: 'dashboard-123',
title: 'Test Dashboard',
schemaVersion: model.schemaVersion, // This value is based on public/app/features/dashboard/state/DashboardMigrator.ts#L81

View File

@ -1,23 +1,14 @@
import { Panel, VariableModel } from '@grafana/schema/dist/esm/index';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { DashboardModel } from '../state';
import { DashboardModel, PanelModel } from '../state';
export function trackDashboardLoaded(dashboard: DashboardModel, duration: number, versionBeforeMigration?: number) {
export function trackDashboardLoaded(dashboard: DashboardModel, duration?: number, versionBeforeMigration?: number) {
// Count the different types of variables
const variables = dashboard.templating.list
.map((v) => v.type)
.reduce((r: Record<string, number>, k) => {
r[variableName(k)] = 1 + r[variableName(k)] || 1;
return r;
}, {});
const variables = getVariables(dashboard.templating.list);
// Count the different types of panels
const panels = dashboard.panels
.map((p) => p.type)
.reduce((r: Record<string, number>, p) => {
r[panelName(p)] = 1 + r[panelName(p)] || 1;
return r;
}, {});
const panels = getPanelCounts(dashboard.panels);
DashboardInteractions.dashboardInitialized({
uid: dashboard.uid,
@ -31,8 +22,49 @@ export function trackDashboardLoaded(dashboard: DashboardModel, duration: number
settings_nowdelay: dashboard.timepicker.nowDelay,
settings_livenow: !!dashboard.liveNow,
duration,
isScene: false,
});
}
export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?: number) {
const initialSaveModel = dashboard.getInitialSaveModel();
if (initialSaveModel) {
const panels = getPanelCounts(initialSaveModel.panels || []);
const variables = getVariables(initialSaveModel.templating?.list || []);
DashboardInteractions.dashboardInitialized({
uid: initialSaveModel.uid,
title: initialSaveModel.title,
theme: undefined,
schemaVersion: initialSaveModel.schemaVersion,
version_before_migration: initialSaveModel.version,
panels_count: initialSaveModel.panels?.length || 0,
...panels,
...variables,
settings_nowdelay: undefined,
settings_livenow: !!initialSaveModel.liveNow,
duration,
isScene: true,
});
}
}
function getPanelCounts(panels: Panel[] | PanelModel[]) {
return panels
.map((p) => p.type)
.reduce((r: Record<string, number>, p) => {
r[panelName(p)] = 1 + r[panelName(p)] || 1;
return r;
}, {});
}
function getVariables(variableList: VariableModel[]) {
return variableList
.map((v) => v.type)
.reduce((r: Record<string, number>, k) => {
r[variableName(k)] = 1 + r[variableName(k)] || 1;
return r;
}, {});
}
const variableName = (type: string) => `variable_type_${type}_count`;
const panelName = (type: string) => `panel_type_${type}_count`;