mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* 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
238 lines
7.3 KiB
TypeScript
238 lines
7.3 KiB
TypeScript
import { isDeepStrictEqual } from 'node:util';
|
|
|
|
import {
|
|
XTREAM_SCENARIO_ID,
|
|
type XtreamPerformanceManifest,
|
|
type XtreamScenarioId,
|
|
} from './xtream-benchmark-contract';
|
|
import { parseXtreamManifest } from './xtream-control-state-schema';
|
|
|
|
interface XtreamCatalogPartition {
|
|
readonly count: number;
|
|
readonly distinctXtreamIdCount: number;
|
|
readonly invalidNameCount: number;
|
|
readonly maxXtreamId: number | null;
|
|
readonly minXtreamId: number | null;
|
|
}
|
|
|
|
interface XtreamContentPartition {
|
|
readonly count: number;
|
|
readonly distinctXtreamIdCount: number;
|
|
readonly invalidCategoryCardinalityCount: number;
|
|
readonly invalidTitleCount: number;
|
|
readonly maxXtreamId: number | null;
|
|
readonly minXtreamId: number | null;
|
|
}
|
|
|
|
export interface XtreamCatalogDatabaseSnapshot {
|
|
readonly captureStoppedEpochMs: number;
|
|
readonly categories: {
|
|
readonly live: XtreamCatalogPartition;
|
|
readonly movies: XtreamCatalogPartition;
|
|
readonly series: XtreamCatalogPartition;
|
|
};
|
|
readonly categoryCount: number;
|
|
readonly content: {
|
|
readonly live: XtreamContentPartition;
|
|
readonly movie: XtreamContentPartition;
|
|
readonly series: XtreamContentPartition;
|
|
};
|
|
readonly contentCount: number;
|
|
readonly databaseReadStartedEpochMs: number;
|
|
readonly importStatuses: {
|
|
readonly live: string | null;
|
|
readonly movie: string | null;
|
|
readonly series: string | null;
|
|
};
|
|
readonly matchingPlaylistCount: number;
|
|
readonly matchingPlaylistType: string | null;
|
|
readonly playlistCount: number;
|
|
}
|
|
|
|
export interface XtreamCatalogVerification {
|
|
readonly databaseStateValid: true;
|
|
readonly fixtureIdentityValid: true;
|
|
readonly providerCategoryCount: number;
|
|
readonly providerItemCount: number;
|
|
readonly verifiedAfterCapture: true;
|
|
}
|
|
|
|
export function assertXtreamCatalogDatabaseSnapshot(
|
|
snapshot: unknown,
|
|
manifest: XtreamPerformanceManifest,
|
|
scenarioId: XtreamScenarioId
|
|
): asserts snapshot is XtreamCatalogDatabaseSnapshot {
|
|
try {
|
|
const canonicalManifest = parseXtreamManifest(manifest);
|
|
assertCatalogSnapshot(snapshot, canonicalManifest, scenarioId);
|
|
} catch {
|
|
throw new Error('invalid Xtream database catalog state');
|
|
}
|
|
}
|
|
|
|
export function createXtreamCatalogVerification(
|
|
snapshot: unknown,
|
|
manifest: XtreamPerformanceManifest,
|
|
scenarioId: XtreamScenarioId
|
|
): XtreamCatalogVerification {
|
|
try {
|
|
const canonicalManifest = parseXtreamManifest(manifest);
|
|
assertCatalogSnapshot(snapshot, canonicalManifest, scenarioId);
|
|
return Object.freeze({
|
|
databaseStateValid: true,
|
|
fixtureIdentityValid: true,
|
|
providerCategoryCount: canonicalManifest.counts.categories.total,
|
|
providerItemCount: canonicalManifest.counts.items.total,
|
|
verifiedAfterCapture: true,
|
|
});
|
|
} catch {
|
|
throw new Error('invalid Xtream database catalog state');
|
|
}
|
|
}
|
|
|
|
function assertCatalogSnapshot(
|
|
snapshot: unknown,
|
|
manifest: XtreamPerformanceManifest,
|
|
scenarioId: XtreamScenarioId
|
|
): void {
|
|
const expected = expectedSnapshot(manifest, scenarioId);
|
|
const candidate = snapshotTopLevel(snapshot);
|
|
if (
|
|
!isSnapshotTimingValid(candidate) ||
|
|
!isDeepStrictEqual(withoutSnapshotTiming(candidate), expected)
|
|
) {
|
|
invalid();
|
|
}
|
|
}
|
|
|
|
function expectedSnapshot(
|
|
manifest: XtreamPerformanceManifest,
|
|
scenarioId: XtreamScenarioId
|
|
): XtreamCatalogDatabaseSnapshot {
|
|
if (!Object.values(XTREAM_SCENARIO_ID).includes(scenarioId)) {
|
|
invalid();
|
|
}
|
|
const empty = isEmptyScenario(scenarioId);
|
|
const deleted = scenarioId === XTREAM_SCENARIO_ID.DELETE_LARGE;
|
|
const cancelled = scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT;
|
|
const expected = {
|
|
captureStoppedEpochMs: 1,
|
|
categories: {
|
|
live: categoryPartition(
|
|
empty ? 0 : manifest.counts.categories.live,
|
|
91_101
|
|
),
|
|
movies: categoryPartition(
|
|
empty ? 0 : manifest.counts.categories.vod,
|
|
92_101
|
|
),
|
|
series: categoryPartition(
|
|
empty ? 0 : manifest.counts.categories.series,
|
|
93_101
|
|
),
|
|
},
|
|
categoryCount: empty ? 0 : manifest.counts.categories.total,
|
|
content: {
|
|
live: contentPartition(
|
|
empty ? 0 : manifest.counts.items.live,
|
|
1_000_000
|
|
),
|
|
movie: contentPartition(
|
|
empty ? 0 : manifest.counts.items.vod,
|
|
2_000_000
|
|
),
|
|
series: contentPartition(
|
|
empty ? 0 : manifest.counts.items.series,
|
|
3_000_000
|
|
),
|
|
},
|
|
contentCount: empty ? 0 : manifest.counts.items.total,
|
|
databaseReadStartedEpochMs: 1,
|
|
importStatuses: {
|
|
live: cancelled ? 'failed' : 'completed',
|
|
movie: cancelled ? 'failed' : 'completed',
|
|
series: cancelled ? 'failed' : 'completed',
|
|
},
|
|
matchingPlaylistCount: deleted ? 0 : 1,
|
|
matchingPlaylistType: deleted ? null : 'xtream',
|
|
playlistCount: deleted ? 0 : 1,
|
|
} satisfies XtreamCatalogDatabaseSnapshot;
|
|
return {
|
|
...expected,
|
|
captureStoppedEpochMs: Number.NaN,
|
|
databaseReadStartedEpochMs: Number.NaN,
|
|
};
|
|
}
|
|
|
|
function snapshotTopLevel(value: unknown): XtreamCatalogDatabaseSnapshot {
|
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
invalid();
|
|
}
|
|
return { ...value } as XtreamCatalogDatabaseSnapshot;
|
|
}
|
|
|
|
function isSnapshotTimingValid(
|
|
snapshot: XtreamCatalogDatabaseSnapshot
|
|
): boolean {
|
|
if (
|
|
!isPositiveFinite(snapshot.captureStoppedEpochMs) ||
|
|
!isPositiveFinite(snapshot.databaseReadStartedEpochMs) ||
|
|
snapshot.databaseReadStartedEpochMs < snapshot.captureStoppedEpochMs
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function withoutSnapshotTiming(
|
|
snapshot: XtreamCatalogDatabaseSnapshot
|
|
): XtreamCatalogDatabaseSnapshot {
|
|
return {
|
|
...snapshot,
|
|
captureStoppedEpochMs: Number.NaN,
|
|
databaseReadStartedEpochMs: Number.NaN,
|
|
};
|
|
}
|
|
|
|
function isEmptyScenario(scenarioId: XtreamScenarioId): boolean {
|
|
return (
|
|
scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT ||
|
|
scenarioId === XTREAM_SCENARIO_ID.DELETE_LARGE
|
|
);
|
|
}
|
|
|
|
function categoryPartition(
|
|
count: number,
|
|
firstXtreamId: number
|
|
): XtreamCatalogPartition {
|
|
return {
|
|
count,
|
|
distinctXtreamIdCount: count,
|
|
invalidNameCount: 0,
|
|
maxXtreamId: count === 0 ? null : firstXtreamId + count - 1,
|
|
minXtreamId: count === 0 ? null : firstXtreamId,
|
|
};
|
|
}
|
|
|
|
function contentPartition(
|
|
count: number,
|
|
firstXtreamId: number
|
|
): XtreamContentPartition {
|
|
return {
|
|
count,
|
|
distinctXtreamIdCount: count,
|
|
invalidCategoryCardinalityCount: 0,
|
|
invalidTitleCount: 0,
|
|
maxXtreamId: count === 0 ? null : firstXtreamId + count - 1,
|
|
minXtreamId: count === 0 ? null : firstXtreamId,
|
|
};
|
|
}
|
|
|
|
function isPositiveFinite(value: unknown): value is number {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
}
|
|
|
|
function invalid(): never {
|
|
throw new Error('invalid');
|
|
}
|