use std::{
alloc::{dealloc, Layout},
cell::{Cell, LazyCell, RefCell},
collections::{hash_map::Entry, HashMap},
hash::Hash,
mem::{replace, swap, take, transmute},
ptr::{drop_in_place, NonNull},
};
#[cfg(not(loom))]
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
#[cfg(loom)]
use loom::{
lazy_static,
sync::atomic::{AtomicPtr, AtomicUsize, Ordering},
thread_local,
};
#[cfg(not(loom))]
use parking_lot::{Mutex, RwLock};
#[cfg(loom)]
use crate::sync::loom_ext::{Mutex, RwLock};
use crate::{ptr::Erased, Trace, Visitor};
use super::{default_collect_condition, CollectCondition, CollectInfo, Gc, GcBox, CURRENT_TAG};
struct GarbageTruck {
contents: Mutex<LazyCell<HashMap<AllocationId, TrashCan>>>,
collecting_lock: RwLock<()>,
n_gcs_dropped: AtomicUsize,
n_gcs_existing: AtomicUsize,
collect_condition: AtomicPtr<()>,
}
pub(super) struct Dumpster {
pub contents: RefCell<HashMap<AllocationId, TrashCan>>,
n_drops: Cell<usize>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub(super) struct AllocationId(NonNull<GcBox<()>>);
#[derive(Debug)]
pub(super) struct TrashCan {
ptr: Erased,
dfs_fn: unsafe fn(Erased, &mut HashMap<AllocationId, AllocationInfo>),
}
#[derive(Debug)]
struct AllocationInfo {
ptr: Erased,
weak_drop_fn: unsafe fn(Erased),
reachability: Reachability,
}
#[derive(Debug)]
enum Reachability {
Unknown {
children: Vec<AllocationId>,
n_unaccounted: usize,
destroy_fn: unsafe fn(Erased, &HashMap<AllocationId, AllocationInfo>),
},
Reachable,
}
#[cfg(not(loom))]
static GARBAGE_TRUCK: GarbageTruck = GarbageTruck::new();
#[cfg(loom)]
lazy_static! {
static ref GARBAGE_TRUCK: GarbageTruck = GarbageTruck::new();
}
thread_local! {
pub(super) static DUMPSTER: Dumpster = Dumpster {
contents: RefCell::new(HashMap::new()),
n_drops: Cell::new(0),
};
}
#[cfg(not(loom))]
thread_local! {
static CLEANING: Cell<bool> = const { Cell::new(false) };
}
#[cfg(loom)]
thread_local! {
static CLEANING: Cell<bool> = Cell::new(false);
}
pub fn collect_all_await() {
DUMPSTER.with(|d| d.deliver_to(&GARBAGE_TRUCK));
GARBAGE_TRUCK.collect_all();
drop(GARBAGE_TRUCK.collecting_lock.read());
}
pub fn notify_dropped_gc() {
GARBAGE_TRUCK.n_gcs_existing.fetch_sub(1, Ordering::Relaxed);
GARBAGE_TRUCK.n_gcs_dropped.fetch_add(1, Ordering::Relaxed);
DUMPSTER.with(|dumpster| {
dumpster.n_drops.set(dumpster.n_drops.get() + 1);
if dumpster.is_full() {
dumpster.deliver_to(&GARBAGE_TRUCK);
}
});
let collect_cond = unsafe {
transmute::<*mut (), CollectCondition>(
GARBAGE_TRUCK.collect_condition.load(Ordering::Relaxed),
)
};
if collect_cond(&CollectInfo { _private: () }) {
GARBAGE_TRUCK.collect_all();
}
}
pub fn notify_created_gc() {
GARBAGE_TRUCK.n_gcs_existing.fetch_add(1, Ordering::Relaxed);
}
pub(super) unsafe fn mark_dirty<T>(allocation: NonNull<GcBox<T>>)
where
T: Trace + Send + Sync + ?Sized,
{
DUMPSTER.with(|dumpster| {
if dumpster
.contents
.borrow_mut()
.insert(
AllocationId::from(allocation),
TrashCan {
ptr: Erased::new(allocation),
dfs_fn: dfs::<T>,
},
)
.is_none()
{
unsafe { allocation.as_ref() }
.weak
.fetch_add(1, Ordering::Acquire);
}
});
}
pub(super) fn mark_clean<T>(allocation: &GcBox<T>)
where
T: Trace + Send + Sync + ?Sized,
{
DUMPSTER.with(|dumpster| {
if dumpster
.contents
.borrow_mut()
.remove(&AllocationId::from(allocation))
.is_some()
{
allocation.weak.fetch_sub(1, Ordering::Release);
}
});
}
#[cfg(test)]
pub(super) fn deliver_dumpster() {
DUMPSTER.with(|d| d.deliver_to(&GARBAGE_TRUCK));
}
pub fn set_collect_condition(f: CollectCondition) {
GARBAGE_TRUCK
.collect_condition
.store(f as *mut (), Ordering::Relaxed);
}
pub fn n_gcs_dropped() -> usize {
GARBAGE_TRUCK.n_gcs_dropped.load(Ordering::Relaxed)
}
pub fn n_gcs_existing() -> usize {
GARBAGE_TRUCK.n_gcs_existing.load(Ordering::Relaxed)
}
impl Dumpster {
fn deliver_to(&self, garbage_truck: &GarbageTruck) {
self.n_drops.set(0);
let mut guard = garbage_truck.contents.lock();
self.deliver_to_contents(&mut guard);
}
fn deliver_to_contents(&self, contents: &mut HashMap<AllocationId, TrashCan>) {
for (id, can) in self.contents.borrow_mut().drain() {
if contents.insert(id, can).is_some() {
unsafe {
id.0.as_ref()
}
.weak
.fetch_sub(1, Ordering::Release);
}
}
}
fn is_full(&self) -> bool {
self.contents.borrow().len() > 100_000 || self.n_drops.get() > 100_000
}
}
impl GarbageTruck {
#[cfg(not(loom))]
const fn new() -> Self {
Self {
contents: Mutex::new(LazyCell::new(HashMap::new)),
collecting_lock: RwLock::new(()),
n_gcs_dropped: AtomicUsize::new(0),
n_gcs_existing: AtomicUsize::new(0),
collect_condition: AtomicPtr::new(default_collect_condition as *mut ()),
}
}
#[cfg(loom)]
fn new() -> Self {
Self {
contents: Mutex::new(LazyCell::new(HashMap::new)),
collecting_lock: RwLock::new(()),
n_gcs_dropped: AtomicUsize::new(0),
n_gcs_existing: AtomicUsize::new(0),
collect_condition: AtomicPtr::new(default_collect_condition as *mut ()),
}
}
fn collect_all(&self) {
let collecting_guard = self.collecting_lock.write();
self.n_gcs_dropped.store(0, Ordering::Relaxed);
let to_collect = take(&mut **self.contents.lock());
let mut ref_graph = HashMap::with_capacity(to_collect.len());
CURRENT_TAG.fetch_add(1, Ordering::Release);
for (_, TrashCan { ptr, dfs_fn }) in to_collect {
unsafe {
dfs_fn(ptr, &mut ref_graph);
}
}
let root_ids = ref_graph
.iter()
.filter_map(|(&k, v)| match v.reachability {
Reachability::Reachable => Some(k),
Reachability::Unknown { n_unaccounted, .. } => (n_unaccounted > 0
|| unsafe {
k.0.as_ref().weak.load(Ordering::Acquire) > 1
})
.then_some(k),
})
.collect::<Vec<_>>();
for root_id in root_ids {
mark(root_id, &mut ref_graph);
}
CLEANING.with(|c| c.set(true));
let mut weak_destroys = Vec::new();
for (id, node) in &ref_graph {
let header_ref = unsafe { id.0.as_ref() };
match node.reachability {
Reachability::Unknown { destroy_fn, .. } => unsafe {
destroy_fn(node.ptr, &ref_graph);
},
Reachability::Reachable => {
if header_ref.weak.fetch_sub(1, Ordering::Release) == 1
&& header_ref.strong.load(Ordering::Acquire) == 0
{
weak_destroys.push((node.weak_drop_fn, node.ptr));
}
}
}
}
CLEANING.with(|c| c.set(false));
for (drop_fn, ptr) in weak_destroys {
unsafe {
drop_fn(ptr);
};
}
drop(collecting_guard);
}
}
unsafe fn dfs<T: Trace + Send + Sync + ?Sized>(
ptr: Erased,
ref_graph: &mut HashMap<AllocationId, AllocationInfo>,
) {
let box_ref = unsafe {
ptr.specify::<GcBox<T>>().as_ref()
};
let starting_id = AllocationId::from(box_ref);
let Entry::Vacant(v) = ref_graph.entry(starting_id) else {
box_ref.weak.fetch_sub(1, Ordering::Release);
return;
};
let strong_count = box_ref.strong.load(Ordering::Acquire);
v.insert(AllocationInfo {
ptr,
weak_drop_fn: drop_weak_zero::<T>,
reachability: Reachability::Unknown {
children: Vec::new(),
n_unaccounted: strong_count,
destroy_fn: destroy_erased::<T>,
},
});
if box_ref
.value
.accept(&mut Dfs {
ref_graph,
current_id: starting_id,
})
.is_err()
|| box_ref.generation.load(Ordering::Acquire) >= CURRENT_TAG.load(Ordering::Relaxed)
{
mark(starting_id, ref_graph);
}
}
#[derive(Debug)]
pub(super) struct Dfs<'a> {
ref_graph: &'a mut HashMap<AllocationId, AllocationInfo>,
current_id: AllocationId,
}
impl Visitor for Dfs<'_> {
fn visit_sync<T>(&mut self, gc: &Gc<T>)
where
T: Trace + Send + Sync + ?Sized,
{
if Gc::is_dead(gc) {
return;
}
let ptr = unsafe {
gc.ptr.get().unwrap()
};
let box_ref = unsafe {
ptr.as_ref()
};
let current_tag = CURRENT_TAG.load(Ordering::Relaxed);
if gc.tag.swap(current_tag, Ordering::Relaxed) >= current_tag
|| box_ref.generation.load(Ordering::Acquire) >= current_tag
{
mark(self.current_id, self.ref_graph);
return;
}
let mut new_id = AllocationId::from(box_ref);
let Reachability::Unknown {
ref mut children, ..
} = self
.ref_graph
.get_mut(&self.current_id)
.unwrap()
.reachability
else {
return;
};
children.push(new_id);
match self.ref_graph.entry(new_id) {
Entry::Occupied(mut o) => match o.get_mut().reachability {
Reachability::Unknown {
ref mut n_unaccounted,
..
} => {
*n_unaccounted -= 1;
}
Reachability::Reachable => (),
},
Entry::Vacant(v) => {
let strong_count = box_ref.strong.load(Ordering::Acquire);
box_ref.weak.fetch_add(1, Ordering::Acquire);
v.insert(AllocationInfo {
ptr: Erased::new(ptr),
weak_drop_fn: drop_weak_zero::<T>,
reachability: Reachability::Unknown {
children: Vec::new(),
n_unaccounted: strong_count - 1,
destroy_fn: destroy_erased::<T>,
},
});
swap(&mut new_id, &mut self.current_id);
if box_ref.value.accept(self).is_err()
|| box_ref.generation.load(Ordering::Acquire) >= current_tag
{
mark(self.current_id, self.ref_graph);
}
swap(&mut new_id, &mut self.current_id);
}
}
}
fn visit_unsync<T>(&mut self, _: &crate::unsync::Gc<T>)
where
T: Trace + ?Sized,
{
unreachable!("sync Gc cannot own an unsync Gc");
}
}
fn mark(root: AllocationId, graph: &mut HashMap<AllocationId, AllocationInfo>) {
let node = graph.get_mut(&root).unwrap();
if let Reachability::Unknown { children, .. } =
replace(&mut node.reachability, Reachability::Reachable)
{
for child in children {
mark(child, graph);
}
}
}
pub(super) struct PrepareForDestruction<'a> {
graph: &'a HashMap<AllocationId, AllocationInfo>,
}
impl Visitor for PrepareForDestruction<'_> {
fn visit_sync<T>(&mut self, gc: &crate::sync::Gc<T>)
where
T: Trace + Send + Sync + ?Sized,
{
if Gc::is_dead(gc) {
return;
}
let id = AllocationId::from(unsafe {
gc.ptr.get().unwrap()
});
if matches!(self.graph[&id].reachability, Reachability::Reachable) {
unsafe {
id.0.as_ref().strong.fetch_sub(1, Ordering::Release);
}
}
unsafe {
gc.kill();
}
}
fn visit_unsync<T>(&mut self, _: &crate::unsync::Gc<T>)
where
T: Trace + ?Sized,
{
unreachable!("no unsync members of sync Gc possible!");
}
}
unsafe fn destroy_erased<T: Trace + Send + Sync + ?Sized>(
ptr: Erased,
graph: &HashMap<AllocationId, AllocationInfo>,
) {
let specified = ptr.specify::<GcBox<T>>().as_mut();
specified
.value
.accept(&mut PrepareForDestruction { graph })
.expect("allocation assumed to be unreachable but somehow was accessed");
let layout = Layout::for_value(specified);
drop_in_place(specified);
dealloc(std::ptr::from_mut::<GcBox<T>>(specified).cast(), layout);
}
unsafe fn drop_weak_zero<T: Trace + Send + Sync + ?Sized>(ptr: Erased) {
let mut specified = ptr.specify::<GcBox<T>>();
assert_eq!(specified.as_ref().weak.load(Ordering::Relaxed), 0);
assert_eq!(specified.as_ref().strong.load(Ordering::Relaxed), 0);
let layout = Layout::for_value(specified.as_ref());
drop_in_place(specified.as_mut());
dealloc(specified.as_ptr().cast(), layout);
}
unsafe impl Send for AllocationId {}
unsafe impl Sync for AllocationId {}
impl<T> From<&GcBox<T>> for AllocationId
where
T: Trace + Send + Sync + ?Sized,
{
fn from(value: &GcBox<T>) -> Self {
AllocationId(NonNull::from(value).cast())
}
}
impl<T> From<NonNull<GcBox<T>>> for AllocationId
where
T: Trace + Send + Sync + ?Sized,
{
fn from(value: NonNull<GcBox<T>>) -> Self {
AllocationId(value.cast())
}
}
#[cfg(not(loom))] impl Drop for Dumpster {
fn drop(&mut self) {
self.deliver_to(&GARBAGE_TRUCK);
}
}