onesvm-browser-server/bench/chrome-headless-shell/summarize.py
chii 983259836d chore: init workspace with onesvm-dev-md + casa-commander
docs: 联网搜索服务架构方案全套(plan-final/design-arch/选型决策/整合导览/MCP文档/部署预设/联调手册)
bench: 5 方案 + 代理 + 站点矩阵本机实测工程(无密钥)
部署目标:primary mgr1 先行测试(待批准后执行)
2026-09-01 15:19:52 +08:00

275 lines
8.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Build lightpanda/results.json from raw artifacts. No secrets."""
import json, math, os, statistics
from pathlib import Path
ROOT = Path(__file__).resolve().parent
RAW = ROOT / "raw"
SCHEME = "chrome-headless-shell"
def percentile(xs, p):
if not xs:
return None
s = sorted(xs)
if len(s) == 1:
return round(s[0], 4)
k = (len(s) - 1) * p / 100.0
f = math.floor(k)
c = math.ceil(k)
if f == c:
return round(s[int(k)], 4)
return round(s[f] + (s[c] - s[f]) * (k - f), 4)
def load_jsonl(p):
rows = []
if not p.exists():
return rows
for line in p.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def tsv_mibs(p):
vals = []
if not p.exists():
return vals
lines = p.read_text(encoding="utf-8").splitlines()
if len(lines) < 2:
return vals
hdr = lines[0].split("\t")
i = hdr.index("mem_mib") if "mem_mib" in hdr else 4
for line in lines[1:]:
parts = line.split("\t")
if len(parts) <= i:
continue
try:
v = float(parts[i])
except ValueError:
continue
if v > 0:
vals.append(v)
return vals
def median(xs):
return round(statistics.median(xs), 3) if xs else None
def pmax(xs):
return round(max(xs), 3) if xs else None
def mem_score(idle, delta, burst):
def band(v, cuts):
if v is None:
return None
for score, lim in cuts:
if v <= lim:
return score
return 0
parts = [
band(idle, [(100, 60), (80, 100), (60, 200), (40, 400)]),
band(delta, [(100, 30), (80, 80), (60, 200), (40, 350)]),
band(burst, [(100, 200), (80, 400), (60, 700), (40, 1000)]),
]
parts = [x for x in parts if x is not None]
return round(sum(parts) / 3, 1) if parts else None
def quality_js(t4):
ctrl = [r for r in t4 if r.get("template") == "T4-ctrl"]
shop = [r for r in t4 if r.get("template") == "T4-shop"]
if any(r.get("assert_ok") for r in ctrl):
if any(r.get("assert_ok") and not r.get("note") == "shop_fields_missing" for r in shop) or any(
r.get("assert_ok") for r in shop
):
return 100
# ctrl ok, shop fields missing but text present
if shop and any((r.get("text_len") or 0) > 200 for r in shop):
return 70
return 70
if any((r.get("text_len") or 0) > 100 for r in t4):
return 40
return 0
def stab_score(rate, oom):
if oom:
return 0
if rate is None:
return None
if rate >= 0.99:
return 100
if rate >= 0.95:
return 80
if rate >= 0.85:
return 60
if rate >= 0.70:
return 40
return 0
def lat_t4(p95):
if p95 is None:
return None
if p95 <= 5:
return 100
if p95 <= 12:
return 70
if p95 <= 25:
return 40
return 0
def lat_t5(t5_p95, t4_p95):
if t5_p95 is None:
return None
if t4_p95 and t4_p95 > 0:
ratio = t5_p95 / t4_p95
if ratio <= 3:
return 100
if ratio <= 5:
return 70
if ratio <= 8:
return 40
return 0
return 40 if t5_p95 <= 25 else 0
def main():
t4 = load_jsonl(RAW / "t4.jsonl")
t5 = load_jsonl(RAW / "t5.jsonl")
idle = tsv_mibs(RAW / "idle.tsv")
sess = tsv_mibs(RAW / "session.tsv")
burst = tsv_mibs(RAW / "t5-stats.tsv")
meta = {}
if (RAW / "image-meta.json").exists():
meta = json.loads((RAW / "image-meta.json").read_text(encoding="utf-8"))
idle_p50 = median(idle)
sess_peak = pmax(sess)
burst_peak = pmax(burst)
delta = None
if idle_p50 is not None and sess_peak is not None:
delta = round(max(0.0, sess_peak - idle_p50), 3)
def pack(rows, key):
sub = [r for r in rows if r.get("template") == key]
times = [r["t_total"] for r in sub if r.get("t_total") is not None]
return {
"n": len(sub),
"success": sum(1 for r in sub if r.get("assert_ok")),
"http_codes": [r.get("http_code") for r in sub],
"p50_s": percentile(times, 50),
"p95_s": percentile(times, 95),
"assert_ok": any(r.get("assert_ok") for r in sub),
"note": "; ".join(sorted({r.get("note") for r in sub if r.get("note")})),
"html_vs_text": [
{"html_len": r.get("html_len"), "text_len": r.get("text_len"), "title": r.get("title")}
for r in sub[:3]
],
}
t4_ctrl_times = [r["t_total"] for r in t4 if r.get("template") == "T4-ctrl" and r.get("t_total") is not None]
t5_ok = [r for r in t5 if r.get("assert_ok")]
t5_times = [r["t_total"] for r in t5 if r.get("t_total") is not None]
t5_n = len(t5)
t5_rate = (len(t5_ok) / t5_n) if t5_n else None
# T5 assertion uses T4-ctrl quality; scheme stability also considers T4-ctrl
ctrl = [r for r in t4 if r.get("template") == "T4-ctrl"]
ctrl_rate = (sum(1 for r in ctrl if r.get("assert_ok")) / len(ctrl)) if ctrl else None
combined_rate = t5_rate if t5_rate is not None else ctrl_rate
t4_waf = []
for r in t4:
t4_waf.append({
"tier": r.get("template"),
"url": r.get("url"),
"blocked": r.get("blocked"),
"vendor": r.get("challenge_vendor"),
"http_status_in_page": r.get("http_status_in_page"),
"final_url": r.get("final_url"),
"title": r.get("title"),
})
oom = bool(meta.get("oom"))
q = quality_js(t4)
t4_p95 = percentile(t4_ctrl_times, 95)
t5_p95 = percentile(t5_times, 95)
l4 = lat_t4(t4_p95)
l5 = lat_t5(t5_p95, t4_p95)
lat_parts = [x for x in (l4, l5) if x is not None]
lat = round(sum(lat_parts) / len(lat_parts), 1) if lat_parts else None
mem = mem_score(idle_p50, delta, burst_peak)
stab = stab_score(combined_rate, oom)
ops = 70 if SCHEME == "lightpanda" else 55
total = None
if None not in (mem, q, stab, lat, ops):
total = round(mem * 0.30 + q * 0.25 + stab * 0.20 + lat * 0.15 + ops * 0.10, 1)
queue = meta.get("queue") or {}
out = {
"scheme": SCHEME,
"group": "foreign_proxy",
"image": {
"ref": meta.get("ref"),
"digest": meta.get("digest"),
"id": meta.get("id"),
"arch": meta.get("arch"),
"pull": meta.get("pull"),
},
"probed_at": meta.get("probed_at"),
"commands": meta.get("commands") or [],
"proxy": meta.get("proxy") or {},
"rss_source": "docker_stats+cgroup",
"idle_mb_p50": idle_p50,
"session_delta_mb": delta,
"burst_peak_mb": burst_peak,
"session_peak_mb": sess_peak,
"templates": {
"T4-ctrl": pack(t4, "T4-ctrl"),
"T4-medium": pack(t4, "T4-medium"),
"T4-shop": pack(t4, "T4-shop"),
"T4-amz": pack(t4, "T4-amz"),
"T4-t3x": pack(t4, "T4-t3x"),
},
"t5": {
"n": t5_n,
"parallelism": meta.get("t5_parallelism") or 4,
"queue_impl": meta.get("queue_impl") or "client_fifo",
"success": len(t5_ok),
"success_rate": t5_rate,
"p50_s": percentile(t5_times, 50),
"p95_s": t5_p95,
"oom": oom,
"http_429": 0,
"http_503": 0,
"first_complete_s": queue.get("first_complete_s"),
"last_complete_s": queue.get("last_complete_s"),
"queue_wait_p50_s": queue.get("queue_wait_p50_s"),
"queue_wait_p95_s": queue.get("queue_wait_p95_s"),
"rejected": queue.get("rejected") or 0,
"wall_s": meta.get("t5_wall_s"),
"note": "生产应是 SQLite WAL + CONCURRENT;本轮用脚本侧 worker 池代替",
},
"t4_waf": t4_waf,
"scorecard": {
"mem": mem,
"quality": q,
"stability": stab,
"latency": lat,
"ops": ops,
"total": total,
},
"notes": meta.get("notes") or [],
}
(ROOT / "results.json").write_text(json.dumps(out, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"wrote": str(ROOT / "results.json"), "scorecard": out["scorecard"]}, ensure_ascii=False))
if __name__ == "__main__":
main()