#![allow(clippy::inline_always, reason = "hot bump-allocator helpers must inline into their callers")]
use alloc::sync::Arc as StdArc;
use core::cell::Cell;
use core::fmt;
use core::ptr::NonNull;
use allocator_api2::alloc::{AllocError, Allocator, Global};
use crate::arena_builder::ArenaBuilder;
#[cfg(feature = "stats")]
use crate::arena_stats::ArenaStats;
use crate::internal::chunk_mutator::ChunkMutator;
use crate::internal::chunk_provider::{ChunkProvider, ChunkProviderConfig};
use crate::internal::chunk_ref::ChunkRef;
use crate::internal::constants::{MAX_NORMAL_ALLOC, SizeClass};
use crate::internal::current_chunk::CurrentChunk;
use crate::internal::local_chunk::LocalChunk;
use crate::internal::shared_chunk::SharedChunk;
const LARGE_SHARED_REF_SURPLUS: u32 = 1 << 30;
mod alloc_growable;
pub(crate) mod alloc_prefixed;
mod alloc_slice_arc;
mod alloc_slice_box;
mod alloc_slice_ref;
mod alloc_str;
mod alloc_uninit;
#[cfg(feature = "dst")]
mod alloc_unsized;
#[cfg(feature = "utf16")]
mod alloc_utf16;
pub(crate) mod alloc_value;
mod reserve;
mod retired_local;
use retired_local::RetiredLocalChunks;
pub struct Arena<A: Allocator + Clone = Global> {
current_local: CurrentChunk<LocalChunk<A>>,
current_shared: CurrentChunk<SharedChunk<A>>,
local_shared_count: Cell<u32>,
retired_local: RetiredLocalChunks<A>,
next_local_class: Cell<SizeClass>,
next_shared_class: Cell<SizeClass>,
provider: StdArc<ChunkProvider<A>>,
#[cfg(feature = "stats")]
relocations: Cell<u64>,
}
impl Arena<Global> {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::new_in(Global)
}
#[inline]
#[cfg_attr(test, mutants::skip)] pub fn try_new() -> Result<Self, AllocError> {
Self::try_new_in(Global)
}
#[must_use]
#[inline]
#[cfg_attr(test, mutants::skip)] pub fn builder() -> ArenaBuilder<Global> {
ArenaBuilder::new()
}
}
impl Default for Arena<Global> {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl<A: Allocator + Clone> Arena<A> {
#[must_use]
#[inline]
pub fn builder_in(allocator: A) -> ArenaBuilder<A> {
ArenaBuilder::new_in(allocator)
}
#[must_use]
#[inline]
pub fn new_in(allocator: A) -> Self
where
A: 'static,
{
expect_alloc(Self::try_from_config(allocator, MAX_NORMAL_ALLOC, None))
}
#[inline]
pub fn try_new_in(allocator: A) -> Result<Self, AllocError>
where
A: 'static,
{
Self::try_from_config(allocator, MAX_NORMAL_ALLOC, None)
}
#[allow(
clippy::unnecessary_wraps,
reason = "Result return is part of try_from_config's contract; callers propagate the error"
)]
pub(crate) fn try_from_config(allocator: A, max_normal_alloc: usize, byte_budget: Option<usize>) -> Result<Self, AllocError> {
let config = ChunkProviderConfig::new(byte_budget.unwrap_or(usize::MAX), max_normal_alloc);
let provider = ChunkProvider::new(allocator, config);
Ok(Self {
current_local: CurrentChunk::new(ChunkMutator::<LocalChunk<A>>::empty()),
current_shared: CurrentChunk::new(ChunkMutator::<SharedChunk<A>>::empty()),
local_shared_count: Cell::new(0),
retired_local: RetiredLocalChunks::new(),
next_local_class: Cell::new(SizeClass::ZERO),
next_shared_class: Cell::new(SizeClass::ZERO),
provider,
#[cfg(feature = "stats")]
relocations: Cell::new(0),
})
}
#[cfg_attr(test, mutants::skip)] pub(crate) fn preallocate_one_local(&self, class: SizeClass) -> Result<(), AllocError> {
self.provider.preallocate_local(class)?;
if class > self.next_local_class.get() {
self.next_local_class.set(class);
}
Ok(())
}
#[cfg_attr(test, mutants::skip)] pub(crate) fn preallocate_one_shared(&self, class: SizeClass) -> Result<(), AllocError> {
self.provider.preallocate_shared(class)?;
if class > self.next_shared_class.get() {
self.next_shared_class.set(class);
}
Ok(())
}
#[must_use]
#[inline]
pub fn allocator(&self) -> &A {
self.provider.allocator()
}
#[cfg(feature = "stats")]
#[cfg_attr(docsrs, doc(cfg(feature = "stats")))]
#[must_use]
#[inline]
pub fn stats(&self) -> ArenaStats {
let chunks = self.provider.chunk_alloc_stats();
let current_local_free = u64::from(self.current_local.borrow().wasted_tail_for_stats());
let current_shared_free = u64::from(self.current_shared.borrow().wasted_tail_for_stats());
ArenaStats {
total_bytes_allocated: self.provider.bytes_outstanding(),
wasted_tail_bytes: self.provider.wasted_tail_bytes() + current_local_free + current_shared_free,
normal_local_chunks_allocated: chunks.normal_local(),
oversized_local_chunks_allocated: chunks.oversized_local(),
normal_shared_chunks_allocated: chunks.normal_shared(),
oversized_shared_chunks_allocated: chunks.oversized_shared(),
relocations: self.relocations.get(),
..ArenaStats::default()
}
}
#[cfg(feature = "stats")]
#[inline(always)]
pub(crate) fn record_relocation(&self) {
self.relocations.set(self.relocations.get() + 1);
}
#[cold]
pub fn reset(&mut self) {
self.reconcile_shared_surplus();
self.retired_local.clear();
*self.current_local.get_mut() = ChunkMutator::<LocalChunk<A>>::empty();
*self.current_shared.get_mut() = ChunkMutator::<SharedChunk<A>>::empty();
}
#[cfg(feature = "zerocopy")]
#[cfg_attr(docsrs, doc(cfg(feature = "zerocopy")))]
#[inline]
#[must_use]
pub const fn zerocopy(&self) -> crate::zerocopy::ZerocopyView<'_, A> {
crate::zerocopy::ZerocopyView::new(self)
}
#[cfg(feature = "bytemuck")]
#[cfg_attr(docsrs, doc(cfg(feature = "bytemuck")))]
#[inline]
#[must_use]
pub const fn bytemuck(&self) -> crate::bytemuck::BytemuckView<'_, A> {
crate::bytemuck::BytemuckView::new(self)
}
#[inline(always)]
pub(crate) fn current_local(&self) -> &ChunkMutator<LocalChunk<A>> {
self.current_local.borrow()
}
#[inline]
pub(crate) fn max_normal_alloc(&self) -> usize {
self.provider.config().max_normal_alloc()
}
#[inline]
pub(crate) fn is_oversized_shared(&self, min_payload: usize) -> bool {
min_payload > self.max_normal_alloc()
}
#[inline]
pub(crate) fn is_oversized_local(&self, min_payload: usize) -> bool {
min_payload > self.max_normal_alloc()
}
#[inline]
pub(crate) fn try_grow_local_in_place(&self, base_addr: usize, old_bytes: usize, new_bytes: usize) -> bool {
self.current_local.borrow().try_grow_in_place(base_addr, old_bytes, new_bytes)
}
#[inline(always)]
pub(crate) fn current_shared(&self) -> &ChunkMutator<SharedChunk<A>> {
self.current_shared.borrow()
}
#[cold]
#[inline(never)]
#[cfg_attr(test, mutants::skip)]
pub(crate) fn refill_local(&self, min_payload: usize) -> Result<(), AllocError> {
let new_chunk = self.provider.acquire_local(min_payload, self.next_local_class.get())?;
let new_mutator = unsafe { ChunkMutator::<LocalChunk<A>>::from_owned(new_chunk) };
let old = self.current_local.replace(new_mutator);
self.retired_local.push(old);
self.next_local_class.set(self.next_local_class.get().saturating_inc());
Ok(())
}
#[cold]
#[inline(never)]
#[cfg_attr(test, mutants::skip)] pub(crate) fn refill_shared(&self, min_payload: usize) -> Result<(), AllocError> {
self.reconcile_shared_surplus();
self.current_shared.drop_replace(ChunkMutator::<SharedChunk<A>>::empty());
if self.current_shared.borrow().chunk_ptr().is_some() {
return Ok(());
}
let new_chunk = self.provider.acquire_shared(min_payload, self.next_shared_class.get())?;
unsafe { new_chunk.as_ref().pre_credit_refs(LARGE_SHARED_REF_SURPLUS as usize) };
debug_assert_eq!(self.local_shared_count.get(), 0, "local_shared_count must be 0 after reconcile");
let new_mutator = unsafe { ChunkMutator::<SharedChunk<A>>::from_owned(new_chunk) };
self.current_shared.drop_replace(new_mutator);
self.next_shared_class.set(self.next_shared_class.get().saturating_inc());
Ok(())
}
#[cold]
#[inline(never)]
pub(crate) fn alloc_oversized_shared_with<R>(
&self,
min_payload: usize,
do_alloc: impl FnOnce(&ChunkMutator<SharedChunk<A>>, NonNull<SharedChunk<A>>) -> R,
) -> Result<R, AllocError> {
let chunk = self.provider.acquire_oversized_shared(min_payload)?;
let mutator = unsafe { ChunkMutator::<SharedChunk<A>>::from_owned(chunk) };
Ok(do_alloc(&mutator, chunk))
}
#[cold]
#[inline(never)]
#[allow(
clippy::type_complexity,
reason = "Returning both the mutator and the chunk pointer keeps the cold helper closure-free"
)]
pub(crate) fn acquire_oversized_shared_mutator(
&self,
min_payload: usize,
) -> Result<(ChunkMutator<SharedChunk<A>>, NonNull<SharedChunk<A>>), AllocError> {
let chunk = self.provider.acquire_oversized_shared(min_payload)?;
let mutator = unsafe { ChunkMutator::<SharedChunk<A>>::from_owned(chunk) };
Ok((mutator, chunk))
}
#[cold]
#[inline(never)]
pub(crate) fn alloc_oversized_local_with<R>(
&self,
min_payload: usize,
do_alloc: impl FnOnce(&ChunkMutator<LocalChunk<A>>) -> R,
) -> Result<R, AllocError> {
let chunk = self.provider.acquire_oversized_local(min_payload)?;
let mutator = unsafe { ChunkMutator::<LocalChunk<A>>::from_owned(chunk) };
let result = do_alloc(&mutator);
self.retired_local.push(mutator);
Ok(result)
}
#[cold]
#[inline(never)]
pub(crate) fn acquire_oversized_local_mutator(&self, min_payload: usize) -> Result<ChunkMutator<LocalChunk<A>>, AllocError> {
let chunk = self.provider.acquire_oversized_local(min_payload)?;
Ok(unsafe { ChunkMutator::<LocalChunk<A>>::from_owned(chunk) })
}
#[cold]
#[inline(never)]
pub(crate) fn retain_oversized_local_mutator(&self, mutator: ChunkMutator<LocalChunk<A>>) {
self.retired_local.push(mutator);
}
#[expect(clippy::inline_always, reason = "hot-path entry; must inline fully for arena performance")]
#[inline(always)]
pub(crate) fn acquire_current_shared_chunk_ref(&self, chunk_ptr: NonNull<SharedChunk<A>>) -> ChunkRef<A> {
self.local_shared_count.set(self.local_shared_count.get().wrapping_add(1));
unsafe { ChunkRef::<A>::adopt(chunk_ptr) }
}
#[inline]
fn reconcile_shared_surplus(&self) {
let local = self.local_shared_count.replace(0);
let Some(chunk) = self.current_shared.borrow().chunk_ptr() else {
debug_assert_eq!(local, 0, "local_shared_count must be 0 when no shared chunk installed");
return;
};
let refund = LARGE_SHARED_REF_SURPLUS - local;
unsafe { chunk.as_ref().refund_refs(refund as usize) };
}
}
impl<A: Allocator + Clone> Drop for Arena<A> {
fn drop(&mut self) {
self.reconcile_shared_surplus();
}
}
impl<A: Allocator + Clone> fmt::Debug for Arena<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Arena").finish_non_exhaustive()
}
}
pub(crate) trait ExpectAlloc<T> {
fn expect_alloc(self) -> T;
}
impl<T> ExpectAlloc<T> for Result<T, AllocError> {
#[inline]
#[track_caller]
#[cfg_attr(coverage_nightly, coverage(off))]
fn expect_alloc(self) -> T {
#[allow(clippy::panic, reason = "documented panic path of the panicking alloc API")]
#[allow(clippy::match_wild_err_arm, reason = "documented panic path of the panicking alloc API")]
match self {
Ok(v) => v,
Err(_) => panic!("multitude: allocator returned AllocError"),
}
}
}
macro_rules! panic_alloc {
() => {{
$crate::arena::ExpectAlloc::expect_alloc(::core::result::Result::<(), allocator_api2::alloc::AllocError>::Err(
allocator_api2::alloc::AllocError,
))
}};
}
pub(crate) use panic_alloc;
#[cold]
#[inline(never)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) fn expect_alloc<T>(r: Result<T, AllocError>) -> T {
(r).expect_alloc()
}