--- name: efficient-polling-scripts description: Use BEFORE writing any shell or Python script that runs on a timer, per-tick status bar (i3blocks/waybar/polybar), cron-like loop, or any repeated invocation. Prevents fork-storm anti-patterns that can consume many CPU-hours per day from tiny polling scripts. --- # Efficient Polling & Status-Bar Scripts ## When this applies Any script that runs **frequently** — per second or per few seconds — especially: - i3blocks / waybar / polybar / xmobar / tmux status-line scripts - cron / systemd-timer jobs with intervals < 1 min - watcher loops invoked by another process every tick - Python CLIs invoked from a shell hot loop A single fork pipeline running once per second will consume ~30–50 CPU-minutes per day per forked helper. Five such scripts with 3–8 helpers each turn into **days of CPU-time lost per day** and tens of thousands of forked processes showing up in `atop`. ## The rules ### R1. Zero forks in the hot path when possible Every `$(...)`, backtick, and `|` in a shell script forks a process. Favor bash builtins: | Instead of | Use | | ------------------------------- | ----------------------------------------------------------------------------------- | | `$(cat /proc/loadavg)` | `$(/dev/null; done ``` Target: a 1-Hz script should take < 2 ms per invocation on a modern desktop. A 5-second-interval script can afford ~20 ms. If you're over budget, count the `execve` with `strace -c` and remove forks. ## Python-specific rules (for daemons, not hot-loop callees) - Use `pathlib.Path.read_text()` / `read_bytes()` — one syscall, no subprocess. - Open `/sys` / `/proc` files with the builtin `open()`; they're tiny reads. - For event loops, use `asyncio` / `selectors` to block on fds (same idea as `read` in bash) instead of `time.sleep()` in a polling loop. - Don't shell out with `subprocess.run("sensors")` when `/sys/class/hwmon` exists. - Cache `psutil` objects across ticks — `psutil.cpu_percent(interval=None)` uses deltas and is O(1) after the first call. ## Common red flags (search for these in review) - `while true` / `while :` with a `sleep` and no event source - `$(…|…|…)` chains with three or more pipes in a status-bar script - `| awk`, `| grep`, `| tr`, `| cut`, `| sed`, `| head`, `| tail` where bash builtins would do - `$(cat foo)` anywhere — always replaceable with `$(&1 | grep -E 'execve|clone'` — fork count matches expectation. 3. `time for _ in {1..10000}; do script.sh >/dev/null; done` — under budget. 4. For persist scripts: run for 60 s under `perf stat -p $PID` — CPU time near zero when idle. 5. Running under the `monitors.slice` unit — verify with `systemctl --user status monitors.slice`. ## Reference implementations in this repo - `linux_configuration/i3-configuration/i3blocks/volume.sh` — persist mode with `pactl subscribe`. - `linux_configuration/i3-configuration/i3blocks/gpu_monitor.sh` — persist mode with `nvidia-smi --loop`. - `linux_configuration/i3-configuration/i3blocks/battery_status.sh` — zero-fork via `/sys/class/power_supply`. - `linux_configuration/i3-configuration/i3blocks/cpu_monitor.sh` — zero-fork via `/proc/loadavg` + `/sys/class/hwmon`. - `linux_configuration/i3-configuration/i3blocks/motherboard_temp.sh` — zero-fork via `/sys/class/hwmon`. - `linux_configuration/scripts/system-maintenance/systemd/monitors.slice` — resource-cap slice.