summaryrefslogtreecommitdiff
path: root/scripts/analyze-p70.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/analyze-p70.py')
-rwxr-xr-xscripts/analyze-p70.py160
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())