/** * 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 [--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 = />>> 任务全部完成 << [--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();