Files
Nodecast/test/t42LiveListCpuRegression.test.js
blomios 8b1daea23c fix(live): bound live channel listing queries
Optimize the live channel page path so unfiltered catalog pages are counted and paged in SQLite before serialization and enrichment.

Add the T42 CPU regression test covering a 50000-channel projected catalog.

Refs #42

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 17:17:16 +02:00

137 lines
4.8 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { performance } = require('node:perf_hooks');
const Database = require('better-sqlite3');
const liveProjection = require('../server/services/liveProjectionService');
const {
ensureColumns,
LIVE_PROJECTION_COLUMNS,
LIVE_PROJECTION_SCHEMA_SQL
} = require('../server/db/liveProjectionSchema');
function createProjectionDb() {
const db = new Database(':memory:');
db.pragma('journal_mode = MEMORY');
db.pragma('synchronous = OFF');
db.exec(`
CREATE TABLE categories (
id TEXT PRIMARY KEY,
source_id INTEGER NOT NULL,
category_id TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
parent_id TEXT,
is_hidden INTEGER DEFAULT 0,
data JSON
);
CREATE TABLE playlist_items (
id TEXT PRIMARY KEY,
source_id INTEGER NOT NULL,
item_id TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
category_id TEXT,
parent_id TEXT,
stream_icon TEXT,
stream_url TEXT,
container_extension TEXT,
rating REAL,
year TEXT,
added_at TEXT,
is_hidden INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
data JSON
);
CREATE TABLE favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
source_id INTEGER NOT NULL,
item_id TEXT NOT NULL,
item_type TEXT NOT NULL
);
`);
ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS);
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
db.prepare(`
INSERT INTO categories (id, source_id, category_id, type, name)
VALUES ('cat:1', 1, 'general', 'live', 'General')
`).run();
return db;
}
function seedProjectedChannels(db, total) {
const logical = db.prepare(`
INSERT INTO logical_channels
(id, fingerprint, display_name, preferred_variant_id, variant_count, created_at, updated_at)
VALUES (?, ?, ?, ?, 1, 1, 1)
`);
const item = db.prepare(`
INSERT INTO playlist_items
(id, source_id, item_id, type, name, category_id, stream_icon, stream_url, data,
logical_channel_id, grouping_fingerprint, grouping_input_hash, quality_label, quality_rank,
variant_rank, language_code, language_confidence, language_detector, language_input_hash)
VALUES (?, 1, ?, 'live', ?, 'general', '', ?, '{}', ?, ?, ?, 'HD', 2, 0, 'en', 1, 'algo', ?)
`);
const insertAll = db.transaction(() => {
for (let index = 0; index < total; index++) {
const id = `1:${index}`;
const logicalId = `logical:${index}`;
const name = `Channel ${String(index).padStart(6, '0')}`;
logical.run(logicalId, `channel ${index}`, name, id);
item.run(
id,
String(index),
name,
`https://streams.test/${index}.m3u8`,
logicalId,
`channel ${index}`,
`group-hash-${index}`,
`language-hash-${index}`
);
}
});
insertAll();
}
test('/api/live/channels first page stays CPU-bounded for a large projected catalog', () => {
const db = createProjectionDb();
seedProjectedChannels(db, 50000);
try {
const cpuBefore = process.cpuUsage();
const started = performance.now();
const result = liveProjection.getChannelPage({
includeHidden: false,
userId: 7,
page: 1,
limit: 100
}, db, {
iptvOrgCatalog: { findMatch: () => null }
});
const elapsedMs = performance.now() - started;
const cpu = process.cpuUsage(cpuBefore);
const metrics = {
rows: 50000,
returnedChannels: result.channels.length,
totalChannels: result.page.total,
elapsedMs: Math.round(elapsedMs),
cpuUserMs: Math.round(cpu.user / 1000),
cpuSystemMs: Math.round(cpu.system / 1000),
payloadBytes: Buffer.byteLength(JSON.stringify({
filters: { availableLanguages: result.availableLanguages },
channels: result.channels,
page: result.page
}))
};
console.log('[T42 live list metrics]', JSON.stringify(metrics));
assert.equal(result.channels.length, 100);
assert.ok(
metrics.elapsedMs <= 500 && metrics.cpuUserMs <= 500,
`first live page should be bounded; measured ${JSON.stringify(metrics)}`
);
} finally {
db.close();
}
});