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}