use core::ptr;
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering};
type BitWord = u32;
type AtomicBitWord = AtomicU32;
const WORD_BITS: usize = BitWord::BITS as usize;
use crate::os;
use crate::types::SEGMENT_SIZE;
const MAX_ARENAS: usize = 32;
const MAX_CHUNKS: usize = 1024;
pub struct Arena {
pub base: *mut u8,
pub size: usize,
pub chunks: usize,
chunks_live: AtomicUsize,
pub exclusive: bool,
pub owned: bool,
pub numa_node: i32,
used: [AtomicBitWord; MAX_CHUNKS / WORD_BITS],
dirty: [AtomicBitWord; MAX_CHUNKS / WORD_BITS],
}
static ARENAS: [AtomicPtr<Arena>; MAX_ARENAS] =
[const { AtomicPtr::new(ptr::null_mut()) }; MAX_ARENAS];
static ARENA_COUNT: AtomicUsize = AtomicUsize::new(0);
fn arena_register(
base: *mut u8,
size: usize,
exclusive: bool,
owned: bool,
numa_node: i32,
) -> Result<i32, ()> {
let chunks = size / SEGMENT_SIZE;
if chunks == 0 || chunks > MAX_CHUNKS || !base.addr().is_multiple_of(SEGMENT_SIZE) {
return Err(());
}
let desc = os::alloc_aligned(core::mem::size_of::<Arena>(), os::page_size(), true, false)
.map_err(|_| ())?;
let a: *mut Arena = desc.ptr.cast();
unsafe {
(*a).base = base;
(*a).size = size;
(*a).chunks = chunks;
(*a).chunks_live.store(chunks, Ordering::Release);
(*a).exclusive = exclusive;
(*a).owned = owned;
(*a).numa_node = numa_node;
}
let id = ARENA_COUNT.fetch_add(1, Ordering::AcqRel);
if id >= MAX_ARENAS {
ARENA_COUNT.fetch_sub(1, Ordering::AcqRel);
return Err(());
}
ARENAS[id].store(a, Ordering::Release);
Ok(id as i32)
}
#[allow(clippy::result_unit_err)] pub fn reserve_os_memory_ex(
size: usize,
_commit: bool,
allow_large: bool,
exclusive: bool,
) -> Result<i32, ()> {
if crate::FIXED_REGION {
return Err(());
}
let total = size.div_ceil(SEGMENT_SIZE) * SEGMENT_SIZE;
let b = os::alloc_aligned(total, SEGMENT_SIZE, true, allow_large).map_err(|_| ())?;
arena_register(b.ptr, total, exclusive, true, -1)
}
#[allow(clippy::result_unit_err)] #[allow(
clippy::fn_params_excessive_bools,
reason = "mirrors mi_manage_os_memory_ex's C signature 1:1; grouping the \
flags into a struct would break the ABI parity this crate exists for"
)]
pub fn manage_os_memory_ex(
start: *mut u8,
size: usize,
_is_committed: bool,
_is_large: bool,
_is_zero: bool,
numa_node: i32,
exclusive: bool,
) -> Result<i32, ()> {
if crate::FIXED_REGION {
return Err(());
}
let lo_addr = (start.addr() + SEGMENT_SIZE - 1) & !(SEGMENT_SIZE - 1);
let hi = (start.addr() + size) & !(SEGMENT_SIZE - 1);
if hi <= lo_addr {
return Err(());
}
let lo = start.with_addr(lo_addr);
let id = arena_register(lo, hi - lo_addr, exclusive, false, numa_node)?;
let a = ARENAS[id as usize].load(Ordering::Acquire);
unsafe {
let chunks = (*a).chunks_live.load(Ordering::Acquire);
for w in 0..chunks.div_ceil(WORD_BITS) {
let bits = if (w + 1) * WORD_BITS <= chunks {
BitWord::MAX
} else {
((1 as BitWord) << (chunks % WORD_BITS)) - 1
};
(*a).dirty[w].store(bits, Ordering::Relaxed);
}
}
Ok(id)
}
const DEFAULT_ARENA_PAYS: bool = !cfg!(all(target_arch = "wasm32", not(miri)));
fn reserve_default_arena_on_miss() -> bool {
static RESERVE: AtomicUsize = AtomicUsize::new(0);
if !DEFAULT_ARENA_PAYS {
return false;
}
let reserve = crate::options::get_size(23); if reserve < SEGMENT_SIZE {
return false;
}
match RESERVE.compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => {
let ok = reserve_os_memory_ex(reserve, true, false, false).is_ok();
RESERVE.store(if ok { 0 } else { 2 }, Ordering::Release);
ok
}
Err(1) => {
while RESERVE.load(Ordering::Acquire) == 1 {
core::hint::spin_loop();
}
RESERVE.load(Ordering::Acquire) == 0
}
Err(_) => false,
}
}
pub fn chunk_alloc(restrict_id: i32) -> Option<(*mut u8, bool)> {
if crate::FIXED_REGION {
return None;
}
if restrict_id < 0 && crate::options::is_enabled(27) {
return None; }
if let Some(r) = chunk_alloc_inner(restrict_id) {
return Some(r);
}
if restrict_id >= 0 || !reserve_default_arena_on_miss() {
return None;
}
chunk_alloc_inner(restrict_id)
}
#[allow(clippy::needless_range_loop)] fn chunk_alloc_inner(restrict_id: i32) -> Option<(*mut u8, bool)> {
let n = ARENA_COUNT.load(Ordering::Acquire).min(MAX_ARENAS);
for id in 0..n {
if restrict_id >= 0 && id != restrict_id as usize {
continue;
}
let a = ARENAS[id].load(Ordering::Acquire);
if a.is_null() {
continue;
}
unsafe {
if restrict_id < 0 && (*a).exclusive {
continue;
}
let chunks = (*a).chunks_live.load(Ordering::Acquire);
let words = chunks.div_ceil(WORD_BITS);
for w in 0..words {
loop {
let cur = (*a).used[w].load(Ordering::Acquire);
let limit = if (w + 1) * WORD_BITS <= chunks {
WORD_BITS
} else {
chunks % WORD_BITS
};
let free_bits = !cur
& if limit == WORD_BITS {
BitWord::MAX
} else {
((1 as BitWord) << limit) - 1
};
if free_bits == 0 {
break;
}
let bit = free_bits.trailing_zeros() as usize;
if (*a).used[w]
.compare_exchange_weak(
cur,
cur | (1 << bit),
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err()
{
continue;
}
let idx = w * WORD_BITS + bit;
let was_dirty =
(*a).dirty[w].fetch_or(1 << bit, Ordering::AcqRel) & (1 << bit) != 0;
let p = (*a).base.add(idx * SEGMENT_SIZE);
return Some((p, !was_dirty));
}
}
}
}
None
}
static MULTI_LOCK: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
#[allow(clippy::needless_range_loop)] pub fn chunk_alloc_n(restrict_id: i32, n: usize) -> Option<(*mut u8, bool)> {
if crate::FIXED_REGION {
return None;
}
if n == 1 {
return chunk_alloc(restrict_id);
}
if restrict_id < 0 && crate::options::is_enabled(27) {
return None;
}
if let Some(r) = chunk_alloc_n_inner(restrict_id, n) {
return Some(r);
}
if restrict_id >= 0 || !reserve_default_arena_on_miss() {
return None;
}
chunk_alloc_n_inner(restrict_id, n)
}
#[allow(clippy::needless_range_loop)] fn chunk_alloc_n_inner(restrict_id: i32, n: usize) -> Option<(*mut u8, bool)> {
while MULTI_LOCK
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
let result = (|| {
let count = ARENA_COUNT.load(Ordering::Acquire).min(MAX_ARENAS);
for id in 0..count {
if restrict_id >= 0 && id != restrict_id as usize {
continue;
}
let a = ARENAS[id].load(Ordering::Acquire);
if a.is_null() {
continue;
}
unsafe {
if restrict_id < 0 && (*a).exclusive {
continue;
}
let chunks = (*a).chunks_live.load(Ordering::Acquire);
if n > chunks {
continue;
}
let mut run = 0usize;
let mut idx = 0usize;
while idx < chunks {
let bit = (*a).used[idx / WORD_BITS].load(Ordering::Acquire)
& (1 << (idx % WORD_BITS));
run = if bit == 0 { run + 1 } else { 0 };
if run == n {
let start = idx + 1 - n;
let mut conflict = None;
for j in start..=idx {
let prev = (*a).used[j / WORD_BITS]
.fetch_or(1 << (j % WORD_BITS), Ordering::AcqRel);
if prev & (1 << (j % WORD_BITS)) != 0 {
conflict = Some(j);
break;
}
}
if let Some(c) = conflict {
for j in start..c {
(*a).used[j / WORD_BITS]
.fetch_and(!(1 << (j % WORD_BITS)), Ordering::AcqRel);
}
run = 0;
idx = c + 1;
continue;
}
let mut any_dirty = false;
for j in start..=idx {
any_dirty |= (*a).dirty[j / WORD_BITS]
.fetch_or(1 << (j % WORD_BITS), Ordering::AcqRel)
& (1 << (j % WORD_BITS))
!= 0;
}
let p = (*a).base.add(start * SEGMENT_SIZE);
return Some((p, !any_dirty));
}
idx += 1;
}
}
}
None
})();
MULTI_LOCK.store(false, Ordering::Release);
result
}
#[allow(clippy::needless_range_loop)] pub fn chunk_free_n(p: *mut u8, n: usize) -> bool {
if crate::FIXED_REGION {
return false; }
let addr = p.addr();
let count = ARENA_COUNT.load(Ordering::Acquire).min(MAX_ARENAS);
for id in 0..count {
let a = ARENAS[id].load(Ordering::Acquire);
if a.is_null() {
continue;
}
unsafe {
if addr >= (*a).base.addr()
&& addr < (*a).base.addr() + (*a).chunks_live.load(Ordering::Acquire) * SEGMENT_SIZE
{
let start = (addr - (*a).base.addr()) / SEGMENT_SIZE;
for j in start..start + n {
(*a).used[j / WORD_BITS].fetch_and(!(1 << (j % WORD_BITS)), Ordering::AcqRel);
}
return true;
}
}
}
false
}
#[allow(clippy::needless_range_loop)] pub fn chunk_free(p: *mut u8) -> bool {
if crate::FIXED_REGION {
return false;
}
let addr = p.addr();
let n = ARENA_COUNT.load(Ordering::Acquire).min(MAX_ARENAS);
for id in 0..n {
let a = ARENAS[id].load(Ordering::Acquire);
if a.is_null() {
continue;
}
unsafe {
if addr >= (*a).base.addr()
&& addr < (*a).base.addr() + (*a).chunks_live.load(Ordering::Acquire) * SEGMENT_SIZE
{
let idx = (addr - (*a).base.addr()) / SEGMENT_SIZE;
(*a).used[idx / WORD_BITS].fetch_and(!(1 << (idx % WORD_BITS)), Ordering::AcqRel);
return true;
}
}
}
false
}
#[allow(clippy::needless_range_loop)] pub(crate) fn adopt_os_block(ptr: *mut u8, size: usize) -> Option<i32> {
let addr = ptr.addr();
if size == 0 || !addr.is_multiple_of(SEGMENT_SIZE) || !size.is_multiple_of(SEGMENT_SIZE) {
return None;
}
let n = size / SEGMENT_SIZE;
while MULTI_LOCK
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
let id = (|| {
let count = ARENA_COUNT.load(Ordering::Acquire).min(MAX_ARENAS);
for id in 0..count {
let a = ARENAS[id].load(Ordering::Acquire);
if a.is_null() {
continue;
}
unsafe {
if (*a).exclusive || !(*a).owned {
continue;
}
let chunks = (*a).chunks_live.load(Ordering::Acquire);
if (*a).base.addr() + chunks * SEGMENT_SIZE != addr || chunks + n > MAX_CHUNKS {
continue;
}
for j in chunks..chunks + n {
(*a).dirty[j / WORD_BITS].fetch_or(1 << (j % WORD_BITS), Ordering::AcqRel);
(*a).used[j / WORD_BITS].fetch_and(!(1 << (j % WORD_BITS)), Ordering::AcqRel);
}
(*a).chunks_live.store(chunks + n, Ordering::Release);
return Some(id as i32);
}
}
None
})();
MULTI_LOCK.store(false, Ordering::Release);
if id.is_some() {
return id;
}
let id = arena_register(ptr, size, false, true, -1).ok()?;
let a = ARENAS[id as usize].load(Ordering::Acquire);
unsafe {
for j in 0..n {
(*a).dirty[j / WORD_BITS].fetch_or(1 << (j % WORD_BITS), Ordering::AcqRel);
}
}
Some(id)
}
pub fn arena_area(id: i32) -> (*mut u8, usize) {
if crate::FIXED_REGION || id < 0 || id as usize >= ARENA_COUNT.load(Ordering::Acquire) {
return (ptr::null_mut(), 0);
}
let a = ARENAS[id as usize].load(Ordering::Acquire);
if a.is_null() {
return (ptr::null_mut(), 0);
}
unsafe {
(
(*a).base,
(*a).chunks_live.load(Ordering::Acquire) * SEGMENT_SIZE,
)
}
}
#[cfg(feature = "std")]
#[allow(clippy::needless_range_loop)] pub fn arenas_print(out: &mut dyn FnMut(&str)) {
let n = ARENA_COUNT.load(Ordering::Acquire).min(MAX_ARENAS);
if n == 0 {
out("arenas: none\n");
return;
}
for id in 0..n {
let a = ARENAS[id].load(Ordering::Acquire);
if a.is_null() {
continue;
}
unsafe {
let chunks = (*a).chunks_live.load(Ordering::Acquire);
let mut used = 0usize;
for w in 0..chunks.div_ceil(WORD_BITS) {
used += (*a).used[w].load(Ordering::Relaxed).count_ones() as usize;
}
let mut line = heapless_fmt(
id,
(*a).base.addr(),
chunks * SEGMENT_SIZE,
used,
chunks,
(*a).exclusive,
);
out(line.as_str());
line.clear();
}
}
}
#[cfg(feature = "std")]
fn heapless_fmt(
id: usize,
base: usize,
size: usize,
used: usize,
chunks: usize,
excl: bool,
) -> std::string::String {
std::format!(
"arena {id}: base {base:#x} size {} MiB, {used}/{chunks} chunks used{}\n",
size / (1024 * 1024),
if excl { " (exclusive)" } else { "" }
)
}
#[cfg(test)]
mod adopt_tests {
use super::*;
fn lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn drain(id: i32) -> Vec<(*mut u8, bool)> {
let mut held = Vec::new();
while let Some(got) = chunk_alloc(id) {
held.push(got);
assert!(held.len() <= MAX_CHUNKS, "drain exceeded arena capacity");
}
held
}
fn free_all(held: &[(*mut u8, bool)]) {
for &(p, _) in held {
assert!(chunk_free(p), "drained chunk did not free back");
}
}
#[test]
fn adopted_block_recycles_through_chunk_alloc() {
let _g = lock();
let b = os::alloc_aligned(2 * SEGMENT_SIZE, SEGMENT_SIZE, true, false)
.expect("64 MiB test reservation");
let id = adopt_os_block(b.ptr, 2 * SEGMENT_SIZE).expect("adoptable block");
let ours = [b.ptr.addr(), b.ptr.addr() + SEGMENT_SIZE];
for round in 0..3 {
let held = drain(id);
let mut got: Vec<usize> = held
.iter()
.map(|&(p, _)| p.addr())
.filter(|a| ours.contains(a))
.collect();
got.sort_unstable();
assert_eq!(got, ours, "round {round}: adopted chunks not recycled");
for &(p, zero) in &held {
if ours.contains(&p.addr()) {
assert!(!zero, "adopted chunk claimed to be zero");
}
}
free_all(&held);
}
}
#[test]
fn adjacent_adoption_extends_in_place() {
let _g = lock();
let b = os::alloc_aligned(3 * SEGMENT_SIZE, SEGMENT_SIZE, true, false)
.expect("96 MiB test reservation");
let first = adopt_os_block(b.ptr, SEGMENT_SIZE).expect("first block");
let mid = unsafe { b.ptr.add(SEGMENT_SIZE) };
let second = adopt_os_block(mid, 2 * SEGMENT_SIZE).expect("adjacent block");
assert_eq!(
first, second,
"adjacent block registered a new arena instead of extending"
);
let (base, sz) = arena_area(first);
assert!(
base.addr() <= b.ptr.addr() && base.addr() + sz >= b.ptr.addr() + 3 * SEGMENT_SIZE,
"extension did not publish the enlarged area"
);
let ours = [
b.ptr.addr(),
b.ptr.addr() + SEGMENT_SIZE,
b.ptr.addr() + 2 * SEGMENT_SIZE,
];
let held = drain(first);
let mut got: Vec<usize> = held
.iter()
.map(|&(p, _)| p.addr())
.filter(|a| ours.contains(a))
.collect();
got.sort_unstable();
assert_eq!(got, ours, "extended arena did not serve all three chunks");
for &(p, zero) in &held {
if ours.contains(&p.addr()) {
assert!(!zero, "extended chunk claimed to be zero");
}
}
free_all(&held);
}
#[test]
fn ragged_blocks_are_refused() {
let _g = lock();
let b = os::alloc_aligned(SEGMENT_SIZE, SEGMENT_SIZE, true, false)
.expect("32 MiB test reservation");
unsafe {
let misaligned = b.ptr.add(os::page_size());
assert!(adopt_os_block(misaligned, SEGMENT_SIZE - os::page_size()).is_none());
assert!(adopt_os_block(b.ptr, SEGMENT_SIZE / 2).is_none());
assert!(adopt_os_block(b.ptr, 0).is_none());
}
let freed = os::OsBlock {
ptr: b.ptr,
size: b.size,
is_large: false,
is_zero: false,
};
unsafe { os::free(freed).expect("native free") };
}
}