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            #[cfg(not(target_arch = "wasm32"))]
460            let default_soft_limit: usize = (system_memory::total() / 3) as usize;
461            #[cfg(target_arch = "wasm32")]
462            let default_soft_limit: usize = 64 * 1024 * 1024;
463
464            let soft_limit_mb: usize = match std::env::var("CLJRS_GC_SOFT_LIMIT_MB").ok() {
465                Some(s) => s.parse::<usize>().unwrap() * (1024 * 1024),
466                None => default_soft_limit,
467            };
468            let hard_limit_mb: usize = match std::env::var("CLJRS_GC_HARD_LIMIT_MB").ok() {
469                Some(s) => s.parse::<usize>().unwrap() * (1024 * 1024),
470                None => soft_limit_mb,
471            };
472            self.set_config(Arc::new(GcConfig::with_limits(
473                soft_limit_mb,
474                hard_limit_mb,
475            )));
476        }
477
478        pub fn register_root_tracer(
479            &self,
480            tracer: impl Fn(&mut MarkVisitor) + Send + Sync + 'static,
481        ) {
482            self.root_tracers.lock().unwrap().push(Box::new(tracer));
483        }
484
485        pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
486            let tracers = self.root_tracers.lock().unwrap();
487            for tracer in tracers.iter() {
488                tracer(visitor);
489            }
490        }
491
492        pub fn memory_in_use(&self) -> usize {
493            self.memory_in_use.load(Ordering::Relaxed)
494        }
495
496        #[cfg(test)]
497        pub fn set_memory_in_use(&self, bytes: usize) {
498            self.memory_in_use.store(bytes, Ordering::Relaxed);
499        }
500
501        pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
502            crate::cancellation::safepoint();
503            let heap_extra = value.gc_size_extra();
504            let gc_box = Box::new(GcBox {
505                header: GcBoxHeader::new::<T>(heap_extra),
506                value,
507            });
508            let obj_size = gc_box.header.size; // GcBox<T> size + gc_size_extra()
509            let raw: *mut GcBox<T> = Box::into_raw(gc_box);
510            {
511                let mut inner = self.inner.lock().unwrap();
512                unsafe {
513                    (*raw).header.next.set(inner.head);
514                    inner.head = raw as *mut GcBoxHeader;
515                }
516                inner.count += 1;
517                inner.total_allocated += 1;
518            }
519            self.total_allocated_bytes
520                .fetch_add(obj_size, Ordering::Relaxed);
521            crate::stats::GC_STATS.record_gc_alloc(obj_size);
522            let current_usage =
523                self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
524
525            if let Some(config) = self.config.lock().unwrap().as_ref()
526                && config.soft_limit_exceeded(current_usage)
527            {
528                if self.gc_suppressed.load(Ordering::Relaxed) {
529                    // Suppression active: only re-enable GC once memory has
530                    // grown past the threshold set by the last zero-yield
531                    // collection (current_memory + soft_limit/10).
532                    let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
533                    if current_usage > threshold {
534                        self.gc_suppressed.store(false, Ordering::Relaxed);
535                        crate::cancellation::request_gc();
536                    }
537                } else {
538                    crate::cancellation::request_gc();
539                }
540            }
541
542            register_alloc(raw as *mut GcBoxHeader);
543            GcPtr(unsafe { NonNull::new_unchecked(raw) })
544        }
545
546        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
547            let pre_count = self.inner.lock().unwrap().count;
548            let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
549            cljrs_logging::feat_debug!(
550                "gc",
551                "starting collection: {} objects, ~{} bytes in use",
552                pre_count,
553                pre_memory
554            );
555            let mark_start = std::time::Instant::now();
556            let mut visitor = MarkVisitor::new();
557            trace_roots(&mut visitor);
558            cljrs_logging::feat_debug!(
559                "gc",
560                "starting drain with {} grey objects",
561                visitor.grey.len()
562            );
563            visitor.drain();
564            let mark_elapsed = mark_start.elapsed();
565
566            let sweep_start = std::time::Instant::now();
567            let mut inner = self.inner.lock().unwrap();
568            let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
569            let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
570            // Bytes of objects with lives==0 that will be freed now.
571            let mut freed_bytes: usize = 0;
572            let mut current = inner.head;
573            while !current.is_null() {
574                let header = unsafe { &*current };
575                let next = header.next.get();
576                let lives = header.lives.get();
577                let obj_size = header.size;
578                if lives >= GC_INITIAL_LIVES {
579                    // Marked reachable this cycle — reset grace counter.
580                    header.lives.set(GC_INITIAL_LIVES - 1);
581                    live.push(current);
582                } else if lives > 0 {
583                    // In grace period (unreachable but not yet freed).
584                    header.lives.set(lives - 1);
585                    live.push(current);
586                } else {
587                    // Grace period exhausted — collect now.
588                    freed_bytes += obj_size;
589                    dead.push(current);
590                }
591                current = next;
592            }
593            let freed_count = dead.len();
594            for ptr in dead {
595                let header = unsafe { &*ptr };
596                unsafe { (header.drop_fn)(ptr) };
597                inner.count -= 1;
598                inner.total_freed += 1;
599            }
600            inner.head = std::ptr::null_mut();
601            for ptr in live {
602                let header = unsafe { &*ptr };
603                header.next.set(inner.head);
604                inner.head = ptr;
605            }
606            // Decrement memory_in_use by the bytes actually freed.  All heap
607            // objects (live + grace-period) remain counted; only physically
608            // freed objects are subtracted.  This keeps memory pressure
609            // accurate so GC fires again when the heap genuinely grows.
610            self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
611            let sweep_elapsed = sweep_start.elapsed();
612            crate::stats::GC_STATS.record_gc_pause(
613                mark_elapsed + sweep_elapsed,
614                freed_count as u64,
615                freed_bytes as u64,
616            );
617            let post_memory = self.memory_in_use.load(Ordering::Relaxed);
618            cljrs_logging::feat_debug!(
619                "gc",
620                "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
621                freed_count,
622                freed_bytes,
623                inner.count,
624                post_memory,
625                mark_elapsed,
626                sweep_elapsed
627            );
628            if freed_count == 0 {
629                // Zero-yield collection: exponential-backoff suppression.
630                // Each consecutive zero-yield cycle doubles the headroom before
631                // the next GC attempt (capped at soft_limit/4).  This prevents a
632                // GC storm during deep recursion where all objects are live —
633                // without backoff, GC fires every soft_limit/10 bytes, tracing
634                // the entire live set O(N) times to no benefit.
635                // The headroom resets to soft_limit/10 when GC frees something.
636                // Cap at soft_limit/4 (not soft_limit) so that GC still fires
637                // frequently enough to catch short-lived test allocations after
638                // a long namespace-loading phase of zero-yield cycles.
639                let soft_limit = self
640                    .config
641                    .lock()
642                    .unwrap()
643                    .as_ref()
644                    .map(|c| c.soft_limit())
645                    .unwrap_or(64 * 1024 * 1024);
646                let base_headroom = (soft_limit / 10).max(1);
647                let max_headroom = (soft_limit / 4).max(base_headroom);
648                let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
649                let headroom = if prev_headroom == 0 {
650                    base_headroom
651                } else {
652                    prev_headroom.saturating_mul(2).min(max_headroom)
653                };
654                self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
655                self.suppressed_threshold
656                    .store(post_memory + headroom, Ordering::Relaxed);
657                self.gc_suppressed.store(true, Ordering::Relaxed);
658            } else {
659                // GC freed something: reset exponential backoff.
660                self.zero_yield_headroom.store(0, Ordering::Relaxed);
661                self.gc_suppressed.store(false, Ordering::Relaxed);
662            }
663        }
664
665        pub fn count(&self) -> usize {
666            self.inner.lock().unwrap().count
667        }
668        pub fn total_allocated(&self) -> usize {
669            self.inner.lock().unwrap().total_allocated
670        }
671        pub fn total_freed(&self) -> usize {
672            self.inner.lock().unwrap().total_freed
673        }
674
675        pub fn collect_auto(&self) -> bool {
676            cljrs_logging::feat_debug!("gc", "automatic collection requested");
677            let Some(_stw_guard) = crate::cancellation::begin_stw() else {
678                cljrs_logging::feat_debug!("gc", "automatic collection skipped");
679                return false;
680            };
681            self.collect(|visitor| self.trace_registered_roots(visitor));
682            true
683        }
684    }
685
686    pub static HEAP: GcHeap = GcHeap::new();
687
688    thread_local! {
689        pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
690    }
691
692    pub struct AllocRootGuard {
693        saved_len: usize,
694    }
695
696    impl Drop for AllocRootGuard {
697        fn drop(&mut self) {
698            ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
699        }
700    }
701
702    pub fn push_alloc_frame() -> AllocRootGuard {
703        let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
704        AllocRootGuard { saved_len }
705    }
706
707    fn register_alloc(header: *mut GcBoxHeader) {
708        ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
709    }
710
711    pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
712        ALLOC_ROOTS.with(|roots| {
713            let roots = roots.borrow();
714            for &header in roots.iter() {
715                unsafe { visitor.mark_header(header) };
716            }
717        });
718    }
719}
720
721// =============================================================================
722// no-gc stubs
723// =============================================================================
724
725#[cfg(feature = "no-gc")]
726mod nogc_stubs {
727    use crate::MarkVisitor;
728    use std::sync::Arc;
729
730    #[derive(Debug, Clone)]
731    pub struct GcConfig;
732    impl GcConfig {
733        pub fn new() -> Self {
734            Self
735        }
736        pub fn with_hard_limit(_: usize) -> Self {
737            Self
738        }
739        pub fn with_limits(_: usize, _: usize) -> Self {
740            Self
741        }
742    }
743    impl Default for GcConfig {
744        fn default() -> Self {
745            Self::new()
746        }
747    }
748
749    pub struct GcHeap;
750    impl Default for GcHeap {
751        fn default() -> Self {
752            Self::new()
753        }
754    }
755    impl GcHeap {
756        pub const fn new() -> Self {
757            Self
758        }
759        pub fn set_config(&self, _: Arc<GcConfig>) {}
760        pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + Send + Sync + 'static) {}
761        pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
762        pub fn memory_in_use(&self) -> usize {
763            0
764        }
765        pub fn count(&self) -> usize {
766            0
767        }
768        pub fn total_allocated(&self) -> usize {
769            0
770        }
771        pub fn total_freed(&self) -> usize {
772            0
773        }
774        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
775        pub fn collect_auto(&self) -> bool {
776            false
777        }
778    }
779    unsafe impl Sync for GcHeap {}
780    pub static HEAP: GcHeap = GcHeap::new();
781
782    pub struct MutatorGuard;
783    impl Drop for MutatorGuard {
784        fn drop(&mut self) {}
785    }
786    pub struct StwGuard;
787    impl Drop for StwGuard {
788        fn drop(&mut self) {}
789    }
790    pub struct GcParked;
791    pub struct CancellableGuard;
792
793    pub struct GcCancellationStub;
794    impl GcCancellationStub {
795        pub const fn new() -> Self {
796            Self
797        }
798        pub fn in_progress(&self) -> bool {
799            false
800        }
801    }
802    pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
803
804    pub fn safepoint() {}
805    pub fn gc_requested() -> bool {
806        false
807    }
808    pub fn take_gc_request() -> bool {
809        false
810    }
811    pub fn begin_stw() -> Option<StwGuard> {
812        None
813    }
814    pub fn register_mutator() -> MutatorGuard {
815        MutatorGuard
816    }
817    pub fn registered_threads() -> usize {
818        0
819    }
820    pub fn request_gc() {}
821    pub fn check_cancellation() -> Result<(), GcParked> {
822        Ok(())
823    }
824    pub fn park_thread() {}
825    pub fn unpark_thread() {}
826    pub fn wait_for_threads_to_park() {}
827
828    pub struct AllocRootGuard;
829    impl Drop for AllocRootGuard {
830        fn drop(&mut self) {}
831    }
832    pub fn push_alloc_frame() -> AllocRootGuard {
833        AllocRootGuard
834    }
835}
836
837// =============================================================================
838// Tests
839// =============================================================================
840
841#[cfg(all(test, not(feature = "no-gc")))]
842mod tests {
843    use super::*;
844    use std::sync::{Arc, Mutex};
845
846    #[derive(Debug)]
847    #[allow(dead_code)]
848    struct Tracked {
849        value: i32,
850        dropped: Arc<Mutex<bool>>,
851    }
852    impl Drop for Tracked {
853        fn drop(&mut self) {
854            *self.dropped.lock().unwrap() = true;
855        }
856    }
857    impl Trace for Tracked {
858        fn trace(&self, _: &mut MarkVisitor) {}
859    }
860
861    #[derive(Debug)]
862    #[allow(dead_code)]
863    struct Parent {
864        child: GcPtr<Tracked>,
865    }
866    impl Trace for Parent {
867        fn trace(&self, visitor: &mut MarkVisitor) {
868            visitor.visit(&self.child);
869        }
870    }
871
872    fn fresh_heap() -> gc_full::GcHeap {
873        let heap = gc_full::GcHeap::new();
874        heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
875        heap
876    }
877
878    #[test]
879    fn alloc_and_get() {
880        let heap = fresh_heap();
881        let p = heap.alloc(42i64);
882        assert_eq!(*p.get(), 42);
883        assert_eq!(heap.count(), 1);
884    }
885
886    #[test]
887    fn clone_is_same_ptr() {
888        let heap = fresh_heap();
889        let p = heap.alloc(99i64);
890        let q = p.clone();
891        assert!(GcPtr::ptr_eq(&p, &q));
892    }
893
894    #[test]
895    fn collect_keeps_reachable() {
896        let heap = fresh_heap();
897        let dropped = Arc::new(Mutex::new(false));
898        let p = heap.alloc(Tracked {
899            value: 2,
900            dropped: dropped.clone(),
901        });
902        heap.collect(|vis| vis.visit(&p));
903        assert_eq!(heap.count(), 1);
904        assert!(!*dropped.lock().unwrap());
905    }
906}
907
908#[cfg(all(test, feature = "no-gc"))]
909mod nogc_tests {
910    use super::*;
911    use alloc_ctx::{ScratchGuard, StaticCtxGuard};
912
913    #[test]
914    fn alloc_in_static_context() {
915        let _g = StaticCtxGuard::new();
916        let p = GcPtr::new(42i64);
917        assert_eq!(*p.get(), 42);
918    }
919
920    #[test]
921    fn alloc_in_scratch_region() {
922        let mut scratch = ScratchGuard::new();
923        let p = GcPtr::new(99i64);
924        assert_eq!(*p.get(), 99);
925        scratch.pop_for_return();
926        assert_eq!(*p.get(), 99);
927        // scratch drops here, resets the region
928    }
929
930    #[test]
931    fn ptr_eq() {
932        let _g = StaticCtxGuard::new();
933        let p = GcPtr::new(1i64);
934        let q = p.clone();
935        assert!(GcPtr::ptr_eq(&p, &q));
936    }
937}