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
366 lines
12 KiB
TypeScript
366 lines
12 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { describe, it } from 'node:test';
|
|
|
|
import {
|
|
readXtreamCatalogDatabaseSnapshot,
|
|
type XtreamCatalogDatabaseEvaluator,
|
|
} from './xtream-catalog-database-reader';
|
|
|
|
interface TestStatement {
|
|
get(...parameters: readonly (number | string)[]): unknown;
|
|
run(...parameters: readonly (number | string)[]): unknown;
|
|
}
|
|
|
|
interface TestDatabase {
|
|
close(): void;
|
|
exec(source: string): void;
|
|
prepare(source: string): TestStatement;
|
|
}
|
|
|
|
interface TestDatabaseConstructor {
|
|
new (path: string): TestDatabase;
|
|
}
|
|
|
|
interface RuntimeProcess {
|
|
getBuiltinModule(id: string): unknown;
|
|
}
|
|
|
|
const { DatabaseSync } = (
|
|
process as unknown as RuntimeProcess
|
|
).getBuiltinModule('node:sqlite') as {
|
|
readonly DatabaseSync: TestDatabaseConstructor;
|
|
};
|
|
|
|
describe('Xtream catalog database reader', () => {
|
|
it('reads the scoped catalog and import state after capture stops', async () => {
|
|
const database = createDatabase();
|
|
seedPlaylist(database, 'playlist-1');
|
|
seedPartition(database, {
|
|
categoryId: 91_101,
|
|
categoryName: 'Performance Live 01',
|
|
categoryType: 'live',
|
|
contentBase: 1_000_000,
|
|
contentType: 'live',
|
|
playlistId: 'playlist-1',
|
|
titleLabel: 'Live',
|
|
});
|
|
seedPartition(database, {
|
|
categoryId: 92_101,
|
|
categoryName: 'Performance VOD 01',
|
|
categoryType: 'movies',
|
|
contentBase: 2_000_000,
|
|
contentType: 'movie',
|
|
playlistId: 'playlist-1',
|
|
titleLabel: 'Movie',
|
|
});
|
|
seedPartition(database, {
|
|
categoryId: 93_101,
|
|
categoryName: 'Performance Series 01',
|
|
categoryType: 'series',
|
|
contentBase: 3_000_000,
|
|
contentType: 'series',
|
|
playlistId: 'playlist-1',
|
|
titleLabel: 'Series',
|
|
});
|
|
seedStatuses(database, 'playlist-1', 'completed');
|
|
const evaluator = evaluatorFor(database, 2_001);
|
|
|
|
const snapshot = await readXtreamCatalogDatabaseSnapshot(evaluator, {
|
|
captureStoppedEpochMs: 2_000,
|
|
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
|
playlistId: 'playlist-1',
|
|
requireFromPath: '/workspace/dist/apps/electron-backend/main.js',
|
|
});
|
|
|
|
assert.deepEqual(snapshot, {
|
|
captureStoppedEpochMs: 2_000,
|
|
categories: {
|
|
live: categoryPartition(1, 91_101),
|
|
movies: categoryPartition(1, 92_101),
|
|
series: categoryPartition(1, 93_101),
|
|
},
|
|
categoryCount: 3,
|
|
content: {
|
|
live: contentPartition(1_000, 1_000_000),
|
|
movie: contentPartition(1_000, 2_000_000),
|
|
series: contentPartition(1_000, 3_000_000),
|
|
},
|
|
contentCount: 3_000,
|
|
databaseReadStartedEpochMs: 2_001,
|
|
importStatuses: {
|
|
live: 'completed',
|
|
movie: 'completed',
|
|
series: 'completed',
|
|
},
|
|
matchingPlaylistCount: 1,
|
|
matchingPlaylistType: 'xtream',
|
|
playlistCount: 1,
|
|
});
|
|
database.close();
|
|
});
|
|
|
|
it('reports malformed fixture rows and incomplete category cardinality', async () => {
|
|
const database = createDatabase();
|
|
seedPlaylist(database, 'playlist-1');
|
|
database.exec(`
|
|
INSERT INTO categories (playlist_id, name, type, xtream_id)
|
|
VALUES ('playlist-1', 'wrong category', 'live', 91101);
|
|
INSERT INTO content (category_id, title, xtream_id, type)
|
|
VALUES (last_insert_rowid(), 'wrong title', 1000000, 'live');
|
|
`);
|
|
seedStatuses(database, 'playlist-1', 'cancelled');
|
|
|
|
const snapshot = await readXtreamCatalogDatabaseSnapshot(
|
|
evaluatorFor(database, 3_001),
|
|
{
|
|
captureStoppedEpochMs: 3_000,
|
|
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
|
playlistId: 'playlist-1',
|
|
requireFromPath:
|
|
'/workspace/dist/apps/electron-backend/main.js',
|
|
}
|
|
);
|
|
|
|
assert.equal(snapshot.categories.live.invalidNameCount, 1);
|
|
assert.equal(snapshot.content.live.invalidTitleCount, 1);
|
|
assert.equal(snapshot.content.live.invalidCategoryCardinalityCount, 1);
|
|
assert.deepEqual(snapshot.importStatuses, {
|
|
live: 'cancelled',
|
|
movie: 'cancelled',
|
|
series: 'cancelled',
|
|
});
|
|
database.close();
|
|
});
|
|
|
|
it('returns an empty deleted state without inventing a playlist type', async () => {
|
|
const database = createDatabase();
|
|
seedStatuses(database, 'playlist-1', 'completed');
|
|
|
|
const snapshot = await readXtreamCatalogDatabaseSnapshot(
|
|
evaluatorFor(database, 4_001),
|
|
{
|
|
captureStoppedEpochMs: 4_000,
|
|
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
|
playlistId: 'playlist-1',
|
|
requireFromPath:
|
|
'/workspace/dist/apps/electron-backend/main.js',
|
|
}
|
|
);
|
|
|
|
assert.equal(snapshot.categoryCount, 0);
|
|
assert.equal(snapshot.contentCount, 0);
|
|
assert.equal(snapshot.matchingPlaylistCount, 0);
|
|
assert.equal(snapshot.matchingPlaylistType, null);
|
|
assert.equal(snapshot.playlistCount, 0);
|
|
database.close();
|
|
});
|
|
|
|
it('fails closed for pre-capture reads and malformed main-process results', async () => {
|
|
const database = createDatabase();
|
|
const base = {
|
|
captureStoppedEpochMs: 5_000,
|
|
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
|
playlistId: 'playlist-1',
|
|
requireFromPath: '/workspace/dist/apps/electron-backend/main.js',
|
|
};
|
|
|
|
await assert.rejects(
|
|
readXtreamCatalogDatabaseSnapshot(
|
|
evaluatorFor(database, 4_999),
|
|
base
|
|
),
|
|
/invalid Xtream catalog database read/
|
|
);
|
|
await assert.rejects(
|
|
readXtreamCatalogDatabaseSnapshot(
|
|
{
|
|
evaluate: async () => ({
|
|
databaseReadStartedEpochMs: 5_001,
|
|
rows: [],
|
|
}),
|
|
},
|
|
base
|
|
),
|
|
/invalid Xtream catalog database read/
|
|
);
|
|
await assert.rejects(
|
|
readXtreamCatalogDatabaseSnapshot(evaluatorFor(database, 5_001), {
|
|
...base,
|
|
dataDirectory: 'relative',
|
|
}),
|
|
/invalid Xtream catalog database read/
|
|
);
|
|
database.close();
|
|
});
|
|
|
|
it('accepts high-resolution fractional capture and database epochs', async () => {
|
|
const database = createDatabase();
|
|
|
|
const snapshot = await readXtreamCatalogDatabaseSnapshot(
|
|
evaluatorFor(database, 6_000.75),
|
|
{
|
|
captureStoppedEpochMs: 6_000.5,
|
|
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
|
playlistId: 'playlist-1',
|
|
requireFromPath:
|
|
'/workspace/dist/apps/electron-backend/main.js',
|
|
}
|
|
);
|
|
|
|
assert.equal(snapshot.captureStoppedEpochMs, 6_000.5);
|
|
assert.equal(snapshot.databaseReadStartedEpochMs, 6_000.75);
|
|
database.close();
|
|
});
|
|
|
|
it('uses the high-resolution epoch clock inside the main-process read boundary', () => {
|
|
const source = readFileSync(
|
|
resolve(
|
|
process.cwd(),
|
|
'src/performance/xtream-catalog-database-reader.ts'
|
|
),
|
|
'utf8'
|
|
);
|
|
|
|
assert.match(source, /getBuiltinModule\(\s*'node:perf_hooks'\s*\)/u);
|
|
assert.match(
|
|
source,
|
|
/perfHooks\.performance\.timeOrigin\s*\+\s*perfHooks\.performance\.now\(\)/u
|
|
);
|
|
assert.doesNotMatch(source, /Date\.now\(\)/u);
|
|
});
|
|
});
|
|
|
|
interface QueryDescriptor {
|
|
readonly parameters: readonly (number | string)[];
|
|
readonly sql: string;
|
|
}
|
|
|
|
interface EvaluationInput {
|
|
readonly queries: readonly QueryDescriptor[];
|
|
}
|
|
|
|
function evaluatorFor(
|
|
database: TestDatabase,
|
|
databaseReadStartedEpochMs: number
|
|
): XtreamCatalogDatabaseEvaluator {
|
|
return {
|
|
evaluate: async (_pageFunction: unknown, value: unknown) => {
|
|
const input = value as unknown as EvaluationInput;
|
|
return {
|
|
databaseReadStartedEpochMs,
|
|
rows: input.queries.map((query) =>
|
|
database.prepare(query.sql).get(...query.parameters)
|
|
),
|
|
};
|
|
},
|
|
} as unknown as XtreamCatalogDatabaseEvaluator;
|
|
}
|
|
|
|
function createDatabase(): TestDatabase {
|
|
const database = new DatabaseSync(':memory:');
|
|
database.exec(`
|
|
CREATE TABLE playlists (id TEXT PRIMARY KEY, type TEXT NOT NULL);
|
|
CREATE TABLE app_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
CREATE TABLE categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
playlist_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
xtream_id INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE content (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
category_id INTEGER NOT NULL,
|
|
title TEXT NOT NULL,
|
|
xtream_id INTEGER NOT NULL,
|
|
type TEXT NOT NULL
|
|
);
|
|
`);
|
|
return database;
|
|
}
|
|
|
|
function seedPlaylist(database: TestDatabase, playlistId: string): void {
|
|
database
|
|
.prepare('INSERT INTO playlists (id, type) VALUES (?, ?)')
|
|
.run(playlistId, 'xtream');
|
|
}
|
|
|
|
function seedStatuses(
|
|
database: TestDatabase,
|
|
playlistId: string,
|
|
status: string
|
|
): void {
|
|
const insert = database.prepare(
|
|
'INSERT INTO app_state (key, value) VALUES (?, ?)'
|
|
);
|
|
for (const type of ['live', 'movie', 'series']) {
|
|
insert.run(`xtream-import-status:${playlistId}:${type}`, status);
|
|
}
|
|
}
|
|
|
|
function seedPartition(
|
|
database: TestDatabase,
|
|
options: {
|
|
readonly categoryId: number;
|
|
readonly categoryName: string;
|
|
readonly categoryType: string;
|
|
readonly contentBase: number;
|
|
readonly contentType: string;
|
|
readonly playlistId: string;
|
|
readonly titleLabel: string;
|
|
}
|
|
): void {
|
|
database
|
|
.prepare(
|
|
`INSERT INTO categories (playlist_id, name, type, xtream_id)
|
|
VALUES (?, ?, ?, ?)`
|
|
)
|
|
.run(
|
|
options.playlistId,
|
|
options.categoryName,
|
|
options.categoryType,
|
|
options.categoryId
|
|
);
|
|
const categoryRow = database
|
|
.prepare('SELECT id FROM categories WHERE playlist_id = ? AND type = ?')
|
|
.get(options.playlistId, options.categoryType) as { id: number };
|
|
const insert = database.prepare(
|
|
`INSERT INTO content (category_id, title, xtream_id, type)
|
|
VALUES (?, ?, ?, ?)`
|
|
);
|
|
for (let index = 0; index < 1_000; index += 1) {
|
|
insert.run(
|
|
categoryRow.id,
|
|
`Performance ${options.titleLabel} ${String(index + 1).padStart(
|
|
6,
|
|
'0'
|
|
)}`,
|
|
options.contentBase + index,
|
|
options.contentType
|
|
);
|
|
}
|
|
}
|
|
|
|
function categoryPartition(count: number, id: number) {
|
|
return {
|
|
count,
|
|
distinctXtreamIdCount: count,
|
|
invalidNameCount: 0,
|
|
maxXtreamId: id,
|
|
minXtreamId: id,
|
|
};
|
|
}
|
|
|
|
function contentPartition(count: number, id: number) {
|
|
return {
|
|
count,
|
|
distinctXtreamIdCount: count,
|
|
invalidCategoryCardinalityCount: 0,
|
|
invalidTitleCount: 0,
|
|
maxXtreamId: id + count - 1,
|
|
minXtreamId: id,
|
|
};
|
|
}
|