Skip to main content

vmo_fifo/
ring_allocator.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// A token representing an uncommitted allocation block in the VMO.
6#[derive(Debug, PartialEq, Eq)]
7pub struct AllocationToken {
8    /// The physical byte offset within the payload region where the slice should be copied.
9    offset: u32,
10
11    /// Bytes (including alignment padding) consumed by this token. This is used to cancel
12    /// allocations (e.g. if the corresponding message is not successfully queued).
13    bytes_added: u64,
14}
15
16impl AllocationToken {
17    /// Gets the physical byte offset within the payload region where the data is stored.
18    pub fn offset(&self) -> u32 {
19        self.offset
20    }
21
22    /// Gets the bytes (including alignment padding) consumed by this token.
23    pub(crate) fn bytes_added(&self) -> u64 {
24        self.bytes_added
25    }
26}
27
28// A ring buffer allocator intended to be used on the payload region of `SharedQueue`.
29pub(crate) struct RingAllocator {
30    // The size of the shared VMO payload region in bytes.
31    payload_capacity: u64,
32
33    // The maximum number of message slots in the shared VMO queue.
34    queue_capacity: u64,
35
36    // The byte alignment boundaries required for all allocations (e.g., 4096).
37    // Must be a power of two: The byte padding calculations use bitwise masking `(x + (a - 1)) &
38    // !(a - 1)`. This avoids costly division instructions but fundamentally limits alignment bounds
39    // to base-2 (powers of two).
40    alignment: u64,
41
42    // The cumulative number of bytes historically allocated.
43    allocated_bytes: u64,
44
45    // The cumulative number of bytes successfully freed from the queue.
46    freed_bytes: u64,
47
48    // Tracks the last read index evaluated for reclamation so we don't scan slots twice.
49    last_reclaimed_read_index: u64,
50
51    // Tracks the bytes allocated for each slot index. When slots are reclaimed, this is added to
52    // `freed_bytes` to release the allocation.
53    slot_allocations: Box<[u64]>,
54}
55
56impl RingAllocator {
57    // Creates a new `RingAllocator`.
58    //
59    // - `payload_capacity`: The total size of the payload region in bytes.
60    // - `queue_capacity`: The maximum number of message slots in the SharedQueue.
61    // - `alignment`: The byte alignment boundaries required for allocations. Modulo padding
62    //                is applied so that all payload offsets begin aligned to this size. Must be a
63    //                power of two.
64    pub(crate) fn new(payload_capacity: usize, queue_capacity: usize, alignment: usize) -> Self {
65        assert!(
66            alignment > 0 && alignment.is_power_of_two(),
67            "Alignment must be a power of two to support bitwise alignment arithmetic."
68        );
69        Self {
70            payload_capacity: payload_capacity as u64,
71            queue_capacity: queue_capacity as u64,
72            alignment: alignment as u64,
73            allocated_bytes: 0,
74            freed_bytes: 0,
75            last_reclaimed_read_index: 0,
76            slot_allocations: vec![0; queue_capacity].into_boxed_slice(),
77        }
78    }
79
80    // Attempts to allocate `size` bytes of strictly contiguous space in the VMO payload region.
81    // Returns an AllocationToken on success, or None if there is not enough space.
82    pub(crate) fn allocate(&mut self, size: usize) -> Option<AllocationToken> {
83        let align = self.alignment;
84
85        // Enforce size alignment: round it up to the nearest multiple of alignment.
86        let aligned_size = (size as u64 + align - 1) & !(align - 1);
87        if aligned_size > self.payload_capacity {
88            return None;
89        }
90
91        let physical_head = self.allocated_bytes % self.payload_capacity;
92        // Padding required to align the physical_head offset
93        let padding = (align - (physical_head % align)) % align;
94        let padded_size = aligned_size + padding;
95
96        let space_to_end = self.payload_capacity - physical_head;
97        let (bytes_added, offset) = if padded_size <= space_to_end {
98            // It physically fits without wrapping around.
99            (padded_size, (physical_head + padding) as u32)
100        } else {
101            // It does not fit before the end of the VMO, abandon the remaining end of the VMO and
102            // wrap around. The new physical offset is 0 (which is inherently aligned).
103            (space_to_end + aligned_size, 0)
104        };
105
106        let active_bytes = self.allocated_bytes - self.freed_bytes;
107        if active_bytes + bytes_added <= self.payload_capacity {
108            self.allocated_bytes += bytes_added;
109            Some(AllocationToken { offset, bytes_added })
110        } else {
111            None
112        }
113    }
114
115    // Informs the allocator that the most recent allocation has been successfully queued.
116    // Associates the token's allocated byte count with `slot_index` so those bytes are freed when
117    // the receiver consumes the slot.
118    pub(crate) fn commit_allocation_to_slot(&mut self, slot_index: u64, token: AllocationToken) {
119        let index = (slot_index % self.queue_capacity) as usize;
120        self.slot_allocations[index] = token.bytes_added();
121    }
122
123    // Cancel the uncommitted allocation.
124    pub(crate) fn cancel_allocation(&mut self, token: AllocationToken) {
125        self.allocated_bytes -= token.bytes_added();
126        // If all previously committed allocations have already been reclaimed, the logical buffer
127        // is empty. Reset both counters back to 0 to promote reuse of memory at the start of the
128        // VMO.
129        if self.allocated_bytes == self.freed_bytes {
130            self.allocated_bytes = 0;
131            self.freed_bytes = 0;
132        }
133    }
134
135    // Frees memory associated with messages that the receiver has finished processing.
136    // TODO(https://fxbug.dev/530494057): Add function to decommit memory pages from the VMO when
137    // they are reclaimed to avoid holding onto physical RAM while the device is idle.
138    pub(crate) fn reclaim_consumed_slots(&mut self, new_read_index: u64) {
139        for i in self.last_reclaimed_read_index..new_read_index {
140            let slot = (i % self.queue_capacity) as usize;
141            self.freed_bytes += std::mem::take(&mut self.slot_allocations[slot]);
142        }
143        self.last_reclaimed_read_index = new_read_index;
144
145        // If the receiver has consumed all outstanding payload allocations, the logical buffer is
146        // empty. Reset both counters back to 0 to promote reuse of memory at the start of the VMO.
147        if self.allocated_bytes == self.freed_bytes {
148            self.allocated_bytes = 0;
149            self.freed_bytes = 0;
150        }
151    }
152
153    // Returns false if the requested size is permanently impossible to fit inside the payload
154    // region, regardless of how much space the queue eventually frees up.
155    pub(crate) fn is_within_capacity(&self, size: usize) -> bool {
156        let align = self.alignment;
157        let aligned_size = (size as u64 + align - 1) & !(align - 1);
158        aligned_size <= self.payload_capacity
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn active_bytes(allocator: &RingAllocator) -> u64 {
167        allocator.allocated_bytes - allocator.freed_bytes
168    }
169
170    #[test]
171    fn test_allocate_sequential() {
172        let mut allocator = RingAllocator::new(100, 10, 8);
173
174        let t1 = allocator.allocate(20).unwrap();
175        assert_eq!(t1.offset(), 0);
176        assert_eq!(t1.bytes_added(), 24); // rounded up to 24 by 8 byte align
177        allocator.commit_allocation_to_slot(0, t1);
178
179        let t2 = allocator.allocate(32).unwrap();
180        assert_eq!(t2.offset(), 24);
181        assert_eq!(t2.bytes_added(), 32);
182        allocator.commit_allocation_to_slot(1, t2);
183
184        assert_eq!(active_bytes(&allocator), 56);
185    }
186
187    #[test]
188    fn test_cancel_allocation() {
189        let mut allocator = RingAllocator::new(100, 10, 8);
190
191        let t1 = allocator.allocate(50).unwrap();
192        assert_eq!(active_bytes(&allocator), 56);
193
194        allocator.cancel_allocation(t1);
195
196        // Active bytes perfectly zeroed out! Ready to allocate anew.
197        assert_eq!(active_bytes(&allocator), 0);
198        assert_eq!(allocator.allocated_bytes, 0);
199    }
200
201    #[test]
202    fn test_empty_vs_full() {
203        let mut allocator = RingAllocator::new(128, 10, 64);
204
205        let t1 = allocator.allocate(64).unwrap();
206        assert_eq!(active_bytes(&allocator), 64);
207        allocator.commit_allocation_to_slot(0, t1);
208
209        let t2 = allocator.allocate(64).unwrap();
210        assert_eq!(active_bytes(&allocator), 128);
211        allocator.commit_allocation_to_slot(1, t2);
212
213        // Allocate should return None as ring is full.
214        assert!(allocator.allocate(10).is_none());
215
216        allocator.reclaim_consumed_slots(1);
217        assert_eq!(active_bytes(&allocator), 64);
218
219        allocator.reclaim_consumed_slots(2);
220        assert_eq!(active_bytes(&allocator), 0);
221    }
222
223    #[test]
224    fn test_zero_size_allocation_after_nonzero_reclaim() {
225        let mut allocator = RingAllocator::new(65536, 16, 4096);
226        let mut read_index = 0;
227
228        // Slot 0 allocates 8192 bytes.
229        let t0 = allocator.allocate(8192).unwrap();
230        assert_eq!(t0.bytes_added(), 8192);
231        allocator.commit_allocation_to_slot(0, t0);
232
233        // Slot 1 allocates 0 bytes.
234        let t1 = allocator.allocate(0).unwrap();
235        assert_eq!(t1.bytes_added(), 0);
236        allocator.commit_allocation_to_slot(1, t1);
237
238        // Consume slot 0 while slot 1 remains in the queue.
239        read_index += 1;
240        allocator.reclaim_consumed_slots(read_index);
241        assert_eq!(active_bytes(&allocator), 0);
242
243        // Slot 2 allocates 4096 bytes.
244        let t2 = allocator.allocate(4096).unwrap();
245        assert_eq!(t2.bytes_added(), 4096);
246        allocator.commit_allocation_to_slot(2, t2);
247
248        // Consume slot 1 (0 bytes); slot 2 (4096 bytes) remains active.
249        read_index += 1;
250        allocator.reclaim_consumed_slots(read_index);
251        assert_eq!(active_bytes(&allocator), 4096);
252
253        // Slot 3 allocates 4096 bytes (slots 2 and 3 are now active).
254        let t3 = allocator.allocate(4096).unwrap();
255        allocator.commit_allocation_to_slot(3, t3);
256        assert_eq!(active_bytes(&allocator), 8192);
257
258        // Consume slots 2 and 3.
259        read_index += 2;
260        allocator.reclaim_consumed_slots(read_index);
261        assert_eq!(active_bytes(&allocator), 0);
262    }
263}