diff options
Diffstat (limited to 'scripts/analyze-p70.py')
| -rwxr-xr-x | scripts/analyze-p70.py | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/scripts/analyze-p70.py b/scripts/analyze-p70.py new file mode 100755 index 0000000..acafe32 --- /dev/null +++ b/scripts/analyze-p70.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""P70(启动帧率与渲染绘制概要基准)分析:汇总 Kernel 吞吐 + Desktop 真实帧循环。 + +输入: + <record-dir>/kernel-baseline.txt Headless --performance-baseline 输出 + <record-dir>/desktop-log.txt LV_FRAME_LOG=1 逐帧 p70-frame 行 +判据: + 1. Kernel:final_digest == 953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C, + determinism=1,并报告 ticks_per_second / allocated_bytes / gen0..2。 + 2. Desktop:p70-frame 行 >= 1;由墙钟帧间隔算 mean/min/max/p50/p95/p99、std、fps, + 统计 >25ms(约 1.5×16.7)与 >33.3ms 的掉帧数;逐帧线程分配 mean/p95 KB。 + 不宣称未测指标;帧率依赖机器/xvfb,仅报实测值。 + 3. 只做真实数字汇总,不设硬性 fps 门槛(除 >0、数值合法)。 + +Usage: python3 scripts/analyze-p70.py <record-dir> <out-txt> +Exit 0 pass / 1 fail / 2 usage. +""" + +import re +import statistics +import sys +from pathlib import Path + +FRAME_RE = re.compile( + r"^p70-frame index=(\d+) ms=([\d.]+) alloc_kb=([\d.]+) tick=(\d+) panel=(\S+)$", + re.MULTILINE, +) +KERNEL_SAMPLE_RE = re.compile(r"^performance_sample repetition=(\d+) .*$", re.MULTILINE) +DIGEST_RE = re.compile(r"final_digest=([0-9A-F]+)") +TPS_RE = re.compile(r"ticks_per_second=([\d.]+)") +ALLOC_RE = re.compile(r"allocated_bytes=(\d+)") +GEN_RE = re.compile(r"gen0=(\d+) gen1=(\d+) gen2=(\d+)") +DET_RE = re.compile(r"performance_determinism=(\w+)") +COUNTERS_RE = re.compile(r"^dotnet-counters=(\S+)", re.MULTILINE) + +PINNED_DIGEST = "953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C" +FPS_60_MS = 1000.0 / 60.0 +DROPPED_1X = 1.5 * FPS_60_MS +DROPPED_2X = 2.0 * FPS_60_MS + + +def percentile(sorted_values, q): + if not sorted_values: + return 0.0 + if len(sorted_values) == 1: + return sorted_values[0] + pos = q * (len(sorted_values) - 1) + lo = int(pos) + hi = min(lo + 1, len(sorted_values) - 1) + frac = pos - lo + return sorted_values[lo] * (1 - frac) + sorted_values[hi] * frac + + +def main(): + if len(sys.argv) != 3: + print(__doc__) + return 2 + rec = Path(sys.argv[1]) + out = Path(sys.argv[2]) + failures = [] + lines = [ + "P70 启动帧率与渲染绘制概要基准 — 真实可复跑数据", + "Kernel:Headless --performance-baseline(已有入口)", + "Desktop:xvfb 1280x720 真实帧循环(LV_FRAME_LOG=1,墙钟帧间隔 + 线程分配)", + "", + ] + + # ---- Kernel ---- + kernel_path = rec / "kernel-baseline.txt" + if not kernel_path.exists(): + failures.append("missing kernel-baseline.txt") + else: + kernel = kernel_path.read_text(encoding="utf-8", errors="replace") + samples = KERNEL_SAMPLE_RE.findall(kernel) + digests = DIGEST_RE.findall(kernel) + det = DET_RE.search(kernel) + tps = [float(v) for v in TPS_RE.findall(kernel)] + alloc = [int(v) for v in ALLOC_RE.findall(kernel)] + gens = GEN_RE.findall(kernel) + lines.append("## Kernel 吞吐(Sim.step,6000 measure tick)") + lines.append(f"samples={len(samples)} digests={len(digests)} determinism={det.group(1) if det else '<missing>'}") + if digests: + lines.append(f"final_digest={digests[-1]}") + if digests[-1] != PINNED_DIGEST: + failures.append(f"final_digest {digests[-1]} != pinned {PINNED_DIGEST}") + else: + failures.append("kernel log missing final_digest") + if tps: + lines.append( + f"ticks_per_second: mean={statistics.mean(tps):.1f} min={min(tps):.1f} max={max(tps):.1f} n={len(tps)}" + ) + if alloc: + lines.append( + f"allocated_bytes: mean={statistics.mean(alloc):.0f} min={min(alloc)} max={max(alloc)} n={len(alloc)}" + ) + if gens: + g = gens[-1] + lines.append(f"gc collections (last sample): gen0={g[0]} gen1={g[1]} gen2={g[2]}") + if det is None or det.group(1) != "PASS": + failures.append("performance_determinism != PASS") + lines.append("") + + # ---- Desktop ---- + env_path = rec / "env.txt" + counters = COUNTERS_RE.search(env_path.read_text(encoding="utf-8", errors="replace")) if env_path.exists() else None + desktop_path = rec / "desktop-log.txt" + if not desktop_path.exists(): + failures.append("missing desktop-log.txt") + else: + log = desktop_path.read_text(encoding="utf-8", errors="replace") + frames = FRAME_RE.findall(log) + lines.append("## Desktop 渲染帧(真实帧循环)") + lines.append(f"frames_logged={len(frames)}") + if not frames: + failures.append("no p70-frame lines") + else: + ms = [float(f[1]) for f in frames] + alloc_kb = [float(f[2]) for f in frames] + total_ms = sum(ms) + fps = (len(ms) / total_ms * 1000.0) if total_ms > 0 else 0.0 + ordered = sorted(ms) + mean_ms = statistics.mean(ms) + std_ms = statistics.pstdev(ms) if len(ms) > 1 else 0.0 + dropped_1x = sum(1 for v in ms if v > DROPPED_1X) + dropped_2x = sum(1 for v in ms if v > DROPPED_2X) + lines.append( + f"interval_ms: mean={mean_ms:.3f} min={min(ms):.3f} max={max(ms):.3f} " + f"p50={percentile(ordered, 0.50):.3f} p95={percentile(ordered, 0.95):.3f} " + f"p99={percentile(ordered, 0.99):.3f} std={std_ms:.3f}" + ) + lines.append(f"measured_fps={fps:.1f} (frames={len(ms)}, wall_ms={total_ms:.1f})") + lines.append( + f"dropped_frames: >{DROPPED_1X:.1f}ms={dropped_1x} >{DROPPED_2X:.1f}ms={dropped_2x} " + f"({dropped_1x / len(ms) * 100.0:.2f}% / {dropped_2x / len(ms) * 100.0:.2f}%)" + ) + lines.append( + f"per_frame_alloc_kb: mean={statistics.mean(alloc_kb):.2f} " + f"min={min(alloc_kb):.2f} max={max(alloc_kb):.2f} p95={percentile(sorted(alloc_kb), 0.95):.2f}" + ) + if not (0.0 < mean_ms < 60000.0): + failures.append(f"mean frame ms {mean_ms} out of plausible range") + if fps <= 0.0: + failures.append("measured fps <= 0") + lines.append(f"dotnet-counters={counters.group(1) if counters else '<unknown>'}") + lines.append("(帧率随机器/xvfb 软件渲染波动;本报告只列本次实测值,不外推。)") + lines.append("") + + lines.append("bounds: Kernel/Sim 未改(digest 钉值);Desktop 仅新增 LV_FRAME_LOG 逐帧日志,不改渲染口径/字形表。") + lines.append("") + verdict = "PASS" if not failures else "FAIL" + lines.append(f"verdict: {verdict}") + for f in failures: + lines.append(f"FAIL: {f}") + out.write_text("\n".join(lines) + "\n", encoding="utf-8") + print("\n".join(lines)) + return 0 if not failures else 1 + + +if __name__ == "__main__": + sys.exit(main()) |
