dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

123 lines4386 bytesexecutable
1#!/usr/bin/env python3
2"""Compare MangoHud frame logs from CachyOS and NixOS (see MIGRATION.md).
3
4 tools/bench-compare every log in ~/benchmarks
5 tools/bench-compare DIR|FILE ... the logs given
6
7Logs are grouped by the OS MangoHud wrote into each file's header, so logs
8from both systems can sit in one folder. For each run it shows the average
9FPS, the 1% and 0.1% lows (the FPS of the slowest 1% and 0.1% of frames) and
10the longest frame; then each system's average over its runs, and how far
11NixOS is from CachyOS.
12
13Record with log_interval=0 so every frame is logged; with sampled logs the
14lows are only estimates.
15"""
16import csv
17import statistics
18import sys
19from pathlib import Path
20
21
22def read_log(path):
23 """Return (os name, frame times in ms) from one MangoHud CSV log."""
24 rows = list(csv.reader(path.open(newline="", errors="replace")))
25 os_name = "unknown"
26 frametimes = None
27 for i, row in enumerate(rows):
28 cols = [c.strip().lower() for c in row]
29 if cols[:1] == ["os"] and i + 1 < len(rows) and rows[i + 1]:
30 os_name = rows[i + 1][0].strip() or os_name
31 elif "frametime" in cols:
32 col = cols.index("frametime")
33 frametimes = []
34 for data in rows[i + 1 :]:
35 try:
36 frametimes.append(float(data[col]))
37 except (IndexError, ValueError):
38 continue
39 break
40 if not frametimes:
41 return os_name, []
42 # Older MangoHud versions log microseconds, newer ones milliseconds.
43 if statistics.median(frametimes) > 200:
44 frametimes = [t / 1000 for t in frametimes]
45 return os_name, [t for t in frametimes if t > 0]
46
47
48def low(frametimes, fraction):
49 """FPS of the slowest `fraction` of frames (averaged, as MangoHud does)."""
50 worst = sorted(frametimes, reverse=True)
51 n = max(1, int(len(worst) * fraction))
52 return 1000 / statistics.fmean(worst[:n])
53
54
55def stats(frametimes):
56 return {
57 "avg": 1000 / statistics.fmean(frametimes),
58 "1%": low(frametimes, 0.01),
59 "0.1%": low(frametimes, 0.001),
60 "max ms": max(frametimes),
61 }
62
63
64def short_os(name):
65 lower = name.lower()
66 if "cachyos" in lower:
67 return "CachyOS"
68 if "nixos" in lower:
69 return "NixOS"
70 return name
71
72
73def main(args):
74 targets = [Path(a) for a in args] or [Path.home() / "benchmarks"]
75 files = []
76 for target in targets:
77 if target.is_dir():
78 files += sorted(target.rglob("*.csv"))
79 elif target.exists():
80 files.append(target)
81 else:
82 sys.exit(f"bench-compare: {target} doesn't exist")
83 files = [f for f in files if not f.stem.endswith("_summary")]
84 if not files:
85 sys.exit("bench-compare: no MangoHud logs found")
86
87 keys = ["avg", "1%", "0.1%", "max ms"]
88 header = f"{'':38}" + "".join(f"{k:>9}" for k in keys)
89 groups = {}
90 print(header)
91 for f in files:
92 os_name, frametimes = read_log(f)
93 if len(frametimes) < 100:
94 print(f"{f.name[:38]:38} skipped: {len(frametimes)} frames")
95 continue
96 s = stats(frametimes)
97 system = short_os(os_name)
98 groups.setdefault(system, []).append(s)
99 label = f"{system} {f.name}"[:38]
100 print(f"{label:38}" + "".join(f"{s[k]:9.1f}" for k in keys))
101
102 print()
103 print(f"{'average of runs':38}" + "".join(f"{k:>9}" for k in keys))
104 means = {}
105 for system, runs in groups.items():
106 means[system] = {k: statistics.fmean(r[k] for r in runs) for k in keys}
107 label = f"{system} ({len(runs)} runs)"
108 print(f"{label:38}" + "".join(f"{means[system][k]:9.1f}" for k in keys))
109
110 if "CachyOS" in means and "NixOS" in means:
111 a, b = means["CachyOS"], means["NixOS"]
112 diff = "".join(f"{(b[k] - a[k]) / a[k] * 100:+8.1f}%" for k in keys)
113 print(f"{'NixOS vs CachyOS':38}{diff}")
114 print("(higher is better for FPS columns, lower is better for max ms)")
115 # Run-to-run noise on the same system, to judge whether a gap is real.
116 for system, runs in groups.items():
117 if len(runs) > 1:
118 spread = statistics.pstdev(r["avg"] for r in runs)
119 print(f"{system}: average FPS varies ±{spread:.1f} between runs")
120
121
122if __name__ == "__main__":
123 main(sys.argv[1:])