Skip to main content

media_pp\core/
pool.rs

1use std::{
2    ops::{Deref, DerefMut},
3    sync::Arc,
4};
5
6use crossbeam_queue::SegQueue;
7
8/// A growable pool of reusable `T`s — lets a frame-producing element
9/// (see [`crate::elements::SwDecoder`], [`crate::elements::Scaler`]) hand
10/// out the *same* buffers over and over instead of allocating a fresh one
11/// (and freeing the old one) every single frame. Reusing an
12/// already-allocated `ffmpeg_next::frame::Video` also lets ffmpeg's own
13/// `avcodec_receive_frame`/`sws_scale` skip *their* internal buffer
14/// allocation when the reused frame already matches — not just savings
15/// on this crate's side.
16///
17/// Has no capacity wait or "pool exhausted" result:
18/// [`UnboundObjectPool::get`] pops a previously-returned item if one's
19/// available, or calls `init` to build a fresh one on the spot otherwise.
20/// The pool therefore grows to whatever depth turns out to be needed (e.g.
21/// however many frames a downstream `Queue` lets pile up at once). As with
22/// any caller-provided closure, a panic inside `init` still propagates.
23///
24/// Deliberately a private implementation detail owned by whichever
25/// element produces the frames (a struct field, initialized once in that
26/// element's own constructor) — not something the `Pipeline` holds or
27/// passes around. Nothing outside that one element needs to know a pool
28/// is involved at all; sharing the *frames themselves* downstream still
29/// goes through the ordinary `Arc<UnboundObjectPoolRef<T>>` in
30/// [`crate::buffer::MediaBuffer::Video`].
31pub struct UnboundObjectPool<T: Send> {
32    share: Arc<Share<T>>,
33    init: Box<dyn Fn() -> T + Send + Sync>,
34}
35
36struct Share<T: Send> {
37    pool: SegQueue<Box<T>>,
38    release: Box<dyn Fn(&mut T) + Send + Sync>,
39}
40
41impl<T: Send + Sync> UnboundObjectPool<T> {
42    /// Pre-fills with `size` items built via `init` (`0` is fine — the
43    /// pool still grows on demand, it just starts out empty and pays for
44    /// the first `size`-ish `get()` calls up front instead of amortized
45    /// over the stream). `release` runs on an item right before it goes
46    /// back into the pool (e.g. to reset state) — a no-op closure is
47    /// fine if there's nothing to reset, which is the common case for a
48    /// video frame: the next `consume` overwrites every pixel (and every
49    /// piece of metadata it cares about, like `pts`) before anyone
50    /// downstream sees it again.
51    pub fn new(
52        size: usize,
53        init: impl Fn() -> T + Send + Sync + 'static,
54        release: impl Fn(&mut T) + Send + Sync + 'static,
55    ) -> UnboundObjectPool<T> {
56        let pool = SegQueue::new();
57        for _ in 0..size {
58            pool.push(Box::new(init()));
59        }
60
61        UnboundObjectPool {
62            share: Arc::new(Share {
63                pool,
64                release: Box::new(release),
65            }),
66            init: Box::new(init),
67        }
68    }
69
70    /// Never waits for a pooled item and has no exhaustion error — see the
71    /// type docs. If the pool is empty, this calls the supplied `init`
72    /// closure directly.
73    pub fn get(&self) -> UnboundObjectPoolRef<T> {
74        let item = self
75            .share
76            .pool
77            .pop()
78            .unwrap_or_else(|| Box::new((self.init)()));
79        UnboundObjectPoolRef {
80            share: self.share.clone(),
81            item: Some(item),
82        }
83    }
84
85    /// How many items are currently sitting in the pool, unused. Mainly
86    /// for tests/diagnostics — nothing in this crate depends on this
87    /// number for correctness.
88    pub fn size(&self) -> usize {
89        self.share.pool.len()
90    }
91}
92
93/// One item borrowed from an [`UnboundObjectPool`] — `Deref`/`DerefMut`
94/// to `T` for normal use, and returns itself to the pool (after running
95/// `release` on it) when dropped.
96///
97/// Meant to be wrapped in an `Arc` wherever it needs to be shared/cloned
98/// downstream (see [`crate::buffer::MediaBuffer::Video`]) — cloning the
99/// `Arc` is what lets e.g. [`crate::elements::Tee`] fan the same frame
100/// out to multiple branches cheaply, and the item only actually goes
101/// back to the pool once every one of those clones has been dropped.
102/// This type itself is deliberately *not* `Clone`: only one thing can
103/// hold the actual boxed value at a time, or "return it once the last
104/// reference drops" wouldn't mean anything.
105pub struct UnboundObjectPoolRef<T: Send> {
106    share: Arc<Share<T>>,
107    item: Option<Box<T>>,
108}
109
110impl<T: Send> Deref for UnboundObjectPoolRef<T> {
111    type Target = T;
112
113    fn deref(&self) -> &Self::Target {
114        self.item.as_deref().expect("item only taken in Drop")
115    }
116}
117
118impl<T: Send> DerefMut for UnboundObjectPoolRef<T> {
119    fn deref_mut(&mut self) -> &mut Self::Target {
120        self.item.as_deref_mut().expect("item only taken in Drop")
121    }
122}
123
124impl<T: Send> Drop for UnboundObjectPoolRef<T> {
125    fn drop(&mut self) {
126        let mut item = self.item.take().expect("item only taken once, here");
127        (self.share.release)(&mut item);
128        self.share.pool.push(item);
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use std::sync::{
135        Arc,
136        atomic::{AtomicUsize, Ordering},
137    };
138
139    use super::*;
140
141    #[test]
142    fn returned_item_is_reused_by_the_next_get() {
143        let pool = UnboundObjectPool::new(0, || 0i32, |_| {});
144        assert_eq!(pool.size(), 0);
145
146        let item = pool.get();
147        assert_eq!(pool.size(), 0, "checked out, not sitting in the pool");
148
149        drop(item);
150        assert_eq!(pool.size(), 1, "returned automatically on drop");
151
152        let _item2 = pool.get();
153        assert_eq!(pool.size(), 0, "reused, not left behind");
154    }
155
156    #[test]
157    fn get_never_fails_even_when_empty() {
158        let pool = UnboundObjectPool::new(0, || 5i32, |_| {});
159        // Nothing's ever been returned, so both of these fall back to
160        // `init` — proves `get` doesn't block/panic/return `Option` when
161        // the pool has nothing to give out.
162        assert_eq!(*pool.get(), 5);
163        assert_eq!(*pool.get(), 5);
164    }
165
166    #[test]
167    fn release_runs_before_the_item_goes_back_into_the_pool() {
168        let release_calls = Arc::new(AtomicUsize::new(0));
169        let counted = release_calls.clone();
170        let pool = UnboundObjectPool::new(
171            1,
172            || 0i32,
173            move |_| {
174                counted.fetch_add(1, Ordering::SeqCst);
175            },
176        );
177
178        drop(pool.get());
179        assert_eq!(release_calls.load(Ordering::SeqCst), 1);
180    }
181
182    #[test]
183    fn dropping_the_last_arc_clone_returns_the_item() {
184        // Mirrors how `MediaBuffer::Video` actually uses this: wrapped in
185        // an `Arc` so it can be cheaply cloned downstream (e.g. by
186        // `Tee`), only going back to the pool once every clone is gone —
187        // not on the first `Arc` that happens to drop.
188        let pool = UnboundObjectPool::new(0, || 0i32, |_| {});
189        let shared = Arc::new(pool.get());
190        let clone = shared.clone();
191
192        drop(shared);
193        assert_eq!(pool.size(), 0, "one clone still alive — not returned yet");
194
195        drop(clone);
196        assert_eq!(pool.size(), 1, "last clone dropped — now it's returned");
197    }
198}