diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 6fc7ad1..5c08657 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -28,12 +28,18 @@ in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in ## NEXT -### 76. Hibernation default + zram `[big]` +### 76. Hibernation default `[big]` **Product intent (Bernardo 2026-07-10):** hibernation on by default; zram on alongside for live memory pressure. Disk swap ≥ RAM for the hibernate image; zram high-priority for day-to-day swap (not sufficient 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 yet):** LUKS (`crypto_LUKS` → mapper `crypted`) holds one BTRFS with `@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):** -1. **Design** — default `swapSize` (= RAM? round up?), zram size/priority - vs disk prio, LUKS+systemd-initrd resume (already used), what when - install chose `swapSize=0`, migration for existing machines. -2. **Module** — NixOS: enable zram by default; ensure hibernation knobs - are coherent when swapfile exists (resume device/offset); Power menu - Hibernate already present — must work when swap is configured. +1. **Design** — default `swapSize` (= RAM? round up?), LUKS+systemd-initrd + resume (already used), what when install chose `swapSize=0`, migration + for existing machines. (zram priority-vs-disk already settled + shipped.) +2. **Module** — NixOS: ensure hibernation knobs are coherent when swapfile + exists (resume device/offset); Power menu Hibernate already present — + must work when swap is configured. (zram already default-on in oom.nix.) 3. **Installer** — default new installs to hibernation-ready `@swap` swapfile (encrypted with root); keep `0` = no swap opt-out; document migration. @@ -62,11 +68,9 @@ Installer path already sketches this (`@swap` + resume_offset patch in (`runNixOSTest` or install-test style) — encrypted root + BTRFS `@swap` swapfile + resume_offset; prove hibernate→resume path at least as far as QEMU allows (fail the item if only “config - evaluates”). Also assert zram is active with expected priority - relative to the swapfile. + evaluates”). - **V3:** real laptop (this LUKS+@swap layout): Hibernate → power off - → resume session; queue HARDWARE-QUEUE. zram under load is - secondary. + → resume session; queue HARDWARE-QUEUE. **Out of scope:** TLP; formatter. Prefer boring NixOS knobs; extend the existing installer swapfile story rather than inventing a second layout. diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 9390ee4..374bc73 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -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) - **Task:** BACKLOG #41 — final on-hardware confirmation (Bernardo reported PASS). - **Did:** #41 already out of NEXT + portal rule shipped diff --git a/flake.nix b/flake.nix index 69013b5..34f1396 100644 --- a/flake.nix +++ b/flake.nix @@ -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 # UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed # upstream after 6.17.1) but runs fine as root — which is how the diff --git a/modules/nixos/oom.nix b/modules/nixos/oom.nix index 13f378e..bc1419e 100644 --- a/modules/nixos/oom.nix +++ b/modules/nixos/oom.nix @@ -14,9 +14,28 @@ # the build step, keep the session". nixpkgs default-enables oomd in an # inert state (no slices monitored); it's disabled outright below so # 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, ... }: { + 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 = { enable = lib.mkDefault true;