Skip to main content

rts_alloc/
remote_free_batch.rs

1use crate::allocator::{Allocator, AllocatorBase, FreeOnlyAllocator};
2use core::ptr::NonNull;
3
4/// Collects frees and publishes remote frees in batches grouped by owning worker.
5///
6/// Frees owned by the worker represented by the [`Allocator`] handle that created
7/// the batch are reclaimed immediately. All other frees are linked into intrusive
8/// chains and published when [`Self::flush`] is called or the batch is dropped.
9pub struct RemoteFreeBatch<'a> {
10    source: RemoteFreeBatchSource<'a>,
11    chains: Vec<WorkerRemoteFreeChain>,
12}
13
14enum RemoteFreeBatchSource<'a> {
15    Allocator(&'a Allocator),
16    FreeOnly(&'a FreeOnlyAllocator),
17}
18
19struct WorkerRemoteFreeChain {
20    // INVARIANT: `head` and `tail` are valid, uniquely freed allocation offsets
21    // whose intrusive links form a private chain owned by the batch.
22    worker_index: u32,
23    head: usize,
24    tail: usize,
25}
26
27impl RemoteFreeBatchSource<'_> {
28    fn base(&self) -> &AllocatorBase {
29        match self {
30            Self::Allocator(allocator) => allocator.base(),
31            Self::FreeOnly(allocator) => allocator.base(),
32        }
33    }
34
35    /// Find the offset for an allocation pointer.
36    ///
37    /// # Safety
38    /// - `ptr` must be a valid pointer into this allocator region.
39    unsafe fn offset(&self, ptr: NonNull<u8>) -> usize {
40        match self {
41            Self::Allocator(allocator) => unsafe { allocator.offset(ptr) },
42            Self::FreeOnly(allocator) => unsafe { allocator.offset(ptr) },
43        }
44    }
45}
46
47impl<'a> RemoteFreeBatch<'a> {
48    fn new(source: RemoteFreeBatchSource<'a>) -> Self {
49        Self {
50            source,
51            chains: Vec::new(),
52        }
53    }
54
55    /// Free a block of memory from this allocator region.
56    ///
57    /// Locally owned allocations are reclaimed immediately. Remote frees remain
58    /// private to this batch until it is flushed or dropped.
59    ///
60    /// # Safety
61    /// - `ptr` must point to a valid allocation in this allocator region.
62    /// - The `ptr` must not have been freed before or added to another free batch.
63    pub unsafe fn free(&mut self, ptr: NonNull<u8>) {
64        // SAFETY: The caller guarantees that the pointer refers to a valid allocation
65        // in this allocator region.
66        let offset = unsafe { self.source.offset(ptr) };
67        // SAFETY: The offset was derived from the caller-provided allocation pointer.
68        unsafe { self.free_offset(offset) };
69    }
70
71    /// Free a block of memory from this allocator region.
72    ///
73    /// Locally owned allocations are reclaimed immediately. Remote frees remain
74    /// private to this batch until it is flushed or dropped.
75    ///
76    /// # Safety
77    /// - `offset` must identify a valid allocation in this allocator region.
78    /// - The `offset` must not have been freed before or added to another free batch.
79    pub unsafe fn free_offset(&mut self, offset: usize) {
80        // SAFETY: The caller guarantees that `offset` refers to a valid allocation.
81        let Some((allocation_indexes, worker_index)) = (unsafe {
82            self.source
83                .base()
84                .allocation_indexes_and_assigned_worker(offset)
85        }) else {
86            return;
87        };
88
89        if let RemoteFreeBatchSource::Allocator(allocator) = &self.source {
90            if allocator.worker_index() == worker_index {
91                // SAFETY: The indexes came from the caller's valid offset and the
92                // ownership check above confirms its slab is local.
93                unsafe { allocator.free_local(allocation_indexes) };
94                return;
95            }
96        }
97
98        // SAFETY: The caller guarantees that `offset` refers to a valid, uniquely
99        // freed allocation and `worker_index` was read from its slab metadata.
100        unsafe { self.push_remote(worker_index, offset) };
101    }
102
103    /// Publish all remote frees currently held by this batch.
104    ///
105    /// The batch may be reused after flushing. Capacity allocated for destination
106    /// workers is retained.
107    pub fn flush(&mut self) {
108        while let Some(chain) = self.chains.pop() {
109            // SAFETY: Chains can only be created from offsets accepted by the unsafe
110            // `free` methods and remain private until they are removed here.
111            unsafe {
112                self.source.base().publish_remote_free_chain(
113                    chain.worker_index,
114                    chain.head,
115                    chain.tail,
116                )
117            };
118        }
119    }
120
121    /// Add an allocation to its worker's private remote-free chain.
122    ///
123    /// # Safety
124    /// - `offset` must refer to a valid, uniquely freed allocation.
125    /// - `worker_index` must be the worker currently assigned to its slab.
126    unsafe fn push_remote(&mut self, worker_index: u32, offset: usize) {
127        if let Some(chain) = self
128            .chains
129            .iter_mut()
130            .find(|chain| chain.worker_index == worker_index)
131        {
132            // SAFETY: Guaranteed by the caller; `chain.head` is another valid
133            // offset in the same private chain.
134            unsafe { self.source.base().set_remote_free_next(offset, chain.head) };
135            chain.head = offset;
136            return;
137        }
138
139        // Register the destination before modifying the allocation. If allocation
140        // fails, dropping the batch can still publish every previously linked chain.
141        self.chains.push(WorkerRemoteFreeChain {
142            worker_index,
143            head: offset,
144            tail: offset,
145        });
146    }
147}
148
149impl Drop for RemoteFreeBatch<'_> {
150    fn drop(&mut self) {
151        self.flush();
152    }
153}
154
155impl Allocator {
156    /// Create a batch that groups remote frees by owning worker.
157    ///
158    /// Locally owned allocations are reclaimed immediately. Remote frees are
159    /// published by [`RemoteFreeBatch::flush`] or when the batch is dropped.
160    pub fn remote_free_batch(&self) -> RemoteFreeBatch<'_> {
161        RemoteFreeBatch::new(RemoteFreeBatchSource::Allocator(self))
162    }
163}
164
165impl FreeOnlyAllocator {
166    /// Create a batch that groups remote frees by owning worker.
167    ///
168    /// Remote frees are published by [`RemoteFreeBatch::flush`] or when the
169    /// batch is dropped.
170    pub fn remote_free_batch(&self) -> RemoteFreeBatch<'_> {
171        RemoteFreeBatch::new(RemoteFreeBatchSource::FreeOnly(self))
172    }
173}