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    CancellableGuard, MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested,
28    park_thread, register_mutator, registered_threads, request_gc, safepoint, take_gc_request,
29    unpark_thread, 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::{AllocRootGuard, GcHeap, HEAP, push_alloc_frame, trace_thread_alloc_roots};
36#[cfg(feature = "no-gc")]
37pub use nogc_stubs::{
38    AllocRootGuard, CONFIG_CANCELLATION, CancellableGuard, GcConfig, GcHeap, GcParked, HEAP,
39    MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested, park_thread,
40    push_alloc_frame, register_mutator, registered_threads, request_gc, safepoint, take_gc_request,
41    unpark_thread, wait_for_threads_to_park,
42};
43
44/// Return `true` if `addr` was allocated by the global `StaticArena`.
45///
46/// Available only in `no-gc` debug builds.  Downstream crates (`cljrs-value`)
47/// use this to implement write-site provenance assertions.
48#[cfg(all(feature = "no-gc", debug_assertions))]
49pub fn is_static_addr(addr: usize) -> bool {
50    static_arena::is_static_addr(addr)
51}
52
53// ── Trace trait ───────────────────────────────────────────────────────────────
54
55/// Implemented by every type that can be stored behind a [`GcPtr`].
56///
57/// The `gc_size_extra` method accounts for heap bytes owned by the value that
58/// are NOT captured by `size_of::<GcBox<T>>()` (e.g. `Vec` buffers, `String`
59/// capacity, `Form` AST trees stored inline).  The default returns 0, which is
60/// correct for primitives and types with no out-of-line heap.
61///
62/// Rules for implementors of `gc_size_extra`:
63/// - Count only bytes THIS value owns and will free when dropped.
64/// - Do NOT cross `GcPtr` boundaries — each pointed-to box is counted
65///   separately when it is allocated.
66pub trait Trace: Send + Sync {
67    fn trace(&self, visitor: &mut MarkVisitor);
68
69    fn gc_size_extra(&self) -> usize {
70        0
71    }
72}
73
74// ── Leaf Trace impls ──────────────────────────────────────────────────────────
75
76impl Trace for String {
77    fn trace(&self, _: &mut MarkVisitor) {}
78
79    fn gc_size_extra(&self) -> usize {
80        self.capacity()
81    }
82}
83impl Trace for i64 {
84    fn trace(&self, _: &mut MarkVisitor) {}
85}
86impl Trace for f64 {
87    fn trace(&self, _: &mut MarkVisitor) {}
88}
89impl Trace for bool {
90    fn trace(&self, _: &mut MarkVisitor) {}
91}
92impl Trace for num_bigint::BigInt {
93    fn trace(&self, _: &mut MarkVisitor) {}
94}
95impl Trace for bigdecimal::BigDecimal {
96    fn trace(&self, _: &mut MarkVisitor) {}
97}
98impl Trace for num_rational::Ratio<num_bigint::BigInt> {
99    fn trace(&self, _: &mut MarkVisitor) {}
100}
101impl Trace for regex::Regex {
102    fn trace(&self, _: &mut MarkVisitor) {}
103}
104macro_rules! impl_trace_prim_array {
105    ($t:ty) => {
106        impl Trace for std::sync::Mutex<Vec<$t>> {
107            fn trace(&self, _: &mut MarkVisitor) {}
108            fn gc_size_extra(&self) -> usize {
109                self.lock().unwrap().capacity() * std::mem::size_of::<$t>()
110            }
111        }
112    };
113}
114impl_trace_prim_array!(i32);
115impl_trace_prim_array!(i64);
116impl_trace_prim_array!(i16);
117impl_trace_prim_array!(i8);
118impl_trace_prim_array!(char);
119impl_trace_prim_array!(f64);
120impl_trace_prim_array!(f32);
121impl_trace_prim_array!(bool);
122
123// ── GcVisitor ─────────────────────────────────────────────────────────────────
124
125pub trait GcVisitor {
126    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>);
127}
128
129// =============================================================================
130// GC build: GcBox with header, MarkVisitor with grey stack
131// =============================================================================
132
133#[cfg(not(feature = "no-gc"))]
134pub use self::gc_header::{GcBox, GcBoxHeader};
135
136#[cfg(not(feature = "no-gc"))]
137mod gc_header {
138    use crate::{MarkVisitor, Trace};
139    use std::cell::Cell;
140
141    // Objects start at lives = GC_INITIAL_LIVES - 1.  The mark phase sets
142    // lives = GC_INITIAL_LIVES for reachable objects; sweep frees objects
143    // whose lives reach 0.  A value of 2 gives exactly one cycle of grace:
144    // enough to cover the window between an alloc frame dropping and the
145    // next GC safepoint (where VALUE_ROOTS or a new alloc frame re-roots it).
146    // 10 was chosen conservatively but keeps 9× more garbage in RAM than
147    // necessary, worsening OOM pressure under test suites with many forms.
148    pub(crate) const GC_INITIAL_LIVES: u8 = 2;
149
150    #[cfg(debug_assertions)]
151    pub(crate) const GC_MAGIC_ALIVE: u64 = 0xCAFE_BABE_DEAD_BEEF;
152    #[cfg(debug_assertions)]
153    pub(crate) const GC_MAGIC_FREED: u64 = 0xDEAD_DEAD_DEAD_DEAD;
154
155    #[repr(C)]
156    pub struct GcBoxHeader {
157        #[cfg(debug_assertions)]
158        pub(crate) magic: Cell<u64>,
159        /// Exact size of the GcBox<T> allocation in bytes.
160        pub(crate) size: usize,
161        pub(crate) lives: Cell<u8>,
162        pub(crate) next: Cell<*mut GcBoxHeader>,
163        pub(crate) trace_fn: unsafe fn(*const GcBoxHeader, &mut MarkVisitor),
164        pub(crate) drop_fn: unsafe fn(*mut GcBoxHeader),
165    }
166
167    impl GcBoxHeader {
168        pub(crate) fn new<T: Trace + 'static>(heap_extra: usize) -> Self {
169            Self {
170                #[cfg(debug_assertions)]
171                magic: Cell::new(GC_MAGIC_ALIVE),
172                size: std::mem::size_of::<GcBox<T>>() + heap_extra,
173                lives: Cell::new(GC_INITIAL_LIVES - 1),
174                next: Cell::new(std::ptr::null_mut()),
175                trace_fn: trace_gc_box::<T>,
176                drop_fn: drop_gc_box::<T>,
177            }
178        }
179    }
180
181    unsafe impl Send for GcBoxHeader {}
182    unsafe impl Sync for GcBoxHeader {}
183
184    #[repr(C)]
185    pub struct GcBox<T: Trace + 'static> {
186        pub(crate) header: GcBoxHeader,
187        pub value: T,
188    }
189
190    pub(crate) unsafe fn trace_gc_box<T: Trace + 'static>(
191        header: *const GcBoxHeader,
192        visitor: &mut MarkVisitor,
193    ) {
194        unsafe {
195            let gc_box = header as *const GcBox<T>;
196            (*gc_box).value.trace(visitor);
197        }
198    }
199
200    pub(crate) unsafe fn drop_gc_box<T: Trace + 'static>(header: *mut GcBoxHeader) {
201        unsafe {
202            #[cfg(debug_assertions)]
203            {
204                (*header).magic.set(GC_MAGIC_FREED);
205            }
206            let gc_box = header as *mut GcBox<T>;
207            drop(Box::from_raw(gc_box));
208        }
209    }
210}
211
212// =============================================================================
213// no-gc build: GcBox without header
214// =============================================================================
215
216#[cfg(feature = "no-gc")]
217pub use self::nogc_box::GcBox;
218
219#[cfg(feature = "no-gc")]
220mod nogc_box {
221    use crate::Trace;
222
223    pub struct GcBox<T: Trace + 'static> {
224        pub value: T,
225    }
226}
227
228// =============================================================================
229// MarkVisitor: full under GC, stub under no-gc
230// =============================================================================
231
232#[cfg(not(feature = "no-gc"))]
233#[derive(Default)]
234pub struct MarkVisitor {
235    pub(crate) grey: Vec<*mut GcBoxHeader>,
236}
237
238#[cfg(feature = "no-gc")]
239pub struct MarkVisitor;
240
241#[cfg(not(feature = "no-gc"))]
242impl MarkVisitor {
243    pub fn new() -> Self {
244        Self::default()
245    }
246
247    pub fn grey_len(&self) -> usize {
248        self.grey.len()
249    }
250
251    pub unsafe fn mark_header(&mut self, header: *mut GcBoxHeader) {
252        use gc_header::GC_INITIAL_LIVES;
253        let h = unsafe { &*header };
254        if h.lives.get() < GC_INITIAL_LIVES {
255            h.lives.set(GC_INITIAL_LIVES);
256            self.grey.push(header);
257        }
258    }
259
260    pub(crate) fn drain(&mut self) {
261        let mut visited = 0usize;
262        while let Some(header) = self.grey.pop() {
263            visited += 1;
264            let h = unsafe { &*header };
265            unsafe { (h.trace_fn)(header as *const GcBoxHeader, self) };
266        }
267        cljrs_logging::feat_debug!("gc", "drain visited {} objects", visited);
268    }
269}
270
271#[cfg(not(feature = "no-gc"))]
272impl GcVisitor for MarkVisitor {
273    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
274        use gc_header::GC_INITIAL_LIVES;
275        let header = unsafe { &(*ptr.0.as_ptr()).header };
276        if header.lives.get() < GC_INITIAL_LIVES {
277            header.lives.set(GC_INITIAL_LIVES);
278            self.grey.push(ptr.0.as_ptr() as *mut GcBoxHeader);
279        }
280    }
281}
282
283#[cfg(feature = "no-gc")]
284impl MarkVisitor {
285    pub fn grey_len(&self) -> usize {
286        0
287    }
288    pub unsafe fn mark_header(&mut self, _: *mut u8) {}
289}
290
291#[cfg(feature = "no-gc")]
292impl GcVisitor for MarkVisitor {
293    fn visit<T: Trace + 'static>(&mut self, _: &GcPtr<T>) {}
294}
295
296// =============================================================================
297// GcPtr — always present
298// =============================================================================
299
300pub struct GcPtr<T: Trace + 'static>(NonNull<GcBox<T>>);
301
302unsafe impl<T: Trace + 'static> Send for GcPtr<T> {}
303unsafe impl<T: Trace + 'static> Sync for GcPtr<T> {}
304
305impl<T: Trace + 'static> GcPtr<T> {
306    #[cfg(not(feature = "no-gc"))]
307    pub fn new(value: T) -> Self {
308        gc_full::HEAP.alloc(value)
309    }
310
311    #[cfg(feature = "no-gc")]
312    pub fn new(value: T) -> Self {
313        alloc_ctx::alloc_in_ctx(value)
314    }
315
316    pub fn get(&self) -> &T {
317        #[cfg(all(debug_assertions, not(feature = "no-gc")))]
318        {
319            use gc_header::GC_MAGIC_ALIVE;
320            let header = unsafe { &(*self.0.as_ptr()).header };
321            assert_eq!(
322                header.magic.get(),
323                GC_MAGIC_ALIVE,
324                "GcPtr::get() on freed object! magic={:#x}",
325                header.magic.get(),
326            );
327        }
328        unsafe { &(*self.0.as_ptr()).value }
329    }
330
331    pub fn get_mut(&mut self) -> &mut T {
332        #[cfg(all(debug_assertions, not(feature = "no-gc")))]
333        {
334            use gc_header::GC_MAGIC_ALIVE;
335            let header = unsafe { &(*self.0.as_ptr()).header };
336            assert_eq!(
337                header.magic.get(),
338                GC_MAGIC_ALIVE,
339                "GcPtr::get_mut() on freed object! magic={:#x}",
340                header.magic.get(),
341            );
342        }
343        unsafe { &mut (*self.0.as_ptr()).value }
344    }
345
346    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
347        a.0 == b.0
348    }
349
350    /// Return `true` if this pointer was allocated by the global `StaticArena`.
351    ///
352    /// Only meaningful (and only compiled) in `no-gc` debug builds.  Used by
353    /// write-site assertions in `Atom::reset` / `Var::bind` to catch
354    /// region-local values being stored in program-lifetime containers.
355    #[cfg(all(feature = "no-gc", debug_assertions))]
356    pub fn is_static_alloc(&self) -> bool {
357        static_arena::is_static_addr(self.0.as_ptr() as usize)
358    }
359}
360
361impl<T: Trace + 'static> Clone for GcPtr<T> {
362    fn clone(&self) -> Self {
363        GcPtr(self.0)
364    }
365}
366
367impl<T: Trace + 'static + std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        unsafe { (*self.0.as_ptr()).value.fmt(f) }
370    }
371}
372
373impl<T: Trace + 'static> Drop for GcPtr<T> {
374    fn drop(&mut self) {}
375}
376
377// =============================================================================
378// Full GC implementation (default build)
379// =============================================================================
380
381#[cfg(not(feature = "no-gc"))]
382mod gc_full {
383    use std::cell::RefCell;
384    use std::ptr::NonNull;
385    use std::sync::atomic::{AtomicUsize, Ordering};
386    use std::sync::{Arc, Mutex};
387
388    use crate::config::GcConfig;
389    use crate::gc_header::GC_INITIAL_LIVES;
390    use crate::{GcBox, GcBoxHeader, GcPtr, MarkVisitor, Trace};
391
392    type RootTracer = Box<dyn Fn(&mut MarkVisitor) + Send + Sync>;
393
394    pub struct GcHeap {
395        inner: Mutex<GcHeapInner>,
396        config: Mutex<Option<Arc<GcConfig>>>,
397        memory_in_use: AtomicUsize,
398        total_allocated_bytes: AtomicUsize,
399        root_tracers: Mutex<Vec<RootTracer>>,
400        gc_suppressed: std::sync::atomic::AtomicBool,
401        /// memory_in_use threshold above which GC is re-enabled after a
402        /// zero-yield collection.  The headroom doubles on each consecutive
403        /// zero-yield cycle (exponential backoff, capped at soft_limit) so a
404        /// long computation where all objects are live doesn't spin in a
405        /// constant GC storm of O(N) sweeps.  Resets to the base headroom
406        /// (soft_limit / 10) once GC actually frees something.
407        suppressed_threshold: AtomicUsize,
408        /// Current headroom used for exponential backoff after zero-yield cycles.
409        zero_yield_headroom: AtomicUsize,
410    }
411
412    struct GcHeapInner {
413        head: *mut GcBoxHeader,
414        count: usize,
415        total_allocated: usize,
416        total_freed: usize,
417    }
418
419    unsafe impl Send for GcHeapInner {}
420
421    impl GcHeapInner {
422        const fn new() -> Self {
423            Self {
424                head: std::ptr::null_mut(),
425                count: 0,
426                total_allocated: 0,
427                total_freed: 0,
428            }
429        }
430    }
431
432    unsafe impl Sync for GcHeap {}
433
434    impl Default for GcHeap {
435        fn default() -> Self {
436            Self::new()
437        }
438    }
439
440    impl GcHeap {
441        pub const fn new() -> Self {
442            Self {
443                inner: Mutex::new(GcHeapInner::new()),
444                config: Mutex::new(None),
445                memory_in_use: AtomicUsize::new(0),
446                total_allocated_bytes: AtomicUsize::new(0),
447                root_tracers: Mutex::new(Vec::new()),
448                gc_suppressed: std::sync::atomic::AtomicBool::new(false),
449                suppressed_threshold: AtomicUsize::new(0),
450                zero_yield_headroom: AtomicUsize::new(0),
451            }
452        }
453
454        pub fn set_config(&self, config: Arc<GcConfig>) {
455            *self.config.lock().unwrap() = Some(config);
456        }
457
458        pub fn set_config_from_env(&self) {
459            let soft_limit_mb: usize = match std::env::var("CLJRS_GC_SOFT_LIMIT_MB").ok() {
460                Some(s) => s.parse::<usize>().unwrap() * (1024 * 1024),
461                None => (system_memory::total() / 3) as usize,
462            };
463            let hard_limit_mb: usize = match std::env::var("CLJRS_GC_HARD_LIMIT_MB").ok() {
464                Some(s) => s.parse::<usize>().unwrap() * (1024 * 1024),
465                None => soft_limit_mb,
466            };
467            self.set_config(Arc::new(GcConfig::with_limits(
468                soft_limit_mb,
469                hard_limit_mb,
470            )));
471        }
472
473        pub fn register_root_tracer(
474            &self,
475            tracer: impl Fn(&mut MarkVisitor) + Send + Sync + 'static,
476        ) {
477            self.root_tracers.lock().unwrap().push(Box::new(tracer));
478        }
479
480        pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
481            let tracers = self.root_tracers.lock().unwrap();
482            for tracer in tracers.iter() {
483                tracer(visitor);
484            }
485        }
486
487        pub fn memory_in_use(&self) -> usize {
488            self.memory_in_use.load(Ordering::Relaxed)
489        }
490
491        #[cfg(test)]
492        pub fn set_memory_in_use(&self, bytes: usize) {
493            self.memory_in_use.store(bytes, Ordering::Relaxed);
494        }
495
496        pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
497            crate::cancellation::safepoint();
498            let heap_extra = value.gc_size_extra();
499            let gc_box = Box::new(GcBox {
500                header: GcBoxHeader::new::<T>(heap_extra),
501                value,
502            });
503            let obj_size = gc_box.header.size; // GcBox<T> size + gc_size_extra()
504            let raw: *mut GcBox<T> = Box::into_raw(gc_box);
505            {
506                let mut inner = self.inner.lock().unwrap();
507                unsafe {
508                    (*raw).header.next.set(inner.head);
509                    inner.head = raw as *mut GcBoxHeader;
510                }
511                inner.count += 1;
512                inner.total_allocated += 1;
513            }
514            self.total_allocated_bytes
515                .fetch_add(obj_size, Ordering::Relaxed);
516            crate::stats::GC_STATS.record_gc_alloc(obj_size);
517            let current_usage =
518                self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
519
520            if let Some(config) = self.config.lock().unwrap().as_ref()
521                && config.soft_limit_exceeded(current_usage)
522            {
523                if self.gc_suppressed.load(Ordering::Relaxed) {
524                    // Suppression active: only re-enable GC once memory has
525                    // grown past the threshold set by the last zero-yield
526                    // collection (current_memory + soft_limit/10).
527                    let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
528                    if current_usage > threshold {
529                        self.gc_suppressed.store(false, Ordering::Relaxed);
530                        crate::cancellation::request_gc();
531                    }
532                } else {
533                    crate::cancellation::request_gc();
534                }
535            }
536
537            register_alloc(raw as *mut GcBoxHeader);
538            GcPtr(unsafe { NonNull::new_unchecked(raw) })
539        }
540
541        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
542            let pre_count = self.inner.lock().unwrap().count;
543            let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
544            cljrs_logging::feat_debug!(
545                "gc",
546                "starting collection: {} objects, ~{} bytes in use",
547                pre_count,
548                pre_memory
549            );
550            let mark_start = std::time::Instant::now();
551            let mut visitor = MarkVisitor::new();
552            trace_roots(&mut visitor);
553            cljrs_logging::feat_debug!(
554                "gc",
555                "starting drain with {} grey objects",
556                visitor.grey.len()
557            );
558            visitor.drain();
559            let mark_elapsed = mark_start.elapsed();
560
561            let sweep_start = std::time::Instant::now();
562            let mut inner = self.inner.lock().unwrap();
563            let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
564            let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
565            // Bytes of objects with lives==0 that will be freed now.
566            let mut freed_bytes: usize = 0;
567            let mut current = inner.head;
568            while !current.is_null() {
569                let header = unsafe { &*current };
570                let next = header.next.get();
571                let lives = header.lives.get();
572                let obj_size = header.size;
573                if lives >= GC_INITIAL_LIVES {
574                    // Marked reachable this cycle — reset grace counter.
575                    header.lives.set(GC_INITIAL_LIVES - 1);
576                    live.push(current);
577                } else if lives > 0 {
578                    // In grace period (unreachable but not yet freed).
579                    header.lives.set(lives - 1);
580                    live.push(current);
581                } else {
582                    // Grace period exhausted — collect now.
583                    freed_bytes += obj_size;
584                    dead.push(current);
585                }
586                current = next;
587            }
588            let freed_count = dead.len();
589            for ptr in dead {
590                let header = unsafe { &*ptr };
591                unsafe { (header.drop_fn)(ptr) };
592                inner.count -= 1;
593                inner.total_freed += 1;
594            }
595            inner.head = std::ptr::null_mut();
596            for ptr in live {
597                let header = unsafe { &*ptr };
598                header.next.set(inner.head);
599                inner.head = ptr;
600            }
601            // Decrement memory_in_use by the bytes actually freed.  All heap
602            // objects (live + grace-period) remain counted; only physically
603            // freed objects are subtracted.  This keeps memory pressure
604            // accurate so GC fires again when the heap genuinely grows.
605            self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
606            let sweep_elapsed = sweep_start.elapsed();
607            crate::stats::GC_STATS.record_gc_pause(
608                mark_elapsed + sweep_elapsed,
609                freed_count as u64,
610                freed_bytes as u64,
611            );
612            let post_memory = self.memory_in_use.load(Ordering::Relaxed);
613            cljrs_logging::feat_debug!(
614                "gc",
615                "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
616                freed_count,
617                freed_bytes,
618                inner.count,
619                post_memory,
620                mark_elapsed,
621                sweep_elapsed
622            );
623            if freed_count == 0 {
624                // Zero-yield collection: exponential-backoff suppression.
625                // Each consecutive zero-yield cycle doubles the headroom before
626                // the next GC attempt (capped at soft_limit/4).  This prevents a
627                // GC storm during deep recursion where all objects are live —
628                // without backoff, GC fires every soft_limit/10 bytes, tracing
629                // the entire live set O(N) times to no benefit.
630                // The headroom resets to soft_limit/10 when GC frees something.
631                // Cap at soft_limit/4 (not soft_limit) so that GC still fires
632                // frequently enough to catch short-lived test allocations after
633                // a long namespace-loading phase of zero-yield cycles.
634                let soft_limit = self
635                    .config
636                    .lock()
637                    .unwrap()
638                    .as_ref()
639                    .map(|c| c.soft_limit())
640                    .unwrap_or(64 * 1024 * 1024);
641                let base_headroom = (soft_limit / 10).max(1);
642                let max_headroom = (soft_limit / 4).max(base_headroom);
643                let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
644                let headroom = if prev_headroom == 0 {
645                    base_headroom
646                } else {
647                    prev_headroom.saturating_mul(2).min(max_headroom)
648                };
649                self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
650                self.suppressed_threshold
651                    .store(post_memory + headroom, Ordering::Relaxed);
652                self.gc_suppressed.store(true, Ordering::Relaxed);
653            } else {
654                // GC freed something: reset exponential backoff.
655                self.zero_yield_headroom.store(0, Ordering::Relaxed);
656                self.gc_suppressed.store(false, Ordering::Relaxed);
657            }
658        }
659
660        pub fn count(&self) -> usize {
661            self.inner.lock().unwrap().count
662        }
663        pub fn total_allocated(&self) -> usize {
664            self.inner.lock().unwrap().total_allocated
665        }
666        pub fn total_freed(&self) -> usize {
667            self.inner.lock().unwrap().total_freed
668        }
669
670        pub fn collect_auto(&self) -> bool {
671            cljrs_logging::feat_debug!("gc", "automatic collection requested");
672            let Some(_stw_guard) = crate::cancellation::begin_stw() else {
673                cljrs_logging::feat_debug!("gc", "automatic collection skipped");
674                return false;
675            };
676            self.collect(|visitor| self.trace_registered_roots(visitor));
677            true
678        }
679    }
680
681    pub static HEAP: GcHeap = GcHeap::new();
682
683    thread_local! {
684        pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
685    }
686
687    pub struct AllocRootGuard {
688        saved_len: usize,
689    }
690
691    impl Drop for AllocRootGuard {
692        fn drop(&mut self) {
693            ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
694        }
695    }
696
697    pub fn push_alloc_frame() -> AllocRootGuard {
698        let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
699        AllocRootGuard { saved_len }
700    }
701
702    fn register_alloc(header: *mut GcBoxHeader) {
703        ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
704    }
705
706    pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
707        ALLOC_ROOTS.with(|roots| {
708            let roots = roots.borrow();
709            for &header in roots.iter() {
710                unsafe { visitor.mark_header(header) };
711            }
712        });
713    }
714}
715
716// =============================================================================
717// no-gc stubs
718// =============================================================================
719
720#[cfg(feature = "no-gc")]
721mod nogc_stubs {
722    use crate::MarkVisitor;
723    use std::sync::Arc;
724
725    #[derive(Debug, Clone)]
726    pub struct GcConfig;
727    impl GcConfig {
728        pub fn new() -> Self {
729            Self
730        }
731        pub fn with_hard_limit(_: usize) -> Self {
732            Self
733        }
734        pub fn with_limits(_: usize, _: usize) -> Self {
735            Self
736        }
737    }
738    impl Default for GcConfig {
739        fn default() -> Self {
740            Self::new()
741        }
742    }
743
744    pub struct GcHeap;
745    impl Default for GcHeap {
746        fn default() -> Self {
747            Self::new()
748        }
749    }
750    impl GcHeap {
751        pub const fn new() -> Self {
752            Self
753        }
754        pub fn set_config(&self, _: Arc<GcConfig>) {}
755        pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + Send + Sync + 'static) {}
756        pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
757        pub fn memory_in_use(&self) -> usize {
758            0
759        }
760        pub fn count(&self) -> usize {
761            0
762        }
763        pub fn total_allocated(&self) -> usize {
764            0
765        }
766        pub fn total_freed(&self) -> usize {
767            0
768        }
769        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
770        pub fn collect_auto(&self) -> bool {
771            false
772        }
773    }
774    unsafe impl Sync for GcHeap {}
775    pub static HEAP: GcHeap = GcHeap::new();
776
777    pub struct MutatorGuard;
778    impl Drop for MutatorGuard {
779        fn drop(&mut self) {}
780    }
781    pub struct StwGuard;
782    impl Drop for StwGuard {
783        fn drop(&mut self) {}
784    }
785    pub struct GcParked;
786    pub struct CancellableGuard;
787
788    pub struct GcCancellationStub;
789    impl GcCancellationStub {
790        pub const fn new() -> Self {
791            Self
792        }
793        pub fn in_progress(&self) -> bool {
794            false
795        }
796    }
797    pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
798
799    pub fn safepoint() {}
800    pub fn gc_requested() -> bool {
801        false
802    }
803    pub fn take_gc_request() -> bool {
804        false
805    }
806    pub fn begin_stw() -> Option<StwGuard> {
807        None
808    }
809    pub fn register_mutator() -> MutatorGuard {
810        MutatorGuard
811    }
812    pub fn registered_threads() -> usize {
813        0
814    }
815    pub fn request_gc() {}
816    pub fn check_cancellation() -> Result<(), GcParked> {
817        Ok(())
818    }
819    pub fn park_thread() {}
820    pub fn unpark_thread() {}
821    pub fn wait_for_threads_to_park() {}
822
823    pub struct AllocRootGuard;
824    impl Drop for AllocRootGuard {
825        fn drop(&mut self) {}
826    }
827    pub fn push_alloc_frame() -> AllocRootGuard {
828        AllocRootGuard
829    }
830}
831
832// =============================================================================
833// Tests
834// =============================================================================
835
836#[cfg(all(test, not(feature = "no-gc")))]
837mod tests {
838    use super::*;
839    use std::sync::{Arc, Mutex};
840
841    #[derive(Debug)]
842    #[allow(dead_code)]
843    struct Tracked {
844        value: i32,
845        dropped: Arc<Mutex<bool>>,
846    }
847    impl Drop for Tracked {
848        fn drop(&mut self) {
849            *self.dropped.lock().unwrap() = true;
850        }
851    }
852    impl Trace for Tracked {
853        fn trace(&self, _: &mut MarkVisitor) {}
854    }
855
856    #[derive(Debug)]
857    #[allow(dead_code)]
858    struct Parent {
859        child: GcPtr<Tracked>,
860    }
861    impl Trace for Parent {
862        fn trace(&self, visitor: &mut MarkVisitor) {
863            visitor.visit(&self.child);
864        }
865    }
866
867    fn fresh_heap() -> gc_full::GcHeap {
868        let heap = gc_full::GcHeap::new();
869        heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
870        heap
871    }
872
873    #[test]
874    fn alloc_and_get() {
875        let heap = fresh_heap();
876        let p = heap.alloc(42i64);
877        assert_eq!(*p.get(), 42);
878        assert_eq!(heap.count(), 1);
879    }
880
881    #[test]
882    fn clone_is_same_ptr() {
883        let heap = fresh_heap();
884        let p = heap.alloc(99i64);
885        let q = p.clone();
886        assert!(GcPtr::ptr_eq(&p, &q));
887    }
888
889    #[test]
890    fn collect_keeps_reachable() {
891        let heap = fresh_heap();
892        let dropped = Arc::new(Mutex::new(false));
893        let p = heap.alloc(Tracked {
894            value: 2,
895            dropped: dropped.clone(),
896        });
897        heap.collect(|vis| vis.visit(&p));
898        assert_eq!(heap.count(), 1);
899        assert!(!*dropped.lock().unwrap());
900    }
901}
902
903#[cfg(all(test, feature = "no-gc"))]
904mod nogc_tests {
905    use super::*;
906    use alloc_ctx::{ScratchGuard, StaticCtxGuard};
907
908    #[test]
909    fn alloc_in_static_context() {
910        let _g = StaticCtxGuard::new();
911        let p = GcPtr::new(42i64);
912        assert_eq!(*p.get(), 42);
913    }
914
915    #[test]
916    fn alloc_in_scratch_region() {
917        let mut scratch = ScratchGuard::new();
918        let p = GcPtr::new(99i64);
919        assert_eq!(*p.get(), 99);
920        scratch.pop_for_return();
921        assert_eq!(*p.get(), 99);
922        // scratch drops here, resets the region
923    }
924
925    #[test]
926    fn ptr_eq() {
927        let _g = StaticCtxGuard::new();
928        let p = GcPtr::new(1i64);
929        let q = p.clone();
930        assert!(GcPtr::ptr_eq(&p, &q));
931    }
932}