--- 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. ### R9. Never flush+rebuild firewall rules unconditionally in a poll loop `iptables -F chain` followed by dozens of `iptables -A` calls every tick is one of the most expensive fork patterns possible — each rule insert is its own fork, plus it contends the xtables lock with the kernel's own netfilter subsystem (netd on Android, firewalld/nftables on Linux). This is a specific enough, and costly enough, instance of R6-style unconditional recomputation to call out on its own: a real incident (`phone_focus_mode/dns_enforcer.sh`, see `feedback-iptables-hash-gate-before-rebuild` in this user's memory) rebuilt ~100 rules every 20s with no state check at all, and measured out to pegging `netd` at a sustained ~50% CPU, contributing to a phone overheating. Fix: check whether the current state is already correct before touching iptables at all — ```bash # Cheap check (2-3 forks): is the chain already right? chain_intact() { "$1" -C OUTPUT -j "$2" >/dev/null 2>&1 || return 1 [ "$("$1" -S "$2" 2>/dev/null | grep -c '^-A')" = "$3" ] } # Only pay for the expensive flush+rebuild (50-100+ forks) when it's wrong. chain_intact iptables MY_CHAIN "$expected_count" || rebuild_my_chain ``` This preserves self-healing (a flush by the kernel or another process is still detected and repaired on the next tick) while skipping the ~95% of ticks where nothing changed. If diagnosing whether this pattern is already live in a running system, `kill -STOP`/`kill -CONT` the suspect script's PID and compare `/proc//stat` utime+stime jiffy deltas of the netfilter daemon before/after — don't trust `dumpsys cpuinfo` (Android) or similar cached snapshot tools, some builds don't refresh them between calls. ## 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/system-maintenance/systemd/monitors.slice` — resource-cap slice. - `phone_focus_mode/dns_enforcer.sh::chain_intact()`/`expected_rule_count()` — R9's hash/count-gate pattern, fixed 2026-07-03 after it was found pegging Android's `netd` at ~50% CPU. - `phone_focus_mode/hosts_enforcer.sh::sync_magisk_module` — the original hash-before-write pattern R9 mirrors.