api-server / tests /stress /analyze-hf-logs.js
XWX-AI's picture
fix(widget): DOCX图表fallback成数据表格根因修复 — widget渲染浏览器池+CDP有限超时+渲染硬超时+队列超时取消+BrowserPool补位bug+body-parser JSON兜底, bump v2.1.11
24a2ddf
Raw
History Blame Contribute Delete
9.86 kB
/**
* Analyze Hugging Face Space run logs for PDF/Widget export usage statistics.
*
* Parses the SSE stream returned by:
* curl -s -H "Authorization: Bearer $HF_TOKEN" \
* "https://huggingface.co/api/spaces/XWX-AI/api-server/logs/run"
*
* Extracts per-request telemetry (platform / version / language / export counts /
* message count / image count), per-request latency, and setContent timing.
* Writes a UTF-8 markdown report to tests/stress/results/.
*
* Usage:
* node tests/stress/analyze-hf-logs.js <raw-sse-file> [--out report.md]
*/
const fs = require('fs');
const path = require('path');
const parseArgs = (argv) => {
const args = { _: [] };
for (let i = 0; i < argv.length; i++) {
const m = argv[i].match(/^--([^=]+)(?:=(.*))?$/);
if (!m) { args._.push(argv[i]); continue; }
if (m[2] !== undefined) {
args[m[1]] = m[2];
} else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
args[m[1]] = argv[++i];
} else {
args[m[1]] = true;
}
}
return args;
};
// ─── log line helpers (all server log lines are Chinese; match by unique ASCII anchors) ───
const RE_TELEMETRY = /收到请求 \| 平台: (\S+) \| 版本: (\S+) \| 语言: (\S+)/;
const RE_EXPORT_SUMMARY = /导出: (\d+)次 \| 格式: PDF:(\d+), MD:(\d+), TXT:(\d+), DOCX:(\d+), JSON:(\d+), CLIP:(\d+), NOTION:(\d+) \| 消息: (\d+)条 \| 图片: (\d+)张/;
const RE_DONE = />>> 任务全部完成 <<</;
const RE_CONTENT_LOADED = /页面内容加载完成/;
const RE_PDF_START = /正在生成 PDF 二进制流/;
const RE_HTML_PARSE = /解析请求完成: HTML ([\d.]+) MB/;
const RE_LAUNCH_READY = /浏览器启动成功/;
const RE_FILLING = /正在填充页面内容|正在通过临时文件加载页面内容/;
const RE_RENDER_DONE = /render_charts DONE: (\d+) OK, (\d+) FAIL, ([\d.]+)s/;
const RE_BROWSER_LAUNCH = /正在从浏览器池获取浏览器|正在启动浏览器/;
function hourOf(ts) {
const d = new Date(ts);
return d.getUTCHours();
}
function main() {
const args = parseArgs(process.argv.slice(2));
const inFile = args._ ? args._[0] : null;
if (!inFile) {
console.error('Usage: node tests/stress/analyze-hf-logs.js <sse-log-file> [--out report.md]');
process.exit(1);
}
const raw = fs.readFileSync(inFile, 'utf8');
const requests = []; // one entry per completed PDF-GEN request
let pending = null;
let renderCalls = [];
for (const line of raw.split('\n')) {
if (!line.startsWith('data: ')) continue;
let obj;
try { obj = JSON.parse(line.slice(6)); } catch (e) { continue; }
const msg = obj.data;
if (!msg) continue;
const t = obj.timestamp;
const tele = msg.match(RE_TELEMETRY);
if (tele) {
pending = {
ts: t,
platform: tele[1],
version: tele[2],
language: tele[3],
exports: null, htmlMB: null, latencyMs: null, setContentMs: null, pdfMs: null, launchMs: null,
};
continue;
}
const sum = msg.match(RE_EXPORT_SUMMARY);
if (sum && pending) {
pending.exports = {
total: +sum[1], pdf: +sum[2], md: +sum[3], txt: +sum[4], docx: +sum[5], json: +sum[6], clip: +sum[7], notion: +sum[8],
messages: +sum[9], images: +sum[10],
};
continue;
}
const hp = msg.match(RE_HTML_PARSE);
if (hp && pending) {
pending.htmlMB = parseFloat(hp[1]);
continue;
}
if (RE_BROWSER_LAUNCH.test(msg) && pending && pending.launchMs === null) {
pending._launchT = new Date(t).getTime();
continue;
}
if (RE_LAUNCH_READY.test(msg) && pending && pending._launchT) {
pending.launchMs = new Date(t).getTime() - pending._launchT;
continue;
}
if (RE_FILLING.test(msg) && pending && pending._fillT === undefined) {
pending._fillT = new Date(t).getTime();
continue;
}
if (RE_CONTENT_LOADED.test(msg) && pending && pending._fillT !== undefined && pending.setContentMs === null) {
// setContent 阶段 = 开始填充页面内容 → 页面内容加载完成(含 networkidle 等待)
pending.setContentMs = new Date(t).getTime() - pending._fillT;
continue;
}
if (RE_PDF_START.test(msg) && pending && pending.pdfMs === null) {
pending._pdfStart = new Date(t).getTime();
continue;
}
if (RE_DONE.test(msg) && pending) {
pending.latencyMs = new Date(t).getTime() - new Date(pending.ts).getTime();
if (pending._pdfStart) pending.pdfMs = new Date(t).getTime() - pending._pdfStart;
requests.push(pending);
pending = null;
continue;
}
const rd = msg.match(RE_RENDER_DONE);
if (rd) {
renderCalls.push({ ts: t, ok: +rd[1], fail: +rd[2], sec: parseFloat(rd[3]) });
}
}
// ─── aggregate ───
const total = requests.length;
const byPlatform = {};
const byVersion = {};
const byLanguage = {};
const byHour = {};
const pdfExports = { total: 0 };
for (const r of requests) {
byPlatform[r.platform] = (byPlatform[r.platform] || 0) + 1;
byVersion[r.version] = (byVersion[r.version] || 0) + 1;
byLanguage[r.language] = (byLanguage[r.language] || 0) + 1;
const h = hourOf(r.ts);
byHour[h] = (byHour[h] || 0) + 1;
if (r.exports) {
for (const k of ['pdf', 'md', 'txt', 'docx', 'json', 'clip', 'notion']) {
pdfExports[k] = (pdfExports[k] || 0) + r.exports[k];
}
pdfExports.total += r.exports.total;
}
}
const lat = requests.map((r) => r.latencyMs).filter((v) => v != null).sort((a, b) => a - b);
const sc = requests.map((r) => r.setContentMs).filter((v) => v != null).sort((a, b) => a - b);
const pm = requests.map((r) => r.pdfMs).filter((v) => v != null).sort((a, b) => a - b);
const lm = requests.map((r) => r.launchMs).filter((v) => v != null).sort((a, b) => a - b);
const pct = (arr, p) => arr.length ? arr[Math.min(arr.length - 1, Math.ceil((p / 100) * arr.length) - 1)] : 0;
const avg = (arr) => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0;
const ts = requests.map((r) => new Date(r.ts).getTime()).sort((a, b) => a - b);
const spanH = ts.length >= 2 ? ((ts[ts.length - 1] - ts[0]) / 3600000) : 0;
// ─── report ───
const lines = [];
lines.push('# Hugging Face 生产日志分析');
lines.push('');
lines.push(`分析时间: ${new Date().toISOString()}`);
lines.push(`日志文件: ${path.basename(inFile)}`);
lines.push('');
lines.push('## 总览');
lines.push('');
lines.push(`| 指标 | 值 |`);
lines.push(`|------|-----|`);
lines.push(`| 完整 PDF 请求数 | ${total} |`);
lines.push(`| 时间跨度 | ${spanH.toFixed(1)} 小时 |`);
lines.push(`| 平均每请求延迟 | ${avg(lat)} ms |`);
lines.push(`| 延迟 p50 / p95 | ${pct(lat, 50)} / ${pct(lat, 95)} ms |`);
lines.push(`| setContent 平均 | ${avg(sc)} ms(${sc.length} 个样本) |`);
lines.push(`| page.pdf() 平均 | ${avg(pm)} ms(${pm.length} 个样本) |`);
lines.push(`| 浏览器启动平均 | ${avg(lm)} ms(${lm.length} 个样本) |`);
lines.push(`| 累计导出格式 | ${Object.entries(pdfExports).map(([k, v]) => `${k}:${v}`).join(',')} |`);
lines.push(`| render_charts 调用 | ${renderCalls.length} 次,成功 ${renderCalls.reduce((a, c) => a + c.ok, 0)},失败 ${renderCalls.reduce((a, c) => a + c.fail, 0)} |`);
lines.push('');
lines.push('## 平台分布');
lines.push('');
lines.push('| 平台 | 请求数 |');
lines.push('|------|--------|');
for (const [k, v] of Object.entries(byPlatform).sort((a, b) => b[1] - a[1])) lines.push(`| ${k} | ${v} |`);
lines.push('');
lines.push('## 插件版本分布');
lines.push('');
lines.push('| 版本 | 请求数 |');
lines.push('|------|--------|');
for (const [k, v] of Object.entries(byVersion).sort((a, b) => b[1] - a[1])) lines.push(`| ${k} | ${v} |`);
lines.push('');
lines.push('## 语言分布');
lines.push('');
lines.push('| 语言 | 请求数 |');
lines.push('|------|--------|');
for (const [k, v] of Object.entries(byLanguage).sort((a, b) => b[1] - a[1])) lines.push(`| ${k} | ${v} |`);
lines.push('');
lines.push('## 小时分布 (UTC)');
lines.push('');
lines.push('| 小时 | 请求数 |');
lines.push('|------|--------|');
for (let h = 0; h < 24; h++) if (byHour[h]) lines.push(`| ${h} | ${byHour[h]} |`);
lines.push('');
lines.push('## 明细(最近 40 条)');
lines.push('');
lines.push('| 时间 (UTC) | 平台 | 版本 | 语言 | 延迟 | setContent | PDF | HTML MB | 格式 |');
lines.push('|-----------|------|------|------|------|-----------|-----|---------|------|');
const sorted = [...requests].sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, 40);
for (const r of sorted) {
const e = r.exports;
lines.push(`| ${r.ts} | ${r.platform} | ${r.version} | ${r.language} | ${r.latencyMs ?? '-'}ms | ${r.setContentMs ?? '-'}ms | ${r.pdfMs ?? '-'}ms | ${r.htmlMB ?? '-'} | ${e ? `PDF:${e.pdf} MD:${e.md} DOCX:${e.docx} NOTION:${e.notion}` : '-'} |`);
}
const out = args.out || path.join(__dirname, 'results', `hf-analysis-${new Date().toISOString().slice(0, 10)}.md`);
if (!fs.existsSync(path.dirname(out))) fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, lines.join('\n'), 'utf8');
console.log(`Report written: ${out}`);
console.log(`Requests: ${total} | avg latency: ${avg(lat)}ms | p95: ${pct(lat, 95)}ms | avg setContent: ${avg(sc)}ms | avg pdf(): ${avg(pm)}ms | exports: ${JSON.stringify(pdfExports)}`);
console.log(`Platforms: ${JSON.stringify(byPlatform)}`);
console.log(`Versions: ${JSON.stringify(byVersion)}`);
console.log(`Languages: ${JSON.stringify(byLanguage)}`);
console.log(`Hours(UTC): ${JSON.stringify(byHour)}`);
console.log(`render_charts: ${renderCalls.length} calls`);
}
main();