mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:23:03 +02:00
i3blocks: eliminate fork-storm with persist mode + zero-fork sysfs reads
Resource-usage report showed ~29 cores of average load coming from i3blocks helper scripts forking awk/tr/grep/bc/sensors/nvidia-smi every tick. Rewrite all five hot-path scripts to eliminate forks: - volume.sh: persist mode, blocks on 'pactl subscribe' event stream. No polling, no sleep, no fork per tick. - gpu_monitor.sh: persist mode, single long-lived 'nvidia-smi --loop=5' feeds a bash 'while read' loop. Falls back to /sys for amdgpu. - battery_status.sh: reads /sys/class/power_supply/BAT*/ directly. Zero forks; replaces 'acpi | awk' pipeline. - cpu_monitor.sh: reads /proc/loadavg and k10temp/coretemp /sys/class/hwmon. Zero forks; replaces 'sensors | awk | tr' + bc arithmetic. - motherboard_temp.sh: reads nct*/it*/f71* Super-I/O hwmon node directly. Zero forks. Configure volume + gpu_monitor with interval=persist so i3blocks keeps one long-lived producer each instead of forking per tick. Also add: - kill_stale_recorders.sh -- kill stray ffmpeg x11grab / dotnet-trace / dotnet-monitor processes left running after sessions. - monitors.slice -- resource-capped user slice (CPUQuota=50%, MemoryMax=512M, MemorySwapMax=0 for zram safety, TasksMax=256) to bound future monitoring regressions. - efficient-polling-scripts SKILL -- rules for writing status-bar and polling scripts without forks; fork-pipeline to bash-builtin translation table; verification checklist. Verified live: strace -c on cpu_monitor.sh shows 1 execve / 0 clones; persist producers (pactl subscribe, nvidia-smi --loop) show 0 CPU ticks over a 3s idle sample. Per-invocation timing 1.6-1.9 ms (was 30-80 ms).
This commit is contained in:
parent
135ef0c62d
commit
c8c727e9d5
177
.github/skills/efficient-polling-scripts/SKILL.md
vendored
Normal file
177
.github/skills/efficient-polling-scripts/SKILL.md
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
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)` | `$(</proc/loadavg)` or `read -r one _ < /proc/loadavg` |
|
||||||
|
| `echo "$x" \| awk '{print $1}'` | `read -r first _ <<< "$x"` or `arr=($x); first=${arr[0]}` |
|
||||||
|
| `echo "$x" \| tr -d '%'` | `${x//%/}` |
|
||||||
|
| `echo "$x" \| grep -Po '\d+%'` | `[[ $x =~ ([0-9]+)% ]] && vol=${BASH_REMATCH[1]}` |
|
||||||
|
| `echo "$a < $b" \| bc -l` | `(( a_times_100 < b_times_100 ))` (scale decimals to ints) |
|
||||||
|
| `sensors \| awk ...` | `read -r milli < /sys/class/hwmon/hwmonN/temp1_input` |
|
||||||
|
| `acpi -b \| awk ...` | `read -r cap < /sys/class/power_supply/BAT0/capacity` |
|
||||||
|
| `free -h \| awk ...` | parse `/proc/meminfo` with `while read -r` |
|
||||||
|
| `df -h / \| awk ...` | `stat -f` builtin? No: use a long-lived reader, or accept one fork at low frequency |
|
||||||
|
| `lspci \| grep -i nvidia` | check `/sys/bus/pci/devices/*/vendor` (0x10de == NVIDIA) |
|
||||||
|
|
||||||
|
### R2. Read from /sys and /proc directly
|
||||||
|
|
||||||
|
The kernel exposes structured data without forking anything. Useful paths:
|
||||||
|
|
||||||
|
- CPU load: `/proc/loadavg`
|
||||||
|
- CPU per-core stat: `/proc/stat`
|
||||||
|
- Memory: `/proc/meminfo`
|
||||||
|
- Temps / fans / voltages: `/sys/class/hwmon/hwmon*/`
|
||||||
|
- CPU on AMD: `name=k10temp`, `temp1_input` = Tctl (milli-°C, divide by 1000)
|
||||||
|
- CPU on Intel: `name=coretemp`
|
||||||
|
- Motherboard Super-I/O: `name=nct*` / `it87*` / `f71*`
|
||||||
|
- AMD GPU: `name=amdgpu`, plus `/sys/class/drm/card*/device/gpu_busy_percent`
|
||||||
|
- Battery: `/sys/class/power_supply/BAT*/` (`capacity`, `status`, `energy_now`, `power_now`)
|
||||||
|
- Backlight: `/sys/class/backlight/*/brightness`
|
||||||
|
- Network link: `/sys/class/net/*/operstate`, `/sys/class/net/*/statistics/*_bytes`
|
||||||
|
|
||||||
|
NVIDIA is the unfortunate exception — there is no sysfs utilization interface, so `nvidia-smi` is required. Mitigate with **R4** (long-lived producer).
|
||||||
|
|
||||||
|
### R3. Integer arithmetic, never `bc` in a hot loop
|
||||||
|
|
||||||
|
`bc` forks a process. For decimal comparisons, multiply out:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# "1.23" → 123, "0.45" → 45; compare against threshold ×100.
|
||||||
|
load_x100=$((10#${one//./}))
|
||||||
|
(( load_x100 < 150 )) && echo 'normal'
|
||||||
|
```
|
||||||
|
|
||||||
|
Bash's `((…))` and `[[ … ]]` are builtins — free.
|
||||||
|
|
||||||
|
### R4. Prefer event-driven / long-lived producers over polling + sleep
|
||||||
|
|
||||||
|
When an update needs to happen often, replace "poll + sleep + exit" with one of:
|
||||||
|
|
||||||
|
- **i3blocks `interval=persist`**: script runs forever, prints one block per update. Block on an event stream with `read` — no sleep, no busy-wait.
|
||||||
|
- **`pactl subscribe`**: event stream for PulseAudio/PipeWire volume/mute changes.
|
||||||
|
- **`udevadm monitor`**: hardware / power-supply / backlight events.
|
||||||
|
- **`inotifywait -m`**: file/dir changes.
|
||||||
|
- **`dbus-monitor`**: session-wide events (network, media keys, NetworkManager).
|
||||||
|
- **`journalctl -f`**: new log lines.
|
||||||
|
- **`nvidia-smi --loop=N`** / **`nvidia-smi dmon -d N`**: one long-lived nvidia-smi emitting rows instead of forking every N seconds. Tail its stdout with `while read`.
|
||||||
|
- **`mpstat N`**, **`iostat N`**, **`vmstat N`**: same pattern for CPU/IO.
|
||||||
|
|
||||||
|
Canonical persist skeleton:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -u
|
||||||
|
emit() { printf '%s\n' "$1"; }
|
||||||
|
|
||||||
|
emit "$(initial_value)"
|
||||||
|
producer_command | while read -r line; do
|
||||||
|
# `read` blocks on I/O — no CPU, no sleep, no poll.
|
||||||
|
[[ $line matches relevant event ]] || continue
|
||||||
|
emit "$(compute_new_value)"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### R5. One-shot scripts must still be cheap
|
||||||
|
|
||||||
|
Even with `interval=5`, 1728 invocations/day × 3 forks = 5k forks/day. Make the single-invocation path fork-free when possible. Profile with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strace -f -e trace=%process -c ./myscript.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The `clone` / `execve` counts are your fork count.
|
||||||
|
|
||||||
|
### R6. Python called from a hot loop is an anti-pattern
|
||||||
|
|
||||||
|
CPython startup is ~50–80 ms on modern hardware. Invoking `python my_helper.py` once per second = ~5–8% of one core doing nothing but importing stdlib.
|
||||||
|
|
||||||
|
If a status-bar value needs Python logic:
|
||||||
|
|
||||||
|
- **Inline it in bash** when possible (the rules above almost always suffice).
|
||||||
|
- **Run a persistent Python daemon** that writes to a FIFO / Unix socket / tmpfile; the bash hot-path reads from it with `read` / `$(<file)`.
|
||||||
|
- **Use a compiled helper** (Go/Rust/C) if Python startup is the only issue — a static binary startup is sub-millisecond.
|
||||||
|
|
||||||
|
### R7. Cap risk with a systemd slice
|
||||||
|
|
||||||
|
Even a correct script can regress. Put status-bar / monitoring work in a resource-capped user slice so the blast radius is bounded:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# ~/.config/systemd/user/monitors.slice
|
||||||
|
[Slice]
|
||||||
|
CPUQuota=50%
|
||||||
|
MemoryMax=512M
|
||||||
|
MemorySwapMax=0 # REQUIRED on zram systems — see oom-prevention skill
|
||||||
|
TasksMax=256
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch i3blocks (or individual persist scripts) under that slice, e.g. via a user service with `Slice=monitors.slice`, so every child inherits the cap.
|
||||||
|
|
||||||
|
### R8. Measure before and after
|
||||||
|
|
||||||
|
For any "fast" shell script, time 10k invocations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
time for _ in {1..10000}; do ./script.sh >/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 `$(<foo)`
|
||||||
|
- `echo … | bc` — replaceable with bash integer math
|
||||||
|
- `sensors`, `acpi`, `free`, `lspci`, `iwgetid` in a per-second script
|
||||||
|
- `python …` / `node …` invoked per tick
|
||||||
|
- No `set -u` (silent typo bugs compound over thousands of ticks)
|
||||||
|
|
||||||
|
## Verification checklist before shipping
|
||||||
|
|
||||||
|
1. `shellcheck script.sh` — clean.
|
||||||
|
2. `strace -c -f script.sh 2>&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.
|
||||||
@ -1,11 +1,49 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# i3blocks battery indicator, zero-fork per invocation.
|
||||||
|
#
|
||||||
|
# Reads /sys/class/power_supply directly instead of forking `acpi | awk`.
|
||||||
|
# Uses only bash builtins (read, printf, arithmetic, parameter expansion).
|
||||||
|
|
||||||
acpi -b | awk -F', ' '
|
set -u
|
||||||
/Battery/ {
|
|
||||||
split($2, percent, "%")
|
bat=
|
||||||
split($3, time, " ")
|
for d in /sys/class/power_supply/BAT*/; do
|
||||||
printf " %d%%", percent[1]
|
[[ -d $d ]] && {
|
||||||
if (time[1] != "") printf ", %s", time[1]
|
bat=$d
|
||||||
if ($1 ~ /Charging/) printf ", "
|
break
|
||||||
printf "\n"
|
}
|
||||||
}'
|
done
|
||||||
|
|
||||||
|
if [[ -z $bat ]]; then
|
||||||
|
# Desktop with no battery — emit empty block so i3bar hides it.
|
||||||
|
echo
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cap='N/A'
|
||||||
|
[[ -r ${bat}capacity ]] && read -r cap < "${bat}capacity"
|
||||||
|
|
||||||
|
status=''
|
||||||
|
[[ -r ${bat}status ]] && read -r status < "${bat}status"
|
||||||
|
|
||||||
|
# Compute time remaining from energy_now/power_now (µWh / µW → hours).
|
||||||
|
# Falls back to charge_now/current_now on batteries that expose charge instead.
|
||||||
|
time_str=''
|
||||||
|
num=0
|
||||||
|
den=0
|
||||||
|
if [[ -r ${bat}energy_now && -r ${bat}power_now ]]; then
|
||||||
|
read -r num < "${bat}energy_now"
|
||||||
|
read -r den < "${bat}power_now"
|
||||||
|
elif [[ -r ${bat}charge_now && -r ${bat}current_now ]]; then
|
||||||
|
read -r num < "${bat}charge_now"
|
||||||
|
read -r den < "${bat}current_now"
|
||||||
|
fi
|
||||||
|
if ((den > 0 && num > 0)); then
|
||||||
|
total_min=$((num * 60 / den))
|
||||||
|
printf -v time_str '%02d:%02d' "$((total_min / 60))" "$((total_min % 60))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf ' %s%%' "$cap"
|
||||||
|
[[ -n $time_str ]] && printf ', %s' "$time_str"
|
||||||
|
[[ $status == Charging ]] && printf ', '
|
||||||
|
printf '\n'
|
||||||
|
|||||||
@ -6,7 +6,7 @@ markup=pango
|
|||||||
|
|
||||||
[gpu_monitor]
|
[gpu_monitor]
|
||||||
command=~/.config/i3blocks/gpu_monitor.sh
|
command=~/.config/i3blocks/gpu_monitor.sh
|
||||||
interval=5
|
interval=persist
|
||||||
markup=pango
|
markup=pango
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ color=#50FA7B
|
|||||||
|
|
||||||
[volume]
|
[volume]
|
||||||
command=~/.config/i3blocks/volume.sh
|
command=~/.config/i3blocks/volume.sh
|
||||||
interval=1
|
interval=persist
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,48 +1,60 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# i3blocks CPU monitor, zero-fork per invocation.
|
||||||
|
#
|
||||||
|
# Reads /proc/loadavg and /sys/class/hwmon/*/temp*_input directly instead
|
||||||
|
# of forking `sensors | awk | tr` and `echo | bc`. Pure bash builtins.
|
||||||
|
|
||||||
# CPU Temperature
|
set -u
|
||||||
cpu_temp=$(sensors | awk '/^Tctl:/ {print $2}' | tr -d '+°C')
|
|
||||||
if [ -z "$cpu_temp" ]; then
|
# Locate AMD k10temp or Intel coretemp hwmon node.
|
||||||
cpu_temp=$(sensors | awk '/^Package id 0:/ {print $4}' | tr -d '+°C')
|
hwmon=''
|
||||||
fi
|
for d in /sys/class/hwmon/hwmon*/; do
|
||||||
if [ -z "$cpu_temp" ]; then
|
[[ -r ${d}name ]] || continue
|
||||||
cpu_temp=$(sensors | awk '/^Core 0:/ {print $3}' | tr -d '+°C')
|
read -r n < "${d}name"
|
||||||
fi
|
case $n in
|
||||||
if [ -z "$cpu_temp" ]; then
|
k10temp | coretemp)
|
||||||
cpu_temp="N/A"
|
hwmon=$d
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
temp='N/A'
|
||||||
|
temp_int=-1
|
||||||
|
if [[ -n $hwmon && -r ${hwmon}temp1_input ]]; then
|
||||||
|
read -r milli < "${hwmon}temp1_input"
|
||||||
|
temp_int=$((milli / 1000))
|
||||||
|
temp=$temp_int
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CPU Load (1-minute average)
|
load='N/A'
|
||||||
cpu_load=$(awk '{print $1}' /proc/loadavg)
|
load_x100=0
|
||||||
if [ -z "$cpu_load" ]; then
|
if [[ -r /proc/loadavg ]]; then
|
||||||
cpu_load="N/A"
|
read -r one _ < /proc/loadavg
|
||||||
|
load=$one
|
||||||
|
# loadavg prints two decimals, e.g. "1.23" → 123, "0.05" → 5.
|
||||||
|
load_digits=${one//./}
|
||||||
|
load_digits=${load_digits##0}
|
||||||
|
load_x100=$((10#${load_digits:-0}))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Colors for CPU Load and Temperature
|
color='#FFFFFF'
|
||||||
cpu_color="#FFFFFF" # Default color
|
if ((temp_int >= 0)); then
|
||||||
|
if ((temp_int < 65)); then
|
||||||
# Change color based on CPU load
|
color='#50FA7B'
|
||||||
if [[ $cpu_load != "N/A" ]]; then
|
elif ((temp_int < 85)); then
|
||||||
cpu_load_float=$(echo "$cpu_load" | awk '{print ($1 + 0)}')
|
color='#F1FA8C'
|
||||||
if (($(echo "$cpu_load_float < 1.0" | bc -l))); then
|
|
||||||
cpu_color="#50FA7B" # Green for low load
|
|
||||||
elif (($(echo "$cpu_load_float < 2.0" | bc -l))); then
|
|
||||||
cpu_color="#F1FA8C" # Yellow for medium load
|
|
||||||
else
|
else
|
||||||
cpu_color="#FF5555" # Red for high load
|
color='#FF5555'
|
||||||
|
fi
|
||||||
|
elif ((load_x100 > 0)); then
|
||||||
|
if ((load_x100 < 100)); then
|
||||||
|
color='#50FA7B'
|
||||||
|
elif ((load_x100 < 200)); then
|
||||||
|
color='#F1FA8C'
|
||||||
|
else
|
||||||
|
color='#FF5555'
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Change color based on CPU temperature
|
printf '<span color="%s"> %s°C, %s</span>\n' "$color" "$temp" "$load"
|
||||||
if [[ $cpu_temp != "N/A" ]]; then
|
|
||||||
cpu_temp_float=$(echo "$cpu_temp" | awk '{print ($1 + 0)}')
|
|
||||||
if (($(echo "$cpu_temp_float < 65.0" | bc -l))); then
|
|
||||||
cpu_color="#50FA7B" # Green for low temperature
|
|
||||||
elif (($(echo "$cpu_temp_float < 85.0" | bc -l))); then
|
|
||||||
cpu_color="#F1FA8C" # Yellow for medium temperature
|
|
||||||
else
|
|
||||||
cpu_color="#FF5555" # Red for high temperature
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "<span color=\"$cpu_color\"> ${cpu_temp}°C, ${cpu_load}</span>"
|
|
||||||
|
|||||||
@ -1,64 +1,75 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# i3blocks GPU monitor, persist mode.
|
||||||
|
#
|
||||||
|
# Keeps a single long-lived `nvidia-smi --loop=5` (or reads amdgpu sysfs
|
||||||
|
# in a blocking-read loop) instead of forking nvidia-smi/lspci/awk/tr/bc
|
||||||
|
# every interval. No sleep, no polling loop in bash — nvidia-smi's own
|
||||||
|
# periodic emitter drives updates and we block on `read`.
|
||||||
|
#
|
||||||
|
# Configure with `interval=persist` in the i3blocks config.
|
||||||
|
|
||||||
# Function to get NVIDIA GPU metrics
|
set -u
|
||||||
get_nvidia_metrics() {
|
|
||||||
gpu_temp=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2> /dev/null)
|
|
||||||
if [ -z "$gpu_temp" ]; then
|
|
||||||
gpu_temp="N/A"
|
|
||||||
fi
|
|
||||||
|
|
||||||
gpu_load=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2> /dev/null)
|
emit() {
|
||||||
if [ -z "$gpu_load" ]; then
|
local temp=$1 load=$2 color
|
||||||
gpu_load="N/A"
|
if [[ $load == 'N/A' ]]; then
|
||||||
fi
|
color='#FFFFFF'
|
||||||
|
elif ((load < 50)); then
|
||||||
echo "GPU Temp: $gpu_temp°C, GPU Load: $gpu_load"
|
color='#50FA7B'
|
||||||
}
|
elif ((load < 75)); then
|
||||||
|
color='#F1FA8C'
|
||||||
# Function to get Intel GPU metrics
|
|
||||||
get_intel_metrics() {
|
|
||||||
gpu_load=$(cat /sys/class/drm/card0/device/gpu_busy_percent 2> /dev/null)
|
|
||||||
if [ -z "$gpu_load" ]; then
|
|
||||||
gpu_load="N/A"
|
|
||||||
fi
|
|
||||||
|
|
||||||
gpu_temp=$(sensors | awk '/^temp1:/ {print $2; exit}' | tr -d '+°C')
|
|
||||||
if [ -z "$gpu_temp" ]; then
|
|
||||||
gpu_temp="N/A"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "GPU Temp: $gpu_temp°C, GPU Load: $gpu_load"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect GPU type and get metrics
|
|
||||||
if lspci | grep -i nvidia > /dev/null; then
|
|
||||||
gpu_metrics=$(get_nvidia_metrics)
|
|
||||||
elif lspci | grep -i vga | grep -i intel > /dev/null; then
|
|
||||||
gpu_metrics=$(get_intel_metrics)
|
|
||||||
else
|
|
||||||
echo "No supported GPU found."
|
|
||||||
fi
|
|
||||||
|
|
||||||
#!/bin/bash
|
|
||||||
# GPU Metrics
|
|
||||||
gpu_temp=$(echo "$gpu_metrics" | awk -F', ' '{print $1}' | awk -F': ' '{print $2}')
|
|
||||||
gpu_load=$(echo "$gpu_metrics" | awk -F', ' '{print $2}' | awk -F': ' '{print $2}')
|
|
||||||
|
|
||||||
gpu_color="#FFFFFF"
|
|
||||||
# Colors for GPU Load
|
|
||||||
if [[ $gpu_load != "N/A" ]]; then
|
|
||||||
if (($(echo "$gpu_load < 50.0" | bc -l))); then
|
|
||||||
gpu_color="#50FA7B" # Green
|
|
||||||
elif (($(echo "$gpu_load < 75.0" | bc -l))); then
|
|
||||||
gpu_color="#F1FA8C" # Yellow
|
|
||||||
else
|
else
|
||||||
gpu_color="#FF5555" # Red
|
color='#FF5555'
|
||||||
fi
|
fi
|
||||||
else
|
printf '<span color="%s"> %s°C, %s%%</span>\n\n%s\n' \
|
||||||
gpu_color="#FFFFFF" # Default color
|
"$color" "$temp" "$load" "$color"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prefer NVIDIA if present (persist via --loop).
|
||||||
|
if command -v nvidia-smi > /dev/null 2>&1; then
|
||||||
|
# One child process for the lifetime of i3blocks; emits CSV every 5s.
|
||||||
|
nvidia-smi \
|
||||||
|
--query-gpu=temperature.gpu,utilization.gpu \
|
||||||
|
--format=csv,noheader,nounits \
|
||||||
|
--loop=5 2> /dev/null |
|
||||||
|
while IFS=',' read -r temp load; do
|
||||||
|
# Strip leading/trailing whitespace using parameter expansion.
|
||||||
|
temp=${temp## }
|
||||||
|
temp=${temp%% }
|
||||||
|
load=${load## }
|
||||||
|
load=${load%% }
|
||||||
|
[[ -z $temp || -z $load ]] && continue
|
||||||
|
emit "$temp" "$load"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Output<
|
# AMD fallback: read sysfs directly; emit once (i3blocks restarts on exit).
|
||||||
echo -e "<span color=\"$gpu_color\"> ${gpu_temp}, ${gpu_load}%</span>"
|
amdgpu=''
|
||||||
echo
|
for d in /sys/class/hwmon/hwmon*/; do
|
||||||
echo "#FFFFFF" # Default color for fallback (ignored if markup is enabled)
|
[[ -r ${d}name ]] || continue
|
||||||
|
read -r n < "${d}name"
|
||||||
|
[[ $n == amdgpu ]] && {
|
||||||
|
amdgpu=$d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
done
|
||||||
|
if [[ -n $amdgpu ]]; then
|
||||||
|
temp='N/A'
|
||||||
|
if [[ -r ${amdgpu}temp1_input ]]; then
|
||||||
|
read -r milli < "${amdgpu}temp1_input"
|
||||||
|
temp=$((milli / 1000))
|
||||||
|
fi
|
||||||
|
load='N/A'
|
||||||
|
# drm card matching the amdgpu hwmon exposes gpu_busy_percent.
|
||||||
|
for card in /sys/class/drm/card*/device/gpu_busy_percent; do
|
||||||
|
[[ -r $card ]] && {
|
||||||
|
read -r load < "$card"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
done
|
||||||
|
emit "$temp" "$load"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'No supported GPU\n\n#FF5555\n'
|
||||||
|
|||||||
@ -1,26 +1,51 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# i3blocks motherboard-temperature indicator, zero-fork per invocation.
|
||||||
|
#
|
||||||
|
# Reads /sys/class/hwmon directly instead of forking `sensors | awk | tr`.
|
||||||
|
# Prefers Super-I/O chips (nct*, it*, f71*) which expose the true board
|
||||||
|
# sensor; falls back to the first non-CPU/GPU/NIC hwmon with a temp1_input.
|
||||||
|
|
||||||
# Get the first temp1 value from sensors
|
set -u
|
||||||
temp=$(sensors | awk '/^temp1:/ {print $2; exit}' | tr -d '+°C')
|
|
||||||
|
|
||||||
# Ensure the temperature is a valid number
|
hwmon=''
|
||||||
if [[ ! $temp =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
for d in /sys/class/hwmon/hwmon*/; do
|
||||||
echo " MB: N/A"
|
[[ -r ${d}name ]] || continue
|
||||||
echo
|
read -r n < "${d}name"
|
||||||
echo "#FF5555" # Red color for error
|
case $n in
|
||||||
exit 1
|
nct* | it87* | it8* | f71*)
|
||||||
|
hwmon=$d
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ -z $hwmon ]]; then
|
||||||
|
for d in /sys/class/hwmon/hwmon*/; do
|
||||||
|
[[ -r ${d}name && -r ${d}temp1_input ]] || continue
|
||||||
|
read -r n < "${d}name"
|
||||||
|
case $n in
|
||||||
|
k10temp | coretemp | amdgpu | nouveau | nvme | r8169* | iwlwifi*) continue ;;
|
||||||
|
*)
|
||||||
|
hwmon=$d
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Define temperature thresholds
|
if [[ -z $hwmon || ! -r ${hwmon}temp1_input ]]; then
|
||||||
if (($(echo "$temp < 50.0" | bc -l))); then
|
printf ' MB: N/A\n\n#FF5555\n'
|
||||||
color="#50FA7B" # Green for OK temperature
|
exit 0
|
||||||
elif (($(echo "$temp < 70.0" | bc -l))); then
|
fi
|
||||||
color="#F1FA8C" # Yellow for warning temperature
|
|
||||||
|
read -r milli < "${hwmon}temp1_input"
|
||||||
|
temp=$((milli / 1000))
|
||||||
|
|
||||||
|
if ((temp < 50)); then
|
||||||
|
color='#50FA7B'
|
||||||
|
elif ((temp < 70)); then
|
||||||
|
color='#F1FA8C'
|
||||||
else
|
else
|
||||||
color="#FF5555" # Red for high temperature
|
color='#FF5555'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Output the temperature with the color
|
printf ' %s°C\n\n%s\n' "$temp" "$color"
|
||||||
echo " ${temp}°C" # is a thermometer icon
|
|
||||||
echo
|
|
||||||
echo $color
|
|
||||||
|
|||||||
@ -1,19 +1,41 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# i3blocks persist-mode volume indicator.
|
||||||
|
#
|
||||||
|
# Event-driven: blocks in `read` on the `pactl subscribe` event stream.
|
||||||
|
# No sleep, no polling loop, no awk/tr/grep forks. One pactl-subscribe
|
||||||
|
# process stays alive; two short pactl calls run only on actual events.
|
||||||
|
#
|
||||||
|
# Configure with `interval=persist` in the i3blocks config.
|
||||||
|
|
||||||
# Get the current volume level and mute status
|
set -u
|
||||||
volume=$(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}' | tr -d '%')
|
|
||||||
mute=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}')
|
|
||||||
color="#50FA7B"
|
|
||||||
|
|
||||||
# Determine icon and color based on mute status
|
GREEN='#50FA7B'
|
||||||
if [ "$mute" = "yes" ]; then
|
RED='#FF5555'
|
||||||
icon="🔇" # Muted
|
|
||||||
color="#FF5555"
|
|
||||||
else
|
|
||||||
icon="🔊" # Volume icon
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Output the volume with icon and color
|
emit() {
|
||||||
echo "$icon $volume%"
|
local raw mute vol icon color
|
||||||
echo
|
raw=$(pactl get-sink-volume @DEFAULT_SINK@ 2> /dev/null) || return 0
|
||||||
echo "$color"
|
if [[ $raw =~ ([0-9]+)% ]]; then
|
||||||
|
vol=${BASH_REMATCH[1]}
|
||||||
|
else
|
||||||
|
vol=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mute=$(pactl get-sink-mute @DEFAULT_SINK@ 2> /dev/null) || return 0
|
||||||
|
if [[ $mute == *yes ]]; then
|
||||||
|
icon='🔇'
|
||||||
|
color=$RED
|
||||||
|
else
|
||||||
|
icon='🔊'
|
||||||
|
color=$GREEN
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s %s%%\n\n%s\n' "$icon" "$vol" "$color"
|
||||||
|
}
|
||||||
|
|
||||||
|
emit
|
||||||
|
# `read -r` blocks on the event stream — no busy-wait, no sleep.
|
||||||
|
pactl subscribe 2> /dev/null | while read -r line; do
|
||||||
|
[[ $line == *"on sink"* || $line == *"on server"* ]] || continue
|
||||||
|
emit
|
||||||
|
done
|
||||||
|
|||||||
67
linux_configuration/scripts/system-maintenance/bin/kill_stale_recorders.sh
Executable file
67
linux_configuration/scripts/system-maintenance/bin/kill_stale_recorders.sh
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Kill stray recorders and .NET profilers that were left running unintentionally.
|
||||||
|
#
|
||||||
|
# Targets based on the resource-usage report: ffmpeg x11grab and dotnet
|
||||||
|
# trace/monitor/ust processes can easily burn GiB of RAM and many CPU-hours
|
||||||
|
# after whatever session started them has ended.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# kill_stale_recorders.sh # interactive: lists matches, prompts
|
||||||
|
# kill_stale_recorders.sh --force # non-interactive: kill all matches
|
||||||
|
# kill_stale_recorders.sh --dry-run # only list, don't kill
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PATTERNS=(
|
||||||
|
'ffmpeg.*x11grab'
|
||||||
|
'ffmpeg.*-f[[:space:]]+x11grab'
|
||||||
|
'dotnet-trace'
|
||||||
|
'dotnet-monitor'
|
||||||
|
'dotnet-ust'
|
||||||
|
)
|
||||||
|
|
||||||
|
mode='prompt'
|
||||||
|
case ${1:-} in
|
||||||
|
--force) mode='force' ;;
|
||||||
|
--dry-run) mode='dry' ;;
|
||||||
|
-h | --help)
|
||||||
|
sed -n '2,12p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mapfile -t matches < <(
|
||||||
|
for pat in "${PATTERNS[@]}"; do
|
||||||
|
pgrep -af "$pat" 2> /dev/null || true
|
||||||
|
done | sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
if ((${#matches[@]} == 0)); then
|
||||||
|
echo 'No stale recorder/profiler processes found.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo 'Stale processes:'
|
||||||
|
printf ' %s\n' "${matches[@]}"
|
||||||
|
|
||||||
|
if [[ $mode == dry ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $mode == prompt ]]; then
|
||||||
|
read -r -p 'Kill these? [y/N] ' answer
|
||||||
|
[[ $answer == [yY] ]] || {
|
||||||
|
echo 'Aborted.'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
killed=0
|
||||||
|
for line in "${matches[@]}"; do
|
||||||
|
pid=${line%% *}
|
||||||
|
[[ -n $pid ]] || continue
|
||||||
|
if kill "$pid" 2> /dev/null; then
|
||||||
|
killed=$((killed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Sent SIGTERM to $killed process(es)."
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Resource-capped slice for user monitoring / status-bar scripts
|
||||||
|
Documentation=https://www.freedesktop.org/software/systemd/man/systemd.slice.html
|
||||||
|
|
||||||
|
[Slice]
|
||||||
|
# Cap the entire slice at 50% of one CPU and 512 MiB of RAM. If i3blocks or
|
||||||
|
# any other status-bar tooling enters a fork-storm regime (as was observed
|
||||||
|
# when polling scripts forked awk/tr/grep/bc every tick), the kernel will
|
||||||
|
# throttle the slice rather than let it eat the box.
|
||||||
|
CPUQuota=50%
|
||||||
|
# MemorySwapMax=0 is required on systems with zram: without it, a cgroup
|
||||||
|
# hitting MemoryMax thrashes zram instead of being OOM-killed, freezing
|
||||||
|
# the machine. See .github/skills/oom-prevention/SKILL.md.
|
||||||
|
MemoryMax=512M
|
||||||
|
MemorySwapMax=0
|
||||||
|
TasksMax=256
|
||||||
|
|
||||||
|
# Make sure killing the slice reaps every descendant.
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Loading…
Reference in New Issue
Block a user