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}