perf(idle-off): replace controller-watch fork storm with a single systemd-inhibit

turn_off_auto_idle_screen_shutdown.sh --watch-controller forked 4 xset +
1 xdotool per joystick event (blocking on a dd read, debounced by sleep
0.3) — ~21 procs/s while a controller was connected during gaming. The
xset poking was redundant: startup already disables DPMS/blanking.

Replace reset_idle_activity / watch_js_device / the polling watcher with
one long-lived `systemd-inhibit --what=idle:sleep` held only while a
/dev/input/js* device is present, re-evaluated on udev input add/remove
events (event-driven; 30s presence-poll fallback). EXIT+INT/TERM traps
release the inhibitor on every termination path.

Verified live: subtree stays {systemd-inhibit, udevadm} with zero
dd/xset/xdotool; exactly one inhibitor held; clean release on SIGTERM,
no orphans. Takes that loop from ~21 forks/s to 0.

Behavior change: keeps the session awake while a controller is connected
(not only during active input).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-06-04 18:14:07 +02:00
parent 20d5d1f89b
commit fcd3f7ed2f
2 changed files with 113 additions and 71 deletions

View File

@ -0,0 +1,38 @@
{
"intent": "Eliminate the fork storm from turn_off_auto_idle_screen_shutdown.sh's controller watcher. Previously, while a game controller was connected, each joystick event forked 4 xset + 1 xdotool + a dd read + a sleep (~21 forks/s during gaming). The session must still be kept awake while a controller is connected, but with no per-event forks.",
"scope": [
"linux_configuration/scripts/single_use/utils/turn_off_auto_idle_screen_shutdown.sh",
"Non-goal: the one-shot idle-disable steps (xset/gsettings at startup) are unchanged",
"Non-goal: external chronyc forks (~1/s) which originate outside this repo"
],
"changes": [
"Replaced reset_idle_activity + watch_js_device + the polling start_controller_watchers with a single long-lived `systemd-inhibit --what=idle:sleep` lock held only while a /dev/input/js* device is present.",
"Controller presence is re-evaluated on udev input add/remove events (event-driven, no polling), with a 30 s presence-poll fallback when udevadm is absent.",
"Cleanup hardened: EXIT plus INT/TERM traps release the inhibitor on any termination path; presence check is a pure-bash glob (zero forks)."
],
"verification": [
{
"command": "bash turn_off_auto_idle_screen_shutdown.sh --watch-controller (with js0 connected)",
"result": "pass",
"evidence": "systemd-inhibit --list shows exactly one 'game controller connected' lock; the watcher subtree stays {systemd-inhibit, udevadm} with zero dd/xset/xdotool over a 3 s sample, versus the old watcher still churning dd."
},
{
"command": "kill -TERM <watcher>; systemd-inhibit --list",
"result": "pass",
"evidence": "Watcher exits cleanly, 0 inhibitors remain, no orphaned systemd-inhibit reparented to init."
},
{
"command": "bash -n + shellcheck",
"result": "pass",
"evidence": "syntax OK; shellcheck clean."
}
],
"risks": [
"Semantics changed from 'awake only during active controller input' to 'awake while a controller is connected'; a permanently-plugged controller will keep the session from auto-idling.",
"If the holding process is SIGKILLed outside a systemd cgroup (e.g. i3 crash), the inhibitor lingers until session end; EXIT trap covers normal termination."
],
"rollback": [
"git checkout the script to restore the previous controller watcher.",
"Re-run with --watch-controller and confirm whether xset/dd churn returns."
]
}

View File

@ -12,7 +12,7 @@
# Optional persistence (requires sudo): # Optional persistence (requires sudo):
# --persist-systemd -> Set IdleAction=ignore in /etc/systemd/logind.conf and restart logind # --persist-systemd -> Set IdleAction=ignore in /etc/systemd/logind.conf and restart logind
# Optional activity watcher: # Optional activity watcher:
# --watch-controller -> Treat game controller (e.g., Xbox) input as user activity to keep session awake # --watch-controller -> Hold a systemd idle/sleep inhibitor while a game controller is connected (keeps the session awake, fork-free)
# #
# Notes: # Notes:
# - This script focuses on keeping the screen on and unlocked. Use with care on shared systems. # - This script focuses on keeping the screen on and unlocked. Use with care on shared systems.
@ -42,7 +42,7 @@ Disables idle detection, screen blanking, and auto-lock for the current session.
Options: Options:
--persist-systemd Also set IdleAction=ignore in /etc/systemd/logind.conf (needs sudo) --persist-systemd Also set IdleAction=ignore in /etc/systemd/logind.conf (needs sudo)
--watch-controller Watch game controllers and generate activity to keep the session awake --watch-controller Hold an idle/sleep inhibitor while a game controller is connected
-h, --help Show this help and exit -h, --help Show this help and exit
What this does: What this does:
@ -52,7 +52,7 @@ What this does:
- Sway: kill swayidle if running - Sway: kill swayidle if running
- TTY: setterm -blank 0 -powersave off -powerdown 0 - TTY: setterm -blank 0 -powersave off -powerdown 0
- Optional: systemd-logind IdleAction=ignore - Optional: systemd-logind IdleAction=ignore
- Optional: watch controller input and reset idle timers - Optional: hold a systemd idle inhibitor while a controller is connected
EOF EOF
exit 0 exit 0
;; ;;
@ -136,76 +136,85 @@ disable_tty_idle() {
fi fi
} }
reset_idle_activity() { # PID of the single long-lived idle/sleep inhibitor we hold while a controller
# Trigger activity hints depending on environment # is connected. Empty when no inhibitor is active.
if [[ -n ${DISPLAY:-} ]]; then inhibit_pid=""
if has_cmd xset; then
xset s reset || true start_idle_inhibit() {
xset -dpms || true # Hold one systemd idle/sleep inhibitor for the whole time a controller is
xset s off || true # connected. This replaces the previous per-event fork storm (4 xset + an
xset s noblank || true # xdotool + a dd read + a sleep on *every* joystick event, ~21 forks/s while
fi # gaming): a single long-lived process keeps logind from idling, suspending,
if has_cmd xdotool; then # or locking, while X11 blanking stays off thanks to the one-shot
# No-op mousemove to generate X11 activity without visible movement # disable_x11_idle above. Idempotent — a live inhibitor is reused.
xdotool mousemove_relative -- 0 0 2> /dev/null || true if [[ -n $inhibit_pid ]] && kill -0 "$inhibit_pid" 2> /dev/null; then
fi return 0
fi fi
systemd-inhibit --what=idle:sleep --who="idle-off" \
--why="game controller connected" sleep infinity &
inhibit_pid=$!
log "Holding idle/sleep inhibitor (pid ${inhibit_pid}) while a controller is connected"
} }
watch_js_device() { stop_idle_inhibit() {
local dev="$1" if [[ -z $inhibit_pid ]]; then
log "Watching controller device: $dev" return 0
while :; do
if [[ ! -e $dev ]]; then
warn "Device disappeared: $dev"
break
fi
# Joystick API event size is 8 bytes; block until an event arrives
if dd if="$dev" bs=8 count=1 status=none of=/dev/null; then
reset_idle_activity
# Debounce bursts of events
sleep 0.3
else
# On read error (e.g., permission), backoff
sleep 1
fi fi
kill "$inhibit_pid" 2> /dev/null || true
wait "$inhibit_pid" 2> /dev/null || true
inhibit_pid=""
log "Released idle/sleep inhibitor; normal idle behaviour resumes"
}
controller_connected() {
# Pure-bash glob check — zero forks. True if any /dev/input/js* node exists.
local dev
for dev in /dev/input/js*; do
[[ -e $dev ]] && return 0
done done
return 1
}
sync_inhibit_to_controllers() {
# Hold the inhibitor exactly when a controller is present.
if controller_connected; then
start_idle_inhibit
else
stop_idle_inhibit
fi
} }
start_controller_watchers() { start_controller_watchers() {
# Attempt to watch all /dev/input/js* devices; rescan periodically for new ones # Event-driven and fork-free in the hot path: react only to input-device
declare -A pids # add/remove (rare udev events), never to individual joystick *input* events,
# and hold a single systemd-inhibit lock while a controller is present.
# Initial permission check if ! has_cmd systemd-inhibit; then
local any_js=false any_readable=false warn "systemd-inhibit not found; cannot hold an idle inhibitor"
for dev in /dev/input/js*; do return 0
[[ -e $dev ]] || continue
any_js=true
if [[ -r $dev ]]; then any_readable=true; fi
done
if [[ $any_js == true && $any_readable == false ]]; then
warn "No read permission to /dev/input/js*; add your user to the 'input' group or create udev rules."
fi fi
# EXIT covers every termination path (including a SIGTERM that interrupts the
# blocking read below); INT/TERM additionally give a clean exit status.
trap 'stop_idle_inhibit' EXIT
trap 'exit 0' INT TERM
while :; do sync_inhibit_to_controllers # apply current state once at startup
local found_any=false
for dev in /dev/input/js*; do if has_cmd udevadm; then
[[ -e $dev ]] || continue log "Watching controller hotplug via udev (no polling)"
found_any=true # Process substitution (not a pipe) keeps the loop in this shell so
if [[ -z ${pids[$dev]:-} ]] || ! kill -0 "${pids[$dev]}" 2> /dev/null; then # inhibit_pid persists across events.
# Start a watcher for this device in background while read -r _; do
watch_js_device "$dev" & sync_inhibit_to_controllers
pids[$dev]=$! done < <(udevadm monitor --udev --subsystem-match=input 2> /dev/null)
fi
done
if [[ $found_any == false ]]; then
# No joystick devices; quiet rescan
sleep 5
else else
# Rescan less frequently when active # Fallback when udevadm is unavailable: a low-frequency presence poll. One
sleep 2 # sleep per 30 s cycle (~0.03 forks/s) versus the old ~21 forks/s.
fi warn "udevadm not found; falling back to a 30 s presence poll"
while :; do
sync_inhibit_to_controllers
sleep 30
done done
fi
} }
persist_with_systemd_logind() { persist_with_systemd_logind() {
@ -255,14 +264,9 @@ main() {
persist_with_systemd_logind persist_with_systemd_logind
if [[ $watch_controller == true ]]; then if [[ $watch_controller == true ]]; then
log "Controller activity watcher enabled" log "Controller activity watcher enabled (idle-inhibitor mode)"
# Keep the script alive to watch controllers # Blocks until terminated; releases the inhibitor on exit via its own trap.
start_controller_watchers & start_controller_watchers
watcher_pid=$!
log "Watcher PID: $watcher_pid"
# Wait indefinitely and forward termination
trap 'log "Stopping controller watcher"; kill "$watcher_pid" 2>/dev/null || true; exit 0' INT TERM
wait "$watcher_pid"
else else
log "Done. The screen should no longer blank, lock, or power down automatically." log "Done. The screen should no longer blank, lock, or power down automatically."
fi fi