Skip to main content

cljrs_gc/
lib.rs

1//! Garbage collector (default) or region-based allocator (`no-gc` feature) for clojurust.
2
3#![allow(clippy::missing_safety_doc)]
4#![allow(private_interfaces)]
5
6use std::ptr::NonNull;
7
8pub mod region;
9pub mod stats;
10
11#[cfg(not(feature = "no-gc"))]
12pub mod cancellation;
13#[cfg(not(feature = "no-gc"))]
14pub mod config;
15
16#[cfg(feature = "no-gc")]
17pub mod alloc_ctx;
18#[cfg(feature = "no-gc")]
19pub mod static_arena;
20
21pub use stats::{CLJRS_GC_STATS_ENV, GC_STATS, GcStats, GcStatsSnapshot, dump_stats_from_env};
22
23// ── Re-exports from active implementation ─────────────────────────────────────
24
25#[cfg(not(feature = "no-gc"))]
26pub use cancellation::{
27    MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested, park_thread,
28    register_mutator, registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
29    wait_for_threads_to_park,
30};
31#[cfg(not(feature = "no-gc"))]
32pub use config::{GC_CANCELLATION as CONFIG_CANCELLATION, GcConfig, GcParked};
33
34#[cfg(not(feature = "no-gc"))]
35pub use gc_full::{
36    AllocRootGuard, GcHeap, HEAP, HeapProxy, push_alloc_frame, trace_thread_alloc_roots,
37};
38#[cfg(feature = "no-gc")]
39pub use nogc_stubs::{
40    AllocRootGuard, CONFIG_CANCELLATION, GcConfig, GcHeap, GcParked, HEAP, MutatorGuard, StwGuard,
41    begin_stw, check_cancellation, gc_requested, park_thread, push_alloc_frame, register_mutator,
42    registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
43    wait_for_threads_to_park,
44};
45
46/// Return `true` if `addr` was allocated by the global `StaticArena`.
47///
48/// Available only in `no-gc` debug builds.  Downstream crates (`cljrs-value`)
49/// use this to implement write-site provenance assertions.
50#[cfg(all(feature = "no-gc", debug_assertions))]
51pub fn is_static_addr(addr: usize) -> bool {
52    static_arena::is_static_addr(addr)
53}
54
55// ── Trace trait ───────────────────────────────────────────────────────────────
56
57/// Implemented by every type that can be stored behind a [`GcPtr`].
58///
59/// The `gc_size_extra` method accounts for heap bytes owned by the value that
60/// are NOT captured by `size_of::<GcBox<T>>()` (e.g. `Vec` buffers, `String`
61/// capacity, `Form` AST trees stored inline).  The default returns 0, which is
62/// correct for primitives and types with no out-of-line heap.
63///
64/// Rules for implementors of `gc_size_extra`:
65/// - Count only bytes THIS value owns and will free when dropped.
66/// - Do NOT cross `GcPtr` boundaries — each pointed-to box is counted
67///   separately when it is allocated.
68pub trait Trace {
69    fn trace(&self, visitor: &mut MarkVisitor);
70
71    fn gc_size_extra(&self) -> usize {
72        0
73    }
74}
75
76// ── Leaf Trace impls ──────────────────────────────────────────────────────────
77
78impl Trace for String {
79    fn trace(&self, _: &mut MarkVisitor) {}
80
81    fn gc_size_extra(&self) -> usize {
82        self.capacity()
83    }
84}
85impl Trace for i64 {
86    fn trace(&self, _: &mut MarkVisitor) {}
87}
88impl Trace for f64 {
89    fn trace(&self, _: &mut MarkVisitor) {}
90}
91impl Trace for bool {
92    fn trace(&self, _: &mut MarkVisitor) {}
93}
94impl Trace for num_bigint::BigInt {
95    fn trace(&self, _: &mut MarkVisitor) {}
96}
97impl Trace for bigdecimal::BigDecimal {
98    fn trace(&self, _: &mut MarkVisitor) {}
99}
100impl Trace for num_rational::Ratio<num_bigint::BigInt> {
101    fn trace(&self, _: &mut MarkVisitor) {}
102}
103impl Trace for regex::Regex {
104    fn trace(&self, _: &mut MarkVisitor) {}
105}
106macro_rules! impl_trace_prim_array {
107    ($t:ty) => {
108        impl Trace for std::sync::Mutex<Vec<$t>> {
109            fn trace(&self, _: &mut MarkVisitor) {}
110            fn gc_size_extra(&self) -> usize {
111                self.lock().unwrap().capacity() * std::mem::size_of::<$t>()
112            }
113        }
114    };
115}
116impl_trace_prim_array!(i32);
117impl_trace_prim_array!(i64);
118impl_trace_prim_array!(i16);
119impl_trace_prim_array!(i8);
120impl_trace_prim_array!(char);
121impl_trace_prim_array!(f64);
122impl_trace_prim_array!(f32);
123impl_trace_prim_array!(bool);
124
125// ── GcVisitor ─────────────────────────────────────────────────────────────────
126
127pub trait GcVisitor {
128    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>);
129}
130
131// =============================================================================
132// GC build: GcBox with header, MarkVisitor with grey stack
133// =============================================================================
134
135#[cfg(not(feature = "no-gc"))]
136pub use self::gc_header::{GcBox, GcBoxHeader};
137
138#[cfg(not(feature = "no-gc"))]
139mod gc_header {
140    use crate::{MarkVisitor, Trace};
141    use std::cell::Cell;
142
143    // Objects start at lives = GC_INITIAL_LIVES - 1.  The mark phase sets
144    // lives = GC_INITIAL_LIVES for reachable objects; sweep frees objects
145    // whose lives reach 0.  A value of 2 gives exactly one cycle of grace:
146    // enough to cover the window between an alloc frame dropping and the
147    // next GC safepoint (where VALUE_ROOTS or a new alloc frame re-roots it).
148    // 10 was chosen conservatively but keeps 9× more garbage in RAM than
149    // necessary, worsening OOM pressure under test suites with many forms.
150    pub(crate) const GC_INITIAL_LIVES: u8 = 2;
151
152    #[cfg(debug_assertions)]
153    pub(crate) const GC_MAGIC_ALIVE: u64 = 0xCAFE_BABE_DEAD_BEEF;
154    #[cfg(debug_assertions)]
155    pub(crate) const GC_MAGIC_FREED: u64 = 0xDEAD_DEAD_DEAD_DEAD;
156
157    #[repr(C)]
158    pub struct GcBoxHeader {
159        #[cfg(debug_assertions)]
160        pub(crate) magic: Cell<u64>,
161        /// Exact size of the GcBox<T> allocation in bytes.
162        pub(crate) size: usize,
163        pub(crate) lives: Cell<u8>,
164        pub(crate) next: Cell<*mut GcBoxHeader>,
165        pub(crate) trace_fn: unsafe fn(*const GcBoxHeader, &mut MarkVisitor),
166        pub(crate) drop_fn: unsafe fn(*mut GcBoxHeader),
167    }
168
169    impl GcBoxHeader {
170        pub(crate) fn new<T: Trace + 'static>(heap_extra: usize) -> Self {
171            Self {
172                #[cfg(debug_assertions)]
173                magic: Cell::new(GC_MAGIC_ALIVE),
174                size: std::mem::size_of::<GcBox<T>>() + heap_extra,
175                lives: Cell::new(GC_INITIAL_LIVES - 1),
176                next: Cell::new(std::ptr::null_mut()),
177                trace_fn: trace_gc_box::<T>,
178                drop_fn: drop_gc_box::<T>,
179            }
180        }
181    }
182
183    unsafe impl Send for GcBoxHeader {}
184    unsafe impl Sync for GcBoxHeader {}
185
186    #[repr(C)]
187    pub struct GcBox<T: Trace + 'static> {
188        pub(crate) header: GcBoxHeader,
189        pub value: T,
190    }
191
192    pub(crate) unsafe fn trace_gc_box<T: Trace + 'static>(
193        header: *const GcBoxHeader,
194        visitor: &mut MarkVisitor,
195    ) {
196        unsafe {
197            let gc_box = header as *const GcBox<T>;
198            (*gc_box).value.trace(visitor);
199        }
200    }
201
202    pub(crate) unsafe fn drop_gc_box<T: Trace + 'static>(header: *mut GcBoxHeader) {
203        unsafe {
204            #[cfg(debug_assertions)]
205            {
206                (*header).magic.set(GC_MAGIC_FREED);
207            }
208            let gc_box = header as *mut GcBox<T>;
209            drop(Box::from_raw(gc_box));
210        }
211    }
212}
213
214// =============================================================================
215// no-gc build: GcBox without header
216// =============================================================================
217
218#[cfg(feature = "no-gc")]
219pub use self::nogc_box::GcBox;
220
221#[cfg(feature = "no-gc")]
222mod nogc_box {
223    use crate::Trace;
224
225    pub struct GcBox<T: Trace + 'static> {
226        pub value: T,
227    }
228}
229
230// =============================================================================
231// MarkVisitor: full under GC, stub under no-gc
232// =============================================================================
233
234#[cfg(not(feature = "no-gc"))]
235#[derive(Default)]
236pub struct MarkVisitor {
237    pub(crate) grey: Vec<*mut GcBoxHeader>,
238}
239
240#[cfg(feature = "no-gc")]
241pub struct MarkVisitor;
242
243#[cfg(not(feature = "no-gc"))]
244impl MarkVisitor {
245    pub fn new() -> Self {
246        Self::default()
247    }
248
249    pub fn grey_len(&self) -> usize {
250        self.grey.len()
251    }
252
253    pub unsafe fn mark_header(&mut self, header: *mut GcBoxHeader) {
254        use gc_header::GC_INITIAL_LIVES;
255        let h = unsafe { &*header };
256        if h.lives.get() < GC_INITIAL_LIVES {
257            h.lives.set(GC_INITIAL_LIVES);
258            self.grey.push(header);
259        }
260    }
261
262    pub(crate) fn drain(&mut self) {
263        let mut visited = 0usize;
264        while let Some(header) = self.grey.pop() {
265            visited += 1;
266            let h = unsafe { &*header };
267            unsafe { (h.trace_fn)(header as *const GcBoxHeader, self) };
268        }
269        tracing::debug!(target: "gc", "drain visited {} objects", visited);
270    }
271}
272
273#[cfg(not(feature = "no-gc"))]
274impl GcVisitor for MarkVisitor {
275    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
276        use gc_header::GC_INITIAL_LIVES;
277        // Region-allocated objects are not on the GC heap; their lifetime is
278        // bounded by the enclosing `Region`, not the collector.  Never
279        // dereference one here: once the region's scope ends its memory is
280        // freed/reused, so the header would be garbage and we'd follow a
281        // dangling `trace_fn`.  Live regions are traced as roots separately
282        // (see `region::trace_active_regions`), so a region's heap-allocated
283        // children are still kept alive.
284        if ptr.is_region_alloc() {
285            return;
286        }
287        let raw = ptr.raw();
288        let header = unsafe { &(*raw).header };
289        if header.lives.get() < GC_INITIAL_LIVES {
290            header.lives.set(GC_INITIAL_LIVES);
291            self.grey.push(raw as *mut GcBoxHeader);
292        }
293    }
294}
295
296#[cfg(feature = "no-gc")]
297impl MarkVisitor {
298    pub fn grey_len(&self) -> usize {
299        0
300    }
301    pub unsafe fn mark_header(&mut self, _: *mut u8) {}
302}
303
304#[cfg(feature = "no-gc")]
305impl GcVisitor for MarkVisitor {
306    fn visit<T: Trace + 'static>(&mut self, _: &GcPtr<T>) {}
307}
308
309// =============================================================================
310// GcPtr — always present
311// =============================================================================
312
313pub struct GcPtr<T: Trace + 'static>(NonNull<GcBox<T>>);
314
315/// Low pointer bit reserved to mark region-allocated `GcPtr`s in GC builds.
316///
317/// `GcBox<T>` (a `GcBoxHeader` followed by the value) is always ≥8-byte
318/// aligned in GC builds, so bit 0 is free.  Region-allocated pointers set it;
319/// GC-heap pointers leave it clear.  This lets the mark phase distinguish a
320/// region object from a heap object **without dereferencing it** — essential,
321/// because a region whose scope has ended leaves dangling pointers whose
322/// headers point at freed (or reused) memory.
323#[cfg(not(feature = "no-gc"))]
324pub(crate) const REGION_PTR_TAG: usize = 1;
325
326impl<T: Trace + 'static> GcPtr<T> {
327    #[cfg(not(feature = "no-gc"))]
328    pub fn new(value: T) -> Self {
329        gc_full::HEAP.alloc(value)
330    }
331
332    #[cfg(feature = "no-gc")]
333    pub fn new(value: T) -> Self {
334        alloc_ctx::alloc_in_ctx(value)
335    }
336
337    /// The untagged `GcBox<T>` address.  In GC builds this masks off the
338    /// region-provenance tag bit; in no-gc builds pointers are never tagged.
339    #[inline]
340    fn raw(&self) -> *mut GcBox<T> {
341        #[cfg(not(feature = "no-gc"))]
342        {
343            (self.0.as_ptr() as usize & !REGION_PTR_TAG) as *mut GcBox<T>
344        }
345        #[cfg(feature = "no-gc")]
346        {
347            self.0.as_ptr()
348        }
349    }
350
351    /// `true` if this pointer was bump-allocated in a [`region::Region`]
352    /// rather than the GC heap.  Region objects are not GC-managed.
353    #[cfg(not(feature = "no-gc"))]
354    #[inline]
355    pub fn is_region_alloc(&self) -> bool {
356        (self.0.as_ptr() as usize & REGION_PTR_TAG) != 0
357    }
358
359    /// Construct a region-tagged pointer from a raw `GcBox<T>` allocated in a
360    /// bump region.
361    ///
362    /// # Safety
363    /// `raw` must be a valid, non-null, ≥8-aligned `GcBox<T>` whose header was
364    /// initialised by [`region::Region::alloc`].
365    #[cfg(not(feature = "no-gc"))]
366    #[inline]
367    pub(crate) unsafe fn from_region_raw(raw: *mut GcBox<T>) -> Self {
368        GcPtr(unsafe { NonNull::new_unchecked((raw as usize | REGION_PTR_TAG) as *mut GcBox<T>) })
369    }
370
371    pub fn get(&self) -> &T {
372        #[cfg(all(debug_assertions, not(feature = "no-gc")))]
373        {
374            use gc_header::GC_MAGIC_ALIVE;
375            let header = unsafe { &(*self.raw()).header };
376            assert_eq!(
377                header.magic.get(),
378                GC_MAGIC_ALIVE,
379                "GcPtr::get() on freed object! magic={:#x}",
380                header.magic.get(),
381            );
382        }
383        unsafe { &(*self.raw()).value }
384    }
385
386    pub fn get_mut(&mut self) -> &mut T {
387        #[cfg(all(debug_assertions, not(feature = "no-gc")))]
388        {
389            use gc_header::GC_MAGIC_ALIVE;
390            let header = unsafe { &(*self.raw()).header };
391            assert_eq!(
392                header.magic.get(),
393                GC_MAGIC_ALIVE,
394                "GcPtr::get_mut() on freed object! magic={:#x}",
395                header.magic.get(),
396            );
397        }
398        unsafe { &mut (*self.raw()).value }
399    }
400
401    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
402        a.0 == b.0
403    }
404
405    /// Return `true` if this pointer was allocated by the global `StaticArena`.
406    ///
407    /// Only meaningful (and only compiled) in `no-gc` debug builds.  Used by
408    /// write-site assertions in `Atom::reset` / `Var::bind` to catch
409    /// region-local values being stored in program-lifetime containers.
410    #[cfg(all(feature = "no-gc", debug_assertions))]
411    pub fn is_static_alloc(&self) -> bool {
412        alloc_ctx::invocation_is_active() || static_arena::is_static_addr(self.0.as_ptr() as usize)
413    }
414}
415
416impl<T: Trace + 'static> Clone for GcPtr<T> {
417    fn clone(&self) -> Self {
418        GcPtr(self.0)
419    }
420}
421
422impl<T: Trace + 'static + std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
423    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424        unsafe { (*self.raw()).value.fmt(f) }
425    }
426}
427
428impl<T: Trace + 'static> Drop for GcPtr<T> {
429    fn drop(&mut self) {}
430}
431
432// =============================================================================
433// StaticGcPtr — Send+Sync pointer to program-lifetime data
434// =============================================================================
435
436/// A raw pointer to a value that lives for the entire program lifetime.
437///
438/// Backed by the global `StaticArena` (in `no-gc` builds) or by `Box::leak`
439/// (in GC builds).  Either way the pointee is never freed and never moved, so
440/// it is safe to share across isolate threads.
441///
442/// `StaticGcPtr<T>` wraps `*const T` — it does **not** involve a `GcBox`
443/// header — so it is independent of the GC build mode and carries no GC
444/// overhead.
445pub struct StaticGcPtr<T: 'static>(NonNull<T>);
446
447// SAFETY: program-lifetime allocations are never moved, freed, or mutated
448// after the initial write.  The stored types (Keyword, Symbol, …) are
449// themselves `Sync` (no unsynchronised interior mutability).
450unsafe impl<T: 'static> Send for StaticGcPtr<T> {}
451unsafe impl<T: 'static> Sync for StaticGcPtr<T> {}
452
453impl<T: 'static> StaticGcPtr<T> {
454    /// Borrow the contained value.
455    pub fn get(&self) -> &T {
456        // SAFETY: pointer is program-lifetime, always valid.
457        unsafe { self.0.as_ref() }
458    }
459
460    /// Pointer equality: `true` iff both `StaticGcPtr`s point to the exact
461    /// same allocation (i.e. the same interned entry).
462    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
463        a.0 == b.0
464    }
465}
466
467impl<T: 'static> Clone for StaticGcPtr<T> {
468    fn clone(&self) -> Self {
469        StaticGcPtr(self.0)
470    }
471}
472
473impl<T: 'static + std::fmt::Debug> std::fmt::Debug for StaticGcPtr<T> {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        unsafe { self.0.as_ref().fmt(f) }
476    }
477}
478
479/// Allocate `value` as program-lifetime memory and return a [`StaticGcPtr`].
480///
481/// In `no-gc` builds the allocation comes from the global bump-allocated
482/// `StaticArena` (never freed, no GC header overhead).  In GC builds
483/// `Box::leak` is used instead — the memory lives until the process exits.
484pub fn static_alloc<T: 'static>(value: T) -> StaticGcPtr<T> {
485    #[cfg(feature = "no-gc")]
486    {
487        static_arena::static_alloc_val(value)
488    }
489    #[cfg(not(feature = "no-gc"))]
490    {
491        StaticGcPtr(NonNull::from(Box::leak(Box::new(value))))
492    }
493}
494
495// =============================================================================
496// Full GC implementation (default build)
497// =============================================================================
498
499#[cfg(not(feature = "no-gc"))]
500mod gc_full {
501    use std::cell::RefCell;
502    use std::ptr::NonNull;
503    use std::sync::atomic::{AtomicUsize, Ordering};
504    use std::sync::{Arc, Mutex};
505
506    use crate::config::GcConfig;
507    use crate::gc_header::GC_INITIAL_LIVES;
508    use crate::{GcBox, GcBoxHeader, GcPtr, MarkVisitor, Trace};
509
510    type RootTracer = Box<dyn Fn(&mut MarkVisitor)>;
511
512    pub struct GcHeap {
513        inner: Mutex<GcHeapInner>,
514        config: Mutex<Option<Arc<GcConfig>>>,
515        memory_in_use: AtomicUsize,
516        total_allocated_bytes: AtomicUsize,
517        root_tracers: Mutex<Vec<RootTracer>>,
518        gc_suppressed: std::sync::atomic::AtomicBool,
519        /// memory_in_use threshold above which GC is re-enabled after a
520        /// zero-yield collection.  The headroom doubles on each consecutive
521        /// zero-yield cycle (exponential backoff, capped at soft_limit) so a
522        /// long computation where all objects are live doesn't spin in a
523        /// constant GC storm of O(N) sweeps.  Resets to the base headroom
524        /// (soft_limit / 10) once GC actually frees something.
525        suppressed_threshold: AtomicUsize,
526        /// Current headroom used for exponential backoff after zero-yield cycles.
527        zero_yield_headroom: AtomicUsize,
528    }
529
530    struct GcHeapInner {
531        head: *mut GcBoxHeader,
532        count: usize,
533        total_allocated: usize,
534        total_freed: usize,
535    }
536
537    unsafe impl Send for GcHeapInner {}
538
539    impl GcHeapInner {
540        const fn new() -> Self {
541            Self {
542                head: std::ptr::null_mut(),
543                count: 0,
544                total_allocated: 0,
545                total_freed: 0,
546            }
547        }
548    }
549
550    unsafe impl Sync for GcHeap {}
551
552    impl Default for GcHeap {
553        fn default() -> Self {
554            Self::new()
555        }
556    }
557
558    /// Parse a megabyte limit from a (raw) environment value into a byte count,
559    /// falling back to `default` (and warning) on malformed input rather than
560    /// panicking on user misconfiguration. `value` is `None` when the variable
561    /// is unset. Saturates instead of overflowing on absurdly large values.
562    pub(crate) fn parse_limit_mb(var: &str, value: Option<&str>, default: usize) -> usize {
563        match value {
564            Some(s) => match s.trim().parse::<usize>() {
565                Ok(mb) => mb.saturating_mul(1024 * 1024),
566                Err(_) => {
567                    eprintln!(
568                        "[gc] warning: ignoring invalid {var}={s:?} (expected a number of megabytes)"
569                    );
570                    default
571                }
572            },
573            None => default,
574        }
575    }
576
577    impl GcHeap {
578        pub const fn new() -> Self {
579            Self {
580                inner: Mutex::new(GcHeapInner::new()),
581                config: Mutex::new(None),
582                memory_in_use: AtomicUsize::new(0),
583                total_allocated_bytes: AtomicUsize::new(0),
584                root_tracers: Mutex::new(Vec::new()),
585                gc_suppressed: std::sync::atomic::AtomicBool::new(false),
586                suppressed_threshold: AtomicUsize::new(0),
587                zero_yield_headroom: AtomicUsize::new(0),
588            }
589        }
590
591        pub fn set_config(&self, config: Arc<GcConfig>) {
592            *self.config.lock().unwrap() = Some(config);
593        }
594
595        pub fn set_config_from_env(&self) {
596            #[cfg(not(target_arch = "wasm32"))]
597            let default_soft_limit: usize = (system_memory::total() / 3) as usize;
598            #[cfg(target_arch = "wasm32")]
599            let default_soft_limit: usize = 64 * 1024 * 1024;
600
601            let soft_limit_mb = parse_limit_mb(
602                "CLJRS_GC_SOFT_LIMIT_MB",
603                std::env::var("CLJRS_GC_SOFT_LIMIT_MB").ok().as_deref(),
604                default_soft_limit,
605            );
606            let hard_limit_mb = parse_limit_mb(
607                "CLJRS_GC_HARD_LIMIT_MB",
608                std::env::var("CLJRS_GC_HARD_LIMIT_MB").ok().as_deref(),
609                soft_limit_mb,
610            );
611            self.set_config(Arc::new(GcConfig::with_limits(
612                soft_limit_mb,
613                hard_limit_mb,
614            )));
615        }
616
617        pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
618            self.root_tracers.lock().unwrap().push(Box::new(tracer));
619        }
620
621        pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
622            let tracers = self.root_tracers.lock().unwrap();
623            for tracer in tracers.iter() {
624                tracer(visitor);
625            }
626        }
627
628        pub fn memory_in_use(&self) -> usize {
629            self.memory_in_use.load(Ordering::Relaxed)
630        }
631
632        #[cfg(test)]
633        pub fn set_memory_in_use(&self, bytes: usize) {
634            self.memory_in_use.store(bytes, Ordering::Relaxed);
635        }
636
637        pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
638            crate::cancellation::safepoint();
639            let heap_extra = value.gc_size_extra();
640            let gc_box = Box::new(GcBox {
641                header: GcBoxHeader::new::<T>(heap_extra),
642                value,
643            });
644            let obj_size = gc_box.header.size; // GcBox<T> size + gc_size_extra()
645            let raw: *mut GcBox<T> = Box::into_raw(gc_box);
646            {
647                let mut inner = self.inner.lock().unwrap();
648                unsafe {
649                    (*raw).header.next.set(inner.head);
650                    inner.head = raw as *mut GcBoxHeader;
651                }
652                inner.count += 1;
653                inner.total_allocated += 1;
654            }
655            self.total_allocated_bytes
656                .fetch_add(obj_size, Ordering::Relaxed);
657            crate::stats::GC_STATS.record_gc_alloc(obj_size);
658            let current_usage =
659                self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
660
661            if let Some(config) = self.config.lock().unwrap().as_ref()
662                && config.soft_limit_exceeded(current_usage)
663            {
664                if self.gc_suppressed.load(Ordering::Relaxed) {
665                    // Suppression active: only re-enable GC once memory has
666                    // grown past the threshold set by the last zero-yield
667                    // collection (current_memory + soft_limit/10).
668                    let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
669                    if current_usage > threshold {
670                        self.gc_suppressed.store(false, Ordering::Relaxed);
671                        crate::cancellation::request_gc();
672                    }
673                } else {
674                    crate::cancellation::request_gc();
675                }
676            }
677
678            register_alloc(raw as *mut GcBoxHeader);
679            GcPtr(unsafe { NonNull::new_unchecked(raw) })
680        }
681
682        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
683            let pre_count = self.inner.lock().unwrap().count;
684            let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
685            tracing::debug!(
686                target: "gc",
687                "starting collection: {} objects, ~{} bytes in use",
688                pre_count,
689                pre_memory
690            );
691            let mark_start = std::time::Instant::now();
692            let mut visitor = MarkVisitor::new();
693            trace_roots(&mut visitor);
694            // Active bump regions are additional roots: a live region object
695            // may hold `GcPtr`s into the heap, and those heap objects must not
696            // be collected while the region can still reach them.  The mark
697            // phase skips region objects themselves (they are not heap-managed),
698            // so we trace their children here instead.
699            crate::region::trace_active_regions(&mut visitor);
700            // Retired (poisoned) regions are immortal roots: their objects may
701            // still be referenced and may hold `GcPtr`s into the heap.
702            crate::region::trace_retired_regions(&mut visitor);
703            tracing::debug!(
704                target: "gc",
705                "starting drain with {} grey objects",
706                visitor.grey.len()
707            );
708            visitor.drain();
709            let mark_elapsed = mark_start.elapsed();
710
711            let sweep_start = std::time::Instant::now();
712            let mut inner = self.inner.lock().unwrap();
713            let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
714            let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
715            // Bytes of objects with lives==0 that will be freed now.
716            let mut freed_bytes: usize = 0;
717            let mut current = inner.head;
718            while !current.is_null() {
719                let header = unsafe { &*current };
720                let next = header.next.get();
721                let lives = header.lives.get();
722                let obj_size = header.size;
723                if lives >= GC_INITIAL_LIVES {
724                    // Marked reachable this cycle — reset grace counter.
725                    header.lives.set(GC_INITIAL_LIVES - 1);
726                    live.push(current);
727                } else if lives > 0 {
728                    // In grace period (unreachable but not yet freed).
729                    header.lives.set(lives - 1);
730                    live.push(current);
731                } else {
732                    // Grace period exhausted — collect now.
733                    freed_bytes += obj_size;
734                    dead.push(current);
735                }
736                current = next;
737            }
738            let freed_count = dead.len();
739            for ptr in dead {
740                let header = unsafe { &*ptr };
741                unsafe { (header.drop_fn)(ptr) };
742                inner.count -= 1;
743                inner.total_freed += 1;
744            }
745            inner.head = std::ptr::null_mut();
746            for ptr in live {
747                let header = unsafe { &*ptr };
748                header.next.set(inner.head);
749                inner.head = ptr;
750            }
751            // Decrement memory_in_use by the bytes actually freed.  All heap
752            // objects (live + grace-period) remain counted; only physically
753            // freed objects are subtracted.  This keeps memory pressure
754            // accurate so GC fires again when the heap genuinely grows.
755            self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
756            let sweep_elapsed = sweep_start.elapsed();
757            crate::stats::GC_STATS.record_gc_pause(
758                mark_elapsed + sweep_elapsed,
759                freed_count as u64,
760                freed_bytes as u64,
761            );
762            let post_memory = self.memory_in_use.load(Ordering::Relaxed);
763            tracing::debug!(
764                target: "gc",
765                "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
766                freed_count,
767                freed_bytes,
768                inner.count,
769                post_memory,
770                mark_elapsed,
771                sweep_elapsed
772            );
773            if freed_count == 0 {
774                // Zero-yield collection: exponential-backoff suppression.
775                // Each consecutive zero-yield cycle doubles the headroom before
776                // the next GC attempt (capped at soft_limit/4).  This prevents a
777                // GC storm during deep recursion where all objects are live —
778                // without backoff, GC fires every soft_limit/10 bytes, tracing
779                // the entire live set O(N) times to no benefit.
780                // The headroom resets to soft_limit/10 when GC frees something.
781                // Cap at soft_limit/4 (not soft_limit) so that GC still fires
782                // frequently enough to catch short-lived test allocations after
783                // a long namespace-loading phase of zero-yield cycles.
784                let soft_limit = self
785                    .config
786                    .lock()
787                    .unwrap()
788                    .as_ref()
789                    .map(|c| c.soft_limit())
790                    .unwrap_or(64 * 1024 * 1024);
791                let base_headroom = (soft_limit / 10).max(1);
792                let max_headroom = (soft_limit / 4).max(base_headroom);
793                let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
794                let headroom = if prev_headroom == 0 {
795                    base_headroom
796                } else {
797                    prev_headroom.saturating_mul(2).min(max_headroom)
798                };
799                self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
800                self.suppressed_threshold
801                    .store(post_memory + headroom, Ordering::Relaxed);
802                self.gc_suppressed.store(true, Ordering::Relaxed);
803            } else {
804                // GC freed something: reset exponential backoff.
805                self.zero_yield_headroom.store(0, Ordering::Relaxed);
806                self.gc_suppressed.store(false, Ordering::Relaxed);
807            }
808        }
809
810        pub fn count(&self) -> usize {
811            self.inner.lock().unwrap().count
812        }
813        pub fn total_allocated(&self) -> usize {
814            self.inner.lock().unwrap().total_allocated
815        }
816        pub fn total_freed(&self) -> usize {
817            self.inner.lock().unwrap().total_freed
818        }
819
820        pub fn collect_auto(&self) -> bool {
821            tracing::debug!(target: "gc", "automatic collection requested");
822            let Some(_stw_guard) = crate::cancellation::begin_stw() else {
823                tracing::debug!(target: "gc", "automatic collection skipped");
824                return false;
825            };
826            self.collect(|visitor| self.trace_registered_roots(visitor));
827            true
828        }
829    }
830
831    thread_local! {
832        static ISOLATE_HEAP: GcHeap = const { GcHeap::new() };
833    }
834
835    /// Zero-sized proxy that dispatches all heap operations to the calling
836    /// thread's [`GcHeap`] via the `ISOLATE_HEAP` thread-local.
837    ///
838    /// This means every isolate (OS thread) owns an independent heap; GC runs
839    /// fully in parallel on different threads with no cross-isolate coordination.
840    pub struct HeapProxy;
841
842    impl HeapProxy {
843        pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
844            ISOLATE_HEAP.with(|h| h.alloc(value))
845        }
846
847        pub fn set_config(&self, config: Arc<GcConfig>) {
848            ISOLATE_HEAP.with(|h| h.set_config(config));
849        }
850
851        pub fn set_config_from_env(&self) {
852            ISOLATE_HEAP.with(|h| h.set_config_from_env());
853        }
854
855        pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
856            ISOLATE_HEAP.with(|h| h.register_root_tracer(tracer));
857        }
858
859        pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
860            ISOLATE_HEAP.with(|h| h.trace_registered_roots(visitor));
861        }
862
863        pub fn memory_in_use(&self) -> usize {
864            ISOLATE_HEAP.with(|h| h.memory_in_use())
865        }
866
867        pub fn count(&self) -> usize {
868            ISOLATE_HEAP.with(|h| h.count())
869        }
870
871        pub fn total_allocated(&self) -> usize {
872            ISOLATE_HEAP.with(|h| h.total_allocated())
873        }
874
875        pub fn total_freed(&self) -> usize {
876            ISOLATE_HEAP.with(|h| h.total_freed())
877        }
878
879        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
880            ISOLATE_HEAP.with(|h| h.collect(trace_roots));
881        }
882
883        pub fn collect_auto(&self) -> bool {
884            ISOLATE_HEAP.with(|h| h.collect_auto())
885        }
886
887        #[cfg(test)]
888        pub fn set_memory_in_use(&self, bytes: usize) {
889            ISOLATE_HEAP.with(|h| h.set_memory_in_use(bytes));
890        }
891    }
892
893    // SAFETY: HeapProxy is zero-sized; all state lives in a thread-local GcHeap.
894    // The Send + Sync impls are needed so `pub static HEAP: HeapProxy` is valid.
895    unsafe impl Sync for HeapProxy {}
896
897    pub static HEAP: HeapProxy = HeapProxy;
898
899    thread_local! {
900        pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
901    }
902
903    pub struct AllocRootGuard {
904        saved_len: usize,
905    }
906
907    impl Drop for AllocRootGuard {
908        fn drop(&mut self) {
909            ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
910        }
911    }
912
913    pub fn push_alloc_frame() -> AllocRootGuard {
914        let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
915        AllocRootGuard { saved_len }
916    }
917
918    fn register_alloc(header: *mut GcBoxHeader) {
919        ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
920    }
921
922    pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
923        ALLOC_ROOTS.with(|roots| {
924            let roots = roots.borrow();
925            for &header in roots.iter() {
926                unsafe { visitor.mark_header(header) };
927            }
928        });
929    }
930}
931
932// =============================================================================
933// no-gc stubs
934// =============================================================================
935
936#[cfg(feature = "no-gc")]
937mod nogc_stubs {
938    use crate::MarkVisitor;
939    use std::sync::Arc;
940
941    #[derive(Debug, Clone)]
942    pub struct GcConfig;
943    impl GcConfig {
944        pub fn new() -> Self {
945            Self
946        }
947        pub fn with_hard_limit(_: usize) -> Self {
948            Self
949        }
950        pub fn with_limits(_: usize, _: usize) -> Self {
951            Self
952        }
953    }
954    impl Default for GcConfig {
955        fn default() -> Self {
956            Self::new()
957        }
958    }
959
960    pub struct GcHeap;
961    impl Default for GcHeap {
962        fn default() -> Self {
963            Self::new()
964        }
965    }
966    impl GcHeap {
967        pub const fn new() -> Self {
968            Self
969        }
970        pub fn set_config(&self, _: Arc<GcConfig>) {}
971        /// No-op counterpart of the GC build's env-driven configuration.
972        /// Region allocation has no soft limit to configure, but the runtime
973        /// builder calls this unconditionally, so the method must exist in
974        /// both builds.
975        pub fn set_config_from_env(&self) {}
976        pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + 'static) {}
977        pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
978        pub fn memory_in_use(&self) -> usize {
979            0
980        }
981        pub fn count(&self) -> usize {
982            0
983        }
984        pub fn total_allocated(&self) -> usize {
985            0
986        }
987        pub fn total_freed(&self) -> usize {
988            0
989        }
990        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
991        pub fn collect_auto(&self) -> bool {
992            false
993        }
994    }
995    unsafe impl Sync for GcHeap {}
996    pub static HEAP: GcHeap = GcHeap::new();
997
998    pub struct MutatorGuard;
999    impl Drop for MutatorGuard {
1000        fn drop(&mut self) {}
1001    }
1002    pub struct StwGuard;
1003    impl Drop for StwGuard {
1004        fn drop(&mut self) {}
1005    }
1006    pub struct GcParked;
1007
1008    pub struct GcCancellationStub;
1009    impl GcCancellationStub {
1010        pub const fn new() -> Self {
1011            Self
1012        }
1013        pub fn in_progress(&self) -> bool {
1014            false
1015        }
1016    }
1017    pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
1018
1019    pub fn safepoint() {}
1020    pub fn gc_requested() -> bool {
1021        false
1022    }
1023    pub fn take_gc_request() -> bool {
1024        false
1025    }
1026    pub fn begin_stw() -> Option<StwGuard> {
1027        None
1028    }
1029    pub fn register_mutator() -> MutatorGuard {
1030        MutatorGuard
1031    }
1032    pub fn registered_threads() -> usize {
1033        0
1034    }
1035    pub fn request_gc() {}
1036    pub fn check_cancellation() -> Result<(), GcParked> {
1037        Ok(())
1038    }
1039    pub fn park_thread() {}
1040    pub fn unpark_thread() {}
1041    pub fn wait_for_threads_to_park() {}
1042
1043    pub struct AllocRootGuard;
1044    impl Drop for AllocRootGuard {
1045        fn drop(&mut self) {}
1046    }
1047    pub fn push_alloc_frame() -> AllocRootGuard {
1048        AllocRootGuard
1049    }
1050}
1051
1052// =============================================================================
1053// Tests
1054// =============================================================================
1055
1056#[cfg(all(test, not(feature = "no-gc")))]
1057mod tests {
1058    use super::*;
1059    use std::sync::{Arc, Mutex};
1060
1061    #[derive(Debug)]
1062    #[allow(dead_code)]
1063    struct Tracked {
1064        value: i32,
1065        dropped: Arc<Mutex<bool>>,
1066    }
1067    impl Drop for Tracked {
1068        fn drop(&mut self) {
1069            *self.dropped.lock().unwrap() = true;
1070        }
1071    }
1072    impl Trace for Tracked {
1073        fn trace(&self, _: &mut MarkVisitor) {}
1074    }
1075
1076    #[derive(Debug)]
1077    #[allow(dead_code)]
1078    struct Parent {
1079        child: GcPtr<Tracked>,
1080    }
1081    impl Trace for Parent {
1082        fn trace(&self, visitor: &mut MarkVisitor) {
1083            visitor.visit(&self.child);
1084        }
1085    }
1086
1087    fn fresh_heap() -> gc_full::GcHeap {
1088        let heap = gc_full::GcHeap::new();
1089        heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
1090        heap
1091    }
1092
1093    #[test]
1094    fn alloc_and_get() {
1095        let heap = fresh_heap();
1096        let p = heap.alloc(42i64);
1097        assert_eq!(*p.get(), 42);
1098        assert_eq!(heap.count(), 1);
1099    }
1100
1101    #[test]
1102    fn clone_is_same_ptr() {
1103        let heap = fresh_heap();
1104        let p = heap.alloc(99i64);
1105        let q = p.clone();
1106        assert!(GcPtr::ptr_eq(&p, &q));
1107    }
1108
1109    #[test]
1110    fn parse_limit_mb_handles_valid_unset_and_malformed() {
1111        // Valid: converts megabytes to bytes.
1112        assert_eq!(gc_full::parse_limit_mb("X", Some("4"), 7), 4 * 1024 * 1024);
1113        // Surrounding whitespace is tolerated.
1114        assert_eq!(
1115            gc_full::parse_limit_mb("X", Some(" 4 "), 7),
1116            4 * 1024 * 1024
1117        );
1118        // Unset: falls back to the default.
1119        assert_eq!(gc_full::parse_limit_mb("X", None, 7), 7);
1120        // Malformed must NOT panic — it falls back to the default.
1121        assert_eq!(gc_full::parse_limit_mb("X", Some("foo"), 7), 7);
1122        assert_eq!(gc_full::parse_limit_mb("X", Some(""), 7), 7);
1123        assert_eq!(gc_full::parse_limit_mb("X", Some("-1"), 7), 7);
1124        // Absurdly large value saturates rather than overflowing.
1125        assert_eq!(
1126            gc_full::parse_limit_mb("X", Some(&usize::MAX.to_string()), 7),
1127            usize::MAX
1128        );
1129    }
1130
1131    #[test]
1132    fn collect_keeps_reachable() {
1133        let heap = fresh_heap();
1134        let dropped = Arc::new(Mutex::new(false));
1135        let p = heap.alloc(Tracked {
1136            value: 2,
1137            dropped: dropped.clone(),
1138        });
1139        heap.collect(|vis| vis.visit(&p));
1140        assert_eq!(heap.count(), 1);
1141        assert!(!*dropped.lock().unwrap());
1142    }
1143
1144    #[test]
1145    fn b1_two_isolates_independent_heaps() {
1146        use std::sync::{Arc, Barrier};
1147        // Each thread has its own ISOLATE_HEAP; allocations on one do not appear
1148        // in the other.
1149        let barrier = Arc::new(Barrier::new(2));
1150        let b1 = barrier.clone();
1151        let h1 = std::thread::Builder::new()
1152            .name("isolate-1".into())
1153            .spawn(move || {
1154                let _mutator = crate::register_mutator();
1155                // Allocate 100 objects on this isolate's heap
1156                let _ptrs: Vec<_> = (0_i64..100)
1157                    .map(|i| crate::gc_full::HEAP.alloc(i))
1158                    .collect();
1159                b1.wait(); // both threads are now at peak allocation
1160                // This isolate has exactly 100 live objects
1161                assert_eq!(
1162                    crate::gc_full::HEAP.count(),
1163                    100,
1164                    "isolate-1 heap count should be 100"
1165                );
1166            })
1167            .unwrap();
1168
1169        let b2 = barrier.clone();
1170        let h2 = std::thread::Builder::new()
1171            .name("isolate-2".into())
1172            .spawn(move || {
1173                let _mutator = crate::register_mutator();
1174                // Allocate 200 objects on this isolate's heap
1175                let _ptrs: Vec<_> = (0_i64..200)
1176                    .map(|i| crate::gc_full::HEAP.alloc(i))
1177                    .collect();
1178                b2.wait();
1179                // This isolate has exactly 200 live objects, unaffected by isolate-1
1180                assert_eq!(
1181                    crate::gc_full::HEAP.count(),
1182                    200,
1183                    "isolate-2 heap count should be 200"
1184                );
1185            })
1186            .unwrap();
1187
1188        h1.join().expect("isolate-1 panicked");
1189        h2.join().expect("isolate-2 panicked");
1190    }
1191
1192    #[test]
1193    fn b1_two_isolates_gc_independently() {
1194        // Two threads run allocation-heavy loops and GC their own heaps independently.
1195        let h1 = std::thread::Builder::new()
1196            .name("gc-isolate-1".into())
1197            .spawn(|| {
1198                let _mutator = crate::register_mutator();
1199                let heap = &crate::gc_full::HEAP;
1200                heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1201                // Allocate in batches and collect; each collection touches only this heap
1202                for _ in 0..5 {
1203                    let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1204                    // drive a manual collect with no roots so objects are freed
1205                    heap.collect(|_| {});
1206                    heap.collect(|_| {}); // second pass clears grace-period objects
1207                }
1208                // After all collections the heap should be empty (or close to it).
1209                // We don't assert an exact count because alloc_frame roots may keep
1210                // some alive; just assert we can collect without panicking.
1211            })
1212            .unwrap();
1213
1214        let h2 = std::thread::Builder::new()
1215            .name("gc-isolate-2".into())
1216            .spawn(|| {
1217                let _mutator = crate::register_mutator();
1218                let heap = &crate::gc_full::HEAP;
1219                heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1220                for _ in 0..5 {
1221                    let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1222                    heap.collect(|_| {});
1223                    heap.collect(|_| {});
1224                }
1225            })
1226            .unwrap();
1227
1228        h1.join().expect("gc-isolate-1 panicked");
1229        h2.join().expect("gc-isolate-2 panicked");
1230    }
1231}
1232
1233#[cfg(all(test, feature = "no-gc"))]
1234mod nogc_tests {
1235    use super::*;
1236    use alloc_ctx::{ScratchGuard, StaticCtxGuard};
1237
1238    #[test]
1239    fn alloc_in_static_context() {
1240        let _g = StaticCtxGuard::new();
1241        let p = GcPtr::new(42i64);
1242        assert_eq!(*p.get(), 42);
1243    }
1244
1245    #[test]
1246    fn alloc_in_scratch_region() {
1247        let mut scratch = ScratchGuard::new();
1248        let p = GcPtr::new(99i64);
1249        assert_eq!(*p.get(), 99);
1250        scratch.pop_for_return();
1251        assert_eq!(*p.get(), 99);
1252        // scratch drops here, resets the region
1253    }
1254
1255    #[test]
1256    fn ptr_eq() {
1257        let _g = StaticCtxGuard::new();
1258        let p = GcPtr::new(1i64);
1259        let q = p.clone();
1260        assert!(GcPtr::ptr_eq(&p, &q));
1261    }
1262}