//! Byte-sized bounded producer/consumer channel. //! //! Wraps `std::sync::mpsc::sync_channel` with a byte-accounting //! `Mutex + Condvar` cap. Sender blocks (cooperatively) when //! `used_bytes + item.byte_size() > capacity_bytes`. Receiver //! decrements `used_bytes` when it takes the item. //! //! Why: the existing producer→consumer channel between `DiscStream` //! (PES producer) and `MuxSink` (PES consumer) is bounded by frame //! count. Frame sizes vary 100× between metadata and keyframes, so a //! count-based cap either starves on small frames or buffers far too //! much memory on big ones. Byte-sized accounting sizes the buffer for //! the worst-case input stall (NFS read p99 ≈ 1–2 s × ~15 MB/s peak //! compressed bitrate ≈ ~30 MB) directly. //! //! The underlying mpsc channel is created with a very large slot count //! so the byte cap (not the slot count) is the real backpressure. Slot //! count is only there to give the kernel a small chunk to wake on. use std::sync::mpsc::{Receiver as MpscReceiver, RecvError, SendError, SyncSender, sync_channel}; use std::sync::{Arc, Condvar, Mutex}; /// Default byte cap for the muxer's input channel. Sized to hide a /// worst-case ~2 s NFS read refill at UHD peak compressed bitrate /// (~15 MB/s); 64 MiB gives headroom. Tweakable; not magic. pub const BYTE_CHANNEL_DEFAULT_CAPACITY: usize = 64 * 1024 * 1024; /// Slot capacity of the inner `sync_channel`. Large so the byte cap is /// the real backpressure mechanism — the mpsc slot count only exists /// to give the kernel a chunk to wake on. PES frames are typically /// ~700 B each, so 64 MiB ≈ 90 k frames; 200 k is comfortable headroom. const INNER_SLOT_CAPACITY: usize = 200_000; /// Anything whose in-memory cost can be accounted by a single /// `usize`. Implement on the item type sent through [`Sender`]. pub trait HasByteSize { /// Bytes this item contributes to the channel's used budget. /// Must be > 0 to make progress (a 0-byte item would never /// block the sender no matter the cap; see send_blocks_at_capacity /// test). fn byte_size(&self) -> usize; } impl HasByteSize for crate::pes::PesFrame { fn byte_size(&self) -> usize { // Frame data + the fixed header overhead the serializer // writes (track + pts + keyframe + len). The `Vec` heap // allocation also has alloc-header overhead but that's // <0.1 % at typical frame sizes — folding it in would just // add noise to the budget. self.data.len() + 14 } } /// Shared book-keeping between [`Sender`] and [`Receiver`]. Wrapped in /// an `Arc` because both halves hold it independently. struct Accounting { used: Mutex, cv: Condvar, capacity: usize, } /// Send half of the byte-bounded channel. /// /// `send` blocks (on a `Condvar`) when adding the item would push /// `used_bytes` past `capacity_bytes`. Unblocks when the receiver /// `recv`s items out and notifies. Returns `Err(item)` if the /// receiver has been dropped — mirrors `mpsc::SyncSender::send`. pub struct Sender { tx: SyncSender, acct: Arc, } impl Clone for Sender { fn clone(&self) -> Self { Sender { tx: self.tx.clone(), acct: self.acct.clone(), } } } impl Sender { /// Push one item. Blocks until adding it would not exceed the /// capacity, then sends through the inner mpsc channel. pub fn send(&self, item: T) -> Result<(), SendError> { let sz = item.byte_size(); // Reserve capacity first. The reservation is observable to // other senders via `used`; only after we win the slot do we // hand the item to the inner mpsc channel. That ordering means // `used` is always a conservative upper bound on what's in the // mpsc queue + about-to-be-sent. { let mut used = self.acct.used.lock().expect("byte_channel poisoned"); // An item bigger than the whole capacity will never fit; let // it through anyway as a one-shot reservation, otherwise the // sender deadlocks forever waiting for `used == 0` AND // nothing in flight. The receiver will drain it on the // other side. Same behaviour as `std::sync::mpsc` for // arbitrarily large messages. while *used + sz > self.acct.capacity && *used > 0 { used = self.acct.cv.wait(used).expect("byte_channel cv poisoned"); } *used += sz; } match self.tx.send(item) { Ok(()) => Ok(()), Err(SendError(returned)) => { // Receiver dropped — refund the reservation so a later // sender on a clone doesn't observe phantom used bytes // (the receiver is gone so nobody will decrement). let mut used = self.acct.used.lock().expect("byte_channel poisoned"); *used = used.saturating_sub(sz); self.acct.cv.notify_all(); Err(SendError(returned)) } } } } /// Receive half of the byte-bounded channel. /// /// `recv` blocks on the inner mpsc until an item is available, then /// decrements the byte-accounting and wakes any sender waiting on /// capacity. pub struct Receiver { rx: MpscReceiver, acct: Arc, } impl Receiver { /// Take the next item. Returns `Err(RecvError)` when all senders /// have been dropped and the channel is empty. pub fn recv(&self) -> Result { let item = self.rx.recv()?; let sz = item.byte_size(); let mut used = self.acct.used.lock().expect("byte_channel poisoned"); *used = used.saturating_sub(sz); // Notify all so multi-sender setups wake every blocked sender, // not just one. Wasted wakeups are cheap; missed wakeups would // be a deadlock. self.acct.cv.notify_all(); Ok(item) } } /// Create a byte-bounded channel with the given capacity in bytes. /// Returns a `(Sender, Receiver)` pair; clone the `Sender` for /// multi-producer setups. pub fn channel(capacity_bytes: usize) -> (Sender, Receiver) { let (tx, rx) = sync_channel::(INNER_SLOT_CAPACITY); let acct = Arc::new(Accounting { used: Mutex::new(0), cv: Condvar::new(), capacity: capacity_bytes, }); ( Sender { tx, acct: acct.clone(), }, Receiver { rx, acct }, ) } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread; use std::time::{Duration, Instant}; /// Test payload — its `byte_size` returns whatever we passed at /// construction so capacity math is exact and predictable. #[derive(Clone, Debug, PartialEq, Eq)] struct Item { sz: usize, tag: u32, } impl HasByteSize for Item { fn byte_size(&self) -> usize { self.sz } } #[test] fn send_recv_round_trip() { let (tx, rx) = channel::(1024); for i in 0..5 { tx.send(Item { sz: 100, tag: i }).unwrap(); } for i in 0..5 { let got = rx.recv().unwrap(); assert_eq!(got, Item { sz: 100, tag: i }); } } #[test] fn byte_accounting_decrements_on_recv() { // Internal book-keeping check via observable side-effect: after // sending K items totalling N bytes and receiving them all, a // subsequent send of an N-byte item must NOT block (no items // in flight, all capacity refunded). let (tx, rx) = channel::(1024); for _ in 0..4 { tx.send(Item { sz: 256, tag: 0 }).unwrap(); } for _ in 0..4 { rx.recv().unwrap(); } // Cap is now fully available again. Send a 1024-byte item; the // `used > 0` guard means it goes through alone (no wait). let start = Instant::now(); tx.send(Item { sz: 1024, tag: 99 }).unwrap(); assert!(start.elapsed() < Duration::from_millis(100)); let got = rx.recv().unwrap(); assert_eq!(got.tag, 99); } #[test] fn send_blocks_at_capacity_unblocks_on_recv() { // Cap = 200 bytes, item = 100 bytes. First two sends fit // exactly; the third must block until a recv frees capacity. let (tx, rx) = channel::(200); tx.send(Item { sz: 100, tag: 0 }).unwrap(); tx.send(Item { sz: 100, tag: 1 }).unwrap(); let tx2 = tx.clone(); let sent_at = Arc::new(Mutex::new(None::)); let sent_at2 = sent_at.clone(); let h = thread::spawn(move || { tx2.send(Item { sz: 100, tag: 2 }).unwrap(); *sent_at2.lock().unwrap() = Some(Instant::now()); }); // Give the sender thread a head start; it should be parked in // `cv.wait` because used (200) + 100 > capacity (200). thread::sleep(Duration::from_millis(100)); assert!( sent_at.lock().unwrap().is_none(), "third send should be blocked at capacity" ); // Drain one. Sender wakes and completes. let recv_at = Instant::now(); let got = rx.recv().unwrap(); assert_eq!(got.tag, 0); h.join().unwrap(); let sent_when = sent_at.lock().unwrap().unwrap(); assert!( sent_when >= recv_at, "sender must complete AFTER receiver freed capacity" ); // Drain the remaining two. assert_eq!(rx.recv().unwrap().tag, 1); assert_eq!(rx.recv().unwrap().tag, 2); } #[test] fn item_larger_than_capacity_still_goes_through() { // Pathological case: a single item bigger than the capacity. // The guard `*used > 0` lets it through when the channel is // empty (otherwise the sender deadlocks forever). Matches // `mpsc::SyncSender` semantics for oversize messages. let (tx, rx) = channel::(100); tx.send(Item { sz: 1000, tag: 7 }).unwrap(); let got = rx.recv().unwrap(); assert_eq!(got, Item { sz: 1000, tag: 7 }); } #[test] fn concurrent_send_recv_stress() { // 4 sender threads × 1k items each, 1 receiver. Verify byte // accounting stays sane (channel never deadlocks, every item // arrives exactly once) under contention. const SENDERS: u32 = 4; const PER_SENDER: u32 = 1000; const TOTAL: u32 = SENDERS * PER_SENDER; let (tx, rx) = channel::(8 * 1024); let sent = Arc::new(AtomicUsize::new(0)); let mut handles = Vec::new(); for s in 0..SENDERS { let tx = tx.clone(); let sent = sent.clone(); handles.push(thread::spawn(move || { for i in 0..PER_SENDER { // Vary item size so accounting actually has to // multiplex differently-sized blockers. 1B → 256B. let sz = 1 + ((i as usize) % 256); tx.send(Item { sz, tag: s * PER_SENDER + i, }) .unwrap(); sent.fetch_add(1, Ordering::SeqCst); } })); } // Drop our local sender so the receiver can eventually see // RecvError once all sender clones are done. Cloning the // sender into each producer means each clone Drop'd separately. drop(tx); let mut received = 0u32; while let Ok(_item) = rx.recv() { received += 1; } for h in handles { h.join().unwrap(); } assert_eq!(received, TOTAL); assert_eq!(sent.load(Ordering::SeqCst) as u32, TOTAL); } #[test] fn send_after_recv_dropped_returns_err() { let (tx, rx) = channel::(1024); drop(rx); let r = tx.send(Item { sz: 10, tag: 0 }); assert!(r.is_err()); } }