use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::limits::SizeClass;
pub struct Buffer {
data: Box<[u8]>,
len: usize,
class: SizeClass,
}
impl Buffer {
pub(crate) fn detached(class: SizeClass) -> Buffer {
Buffer::new(class)
}
#[cfg(feature = "reactor")]
pub(crate) fn placeholder() -> Buffer {
Buffer {
data: Box::default(),
len: 0,
class: SizeClass::SMALL,
}
}
fn new(class: SizeClass) -> Buffer {
Buffer {
data: vec![0u8; class.bytes() as usize].into_boxed_slice(),
len: 0,
class,
}
}
#[must_use]
pub fn as_slice(&self) -> &[u8] {
&self.data[..self.len]
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.data[..self.len]
}
pub fn spare(&mut self) -> &mut [u8] {
&mut self.data[self.len..]
}
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn set_len(&mut self, n: usize) {
self.len = n.min(self.data.len());
}
#[must_use]
pub fn capacity(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn class(&self) -> SizeClass {
self.class
}
pub fn clear(&mut self) {
self.len = 0;
}
pub(crate) fn wipe(&mut self) {
let used = &mut self.data[..self.len];
used.fill(0);
std::hint::black_box(&*used);
self.len = 0;
}
pub(crate) fn compact(&mut self, from: usize) {
let from = from.min(self.len);
self.data.copy_within(from..self.len, 0);
self.len -= from;
}
}
impl fmt::Debug for Buffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Buffer")
.field("len", &self.len)
.field("capacity", &self.data.len())
.finish()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PoolStats {
pub allocated: u64,
pub free: u64,
pub budget_left: u64,
pub budget_refusals: u64,
}
pub struct BufferPool {
lists: Vec<(SizeClass, Vec<Buffer>)>,
budget: Arc<AtomicU64>,
wipe: bool,
allocated: u64,
refusals: u64,
}
impl BufferPool {
pub(crate) fn new(budget: Arc<AtomicU64>, wipe: bool) -> BufferPool {
BufferPool {
lists: Vec::with_capacity(4),
budget,
wipe,
allocated: 0,
refusals: 0,
}
}
#[must_use]
pub fn standalone(bytes: u64) -> BufferPool {
BufferPool::new(Arc::new(AtomicU64::new(bytes)), true)
}
pub fn take(&mut self, class: SizeClass) -> Option<Buffer> {
if let Some(buf) = self.list(class).pop() {
return Some(buf);
}
let bytes = u64::from(class.bytes());
let mut left = self.budget.load(Ordering::Relaxed);
loop {
if left < bytes {
self.refusals += 1;
return None;
}
match self.budget.compare_exchange_weak(
left,
left - bytes,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => left = actual,
}
}
self.allocated += 1;
Some(Buffer::new(class))
}
pub fn put(&mut self, mut buf: Buffer) {
if self.wipe {
buf.wipe();
} else {
buf.clear();
}
let class = buf.class;
self.list(class).push(buf);
}
#[must_use]
pub fn stats(&self) -> PoolStats {
PoolStats {
allocated: self.allocated,
free: self.lists.iter().map(|(_, l)| l.len() as u64).sum(),
budget_left: self.budget.load(Ordering::Relaxed),
budget_refusals: self.refusals,
}
}
fn list(&mut self, class: SizeClass) -> &mut Vec<Buffer> {
let pos = match self.lists.iter().position(|(c, _)| *c == class) {
Some(p) => p,
None => {
self.lists.push((class, Vec::new()));
self.lists.len() - 1
}
};
&mut self.lists[pos].1
}
}
impl fmt::Debug for BufferPool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BufferPool")
.field("stats", &self.stats())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pool_reuses_and_wipes() {
let mut pool = BufferPool::standalone(1 << 20);
let mut b = pool.take(SizeClass::SMALL).unwrap();
b.spare()[..6].copy_from_slice(b"secret");
b.set_len(6);
pool.put(b);
let mut b = pool.take(SizeClass::SMALL).unwrap();
assert_eq!(pool.stats().allocated, 1);
assert!(b.is_empty());
b.set_len(6);
assert_eq!(b.as_slice(), &[0u8; 6]);
}
#[test]
fn test_pool_budget_bounds_allocation() {
let mut pool = BufferPool::standalone(u64::from(SizeClass::SMALL.bytes()) * 2);
let a = pool.take(SizeClass::SMALL).unwrap();
let _b = pool.take(SizeClass::SMALL).unwrap();
assert!(pool.take(SizeClass::SMALL).is_none());
assert_eq!(pool.stats().budget_refusals, 1);
pool.put(a);
assert!(pool.take(SizeClass::SMALL).is_some());
}
#[test]
fn test_set_len_saturates_and_compact_moves_tail() {
let mut pool = BufferPool::standalone(1 << 20);
let mut b = pool.take(SizeClass::SMALL).unwrap();
b.set_len(usize::MAX);
assert_eq!(b.len(), b.capacity());
b.set_len(0);
b.spare()[..4].copy_from_slice(b"abcd");
b.set_len(4);
b.compact(2);
assert_eq!(b.as_slice(), b"cd");
b.compact(99);
assert!(b.is_empty());
}
#[test]
fn test_pool_without_wipe_keeps_bytes_but_empties() {
let mut pool = BufferPool::new(Arc::new(AtomicU64::new(1 << 20)), false);
let mut b = pool.take(SizeClass::SMALL).unwrap();
b.spare()[0] = 9;
b.set_len(1);
pool.put(b);
let b = pool.take(SizeClass::SMALL).unwrap();
assert!(b.is_empty());
}
#[test]
#[ignore = "a benchmark"]
fn bench_wipe() {
for (name, used) in [("2 KiB", 2048), ("16 KiB", 16 * 1024)] {
let mut b = Buffer::new(SizeClass::MEDIUM);
let rounds = 1_000_000u32;
let started = std::time::Instant::now();
for _ in 0..rounds {
b.set_len(used);
b.wipe();
}
let ns = started.elapsed().as_nanos() as f64 / f64::from(rounds);
println!("Wipe {name}: {ns:.1} ns");
}
}
}