'use strict'; const test = require('node:test'), assert = require('node:assert/strict'); const fs = require('fs'), os = require('os'), path = require('path'), http = require('http'); const { Tracer, requestTrace } = require('../perf/trace'); const { ReadAhead } = require('../perf/read-ahead'); const { analyse } = require('../tools/perf-report'); const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'circus-perf-')); const lines = dir => fs.readdirSync(dir).filter(n => n.endsWith('.jsonl')) .flatMap(n => fs.readFileSync(path.join(dir, n), 'utf8').trim().split('\n')).filter(Boolean).map(JSON.parse); test('tracer is off without the flag file and writes nothing', () => { const dir = tmp(), tracer = new Tracer({ dir, env: {} }); tracer.check(); tracer.log({ k: 'x' }); tracer.flush(); assert.equal(tracer.on, false); assert.deepEqual(fs.readdirSync(dir), []); }); test('the flag file switches the trace on and off at runtime', () => { const dir = tmp(), tracer = new Tracer({ dir, env: {} }); fs.writeFileSync(path.join(dir, 'trace.on'), ''); tracer.check(); tracer.log({ k: 'x', a: 1 }); fs.rmSync(path.join(dir, 'trace.on')); tracer.check(); tracer.log({ k: 'x', a: 2 }); tracer.flush(); const events = lines(dir); assert.deepEqual(events.map(e => e.k), ['trace', 'x', 'trace']); assert.equal(events[1].a, 1); assert.equal(events[2].on, false); assert.ok(events.every(e => typeof e.t === 'number')); }); test('a served file is traced with storage and network phases', async () => { const dir = tmp(), tracer = new Tracer({ dir, env: { PERF_TRACE: '1' } }); tracer.check(); const file = path.join(dir, 'page.jpg'); fs.writeFileSync(file, Buffer.alloc(3 * 1024 * 1024, 7)); const server = http.createServer((req, res) => { const trace = requestTrace(tracer, req, res); fs.stat(file, (err, st) => { trace.mark('stat'); res.writeHead(200, { 'Content-Length': st.size }); const stream = fs.createReadStream(file); trace.stream(stream); stream.pipe(res); }); }); await new Promise(r => server.listen(0, r)); const { port } = server.address(); const r = await fetch(`http://127.0.0.1:${port}/lib/M/B/C/page.jpg`); assert.equal((await r.arrayBuffer()).byteLength, 3 * 1024 * 1024); await new Promise(r => setTimeout(r, 50)); server.close(); tracer.stopSampler(); tracer.flush(); const req = lines(dir).find(e => e.k === 'req'); assert.equal(req.status, 200); assert.equal(req.bytes, 3 * 1024 * 1024); assert.equal(req.path, '/lib/M/B/C/page.jpg'); for (const field of ['stat', 'open', 'first', 'readEnd', 'disk', 'netWait', 'ms']) assert.equal(typeof req[field], 'number', field); assert.ok(req.ms >= req.readEnd && req.readEnd >= req.first && req.first >= req.open); }); test('read-ahead reads files in order and a newer chapter replaces the queued one', async () => { const dir = tmp(), files = []; for (let i = 0; i < 6; i++) { const f = path.join(dir, `p${i}.jpg`); fs.writeFileSync(f, Buffer.alloc(1000 + i)); files.push(f); } const batches = []; const ahead = new ReadAhead({ concurrency: 1, onBatch: b => batches.push(b) }); const done = new Promise(resolve => { const check = () => (batches.some(b => b.tag === 'B' && !b.replaced) ? resolve() : setTimeout(check, 5)); check(); }); ahead.warm(files.slice(0, 4), 'A'); ahead.warm(files.slice(3), 'B'); await done; assert.equal(batches[0].tag, 'A'); assert.equal(batches[0].replaced, true); const b = batches.find(x => x.tag === 'B'); assert.equal(b.done, 3); assert.equal(b.bytes, 1003 + 1004 + 1005); // Files read recently are skipped the next time. batches.length = 0; ahead.warm(files.slice(4), 'C'); assert.deepEqual(batches.map(x => [x.tag, x.files, x.skipped]), [['C', 0, 2]]); }); test('read-ahead runs the follow-up batch after the current one', async () => { const dir = tmp(), a = path.join(dir, 'a.jpg'), b = path.join(dir, 'b.jpg'); fs.writeFileSync(a, 'a'); fs.writeFileSync(b, 'bb'); const tags = []; const ahead = new ReadAhead({ onBatch: x => tags.push(x.tag) }); ahead.warm([a], 'now'); ahead.then([b], 'next'); await new Promise(r => { const check = () => (tags.length === 2 ? r() : setTimeout(check, 5)); check(); }); assert.deepEqual(tags, ['now', 'next']); }); test('the report blames the storage for slow first reads', () => { const t = Date.parse('2026-10-29T20:00:00Z'); const events = [ { t, k: 'req', m: 'GET', path: '/lib/M/B/C/1.jpg', status: 200, bytes: 300000, stat: 2, open: 3, first: 1900, readEnd: 1950, disk: 1947, netWait: 0, ms: 1955, conn: 1 }, { t: t + 1000, k: 'req', m: 'GET', path: '/lib/M/B/C/2.jpg', status: 200, bytes: 300000, stat: 1, open: 2, first: 5, readEnd: 20, disk: 18, netWait: 0, ms: 22, conn: 2 }, { t: t + 2000, k: 'client', e: 'res', url: '/lib/M/B/C/1.jpg', total: 2100, ttfb: 30, download: 2000, queue: 1, connect: 0, dns: 0 }, { t: t + 3000, k: 'client', e: 'view', wait: 2050, preloaded: false }, { t: t + 4000, k: 'nas', firstMs: 2500, ms: 2600, bytes: 1 }, ]; const o = analyse(events, { slow: 500 }); assert.equal(o.slowServer.count, 1); assert.deepEqual(o.slowServer.by, { firstRead: 1 }); assert.deepEqual(o.slowBrowser.by, { server: 1 }); assert.equal(o.pageTurns.over1s, 1); assert.match(o.verdict.join('\n'), /first read from storage/); });