Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-iteration-assembler.ts
T
4gray 055170d188 test(performance): harden Xtream startup retry (#1307)
* test(performance): harden Xtream startup retry

* test(performance): preserve Xtream teardown failures

* test(performance): retry Xtream profile cleanup
2026-07-29 01:05:32 +02:00

284 lines
10 KiB
TypeScript

import { createHash } from 'node:crypto';
import type { XtreamIterationDefinition } from './xtream-benchmark-contract';
import {
XTREAM_ITERATION_KIND,
XTREAM_SCENARIO_ID,
} from './xtream-benchmark-contract';
import type { XtreamCatalogVerification } from './xtream-catalog-database-verification';
import {
assertXtreamDiagnosticArtifactEvidence,
type XtreamDiagnosticArtifactEvidence,
} from './xtream-diagnostic-artifacts';
import { freezeDeep } from './xtream-exact-schema';
import {
assertXtreamMainEvidenceGates,
createXtreamDatabaseWorkerMetrics,
createXtreamMainMetrics,
createXtreamRendererMetrics,
} from './xtream-iteration-assembly-metrics';
import { deriveXtreamPhaseCapture } from './xtream-iteration-assembly-phases';
import { assembleXtreamScenarioEvidence } from './xtream-iteration-assembly-scenarios';
import { assertXtreamIterationEvidenceBinding } from './xtream-iteration-evidence-binding';
import { assertXtreamIterationResult } from './xtream-iteration-validity';
import type { XtreamMainCaptureResult } from './m3u-refresh-main-capture';
import { attributeXtreamPhases } from './xtream-phase-attribution';
import type { XtreamRendererCaptureMetrics } from './xtream-renderer-capture';
import { validXtreamRendererConsoleErrorEvidence } from './xtream-renderer-console-errors';
import { assertCompleteXtreamRendererProbe } from './xtream-renderer-probe';
import {
XTREAM_SCENARIO_DATABASE_REQUEST_COUNT,
type XtreamScenarioTerminalEvidence,
type XtreamScenarioTriggerEvidence,
} from './xtream-scenario-driver';
import {
XTREAM_NOT_APPLICABLE,
XTREAM_STARTUP_RETRY_REASON,
type XtreamCatalogVerification as XtreamSummaryCatalogVerification,
type XtreamIterationProcessIdentity,
type XtreamRawIterationResult,
type XtreamStartupRetryReason,
} from './xtream-summary-contract';
export interface XtreamIterationProcessEvidence {
readonly electronPid: number;
readonly freshProcessVerified: true;
readonly launchId: string;
readonly profileDirectorySha256: string;
readonly startupAttemptCount: 1 | 2;
readonly startupRetryReasons: readonly XtreamStartupRetryReason[];
}
export interface XtreamIterationAssemblyInput {
readonly catalogVerification: XtreamCatalogVerification;
readonly definition: XtreamIterationDefinition;
readonly diagnosticArtifacts: XtreamDiagnosticArtifactEvidence | null;
readonly mainCapture: XtreamMainCaptureResult;
readonly process: XtreamIterationProcessEvidence;
readonly rendererCapture: XtreamRendererCaptureMetrics;
readonly terminal: XtreamScenarioTerminalEvidence;
readonly trigger: XtreamScenarioTriggerEvidence;
}
export function assembleXtreamRawIteration(
input: XtreamIterationAssemblyInput
): XtreamRawIterationResult {
try {
return assembleStrict(input);
} catch {
throw new Error('xtream-iteration-assembly-invalid');
}
}
function assembleStrict(
input: XtreamIterationAssemblyInput
): XtreamRawIterationResult {
assertInputBoundary(input);
const diagnostic = validateDiagnosticEvidence(input);
assertXtreamMainEvidenceGates(input.mainCapture);
assertCompleteXtreamRendererProbe(input.rendererCapture.probe);
const phaseCapture = deriveXtreamPhaseCapture(
input.definition.scenarioId,
input.mainCapture,
input.rendererCapture
);
const phases = attributeXtreamPhases(phaseCapture);
const scenario = assembleXtreamScenarioEvidence({
main: input.mainCapture,
phaseCapture,
renderer: input.rendererCapture,
scenarioId: input.definition.scenarioId,
terminal: input.terminal,
trigger: input.trigger,
});
const context = {
diagnostic,
kind: input.definition.kind,
};
const raw: XtreamRawIterationResult = {
...input.definition,
cancellation: scenario.cancellation,
cancellationEvidence: scenario.cancellationEvidence,
catalogVerification: copyCatalogVerification(input.catalogVerification),
databaseWorker: createXtreamDatabaseWorkerMetrics(
input.mainCapture,
context
),
main: createXtreamMainMetrics(input.mainCapture, context),
phaseCapture,
phases,
playlistRefreshWorker: {
instantiated: false,
reason: XTREAM_NOT_APPLICABLE.PLAYLIST_REFRESH_WORKER,
},
processIdentity: createProcessIdentity(input),
renderer: createXtreamRendererMetrics(
input.mainCapture,
input.rendererCapture,
context,
scenario.backgroundMetrics
),
validity: {
actionContractValid: true,
catalogContractValid: true,
databaseWorkerCount: input.mainCapture.databaseWorkerCount,
dbPhaseContractValid: true,
diagnosticArtifactsValid:
input.definition.kind === XTREAM_ITERATION_KIND.DIAGNOSTIC
? true
: null,
lateDatabaseRequestCount:
input.mainCapture.lateDatabaseRequestCount,
playlistWorkerCount: input.mainCapture.playlistRefreshWorkerCount,
rendererTargetCount: 1,
},
};
assertXtreamIterationEvidenceBinding(raw);
assertXtreamIterationResult(raw);
return freezeDeep(raw);
}
function assertInputBoundary(input: XtreamIterationAssemblyInput): void {
const scenarioId = input.definition.scenarioId;
if (
!Object.values(XTREAM_SCENARIO_ID).includes(scenarioId) ||
input.mainCapture.requests.length !==
XTREAM_SCENARIO_DATABASE_REQUEST_COUNT[scenarioId] ||
!validXtreamRendererConsoleErrorEvidence(
input.rendererCapture.consoleErrorCount,
input.rendererCapture.consoleErrorBreakdown,
scenarioId
) ||
input.rendererCapture.pageErrorCount !== 0 ||
input.rendererCapture.storeToPaintMs !==
requireEpoch(input.rendererCapture.probe.uiPaintedEpochMs) -
requireEpoch(input.rendererCapture.probe.storeTerminalEpochMs)
) {
invalid();
}
const catalog = input.catalogVerification;
if (
catalog.databaseStateValid !== true ||
catalog.fixtureIdentityValid !== true ||
catalog.verifiedAfterCapture !== true ||
catalog.providerCategoryCount !== 100 ||
catalog.providerItemCount !== 100_000
) {
invalid();
}
}
function copyCatalogVerification(
catalog: XtreamCatalogVerification
): XtreamSummaryCatalogVerification {
return {
databaseStateValid: catalog.databaseStateValid,
fixtureIdentityValid: catalog.fixtureIdentityValid,
providerCategoryCount: catalog.providerCategoryCount as 100,
providerItemCount: catalog.providerItemCount as 100_000,
verifiedAfterCapture: catalog.verifiedAfterCapture,
};
}
function validateDiagnosticEvidence(
input: XtreamIterationAssemblyInput
): XtreamDiagnosticArtifactEvidence | null {
if (input.definition.kind !== XTREAM_ITERATION_KIND.DIAGNOSTIC) {
if (
input.diagnosticArtifacts !== null ||
input.mainCapture.capture.cpuProfilePath !== null ||
input.mainCapture.capture.heapSnapshotPath !== null ||
input.rendererCapture.cpuProfilePath !== null ||
input.rendererCapture.heapSnapshotPath !== null ||
input.rendererCapture.tracePath !== null ||
input.mainCapture.workers.some(
({ profilePath, snapshotPath }) =>
profilePath !== null || snapshotPath !== null
)
) {
invalid();
}
return null;
}
assertXtreamDiagnosticArtifactEvidence(input.diagnosticArtifacts);
if (
input.process.profileDirectorySha256 !==
input.diagnosticArtifacts.directorySha256
) {
invalid();
}
return input.diagnosticArtifacts;
}
function createProcessIdentity(
input: XtreamIterationAssemblyInput
): XtreamIterationProcessIdentity {
const process = input.process;
if (
process.freshProcessVerified !== true ||
!Number.isSafeInteger(process.electronPid) ||
process.electronPid <= 0 ||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(process.launchId) ||
!/^[a-f0-9]{64}$/.test(process.profileDirectorySha256) ||
!validStartupEvidence(process)
) {
invalid();
}
const captureGeneration = input.mainCapture.captureGeneration;
if (!Number.isSafeInteger(captureGeneration) || captureGeneration <= 0) {
invalid();
}
const identity = {
captureGeneration,
electronPid: process.electronPid,
launchId: process.launchId,
profileDirectorySha256: process.profileDirectorySha256,
startupAttemptCount: process.startupAttemptCount,
startupRetryReasons: process.startupRetryReasons,
rendererTargetId: input.rendererCapture.targetId,
browserWindowId:
input.mainCapture.capture.rendererWindow.windowIdentity
.browserWindowId,
webContentsId:
input.mainCapture.capture.rendererWindow.windowIdentity
.webContentsId,
};
return {
captureGeneration,
electronPid: process.electronPid,
freshProcess: true,
generationIdentitySha256: createHash('sha256')
.update(JSON.stringify(identity))
.digest('hex'),
launchId: process.launchId,
profileDirectorySha256: process.profileDirectorySha256,
startupAttemptCount: process.startupAttemptCount,
startupRetryReasons: [...process.startupRetryReasons],
};
}
function validStartupEvidence(
process: XtreamIterationProcessEvidence
): boolean {
return (
(process.startupAttemptCount === 1 ||
process.startupAttemptCount === 2) &&
Array.isArray(process.startupRetryReasons) &&
process.startupRetryReasons.length ===
process.startupAttemptCount - 1 &&
process.startupRetryReasons.every((reason) =>
Object.values(XTREAM_STARTUP_RETRY_REASON).includes(reason)
)
);
}
function requireEpoch(value: unknown): number {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
invalid();
return value;
}
function invalid(): never {
throw new Error('invalid Xtream iteration assembly input');
}