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