Commit cfda4841 by PLN (Algolia)

perf: one owner for the CPU knobs — the Bridge drives thermal-mode, the watcher is gone

The Bridge's perf.py shelled out to its own root script (perf-audio) and ran a
watcher that reasserted its last choice every 30 s, on resume and on every
charger edge. gig-up armed it with 'standard' = --optimize = governor
performance, which pins EPP and runs every idle wakeup at 4-5 GHz. On
2026-09-23 it undid a Silent click from the GNOME panel within 31 s and held
the package at 96-100 C at ~5 % load all morning.

- MODES now name thermal-mode-apply arguments (silent, performance); set_mode
  runs the exact-args sudoers form; detect_mode reads /etc/thermal-mode.conf.
- run_watcher/reconcile/desired-file removed: thermal-mode-reassert.service
  already re-applies the persisted mode on resume and charger events.
- gig-up asserts thermal-mode performance (turbo on, powersave governor, EPP
  balance_performance, RT audio) instead of demanding governor=performance.
- rig-doctor and gig-preflight point at thermal-mode-apply, not perf-audio.
parent e534a059
......@@ -103,19 +103,21 @@ pulsar_up(){ pgrep -x pulsar >/dev/null 2>&1; }
# to Cool in the tray when you're done. RT priority is already fine (rtprio 95).
# Skip with GIG_PERF=off. ---
ensure_perf(){
[ "${GIG_PERF:-standard}" = off ] && { info "perf: auto-assert disabled (GIG_PERF=off)."; return; }
local maxp gov
maxp=$(cat /sys/devices/system/cpu/intel_pstate/max_perf_pct 2>/dev/null || echo 100)
gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null)
if [ "${maxp:-100}" -ge 100 ] 2>/dev/null && [ "$gov" = performance ]; then
ok "perf: CPU uncapped + performance — headroom for MIDI bursts."; return
[ "${GIG_PERF:-performance}" = off ] && { info "perf: auto-assert disabled (GIG_PERF=off)."; return; }
# The gig posture is thermal-mode `performance`: turbo on, governor POWERSAVE +
# EPP balance_performance, RT audio. NOT governor=performance: that pins EPP and
# runs every idle wakeup at ~4-5 GHz (package 96 C at 5 % load, 2026-09-23).
local mode noturbo
mode=$(sed -n 's/^MODE=//p' /etc/thermal-mode.conf 2>/dev/null)
noturbo=$(cat /sys/devices/system/cpu/intel_pstate/no_turbo 2>/dev/null || echo 1)
if [ "$mode" = performance ] && [ "$noturbo" = 0 ]; then
ok "perf: thermal-mode performance holding — turbo headroom for MIDI bursts."; return
fi
info "perf: CPU capped at ${maxp}% ('$gov') → asserting Standard for the set…"
# set_mode writes the desired-state file too, so the watcher won't revert it.
if PYTHONPATH="$DIR/tools/bridge" python3 -c "import perf,sys; ok,m=perf.set_mode('standard'); print(m); sys.exit(0 if ok else 1)" 2>/dev/null; then
ok "perf: Standard set + held by the watcher (fans may rise under load — fine on stage; tray → Cool to revert)."
info "perf: thermal-mode is '${mode:-unset}' → switching to performance for the set…"
if PYTHONPATH="$DIR/tools/bridge" python3 -c "import perf,sys; ok,m=perf.set_mode('performance'); print(m); sys.exit(0 if ok else 1)" 2>/dev/null; then
ok "perf: performance set (thermal-mode owns it; resume/charger reassert included). Back to quiet after: Thermal panel → Silent."
else
warn "perf: auto-set failed — switch to STANDARD in the perf tray/Bridge yourself."
warn "perf: auto-set failed — pick Performance in the Thermal panel yourself."
fi
}
......
......@@ -3,7 +3,7 @@
python3 bridge.py serve [--host --port] # run the dashboard (default 127.0.0.1:8773)
python3 bridge.py status # print a perf/thermal snapshot
python3 bridge.py perf [cool|standard|extreme|normal] # read or switch mode
python3 bridge.py perf [silent|performance] # read or switch mode (thermal-mode)
Runs under system python3 (stdlib only). See README to install the systemd
--user service for always-on autostart.
......@@ -36,11 +36,8 @@ def cmd_perf(a):
def cmd_serve(a):
# Keep the chosen perf regime asserted across AC/battery flips & resume
# (the platform silently resets pstate caps on those events).
import threading
threading.Thread(target=P.run_watcher, daemon=True,
name="perf-watcher").start()
# No perf watcher: thermal-mode-reassert.service re-applies the persisted
# mode on resume and charger events, and a second reasserter here is a fight.
import server
server.run(host=a.host, port=a.port)
......
"""perf — rootless thermal/perf-mode reader + sudo-backed mode switch.
Ported from perf-tray.py (the PyQt tray) with the Qt dropped, so the same proven
logic backs the web Bridge. Reading thermals/mode is rootless (hwmon + cpufreq
sysfs); only *changing* mode needs root, done via `sudo -n` against a ROOT-OWNED,
sudoers-whitelisted copy of perf.sh at /usr/local/sbin/perf-audio (see
perf-audio.sudoers) — never the user-editable repo copy.
"""perf — rootless thermal reader + the Bridge's mode buttons.
Reading thermals is rootless (hwmon + cpufreq sysfs). *Changing* mode goes
through the SRE thermal tooling (`thermal-mode-apply`, via gearbox.HELPER), the
same single owner of the pstate/RAPL/fan knobs that the GNOME panel and
perf-tray drive. This module used to shell out to its own root script
(perf-audio) and ran a watcher that reasserted its choice every 30 s; with two
owners of the same knobs, whichever wrote last won, and on 2026-09-23 the
watcher undid a Silent click within 31 s and held the governor at `performance`
(EPP pinned, ~4 GHz at 5 % load, package 96 C) through a whole morning.
Resume / AC-plug reassert is thermal-mode-reassert.service's job now.
DRY note: perf-tray.py still has its own copy of Thermals/detect_mode; a later
pass can have it import this module. Kept separate now to avoid touching the
......@@ -17,18 +21,16 @@ import os
import subprocess
import time
# Root-owned, sudoers-whitelisted deployment of perf.sh (same as perf-tray).
SCRIPT = os.environ.get("PERF_TRAY_SCRIPT", "/usr/local/sbin/perf-audio")
import gearbox as GB
# mode key -> (label, perf.sh flag). Order = coolest → hottest → normal; the
# tray menu and Bridge toolbar both iterate this, so a new mode lights up in
# both faces automatically.
# mode key -> (label, thermal-mode-apply argument). Order = quiet -> hard; the
# Bridge toolbar iterates this. Every argument is an exact-args NOPASSWD rule in
# /etc/sudoers.d/thermal-mode, so nothing beyond a key from this table ever
# reaches sudo. `performance` is the gig posture: turbo on, but with the
# powersave governor + EPP balance_performance (idle stays calm) and RT audio.
MODES = {
"silent": ("Silent", "--silent"),
"cool": ("Cool", "--cool"),
"standard": ("Standard", "--optimize"),
"extreme": ("Extreme", "--extreme"),
"normal": ("Normal", "--stop"),
"silent": ("Silent", "silent"),
"performance": ("Performance", "performance"),
}
# temp thresholds (°C) → state name; the UI maps state→color (matches tray).
......@@ -252,137 +254,33 @@ class Thermals:
def detect_mode():
gov = _read("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
epp = _read("/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference")
maxp = _read_int("/sys/devices/system/cpu/intel_pstate/max_perf_pct", 100)
minp = _read_int("/sys/devices/system/cpu/intel_pstate/min_perf_pct", 0)
if gov == "performance" and minp >= 100:
return "extreme"
if gov == "performance":
return "standard"
# Silent and cool are both powersave + capped clock; the EPP tells them apart
# (silent biases all the way to "power", cool keeps "balance_performance").
if gov == "powersave" and epp in ("power", "balance_power") and (maxp or 100) < 100:
return "silent"
if epp == "balance_performance" and (maxp or 100) < 100:
return "cool"
return "normal"
"""The mode thermal-mode persisted (its conf is the one source of truth),
or "other" when it is something this toolbar does not offer. The live
sysfs readings in snapshot() say whether it actually holds."""
m = GB.read_conf().get("MODE")
return m if m in MODES else "other"
def script_available():
"""True if the sudoers-whitelisted root perf.sh is deployed."""
return os.path.exists(SCRIPT)
"""True if the root thermal-mode helper is installed."""
return GB.installed()
def set_mode(mode):
"""Switch perf mode via `sudo -n`. Returns (ok, message)."""
"""Switch mode via `sudo -n thermal-mode-apply <mode>`. Returns (ok, message)."""
if mode not in MODES:
return False, f"unknown mode {mode!r}; have {sorted(MODES)}"
if not script_available():
return False, (f"{SCRIPT} not deployed — install the root-owned perf.sh + "
"sudoers rule (see README 'Deploy mode-switching')")
flag = MODES[mode][1]
return False, f"{GB.HELPER} not installed (see SRE/thermal/install.sh)"
try:
r = subprocess.run(["sudo", "-n", SCRIPT, flag],
r = subprocess.run(["sudo", "-n", GB.HELPER, MODES[mode][1]],
capture_output=True, text=True, timeout=30)
except (OSError, subprocess.SubprocessError) as e:
return False, str(e)
out = (r.stdout or "").strip() or (r.stderr or "").strip()
if r.returncode != 0:
return False, (r.stderr or r.stdout or f"exit {r.returncode}").strip()
write_desired(mode) # record intent so the watcher can reassert it (below)
return True, (r.stdout or "ok").strip()
# --- power-aware desired-state maintenance -----------------------------------
# One chosen regime, reasserted whenever the platform silently resets the pstate
# caps underneath us. The trigger that started this: plugging in on AC lifts the
# battery turbo limit, so a "cool" set is quietly undone and temps jump ~25°C.
# Also fires on resume-from-suspend. The Bridge daemon runs run_watcher().
#
# "desired" is the mode the user actually chose (written by set_mode on both the
# tray and web faces). desired absent or "normal" == maintenance off, so we never
# fight a deliberate switch back to platform defaults.
DESIRED_PATH = os.environ.get(
"PERF_DESIRED_FILE", os.path.expanduser("~/.cache/parvagues/perf-desired"))
def read_desired():
"""The mode to keep asserted, or None if maintenance is off."""
m = _read(DESIRED_PATH)
return m if m in MODES else None
def write_desired(mode):
"""Persist the chosen regime (called on every successful mode switch)."""
if mode not in MODES:
return
try:
os.makedirs(os.path.dirname(DESIRED_PATH) or ".", exist_ok=True)
with open(DESIRED_PATH, "w") as f:
f.write(mode)
except OSError:
pass
def on_ac():
"""True on AC, False on battery, None if no mains adapter is exposed."""
for p in sorted(glob.glob("/sys/class/power_supply/A*/online")):
v = _read_int(p)
if v is not None:
return bool(v)
return None
def reconcile(force=False):
"""Reassert the desired mode if live state has drifted from it. Returns
(acted, message). desired absent/"normal" == maintenance off."""
desired = read_desired()
if not desired or desired == "normal":
return False, "maintenance off"
if not force and detect_mode() == desired:
return False, f"{desired} holding"
ok, msg = set_mode(desired)
return ok, (f"reasserted {desired} ({msg})" if ok else f"reassert FAILED ({msg})")
def run_watcher(interval=5.0, drift_every=6, log=None):
"""Daemon loop: keep the desired perf regime across AC/battery flips and
resume-from-suspend. Reasserts on a power-source edge and on resume (both
force), plus a periodic drift check as the safety net. Blocks; run in a
thread. `log` is a callable(str); defaults to stdout."""
emit = log or (lambda s: print(f"[perf-watch] {s}", flush=True))
if read_desired() is None: # seed from whatever is live right now
cur = detect_mode()
if cur in MODES and cur != "normal":
write_desired(cur)
# BOOTTIME (not MONOTONIC) counts time spent suspended, so a resume shows up
# as a big gap here — MONOTONIC freezes across suspend and would miss it,
# leaving only the slow periodic drift check to catch a post-resume reset.
_bt = getattr(time, "CLOCK_BOOTTIME", None)
uptime = (lambda: time.clock_gettime(_bt)) if _bt is not None else time.monotonic
emit(f"watching (every {interval}s) on_ac={on_ac()} desired={read_desired()}")
last_ac = on_ac()
last_clock = uptime()
i = 0
while True:
time.sleep(interval)
i += 1
now = uptime()
resumed = (now - last_clock) > interval * 3 # gap ≫ sleep ⇒ we were suspended
last_clock = now
ac = on_ac()
edge = ac is not None and last_ac is not None and ac != last_ac
if edge or resumed:
reason = ("power→" + ("AC" if ac else "battery")) if edge else "resume"
time.sleep(2) # let the platform settle
_, msg = reconcile(force=True)
emit(f"{reason}: {msg}")
elif i % drift_every == 0:
acted, msg = reconcile(force=False)
if acted:
emit(f"drift: {msg}")
last_ac = ac if ac is not None else last_ac
return False, out or f"exit {r.returncode}"
return True, out or "ok"
def snapshot(therm: "Thermals | None" = None) -> dict:
......
......@@ -19,24 +19,41 @@ def test_temp_state_thresholds():
def test_modes_shape():
assert set(P.MODES) == {"silent", "cool", "standard", "extreme", "normal"}
assert P.MODES["extreme"][1] == "--extreme"
assert P.MODES["normal"][1] == "--stop"
assert P.MODES["silent"][1] == "--silent"
def test_detect_mode_silent_vs_cool(monkeypatch):
# silent and cool share powersave + a capped clock; EPP disambiguates.
def fake_reads(gov, epp, maxp):
monkeypatch.setattr(P, "_read", lambda p, d=None:
{"scaling_governor": gov,
"energy_performance_preference": epp}.get(p.split("/")[-1], d))
monkeypatch.setattr(P, "_read_int", lambda p, d=None:
maxp if p.endswith("max_perf_pct") else (0 if p.endswith("min_perf_pct") else d))
fake_reads("powersave", "power", 55)
# Every argument must be an exact-args sudoers form of thermal-mode-apply.
assert set(P.MODES) == {"silent", "performance"}
assert all(arg in {"silent", "performance"} for _, arg in P.MODES.values())
def test_detect_mode_reads_the_thermal_mode_conf(monkeypatch, tmp_path):
conf = tmp_path / "thermal-mode.conf"
monkeypatch.setattr(P.GB, "CONF", conf)
conf.write_text("MODE=silent\nTRIM=auto\n")
assert P.detect_mode() == "silent"
fake_reads("powersave", "balance_performance", 85)
assert P.detect_mode() == "cool"
conf.write_text("MODE=performance\n")
assert P.detect_mode() == "performance"
conf.write_text("MODE=cool\n") # a mode the toolbar does not offer
assert P.detect_mode() == "other"
conf.unlink()
assert P.detect_mode() == "other"
def test_set_mode_argv_is_the_whitelisted_form(monkeypatch):
import types
seen = {}
def fake_run(argv, **kw):
seen["argv"] = argv
return types.SimpleNamespace(returncode=0, stdout="applied\n", stderr="")
monkeypatch.setattr(P, "script_available", lambda: True)
monkeypatch.setattr(P.subprocess, "run", fake_run)
assert P.set_mode("performance") == (True, "applied")
assert seen["argv"] == ["sudo", "-n", P.GB.HELPER, "performance"]
def test_no_watcher_left():
# Two reasserters of one set of knobs is a fight (2026-09-23).
assert not hasattr(P, "run_watcher") and not hasattr(P, "reconcile")
def test_gpu_manager_mode_parse_and_cache(monkeypatch):
......@@ -70,8 +87,8 @@ def test_set_mode_rejects_unknown():
def test_set_mode_guards_missing_script(monkeypatch):
monkeypatch.setattr(P, "script_available", lambda: False)
ok, msg = P.set_mode("cool")
assert ok is False and "not deployed" in msg
ok, msg = P.set_mode("silent")
assert ok is False and "not installed" in msg
def test_snapshot_shape():
......@@ -80,10 +97,10 @@ def test_snapshot_shape():
"freq_max_mhz", "throttle_delta", "script_available", "modes",
"dgpu", "gpu_manager", "thermald"):
assert k in s
assert s["mode"] in P.MODES
assert s["mode"] in set(P.MODES) | {"other"}
assert isinstance(s["cores_c"], list)
assert isinstance(s["script_available"], bool)
def test_detect_mode_returns_known():
assert P.detect_mode() in P.MODES
assert P.detect_mode() in set(P.MODES) | {"other"}
......@@ -218,7 +218,7 @@ def check_audio_rt() -> None:
f"{policy} prio {prio.group(1) if prio else '?'}")
else:
add(f"{proc} realtime", WARN, f"policy={policy} (not realtime)",
"sudo /usr/local/sbin/perf-audio --optimize")
"sudo -n /usr/local/sbin/thermal-mode-apply performance # RT audio chain")
def check_superdirt_fresh() -> None:
......@@ -367,8 +367,7 @@ def check_editor() -> None:
worst = max(nices)
if worst > 0:
add("editor priority", WARN, f"nice={worst} (deprioritized)",
"sudo install -m 755 ~/Work/Sound/Tidal/perf.sh /usr/local/sbin/perf-audio"
" # then: bridge.py perf standard")
"sudo renice -n 0 -p $(pgrep -d' ' -f pulsar)")
else:
add("editor priority", OK, f"nice={worst}")
......
......@@ -1181,27 +1181,22 @@ def check_audio_interface() -> None:
# INSTALL SURFACE (not in git — the real portability blockers)
# --------------------------------------------------------------------------- #
def check_perf_audio() -> None:
bin_state = path_state(Path("/usr/local/sbin/perf-audio"))
sudo_state = path_state(Path("/etc/sudoers.d/perf-audio"))
install_cmd = (
"sudo install -m 755 -o root -g root ~/Work/Sound/Tidal/perf.sh "
"/usr/local/sbin/perf-audio && "
"sudo install -m 440 -o root -g root ~/Work/Sound/Tidal/perf-audio.sudoers "
"/etc/sudoers.d/perf-audio && sudo visudo -cf /etc/sudoers.d/perf-audio"
)
# The mode switch is the SRE thermal tooling now (thermal-mode-apply +
# its exact-args sudoers); perf-audio is retired (SRE TODO.d #52).
bin_state = path_state(Path("/usr/local/sbin/thermal-mode-apply"))
sudo_state = path_state(Path("/etc/sudoers.d/thermal-mode"))
install_cmd = "sudo ~/Work/Perso/SRE/thermal/install.sh"
if bin_state == "present" and sudo_state == "present":
add("INSTALL SURFACE", "perf-audio + sudoers", PASS,
"/usr/local/sbin/perf-audio and /etc/sudoers.d/perf-audio both present")
elif sudo_state == "unknown":
add("INSTALL SURFACE", "perf-audio + sudoers", WARN,
f"bin={bin_state}, sudoers=permission denied to check as this user "
f"(that's expected — /etc/sudoers.d is 750 root:root)",
f"verify as root: sudo test -e /etc/sudoers.d/perf-audio && echo ok. "
f"If missing: {install_cmd}")
add("INSTALL SURFACE", "thermal-mode + sudoers", PASS,
"/usr/local/sbin/thermal-mode-apply and /etc/sudoers.d/thermal-mode both present")
elif sudo_state == "unknown" and bin_state == "present":
add("INSTALL SURFACE", "thermal-mode + sudoers", PASS,
"helper present; /etc/sudoers.d is 750 root:root so the rule itself "
"is checked by: sudo -n -l | grep thermal-mode-apply")
else:
add("INSTALL SURFACE", "perf-audio + sudoers", FAIL,
f"bin={bin_state}, sudoers={sudo_state} — perf tray can't switch "
f"modes without a password prompt", install_cmd)
add("INSTALL SURFACE", "thermal-mode + sudoers", FAIL,
f"bin={bin_state}, sudoers={sudo_state} — the Bridge and tray can't "
f"switch modes without a password prompt", install_cmd)
# CAP_DAC_OVERRIDE is capability number 1, so bit 1 of the capability bitmask.
......
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