Files
SerpMax/stress/report.ts
T

126 lines
6.4 KiB
TypeScript

/** # 脚本说明
* 压测数据汇总(服务器上运行):
* 输入 data/<label>-stats.jsonl(采样器输出)+ data/<label>-launched.json(发射清单)
* 输出:
* - 峰值/残存浏览器数、发射总量、系统峰值(avail 最低/swap 最高/PSS 最高)
* - 每 Profile 稳定期均值(PSS/swap/CPU/进程数)与分布(p50/p90/p99/max)
* - 每 Profile 磁盘占用(du data/profiles/<fp>/browser)
* - top 明细表(MD 片段)→ data/<label>-summary.json + data/<label>-summary.md
* 用法:bun report.ts --label test1 [--tail 10]
*/
const fs = require("fs") as typeof import("fs");
import { $ } from "bun";
const args = process.argv.slice(2);
function argOf(k: string, d: string): string { const i = args.indexOf(k); return i >= 0 && i + 1 < args.length ? args[i + 1] : d; }
const LABEL = argOf("--label", "test1");
const TAILN = Number(argOf("--tail", "10"));
const DATA = `${new URL(".", import.meta.url).pathname}data`;
/** # 成员变量 采样行类型 **/
type Sample = { ts: string; browsers: number; totalPssMB: number; totalRssMB: number; totalSwapMB: number; totalProcs: number; availMB: number; swapUsedMB: number; load: number[]; detail: any[] };
const lines = fs.readFileSync(`${DATA}/${LABEL}-stats.jsonl`, "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l) as Sample);
console.log(`samples=${lines.length}`);
const peak = {
browsers: Math.max(...lines.map(s => s.browsers)),
pssMB: Math.max(...lines.map(s => s.totalPssMB)),
minAvailMB: Math.min(...lines.map(s => s.availMB)),
maxSwapMB: Math.max(...lines.map(s => s.swapUsedMB)),
maxLoad: Math.max(...lines.map(s => s.load?.[0] ?? 0)),
maxProcs: Math.max(...lines.map(s => s.totalProcs)),
};
const last = lines[lines.length - 1];
console.log(`peak: ${JSON.stringify(peak)}`);
console.log(`last: browsers=${last.browsers} pss=${Math.round(last.totalPssMB)}MB avail=${Math.round(last.availMB)}MB`);
/** # 成员变量 稳定期尾窗样本(后 N 个) **/
const tail = lines.slice(-TAILN);
/** # 函数:取多组数分布(p50/p90/p99/max) **/
function dist(values: number[]): { p50: number; p90: number; p99: number; max: number } | null {
if (!values.length) return null;
const v = [...values].sort((a, b) => a - b);
const q = (p: number) => v[Math.min(v.length - 1, Math.floor(p * (v.length - 1)))];
return { p50: q(0.5), p90: q(0.9), p99: q(0.99), max: v[v.length - 1] };
}
/** # 函数:收集每 Profile 稳定期聚合(窗口内出现的样本取均值;现存取最后样本) **/
const per = new Map<string, { fp: string; pss: number[]; swap: number[]; procs: number[]; cpu: number[]; udd: string; lastTs: string }>();
for (const s of tail) {
for (const t of s.detail) {
const e = per.get(t.fp) ?? { fp: t.fp, pss: [], swap: [], procs: [], cpu: [], udd: t.userDataDir ?? "", lastTs: s.ts };
e.pss.push(t.pssKB / 1024);
e.swap.push((t.swapPssKB ?? t.swapKB) / 1024);
e.procs.push(t.procs);
e.cpu.push((t.cpuDeltaMs ?? t.cpuMs) / 1000);
e.lastTs = s.ts;
e.udd = e.udd || t.userDataDir || "";
per.set(t.fp, e);
}
}
const avg = (a: number[]) => (a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0);
const rows = [...per.values()].map(e => ({
fp: e.fp,
pssMB: avg(e.pss),
swapMB: avg(e.swap),
procs: avg(e.procs),
cpuSec: avg(e.cpu),
}));
/** # 成员变量 每 Profile 磁盘占用(du user-data 目录) **/
const disk: Record<string, number> = {};
try {
const paths = rows.map(r => r.udd).filter(Boolean).slice(0, 400);
const chunks: { stdout: string }[] = [];
for (let i = 0; i < paths.length; i += 200) {
const p = Bun.spawnSync(["du", "-sb", ...paths.slice(i, i + 200)], { stdout: "pipe" });
chunks.push(p as unknown as { stdout: string });
}
for (const p of chunks) {
for (const line of (p as any).stdout.toString().split("\n")) {
const m = /^(\d+)\t(.+)$/.exec(line);
if (m) disk[m[2].split("/").slice(-2)[0]] = Number(m[1]);
}
}
console.log(`disk rows=${Object.keys(disk).length}`);
} catch (e) { console.error("du 失败", e); }
/** # 成员变量 汇总分布 **/
const dPss = dist(rows.map(r => r.pssMB));
const dSwap = dist(rows.map(r => r.swapMB));
const dProcs = dist(rows.map(r => r.procs));
const dCpu = dist(rows.map(r => r.cpuSec));
const dDisk = dist(rows.map(r => disk[r.fp] ?? 0));
const aliveFps = new Set(last.detail.map(t => t.fp));
const summary = {
label: LABEL,
samples: lines.length,
tailSamples: tail.length,
peak,
last: { ts: last.ts, browsers: last.browsers, pssMB: Math.round(last.totalPssMB), availMB: Math.round(last.availMB), swapUsedMB: Math.round(last.swapUsedMB), load: last.load, procs: last.totalProcs },
aliveRows: rows.length,
distPerProfile: { pssMB: dPss, swapMB: dSwap, procs: dProcs, cpuSec: dCpu, diskMB: dDisk ? { p50: dDisk.p50 / 1048576, p90: dDisk.p90 / 1048576, max: dDisk.max / 1048576 } : null },
top10: rows.sort((a, b) => b.pssMB - a.pssMB).slice(0, 10).map(r => ({ fp: r.fp, pssMB: Math.round(r.pssMB), swapMB: Math.round(r.swapMB), procs: Math.round(r.procs), diskMB: Math.round((disk[r.fp] ?? 0) / 1048576) })),
};
await Bun.write(`${DATA}/${LABEL}-summary.json`, JSON.stringify(summary, null, 2));
const md = [
`## ${LABEL} 汇总`,
`- 采样 ${lines.length} 轮(尾窗 ${tail.length} 轮);峰值浏览器 **${peak.browsers}**;残存 **${summary.last.browsers}**`,
`- 峰值系统:PSS ${Math.round(peak.pssMB)}MB / avail 最低 ${Math.round(peak.minAvailMB)}MB / swap 最高 ${Math.round(peak.maxSwapMB)}MB / load ${Math.round(peak.maxLoad)} / 总进程 ${peak.maxProcs}`,
`- 每 Profile(稳定期均值,MB):PSS p50=${dPss?.p50.toFixed(0)} p90=${dPss?.p90.toFixed(0)} p99=${dPss?.p99.toFixed(0)} max=${dPss?.max.toFixed(0)}`,
`- 每 Profile:swapMB p50=${dSwap?.p50.toFixed(0)} / 进程数 avg=${(rows.reduce((a, b) => a + b.procs, 0) / rows.length).toFixed(1)} / CPU 秒 avg=${(rows.reduce((a, b) => a + b.cpuSec, 0) / rows.length).toFixed(0)}`,
`- 每 Profile 磁盘(user-data 目录,MB):p50=${dDisk ? (dDisk.p50 / 1048576).toFixed(0) : "-"} max=${dDisk ? (dDisk.max / 1048576).toFixed(0) : "-"}`,
"",
markdownTable(summary.top10),
].join("\n");
await Bun.write(`${DATA}/${LABEL}-summary.md`, md);
console.log(md);
/** # 函数:top10 markdown 表 **/
function markdownTable(rows: any[]): string {
const head = "| fp | PSS MB | swap MB | procs | disk MB |\n|---|---|---|---|---|";
return [head, ...rows.map(r => `| ${r.fp} | ${r.pssMB} | ${r.swapMB} | ${r.procs} | ${r.diskMB} |`)].join("\n");
}