Skip to main content

phantom_protocol/transport/
buffer_pool.rs

1//! Pre-allocated Buffer Pool
2//!
3//! Recycles I/O buffers to avoid a per-packet heap allocation. A two-level pool: a
4//! lock-free global `ArrayQueue` backed by a per-thread `LOCAL_POOL` cache, so the
5//! hot acquire/return path usually stays thread-local and contention-free, spilling
6//! to the global queue only in batches.
7//!
8//! Used by the low-level `transport::udp_transport` socket helper and the
9//! `buffer_pool_bench`. NOTE: the production `SessionTransport`-level recv path
10//! (`TcpSessionTransport`) uses a persistent `BytesMut` accumulator instead, so
11//! this pool is the alternate / lower-level path rather than the main data plane.
12
13use crossbeam_queue::ArrayQueue;
14use std::cell::RefCell;
15use std::sync::atomic::{AtomicUsize, Ordering};
16
17const BATCH_SIZE: usize = 32;
18const MAX_LOCAL_BUFFERS: usize = 64;
19
20/// A pool of pre-allocated buffers for zero-allocation I/O
21pub struct BufferPool {
22    /// Global pool of available buffers
23    buffers: ArrayQueue<Vec<u8>>,
24    /// Buffer size
25    buffer_size: usize,
26    /// Max pool size
27    #[allow(dead_code)]
28    max_buffers: usize,
29    /// Stats: total allocations
30    allocations: AtomicUsize,
31    /// Stats: pool hits
32    hits: AtomicUsize,
33}
34
35thread_local! {
36    static LOCAL_POOL: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::with_capacity(MAX_LOCAL_BUFFERS));
37}
38
39impl BufferPool {
40    /// Create a new buffer pool
41    pub fn new(buffer_size: usize, initial_count: usize, max_buffers: usize) -> Self {
42        let buffers = ArrayQueue::new(max_buffers);
43        let count = std::cmp::min(initial_count, max_buffers);
44        for _ in 0..count {
45            let _ = buffers.push(vec![0u8; buffer_size]);
46        }
47
48        Self {
49            buffers,
50            buffer_size,
51            max_buffers,
52            allocations: AtomicUsize::new(count),
53            hits: AtomicUsize::new(0),
54        }
55    }
56
57    /// Acquire a buffer from the pool
58    #[inline]
59    pub fn acquire(&self) -> PooledBuffer<'_> {
60        let mut buffer: Option<Vec<u8>> = LOCAL_POOL.with(|local| {
61            let mut local_pool = local.borrow_mut();
62            if let Some(mut buf) = local_pool.pop() {
63                buf.clear();
64                return Some(buf);
65            }
66            None
67        });
68
69        if buffer.is_none() {
70            // Refill local pool from global pool
71            LOCAL_POOL.with(|local| {
72                let mut local_pool = local.borrow_mut();
73                for _ in 0..BATCH_SIZE {
74                    if let Some(mut buf) = self.buffers.pop() {
75                        buf.clear();
76                        local_pool.push(buf);
77                    } else {
78                        break;
79                    }
80                }
81            });
82
83            buffer = LOCAL_POOL.with(|local| {
84                let mut local_pool = local.borrow_mut();
85                local_pool.pop()
86            });
87        }
88
89        let buffer = if let Some(buf) = buffer {
90            self.hits.fetch_add(1, Ordering::Relaxed);
91            buf
92        } else {
93            self.allocations.fetch_add(1, Ordering::Relaxed);
94            Vec::with_capacity(self.buffer_size)
95        };
96
97        PooledBuffer { buffer, pool: self }
98    }
99
100    /// Return a buffer to the pool
101    #[inline]
102    fn return_buffer(&self, mut buffer: Vec<u8>) {
103        buffer.clear();
104        LOCAL_POOL.with(|local| {
105            let mut local_pool = local.borrow_mut();
106            if local_pool.len() < MAX_LOCAL_BUFFERS {
107                local_pool.push(buffer);
108            } else {
109                // If local pool is full, flush half back to global pool
110                let half = MAX_LOCAL_BUFFERS / 2;
111                for _ in 0..half {
112                    if let Some(buf) = local_pool.pop() {
113                        let _ = self.buffers.push(buf);
114                    }
115                }
116
117                local_pool.push(buffer);
118            }
119        });
120    }
121
122    /// Get pool statistics
123    pub fn stats(&self) -> PoolStats {
124        PoolStats {
125            allocations: self.allocations.load(Ordering::Relaxed),
126            hits: self.hits.load(Ordering::Relaxed),
127            pool_size: self.buffers.len(),
128        }
129    }
130}
131
132/// A buffer borrowed from the pool
133pub struct PooledBuffer<'a> {
134    buffer: Vec<u8>,
135    pool: &'a BufferPool,
136}
137
138impl<'a> PooledBuffer<'a> {
139    /// Get mutable reference to inner buffer
140    ///
141    /// Note: returns `&mut Vec<u8>` (not `&mut T`) so it cannot implement
142    /// `std::convert::AsMut` without a concrete target type — the inherent
143    /// method intentionally provides the richer `Vec` interface.
144    #[allow(clippy::should_implement_trait)]
145    #[inline]
146    pub fn as_mut(&mut self) -> &mut Vec<u8> {
147        &mut self.buffer
148    }
149
150    /// Get reference to inner buffer
151    ///
152    /// Note: returns `&[u8]` (not `&Vec<u8>`) so the signature differs from
153    /// what `std::convert::AsRef<Vec<u8>>` would produce — an inherent method
154    /// avoids the ambiguity.
155    #[allow(clippy::should_implement_trait)]
156    #[inline]
157    pub fn as_ref(&self) -> &[u8] {
158        &self.buffer
159    }
160
161    /// Get the buffer length
162    #[inline]
163    pub fn len(&self) -> usize {
164        self.buffer.len()
165    }
166
167    /// Check if buffer is empty
168    #[inline]
169    pub fn is_empty(&self) -> bool {
170        self.buffer.is_empty()
171    }
172}
173
174impl<'a> std::ops::Deref for PooledBuffer<'a> {
175    type Target = Vec<u8>;
176
177    #[inline]
178    fn deref(&self) -> &Self::Target {
179        &self.buffer
180    }
181}
182
183impl<'a> std::ops::DerefMut for PooledBuffer<'a> {
184    #[inline]
185    fn deref_mut(&mut self) -> &mut Self::Target {
186        &mut self.buffer
187    }
188}
189
190impl<'a> Drop for PooledBuffer<'a> {
191    fn drop(&mut self) {
192        let buffer = std::mem::take(&mut self.buffer);
193        // Only return if it has some capacity (not completely empty shell)
194        if buffer.capacity() > 0 {
195            self.pool.return_buffer(buffer);
196        }
197    }
198}
199
200/// Pool statistics
201#[derive(Debug, Clone, Copy)]
202pub struct PoolStats {
203    pub allocations: usize,
204    pub hits: usize,
205    pub pool_size: usize,
206}
207
208impl PoolStats {
209    /// Hit rate (0.0 - 1.0)
210    pub fn hit_rate(&self) -> f64 {
211        if self.allocations + self.hits == 0 {
212            0.0
213        } else {
214            self.hits as f64 / (self.allocations + self.hits) as f64
215        }
216    }
217}
218
219/// Global buffer pool for common use
220static GLOBAL_POOL: std::sync::OnceLock<BufferPool> = std::sync::OnceLock::new();
221
222/// Get the global buffer pool
223pub fn global_pool() -> &'static BufferPool {
224    GLOBAL_POOL.get_or_init(|| {
225        // 64 KB buffers, 1024 initial, 65536 max
226        BufferPool::new(64 * 1024, 1024, 65536)
227    })
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::thread;
234
235    #[test]
236    fn test_buffer_pool() {
237        let pool = BufferPool::new(1024, 4, 16);
238
239        let mut buf1 = pool.acquire();
240        buf1.extend_from_slice(b"hello");
241        assert_eq!(buf1.len(), 5);
242
243        let buf2 = pool.acquire();
244        assert_eq!(buf2.len(), 0);
245
246        drop(buf1);
247        drop(buf2);
248
249        // After returning, buffers are pushed to local pool.
250        // It preloaded 4 buffers initially, we used 2 and returned 2. So it has 4.
251        LOCAL_POOL.with(|local| {
252            assert_eq!(local.borrow().len(), 4);
253        });
254    }
255
256    #[test]
257    fn test_thread_local_flushing() {
258        let pool = std::sync::Arc::new(BufferPool::new(1024, 0, 100));
259
260        let p_clone = pool.clone();
261        thread::spawn(move || {
262            let mut bufs = Vec::new();
263            // Allocate 70 buffers to exceed MAX_LOCAL_BUFFERS (64)
264            for _ in 0..70 {
265                bufs.push(p_clone.acquire());
266            }
267
268            // Drop all
269            drop(bufs);
270
271            // Local pool should have 64 buffers (or less if flushed), global should have some
272            let mut count = 0;
273            LOCAL_POOL.with(|local| {
274                count = local.borrow().len();
275            });
276            assert!(count <= MAX_LOCAL_BUFFERS);
277        })
278        .join()
279        .unwrap();
280
281        // global pool should have received the flushed buffers
282        assert!(pool.buffers.len() > 0);
283    }
284}