mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-20 13:30:27 +01:00
Frontend performance metrics (#2439)
* Added enriched metrics * Add more traces for observability
This commit is contained in:
@@ -3,15 +3,28 @@ import { ApplicationMetricPhase, CommonMetricPhase, ScenarioConfig } from "./Sce
|
||||
|
||||
export const scenarioConfigs: Record<MetricScenario, ScenarioConfig> = {
|
||||
[MetricScenario.ApplicationLoad]: {
|
||||
requiredPhases: [ApplicationMetricPhase.ExplorerInitialized, CommonMetricPhase.Interactive],
|
||||
requiredPhases: [
|
||||
ApplicationMetricPhase.PlatformConfigured,
|
||||
ApplicationMetricPhase.CopilotConfigured,
|
||||
ApplicationMetricPhase.SampleDataLoaded,
|
||||
ApplicationMetricPhase.ExplorerInitialized,
|
||||
CommonMetricPhase.Interactive,
|
||||
],
|
||||
deferredPhases: [
|
||||
ApplicationMetricPhase.CopilotConfigured,
|
||||
ApplicationMetricPhase.SampleDataLoaded,
|
||||
ApplicationMetricPhase.ExplorerInitialized,
|
||||
],
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
[MetricScenario.DatabaseLoad]: {
|
||||
requiredPhases: [
|
||||
ApplicationMetricPhase.DatabasesFetched,
|
||||
ApplicationMetricPhase.CollectionsLoaded,
|
||||
ApplicationMetricPhase.DatabaseTreeRendered,
|
||||
CommonMetricPhase.Interactive,
|
||||
],
|
||||
deferredPhases: [ApplicationMetricPhase.CollectionsLoaded, ApplicationMetricPhase.DatabaseTreeRendered],
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,11 @@ export enum CommonMetricPhase {
|
||||
// Application-specific phases
|
||||
export enum ApplicationMetricPhase {
|
||||
ExplorerInitialized = "ExplorerInitialized",
|
||||
PlatformConfigured = "PlatformConfigured",
|
||||
CopilotConfigured = "CopilotConfigured",
|
||||
SampleDataLoaded = "SampleDataLoaded",
|
||||
DatabasesFetched = "DatabasesFetched",
|
||||
CollectionsLoaded = "CollectionsLoaded",
|
||||
DatabaseTreeRendered = "DatabaseTreeRendered",
|
||||
}
|
||||
|
||||
@@ -18,6 +22,7 @@ export type MetricPhase = CommonMetricPhase | ApplicationMetricPhase;
|
||||
|
||||
export interface ScenarioConfig<TPhase extends string = MetricPhase> {
|
||||
requiredPhases: TPhase[];
|
||||
deferredPhases?: TPhase[]; // Phases not auto-started at scenario start; started explicitly via startPhase()
|
||||
timeoutMs: number;
|
||||
validate?: (ctx: ScenarioContextSnapshot<TPhase>) => boolean; // Optional custom validation
|
||||
}
|
||||
|
||||
@@ -110,7 +110,13 @@ describe("ScenarioMonitor", () => {
|
||||
// Start scenario
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete all phases to emit
|
||||
// Complete all phases to emit (PlatformConfigured auto-started, deferred phases need start+complete)
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
@@ -161,13 +167,13 @@ describe("ScenarioMonitor", () => {
|
||||
it("emits healthy even with partial phase completion and expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
// Complete one non-deferred phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire (Interactive phase not completed)
|
||||
// Let timeout fire (deferred phases and Interactive not completed)
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
@@ -175,7 +181,7 @@ describe("ScenarioMonitor", () => {
|
||||
healthy: true,
|
||||
timedOut: true,
|
||||
hasExpectedFailure: true,
|
||||
completedPhases: expect.arrayContaining(["ExplorerInitialized"]),
|
||||
completedPhases: expect.arrayContaining(["PlatformConfigured"]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -216,7 +222,13 @@ describe("ScenarioMonitor", () => {
|
||||
it("emits healthy when all phases complete", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete all required phases
|
||||
// Complete all required phases (PlatformConfigured + Interactive auto-started, deferred need start)
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
@@ -225,7 +237,13 @@ describe("ScenarioMonitor", () => {
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: true,
|
||||
timedOut: false,
|
||||
completedPhases: expect.arrayContaining(["ExplorerInitialized", "Interactive"]),
|
||||
completedPhases: expect.arrayContaining([
|
||||
"PlatformConfigured",
|
||||
"CopilotConfigured",
|
||||
"SampleDataLoaded",
|
||||
"ExplorerInitialized",
|
||||
"Interactive",
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -233,8 +251,8 @@ describe("ScenarioMonitor", () => {
|
||||
it("does not emit until all phases complete", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete only one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
// Complete only one non-deferred phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
|
||||
|
||||
expect(reportMetric).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -246,7 +264,13 @@ describe("ScenarioMonitor", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Complete ApplicationLoad
|
||||
// Complete ApplicationLoad (all phases including deferred)
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
|
||||
@@ -87,8 +87,12 @@ class ScenarioMonitor {
|
||||
hasExpectedFailure: false,
|
||||
};
|
||||
|
||||
// Start all required phases at scenario start time
|
||||
// Start all required phases at scenario start time, except deferred ones
|
||||
const deferredSet = new Set(config.deferredPhases ?? []);
|
||||
config.requiredPhases.forEach((phase) => {
|
||||
if (deferredSet.has(phase)) {
|
||||
return; // Deferred phases are started explicitly via startPhase()
|
||||
}
|
||||
const phaseStartMarkName = `scenario_${scenario}_${phase}_start`;
|
||||
performance.mark(phaseStartMarkName);
|
||||
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
|
||||
@@ -135,6 +139,11 @@ class ScenarioMonitor {
|
||||
if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || ctx.phases.has(phase)) {
|
||||
return;
|
||||
}
|
||||
// Only deferred phases can be started via startPhase(); non-deferred are auto-started in start()
|
||||
const isDeferredPhase = ctx.config.deferredPhases?.includes(phase) ?? false;
|
||||
if (!isDeferredPhase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startMarkName = `scenario_${scenario}_${phase}_start`;
|
||||
performance.mark(startMarkName);
|
||||
@@ -147,10 +156,37 @@ class ScenarioMonitor {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a phase as skipped (e.g. copilot disabled). Removes the phase from
|
||||
* requiredPhases so it no longer blocks scenario completion.
|
||||
*/
|
||||
skipPhase(scenario: MetricScenario, phase: MetricPhase) {
|
||||
const ctx = this.contexts.get(scenario);
|
||||
if (!ctx || ctx.emitted) {
|
||||
return;
|
||||
}
|
||||
// Remove from requiredPhases so it doesn't block completion
|
||||
ctx.config = {
|
||||
...ctx.config,
|
||||
requiredPhases: ctx.config.requiredPhases.filter((p) => p !== phase),
|
||||
deferredPhases: ctx.config.deferredPhases?.filter((p) => p !== phase),
|
||||
};
|
||||
|
||||
this.devLog(`phase_skip: ${scenario}.${phase}`);
|
||||
|
||||
traceMark(Action.MetricsScenario, {
|
||||
event: "phase_skip",
|
||||
scenario,
|
||||
phase,
|
||||
});
|
||||
|
||||
this.tryEmitIfReady(ctx);
|
||||
}
|
||||
|
||||
completePhase(scenario: MetricScenario, phase: MetricPhase) {
|
||||
const ctx = this.contexts.get(scenario);
|
||||
const phaseCtx = ctx?.phases.get(phase);
|
||||
if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || !phaseCtx) {
|
||||
if (!ctx || ctx.emitted || ctx.completed.has(phase) || !ctx.config.requiredPhases.includes(phase) || !phaseCtx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,8 +392,7 @@ class ScenarioMonitor {
|
||||
|
||||
private cleanupPerformanceEntries(ctx: InternalScenarioContext) {
|
||||
performance.clearMarks(ctx.startMarkName);
|
||||
ctx.config.requiredPhases.forEach((phase) => {
|
||||
const phaseCtx = ctx.phases.get(phase);
|
||||
ctx.phases.forEach((phaseCtx, phase) => {
|
||||
if (phaseCtx?.startMarkName) {
|
||||
performance.clearMarks(phaseCtx.startMarkName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user