Skip to main content

runsync_transfer/
pool.rs

1//! Chunk buffer recycling.
2//!
3//! A 100 GB transfer at 1 MiB chunks is 100k chunks, each needing a plaintext
4//! buffer and an output buffer. Allocating and freeing 200k multi-megabyte
5//! buffers hands the allocator a workload it will happily serve and then return
6//! to the OS, so the process spends its time in `madvise`/page faults instead of
7//! moving bytes. Recycling keeps a small fixed set of hot buffers instead.
8
9use parking_lot::Mutex;
10use std::sync::Arc;
11
12/// Fixed-capacity stack of reusable byte buffers.
13pub struct BufPool {
14    free: Mutex<Vec<Vec<u8>>>,
15    capacity: usize,
16    buf_hint: usize,
17}
18
19impl BufPool {
20    /// `capacity` buffers retained at most, each pre-sized to `buf_hint`.
21    pub fn new(capacity: usize, buf_hint: usize) -> Arc<Self> {
22        Arc::new(Self {
23            free: Mutex::new(Vec::with_capacity(capacity)),
24            capacity,
25            buf_hint,
26        })
27    }
28
29    /// Take a buffer. Always returned empty, with `buf_hint` capacity available.
30    pub fn take(&self) -> Vec<u8> {
31        match self.free.lock().pop() {
32            Some(mut b) => {
33                b.clear();
34                b
35            }
36            None => Vec::with_capacity(self.buf_hint),
37        }
38    }
39
40    /// Return a buffer for reuse.
41    pub fn put(&self, buf: Vec<u8>) {
42        // Do not retain a buffer that grew far past the hint: a single
43        // pathological chunk should not pin an oversized allocation forever.
44        if buf.capacity() > self.buf_hint * 4 {
45            return;
46        }
47        let mut free = self.free.lock();
48        if free.len() < self.capacity {
49            free.push(buf);
50        }
51    }
52}
53
54/// A buffer that returns itself to its pool when dropped.
55pub struct Pooled {
56    buf: Option<Vec<u8>>,
57    pool: Arc<BufPool>,
58}
59
60impl Pooled {
61    pub fn new(pool: Arc<BufPool>) -> Self {
62        let buf = pool.take();
63        Self {
64            buf: Some(buf),
65            pool,
66        }
67    }
68}
69
70impl std::ops::Deref for Pooled {
71    type Target = Vec<u8>;
72    fn deref(&self) -> &Vec<u8> {
73        self.buf.as_ref().expect("buffer taken only on drop")
74    }
75}
76
77impl std::ops::DerefMut for Pooled {
78    fn deref_mut(&mut self) -> &mut Vec<u8> {
79        self.buf.as_mut().expect("buffer taken only on drop")
80    }
81}
82
83impl Drop for Pooled {
84    fn drop(&mut self) {
85        if let Some(b) = self.buf.take() {
86            self.pool.put(b);
87        }
88    }
89}
90
91/// A small stack of reusable objects, handed out to workers and returned when
92/// they finish.
93///
94/// The sealer this holds carries a per-file subkey cache. Sharing one behind a
95/// mutex would serialise every encode on a stream — including the file read
96/// that happens under the same critical section. Pooling gives each concurrent
97/// worker its own, with the caches surviving between chunks.
98pub struct ObjPool<T> {
99    free: Mutex<Vec<T>>,
100    capacity: usize,
101}
102
103impl<T> ObjPool<T> {
104    pub fn new(capacity: usize) -> Arc<Self> {
105        Arc::new(Self {
106            free: Mutex::new(Vec::with_capacity(capacity)),
107            capacity,
108        })
109    }
110
111    /// Take an object, or build one with `make` if the pool is empty.
112    pub fn take_or<F: FnOnce() -> T>(&self, make: F) -> T {
113        match self.free.lock().pop() {
114            Some(o) => o,
115            None => make(),
116        }
117    }
118
119    pub fn put(&self, obj: T) {
120        let mut free = self.free.lock();
121        if free.len() < self.capacity {
122            free.push(obj);
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn obj_pool_reuses_and_bounds() {
133        let pool: Arc<ObjPool<Vec<u32>>> = ObjPool::new(2);
134        let made = std::sync::atomic::AtomicUsize::new(0);
135        let mk = || {
136            made.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
137            Vec::new()
138        };
139        let a = pool.take_or(mk);
140        let b = pool.take_or(mk);
141        assert_eq!(made.load(std::sync::atomic::Ordering::SeqCst), 2);
142        pool.put(a);
143        pool.put(b);
144        // Both come back from the pool rather than being rebuilt.
145        let _ = pool.take_or(mk);
146        let _ = pool.take_or(mk);
147        assert_eq!(made.load(std::sync::atomic::Ordering::SeqCst), 2);
148        // Over capacity, extras are dropped rather than retained.
149        for _ in 0..8 {
150            pool.put(Vec::new());
151        }
152        assert_eq!(pool.free.lock().len(), 2);
153    }
154
155    #[test]
156    fn buffers_are_recycled_not_reallocated() {
157        let pool = BufPool::new(4, 1024);
158        let ptr = {
159            let mut b = Pooled::new(pool.clone());
160            b.extend_from_slice(&[1u8; 512]);
161            b.as_ptr()
162        };
163        let b2 = Pooled::new(pool.clone());
164        assert!(b2.is_empty(), "recycled buffers come back empty");
165        assert_eq!(b2.as_ptr(), ptr, "the same allocation came back");
166    }
167
168    #[test]
169    fn pool_respects_its_capacity() {
170        let pool = BufPool::new(2, 128);
171        let bufs: Vec<_> = (0..8).map(|_| Pooled::new(pool.clone())).collect();
172        drop(bufs);
173        assert_eq!(pool.free.lock().len(), 2);
174    }
175
176    #[test]
177    fn oversized_buffers_are_dropped_rather_than_retained() {
178        let pool = BufPool::new(4, 100);
179        {
180            let mut b = Pooled::new(pool.clone());
181            b.resize(10_000, 0);
182        }
183        assert_eq!(pool.free.lock().len(), 0);
184    }
185
186    #[test]
187    fn concurrent_take_and_put_is_sound() {
188        let pool = BufPool::new(8, 4096);
189        let threads: Vec<_> = (0..8)
190            .map(|_| {
191                let pool = pool.clone();
192                std::thread::spawn(move || {
193                    for _ in 0..1000 {
194                        let mut b = Pooled::new(pool.clone());
195                        b.extend_from_slice(&[7u8; 100]);
196                        assert_eq!(b.len(), 100);
197                    }
198                })
199            })
200            .collect();
201        for t in threads {
202            t.join().unwrap();
203        }
204        assert!(pool.free.lock().len() <= 8);
205    }
206}