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 Admission, CriticalSection, IsolateAccount, IsolateControl, MemoryCharge, MemoryClass,
24 MemoryConfig, MemoryConfigError, MemoryLimitExceeded, MemorySnapshot, PressureLevel,
25 ProcessMemoryGovernor, ShedHookId, enter_critical, governor, register_current_isolate,
26 resolve_runtime_fault, with_critical,
27};
28pub use stats::{
29 CLJRS_GC_STATS_ENV, GC_STATS, GcStats, GcStatsSnapshot, dump_stats_from_env,
30 report as stats_report,
31};
32
33#[cfg(not(feature = "no-gc"))]
36pub use cancellation::{
37 MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested, park_thread,
38 register_mutator, registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
39 wait_for_threads_to_park,
40};
41#[cfg(not(feature = "no-gc"))]
42pub use config::{GC_CANCELLATION as CONFIG_CANCELLATION, GcConfig, GcParked};
43
44#[cfg(not(feature = "no-gc"))]
45pub use gc_full::{
46 AllocRootGuard, GcHeap, HEAP, HeapProxy, push_alloc_frame, trace_thread_alloc_roots,
47};
48#[cfg(feature = "no-gc")]
49pub use nogc_stubs::{
50 AllocRootGuard, CONFIG_CANCELLATION, GcConfig, GcHeap, GcParked, HEAP, MutatorGuard, StwGuard,
51 begin_stw, check_cancellation, gc_requested, park_thread, push_alloc_frame, register_mutator,
52 registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
53 wait_for_threads_to_park,
54};
55
56#[cfg(all(feature = "no-gc", debug_assertions))]
61pub fn is_static_addr(addr: usize) -> bool {
62 static_arena::is_static_addr(addr)
63}
64
65pub trait Trace {
79 fn trace(&self, visitor: &mut MarkVisitor);
80
81 fn gc_size_extra(&self) -> usize {
82 0
83 }
84}
85
86impl Trace for String {
89 fn trace(&self, _: &mut MarkVisitor) {}
90
91 fn gc_size_extra(&self) -> usize {
92 self.capacity()
93 }
94}
95impl Trace for i64 {
96 fn trace(&self, _: &mut MarkVisitor) {}
97}
98impl Trace for f64 {
99 fn trace(&self, _: &mut MarkVisitor) {}
100}
101impl Trace for bool {
102 fn trace(&self, _: &mut MarkVisitor) {}
103}
104impl Trace for num_bigint::BigInt {
105 fn trace(&self, _: &mut MarkVisitor) {}
106}
107impl Trace for bigdecimal::BigDecimal {
108 fn trace(&self, _: &mut MarkVisitor) {}
109}
110impl Trace for num_rational::Ratio<num_bigint::BigInt> {
111 fn trace(&self, _: &mut MarkVisitor) {}
112}
113macro_rules! impl_trace_prim_array {
114 ($t:ty) => {
115 impl Trace for std::sync::Mutex<Vec<$t>> {
116 fn trace(&self, _: &mut MarkVisitor) {}
117 fn gc_size_extra(&self) -> usize {
118 self.lock().unwrap().capacity() * std::mem::size_of::<$t>()
119 }
120 }
121 };
122}
123impl_trace_prim_array!(i32);
124impl_trace_prim_array!(i64);
125impl_trace_prim_array!(i16);
126impl_trace_prim_array!(i8);
127impl_trace_prim_array!(char);
128impl_trace_prim_array!(f64);
129impl_trace_prim_array!(f32);
130impl_trace_prim_array!(bool);
131
132pub trait GcVisitor {
135 fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>);
136}
137
138#[cfg(not(feature = "no-gc"))]
143pub use self::gc_header::{GcBox, GcBoxHeader};
144
145#[cfg(not(feature = "no-gc"))]
146mod gc_header {
147 use crate::{MarkVisitor, Trace};
148 use std::cell::Cell;
149
150 pub(crate) const GC_INITIAL_LIVES: u8 = 2;
158
159 #[cfg(debug_assertions)]
160 pub(crate) const GC_MAGIC_ALIVE: u64 = 0xCAFE_BABE_DEAD_BEEF;
161 #[cfg(debug_assertions)]
162 pub(crate) const GC_MAGIC_FREED: u64 = 0xDEAD_DEAD_DEAD_DEAD;
163
164 #[repr(C)]
165 pub struct GcBoxHeader {
166 #[cfg(debug_assertions)]
167 pub(crate) magic: Cell<u64>,
168 pub(crate) size: usize,
170 pub(crate) lives: Cell<u8>,
171 pub(crate) next: Cell<*mut GcBoxHeader>,
172 pub(crate) trace_fn: unsafe fn(*const GcBoxHeader, &mut MarkVisitor),
173 pub(crate) drop_fn: unsafe fn(*mut GcBoxHeader),
174 }
175
176 impl GcBoxHeader {
177 pub(crate) fn new<T: Trace + 'static>(heap_extra: usize) -> Self {
178 Self {
179 #[cfg(debug_assertions)]
180 magic: Cell::new(GC_MAGIC_ALIVE),
181 size: std::mem::size_of::<GcBox<T>>() + heap_extra,
182 lives: Cell::new(GC_INITIAL_LIVES - 1),
183 next: Cell::new(std::ptr::null_mut()),
184 trace_fn: trace_gc_box::<T>,
185 drop_fn: drop_gc_box::<T>,
186 }
187 }
188 }
189
190 unsafe impl Send for GcBoxHeader {}
191 unsafe impl Sync for GcBoxHeader {}
192
193 #[repr(C)]
194 pub struct GcBox<T: Trace + 'static> {
195 pub(crate) header: GcBoxHeader,
196 pub value: T,
197 }
198
199 pub(crate) unsafe fn trace_gc_box<T: Trace + 'static>(
200 header: *const GcBoxHeader,
201 visitor: &mut MarkVisitor,
202 ) {
203 unsafe {
204 let gc_box = header as *const GcBox<T>;
205 (*gc_box).value.trace(visitor);
206 }
207 }
208
209 pub(crate) unsafe fn drop_gc_box<T: Trace + 'static>(header: *mut GcBoxHeader) {
210 unsafe {
211 #[cfg(debug_assertions)]
212 {
213 (*header).magic.set(GC_MAGIC_FREED);
214 }
215 let gc_box = header as *mut GcBox<T>;
216 drop(Box::from_raw(gc_box));
217 }
218 }
219}
220
221#[cfg(feature = "no-gc")]
226pub use self::nogc_box::GcBox;
227
228#[cfg(feature = "no-gc")]
229mod nogc_box {
230 use crate::Trace;
231
232 pub struct GcBox<T: Trace + 'static> {
233 pub value: T,
234 }
235}
236
237#[cfg(not(feature = "no-gc"))]
242#[derive(Default)]
243pub struct MarkVisitor {
244 pub(crate) grey: Vec<*mut GcBoxHeader>,
245}
246
247#[cfg(feature = "no-gc")]
248pub struct MarkVisitor;
249
250#[cfg(not(feature = "no-gc"))]
251impl MarkVisitor {
252 pub fn new() -> Self {
253 Self::default()
254 }
255
256 pub fn grey_len(&self) -> usize {
257 self.grey.len()
258 }
259
260 pub unsafe fn mark_header(&mut self, header: *mut GcBoxHeader) {
261 use gc_header::GC_INITIAL_LIVES;
262 let h = unsafe { &*header };
263 if h.lives.get() < GC_INITIAL_LIVES {
264 h.lives.set(GC_INITIAL_LIVES);
265 self.grey.push(header);
266 }
267 }
268
269 pub(crate) fn drain(&mut self) {
270 let mut visited = 0usize;
271 while let Some(header) = self.grey.pop() {
272 visited += 1;
273 let h = unsafe { &*header };
274 unsafe { (h.trace_fn)(header as *const GcBoxHeader, self) };
275 }
276 tracing::debug!(target: "gc", "drain visited {} objects", visited);
277 }
278}
279
280#[cfg(not(feature = "no-gc"))]
281impl GcVisitor for MarkVisitor {
282 fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
283 use gc_header::GC_INITIAL_LIVES;
284 if ptr.is_region_alloc() {
292 return;
293 }
294 let raw = ptr.raw();
295 let header = unsafe { &(*raw).header };
296 if header.lives.get() < GC_INITIAL_LIVES {
297 header.lives.set(GC_INITIAL_LIVES);
298 self.grey.push(raw as *mut GcBoxHeader);
299 }
300 }
301}
302
303#[cfg(feature = "no-gc")]
304impl MarkVisitor {
305 pub fn grey_len(&self) -> usize {
306 0
307 }
308 pub unsafe fn mark_header(&mut self, _: *mut u8) {}
309}
310
311#[cfg(feature = "no-gc")]
312impl GcVisitor for MarkVisitor {
313 fn visit<T: Trace + 'static>(&mut self, _: &GcPtr<T>) {}
314}
315
316pub struct GcPtr<T: Trace + 'static>(NonNull<GcBox<T>>);
321
322#[cfg(not(feature = "no-gc"))]
331pub(crate) const REGION_PTR_TAG: usize = 1;
332
333pub struct AllocError<T> {
336 value: T,
337 error: MemoryLimitExceeded,
338}
339
340impl<T> AllocError<T> {
341 pub fn error(&self) -> MemoryLimitExceeded {
343 self.error
344 }
345
346 pub fn into_inner(self) -> T {
348 self.value
349 }
350
351 pub fn into_parts(self) -> (T, MemoryLimitExceeded) {
353 (self.value, self.error)
354 }
355}
356
357impl<T> std::fmt::Debug for AllocError<T> {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 f.debug_struct("AllocError")
360 .field("error", &self.error)
361 .finish_non_exhaustive()
362 }
363}
364
365impl<T> std::fmt::Display for AllocError<T> {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 self.error.fmt(f)
368 }
369}
370
371impl<T> std::error::Error for AllocError<T> {}
372
373impl<T> From<AllocError<T>> for MemoryLimitExceeded {
374 fn from(e: AllocError<T>) -> Self {
375 e.error
376 }
377}
378
379impl<T: Trace + 'static> GcPtr<T> {
380 #[cfg(not(feature = "no-gc"))]
385 pub fn new(value: T) -> Self {
386 gc_full::HEAP.alloc(value)
387 }
388
389 #[cfg(feature = "no-gc")]
390 pub fn new(value: T) -> Self {
391 alloc_ctx::alloc_in_ctx(value)
392 }
393
394 pub fn try_new(value: T) -> Result<Self, AllocError<T>> {
403 #[cfg(not(feature = "no-gc"))]
404 {
405 let bytes = std::mem::size_of::<GcBox<T>>() + value.gc_size_extra();
406 match governor::try_reserve_current(bytes) {
407 Ok(()) => Ok(Self::new(value)),
408 Err(error) => Err(AllocError { value, error }),
409 }
410 }
411 #[cfg(feature = "no-gc")]
412 {
413 alloc_ctx::try_alloc_in_ctx(value)
414 }
415 }
416
417 pub fn new_critical(value: T) -> Self {
421 let _section = governor::enter_critical();
422 Self::new(value)
423 }
424
425 #[inline]
428 fn raw(&self) -> *mut GcBox<T> {
429 #[cfg(not(feature = "no-gc"))]
430 {
431 (self.0.as_ptr() as usize & !REGION_PTR_TAG) as *mut GcBox<T>
432 }
433 #[cfg(feature = "no-gc")]
434 {
435 self.0.as_ptr()
436 }
437 }
438
439 #[cfg(not(feature = "no-gc"))]
442 #[inline]
443 pub fn is_region_alloc(&self) -> bool {
444 (self.0.as_ptr() as usize & REGION_PTR_TAG) != 0
445 }
446
447 #[cfg(not(feature = "no-gc"))]
454 #[inline]
455 pub(crate) unsafe fn from_region_raw(raw: *mut GcBox<T>) -> Self {
456 GcPtr(unsafe { NonNull::new_unchecked((raw as usize | REGION_PTR_TAG) as *mut GcBox<T>) })
457 }
458
459 pub fn get(&self) -> &T {
460 #[cfg(all(debug_assertions, not(feature = "no-gc")))]
461 {
462 use gc_header::GC_MAGIC_ALIVE;
463 let header = unsafe { &(*self.raw()).header };
464 assert_eq!(
465 header.magic.get(),
466 GC_MAGIC_ALIVE,
467 "GcPtr::get() on freed object! magic={:#x}",
468 header.magic.get(),
469 );
470 }
471 unsafe { &(*self.raw()).value }
472 }
473
474 pub fn get_mut(&mut self) -> &mut T {
475 #[cfg(all(debug_assertions, not(feature = "no-gc")))]
476 {
477 use gc_header::GC_MAGIC_ALIVE;
478 let header = unsafe { &(*self.raw()).header };
479 assert_eq!(
480 header.magic.get(),
481 GC_MAGIC_ALIVE,
482 "GcPtr::get_mut() on freed object! magic={:#x}",
483 header.magic.get(),
484 );
485 }
486 unsafe { &mut (*self.raw()).value }
487 }
488
489 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
490 a.0 == b.0
491 }
492
493 #[cfg(all(feature = "no-gc", debug_assertions))]
499 pub fn is_static_alloc(&self) -> bool {
500 alloc_ctx::invocation_is_active() || static_arena::is_static_addr(self.0.as_ptr() as usize)
501 }
502}
503
504impl<T: Trace + 'static> Clone for GcPtr<T> {
505 fn clone(&self) -> Self {
506 GcPtr(self.0)
507 }
508}
509
510impl<T: Trace + 'static + std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
511 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512 unsafe { (*self.raw()).value.fmt(f) }
513 }
514}
515
516impl<T: Trace + 'static> Drop for GcPtr<T> {
517 fn drop(&mut self) {}
518}
519
520pub struct StaticGcPtr<T: 'static>(NonNull<T>);
534
535unsafe impl<T: 'static> Send for StaticGcPtr<T> {}
539unsafe impl<T: 'static> Sync for StaticGcPtr<T> {}
540
541impl<T: 'static> StaticGcPtr<T> {
542 pub fn get(&self) -> &T {
544 unsafe { self.0.as_ref() }
546 }
547
548 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
551 a.0 == b.0
552 }
553}
554
555impl<T: 'static> Clone for StaticGcPtr<T> {
556 fn clone(&self) -> Self {
557 StaticGcPtr(self.0)
558 }
559}
560
561impl<T: 'static + std::fmt::Debug> std::fmt::Debug for StaticGcPtr<T> {
562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563 unsafe { self.0.as_ref().fmt(f) }
564 }
565}
566
567pub fn static_alloc<T: 'static>(value: T) -> StaticGcPtr<T> {
575 static_alloc_inner(value)
576}
577
578pub fn try_static_alloc<T: 'static>(value: T) -> Result<StaticGcPtr<T>, AllocError<T>> {
582 #[cfg(not(feature = "no-gc"))]
583 {
584 match governor::governor()
585 .try_charge_permanent(governor::MemoryClass::Static, std::mem::size_of::<T>())
586 {
587 Ok(()) => Ok(StaticGcPtr(NonNull::from(Box::leak(Box::new(value))))),
588 Err(error) => Err(AllocError { value, error }),
589 }
590 }
591 #[cfg(feature = "no-gc")]
592 {
593 static_arena::static_arena().try_alloc_val(value)
594 }
595}
596
597fn static_alloc_inner<T: 'static>(value: T) -> StaticGcPtr<T> {
598 #[cfg(feature = "no-gc")]
599 {
600 static_arena::static_alloc_val(value)
601 }
602 #[cfg(not(feature = "no-gc"))]
603 {
604 governor::governor()
605 .charge_permanent(governor::MemoryClass::Static, std::mem::size_of::<T>());
606 StaticGcPtr(NonNull::from(Box::leak(Box::new(value))))
607 }
608}
609
610#[cfg(not(feature = "no-gc"))]
615mod gc_full {
616 use std::cell::RefCell;
617 use std::ptr::NonNull;
618 use std::sync::atomic::{AtomicUsize, Ordering};
619 use std::sync::{Arc, Mutex};
620
621 use crate::config::GcConfig;
622 use crate::gc_header::GC_INITIAL_LIVES;
623 use crate::{GcBox, GcBoxHeader, GcPtr, MarkVisitor, Trace};
624
625 type RootTracer = Box<dyn Fn(&mut MarkVisitor)>;
626
627 pub struct GcHeap {
628 inner: Mutex<GcHeapInner>,
629 config: Mutex<Option<Arc<GcConfig>>>,
630 memory_in_use: AtomicUsize,
631 total_allocated_bytes: AtomicUsize,
632 root_tracers: Mutex<Vec<RootTracer>>,
633 gc_suppressed: std::sync::atomic::AtomicBool,
634 suppressed_threshold: AtomicUsize,
641 zero_yield_headroom: AtomicUsize,
643 }
644
645 struct GcHeapInner {
646 head: *mut GcBoxHeader,
647 count: usize,
648 total_allocated: usize,
649 total_freed: usize,
650 }
651
652 unsafe impl Send for GcHeapInner {}
653
654 impl GcHeapInner {
655 const fn new() -> Self {
656 Self {
657 head: std::ptr::null_mut(),
658 count: 0,
659 total_allocated: 0,
660 total_freed: 0,
661 }
662 }
663 }
664
665 unsafe impl Sync for GcHeap {}
666
667 impl Default for GcHeap {
668 fn default() -> Self {
669 Self::new()
670 }
671 }
672
673 pub(crate) fn parse_limit_mb(var: &str, value: Option<&str>, default: usize) -> usize {
678 match value {
679 Some(s) => match s.trim().parse::<usize>() {
680 Ok(mb) => mb.saturating_mul(1024 * 1024),
681 Err(_) => {
682 eprintln!(
683 "[gc] warning: ignoring invalid {var}={s:?} (expected a number of megabytes)"
684 );
685 default
686 }
687 },
688 None => default,
689 }
690 }
691
692 impl GcHeap {
693 pub const fn new() -> Self {
694 Self {
695 inner: Mutex::new(GcHeapInner::new()),
696 config: Mutex::new(None),
697 memory_in_use: AtomicUsize::new(0),
698 total_allocated_bytes: AtomicUsize::new(0),
699 root_tracers: Mutex::new(Vec::new()),
700 gc_suppressed: std::sync::atomic::AtomicBool::new(false),
701 suppressed_threshold: AtomicUsize::new(0),
702 zero_yield_headroom: AtomicUsize::new(0),
703 }
704 }
705
706 pub fn set_config(&self, config: Arc<GcConfig>) {
707 *self.config.lock().unwrap() = Some(config);
708 }
709
710 pub fn set_config_from_env(&self) {
711 #[cfg(not(target_arch = "wasm32"))]
712 let default_soft_limit: usize = (system_memory::total() / 3) as usize;
713 #[cfg(target_arch = "wasm32")]
714 let default_soft_limit: usize = 64 * 1024 * 1024;
715
716 let soft_limit_mb = parse_limit_mb(
717 "CLJRS_GC_SOFT_LIMIT_MB",
718 std::env::var("CLJRS_GC_SOFT_LIMIT_MB").ok().as_deref(),
719 default_soft_limit,
720 );
721 let mut hard_limit_mb = parse_limit_mb(
722 "CLJRS_GC_HARD_LIMIT_MB",
723 std::env::var("CLJRS_GC_HARD_LIMIT_MB").ok().as_deref(),
724 soft_limit_mb,
725 );
726 if hard_limit_mb < soft_limit_mb {
727 eprintln!(
728 "[gc] warning: CLJRS_GC_HARD_LIMIT_MB is below CLJRS_GC_SOFT_LIMIT_MB; using the soft limit for both"
729 );
730 hard_limit_mb = soft_limit_mb;
731 }
732 self.set_config(Arc::new(GcConfig::with_limits(
733 soft_limit_mb,
734 hard_limit_mb,
735 )));
736 }
737
738 pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
739 self.root_tracers.lock().unwrap().push(Box::new(tracer));
740 }
741
742 pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
743 let tracers = self.root_tracers.lock().unwrap();
744 for tracer in tracers.iter() {
745 tracer(visitor);
746 }
747 }
748
749 pub fn memory_in_use(&self) -> usize {
750 self.memory_in_use.load(Ordering::Relaxed)
751 }
752
753 #[cfg(test)]
754 pub fn set_memory_in_use(&self, bytes: usize) {
755 self.memory_in_use.store(bytes, Ordering::Relaxed);
756 }
757
758 pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
759 crate::cancellation::safepoint();
760 let heap_extra = value.gc_size_extra();
761 let gc_box = Box::new(GcBox {
762 header: GcBoxHeader::new::<T>(heap_extra),
763 value,
764 });
765 let obj_size = gc_box.header.size; let raw: *mut GcBox<T> = Box::into_raw(gc_box);
767 {
768 let mut inner = self.inner.lock().unwrap();
769 unsafe {
770 (*raw).header.next.set(inner.head);
771 inner.head = raw as *mut GcBoxHeader;
772 }
773 inner.count += 1;
774 inner.total_allocated += 1;
775 }
776 self.total_allocated_bytes
777 .fetch_add(obj_size, Ordering::Relaxed);
778 if crate::governor::with_current_account(|a| a.charge(obj_size)).is_none() {
781 crate::stats::GC_STATS.record_gc_alloc(obj_size);
782 }
783 let current_usage =
784 self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
785
786 if let Some(config) = self.config.lock().unwrap().as_ref()
787 && config.soft_limit_exceeded(current_usage)
788 {
789 if self.gc_suppressed.load(Ordering::Relaxed) {
790 let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
794 if current_usage > threshold {
795 self.gc_suppressed.store(false, Ordering::Relaxed);
796 crate::cancellation::request_gc();
797 }
798 } else {
799 crate::cancellation::request_gc();
800 }
801 }
802
803 register_alloc(raw as *mut GcBoxHeader);
804 GcPtr(unsafe { NonNull::new_unchecked(raw) })
805 }
806
807 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
808 let pre_count = self.inner.lock().unwrap().count;
809 let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
810 tracing::debug!(
811 target: "gc",
812 "starting collection: {} objects, ~{} bytes in use",
813 pre_count,
814 pre_memory
815 );
816 let mark_start = std::time::Instant::now();
817 let mut visitor = MarkVisitor::new();
818 trace_roots(&mut visitor);
819 crate::region::trace_active_regions(&mut visitor);
825 crate::region::trace_retired_regions(&mut visitor);
828 tracing::debug!(
829 target: "gc",
830 "starting drain with {} grey objects",
831 visitor.grey.len()
832 );
833 visitor.drain();
834 let mark_elapsed = mark_start.elapsed();
835
836 let sweep_start = std::time::Instant::now();
837 let mut inner = self.inner.lock().unwrap();
838 let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
839 let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
840 let mut freed_bytes: usize = 0;
842 let mut current = inner.head;
843 while !current.is_null() {
844 let header = unsafe { &*current };
845 let next = header.next.get();
846 let lives = header.lives.get();
847 let obj_size = header.size;
848 if lives >= GC_INITIAL_LIVES {
849 header.lives.set(GC_INITIAL_LIVES - 1);
851 live.push(current);
852 } else if lives > 0 {
853 header.lives.set(lives - 1);
855 live.push(current);
856 } else {
857 freed_bytes += obj_size;
859 dead.push(current);
860 }
861 current = next;
862 }
863 let freed_count = dead.len();
864 for ptr in dead {
865 let header = unsafe { &*ptr };
866 unsafe { (header.drop_fn)(ptr) };
867 inner.count -= 1;
868 inner.total_freed += 1;
869 }
870 inner.head = std::ptr::null_mut();
871 for ptr in live {
872 let header = unsafe { &*ptr };
873 header.next.set(inner.head);
874 inner.head = ptr;
875 }
876 self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
881 let sweep_elapsed = sweep_start.elapsed();
882 let post_memory = self.memory_in_use.load(Ordering::Relaxed);
883 crate::governor::with_current_account(|a| {
884 a.release(freed_bytes);
885 a.record_collection(crate::governor::CollectionReport {
886 bytes_before: pre_memory,
887 bytes_after: post_memory,
888 bytes_returned: freed_bytes,
889 duration: mark_elapsed + sweep_elapsed,
890 });
891 });
892 crate::stats::GC_STATS.record_gc_pause(
893 mark_elapsed + sweep_elapsed,
894 freed_count as u64,
895 freed_bytes as u64,
896 );
897 tracing::debug!(
898 target: "gc",
899 "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
900 freed_count,
901 freed_bytes,
902 inner.count,
903 post_memory,
904 mark_elapsed,
905 sweep_elapsed
906 );
907 if freed_count == 0 {
908 let soft_limit = self
919 .config
920 .lock()
921 .unwrap()
922 .as_ref()
923 .map(|c| c.soft_limit())
924 .unwrap_or(64 * 1024 * 1024);
925 let base_headroom = (soft_limit / 10).max(1);
926 let max_headroom = (soft_limit / 4).max(base_headroom);
927 let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
928 let headroom = if prev_headroom == 0 {
929 base_headroom
930 } else {
931 prev_headroom.saturating_mul(2).min(max_headroom)
932 };
933 self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
934 self.suppressed_threshold
935 .store(post_memory + headroom, Ordering::Relaxed);
936 self.gc_suppressed.store(true, Ordering::Relaxed);
937 } else {
938 self.zero_yield_headroom.store(0, Ordering::Relaxed);
940 self.gc_suppressed.store(false, Ordering::Relaxed);
941 }
942 }
943
944 pub fn count(&self) -> usize {
945 self.inner.lock().unwrap().count
946 }
947 pub fn total_allocated(&self) -> usize {
948 self.inner.lock().unwrap().total_allocated
949 }
950 pub fn total_freed(&self) -> usize {
951 self.inner.lock().unwrap().total_freed
952 }
953
954 pub fn collect_auto(&self) -> bool {
955 tracing::debug!(target: "gc", "automatic collection requested");
956 let Some(_stw_guard) = crate::cancellation::begin_stw() else {
957 tracing::debug!(target: "gc", "automatic collection skipped");
958 return false;
959 };
960 self.collect(|visitor| self.trace_registered_roots(visitor));
961 true
962 }
963 }
964
965 thread_local! {
966 static ISOLATE_HEAP: GcHeap = const { GcHeap::new() };
967 }
968
969 pub struct HeapProxy;
975
976 impl HeapProxy {
977 pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
978 ISOLATE_HEAP.with(|h| h.alloc(value))
979 }
980
981 pub fn set_config(&self, config: Arc<GcConfig>) {
982 ISOLATE_HEAP.with(|h| h.set_config(config));
983 }
984
985 pub fn set_config_from_env(&self) {
986 ISOLATE_HEAP.with(|h| h.set_config_from_env());
987 }
988
989 pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
990 ISOLATE_HEAP.with(|h| h.register_root_tracer(tracer));
991 }
992
993 pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
994 ISOLATE_HEAP.with(|h| h.trace_registered_roots(visitor));
995 }
996
997 pub fn memory_in_use(&self) -> usize {
998 ISOLATE_HEAP.with(|h| h.memory_in_use())
999 }
1000
1001 pub fn count(&self) -> usize {
1002 ISOLATE_HEAP.with(|h| h.count())
1003 }
1004
1005 pub fn total_allocated(&self) -> usize {
1006 ISOLATE_HEAP.with(|h| h.total_allocated())
1007 }
1008
1009 pub fn total_freed(&self) -> usize {
1010 ISOLATE_HEAP.with(|h| h.total_freed())
1011 }
1012
1013 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
1014 ISOLATE_HEAP.with(|h| h.collect(trace_roots));
1015 }
1016
1017 pub fn collect_auto(&self) -> bool {
1018 ISOLATE_HEAP.with(|h| h.collect_auto())
1019 }
1020
1021 #[cfg(test)]
1022 pub fn set_memory_in_use(&self, bytes: usize) {
1023 ISOLATE_HEAP.with(|h| h.set_memory_in_use(bytes));
1024 }
1025 }
1026
1027 unsafe impl Sync for HeapProxy {}
1030
1031 pub static HEAP: HeapProxy = HeapProxy;
1032
1033 thread_local! {
1034 pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
1035 }
1036
1037 pub struct AllocRootGuard {
1038 saved_len: usize,
1039 }
1040
1041 impl Drop for AllocRootGuard {
1042 fn drop(&mut self) {
1043 ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
1044 }
1045 }
1046
1047 pub fn push_alloc_frame() -> AllocRootGuard {
1048 let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
1049 AllocRootGuard { saved_len }
1050 }
1051
1052 fn register_alloc(header: *mut GcBoxHeader) {
1053 ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
1054 }
1055
1056 pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
1057 ALLOC_ROOTS.with(|roots| {
1058 let roots = roots.borrow();
1059 for &header in roots.iter() {
1060 unsafe { visitor.mark_header(header) };
1061 }
1062 });
1063 }
1064}
1065
1066#[cfg(feature = "no-gc")]
1071mod nogc_stubs {
1072 use crate::MarkVisitor;
1073 use std::sync::Arc;
1074
1075 #[derive(Debug, Clone)]
1076 pub struct GcConfig;
1077 impl GcConfig {
1078 pub fn new() -> Self {
1079 Self
1080 }
1081 pub fn with_hard_limit(_: usize) -> Self {
1082 Self
1083 }
1084 pub fn with_limits(_: usize, _: usize) -> Self {
1085 Self
1086 }
1087 pub fn with_soft_limit(_: usize) -> Self {
1088 Self
1089 }
1090 pub fn try_with_limits(
1091 soft_limit: usize,
1092 hard_limit: usize,
1093 ) -> Result<Self, crate::governor::MemoryConfigError> {
1094 crate::governor::MemoryConfig::with_limits(soft_limit, hard_limit).map(|_| Self)
1095 }
1096 pub fn validate(&self) -> Result<(), crate::governor::MemoryConfigError> {
1097 Ok(())
1098 }
1099 }
1100 impl Default for GcConfig {
1101 fn default() -> Self {
1102 Self::new()
1103 }
1104 }
1105
1106 pub struct GcHeap;
1107 impl Default for GcHeap {
1108 fn default() -> Self {
1109 Self::new()
1110 }
1111 }
1112 impl GcHeap {
1113 pub const fn new() -> Self {
1114 Self
1115 }
1116 pub fn set_config(&self, _: Arc<GcConfig>) {}
1117 pub fn set_config_from_env(&self) {}
1122 pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + 'static) {}
1123 pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
1124 pub fn memory_in_use(&self) -> usize {
1125 0
1126 }
1127 pub fn count(&self) -> usize {
1128 0
1129 }
1130 pub fn total_allocated(&self) -> usize {
1131 0
1132 }
1133 pub fn total_freed(&self) -> usize {
1134 0
1135 }
1136 pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
1137 pub fn collect_auto(&self) -> bool {
1138 false
1139 }
1140 }
1141 unsafe impl Sync for GcHeap {}
1142 pub static HEAP: GcHeap = GcHeap::new();
1143
1144 pub struct MutatorGuard;
1145 impl Drop for MutatorGuard {
1146 fn drop(&mut self) {}
1147 }
1148 pub struct StwGuard;
1149 impl Drop for StwGuard {
1150 fn drop(&mut self) {}
1151 }
1152 pub struct GcParked;
1153
1154 pub struct GcCancellationStub;
1155 impl GcCancellationStub {
1156 pub const fn new() -> Self {
1157 Self
1158 }
1159 pub fn in_progress(&self) -> bool {
1160 false
1161 }
1162 }
1163 pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
1164
1165 pub fn safepoint() {}
1166 pub fn gc_requested() -> bool {
1167 false
1168 }
1169 pub fn take_gc_request() -> bool {
1170 false
1171 }
1172 pub fn begin_stw() -> Option<StwGuard> {
1173 None
1174 }
1175 pub fn register_mutator() -> MutatorGuard {
1176 MutatorGuard
1177 }
1178 pub fn registered_threads() -> usize {
1179 0
1180 }
1181 pub fn request_gc() {}
1182 pub fn check_cancellation() -> Result<(), GcParked> {
1183 Ok(())
1184 }
1185 pub fn park_thread() {}
1186 pub fn unpark_thread() {}
1187 pub fn wait_for_threads_to_park() {}
1188
1189 pub struct AllocRootGuard;
1190 impl Drop for AllocRootGuard {
1191 fn drop(&mut self) {}
1192 }
1193 pub fn push_alloc_frame() -> AllocRootGuard {
1194 AllocRootGuard
1195 }
1196}
1197
1198#[cfg(all(test, not(feature = "no-gc")))]
1203mod tests {
1204 use super::*;
1205 use std::sync::{Arc, Mutex};
1206
1207 #[derive(Debug)]
1208 #[allow(dead_code)]
1209 struct Tracked {
1210 value: i32,
1211 dropped: Arc<Mutex<bool>>,
1212 }
1213 impl Drop for Tracked {
1214 fn drop(&mut self) {
1215 *self.dropped.lock().unwrap() = true;
1216 }
1217 }
1218 impl Trace for Tracked {
1219 fn trace(&self, _: &mut MarkVisitor) {}
1220 }
1221
1222 #[derive(Debug)]
1223 #[allow(dead_code)]
1224 struct Parent {
1225 child: GcPtr<Tracked>,
1226 }
1227 impl Trace for Parent {
1228 fn trace(&self, visitor: &mut MarkVisitor) {
1229 visitor.visit(&self.child);
1230 }
1231 }
1232
1233 fn fresh_heap() -> gc_full::GcHeap {
1234 let heap = gc_full::GcHeap::new();
1235 heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
1236 heap
1237 }
1238
1239 #[test]
1240 fn alloc_and_get() {
1241 let heap = fresh_heap();
1242 let p = heap.alloc(42i64);
1243 assert_eq!(*p.get(), 42);
1244 assert_eq!(heap.count(), 1);
1245 }
1246
1247 #[test]
1248 fn clone_is_same_ptr() {
1249 let heap = fresh_heap();
1250 let p = heap.alloc(99i64);
1251 let q = p.clone();
1252 assert!(GcPtr::ptr_eq(&p, &q));
1253 }
1254
1255 #[test]
1256 fn parse_limit_mb_handles_valid_unset_and_malformed() {
1257 assert_eq!(gc_full::parse_limit_mb("X", Some("4"), 7), 4 * 1024 * 1024);
1259 assert_eq!(
1261 gc_full::parse_limit_mb("X", Some(" 4 "), 7),
1262 4 * 1024 * 1024
1263 );
1264 assert_eq!(gc_full::parse_limit_mb("X", None, 7), 7);
1266 assert_eq!(gc_full::parse_limit_mb("X", Some("foo"), 7), 7);
1268 assert_eq!(gc_full::parse_limit_mb("X", Some(""), 7), 7);
1269 assert_eq!(gc_full::parse_limit_mb("X", Some("-1"), 7), 7);
1270 assert_eq!(
1272 gc_full::parse_limit_mb("X", Some(&usize::MAX.to_string()), 7),
1273 usize::MAX
1274 );
1275 }
1276
1277 #[test]
1278 fn collect_keeps_reachable() {
1279 let heap = fresh_heap();
1280 let dropped = Arc::new(Mutex::new(false));
1281 let p = heap.alloc(Tracked {
1282 value: 2,
1283 dropped: dropped.clone(),
1284 });
1285 heap.collect(|vis| vis.visit(&p));
1286 assert_eq!(heap.count(), 1);
1287 assert!(!*dropped.lock().unwrap());
1288 }
1289
1290 #[test]
1291 fn b1_two_isolates_independent_heaps() {
1292 use std::sync::{Arc, Barrier};
1293 let barrier = Arc::new(Barrier::new(2));
1296 let b1 = barrier.clone();
1297 let h1 = std::thread::Builder::new()
1298 .name("isolate-1".into())
1299 .spawn(move || {
1300 let _mutator = crate::register_mutator();
1301 let _ptrs: Vec<_> = (0_i64..100)
1303 .map(|i| crate::gc_full::HEAP.alloc(i))
1304 .collect();
1305 b1.wait(); assert_eq!(
1308 crate::gc_full::HEAP.count(),
1309 100,
1310 "isolate-1 heap count should be 100"
1311 );
1312 })
1313 .unwrap();
1314
1315 let b2 = barrier.clone();
1316 let h2 = std::thread::Builder::new()
1317 .name("isolate-2".into())
1318 .spawn(move || {
1319 let _mutator = crate::register_mutator();
1320 let _ptrs: Vec<_> = (0_i64..200)
1322 .map(|i| crate::gc_full::HEAP.alloc(i))
1323 .collect();
1324 b2.wait();
1325 assert_eq!(
1327 crate::gc_full::HEAP.count(),
1328 200,
1329 "isolate-2 heap count should be 200"
1330 );
1331 })
1332 .unwrap();
1333
1334 h1.join().expect("isolate-1 panicked");
1335 h2.join().expect("isolate-2 panicked");
1336 }
1337
1338 #[test]
1339 fn b1_two_isolates_gc_independently() {
1340 let h1 = std::thread::Builder::new()
1342 .name("gc-isolate-1".into())
1343 .spawn(|| {
1344 let _mutator = crate::register_mutator();
1345 let heap = &crate::gc_full::HEAP;
1346 heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1347 for _ in 0..5 {
1349 let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1350 heap.collect(|_| {});
1352 heap.collect(|_| {}); }
1354 })
1358 .unwrap();
1359
1360 let h2 = std::thread::Builder::new()
1361 .name("gc-isolate-2".into())
1362 .spawn(|| {
1363 let _mutator = crate::register_mutator();
1364 let heap = &crate::gc_full::HEAP;
1365 heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1366 for _ in 0..5 {
1367 let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1368 heap.collect(|_| {});
1369 heap.collect(|_| {});
1370 }
1371 })
1372 .unwrap();
1373
1374 h1.join().expect("gc-isolate-1 panicked");
1375 h2.join().expect("gc-isolate-2 panicked");
1376 }
1377}
1378
1379#[cfg(all(test, feature = "no-gc"))]
1380mod nogc_tests {
1381 use super::*;
1382 use alloc_ctx::{ScratchGuard, StaticCtxGuard};
1383
1384 #[test]
1385 fn alloc_in_static_context() {
1386 let _g = StaticCtxGuard::new();
1387 let p = GcPtr::new(42i64);
1388 assert_eq!(*p.get(), 42);
1389 }
1390
1391 #[test]
1392 fn alloc_in_scratch_region() {
1393 let mut scratch = ScratchGuard::new();
1394 let p = GcPtr::new(99i64);
1395 assert_eq!(*p.get(), 99);
1396 scratch.pop_for_return();
1397 assert_eq!(*p.get(), 99);
1398 }
1400
1401 #[test]
1402 fn ptr_eq() {
1403 let _g = StaticCtxGuard::new();
1404 let p = GcPtr::new(1i64);
1405 let q = p.clone();
1406 assert!(GcPtr::ptr_eq(&p, &q));
1407 }
1408}