Arena allocator for building bytes::Bytes without copying the final payload.
Write into a Buffer, call freeze(), and hand off Bytes backed by arena memory. The slot or block returns to the arena when the last clone or slice drops.
FixedArena is the recommended high-throughput path when one slot size can cover the workload: uniform slots, a bitmap claim/release path, and predictable allocation cost.
BuddyArena is the variable-size allocator. Use it when request sizes swing enough that one fixed slot size would waste memory or spill too often. It rounds requests up to powers of two, splits larger blocks on demand, and coalesces neighbors on release.
use core::num::NonZeroUsize;
use arena_alligator::FixedArena;
use bytes::BufMut;
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1024).unwrap(),
NonZeroUsize::new(4096).unwrap(),
).build()?;
let mut buf = arena.allocate()?;
buf.put_slice(b"request payload");
let _bytes = buf.freeze();
# Ok::<(), Box<dyn core::error::Error>>(())This crate is #![no_std] and depends only on alloc. It works on targets
with a global allocator and pointer-width atomics, including embedded systems
using embedded-alloc or similar.
# Embedded / no_std: disable default features
[dependencies]
arena-alligator = { version = "0.6", default-features = false }| Feature | Default | What it enables |
|---|---|---|
std |
yes | Standard-library integrations and dependency features; required by async-alloc |
libc |
yes | Page size detection via sysconf on Unix |
async-alloc |
no | allocate_async() via tokio (implies std) |
hazmat-raw-access |
no | Raw pointer access to arena memory |
| Arena | Start here when | Why |
|---|---|---|
FixedArena |
Most allocations fit one chosen slot size, or a small set of predictable slot sizes across separate arenas | Fastest path, simplest capacity planning, lowest allocator overhead |
BuddyArena |
Request sizes vary enough that fixed slots would waste memory or spill too often | One shared region, power-of-two block reuse, split/coalesce behavior for variable-size workloads |
Use FixedArena by default. Use BuddyArena only when variable-size allocation is a hard requirement.
use core::num::NonZeroUsize;
use arena_alligator::{BuddyArena, BuddyGeometry};
use bytes::BufMut;
let arena = BuddyArena::builder(
BuddyGeometry::exact(
NonZeroUsize::new(64 * 1024 * 1024).unwrap(),
NonZeroUsize::new(256).unwrap(),
)?,
)
.build()?;
let mut buf = arena.allocate(NonZeroUsize::new(8192).unwrap())?;
buf.put_slice(&vec![0u8; 8192]);
let _bytes = buf.freeze();
# Ok::<(), Box<dyn core::error::Error>>(())BuddyGeometry::exact(...) validates a geometry that is already chosen. Invalid inputs fail immediately.
BuddyGeometry::nearest(...) snaps the requested total size and minimum block size up to the nearest valid buddy geometry. Use it when the target shape is approximate and automatic adjustment is acceptable.
By default, writing past capacity panics. That follows the contract of a fixed-size BufMut: once capacity is exhausted, additional writes are an error unless a different behavior is selected explicitly.
With auto_spill(), the buffer copies its current contents to heap-backed storage, releases the arena allocation immediately, and keeps writing on the heap. freeze() still returns Bytes.
# use core::num::NonZeroUsize;
# use arena_alligator::FixedArena;
# use bytes::BufMut;
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1024).unwrap(),
NonZeroUsize::new(1024).unwrap(),
).auto_spill().build()?;
let mut buf = arena.allocate()?;
buf.put_slice(&[0u8; 2048]);
assert!(buf.is_spilled());
let _bytes = buf.freeze();
# Ok::<(), Box<dyn core::error::Error>>(())By default, arena allocations use InitPolicy::Uninit. That follows Rust's usual high-performance model for writable capacity: newly allocated bytes are not zeroed, and only written bytes become part of the frozen Bytes.
For security-sensitive workloads, InitPolicy::Zero zeroes memory on return to the arena and on first allocation. Returned slots and blocks are scrubbed before being marked free, preventing data leaks between callers. All zeroing uses the zeroize crate (compiler-guaranteed not elided).
# use core::num::NonZeroUsize;
# use arena_alligator::{FixedArena, InitPolicy};
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(1024).unwrap(),
NonZeroUsize::new(4096).unwrap(),
)
.init_policy(InitPolicy::Zero)
.build()?;
# Ok::<(), Box<dyn core::error::Error>>(())Use from_raw() when the backing region already exists and the arena should
build on top of it instead of allocating its own memory. This is the path for
shared memory, mmap'd regions, static buffers, and other externally-provisioned
storage.
For &'static mut buffers, prefer the safe from_static() wrapper. Keep
from_raw() for pointer/length regions and custom deallocation strategies.
The unsafe boundary is at construction time: the caller must provide a valid,
exclusive region and the correct deallocation strategy. After construction, the
arena uses the same safe allocate() / freeze() flow as the ordinary builder
path.
SlotSpec derives fixed-slot geometry from a caller-provided region.
BuddyHint derives buddy geometry from the same kind of region.
Use NoDealloc when the caller retains responsibility for freeing the backing
memory.
# use core::num::NonZeroUsize;
# use arena_alligator::{FixedArena, SlotSpec};
static mut BLOCK: [u8; 4096] = [0; 4096];
fn nz(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).unwrap()
}
#[allow(static_mut_refs)]
let arena = FixedArena::from_static(unsafe { &mut BLOCK }, SlotSpec::Count(nz(4)))
.build()?;
assert_eq!(arena.slot_count(), 4);
# Ok::<(), Box<dyn core::error::Error>>(())For protocols or layouts that need direct pointer access, enable the
hazmat-raw-access feature and opt in per arena builder with
.hazmat_raw_access().
RawRegion bypasses Buffer and BufMut. You write through raw pointers or
MaybeUninit<u8> slices, then call unsafe freeze(range) when the frozen range
is fully initialized.
Returned Bytes are ordinary bytes::Bytes: clones and slices keep the arena
backing allocation pinned until every reference drops. Keep this off the default
path unless you need the extra control.
[dependencies]
arena-alligator = { version = "0.6", features = ["hazmat-raw-access"] }# use core::num::NonZeroUsize;
# use arena_alligator::FixedArena;
fn nz(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).unwrap()
}
let arena = FixedArena::with_slot_capacity(nz(4), nz(128))
.hazmat_raw_access()
.build()?;
let mut raw = arena.raw_alloc()?;
let ptr = raw.as_mut_ptr();
unsafe {
let len = 5u32.to_le_bytes();
core::ptr::copy_nonoverlapping(len.as_ptr(), ptr, 4);
core::ptr::copy_nonoverlapping(b"hello".as_ptr(), ptr.add(4), 5);
}
let payload = unsafe { raw.freeze(4..9) }?;
assert_eq!(&payload[..], b"hello");
# Ok::<(), Box<dyn core::error::Error>>(())See docs/hazmat.md for the sharp edges and the visibility
rules for buddy raw allocations.
With the async-alloc feature, both arena types support allocate_async(). The task waits until capacity becomes available instead of busy-looping or falling back to the heap. AsyncBuddyArena::allocate_async() returns a Result: it fails fast with AllocError::RequestTooLarge for a request larger than the arena could ever satisfy, instead of waiting forever.
[dependencies]
arena-alligator = { version = "0.6", features = ["async-alloc"] }let arena = Arc::new(
FixedArena::with_slot_capacity(
NonZeroUsize::new(2).unwrap(),
NonZeroUsize::new(256).unwrap(),
)
.build_async()
.unwrap(),
);
let buf = arena.allocate_async().await;metrics() snapshots allocator state. Fixed reports allocation, failure, spill, and live-capacity counters. Buddy adds split, coalesce, and largest-free-block data so fragmentation pressure is visible directly.
In load tests, watch spill_count and largest_free_block over time to catch pressure and fragmentation early.
BytesExt::into_owned() is the explicit handoff from arena-backed frozen bytes to owned mutable heap storage. Unlike auto_spill(), which changes storage implicitly on write overflow, into_owned() makes the copy at the point where the caller chooses to leave arena-backed storage:
use core::num::NonZeroUsize;
use arena_alligator::{FixedArena, BytesExt};
use bytes::BufMut;
let arena = FixedArena::with_slot_capacity(
NonZeroUsize::new(4).unwrap(),
NonZeroUsize::new(64).unwrap(),
).build()?;
let mut buf = arena.allocate()?;
buf.put_slice(b"hello");
let frozen = buf.freeze();
let mut owned = frozen.into_owned();
// Arena slot is freed; owned is heap-backed and mutable
owned.put_slice(b" world");
assert_eq!(&owned[..], b"hello world");
# Ok::<(), Box<dyn core::error::Error>>(())Start with fixed_buffer, then run buddy_buffer for the variable-size path.
| Example | What it shows |
|---|---|
fixed_buffer |
Allocate, write, freeze, and send across threads |
buddy_buffer |
Variable-size allocations with split and coalesce |
spill_buffer |
Auto-spill to heap when a buffer outgrows slot capacity |
hazmat_fixed_raw |
Raw fixed-slot access with header/payload freeze |
hazmat_buddy_raw |
Raw buddy access with visible-capacity behavior |
async_alloc |
Wait for capacity with allocate_async() |
treiber_waker |
Custom Waiter impl using a lock-free Treiber stack |
mise run examplesBenchmark summary tables and local Criterion HTML report links are in
docs/benchmarks.md.
That page includes both the Apple M4 Max baseline and a real-hardware k8s run summary.
Run benchmarks with:
mise run bench
mise run bench:extremebench:extreme enables an additional high-thread contention point (40 threads by default).
Override via ARENA_BENCH_EXTREME_THREADS=<n>.
This repository uses mise as its task runner. Install it from the official guide: https://mise.jdx.dev/installing-mise.html.
Common commands in this repository:
mise run test
mise run format:fix
mise run clippy
mise run examples
mise run benchList all available tasks with:
mise tasks --allThe crate is exercised under standard tests, doctests, examples, and targeted concurrency validation:
mirichecks unsafe code paths for undefined behavior regressions.loommodels the atomic bitmap claim/release paths under many thread interleavings. The async waiter machinery is validated with targeted concurrency and cancellation tests, not loom.- CI also runs formatting, clippy, docs, examples, and MSRV coverage.
- NUMA-aware deployment pattern: per-node arenas, thread pinning, and bounded cross-node fallback.
Release notes are in CHANGELOG.md.
As of 0.6.0, the API is stabilized. Any future API changes will ship with adapters rather than break the contract directly.
See CONTRIBUTING.md for development workflow and PR expectations.
MIT