mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
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
This commit is contained in:
1 parent
5e725a06f3
commit
a2fafcfc08
256 files changed
+43760
-883
No files matched your search
@@ -0,0 +1,365 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user