一个可复现的 NixOS 配置框架性能基准项目,用相同的合成工作负载对比:
- Snowveil
- Snowfall Lib
- flake-parts
- nixos-unified
- Den
- plain flake:无框架基线
项目依据各框架的官方组织方式设计:Snowveil、Snowfall 使用目录约定和自动发现;flake-parts 与 nixos-unified 使用 flake module 装配;Den 使用 aspect;plain flake 直接调用 nixpkgs.lib.nixosSystem。各案例使用自己的真实框架入口,不用模拟实现替代框架。
当前锁文件生成于 2026-09-01。精确 revision、
narHash和传递输入以各案例的flake.lock为准。
| 规模 | 主机数 | 合成 NixOS 模块数 | 用途 |
|---|---|---|---|
tiny |
1 | 8 | 烟雾测试与快速回归 |
small |
4 | 32 | 个人多主机配置 |
medium |
16 | 128 | 团队或同构服务器集群 |
large |
64 | 512 | 大型配置仓库与压力测试 |
每个合成模块向 benchmark.moduleIds 追加唯一整数。所有框架中的每台主机都必须得到相同的:
moduleCountchecksumhostIdnetworking.hostNamesystem.build.toplevel.drvPath
smoke_test.py 会检查这些不变量,避免“更快”只是因为某个框架漏装模块。
.
├── benchmark-matrix.json # 规模定义
├── cases/
│ ├── tiny/{plain,snowveil,flake-parts,nixos-unified,snowfall,den}/
│ ├── small/...
│ ├── medium/...
│ └── large/...
├── docs/methodology.md # 公平性、指标和限制
├── scripts/
│ ├── generate_cases.py # 确定性生成全部案例
│ ├── smoke_test.py # 求值正确性检查
│ ├── run_benchmarks.py # 采样器
│ └── render_report.py # JSON/CSV/Markdown 汇总
├── flake.nix # apps、开发环境和 formatter
└── flake.lock
| 框架 | 案例组织方式 | 模块如何进入主机 |
|---|---|---|
| plain | modules/features/*.nix + 显式列表 |
nixosSystem.modules |
| Snowveil | modules/*/nixos.nix、hosts/*/{meta,default}.nix |
Snowveil 自动发现并注入 |
| flake-parts | flake module 中声明 flake.nixosConfigurations |
flake-parts 装配 outputs,显式模块列表 |
| nixos-unified | flake-parts module 中调用 config.flake.nixos-unified.lib.mkLinuxSystem |
封装 nixosSystem,显式模块列表 |
| Snowfall | modules/nixos/*/default.nix、systems/<system>/*/default.nix |
Snowfall 自动发现并注入 |
| Den | 每个 feature/host 一个 aspect module | den.default.includes + host aspect |
这不是“只测模块系统”的微基准,而是测量用户采用各框架惯用组织方式时的端到端求值成本,因此目录扫描、框架抽象和 output 装配都会计入。
为避免 flake-show 被框架附带但与本基准无关的 checks/packages 干扰,各案例统一只暴露共享的 nixosConfigurations 与 benchmarks 探针;Snowfall 因其生成配置对 self.pkgs 的内部引用额外保留 pkgs output。
以下快照来自一次 --quick 运行(tiny + small,eval-first/eval-all/drv-first 三个核心任务,cold + warm 两种模式,每场景预热 1 次、正式采样 2 次,seed 20260902),完整 72 行数据见 results/20260902T152044Z/report.md 与 summary.csv。求值耗时(中位数,单位 s):
| 框架 | tiny eval-all cold | small eval-all cold | small drv-first cold | small eval-all 相对 plain |
|---|---|---|---|---|
| plain(基线) | 1.657 | 2.924 | 8.441 | 1.00× |
| Snowveil | 1.804 | 2.589 | 7.628 | 0.89× |
| flake-parts | 1.691 | 3.049 | 8.280 | 1.04× |
| nixos-unified | 1.757 | 3.100 | 7.828 | 1.06× |
| Snowfall | 2.196 | 3.584 | 7.932 | 1.23× |
| Den | 1.959 | 3.275 | 8.145 | 1.12× |
要点:
eval-*任务里框架差异最明显:Snowfall 最慢(约 +10%–38%),Den 次之(约 +12%–30%);Snowveil、flake-parts、nixos-unified 与 plain 接近,其中 Snowveil 在 small/eval-all 甚至快于 plain(约 0.89×),且其 RSS 明显更低(small/eval-all 约 354 MiB,其余约 436–472 MiB)。drv-*任务中所有框架都收敛到 plain 附近(约 0.90×–1.11×),因为 nixpkgs/NixOS derivation 实例化开销占主导,淹没了框架抽象与目录发现的差异。- 该结果仅对应上述受限规模与短采样,正式结论应固定 CPU governor、停止后台负载并增加 runs/seed 后重跑。
cd nixos-framework-benchmarks
# 进入包含 nix、GNU time、jq、nixfmt、statix、deadnix 的环境
nix develop
# 验证六种框架的 tiny 案例
nix run .#smoke
# 快速基准:tiny + small,核心任务,cold + warm,各 2 次
nix run .#benchmark -- --quick
# 假设输出目录为 results/20260901T120000Z
nix run .#report -- results/20260901T120000Z完整基准默认会运行 4 个规模、6 种实现、6 个任务、2 种模式,每个场景 1 次预热和 5 次正式采样:
nix run .#benchmark默认场景顺序使用固定 seed 随机化,减少框架顺序与温度/缓存漂移的关联。可改变 seed 或强制矩阵顺序:
nix run .#benchmark -- --seed 42
nix run .#benchmark -- --ordered案例已提交并包含锁文件;只有调整规模或生成器后才需要重新生成:
# 重新生成并重新锁定每个框架;需要网络
nix run .#generate -- --lock
# 只生成选定组合
nix run .#generate -- --scales tiny,small --frameworks snowveil,den--lock 先为每个框架的第一个规模生成锁文件,再复制到同框架的其他规模。因为同一框架在所有规模中使用完全相同的 input 图,这可确保版本一致。
nix run .#smoke -- --scales tiny,small,medium,largenix run .#benchmark -- \
--frameworks plain,snowveil,snowfall,flake-parts,nixos-unified,den \
--scales small,medium,large \
--tasks flake-show,eval-first,eval-all,drv-first,drv-all \
--modes cold,warm \
--warmup 2 \
--runs 10 \
--seed 20260901可用任务:
metadata:解析 flake input 元数据,主要观察输入图管理开销。flake-show:枚举 flake outputs。eval-first/eval-last:强制首台/末台主机的合成配置摘要。eval-all:强制所有主机的配置摘要。drv-first/drv-last:求值一台主机的 toplevel derivation path。drv-all:求值所有主机的 toplevel derivation path。flake-check:执行nix flake check --no-build;适合作为额外压力任务,不在默认任务中。
Linux 上可将整个基准固定到指定 CPU:
taskset -c 2 nix run .#benchmark -- --runs 10 --warmup 2发布结果前还应尽量使用稳定的 CPU governor、关闭高负载后台任务,并在相同电源/温度条件下重复运行。
每个样本写入 samples.jsonl,包括:
- Python monotonic wall time
- GNU time 的 user/system CPU、最大 RSS、page faults、context switches、文件系统 I/O
NIX_SHOW_STATS=1的 evaluator CPU、heap、thunks、function calls、lookups、sets/lists 等原始统计- 完整命令、返回码、stdout/stderr 字节数和失败尾部日志
报告器生成:
summary.jsonsummary.csvreport.md
报告包含均值、中位数、标准差、最小值、最大值、p95、RSS、Nix CPU,以及相对 plain 和相对最快实现的倍率。
cold:传入--no-eval-cache,禁用 Nix evaluator cache。warm:保留 evaluator cache,并在正式采样前预热。
两种模式都不会清空 Linux page cache,也不会清空 /nix/store。因此这里的 cold 是“evaluator-cache cold”,不是重启机器后的绝对冷启动。详细边界见 docs/methodology.md。
完整构建通常主要受 nixpkgs revision、二进制缓存、下载带宽、磁盘和具体软件集合影响,容易掩盖配置框架的求值差异。本项目默认比较 outputs 构造、NixOS module 合并与 derivation 实例化;drv-* 会求值得到 toplevel derivation,但不实际构建系统 closure。
如需测构建,可在严格控制缓存与工作负载后,基于案例执行:
nix build --no-link path:cases/medium/snowveil#nixosConfigurations.host-000.config.system.build.toplevel此类结果应单独报告,不应与纯求值结果混为一谈。