use std::cell::{Cell, RefCell};
use std::ops::Deref;
use crate::allocator::Allocator;
const MAX_IDLE_PER_WORKER: usize = 4;
thread_local! {
static IDLE: RefCell<std::vec::Vec<Allocator>> = const { RefCell::new(std::vec::Vec::new()) };
static CHECKED_OUT: Cell<usize> = const { Cell::new(0) };
}
pub fn acquire() -> PooledAllocator {
let allocator = IDLE
.try_with(|idle| idle.borrow_mut().pop())
.unwrap_or_default();
let allocator = allocator.unwrap_or_default();
debug_assert_eq!(
allocator.allocated_bytes(),
0,
"pooled arena handed out without a reset"
);
let _ = CHECKED_OUT.try_with(|count| count.set(count.get() + 1));
PooledAllocator { allocator }
}
#[inline]
pub fn checked_out() -> usize {
CHECKED_OUT.try_with(Cell::get).unwrap_or(0)
}
#[inline]
pub fn idle() -> usize {
IDLE.try_with(|idle| idle.borrow().len()).unwrap_or(0)
}
pub fn clear() {
let _ = IDLE.try_with(|idle| idle.borrow_mut().clear());
}
pub struct PooledAllocator {
allocator: Allocator,
}
impl PooledAllocator {
#[inline]
pub fn allocator(&self) -> &Allocator {
&self.allocator
}
}
impl Deref for PooledAllocator {
type Target = Allocator;
#[inline]
fn deref(&self) -> &Self::Target {
self.allocator()
}
}
impl Drop for PooledAllocator {
fn drop(&mut self) {
let mut allocator = core::mem::take(&mut self.allocator);
allocator.reset();
let _ = CHECKED_OUT.try_with(|count| count.set(count.get().saturating_sub(1)));
let _ = IDLE.try_with(|idle| {
let mut idle = idle.borrow_mut();
if idle.len() < MAX_IDLE_PER_WORKER {
idle.push(allocator);
}
});
}
}
#[cfg(test)]
mod tests {
use super::{MAX_IDLE_PER_WORKER, acquire, checked_out, clear, idle};
fn with_empty_pool(body: impl FnOnce()) {
clear();
assert_eq!((checked_out(), idle()), (0, 0));
body();
clear();
}
#[test]
fn test_the_next_file_reuses_the_same_arena_memory() {
with_empty_pool(|| {
let first_address = {
let arena = acquire();
arena.alloc_str("file N").as_ptr() as usize
};
assert_eq!(idle(), 1);
let arena = acquire();
assert_eq!(arena.allocated_bytes(), 0);
assert_eq!(arena.alloc_str("file M").as_ptr() as usize, first_address);
});
}
#[test]
fn test_checked_out_tracks_guard_scope() {
with_empty_pool(|| {
let outer = acquire();
assert_eq!(checked_out(), 1);
{
let inner = acquire();
assert_eq!(checked_out(), 2);
assert_ne!(inner.stamp(), outer.stamp());
}
assert_eq!(checked_out(), 1);
drop(outer);
assert_eq!((checked_out(), idle()), (0, 2));
});
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "arena-backed value outlived its compile")]
fn test_recycling_a_guard_makes_its_stamps_stale() {
clear();
let stamp = {
let arena = acquire();
let _ = arena.alloc_str("file N");
arena.stamp()
};
let arena = acquire();
arena.assert_stamp_current(stamp);
}
#[test]
fn test_a_live_guard_keeps_its_stamp_current() {
with_empty_pool(|| {
let arena = acquire();
let stamp = arena.stamp();
let _ = arena.alloc_str("still the same compile");
arena.assert_stamp_current(stamp);
assert_eq!(arena.stamp(), stamp);
});
}
#[test]
fn test_idle_arenas_are_bounded() {
with_empty_pool(|| {
let guards: std::vec::Vec<_> =
(0..MAX_IDLE_PER_WORKER + 3).map(|_| acquire()).collect();
assert_eq!(checked_out(), MAX_IDLE_PER_WORKER + 3);
drop(guards);
assert_eq!((checked_out(), idle()), (0, MAX_IDLE_PER_WORKER));
});
}
#[test]
fn test_pools_are_per_worker() {
with_empty_pool(|| {
let outer = acquire();
let _ = outer.alloc_str("owned by this worker");
let observed = std::thread::spawn(|| {
let idle_at_start = idle();
let arena = acquire();
let _ = arena.alloc_str("owned by the other worker");
let checked_out_inside = checked_out();
drop(arena);
(idle_at_start, checked_out_inside, idle(), checked_out())
})
.join()
.expect("the worker thread runs to completion");
assert_eq!(observed, (0, 1, 1, 0));
assert_eq!((checked_out(), idle()), (1, 0));
});
}
#[test]
fn test_parked_values_drop_when_the_guard_returns() {
use std::sync::atomic::{AtomicUsize, Ordering};
static DROPS: AtomicUsize = AtomicUsize::new(0);
struct Parked;
impl Drop for Parked {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::Relaxed);
}
}
with_empty_pool(|| {
let before = DROPS.load(Ordering::Relaxed);
for _ in 0..8 {
let arena = acquire();
let parked: &Parked = arena.alloc_owned(Parked);
let _ = std::hint::black_box(parked);
}
assert_eq!(DROPS.load(Ordering::Relaxed), before + 8);
});
}
}