Skip to main content

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}