1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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())
|