we (web engine): Experimental web browser project to understand the limits of Claude
at map-set 401 lines 12 kB view raw
1//! Tri-color mark-and-sweep garbage collector. 2//! 3//! Manages heap-allocated JS objects (plain objects, arrays, functions) using 4//! the tri-color invariant: white (unreached), gray (reached, children not yet 5//! scanned), black (fully scanned). The collector is stop-the-world and 6//! triggered when live object count exceeds a configurable threshold. 7 8/// Mark color for tri-color marking. 9#[derive(Debug, Clone, Copy, PartialEq, Eq)] 10enum Color { 11 /// Not yet visited — candidate for collection. 12 White, 13 /// Visited but children not yet scanned. 14 Gray, 15 /// Fully scanned — reachable and all children traced. 16 Black, 17} 18 19/// A reference (handle) to a GC-managed object. 20/// 21/// This is a lightweight index into the GC heap. It is `Copy` so it can be 22/// freely duplicated — the GC is responsible for tracking liveness. 23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 24pub struct GcRef(u32); 25 26/// Trait for types that can be traced by the garbage collector. 27/// 28/// Implementors must report all `GcRef` values they hold so the GC can 29/// traverse the object graph. 30pub trait Traceable { 31 /// Call `visitor` once for each `GcRef` this object holds. 32 fn trace(&self, visitor: &mut dyn FnMut(GcRef)); 33} 34 35/// A heap slot containing GC metadata and the object data. 36struct HeapSlot<T> { 37 color: Color, 38 data: T, 39} 40 41/// Statistics about the GC heap. 42#[derive(Debug, Clone)] 43pub struct GcStats { 44 /// Number of live (allocated) objects. 45 pub live_count: usize, 46 /// Total heap slots (including free slots). 47 pub total_slots: usize, 48 /// Number of collections performed so far. 49 pub collections: usize, 50} 51 52/// The garbage collector. 53/// 54/// Manages a heap of `T` objects. Objects are allocated via [`alloc`] and 55/// accessed via [`get`]/[`get_mut`]. Collection is triggered manually or 56/// when [`should_collect`] returns true. 57pub struct Gc<T: Traceable> { 58 /// The heap: a vector of optional slots (None = free). 59 heap: Vec<Option<HeapSlot<T>>>, 60 /// Free slot indices for O(1) allocation. 61 free_list: Vec<u32>, 62 /// Number of currently live objects. 63 live_count: usize, 64 /// Collection is suggested when live_count exceeds this. 65 threshold: usize, 66 /// Total collections performed. 67 collections: usize, 68} 69 70/// Initial collection threshold. 71const INITIAL_THRESHOLD: usize = 256; 72 73impl<T: Traceable> Gc<T> { 74 /// Create a new, empty GC heap. 75 pub fn new() -> Self { 76 Self { 77 heap: Vec::new(), 78 free_list: Vec::new(), 79 live_count: 0, 80 threshold: INITIAL_THRESHOLD, 81 collections: 0, 82 } 83 } 84 85 /// Allocate a new object on the heap, returning a `GcRef` handle. 86 pub fn alloc(&mut self, data: T) -> GcRef { 87 let slot = HeapSlot { 88 color: Color::White, 89 data, 90 }; 91 let idx = if let Some(free_idx) = self.free_list.pop() { 92 let i = free_idx as usize; 93 self.heap[i] = Some(slot); 94 free_idx 95 } else { 96 let i = self.heap.len(); 97 self.heap.push(Some(slot)); 98 i as u32 99 }; 100 self.live_count += 1; 101 GcRef(idx) 102 } 103 104 /// Get an immutable reference to the object at `r`. 105 /// 106 /// Returns `None` if the slot has been freed (stale reference). 107 pub fn get(&self, r: GcRef) -> Option<&T> { 108 let idx = r.0 as usize; 109 self.heap 110 .get(idx) 111 .and_then(|slot| slot.as_ref().map(|s| &s.data)) 112 } 113 114 /// Get a mutable reference to the object at `r`. 115 /// 116 /// Returns `None` if the slot has been freed (stale reference). 117 pub fn get_mut(&mut self, r: GcRef) -> Option<&mut T> { 118 let idx = r.0 as usize; 119 self.heap 120 .get_mut(idx) 121 .and_then(|slot| slot.as_mut().map(|s| &mut s.data)) 122 } 123 124 /// Returns `true` if a collection should be triggered. 125 pub fn should_collect(&self) -> bool { 126 self.live_count >= self.threshold 127 } 128 129 /// Perform a garbage collection pass. 130 /// 131 /// `roots` must contain every `GcRef` reachable from the mutator (VM 132 /// registers, globals, call stack). Any object not transitively reachable 133 /// from a root is freed. 134 pub fn collect(&mut self, roots: &[GcRef]) { 135 self.mark(roots); 136 self.sweep(); 137 self.collections += 1; 138 139 // Grow threshold so we don't collect too often. 140 if self.live_count >= self.threshold { 141 self.threshold = self.live_count * 2; 142 } 143 } 144 145 /// Return heap statistics. 146 pub fn stats(&self) -> GcStats { 147 GcStats { 148 live_count: self.live_count, 149 total_slots: self.heap.len(), 150 collections: self.collections, 151 } 152 } 153 154 /// Mark phase: starting from roots, color all reachable objects black. 155 fn mark(&mut self, roots: &[GcRef]) { 156 let mut gray_stack: Vec<GcRef> = Vec::new(); 157 158 // Color roots gray. 159 for &root in roots { 160 let idx = root.0 as usize; 161 if idx < self.heap.len() { 162 if let Some(entry) = &mut self.heap[idx] { 163 if entry.color == Color::White { 164 entry.color = Color::Gray; 165 gray_stack.push(root); 166 } 167 } 168 } 169 } 170 171 // Process the gray stack. 172 while let Some(gc_ref) = gray_stack.pop() { 173 let idx = gc_ref.0 as usize; 174 175 // Collect child references (scoped borrow). 176 let children = { 177 let mut c = Vec::new(); 178 if let Some(entry) = &self.heap[idx] { 179 entry.data.trace(&mut |child| c.push(child)); 180 } 181 c 182 }; 183 184 // Mark current object black. 185 if let Some(entry) = &mut self.heap[idx] { 186 entry.color = Color::Black; 187 } 188 189 // Mark white children gray. 190 for child in children { 191 let cidx = child.0 as usize; 192 if cidx < self.heap.len() { 193 if let Some(entry) = &mut self.heap[cidx] { 194 if entry.color == Color::White { 195 entry.color = Color::Gray; 196 gray_stack.push(child); 197 } 198 } 199 } 200 } 201 } 202 } 203 204 /// Sweep phase: free all white objects, reset black objects to white. 205 fn sweep(&mut self) { 206 for i in 0..self.heap.len() { 207 let should_free = match &self.heap[i] { 208 Some(entry) => entry.color == Color::White, 209 None => false, 210 }; 211 if should_free { 212 self.heap[i] = None; 213 self.free_list.push(i as u32); 214 self.live_count -= 1; 215 } else if let Some(entry) = &mut self.heap[i] { 216 // Reset black → white for next cycle. 217 entry.color = Color::White; 218 } 219 } 220 } 221} 222 223impl<T: Traceable> Default for Gc<T> { 224 fn default() -> Self { 225 Self::new() 226 } 227} 228 229#[cfg(test)] 230mod tests { 231 use super::*; 232 233 /// A simple test object that holds GcRef children. 234 struct TestObj { 235 children: Vec<GcRef>, 236 } 237 238 impl Traceable for TestObj { 239 fn trace(&self, visitor: &mut dyn FnMut(GcRef)) { 240 for &child in &self.children { 241 visitor(child); 242 } 243 } 244 } 245 246 #[test] 247 fn test_alloc_and_get() { 248 let mut gc = Gc::new(); 249 let r = gc.alloc(TestObj { 250 children: Vec::new(), 251 }); 252 assert!(gc.get(r).is_some()); 253 assert_eq!(gc.stats().live_count, 1); 254 } 255 256 #[test] 257 fn test_collect_unreachable() { 258 let mut gc = Gc::new(); 259 let r1 = gc.alloc(TestObj { 260 children: Vec::new(), 261 }); 262 let r2 = gc.alloc(TestObj { 263 children: Vec::new(), 264 }); 265 assert_eq!(gc.stats().live_count, 2); 266 267 // Collect with no roots — everything is freed. 268 gc.collect(&[]); 269 assert_eq!(gc.stats().live_count, 0); 270 assert!(gc.get(r1).is_none()); 271 assert!(gc.get(r2).is_none()); 272 } 273 274 #[test] 275 fn test_collect_reachable() { 276 let mut gc = Gc::new(); 277 let r1 = gc.alloc(TestObj { 278 children: Vec::new(), 279 }); 280 let r2 = gc.alloc(TestObj { 281 children: Vec::new(), 282 }); 283 assert_eq!(gc.stats().live_count, 2); 284 285 // Only r1 is a root. 286 gc.collect(&[r1]); 287 assert_eq!(gc.stats().live_count, 1); 288 assert!(gc.get(r1).is_some()); 289 assert!(gc.get(r2).is_none()); 290 } 291 292 #[test] 293 fn test_collect_transitive() { 294 let mut gc = Gc::new(); 295 let r1 = gc.alloc(TestObj { 296 children: Vec::new(), 297 }); 298 let r2 = gc.alloc(TestObj { 299 children: Vec::new(), 300 }); 301 let r3 = gc.alloc(TestObj { 302 children: Vec::new(), 303 }); 304 305 // r1 -> r2 -> r3 306 gc.get_mut(r1).unwrap().children.push(r2); 307 gc.get_mut(r2).unwrap().children.push(r3); 308 309 gc.collect(&[r1]); 310 assert_eq!(gc.stats().live_count, 3); 311 assert!(gc.get(r1).is_some()); 312 assert!(gc.get(r2).is_some()); 313 assert!(gc.get(r3).is_some()); 314 } 315 316 #[test] 317 fn test_cycle_collection() { 318 let mut gc = Gc::new(); 319 let r1 = gc.alloc(TestObj { 320 children: Vec::new(), 321 }); 322 let r2 = gc.alloc(TestObj { 323 children: Vec::new(), 324 }); 325 326 // Create a cycle: r1 -> r2 -> r1. 327 gc.get_mut(r1).unwrap().children.push(r2); 328 gc.get_mut(r2).unwrap().children.push(r1); 329 330 // With r1 as root, both survive. 331 gc.collect(&[r1]); 332 assert_eq!(gc.stats().live_count, 2); 333 334 // With no roots, both are freed (cycle is collected). 335 gc.collect(&[]); 336 assert_eq!(gc.stats().live_count, 0); 337 } 338 339 #[test] 340 fn test_free_list_reuse() { 341 let mut gc = Gc::new(); 342 let _r1 = gc.alloc(TestObj { 343 children: Vec::new(), 344 }); 345 let _r2 = gc.alloc(TestObj { 346 children: Vec::new(), 347 }); 348 349 // Free both. 350 gc.collect(&[]); 351 assert_eq!(gc.stats().live_count, 0); 352 assert_eq!(gc.stats().total_slots, 2); 353 354 // Allocate again — should reuse freed slots. 355 let _r3 = gc.alloc(TestObj { 356 children: Vec::new(), 357 }); 358 let _r4 = gc.alloc(TestObj { 359 children: Vec::new(), 360 }); 361 assert_eq!(gc.stats().live_count, 2); 362 assert_eq!(gc.stats().total_slots, 2); // No growth. 363 } 364 365 #[test] 366 fn test_should_collect_threshold() { 367 let mut gc: Gc<TestObj> = Gc::new(); 368 assert!(!gc.should_collect()); 369 370 // Allocate up to threshold. 371 for _ in 0..INITIAL_THRESHOLD { 372 gc.alloc(TestObj { 373 children: Vec::new(), 374 }); 375 } 376 assert!(gc.should_collect()); 377 } 378 379 #[test] 380 fn test_stress() { 381 let mut gc = Gc::new(); 382 let mut live_refs = Vec::new(); 383 384 for i in 0..1000 { 385 let r = gc.alloc(TestObj { 386 children: Vec::new(), 387 }); 388 if i % 3 == 0 { 389 live_refs.push(r); 390 } 391 } 392 393 gc.collect(&live_refs); 394 // Only refs kept in live_refs should survive. 395 assert_eq!(gc.stats().live_count, live_refs.len()); 396 397 for r in &live_refs { 398 assert!(gc.get(*r).is_some()); 399 } 400 } 401}