use std::cell::{Cell, RefCell};
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, PartialEq, Eq, Debug)]
pub enum Color {
White,
Gray,
Black,
}
#[repr(C)]
pub struct GcHeader {
color: Cell<Color>,
finalized: Cell<bool>,
next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
size: usize,
}
impl GcHeader {
fn new_white(size: usize) -> Self {
Self {
color: Cell::new(Color::White),
finalized: Cell::new(false),
next: Cell::new(None),
size,
}
}
}
#[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),
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
}
}
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) }
}
}
pub struct Heap {
head: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
bytes: 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),
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),
value,
});
boxed.header.next.set(self.head.get());
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;
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 threshold_bytes(&self) -> usize {
self.threshold.get()
}
pub fn would_collect(&self) -> bool {
!self.paused.get() && self.bytes.get() >= self.threshold.get()
}
pub fn collections(&self) -> usize {
self.collections.get()
}
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() != Color::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 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 cursor = self.head.get();
while let Some(ptr) = cursor {
let header = unsafe { &(*ptr.as_ptr()).header };
header.color.set(Color::White);
cursor = header.next.get();
}
let mut marker = Marker::new();
roots.trace(&mut marker);
marker.drain_gray_queue();
post_mark(&mut marker);
marker.drain_gray_queue();
}
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 {
let mut did_work = false;
loop {
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;
}
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);
} 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;
}
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);
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::Finalize => {
self.finish_cycle();
self.state.set(GcState::Pause);
return did_work;
}
}
}
}
fn start_cycle(&self, roots: &dyn Trace) {
let mut cursor = self.head.get();
while let Some(ptr) = cursor {
let header = unsafe { &(*ptr.as_ptr()).header };
header.color.set(Color::White);
cursor = header.next.get();
}
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 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 = unsafe { &(*ptr.as_ptr()).header };
let next = header.next.get();
match header.color.get() {
Color::White => {
prev_cell.set(next);
freed_bytes += header.size;
unsafe {
let _ = Box::from_raw(ptr.as_ptr());
}
}
Color::Black | Color::Gray => {
header.color.set(Color::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 finish_cycle(&self) {
*self.marker.borrow_mut() = None;
self.sweep_prev_next.set(None);
self.threshold
.set(self.bytes.get() * self.pause_multiplier.get() / 100);
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);
self.state.set(GcState::Pause);
}
}
pub fn gc_state(&self) -> GcState {
self.state.get()
}
pub fn allgc_count(&self) -> usize {
let mut count = 0usize;
let mut cursor = self.head.get();
while let Some(ptr) = cursor {
count += 1;
let header = unsafe { &(*ptr.as_ptr()).header };
cursor = header.next.get();
}
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);
while let Some(ptr) = cursor {
let next = unsafe { (*ptr.as_ptr()).header.next.get() };
unsafe {
let _ = Box::from_raw(ptr.as_ptr());
}
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 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 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);
}
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 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);
}
}