feat(memory): enable zram compressed-RAM swap by default (#76 slice)
All checks were successful
Check / eval (push) Successful in 4m1s

First slice of BACKLOG #76 (hibernation + zram, split): zramSwap on by
default in modules/nixos/oom.nix — the memory-pressure layer before
earlyoom. zstd @ 50% RAM (nixpkgs defaults, explicit for reproducibility),
priority 100 so day-to-day paging fills zram first and any disk swapfile
stays reserved for #76's hibernate image (hibernation can't resume from
volatile zram). Follows the earlyoom mkDefault precedent — no toggle.

Adds checks.zram-swap runNixOSTest importing oom.nix, asserting the device
is an active swap, uses [zstd], and carries priority 100.

Verified: V2 checks.zram-swap PASS (/dev/zram0 in /proc/swaps, zstd,
prio 100); V1 full distro toplevel builds; V0 flake check --no-build green.
#76 remainder (disk swapfile default + resume + installer) still open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-07-10 16:07:55 +01:00
parent 59975a6a59
commit 2055842c33
4 changed files with 75 additions and 11 deletions

View File

@@ -28,12 +28,18 @@ in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in
## NEXT ## NEXT
### 76. Hibernation default + zram `[big]` ### 76. Hibernation default `[big]`
**Product intent (Bernardo 2026-07-10):** hibernation on by default; **Product intent (Bernardo 2026-07-10):** hibernation on by default;
zram on alongside for live memory pressure. Disk swap ≥ RAM for the zram on alongside for live memory pressure. Disk swap ≥ RAM for the
hibernate image; zram high-priority for day-to-day swap (not sufficient hibernate image; zram high-priority for day-to-day swap (not sufficient
alone for resume). alone for resume).
**✓ zram slice shipped (2026-07-10):** `zramSwap` on by default in
`modules/nixos/oom.nix` (zstd, 50% RAM, priority 100 so day-to-day paging
stays off the disk swap reserved for the hibernate image); `checks.zram-swap`
VM test asserts device/algorithm/priority (V2 pass). **Remainder below is the
hibernation half** — disk swapfile default + resume + installer.
**Reference layout (this machine — already hibernation-capable, no zram **Reference layout (this machine — already hibernation-capable, no zram
yet):** LUKS (`crypto_LUKS` → mapper `crypted`) holds one BTRFS with yet):** LUKS (`crypto_LUKS` → mapper `crypted`) holds one BTRFS with
`@swap` mounted at `/swap`; swapfile `/swap/swapfile` (here 16G); `@swap` mounted at `/swap`; swapfile `/swap/swapfile` (here 16G);
@@ -47,12 +53,12 @@ Installer path already sketches this (`@swap` + resume_offset patch in
**Scope (split before/while implementing — do not land as one mega-PR):** **Scope (split before/while implementing — do not land as one mega-PR):**
1. **Design** — default `swapSize` (= RAM? round up?), zram size/priority 1. **Design** — default `swapSize` (= RAM? round up?), LUKS+systemd-initrd
vs disk prio, LUKS+systemd-initrd resume (already used), what when resume (already used), what when install chose `swapSize=0`, migration
install chose `swapSize=0`, migration for existing machines. for existing machines. (zram priority-vs-disk already settled + shipped.)
2. **Module** — NixOS: enable zram by default; ensure hibernation knobs 2. **Module** — NixOS: ensure hibernation knobs are coherent when swapfile
are coherent when swapfile exists (resume device/offset); Power menu exists (resume device/offset); Power menu Hibernate already present —
Hibernate already present — must work when swap is configured. must work when swap is configured. (zram already default-on in oom.nix.)
3. **Installer** — default new installs to hibernation-ready `@swap` 3. **Installer** — default new installs to hibernation-ready `@swap`
swapfile (encrypted with root); keep `0` = no swap opt-out; document swapfile (encrypted with root); keep `0` = no swap opt-out; document
migration. migration.
@@ -62,11 +68,9 @@ Installer path already sketches this (`@swap` + resume_offset patch in
(`runNixOSTest` or install-test style) — encrypted root + BTRFS (`runNixOSTest` or install-test style) — encrypted root + BTRFS
`@swap` swapfile + resume_offset; prove hibernate→resume path at `@swap` swapfile + resume_offset; prove hibernate→resume path at
least as far as QEMU allows (fail the item if only “config least as far as QEMU allows (fail the item if only “config
evaluates”). Also assert zram is active with expected priority evaluates”).
relative to the swapfile.
- **V3:** real laptop (this LUKS+@swap layout): Hibernate → power off - **V3:** real laptop (this LUKS+@swap layout): Hibernate → power off
→ resume session; queue HARDWARE-QUEUE. zram under load is → resume session; queue HARDWARE-QUEUE.
secondary.
**Out of scope:** TLP; formatter. Prefer boring NixOS knobs; extend the **Out of scope:** TLP; formatter. Prefer boring NixOS knobs; extend the
existing installer swapfile story rather than inventing a second layout. existing installer swapfile story rather than inventing a second layout.

View File

@@ -17,6 +17,22 @@ Template:
--- ---
## 2026-07-10 — #76 zram slice: compressed-RAM swap default
- **Task:** BACKLOG #76 (`[big]`) — split; took the low-risk zram-default slice.
- **Did:** `zramSwap` on by default in `modules/nixos/oom.nix` (zstd, 50% RAM,
priority 100) as the memory-pressure layer before earlyoom, following the
earlyoom mkDefault precedent (no toggle). High priority keeps day-to-day
paging in zram so a disk swapfile stays reserved for #76's hibernate image.
Added `checks.zram-swap` runNixOSTest (imports oom.nix). Split #76 in
BACKLOG → remainder is the hibernation half (swapfile/resume/installer).
- **Verified:** **V2**`nix build .#checks…zram-swap` PASS: /dev/zram0 active
in /proc/swaps, `[zstd]` comp_algorithm, priority 100. **V1** full distro
toplevel builds. **V0** flake check --no-build green.
- **Pending:** hibernation half of #76 still open (needs hibernate→resume VM
test + V3 laptop). No V3 for zram (under-load tuning is secondary).
- **Next suggestion:** #76 hibernation half — design default swapSize + the
resume/installer wiring; it's the boot-critical, V2-hibernate + V3 part.
## 2026-07-10 — #41 hardware-confirmed: OpenFile portal floats (V3 close) ## 2026-07-10 — #41 hardware-confirmed: OpenFile portal floats (V3 close)
- **Task:** BACKLOG #41 — final on-hardware confirmation (Bernardo reported PASS). - **Task:** BACKLOG #41 — final on-hardware confirmation (Bernardo reported PASS).
- **Did:** #41 already out of NEXT + portal rule shipped - **Did:** #41 already out of NEXT + portal rule shipped

View File

@@ -659,6 +659,31 @@
''; '';
}; };
# zram compressed-RAM swap (oom.nix, BACKLOG #76 slice): enabled
# by default as the memory-pressure layer *before* earlyoom. Prove
# the device comes up as an active swap, uses zstd, and carries the
# high priority that keeps day-to-day paging off any disk swapfile
# (which #76 reserves for the hibernate image). Imports oom.nix
# directly so the check tracks the module, not a hand-copy.
zram-swap = pkgs.testers.runNixOSTest {
name = "nomarchy-zram-swap";
nodes.machine = { ... }: {
imports = [ ./modules/nixos/oom.nix ];
virtualisation.memorySize = 1024;
};
testScript = ''
# zram-generator brings the device up as swap shortly after boot.
machine.wait_until_succeeds("grep -q /dev/zram0 /proc/swaps", timeout=60)
# Configured compression algorithm is the selected one ([zstd]).
machine.succeed("grep -q '\\[zstd\\]' /sys/block/zram0/comp_algorithm")
# High priority (100) so the kernel fills zram before any disk
# swap the ordering #76's hibernate image depends on.
machine.succeed(
"awk '$1==\"/dev/zram0\" && $5==100 {ok=1} END{exit !ok}' /proc/swaps"
)
'';
};
# Snapshot GUI canary: btrfs-assistant crashes in libbtrfsutil's # Snapshot GUI canary: btrfs-assistant crashes in libbtrfsutil's
# UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed # UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed
# upstream after 6.17.1) but runs fine as root — which is how the # upstream after 6.17.1) but runs fine as root — which is how the

View File

@@ -14,9 +14,28 @@
# the build step, keep the session". nixpkgs default-enables oomd in an # the build step, keep the session". nixpkgs default-enables oomd in an
# inert state (no slices monitored); it's disabled outright below so # inert state (no slices monitored); it's disabled outright below so
# there is exactly one owner of the OOM story. # there is exactly one owner of the OOM story.
#
# zram sits one layer earlier: a compressed RAM swap that absorbs
# day-to-day memory pressure (a browser's idle tabs, a big eval's cold
# pages) before it ever reaches the earlyoom threshold — more headroom
# on the same RAM, no disk. High swap priority so the kernel fills zram
# first; any real disk swapfile stays reserved for the hibernate image
# (BACKLOG #76's other half — hibernation can't resume from volatile
# zram, so day-to-day paging must not consume the disk swap). zstd @ 50%
# RAM are the nixpkgs defaults, kept explicit for reproducibility.
{ lib, ... }: { lib, ... }:
{ {
zramSwap = {
enable = lib.mkDefault true;
algorithm = lib.mkDefault "zstd";
memoryPercent = lib.mkDefault 50;
# Beats any disk swapfile's auto-assigned (negative) priority, so
# day-to-day paging lands in zram and the disk swap is left for
# hibernation. See the header note and BACKLOG #76.
priority = lib.mkDefault 100;
};
services.earlyoom = { services.earlyoom = {
enable = lib.mkDefault true; enable = lib.mkDefault true;