use std::alloc::Layout;
use std::fmt;
use std::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;
}
}
#[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))
}
}
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 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,
layout: Layout,
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,
#[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 {
std::alloc::dealloc(self.ptr, self.layout);
}
}
}
#[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(),
}
}
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(),
}
}
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 => {
unsafe {
self.inner
.ptr
.add(offset)
.write_bytes(0, self.inner.slot_capacity);
}
}
InitPolicy::Uninit => {}
}
self.inner
.metrics
.record_alloc_success(self.inner.slot_capacity);
Ok(Buffer::new_fixed(
Arc::clone(&self.inner),
slot_idx,
offset,
self.inner.slot_capacity,
))
}
}
pub struct FixedArenaBuilder {
slot_count: NonZeroUsize,
slot_capacity: NonZeroUsize,
config: BuildConfig,
}
impl FixedArenaBuilder {
pub fn alignment(mut self, n: usize) -> Self {
self.config.alignment = n;
self
}
pub fn auto_spill(mut self) -> Self {
self.config.auto_spill = true;
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
}
pub fn build(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)
}
pub fn build_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,
))
}
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 { std::alloc::alloc(layout) };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
let inner = ArenaInner {
ptr,
layout,
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),
#[cfg(feature = "async-alloc")]
wake_handle,
};
Ok(FixedArena {
inner: Arc::new(inner),
})
}
}
#[cfg(feature = "async-alloc")]
impl FixedArenaBuilder {
pub fn build_async(self) -> Result<crate::async_alloc::AsyncFixedArena, BuildError> {
self.build_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,
{
let page_size = self.config.page_size.resolve();
let waiters = std::sync::Arc::new(waiters);
let arena = self.build_inner(Some(crate::async_alloc::WakeHandle::new(
std::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))
}
}
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 std::num::NonZeroUsize;
fn nz(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).unwrap()
}
#[test]
fn build_basic_arena() {
let arena = FixedArena::with_slot_capacity(nz(4), nz(64))
.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(nz(4), nz(64))
.alignment(3)
.build()
.unwrap_err();
assert_eq!(err, BuildError::InvalidAlignment);
}
#[test]
fn build_zero_alignment_fails() {
let err = FixedArena::with_slot_capacity(nz(4), nz(64))
.alignment(0)
.build()
.unwrap_err();
assert_eq!(err, BuildError::InvalidAlignment);
}
#[test]
fn metrics_track_allocate_free_and_failure() {
let arena = FixedArena::with_slot_capacity(nz(1), nz(64))
.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(nz(usize::MAX), nz(2))
.build()
.unwrap_err();
assert_eq!(err, BuildError::SizeOverflow);
}
#[test]
fn alignment_rounding_overflow_fails() {
let err = FixedArena::with_slot_capacity(nz(1), nz(usize::MAX))
.alignment(2)
.build()
.unwrap_err();
assert_eq!(err, BuildError::SizeOverflow);
}
#[test]
fn alignment_rounds_capacity_up() {
let arena = FixedArena::with_slot_capacity(nz(2), nz(100))
.alignment(64)
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 128);
}
#[test]
fn alignment_4096_rounds_up() {
let arena = FixedArena::with_slot_capacity(nz(4), nz(100))
.alignment(4096)
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 4096);
}
#[test]
fn prefault_disabled_builds() {
let arena = FixedArena::with_slot_capacity(nz(4), nz(64))
.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(nz(4), nz(4096))
.page_size(PageSize::Size(nz(4096)))
.build()
.unwrap();
assert_eq!(arena.slot_count(), 4);
}
#[cfg(all(unix, feature = "libc"))]
#[test]
fn prefault_auto_builds() {
let arena = FixedArena::with_slot_capacity(nz(4), nz(4096))
.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(nz(4), nz(4096))
.page_size(PageSize::Size(nz(4096)))
.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(nz(4), nz(64))
.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(nz(2), nz(64))
.build()
.unwrap();
let arena2 = arena.clone();
assert_eq!(arena.slot_count(), arena2.slot_count());
assert_eq!(arena.slot_capacity(), arena2.slot_capacity());
}
#[test]
fn allocate_and_drop() {
let arena = FixedArena::with_slot_capacity(nz(2), nz(64))
.build()
.unwrap();
let buf1 = arena.allocate().unwrap();
let buf2 = arena.allocate().unwrap();
assert!(arena.allocate().is_err(), "arena should be full");
drop(buf1);
let _buf3 = arena.allocate().unwrap();
drop(buf2);
}
#[test]
fn allocate_full_returns_arena_full() {
let arena = FixedArena::with_slot_capacity(nz(1), nz(32))
.build()
.unwrap();
let _buf = arena.allocate().unwrap();
let err = arena.allocate().unwrap_err();
assert_eq!(err, crate::AllocError::ArenaFull);
}
#[test]
fn drop_returns_slot() {
let arena = FixedArena::with_slot_capacity(nz(1), nz(32))
.build()
.unwrap();
let buf = arena.allocate().unwrap();
drop(buf);
assert!(
arena.allocate().is_ok(),
"slot should be available after drop"
);
}
#[test]
fn init_policy_zero_fills_slot() {
use bytes::BufMut;
let arena = FixedArena::with_slot_capacity(nz(1), nz(64))
.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 { std::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(nz(4), nz(256))
.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(nz(3), nz(1000))
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 334);
}
#[test]
fn builder_arena_capacity_with_alignment() {
let arena = FixedArena::with_arena_capacity(nz(3), nz(1000))
.alignment(64)
.build()
.unwrap();
assert_eq!(arena.slot_capacity(), 384);
}
}