summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/analyze-p70.py160
-rwxr-xr-xscripts/bench-p70.sh82
2 files changed, 242 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())
diff --git a/scripts/bench-p70.sh b/scripts/bench-p70.sh
new file mode 100755
index 0000000..f035bb2
--- /dev/null
+++ b/scripts/bench-p70.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+# P70:启动帧率与渲染绘制概要基准(真实可复跑,不宣称未测性能)。
+#
+# 两档:
+# * Kernel 吞吐:Headless `--performance-baseline`(6000 measure tick ×3,已有入口),落 kernel-baseline.txt。
+# * Desktop 渲染帧:xvfb 真实帧循环 N 秒(默认 60),逐帧打印墙钟帧间隔与线程分配增量
+# (LV_FRAME_LOG=1,仅新增日志、不改渲染逻辑),落 desktop-log.txt。
+# 之后由 scripts/analyze-p70.py 汇总裁决到 docs/evidence/p70-analysis.txt。
+#
+# 用法:bash scripts/bench-p70.sh [out-dir] (out-dir 默认 /tmp/opencode/lv-p70)
+# 环境:P70_SECONDS(默认 60)基准秒数;P70_FORCE=1 允许 fps=0 时仍返回 0。
+set -u
+
+REPO="$(cd "$(dirname "$0")/.." && pwd)"
+OUT="${1:-/tmp/opencode/lv-p70}"
+BENCH_SECONDS="${P70_SECONDS:-60}"
+XVFB_ARGS="-screen 0 1280x720x24"
+
+KERNEL_DLL="$REPO/src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll"
+DESKTOP_DLL="$REPO/src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll"
+KERNEL_LOG="$OUT/kernel-baseline.txt"
+DESKTOP_LOG="$OUT/desktop-log.txt"
+
+mkdir -p "$OUT"
+echo "[p70] repo=$REPO out=$OUT bench_seconds=$BENCH_SECONDS"
+echo "[p70] started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+echo "[p70] kernel_dll=$KERNEL_DLL"
+echo "[p70] desktop_dll=$DESKTOP_DLL"
+
+for dll in "$KERNEL_DLL" "$DESKTOP_DLL"; do
+ if [ ! -f "$dll" ]; then
+ echo "[p70] FATAL missing $dll(先 dotnet build LivingVillage.sln -c Release)" >&2
+ exit 90
+ fi
+done
+
+if command -v dotnet-counters >/dev/null 2>&1; then
+ COUNTERS=available
+else
+ COUNTERS=missing
+fi
+if command -v xvfb-run >/dev/null 2>&1; then
+ XVFB=available
+else
+ XVFB=missing
+fi
+{
+ echo "dotnet-counters=$COUNTERS"
+ echo "xvfb-run=$XVFB"
+ echo "bench_seconds=$BENCH_SECONDS"
+ echo "desktop_dll=$DESKTOP_DLL"
+ echo "kernel_dll=$KERNEL_DLL"
+} > "$OUT/env.txt"
+echo "[p70] dotnet-counters=$COUNTERS"
+if [ "$XVFB" = "missing" ]; then
+ echo "[p70] FATAL xvfb-run 不可用(Desktop 需虚拟显示)" >&2
+ exit 91
+fi
+echo "[p70] xvfb-run=$XVFB(不阻塞:逐帧分配用 GC.GetAllocatedBytesForCurrentThread 实测)"
+
+echo "[p70] kernel baseline: dotnet $KERNEL_DLL --performance-baseline"
+dotnet "$KERNEL_DLL" --performance-baseline > "$KERNEL_LOG" 2>&1
+KCODE=$?
+echo "[p70] kernel exit=$KCODE"
+
+echo "[p70] desktop frame loop: ${BENCH_SECONDS}s @ 1280x720 under xvfb"
+timeout $((BENCH_SECONDS + 180)) xvfb-run -a -s "$XVFB_ARGS" env \
+ LV_AUTOPLAY_SAMPLE=1 \
+ LV_PERF_SOAK_SECONDS="$BENCH_SECONDS" \
+ LV_FRAME_LOG=1 \
+ LV_UNLOCK_FPS=1 \
+ LV_LEGACY_MAP=1 \
+ LV_RECORD_DIR="$OUT" \
+ dotnet "$DESKTOP_DLL" > "$DESKTOP_LOG" 2>&1
+DCODE=$?
+echo "[p70] desktop exit=$DCODE frames=$(grep -c '^p70-frame ' "$DESKTOP_LOG" || true)"
+echo "[p70] ended_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+
+python3 "$REPO/scripts/analyze-p70.py" "$OUT" "$REPO/docs/evidence/p70-analysis.txt"
+ACODE=$?
+echo "[p70] analyze exit=$ACODE -> $REPO/docs/evidence/p70-analysis.txt"
+exit "$ACODE"