1#![allow(clippy::missing_safety_doc)]
4#![allow(private_interfaces)]
5
6use std::ptr::NonNull;
7
8pub mod governor;
9pub mod region;
10pub mod stats;
11
12#[cfg(not(feature = "no-gc"))]
13pub mod cancellation;
14#[cfg(not(feature = "no-gc"))]
15pub mod config;
16
17#[cfg(feature = "no-gc")]
18pub mod alloc_ctx;
19#[cfg(feature = "no-gc")]
20pub mod static_arena;
21
22pub use governor::{
23 IsolateAccount, IsolateControl, MemoryCharge, MemoryClass, MemoryConfig, MemoryConfigError,
24 MemoryLimitExceeded, MemorySnapshot, PressureLevel, ProcessMemoryGovernor, governor,
25 register_current_isolate,
26};
27pub use stats::{
28 CLJRS_GC_STATS_ENV, GC_STATS, GcStats, GcStatsSnapshot, dump_stats_from_env,
29 report as stats_report,
30};
31
32#[cfg(not(feature = "no-gc"))]
35pub use cancellation::{
36 MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested, park_thread,
37 register_mutator, registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
38 wait_for_threads_to_park,
39};
40#[cfg(not(feature = "no-gc"))]
41pub use config::{GC_CANCELLATION as CONFIG_CANCELLATION, GcConfig, GcParked};
42
43#[cfg(not(feature = "no-gc"))]
44pub use gc_full::{
45 AllocRootGuard, GcHeap, HEAP, HeapProxy, push_alloc_frame, trace_thread_alloc_roots,
46};
47#[cfg(feature = "no-gc")]
48pub use nogc_stubs::{
49 AllocRootGuard, CONFIG_CANCELLATION, GcConfig, GcHeap, GcParked, HEAP, MutatorGuard, StwGuard,
50 begin_stw, check_cancellation, gc_requested, park_thread, push_alloc_frame, register_mutator,
51 registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
52 wait_for_threads_to_park,
53};
54
55#[cfg(all(feature = "no-gc", debug_assertions))]
60pub fn is_static_addr(addr: usize) -> bool {
61 static_arena::is_static_addr(addr)
62}
63
64pub trait Trace {
78 fn trace(&self, visitor: &mut MarkVisitor);
79
80 fn gc_size_extra(&self) -> usize {
81 0
82 }
83}
84
85impl Trace for String {
88 fn trace(&self, _: &mut MarkVisitor) {}
89
90 fn gc_size_extra(&self) -> usize {
91 self.capacity()
92 }
93}
94impl Trace for i64 {
95 fn trace(&self, _: &mut MarkVisitor) {}
96}
97impl Trace for f64 {
98 fn trace(&self, _: &mut MarkVisitor) {}
99}
100impl Trace for bool {
101 fn trace(&self, _: &mut MarkVisitor) {}
102}
103impl Trace for num_bigint::BigInt {
104 fn trace(&self, _: &mut MarkVisitor) {}
105}
106impl Trace for bigdecimal::BigDecimal {
107 fn trace(&self, _: &mut MarkVisitor) {}
108}
109impl Trace for num_rational::Ratio<num_bigint::BigInt> {
110 fn trace(&self, _: &mut MarkVisitor) {}
111}
112macro_rules! impl_trace_prim_array {
113 ($t:ty) => {
114 impl Trace for std::sync::Mutex<Vec<$t>> {
115 fn trace(&self, _: &mut MarkVisitor) {}
116 fn gc_size_extra(&self) -> usize {
117 self.lock().unwrap().capacity() * std::mem::size_of::<$t>()
118 }
119 }
120 };
121}
122impl_trace_prim_array!(i32);
123impl_trace_prim_array!(i64);
124impl_trace_prim_array!(i16);
125impl_trace_prim_array!(i8);
126impl_trace_prim_array!(char);
127impl_trace_prim_array!(f64);
128impl_trace_prim_array!(f32);
129impl_trace_prim_array!(bool);
130
131pub trait GcVisitor {
134 fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>);
135}
136
137#[cfg(not(feature = "no-gc"))]
142pub use self::gc_header::{GcBox, GcBoxHeader};
143
144#[cfg(not(feature = "no-gc"))]
145mod gc_header {
146 use crate::{MarkVisitor, Trace};
147 use std::cell::Cell;
148
149 pub(crate) const GC_INITIAL_LIVES: u8 = 2;
157
158 #[cfg(debug_assertions)]
159 pub(crate) const GC_MAGIC_ALIVE: u64 = 0xCAFE_BABE_DEAD_BEEF;
160 #[cfg(debug_assertions)]
161 pub(crate) const GC_MAGIC_FREED: u64 = 0xDEAD_DEAD_DEAD_DEAD;
162
163 #[repr(C)]
164 pub struct GcBoxHeader {
165 #[cfg(debug_assertions)]
166 pub(crate) magic: Cell<u64>,
167 pub(crate) size: usize,
169 pub(crate) lives: Cell<u8>,
170 pub(crate) next: Cell<*mut GcBoxHeader>,
171 pub(crate) trace_fn: unsafe fn(*const GcBoxHeader, &mut MarkVisitor),
172 pub(crate) drop_fn: unsafe fn(*mut GcBoxHeader),
173 }
174
175 impl GcBoxHeader {
176 pub(crate) fn new<T: Trace + 'static>(heap_extra: usize) -> Self {
177 Self {
178 #[cfg(debug_assertions)]
179 magic: Cell::new(GC_MAGIC_ALIVE),
180 size: std::mem::size_of::<GcBox<T>>() + heap_extra,
181 lives: Cell::new(GC_INITIAL_LIVES - 1),
182 next: Cell::new(std::ptr::null_mut()),
183 trace_fn: trace_gc_box::<T>,
184 drop_fn: drop_gc_box::<T>,
185 }
186 }
187 }
188
189 unsafe impl Send for GcBoxHeader {}
190 unsafe impl Sync for GcBoxHeader {}
191
192 #[repr(C)]
193 pub struct GcBox<T: Trace + 'static> {
194 pub(crate) header: GcBoxHeader,
195 pub value: T,
196 }
197
198 pub(crate) unsafe fn trace_gc_box<T: Trace + 'static>(
199 header: *const GcBoxHeader,
200 visitor: &mut MarkVisitor,
201 ) {
202 unsafe {
203 let gc_box = header as *const GcBox<T>;
204 (*gc_box).value.trace(visitor);
205 }
206 }
207
208 pub(crate) unsafe fn drop_gc_box<T: Trace + 'static>(header: *mut GcBoxHeader) {
209 unsafe {
210 #[cfg(debug_assertions)]
211 {
212 (*header).magic.set(GC_MAGIC_FREED);
213 }
214 let gc_box = header as *mut GcBox<T>;
215 drop(Box::from_raw(gc_box));
216 }
217 }
218}
219
220#[cfg(feature = "no-gc")]
225pub use self::nogc_box::GcBox;
226
227#[cfg(feature = "no-gc")]
228mod nogc_box {
229 use crate::Trace;
230
231 pub struct GcBox<T: Trace + 'static> {
232 pub value: T,
233 }
234}
235
236#[cfg(not(feature = "no-gc"))]
241#[derive(Default)]
242pub struct MarkVisitor {
243 pub(crate) grey: Vec<*mut GcBoxHeader>,
244}
245
246#[cfg(feature = "no-gc")]
247pub struct MarkVisitor;
248
249#[cfg(not(feature = "no-gc"))]
250impl MarkVisitor {
251 pub fn new() -> Self {
252 Self::default()
253 }
254
255 pub fn grey_len(&self) -> usize {
256 self.grey.len()
257 }
258
259 pub unsafe fn mark_header(&mut self, header: *mut GcBoxHeader) {
260 use gc_header::GC_INITIAL_LIVES;
261 let h = unsafe { &*header };
262 if h.lives.get() < GC_INITIAL_LIVES {
263 h.lives.set(GC_INITIAL_LIVES);
264 self.grey.push(header);
265 }
266 }
267
268 pub(crate) fn drain(&mut self) {
269 let mut visited = 0usize;
270 while let Some(header) = self.grey.pop() {
271 visited += 1;
272 let h = unsafe { &*header };
273 unsafe { (h.trace_fn)(header as *const GcBoxHeader, self) };
274 }
275 tracing::debug!(target: "gc", "drain visited {} objects", visited);
276 }
277}
278
279#[cfg(not(feature = "no-gc"))]
280impl GcVisitor for MarkVisitor {
281 fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
282 use gc_header::GC_INITIAL_LIVES;
283 if ptr.is_region_alloc() {
291 return;
292 }
293 let raw = ptr.raw();
294 let header = unsafe { &(*raw).header };
295 if header.lives.get() < GC_INITIAL_LIVES {
296 header.lives.set(GC_INITIAL_LIVES);
297 self.grey.push(raw as *mut GcBoxHeader);
298 }
299 }
300}
301
302#[cfg(feature = "no-gc")]
303impl MarkVisitor {
304 pub fn grey_len(&self) -> usize {
305 0
306 }
307 pub unsafe fn mark_header(&mut self, _: *mut u8) {}
308}
309
310#[cfg(feature = "no-gc")]
311impl GcVisitor for MarkVisitor {
312 fn visit<T: Trace + 'static>(&mut self, _: &GcPtr<T>) {}
313}
314
315pub struct GcPtr<T: Trace + 'static>(NonNull<GcBox<T>>);
320
321#[cfg(not(feature = "no-gc"))]
330pub(crate) const REGION_PTR_TAG: usize = 1;
331
332impl<T: Trace + 'static> GcPtr<T> {
333 #[cfg(not(feature = "no-gc"))]
334 pub fn new(value: T) -> Self {
335 gc_full::HEAP.alloc(value)
336 }
337
338 #[cfg(feature = "no-gc")]
339 pub fn new(value: T) -> Self {
340 alloc_ctx::alloc_in_ctx(value)
341 }
342
343 #[inline]
346 fn raw(&self) -> *mut GcBox<T> {
347 #[cfg(not(feature = "no-gc"))]
348 {
349 (self.0.as_ptr() as usize & !REGION_PTR_TAG) as *mut GcBox<T>
350 }
351 #[cfg(feature = "no-gc")]
352 {
353 self.0.as_ptr()
354 }
355 }
356
357 #[cfg(not(feature = "no-gc"))]
360 #[inline]
361 pub fn is_region_alloc(&self) -> bool {
362 (self.0.as_ptr() as usize & REGION_PTR_TAG) != 0
363 }
364
365 #[cfg(not(feature = "no-gc"))]
372 #[inline]
373 pub(crate) unsafe fn from_region_raw(raw: *mut GcBox<T>) -> Self {
374 GcPtr(unsafe { NonNull::new_unchecked((raw as usize | REGION_PTR_TAG) as *mut GcBox<T>) })
375 }
376
377 pub fn get(&self) -> &T {
378 #[cfg(all(debug_assertions, not(feature = "no-gc")))]
379 {
380 use gc_header::GC_MAGIC_ALIVE;
381 let header = unsafe { &(*self.raw()).header };
382 assert_eq!(
383 header.magic.get(),
384 GC_MAGIC_ALIVE,
385 "GcPtr::get() on freed object! magic={:#x}",
386 header.magic.get(),
387 );
388 }
389 unsafe { &(*self.raw()).value }
390 }
391
392 pub fn get_mut(&mut self) -> &mut T {
393 #[cfg(all(debug_assertions, not(feature = "no-gc")))]
394 {
395 use gc_header::GC_MAGIC_ALIVE;
396 let header = unsafe { &(*self.raw()).header };
397 assert_eq!(
398 header.magic.get(),
399 GC_MAGIC_ALIVE,
400 "GcPtr::get_mut() on freed object! magic={:#x}",
401 header.magic.get(),
402 );
403 }
404 unsafe { &mut (*self.raw()).value }
405 }
406
407 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
408 a.0 == b.0
409 }
410
411 #[cfg(all(feature = "no-gc", debug_assertions))]
417 pub fn is_static_alloc(&self) -> bool {
418 alloc_ctx::invocation_is_active() || static_arena::is_static_addr(self.0.as_ptr() as usize)
419 }
420}
421
422impl<T: Trace + 'static> Clone for GcPtr<T> {
423 fn clone(&self) -> Self {
424 GcPtr(self.0)
425 }
426}
427
428impl<T: Trace + 'static + std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 unsafe { (*self.raw()).value.fmt(f) }
431 }
432}
433
434impl<T: Trace + 'static> Drop for GcPtr<T> {
435 fn drop(&mut self) {}
436}
437
438pub struct StaticGcPtr<T: 'static>(NonNull<T>);
452
453unsafe impl<T: 'static> Send for StaticGcPtr<T> {}
457unsafe impl<T: 'static> Sync for StaticGcPtr<T> {}
458
459impl<T: 'static> StaticGcPtr<T> {
460 pub fn get(&self) -> &T {
462 unsafe { self.0.as_ref() }
464 }
465
466 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
469 a.0 == b.0
470 }
471}
472
473impl<T: 'static> Clone for StaticGcPtr<T> {
474 fn clone(&self) -> Self {
475 StaticGcPtr(self.0)
476 }
477}
478
479impl<T: 'static + std::fmt::Debug> std::fmt::Debug for StaticGcPtr<T> {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481 unsafe { self.0.as_ref().fmt(f) }
482 }
483}
484
485pub fn static_alloc<T: 'static>(value: T) -> StaticGcPtr<T> {
491 #[cfg(feature = "no-gc")]
492 {
493 static_arena::static_alloc_val(value)
494 }
495 #[cfg(not(feature = "no-gc"))]
496 {
497 StaticGcPtr(NonNull::from(Box::leak(Box::new(value))))
498 }
499}
500
501#[cfg(not(feature = "no-gc"))]
506mod gc_full {
507 use std::cell::RefCell;
508 use std::ptr::NonNull;
509 use std::sync::atomic::{AtomicUsize, Ordering};
510 use std::sync::{Arc, Mutex};
511
512 use crate::config::GcConfig;
513 use crate::gc_header::GC_INITIAL_LIVES;
514 use crate::{GcBox, GcBoxHeader, GcPtr, MarkVisitor, Trace};
515
516 type RootTracer = Box<dyn Fn(&mut MarkVisitor)>;
517
518 pub struct GcHeap {
519 inner: Mutex<GcHeapInner>,
520 config: Mutex<Option<Arc<GcConfig>>>,
521 memory_in_use: AtomicUsize,
522 total_allocated_bytes: AtomicUsize,
523 root_tracers: Mutex<Vec<RootTracer>>,
524 gc_suppressed: std::sync::atomic::AtomicBool,
525 suppressed_threshold: AtomicUsize,
532 zero_yield_headroom: AtomicUsize,
534 }
535
536 struct GcHeapInner {
537 head: *mut GcBoxHeader,
538 count: usize,
539 total_allocated: usize,
540 total_freed: usize,
541 }
542
543 unsafe impl Send for GcHeapInner {}
544
545 impl GcHeapInner {
546 const fn new() -> Self {
547 Self {
548 head: std::ptr::null_mut(),
549 count: 0,
550 total_allocated: 0,
551 total_freed: 0,
552 }
553 }
554 }
555
556 unsafe impl Sync for GcHeap {}
557
558 impl Default for GcHeap {
559 fn default() -> Self {
560 Self::new()
561 }
562 }
563
564 pub(crate) fn parse_limit_mb(var: &str, value: Option<&str>, default: usize) -> usize {
569 match value {
570 Some(s) => match s.trim().parse::<usize>() {
571 Ok(mb) => mb.saturating_mul(1024 * 1024),
572 Err(_) => {
573 eprintln!(
574 "[gc] warning: ignoring invalid {var}={s:?} (expected a number of megabytes)"
575 );
576 default
577 }
578 },
579 None => default,
580 }
581 }
582
583 impl GcHeap {
584 pub const fn new() -> Self {
585 Self {
586 inner: Mutex::new(GcHeapInner::new()),
587 config: Mutex::new(None),
588 memory_in_use: AtomicUsize::new(0),
589 total_allocated_bytes: AtomicUsize::new(0),
590 root_tracers: Mutex::new(Vec::new()),
591 gc_suppressed: std::sync::atomic::AtomicBool::new(false),
592 suppressed_threshold: AtomicUsize::new(0),
593 zero_yield_headroom: AtomicUsize::new(0),
594 }
595 }
596
597 pub fn set_config(&self, config: Arc<GcConfig>) {
598 *self.config.lock().unwrap() = Some(config);
599 }
600
601 pub fn set_config_from_env(&self) {
602 #[cfg(not(target_arch = "wasm32"))]
603 let default_soft_limit: usize = (system_memory::total() / 3) as usize;
604 #[cfg(target_arch = "wasm32")]
605 let default_soft_limit: usize = 64 * 1024 * 1024;
606
607 let soft_limit_mb = parse_limit_mb(
608 "CLJRS_GC_SOFT_LIMIT_MB",
609 std::env::var("CLJRS_GC_SOFT_LIMIT_MB").ok().as_deref(),
610 default_soft_limit,
611 );
612 let mut hard_limit_mb = parse_limit_mb(
613 "CLJRS_GC_HARD_LIMIT_MB",
614 std::env::var("CLJRS_GC_HARD_LIMIT_MB").ok().as_deref(),
615 soft_limit_mb,
616 );
617 if hard_limit_mb < soft_limit_mb {
618 eprintln!(
619 "[gc] warning: CLJRS_GC_HARD_LIMIT_MB is below CLJRS_GC_SOFT_LIMIT_MB; using the soft limit for both"
620 );
621 hard_limit_mb = soft_limit_mb;
622 }
623 self.set_config(Arc::new(GcConfig::with_limits(
624 soft_limit_mb,
625 hard_limit_mb,
626 )));
627 }
628
629 pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
630 self.root_tracers.lock().unwrap().push(Box::new(tracer));
631 }
632
633 pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
634 let tracers = self.root_tracers.lock().unwrap();
635 for tracer in tracers.iter() {
636 tracer(visitor);
637 }
638 }
639
640 pub fn memory_in_use(&self) -> usize {
641 self.memory_in_use.load(Ordering::Relaxed)
642 }
643
644 #[cfg(test)]
645 pub fn set_memory_in_use(&self, bytes: usize) {
646 self.memory_in_use.store(bytes, Ordering::Relaxed);
647 }
648
649 pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
650 crate::cancellation::safepoint();
651 let heap_extra = value.gc_size_extra();
652 let gc_box = Box::new(GcBox {
653 header: GcBoxHeader::new::<T>(heap_extra),
654 value,
655 });
656 let obj_size = gc_box.header.size; let raw: *mut GcBox<T> = Box::into_raw(gc_box);
658 {
659 let mut inner = self.inner.lock().unwrap();
660 unsafe {
661 (*raw).header.next.set(inner.head);
662 inner.head = raw as *mut GcBoxHeader;
663 }
664 inner.count += 1;
665 inner.total_allocated += 1;
666 }
667 self.total_allocated_bytes
668 .fetch_add(obj_size, Ordering::Relaxed);
669 if crate::governor::with_current_account(|a| a.charge(obj_size)).is_none() {
672 crate::stats::GC_STATS.record_gc_alloc(obj_size);
673 }
674 let current_usage =
675 self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
676
677 if let Some(config) = self.config.lock().unwrap().as_ref()
678 && config.soft_limit_exceeded(current_usage)
679 {
680 if self.gc_suppressed.load(Ordering::Relaxed) {
681 let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
685 if current_usage > threshold {
686 self.gc_suppressed.store(false, Ordering::Relaxed);
687 crate::cancellation::request_gc();
688 }
689 } else {
690 crate::cancellation::request_gc();
691 }
692 }
693
694 register_alloc(raw as *mut GcBoxHeader);
695 GcPtr(unsafe { NonNull::new_unchecked(raw) })
696 }
697
698 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
699 let pre_count = self.inner.lock().unwrap().count;
700 let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
701 tracing::debug!(
702 target: "gc",
703 "starting collection: {} objects, ~{} bytes in use",
704 pre_count,
705 pre_memory
706 );
707 let mark_start = std::time::Instant::now();
708 let mut visitor = MarkVisitor::new();
709 trace_roots(&mut visitor);
710 crate::region::trace_active_regions(&mut visitor);
716 crate::region::trace_retired_regions(&mut visitor);
719 tracing::debug!(
720 target: "gc",
721 "starting drain with {} grey objects",
722 visitor.grey.len()
723 );
724 visitor.drain();
725 let mark_elapsed = mark_start.elapsed();
726
727 let sweep_start = std::time::Instant::now();
728 let mut inner = self.inner.lock().unwrap();
729 let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
730 let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
731 let mut freed_bytes: usize = 0;
733 let mut current = inner.head;
734 while !current.is_null() {
735 let header = unsafe { &*current };
736 let next = header.next.get();
737 let lives = header.lives.get();
738 let obj_size = header.size;
739 if lives >= GC_INITIAL_LIVES {
740 header.lives.set(GC_INITIAL_LIVES - 1);
742 live.push(current);
743 } else if lives > 0 {
744 header.lives.set(lives - 1);
746 live.push(current);
747 } else {
748 freed_bytes += obj_size;
750 dead.push(current);
751 }
752 current = next;
753 }
754 let freed_count = dead.len();
755 for ptr in dead {
756 let header = unsafe { &*ptr };
757 unsafe { (header.drop_fn)(ptr) };
758 inner.count -= 1;
759 inner.total_freed += 1;
760 }
761 inner.head = std::ptr::null_mut();
762 for ptr in live {
763 let header = unsafe { &*ptr };
764 header.next.set(inner.head);
765 inner.head = ptr;
766 }
767 self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
772 let sweep_elapsed = sweep_start.elapsed();
773 let post_memory = self.memory_in_use.load(Ordering::Relaxed);
774 crate::governor::with_current_account(|a| {
775 a.release(freed_bytes);
776 a.record_collection(crate::governor::CollectionReport {
777 bytes_before: pre_memory,
778 bytes_after: post_memory,
779 bytes_returned: freed_bytes,
780 duration: mark_elapsed + sweep_elapsed,
781 });
782 });
783 crate::stats::GC_STATS.record_gc_pause(
784 mark_elapsed + sweep_elapsed,
785 freed_count as u64,
786 freed_bytes as u64,
787 );
788 tracing::debug!(
789 target: "gc",
790 "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
791 freed_count,
792 freed_bytes,
793 inner.count,
794 post_memory,
795 mark_elapsed,
796 sweep_elapsed
797 );
798 if freed_count == 0 {
799 let soft_limit = self
810 .config
811 .lock()
812 .unwrap()
813 .as_ref()
814 .map(|c| c.soft_limit())
815 .unwrap_or(64 * 1024 * 1024);
816 let base_headroom = (soft_limit / 10).max(1);
817 let max_headroom = (soft_limit / 4).max(base_headroom);
818 let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
819 let headroom = if prev_headroom == 0 {
820 base_headroom
821 } else {
822 prev_headroom.saturating_mul(2).min(max_headroom)
823 };
824 self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
825 self.suppressed_threshold
826 .store(post_memory + headroom, Ordering::Relaxed);
827 self.gc_suppressed.store(true, Ordering::Relaxed);
828 } else {
829 self.zero_yield_headroom.store(0, Ordering::Relaxed);
831 self.gc_suppressed.store(false, Ordering::Relaxed);
832 }
833 }
834
835 pub fn count(&self) -> usize {
836 self.inner.lock().unwrap().count
837 }
838 pub fn total_allocated(&self) -> usize {
839 self.inner.lock().unwrap().total_allocated
840 }
841 pub fn total_freed(&self) -> usize {
842 self.inner.lock().unwrap().total_freed
843 }
844
845 pub fn collect_auto(&self) -> bool {
846 tracing::debug!(target: "gc", "automatic collection requested");
847 let Some(_stw_guard) = crate::cancellation::begin_stw() else {
848 tracing::debug!(target: "gc", "automatic collection skipped");
849 return false;
850 };
851 self.collect(|visitor| self.trace_registered_roots(visitor));
852 true
853 }
854 }
855
856 thread_local! {
857 static ISOLATE_HEAP: GcHeap = const { GcHeap::new() };
858 }
859
860 pub struct HeapProxy;
866
867 impl HeapProxy {
868 pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
869 ISOLATE_HEAP.with(|h| h.alloc(value))
870 }
871
872 pub fn set_config(&self, config: Arc<GcConfig>) {
873 ISOLATE_HEAP.with(|h| h.set_config(config));
874 }
875
876 pub fn set_config_from_env(&self) {
877 ISOLATE_HEAP.with(|h| h.set_config_from_env());
878 }
879
880 pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
881 ISOLATE_HEAP.with(|h| h.register_root_tracer(tracer));
882 }
883
884 pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
885 ISOLATE_HEAP.with(|h| h.trace_registered_roots(visitor));
886 }
887
888 pub fn memory_in_use(&self) -> usize {
889 ISOLATE_HEAP.with(|h| h.memory_in_use())
890 }
891
892 pub fn count(&self) -> usize {
893 ISOLATE_HEAP.with(|h| h.count())
894 }
895
896 pub fn total_allocated(&self) -> usize {
897 ISOLATE_HEAP.with(|h| h.total_allocated())
898 }
899
900 pub fn total_freed(&self) -> usize {
901 ISOLATE_HEAP.with(|h| h.total_freed())
902 }
903
904 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
905 ISOLATE_HEAP.with(|h| h.collect(trace_roots));
906 }
907
908 pub fn collect_auto(&self) -> bool {
909 ISOLATE_HEAP.with(|h| h.collect_auto())
910 }
911
912 #[cfg(test)]
913 pub fn set_memory_in_use(&self, bytes: usize) {
914 ISOLATE_HEAP.with(|h| h.set_memory_in_use(bytes));
915 }
916 }
917
918 unsafe impl Sync for HeapProxy {}
921
922 pub static HEAP: HeapProxy = HeapProxy;
923
924 thread_local! {
925 pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
926 }
927
928 pub struct AllocRootGuard {
929 saved_len: usize,
930 }
931
932 impl Drop for AllocRootGuard {
933 fn drop(&mut self) {
934 ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
935 }
936 }
937
938 pub fn push_alloc_frame() -> AllocRootGuard {
939 let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
940 AllocRootGuard { saved_len }
941 }
942
943 fn register_alloc(header: *mut GcBoxHeader) {
944 ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
945 }
946
947 pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
948 ALLOC_ROOTS.with(|roots| {
949 let roots = roots.borrow();
950 for &header in roots.iter() {
951 unsafe { visitor.mark_header(header) };
952 }
953 });
954 }
955}
956
957#[cfg(feature = "no-gc")]
962mod nogc_stubs {
963 use crate::MarkVisitor;
964 use std::sync::Arc;
965
966 #[derive(Debug, Clone)]
967 pub struct GcConfig;
968 impl GcConfig {
969 pub fn new() -> Self {
970 Self
971 }
972 pub fn with_hard_limit(_: usize) -> Self {
973 Self
974 }
975 pub fn with_limits(_: usize, _: usize) -> Self {
976 Self
977 }
978 pub fn with_soft_limit(_: usize) -> Self {
979 Self
980 }
981 pub fn try_with_limits(
982 soft_limit: usize,
983 hard_limit: usize,
984 ) -> Result<Self, crate::governor::MemoryConfigError> {
985 crate::governor::MemoryConfig::with_limits(soft_limit, hard_limit).map(|_| Self)
986 }
987 pub fn validate(&self) -> Result<(), crate::governor::MemoryConfigError> {
988 Ok(())
989 }
990 }
991 impl Default for GcConfig {
992 fn default() -> Self {
993 Self::new()
994 }
995 }
996
997 pub struct GcHeap;
998 impl Default for GcHeap {
999 fn default() -> Self {
1000 Self::new()
1001 }
1002 }
1003 impl GcHeap {
1004 pub const fn new() -> Self {
1005 Self
1006 }
1007 pub fn set_config(&self, _: Arc<GcConfig>) {}
1008 pub fn set_config_from_env(&self) {}
1013 pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + 'static) {}
1014 pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
1015 pub fn memory_in_use(&self) -> usize {
1016 0
1017 }
1018 pub fn count(&self) -> usize {
1019 0
1020 }
1021 pub fn total_allocated(&self) -> usize {
1022 0
1023 }
1024 pub fn total_freed(&self) -> usize {
1025 0
1026 }
1027 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
1028 pub fn collect_auto(&self) -> bool {
1029 false
1030 }
1031 }
1032 unsafe impl Sync for GcHeap {}
1033 pub static HEAP: GcHeap = GcHeap::new();
1034
1035 pub struct MutatorGuard;
1036 impl Drop for MutatorGuard {
1037 fn drop(&mut self) {}
1038 }
1039 pub struct StwGuard;
1040 impl Drop for StwGuard {
1041 fn drop(&mut self) {}
1042 }
1043 pub struct GcParked;
1044
1045 pub struct GcCancellationStub;
1046 impl GcCancellationStub {
1047 pub const fn new() -> Self {
1048 Self
1049 }
1050 pub fn in_progress(&self) -> bool {
1051 false
1052 }
1053 }
1054 pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
1055
1056 pub fn safepoint() {}
1057 pub fn gc_requested() -> bool {
1058 false
1059 }
1060 pub fn take_gc_request() -> bool {
1061 false
1062 }
1063 pub fn begin_stw() -> Option<StwGuard> {
1064 None
1065 }
1066 pub fn register_mutator() -> MutatorGuard {
1067 MutatorGuard
1068 }
1069 pub fn registered_threads() -> usize {
1070 0
1071 }
1072 pub fn request_gc() {}
1073 pub fn check_cancellation() -> Result<(), GcParked> {
1074 Ok(())
1075 }
1076 pub fn park_thread() {}
1077 pub fn unpark_thread() {}
1078 pub fn wait_for_threads_to_park() {}
1079
1080 pub struct AllocRootGuard;
1081 impl Drop for AllocRootGuard {
1082 fn drop(&mut self) {}
1083 }
1084 pub fn push_alloc_frame() -> AllocRootGuard {
1085 AllocRootGuard
1086 }
1087}
1088
1089#[cfg(all(test, not(feature = "no-gc")))]
1094mod tests {
1095 use super::*;
1096 use std::sync::{Arc, Mutex};
1097
1098 #[derive(Debug)]
1099 #[allow(dead_code)]
1100 struct Tracked {
1101 value: i32,
1102 dropped: Arc<Mutex<bool>>,
1103 }
1104 impl Drop for Tracked {
1105 fn drop(&mut self) {
1106 *self.dropped.lock().unwrap() = true;
1107 }
1108 }
1109 impl Trace for Tracked {
1110 fn trace(&self, _: &mut MarkVisitor) {}
1111 }
1112
1113 #[derive(Debug)]
1114 #[allow(dead_code)]
1115 struct Parent {
1116 child: GcPtr<Tracked>,
1117 }
1118 impl Trace for Parent {
1119 fn trace(&self, visitor: &mut MarkVisitor) {
1120 visitor.visit(&self.child);
1121 }
1122 }
1123
1124 fn fresh_heap() -> gc_full::GcHeap {
1125 let heap = gc_full::GcHeap::new();
1126 heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
1127 heap
1128 }
1129
1130 #[test]
1131 fn alloc_and_get() {
1132 let heap = fresh_heap();
1133 let p = heap.alloc(42i64);
1134 assert_eq!(*p.get(), 42);
1135 assert_eq!(heap.count(), 1);
1136 }
1137
1138 #[test]
1139 fn clone_is_same_ptr() {
1140 let heap = fresh_heap();
1141 let p = heap.alloc(99i64);
1142 let q = p.clone();
1143 assert!(GcPtr::ptr_eq(&p, &q));
1144 }
1145
1146 #[test]
1147 fn parse_limit_mb_handles_valid_unset_and_malformed() {
1148 assert_eq!(gc_full::parse_limit_mb("X", Some("4"), 7), 4 * 1024 * 1024);
1150 assert_eq!(
1152 gc_full::parse_limit_mb("X", Some(" 4 "), 7),
1153 4 * 1024 * 1024
1154 );
1155 assert_eq!(gc_full::parse_limit_mb("X", None, 7), 7);
1157 assert_eq!(gc_full::parse_limit_mb("X", Some("foo"), 7), 7);
1159 assert_eq!(gc_full::parse_limit_mb("X", Some(""), 7), 7);
1160 assert_eq!(gc_full::parse_limit_mb("X", Some("-1"), 7), 7);
1161 assert_eq!(
1163 gc_full::parse_limit_mb("X", Some(&usize::MAX.to_string()), 7),
1164 usize::MAX
1165 );
1166 }
1167
1168 #[test]
1169 fn collect_keeps_reachable() {
1170 let heap = fresh_heap();
1171 let dropped = Arc::new(Mutex::new(false));
1172 let p = heap.alloc(Tracked {
1173 value: 2,
1174 dropped: dropped.clone(),
1175 });
1176 heap.collect(|vis| vis.visit(&p));
1177 assert_eq!(heap.count(), 1);
1178 assert!(!*dropped.lock().unwrap());
1179 }
1180
1181 #[test]
1182 fn b1_two_isolates_independent_heaps() {
1183 use std::sync::{Arc, Barrier};
1184 let barrier = Arc::new(Barrier::new(2));
1187 let b1 = barrier.clone();
1188 let h1 = std::thread::Builder::new()
1189 .name("isolate-1".into())
1190 .spawn(move || {
1191 let _mutator = crate::register_mutator();
1192 let _ptrs: Vec<_> = (0_i64..100)
1194 .map(|i| crate::gc_full::HEAP.alloc(i))
1195 .collect();
1196 b1.wait(); assert_eq!(
1199 crate::gc_full::HEAP.count(),
1200 100,
1201 "isolate-1 heap count should be 100"
1202 );
1203 })
1204 .unwrap();
1205
1206 let b2 = barrier.clone();
1207 let h2 = std::thread::Builder::new()
1208 .name("isolate-2".into())
1209 .spawn(move || {
1210 let _mutator = crate::register_mutator();
1211 let _ptrs: Vec<_> = (0_i64..200)
1213 .map(|i| crate::gc_full::HEAP.alloc(i))
1214 .collect();
1215 b2.wait();
1216 assert_eq!(
1218 crate::gc_full::HEAP.count(),
1219 200,
1220 "isolate-2 heap count should be 200"
1221 );
1222 })
1223 .unwrap();
1224
1225 h1.join().expect("isolate-1 panicked");
1226 h2.join().expect("isolate-2 panicked");
1227 }
1228
1229 #[test]
1230 fn b1_two_isolates_gc_independently() {
1231 let h1 = std::thread::Builder::new()
1233 .name("gc-isolate-1".into())
1234 .spawn(|| {
1235 let _mutator = crate::register_mutator();
1236 let heap = &crate::gc_full::HEAP;
1237 heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1238 for _ in 0..5 {
1240 let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1241 heap.collect(|_| {});
1243 heap.collect(|_| {}); }
1245 })
1249 .unwrap();
1250
1251 let h2 = std::thread::Builder::new()
1252 .name("gc-isolate-2".into())
1253 .spawn(|| {
1254 let _mutator = crate::register_mutator();
1255 let heap = &crate::gc_full::HEAP;
1256 heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1257 for _ in 0..5 {
1258 let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1259 heap.collect(|_| {});
1260 heap.collect(|_| {});
1261 }
1262 })
1263 .unwrap();
1264
1265 h1.join().expect("gc-isolate-1 panicked");
1266 h2.join().expect("gc-isolate-2 panicked");
1267 }
1268}
1269
1270#[cfg(all(test, feature = "no-gc"))]
1271mod nogc_tests {
1272 use super::*;
1273 use alloc_ctx::{ScratchGuard, StaticCtxGuard};
1274
1275 #[test]
1276 fn alloc_in_static_context() {
1277 let _g = StaticCtxGuard::new();
1278 let p = GcPtr::new(42i64);
1279 assert_eq!(*p.get(), 42);
1280 }
1281
1282 #[test]
1283 fn alloc_in_scratch_region() {
1284 let mut scratch = ScratchGuard::new();
1285 let p = GcPtr::new(99i64);
1286 assert_eq!(*p.get(), 99);
1287 scratch.pop_for_return();
1288 assert_eq!(*p.get(), 99);
1289 }
1291
1292 #[test]
1293 fn ptr_eq() {
1294 let _g = StaticCtxGuard::new();
1295 let p = GcPtr::new(1i64);
1296 let q = p.clone();
1297 assert!(GcPtr::ptr_eq(&p, &q));
1298 }
1299}