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(); } });