Size zram the way Fedora does

zram-size = min(ram, 8192) is Fedora's default since F34, where
zram-fraction=1.0 with max-zram-size=8192 has run on desktops for five
years. The previous min(ram / 2, 8192) halved that on every machine
below 16G with nothing behind the fraction; at 16G and above the two
already agree.

A changed size only reaches a running device on the next boot, so the
migration resizes an empty device now and asks for a reboot otherwise.
It asks the generator what the config evaluates to rather than
repeating the expression here, and does neither when the device already
matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-26 12:58:02 -07:00
co-authored by Claude Opus 5
parent f01acc1de9
commit 187c2ecf7f
3 changed files with 159 additions and 5 deletions
@@ -1,10 +1,17 @@
# Compressed swap in RAM. zstd averages around 3:1, so the worst case for a # Compressed swap in RAM, sized the way Fedora has shipped it since F34: all of
# full device is roughly a third of the size below. # memory, capped at 8G. Fedora weighed the worry that incompressible pages would
# tie up RAM against five years of desktops reporting no trouble from it, which
# is more field evidence than we could gather on our own.
# #
# The generator's own default caps this at 4G, which sends machines with a lot # The cap is what bounds the downside. A browser-heavy desktop can drive zstd
# of memory to the disk swapfile far earlier than they need to go. # down near 1.1:1, where the device backs far less than it advertises, and 8G
# keeps that bounded on any machine large enough to notice. The disk swapfile
# sits behind it at pri=0 for everything past that.
#
# Fedora spells this zram-fraction = 1.0 with max-zram-size = 8192; both keys
# are deprecated in favour of zram-size, which computes the same numbers.
[zram0] [zram0]
zram-size = min(ram / 2, 8192) zram-size = min(ram, 8192)
compression-algorithm = zstd compression-algorithm = zstd
# Above the pri=0 that omarchy-hibernation-setup gives the disk swapfile, so # Above the pri=0 that omarchy-hibernation-setup gives the disk swapfile, so
+38
View File
@@ -0,0 +1,38 @@
echo "Resize zram to match the shipped config"
# 90-omarchy.conf changed the device size. The migration that first applied the
# zram tuning is one-shot, so machines that already ran it have the new file on
# disk and the old device still running; nothing would pick the size up until
# something else rebooted them.
zram_disksize=${OMARCHY_ZRAM_DISKSIZE:-/sys/block/zram0/disksize}
swaps=${OMARCHY_SWAPS:-/proc/swaps}
# The size is an expression, and the generator is the only thing that evaluates
# it. Ask it what the shipped config comes to rather than repeating the
# arithmetic here, where the two would drift apart on the next tuning change.
units=$(mktemp -d)
trap 'rm -rf "$units"' EXIT
desired_mb=$(/usr/lib/systemd/system-generators/zram-generator "$units" 2>&1 |
grep -oP '/dev/zram0 with \K[0-9]+') || true
actual_bytes=$(cat "$zram_disksize" 2>/dev/null) || actual_bytes=0
# A device already at the right size needs neither a restart nor a reboot,
# whether an earlier boot picked the config up or another user got here first.
if [[ -n $desired_mb && $actual_bytes == $((desired_mb * 1024 * 1024)) ]]; then
exit 0
fi
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.
zram_used=$(awk '$1 == "/dev/zram0" {print $4}' "$swaps")
if [[ ${zram_used:-0} == 0 ]] && sudo systemctl restart dev-zram0.swap; then
exit 0
fi
fi
omarchy-state set reboot-required
+109
View File
@@ -0,0 +1,109 @@
#!/bin/bash
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command /usr/lib/systemd/system-generators/zram-generator
migration=$(grep -rl 'Resize zram to match the shipped config' "$ROOT/migrations" | head -n 1 || true)
[[ -n $migration ]] || fail "zram resize migration exists"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# The migration shells out to sudo, systemctl and omarchy-state. Stub all three
# so the test never touches the real system, and record what each run did.
stub_bin="$TMPDIR/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
cat >"$stub_bin/systemctl" <<'STUB'
#!/bin/bash
echo "systemctl $*" >>"$ACTIONS"
[[ ${SYSTEMCTL_FAIL:-0} == 1 ]] && exit 1
exit 0
STUB
cat >"$stub_bin/omarchy-state" <<'STUB'
#!/bin/bash
echo "omarchy-state $*" >>"$ACTIONS"
STUB
chmod +x "$stub_bin/sudo" "$stub_bin/systemctl" "$stub_bin/omarchy-state"
# ZRAM_GENERATOR_ROOT redirects both the config search and /proc/meminfo, so the
# migration's own generator call resolves against this fixture instead of the
# host. 16G of RAM against the shipped config is a 8192MB device.
gen_root="$TMPDIR/genroot"
mkdir -p "$gen_root/usr/lib/systemd/zram-generator.conf.d" "$gen_root/proc"
cp "$ROOT/default/systemd/zram-generator.conf.d/90-omarchy.conf" \
"$gen_root/usr/lib/systemd/zram-generator.conf.d/"
printf 'MemTotal: %d kB\n' $((16 * 1024 * 1024)) >"$gen_root/proc/meminfo"
desired_bytes=$((8192 * 1024 * 1024))
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
# whole chain on a non-zero exit, so match that invocation exactly.
run_migration() {
local disksize="$1" used="$2" fail_reload="${3:-0}"
printf '%s' "$disksize" >"$TMPDIR/disksize"
printf 'Filename\tType\tSize\tUsed\tPriority\n' >"$TMPDIR/swaps"
[[ -n $used ]] &&
printf '/dev/zram0 partition 8388604 %s 100\n' "$used" >>"$TMPDIR/swaps"
: >"$TMPDIR/actions"
PATH="$stub_bin:$PATH" \
ACTIONS="$TMPDIR/actions" \
SYSTEMCTL_FAIL="$fail_reload" \
ZRAM_GENERATOR_ROOT="$gen_root" \
OMARCHY_ZRAM_DISKSIZE="$TMPDIR/disksize" \
OMARCHY_SWAPS="$TMPDIR/swaps" \
bash -euo pipefail "$migration" >/dev/null ||
fail "migration exits clean (disksize=$disksize used=$used reload_fail=$fail_reload)"
}
did() { grep -qF "$1" "$TMPDIR/actions"; }
# Already the size the config asks for: the device is correct however it got
# there, so the migration must not restart it or ask for a reboot.
run_migration "$desired_bytes" 4193612
[[ -s $TMPDIR/actions ]] && fail "correctly sized device is left alone" \
"expected no privileged calls, got: $(cat "$TMPDIR/actions")"
pass "correctly sized device is left alone"
pass "correctly sized device asks for no reboot"
# Right size but still in use, and the reload never even happens.
run_migration "$desired_bytes" 0
did "systemctl restart" && fail "correctly sized empty device is left alone"
pass "correctly sized empty device is left alone"
# Wrong size and empty: safe to resize now.
run_migration $((4096 * 1024 * 1024)) 0
did "systemctl restart dev-zram0.swap" || fail "empty device is restarted"
did "omarchy-state set reboot-required" && fail "empty device asks for no reboot"
pass "empty device is restarted"
pass "empty device asks for no reboot"
# Wrong size with pages stored: resizing would fault them all back in, so defer.
run_migration $((4096 * 1024 * 1024)) 4193612
did "systemctl restart" && fail "device in use is not restarted"
did "omarchy-state set reboot-required" || fail "device in use asks for a reboot"
pass "device in use is not restarted"
pass "device in use asks for a reboot"
# No zram device at all reads as empty, and the restart brings it up.
run_migration "" ""
did "systemctl restart dev-zram0.swap" || fail "absent device is created"
pass "absent device is created"
# A failed daemon-reload must fall back to asking for a reboot.
run_migration $((4096 * 1024 * 1024)) 0 1
did "systemctl restart" && fail "failed reload does not restart"
did "omarchy-state set reboot-required" || fail "failed reload asks for a reboot"
pass "failed reload does not restart"
pass "failed reload asks for a reboot"