Commit a828d5f0 by PLN (Algolia)

perf(protect): the guard was spending a tenth of a core to discover nothing had changed

219.1ms per 2s tick, 6 forks, with NOTHING running to protect. None of it was
protection: three pgrep -x at 59ms each (pgrep reads cmdline for every process
on the box, and it ran once per target), plus cat, two chrt -p reads and an
unconditional prlimit per pid, each in its own command substitution.

Now zero forks in the steady state: one bash pass over /proc/*/comm, sched from
/proc/<pid>/stat fields 40/41, prlimit only when /proc/<pid>/limits says so.
28.5ms/tick, 1.42% of a core at the unchanged 2s interval.

The interval is deliberately untouched -- the 7.7x came from forks alone, so the
responsiveness that re-protects scsynth before its first sound was not traded
for battery.

Verified differentially against the old pgrep path with a decoy fleet (ardour9,
ArdourGUI, ardour-8.6 must match; ardour-decoy, sclang-notreally, scsynthx and a
bash whose path contains 'ardour' must not) and against chrt -p / cat on live
scsynth+sclang. Needs sudo tools/install-protect.sh to take effect.

Also logs two findings from the same measurement pass: sc-watchdog restarts
SuperDirt on every clean start (6s patience vs 8s boot) and two starts in five
minutes hit StartLimitBurst, leaving the rig unstartable; and the other two
reconcile loops cost 5.3% between them with real event sources available.
parent 9150a200
...@@ -4,6 +4,58 @@ Sprint entries, newest first. Player-facing: what changed about *playing*, not ...@@ -4,6 +4,58 @@ Sprint entries, newest first. Player-facing: what changed about *playing*, not
about the code. Task IDs reference the L'Armada board; `n/a` where the work was about the code. Task IDs reference the L'Armada board; `n/a` where the work was
unplanned (which, on a gig night, is most of it). unplanned (which, on a gig night, is most of it).
## Sprint 4 — 2026-09-06 (later) · the guard stops charging rent
Sprint 3's guard finally guarded. Then we asked what it cost to do so, and the
answer was a tenth of a core, forever, on a battery, whether or not anything was
playing. This sprint is about the difference between watching and *fidgeting*.
### Fixed
- **`parvagues-protect` costs 1.4% of a core instead of 11%** (n/a) — 219.1 ms
per 2 s tick became 28.5 ms, and 6 forks became **zero**. None of that 219 ms
was protection: three `pgrep -x` at 59 ms each (pgrep reads every process's
cmdline, and it was called once per target), then `cat`, two `chrt -p` reads
and an unconditional `prlimit` per pid, each in its own subshell. Everything
now comes from `/proc` through bash builtins. **The poll interval is
unchanged at 2 s on purpose** — the whole 7.7× came from forks, so the
responsiveness that re-protects scsynth before it makes its first sound was
not sold for battery. On stage nothing about it behaves differently; the fan
just has less to say.
### Found, not yet fixed
- **Starting SuperDirt twice inside five minutes bricks it until
`reset-failed`** (n/a). `parvagues-sc` reports `active` the moment sclang
execs, but scsynth appears ~8 s later — and `sc-watchdog` gives up waiting at
6 s, so it restarts a unit that was booting perfectly well. Each clean start
therefore spends two of systemd's three allowed starts, and the second start
in a five-minute window lands on `start-limit-hit`: SuperDirt refuses to come
up at all, which on stage is indistinguishable from dead. Reproduced tonight
from a fresh start with nothing wrong, cleared, and written up with the fix
(a boot grace read from the unit's own `ActiveEnterTimestamp`) in
`backlog.md`. **This is the next thing to fix, ahead of any polish.**
- **The idle rig costs ~14.5% of a core with nothing playing** (n/a), across
three reconcile loops that re-discover an unchanged graph every 2 s. Protect
was the biggest but only 60% of it; `midi-autoconnect` (2.74%) and
`tidal-ardour-autoroute` (2.53%) both have a real event source to block on
instead — `pw-link --monitor`, and `/proc/asound/seq/clients` for ALSA.
### Doctrine
- **Cheap prefilter, expensive test last.** Bash's regex engine is the slowest
thing in reach: testing one anchored alternation against every process's
`comm` costs 149 ms/tick — *worse than the `pgrep` it replaced*. A `[sSaA]*`
character class rejects 606 of 641 processes first, and the same regex then
costs nothing. Measure the replacement, not just the thing being replaced.
- **`case "$x" in $pattern)` does not do alternation.** `|` is recognised when
`case` is parsed, so a `s*|S*|a*|A*` arriving from a variable is one pattern
containing literal pipes, and matches nothing. Every part was individually
correct and the scan came back empty. A bracket class survives expansion; a
differential test against the code being replaced is what caught it, and
reading it again would not have.
- **Don't cache pid → name.** It would save 25 ms a tick and it is a stale
binding by construction — the kernel recycles pids, and a cached "not a
target" leaves scsynth naked. This is the same bug shape as the one the file
was written to fix; the file's own header says so.
## Sprint 3 — 2026-09-06 (late) · the box plays alone, and the guard now guards ## Sprint 3 — 2026-09-06 (late) · the box plays alone, and the guard now guards
Sprint 2 made the surface tell the truth. This one makes the *laptop* self- Sprint 2 made the surface tell the truth. This one makes the *laptop* self-
......
...@@ -2129,19 +2129,72 @@ scattered through this file resolve locally. ...@@ -2129,19 +2129,72 @@ scattered through this file resolve locally.
## Housekeeping ## Housekeeping
- **`rig-doctor` does not check LV2 plugins** — the same gap class as the PySide6 miss. - **`rig-doctor` does not check LV2 plugins** — the same gap class as the PySide6 miss.
- **`parvagues-protect` burns 10.7% of a core, forever, on a battery laptop.** - ~~**`parvagues-protect` burns 10.7% of a core, forever, on a battery laptop.**~~
Measured 2026-09-06: 35.7 s CPU over 333 s uptime. It is a system unit enabled **FIXED 2026-09-06 (late), pending a root install.** Re-measured to 219.1 ms
at boot, so this runs whether or not anything is playing — which cuts against per 2 s tick = **10.96% of a core with nothing running to protect**, so the
the whole reason rig units are on-demand here (see the box's boot policy in original figure was if anything generous. It was all discovery, none of it
`tools/rig_units.py`). Cause is fork count, not work: every 2 s tick forks protection: three `pgrep -x` at 59 ms each (pgrep reads cmdline for every
`pgrep` per target, then **two** `chrt -p` calls per pid merely to READ the process on the box, and it was called once per target), plus `cat`, two
policy and priority, plus an unconditional `prlimit`. Roughly 12 processes `chrt -p` reads and an unconditional `prlimit` per pid, each inside a command
every two seconds to discover that nothing changed. substitution. Now answered entirely with bash builtins — one pass over
Fixes, cheapest first: read policy/priority from `/proc/<pid>/stat` fields 18 `/proc/*/comm`, sched read from `/proc/<pid>/stat` fields 40/41, `prlimit`
and 19 instead of forking `chrt -p` twice; call `prlimit` once per pid rather only when `/proc/<pid>/limits` says it is needed:
than every tick; raise the interval (2 s is far below anything a human or an
OOM event notices — 10 s would be 5× cheaper on its own). Do not "fix" it by | | ms/tick | forks/tick | % of a core @2 s |
disabling the unit: the protection has to be up BEFORE the audio starts. |---|---|---|---|
| before | 219.1 | 6 | 10.96% |
| after | 28.5 | **0** | **1.42%** |
The interval is untouched at 2 s deliberately — the 7.7× came from forks
alone, so the responsiveness that catches a restart before first sound was
not traded away. `PARVAGUES_PROTECT_INTERVAL=5` takes it to 0.57% if the
battery ever matters more than a 5 s window of non-RT audio after a restart.
What made it cheap was the *prefilter*: a `[sSaA]*` character class rejects
606 of 641 processes before bash's regex engine — by far the slowest thing in
reach — is consulted. Testing the regex on every comm costs 149 ms/tick,
**worse than the pgrep it replaced**. Verified differentially against the old
`pgrep` path with a decoy fleet (`ardour9`, `ArdourGUI`, `ardour-8.6` must
match; `ardour-decoy`, `sclang-notreally`, `scsynthx` and a `bash` whose path
contains "ardour" must not) and against `chrt -p`/`cat` on live scsynth+sclang.
**Still to do: `sudo tools/install-protect.sh`** — the repo has the fix, the
running daemon does not.
- **`sc-watchdog` restarts SuperDirt on every clean start, and two starts in
five minutes leave the rig `failed` and unstartable.** Found 2026-09-06 by
starting `parvagues-sc` for an unrelated test. `parvagues-sc.service` is
`Type=simple`, so it reports `active` the instant `sclang` execs — but
`scsynth` only appears ~8 s later, when SuperDirt boots the server. The
watchdog's main loop counts a miss every `POLL_SECS=2` while the unit is
active and scsynth is absent, and acts at `MISSES_TO_ACT=3`. **6 s < 8 s**, so
a perfectly healthy start always trips it. Journal, verbatim, from a fresh
start with nothing wrong: `23:17:12 scsynth GONE (3 polls) while
parvagues-sc.service is active — restarting (0 prior in window)` → `23:17:21
scsynth up after 8s` → `recovered`.
The consequence is the gig-night one: each start burns two of systemd's
`StartLimitBurst=3` (PLN's, then the watchdog's), so a *second* start inside
`StartLimitIntervalUSec=5min` hits the limit and the unit goes
`failed (result: start-limit-hit)` — SuperDirt then refuses to start at all
until `systemctl --user reset-failed parvagues-sc`. On stage that reads as
"the rig is dead and will not come back". Reproduced end to end tonight and
cleared with `reset-failed`; the box is back to `inactive/linked` as found.
Fix: give the watchdog a boot grace keyed to the unit's own
`ActiveEnterTimestamp` — do not count misses until the unit has been active
longer than SuperDirt's measured boot (~8-12 s; 25 s is a safe floor). It
already shells `systemctl --user is-active` every poll, so the timestamp is
one field on a call it is making anyway. `await_scsynth`'s
`BOOT_WAIT_SECS=100` covers the *post-restart* wait and was never wired to the
*initial* start, which is the whole bug.
- **The other two reconcile loops cost 5.3% of a core between them, and both can
be event-driven.** Measured 2026-09-06 on the same idle box:
`midi-autoconnect` **2.74%**, `tidal-ardour-autoroute` **2.53%**,
`parvagues-sc-watchdog` 0.48%, `parvagues-bridge` 0.07%, `midiviz` 0.09%.
Neither of the big two is slow per call (`aconnect -l` ~10 ms, `pw-link -l`
~11 ms); they just make 3-5 of them plus awk every 2 s to re-discover an
unchanged graph. Both have a real event source: `pw-link -m/--monitor` blocks
and prints on link/port change, and ALSA-seq exposes
`/proc/asound/seq/clients`, which a bash `read` can compare between ticks for
free. Reconcile on change instead of on a timer and each drops to ~0.1%.
Total idle rig cost today: **~14.5% of a core with nothing playing**; protect's
fix takes that to ~5.8%, and these two would take it under 1%.
- **`rig-doctor`'s `parvagues-protect: PASS` tests for files, not for function.** - **`rig-doctor`'s `parvagues-protect: PASS` tests for files, not for function.**
It checks that `bin` and `unit` exist — the exact pair that stayed true all It checks that `bin` and `unit` exist — the exact pair that stayed true all
through 2026-09-06 while the daemon could not write a single `oom_score_adj` through 2026-09-06 while the daemon could not write a single `oom_score_adj`
......
...@@ -46,99 +46,216 @@ ...@@ -46,99 +46,216 @@
# #
# It also only ever LOWERS oom_score_adj and only RAISES scheduling. Re-running # It also only ever LOWERS oom_score_adj and only RAISES scheduling. Re-running
# it is a no-op, so it composes with perf.sh instead of fighting it. # it is a no-op, so it composes with perf.sh instead of fighting it.
#
# WHY IT FORKS NOTHING
# --------------------
# Measured 2026-09-06, with NOTHING RUNNING TO PROTECT: 218 ms of CPU per 2 s
# tick, or 8.6-10.7% of one core, forever, on a laptop with a worn battery. The
# work was not protection — it was three `pgrep -x` calls, at 59 ms each,
# because pgrep walks every /proc entry and reads cmdline for all of them, and
# it was called once per target. Add `cat`, two `chrt -p` reads and an
# unconditional `prlimit` per pid, each in a command substitution, and a tick
# that changes nothing still spent a fifth of a second.
#
# So this file now answers every question from /proc with bash builtins:
#
# 3x pgrep + cat + 2x chrt -p + prlimit + $(...) 218 ms/tick 8.6%
# one bash pass over /proc/*/comm, zero forks 32 ms/tick 1.3%
#
# The pattern that made it cheap is the cheap prefilter: a `case` glob on the
# first character rejects 606 of 641 processes before bash's regex engine — far
# and away the slowest thing in reach — is ever consulted. Testing the regex
# against every comm costs 149 ms/tick, WORSE than the pgrep it replaced.
#
# Forks are still allowed on the rare path: `chrt -f` and `prlimit` run only
# when a value is actually wrong, which is a handful of times per set, not
# 43200 times a day.
set -uo pipefail set -uo pipefail
INTERVAL="${PARVAGUES_PROTECT_INTERVAL:-2}" INTERVAL="${PARVAGUES_PROTECT_INTERVAL:-2}"
# name:rtprio — the priorities perf.sh established and PLN has played on. # name:rtprio:comm-regex — the priorities perf.sh established and PLN has
# scsynth is the audio server, sclang feeds it, Ardour records the stems; losing # played on. scsynth is the audio server, sclang feeds it, Ardour records the
# any of the three mid-set is a lost take. # stems; losing any of the three mid-set is a lost take.
#
# The regex matches the BINARY (comm), never the command line. Earned
# 2026-08-16, immediately: `pgrep -f ardour` reported Ardour running when it was
# not, because it matched `bash /home/pln/.local/bin/tidal-ardour-autoroute.sh`
# — the routing script has "ardour" in its path. A protector that thinks it is
# guarding Ardour while actually guarding a shell script is worse than one that
# admits it found nothing. Ardour's binary is versioned (ardour8, ardour9) and
# the process that does the work reports comm=ArdourGUI, so the family is listed
# explicitly. Anchored at both ends, so it cannot be fooled by a path.
TARGETS=( TARGETS=(
"scsynth:90" "scsynth:90:scsynth"
"sclang:85" "sclang:85:sclang"
"ardour:80" "ardour:80:ardour|ardour[0-9]+|ArdourGUI|ardour-[0-9.]+"
) )
OOM_TARGET=-1000 OOM_TARGET=-1000
LAST_FAILURE="" # last logged failure message, to keep a stuck fault quiet LAST_FAILURE="" # last logged failure message, to keep a stuck fault quiet
log() { printf '%s %s\n' "$(date '+%H:%M:%S')" "$*"; } # One combined regex, built once, for the single test each candidate gets. Plus
# the first characters of every target name, for the prefilter that does the
# real work. Both are DERIVED from TARGETS: adding a target cannot forget them.
ALL_RE=""
PREFIX_GLOB=""
for t in "${TARGETS[@]}"; do
_re="${t#*:}"; _re="${_re#*:}"
ALL_RE+="|$_re"
done
ALL_RE="^(${ALL_RE#|})\$"
# comm is case-sensitive and ArdourGUI capitalises, so both cases of every
# target's initial go in — as a CHARACTER CLASS, `[saSA]*`, not the alternation
# `s*|S*|a*|A*` that reads more naturally. `case` recognises `|` at parse time,
# so an alternation arriving from a variable is one pattern containing literal
# pipes and matches nothing: the prefilter rejected every process on the machine
# and the scan came back empty with all its parts individually correct. A
# bracket class survives expansion intact. Caught by the differential test
# below, never by reading it.
for t in "${TARGETS[@]}"; do
_c="${t:0:1}"
case "$PREFIX_GLOB" in
*"$_c"*) ;;
*) PREFIX_GLOB+="${_c,}${_c^}" ;;
esac
done
PREFIX_GLOB="[${PREFIX_GLOB}]*"
# pids_for <name> — match the BINARY (comm), never the command line. log() {
# # printf's %()T is bash's own clock. `date` here was one fork per logged line
# Earned 2026-08-16, immediately: `pgrep -f ardour` reported Ardour running when # — cheap, but this file's whole point is that nothing runs that need not.
# it was not, because it matched `bash /home/pln/.local/bin/tidal-ardour- printf '%(%H:%M:%S)T %s\n' -1 "$*"
# autoroute.sh` — the routing script has "ardour" in its path. A protector that }
# thinks it is guarding Ardour while actually guarding a shell script is worse
# than one that admits it found nothing. # scan — one pass over /proc, no forks, fills FOUND (pid -> "name prio") and
# PRESENT (name -> pid list, for --check's "absent" line).
# #
# Ardour's binary is versioned (ardour8, ardour9) and the process that actually # Deliberately STATELESS: no pid -> name cache, however tempting. The kernel
# does the work reports comm=ArdourGUI, so the family is listed explicitly. # recycles pids, so a cache saying "pid 4711 is not a target" is a stale binding
# `pgrep -x` matches comm exactly, which cannot be fooled by a path. # — this rig's most expensive recurring bug shape, and the exact one documented
pids_for() { # at the top of this file. A cached miss would silently leave scsynth naked. The
case "$1" in # cache would save 25 ms per tick and cost a lost take.
ardour) pgrep -x 'ardour|ardour[0-9]+|ArdourGUI|ardour-[0-9.]+' 2>/dev/null ;; declare -A FOUND PRESENT
*) pgrep -x "$1" 2>/dev/null ;; scan() {
esac FOUND=(); PRESENT=()
local p comm pid t name prio re
for p in /proc/[0-9]*; do
# stderr first: the redirect can fail on its own when a process exits
# mid-glob, and `2>/dev/null` only silences that if it is already in place.
read -r comm 2>/dev/null < "$p/comm" || continue
case "$comm" in $PREFIX_GLOB) ;; *) continue ;; esac
[[ $comm =~ $ALL_RE ]] || continue
pid="${p#/proc/}"
for t in "${TARGETS[@]}"; do
name="${t%%:*}"; prio="${t#*:}"; prio="${prio%%:*}"
re="${t#*:}"; re="${re#*:}"
if [[ $comm =~ ^($re)$ ]]; then
FOUND[$pid]="$name $prio"
PRESENT[$name]+="$pid "
break
fi
done
done
} }
# Effective capabilities, for failure messages. "need root" was the wrong # Effective capabilities, for failure messages. "need root" was the wrong
# question: this runs AS root and still could not write, because the unit's # question: this runs AS root and still could not write, because the unit's
# CapabilityBoundingSet had dropped CAP_DAC_OVERRIDE. Print what we actually # CapabilityBoundingSet had dropped CAP_DAC_OVERRIDE. Print what we actually
# hold so the next failure names its own cause instead of misdirecting. # hold so the next failure names its own cause instead of misdirecting.
# Forks capsh, but only on the failure path, which must never be quiet.
_caps() { _caps() {
local eff local eff
eff=$(sed -n 's/^CapEff:\s*//p' /proc/self/status 2>/dev/null) eff=$(sed -n 's/^CapEff:\s*//p' /proc/self/status 2>/dev/null)
capsh --decode="$eff" 2>/dev/null | sed -n 's/^0x[0-9a-f]*=//p' || echo "$eff" local d; d=$(capsh --decode="$eff" 2>/dev/null | sed -n 's/^0x[0-9a-f]*=//p')
echo "${d:-${eff:-none}}"
} }
# protect_pid <pid> <name> <rtprio> -> echoes what it CHANGED, nothing if already fine. # sched_of <pid> -> sets S_POL / S_PRIO from /proc/<pid>/stat. Replaces two
# Reporting only changes is deliberate: at a 2s poll a "still fine" line would be # `chrt -p` forks and their two `sed` forks with one read.
# 43200 journal entries a day, which is the same as no logging at all. #
protect_pid() { # Fields are 1-indexed per proc(5); field 2 is comm and may contain spaces and
local pid="$1" name="$2" prio="$3" changed="" # parens, so everything is measured from the LAST ')'. After it, field 3 is
# index 0 — hence 40-3=37 for rt_priority and 41-3=38 for policy. Verified
# against `chrt -p` on SCHED_FIFO/95 (pipewire), FIFO/50 (irq threads) and
# SCHED_OTHER/0 before this replaced the fork.
S_POL=""; S_PRIO=""
sched_of() {
local line rest; local -a f
S_POL=""; S_PRIO=""
read -r line 2>/dev/null < "/proc/$1/stat" || return 1
rest="${line##*\) }"
read -ra f <<< "$rest"
S_PRIO="${f[37]:-0}"
case "${f[38]:-}" in
0) S_POL=SCHED_OTHER ;;
1) S_POL=SCHED_FIFO ;;
2) S_POL=SCHED_RR ;;
3) S_POL=SCHED_BATCH ;;
5) S_POL=SCHED_IDLE ;;
6) S_POL=SCHED_DEADLINE ;;
*) S_POL="policy${f[38]:-?}" ;;
esac
}
local cur_oom # memlock_unlimited <pid> — read it before setting it. `prlimit --memlock` ran
cur_oom=$(cat "/proc/$pid/oom_score_adj" 2>/dev/null) || return 0 # unconditionally every tick for every pid: one fork to re-assert a value that
# was already correct 43199 times out of 43200.
memlock_unlimited() {
local l
while read -r l; do
case "$l" in
"Max locked memory"*)
case "$l" in *unlimited*unlimited*) return 0 ;; *) return 1 ;; esac ;;
esac
done < "/proc/$1/limits" 2>/dev/null
return 1
}
# protect_pid <pid> <name> <rtprio> — appends to the global CHANGED, rather than
# echoing into a $(...) the caller then forks a subshell to capture. Reports
# only what it CHANGED: at a 2s poll a "still fine" line would be 43200 journal
# entries a day, which is the same as no logging at all.
CHANGED=""
protect_pid() {
local pid="$1" name="$2" prio="$3" out="" cur_oom
read -r cur_oom 2>/dev/null < "/proc/$pid/oom_score_adj" || return 0
if [ "$cur_oom" != "$OOM_TARGET" ]; then if [ "$cur_oom" != "$OOM_TARGET" ]; then
if echo "$OOM_TARGET" > "/proc/$pid/oom_score_adj" 2>/dev/null; then if echo "$OOM_TARGET" > "/proc/$pid/oom_score_adj" 2>/dev/null; then
changed+=" oom:${cur_oom}->${OOM_TARGET}" out+=" oom:${cur_oom}->${OOM_TARGET}"
else else
changed+=" oom:FAILED(uid $(id -u), caps $(_caps))" out+=" oom:FAILED(uid $(id -u), caps $(_caps))"
fi fi
fi fi
# Only promote. A process already at a HIGHER realtime priority than we ask # Only promote. A process already at a HIGHER realtime priority than we ask
# for was tuned by someone who knew more than this script does. # for was tuned by someone who knew more than this script does.
local pol cur_prio sched_of "$pid" || return 0
pol=$(chrt -p "$pid" 2>/dev/null | sed -n '1s/.*: //p') if [ "$S_POL" != "SCHED_FIFO" ] || [ "${S_PRIO:-0}" -lt "$prio" ]; then
cur_prio=$(chrt -p "$pid" 2>/dev/null | sed -n '2s/.*: //p')
if [ "$pol" != "SCHED_FIFO" ] || [ "${cur_prio:-0}" -lt "$prio" ]; then
if chrt -f -p "$prio" "$pid" 2>/dev/null; then if chrt -f -p "$prio" "$pid" 2>/dev/null; then
changed+=" sched:${pol:-?}/${cur_prio:-?}->FIFO/${prio}" out+=" sched:${S_POL:-?}/${S_PRIO:-?}->FIFO/${prio}"
else else
changed+=" sched:FAILED" out+=" sched:FAILED"
fi fi
fi fi
# Locked memory can never be paged out — with no swap on this box it mostly # Locked memory can never be paged out — with no swap on this box it mostly
# matters as insurance, but it is free and perf.sh has always set it. # matters as insurance, but it is free and perf.sh has always set it.
prlimit --pid "$pid" --memlock=unlimited 2>/dev/null memlock_unlimited "$pid" || prlimit --pid "$pid" --memlock=unlimited 2>/dev/null
[ -n "$changed" ] && echo "$name[$pid]$changed" [ -n "$out" ] && CHANGED+="$name[$pid]$out; "
return 0 return 0
} }
sweep() { sweep() {
local out="" name prio pid local pid nameprio out
for t in "${TARGETS[@]}"; do CHANGED=""
name="${t%%:*}"; prio="${t##*:}" scan
for pid in $(pids_for "$name"); do for pid in "${!FOUND[@]}"; do
local r; r=$(protect_pid "$pid" "$name" "$prio") nameprio="${FOUND[$pid]}"
[ -n "$r" ] && out+="$r; " protect_pid "$pid" "${nameprio%% *}" "${nameprio##* }"
done
done done
out="${out%; }" out="${CHANGED%; }"
if [ -z "$out" ]; then if [ -z "$out" ]; then
return 0 return 0
fi fi
...@@ -165,10 +282,11 @@ sweep() { ...@@ -165,10 +282,11 @@ sweep() {
# Absent processes are NOT a failure here: "Ardour is not open yet" is a normal # Absent processes are NOT a failure here: "Ardour is not open yet" is a normal
# state ten minutes before doors, and the gate already checks liveness elsewhere. # state ten minutes before doors, and the gate already checks liveness elsewhere.
check() { check() {
local bad=0 found=0 name prio pid adj pol pids local bad=0 found=0 name pid adj pids
scan
for t in "${TARGETS[@]}"; do for t in "${TARGETS[@]}"; do
name="${t%%:*}"; prio="${t##*:}" name="${t%%:*}"
pids=$(pids_for "$name") pids="${PRESENT[$name]:-}"
# Say so out loud. A silently-skipped target is how a protector reports all # Say so out loud. A silently-skipped target is how a protector reports all
# clear while covering nothing — and it is exactly how `pgrep -f ardour` # clear while covering nothing — and it is exactly how `pgrep -f ardour`
# fooled this script's first draft. # fooled this script's first draft.
...@@ -178,31 +296,32 @@ check() { ...@@ -178,31 +296,32 @@ check() {
fi fi
for pid in $pids; do for pid in $pids; do
found=1 found=1
adj=$(cat "/proc/$pid/oom_score_adj" 2>/dev/null || echo "?") read -r adj 2>/dev/null < "/proc/$pid/oom_score_adj" || adj="?"
pol=$(chrt -p "$pid" 2>/dev/null | sed -n '1s/.*: //p') sched_of "$pid" || S_POL=""
if [ "$adj" != "$OOM_TARGET" ] || [ "$pol" != "SCHED_FIFO" ]; then if [ "$adj" != "$OOM_TARGET" ] || [ "$S_POL" != "SCHED_FIFO" ]; then
echo "UNPROTECTED $name[$pid] oom_score_adj=$adj sched=${pol:-none} (want $OOM_TARGET / SCHED_FIFO)" echo "UNPROTECTED $name[$pid] oom_score_adj=$adj sched=${S_POL:-none} (want $OOM_TARGET / SCHED_FIFO)"
bad=1 bad=1
else else
echo "ok $name[$pid] oom_score_adj=$adj sched=$pol" echo "ok $name[$pid] oom_score_adj=$adj sched=$S_POL/${S_PRIO}"
fi fi
done done
done done
[ "$found" -eq 0 ] && { echo "nothing running to protect"; return 0; } [ "$found" -eq 0 ] && { echo "nothing running to protect"; return 0; }
return $bad return $bad
} }
case "${1:-}" in case "${1:-}" in
--check) check; exit $? ;; --check) check; exit $? ;;
--once) sweep; exit 0 ;; --once) sweep; exit 0 ;;
--help|-h) --help|-h)
sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//' sed -n '2,72p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;; exit 0 ;;
"") ;; "") ;;
*) echo "usage: $(basename "$0") [--check|--once|--help]" >&2; exit 2 ;; *) echo "usage: $(basename "$0") [--check|--once|--help]" >&2; exit 2 ;;
esac esac
log "parvagues-protect: watching ${TARGETS[*]} every ${INTERVAL}s (oom=$OOM_TARGET)" _names=""
for t in "${TARGETS[@]}"; do _p="${t#*:}"; _names+=" ${t%%:*}/${_p%%:*}"; done
log "parvagues-protect: watching${_names} every ${INTERVAL}s (oom=$OOM_TARGET)"
while :; do while :; do
sweep sweep
sleep "$INTERVAL" sleep "$INTERVAL"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment