AetherEngine 5.20.8
Long-running software-host sessions no longer leak into a Jetsam kill (#205)
A live session on the SoftwarePlaybackHost grew its heap for as long as it ran. After 30 to 60 minutes on an Apple TV the host app was terminated under memory pressure, and nothing the engine tracks itself pointed at a culprit: the packet ring, the segment cache and the audio FIFO all stayed flat while mallocBlocks climbed linearly at roughly 4,200 blocks per second.
The growth was untracked small allocations, and the cause was structural. Each of the host's long-running loops (the live reader on demuxQueue, the live feeder and its audio look-ahead pump on feedQueue, and the VOD demux loop) runs inside a single DispatchQueue.async work item that does not return for the lifetime of the session. GCD drains a thread's autorelease pool when the work item finishes, so for these loops it never drained at all. Every per-iteration ObjC and CoreFoundation temporary accumulated: the isReadyForMoreMediaData back-pressure polling, which reaches 200 Hz while parked, the renderer and audio enqueues, the NSDate bridging in condition.wait(until:), and log formatting. SoftwareVideoDecoder already wrapped its per-frame emit in an autoreleasepool for exactly this reason; the loops themselves never got one.
Each loop body now lives in a local iteration function that the outer while calls inside an autoreleasepool, so the pool drains once per packet. The drains also cover the inner wait and poll loops, which matters because a single outer iteration can park for a long time: the paused condition wait that ticks every 0.5 s, the video back-pressure spin, the live-edge wait, and the background audio-only pacing gate. A pool around only the outer body would have let a long pause keep accruing.
Measured on an Apple TV 4K (3rd generation) running tvOS 26.5, live TV through the software host:
| before | after | |
|---|---|---|
| physical footprint | 174 MB to 451 MB over 571 s, linear | 162 MB to 164 MB over 782 s, flat |
mallocBlocks |
483k to 2.87M, no plateau | 355k to 916k, plateaus at t=602s |
AudioPlaybackHost.runDemuxLoop has the same loop shape and received the same treatment. No leak was measurable there (its physical footprint was already flat across a five minute Opus session), so that half is parity rather than a fix.
Reported, diagnosed and fixed by @nathanpiper, including the on-device before and after measurements above. The background audio-only pacing gate, the mirror image of the video back-pressure gate, was swept in a follow-up commit.
Covered by: 999 tests green, strict-concurrency clean, tvOS and iOS Simulator builds green.