Skip to main content

net_io/
buf.rs

1//! Pooled buffers with a total budget, wiped on release.
2
3use std::fmt;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use crate::limits::SizeClass;
8
9/// A byte buffer from a pool: a fixed capacity (its size class) and a filled length.
10///
11/// # Examples
12///
13/// ```
14/// use net_io::{BufferPool, SizeClass};
15/// let mut pool = BufferPool::standalone(1 << 20);
16/// let mut buf = pool.take(SizeClass::SMALL).unwrap();
17/// buf.spare()[..5].copy_from_slice(b"hello");
18/// buf.set_len(5);
19/// assert_eq!(buf.as_slice(), b"hello");
20/// pool.put(buf);
21/// ```
22pub struct Buffer {
23    data: Box<[u8]>,
24    len: usize,
25    class: SizeClass,
26}
27
28impl Buffer {
29    /// A buffer not drawn from any pool's free list (the caller already charged the budget).
30    pub(crate) fn detached(class: SizeClass) -> Buffer {
31        Buffer::new(class)
32    }
33
34    /// An empty stand-in that owns no memory, left where a buffer travels with a
35    /// driver operation.
36    #[cfg(feature = "reactor")]
37    pub(crate) fn placeholder() -> Buffer {
38        Buffer {
39            data: Box::default(),
40            len: 0,
41            class: SizeClass::SMALL,
42        }
43    }
44
45    fn new(class: SizeClass) -> Buffer {
46        Buffer {
47            data: vec![0u8; class.bytes() as usize].into_boxed_slice(),
48            len: 0,
49            class,
50        }
51    }
52
53    /// The filled part.
54    #[must_use]
55    pub fn as_slice(&self) -> &[u8] {
56        &self.data[..self.len]
57    }
58
59    /// The filled part, mutable.
60    pub fn as_mut_slice(&mut self) -> &mut [u8] {
61        &mut self.data[..self.len]
62    }
63
64    /// The unfilled tail.
65    pub fn spare(&mut self) -> &mut [u8] {
66        &mut self.data[self.len..]
67    }
68
69    /// The filled length.
70    #[must_use]
71    pub fn len(&self) -> usize {
72        self.len
73    }
74
75    /// Whether nothing is filled.
76    #[must_use]
77    pub fn is_empty(&self) -> bool {
78        self.len == 0
79    }
80
81    /// Sets the filled length; saturates at the capacity.
82    pub fn set_len(&mut self, n: usize) {
83        self.len = n.min(self.data.len());
84    }
85
86    /// The capacity in bytes.
87    #[must_use]
88    pub fn capacity(&self) -> usize {
89        self.data.len()
90    }
91
92    /// The size class.
93    #[must_use]
94    pub fn class(&self) -> SizeClass {
95        self.class
96    }
97
98    /// Empties the buffer (does not wipe; the pool wipes on release).
99    pub fn clear(&mut self) {
100        self.len = 0;
101    }
102
103    /// Overwrites the filled part with zeroes and empties the buffer.
104    pub(crate) fn wipe(&mut self) {
105        let used = &mut self.data[..self.len];
106        used.fill(0);
107        // The buffer stays reachable and is read again by the next owner, so the
108        // stores cannot be removed as dead; black_box makes that explicit.
109        std::hint::black_box(&*used);
110        self.len = 0;
111    }
112
113    /// Moves the bytes `from..` to the front.
114    pub(crate) fn compact(&mut self, from: usize) {
115        let from = from.min(self.len);
116        self.data.copy_within(from..self.len, 0);
117        self.len -= from;
118    }
119}
120
121impl fmt::Debug for Buffer {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.debug_struct("Buffer")
124            .field("len", &self.len)
125            .field("capacity", &self.data.len())
126            .finish()
127    }
128}
129
130/// Counts of one pool.
131///
132/// ```
133/// let pool = net_io::BufferPool::standalone(1 << 20);
134/// assert_eq!(pool.stats().allocated, 0);
135/// ```
136#[non_exhaustive]
137#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
138pub struct PoolStats {
139    /// Buffers this pool allocated.
140    pub allocated: u64,
141    /// Buffers in its free lists.
142    pub free: u64,
143    /// Bytes left in the shared budget.
144    pub budget_left: u64,
145    /// Takes refused because the budget was spent.
146    pub budget_refusals: u64,
147}
148
149/// A worker's buffer pool: free lists per size class, drawing new buffers from a
150/// budget shared by every worker of a server.
151pub struct BufferPool {
152    lists: Vec<(SizeClass, Vec<Buffer>)>,
153    budget: Arc<AtomicU64>,
154    wipe: bool,
155    allocated: u64,
156    refusals: u64,
157}
158
159impl BufferPool {
160    pub(crate) fn new(budget: Arc<AtomicU64>, wipe: bool) -> BufferPool {
161        BufferPool {
162            lists: Vec::with_capacity(4),
163            budget,
164            wipe,
165            allocated: 0,
166            refusals: 0,
167        }
168    }
169
170    /// A pool with its own budget of `bytes`, for use outside a server.
171    ///
172    /// ```
173    /// let mut pool = net_io::BufferPool::standalone(64 * 1024);
174    /// assert!(pool.take(net_io::SizeClass::LARGE).is_some());
175    /// assert!(pool.take(net_io::SizeClass::LARGE).is_none()); // budget spent
176    /// ```
177    #[must_use]
178    pub fn standalone(bytes: u64) -> BufferPool {
179        BufferPool::new(Arc::new(AtomicU64::new(bytes)), true)
180    }
181
182    /// A buffer of `class`, or `None` when the free list is empty and the budget is spent.
183    pub fn take(&mut self, class: SizeClass) -> Option<Buffer> {
184        if let Some(buf) = self.list(class).pop() {
185            return Some(buf);
186        }
187        let bytes = u64::from(class.bytes());
188        let mut left = self.budget.load(Ordering::Relaxed);
189        loop {
190            if left < bytes {
191                self.refusals += 1;
192                return None;
193            }
194            match self.budget.compare_exchange_weak(
195                left,
196                left - bytes,
197                Ordering::AcqRel,
198                Ordering::Relaxed,
199            ) {
200                Ok(_) => break,
201                Err(actual) => left = actual,
202            }
203        }
204        self.allocated += 1;
205        Some(Buffer::new(class))
206    }
207
208    /// Returns a buffer; its used part is wiped first unless wiping is off.
209    ///
210    /// A buffer from another worker's pool is accepted (it is just memory).
211    pub fn put(&mut self, mut buf: Buffer) {
212        if self.wipe {
213            buf.wipe();
214        } else {
215            buf.clear();
216        }
217        let class = buf.class;
218        self.list(class).push(buf);
219    }
220
221    /// Counts of this pool.
222    #[must_use]
223    pub fn stats(&self) -> PoolStats {
224        PoolStats {
225            allocated: self.allocated,
226            free: self.lists.iter().map(|(_, l)| l.len() as u64).sum(),
227            budget_left: self.budget.load(Ordering::Relaxed),
228            budget_refusals: self.refusals,
229        }
230    }
231
232    fn list(&mut self, class: SizeClass) -> &mut Vec<Buffer> {
233        let pos = match self.lists.iter().position(|(c, _)| *c == class) {
234            Some(p) => p,
235            None => {
236                self.lists.push((class, Vec::new()));
237                self.lists.len() - 1
238            }
239        };
240        &mut self.lists[pos].1
241    }
242}
243
244impl fmt::Debug for BufferPool {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        f.debug_struct("BufferPool")
247            .field("stats", &self.stats())
248            .finish()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_pool_reuses_and_wipes() {
258        let mut pool = BufferPool::standalone(1 << 20);
259        let mut b = pool.take(SizeClass::SMALL).unwrap();
260        b.spare()[..6].copy_from_slice(b"secret");
261        b.set_len(6);
262        pool.put(b);
263        let mut b = pool.take(SizeClass::SMALL).unwrap();
264        assert_eq!(pool.stats().allocated, 1);
265        assert!(b.is_empty());
266        b.set_len(6);
267        assert_eq!(b.as_slice(), &[0u8; 6]);
268    }
269
270    #[test]
271    fn test_pool_budget_bounds_allocation() {
272        let mut pool = BufferPool::standalone(u64::from(SizeClass::SMALL.bytes()) * 2);
273        let a = pool.take(SizeClass::SMALL).unwrap();
274        let _b = pool.take(SizeClass::SMALL).unwrap();
275        assert!(pool.take(SizeClass::SMALL).is_none());
276        assert_eq!(pool.stats().budget_refusals, 1);
277        pool.put(a);
278        assert!(pool.take(SizeClass::SMALL).is_some());
279    }
280
281    #[test]
282    fn test_set_len_saturates_and_compact_moves_tail() {
283        let mut pool = BufferPool::standalone(1 << 20);
284        let mut b = pool.take(SizeClass::SMALL).unwrap();
285        b.set_len(usize::MAX);
286        assert_eq!(b.len(), b.capacity());
287        b.set_len(0);
288        b.spare()[..4].copy_from_slice(b"abcd");
289        b.set_len(4);
290        b.compact(2);
291        assert_eq!(b.as_slice(), b"cd");
292        b.compact(99);
293        assert!(b.is_empty());
294    }
295
296    #[test]
297    fn test_pool_without_wipe_keeps_bytes_but_empties() {
298        let mut pool = BufferPool::new(Arc::new(AtomicU64::new(1 << 20)), false);
299        let mut b = pool.take(SizeClass::SMALL).unwrap();
300        b.spare()[0] = 9;
301        b.set_len(1);
302        pool.put(b);
303        let b = pool.take(SizeClass::SMALL).unwrap();
304        assert!(b.is_empty());
305    }
306
307    /// The cost of T-16: `cargo test --release --lib bench_ -- --ignored --nocapture`.
308    #[test]
309    #[ignore = "a benchmark"]
310    fn bench_wipe() {
311        for (name, used) in [("2 KiB", 2048), ("16 KiB", 16 * 1024)] {
312            let mut b = Buffer::new(SizeClass::MEDIUM);
313            let rounds = 1_000_000u32;
314            let started = std::time::Instant::now();
315            for _ in 0..rounds {
316                b.set_len(used);
317                b.wipe();
318            }
319            let ns = started.elapsed().as_nanos() as f64 / f64::from(rounds);
320            println!("Wipe {name}: {ns:.1} ns");
321        }
322    }
323}