#![allow(clippy::missing_safety_doc)]
pub mod cancellation;
pub mod config;
pub mod region;
pub use cancellation::{
CancellableGuard, MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested,
park_thread, register_mutator, registered_threads, request_gc, safepoint, take_gc_request,
unpark_thread, wait_for_threads_to_park,
};
use std::cell::Cell;
use std::ptr::NonNull;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
pub use config::{GC_CANCELLATION as CONFIG_CANCELLATION, GcConfig, GcParked};
pub struct GcPtr<T: Trace + 'static>(NonNull<GcBox<T>>);
pub trait Trace: Send + Sync {
fn trace(&self, visitor: &mut MarkVisitor);
}
impl Trace for String {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for i64 {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for f64 {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for bool {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for num_bigint::BigInt {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for bigdecimal::BigDecimal {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for num_rational::Ratio<num_bigint::BigInt> {
fn trace(&self, _: &mut MarkVisitor) {}
}
impl Trace for regex::Regex {
fn trace(&self, _: &mut MarkVisitor) {}
}
pub trait GcVisitor {
fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>);
}
#[repr(C)]
pub(crate) struct GcBoxHeader {
marked: Cell<bool>,
next: Cell<*mut GcBoxHeader>,
trace_fn: unsafe fn(*const GcBoxHeader, &mut MarkVisitor),
drop_fn: unsafe fn(*mut GcBoxHeader),
}
impl GcBoxHeader {
pub(crate) fn new<T: Trace + 'static>() -> Self {
Self {
marked: Cell::new(false),
next: Cell::new(std::ptr::null_mut()),
trace_fn: trace_gc_box::<T>,
drop_fn: drop_gc_box::<T>,
}
}
}
unsafe impl Send for GcBoxHeader {}
unsafe impl Sync for GcBoxHeader {}
#[repr(C)]
pub(crate) struct GcBox<T: Trace + 'static> {
pub(crate) header: GcBoxHeader,
pub(crate) value: T,
}
pub(crate) unsafe fn trace_gc_box<T: Trace + 'static>(
header: *const GcBoxHeader,
visitor: &mut MarkVisitor,
) {
unsafe {
let gc_box = header as *const GcBox<T>;
(*gc_box).value.trace(visitor);
}
}
unsafe fn drop_gc_box<T: Trace + 'static>(header: *mut GcBoxHeader) {
unsafe {
let gc_box = header as *mut GcBox<T>;
drop(Box::from_raw(gc_box));
}
}
struct GcHeapInner {
head: *mut GcBoxHeader,
count: usize,
total_allocated: usize,
total_freed: usize,
}
unsafe impl Send for GcHeapInner {}
impl GcHeapInner {
const fn new() -> Self {
Self {
head: std::ptr::null_mut(),
count: 0,
total_allocated: 0,
total_freed: 0,
}
}
}
const ESTIMATED_OBJECT_SIZE: usize = 48;
type RootTracer = Box<dyn Fn(&mut MarkVisitor) + Send + Sync>;
pub struct GcHeap {
inner: Mutex<GcHeapInner>,
config: Mutex<Option<Arc<GcConfig>>>,
memory_in_use: AtomicUsize,
total_allocated_bytes: AtomicUsize,
root_tracers: Mutex<Vec<RootTracer>>,
}
unsafe impl Sync for GcHeap {}
impl Default for GcHeap {
fn default() -> Self {
Self::new()
}
}
impl GcHeap {
pub const fn new() -> Self {
Self {
inner: Mutex::new(GcHeapInner::new()),
config: Mutex::new(None),
memory_in_use: AtomicUsize::new(0),
total_allocated_bytes: AtomicUsize::new(0),
root_tracers: Mutex::new(Vec::new()),
}
}
pub fn set_config(&self, config: Arc<GcConfig>) {
*self.config.lock().unwrap() = Some(config);
}
pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + Send + Sync + 'static) {
self.root_tracers.lock().unwrap().push(Box::new(tracer));
}
pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
let tracers = self.root_tracers.lock().unwrap();
for tracer in tracers.iter() {
tracer(visitor);
}
}
pub fn memory_in_use(&self) -> usize {
self.memory_in_use.load(Ordering::Relaxed)
}
#[cfg(test)]
pub fn set_memory_in_use(&self, bytes: usize) {
self.memory_in_use.store(bytes, Ordering::Relaxed);
}
pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
cancellation::safepoint();
let estimated_size = ESTIMATED_OBJECT_SIZE;
let gc_box = Box::new(GcBox {
header: GcBoxHeader::new::<T>(),
value,
});
let raw: *mut GcBox<T> = Box::into_raw(gc_box);
{
let mut inner = self.inner.lock().unwrap();
unsafe {
(*raw).header.next.set(inner.head);
inner.head = raw as *mut GcBoxHeader;
}
inner.count += 1;
inner.total_allocated += 1;
}
self.total_allocated_bytes
.fetch_add(estimated_size, Ordering::Relaxed);
let current_usage = self
.memory_in_use
.fetch_add(estimated_size, Ordering::Relaxed)
+ estimated_size;
if let Some(config) = self.config.lock().unwrap().as_ref()
&& config.soft_limit_exceeded(current_usage)
{
cancellation::request_gc();
}
GcPtr(unsafe { NonNull::new_unchecked(raw) })
}
pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
let pre_count = self.inner.lock().unwrap().count;
let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
cljrs_logging::feat_debug!(
"gc",
"starting collection: {} objects, ~{} bytes in use",
pre_count,
pre_memory
);
let mark_start = std::time::Instant::now();
let mut visitor = MarkVisitor::new();
trace_roots(&mut visitor);
visitor.drain();
let mark_elapsed = mark_start.elapsed();
let sweep_start = std::time::Instant::now();
let mut inner = self.inner.lock().unwrap();
let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
let mut current = inner.head;
while !current.is_null() {
let header = unsafe { &*current };
let next = header.next.get();
if header.marked.get() {
header.marked.set(false); live.push(current);
} else {
dead.push(current);
}
current = next;
}
let freed_count = dead.len();
for ptr in dead {
let header = unsafe { &*ptr };
unsafe { (header.drop_fn)(ptr) };
inner.count -= 1;
inner.total_freed += 1;
}
inner.head = std::ptr::null_mut();
for ptr in live {
let header = unsafe { &*ptr };
header.next.set(inner.head);
inner.head = ptr;
}
let freed_bytes = freed_count * ESTIMATED_OBJECT_SIZE;
self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
let sweep_elapsed = sweep_start.elapsed();
let post_memory = self.memory_in_use.load(Ordering::Relaxed);
cljrs_logging::feat_debug!(
"gc",
"collection complete: freed {} objects (~{} bytes), {} objects remaining (~{} bytes), mark={:.2?} sweep={:.2?}",
freed_count,
freed_bytes,
inner.count,
post_memory,
mark_elapsed,
sweep_elapsed
);
}
pub fn count(&self) -> usize {
self.inner.lock().unwrap().count
}
pub fn total_allocated(&self) -> usize {
self.inner.lock().unwrap().total_allocated
}
pub fn total_freed(&self) -> usize {
self.inner.lock().unwrap().total_freed
}
pub fn collect_auto(&self) -> bool {
cljrs_logging::feat_debug!("gc", "automatic collection requested");
let Some(_stw_guard) = cancellation::begin_stw() else {
cljrs_logging::feat_debug!(
"gc",
"automatic collection skipped: another thread is already collecting"
);
return false;
};
cljrs_logging::feat_debug!(
"gc",
"stop-the-world acquired, {} mutator thread(s) parked",
cancellation::registered_threads()
);
self.collect(|visitor| {
self.trace_registered_roots(visitor);
});
true
}
}
pub struct MarkVisitor {
grey: Vec<*mut GcBoxHeader>,
}
unsafe impl Send for MarkVisitor {}
unsafe impl Sync for MarkVisitor {}
impl MarkVisitor {
fn new() -> Self {
Self { grey: Vec::new() }
}
fn drain(&mut self) {
while let Some(header) = self.grey.pop() {
let h = unsafe { &*header };
unsafe { (h.trace_fn)(header as *const GcBoxHeader, self) };
}
}
}
impl GcVisitor for MarkVisitor {
fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
let header = unsafe { &(*ptr.0.as_ptr()).header };
if !header.marked.get() {
header.marked.set(true);
self.grey.push(ptr.0.as_ptr() as *mut GcBoxHeader);
}
}
}
pub static HEAP: GcHeap = GcHeap::new();
unsafe impl<T: Trace + 'static> Send for GcPtr<T> {}
unsafe impl<T: Trace + 'static> Sync for GcPtr<T> {}
impl<T: Trace + 'static> GcPtr<T> {
pub fn new(value: T) -> Self {
HEAP.alloc(value)
}
pub fn get(&self) -> &T {
unsafe { &(*self.0.as_ptr()).value }
}
pub fn get_mut(&mut self) -> &mut T {
unsafe { &mut (*self.0.as_ptr()).value }
}
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
a.0 == b.0
}
}
impl<T: Trace + 'static> Clone for GcPtr<T> {
fn clone(&self) -> Self {
GcPtr(self.0)
}
}
impl<T: Trace + 'static + std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
unsafe { (*self.0.as_ptr()).value.fmt(f) }
}
}
impl<T: Trace + 'static> Drop for GcPtr<T> {
fn drop(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct Tracked {
value: i32,
dropped: Arc<Mutex<bool>>,
}
impl Drop for Tracked {
fn drop(&mut self) {
*self.dropped.lock().unwrap() = true;
}
}
impl Trace for Tracked {
fn trace(&self, _: &mut MarkVisitor) {}
}
#[derive(Debug)]
struct Parent {
child: GcPtr<Tracked>,
}
impl Trace for Parent {
fn trace(&self, visitor: &mut MarkVisitor) {
visitor.visit(&self.child);
}
}
fn fresh_heap() -> GcHeap {
let heap = GcHeap::new();
let config = Arc::new(GcConfig::with_limits(10000, 50000));
heap.set_config(config);
heap
}
#[test]
fn alloc_and_get() {
let heap = fresh_heap();
let p = heap.alloc(42i64);
assert_eq!(*p.get(), 42);
assert_eq!(heap.count(), 1);
}
#[test]
fn clone_is_same_ptr() {
let heap = fresh_heap();
let p = heap.alloc(99i64);
let q = p.clone();
assert!(GcPtr::ptr_eq(&p, &q));
}
#[test]
fn collect_frees_unreachable() {
let heap = fresh_heap();
let dropped = Arc::new(Mutex::new(false));
let _p = heap.alloc(Tracked {
value: 1,
dropped: dropped.clone(),
});
assert_eq!(heap.count(), 1);
heap.collect(|_| {});
assert_eq!(heap.count(), 0);
assert!(*dropped.lock().unwrap(), "object should have been dropped");
}
#[test]
fn collect_keeps_reachable() {
let heap = fresh_heap();
let dropped = Arc::new(Mutex::new(false));
let p = heap.alloc(Tracked {
value: 2,
dropped: dropped.clone(),
});
heap.collect(|vis| vis.visit(&p));
assert_eq!(heap.count(), 1);
assert!(!*dropped.lock().unwrap(), "reachable object must survive");
}
#[test]
fn collect_traces_children() {
let heap = fresh_heap();
let child_dropped = Arc::new(Mutex::new(false));
let child = heap.alloc(Tracked {
value: 10,
dropped: child_dropped.clone(),
});
let parent = heap.alloc(Parent {
child: child.clone(),
});
assert_eq!(heap.count(), 2);
heap.collect(|vis| vis.visit(&parent));
assert_eq!(heap.count(), 2);
assert!(!*child_dropped.lock().unwrap());
}
#[test]
fn collect_frees_two_unreachable() {
let heap = fresh_heap();
let d1 = Arc::new(Mutex::new(false));
let d2 = Arc::new(Mutex::new(false));
let _a = heap.alloc(Tracked {
value: 1,
dropped: d1.clone(),
});
let _b = heap.alloc(Tracked {
value: 2,
dropped: d2.clone(),
});
heap.collect(|_| {});
assert!(*d1.lock().unwrap());
assert!(*d2.lock().unwrap());
assert_eq!(heap.count(), 0);
}
#[test]
fn total_stats() {
let heap = fresh_heap();
let p = heap.alloc(1i64);
let _q = heap.alloc(2i64);
assert_eq!(heap.total_allocated(), 2);
heap.collect(|vis| vis.visit(&p));
assert_eq!(heap.count(), 1);
assert_eq!(heap.total_freed(), 1);
}
}
impl Trace for std::sync::Mutex<Vec<i32>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<i64>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<i16>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<i8>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<char>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<f64>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<f32>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}
impl Trace for std::sync::Mutex<Vec<bool>> {
fn trace(&self, _visitor: &mut MarkVisitor) {}
}