use std::collections::BTreeMap;
use std::env;
use std::fmt;
use std::mem::size_of;
use num_complex::{Complex32, Complex64};
use crate::CacheStats;
pub const BUFFER_POOL_MAX_RETAINED_BYTES_ENV: &str = "TENFERRO_BUFFER_POOL_MAX_RETAINED_BYTES";
pub const DEFAULT_MAX_RETAINED_CAPACITY_BYTES: usize = 100 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BufferPoolStats {
pub buffers: usize,
pub capacity_bytes: usize,
}
pub struct BufferPool {
f64_pool: BTreeMap<usize, Vec<Vec<f64>>>,
f32_pool: BTreeMap<usize, Vec<Vec<f32>>>,
i32_pool: BTreeMap<usize, Vec<Vec<i32>>>,
i64_pool: BTreeMap<usize, Vec<Vec<i64>>>,
bool_pool: BTreeMap<usize, Vec<Vec<bool>>>,
c64_pool: BTreeMap<usize, Vec<Vec<Complex64>>>,
c32_pool: BTreeMap<usize, Vec<Vec<Complex32>>>,
retained_capacity_bytes: usize,
max_retained_capacity_bytes: usize,
}
impl fmt::Debug for BufferPool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BufferPool")
.field("stats", &self.stats())
.field(
"max_retained_capacity_bytes",
&self.max_retained_capacity_bytes,
)
.finish_non_exhaustive()
}
}
pub trait PoolScalar: Copy + Sized + Send + Sync + private::Sealed {
fn pool_zero() -> Self;
unsafe fn pool_acquire(pool: &mut BufferPool, len: usize) -> Vec<Self>;
fn pool_acquire_zeroed(pool: &mut BufferPool, len: usize) -> Vec<Self>;
fn pool_release(pool: &mut BufferPool, buf: Vec<Self>);
}
mod private {
pub trait Sealed {}
impl Sealed for f64 {}
impl Sealed for f32 {}
impl Sealed for i32 {}
impl Sealed for i64 {}
impl Sealed for bool {}
impl Sealed for num_complex::Complex64 {}
impl Sealed for num_complex::Complex32 {}
}
fn take_best_fit<T>(pool: &mut BTreeMap<usize, Vec<Vec<T>>>, len: usize) -> Option<Vec<T>> {
let key = *pool.range(len..).next()?.0;
let buf = {
let vecs = pool.get_mut(&key)?;
vecs.pop()
};
if pool.get(&key).is_some_and(Vec::is_empty) {
pool.remove(&key);
}
buf
}
fn pool_len<T>(pool: &BTreeMap<usize, Vec<Vec<T>>>) -> usize {
pool.values().map(Vec::len).sum()
}
fn evict_one_from_pool<T>(pool: &mut BTreeMap<usize, Vec<Vec<T>>>) -> Option<usize> {
let key = *pool.keys().next()?;
let vecs = pool.get_mut(&key)?;
let _ = vecs.pop()?;
if vecs.is_empty() {
pool.remove(&key);
}
Some(key.saturating_mul(size_of::<T>()))
}
#[derive(Clone, Copy)]
enum TypedPoolKind {
F64,
F32,
I32,
I64,
Bool,
C64,
C32,
}
fn smallest_pool_candidate<T>(
pool: &BTreeMap<usize, Vec<Vec<T>>>,
kind: TypedPoolKind,
) -> Option<(usize, TypedPoolKind)> {
pool.keys()
.next()
.map(|&capacity| (capacity.saturating_mul(size_of::<T>()), kind))
}
macro_rules! impl_pool_scalar {
($ty:ty, $field:ident, $zero:expr) => {
impl PoolScalar for $ty {
fn pool_zero() -> Self {
$zero
}
#[allow(clippy::uninit_vec)]
unsafe fn pool_acquire(pool: &mut BufferPool, len: usize) -> Vec<Self> {
match take_best_fit(&mut pool.$field, len) {
Some(mut buf) => {
pool.retained_capacity_bytes = pool
.retained_capacity_bytes
.saturating_sub(buf.capacity().saturating_mul(size_of::<Self>()));
unsafe { buf.set_len(len) };
buf
}
None => {
let mut buf = Vec::with_capacity(len);
unsafe { buf.set_len(len) };
buf
}
}
}
fn pool_acquire_zeroed(pool: &mut BufferPool, len: usize) -> Vec<Self> {
match take_best_fit(&mut pool.$field, len) {
Some(mut buf) => {
pool.retained_capacity_bytes = pool
.retained_capacity_bytes
.saturating_sub(buf.capacity().saturating_mul(size_of::<Self>()));
buf.resize(len, Self::pool_zero());
buf.fill(Self::pool_zero());
buf
}
None => vec![Self::pool_zero(); len],
}
}
fn pool_release(pool: &mut BufferPool, buf: Vec<Self>) {
let cap = buf.capacity();
if cap > 0 {
pool.retained_capacity_bytes = pool
.retained_capacity_bytes
.saturating_add(cap.saturating_mul(size_of::<Self>()));
pool.$field.entry(cap).or_default().push(buf);
pool.enforce_retention_limit();
}
}
}
};
}
impl_pool_scalar!(f64, f64_pool, 0.0);
impl_pool_scalar!(f32, f32_pool, 0.0);
impl_pool_scalar!(i32, i32_pool, 0);
impl_pool_scalar!(i64, i64_pool, 0);
impl_pool_scalar!(bool, bool_pool, false);
impl_pool_scalar!(Complex64, c64_pool, Complex64::new(0.0, 0.0));
impl_pool_scalar!(Complex32, c32_pool, Complex32::new(0.0, 0.0));
impl BufferPool {
pub fn new() -> Self {
Self::with_max_retained_capacity_bytes(default_max_retained_capacity_bytes())
}
pub fn with_max_retained_capacity_bytes(max_retained_capacity_bytes: usize) -> Self {
Self {
f64_pool: BTreeMap::new(),
f32_pool: BTreeMap::new(),
i32_pool: BTreeMap::new(),
i64_pool: BTreeMap::new(),
bool_pool: BTreeMap::new(),
c64_pool: BTreeMap::new(),
c32_pool: BTreeMap::new(),
retained_capacity_bytes: 0,
max_retained_capacity_bytes,
}
}
pub fn unbounded() -> Self {
Self::with_max_retained_capacity_bytes(usize::MAX)
}
pub fn max_retained_capacity_bytes(&self) -> usize {
self.max_retained_capacity_bytes
}
pub fn set_max_retained_capacity_bytes(&mut self, max_retained_capacity_bytes: usize) {
self.max_retained_capacity_bytes = max_retained_capacity_bytes;
self.enforce_retention_limit();
}
pub fn len(&self) -> usize {
self.stats().buffers
}
pub fn retained_capacity_bytes(&self) -> usize {
self.stats().capacity_bytes
}
pub fn stats(&self) -> BufferPoolStats {
BufferPoolStats {
buffers: pool_len(&self.f64_pool)
+ pool_len(&self.f32_pool)
+ pool_len(&self.i32_pool)
+ pool_len(&self.i64_pool)
+ pool_len(&self.bool_pool)
+ pool_len(&self.c64_pool)
+ pool_len(&self.c32_pool),
capacity_bytes: self.retained_capacity_bytes,
}
}
pub fn cache_stats(&self) -> CacheStats {
let stats = self.stats();
CacheStats {
entries: stats.buffers,
retained_bytes: stats.capacity_bytes,
}
}
pub fn acquire_with_capacity<T: PoolScalar>(&mut self, cap: usize) -> Vec<T> {
if cap == 0 {
return Vec::new();
}
let mut buf = unsafe { T::pool_acquire(self, cap) };
unsafe { buf.set_len(0) };
buf
}
pub fn acquire_zeroed<T: PoolScalar>(&mut self, len: usize) -> Vec<T> {
T::pool_acquire_zeroed(self, len)
}
pub fn is_empty(&self) -> bool {
self.f64_pool.is_empty()
&& self.f32_pool.is_empty()
&& self.i32_pool.is_empty()
&& self.i64_pool.is_empty()
&& self.bool_pool.is_empty()
&& self.c64_pool.is_empty()
&& self.c32_pool.is_empty()
}
pub fn clear(&mut self) {
self.f64_pool.clear();
self.f32_pool.clear();
self.i32_pool.clear();
self.i64_pool.clear();
self.bool_pool.clear();
self.c64_pool.clear();
self.c32_pool.clear();
self.retained_capacity_bytes = 0;
}
fn enforce_retention_limit(&mut self) {
while self.retained_capacity_bytes > self.max_retained_capacity_bytes {
let Some(evicted_bytes) = self.evict_smallest_retained_buffer() else {
self.retained_capacity_bytes = 0;
return;
};
self.retained_capacity_bytes =
self.retained_capacity_bytes.saturating_sub(evicted_bytes);
}
}
fn evict_smallest_retained_buffer(&mut self) -> Option<usize> {
let candidates = [
smallest_pool_candidate(&self.f64_pool, TypedPoolKind::F64),
smallest_pool_candidate(&self.f32_pool, TypedPoolKind::F32),
smallest_pool_candidate(&self.i32_pool, TypedPoolKind::I32),
smallest_pool_candidate(&self.i64_pool, TypedPoolKind::I64),
smallest_pool_candidate(&self.bool_pool, TypedPoolKind::Bool),
smallest_pool_candidate(&self.c64_pool, TypedPoolKind::C64),
smallest_pool_candidate(&self.c32_pool, TypedPoolKind::C32),
];
let (_, kind) = candidates
.into_iter()
.flatten()
.min_by_key(|(bytes, _)| *bytes)?;
match kind {
TypedPoolKind::F64 => evict_one_from_pool(&mut self.f64_pool),
TypedPoolKind::F32 => evict_one_from_pool(&mut self.f32_pool),
TypedPoolKind::I32 => evict_one_from_pool(&mut self.i32_pool),
TypedPoolKind::I64 => evict_one_from_pool(&mut self.i64_pool),
TypedPoolKind::Bool => evict_one_from_pool(&mut self.bool_pool),
TypedPoolKind::C64 => evict_one_from_pool(&mut self.c64_pool),
TypedPoolKind::C32 => evict_one_from_pool(&mut self.c32_pool),
}
}
}
fn default_max_retained_capacity_bytes() -> usize {
env::var(BUFFER_POOL_MAX_RETAINED_BYTES_ENV)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(DEFAULT_MAX_RETAINED_CAPACITY_BYTES)
}
impl Default for BufferPool {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests;