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.
Showing
tools/gig-log.py
0 → 100644
This diff is collapsed.
Click to expand it.
tools/tests/test_gig_log.py
0 → 100644
This diff is collapsed.
Click to expand it.
Please
register
or
sign in
to comment