Files
iptvnator/apps/electron-backend-e2e/src/performance/performance-phase-events.ts
T
4gray a2fafcfc08 test(performance): add end-to-end Xtream benchmark harness (#1300)
* docs(performance): plan Xtream benchmark

* feat(xtream-mock-server): add deterministic 100k fixture

* style(xtream-mock-server): apply repository formatting

* fix(xtream-mock-server): harden performance fixture data

* feat(xtream-mock-server): add performance control plane

* docs(performance): correct Xtream capture plan

* fix(xtream-mock-server): harden performance controls

* fix(xtream-mock-server): harden control lifecycle

* feat(performance): add Xtream preload markers

* feat(performance): trace Xtream main phases

* feat(performance): mark Xtream store publications

* feat(performance): trace Xtream database phases

* feat(performance): trace Xtream delete cancellation

* feat(performance): capture Xtream phase attribution

* feat(performance): mark Sources Xtream refresh

* test(performance): define Xtream benchmark evidence contracts

* test(performance): add Xtream benchmark runner

* test(performance): surface failure evidence writes

* test(performance): align database read clock

* test(performance): preserve capture failure contracts
2026-07-28 08:08:07 +02:00

268 lines
8.2 KiB
TypeScript

import type { PerformancePhaseMetadata } from '@iptvnator/shared/interfaces';
export interface PairedPerformancePhase {
readonly durationMs: number;
readonly endedEpochMs: number;
readonly metadata: PerformancePhaseMetadata | null;
readonly phase: string;
readonly requestId: string;
readonly startedEpochMs: number;
}
interface ParsedPerformancePhaseEvent {
readonly boundary: 'end' | 'start';
readonly durationMs: number | null;
readonly epochMs: number;
readonly metadata: PerformancePhaseMetadata | null;
readonly phase: string;
readonly requestId: string;
}
export interface DiagnosticsPerformancePhaseEventParserOptions {
readonly allowedPhases: readonly string[];
readonly timelineType: string;
}
export interface DiagnosticsPerformancePhaseTimelineEvent {
readonly boundary: 'end' | 'start';
readonly byteCount?: number;
readonly durationMs: number | null;
readonly epochMs: number;
readonly itemCount?: number;
readonly phase: string;
readonly requestId: string;
readonly type: string;
}
export function createDiagnosticsPerformancePhaseEventParser(
options: DiagnosticsPerformancePhaseEventParserOptions
): (input: unknown) => DiagnosticsPerformancePhaseTimelineEvent | null {
const allowedPhases = new Set(options.allowedPhases);
const eventKeys = new Set([
'boundary',
'durationMs',
'epochMs',
'metadata',
'phase',
'requestId',
]);
const metadataKeys = new Set(['byteCount', 'itemCount']);
const helpers = {
isCount(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
},
isRecord(value: unknown): value is Record<string, unknown> {
return (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
);
},
};
return (input: unknown) => {
if (
!helpers.isRecord(input) ||
Object.keys(input).some((key) => !eventKeys.has(key))
) {
return null;
}
const boundary = input['boundary'];
const durationMs = input['durationMs'];
const epochMs = input['epochMs'];
const metadata = input['metadata'];
const phase = input['phase'];
const requestId = input['requestId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
typeof epochMs !== 'number' ||
!Number.isFinite(epochMs) ||
epochMs < 0 ||
typeof phase !== 'string' ||
!allowedPhases.has(phase) ||
typeof requestId !== 'string' ||
requestId.length === 0 ||
(boundary === 'start' && durationMs !== null) ||
(boundary === 'end' &&
(typeof durationMs !== 'number' ||
!Number.isFinite(durationMs) ||
durationMs < 0))
) {
return null;
}
if (
metadata !== undefined &&
(!helpers.isRecord(metadata) ||
Object.keys(metadata).some((key) => !metadataKeys.has(key)) ||
Object.values(metadata).some(
(value) => !helpers.isCount(value)
))
) {
return null;
}
const byteCount = helpers.isRecord(metadata)
? metadata['byteCount']
: undefined;
const itemCount = helpers.isRecord(metadata)
? metadata['itemCount']
: undefined;
return {
boundary,
...(helpers.isCount(byteCount) ? { byteCount } : {}),
durationMs: durationMs as number | null,
epochMs,
...(helpers.isCount(itemCount) ? { itemCount } : {}),
phase,
requestId,
type: options.timelineType,
};
};
}
export function pairPerformancePhaseEvents(
input: readonly unknown[]
): readonly PairedPerformancePhase[] {
const grouped = new Map<string, ParsedPerformancePhaseEvent[]>();
for (const value of input) {
const event = parsePerformancePhaseEvent(value);
const key = `${event.requestId}\u0000${event.phase}`;
const events = grouped.get(key) ?? [];
events.push(event);
if (events.length > 2) {
throw invalidPhaseEvents();
}
grouped.set(key, events);
}
const paired: PairedPerformancePhase[] = [];
for (const events of grouped.values()) {
const start = events.find((event) => event.boundary === 'start');
const end = events.find((event) => event.boundary === 'end');
if (
events.length !== 2 ||
!start ||
!end ||
start.durationMs !== null ||
start.metadata !== null ||
end.durationMs === null ||
end.epochMs < start.epochMs
) {
throw invalidPhaseEvents();
}
paired.push(
Object.freeze({
durationMs: end.durationMs,
endedEpochMs: end.epochMs,
metadata: end.metadata,
phase: start.phase,
requestId: start.requestId,
startedEpochMs: start.epochMs,
})
);
}
paired.sort(
(left, right) =>
left.startedEpochMs - right.startedEpochMs ||
left.endedEpochMs - right.endedEpochMs ||
left.requestId.localeCompare(right.requestId) ||
left.phase.localeCompare(right.phase)
);
return Object.freeze(paired);
}
export function requirePerformancePhase(
phases: readonly PairedPerformancePhase[],
requestId: string,
phase: string
): PairedPerformancePhase {
const matches = phases.filter(
(candidate) =>
candidate.requestId === requestId && candidate.phase === phase
);
if (matches.length === 0) {
throw new Error(`performance-phase-missing:${requestId}:${phase}`);
}
if (matches.length !== 1 || !matches[0]) {
throw new Error(`performance-phase-ambiguous:${requestId}:${phase}`);
}
return matches[0];
}
function parsePerformancePhaseEvent(
value: unknown
): ParsedPerformancePhaseEvent {
if (!isRecord(value)) {
throw invalidPhaseEvents();
}
const boundary = value['boundary'];
const durationMs = value['durationMs'];
const epochMs = value['epochMs'];
const phase = value['phase'];
const requestId = value['requestId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
(durationMs !== null && !isFiniteNonNegativeNumber(durationMs)) ||
!isFiniteNonNegativeNumber(epochMs) ||
typeof phase !== 'string' ||
phase.length === 0 ||
typeof requestId !== 'string' ||
requestId.length === 0
) {
throw invalidPhaseEvents();
}
return {
boundary,
durationMs,
epochMs,
metadata: parseMetadata(value['metadata']),
phase,
requestId,
};
}
function parseMetadata(value: unknown): PerformancePhaseMetadata | null {
if (value === undefined) {
return null;
}
if (!isRecord(value)) {
throw invalidPhaseEvents();
}
const keys = Object.keys(value);
if (
keys.some((key) => key !== 'byteCount' && key !== 'itemCount') ||
keys.some((key) => !isSafeCount(value[key]))
) {
throw invalidPhaseEvents();
}
return Object.freeze({
...(value['byteCount'] === undefined
? {}
: { byteCount: Number(value['byteCount']) }),
...(value['itemCount'] === undefined
? {}
: { itemCount: Number(value['itemCount']) }),
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function isSafeCount(value: unknown): boolean {
return (
value === undefined ||
(Number.isSafeInteger(value) && Number(value) >= 0)
);
}
function invalidPhaseEvents(): Error {
return new Error('performance-phase-events-invalid');
}