Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions default/systemd/user/app.slice.d/10-oomd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Make user apps the only thing systemd-oomd is allowed to kill.
#
# Hyprland runs in session.slice, as wayland-wm@hyprland.desktop.service, while
# everything launched through uwsm-app lands in app.slice/app-*.scope. Marking
# only app.slice as a kill candidate means the compositor is structurally
# ineligible: oomd takes the browser or terminal that caused the pressure, and
# the session survives to show the notification about it. Setting this on
# user@.service instead would put the compositor back in the candidate pool.
#
# Swap kill is a backstop for the slower shape of the same problem, where swap
# fills before pressure spikes. It uses the global SwapUsedLimit (90%) from
# /etc/systemd/oomd.conf.d/10-omarchy.conf, which also carries the pressure
# thresholds.
[Slice]
ManagedOOMMemoryPressure=kill
ManagedOOMSwap=kill
1 change: 1 addition & 0 deletions docs/file-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ default/** ──► omarchy-settings /usr/share/omarchy
├─ xdg-terminal-exec/*.list /usr/share/xdg-terminal-exec/
├─ applications/mimeapps.list /usr/share/applications/mimeapps.list
├─ systemd/user/*.{service,path} /usr/lib/systemd/user/
├─ systemd/user/app.slice.d/10-oomd.conf /usr/lib/systemd/user/app.slice.d/
├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/
├─ systemd/zram-generator.conf.d/90-omarchy.conf /usr/lib/systemd/zram-generator.conf.d/
├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/
Expand Down
21 changes: 21 additions & 0 deletions etc/systemd/oomd.conf.d/10-omarchy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Without an OOM daemon there is nothing between "memory is tight" and
# "processes start dying at random". The kernel OOM killer only fires once an
# allocation has already failed outright, which is far too late for the
# desktop: the machine thrashes in reclaim until something fails somewhere
# fatal. Seen in practice as Hyprland taking SIGBUS mid-memcpy from a Wayland
# client's shm pool, because the client was being torn down by the same memory
# pressure. One app misbehaving should not cost the whole session.
#
# systemd-oomd keys on PSI stall time rather than on free pages, so it acts
# while the machine is still thrashing instead of after an allocation failed.
# Deliberately not earlyoom: that fires only when free RAM *and* free swap are
# both under threshold, so a large, mostly-unused swapfile keeps it asleep
# through exactly this failure.
#
# 50% means half of a ten-second window was spent stalled on memory reclaim.
# Held for 20s straight, the desktop is already unusable and losing one app is
# the cheaper outcome. Which cgroups are eligible is set separately, in
# /usr/lib/systemd/user/app.slice.d/10-oomd.conf.
[OOM]
DefaultMemoryPressureDurationSec=20s
DefaultMemoryPressureLimit=50%
4 changes: 4 additions & 0 deletions install/config/enable-services.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ systemctl enable NetworkManager.service
systemctl mask NetworkManager-wait-online.service
systemctl enable power-profiles-daemon.service
systemctl enable sddm.service
# Kill one runaway app scope instead of letting reclaim thrashing take the
# whole session down. [Install] pulls in systemd-oomd.socket via Also=, which
# is what the user manager reports app.slice candidacy over.
systemctl enable systemd-oomd.service
31 changes: 31 additions & 0 deletions migrations/1785424256.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
echo "Let systemd-oomd kill a runaway app instead of the whole session"

# New installs get this from install/config/enable-services.sh. Existing ones
# have never had an OOM daemon, so nothing stands between memory pressure and
# the session falling over.

as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}

# Machine-wide, so a second user on the same box finds it already done.
if systemctl is-enabled --quiet systemd-oomd.service 2>/dev/null; then
# Already enabled means it started before the package delivered our
# thresholds in /etc/systemd/oomd.conf.d/, and oomd only reads that at
# startup. Restart so it doesn't run with stale limits until reboot.
as_root systemctl try-restart systemd-oomd.service >/dev/null 2>&1 || true
else
as_root systemctl enable --now systemd-oomd.service >/dev/null 2>&1 ||
echo "Could not enable systemd-oomd.service; memory pressure will still take the session down."
fi

# Pick up /usr/lib/systemd/user/app.slice.d/10-oomd.conf without waiting for
# the next login. That drop-in is what marks app.slice as a kill candidate;
# until the user manager reloads and reports it, oomd is running with nothing
# to act on. An `omarchy update` over SSH or from a TTY has no user manager to
# reload, and there the next graphical login picks it up on its own.
systemctl --user daemon-reload >/dev/null 2>&1 || true
31 changes: 31 additions & 0 deletions test/shell.d/systemd-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,34 @@ grep -F 'omarchy-fcitx5.service' "$first_run_units" >/dev/null ||
grep -F 'fcitx5' "$ROOT/default/hypr/autostart.lua" >/dev/null &&
fail "fcitx5 is autostarted from Hyprland; an unsupervised launch dies silently and takes every compose sequence with it"
pass "fcitx5 runs supervised, so a lost input method comes back instead of killing XCompose until logout"

oomd_slice="$ROOT/default/systemd/user/app.slice.d/10-oomd.conf"
grep -Fx 'ManagedOOMMemoryPressure=kill' "$oomd_slice" >/dev/null ||
fail "nothing is a kill candidate, so systemd-oomd watches the machine thrash and never acts"
grep -Fx 'ManagedOOMSwap=kill' "$oomd_slice" >/dev/null ||
fail "no swap backstop for the slower shape of the same failure"

# Hyprland lives in session.slice/wayland-wm@hyprland.desktop.service. Marking
# any ancestor of that as a kill candidate puts the compositor back in the
# victim pool, which is the crash this whole thing exists to prevent.
candidates=$(grep -rlE '^ManagedOOM(MemoryPressure|Swap)=kill' "$ROOT/default/systemd" "$ROOT/etc/systemd" 2>/dev/null || true)
[[ $candidates == "$oomd_slice" ]] ||
fail "systemd-oomd kill candidacy is set outside app.slice, which can select the compositor: $candidates"
pass "only user app scopes are systemd-oomd kill candidates"

oomd_conf="$ROOT/etc/systemd/oomd.conf.d/10-omarchy.conf"
grep -Fx 'DefaultMemoryPressureLimit=50%' "$oomd_conf" >/dev/null ||
fail "no pressure limit; the 60% default rides thrashing longer than a desktop stays usable"
grep -Fx 'DefaultMemoryPressureDurationSec=20s' "$oomd_conf" >/dev/null ||
fail "no pressure duration set for the tightened limit"
pass "systemd-oomd acts on sustained memory stall"

grep -Fx 'systemctl enable systemd-oomd.service' "$ROOT/install/config/enable-services.sh" >/dev/null ||
fail "new installs ship the oomd drop-ins with the daemon that reads them disabled"

oomd_migration=$(grep -rl 'systemd-oomd.service' "$ROOT/migrations" | head -n 1 || true)
[[ -n $oomd_migration ]] ||
fail "existing installs never enable systemd-oomd; enable-services.sh only runs at install time"
grep -F 'systemctl --user daemon-reload' "$oomd_migration" >/dev/null ||
fail "migration leaves the user manager unaware of app.slice candidacy until the next login"
pass "existing installs enable systemd-oomd and report app.slice without a relogin"