use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::marker::PhantomData;
use std::ptr::NonNull;
thread_local! {
static CURRENT_HEAP_STACK: RefCell<Vec<NonNull<Heap>>> = const { RefCell::new(Vec::new()) };
}
pub struct HeapGuard {
_private: (),
}
impl HeapGuard {
pub fn push(heap: &Heap) -> Self {
let ptr = NonNull::from(heap);
CURRENT_HEAP_STACK.with(|stack| stack.borrow_mut().push(ptr));
HeapGuard { _private: () }
}
}
impl Drop for HeapGuard {
fn drop(&mut self) {
CURRENT_HEAP_STACK.with(|stack| {
let popped = stack.borrow_mut().pop();
debug_assert!(popped.is_some(), "HeapGuard::drop with empty stack");
});
}
}
pub fn with_current_heap<R>(f: impl for<'a> FnOnce(Option<&'a Heap>) -> R) -> R {
CURRENT_HEAP_STACK.with(|stack| {
let ptr = stack.borrow().last().copied();
let heap = ptr.map(|ptr| unsafe { &*ptr.as_ptr() });
f(heap)
})
}
#[derive(Copy, Clone, Debug)]
pub struct HeapRef {
ptr: NonNull<Heap>,
}
impl HeapRef {
pub fn from_heap(heap: &Heap) -> Self {
HeapRef {
ptr: NonNull::from(heap),
}
}
pub fn contains_allocation(self, identity: usize, token: usize) -> bool {
unsafe { self.ptr.as_ref() }.contains_allocation(identity, token)
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Color {
White0,
White1,
Gray,
Black,
}
impl Color {
pub fn is_white(self) -> bool {
matches!(self, Color::White0 | Color::White1)
}
fn other_white(self) -> Self {
match self {
Color::White0 => Color::White1,
Color::White1 => Color::White0,
Color::Gray | Color::Black => self,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum GcAge {
New,
Survival,
Old0,
Old1,
Old,
Touched1,
Touched2,
}
impl GcAge {
pub fn is_old(self) -> bool {
!matches!(self, GcAge::New | GcAge::Survival)
}
fn next_after_minor(self) -> Self {
match self {
GcAge::New => GcAge::Survival,
GcAge::Survival | GcAge::Old0 => GcAge::Old1,
GcAge::Old1 | GcAge::Old | GcAge::Touched2 => GcAge::Old,
GcAge::Touched1 => GcAge::Touched2,
}
}
}
#[repr(C)]
pub struct GcHeader {
color: Cell<Color>,
age: Cell<GcAge>,
finalized: Cell<bool>,
collected: Cell<bool>,
next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
size: Cell<usize>,
type_name: &'static str,
}
impl GcHeader {
fn new_white(size: usize, color: Color, type_name: &'static str) -> Self {
Self {
color: Cell::new(color),
age: Cell::new(GcAge::New),
finalized: Cell::new(false),
collected: Cell::new(false),
next: Cell::new(None),
size: Cell::new(size),
type_name,
}
}
}
#[repr(C)]
pub struct GcBox<T: ?Sized> {
header: GcHeader,
value: T,
}
impl<T: ?Sized> GcBox<T> {
pub fn header(&self) -> &GcHeader {
&self.header
}
pub fn value(&self) -> &T {
&self.value
}
}
pub struct Gc<T: ?Sized> {
ptr: NonNull<GcBox<T>>,
_marker: PhantomData<T>,
}
impl<T: ?Sized> Copy for Gc<T> {}
impl<T: ?Sized> Clone for Gc<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized> PartialEq for Gc<T> {
fn eq(&self, other: &Self) -> bool {
std::ptr::addr_eq(self.ptr.as_ptr(), other.ptr.as_ptr())
}
}
impl<T: ?Sized> Eq for Gc<T> {}
impl<T: ?Sized> std::hash::Hash for Gc<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.ptr.as_ptr().hash(state)
}
}
impl<T: ?Sized> std::fmt::Debug for Gc<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Gc({:p})", self.ptr.as_ptr())
}
}
impl<T: Trace + 'static> Gc<T> {
pub fn new_uncollected(value: T) -> Self {
let size = std::mem::size_of::<T>();
let boxed = Box::new(GcBox {
header: GcHeader::new_white(size, Color::White0, std::any::type_name::<T>()),
value,
});
Gc {
ptr: NonNull::new(Box::into_raw(boxed)).expect("Box::into_raw is non-null"),
_marker: PhantomData,
}
}
}
impl<T: ?Sized> Gc<T> {
pub fn ptr_eq(a: Self, b: Self) -> bool {
std::ptr::addr_eq(a.ptr.as_ptr(), b.ptr.as_ptr())
}
pub fn identity(self) -> usize {
self.ptr.as_ptr() as *const () as usize
}
fn as_box(&self) -> &GcBox<T> {
unsafe { self.ptr.as_ref() }
}
fn header(&self) -> &GcHeader {
&self.as_box().header
}
pub fn color(self) -> Color {
self.header().color.get()
}
pub fn set_color(self, color: Color) {
self.header().color.set(color);
}
pub fn age(self) -> GcAge {
self.header().age.get()
}
pub fn set_age(self, age: GcAge) {
self.header().age.set(age);
}
pub fn account_buffer(&self, heap: &Heap, delta: isize) {
if delta == 0 {
return;
}
let header = self.header();
if !header.collected.get() {
return;
}
if delta >= 0 {
header.size.set(header.size.get().saturating_add(delta as usize));
} else {
header
.size
.set(header.size.get().saturating_sub((-delta) as usize));
}
heap.adjust_bytes(delta);
}
}
impl<T: ?Sized> std::ops::Deref for Gc<T> {
type Target = T;
fn deref(&self) -> &T {
&self.as_box().value
}
}
impl<T: ?Sized> AsRef<T> for Gc<T> {
fn as_ref(&self) -> &T {
&self.as_box().value
}
}
pub trait Trace {
fn trace(&self, m: &mut Marker);
}
impl<T: Trace> Trace for Vec<T> {
fn trace(&self, m: &mut Marker) {
for item in self.iter() {
item.trace(m);
}
}
}
impl<T: Trace> Trace for Option<T> {
fn trace(&self, m: &mut Marker) {
if let Some(v) = self {
v.trace(m);
}
}
}
impl<T: Trace + ?Sized> Trace for Box<T> {
fn trace(&self, m: &mut Marker) {
(**self).trace(m);
}
}
impl<T: Trace + ?Sized> Trace for std::rc::Rc<T> {
fn trace(&self, m: &mut Marker) {
(**self).trace(m);
}
}
impl<T: Trace> Trace for RefCell<T> {
fn trace(&self, m: &mut Marker) {
self.borrow().trace(m);
}
}
impl<T: Trace + 'static> Trace for Gc<T> {
fn trace(&self, m: &mut Marker) {
m.mark(*self);
}
}
macro_rules! trace_noop {
($($t:ty),*) => {
$(impl Trace for $t {
fn trace(&self, _m: &mut Marker) {}
})*
};
}
trace_noop!(
bool, u8, u16, u32, u64, u128, usize,
i8, i16, i32, i64, i128, isize,
f32, f64, char, String, str
);
impl<T> Trace for std::marker::PhantomData<T> {
fn trace(&self, _m: &mut Marker) {}
}
pub struct Marker {
gray_queue: Vec<NonNull<GcBox<dyn Trace>>>,
visited: std::collections::HashSet<usize>,
}
impl Marker {
fn new() -> Self {
Self {
gray_queue: Vec::with_capacity(256),
visited: std::collections::HashSet::new(),
}
}
pub fn mark<T: Trace + 'static>(&mut self, gc: Gc<T>) {
let id = gc.identity();
if self.visited.insert(id) {
let header = gc.header();
header.color.set(Color::Gray);
let ptr: NonNull<GcBox<dyn Trace>> = gc.ptr;
self.gray_queue.push(ptr);
}
}
pub fn try_visit(&mut self, addr: usize) -> bool {
self.visited.insert(addr)
}
pub fn is_visited(&self, id: usize) -> bool {
self.visited.contains(&id)
}
pub fn visited_count(&self) -> usize {
self.visited.len()
}
pub fn drain_gray_queue(&mut self) {
while let Some(gray_ptr) = self.gray_queue.pop() {
unsafe {
let bx = gray_ptr.as_ref();
bx.header.color.set(Color::Black);
bx.value.trace(self);
}
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum GcState {
Pause,
Propagate,
Atomic,
Sweep,
Finalize,
}
impl GcState {
pub fn is_pause(self) -> bool {
matches!(self, GcState::Pause)
}
pub fn is_propagate(self) -> bool {
matches!(self, GcState::Propagate)
}
pub fn is_sweep(self) -> bool {
matches!(self, GcState::Sweep)
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum StepOutcome {
Paused,
InProgress,
SkippedStopped,
}
#[derive(Copy, Clone, Debug)]
pub struct StepBudget {
pub remaining_work: isize,
pub max_credit: isize,
}
impl StepBudget {
pub fn from_work(work: isize) -> Self {
Self { remaining_work: work.max(1), max_credit: work.max(1) }
}
}
const GC_MIN_THRESHOLD: usize = 1024 * 1024;
pub struct Heap {
head: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
bytes: Cell<usize>,
current_white: Cell<Color>,
allocation_tokens: RefCell<HashMap<usize, usize>>,
next_allocation_token: Cell<usize>,
threshold: Cell<usize>,
pause_multiplier: Cell<usize>,
state: Cell<GcState>,
paused: Cell<bool>,
collections: Cell<usize>,
marker: RefCell<Option<Marker>>,
sweep_prev_next: Cell<Option<NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>>>,
}
impl Default for Heap {
fn default() -> Self {
Self::new()
}
}
impl Heap {
pub fn new() -> Self {
Self {
head: Cell::new(None),
bytes: Cell::new(0),
current_white: Cell::new(Color::White0),
allocation_tokens: RefCell::new(HashMap::new()),
next_allocation_token: Cell::new(1),
threshold: Cell::new(64 * 1024), pause_multiplier: Cell::new(200), state: Cell::new(GcState::Pause),
paused: Cell::new(true), collections: Cell::new(0),
marker: RefCell::new(None),
sweep_prev_next: Cell::new(None),
}
}
pub fn unpause(&self) {
self.paused.set(false);
}
pub fn is_paused(&self) -> bool {
self.paused.get()
}
pub fn allocate<T: Trace + 'static>(&self, value: T) -> Gc<T> {
let size = std::mem::size_of::<GcBox<T>>();
let boxed = Box::new(GcBox {
header: GcHeader::new_white(
size,
self.current_white.get(),
std::any::type_name::<T>(),
),
value,
});
boxed.header.next.set(self.head.get());
boxed.header.collected.set(true);
let raw: *mut GcBox<T> = Box::into_raw(boxed);
let ptr: NonNull<GcBox<T>> =
NonNull::new(raw).expect("Box::into_raw is non-null");
let dyn_ptr: NonNull<GcBox<dyn Trace>> = ptr;
let identity = ptr.as_ptr() as *const () as usize;
let token = self.next_token();
self.allocation_tokens
.borrow_mut()
.insert(identity, token);
self.head.set(Some(dyn_ptr));
self.bytes.set(self.bytes.get() + size);
Gc {
ptr,
_marker: PhantomData,
}
}
pub fn bytes_used(&self) -> usize {
self.bytes.get()
}
pub fn adjust_bytes(&self, delta: isize) {
if delta >= 0 {
self.bytes.set(self.bytes.get().saturating_add(delta as usize));
} else {
self.bytes
.set(self.bytes.get().saturating_sub((-delta) as usize));
}
}
pub fn threshold_bytes(&self) -> usize {
self.threshold.get()
}
pub fn set_threshold_bytes(&self, threshold: usize) {
self.threshold.set(threshold.max(1));
}
pub fn would_collect(&self) -> bool {
!self.paused.get() && self.bytes.get() >= self.threshold.get()
}
pub fn collections(&self) -> usize {
self.collections.get()
}
fn next_token(&self) -> usize {
let token = self.next_allocation_token.get().max(1);
let next = token.checked_add(1).unwrap_or(1).max(1);
self.next_allocation_token.set(next);
token
}
fn current_white(&self) -> Color {
self.current_white.get()
}
fn other_white(&self) -> Color {
self.current_white.get().other_white()
}
fn flip_current_white(&self) {
self.current_white.set(self.other_white());
}
fn for_each_header(&self, mut f: impl FnMut(&GcHeader)) {
let mut cursor = self.head.get();
while let Some(ptr) = cursor {
let header = self.header_from_ptr(ptr);
cursor = header.next.get();
f(header);
}
}
fn header_from_ptr<'a>(&'a self, ptr: NonNull<GcBox<dyn Trace>>) -> &'a GcHeader {
unsafe { &(*ptr.as_ptr()).header }
}
pub fn allocation_token(&self, identity: usize) -> Option<usize> {
self.allocation_tokens.borrow().get(&identity).copied()
}
pub fn contains_allocation(&self, identity: usize, token: usize) -> bool {
self.allocation_token(identity) == Some(token)
}
pub fn barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
where
P: Trace + 'static,
C: Trace + 'static,
{
if self.paused.get() || self.state.get().is_pause() {
return;
}
if parent.header().color.get() != Color::Black {
return;
}
if !child.header().color.get().is_white() {
return;
}
child.header().color.set(Color::Gray);
if let Ok(mut m_opt) = self.marker.try_borrow_mut() {
if let Some(m) = m_opt.as_mut() {
let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
m.gray_queue.push(ptr);
m.visited.insert(child.identity());
}
}
}
pub fn generational_forward_barrier<P, C>(&self, parent: Gc<P>, child: Gc<C>)
where
P: Trace + 'static,
C: Trace + 'static,
{
if parent.age().is_old() && !child.age().is_old() {
child.set_age(GcAge::Old0);
}
self.barrier(parent, child);
}
pub fn generational_backward_barrier<P>(&self, parent: Gc<P>)
where
P: Trace + 'static,
{
if parent.age().is_old() {
parent.set_color(Color::Gray);
parent.set_age(GcAge::Touched1);
}
}
pub fn step(&self, roots: &dyn Trace) {
self.step_with_post_mark(roots, |_: &mut Marker| {});
}
pub fn step_with_post_mark<F: FnMut(&mut Marker)>(
&self,
roots: &dyn Trace,
post_mark: F,
) {
if self.paused.get() {
return;
}
if self.bytes.get() < self.threshold.get() {
return;
}
self.full_collect_with_post_mark(roots, post_mark);
}
pub fn full_collect(&self, roots: &dyn Trace) {
self.full_collect_with_post_mark(roots, |_: &mut Marker| {});
}
pub fn mark_only_with_post_mark<F: FnMut(&mut Marker)>(
&self,
roots: &dyn Trace,
mut post_mark: F,
) {
if self.paused.get() {
return;
}
let mut marker = Marker::new();
roots.trace(&mut marker);
marker.drain_gray_queue();
post_mark(&mut marker);
marker.drain_gray_queue();
}
pub fn promote_all_to_old(&self) {
self.for_each_header(|header| {
header.age.set(GcAge::Old);
header.color.set(Color::Black);
});
}
pub fn reset_all_ages(&self) {
let current_white = self.current_white();
self.for_each_header(|header| {
header.age.set(GcAge::New);
header.color.set(current_white);
});
}
pub fn minor_collect_with_post_mark<F: FnMut(&mut Marker)>(
&self,
roots: &dyn Trace,
mut post_mark: F,
) {
if self.paused.get() {
return;
}
if !self.state.get().is_pause() {
self.abort_cycle();
}
self.state.set(GcState::Propagate);
let mut marker = Marker::new();
roots.trace(&mut marker);
marker.drain_gray_queue();
self.state.set(GcState::Atomic);
post_mark(&mut marker);
marker.drain_gray_queue();
self.state.set(GcState::Sweep);
self.sweep_young();
*self.marker.borrow_mut() = None;
self.sweep_prev_next.set(None);
self.state.set(GcState::Pause);
self.collections.set(self.collections.get() + 1);
}
pub fn full_collect_with_post_mark<F: FnMut(&mut Marker)>(
&self,
roots: &dyn Trace,
mut post_mark: F,
) {
if self.paused.get() {
return;
}
if !self.state.get().is_pause() {
self.abort_cycle();
}
let unlimited = StepBudget { remaining_work: isize::MAX, max_credit: isize::MAX };
loop {
let outcome = self.incremental_step_with_post_mark(roots, unlimited, &mut post_mark);
if matches!(outcome, StepOutcome::Paused | StepOutcome::SkippedStopped) {
break;
}
}
}
pub fn incremental_step_with_post_mark<F: FnMut(&mut Marker)>(
&self,
roots: &dyn Trace,
mut budget: StepBudget,
mut post_mark: F,
) -> StepOutcome {
if self.paused.get() {
return StepOutcome::SkippedStopped;
}
self.run_budgeted(roots, &mut budget, &mut post_mark);
if self.state.get().is_pause() {
StepOutcome::Paused
} else {
StepOutcome::InProgress
}
}
fn run_budgeted(
&self,
roots: &dyn Trace,
budget: &mut StepBudget,
post_mark: &mut dyn FnMut(&mut Marker),
) -> bool {
self.run_budgeted_until(roots, budget, post_mark, None)
}
fn run_budgeted_until(
&self,
roots: &dyn Trace,
budget: &mut StepBudget,
post_mark: &mut dyn FnMut(&mut Marker),
stop_at: Option<GcState>,
) -> bool {
let mut did_work = false;
loop {
if stop_at == Some(self.state.get()) {
return did_work;
}
if budget.remaining_work <= -budget.max_credit {
return did_work;
}
match self.state.get() {
GcState::Pause => {
self.start_cycle(roots);
self.state.set(GcState::Propagate);
budget.remaining_work -= 1;
did_work = true;
if stop_at == Some(GcState::Propagate) {
return did_work;
}
}
GcState::Propagate => {
let work = self.drain_gray_budgeted(budget.remaining_work.max(1));
budget.remaining_work -= work as isize;
did_work = did_work || work > 0;
let empty = {
let m = self.marker.borrow();
m.as_ref().map(|m| m.gray_queue.is_empty()).unwrap_or(true)
};
if empty {
self.state.set(GcState::Atomic);
if stop_at == Some(GcState::Atomic) {
return did_work;
}
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::Atomic => {
self.run_atomic(post_mark);
self.state.set(GcState::Sweep);
budget.remaining_work -= 1;
did_work = true;
if stop_at == Some(GcState::Sweep) {
return did_work;
}
}
GcState::Sweep => {
let work = self.sweep_budgeted(budget.remaining_work.max(1));
budget.remaining_work -= work as isize;
did_work = did_work || work > 0;
if self.sweep_prev_next.get().is_none() {
self.state.set(GcState::Finalize);
if stop_at == Some(GcState::Finalize) {
return did_work;
}
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::Finalize => {
self.finish_cycle();
self.state.set(GcState::Pause);
if stop_at == Some(GcState::Pause) {
return did_work;
}
return did_work;
}
}
}
}
pub fn incremental_run_until_state_with_post_mark<F: FnMut(&mut Marker)>(
&self,
roots: &dyn Trace,
target: GcState,
max_work: isize,
mut post_mark: F,
) -> StepOutcome {
if self.paused.get() {
return StepOutcome::SkippedStopped;
}
let work = max_work.max(1);
let mut budget = StepBudget {
remaining_work: work,
max_credit: work,
};
self.run_budgeted_until(roots, &mut budget, &mut post_mark, Some(target));
if self.state.get().is_pause() {
StepOutcome::Paused
} else {
StepOutcome::InProgress
}
}
fn start_cycle(&self, roots: &dyn Trace) {
self.flip_current_white();
let mut marker = Marker::new();
roots.trace(&mut marker);
*self.marker.borrow_mut() = Some(marker);
self.sweep_prev_next.set(None);
}
fn drain_gray_budgeted(&self, max_units: isize) -> usize {
let mut m_opt = self.marker.borrow_mut();
let marker = match m_opt.as_mut() {
Some(m) => m,
None => return 0,
};
let mut work = 0usize;
let mut budget = max_units;
while budget > 0 {
let next = match marker.gray_queue.pop() {
Some(p) => p,
None => break,
};
unsafe {
let bx = next.as_ref();
bx.header.color.set(Color::Black);
bx.value.trace(marker);
}
work += 1;
budget -= 1;
}
work
}
fn run_atomic(&self, post_mark: &mut dyn FnMut(&mut Marker)) {
let mut m_opt = self.marker.borrow_mut();
if let Some(marker) = m_opt.as_mut() {
marker.drain_gray_queue();
post_mark(marker);
marker.drain_gray_queue();
}
self.sweep_prev_next.set(Some(NonNull::from(&self.head)));
}
fn sweep_budgeted(&self, max_units: isize) -> usize {
let mut work = 0usize;
let mut budget = max_units;
let mut freed_bytes = 0usize;
let current_white = self.current_white();
let dead_white = self.other_white();
let mut prev_next_ptr = match self.sweep_prev_next.get() {
Some(p) => p,
None => return 0,
};
while budget > 0 {
let prev_cell = unsafe { prev_next_ptr.as_ref() };
let cursor = prev_cell.get();
let ptr = match cursor {
Some(p) => p,
None => {
self.sweep_prev_next.set(None);
break;
}
};
let header = self.header_from_ptr(ptr);
let next = header.next.get();
let color = header.color.get();
if color == dead_white {
prev_cell.set(next);
freed_bytes += header.size.get();
self.allocation_tokens
.borrow_mut()
.remove(&(ptr.as_ptr() as *const () as usize));
unsafe {
let _ = Box::from_raw(ptr.as_ptr());
}
} else {
if matches!(color, Color::Black | Color::Gray) {
header.color.set(current_white);
}
prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
self.sweep_prev_next.set(Some(prev_next_ptr));
}
work += 1;
budget -= 1;
}
if freed_bytes > 0 {
self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
}
work
}
fn sweep_young(&self) {
let mut freed_bytes = 0usize;
let current_white = self.current_white();
let mut prev_next_ptr = NonNull::from(&self.head);
loop {
let prev_cell = unsafe { prev_next_ptr.as_ref() };
let Some(ptr) = prev_cell.get() else { break; };
let header = self.header_from_ptr(ptr);
let next = header.next.get();
if header.color.get().is_white() && !header.age.get().is_old() {
prev_cell.set(next);
freed_bytes += header.size.get();
self.allocation_tokens
.borrow_mut()
.remove(&(ptr.as_ptr() as *const () as usize));
unsafe {
let _ = Box::from_raw(ptr.as_ptr());
}
continue;
}
if !header.color.get().is_white() {
let age = header.age.get();
header.age.set(age.next_after_minor());
match age {
GcAge::New => header.color.set(current_white),
GcAge::Touched1 | GcAge::Touched2 => header.color.set(Color::Black),
_ => {}
}
}
prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
}
if freed_bytes > 0 {
self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
}
}
fn finish_cycle(&self) {
*self.marker.borrow_mut() = None;
self.sweep_prev_next.set(None);
let next = self
.bytes
.get()
.saturating_mul(self.pause_multiplier.get())
/ 100;
self.threshold.set(next.max(GC_MIN_THRESHOLD));
self.collections.set(self.collections.get() + 1);
}
fn abort_cycle(&self) {
if !self.state.get().is_pause() {
*self.marker.borrow_mut() = None;
self.sweep_prev_next.set(None);
let current_white = self.current_white();
self.for_each_header(|header| {
header.color.set(current_white);
});
self.state.set(GcState::Pause);
}
}
pub fn gc_state(&self) -> GcState {
self.state.get()
}
pub fn allgc_count(&self) -> usize {
let mut count = 0usize;
self.for_each_header(|_| {
count += 1;
});
count
}
pub fn type_name_count(&self, mut predicate: impl FnMut(&'static str) -> bool) -> usize {
let mut count = 0usize;
self.for_each_header(|header| {
if predicate(header.type_name) {
count += 1;
}
});
count
}
pub fn drop_all(&self) {
*self.marker.borrow_mut() = None;
self.sweep_prev_next.set(None);
self.state.set(GcState::Pause);
let mut cursor = self.head.get();
self.head.set(None);
self.allocation_tokens.borrow_mut().clear();
while let Some(ptr) = cursor {
let next = unsafe {
let next = (*ptr.as_ptr()).header.next.get();
let _ = Box::from_raw(ptr.as_ptr());
next
};
cursor = next;
}
self.bytes.set(0);
}
}
impl Drop for Heap {
fn drop(&mut self) {
self.drop_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Cell0 {
next: Cell<Option<Gc<Cell0>>>,
marker_calls: Cell<usize>,
}
impl Trace for Cell0 {
fn trace(&self, m: &mut Marker) {
self.marker_calls.set(self.marker_calls.get() + 1);
if let Some(n) = self.next.get() {
m.mark(n);
}
}
}
struct OneRoot(Option<Gc<Cell0>>);
impl Trace for OneRoot {
fn trace(&self, m: &mut Marker) {
if let Some(g) = self.0 {
m.mark(g);
}
}
}
#[test]
fn alloc_and_drop_all() {
let heap = Heap::new();
heap.unpause();
let _a = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let _b = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
assert!(heap.bytes_used() > 0);
heap.drop_all();
assert_eq!(heap.bytes_used(), 0);
}
#[test]
fn account_buffer_refunded_on_sweep() {
let heap = Heap::new();
heap.unpause();
let baseline = heap.bytes_used();
{
let a = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
assert!(heap.bytes_used() > baseline);
a.account_buffer(&heap, 4096);
assert_eq!(a.header().size.get(), std::mem::size_of::<GcBox<Cell0>>() + 4096);
}
heap.full_collect(&OneRoot(None));
assert_eq!(
heap.bytes_used(),
baseline,
"account_buffer charge must be fully refunded by sweep"
);
}
#[test]
fn account_buffer_noop_on_uncollected() {
let heap = Heap::new();
heap.unpause();
let g = Gc::new_uncollected(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let before = heap.bytes_used();
g.account_buffer(&heap, 8192);
assert_eq!(heap.bytes_used(), before, "uncollected box must not charge the pacer");
}
#[test]
fn collect_unreachable_frees_bytes() {
let heap = Heap::new();
heap.unpause();
let _a = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let bytes_before = heap.bytes_used();
assert!(bytes_before > 0);
heap.full_collect(&OneRoot(None));
assert_eq!(heap.bytes_used(), 0);
assert_eq!(heap.collections(), 1);
}
#[test]
fn collect_keeps_reachable() {
let heap = Heap::new();
heap.unpause();
let root = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let bytes_before = heap.bytes_used();
heap.full_collect(&OneRoot(Some(root)));
assert_eq!(heap.bytes_used(), bytes_before);
assert_eq!(root.marker_calls.get(), 1);
}
#[test]
fn allocations_start_new_and_white() {
let heap = Heap::new();
heap.unpause();
let obj = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
assert_eq!(obj.age(), GcAge::New);
assert!(obj.color().is_white());
}
#[test]
fn allocation_tokens_track_exact_live_box() {
let heap = Heap::new();
heap.unpause();
let obj = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let id = obj.identity();
let token = heap
.allocation_token(id)
.expect("heap allocation should get a token");
assert!(heap.contains_allocation(id, token));
assert!(!heap.contains_allocation(id, token + 1));
heap.full_collect(&OneRoot(None));
assert_eq!(heap.allocation_token(id), None);
assert!(!heap.contains_allocation(id, token));
}
#[test]
fn allocation_during_incremental_sweep_survives_current_cycle() {
let heap = Heap::new();
heap.unpause();
let old_dead = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let old_id = old_dead.identity();
let outcome = heap.incremental_step_with_post_mark(
&OneRoot(None),
StepBudget::from_work(1),
|_| {},
);
assert_eq!(outcome, StepOutcome::InProgress);
assert_eq!(heap.gc_state(), GcState::Sweep);
let new_during_sweep = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let new_id = new_during_sweep.identity();
loop {
let outcome = heap.incremental_step_with_post_mark(
&OneRoot(None),
StepBudget::from_work(64),
|_| {},
);
if matches!(outcome, StepOutcome::Paused) {
break;
}
}
assert_eq!(heap.allocation_token(old_id), None);
assert!(heap.allocation_token(new_id).is_some());
assert_eq!(heap.allgc_count(), 1);
heap.full_collect(&OneRoot(None));
assert_eq!(heap.allocation_token(new_id), None);
assert_eq!(heap.allgc_count(), 0);
}
#[test]
fn promote_and_reset_all_ages() {
let heap = Heap::new();
heap.unpause();
let a = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let b = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.promote_all_to_old();
assert_eq!(a.age(), GcAge::Old);
assert_eq!(b.age(), GcAge::Old);
assert_eq!(a.color(), Color::Black);
assert_eq!(b.color(), Color::Black);
heap.reset_all_ages();
assert_eq!(a.age(), GcAge::New);
assert_eq!(b.age(), GcAge::New);
assert!(a.color().is_white());
assert!(b.color().is_white());
}
#[test]
fn generational_barriers_update_ages() {
let heap = Heap::new();
heap.unpause();
let parent = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let child = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
parent.set_age(GcAge::Old);
parent.set_color(Color::Black);
heap.generational_backward_barrier(parent);
assert_eq!(parent.age(), GcAge::Touched1);
assert_eq!(parent.color(), Color::Gray);
heap.generational_forward_barrier(parent, child);
assert_eq!(child.age(), GcAge::Old0);
}
#[test]
fn minor_collect_frees_young_and_keeps_old() {
let heap = Heap::new();
heap.unpause();
let old_unreachable = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
old_unreachable.set_age(GcAge::Old);
old_unreachable.set_color(Color::Black);
let _young_unreachable = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let young_survivor = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.minor_collect_with_post_mark(&OneRoot(Some(young_survivor)), |_| {});
assert_eq!(heap.allgc_count(), 2);
assert_eq!(old_unreachable.age(), GcAge::Old);
assert_eq!(young_survivor.age(), GcAge::Survival);
assert!(young_survivor.color().is_white());
}
#[test]
fn collect_traverses_cycles() {
let heap = Heap::new();
heap.unpause();
let a = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let b = heap.allocate(Cell0 {
next: Cell::new(Some(a)),
marker_calls: Cell::new(0),
});
a.next.set(Some(b)); heap.full_collect(&OneRoot(Some(a)));
assert_eq!(a.marker_calls.get(), 1);
assert_eq!(b.marker_calls.get(), 1);
heap.full_collect(&OneRoot(None));
assert_eq!(heap.bytes_used(), 0);
}
#[test]
fn heap_guard_stacks() {
assert!(with_current_heap(|heap| heap.is_none()), "no guard initially");
let h1 = Heap::new();
h1.unpause();
{
let _g1 = HeapGuard::push(&h1);
assert!(with_current_heap(|heap| heap.is_some()));
let h2 = Heap::new();
h2.unpause();
{
let _g2 = HeapGuard::push(&h2);
with_current_heap(|heap| {
assert!(std::ptr::addr_eq(
heap.unwrap() as *const _,
&h2 as *const _,
));
});
}
with_current_heap(|heap| {
assert!(std::ptr::addr_eq(
heap.unwrap() as *const _,
&h1 as *const _,
));
});
}
assert!(
with_current_heap(|heap| heap.is_none()),
"stack empty after all guards drop"
);
}
#[test]
fn step_threshold_triggers_collect() {
let heap = Heap::new();
heap.unpause();
let mut keeps = Vec::new();
for _ in 0..64 {
keeps.push(heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
}));
}
struct ManyRoots<'a>(&'a [Gc<Cell0>]);
impl<'a> Trace for ManyRoots<'a> {
fn trace(&self, m: &mut Marker) {
for g in self.0.iter() {
m.mark(*g);
}
}
}
heap.step(&ManyRoots(&keeps));
assert!(heap.bytes_used() > 0);
}
#[test]
fn threshold_floored_after_collecting_tiny_heap() {
let heap = Heap::new();
heap.unpause();
struct NoRoots;
impl Trace for NoRoots {
fn trace(&self, _m: &mut Marker) {}
}
for _ in 0..200 {
heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
}
heap.full_collect(&NoRoots);
assert!(
heap.threshold_bytes() >= GC_MIN_THRESHOLD,
"threshold {} collapsed below floor {}; a churning program would full-collect per allocation",
heap.threshold_bytes(),
GC_MIN_THRESHOLD
);
}
fn build_chain(heap: &Heap, len: usize) -> Gc<Cell0> {
let head = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let mut cur = head;
for _ in 1..len {
let n = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
cur.next.set(Some(n));
cur = n;
}
head
}
#[test]
fn budget_zero_does_some_work() {
let heap = Heap::new();
heap.unpause();
let head = build_chain(&heap, 8);
let roots = OneRoot(Some(head));
let budget = StepBudget::from_work(0);
let outcome = heap.incremental_step_with_post_mark(&roots, budget, |_| {});
assert_ne!(outcome, StepOutcome::SkippedStopped);
assert_ne!(heap.gc_state(), GcState::Pause);
}
#[test]
fn run_until_state_stops_before_next_phase_work() {
let heap = Heap::new();
heap.unpause();
let head = build_chain(&heap, 8);
let roots = OneRoot(Some(head));
let atomic_calls = Cell::new(0);
let outcome = heap.incremental_run_until_state_with_post_mark(
&roots,
GcState::Atomic,
1024,
|_| atomic_calls.set(atomic_calls.get() + 1),
);
assert_eq!(outcome, StepOutcome::InProgress);
assert_eq!(heap.gc_state(), GcState::Atomic);
assert_eq!(atomic_calls.get(), 0, "atomic hook must not run before inspection");
let outcome = heap.incremental_run_until_state_with_post_mark(
&roots,
GcState::Sweep,
1024,
|_| atomic_calls.set(atomic_calls.get() + 1),
);
assert_eq!(outcome, StepOutcome::InProgress);
assert_eq!(heap.gc_state(), GcState::Sweep);
assert_eq!(atomic_calls.get(), 1, "entering sweep must run the atomic hook once");
}
#[test]
fn larger_budget_drains_more_gray_than_smaller() {
let small_heap = Heap::new();
small_heap.unpause();
let h1 = build_chain(&small_heap, 64);
let r1 = OneRoot(Some(h1));
let mut small_calls = 0;
loop {
small_calls += 1;
let outcome = small_heap.incremental_step_with_post_mark(
&r1,
StepBudget::from_work(2),
|_| {},
);
if outcome == StepOutcome::Paused {
break;
}
assert!(small_calls < 10_000, "did not converge");
}
let big_heap = Heap::new();
big_heap.unpause();
let h2 = build_chain(&big_heap, 64);
let r2 = OneRoot(Some(h2));
let mut big_calls = 0;
loop {
big_calls += 1;
let outcome = big_heap.incremental_step_with_post_mark(
&r2,
StepBudget::from_work(64),
|_| {},
);
if outcome == StepOutcome::Paused {
break;
}
assert!(big_calls < 10_000, "did not converge");
}
assert!(
big_calls < small_calls,
"expected big_calls={} < small_calls={}",
big_calls,
small_calls
);
}
#[test]
fn sweep_can_pause_and_resume() {
let heap = Heap::new();
heap.unpause();
for _ in 0..16 {
let _ = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
}
let roots = OneRoot(None);
let bytes_before = heap.bytes_used();
assert!(bytes_before > 0);
let mut step_count = 0;
let mut saw_in_progress_during_sweep = false;
loop {
step_count += 1;
let outcome = heap.incremental_step_with_post_mark(
&roots,
StepBudget::from_work(2),
|_| {},
);
if heap.gc_state() == GcState::Sweep && outcome == StepOutcome::InProgress {
saw_in_progress_during_sweep = true;
}
if outcome == StepOutcome::Paused {
break;
}
assert!(step_count < 10_000, "did not converge");
}
assert!(saw_in_progress_during_sweep, "sweep never paused mid-list");
assert_eq!(heap.bytes_used(), 0);
}
#[test]
fn post_mark_runs_once_per_atomic() {
let heap = Heap::new();
heap.unpause();
for _ in 0..32 {
let _ = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
}
let roots = OneRoot(None);
let call_count = std::cell::Cell::new(0);
let mut step_count = 0;
loop {
step_count += 1;
let outcome = heap.incremental_step_with_post_mark(
&roots,
StepBudget::from_work(2),
|_| {
call_count.set(call_count.get() + 1);
},
);
if outcome == StepOutcome::Paused {
break;
}
assert!(step_count < 10_000, "did not converge");
}
assert_eq!(call_count.get(), 1, "post_mark must run exactly once per cycle");
}
#[test]
fn full_collect_equivalent_to_incremental_to_pause() {
let h1 = Heap::new();
h1.unpause();
let head1 = h1.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let _orphan1 = h1.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let _orphan2 = h1.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let roots1 = OneRoot(Some(head1));
h1.full_collect(&roots1);
let bytes_full = h1.bytes_used();
let h2 = Heap::new();
h2.unpause();
let head2 = h2.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let _orphan3 = h2.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let _orphan4 = h2.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let roots2 = OneRoot(Some(head2));
loop {
let outcome = h2.incremental_step_with_post_mark(
&roots2,
StepBudget::from_work(1),
|_| {},
);
if outcome == StepOutcome::Paused {
break;
}
}
assert_eq!(h2.bytes_used(), bytes_full);
}
}