blob: daccbc8551353b6ddb11147a6e122cee7454e58e [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! An RCU-protected, lock-free integer ID radix tree (IDR).
//!
//! Maps 32-bit integer IDs to objects (`Arc<T>`), optimized for workloads with heavily
//! contended concurrent reads and serialized mutations (such as PID tables, task registries,
//! and descriptor tables).
//!
//! # Concurrency Model
//!
//! - **Readers (`lookup`, `iter`)**: Completely lock-free and wait-free. Traversal operates
//! under an [`RcuReadScope`], ensuring memory reclamation safety without blocking or
//! interfering with writers.
//! - **Writers (`alloc`, `reserve_id`, `remove`)**: Mutations are serialized
//! via an [`IdrGuard`] acquired with [`Idr::lock`], while atomic pointer updates and memory
//! barriers allow concurrent readers to proceed in parallel without interruption.
//!
//! # Key Operations
//!
//! - [`Idr::lock`]: Acquires the writer lock, returning an [`IdrGuard`] for mutating operations.
//! - [`Idr::lookup`]: Retrieves the object for a given ID without locking.
//! - [`Idr::iter`]: Iterates over all active `(u32, &Arc<T>)` entries under an RCU scope.
//! - [`Idr::max`]: Returns the maximal value allowed for allocation in this `Idr`.
//! - [`Idr::set_max`]: Sets the maximal value allowed for allocation in this `Idr`.
//! - [`IdrGuard::alloc`]: Allocates an ID using the configured allocation policy (linear from 0
//! or cyclic from cursor).
//! - [`IdrGuard::reserve_id`]: Marks a specific ID as occupied without inserting an element.
//! - [`IdrGuard::remove`]: Removes an item and restores slot availability.
//!
//! # Structural Architecture
//!
//! The tree is a 64-ary radix tree (consuming 6 bits per layer, up to 6 layers for 32-bit IDs):
//! - Intermediate nodes (`layer > 0`) route down to child nodes.
//! - Leaf nodes (`layer == 0`) hold concrete `Arc<T>` entries.
//! - Each node maintains an atomic `free_bitmap` tracking capacity across its 64 sub-slots,
//! enabling $O(1)$ child selection and efficient subtree skipping during allocation.
//! - The tree dynamically grows upwards in layers as ID requirements expand.
use fuchsia_rcu::{RcuDroppable, RcuDroppableArc, RcuOptionBox};
use smallvec::SmallVec;
use starnix_rcu::RcuReadScope;
use starnix_sync::{LockDepGuard, LockDepMutex, LockLevel};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
/// The number of bits consumed per layer of the tree structure.
/// Chosen as 6 because 2^6 = 64, mapping perfectly to a 64-bit word (`u64`)
/// used for atomic lock-free bitmaps per node.
const BITS_PER_LEVEL: u32 = 6;
/// The maximum number of children per node, directly derived from the bits per level.
const NODE_CAPACITY: usize = 1 << BITS_PER_LEVEL;
/// The bitmask used to extract the current layer's routing portion from a target ID.
const LEVEL_MASK: u32 = (1 << BITS_PER_LEVEL) - 1;
/// The theoretical maximum depth required to completely map a 32-bit integer space.
const MAX_DEPTH: u32 = (32 + BITS_PER_LEVEL - 1) / BITS_PER_LEVEL;
/// The allocation mode used by an [`Idr`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IdrAllocMode {
/// Allocates the lowest available ID starting from 0.
#[default]
Linear,
/// Allocates IDs sequentially starting from the previous cursor position,
/// wrapping around when hitting upper bounds.
Cyclic {
/// The minimum ID to allocate after wrapping around (or 0 if `None`).
min_after_wrap: Option<u32>,
},
}
/// A concurrent, lock-free radix tree mapped structure primarily employed to map 32-bit
/// IDs to objects. Optimized for massively contended reads.
pub struct Idr<T: RcuDroppable + Send + Sync + 'static, L: LockLevel> {
/// Serializes all mutating writes (allocations and removals) modifying the
/// tree structure. The protected `u32` value tracks the starting cursor for
/// cyclic allocations.
writer_lock: LockDepMutex<u32, L>,
/// The top-level entry point descending into the tree. Atomically replaced
/// whenever the structure grows upwards.
root: RcuDroppableArc<IdrNode<T>>,
/// The allocation mode determining whether IDs are allocated linearly or cyclically.
alloc_mode: IdrAllocMode,
/// The maximal value allowed for allocation in this `Idr`.
max: AtomicU32,
}
impl<T: RcuDroppable + Send + Sync + 'static, L: LockLevel> Default for Idr<T, L> {
fn default() -> Self {
Self::new(IdrAllocMode::default())
}
}
impl<T: RcuDroppable + Send + Sync + 'static, L: LockLevel> Idr<T, L> {
/// Creates a new `Idr` radix tree with the specified allocation mode.
pub fn new(alloc_mode: IdrAllocMode) -> Self {
Self {
writer_lock: LockDepMutex::new(0),
root: RcuDroppableArc::new(Arc::new(IdrNode::new(0))),
alloc_mode,
max: AtomicU32::new(u32::MAX),
}
}
/// Creates a new cyclic `Idr` radix tree with an optional minimum ID after wrapping.
pub fn new_cyclic(min_after_wrap: Option<u32>) -> Self {
Self::new(IdrAllocMode::Cyclic { min_after_wrap })
}
/// Returns the maximal value allowed for allocation in this `Idr`.
pub fn max(&self) -> u32 {
self.max.load(Ordering::Relaxed)
}
/// Sets the maximal value allowed for allocation in this `Idr`.
pub fn set_max(&self, max: u32) {
self.max.store(max, Ordering::Relaxed);
}
/// Acquires the writer lock, returning an [`IdrGuard`] that provides mutating operations.
pub fn lock(&self) -> IdrGuard<'_, T, L> {
IdrGuard { idr: self, cursor: self.writer_lock.lock() }
}
/// RCU protected lock-free lookup for readers
pub fn lookup(&self, id: u32, scope: &RcuReadScope) -> Option<Arc<T>> {
let mut current_node = self.root.as_ref(scope);
let capacity = current_node.capacity();
if (id as u64) >= capacity {
return None;
}
loop {
let index = current_node.index_for_id(id);
let entry = current_node.children[index].as_ref(scope);
match entry {
Some(IdrEntry::Leaf(arc)) => return Some(arc.clone()),
Some(IdrEntry::Node(next)) => {
current_node = &**next;
}
None => return None,
}
}
}
/// Returns a lock-free RCU iterator over all entries
pub fn iter<'a>(&'a self, scope: &'a RcuReadScope) -> IdrIterator<'a, T> {
let mut stack = SmallVec::new();
let root = self.root.as_ref(scope);
stack.push((root, 0, 0));
IdrIterator { scope, stack }
}
/// Iteratively searches for the lowest available free ID >= `start_id` and <= `max_id`.
/// Populates `path` with the `(node, child_index)` sequence from root to leaf.
fn find_free_slot<'a>(
&self,
start_id: u32,
max_id: u32,
path: &mut SmallVec<[(&'a IdrNode<T>, usize); MAX_DEPTH as usize]>,
scope: &'a RcuReadScope,
) -> Option<u32> {
if start_id > max_id {
return None;
}
// Traversal stack storing the path from root and unexplored sibling candidates per
// layer. Enables iterative backtracking without recursion or heap allocations.
struct StackEntry<'a, T: RcuDroppable + Send + Sync + 'static> {
node: &'a IdrNode<T>,
// Unexplored candidate slots at this level with open capacity.
free_bits: u64,
// True if slot selection at this level is restricted to indices >= start_id.
min_constrained: bool,
// True if slot selection at this level is restricted to indices <= max_id.
max_constrained: bool,
// The child slot index selected when descending to the next layer.
chosen_index: usize,
}
let root = self.root.as_ref(scope);
let mut stack = SmallVec::<[StackEntry<'a, T>; MAX_DEPTH as usize]>::new();
// When constrained by start_id, mask out slots below the cursor index.
let mut initial_free_bits = root.free_bitmap.load(Ordering::Relaxed);
let min_constrained = start_id > 0;
if min_constrained {
let cursor_index = root.index_for_id(start_id);
initial_free_bits &= !((1u64 << cursor_index) - 1);
}
// When constrained by max_id, mask out slots above the max index.
let max_constrained = (max_id as u64) < root.capacity() - 1;
if max_constrained {
let max_index = root.index_for_id(max_id);
let max_mask = if max_index >= 63 { !0 } else { (1u64 << (max_index + 1)) - 1 };
initial_free_bits &= max_mask;
}
// No candidate slots in [start_id, max_id] at the root level.
if initial_free_bits == 0 {
return None;
}
stack.push(StackEntry {
node: root,
free_bits: initial_free_bits,
min_constrained,
max_constrained,
chosen_index: 0,
});
while let Some(top) = stack.last_mut() {
// Backtrack to the parent layer when all candidate slots in this node are exhausted.
if top.free_bits == 0 {
stack.pop();
continue;
}
let index = top.free_bits.trailing_zeros() as usize;
top.free_bits &= !(1u64 << index);
top.chosen_index = index;
let node = top.node;
if node.layer == 0 {
let id = stack
.iter()
.fold(0u32, |acc, entry| acc | entry.node.id_for_index(entry.chosen_index));
path.extend(stack.into_iter().map(|entry| (entry.node, entry.chosen_index)));
return Some(id);
}
// Deeper layers stay constrained only if descending into the exact boundary slot.
let is_min_constrained = top.min_constrained && (index == node.index_for_id(start_id));
let is_max_constrained = top.max_constrained && (index == node.index_for_id(max_id));
let child = node.get_or_create_child(index, scope);
let mut child_free_bits = child.free_bitmap.load(Ordering::Relaxed);
if is_min_constrained {
let child_cursor = child.index_for_id(start_id);
child_free_bits &= !((1u64 << child_cursor) - 1);
}
if is_max_constrained {
let child_max = child.index_for_id(max_id);
let max_mask = if child_max >= 63 { !0 } else { (1u64 << (child_max + 1)) - 1 };
child_free_bits &= max_mask;
}
// Skip child subtree if constraints left no open slots.
if child_free_bits == 0 {
continue;
}
stack.push(StackEntry {
node: child,
free_bits: child_free_bits,
min_constrained: is_min_constrained,
max_constrained: is_max_constrained,
chosen_index: 0,
});
}
None
}
/// When a node transitions to having 0 free bits, mark it full in its parent
/// The caller MUST hold the `writer_lock`.
fn propagate_fullness(&self, path: &[(&IdrNode<T>, usize)]) {
for i in (0..path.len() - 1).rev() {
let (parent, parent_index) = &path[i];
if !parent.mark_allocated(*parent_index) {
// Parent still has other free slots. Stop propagating.
break;
}
}
}
/// When a node transitions from having 0 free bits to > 0, mark it free in its parent
/// The caller MUST hold the `writer_lock`.
fn propagate_availability(&self, path: &[(&IdrNode<T>, usize)]) {
for i in (0..path.len() - 1).rev() {
let (parent, parent_index) = &path[i];
if !parent.mark_freed(*parent_index) {
// Parent already had other free slots. Stop propagating.
break;
}
}
}
/// Grows the tree by adding a new layer on top of the root.
/// The caller MUST hold the `writer_lock`.
fn grow_tree_by_one_layer(&self, root_arc: &Arc<IdrNode<T>>) -> Option<Arc<IdrNode<T>>> {
let next_layer = root_arc.layer + 1;
if next_layer >= MAX_DEPTH {
return None;
}
let new_root = Arc::new(IdrNode::new(next_layer));
if root_arc.free_bitmap.load(Ordering::Relaxed) == 0 {
new_root.free_bitmap.fetch_and(!1, Ordering::Relaxed);
}
new_root.mark_present(0);
new_root.children[0].update(Some(IdrEntry::Node(root_arc.clone())));
self.root.update(new_root.clone());
Some(new_root)
}
}
/// An RAII guard representing exclusive writer access to an [`Idr`].
///
/// Holding this guard serializes mutations (allocations, reservations, removals)
/// to the radix tree while allowing concurrent lock-free reads.
pub struct IdrGuard<'a, T: RcuDroppable + Send + Sync + 'static, L: LockLevel> {
idr: &'a Idr<T, L>,
cursor: LockDepGuard<'a, u32>,
}
impl<'a, T: RcuDroppable + Send + Sync + 'static, L: LockLevel> std::ops::Deref
for IdrGuard<'a, T, L>
{
type Target = Idr<T, L>;
fn deref(&self) -> &Self::Target {
self.idr
}
}
impl<'a, T: RcuDroppable + Send + Sync + 'static, L: LockLevel> IdrGuard<'a, T, L> {
/// Allocates the next available ID by calling a factory providing the newly
/// acquired ID.
///
/// Depending on the `Idr` configuration, allocation will be either linear starting
/// from 0, or cyclic starting from the previous cursor position.
pub fn alloc<F>(&mut self, factory: F) -> Option<(u32, Arc<T>)>
where
F: FnOnce(u32) -> Arc<T>,
{
let max_id = self.idr.max.load(Ordering::Relaxed);
let (is_cyclic, wrap_min) = match self.idr.alloc_mode {
IdrAllocMode::Linear => (false, 0),
IdrAllocMode::Cyclic { min_after_wrap } => (true, min_after_wrap.unwrap_or(0)),
};
let (start_id, wrapped) = if is_cyclic {
if *self.cursor > max_id { (wrap_min, true) } else { (*self.cursor, false) }
} else {
(0, false)
};
if start_id > max_id {
return None;
}
let mut root_arc = self.idr.root.to_arc();
// Ensure the tree is large enough:
// 1. If the root free bitmap is 0, the entire current tree capacity is exhausted,
// requiring a new root layer on top if capacity <= max_id.
// 2. If `start_id` exceeds current tree capacity (e.g. after wrapping or initial placement),
// grow the tree until capacity covers `start_id` or until MAX_DEPTH is reached.
while (start_id as u64) >= root_arc.capacity()
|| (root_arc.free_bitmap.load(Ordering::Relaxed) == 0
&& root_arc.capacity() <= (max_id as u64))
{
if let Some(new_root) = self.idr.grow_tree_by_one_layer(&root_arc) {
root_arc = new_root;
} else {
// Tree reached maximum allowable depth and cannot grow further.
return None;
}
}
let scope = RcuReadScope::new();
let mut path = SmallVec::<[(&IdrNode<T>, usize); MAX_DEPTH as usize]>::new();
// First attempt: search for a free slot >= `start_id` and <= `max_id`.
let id_opt = self.idr.find_free_slot(start_id, max_id, &mut path, &scope).or_else(|| {
// If cyclic allocation failed to find a slot between `start_id`
// and `max_id`, wrap around to search from `wrap_min` up to `max_id`.
if is_cyclic && !wrapped && start_id > wrap_min && wrap_min <= max_id {
path.clear();
self.idr.find_free_slot(wrap_min, max_id, &mut path, &scope)
} else {
None
}
});
let id = id_opt?;
let (leaf_node, leaf_index) = path.last().expect("path should not be empty");
let leaf_index = *leaf_index;
let item = factory(id);
// Install the newly constructed leaf item at the target slot.
leaf_node.children[leaf_index].update(Some(IdrEntry::Leaf(item.clone())));
leaf_node.mark_present(leaf_index);
// Mark the leaf slot as allocated in its free bitmap. If this clears the final free bit
// in the leaf node, propagate the full state up the ancestor chain via `propagate_fullness`.
if leaf_node.mark_allocated(leaf_index) {
self.idr.propagate_fullness(&path);
}
// For cyclic allocations, advance the cursor to `id + 1`, wrapping to `wrap_min` when
// exceeding `max_id` or on u32 overflow.
if is_cyclic {
let next_cursor = id.wrapping_add(1);
*self.cursor = if next_cursor > max_id || (next_cursor == 0 && wrap_min > 0) {
wrap_min
} else {
next_cursor
};
}
Some((id, item))
}
/// Marks a specific ID as unavailable so the allocator will never return it.
/// Does not populate the tree with an item.
pub fn reserve_id(&mut self, id: u32) {
let mut root_arc = self.idr.root.to_arc();
loop {
let capacity = root_arc.capacity();
if (id as u64) < capacity {
break;
}
if let Some(new_root) = self.idr.grow_tree_by_one_layer(&root_arc) {
root_arc = new_root;
} else {
return; // Exceeded max depth
}
}
let scope = RcuReadScope::new();
let mut current_node = root_arc.as_ref();
let mut path = SmallVec::<[(&IdrNode<T>, usize); MAX_DEPTH as usize]>::new();
loop {
let index = current_node.index_for_id(id);
path.push((current_node, index));
// Reached a leaf node. Claim the slot.
if current_node.layer == 0 {
// If it was previously free, and this clears the last free bit, propagate fullness
if current_node.mark_allocated(index) {
self.idr.propagate_fullness(&path);
}
return;
}
// Descend to the next layer.
current_node = current_node.get_or_create_child(index, &scope);
}
}
/// Removes an item by ID.
pub fn remove(&mut self, id: u32) {
let scope = RcuReadScope::new();
let mut current_node = self.idr.root.as_ref(&scope);
if (id as u64) >= current_node.capacity() {
return;
}
let mut path = SmallVec::<[(&IdrNode<T>, usize); MAX_DEPTH as usize]>::new();
loop {
let index = current_node.index_for_id(id);
path.push((current_node, index));
let Some(child) = current_node.children[index].as_ref(&scope) else {
// Nothing to remove
return;
};
if current_node.layer == 0 {
current_node.children[index].update(None);
current_node.mark_absent(index);
if current_node.mark_freed(index) {
self.idr.propagate_availability(&path);
}
return;
} else {
current_node = match child {
IdrEntry::Node(n) => &**n,
_ => unreachable!("Tree corruption: expected a Node entry here"),
};
}
}
}
/// Returns the current cursor position for cyclic allocations.
#[cfg(test)]
fn cursor(&self) -> u32 {
*self.cursor
}
/// Sets the cursor position for cyclic allocations.
#[cfg(test)]
fn set_cursor(&mut self, cursor: u32) {
*self.cursor = cursor;
}
}
/// A lock-free iterator traversing the allocated elements inside the radix tree
/// under an automated RCU read scope, operating without acquiring any thread locks.
pub struct IdrIterator<'a, T: RcuDroppable + Send + Sync + 'static> {
/// The ambient read scope keeping the traversed tree nodes pinned in memory.
scope: &'a RcuReadScope,
/// The traversal stack keeping track of the current path, the next index, and
/// the accumulated ID prefix.
stack: SmallVec<[(&'a IdrNode<T>, usize, u32); MAX_DEPTH as usize]>,
}
impl<'a, T: RcuDroppable + Send + Sync + 'static> Iterator for IdrIterator<'a, T> {
type Item = (u32, &'a Arc<T>);
fn next(&mut self) -> Option<Self::Item> {
while let Some((node, index, id_base)) = self.stack.pop() {
let presence = node.presence_bitmap.load(Ordering::Relaxed);
let mask = if index >= NODE_CAPACITY { 0 } else { !((1u64 << index) - 1) };
let remaining = presence & mask;
if remaining == 0 {
continue;
}
let next_bit = remaining.trailing_zeros() as usize;
self.stack.push((node, next_bit + 1, id_base));
let entry_opt = node.children[next_bit].as_ref(self.scope);
if let Some(entry) = entry_opt {
let child_id = id_base | node.id_for_index(next_bit);
match entry {
IdrEntry::Node(child_arc) => {
self.stack.push((child_arc.as_ref(), 0, child_id));
}
IdrEntry::Leaf(arc) => {
return Some((child_id, arc));
}
}
}
}
None
}
}
/// Represents a single slot within an `IdrNode`'s capability array.
#[derive(Debug)]
enum IdrEntry<T: RcuDroppable + Send + Sync + 'static> {
/// An intermediate branch pointing to the next layer down the tree structure.
Node(Arc<IdrNode<T>>),
/// A concrete element residing at the bottom layer.
Leaf(Arc<T>),
}
// SAFETY: All variants contain only types that are `RcuDroppable` (`Arc<IdrNode<T>>` and `Arc<T>`).
// A manual implementation is necessary because deriving `RcuDroppable` on both types triggers a
// recursive evaluation overflow (Rust issue #26925) due to mutual recursion with `IdrNode`.
unsafe impl<T: RcuDroppable + Send + Sync + 'static> RcuDroppable for IdrEntry<T> {}
#[derive(Debug, RcuDroppable)]
struct IdrNode<T: RcuDroppable + Send + Sync + 'static> {
/// The structural height of this node in the tree. Leaf nodes holding concrete
/// elements sit at layer 0. Intermediate branches exist at layers > 0.
layer: u32,
/// Tracks allocation capacity across the 64 sub-slots. A `1` bit indicates the
/// corresponding slot (or its sub-branch) still has open IDs available. A `0`
/// bit signifies the branch or leaf is at 100% capacity (or reserved).
free_bitmap: AtomicU64,
/// Tracks structural instantiation of the 64 sub-slots. A `1` bit indicates an
/// intermediate branch node has physically been allocated into memory. Used strictly
/// by the lock-free iterator to gracefully skip over uninstantiated memory gaps.
presence_bitmap: AtomicU64,
/// The contiguous memory array branching off this node, storing inner `IdrNode`
/// branches (when layer > 0) or underlying `Arc<T>` leaf items (when layer == 0).
children: [RcuOptionBox<IdrEntry<T>>; NODE_CAPACITY],
}
impl<T: RcuDroppable + Send + Sync + 'static> Default for IdrNode<T> {
fn default() -> Self {
Self::new(0)
}
}
impl<T: RcuDroppable + Send + Sync + 'static> IdrNode<T> {
fn new(layer: u32) -> Self {
let children = std::array::from_fn(|_| RcuOptionBox::new(None));
Self {
layer,
free_bitmap: AtomicU64::new(!0), // all 1s means all free
presence_bitmap: AtomicU64::new(0),
children,
}
}
/// Computes the total capacity underneath this specific node.
/// Layer 0 nodes possess a capacity of exactly 64. Each higher layer multiplies it by 64.
#[inline]
fn capacity(&self) -> u64 {
1u64 << (BITS_PER_LEVEL * (self.layer + 1))
}
/// Compute the index of the child of this node that contains `id`.
#[inline]
fn index_for_id(&self, id: u32) -> usize {
((id >> (self.layer * BITS_PER_LEVEL)) & LEVEL_MASK) as usize
}
/// Compute the contribution to the final value of the `index` child of this node.
/// Utilized by the iterator to reconstruct numeric IDs.
#[inline]
fn id_for_index(&self, index: usize) -> u32 {
(index as u32) << (self.layer * BITS_PER_LEVEL)
}
/// Clears the `index` free bit.
/// Returns true if this transition caused the node to become completely full (0).
#[inline]
fn mark_allocated(&self, index: usize) -> bool {
let old_free = self.free_bitmap.fetch_and(!(1 << index), Ordering::Relaxed);
old_free == (1 << index)
}
/// Adds the `index` free bit.
/// Returns true if this transition caused the node to transition from completely
/// full (0) to having capacity.
#[inline]
fn mark_freed(&self, index: usize) -> bool {
let old_free = self.free_bitmap.fetch_or(1 << index, Ordering::Relaxed);
old_free == 0
}
/// Marks the `index` slot as populated with a branch or leaf.
#[inline]
fn mark_present(&self, index: usize) {
self.presence_bitmap.fetch_or(1 << index, Ordering::Relaxed);
}
/// Marks the `index` slot as physically empty/removed.
#[inline]
fn mark_absent(&self, index: usize) {
self.presence_bitmap.fetch_and(!(1 << index), Ordering::Relaxed);
}
/// Descends into the specific child branch index. If the branch is currently
/// entirely empty and uninstantiated, it constructs the next layer.
/// Must never be called on layer 0.
fn get_or_create_child<'a>(&'a self, index: usize, scope: &'a RcuReadScope) -> &'a IdrNode<T> {
debug_assert!(self.layer > 0);
if let Some(IdrEntry::Node(n)) = self.children[index].as_ref(scope) {
return &**n;
}
let new_node = Arc::new(IdrNode::new(self.layer - 1));
self.children[index].update(Some(IdrEntry::Node(new_node)));
self.mark_present(index);
match self.children[index].as_ref(scope).unwrap() {
IdrEntry::Node(n) => &**n,
_ => unreachable!("Tree corruption: expected a Node entry here"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use starnix_sync::lock_ordering;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
lock_ordering! {
Terminal(TestIdrLock),
}
type Idr<T> = super::Idr<T, TestIdrLock>;
#[derive(RcuDroppable)]
struct MockItem {
value: u32,
}
#[fuchsia::test]
fn test_basic_alloc_lookup() {
let idr = Idr::default();
assert_eq!(idr.alloc_mode, IdrAllocMode::Linear);
let (id1, _item1) = idr.lock().alloc(|id| Arc::new(MockItem { value: id * 10 })).unwrap();
assert_eq!(id1, 0);
let (id2, _item2) = idr.lock().alloc(|id| Arc::new(MockItem { value: id * 10 })).unwrap();
assert_eq!(id2, 1);
let scope = RcuReadScope::new();
let lookup1 = idr.lookup(0, &scope).unwrap();
assert_eq!(lookup1.value, 0);
let lookup2 = idr.lookup(1, &scope).unwrap();
assert_eq!(lookup2.value, 10);
idr.lock().remove(0);
assert!(idr.lookup(0, &scope).is_none());
// Second item remains completely untouched and safely addressable
let lookup_still_there = idr.lookup(1, &scope).unwrap();
assert_eq!(lookup_still_there.value, 10);
}
#[fuchsia::test]
fn test_tree_growth() {
let idr = Idr::default();
let mut _items = Vec::new();
// NODE_CAPACITY is natively 64. Allocating 150 structurally guarantees forcing the tree to
// grow at least once.
for i in 0..150 {
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item);
assert_eq!(id, i);
}
let scope = RcuReadScope::new();
for i in 0..150 {
let item = idr.lookup(i, &scope).unwrap();
assert_eq!(item.value, i);
}
}
#[fuchsia::test]
fn test_alloc_cyclic() {
let idr = Idr::new_cyclic(None);
let mut _items = Vec::new();
for i in 0..30 {
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item);
assert_eq!(id, i);
}
// Manually bump the cursor.
idr.lock().set_cursor(100);
// Allocation formally continues from new cursor.
let (id, item1) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item1);
assert_eq!(id, 100);
let (id, item2) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item2);
assert_eq!(id, 101);
let scope = RcuReadScope::new();
assert!(idr.lookup(30, &scope).is_none());
assert_eq!(idr.lookup(100, &scope).unwrap().value, 100);
}
#[fuchsia::test]
fn test_alloc_cyclic_start_exceeds_capacity() {
let idr = Idr::new_cyclic(None);
let mut _items = Vec::new();
// Immediately bump start_id above the initial layer 0 capacity (64)
idr.lock().set_cursor(256);
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item);
// It should have assigned exactly 256, successfully growing the tree.
assert_eq!(id, 256);
}
#[fuchsia::test]
fn test_reserve_id() {
let idr = Idr::new_cyclic(None);
let mut _items = Vec::new();
// Reserve an ID within initial layer
idr.lock().reserve_id(10);
let (id1, item1) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item1);
assert_eq!(id1, 0);
// Reserve an ID requiring tree growth
idr.lock().reserve_id(200);
// Fill up to 10
let mut allocated_10 = false;
for _ in 1..15 {
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item);
if id == 10 {
allocated_10 = true;
}
}
assert!(!allocated_10, "ID 10 was allocated despite being reserved");
// Verify ID 200 is skipped when allocating near it
idr.lock().set_cursor(199);
let mut allocated_200 = false;
for _ in 0..5 {
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
_items.push(item);
if id == 200 {
allocated_200 = true;
}
}
assert!(!allocated_200, "ID 200 was allocated despite being reserved");
let scope = RcuReadScope::new();
// Lookup of reserved IDs should return None
assert!(idr.lookup(10, &scope).is_none());
assert!(idr.lookup(200, &scope).is_none());
// Remove should not panic or corrupt it
idr.lock().remove(10);
idr.lock().remove(200);
assert!(idr.lookup(10, &scope).is_none());
}
#[fuchsia::test]
fn test_iter_and_remove() {
let idr = Idr::default();
let mut _items = Vec::new();
// Assigned cleanly to 0
_items.push(idr.lock().alloc(|_| Arc::new(MockItem { value: 10 })).unwrap().1);
// Assigned cleanly to 1
_items.push(idr.lock().alloc(|_| Arc::new(MockItem { value: 20 })).unwrap().1);
// Assigned cleanly to 2
_items.push(idr.lock().alloc(|_| Arc::new(MockItem { value: 30 })).unwrap().1);
// Eliminate middle ID freeing slot 1.
idr.lock().remove(1);
let scope = RcuReadScope::new();
let mut iter = idr.iter(&scope);
let next_a = iter.next();
let (id_a, item_a) = next_a.unwrap();
assert_eq!(id_a, 0);
assert_eq!(item_a.value, 10);
let (id_b, item_b) = iter.next().unwrap();
assert_eq!(id_b, 2);
assert_eq!(item_b.value, 30);
assert!(iter.next().is_none());
}
#[fuchsia::test]
fn test_iter_minimal() {
let idr = Idr::default();
let mut _items = Vec::new();
_items.push(idr.lock().alloc(|value| Arc::new(MockItem { value })).unwrap().1);
let scope = RcuReadScope::new();
let mut iter = idr.iter(&scope);
let next_a = iter.next();
assert!(next_a.is_some(), "iter.next() returned None!");
}
#[fuchsia::test]
fn test_alloc_cyclic_with_gaps() {
let idr = Idr::new_cyclic(None);
let mut _items = Vec::new();
// Allocate 100 items (0..99) across layer 0 and layer 1.
for i in 0..100 {
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
assert_eq!(id, i);
_items.push(item);
}
// Create holes in child 0 (0..63) below 50, but keep 50..63 allocated.
for id in 40..50 {
idr.lock().remove(id);
}
// Advance cursor to 50.
idr.lock().set_cursor(50);
// Cyclic allocation should find next free slot >= 50, which is slot 100 in child 1,
// rather than prematurely wrapping to 40 in child 0.
let (id_100, item_100) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
assert_eq!(id_100, 100);
_items.push(item_100);
// Subsequent allocations should continue forward monotonically across child boundaries (101..130).
for expected in 101..130 {
let (id, item) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
assert_eq!(id, expected);
_items.push(item);
}
}
#[fuchsia::test]
fn test_concurrent_readers_and_writers() {
let idr = Arc::new(Idr::new_cyclic(None));
let running = Arc::new(AtomicBool::new(true));
// Pre-populate some items
for _i in 0..50 {
idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
}
let mut reader_handles = Vec::new();
for _ in 0..4 {
let idr_clone = Arc::clone(&idr);
let running_clone = Arc::clone(&running);
reader_handles.push(std::thread::spawn(move || {
while running_clone.load(Ordering::Relaxed) {
let scope = RcuReadScope::new();
// Random lookups
for id in 0..100 {
if let Some(item) = idr_clone.lookup(id, &scope) {
assert_eq!(item.value, id);
}
}
// Iterator traversal
let iter = idr_clone.iter(&scope);
for (id, item) in iter {
assert_eq!(item.value, id);
}
}
}));
}
let running_clone = Arc::clone(&running);
let rcu_advancer = std::thread::spawn(move || {
while running_clone.load(Ordering::Relaxed) {
fuchsia_rcu::rcu_run_callbacks();
std::thread::sleep(std::time::Duration::from_millis(1));
}
});
// Writer thread performs allocations and deletions
let idr_clone = Arc::clone(&idr);
let writer_handle = std::thread::spawn(move || {
for _ in 0..200 {
let (id, _) =
idr_clone.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
if id > 50 && id % 3 == 0 {
idr_clone.lock().remove(id);
}
}
});
writer_handle.join().unwrap();
running.store(false, Ordering::Relaxed);
rcu_advancer.join().unwrap();
for handle in reader_handles {
handle.join().unwrap();
}
}
#[fuchsia::test]
fn test_u32_max_wrap_around() {
let idr = Idr::<MockItem>::new_cyclic(None);
idr.lock().set_cursor(u32::MAX);
// First allocation is at u32::MAX
let (id1, _item1) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
assert_eq!(id1, u32::MAX);
// Next allocation correctly wraps around to 0
let (id2, _item2) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
assert_eq!(id2, 0);
// Validating they are safely addressable
let scope = RcuReadScope::new();
assert_eq!(idr.lookup(u32::MAX, &scope).unwrap().value, u32::MAX);
assert_eq!(idr.lookup(0, &scope).unwrap().value, 0);
}
#[fuchsia::test]
fn test_overflow_bug_at_u32_max() {
let idr = Idr::<MockItem>::new_cyclic(None);
// Reserve 0 so that if the allocator wraps around safely,
// it assigns 1 (acting as a dual check).
idr.lock().reserve_id(0);
// Reserving `u32::MAX` is required to trigger this bug. If `u32::MAX` is free, an
// allocation starting at `u32::MAX` simply takes it, and the cursor wraps cleanly to `0`.
// By making it occupied, `find_free_slot` descends to the bottom of
// subtree 3, discovers there is no space left at or above the cursor,
// and backtracks all the way up to layer 5.
// If layer 5 is not properly constrained by the maximal value, this backtracking
// causes the allocator to erroneously spill over into the invalid index 4.
idr.lock().reserve_id(u32::MAX);
idr.lock().set_cursor(u32::MAX);
let (id, _) = idr.lock().alloc(|id| Arc::new(MockItem { value: id })).unwrap();
assert_eq!(id, 1);
let scope = RcuReadScope::new();
assert!(idr.lookup(1, &scope).is_some());
}
#[fuchsia::test]
fn test_lock_held_across_operations() {
let idr = Idr::<MockItem>::default();
// Acquire the lock outside the class and perform multiple operations while holding it.
let mut guard = idr.lock();
assert_eq!(guard.cursor(), 0);
guard.reserve_id(0);
let (id1, item1) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id1, 1);
assert_eq!(item1.value, 1);
let (id2, item2) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id2, 2);
assert_eq!(item2.value, 2);
guard.remove(1);
// The slot for ID 1 is now available again.
let (id3, item3) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id3, 1);
assert_eq!(item3.value, 1);
// Readers can still access items through Deref on the lock.
let scope = RcuReadScope::new();
assert_eq!(guard.lookup(1, &scope).unwrap().value, 1);
assert_eq!(guard.lookup(2, &scope).unwrap().value, 2);
// Allocating the next lowest available slot yields 3.
let (id4, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id4, 3);
guard.set_cursor(50);
assert_eq!(guard.cursor(), 50);
}
#[fuchsia::test]
fn test_alloc_cyclic_min_after_wrap() {
let idr = Idr::<MockItem>::new_cyclic(Some(2));
assert_eq!(idr.alloc_mode, IdrAllocMode::Cyclic { min_after_wrap: Some(2) });
let mut guard = idr.lock();
// Allocate slots 0, 1, 2, 3, 4.
let (id0, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id0, 0);
let (id1, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id1, 1);
let (id2, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id2, 2);
let (id3, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id3, 3);
let (id4, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id4, 4);
// Free 0 and 1 so slots 0 and 1 become free in the tree.
guard.remove(0);
guard.remove(1);
// Reserve slots 10..64 within initial layer 0 (capacity 64).
for i in 10..64 {
guard.reserve_id(i);
}
// Advance cursor to 10.
guard.set_cursor(10);
// Since slots 10..64 are reserved and cursor is 10, searching >= 10 fails.
// It wraps around. With min_after_wrap = Some(2), it must skip free slots 0 and 1,
// and find the next free slot >= 2, which is slot 5.
let (id5, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id5, 5);
// Allocate remaining slots up to 9.
for expected in 6..10 {
let (id, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id, expected);
}
// Now slots 2..64 are all occupied (allocated or reserved).
// Slots 0 and 1 remain free.
// Attempting another allocation with min_after_wrap = Some(2) must return None,
// because all slots >= 2 are full.
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
}
#[fuchsia::test]
fn test_alloc_cyclic_wrap_without_min_after_wrap() {
// For an IDR configured without min_after_wrap, wrapping allows allocating slots 0 and 1.
let idr = Idr::<MockItem>::new_cyclic(None);
let mut guard = idr.lock();
let (z0, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(z0, 0);
let (z1, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(z1, 1);
guard.remove(0);
guard.remove(1);
guard.set_cursor(10);
for i in 10..64 {
guard.reserve_id(i);
}
let (id0_after, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id0_after, 0);
let (id1_after, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id1_after, 1);
}
#[fuchsia::test]
fn test_alloc_cyclic_min_after_wrap_at_u32_max() {
let idr = Idr::<MockItem>::new_cyclic(Some(2));
let mut guard = idr.lock();
// Slots 0, 1, 2 are all free. Set cursor to u32::MAX.
guard.set_cursor(u32::MAX);
// Allocate u32::MAX.
let (id_max, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id_max, u32::MAX);
// Cursor should wrap to min_after_wrap (2), not 0.
assert_eq!(guard.cursor(), 2);
// Next allocation starts at cursor (2), skipping slots 0 and 1.
let (id2, _) = guard.alloc(|value| Arc::new(MockItem { value })).unwrap();
assert_eq!(id2, 2);
assert_eq!(guard.cursor(), 3);
let scope = RcuReadScope::new();
assert!(guard.lookup(0, &scope).is_none());
assert!(guard.lookup(1, &scope).is_none());
assert!(guard.lookup(2, &scope).is_some());
assert!(guard.lookup(u32::MAX, &scope).is_some());
}
#[fuchsia::test]
fn test_linear_alloc_max() {
let idr = Idr::<MockItem>::default();
idr.set_max(3);
assert_eq!(idr.max(), 3);
let mut guard = idr.lock();
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 0);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 1);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 2);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 3);
// All IDs <= max are allocated; subsequent allocation fails.
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
// Free ID 1 below max.
guard.remove(1);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 1);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
}
#[fuchsia::test]
fn test_update_max() {
let idr = Idr::<MockItem>::default();
idr.set_max(2);
let mut guard = idr.lock();
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 0);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 1);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 2);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
// Increase maximal value to 5.
guard.set_max(5);
assert_eq!(guard.max(), 5);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 3);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 4);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 5);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
// Lower maximal value to 4. Existing ID 5 remains readable.
guard.set_max(4);
let scope = RcuReadScope::new();
assert!(guard.lookup(5, &scope).is_some());
// Removing 5 does not allow reallocating it because 5 > max (4).
guard.remove(5);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
// Removing 2 allows reallocating it because 2 <= max (4).
guard.remove(2);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 2);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
}
#[fuchsia::test]
fn test_cyclic_alloc_max_wrapping() {
let idr = Idr::<MockItem>::new_cyclic(Some(2));
idr.set_max(5);
let mut guard = idr.lock();
// Initial sequential allocations.
for expected in 0..=5 {
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, expected);
}
// Cursor should wrap to min_after_wrap (2).
assert_eq!(guard.cursor(), 2);
// Slots 2..=5 are full, so allocation returns None.
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
// Free slot 3. Allocation should reuse it and advance cursor to 4.
guard.remove(3);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 3);
assert_eq!(guard.cursor(), 4);
// Free slots 0 and 1. Allocation returns None because wrapping stays >= 2.
guard.remove(0);
guard.remove(1);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
}
#[fuchsia::test]
fn test_cyclic_cursor_above_new_max() {
let idr = Idr::<MockItem>::new_cyclic(None);
let mut guard = idr.lock();
// Advance cursor to 80 and allocate.
guard.set_cursor(80);
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 80);
assert_eq!(guard.cursor(), 81);
// Lower max below current cursor.
guard.set_max(50);
// Next allocation resets cursor to wrap_min (0) and allocates slot 0.
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 0);
assert_eq!(guard.cursor(), 1);
}
#[fuchsia::test]
fn test_max_across_tree_layers() {
// 70 exceeds layer 0 capacity (64), requiring the tree to grow to layer 1.
let idr = Idr::<MockItem>::default();
idr.set_max(70);
let mut guard = idr.lock();
for expected in 0..=70 {
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, expected);
}
// Exhausted up to 70.
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
// Update maximal value to 75.
guard.set_max(75);
for expected in 71..=75 {
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, expected);
}
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
}
#[fuchsia::test]
fn test_max_zero() {
let idr = Idr::<MockItem>::default();
idr.set_max(0);
let mut guard = idr.lock();
assert_eq!(guard.alloc(|value| Arc::new(MockItem { value })).unwrap().0, 0);
assert!(guard.alloc(|value| Arc::new(MockItem { value })).is_none());
}
}