108 lines
6.7 KiB
JavaScript
108 lines
6.7 KiB
JavaScript
'use strict';
|
|
// Diagnostics only: bounded records, aggregated hot paths, no resource payloads or player identifiers.
|
|
function create(options = {}) {
|
|
const now = options.now || (() => Date.now()), output = options.output || (line => console.log(line));
|
|
const origin = now(), records = [], pending = new Map();
|
|
let enabled = !!options.enabled, sequence = 0, session = 0, phase = 'boot', metrics = {}, active = true;
|
|
const imageDetails = new Set(['images.frame', 'images.asset', 'images.engineLoad', 'images.createFrame', 'images.reuse', 'images.download']);
|
|
let imageSamples = { assets: [], downloads: [], failures: [] };
|
|
let sceneSamples = {};
|
|
let timer = null, lastTick = now(), lastFlush = now();
|
|
const round = n => Math.round(n * 10) / 10;
|
|
function emit(event, fields = {}) {
|
|
if (!enabled) return;
|
|
try {
|
|
const record = Object.assign({ event, tMs: round(now() - origin), session, phase }, fields);
|
|
const line = '[LevelPerf] ' + JSON.stringify(record);
|
|
records.push(line); if (records.length > 400) records.shift();
|
|
output(line);
|
|
} catch (_) { /* Diagnostics must never affect loading. */ }
|
|
}
|
|
function metric(name, ms, bytes = 0, failed = false) {
|
|
if (!enabled) return;
|
|
const m = metrics[name] || (metrics[name] = { count: 0, totalMs: 0, maxMs: 0, bytes: 0, errors: 0 });
|
|
m.count++; m.totalMs += ms; m.maxMs = Math.max(m.maxMs, ms); m.bytes += bytes || 0; if (failed) m.errors++;
|
|
}
|
|
function begin(stage, fields = {}) {
|
|
if (!enabled) return () => {};
|
|
const id = ++sequence, start = now(), startSession = session, sceneDetail = stage.startsWith('scene.resource.'), compact = imageDetails.has(stage) || sceneDetail;
|
|
pending.set(id, { stage, start, session: startSession, detail: sceneDetail ? fields : undefined });
|
|
if (!compact) emit('begin', Object.assign({ spanId: id, stage }, fields));
|
|
let ended = false;
|
|
return (result = {}) => {
|
|
if (ended) return; ended = true; pending.delete(id);
|
|
const ms = round(now() - start);
|
|
if (compact) {
|
|
if (!enabled) return;
|
|
const detail = Object.assign({ stage, session: startSession, ms }, fields, result), failed = result.status === 'error';
|
|
metric(stage, ms, 0, failed);
|
|
if (sceneDetail) {
|
|
const list = sceneSamples[stage] || (sceneSamples[stage] = []);
|
|
list.push(detail); list.sort((a, b) => b.ms - a.ms); if (list.length > 3) list.length = 3;
|
|
return;
|
|
}
|
|
if (stage === 'images.asset' || stage === 'images.download') {
|
|
const names = stage === 'images.asset' ? ['lookupMs', 'cachePrepareMs', 'engineLoadMs', 'validateMs'] : ['queueMs', 'networkMs', 'installMs'];
|
|
for (const name of names) if (typeof detail[name] === 'number') metric(stage + '.' + name, detail[name]);
|
|
if (stage === 'images.asset') {
|
|
const source = { '本地缓存': 'local', '临时缓存': 'temporary', '远程下载': 'remote', '内存复用': 'memory', '项目Bundle': 'bundle' }[detail.source] || 'other';
|
|
metric(stage + '.source.' + source, ms, 0, failed);
|
|
}
|
|
// Bounded samples, even if hundreds of downloads fail in the same window.
|
|
const top = list => { list.push(detail); list.sort((a, b) => b.ms - a.ms); if (list.length > 3) list.length = 3; };
|
|
top(stage === 'images.asset' ? imageSamples.assets : imageSamples.downloads);
|
|
if (failed) top(imageSamples.failures);
|
|
}
|
|
return;
|
|
}
|
|
emit('end', Object.assign({ spanId: id, stage, session: startSession, ms }, stage.startsWith('images.') ? fields : {}, result));
|
|
};
|
|
}
|
|
function measure(name, task, bytes = 0) {
|
|
if (!enabled) return task();
|
|
const start = now(); let failed = false;
|
|
try { return task(); } catch (e) { failed = true; throw e; }
|
|
finally { metric(name, now() - start, bytes, failed); }
|
|
}
|
|
function measureAsync(name, task, bytes = 0) {
|
|
if (!enabled) return task();
|
|
const start = now();
|
|
try { return task().then(value => { metric(name, now() - start, bytes); return value; },
|
|
error => { metric(name, now() - start, bytes, true); throw error; }); }
|
|
catch (error) { metric(name, now() - start, bytes, true); throw error; }
|
|
}
|
|
function flush() {
|
|
if (!enabled) return;
|
|
const at = now(), values = metrics, samples = imageSamples, scene = sceneSamples; metrics = {}; sceneSamples = {};
|
|
imageSamples = { assets: [], downloads: [], failures: [] };
|
|
for (const key of Object.keys(values)) { values[key].totalMs = round(values[key].totalMs); values[key].maxMs = round(values[key].maxMs); }
|
|
if (Object.keys(values).length || pending.size) emit('summary', { windowMs: round(at - lastFlush), metrics: values,
|
|
images: samples.assets.length || samples.downloads.length ? samples : undefined,
|
|
scene: Object.keys(scene).length ? scene : undefined,
|
|
pendingCount: pending.size, pending: Array.from(pending, ([id, s]) => Object.assign({ id, stage: s.stage, session: s.session, elapsedMs: round(at - s.start) }, s.detail)).slice(0, 16) });
|
|
lastFlush = at;
|
|
}
|
|
function tick() {
|
|
const at = now(), delay = at - lastTick - 250; lastTick = at;
|
|
if (!enabled || !active) return;
|
|
if (delay >= 100) emit('eventLoopLag', { lagMs: round(delay), pending: Array.from(pending.values(), s => s.stage).slice(0, 12) });
|
|
if (at - lastFlush >= 2000) flush();
|
|
}
|
|
function watch() {
|
|
if (timer) { clearInterval(timer); timer = null; }
|
|
lastTick = now(); lastFlush = now();
|
|
if (enabled && options.watch) { timer = setInterval(tick, 250); if (timer.unref) timer.unref(); }
|
|
}
|
|
const api = { begin, emit, metric, measure, measureAsync, flush, tick,
|
|
enabled: () => enabled,
|
|
setEnabled(value) { if (!value) flush(); enabled = !!value; if (!enabled) { pending.clear(); metrics = {}; sceneSamples = {}; imageSamples = { assets: [], downloads: [], failures: [] }; } watch(); emit('enabled'); },
|
|
session(fields) { flush(); session++; phase = 'login'; emit('login', fields); },
|
|
phase(value, fields) { phase = value; emit('phase', fields); },
|
|
setActive(value) { active = !!value; lastTick = now(); emit('active', { active }); },
|
|
dump() { flush(); return records.join('\n'); },
|
|
dispose() { if (timer) clearInterval(timer); timer = null; }
|
|
};
|
|
watch(); emit('enabled'); return api;
|
|
}
|
|
module.exports = { create };
|