Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b0533f7
feat(performance): extend SceneInteractionProfileEvent with panel met…
dprokop Sep 2, 2025
12ea7b8
feat(performance): add VizPanelRenderProfiler behavior for panel-leve…
dprokop Sep 7, 2025
ab05100
feat(performance): update package.json for VizPanelRenderProfiler int…
dprokop Sep 7, 2025
b436daf
Merge remote-tracking branch 'origin/main' into feat/panel-level-perf…
dprokop Sep 7, 2025
4de9886
VizPanelRenderProfiler: Remove long frame detection
dprokop Sep 7, 2025
e29c60c
S3.0: Panel Lifecycle Tracking - Hook panel lifecycle events
dprokop Sep 8, 2025
bdf9364
feat(performance): S3.1 data transformation performance tracking
dprokop Sep 9, 2025
23a1ceb
S4.0: Interaction correlation - SceneRenderProfiler and VizPanelRende…
dprokop Sep 10, 2025
10f0a17
S5.0: Metrics aggregation - SceneRenderProfiler collects panel metric…
dprokop Sep 10, 2025
88aa434
S5.0: Fix panel metrics collection scene graph traversal
dprokop Sep 10, 2025
c93e7bb
S4.0 + S5.0: Complete interaction correlation and metrics aggregation
dprokop Sep 10, 2025
b58e1c2
Remove PanelPerformanceCollectorLike interface and update tests
dprokop Sep 10, 2025
faa8f11
Refactor VizPanelRenderProfiler to use observer pattern and callback-…
dprokop Sep 15, 2025
40bd335
Remove InteractionBridge and legacy profile callback architecture
dprokop Sep 15, 2025
b57c789
Clean up comments and fix TypeScript errors
dprokop Sep 15, 2025
b5f245e
Replace DashboardPanelProfilingBehavior with PanelProfilingManager co…
dprokop Sep 16, 2025
2f1304a
Performance attribution: Remove legacy code and implement conditional…
dprokop Sep 24, 2025
8d94f08
Merge remote-tracking branch 'origin/main' into feat/faro-tracing-int…
dprokop Sep 24, 2025
57d4139
Fix circular dependency by extracting interaction constants to separa…
dprokop Sep 24, 2025
8a2600d
Add long frames tracking to PerformanceEventData interface
dprokop Sep 24, 2025
778bdb1
Refactor PerformanceEventData into event-specific interfaces
dprokop Sep 25, 2025
ed252cb
Refactor panel performance tracking: consolidate APIs and simplify me…
dprokop Sep 25, 2025
b7729b5
Remove duplicate SceneInteractionProfileEvent interface
dprokop Sep 25, 2025
7f2fb65
Performance profiling refinements: consistent logging, architectural …
dprokop Sep 29, 2025
e3ecb94
Simplify attachProfilerToPanel method return type
dprokop Sep 30, 2025
b3a93ce
Refactor performance profiling system architecture
dprokop Oct 7, 2025
031a4e9
Merge branch 'main' into feat/faro-tracing-integration-part1
dprokop Oct 7, 2025
231d7d2
Remove not used data
dprokop Oct 15, 2025
58739ee
cleanup
dprokop Oct 16, 2025
c065a20
Organise perf utils under a namespace
dprokop Oct 17, 2025
a050ccf
Merge remote-tracking branch 'origin/main' into feat/faro-tracing-int…
dprokop Oct 17, 2025
58d8e4b
Cleanup
dprokop Oct 17, 2025
c531aa3
typecheck
dprokop Oct 17, 2025
bc266af
review feedback
dprokop Oct 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/scenes/src/behaviors/SceneInteractionTracker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SceneObjectBase } from '../core/SceneObjectBase';
import { SceneObject, SceneStatelessBehavior } from '../core/types';
import { SceneRenderProfiler } from './SceneRenderProfiler';
import { SceneRenderProfiler } from '../performance/SceneRenderProfiler';
import { SceneInteractionTrackerState } from './types';

export function isInteractionTracker(s: SceneObject | SceneStatelessBehavior): s is SceneInteractionTracker {
Expand Down
2 changes: 1 addition & 1 deletion packages/scenes/src/behaviors/SceneQueryController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SceneObjectBase } from '../core/SceneObjectBase';
import { SceneObject, SceneStatelessBehavior } from '../core/types';
import { writeSceneLog } from '../utils/writeSceneLog';
import { SceneRenderProfiler } from './SceneRenderProfiler';
import { SceneRenderProfiler } from '../performance/SceneRenderProfiler';
import { SceneQueryControllerEntry, SceneQueryControllerLike, SceneQueryStateControllerState } from './types';

export function isQueryController(s: SceneObject | SceneStatelessBehavior): s is SceneQueryControllerLike {
Expand Down
18 changes: 1 addition & 17 deletions packages/scenes/src/behaviors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,7 @@ export interface SceneQueryControllerEntry {
export type SceneQueryControllerEntryType = 'data' | 'annotations' | 'variable' | 'alerts' | 'plugin' | string;

// Long Frame Detection types (re-exported from LongFrameDetector)
export type { LongFrameEvent, LongFrameCallback } from './LongFrameDetector';

export interface SceneInteractionProfileEvent {
origin: string;
duration: number;
networkDuration: number;
jsHeapSizeLimit: number;
usedJSHeapSize: number;
totalJSHeapSize: number;
crumbs: string[];
startTs: number;
endTs: number;
longFramesCount: number;
longFramesTotalTime: number;
// add more granular data,i.e. network times? slow frames?
}
export type { LongFrameEvent, LongFrameCallback } from '../performance/LongFrameDetector';

export interface SceneComponentInteractionEvent {
origin: string;
Expand All @@ -49,7 +34,6 @@ export interface SceneInteractionTrackerState extends SceneObjectState {
export interface SceneQueryStateControllerState extends SceneObjectState {
isRunning: boolean;
enableProfiling?: boolean;
onProfileComplete?(event: SceneInteractionProfileEvent): void;
}

export interface SceneQueryControllerLike extends SceneObject<SceneQueryStateControllerState> {
Expand Down
2 changes: 1 addition & 1 deletion packages/scenes/src/components/SceneRefreshPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SceneObjectBase } from '../core/SceneObjectBase';
import { sceneGraph } from '../core/sceneGraph';
import { SceneComponentProps, SceneObject, SceneObjectState, SceneObjectUrlValues } from '../core/types';
import { SceneObjectUrlSyncConfig } from '../services/SceneObjectUrlSyncConfig';
import { REFRESH_INTERACTION } from '../behaviors/SceneRenderProfiler';
import { REFRESH_INTERACTION } from '../performance/interactionConstants';

export const DEFAULT_INTERVALS = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];

Expand Down
41 changes: 41 additions & 0 deletions packages/scenes/src/components/VizPanel/VizPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { cloneDeep, isArray, isEmpty, merge, mergeWith } from 'lodash';
import { UserActionEvent } from '../../core/events';
import { evaluateTimeRange } from '../../utils/evaluateTimeRange';
import { LiveNowTimer } from '../../behaviors/LiveNowTimer';
import { VizPanelRenderProfiler } from '../../performance/VizPanelRenderProfiler';
import { registerQueryWithController, wrapPromiseInStateObservable } from '../../querying/registerQueryWithController';
import { SceneDataTransformer } from '../../querying/SceneDataTransformer';
import { SceneQueryRunner } from '../../querying/SceneQueryRunner';
Expand Down Expand Up @@ -140,6 +141,23 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
});
}

/**
* Get the VizPanelRenderProfiler behavior if attached
*/
public getProfiler(): VizPanelRenderProfiler | undefined {
if (!this.state.$behaviors) {
return undefined;
}

for (const behavior of this.state.$behaviors) {
if (behavior instanceof VizPanelRenderProfiler) {
return behavior;
}
}

return undefined;
}

private _onActivate() {
if (!this._plugin) {
this._loadPlugin(this.state.pluginId);
Expand All @@ -157,14 +175,21 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
overwriteFieldConfig?: FieldConfigSource,
isAfterPluginChange?: boolean
) {
const profiler = this.getProfiler();
const plugin = loadPanelPluginSync(pluginId);

if (plugin) {
// Plugin was loaded from cache
const endPluginLoadCallback = profiler?.onPluginLoadStart(pluginId);
endPluginLoadCallback?.(plugin, true);
this._pluginLoaded(plugin, overwriteOptions, overwriteFieldConfig, isAfterPluginChange);
} else {
const { importPanelPlugin } = getPluginImportUtils();

try {
// Start profiling plugin load - get end callback
const endPluginLoadCallback = profiler?.onPluginLoadStart(pluginId);

const panelPromise = importPanelPlugin(pluginId);

const queryControler = sceneGraph.getQueryController(this);
Expand All @@ -175,6 +200,10 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
}

const result = await panelPromise;

// End profiling plugin load (not from cache)
endPluginLoadCallback?.(result, false);

this._pluginLoaded(result, overwriteOptions, overwriteFieldConfig, isAfterPluginChange);
} catch (err: unknown) {
this._pluginLoaded(getPanelPluginNotFound(pluginId));
Expand Down Expand Up @@ -457,8 +486,11 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
* Called from the react render path to apply the field config to the data provided by the data provider
*/
public applyFieldConfig(rawData?: PanelData): PanelData {
const timestamp = performance.now();
const plugin = this._plugin;

const profiler = this.getProfiler();

if (!plugin || plugin.meta.skipDataQuery || !rawData) {
// TODO setup time range subscription instead
return emptyPanelData;
Expand All @@ -469,6 +501,9 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
return this._dataWithFieldConfig;
}

// Start profiling data processing - get end callback
const endFieldConfigCallback = profiler?.onFieldConfigStart(timestamp);

const pluginDataSupport: PanelPluginDataSupport = plugin.dataSupport || { alertStates: false, annotations: false };

const fieldConfigRegistry = plugin.fieldConfigRegistry;
Expand Down Expand Up @@ -515,6 +550,12 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
}

this._prevData = rawData;

// End profiling data processing
if (profiler) {
endFieldConfigCallback?.(performance.now());
}

return this._dataWithFieldConfig;
}

Expand Down
28 changes: 27 additions & 1 deletion packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Trans } from '@grafana/i18n';
import React, { RefCallback, useCallback, useEffect, useMemo } from 'react';
import React, { RefCallback, useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
import { useMeasure } from 'react-use';

// @ts-ignore
Expand Down Expand Up @@ -51,6 +51,32 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
[setPanelAttention]
);

// S3.0 RENDER TRACKING: Simple timing for performance measurement
const profiler = useMemo(() => model.getProfiler(), [model]);

// Capture render start time immediately when component function runs
const currentRenderStart = performance.now();

const endRenderCallbackRef = React.useRef<((endTimestamp: number, duration: number) => void) | null>(null);

useLayoutEffect(() => {
if (profiler) {
const callback = profiler.onSimpleRenderStart(currentRenderStart);
endRenderCallbackRef.current = callback || null;
}
});

// Use useEffect (after DOM updates) to measure complete render cycle timing
useEffect(() => {
if (endRenderCallbackRef.current) {
const timestamp = performance.now();
// Measure from component start to after DOM updates AND effects (complete render cycle)
const duration = timestamp - currentRenderStart;
endRenderCallbackRef.current(timestamp, duration);
endRenderCallbackRef.current = null; // Clear callback after use
}
});

const plugin = model.getPlugin();

const { dragClass, dragClassCancel } = getDragClasses(model);
Expand Down
2 changes: 1 addition & 1 deletion packages/scenes/src/core/SceneTimeRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isValid } from '../utils/date';
import { getQueryController } from './sceneGraph/getQueryController';
import { writeSceneLog } from '../utils/writeSceneLog';
import { isEmpty } from 'lodash';
import { TIME_RANGE_CHANGE_INTERACTION } from '../behaviors/SceneRenderProfiler';
import { TIME_RANGE_CHANGE_INTERACTION } from '../performance/interactionConstants';

export class SceneTimeRange extends SceneObjectBase<SceneTimeRangeState> implements SceneTimeRangeLike {
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['from', 'to', 'timezone', 'time', 'time.window'] });
Expand Down
4 changes: 2 additions & 2 deletions packages/scenes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export * from './core/types';
export * from './core/events';
export { sceneGraph } from './core/sceneGraph';
export * as behaviors from './behaviors';
export * as performanceUtils from './performance';
export { writePerformanceLog } from './utils/writePerformanceLog';
export * as dataLayers from './querying/layers';

export { SceneObjectBase, useSceneObjectState } from './core/SceneObjectBase';
Expand All @@ -53,9 +55,7 @@ export type {
SceneQueryControllerLike,
SceneQueryControllerEntryType,
SceneQueryControllerEntry,
SceneInteractionProfileEvent,
} from './behaviors/types';
export { SceneRenderProfiler } from './behaviors/SceneRenderProfiler';

export * from './variables/types';
export { VariableDependencyConfig } from './variables/VariableDependencyConfig';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { writeSceneLog } from '../utils/writeSceneLog';
import { writePerformanceLog } from '../utils/writePerformanceLog';

const LONG_FRAME_THRESHOLD = 50; // Threshold for both LoAF and manual tracking (ms)

Expand Down Expand Up @@ -49,7 +49,7 @@ export class LongFrameDetector {
*/
public start(callback: LongFrameCallback): void {
if (this.#isTracking) {
writeSceneLog('LongFrameDetector', 'Already tracking frames, stopping previous session');
writePerformanceLog('LFD', 'Already tracking frames, stopping previous session');
this.stop();
}

Expand All @@ -62,8 +62,8 @@ export class LongFrameDetector {
this.startManualFrameTracking();
}

writeSceneLog(
'LongFrameDetector',
writePerformanceLog(
'LFD',
`Started tracking with ${
this.isLoAFAvailable() ? 'LoAF API' : 'manual'
} method, threshold: ${LONG_FRAME_THRESHOLD}ms`
Expand Down Expand Up @@ -99,7 +99,7 @@ export class LongFrameDetector {
*/
private startLoAFTracking(): void {
if (!this.isLoAFAvailable()) {
writeSceneLog('LongFrameDetector', 'LoAF API not available, falling back to manual tracking');
writePerformanceLog('LFD', 'LoAF API not available, falling back to manual tracking');
this.startManualFrameTracking();
return;
}
Expand Down Expand Up @@ -138,13 +138,13 @@ export class LongFrameDetector {
}
}

writeSceneLog('LongFrameDetector', `Long frame detected (LoAF): ${entry.duration}ms at ${entry.startTime}ms`);
writePerformanceLog('LFD', `Long frame detected (LoAF): ${entry.duration}ms at ${entry.startTime}ms`);
}
});

this.#loafObserver.observe({ type: 'long-animation-frame', buffered: false });
} catch (error) {
writeSceneLog('LongFrameDetector', 'Failed to start LoAF tracking, falling back to manual:', error);
writePerformanceLog('LFD', 'Failed to start LoAF tracking, falling back to manual:', error);
this.startManualFrameTracking();
}
}
Expand All @@ -156,7 +156,7 @@ export class LongFrameDetector {
if (this.#loafObserver) {
this.#loafObserver.disconnect();
this.#loafObserver = null;
writeSceneLog('LongFrameDetector', 'Stopped LoAF tracking');
writePerformanceLog('LFD', 'Stopped LoAF tracking');
}
}

Expand All @@ -175,7 +175,7 @@ export class LongFrameDetector {
if (this.#frameTrackingId) {
cancelAnimationFrame(this.#frameTrackingId);
this.#frameTrackingId = null;
writeSceneLog('LongFrameDetector', 'Stopped manual frame tracking');
writePerformanceLog('LFD', 'Stopped manual frame tracking');
}
}

Expand Down Expand Up @@ -222,8 +222,8 @@ export class LongFrameDetector {
}
}

writeSceneLog(
'LongFrameDetector',
writePerformanceLog(
'LFD',
`Long frame detected (manual): ${frameLength}ms (threshold: ${LONG_FRAME_THRESHOLD}ms)`
);
}
Expand Down
90 changes: 90 additions & 0 deletions packages/scenes/src/performance/PanelProfilingManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Unsubscribable } from 'rxjs';
import { VizPanel } from '../components/VizPanel/VizPanel';
import { VizPanelRenderProfiler } from './VizPanelRenderProfiler';
import { SceneObject } from '../core/types';
import { sceneGraph } from '../core/sceneGraph';

export interface PanelProfilingConfig {
watchStateKey?: string; // State property to watch for structural changes (e.g., 'body', 'children')
}

/**
* Manages VizPanelRenderProfiler instances for all panels in a scene object.
* Extracted from DashboardPanelProfilingBehavior to allow composition with SceneRenderProfiler.
*/
export class PanelProfilingManager {
private _sceneObject?: SceneObject;
private _subscriptions: Unsubscribable[] = [];

public constructor(private _config: PanelProfilingConfig) {}

/**
* Attach panel profiling to a scene object
*/
public attachToScene(sceneObject: SceneObject) {
this._sceneObject = sceneObject;

// Subscribe to scene state changes to add profilers to new panels
const subscription = sceneObject.subscribeToState((newState: any, prevState: any) => {
// If watchStateKey is specified, only react to changes in that specific property
if (this._config.watchStateKey) {
if (newState[this._config.watchStateKey] !== prevState[this._config.watchStateKey]) {
this._attachProfilersToPanels();
}
} else {
// Fallback: react to any state change
this._attachProfilersToPanels();
}
});

this._subscriptions.push(subscription);

// Attach profilers to existing panels
this._attachProfilersToPanels();
}

/**
* Attach VizPanelRenderProfiler to a specific panel if it doesn't already have one
* @param panel - The VizPanel to attach profiling to
*/
public attachProfilerToPanel(panel: VizPanel): void {
// Check if profiler already exists
const existingProfiler = panel.state.$behaviors?.find((b) => b instanceof VizPanelRenderProfiler);

if (existingProfiler) {
return; // Already has a profiler
}

// Add profiler behavior
const profiler = new VizPanelRenderProfiler();

panel.setState({
$behaviors: [...(panel.state.$behaviors || []), profiler],
});
}

/**
* Attach VizPanelRenderProfiler to all VizPanels that don't already have one
*/
private _attachProfilersToPanels() {
if (!this._sceneObject) {
return;
}

// Use scene graph to find all VizPanels in the scene
const panels = sceneGraph.findAllObjects(this._sceneObject, (obj) => obj instanceof VizPanel) as VizPanel[];

panels.forEach((panel) => {
this.attachProfilerToPanel(panel);
});
}

/**
* Clean up subscriptions and references
*/
public cleanup() {
this._subscriptions.forEach((sub) => sub.unsubscribe());
this._subscriptions = [];
this._sceneObject = undefined;
}
}
Loading
Loading