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 crate::stats::GC_STATS.record_gc_alloc(obj_size);
670 crate::governor::with_current_account(|a| a.charge(obj_size));
671 let current_usage =
672 self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
673
674 if let Some(config) = self.config.lock().unwrap().as_ref()
675 && config.soft_limit_exceeded(current_usage)
676 {
677 if self.gc_suppressed.load(Ordering::Relaxed) {
678 let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
682 if current_usage > threshold {
683 self.gc_suppressed.store(false, Ordering::Relaxed);
684 crate::cancellation::request_gc();
685 }
686 } else {
687 crate::cancellation::request_gc();
688 }
689 }
690
691 register_alloc(raw as *mut GcBoxHeader);
692 GcPtr(unsafe { NonNull::new_unchecked(raw) })
693 }
694
695 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
696 let pre_count = self.inner.lock().unwrap().count;
697 let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
698 tracing::debug!(
699 target: "gc",
700 "starting collection: {} objects, ~{} bytes in use",
701 pre_count,
702 pre_memory
703 );
704 let mark_start = std::time::Instant::now();
705 let mut visitor = MarkVisitor::new();
706 trace_roots(&mut visitor);
707 crate::region::trace_active_regions(&mut visitor);
713 crate::region::trace_retired_regions(&mut visitor);
716 tracing::debug!(
717 target: "gc",
718 "starting drain with {} grey objects",
719 visitor.grey.len()
720 );
721 visitor.drain();
722 let mark_elapsed = mark_start.elapsed();
723
724 let sweep_start = std::time::Instant::now();
725 let mut inner = self.inner.lock().unwrap();
726 let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
727 let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
728 let mut freed_bytes: usize = 0;
730 let mut current = inner.head;
731 while !current.is_null() {
732 let header = unsafe { &*current };
733 let next = header.next.get();
734 let lives = header.lives.get();
735 let obj_size = header.size;
736 if lives >= GC_INITIAL_LIVES {
737 header.lives.set(GC_INITIAL_LIVES - 1);
739 live.push(current);
740 } else if lives > 0 {
741 header.lives.set(lives - 1);
743 live.push(current);
744 } else {
745 freed_bytes += obj_size;
747 dead.push(current);
748 }
749 current = next;
750 }
751 let freed_count = dead.len();
752 for ptr in dead {
753 let header = unsafe { &*ptr };
754 unsafe { (header.drop_fn)(ptr) };
755 inner.count -= 1;
756 inner.total_freed += 1;
757 }
758 inner.head = std::ptr::null_mut();
759 for ptr in live {
760 let header = unsafe { &*ptr };
761 header.next.set(inner.head);
762 inner.head = ptr;
763 }
764 self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
769 let sweep_elapsed = sweep_start.elapsed();
770 let post_memory = self.memory_in_use.load(Ordering::Relaxed);
771 crate::governor::with_current_account(|a| {
772 a.release(freed_bytes);
773 a.record_collection(crate::governor::CollectionReport {
774 bytes_before: pre_memory,
775 bytes_after: post_memory,
776 bytes_returned: freed_bytes,
777 duration: mark_elapsed + sweep_elapsed,
778 });
779 });
780 crate::stats::GC_STATS.record_gc_pause(
781 mark_elapsed + sweep_elapsed,
782 freed_count as u64,
783 freed_bytes as u64,
784 );
785 tracing::debug!(
786 target: "gc",
787 "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
788 freed_count,
789 freed_bytes,
790 inner.count,
791 post_memory,
792 mark_elapsed,
793 sweep_elapsed
794 );
795 if freed_count == 0 {
796 let soft_limit = self
807 .config
808 .lock()
809 .unwrap()
810 .as_ref()
811 .map(|c| c.soft_limit())
812 .unwrap_or(64 * 1024 * 1024);
813 let base_headroom = (soft_limit / 10).max(1);
814 let max_headroom = (soft_limit / 4).max(base_headroom);
815 let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
816 let headroom = if prev_headroom == 0 {
817 base_headroom
818 } else {
819 prev_headroom.saturating_mul(2).min(max_headroom)
820 };
821 self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
822 self.suppressed_threshold
823 .store(post_memory + headroom, Ordering::Relaxed);
824 self.gc_suppressed.store(true, Ordering::Relaxed);
825 } else {
826 self.zero_yield_headroom.store(0, Ordering::Relaxed);
828 self.gc_suppressed.store(false, Ordering::Relaxed);
829 }
830 }
831
832 pub fn count(&self) -> usize {
833 self.inner.lock().unwrap().count
834 }
835 pub fn total_allocated(&self) -> usize {
836 self.inner.lock().unwrap().total_allocated
837 }
838 pub fn total_freed(&self) -> usize {
839 self.inner.lock().unwrap().total_freed
840 }
841
842 pub fn collect_auto(&self) -> bool {
843 tracing::debug!(target: "gc", "automatic collection requested");
844 let Some(_stw_guard) = crate::cancellation::begin_stw() else {
845 tracing::debug!(target: "gc", "automatic collection skipped");
846 return false;
847 };
848 self.collect(|visitor| self.trace_registered_roots(visitor));
849 true
850 }
851 }
852
853 thread_local! {
854 static ISOLATE_HEAP: GcHeap = const { GcHeap::new() };
855 }
856
857 pub struct HeapProxy;
863
864 impl HeapProxy {
865 pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
866 ISOLATE_HEAP.with(|h| h.alloc(value))
867 }
868
869 pub fn set_config(&self, config: Arc<GcConfig>) {
870 ISOLATE_HEAP.with(|h| h.set_config(config));
871 }
872
873 pub fn set_config_from_env(&self) {
874 ISOLATE_HEAP.with(|h| h.set_config_from_env());
875 }
876
877 pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
878 ISOLATE_HEAP.with(|h| h.register_root_tracer(tracer));
879 }
880
881 pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
882 ISOLATE_HEAP.with(|h| h.trace_registered_roots(visitor));
883 }
884
885 pub fn memory_in_use(&self) -> usize {
886 ISOLATE_HEAP.with(|h| h.memory_in_use())
887 }
888
889 pub fn count(&self) -> usize {
890 ISOLATE_HEAP.with(|h| h.count())
891 }
892
893 pub fn total_allocated(&self) -> usize {
894 ISOLATE_HEAP.with(|h| h.total_allocated())
895 }
896
897 pub fn total_freed(&self) -> usize {
898 ISOLATE_HEAP.with(|h| h.total_freed())
899 }
900
901 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
902 ISOLATE_HEAP.with(|h| h.collect(trace_roots));
903 }
904
905 pub fn collect_auto(&self) -> bool {
906 ISOLATE_HEAP.with(|h| h.collect_auto())
907 }
908
909 #[cfg(test)]
910 pub fn set_memory_in_use(&self, bytes: usize) {
911 ISOLATE_HEAP.with(|h| h.set_memory_in_use(bytes));
912 }
913 }
914
915 unsafe impl Sync for HeapProxy {}
918
919 pub static HEAP: HeapProxy = HeapProxy;
920
921 thread_local! {
922 pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
923 }
924
925 pub struct AllocRootGuard {
926 saved_len: usize,
927 }
928
929 impl Drop for AllocRootGuard {
930 fn drop(&mut self) {
931 ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
932 }
933 }
934
935 pub fn push_alloc_frame() -> AllocRootGuard {
936 let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
937 AllocRootGuard { saved_len }
938 }
939
940 fn register_alloc(header: *mut GcBoxHeader) {
941 ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
942 }
943
944 pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
945 ALLOC_ROOTS.with(|roots| {
946 let roots = roots.borrow();
947 for &header in roots.iter() {
948 unsafe { visitor.mark_header(header) };
949 }
950 });
951 }
952}
953
954#[cfg(feature = "no-gc")]
959mod nogc_stubs {
960 use crate::MarkVisitor;
961 use std::sync::Arc;
962
963 #[derive(Debug, Clone)]
964 pub struct GcConfig;
965 impl GcConfig {
966 pub fn new() -> Self {
967 Self
968 }
969 pub fn with_hard_limit(_: usize) -> Self {
970 Self
971 }
972 pub fn with_limits(_: usize, _: usize) -> Self {
973 Self
974 }
975 pub fn with_soft_limit(_: usize) -> Self {
976 Self
977 }
978 pub fn try_with_limits(
979 soft_limit: usize,
980 hard_limit: usize,
981 ) -> Result<Self, crate::governor::MemoryConfigError> {
982 crate::governor::MemoryConfig::with_limits(soft_limit, hard_limit).map(|_| Self)
983 }
984 pub fn validate(&self) -> Result<(), crate::governor::MemoryConfigError> {
985 Ok(())
986 }
987 }
988 impl Default for GcConfig {
989 fn default() -> Self {
990 Self::new()
991 }
992 }
993
994 pub struct GcHeap;
995 impl Default for GcHeap {
996 fn default() -> Self {
997 Self::new()
998 }
999 }
1000 impl GcHeap {
1001 pub const fn new() -> Self {
1002 Self
1003 }
1004 pub fn set_config(&self, _: Arc<GcConfig>) {}
1005 pub fn set_config_from_env(&self) {}
1010 pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + 'static) {}
1011 pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
1012 pub fn memory_in_use(&self) -> usize {
1013 0
1014 }
1015 pub fn count(&self) -> usize {
1016 0
1017 }
1018 pub fn total_allocated(&self) -> usize {
1019 0
1020 }
1021 pub fn total_freed(&self) -> usize {
1022 0
1023 }
1024 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
1025 pub fn collect_auto(&self) -> bool {
1026 false
1027 }
1028 }
1029 unsafe impl Sync for GcHeap {}
1030 pub static HEAP: GcHeap = GcHeap::new();
1031
1032 pub struct MutatorGuard;
1033 impl Drop for MutatorGuard {
1034 fn drop(&mut self) {}
1035 }
1036 pub struct StwGuard;
1037 impl Drop for StwGuard {
1038 fn drop(&mut self) {}
1039 }
1040 pub struct GcParked;
1041
1042 pub struct GcCancellationStub;
1043 impl GcCancellationStub {
1044 pub const fn new() -> Self {
1045 Self
1046 }
1047 pub fn in_progress(&self) -> bool {
1048 false
1049 }
1050 }
1051 pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
1052
1053 pub fn safepoint() {}
1054 pub fn gc_requested() -> bool {
1055 false
1056 }
1057 pub fn take_gc_request() -> bool {
1058 false
1059 }
1060 pub fn begin_stw() -> Option<StwGuard> {
1061 None
1062 }
1063 pub fn register_mutator() -> MutatorGuard {
1064 MutatorGuard
1065 }
1066 pub fn registered_threads() -> usize {
1067 0
1068 }
1069 pub fn request_gc() {}
1070 pub fn check_cancellation() -> Result<(), GcParked> {
1071 Ok(())
1072 }
1073 pub fn park_thread() {}
1074 pub fn unpark_thread() {}
1075 pub fn wait_for_threads_to_park() {}
1076
1077 pub struct AllocRootGuard;
1078 impl Drop for AllocRootGuard {
1079 fn drop(&mut self) {}
1080 }
1081 pub fn push_alloc_frame() -> AllocRootGuard {
1082 AllocRootGuard
1083 }
1084}
1085
1086#[cfg(all(test, not(feature = "no-gc")))]
1091mod tests {
1092 use super::*;
1093 use std::sync::{Arc, Mutex};
1094
1095 #[derive(Debug)]
1096 #[allow(dead_code)]
1097 struct Tracked {
1098 value: i32,
1099 dropped: Arc<Mutex<bool>>,
1100 }
1101 impl Drop for Tracked {
1102 fn drop(&mut self) {
1103 *self.dropped.lock().unwrap() = true;
1104 }
1105 }
1106 impl Trace for Tracked {
1107 fn trace(&self, _: &mut MarkVisitor) {}
1108 }
1109
1110 #[derive(Debug)]
1111 #[allow(dead_code)]
1112 struct Parent {
1113 child: GcPtr<Tracked>,
1114 }
1115 impl Trace for Parent {
1116 fn trace(&self, visitor: &mut MarkVisitor) {
1117 visitor.visit(&self.child);
1118 }
1119 }
1120
1121 fn fresh_heap() -> gc_full::GcHeap {
1122 let heap = gc_full::GcHeap::new();
1123 heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
1124 heap
1125 }
1126
1127 #[test]
1128 fn alloc_and_get() {
1129 let heap = fresh_heap();
1130 let p = heap.alloc(42i64);
1131 assert_eq!(*p.get(), 42);
1132 assert_eq!(heap.count(), 1);
1133 }
1134
1135 #[test]
1136 fn clone_is_same_ptr() {
1137 let heap = fresh_heap();
1138 let p = heap.alloc(99i64);
1139 let q = p.clone();
1140 assert!(GcPtr::ptr_eq(&p, &q));
1141 }
1142
1143 #[test]
1144 fn parse_limit_mb_handles_valid_unset_and_malformed() {
1145 assert_eq!(gc_full::parse_limit_mb("X", Some("4"), 7), 4 * 1024 * 1024);
1147 assert_eq!(
1149 gc_full::parse_limit_mb("X", Some(" 4 "), 7),
1150 4 * 1024 * 1024
1151 );
1152 assert_eq!(gc_full::parse_limit_mb("X", None, 7), 7);
1154 assert_eq!(gc_full::parse_limit_mb("X", Some("foo"), 7), 7);
1156 assert_eq!(gc_full::parse_limit_mb("X", Some(""), 7), 7);
1157 assert_eq!(gc_full::parse_limit_mb("X", Some("-1"), 7), 7);
1158 assert_eq!(
1160 gc_full::parse_limit_mb("X", Some(&usize::MAX.to_string()), 7),
1161 usize::MAX
1162 );
1163 }
1164
1165 #[test]
1166 fn collect_keeps_reachable() {
1167 let heap = fresh_heap();
1168 let dropped = Arc::new(Mutex::new(false));
1169 let p = heap.alloc(Tracked {
1170 value: 2,
1171 dropped: dropped.clone(),
1172 });
1173 heap.collect(|vis| vis.visit(&p));
1174 assert_eq!(heap.count(), 1);
1175 assert!(!*dropped.lock().unwrap());
1176 }
1177
1178 #[test]
1179 fn b1_two_isolates_independent_heaps() {
1180 use std::sync::{Arc, Barrier};
1181 let barrier = Arc::new(Barrier::new(2));
1184 let b1 = barrier.clone();
1185 let h1 = std::thread::Builder::new()
1186 .name("isolate-1".into())
1187 .spawn(move || {
1188 let _mutator = crate::register_mutator();
1189 let _ptrs: Vec<_> = (0_i64..100)
1191 .map(|i| crate::gc_full::HEAP.alloc(i))
1192 .collect();
1193 b1.wait(); assert_eq!(
1196 crate::gc_full::HEAP.count(),
1197 100,
1198 "isolate-1 heap count should be 100"
1199 );
1200 })
1201 .unwrap();
1202
1203 let b2 = barrier.clone();
1204 let h2 = std::thread::Builder::new()
1205 .name("isolate-2".into())
1206 .spawn(move || {
1207 let _mutator = crate::register_mutator();
1208 let _ptrs: Vec<_> = (0_i64..200)
1210 .map(|i| crate::gc_full::HEAP.alloc(i))
1211 .collect();
1212 b2.wait();
1213 assert_eq!(
1215 crate::gc_full::HEAP.count(),
1216 200,
1217 "isolate-2 heap count should be 200"
1218 );
1219 })
1220 .unwrap();
1221
1222 h1.join().expect("isolate-1 panicked");
1223 h2.join().expect("isolate-2 panicked");
1224 }
1225
1226 #[test]
1227 fn b1_two_isolates_gc_independently() {
1228 let h1 = std::thread::Builder::new()
1230 .name("gc-isolate-1".into())
1231 .spawn(|| {
1232 let _mutator = crate::register_mutator();
1233 let heap = &crate::gc_full::HEAP;
1234 heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1235 for _ in 0..5 {
1237 let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1238 heap.collect(|_| {});
1240 heap.collect(|_| {}); }
1242 })
1246 .unwrap();
1247
1248 let h2 = std::thread::Builder::new()
1249 .name("gc-isolate-2".into())
1250 .spawn(|| {
1251 let _mutator = crate::register_mutator();
1252 let heap = &crate::gc_full::HEAP;
1253 heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1254 for _ in 0..5 {
1255 let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1256 heap.collect(|_| {});
1257 heap.collect(|_| {});
1258 }
1259 })
1260 .unwrap();
1261
1262 h1.join().expect("gc-isolate-1 panicked");
1263 h2.join().expect("gc-isolate-2 panicked");
1264 }
1265}
1266
1267#[cfg(all(test, feature = "no-gc"))]
1268mod nogc_tests {
1269 use super::*;
1270 use alloc_ctx::{ScratchGuard, StaticCtxGuard};
1271
1272 #[test]
1273 fn alloc_in_static_context() {
1274 let _g = StaticCtxGuard::new();
1275 let p = GcPtr::new(42i64);
1276 assert_eq!(*p.get(), 42);
1277 }
1278
1279 #[test]
1280 fn alloc_in_scratch_region() {
1281 let mut scratch = ScratchGuard::new();
1282 let p = GcPtr::new(99i64);
1283 assert_eq!(*p.get(), 99);
1284 scratch.pop_for_return();
1285 assert_eq!(*p.get(), 99);
1286 }
1288
1289 #[test]
1290 fn ptr_eq() {
1291 let _g = StaticCtxGuard::new();
1292 let p = GcPtr::new(1i64);
1293 let q = p.clone();
1294 assert!(GcPtr::ptr_eq(&p, &q));
1295 }
1296}