Spaces:
Running
Running
| /** | |
| * 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(); | |