From 4dcbed7728d441e7fc8d4fd83c4e8971d8b5188c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 24 Jul 2026 19:45:35 -0700 Subject: [PATCH] Tune swap on zram instead of leaving it at kernel defaults Omarchy installed zram-generator but never shipped a config for it, so the only zram tuning any machine had was whatever the ISO wrote once, and the VM sysctls were untouched kernel defaults aimed at spinning disks. The generator's own default caps the device at 4G, which on a large machine sends reclaim to the hibernation swapfile far earlier than it needs to go. Ship min(ram / 2, 8192) instead, matching what Fedora settles on, and state the priority explicitly since it has to sit above the pri=0 that omarchy-hibernation-setup gives the disk swapfile. The four sysctls follow from swap being RAM rather than a disk. swappiness above 100 says evicting an anonymous page beats dropping a page-cache page that would have to be read back, which is true once swap is compressed memory. page-cluster drops to one page per fault because the default of 8 buys readahead for a seek zram doesn't have and pays a decompression for each page. Zeroing watermark_boost_factor stops fragmentation from producing reclaim bursts while memory is still free, and raising watermark_scale_factor gives kswapd room to reclaim in the background instead of letting allocations stall in direct reclaim. The last two are what actually addresses the stutter people notice and misread as swap being used too eagerly: proactive swapping to zram is the cure, and synchronous direct reclaim is the disease. Every ISO-installed machine already has an unowned zram-generator.conf, which pacman would refuse to overwrite, so both upgrade paths need it listed. Co-Authored-By: Claude Opus 5 (1M context) --- bin/omarchy-update-system-pkgs | 1 + bin/omarchy-upgrade-to-quattro | 1 + etc/sysctl.d/99-omarchy-sysctl.conf | 21 +++++++++++++++++++++ etc/systemd/zram-generator.conf | 12 ++++++++++++ migrations/1784961000.sh | 24 ++++++++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 etc/systemd/zram-generator.conf create mode 100644 migrations/1784961000.sh diff --git a/bin/omarchy-update-system-pkgs b/bin/omarchy-update-system-pkgs index ac12a999..564f9548 100755 --- a/bin/omarchy-update-system-pkgs +++ b/bin/omarchy-update-system-pkgs @@ -37,6 +37,7 @@ sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \ --overwrite '/etc/systemd/system/plocate-updatedb.service.d/ac-only.conf' \ --overwrite '/etc/systemd/system.conf.d/20-omarchy-nofile.conf' \ --overwrite '/etc/systemd/user.conf.d/20-omarchy-nofile.conf' \ + --overwrite '/etc/systemd/zram-generator.conf' \ --overwrite '/usr/lib/systemd/system-sleep/unmount-fuse' \ --overwrite '/usr/share/plymouth/themes/omarchy/*' \ --overwrite '/usr/share/sddm/hyprland.lua' \ diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index 46741bb4..22412897 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -1087,6 +1087,7 @@ run_final_system_package_upgrade() { --overwrite '/etc/systemd/system/plocate-updatedb.service.d/ac-only.conf' \ --overwrite '/etc/systemd/system.conf.d/20-omarchy-nofile.conf' \ --overwrite '/etc/systemd/user.conf.d/20-omarchy-nofile.conf' \ + --overwrite '/etc/systemd/zram-generator.conf' \ --overwrite '/usr/lib/systemd/system-sleep/unmount-fuse' \ --overwrite '/usr/share/plymouth/themes/omarchy/*' \ --overwrite '/usr/share/sddm/hyprland.lua' \ diff --git a/etc/sysctl.d/99-omarchy-sysctl.conf b/etc/sysctl.d/99-omarchy-sysctl.conf index b8845467..d5ae09ea 100644 --- a/etc/sysctl.d/99-omarchy-sysctl.conf +++ b/etc/sysctl.d/99-omarchy-sysctl.conf @@ -1,2 +1,23 @@ # Solve common flakiness with SSH (MTU discovery on flaky links). net.ipv4.tcp_mtu_probing=1 + +# Tune reclaim for swap on zram, which is orders of magnitude faster than the +# disk swapfile these defaults assume. + +# Anything above 100 tells the kernel that evicting an anonymous page is +# cheaper than dropping a page-cache page it would have to re-read from disk. +# With a compressed RAM device that is true, so the disk-era default of 60 +# leaves the page cache starved. +vm.swappiness=180 + +# Read one page per swap-in fault. The default of 8 pays for a seek that zram +# doesn't have, and every extra page costs a separate decompression. +vm.page-cluster=0 + +# Don't let external fragmentation raise the watermarks, which produces +# reclaim bursts while memory is still free. +vm.watermark_boost_factor=0 + +# Keep ~1.25% of memory free instead of 0.1%, so kswapd reclaims in the +# background rather than letting allocations stall in direct reclaim. +vm.watermark_scale_factor=125 diff --git a/etc/systemd/zram-generator.conf b/etc/systemd/zram-generator.conf new file mode 100644 index 00000000..d794ad16 --- /dev/null +++ b/etc/systemd/zram-generator.conf @@ -0,0 +1,12 @@ +# Compressed swap in RAM. zstd averages around 3:1, so the worst case for a +# full device is roughly a third of the size below. +# +# The generator's own default caps this at 4G, which sends machines with a lot +# of memory to the disk swapfile far earlier than they need to go. +[zram0] +zram-size = min(ram / 2, 8192) +compression-algorithm = zstd + +# Above the pri=0 that omarchy-hibernation-setup gives the disk swapfile, so +# zram absorbs everything until it's full. +swap-priority = 100 diff --git a/migrations/1784961000.sh b/migrations/1784961000.sh new file mode 100644 index 00000000..75b631da --- /dev/null +++ b/migrations/1784961000.sh @@ -0,0 +1,24 @@ +echo "Tune reclaim for swap on zram" + +# Everything here only applies the shipped config early; boot picks it up +# regardless. Nothing is worth failing the migration chain over, so each step +# falls back to asking for a reboot. + +# Load our file specifically rather than --system, which returns nonzero for +# any invalid key in any admin sysctl file on the machine. +sudo sysctl -p /etc/sysctl.d/99-omarchy-sysctl.conf >/dev/null || true + +if sudo systemctl daemon-reload; then + # Resizing swaps the device off first, which faults every stored page back + # into memory. That's only cheap while it's empty, so a device under + # pressure keeps its old size until the next boot. A device that doesn't + # exist yet reads as empty, which is what we want: the restart brings it up + # against the unit daemon-reload just generated. + zram_used=$(awk '$1 == "/dev/zram0" {print $4}' /proc/swaps) + + if [[ ${zram_used:-0} == 0 ]] && sudo systemctl restart dev-zram0.swap; then + exit 0 + fi +fi + +omarchy-state set reboot-required