/** * benchmark /convert endpoint performance * * usage: * node scripts/bench-convert.js [URL] * * envs: * BENCH_HOST: target server * BENCH_URL: fallback URL * BENCH_FORMAT: mp4|m4a|mp3 * BENCH_TIMEOUT_MS: request timeout */ import { request as httpRequest } from 'node:http'; import { URL } from 'node:url'; const MB = 1024 * 1024; const HOST = process.env.BENCH_HOST || 'http://localhost:5000'; const URL_ARG = process.argv[2] || process.env.BENCH_URL || 'https://www.youtube.com/watch?v=jNQXAC9IVRw'; const FORMAT = process.env.BENCH_FORMAT || 'mp4'; const TIMEOUT_MS = parseInt(process.env.BENCH_TIMEOUT_MS || '180000', 10); const CLIENT_ID = `bench_${Date.now().toString(36)}`; async function resolveFormats(target, urlArg, clientId) { const infoUrl = `${target.origin}/info?url=${encodeURIComponent(urlArg)}&id=${clientId}`; const startedAt = Date.now(); const maxWaitMs = 30_000; let attempt = 0; let lastBody = null; while (Date.now() - startedAt < maxWaitMs) { attempt += 1; const callStart = Date.now(); const info = await fetchJson(infoUrl); console.log( `[bench] /info attempt ${attempt} status=${info.status} (${Date.now() - callStart}ms)` ); if (info.status !== 200) { throw new Error( `/info failed: status=${info.status} body=${JSON.stringify(info.body).slice(0, 200)}` ); } lastBody = info.body; const audioFormats = info.body.audioFormats || []; const videoFormats = info.body.formats || []; const totalFormats = audioFormats.length + videoFormats.length; if (totalFormats > 0) { console.log( `[bench] formats ready after ${Date.now() - startedAt}ms (audio=${audioFormats.length}, video=${videoFormats.length}, isPartial=${Boolean(info.body.isPartial)})` ); return info.body; } console.log( `[bench] partial: isPartial=${Boolean(info.body.isPartial)}, retrying in 500ms` ); await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error( `timed out waiting for formats after ${maxWaitMs}ms; last=${JSON.stringify(lastBody).slice(0, 200)}` ); } function fetchJson(url) { return new Promise((resolve, reject) => { const req = httpRequest(url, { method: 'GET' }, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString('utf8')), }); } catch (err) { reject(err); } }); }); req.on('error', reject); req.setTimeout(TIMEOUT_MS, () => req.destroy(new Error('info timeout'))); req.end(); }); } function streamDownload(url) { return new Promise((resolve, reject) => { const t0 = Date.now(); let firstByteAt = 0; let lastByteAt = 0; let totalBytes = 0; const req = httpRequest(url, { method: 'GET' }, (res) => { if (res.statusCode !== 200) { reject(new Error(`status ${res.statusCode}`)); return; } console.log( `[bench] response: ${res.statusCode} ${res.headers['content-type']}` ); console.log( `[bench] disposition: ${res.headers['content-disposition'] || '(none)'}` ); res.on('data', (chunk) => { if (firstByteAt === 0) firstByteAt = Date.now(); lastByteAt = Date.now(); totalBytes += chunk.length; if (totalBytes % (4 * MB) < chunk.length) { process.stdout.write( `[bench] progress: ${(totalBytes / MB).toFixed(1)} MB\r` ); } }); res.on('end', () => { const totalMs = Math.max(1, Date.now() - t0); const ttfbMs = firstByteAt > 0 ? firstByteAt - t0 : -1; const streamingMs = Math.max(1, lastByteAt - firstByteAt); const wallMbps = totalBytes / MB / (totalMs / 1000); const streamMbps = totalBytes / MB / (streamingMs / 1000); process.stdout.write('\n'); resolve({ totalBytes, totalMs, ttfbMs, streamingMs, wallMbps, streamMbps, }); }); res.on('error', reject); }); req.on('error', reject); req.setTimeout(TIMEOUT_MS, () => req.destroy(new Error('download timeout')) ); req.end(); }); } async function main() { const target = new URL(HOST); console.log(`[bench] host: ${target.origin}`); console.log(`[bench] url: ${URL_ARG}`); console.log(`[bench] format: ${FORMAT}`); console.log('[bench] resolving /info (will await prefetch if partial)...'); const body = await resolveFormats(target, URL_ARG, CLIENT_ID); const audioFormats = body.audioFormats || []; const videoFormats = body.formats || []; const isAudioRequest = ['m4a', 'mp3', 'audio'].includes(FORMAT); let pick = process.env.BENCH_FORMAT_ID; if (!pick) { if (isAudioRequest) { pick = audioFormats[0]?.formatId || videoFormats[0]?.formatId; } else { // prefer 720p mp4 video const candidates = videoFormats.filter( (fmt) => fmt.height && fmt.height <= 1080 && fmt.height >= 360 ); pick = candidates.find((fmt) => fmt.height === 720)?.formatId || candidates[0]?.formatId || videoFormats[0]?.formatId || audioFormats[0]?.formatId; } } if (!pick) { throw new Error( `no format available even after wait (audio=${audioFormats.length}, video=${videoFormats.length})` ); } console.log(`[bench] selected formatId=${pick}`); const params = new URLSearchParams({ url: URL_ARG, format: FORMAT, formatId: String(pick), id: CLIENT_ID, token: CLIENT_ID, title: body.title || 'Bench', artist: body.uploader || 'Bench', }); const convertUrl = `${target.origin}/convert?${params.toString()}`; console.log('[bench] hitting /convert...'); const result = await streamDownload(convertUrl); console.log(''); const sizeMB = result.totalBytes / MB; console.log(`[bench][/convert] size=${sizeMB.toFixed(2)}MB`); console.log(`[bench][/convert] total=${result.totalMs}ms`); console.log(`[bench][/convert] TTFB=${result.ttfbMs}ms`); console.log(`[bench][/convert] streamingOnly=${result.streamingMs}ms`); console.log(''); console.log( `[bench][calc] wall = ${sizeMB.toFixed(2)}MB / ${(result.totalMs / 1000).toFixed(2)}s = ${result.wallMbps.toFixed(2)} MB/s (${(result.wallMbps * 8).toFixed(1)} Mbps)` ); console.log( `[bench][calc] stream= ${sizeMB.toFixed(2)}MB / ${(result.streamingMs / 1000).toFixed(2)}s = ${result.streamMbps.toFixed(2)} MB/s (${(result.streamMbps * 8).toFixed(1)} Mbps)` ); } main().catch((err) => { console.error('[bench] failed:', err.message); process.exitCode = 1; });