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    // Stores a snapshot of `allocated_bytes` for each slot index. When slots are reclaimed, this is
52    // read to release the unused allocations and advance `freed_bytes` to the reclaim target.
53    reclaim_targets: Box<[Option<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.
62    pub(crate) fn new(payload_capacity: usize, queue_capacity: usize, alignment: usize) -> Self {
63        assert!(
64            alignment > 0 && alignment.is_power_of_two(),
65            "Alignment must be a power of two to support bitwise alignment arithmetic."
66        );
67        Self {
68            payload_capacity: payload_capacity as u64,
69            queue_capacity: queue_capacity as u64,
70            alignment: alignment as u64,
71            allocated_bytes: 0,
72            freed_bytes: 0,
73            last_reclaimed_read_index: 0,
74            reclaim_targets: vec![None; queue_capacity].into_boxed_slice(),
75        }
76    }
77
78    // Attempts to allocate `size` bytes of strictly contiguous space in the VMO payload region.
79    // Returns an AllocationToken on success, or None if there is not enough space.
80    pub(crate) fn allocate(&mut self, size: usize) -> Option<AllocationToken> {
81        let align = self.alignment;
82
83        // Enforce size alignment: round it up to the nearest multiple of alignment.
84        let aligned_size = (size as u64 + align - 1) & !(align - 1);
85        if aligned_size > self.payload_capacity {
86            return None;
87        }
88
89        let physical_head = self.allocated_bytes % self.payload_capacity;
90        // Padding required to align the physical_head offset
91        let padding = (align - (physical_head % align)) % align;
92        let padded_size = aligned_size + padding;
93
94        let space_to_end = self.payload_capacity - physical_head;
95        let (bytes_added, offset) = if padded_size <= space_to_end {
96            // It physically fits without wrapping around.
97            (padded_size, (physical_head + padding) as u32)
98        } else {
99            // It does not fit before the end of the VMO, abandon the remaining end of the VMO and
100            // wrap around. The new physical offset is 0 (which is inherently aligned).
101            (space_to_end + aligned_size, 0)
102        };
103
104        let active_bytes = self.allocated_bytes - self.freed_bytes;
105        if active_bytes + bytes_added <= self.payload_capacity {
106            self.allocated_bytes += bytes_added;
107            Some(AllocationToken { offset, bytes_added })
108        } else {
109            None
110        }
111    }
112
113    // Informs the allocator that the most recent allocation has been successfully queued. Link this
114    // allocation to a slot index for later reclamation.
115    pub(crate) fn commit_allocation_to_slot(&mut self, slot_index: u64, _token: AllocationToken) {
116        let index = (slot_index % self.queue_capacity) as usize;
117        self.reclaim_targets[index] = Some(self.allocated_bytes);
118    }
119
120    // Cancel the uncommitted allocation.
121    pub(crate) fn cancel_allocation(&mut self, token: AllocationToken) {
122        self.allocated_bytes -= token.bytes_added();
123    }
124
125    // Frees memory associated with messages that the receiver has finished processing.
126    // TODO(https://fxbug.dev/530494057): Add function to decommit memory pages from the VMO when
127    // they are reclaimed to avoid holding onto physical RAM while the device is idle.
128    pub(crate) fn reclaim_consumed_slots(&mut self, new_read_index: u64) {
129        for i in self.last_reclaimed_read_index..new_read_index {
130            let slot = (i % self.queue_capacity) as usize;
131
132            if let Some(target) = self.reclaim_targets[slot].take() {
133                if target > self.freed_bytes {
134                    self.freed_bytes = target;
135                }
136            }
137        }
138        self.last_reclaimed_read_index = new_read_index;
139
140        // If the receiver has consumed all outstanding messages, the logical buffer is empty.
141        // Reset both pointers back to 0 to promote reuse of memory already in the cache.
142        if self.allocated_bytes == self.freed_bytes {
143            self.allocated_bytes = 0;
144            self.freed_bytes = 0;
145        }
146    }
147
148    // Returns false if the requested size is permanently impossible to fit inside the payload
149    // region, regardless of how much space the queue eventually frees up.
150    pub(crate) fn is_within_capacity(&self, size: usize) -> bool {
151        let align = self.alignment;
152        let aligned_size = (size as u64 + align - 1) & !(align - 1);
153        aligned_size <= self.payload_capacity
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn active_bytes(allocator: &RingAllocator) -> u64 {
162        allocator.allocated_bytes - allocator.freed_bytes
163    }
164
165    #[test]
166    fn test_allocate_sequential() {
167        let mut allocator = RingAllocator::new(100, 10, 8);
168
169        let t1 = allocator.allocate(20).unwrap();
170        assert_eq!(t1.offset(), 0);
171        assert_eq!(t1.bytes_added(), 24); // rounded up to 24 by 8 byte align
172        allocator.commit_allocation_to_slot(0, t1);
173
174        let t2 = allocator.allocate(32).unwrap();
175        assert_eq!(t2.offset(), 24);
176        assert_eq!(t2.bytes_added(), 32);
177        allocator.commit_allocation_to_slot(1, t2);
178
179        assert_eq!(active_bytes(&allocator), 56);
180    }
181
182    #[test]
183    fn test_cancel_allocation() {
184        let mut allocator = RingAllocator::new(100, 10, 8);
185
186        let t1 = allocator.allocate(50).unwrap();
187        assert_eq!(active_bytes(&allocator), 56);
188
189        allocator.cancel_allocation(t1);
190
191        // Active bytes perfectly zeroed out! Ready to allocate anew.
192        assert_eq!(active_bytes(&allocator), 0);
193        assert_eq!(allocator.allocated_bytes, 0);
194    }
195
196    #[test]
197    fn test_empty_vs_full() {
198        let mut allocator = RingAllocator::new(128, 10, 64);
199
200        let t1 = allocator.allocate(64).unwrap();
201        assert_eq!(active_bytes(&allocator), 64);
202        allocator.commit_allocation_to_slot(0, t1);
203
204        let t2 = allocator.allocate(64).unwrap();
205        assert_eq!(active_bytes(&allocator), 128);
206        allocator.commit_allocation_to_slot(1, t2);
207
208        // Allocate should return None as ring is full.
209        assert!(allocator.allocate(10).is_none());
210
211        allocator.reclaim_consumed_slots(1);
212        assert_eq!(active_bytes(&allocator), 64);
213
214        allocator.reclaim_consumed_slots(2);
215        assert_eq!(active_bytes(&allocator), 0);
216    }
217}