From 244367ccb91e1c6e5f7263ad63fbee30838747b2 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 21 May 2026 17:06:11 +1000 Subject: [PATCH] plan: mark phases 7 + 8 done, close BUSY-spike follow-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §11 — annotated Phase 7 and Phase 8 (a–e) inline with what landed and where, so the section now reads as a commit-log index rather than a forward-looking todo. The "Tracked follow-ups" subsection keeps the two trigger-gated dormant items (ephemeral overlay, sub-ms dispatch primitive) and strikes through the filter-playback BUSY spike — 8e's ~3 min release-build capture didn't reproduce the ~8×-baseline outlier pattern from the original 6c smoke finding, so the work to be done collapsed to "instrumentation kept, no code change." §11 preamble now notes "all planned phases (0–8) are done as of 2026-05-21"; §12 picks up the same theme by pointing to team-memory `headroom-project` for current per-risk status. Memory bumps go to ~/.claude-amaan: `headroom-project` description + Phase 7 entry + revised "How to apply" (no more "next planned work"), `headroom-routing-link-bug` moves both Phase 4k "still-open follow-ups" to a "Closed (4l)" section, and MEMORY.md's project hook is updated to reflect "all phases shipped, audio threads validated alloc-free, packaging modules in place". --- PLAN.md | 99 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 28 deletions(-) diff --git a/PLAN.md b/PLAN.md index 7d3e62b..c2b5374 100644 --- a/PLAN.md +++ b/PLAN.md @@ -831,7 +831,10 @@ builds and any CI go through `nix build`. ## 11. Phased implementation -The phases are roughly token-of-work units, not calendar weeks. +The phases are roughly token-of-work units, not calendar weeks. **All +planned phases (0–8) are done as of 2026-05-21**; this section is +preserved as historical context + a reading guide to the commit log. +See [[headroom-project]] in team memory for the per-commit ledger. **Phase 0 — scaffolding.** Flake, workspace, crate skeletons, README, PLAN/IPC docs. *(done as part of this commit)* @@ -891,33 +894,23 @@ Sub-stages used in commits / TODOs: ### Tracked follow-ups (carried past their sub-stage) Items deliberately deferred from earlier sub-stages so they don't get -lost. Pick up by name when the phase that consumes them lands. +lost. Pick up by name when the trigger that gates them fires. - **Ephemeral overlay mutations.** *(4e follow-up.)* All `route.set` / `setting.set` changes are persisted to `overlay.toml`. A `--ephemeral` flag (or `--volatile`) on the CLI for one-shot tweaks that don't outlive the daemon was considered and dropped from v0 for simplicity. Revisit if real users ask for it; the store-level - change is a flag on the setter methods. -- **Filter playback BUSY spikes (periodic, ~10 s cadence).** *(6c - manual smoke finding, 2026-05.)* On a quiet system with AGC and - per-app both off, the filter's `playback_process` BUSY - occasionally spikes from its ~240 μs steady-state to ~2.0 ms, - correlating with output-sink WAIT spikes of similar size. No - audible impact (sub-quantum at 21 ms). The ~10 s cadence rules - out sliding-max worst-case (which would be input-pattern-driven, - not periodic) and Layer A (the spikes persist with `per_app.enabled - = false`). Suspects with 10 s clocks somewhere: WirePlumber session - policy heartbeat, PipeWire internal graph re-eval, or system-level - scheduling (CPU governor, kernel housekeeping). Diagnostic for - Phase 8: timestamp the playback callback, log when its measured - duration crosses ~1 ms; correlate with `journalctl`, - `wireplumber --verbose`, and `pw-dump` snapshots taken around the - spike. If we can't attribute it to PipeWire-side reschedule and - it's something we can fix in our callback, the candidate - workaround is to break the limiter's per-block work into smaller - chunks (cap allocations / pops / branches per call) for more - predictable timing. + change is a flag on the setter methods. **Dormant** — no user has + asked through Phase 8. +- ~~**Filter playback BUSY spikes (periodic, ~10 s cadence).**~~ + **Closed in 8e (`d52cd6d`).** The instrumentation added by 8e + did not reproduce the ~8×-baseline outlier pattern in a ~3 min + release-build capture; steady state was ~2.2 ms / call at this + hardware's quantum with max growing only to 1.3× baseline. + `PlaybackTiming` stays so future regressions surface at WARN. + Original observation may have been a transient WP/PW housekeeping + artefact under a different config; no actionable code change. - **Sub-millisecond dispatch primitive for spike-reactive writes.** *(Phase 6 optimisation, downgraded from prerequisite.)* The 4i `PwCommand` channel uses a 50 ms polling timer, fine for @@ -1042,18 +1035,68 @@ If those three say "fine," the §4.1 promise is upheld in practice and 6c is acceptance-tested. `jack_iodelay` and other true-round-trip tools are overkill. -**Phase 7 — Packaging.** systemd user unit, install paths, default -profile install, basic NixOS module. +**Phase 7 — Packaging.** *Done — `c65c75b`.* `contrib/systemd/headroom.service` +(user-scope, Type=simple, After=pipewire.service, Restart=on-failure, +journald, LimitRTPRIO=20). The package's `postInstall` substitutes +the unit's `@bindir@` placeholder with an absolute store path and +copies `profiles/*.toml` to `share/headroom/profiles/`. Two Nix +modules: `nixosModules.default` (`programs.headroom.enable` — +binary on global PATH + `systemd.packages` for `systemctl --user` +discovery + hard assertion on `services.pipewire.enable`) and +`homeModules.default` (`services.headroom.enable` — symlinks +shipped profiles into `$XDG_CONFIG_HOME/headroom/profiles/`, +`extraProfiles` attrset for per-user overrides, writes the systemd +user unit). README rewritten with install + usage sections. -**Phase 8 — Hardening.** Latency budget verification on real hardware, -Bluetooth-handoff edge case, profile-reload while audio is flowing, -multi-rate hardware, allocation-tracer sweep with -`assert_no_alloc` in debug. +**Phase 8 — Hardening.** *Done — `9220143` + `d52cd6d` + verification.* +- **8a — `assert_no_alloc` on audio-thread callbacks (`9220143`).** + `#[global_allocator] AllocDisabler` in `headroom-cli/src/main.rs` + behind `cfg(debug_assertions)` (release strips it via the crate's + default `disable_release`). The three RT callbacks + (`capture_process`, `playback_process`, `tap_process`) wrap their + body in `assert_no_alloc(|| inner(...))`. Verified by a deliberate + `Vec::with_capacity` injection → SIGABRT on first audio callback; + reverted before commit. Audio thread proven alloc-free under + multi-thousand-callback live load. +- **8b — live profile-reload under signal flow (verification only).** + Edit `$XDG_CONFIG_HOME/headroom/profiles/.toml` while a + sine plays: notify-debouncer-mini fires, `ProfileStore::reload` + runs, `setting.set` propagates via `FilterControl`'s rtrb to the + audio thread. Compressor GR went 0 → −9.3 dB ≈ 1 s after edit + and back to 0 after restore; 180 meter ticks over 9 s with max + inter-tick gap = exact 50.0 ms (the AGC period). No glitches. +- **8c — sink hotplug / default-sink change (verification only).** + `wpctl set-default ` while daemon runs: + `on_metadata_property` fires, `adopt_new_real_sink` runs, + filter.playback re-pinned via 4k explicit-link enforcement, + `routing/real_sink_changed` emitted on the wire. Bounces back + cleanly. +- **8d — multi-rate hardware (partial / deferred).** Filter is + hardcoded F32 stereo @ 48 kHz; PipeWire's link layer inserts a + resampler at the filter.playback → real-sink edge when rates + differ; bus DSP stays at 48 kHz internally. Architecture is + sound; real-hardware validation (USB DAC at 96k etc.) deferred + until available. +- **8e — playback callback timing instrumentation (`d52cd6d`).** + Lock-free `PlaybackTiming` atomics in `meters.rs`; AGC controller + drains once per second and logs at WARN above + `SPIKE_THRESHOLD_US = 5000`. The original ~10 s-cadence ~8× + spike pattern from §11 follow-ups *did not reproduce* in a ~3 min + release-build capture; steady state 2.2 ms / call at ~4 Hz, + max climbed to only 1.3× baseline. Instrumentation kept so + future regressions surface. --- ## 12. Risks & open questions +These are the original v0 design risks — still useful as a checklist +for new contributors. Phase 4k/4l/8c have exercised the routing / +hotplug / default-sink branches; the bullets below are unchanged +since several of them remain live concerns for non-NixOS distros +and multi-rate hardware. See [[headroom-project]] in team memory +for current status per risk. + - **WirePlumber re-linking on device hotplug.** When a Bluetooth headset connects, WP re-evaluates linking. Headroom must re-pin its routed streams. Tractable; the registry events surface this.