Commit 699e93c7 by PLN (Algolia)

perf-tray: show rig health (icon dot on trouble, one readout row)

Reads the health.json gig-log publishes and renders tray_summary(): a red dot
for down or a stale observer, amber for degraded, nothing when healthy. A
single owner-unit action (start lcxl3-driver) appears only while the surface
check fails; the tray never writes the graph. A missing file reads as 'not
observed', never as ok.
parent 3c161f8c
......@@ -54,6 +54,8 @@ from perf import (Thermals, dgpu_state, thermald_active, # noqa: E402
power_caps_w, gpu_manager_mode)
import gearbox as GB # noqa: E402
import launchers as LA # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parent / "tools"))
import rig_health # noqa: E402 (the one health.json gig-log publishes; this only READS it)
SERVICE = "perf-tray" # systemd --user unit controlling autostart
REFRESH_MS = 2000
......@@ -175,7 +177,10 @@ TRIM_INK = {"quiet": "#9fa8da", # indigo: the fans are being held back
"cool": "#4dd0e1"} # cyan: the fans are allowed to work
def make_icon(temp_c, hist=None, gear=None, trim=None):
HEALTH_DOT = {"down": "#ff3b30", "stale": "#ff3b30", "warn": "#ffb020"}
def make_icon(temp_c, hist=None, gear=None, trim=None, badge=None):
"""Heat-coloured badge + the temp number over a REAL sparkline of the recent
package-temp history (the wave is live data now, not a decorative sine),
ringed by the gear + trim so the lever is visible with the menu shut."""
......@@ -236,6 +241,15 @@ def make_icon(temp_c, hist=None, gear=None, trim=None):
i = rw / 2.0
p.drawRoundedRect(QRectF(2 + i, 2 + i, 60 - rw, 60 - rw), 14 - i, 14 - i)
# Rig health: a dot, ONLY on trouble, drawn last so nothing paints over it. A
# healthy rig adds nothing to the icon; the heat badge stays the face.
if badge in HEALTH_DOT:
p.setBrush(QColor(HEALTH_DOT[badge]))
pen = QPen(QColor("white"))
pen.setWidth(3)
p.setPen(pen)
p.drawEllipse(QRectF(40, 2, 22, 22))
p.end()
return QIcon(px)
......@@ -496,6 +510,20 @@ class PerfTray:
a.setFont(f)
self.menu.addAction(a)
self.stat_actions.append(a)
self.health_action = QAction("", self.menu) # rig health readout
self.health_action.setEnabled(False)
hf = self.health_action.font()
hf.setStyleHint(QFont.TypeWriter)
hf.setFamily("monospace")
self.health_action.setFont(hf)
self.menu.addAction(self.health_action)
# Owner-unit start only (the tray never writes the graph). Visible only
# while the surface driver check is failing.
self.fix_driver_action = QAction("↻ start the LCXL3 driver", self.menu)
self.fix_driver_action.triggered.connect(
lambda: QProcess.startDetached("systemctl", ["--user", "start", "lcxl3-driver.service"]))
self.fix_driver_action.setVisible(False)
self.menu.addAction(self.fix_driver_action)
self.menu.aboutToShow.connect(self.refresh)
self.menu.addSeparator()
......@@ -862,7 +890,8 @@ class PerfTray:
self.hist.pop(0)
gb = GB.state()
self.tray.setIcon(make_icon(pkg, self.hist, gb["gear"], gb["trim"]))
hs = rig_health.tray_summary(rig_health.read_health())
self.tray.setIcon(make_icon(pkg, self.hist, gb["gear"], gb["trim"], hs["badge"]))
# Check the gear the FILE says, never one inferred from sysfs. The old
# menu inferred it (perf.detect_mode reads governor/EPP/max_perf_pct), and
......@@ -920,6 +949,8 @@ class PerfTray:
f"freq {favg}/{fmax} MHz avg/max · fans {' / '.join(str(f) for f in fans) or 'n/a'} RPM\n"
f"gpu {gpu} · thermald {td} · throttle Δ {t.throttle_delta()}"
)
if hs["tip"]:
tip += "\n" + "\n".join(hs["tip"])
self.tray.setToolTip(tip)
rows = [
......@@ -930,6 +961,8 @@ class PerfTray:
]
for a, txt in zip(self.stat_actions, rows):
a.setText(txt)
self.health_action.setText(hs["row"])
self.fix_driver_action.setVisible("start_driver" in hs["fixes"])
# The Qt-drawn popup gets the Ship's Bridge look (armada/DESIGN.md): tonal dark
......
......@@ -379,3 +379,35 @@ def read_health(path: Path | None = None, now=time.time) -> dict:
doc["detail"] = f"health.json is {age:.0f}s old: the observer is dead or wedged"
doc["age_s"] = round(age, 1)
return doc
# --------------------------------------------------------------------------- #
# the tray's view (no Qt here: the tray only renders what this returns)
# --------------------------------------------------------------------------- #
def tray_summary(h: dict) -> dict:
"""What the tray should show for a read_health() document.
badge None | "warn" | "down" | "stale" (a dot on the icon, only on trouble)
row one monospace line for the menu
tip extra tooltip lines (failing checks, in words)
fixes action keys the menu may offer; only owner-unit starts, never a
graph write (writer discipline: the tray does not touch pw-link)
"""
overall = h.get("overall", "unknown")
checks = h.get("checks", {})
bad = [(n, c) for n, c in sorted(checks.items()) if c.get("state") == FAIL]
if overall == "ok":
return {"badge": None, "row": "rig: ok", "tip": [], "fixes": []}
if overall == "degraded":
warn = [n for n, c in sorted(checks.items()) if c.get("state") == WARN]
return {"badge": "warn", "row": "rig: degraded · " + ", ".join(warn),
"tip": [f"rig: {n} {checks[n]['detail']}" for n in warn], "fixes": []}
if overall == "down":
return {"badge": "down", "row": "rig: DOWN · " + ", ".join(n for n, _ in bad),
"tip": [f"rig: {n}: {c['detail']}" for n, c in bad],
"fixes": ["start_driver"] if any(n == "surface" for n, _ in bad) else []}
if overall == "stale":
return {"badge": "stale", "row": "rig: observer stale",
"tip": [h.get("detail", "health.json is stale")], "fixes": []}
# unknown / stopped: gig-log is not running. Not a fault on a work day.
return {"badge": None, "row": "rig: not observed (gig-log off)", "tip": [], "fixes": []}
......@@ -234,3 +234,34 @@ def test_evidence_capture_writes_a_private_file_and_keeps_only_a_few(tmp_path):
assert p and p.read_text().startswith("check: audio_clock")
assert oct(p.stat().st_mode & 0o777) == "0o600"
assert len(list(tmp_path.glob("*.txt"))) <= rh.EVIDENCE_KEEP
# --------------------------------------------------------------------------- #
# the tray's view
# --------------------------------------------------------------------------- #
def _doc(overall, **checks):
return {"overall": overall, "checks": {n: {"state": s, "detail": d} for n, (s, d) in checks.items()}}
def test_tray_is_quiet_when_all_is_well():
s = rh.tray_summary(_doc("ok", audio_clock=(rh.OK, "fine")))
assert s["badge"] is None and s["row"] == "rig: ok" and s["fixes"] == []
def test_tray_names_what_is_down_and_offers_the_driver_start_only_for_the_surface():
s = rh.tray_summary(_doc("down", surface=(rh.FAIL, "no faders"), audio_clock=(rh.OK, "x")))
assert s["badge"] == "down" and "surface" in s["row"] and s["fixes"] == ["start_driver"]
s2 = rh.tray_summary(_doc("down", audio_clock=(rh.FAIL, "no hardware clock")))
assert s2["badge"] == "down" and s2["fixes"] == [] # never a graph write from the tray
def test_a_dead_observer_is_loud_but_an_absent_one_is_not():
"""stale = gig-log should be writing and is not (alarm). unknown = it is not
running at all, which is every work day (no badge)."""
assert rh.tray_summary({"overall": "stale", "detail": "x", "checks": {}})["badge"] == "stale"
assert rh.tray_summary({"overall": "unknown", "checks": {}})["badge"] is None
def test_degraded_names_the_warning_checks():
s = rh.tray_summary(_doc("degraded", superdirt=(rh.WARN, "slow")))
assert s["badge"] == "warn" and "superdirt" in s["row"]
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