shape_vm/executor/gc_integration.rs
1//! Garbage collection integration for the VM
2//!
3//! Without `gc` feature: no-ops (all values use Arc reference counting).
4//! With `gc` feature: real root scanning, safepoint polling, and GC triggering.
5//!
6//! ## Collection strategies (gc feature)
7//!
8//! The VM integrates three collection strategies from `shape-gc`:
9//!
10//! 1. **Generational (Young/Old)**: Uses `collect_young()` / `collect_old()` via
11//! the adaptive scheduler to minimize pause times. Young-gen collections are
12//! fast and frequent; old-gen collections happen predictively.
13//!
14//! 2. **Incremental marking**: When a marking cycle is active (`is_marking()`),
15//! the dispatch loop calls `gc_incremental_mark_step()` every 1024 instructions
16//! to make bounded progress on the gray worklist without stopping the world.
17//!
18//! 3. **Full STW (fallback)**: The original `collect()` path, used when the
19//! scheduler recommends `CollectionType::Full` or when no scheduler is enabled.
20
21use crate::memory::{GCResult, GCStats, GarbageCollector};
22
23/// Garbage collection integration for VirtualMachine
24pub trait GCIntegration {
25 /// Maybe trigger garbage collection based on config
26 fn maybe_collect_garbage(&mut self);
27
28 /// Force garbage collection
29 fn force_gc(&mut self) -> GCResult;
30
31 /// Get GC statistics
32 fn gc_stats(&self) -> GCStats;
33
34 /// Get GC heap size
35 fn gc_heap_size(&self) -> usize;
36
37 /// Get GC object count
38 fn gc_object_count(&self) -> usize;
39
40 /// Access the garbage collector
41 fn gc(&self) -> &GarbageCollector;
42
43 /// Access the garbage collector mutably
44 fn gc_mut(&mut self) -> &mut GarbageCollector;
45}
46
47// --- Non-GC implementation (Arc refcounting) ---
48
49#[cfg(not(feature = "gc"))]
50impl GCIntegration for super::VirtualMachine {
51 fn maybe_collect_garbage(&mut self) {
52 // No-op: Arc reference counting handles memory
53 }
54
55 fn force_gc(&mut self) -> GCResult {
56 // No-op: return empty result
57 GCResult::new(0, 0, std::time::Duration::ZERO)
58 }
59
60 fn gc_stats(&self) -> GCStats {
61 self.gc.stats()
62 }
63
64 fn gc_heap_size(&self) -> usize {
65 self.gc.heap_size()
66 }
67
68 fn gc_object_count(&self) -> usize {
69 self.gc.object_count()
70 }
71
72 fn gc(&self) -> &GarbageCollector {
73 &self.gc
74 }
75
76 fn gc_mut(&mut self) -> &mut GarbageCollector {
77 &mut self.gc
78 }
79}
80
81// --- Real GC implementation ---
82
83#[cfg(feature = "gc")]
84impl GCIntegration for super::VirtualMachine {
85 fn maybe_collect_garbage(&mut self) {
86 self.maybe_collect_gc_adaptive();
87 }
88
89 fn force_gc(&mut self) -> GCResult {
90 let start = std::time::Instant::now();
91 let stats_before = self
92 .gc_heap
93 .as_ref()
94 .map(|h| h.stats().total_collected_bytes)
95 .unwrap_or(0);
96 self.run_gc_collection_full();
97 let stats_after = self
98 .gc_heap
99 .as_ref()
100 .map(|h| h.stats().total_collected_bytes)
101 .unwrap_or(0);
102 let result = GCResult::new(0, stats_after - stats_before, start.elapsed());
103 // Record stats in the GarbageCollector for reporting
104 self.gc.record_collection(&result);
105 result
106 }
107
108 fn gc_stats(&self) -> GCStats {
109 self.gc.stats()
110 }
111
112 fn gc_heap_size(&self) -> usize {
113 self.gc_heap.as_ref().map(|h| h.heap_size()).unwrap_or(0)
114 }
115
116 fn gc_object_count(&self) -> usize {
117 0 // Bump allocator doesn't track individual objects
118 }
119
120 fn gc(&self) -> &GarbageCollector {
121 &self.gc
122 }
123
124 fn gc_mut(&mut self) -> &mut GarbageCollector {
125 &mut self.gc
126 }
127}
128
129#[cfg(feature = "gc")]
130impl super::VirtualMachine {
131 // ── Adaptive collection dispatch ────────────────────────────────────
132
133 /// Use the adaptive scheduler to decide what kind of collection to run.
134 ///
135 /// Prefers generational collection (young/old) when possible, falling back
136 /// to full STW only when the scheduler recommends it or is not configured.
137 fn maybe_collect_gc_adaptive(&mut self) {
138 let Some(ref gc_heap) = self.gc_heap else {
139 return;
140 };
141
142 let collection_type = gc_heap.should_collect_adaptive();
143
144 match collection_type {
145 shape_gc::scheduler::CollectionType::None => {}
146 shape_gc::scheduler::CollectionType::Young => {
147 self.run_gc_collection_young();
148 }
149 shape_gc::scheduler::CollectionType::Old => {
150 self.run_gc_collection_old();
151 }
152 shape_gc::scheduler::CollectionType::Full => {
153 self.run_gc_collection_full();
154 }
155 }
156 }
157
158 // ── Root scanning ──────────────────────────────────────────────────
159
160 /// Collect all GC root pointers into a Vec.
161 ///
162 /// Root categories: stack, module bindings (globals), closure upvalues,
163 /// task scheduler callables/results, uncaught exception.
164 ///
165 /// This collects roots into a flat `Vec<*mut u8>` suitable for passing
166 /// to `collect_young()` / `collect_old()`. Fields are borrowed individually
167 /// to avoid a whole-`self` borrow conflict with `gc_heap`.
168 fn collect_gc_roots(&self) -> Vec<*mut u8> {
169 let mut roots = Vec::with_capacity(self.sp + self.module_bindings.len() + 32);
170
171 // 1. Stack slots [0..sp]
172 for i in 0..self.sp {
173 shape_gc::roots::trace_nanboxed_bits(self.stack[i].raw_bits(), &mut |ptr| {
174 roots.push(ptr);
175 });
176 }
177
178 // 2. Module bindings (global variables)
179 for binding in &self.module_bindings {
180 shape_gc::roots::trace_nanboxed_bits(binding.raw_bits(), &mut |ptr| {
181 roots.push(ptr);
182 });
183 }
184
185 // 3. Call stack — closure upvalues
186 for frame in &self.call_stack {
187 if let Some(ref upvalues) = frame.upvalues {
188 for upvalue in upvalues {
189 // WB2 retain-on-read: `get()` now bumps the refcount —
190 // use `get_raw()` here because this is a read-only
191 // root scan that discards the bits without releasing.
192 let nb = upvalue.get_raw();
193 shape_gc::roots::trace_nanboxed_bits(nb.raw_bits(), &mut |ptr| {
194 roots.push(ptr);
195 });
196 }
197 }
198 }
199
200 // 4. Task scheduler — spawned callables and completed results
201 self.task_scheduler.scan_roots(&mut |ptr| {
202 roots.push(ptr);
203 });
204
205 // 5. Uncaught exception
206 if let Some(ref exc) = self.last_uncaught_exception {
207 shape_gc::roots::trace_nanboxed_bits(exc.raw_bits(), &mut |ptr| {
208 roots.push(ptr);
209 });
210 }
211
212 roots
213 }
214
215 // ── Young-generation collection ────────────────────────────────────
216
217 /// Run a young-generation collection, scanning VM roots.
218 ///
219 /// Collects only young-gen regions + dirty card references. Objects that
220 /// have survived enough young-gen cycles are promoted to old gen.
221 fn run_gc_collection_young(&mut self) {
222 let roots = self.collect_gc_roots();
223 let start = std::time::Instant::now();
224
225 let Some(ref mut gc_heap) = self.gc_heap else {
226 return;
227 };
228
229 let stats = gc_heap.collect_young(&roots);
230 let pause_us = start.elapsed().as_micros() as u64;
231
232 // Record in GarbageCollector stats
233 let result = GCResult::new(
234 stats.objects_collected as u64,
235 stats.bytes_collected as u64,
236 start.elapsed(),
237 );
238 self.gc.record_collection(&result);
239
240 // Record in VmMetrics if enabled
241 if let Some(ref mut metrics) = self.metrics {
242 metrics.record_gc_pause(crate::metrics::GcPauseEvent {
243 collection_type: 0, // Young
244 pause_us,
245 bytes_collected: stats.bytes_collected,
246 bytes_promoted: 0, // Promotion tracking is internal to GenerationalCollector
247 timestamp_us: metrics.elapsed_us(),
248 });
249 }
250 }
251
252 // ── Old-generation collection ──────────────────────────────────────
253
254 /// Run an old-generation collection, scanning VM roots.
255 ///
256 /// Full mark-sweep across both generations. Called less frequently than
257 /// young-gen collection, typically when the adaptive scheduler predicts
258 /// old gen is about to fill up.
259 fn run_gc_collection_old(&mut self) {
260 let roots = self.collect_gc_roots();
261 let start = std::time::Instant::now();
262
263 let Some(ref mut gc_heap) = self.gc_heap else {
264 return;
265 };
266
267 let stats = gc_heap.collect_old(&roots);
268 let pause_us = start.elapsed().as_micros() as u64;
269
270 // Record in GarbageCollector stats
271 let result = GCResult::new(
272 stats.objects_collected as u64,
273 stats.bytes_collected as u64,
274 start.elapsed(),
275 );
276 self.gc.record_collection(&result);
277
278 // Record in VmMetrics if enabled
279 if let Some(ref mut metrics) = self.metrics {
280 metrics.record_gc_pause(crate::metrics::GcPauseEvent {
281 collection_type: 1, // Old
282 pause_us,
283 bytes_collected: stats.bytes_collected,
284 bytes_promoted: 0,
285 timestamp_us: metrics.elapsed_us(),
286 });
287 }
288 }
289
290 // ── Full STW collection (fallback) ─────────────────────────────────
291
292 /// Run a full stop-the-world collection, scanning VM roots.
293 ///
294 /// Root categories: stack, module bindings (globals), closure upvalues,
295 /// task scheduler callables/results, uncaught exception.
296 ///
297 /// Fields are accessed individually to avoid a whole-`self` borrow conflict
298 /// with the mutable borrow of `gc_heap`.
299 fn run_gc_collection_full(&mut self) {
300 let start = std::time::Instant::now();
301
302 let Some(ref mut gc_heap) = self.gc_heap else {
303 return;
304 };
305
306 // Borrow each root source separately to satisfy the borrow checker.
307 // The STW `collect()` API takes a callback, so we cannot use
308 // `collect_gc_roots()` here (it would require borrowing `self`
309 // while `gc_heap` is already mutably borrowed).
310 let stack = &self.stack;
311 let sp = self.sp;
312 let module_bindings = &self.module_bindings;
313 let call_stack = &self.call_stack;
314 let task_scheduler = &self.task_scheduler;
315 let last_uncaught_exception = &self.last_uncaught_exception;
316
317 gc_heap.collect(&mut |visitor| {
318 // 1. Stack slots [0..sp]
319 for i in 0..sp {
320 shape_gc::roots::trace_nanboxed_bits(stack[i].raw_bits(), visitor);
321 }
322
323 // 2. Module bindings (global variables)
324 for binding in module_bindings {
325 shape_gc::roots::trace_nanboxed_bits(binding.raw_bits(), visitor);
326 }
327
328 // 3. Call stack — closure upvalues
329 for frame in call_stack {
330 if let Some(ref upvalues) = frame.upvalues {
331 for upvalue in upvalues {
332 // WB2 retain-on-read: read-only root scan.
333 let nb = upvalue.get_raw();
334 shape_gc::roots::trace_nanboxed_bits(nb.raw_bits(), visitor);
335 }
336 }
337 }
338
339 // 4. Task scheduler — spawned callables and completed results
340 task_scheduler.scan_roots(visitor);
341
342 // 5. Uncaught exception
343 if let Some(exc) = last_uncaught_exception {
344 shape_gc::roots::trace_nanboxed_bits(exc.raw_bits(), visitor);
345 }
346 });
347
348 let pause_us = start.elapsed().as_micros() as u64;
349
350 // Record in VmMetrics if enabled
351 if let Some(ref mut metrics) = self.metrics {
352 metrics.record_gc_pause(crate::metrics::GcPauseEvent {
353 collection_type: 2, // Full
354 pause_us,
355 bytes_collected: 0, // STW collect() doesn't return per-call stats easily
356 bytes_promoted: 0,
357 timestamp_us: metrics.elapsed_us(),
358 });
359 }
360 }
361
362 // ── Incremental marking step ───────────────────────────────────────
363
364 /// Perform a bounded incremental marking step.
365 ///
366 /// Called from the dispatch loop every 1024 instructions when a marking
367 /// cycle is active (`gc_heap.is_marking()`). Processes up to `mark_budget`
368 /// gray objects from the worklist without stopping the world.
369 ///
370 /// When the incremental cycle completes (mark termination + sweep), the
371 /// stats are recorded and the byte counter is reset.
372 pub(crate) fn gc_incremental_mark_step(&mut self) {
373 // Budget: number of gray objects to process per dispatch-loop check.
374 // 64 is a good balance between latency (small pauses) and throughput
375 // (not spending too much time in GC overhead).
376 const MARK_BUDGET: usize = 64;
377
378 // Collect roots upfront. For incremental marking, roots are only
379 // needed on the first call (to start the cycle) — subsequent calls
380 // just process the gray worklist. However, `collect_incremental`
381 // handles this internally: it only scans roots when transitioning
382 // from Idle to Marking phase.
383 let roots = self.collect_gc_roots();
384 let start = std::time::Instant::now();
385
386 let Some(ref mut gc_heap) = self.gc_heap else {
387 return;
388 };
389
390 // Borrow the root vec so it lives long enough for the closure.
391 let roots_ref = &roots;
392
393 let result = gc_heap.collect_incremental(MARK_BUDGET, &mut |visitor| {
394 for &ptr in roots_ref.iter() {
395 visitor(ptr);
396 }
397 });
398
399 if let shape_gc::CollectResult::Complete(stats) = result {
400 let pause_us = start.elapsed().as_micros() as u64;
401
402 // Record in GarbageCollector stats
403 let gc_result = GCResult::new(
404 stats.objects_collected as u64,
405 stats.bytes_collected as u64,
406 start.elapsed(),
407 );
408 self.gc.record_collection(&gc_result);
409
410 // Record in VmMetrics if enabled
411 if let Some(ref mut metrics) = self.metrics {
412 metrics.record_gc_pause(crate::metrics::GcPauseEvent {
413 collection_type: 3, // Incremental (completed cycle)
414 pause_us,
415 bytes_collected: stats.bytes_collected,
416 bytes_promoted: 0,
417 timestamp_us: metrics.elapsed_us(),
418 });
419 }
420 }
421 }
422
423 // ── Safepoint polling ──────────────────────────────────────────────
424
425 /// Poll the GC safepoint. Called at interrupt check points.
426 #[inline(always)]
427 pub(crate) fn gc_safepoint_poll(&self) {
428 if let Some(ref gc_heap) = self.gc_heap {
429 shape_gc::safepoint::safepoint_poll(gc_heap.safepoint());
430 }
431 }
432}