#![doc(html_root_url = "https://docs.rs/rcgc/0.1.0")]
use log::trace;
use std::cell::Cell;
use std::fmt;
use std::mem;
use std::ops::Deref;
use std::rc::{Rc, Weak};
pub trait Trace<T: Trace + ?Sized = Self> {
fn trace(&self, tracer: &mut Tracer<T>);
}
impl<T: Copy> Trace for T {
fn trace(&self, _tracer: &mut Tracer<Self>) {}
}
pub struct Tracer<T: Trace + ?Sized> {
traced: bool,
worklist: Vec<Handle<T>>,
}
impl<T: Trace> Tracer<T> {
pub fn trace_handle(&mut self, handle: &Handle<T>) {
let traced = handle.with(|gc| gc.traced() == self.traced);
if !traced {
self.worklist.push(handle.clone());
}
}
fn mark_all(&mut self) {
let mut worklist = mem::replace(&mut self.worklist, Vec::new());
while !worklist.is_empty() {
trace!(
"mark_all iter: processing {} items in worklist",
worklist.len()
);
for handle in worklist.drain(..) {
trace!("mark_all: marking/tracing {:p}", handle);
handle.with(|gc| {
if gc.traced() != self.traced {
gc.metadata.traced.set(self.traced);
gc.object.trace(self);
}
});
}
mem::swap(&mut self.worklist, &mut worklist);
}
}
}
struct Metadata {
traced: Cell<bool>,
}
struct GcData<T: Trace + ?Sized> {
metadata: Metadata,
object: T,
}
impl<T: Trace + ?Sized> GcData<T> {
fn traced(&self) -> bool {
self.metadata.traced.get()
}
}
pub struct Handle<T: Trace + ?Sized> {
inner: Weak<GcData<T>>,
}
impl<T: Trace> Handle<T> {
fn from_weak(weak: Weak<GcData<T>>) -> Self {
Self { inner: weak }
}
pub(crate) fn with<F, R>(&self, f: F) -> R
where
F: FnOnce(Rc<GcData<T>>) -> R,
{
f(self.inner.upgrade().expect("use after free"))
}
}
impl<T: Trace> Trace<T> for Handle<T> {
fn trace(&self, tracer: &mut Tracer<T>) {
tracer.trace_handle(self);
}
}
impl<T: Trace> Clone for Handle<T> {
fn clone(&self) -> Self {
Handle {
inner: self.inner.clone(),
}
}
}
impl<T: Trace> fmt::Pointer for Handle<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.with(|rc| write!(f, "{:p}", rc))
}
}
pub struct Rooted<T: Trace> {
inner: Rc<GcData<T>>,
}
impl<T: Trace> Rooted<T> {
pub fn new_handle(&self) -> Handle<T> {
Handle::from_weak(Rc::downgrade(&self.inner))
}
}
impl<T: Trace> Deref for Rooted<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner.object
}
}
pub struct Gc<T: Trace> {
objs: Vec<Rc<GcData<T>>>,
traced_color: bool,
next_gc: usize,
}
impl<T: Trace> Gc<T> {
pub fn new() -> Self {
Self {
objs: Vec::new(),
traced_color: true,
next_gc: 32,
}
}
pub fn allocate(&mut self, t: T) -> Rooted<T> {
let root = self.allocate_nocollect(t);
if self.estimate_heap_size() >= self.next_gc {
trace!("heap at {}, collecting", self.estimate_heap_size());
self.do_collect();
self.next_gc = self.estimate_heap_size() * 2;
trace!(
"heap remaining: {}. next gc at heap {}",
self.estimate_heap_size(),
self.next_gc
);
}
root
}
pub fn allocate_nocollect(&mut self, t: T) -> Rooted<T> {
let rc = Rc::new(GcData {
metadata: Metadata {
traced: Cell::new(!self.traced_color), },
object: t,
});
self.objs.push(rc.clone());
Rooted { inner: rc }
}
pub fn force_full_collect(&mut self) {
let size_before_collect = self.estimate_heap_size();
for i in 1.. {
let before = self.objs.len();
self.objs
.retain(|obj| Rc::strong_count(obj) > 1 || Rc::weak_count(obj) > 0);
trace!(
"collect weak fp: iteration {}, {}->{}",
i,
before,
self.objs.len()
);
if self.objs.len() == before {
trace!(
"collect weak fp: reached fixpoint after {} iteration(s) (heap {}->{})",
i,
size_before_collect,
self.estimate_heap_size()
);
break;
}
}
let worklist = self
.roots()
.map(Rc::downgrade)
.map(Handle::from_weak)
.collect::<Vec<_>>();
trace!(
"mark-sweep: initial worklist (root set) len: {}",
worklist.len()
);
trace!("mark-sweep: tracing, traced color={}", self.traced_color);
let mut tracer = Tracer {
traced: self.traced_color,
worklist,
};
tracer.mark_all();
let traced_color = self.traced_color;
self.objs.retain(|obj| obj.traced() == traced_color);
self.traced_color = !self.traced_color;
}
fn do_collect(&mut self) {
trace!("do_collect: we're not very smart, forcing full GC");
self.force_full_collect();
}
fn estimate_heap_size(&self) -> usize {
self.objs.len()
}
fn roots(&self) -> impl Iterator<Item = &Rc<GcData<T>>> {
self.objs
.iter()
.filter(|rc| Rc::strong_count(rc) > 1)
.inspect(|rc| {
trace!("root {:p}", rc);
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::{Cell, RefCell};
thread_local! {
static DROPPED_OBJECTS: Cell<usize> = Cell::new(0);
}
fn reset_dropped_objects() {
DROPPED_OBJECTS.with(|o| o.set(0))
}
fn dropped_objects() -> usize {
DROPPED_OBJECTS.with(|o| o.get())
}
struct Object {
refs: RefCell<Vec<Handle<Object>>>,
}
impl Trace for Object {
fn trace(&self, tracer: &mut Tracer<Self>) {
for handle in self.refs.borrow().iter() {
tracer.trace_handle(handle);
}
}
}
impl Object {
fn new() -> Self {
Self {
refs: RefCell::new(Vec::new()),
}
}
fn add_ref(&self, handle: Handle<Object>) {
self.refs.borrow_mut().push(handle);
}
}
impl Drop for Object {
fn drop(&mut self) {
DROPPED_OBJECTS.with(|o| o.set(o.get() + 1));
}
}
fn test<F>(f: F)
where
F: FnOnce(Gc<Object>),
{
reset_dropped_objects();
assert_eq!(dropped_objects(), 0);
env_logger::try_init().ok();
f(Gc::new());
}
fn mkcycle(gc: &mut Gc<Object>) -> Rooted<Object> {
let rooted = gc.allocate_nocollect(Object::new());
assert_eq!(dropped_objects(), 0);
let rooted2 = gc.allocate_nocollect(Object::new());
assert_eq!(dropped_objects(), 0);
rooted.add_ref(rooted2.new_handle());
rooted2.add_ref(rooted.new_handle());
rooted
}
#[test]
fn simple() {
test(|mut gc| {
{
let rooted = gc.allocate_nocollect(Object::new());
assert_eq!(dropped_objects(), 0);
let rooted2 = gc.allocate_nocollect(Object::new());
assert_eq!(dropped_objects(), 0);
rooted.add_ref(rooted2.new_handle());
gc.force_full_collect();
assert_eq!(dropped_objects(), 0);
}
gc.force_full_collect();
assert_eq!(dropped_objects(), 2);
});
}
#[test]
fn traces_references() {
test(|mut gc| {
let root = gc.allocate_nocollect(Object::new());
let nonroot = gc.allocate_nocollect(Object::new());
root.add_ref(nonroot.new_handle());
drop(nonroot);
assert_eq!(dropped_objects(), 0);
gc.force_full_collect();
assert_eq!(dropped_objects(), 0);
})
}
#[test]
fn destroys_objects_on_drop() {
test(|mut gc| {
{
let rooted = gc.allocate_nocollect(Object::new());
assert_eq!(dropped_objects(), 0);
let rooted2 = gc.allocate_nocollect(Object::new());
assert_eq!(dropped_objects(), 0);
rooted.add_ref(rooted2.new_handle());
assert_eq!(dropped_objects(), 0);
}
drop(gc);
assert_eq!(dropped_objects(), 2);
});
}
#[test]
fn destroys_cycles_on_drop() {
test(|mut gc| {
mkcycle(&mut gc);
drop(gc);
assert_eq!(dropped_objects(), 2);
});
}
#[test]
fn destroys_cycles() {
test(|mut gc| {
mkcycle(&mut gc);
gc.force_full_collect();
assert_eq!(dropped_objects(), 2);
drop(gc);
assert_eq!(dropped_objects(), 2);
});
}
#[test]
fn collects_when_heap_grows() {
test(|mut gc| {
let mut roots = Vec::new();
for i in 0..100 {
let root = gc.allocate(Object::new());
if i % 4 == 0 {
roots.push(root);
}
}
assert!(
gc.estimate_heap_size() < 50,
"GC did not collect when filling heap (heap size = {})",
gc.estimate_heap_size()
);
gc.force_full_collect();
});
}
}