mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* test(performance): harden Xtream startup retry * test(performance): preserve Xtream teardown failures * test(performance): retry Xtream profile cleanup
284 lines
10 KiB
TypeScript
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');
|
|
}
|