Skip to content

feat: assert no allocations on the RT audio path#238

Merged
OpenSauce merged 2 commits into
mainfrom
experiment-assert-no-alloc
May 2, 2026
Merged

feat: assert no allocations on the RT audio path#238
OpenSauce merged 2 commits into
mainfrom
experiment-assert-no-alloc

Conversation

@OpenSauce
Copy link
Copy Markdown
Owner

Closes #237.

Summary

  • Adds assert_no_alloc (warn_debug feature) and a new rustortion-core/tests/no_alloc.rs (20 tests) that runs Engine::process inside an assert_no_alloc scope across all 10 stage types, both IR convolver implementations, and the full standalone-style engine.
  • Wraps the existing standalone integration tests (rustortion-standalone/tests/engine.rs) with assert_no_alloc after their original assertions for end-to-end coverage.
  • Documents three known RT-path allocators (peak_meter, pitch_shifter, recorder) with permit_alloc + FIXME comments at their call sites in engine.rs. The underlying modules are the proper fix targets.
  • Wraps the two unavoidable Box::new(...) sites in Engine::handle_messages (IR convolver retire, samplers retire) with permit_allocRtDropHandle takes Box<dyn Send> so type erasure forces an allocation. Follow-up: hold the values as Box<T> on the engine side to remove the allocation entirely.

Known offenders (permit_alloc'd, FIXME)

  • audio/peak_meter.rs:62Arc::new(PeakMeterInfo) per block. Trivial fix: replace Arc<ArcSwap<PeakMeterInfo>> with three atomics.
  • audio/pitch_shifter.rs — Vec scratch buffers in process_block.
  • audio/recorder.rs:47Vec::with_capacity per block.
  • audio/engine.rs:296,333 — boxing for RtDropHandle::retire (channel type erasure).

Test plan

  • cargo test -p rustortion-core --test no_alloc — 20 passed
  • cargo test -p rustortion-standalone --test engine — 7 passed (4 new alloc assertions added)
  • make test — full suite green
  • make lint — clean (clippy with -D warnings -D pedantic -D nursery)

Out of scope (follow-up work)

  • Plugin (rustortion-plugin) feature flag for nih_plug/assert_process_allocs.
  • Standalone JACK callback wrap.
  • Proper fixes for the four FIXME'd allocations above.

Adds the `assert_no_alloc` crate (warn_debug feature) and per-stage,
per-IR-cabinet, and full-engine test coverage that runs `Engine::process`
inside an `assert_no_alloc` scope and asserts zero violations.

Existing standalone integration tests are also wrapped to give end-to-end
coverage. Three known offenders (peak_meter, pitch_shifter, recorder)
allocate on the RT path; their call sites in `engine.rs` are wrapped with
`permit_alloc` and FIXME comments pointing to the underlying modules to
fix in a follow-up.

Two real RT-thread `Box::new` sites in `Engine::handle_messages` (IR
convolver swap and samplers swap) are wrapped with `permit_alloc` because
the `RtDropHandle` channel takes `Box<dyn Send>` and there's no way to
type-erase without boxing — proper fix is a separate refactor.

Closes #237.
@OpenSauce OpenSauce marked this pull request as ready for review May 2, 2026 11:20
Copilot AI review requested due to automatic review settings May 2, 2026 11:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds real-time allocation auditing around the audio engine by introducing assert_no_alloc-based tests in rustortion-core and rustortion-standalone, while annotating a few currently known RT-path allocators inside Engine::process() with permit_alloc. In the wider codebase, this is intended to codify the “no heap traffic on the audio thread” contract and keep future engine/stage changes from regressing it.

Changes:

  • Added a new rustortion-core/tests/no_alloc.rs test suite that exercises the engine hot path, per-stage processing, IR convolution, and standalone-style engine extras under assert_no_alloc.
  • Extended standalone engine integration tests to assert that repeated Engine::process() calls stay allocation-free after setup.
  • Wrapped known RT-path allocation sites in rustortion-core/src/audio/engine.rs with permit_alloc, and added the required assert_no_alloc dependency entries.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
rustortion-standalone/tests/engine.rs Adds allocation assertions to existing standalone engine integration tests.
rustortion-standalone/Cargo.toml Adds assert_no_alloc as a standalone test dependency.
rustortion-core/tests/no_alloc.rs New comprehensive RT no-allocation audit test suite for core engine/stage paths.
rustortion-core/src/audio/engine.rs Marks known allocator call sites with permit_alloc inside the RT processing path.
rustortion-core/Cargo.toml Adds assert_no_alloc to core crate dependencies.
Cargo.lock Locks the new assert_no_alloc dependency and workspace dependency graph updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +394 to +399
fn full_engine_does_not_allocate() {
// Covers: Engine::new path with tuner (disabled), metronome
// (disabled), and peak meter (always-on once present). The peak meter
// allocates internally; engine.rs wraps that call in permit_alloc with
// a FIXME pointing to peak_meter.rs:62.
let (mut engine, _handle) = full_engine(1.0, None);
}

#[test]
fn pitch_shifter_does_not_allocate() {
Comment on lines +165 to +166
// FIXME(no_alloc): PitchShifter::process_block uses Vec scratch
// buffers internally — see audio/pitch_shifter.rs.
Comment on lines +65 to +70
/// Run `engine.process()` `iters` times inside an `assert_no_alloc` scope
/// after one warm-up call. Panics if any allocation was observed.
fn assert_engine_alloc_free(engine: &mut Engine, input: &[f32], output: &mut [f32], iters: usize) {
// Warm up once outside the assertion to amortise any first-call setup.
engine.process(input, output).unwrap();

Comment thread rustortion-core/tests/no_alloc.rs Outdated
Comment on lines +444 to +455
// recorder.rs:47. Recorder::new opens a WAV file + spawns a thread,
// which is one-shot setup wrapped in permit_alloc here.
let (mut engine, handle) = full_engine(1.0, None);
let tmp = std::env::temp_dir().join("rustortion_no_alloc_rec");
let recorder =
permit_alloc(|| Recorder::new(SAMPLE_RATE as u32, tmp.to_str().unwrap()).unwrap());
handle
.start_recording(SAMPLE_RATE, tmp.to_str().unwrap())
.unwrap();
// Drop the locally-built recorder; the engine will use the one it
// created from the StartRecording message.
drop(recorder);
Comment thread rustortion-core/tests/no_alloc.rs Outdated
Comment on lines +365 to +367
let dir = std::env::temp_dir().join("rustortion_no_alloc_ir");
let _ = write_test_ir(&dir, "tiny.wav", 1024);
let loader = IrLoader::new(&dir, SAMPLE_RATE).unwrap();
- Wrap PitchShifter::new in handle_pitch_shift with permit_alloc + FIXME.
  Caught by Copilot review: SetPitchShift drained inside Engine::process()
  constructs the FFT scratch buffers on the RT thread.
- Restructure pitch_shifter_does_not_allocate to send SetPitchShift inside
  the assert scope so the construction path is actually audited (the
  warm-up call would otherwise hide it).
- Drop the dead local Recorder + manual drop in the recorder test that
  also risked a filename-timestamp collision with the engine's recorder.
- Use tempfile::tempdir() for the IR loader and recorder tests so parallel
  runs and stale files can't pollute each other.
- Doc-clarify check_no_alloc and assert_engine_alloc_free contracts.
- Make full_engine_does_not_allocate doc honest about the peak_meter gate.
@OpenSauce OpenSauce enabled auto-merge (squash) May 2, 2026 11:31
@OpenSauce OpenSauce merged commit 635b5a1 into main May 2, 2026
7 checks passed
@OpenSauce OpenSauce deleted the experiment-assert-no-alloc branch May 2, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce no-allocation on the RT audio path

2 participants