Skip to main content

turbo_trace_server/
chunked_vec.rs

1//! A push-only vector that grows in fixed-size chunks instead of one
2//! contiguous reallocating buffer.
3//!
4//! The trade-off vs. `Vec`:
5//! - One pointer indirection per indexed access (chunk lookup → element).
6//! - Slightly larger per-element overhead from chunk pointers (negligible at 64K elements/chunk).
7//! - References returned by `index`/`index_mut` are stable across `push` (a future-useful property;
8//!   not currently relied on).
9//!
10//! API is intentionally minimal — only the operations the trace server
11//! needs (`push`, `len`, indexed access, `get`, `truncate`).
12
13use std::{
14    mem::MaybeUninit,
15    ops::{Index, IndexMut},
16};
17
18/// Number of elements per chunk. Power of two so `idx / CHUNK_SIZE` and
19/// `idx % CHUNK_SIZE` compile to a shift and a mask.
20const CHUNK_SIZE: usize = 1 << 16;
21
22/// Returns the chunk index and intra-chunk offset for an element index.
23#[inline]
24fn split_index(idx: usize) -> (usize, usize) {
25    (idx / CHUNK_SIZE, idx % CHUNK_SIZE)
26}
27
28type Chunk<T> = Box<[MaybeUninit<T>; CHUNK_SIZE]>;
29
30/// Allocate a fresh chunk on the heap without ever materializing a
31/// `CHUNK_SIZE`-element array on the stack.
32fn new_chunk<T>() -> Chunk<T> {
33    // SAFETY: `Box<MaybeUninit<[MaybeUninit<T>; N]>>` and
34    // `Box<[MaybeUninit<T>; N]>` have identical layout; the outer
35    // `MaybeUninit` is just deferring initialization of the array of
36    // uninitialized slots, which trivially satisfies "init".
37    unsafe {
38        let raw: Box<MaybeUninit<[MaybeUninit<T>; CHUNK_SIZE]>> = Box::new_uninit();
39        raw.assume_init()
40    }
41}
42
43pub struct ChunkedVec<T> {
44    chunks: Vec<Chunk<T>>,
45    len: usize,
46}
47
48impl<T> ChunkedVec<T> {
49    pub fn new() -> Self {
50        Self {
51            chunks: Vec::new(),
52            len: 0,
53        }
54    }
55
56    pub fn len(&self) -> usize {
57        self.len
58    }
59
60    /// Append an element. Returns the index it was placed at.
61    pub fn push(&mut self, value: T) -> usize {
62        let idx = self.len;
63        let (chunk_idx, off) = split_index(idx);
64        if off == 0 {
65            // Crossing into a new chunk — allocate it.
66            debug_assert_eq!(chunk_idx, self.chunks.len());
67            self.chunks.push(new_chunk());
68        }
69        self.chunks[chunk_idx][off].write(value);
70        self.len += 1;
71        idx
72    }
73
74    pub fn get(&self, idx: usize) -> Option<&T> {
75        if idx >= self.len {
76            return None;
77        }
78        let (chunk_idx, off) = split_index(idx);
79        // SAFETY: `idx < self.len` ⇒ slot was previously written by
80        // `push` and not freed by `truncate`.
81        Some(unsafe { self.chunks[chunk_idx][off].assume_init_ref() })
82    }
83}
84
85impl<T> Default for ChunkedVec<T> {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl<T> Drop for ChunkedVec<T> {
92    fn drop(&mut self) {
93        // Drop every initialized slot in [new_len, old_len). Walk the
94        // chunks one by one so we visit each `MaybeUninit<T>` exactly
95        // once.
96        let (last_chunk, last_chunk_len) = split_index(self.len);
97
98        for (chunk_index, chunk) in self.chunks.iter_mut().enumerate() {
99            let chunk_end = if chunk_index == last_chunk {
100                last_chunk_len
101            } else {
102                CHUNK_SIZE
103            };
104            for slot in &mut chunk[0..chunk_end] {
105                // SAFETY: the slot was initialized by a prior `push`
106                // and has not yet been dropped by `truncate`.
107                unsafe { slot.assume_init_drop() };
108            }
109        }
110    }
111}
112
113impl<T> Index<usize> for ChunkedVec<T> {
114    type Output = T;
115
116    #[inline]
117    fn index(&self, idx: usize) -> &T {
118        assert!(idx < self.len, "index out of bounds: {idx} >= {}", self.len);
119        let (chunk_idx, off) = split_index(idx);
120        // SAFETY: `idx < self.len` ⇒ slot is initialized.
121        unsafe { self.chunks[chunk_idx][off].assume_init_ref() }
122    }
123}
124
125impl<T> IndexMut<usize> for ChunkedVec<T> {
126    #[inline]
127    fn index_mut(&mut self, idx: usize) -> &mut T {
128        assert!(idx < self.len, "index out of bounds: {idx} >= {}", self.len);
129        let (chunk_idx, off) = split_index(idx);
130        // SAFETY: `idx < self.len` ⇒ slot is initialized.
131        unsafe { self.chunks[chunk_idx][off].assume_init_mut() }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn empty() {
141        let v: ChunkedVec<u32> = ChunkedVec::new();
142        assert_eq!(v.len(), 0);
143        assert!(v.get(0).is_none());
144    }
145
146    #[test]
147    fn push_within_first_chunk() {
148        let mut v = ChunkedVec::new();
149        for i in 0..1000u32 {
150            assert_eq!(v.push(i), i as usize);
151        }
152        assert_eq!(v.len(), 1000);
153        assert_eq!(v[0], 0);
154        assert_eq!(v[999], 999);
155        assert_eq!(v.get(1000), None);
156    }
157
158    #[test]
159    fn push_across_chunk_boundary() {
160        let mut v = ChunkedVec::new();
161        // Push enough to span three chunks.
162        let total = 3 * CHUNK_SIZE + 17;
163        for i in 0..total {
164            v.push(i);
165        }
166        assert_eq!(v.len(), total);
167        assert_eq!(v[0], 0);
168        assert_eq!(v[CHUNK_SIZE - 1], CHUNK_SIZE - 1);
169        assert_eq!(v[CHUNK_SIZE], CHUNK_SIZE);
170        assert_eq!(v[2 * CHUNK_SIZE], 2 * CHUNK_SIZE);
171        assert_eq!(v[total - 1], total - 1);
172    }
173
174    #[test]
175    fn index_mut_writes_through() {
176        let mut v = ChunkedVec::new();
177        for i in 0..(CHUNK_SIZE + 5) {
178            v.push(i);
179        }
180        v[CHUNK_SIZE + 3] = 9999;
181        assert_eq!(v[CHUNK_SIZE + 3], 9999);
182    }
183
184    #[test]
185    fn drops_elements_on_drop() {
186        use std::rc::Rc;
187        let counter = Rc::new(());
188        {
189            let mut v: ChunkedVec<Rc<()>> = ChunkedVec::new();
190            // Span multiple chunks so we exercise the multi-chunk drop path.
191            let total = 2 * CHUNK_SIZE + 5;
192            for _ in 0..total {
193                v.push(counter.clone());
194            }
195            assert_eq!(Rc::strong_count(&counter), total + 1);
196        } // ChunkedVec drop runs here, dropping the remaining CHUNK_SIZE clones.
197        assert_eq!(Rc::strong_count(&counter), 1);
198    }
199}