Voice: OpenWakeWordSession buffers grow without bound; masked today by P0-1 cancellation #32

Closed
opened 2026-05-12 19:06:45 +00:00 by KofTwentyTwo · 1 comment
KofTwentyTwo commented 2026-05-12 19:06:45 +00:00 (Migrated from github.com)

Summary

OpenWakeWordSession.melFrameBuffer and embeddingBuffer grow without bound. Streaming path appends mel frames on every feed(samples:) and advances nextEmbStart. Mel frames are never popped or trimmed; embeddingBuffer.append(emb) is also append-only. The classifier only reads suffix(16), but the rest is retained forever.

Evidence

  • File: packages/Voice/Sources/Voice/WakeWord/OpenWakeWordSession.swift:175-201
  • At 1 mel frame per 80 ms feed, 24h always-on listening accumulates:
    • ~1.08M mel frames × 32 floats × 4 B ≈ 130 MB live retained
    • ~135k embeddings × 96 × 4 B ≈ 50 MB
  • Plus Swift Array-of-Array overhead

Root cause / hypothesis

Today the leak is masked by P0-1 (#28): the DAG dies on every rebuild before it can leak. Fix P0-1 without this, and memory grows unbounded across long-running sessions.

Recommended fix

Trim melFrameBuffer and embeddingBuffer on a rolling-window basis inside feed(samples:):

  • Keep last 76 + embStride mel frames (enough for the embedding window)
  • Keep last 16 embeddings (matches suffix(16) classifier read)
if melFrameBuffer.count > 76 + embStride {
    melFrameBuffer.removeFirst(melFrameBuffer.count - (76 + embStride))
}
if embeddingBuffer.count > 16 {
    embeddingBuffer.removeFirst(embeddingBuffer.count - 16)
}

Test

  • Unit: feed 24h-equivalent of mel frames; assert RSS stays bounded
  • Soak: long-running session post-P0-1-fix → assert memory plateau

Source

  • Audit: .planning/audit-2026-05-12/voice-audit.md (2026-05-12) — finding P1-1

Cross-references

  • Compounds: #28 (P0-1) — must fix together; fixing P0-1 alone exposes this leak
## Summary `OpenWakeWordSession.melFrameBuffer` and `embeddingBuffer` grow without bound. Streaming path appends mel frames on every `feed(samples:)` and advances `nextEmbStart`. Mel frames are never popped or trimmed; `embeddingBuffer.append(emb)` is also append-only. The classifier only reads `suffix(16)`, but the rest is retained forever. ## Evidence - File: `packages/Voice/Sources/Voice/WakeWord/OpenWakeWordSession.swift:175-201` - At 1 mel frame per 80 ms feed, 24h always-on listening accumulates: - ~1.08M mel frames × 32 floats × 4 B ≈ 130 MB live retained - ~135k embeddings × 96 × 4 B ≈ 50 MB - Plus Swift Array-of-Array overhead ## Root cause / hypothesis Today the leak is masked by P0-1 (#28): the DAG dies on every rebuild before it can leak. Fix P0-1 without this, and memory grows unbounded across long-running sessions. ## Recommended fix Trim `melFrameBuffer` and `embeddingBuffer` on a rolling-window basis inside `feed(samples:)`: - Keep last `76 + embStride` mel frames (enough for the embedding window) - Keep last 16 embeddings (matches `suffix(16)` classifier read) ```swift if melFrameBuffer.count > 76 + embStride { melFrameBuffer.removeFirst(melFrameBuffer.count - (76 + embStride)) } if embeddingBuffer.count > 16 { embeddingBuffer.removeFirst(embeddingBuffer.count - 16) } ``` ## Test - Unit: feed 24h-equivalent of mel frames; assert RSS stays bounded - Soak: long-running session post-P0-1-fix → assert memory plateau ## Source - Audit: `.planning/audit-2026-05-12/voice-audit.md` (2026-05-12) — finding P1-1 ## Cross-references - Compounds: #28 (P0-1) — must fix together; fixing P0-1 alone exposes this leak
KofTwentyTwo commented 2026-05-13 11:52:02 +00:00 (Migrated from github.com)

Closed by v0.1 cluster merges on 2026-05-13. Develop now at a8c7f1a0.

Closed by v0.1 cluster merges on 2026-05-13. Develop now at `a8c7f1a0`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/Jarvis#32