#!/usr/bin/env python3 """P70(启动帧率与渲染绘制概要基准)分析:汇总 Kernel 吞吐 + Desktop 真实帧循环。 输入: /kernel-baseline.txt Headless --performance-baseline 输出 /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 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 ''}") 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 ''}") 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())