Repository navigation
Keep frame payloads in slots placed with each pipeline instance - #97
Merged
Merged
Conversation
Each encoded frame and visualizer cell carried its payload in a vector the group audio threads allocated per frame and core freed when its ring dropped it. Each pipeline instance now places a slab of slots of its format's largest frame when it is built; a frame's FramePayload holds one slot and returns it when destroyed, on whichever thread. An instance with every slot taken posts PayloadsFull and waits; core prunes its ring and wakes the pipelines once a slot is free, or arms a timer for its earliest frame. The group-to-core FIFO signals a preallocated Wakeup instead of posting its drain, which allocated on every post on the host.
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-thread payload reclamation and wakeup lifecycle changes warrant human review, particularly with local ESP-IDF build validation outstanding.
0 open findings
What changed in this PR
Replaces per-frame payload allocations with reusable, per-instance slots in the audio pipeline, addressing #68.
Changes:
- Adds slot-backed payload ownership and encoding directly into caller-provided buffers.
- Adds slot-exhaustion recovery and replaces FIFO drain posts with preallocated wakeups.
- Updates tests and documentation for allocation-free steady-state frame production and disposal.
| File | Description |
|---|---|
| tests/test_visualizer_stream.cpp | Uses slot-backed test payloads. |
| tests/test_source_decoder.cpp | Adapts encoder calls to output buffers. |
| tests/test_server_source.cpp | Updates FLAC encoding helpers. |
| tests/test_sched_property.cpp | Uses slot-backed generated frames. |
| tests/test_sched_frames.cpp | Tests slot sizing and reclamation. |
| tests/test_player_stream.cpp | Updates test frame payloads. |
| tests/test_payload_slots.cpp | Tests slot ownership, reuse and moves. |
| tests/test_group_internals.cpp | Tests pruning, wakeups and restarts. |
| tests/test_group_audio.cpp | Adds steady-state allocation checks. |
| tests/test_group_audio_internals.cpp | Tests recovery from exhausted slots. |
| tests/test_frame_encoder.cpp | Tests output bounds and buffer encoding. |
| tests/group_pipelines.h | Adapts fixtures to move-only payloads. |
| tests/freed_blocks.h | Adds sanitizer allocation counting. |
| tests/frame_payloads.h | Adds payload and encoding helpers. |
| tests/CMakeLists.txt | Registers tests and documents private access. |
| src/visualizer_stream.cpp | Reads payloads through spans. |
| src/sched/frames.h | Adds slot-backed frames and capacity sizing. |
| src/profile.h | Defines payload retention allowance. |
| src/player_stream.cpp | Packs chunks from payload spans. |
| src/payload_slots.h | Declares reusable slot storage. |
| src/payload_slots.cpp | Implements slot allocation and reclamation. |
| src/group.h | Declares slot-exhaustion tracking and timer. |
| src/group.cpp | Prunes full rings and wakes pipelines. |
| src/group_audio.h | Adds payload stores and wakeup-based FIFO state. |
| src/group_audio.cpp | Produces slot-backed frames and handles exhaustion. |
| src/frame_payload.h | Defines move-only payload ownership. |
| src/frame_encoder.h | Declares span-based encoding APIs. |
| src/frame_encoder.cpp | Encodes directly into supplied buffers. |
| docs/internals.md | Documents payload recycling and wakeups. |
| docs/conventions.md | Updates allocation and synchronization standards. |
| cmake/sources.cmake | Includes the slot implementation. |
| CLAUDE.md | Updates component and test references. |
| .claude/skills/realtime-review/SKILL.md | Updates FIFO budgeting guidance. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Each encoded frame and visualizer cell carried its payload in a vector that the group audio threads allocated per frame and core freed when its ring dropped the frame. Producing and dropping frames now allocates nothing in steady state, on either thread.
Change
PayloadSlots(payload_slots.h): one pipeline instance's slab of slots, each the format's largest frame (B_f, or a visualizer cell's bytes), placed when the instance is built, with a free list under a leaf mutex. A frame'sFramePayload(frame_payload.h) holds one slot and returns it when destroyed, on any thread.sched::Frame::payloadis aFramePayload.sched::payload_slots(), the frames over the staging ring's span plusprofile::PAYLOAD_LAG_USof frames core may leave unpruned. Sized for the worst case for the instance's life: about 1 MB for a 48 kHz 24-bit stereo FLAC instance on the embedded profile, about 3.7 MB on a host, more at high rates.FrameEncoderencodes into the caller's span, so FLAC and Opus write straight into the slot (noencoded_copy).PayloadsFullonce. Core wakes the pipelines once the ring holds fewer frames than the slots: after a prune or a ring restart, or at a timer set for the earliest frame.GroupToCoreFifosignals a preallocatedWakeupinstead ofEventLoop::post()ing its drain. On the host each post allocated two blocks, which the new allocation test found.internals.mdchannel table and playback steps,conventions.md, CLAUDE.md.Tests
test_payload_slots.cpp: reserve and commit, release in any order, moves, a store that outlives its owner.GroupAudioAllocations.ProducesAndDropsFramesWithoutAllocating: PCM, FLAC and Opus instances and a visualizer analysis allocate nothing after a warm-up, counted through the sanitizer's malloc hook (AllocationCountintests/freed_blocks.h), with a Control row.GroupAudioPipelines.WaitsForPayloadSlotsWithoutLosingInput(white-box, two slots): it postsPayloadsFullonce per wait, and its frames match those of pipelines with room.GroupPayloads.*(white-box): core's prune, wake, timer and the ring restarts.FrameEncoderOutput.RefusesRoomShortOfTheFrameBound, plus the slot sizing and drop tests intest_sched_frames.cpp.FrameRing, notGroup::on_frame().src/esp/is untouched, but the ESP-IDF build was not compiled locally.Closes #68