use core::alloc::Layout;
use core::fmt;
use core::num::NonZeroUsize;
use crate::bitmap::AtomicBitmap;
use crate::buffer::Buffer;
use crate::error::{AllocError, BuildError};
use crate::metrics::{FixedArenaMetrics, MetricsState};
use crate::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageSize {
Unknown,
#[cfg(all(unix, feature = "libc"))]
Auto,
Size(NonZeroUsize),
}
impl PageSize {
pub(crate) fn resolve(self) -> Option<usize> {
match self {
PageSize::Unknown => None,
#[cfg(all(unix, feature = "libc"))]
PageSize::Auto => Some(os_page_size()),
PageSize::Size(n) => Some(n.get()),
}
}
}
#[cfg(all(unix, feature = "libc"))]
fn os_page_size() -> usize {
let ps = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
debug_assert!(ps > 0);
ps as usize
}
pub(crate) fn prefault_region(ptr: *mut u8, len: usize, page_size: usize) {
let mut offset = 0;
while offset < len {
unsafe { ptr.add(offset).write_volatile(0) };
offset += page_size;
}
}
pub(crate) fn zeroize_region(ptr: *mut u8, len: usize) {
let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
zeroize::Zeroize::zeroize(slice);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InitPolicy {
#[default]
Uninit,
Zero,
}
pub struct Unfaulted<A> {
ptr: *mut u8,
total_size: usize,
page_size: Option<usize>,
inner: A,
}
unsafe impl<A: Send> Send for Unfaulted<A> {}
impl<A> Unfaulted<A> {
pub(crate) fn new(ptr: *mut u8, total_size: usize, page_size: Option<usize>, inner: A) -> Self {
Self {
ptr,
total_size,
page_size,
inner,
}
}
pub fn fault_pages(self) -> A {
if let Some(ps) = self.page_size {
prefault_region(self.ptr, self.total_size, ps);
}
self.inner
}
pub fn into_inner(self) -> A {
self.inner
}
}
impl Unfaulted<FixedArena> {
pub fn allocate(self) -> Result<(FixedArena, Buffer), AllocError> {
let arena = self.into_inner();
let buf = arena.allocate()?;
Ok((arena, buf))
}
}
#[derive(Debug, Clone, Copy)]
pub struct Standard;
#[derive(Debug, Clone, Copy)]
pub struct AutoSpill;
#[cfg(feature = "hazmat-raw-access")]
#[derive(Debug, Clone, Copy)]
pub struct HazmatRaw;
pub(crate) struct BuildConfig {
pub(crate) alignment: usize,
pub(crate) auto_spill: bool,
pub(crate) init_policy: InitPolicy,
pub(crate) page_size: PageSize,
}
impl BuildConfig {
pub(crate) fn new() -> Self {
Self {
alignment: 1,
auto_spill: false,
init_policy: InitPolicy::default(),
#[cfg(all(unix, feature = "libc"))]
page_size: PageSize::Auto,
#[cfg(not(all(unix, feature = "libc")))]
page_size: PageSize::Unknown,
}
}
pub(crate) fn new_raw_backed() -> Self {
let mut config = Self::new();
config.page_size = PageSize::Unknown;
config
}
pub(crate) fn validate_alignment(&self) -> Result<(), BuildError> {
if !self.alignment.is_power_of_two() {
return Err(BuildError::InvalidAlignment);
}
Ok(())
}
}
pub(crate) struct ArenaInner {
pub(crate) ptr: *mut u8,
pub(crate) total_size: usize,
pub(crate) slot_capacity: usize,
pub(crate) slot_count: usize,
pub(crate) bitmap: AtomicBitmap,
pub(crate) auto_spill: bool,
pub(crate) init_policy: InitPolicy,
pub(crate) metrics: MetricsState,
pub(crate) zeroed_bitmap: Option<AtomicBitmap>,
dealloc: crate::dealloc::ErasedDealloc,
#[cfg(feature = "async-alloc")]
pub(crate) wake_handle: Option<crate::async_alloc::WakeHandle>,
}
unsafe impl Send for ArenaInner {}
unsafe impl Sync for ArenaInner {}
impl Drop for ArenaInner {
fn drop(&mut self) {
unsafe {
let dealloc =
core::mem::replace(&mut self.dealloc, crate::dealloc::ErasedDealloc::noop());
dealloc.dealloc(self.ptr, self.total_size);
}
}
}
#[derive(Clone)]
pub struct FixedArena {
pub(crate) inner: Arc<ArenaInner>,
}
impl fmt::Debug for FixedArena {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FixedArena")
.field("slot_count", &self.inner.slot_count)
.field("slot_capacity", &self.inner.slot_capacity)
.finish()
}
}
impl FixedArena {
pub fn with_slot_capacity(
slot_count: NonZeroUsize,
slot_capacity: NonZeroUsize,
) -> FixedArenaBuilder {
FixedArenaBuilder {
slot_count,
slot_capacity,
config: BuildConfig::new(),
_mode: core::marker::PhantomData,
}
}
pub fn with_arena_capacity(
slot_count: NonZeroUsize,
arena_capacity: NonZeroUsize,
) -> FixedArenaBuilder {
let per_slot = arena_capacity.get().div_ceil(slot_count.get());
FixedArenaBuilder {
slot_count,
slot_capacity: NonZeroUsize::new(per_slot).unwrap(),
config: BuildConfig::new(),
_mode: core::marker::PhantomData,
}
}
pub unsafe fn from_raw<D: crate::dealloc::Dealloc>(
ptr: *mut u8,
len: usize,
spec: crate::spec::SlotSpec,
dealloc: D,
) -> RawBackedFixedArenaBuilder<D> {
RawBackedFixedArenaBuilder {
ptr,
len,
spec,
dealloc,
config: BuildConfig::new_raw_backed(),
}
}
pub fn from_static(
buf: &'static mut [u8],
spec: crate::spec::SlotSpec,
) -> RawBackedFixedArenaBuilder<crate::dealloc::NoDealloc> {
unsafe { Self::from_raw(buf.as_mut_ptr(), buf.len(), spec, crate::dealloc::NoDealloc) }
}
pub fn slot_count(&self) -> usize {
self.inner.slot_count
}
pub fn slot_capacity(&self) -> usize {
self.inner.slot_capacity
}
pub fn metrics(&self) -> FixedArenaMetrics {
self.inner.metrics.fixed_snapshot()
}
pub fn allocate(&self) -> Result<Buffer, AllocError> {
let Some(slot_idx) = self.inner.bitmap.try_alloc() else {
self.inner.metrics.record_alloc_failure();
return Err(AllocError::ArenaFull);
};
let offset = slot_idx * self.inner.slot_capacity;
match self.inner.init_policy {
InitPolicy::Zero => {
if let Some(ref zeroed_bm) = self.inner.zeroed_bitmap
&& !zeroed_bm.all_set_in_range(slot_idx, slot_idx + 1)
{
unsafe { zeroize_region(self.inner.ptr.add(offset), self.inner.slot_capacity) };
}
}
InitPolicy::Uninit => {}
}
self.inner
.metrics
.record_alloc_success(self.inner.slot_capacity);
Ok(Buffer::new_fixed(
crate::allocation::ArenaRef::Fixed(self.inner.clone()),
self.inner.ptr,
self.inner.auto_spill,
slot_idx,
offset,
self.inner.slot_capacity,
))
}
}
pub struct RawBackedFixedArenaBuilder<D: crate::dealloc::Dealloc> {
ptr: *mut u8,
len: usize,
spec: crate::spec::SlotSpec,
dealloc: D,
config: BuildConfig,
}
impl<D: crate::dealloc::Dealloc> RawBackedFixedArenaBuilder<D> {
pub fn init_policy(mut self, policy: InitPolicy) -> Self {
self.config.init_policy = policy;
self
}
pub fn alignment(mut self, align: usize) -> Self {
self.config.alignment = align;
self
}
pub fn build(self) -> Result<FixedArena, BuildError> {
if self.ptr.is_null() {
return Err(BuildError::NullPointer);
}
self.config.validate_alignment()?;
let page_size = self.config.page_size.resolve();
let (slot_count, slot_capacity) = self.spec.resolve(self.len, self.config.alignment)?;
let zeroed_bitmap = match self.config.init_policy {
InitPolicy::Zero => Some(AtomicBitmap::new_empty(slot_count)),
InitPolicy::Uninit => None,
};
let inner = ArenaInner {
ptr: self.ptr,
total_size: self.len,
slot_capacity,
slot_count,
bitmap: AtomicBitmap::new(slot_count),
auto_spill: false,
init_policy: self.config.init_policy,
metrics: MetricsState::new(slot_count * slot_capacity),
zeroed_bitmap,
dealloc: crate::dealloc::ErasedDealloc::new(self.dealloc),
#[cfg(feature = "async-alloc")]
wake_handle: None,
};
let arena = FixedArena {
inner: Arc::new(inner),
};
if let Some(ps) = page_size {
prefault_region(
arena.inner.ptr,
arena.inner.slot_count * arena.inner.slot_capacity,
ps,
);
}
Ok(arena)
}
}
pub struct FixedArenaBuilder<Mode = Standard> {
slot_count: NonZeroUsize,
slot_capacity: NonZeroUsize,
config: BuildConfig,
_mode: core::marker::PhantomData<Mode>,
}
impl<Mode> FixedArenaBuilder<Mode> {
pub fn alignment(mut self, n: usize) -> Self {
self.config.alignment = n;
self
}
pub fn init_policy(mut self, policy: InitPolicy) -> Self {
self.config.init_policy = policy;
self
}
pub fn page_size(mut self, policy: PageSize) -> Self {
self.config.page_size = policy;
self
}
fn build_inner(
self,
#[cfg(feature = "async-alloc")] wake_handle: Option<crate::async_alloc::WakeHandle>,
) -> Result<FixedArena, BuildError> {
self.config.validate_alignment()?;
let slot_count = self.slot_count.get();
let slot_capacity = self.slot_capacity.get();
let aligned_capacity =
align_up(slot_capacity, self.config.alignment).ok_or(BuildError::SizeOverflow)?;
let total_size = slot_count
.checked_mul(aligned_capacity)
.ok_or(BuildError::SizeOverflow)?;
let layout = Layout::from_size_align(total_size, self.config.alignment)
.map_err(|_| BuildError::SizeOverflow)?;
let ptr = unsafe { alloc::alloc::alloc(layout) };
if ptr.is_null() {
alloc::alloc::handle_alloc_error(layout);
}
let zeroed_bitmap = match self.config.init_policy {
InitPolicy::Zero => Some(AtomicBitmap::new_empty(slot_count)),
InitPolicy::Uninit => None,
};
let inner = ArenaInner {
ptr,
total_size,
slot_capacity: aligned_capacity,
slot_count,
bitmap: AtomicBitmap::new(slot_count),
auto_spill: self.config.auto_spill,
init_policy: self.config.init_policy,
metrics: MetricsState::new(total_size),
zeroed_bitmap,
dealloc: crate::dealloc::ErasedDealloc::new(crate::dealloc::HeapDealloc::new(layout)),
#[cfg(feature = "async-alloc")]
wake_handle,
};
Ok(FixedArena {
inner: Arc::new(inner),
})
}
fn build_fixed(self) -> Result<FixedArena, BuildError> {
let page_size = self.config.page_size.resolve();
let arena = self.build_inner(
#[cfg(feature = "async-alloc")]
None,
)?;
if let Some(ps) = page_size {
prefault_region(
arena.inner.ptr,
arena.inner.slot_count * arena.inner.slot_capacity,
ps,
);
}
Ok(arena)
}
fn build_fixed_unfaulted(self) -> Result<Unfaulted<FixedArena>, BuildError> {
let page_size = self.config.page_size.resolve();
let arena = self.build_inner(
#[cfg(feature = "async-alloc")]
None,
)?;
let total_size = arena.inner.slot_count * arena.inner.slot_capacity;
Ok(Unfaulted::new(
arena.inner.ptr,
total_size,
page_size,
arena,
))
}
#[cfg(feature = "async-alloc")]
fn build_fixed_async_with<W>(
self,
waiters: W,
) -> Result<crate::async_alloc::AsyncFixedArena<W>, BuildError>
where
W: crate::async_alloc::Waiter,
{
let page_size = self.config.page_size.resolve();
let waiters = alloc::sync::Arc::new(waiters);
let arena = self.build_inner(Some(crate::async_alloc::WakeHandle::new(
alloc::sync::Arc::clone(&waiters),
)))?;
if let Some(ps) = page_size {
prefault_region(
arena.inner.ptr,
arena.inner.slot_count * arena.inner.slot_capacity,
ps,
);
}
Ok(crate::async_alloc::AsyncFixedArena::new(arena, waiters))
}
}
impl FixedArenaBuilder<Standard> {
pub fn auto_spill(self) -> FixedArenaBuilder<AutoSpill> {
FixedArenaBuilder {
slot_count: self.slot_count,
slot_capacity: self.slot_capacity,
config: BuildConfig {
auto_spill: true,
..self.config
},
_mode: core::marker::PhantomData,
}
}
pub fn build(self) -> Result<FixedArena, BuildError> {
self.build_fixed()
}
pub fn build_unfaulted(self) -> Result<Unfaulted<FixedArena>, BuildError> {
self.build_fixed_unfaulted()
}
}
impl FixedArenaBuilder<AutoSpill> {
pub fn build(self) -> Result<FixedArena, BuildError> {
self.build_fixed()
}
pub fn build_unfaulted(self) -> Result<Unfaulted<FixedArena>, BuildError> {
self.build_fixed_unfaulted()
}
}
#[cfg(feature = "hazmat-raw-access")]
impl FixedArenaBuilder<Standard> {
pub fn hazmat_raw_access(self) -> FixedArenaBuilder<HazmatRaw> {
FixedArenaBuilder {
slot_count: self.slot_count,
slot_capacity: self.slot_capacity,
config: self.config,
_mode: core::marker::PhantomData,
}
}
}
#[cfg(feature = "hazmat-raw-access")]
impl FixedArenaBuilder<HazmatRaw> {
pub fn build(self) -> Result<crate::hazmat::RawFixedArena, BuildError> {
self.build_fixed().map(crate::hazmat::RawFixedArena)
}
pub fn build_unfaulted(self) -> Result<Unfaulted<crate::hazmat::RawFixedArena>, BuildError> {
let page_size = self.config.page_size.resolve();
let arena = self.build_inner(
#[cfg(feature = "async-alloc")]
None,
)?;
let total_size = arena.inner.slot_count * arena.inner.slot_capacity;
Ok(Unfaulted::new(
arena.inner.ptr,
total_size,
page_size,
crate::hazmat::RawFixedArena(arena),
))
}
}
#[cfg(feature = "async-alloc")]
impl FixedArenaBuilder<Standard> {
pub fn build_async(self) -> Result<crate::async_alloc::AsyncFixedArena, BuildError> {
self.build_fixed_async_with(crate::async_alloc::NotifyWaiters::new(1))
}
pub fn build_async_with<W>(
self,
waiters: W,
) -> Result<crate::async_alloc::AsyncFixedArena<W>, BuildError>
where
W: crate::async_alloc::Waiter,
{
self.build_fixed_async_with(waiters)
}
}
#[cfg(feature = "async-alloc")]
impl FixedArenaBuilder<AutoSpill> {
pub fn build_async(self) -> Result<crate::async_alloc::AsyncFixedArena, BuildError> {
self.build_fixed_async_with(crate::async_alloc::NotifyWaiters::new(1))
}
pub fn build_async_with<W>(
self,
waiters: W,
) -> Result<crate::async_alloc::AsyncFixedArena<W>, BuildError>
where
W: crate::async_alloc::Waiter,
{
self.build_fixed_async_with(waiters)
}
}
fn align_up(value: usize, alignment: usize) -> Option<usize> {
let rounded = value.checked_add(alignment - 1)?;
Some(rounded & !(alignment - 1))
}
#[cfg(test)]
mod tests {
use super::*;
use core::num::NonZeroUsize;
#[test]
fn build_basic_arena() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.build()
.unwrap();
assert_eq!(arena.slot_count(), 4);
assert_eq!(arena.slot_capacity(), 64);
}
#[test]
fn build_invalid_alignment_fails() {
let err = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.alignment(3)
.build()
.unwrap_err();
assert_eq!(err, BuildError::InvalidAlignment);
}
#[test]
fn build_zero_alignment_fails() {
let err = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.alignment(0)
.build()
.unwrap_err();
assert_eq!(err, BuildError::InvalidAlignment);
}
#[test]
fn metrics_track_allocate_free_and_failure() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.build()
.unwrap();
let initial = arena.metrics();
assert_eq!(initial.bytes_reserved, 64);
assert_eq!(initial.bytes_live, 0);
let buf = arena.allocate().unwrap();
let after_alloc = arena.metrics();
assert_eq!(after_alloc.allocations_ok, 1);
assert_eq!(after_alloc.allocations_failed, 0);
assert_eq!(after_alloc.bytes_live, 64);
assert_eq!(arena.allocate().unwrap_err(), AllocError::ArenaFull);
let after_fail = arena.metrics();
assert_eq!(after_fail.allocations_failed, 1);
assert_eq!(after_fail.bytes_live, 64);
drop(buf);
let after_free = arena.metrics();
assert_eq!(after_free.frees, 1);
assert_eq!(after_free.bytes_live, 0);
}
#[test]
fn build_size_overflow_fails() {
let err = FixedArena::with_slot_capacity(
NonZeroUsize::new(usize::MAX).unwrap(),
NonZeroUsize::new(2).unwrap(),
)
.build()
.unwrap_err();
assert_eq!(err, BuildError::SizeOverflow);
}
#[test]
fn alignment_rounding_overflow_fails() {
let err = FixedArena::with_slot_capacity(
NonZeroUsize::new(1).unwrap(),
NonZeroUsize::new(usize::MAX).unwrap(),
)
.alignment(2)
.build()
.unwrap_err();
assert_eq!(err, BuildError::SizeOverflow);
}
#[test]
fn alignment_rounds_capacity_up() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(2).unwrap(),
NonZeroUsize::new(100).unwrap(),
)
.alignment(64)
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 128);
}
#[test]
fn prefault_disabled_builds() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.page_size(PageSize::Unknown)
.build()
.unwrap();
assert_eq!(arena.slot_count(), 4);
}
#[test]
fn prefault_explicit_page_size_builds() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(4096).unwrap(),
)
.page_size(PageSize::Size(NonZeroUsize::new(4096).unwrap()))
.build()
.unwrap();
assert_eq!(arena.slot_count(), 4);
}
#[cfg(all(unix, feature = "libc"))]
#[test]
fn prefault_auto_builds() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(4096).unwrap(),
)
.page_size(PageSize::Auto)
.build()
.unwrap();
assert_eq!(arena.slot_count(), 4);
}
#[test]
fn build_unfaulted_then_fault_pages() {
let faultable = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(4096).unwrap(),
)
.page_size(PageSize::Size(NonZeroUsize::new(4096).unwrap()))
.build_unfaulted()
.unwrap();
let arena = faultable.fault_pages();
assert_eq!(arena.slot_count(), 4);
let _buf = arena.allocate().unwrap();
}
#[test]
fn build_unfaulted_into_inner_skips_fault() {
let faultable = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.page_size(PageSize::Unknown)
.build_unfaulted()
.unwrap();
let arena = faultable.into_inner();
assert_eq!(arena.slot_count(), 4);
let _buf = arena.allocate().unwrap();
}
#[test]
fn clone_shares_inner() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(2).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.build()
.unwrap();
let arena2 = arena.clone();
assert_eq!(arena.slot_count(), arena2.slot_count());
assert_eq!(arena.slot_capacity(), arena2.slot_capacity());
}
#[test]
fn init_policy_zero_fills_slot() {
use bytes::BufMut;
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.init_policy(InitPolicy::Zero)
.page_size(PageSize::Unknown)
.build()
.unwrap();
let mut buf = arena.allocate().unwrap();
buf.put_slice(&[0xAB; 64]);
let bytes = buf.freeze();
drop(bytes);
let buf = arena.allocate().unwrap();
let slot = unsafe { core::slice::from_raw_parts(buf.ptr.add(buf.offset), 64) };
assert!(slot.iter().all(|&b| b == 0), "slot should be zeroed");
}
#[test]
fn init_policy_default_is_uninit() {
assert_eq!(InitPolicy::default(), InitPolicy::Uninit);
}
#[test]
fn builder_with_arena_capacity() {
let arena = FixedArena::with_arena_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(256).unwrap(),
)
.build()
.unwrap();
assert_eq!(arena.slot_count(), 4);
assert_eq!(arena.slot_capacity(), 64);
}
#[test]
fn builder_arena_capacity_rounds_up() {
let arena = FixedArena::with_arena_capacity(
NonZeroUsize::new(3).unwrap(),
NonZeroUsize::new(1000).unwrap(),
)
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 334);
}
#[test]
fn builder_arena_capacity_with_alignment() {
let arena = FixedArena::with_arena_capacity(
NonZeroUsize::new(3).unwrap(),
NonZeroUsize::new(1000).unwrap(),
)
.alignment(64)
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 384);
}
#[test]
fn auto_spill_builder_produces_fixed_arena() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.auto_spill()
.build()
.unwrap();
let _buf = arena.allocate().unwrap();
}
#[cfg(feature = "hazmat-raw-access")]
#[test]
fn hazmat_builder_produces_raw_fixed_arena() {
let raw_arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.hazmat_raw_access()
.build()
.unwrap();
let _buf = raw_arena.allocate().unwrap();
}
#[test]
fn zero_policy_first_alloc_zeroes_cold_memory() {
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1).unwrap(),
NonZeroUsize::new(64).unwrap(),
)
.init_policy(InitPolicy::Zero)
.page_size(PageSize::Unknown)
.build()
.unwrap();
let buf = arena.allocate().unwrap();
let slot = unsafe { core::slice::from_raw_parts(buf.ptr.add(buf.offset), 64) };
assert!(slot.iter().all(|&b| b == 0), "first alloc should be zeroed");
}
}