Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-catalog-database-reader.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

303 lines
9.4 KiB
TypeScript

import type { ElectronApplication } from '@playwright/test';
import { isAbsolute, join } from 'node:path';
import type { XtreamCatalogDatabaseSnapshot } from './xtream-catalog-database-verification';
import {
buildXtreamCatalogDatabaseQueries,
type XtreamCatalogDatabaseQuery,
} from './xtream-catalog-database-queries';
export type XtreamCatalogDatabaseEvaluator = Pick<
ElectronApplication,
'evaluate'
>;
export interface XtreamCatalogDatabaseReadOptions {
readonly captureStoppedEpochMs: number;
readonly dataDirectory: string;
readonly playlistId: string;
readonly requireFromPath: string;
}
interface DatabaseReadInput {
readonly databasePath: string;
readonly queries: readonly XtreamCatalogDatabaseQuery[];
readonly requireFromPath: string;
}
interface DatabaseReadTransport {
readonly databaseReadStartedEpochMs: number;
readonly rows: readonly unknown[];
}
export async function readXtreamCatalogDatabaseSnapshot(
evaluator: XtreamCatalogDatabaseEvaluator,
options: XtreamCatalogDatabaseReadOptions
): Promise<XtreamCatalogDatabaseSnapshot> {
try {
assertOptions(options);
const queries = buildXtreamCatalogDatabaseQueries(options.playlistId);
const transport = (await evaluator.evaluate(
async (_electron, input: DatabaseReadInput) => {
interface SqliteStatement {
get(...parameters: readonly (number | string)[]): unknown;
}
interface SqliteDatabase {
close(): void;
pragma(source: string): unknown;
prepare(source: string): SqliteStatement;
}
interface SqliteConstructor {
new (
path: string,
options: {
readonly: true;
fileMustExist: true;
}
): SqliteDatabase;
}
interface NodeModuleBuiltin {
createRequire(path: string): (id: string) => unknown;
}
interface NodePerfHooksBuiltin {
readonly performance: {
now(): number;
readonly timeOrigin: number;
};
}
const perfHooks = process.getBuiltinModule(
'node:perf_hooks'
) as NodePerfHooksBuiltin;
const databaseReadStartedEpochMs =
perfHooks.performance.timeOrigin +
perfHooks.performance.now();
const nodeModule = process.getBuiltinModule(
'node:module'
) as NodeModuleBuiltin;
const requireFromBuild = nodeModule.createRequire(
input.requireFromPath
);
const Sqlite = requireFromBuild(
'better-sqlite3'
) as SqliteConstructor;
const database = new Sqlite(input.databasePath, {
readonly: true,
fileMustExist: true,
});
try {
database.pragma('query_only = ON');
return {
databaseReadStartedEpochMs,
rows: input.queries.map((query) =>
database.prepare(query.sql).get(...query.parameters)
),
};
} finally {
database.close();
}
},
{
databasePath: join(
options.dataDirectory,
'databases',
'iptvnator.db'
),
queries,
requireFromPath: options.requireFromPath,
}
)) as DatabaseReadTransport;
return snapshotTransport(
transport,
options.captureStoppedEpochMs,
queries.length
);
} catch {
throw new Error('invalid Xtream catalog database read');
}
}
function snapshotTransport(
value: unknown,
captureStoppedEpochMs: number,
expectedRowCount: number
): XtreamCatalogDatabaseSnapshot {
const transport = exactRecord(value, [
'databaseReadStartedEpochMs',
'rows',
]);
const databaseReadStartedEpochMs = transport[
'databaseReadStartedEpochMs'
] as unknown;
const rows = transport['rows'];
if (
!isPositiveFinite(databaseReadStartedEpochMs) ||
databaseReadStartedEpochMs < captureStoppedEpochMs ||
!Array.isArray(rows) ||
rows.length !== expectedRowCount
) {
invalid();
}
const playlist = numericRecord(rows[0], [
'playlistCount',
'matchingPlaylistCount',
'matchingPlaylistType',
]);
const categoryCount = singleCount(rows[1], 'categoryCount');
const contentCount = singleCount(rows[2], 'contentCount');
const categories = [rows[3], rows[4], rows[5]].map(categoryPartition);
const content = [rows[6], rows[7], rows[8]].map(contentPartition);
const importStatuses = statusRecord(rows[9]);
const matchingPlaylistType = playlist['matchingPlaylistType'];
if (
matchingPlaylistType !== null &&
typeof matchingPlaylistType !== 'string'
) {
invalid();
}
return Object.freeze({
captureStoppedEpochMs,
categories: Object.freeze({
live: categories[0],
movies: categories[1],
series: categories[2],
}),
categoryCount,
content: Object.freeze({
live: content[0],
movie: content[1],
series: content[2],
}),
contentCount,
databaseReadStartedEpochMs,
importStatuses,
matchingPlaylistCount: nonNegativeInteger(
playlist['matchingPlaylistCount']
),
matchingPlaylistType,
playlistCount: nonNegativeInteger(playlist['playlistCount']),
});
}
function categoryPartition(value: unknown) {
const row = numericRecord(value, [
'count',
'distinctXtreamIdCount',
'invalidNameCount',
'maxXtreamId',
'minXtreamId',
]);
return Object.freeze({
count: nonNegativeInteger(row['count']),
distinctXtreamIdCount: nonNegativeInteger(row['distinctXtreamIdCount']),
invalidNameCount: nonNegativeInteger(row['invalidNameCount']),
maxXtreamId: nullableInteger(row['maxXtreamId']),
minXtreamId: nullableInteger(row['minXtreamId']),
});
}
function contentPartition(value: unknown) {
const row = numericRecord(value, [
'count',
'distinctXtreamIdCount',
'invalidCategoryCardinalityCount',
'invalidTitleCount',
'maxXtreamId',
'minXtreamId',
]);
return Object.freeze({
count: nonNegativeInteger(row['count']),
distinctXtreamIdCount: nonNegativeInteger(row['distinctXtreamIdCount']),
invalidCategoryCardinalityCount: nonNegativeInteger(
row['invalidCategoryCardinalityCount']
),
invalidTitleCount: nonNegativeInteger(row['invalidTitleCount']),
maxXtreamId: nullableInteger(row['maxXtreamId']),
minXtreamId: nullableInteger(row['minXtreamId']),
});
}
function singleCount(value: unknown, key: string): number {
return nonNegativeInteger(numericRecord(value, [key])[key]);
}
function statusRecord(value: unknown) {
const row = exactRecord(value, ['live', 'movie', 'series']);
for (const key of ['live', 'movie', 'series']) {
if (row[key] !== null && typeof row[key] !== 'string') {
invalid();
}
}
return Object.freeze({
live: row['live'] as string | null,
movie: row['movie'] as string | null,
series: row['series'] as string | null,
});
}
function numericRecord(
value: unknown,
keys: readonly string[]
): Record<string, unknown> {
return exactRecord(value, keys);
}
function exactRecord(
value: unknown,
expectedKeys: readonly string[]
): Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
invalid();
}
const keys = Reflect.ownKeys(value);
if (
keys.length !== expectedKeys.length ||
keys.some(
(key) => typeof key !== 'string' || !expectedKeys.includes(key)
)
) {
invalid();
}
return Object.fromEntries(
expectedKeys.map((key) => [key, Reflect.get(value, key)])
);
}
function nonNegativeInteger(value: unknown): number {
if (
typeof value !== 'number' ||
!Number.isSafeInteger(value) ||
value < 0
) {
invalid();
}
return value;
}
function nullableInteger(value: unknown): number | null {
if (value === null) return null;
return nonNegativeInteger(value);
}
function assertOptions(options: XtreamCatalogDatabaseReadOptions): void {
if (
!isPositiveFinite(options.captureStoppedEpochMs) ||
!isAbsolute(options.dataDirectory) ||
!isAbsolute(options.requireFromPath) ||
options.playlistId.length === 0 ||
options.playlistId.length > 256
) {
invalid();
}
}
function isPositiveFinite(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value > 0;
}
function invalid(): never {
throw new Error('invalid');
}