Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler-fixture-runtime.ts
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

320 lines
11 KiB
TypeScript

import type { RendererPerformancePhaseEvent } from '@iptvnator/shared/logging';
import {
XTREAM_SCENARIO_ID,
type XtreamScenarioId,
} from './xtream-benchmark-contract';
import type {
MainTimelineRecord,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
XTREAM_IPC_PRODUCER_NAMESPACE,
xtreamIpcProducerNamespaceForWorkerOperation,
} from './xtream-ipc-producer-identity';
import type { XtreamRawIterationResult } from './xtream-summary-contract';
import type {
XtreamBackgroundProbeEvidence,
XtreamScenarioTerminalEvidence,
} from './xtream-scenario-driver';
import {
summarizeXtreamUiActionSamples,
type XtreamUiActionProbeResult,
type XtreamUiActionSample,
} from './xtream-ui-action-probe';
const STORE_PREFIX = 'store.';
export function createLegacyTimeline(
requests: readonly WorkerRequestPerformanceMetrics[]
): MainTimelineRecord[] {
const legacy = requests.filter(
(request) =>
xtreamIpcProducerNamespaceForWorkerOperation(
request.operation ?? ''
) === XTREAM_IPC_PRODUCER_NAMESPACE.LEGACY_PRELOAD
);
return legacy.flatMap((request) => {
const source = requireFixtureValue(request.sourceEpochMs);
const received = requireFixtureValue(request.requestReceivedEpochMs);
const response = request.responseEpochMs;
const completion = response + 0.1;
const shared = {
ipcCallId: requireFixtureValue(request.ipcCallId),
operation: requireFixtureValue(request.operation),
playlistId: requireFixtureValue(request.playlistId),
};
return [
{
...shared,
epochMs: (source + received) / 2,
requestId: requireFixtureValue(request.requestId),
sourceEpochMs: source,
type: 'db-request',
},
{
...shared,
epochMs: response,
requestId: requireFixtureValue(request.requestId),
success: true,
type: 'db-response',
},
{
...shared,
epochMs: completion,
sourceEpochMs: completion,
success: true,
type: 'preload-performance-success',
},
];
});
}
export function createRendererCapture(
raw: XtreamRawIterationResult,
background: XtreamUiActionProbeResult | undefined
) {
const storeEvents = raw.phaseCapture.events.filter(({ phase }) =>
phase.startsWith(STORE_PREFIX)
);
const itemsByPhaseId = new Map(
storeEvents
.filter(({ boundary }) => boundary === 'end')
.map(({ correlationId, itemCount }) => [correlationId, itemCount])
);
const performancePhaseEvents = storeEvents.map(
(event): RendererPerformancePhaseEvent => {
const itemCount = itemsByPhaseId.get(event.correlationId) ?? null;
return {
boundary: event.boundary,
epochMs: event.epochMs,
...(itemCount === null
? {}
: { metadata: { items: itemCount } }),
monotonicMs: event.epochMs,
...(event.boundary === 'end' ? { outcome: 'success' } : {}),
phase: event.phase as RendererPerformancePhaseEvent['phase'],
phaseId: event.correlationId,
schemaVersion: 1,
};
}
);
const probe = {
cancelEligibleProgressEpochMs:
raw.cancellation.clickEpochMs === null
? null
: raw.cancellation.clickEpochMs - 0.1,
cancellationClickEpochMs: raw.cancellation.clickEpochMs,
dbCancellationTerminalEpochMs:
raw.cancellation.authoritativeTerminalEpochMs,
dbOperationEvents: [],
domReadyEpochMs: raw.phaseCapture.uiPaintEpochMs - 0.1,
frameGapsMs: [20, 45],
heartbeatDelaysMs: [5, 12],
invalidReason: null,
longTasksMs: [30, 75],
operationStartEpochMs: raw.phaseCapture.operationStartEpochMs,
performancePhaseEvents,
routeReadyEpochMs: raw.phaseCapture.operationStartEpochMs + 0.1,
scenarioId: raw.scenarioId,
storeTerminalEpochMs: Math.max(
...storeEvents
.filter(({ boundary }) => boundary === 'end')
.map(({ epochMs }) => epochMs)
),
terminalEpochMs: raw.phaseCapture.terminalEpochMs,
uiPaintedEpochMs: raw.phaseCapture.uiPaintEpochMs,
};
return {
captureCutoffEpochMs: raw.phaseCapture.terminalEpochMs + 0.5,
consoleErrorBreakdown:
raw.scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT
? {
databaseSaveCancellation: 1,
fetchContentCancellation: 1,
initializationCancellation: 1,
unexpected: 0,
}
: {
databaseSaveCancellation: 0,
fetchContentCancellation: 0,
initializationCancellation: 0,
unexpected: 0,
},
consoleErrorCount:
raw.scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT ? 3 : 0,
cpuProfilePath: null,
frameGap: {
count: 2,
max: 45,
mean: 32.5,
median: 32.5,
min: 20,
p95: 43.75,
p99: 44.75,
},
heapSnapshotPath: null,
heartbeatDelay: {
count: 2,
max: 12,
mean: 8.5,
median: 8.5,
min: 5,
p95: 11.65,
p99: 11.93,
},
longTask: {
count: 2,
max: 75,
mean: 52.5,
median: 52.5,
min: 30,
p95: 72.75,
p99: 74.55,
},
measurementStartEpochMs: raw.phaseCapture.operationStartEpochMs,
pageErrorCount: 0,
peakHeapUsedBytes: raw.renderer.peakHeapUsedBytes,
postGcHeapUsedBytes: raw.renderer.postGcHeapUsedBytes,
probe,
storeToPaintMs:
raw.phaseCapture.uiPaintEpochMs - probe.storeTerminalEpochMs,
targetId: raw.renderer.targetId,
tracePath: null,
...(background ? { background } : {}),
};
}
export function createBackgroundEvidence(
raw: XtreamRawIterationResult,
requests: readonly WorkerRequestPerformanceMetrics[]
): XtreamBackgroundProbeEvidence<XtreamUiActionProbeResult> | null {
if (raw.scenarioId !== XTREAM_SCENARIO_ID.BACKGROUND_UI) return null;
const request = requireFixtureValue(
requests.find(
({ operation }) => operation === 'DB_DELETE_XTREAM_CONTENT'
)
);
const start = requireFixtureValue(request.workStartedEpochMs);
const deleteEnd = requireFixtureValue(request.workEndedEpochMs);
const refreshEnd = Math.max(
...raw.phaseCapture.events
.filter(
({ boundary, phase }) =>
boundary === 'end' &&
phase === 'store.xtream-import-terminal'
)
.map(({ epochMs }) => epochMs)
);
const step = (refreshEnd - deleteEnd) / 12;
const categories = [
'action',
'navigation',
'navigation',
'action',
'search',
] as const;
const kinds = [
'type-filter',
'dashboard',
'sources',
'name-a-z',
'global-search',
] as const;
const samples: XtreamUiActionSample[] = kinds.map((kind, index) => ({
activeAfter: true,
activeBefore: true,
category: requireFixtureValue(categories[index]),
endEpochMs: deleteEnd + step * (index * 2 + 2),
kind,
latencyMs: step,
startEpochMs: deleteEnd + step * (index * 2 + 1),
}));
const summary = summarizeXtreamUiActionSamples(samples);
return {
completedEpochMs: requireFixtureValue(samples.at(-1)).endEpochMs,
operation: 'delete-xtream-content',
operationId: requireFixtureValue(request.operationId),
playlistId: requireFixtureValue(request.playlistId),
result: {
...summary,
dashboardPaintVisibility: 'visible-without-overlay',
samples,
},
startedEpochMs: start,
};
}
export function createTerminal(
raw: XtreamRawIterationResult,
background: XtreamBackgroundProbeEvidence<XtreamUiActionProbeResult> | null
): XtreamScenarioTerminalEvidence {
const cancellation =
raw.cancellation.clickEpochMs === null
? null
: {
armedEpochMs: raw.phaseCapture.operationStartEpochMs - 0.5,
clickEpochMs: raw.cancellation.clickEpochMs,
current: 6_000,
operationId: requireFixtureValue(
raw.cancellation.failedOperationId
),
progressEpochMs: raw.cancellation.clickEpochMs - 0.1,
threshold: 6_000,
total: 60_000,
};
const operation =
background === null
? null
: {
active: false,
operation: 'delete-xtream-content' as const,
operationId: background.operationId,
playlistId: background.playlistId,
startedEpochMs: background.startedEpochMs - 0.1,
terminalEpochMs:
requireFixtureValue(background.result.samples[0])
.startEpochMs -
requireFixtureValue(background.result.samples[0])
.latencyMs,
terminalStatus: 'completed' as const,
};
return {
backgroundProbe: background,
cancellation,
operation,
scenarioId: raw.scenarioId,
terminalEpochMs: raw.phaseCapture.terminalEpochMs - 0.1,
};
}
export function operationOutcomes(
requests: readonly WorkerRequestPerformanceMetrics[]
) {
const counts = new Map<string, number>();
for (const request of requests) {
const outcome = request.success ? 'success' : 'error';
const key = `${request.operation}\0${outcome}`;
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return [...counts].map(([key, count]) => {
const [operation, outcome] = key.split('\0');
return {
count,
operation: requireFixtureValue(operation),
outcome: outcome as 'error' | 'success',
};
});
}
export function scenarioOrdinal(scenarioId: XtreamScenarioId): number {
return Object.values(XTREAM_SCENARIO_ID).indexOf(scenarioId) + 1;
}
function requireFixtureValue<T>(value: T | null | undefined): T {
if (value === null || value === undefined) {
throw new Error('invalid Xtream assembler fixture');
}
return value;
}