Commit 4c57362d by PLN (Algolia)

feat(gig-log): a session recorder, so a run-through can be REVIEWED and not just felt

After a set, "did it glitch?" and "did it get hot?" and "which controls did I
actually use?" are answered from memory. Memory is a bad instrument, and the
answers matter: #8 (thermal impact under load), #56 (Pulsar starving audio),
#46/#11 (which surface controls are really in the hands). PLN records the
run-through in Ardour; this gives that take a machine-readable twin.

WHAT IT DOES
  `gig-log.py record`   1 Hz JSONL: package/core temp, fan, freq, throttle
                        counters, per-core cpu%, and per-gear %cpu + RSS for
                        scsynth / sclang / ArdourGUI / the whole pulsar process
                        group. Plus PipeWire xruns and every LCXL control move.
  `gig-log.py mark ...` annotate the live session ("gimme acid drop")
  `gig-log.py report`   render it back: sparklines, xrun timeline, gear table,
                        a per-CC surface table with first/last touch
  `gig-log.py status`   is it running, what is it writing
  `gig-log.py install`  systemd --user unit, enabled, starts with the session
  `gig-log.py selftest` prove the parsers AND the cost

Wall-clock stamped in the header, so the timeline lines up with the Ardour take.

THE OBSERVER MUST NOT PERTURB — measured, not asserted
This rig has twice produced the fault it was measuring: probe-chain's capture
streams caused the xruns it was hunting, and the LED daemon's per-event fork
caused the lag it was reporting. So: no audio capture at all, no per-sample
subprocess spawn (every number comes from sysfs/procfs), and exactly two
long-lived children (`pw-top -b`, `aseqdump`) drained by threads so a filling
pipe can never block them. Measured cost of the whole thing: 0.40% of one core
with 270 pw-top lines parsed in 10 s; 0.79% as the live systemd unit. The first
selftest reported 0.66% and was FLATTERING ITSELF — it never started the reader
threads, so it measured a sampler with nothing to parse. Same mistake as the
first --bench run in the LED work; fixed, then re-measured lower and honest.

AND THE LOGGER MUST NOT BE A FIREHOSE
A fader sweep is 100+ events/s. A logger that writes them all costs more than
what it measures, so CC/pitchbend are coalesced to one line per control per
second carrying count + first/last/min/max — the #71/#72 fix applied to
ourselves. 128 events on one knob is one line that still shows the whole travel.
NOTES ARE NEVER COALESCED: a CC is a state and may be superseded, a note is an
event and dropping one loses a thing that happened.

COUNTERS ARE DELTAS, AND THE BASELINE IS WHERE THE BUGS LIVED
Two absolute counters here are large and meaningless alone: package_throttle
was 17024 after 2 idle days, and Ardour's PipeWire ERR was 67072. Both count
history, including power excursions and device changes that never touched audio.
So the header records baselines and samples record deltas.

Getting Ardour's baseline right took three rules, and the first report caught
each one:
  1. baseline on FIRST sight -> pw-top prints a zero-filled snapshot before the
     profiler has data, so base=0 and a 25-second IDLE session reported 72096
     xruns.
  2. baseline on SECOND sight -> usually right. pw-top emits a
     non-deterministic NUMBER of zero tables, so it silently reported 67123 on
     the next real run. A rule that is right most of the time is the worst kind
     for a gig log, because the one bad reading looks exactly like a disaster.
  3. baseline = MAX over each node's first 5 sightings. ERR is monotonic within
     a node's lifetime, so the max over a warm-up IS the true starting count —
     no timing assumption at all. A value below the baseline means the node was
     destroyed and recreated, so re-baseline instead of reporting negative.
Validated by three independent 15 s runs, all reporting 0 (measure twice in
time before trusting one reading). The report DECLARES the warm-up blind spot
rather than hiding it.

Also learned on the way: Ardour accumulates ~6 xruns/min even with nothing
playing (67072 -> 67123 -> 67134 across captures minutes apart), which is
exactly why only the session delta may ever be quoted.

FIXED IN THE SHARED READER
perf._read(None) raised TypeError instead of returning the default, so on any
machine without a coretemp/dell_smm hwmon the thermal read CRASHED rather than
reading "unknown" — the Bridge shares this code path. Guarded.

Refactored _proc_cpu_rss into a pure parse_proc_stat() to make it testable, and
it needed to be: pulsar's renderer comms look like `(pulsar) --type=renderer`,
so splitting /proc/pid/stat on whitespace from the left shifts every field and
silently reports some other column as CPU.

TESTS: 71 new (tools/tests/test_gig_log.py), suite 211 -> 282, all green.
Covers real pw-top/aseqdump lines, every baseline regression above, coalescing
invariants, notes-survive-a-CC-flood, a torn final line costing one sample not
the log, absent gear degrading to partial data instead of a crash, and
sparklines bucketing by MAX so a one-second burst inside a 40-minute set cannot
be averaged away.
parent cd5c46bb
......@@ -36,6 +36,12 @@ TEMP_STATES = ((55, "cool"), (70, "warm"), (85, "hot"), (10_000, "critical"))
def _read(path, default=None):
# path can legitimately be None: the hwmon probes return None when a chip is
# absent (no coretemp on AMD, no dell_smm on non-Dell), and callers pass that
# straight back in. open(None) raises TypeError, not OSError, so without this
# guard a missing sensor crashes the reader instead of reading as "unknown".
if not path:
return default
try:
with open(path) as f:
return f.read().strip()
......
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