const test = require('node:test'), assert = require('node:assert/strict'); const { create } = require('../packages/level-resources/perf'); test('diagnostics preserve values and errors, aggregate hot paths and bound retained output', async () => { let time = 0; const lines = []; const perf = create({ enabled: true, now: () => time, output: line => lines.push(JSON.parse(line.slice('[LevelPerf] '.length))) }); perf.session({ version: 'test' }); const end = perf.begin('level.load', { id: 222 }); const sentinel = Error('sentinel'); for (let i = 0; i < 2000; i++) assert.equal(perf.measure('hash', () => { time += 2; return 7; }, 100), 7); assert.throws(() => perf.measure('failed', () => { throw sentinel; }), e => e === sentinel); await assert.rejects(perf.measureAsync('async.failed', () => Promise.reject(sentinel)), e => e === sentinel); assert.equal(await perf.measureAsync('read', async () => 'bytes'), 'bytes'); end({ status: 'ok' }); end(); perf.flush(); assert(lines.length < 10, 'hot paths must not print one log per resource'); const summary = lines.find(l => l.event === 'summary' && l.metrics.hash); assert.equal(summary.metrics.hash.count, 2000); assert.equal(summary.metrics.hash.totalMs, 4000); assert.equal(summary.metrics.hash.maxMs, 2); assert.equal(summary.metrics.hash.bytes, 200000); assert.equal(summary.metrics.failed.errors, 1); assert.equal(lines.filter(l => l.event === 'end').length, 1); assert.equal(lines.find(l => l.event === 'begin').spanId, lines.find(l => l.event === 'end').spanId); for (let i = 0; i < 500; i++) perf.emit('sample'); assert.equal(perf.dump().split('\n').length, 400); perf.setEnabled(false); const count = lines.length; perf.emit('hidden'); perf.measure('hash', () => 1); perf.flush(); assert.equal(lines.length, count); }); test('event-loop lag excludes background time and logging failures do not affect tasks', () => { let time = 0; const lines = []; const perf = create({ enabled: true, now: () => time, output: line => lines.push(line) }); time = 500; perf.tick(); assert.equal(lines.filter(l => l.includes('eventLoopLag')).length, 1); perf.setActive(false); time += 10000; perf.tick(); perf.setActive(true); time += 250; perf.tick(); assert.equal(lines.filter(l => l.includes('eventLoopLag')).length, 1); const broken = create({ enabled: true, output() { throw Error('console unavailable'); } }); assert.equal(broken.measure('ok', () => 42), 42); broken.begin('ok')(); }); test('image details aggregate thousands of loads with bounded slow and failure samples', () => { let time = 0; const lines = []; const perf = create({ enabled: true, now: () => time, output: line => lines.push(JSON.parse(line.slice('[LevelPerf] '.length))) }); const group = perf.begin('images.group', { kind: 'block', count: 1000 }); for (let i = 0; i < 1000; i++) { for (const stage of ['frame', 'engineLoad', 'createFrame', 'reuse']) { const end = perf.begin('images.' + stage, { path: 'image-' + i }); time++; end({ status: 'ok' }); } const end = perf.begin('images.asset', { path: 'image-' + i, kind: 'png' }); time += 10 + i; end({ status: i % 2 ? 'ok' : 'error', source: '本地缓存', lookupMs: 2, cachePrepareMs: 0, engineLoadMs: 8 + i, validateMs: 0 }); end(); const download = perf.begin('images.download', { path: 'image-' + i, queueMs: 5 }); time += 20; download({ status: 'error', networkMs: 20, installMs: 0 }); } group({ status: 'ok' }); perf.flush(); assert.equal(lines.length, 4, 'enabled + group begin/end + one summary, independent of image count'); const summary = lines.find(row => row.event === 'summary'); assert.equal(summary.metrics['images.asset'].count, 1000); assert.equal(summary.metrics['images.asset'].errors, 500); assert.equal(summary.metrics['images.asset.source.local'].count, 1000); assert.equal(summary.metrics['images.asset.lookupMs'].totalMs, 2000); assert.equal(summary.metrics['images.download.queueMs'].totalMs, 5000); assert.equal(summary.metrics['images.download'].errors, 1000); assert.equal(summary.images.assets.length, 3); assert.equal(summary.images.assets[0].path, 'image-999'); assert.equal(summary.images.downloads.length, 3); assert.equal(summary.images.failures.length, 3); assert.equal(lines.find(row => row.event === 'end').kind, 'block'); const count = lines.length; perf.flush(); assert.equal(lines.length, count, 'samples reset after flush'); }); test('silent image stages remain observable while pending and disabled diagnostics stay silent', () => { let time = 0; const lines = []; const perf = create({ enabled: true, now: () => time, output: line => lines.push(JSON.parse(line.slice('[LevelPerf] '.length))) }); const end = perf.begin('images.engineLoad', { path: 'door' }); time = 3000; perf.flush(); const summary = lines.find(row => row.event === 'summary'); assert.equal(summary.pendingCount, 1); assert.equal(summary.pending[0].stage, 'images.engineLoad'); perf.setEnabled(false); const count = lines.length; end({ status: 'error' }); perf.flush(); assert.equal(lines.length, count); }); test('scene resource diagnostics aggregate thousands of dependencies into three slow samples', () => { let time = 0; const rows = []; const perf = create({ enabled: true, now: () => time, output: line => rows.push(JSON.parse(line.slice(12))) }); for (let i = 0; i < 1000; i++) { const end = perf.begin('scene.resource.parse', { resource: 'asset-' + i, requestId: 9 }); time += i; end({ status: 'ok' }); } perf.flush(); assert.equal(rows.length, 2); assert.equal(rows[1].metrics['scene.resource.parse'].count, 1000); assert.equal(rows[1].scene['scene.resource.parse'].length, 3); assert.equal(rows[1].scene['scene.resource.parse'][0].resource, 'asset-999'); perf.flush(); assert.equal(rows.length, 2); });