use std::{
alloc::{dealloc, Layout},
cell::{Cell, RefCell},
collections::{hash_map::Entry, HashMap, HashSet},
mem::take,
num::NonZeroUsize,
ptr::{drop_in_place, NonNull},
};
use crate::{
ptr::Erased,
unsync::{default_collect_condition, CollectInfo, Gc},
Trace, Visitor,
};
use super::{CollectCondition, GcBox};
thread_local! {
static COLLECTING: Cell<bool> = const { Cell::new(false) };
pub(super) static DUMPSTER: Dumpster = Dumpster {
to_collect: RefCell::new(HashMap::new()),
n_ref_drops: Cell::new(0),
n_refs_living: Cell::new(0),
collect_condition: Cell::new(default_collect_condition),
};
}
pub(super) struct Dumpster {
to_collect: RefCell<HashMap<AllocationId, Cleanup>>,
pub n_ref_drops: Cell<usize>,
pub n_refs_living: Cell<usize>,
pub collect_condition: Cell<CollectCondition>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct AllocationId(pub NonNull<Cell<NonZeroUsize>>);
impl<T> From<NonNull<GcBox<T>>> for AllocationId
where
T: Trace + ?Sized,
{
fn from(value: NonNull<GcBox<T>>) -> Self {
AllocationId(value.cast())
}
}
#[derive(Debug)]
struct Cleanup {
dfs_fn: unsafe fn(Erased, &mut Dfs),
mark_fn: unsafe fn(Erased, &mut Mark),
drop_fn: unsafe fn(Erased, &mut DropAlloc<'_>),
ptr: Erased,
}
macro_rules! apply_visitor {
() => {
|ptr, visitor| unsafe {
_ = ptr.specify::<GcBox<T>>().as_ref().value.accept(visitor);
}
};
}
impl Cleanup {
fn new<T: Trace + ?Sized>(box_ptr: NonNull<GcBox<T>>) -> Cleanup {
Cleanup {
dfs_fn: apply_visitor!(),
mark_fn: apply_visitor!(),
drop_fn: drop_assist::<T>,
ptr: Erased::new(box_ptr),
}
}
}
impl Dumpster {
pub fn collect_all(&self) {
if COLLECTING.get() {
return; }
self.n_ref_drops.set(0);
unsafe {
let mut dfs = Dfs {
visited: HashSet::with_capacity(self.to_collect.borrow().len()),
ref_graph: HashMap::with_capacity(self.to_collect.borrow().len()),
};
for (k, v) in &*self.to_collect.borrow() {
if dfs.visited.insert(*k) {
(v.dfs_fn)(v.ptr, &mut dfs);
}
}
let mut mark = Mark {
visited: HashSet::with_capacity(dfs.visited.len()),
};
for (id, reachability) in dfs
.ref_graph
.iter()
.filter(|(_, reachability)| reachability.n_unaccounted != 0)
{
mark.visited.insert(*id);
(reachability.mark_fn)(reachability.ptr, &mut mark);
}
for (id, cleanup) in self
.to_collect
.borrow()
.iter()
.filter(|(id, _)| !dfs.ref_graph.contains_key(id))
{
mark.visited.insert(*id);
(cleanup.mark_fn)(cleanup.ptr, &mut mark);
}
dfs.visited.clear();
let mut decrementer = DropAlloc {
visited: dfs.visited,
reachable: &mark.visited,
};
COLLECTING.set(true);
let mut collectees = take(&mut *self.to_collect.borrow_mut());
for cleanup in collectees
.drain()
.filter_map(|(id, cleanup)| (!mark.visited.contains(&id)).then_some(cleanup))
{
(cleanup.drop_fn)(cleanup.ptr, &mut decrementer);
}
COLLECTING.set(false);
assert!(collectees.is_empty());
let mut new_to_collect = self.to_collect.borrow_mut();
if new_to_collect.is_empty() {
*new_to_collect = collectees;
}
}
}
pub fn mark_dirty<T: Trace + ?Sized>(&self, box_ptr: NonNull<GcBox<T>>) {
self.to_collect
.borrow_mut()
.entry(AllocationId::from(box_ptr))
.or_insert_with(|| Cleanup::new(box_ptr));
}
pub fn mark_cleaned<T: Trace + ?Sized>(&self, box_ptr: NonNull<GcBox<T>>) {
self.to_collect
.borrow_mut()
.remove(&AllocationId::from(box_ptr));
}
pub fn notify_dropped_gc(&self) {
self.n_ref_drops.set(self.n_ref_drops.get() + 1);
let old_refs_living = self.n_refs_living.get();
assert_ne!(
old_refs_living, 0,
"underflow on unsync::Gc number of living Gcs"
);
self.n_refs_living.set(old_refs_living - 1);
if (self.collect_condition.get())(&CollectInfo { _private: () }) {
self.collect_all();
}
}
pub fn notify_created_gc(&self) {
self.n_refs_living.set(self.n_refs_living.get() + 1);
}
}
impl Drop for Dumpster {
fn drop(&mut self) {
self.collect_all();
}
}
pub(super) struct Dfs {
visited: HashSet<AllocationId>,
ref_graph: HashMap<AllocationId, Reachability>,
}
#[derive(Debug)]
struct Reachability {
n_unaccounted: usize,
ptr: Erased,
mark_fn: unsafe fn(Erased, &mut Mark),
}
impl Visitor for Dfs {
fn visit_sync<T>(&mut self, _: &crate::sync::Gc<T>)
where
T: Trace + Send + Sync + ?Sized,
{
}
fn visit_unsync<T>(&mut self, gc: &Gc<T>)
where
T: Trace + ?Sized,
{
if Gc::is_dead(gc) {
return;
}
let ptr = gc.ptr.get().unwrap();
let next_id = AllocationId::from(ptr);
match self.ref_graph.entry(next_id) {
Entry::Occupied(ref mut o) => {
o.get_mut().n_unaccounted -= 1;
}
Entry::Vacant(v) => {
v.insert(Reachability {
n_unaccounted: unsafe { next_id.0.as_ref().get().get() - 1 },
ptr: Erased::new(ptr),
mark_fn: apply_visitor!(),
});
}
}
if self.visited.insert(next_id) {
let _ = unsafe { ptr.as_ref() }.value.accept(self);
}
}
}
pub(super) struct Mark {
visited: HashSet<AllocationId>,
}
impl Visitor for Mark {
fn visit_sync<T>(&mut self, _: &crate::sync::Gc<T>)
where
T: Trace + Send + Sync + ?Sized,
{
}
fn visit_unsync<T>(&mut self, gc: &Gc<T>)
where
T: Trace + ?Sized,
{
if Gc::is_dead(gc) {
return;
}
let ptr = gc.ptr.get().unwrap();
if self.visited.insert(AllocationId::from(ptr)) {
let _ = unsafe { ptr.as_ref().value.accept(self) };
}
}
}
pub(super) struct DropAlloc<'a> {
visited: HashSet<AllocationId>,
reachable: &'a HashSet<AllocationId>,
}
impl Visitor for DropAlloc<'_> {
fn visit_sync<T>(&mut self, _: &crate::sync::Gc<T>)
where
T: Trace + Send + Sync + ?Sized,
{
}
fn visit_unsync<T>(&mut self, gc: &Gc<T>)
where
T: Trace + ?Sized,
{
if Gc::is_dead(gc) {
return;
}
let ptr = gc.ptr.get().unwrap();
let id = AllocationId::from(ptr);
gc.kill();
if self.reachable.contains(&id) {
unsafe {
let cell_ref = &ptr.as_ref().ref_count;
cell_ref.set(NonZeroUsize::new(cell_ref.get().get() - 1).expect(
"reachable allocation cannot be rendered unreachable by deleting lost alloc",
));
}
return;
}
if self.visited.insert(id) {
unsafe {
ptr.as_ref().value.accept(self).unwrap();
let layout = Layout::for_value(ptr.as_ref());
drop_in_place(ptr.as_ptr());
dealloc(ptr.as_ptr().cast(), layout);
}
}
}
}
unsafe fn drop_assist<T: Trace + ?Sized>(ptr: Erased, visitor: &mut DropAlloc<'_>) {
let mut spec = ptr.specify::<GcBox<T>>();
if visitor.visited.insert(AllocationId::from(spec)) {
spec.as_ref().value.accept(visitor).unwrap();
let mut_spec = spec.as_mut();
let layout = Layout::for_value(mut_spec);
drop_in_place(mut_spec);
dealloc(std::ptr::from_mut::<GcBox<T>>(mut_spec).cast(), layout);
}
}