Skip to main content

chronix_analytics/compute/
buffer_pool.rs

1//! Reusable buffer pool to minimize allocation in hot compute paths.
2
3use parking_lot::Mutex;
4
5/// Thread-safe pool of reusable `Vec<T>` buffers.
6///
7/// Buffers are cleared (but not deallocated) on return, preserving capacity.
8///
9/// # Capacity model
10///
11/// The pool enforces both a **count-based** limit (`max_size`) and a
12/// **byte-capacity** limit (`max_pool_bytes`).  A buffer is only cached
13/// when *both* limits allow it, preventing a small number of
14/// oversized buffers from consuming unbounded memory.
15///
16/// # Examples
17///
18/// ```no_run
19/// use chronix_analytics::compute::BufferPool;
20///
21/// let pool = BufferPool::<f64>::new(4);
22/// let mut buf = pool.get(100);
23/// buf.extend_from_slice(&[1.0, 2.0, 3.0]);
24/// assert_eq!(buf.len(), 3);
25/// pool.put(buf);
26/// // Next get reuses the returned buffer's capacity
27/// let buf2 = pool.get(3);
28/// assert!(buf2.capacity() >= 100);
29/// ```
30/// Default byte-capacity budget: 64 MiB.
31const DEFAULT_MAX_POOL_BYTES: usize = 64 * 1024 * 1024;
32
33/// A pool of reusable `Vec<T>` buffers that avoids repeated allocation on
34/// hot compute paths: `get()` hands out a cleared buffer, `put()` returns it.
35pub struct BufferPool<T> {
36    pool: Mutex<Vec<Vec<T>>>,
37    max_size: usize,
38    max_pool_bytes: usize,
39    current_pool_bytes: Mutex<usize>,
40}
41
42impl<T> BufferPool<T> {
43    /// Creates a new buffer pool with the given maximum number of cached
44    /// buffers and a default byte-capacity budget of 64 MiB.
45    pub fn new(max_size: usize) -> Self {
46        Self {
47            pool: Mutex::new(Vec::with_capacity(max_size)),
48            max_size,
49            max_pool_bytes: DEFAULT_MAX_POOL_BYTES,
50            current_pool_bytes: Mutex::new(0),
51        }
52    }
53
54    /// Creates a buffer pool with an explicit byte-capacity budget.
55    pub fn with_max_bytes(max_size: usize, max_pool_bytes: usize) -> Self {
56        Self {
57            pool: Mutex::new(Vec::with_capacity(max_size)),
58            max_size,
59            max_pool_bytes,
60            current_pool_bytes: Mutex::new(0),
61        }
62    }
63
64    /// Returns the total byte capacity currently held by pooled buffers.
65    pub fn pooled_bytes(&self) -> usize {
66        *self.current_pool_bytes.lock()
67    }
68
69    /// Gets a buffer from the pool or allocates a new one.
70    ///
71    /// The returned buffer is empty but may have pre-allocated capacity from
72    /// a previous use. If `min_capacity` is specified, the buffer is guaranteed
73    /// to have at least that capacity.
74    pub fn get(&self, min_capacity: usize) -> Vec<T> {
75        let mut pool = self.pool.lock();
76        if let Some(mut buf) = pool.pop() {
77            let buf_bytes = std::mem::size_of::<T>() * buf.capacity();
78            *self.current_pool_bytes.lock() -= buf_bytes;
79            if buf.capacity() < min_capacity {
80                buf.reserve(min_capacity - buf.len());
81            }
82            buf
83        } else {
84            Vec::with_capacity(min_capacity)
85        }
86    }
87
88    /// Returns a buffer to the pool for reuse.
89    ///
90    /// The buffer is cleared but its capacity is preserved.
91    pub fn put(&self, mut buf: Vec<T>) {
92        buf.clear();
93        let buf_bytes = std::mem::size_of::<T>() * buf.capacity();
94        let mut pool = self.pool.lock();
95        let mut bytes = self.current_pool_bytes.lock();
96        if pool.len() < self.max_size && *bytes + buf_bytes <= self.max_pool_bytes {
97            *bytes += buf_bytes;
98            pool.push(buf);
99        }
100        // else: drop the buffer (pool full or byte budget exceeded)
101    }
102
103    /// Returns the number of buffers currently in the pool.
104    pub fn available(&self) -> usize {
105        self.pool.lock().len()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn get_and_put() {
115        let pool = BufferPool::<f64>::new(2);
116        let mut buf = pool.get(64);
117        buf.extend_from_slice(&[1.0, 2.0, 3.0]);
118        assert_eq!(buf.len(), 3);
119        let cap = buf.capacity();
120
121        pool.put(buf);
122        assert_eq!(pool.available(), 1);
123
124        let buf2 = pool.get(1);
125        assert!(buf2.capacity() >= cap);
126        assert!(buf2.is_empty());
127        assert_eq!(pool.available(), 0);
128    }
129
130    #[test]
131    fn pool_max_size_respected() {
132        let pool = BufferPool::<f64>::new(1);
133        pool.put(vec![1.0, 2.0]);
134        pool.put(vec![3.0, 4.0]); // exceeds max — dropped
135        assert_eq!(pool.available(), 1);
136    }
137
138    #[test]
139    fn min_capacity_honored() {
140        let pool = BufferPool::<f64>::new(2);
141        pool.put(Vec::with_capacity(4));
142        let buf = pool.get(100);
143        assert!(buf.capacity() >= 100);
144    }
145
146    #[test]
147    fn byte_budget_rejects_oversized_buffer() {
148        // Allow at most 64 bytes in the pool (8 f64 elements).
149        let pool = BufferPool::<f64>::with_max_bytes(4, 64);
150        // A buffer with capacity 100 f64 = 800 bytes — exceeds the budget.
151        pool.put(Vec::<f64>::with_capacity(100));
152        assert_eq!(pool.available(), 0, "oversized buffer should be dropped");
153        assert_eq!(pool.pooled_bytes(), 0);
154    }
155
156    #[test]
157    fn byte_budget_accepts_small_buffer() {
158        let pool = BufferPool::<f64>::with_max_bytes(4, 1024);
159        pool.put(Vec::<f64>::with_capacity(8)); // 64 bytes
160        assert_eq!(pool.available(), 1);
161        assert_eq!(pool.pooled_bytes(), 64);
162    }
163
164    #[test]
165    fn byte_budget_decremented_on_get() {
166        let pool = BufferPool::<f64>::with_max_bytes(4, 1024);
167        pool.put(Vec::<f64>::with_capacity(8));
168        assert_eq!(pool.pooled_bytes(), 64);
169
170        let _buf = pool.get(1);
171        assert_eq!(pool.pooled_bytes(), 0);
172    }
173}