Skip to content

Keep frame payloads in slots placed with each pipeline instance - #97

Merged
kahrendt merged 1 commit into
mainfrom
issue-68-frame-payload-slab
Oct 16, 2026
Merged

kahrendt merged 1 commit into
mainfrom
issue-68-frame-payload-slab

Conversation

@kahrendt

Copy link
Copy Markdown
Contributor

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's FramePayload (frame_payload.h) holds one slot and returns it when destroyed, on any thread. sched::Frame::payload is a FramePayload.
  • Slots per instance: sched::payload_slots(), the frames over the staging ring's span plus profile::PAYLOAD_LAG_US of 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.
  • FrameEncoder encodes into the caller's span, so FLAC and Opus write straight into the slot (no encoded_ copy).
  • An instance or analysis with every slot taken makes no progress and posts PayloadsFull once. 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.
  • Folded in: GroupToCoreFifo signals a preallocated Wakeup instead of EventLoop::post()ing its drain. On the host each post allocated two blocks, which the new allocation test found.
  • Docs: the internals.md channel 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 (AllocationCount in tests/freed_blocks.h), with a Control row.
  • GroupAudioPipelines.WaitsForPayloadSlotsWithoutLosingInput (white-box, two slots): it posts PayloadsFull once 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 in test_sched_frames.cpp.
  • Gaps: the allocation test measures core's side through a stand-in delivery into a FrameRing, not Group::on_frame(). src/esp/ is untouched, but the ESP-IDF build was not compiled locally.

Closes #68

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.
@kahrendt
kahrendt marked this pull request as ready for review October 16, 2026 13:25
Copilot AI balanced review requested due to automatic review settings October 16, 2026 13:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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.

@kahrendt
kahrendt merged commit 655a5db into main Oct 16, 2026
14 checks passed
@kahrendt
kahrendt deleted the issue-68-frame-payload-slab branch October 16, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recycle frame payload buffers from core back to the group audio threads

2 participants