use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasherDefault, Hasher};
use std::marker::PhantomData;
use std::ptr::NonNull;
#[derive(Default)]
struct IdentityHasher {
value: u64,
}
impl Hasher for IdentityHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
const PRIME: u64 = 0x0000_0100_0000_01b3;
for &byte in bytes {
self.value ^= u64::from(byte);
self.value = self.value.wrapping_mul(PRIME);
}
}
#[inline]
fn write_usize(&mut self, i: usize) {
self.value = i as u64;
}
#[inline]
fn write_u64(&mut self, i: u64) {
self.value = i;
}
#[inline]
fn finish(&self) -> u64 {
let mut x = self.value;
x ^= x >> 30;
x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
}
type IdentityBuildHasher = BuildHasherDefault<IdentityHasher>;
type IdentityHashSet = HashSet<usize, IdentityBuildHasher>;
type IdentityHashMap<V> = HashMap<usize, V, IdentityBuildHasher>;
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,
}
}
}
pub trait FinalizerEntry: Clone {
fn identity(&self) -> usize;
fn heap_ptr(&self) -> Option<NonNull<GcBox<dyn Trace>>> {
None
}
fn age(&self) -> GcAge;
fn is_finalized(&self) -> bool;
fn set_finalized(&self, finalized: bool);
}
pub trait WeakEntry: Clone {
type Strong: Clone;
fn identity(&self) -> usize;
fn list_kind(&self) -> WeakListKind;
fn upgrade(&self) -> Option<Self::Strong>;
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WeakListKind {
WeakValues,
Ephemeron,
AllWeak,
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct WeakRegistryStats {
pub tracked: usize,
pub snapshot_live: usize,
pub snapshot_dead: usize,
pub retained: usize,
pub weak_values: usize,
pub ephemeron: usize,
pub all_weak: usize,
}
#[derive(Clone, Debug)]
pub struct WeakRegistry<T: WeakEntry> {
weak_values: Vec<T>,
ephemeron: Vec<T>,
all_weak: Vec<T>,
last_stats: WeakRegistryStats,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WeakRegistrySnapshot<T> {
pub weak_values: Vec<T>,
pub ephemeron: Vec<T>,
pub all_weak: Vec<T>,
}
impl<T> Default for WeakRegistrySnapshot<T> {
fn default() -> Self {
Self {
weak_values: Vec::new(),
ephemeron: Vec::new(),
all_weak: Vec::new(),
}
}
}
impl<T> WeakRegistrySnapshot<T> {
pub fn len(&self) -> usize {
self.weak_values
.len()
.saturating_add(self.ephemeron.len())
.saturating_add(self.all_weak.len())
}
pub fn into_flat(self) -> Vec<T> {
self.weak_values
.into_iter()
.chain(self.ephemeron)
.chain(self.all_weak)
.collect()
}
}
impl<T: WeakEntry> Default for WeakRegistry<T> {
fn default() -> Self {
Self {
weak_values: Vec::new(),
ephemeron: Vec::new(),
all_weak: Vec::new(),
last_stats: WeakRegistryStats::default(),
}
}
}
impl<T: WeakEntry> WeakRegistry<T> {
pub fn len(&self) -> usize {
self.weak_values
.len()
.saturating_add(self.ephemeron.len())
.saturating_add(self.all_weak.len())
}
pub fn stats(&self) -> WeakRegistryStats {
self.last_stats
}
fn list_mut(&mut self, kind: WeakListKind) -> &mut Vec<T> {
match kind {
WeakListKind::WeakValues => &mut self.weak_values,
WeakListKind::Ephemeron => &mut self.ephemeron,
WeakListKind::AllWeak => &mut self.all_weak,
}
}
pub fn remove_identity(&mut self, id: usize) {
self.weak_values.retain(|entry| entry.identity() != id);
self.ephemeron.retain(|entry| entry.identity() != id);
self.all_weak.retain(|entry| entry.identity() != id);
self.last_stats.tracked = self.len();
self.last_stats.retained = self.len();
self.update_cohort_stats();
}
fn update_cohort_stats(&mut self) {
self.last_stats.weak_values = self.weak_values.len();
self.last_stats.ephemeron = self.ephemeron.len();
self.last_stats.all_weak = self.all_weak.len();
}
pub fn push_unique(&mut self, entry: T) {
let id = entry.identity();
self.remove_identity(id);
self.list_mut(entry.list_kind()).push(entry);
self.last_stats.tracked = self.len();
self.last_stats.retained = self.len();
self.update_cohort_stats();
}
pub fn live_snapshot_by_kind(&mut self) -> WeakRegistrySnapshot<T::Strong> {
let tracked_before = self.len();
let weak_values_capacity = self.weak_values.len();
let ephemeron_capacity = self.ephemeron.len();
let all_weak_capacity = self.all_weak.len();
let mut seen = std::collections::HashSet::<usize>::with_capacity(tracked_before);
let mut live = WeakRegistrySnapshot {
weak_values: Vec::with_capacity(weak_values_capacity),
ephemeron: Vec::with_capacity(ephemeron_capacity),
all_weak: Vec::with_capacity(all_weak_capacity),
};
let mut dead = 0usize;
let entries = std::mem::take(&mut self.weak_values)
.into_iter()
.chain(std::mem::take(&mut self.ephemeron))
.chain(std::mem::take(&mut self.all_weak));
for entry in entries {
if !seen.insert(entry.identity()) {
continue;
}
match entry.upgrade() {
Some(strong) => {
match entry.list_kind() {
WeakListKind::WeakValues => live.weak_values.push(strong),
WeakListKind::Ephemeron => live.ephemeron.push(strong),
WeakListKind::AllWeak => live.all_weak.push(strong),
}
self.list_mut(entry.list_kind()).push(entry);
}
None => dead += 1,
}
}
self.last_stats = WeakRegistryStats {
tracked: tracked_before,
snapshot_live: live.len(),
snapshot_dead: dead,
retained: self.len(),
weak_values: self.weak_values.len(),
ephemeron: self.ephemeron.len(),
all_weak: self.all_weak.len(),
};
live
}
pub fn live_snapshot(&mut self) -> Vec<T::Strong> {
self.live_snapshot_by_kind().into_flat()
}
pub fn retain_identities(&mut self, ids: &std::collections::HashSet<usize>) {
self.weak_values.retain(|entry| ids.contains(&entry.identity()));
self.ephemeron.retain(|entry| ids.contains(&entry.identity()));
self.all_weak.retain(|entry| ids.contains(&entry.identity()));
self.last_stats.retained = self.len();
self.last_stats.tracked = self.len();
self.update_cohort_stats();
}
}
#[derive(Clone, Debug)]
pub struct FinalizerRegistry<T: FinalizerEntry> {
pending: Vec<T>,
to_be_finalized: Vec<T>,
pending_reallyold: usize,
pending_old1: usize,
pending_survival: usize,
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct FinalizerRegistryStats {
pub pending_young: usize,
pub pending_old: usize,
pub to_be_finalized_young: usize,
pub to_be_finalized_old: usize,
pub finobj_new: usize,
pub finobj_survival: usize,
pub finobj_old1: usize,
pub finobj_reallyold: usize,
pub finobj_minor_scan: usize,
}
impl<T: FinalizerEntry> Default for FinalizerRegistry<T> {
fn default() -> Self {
Self {
pending: Vec::new(),
to_be_finalized: Vec::new(),
pending_reallyold: 0,
pending_old1: 0,
pending_survival: 0,
}
}
}
impl<T: FinalizerEntry> FinalizerRegistry<T> {
fn pending_new_len(&self) -> usize {
self.pending.len().saturating_sub(
self.pending_reallyold
.saturating_add(self.pending_old1)
.saturating_add(self.pending_survival),
)
}
fn minor_scan_start(&self) -> usize {
self.pending_reallyold.saturating_add(self.pending_old1)
}
fn debug_assert_pending_cohorts(&self) {
debug_assert!(
self.pending_reallyold
.saturating_add(self.pending_old1)
.saturating_add(self.pending_survival)
<= self.pending.len()
);
}
pub fn pending(&self) -> &[T] {
&self.pending
}
pub fn pending_snapshot(&self) -> Vec<T> {
self.pending.clone()
}
pub fn pending_minor_snapshot(&self) -> Vec<T> {
self.pending[self.minor_scan_start().min(self.pending.len())..].to_vec()
}
pub fn to_be_finalized(&self) -> &[T] {
&self.to_be_finalized
}
pub fn pending_len(&self) -> usize {
self.pending.len()
}
pub fn to_be_finalized_len(&self) -> usize {
self.to_be_finalized.len()
}
pub fn has_to_be_finalized(&self) -> bool {
!self.to_be_finalized.is_empty()
}
pub fn stats(&self) -> FinalizerRegistryStats {
fn count_by_age<T: FinalizerEntry>(objects: &[T]) -> (usize, usize) {
objects.iter().fold((0usize, 0usize), |(young, old), object| {
if object.age().is_old() {
(young, old + 1)
} else {
(young + 1, old)
}
})
}
let (pending_young, pending_old) = count_by_age(&self.pending);
let (to_be_finalized_young, to_be_finalized_old) =
count_by_age(&self.to_be_finalized);
FinalizerRegistryStats {
pending_young,
pending_old,
to_be_finalized_young,
to_be_finalized_old,
finobj_new: self.pending_new_len(),
finobj_survival: self.pending_survival,
finobj_old1: self.pending_old1,
finobj_reallyold: self.pending_reallyold,
finobj_minor_scan: self.pending.len().saturating_sub(self.minor_scan_start()),
}
}
pub fn push_pending_unique(&mut self, object: T) -> bool {
if object.is_finalized() {
return false;
}
let id = object.identity();
if !self.pending.iter().any(|o| o.identity() == id) {
object.set_finalized(true);
self.pending.push(object);
self.debug_assert_pending_cohorts();
true
} else {
false
}
}
pub fn take_pending(&mut self) -> Vec<T> {
self.pending_reallyold = 0;
self.pending_old1 = 0;
self.pending_survival = 0;
std::mem::take(&mut self.pending)
}
fn retain_pending_not_in(&mut self, ids: &std::collections::HashSet<usize>) {
if ids.is_empty() {
return;
}
let original_reallyold = self.pending_reallyold;
let original_old1 = self.pending_old1;
let original_survival = self.pending_survival;
let mut retained_reallyold = original_reallyold;
let mut retained_old1 = original_old1;
let mut retained_survival = original_survival;
let mut retained = Vec::with_capacity(self.pending.len());
for (index, object) in std::mem::take(&mut self.pending).into_iter().enumerate() {
if ids.contains(&object.identity()) {
if index < original_reallyold {
retained_reallyold -= 1;
} else if index < original_reallyold + original_old1 {
retained_old1 -= 1;
} else if index < original_reallyold + original_old1 + original_survival {
retained_survival -= 1;
}
} else {
retained.push(object);
}
}
self.pending = retained;
self.pending_reallyold = retained_reallyold;
self.pending_old1 = retained_old1;
self.pending_survival = retained_survival;
self.debug_assert_pending_cohorts();
}
pub fn push_to_be_finalized(&mut self, object: T) {
object.set_finalized(true);
self.to_be_finalized.push(object);
}
fn extend_to_be_finalized(&mut self, objects: Vec<T>) -> Vec<T> {
let drain_order: Vec<T> = objects.into_iter().rev().collect();
for object in drain_order.iter().cloned() {
self.push_to_be_finalized(object);
}
drain_order
}
pub fn promote_pending_to_finalized(&mut self, objects: Vec<T>) -> Vec<T> {
if objects.is_empty() {
return Vec::new();
}
let mut ids: std::collections::HashSet<usize> =
std::collections::HashSet::with_capacity(objects.len());
ids.extend(objects.iter().map(|object| object.identity()));
self.retain_pending_not_in(&ids);
self.extend_to_be_finalized(objects)
}
pub fn promote_all_pending_to_old(&mut self) {
self.pending_reallyold = self.pending.len();
self.pending_old1 = 0;
self.pending_survival = 0;
}
pub fn reset_generation_boundaries(&mut self) {
self.pending_reallyold = 0;
self.pending_old1 = 0;
self.pending_survival = 0;
}
pub fn finish_minor_collection(&mut self) {
let new = self.pending_new_len();
self.pending_reallyold = self.pending_reallyold.saturating_add(self.pending_old1);
self.pending_old1 = self.pending_survival;
self.pending_survival = new;
self.debug_assert_pending_cohorts();
}
pub fn pop_to_be_finalized(&mut self) -> Option<T> {
let object = if self.to_be_finalized.is_empty() {
None
} else {
Some(self.to_be_finalized.remove(0))
};
if let Some(ref object) = object {
object.set_finalized(false);
}
object
}
}
#[repr(C)]
pub struct GcHeader {
color: Cell<Color>,
age: Cell<GcAge>,
finalized: Cell<bool>,
collected: Cell<bool>,
next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
gray_next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
gray_listed: Cell<bool>,
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),
gray_next: Cell::new(None),
gray_listed: Cell::new(false),
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,
}
}
pub fn as_trace_ptr(self) -> NonNull<GcBox<dyn Trace>> {
self.ptr
}
}
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 is_finalized(self) -> bool {
self.header().finalized.get()
}
pub fn set_finalized(self, finalized: bool) {
self.header().finalized.set(finalized);
}
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) {}
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct MarkerStats {
pub marked: usize,
pub marked_young: usize,
pub marked_old: usize,
pub traced: usize,
pub traced_young: usize,
pub traced_old: usize,
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct SweepStats {
pub visited: usize,
pub visited_young: usize,
pub visited_old: usize,
pub revisit: usize,
pub freed: usize,
pub freed_bytes: usize,
}
impl SweepStats {
fn record_visit(&mut self, age: GcAge) {
self.visited += 1;
if age.is_old() {
self.visited_old += 1;
} else {
self.visited_young += 1;
}
}
fn record_free(&mut self, bytes: usize) {
self.freed += 1;
self.freed_bytes += bytes;
}
fn add(&mut self, other: Self) {
self.visited += other.visited;
self.visited_young += other.visited_young;
self.visited_old += other.visited_old;
self.revisit += other.revisit;
self.freed += other.freed;
self.freed_bytes += other.freed_bytes;
}
}
struct OldRevisitTracker {
old_revisit_ids: Vec<usize>,
processed_ids: Vec<usize>,
}
impl OldRevisitTracker {
fn new(old_revisit: &[NonNull<GcBox<dyn Trace>>]) -> Option<Self> {
if old_revisit.is_empty() {
return None;
}
let mut old_revisit_ids: Vec<usize> = old_revisit
.iter()
.map(|ptr| ptr.as_ptr() as *const () as usize)
.collect();
old_revisit_ids.sort_unstable();
old_revisit_ids.dedup();
Some(Self {
old_revisit_ids,
processed_ids: Vec::new(),
})
}
#[inline(always)]
fn record_processed(&mut self, id: usize) {
if self.old_revisit_ids.binary_search(&id).is_ok() {
self.processed_ids.push(id);
}
}
fn finish(&mut self) {
self.processed_ids.sort_unstable();
self.processed_ids.dedup();
}
#[inline(always)]
fn was_processed(&self, id: usize) -> bool {
self.processed_ids.binary_search(&id).is_ok()
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct AllGcCohortStats {
pub new: usize,
pub survival: usize,
pub old1: usize,
pub old: usize,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum MarkerMode {
Full,
Minor,
}
pub struct Marker {
gray_queue: Vec<NonNull<GcBox<dyn Trace>>>,
visited: IdentityHashSet,
stats: MarkerStats,
mode: MarkerMode,
}
impl Marker {
fn new_with_capacity(mode: MarkerMode, capacity: usize) -> Self {
Self {
gray_queue: Vec::with_capacity(256),
visited: IdentityHashSet::with_capacity_and_hasher(capacity, IdentityBuildHasher::default()),
stats: MarkerStats::default(),
mode,
}
}
fn new_reserving(capacity: usize) -> Self {
Self::new_with_capacity(MarkerMode::Full, capacity)
}
fn new_minor_reserving(capacity: usize) -> Self {
Self::new_with_capacity(MarkerMode::Minor, capacity)
}
fn should_trace_age(&self, age: GcAge) -> bool {
match self.mode {
MarkerMode::Full => true,
MarkerMode::Minor => !matches!(age, GcAge::Old),
}
}
pub fn mark<T: Trace + 'static>(&mut self, gc: Gc<T>) {
let ptr: NonNull<GcBox<dyn Trace>> = gc.ptr;
self.mark_box(ptr, gc.header(), gc.identity());
}
fn mark_box(&mut self, ptr: NonNull<GcBox<dyn Trace>>, header: &GcHeader, id: usize) {
if self.visited.insert(id) {
let age = header.age.get();
self.stats.marked += 1;
if age.is_old() {
self.stats.marked_old += 1;
} else {
self.stats.marked_young += 1;
}
if self.should_trace_age(age) {
header.color.set(Color::Gray);
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 is_marked_or_old<T: Trace + 'static>(&self, gc: Gc<T>) -> bool {
self.is_visited(gc.identity())
|| (matches!(self.mode, MarkerMode::Minor) && gc.age().is_old())
}
pub fn visited_count(&self) -> usize {
self.visited.len()
}
pub fn stats(&self) -> MarkerStats {
self.stats
}
pub fn drain_gray_queue(&mut self) {
while let Some(gray_ptr) = self.gray_queue.pop() {
unsafe {
let bx = gray_ptr.as_ref();
self.stats.traced += 1;
if bx.header.age.get().is_old() {
self.stats.traced_old += 1;
} else {
self.stats.traced_young += 1;
}
bx.header.color.set(Color::Black);
bx.value.trace(self);
}
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum GcState {
Pause,
Propagate,
EnterAtomic,
Atomic,
SweepAllGc,
SweepFinObj,
SweepToBeFnz,
SweepEnd,
CallFin,
}
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_invariant(self) -> bool {
matches!(self, GcState::Propagate | GcState::EnterAtomic | GcState::Atomic)
}
pub fn is_sweep(self) -> bool {
matches!(
self,
GcState::SweepAllGc
| GcState::SweepFinObj
| GcState::SweepToBeFnz
| GcState::SweepEnd
)
}
}
#[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>>>>,
finobj: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
tobefnz: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
survival: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
old1: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
reallyold: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
firstold1: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
finobjsur: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
finobjold1: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
finobjrold: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
bytes: Cell<usize>,
objects: Cell<usize>,
current_white: Cell<Color>,
allocation_tokens: RefCell<IdentityHashMap<usize>>,
next_allocation_token: Cell<usize>,
threshold: Cell<usize>,
pause_multiplier: Cell<usize>,
state: Cell<GcState>,
paused: Cell<bool>,
collections: Cell<usize>,
minor_collections: Cell<usize>,
full_collections: Cell<usize>,
last_mark_stats: Cell<MarkerStats>,
last_sweep_stats: Cell<SweepStats>,
grayagain: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
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),
finobj: Cell::new(None),
tobefnz: Cell::new(None),
survival: Cell::new(None),
old1: Cell::new(None),
reallyold: Cell::new(None),
firstold1: Cell::new(None),
finobjsur: Cell::new(None),
finobjold1: Cell::new(None),
finobjrold: Cell::new(None),
bytes: Cell::new(0),
objects: Cell::new(0),
current_white: Cell::new(Color::White0),
allocation_tokens: RefCell::new(IdentityHashMap::default()),
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),
minor_collections: Cell::new(0),
full_collections: Cell::new(0),
last_mark_stats: Cell::new(MarkerStats::default()),
last_sweep_stats: Cell::new(SweepStats::default()),
grayagain: Cell::new(None),
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);
self.objects.set(self.objects.get() + 1);
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()
}
pub fn minor_collections(&self) -> usize {
self.minor_collections.get()
}
pub fn full_collections(&self) -> usize {
self.full_collections.get()
}
pub fn last_mark_stats(&self) -> MarkerStats {
self.last_mark_stats.get()
}
pub fn last_sweep_stats(&self) -> SweepStats {
self.last_sweep_stats.get()
}
pub fn allgc_cohort_stats(&self) -> AllGcCohortStats {
let survival = self.survival.get();
let old1 = self.old1.get();
let reallyold = self.reallyold.get();
let mut stats = AllGcCohortStats::default();
let mut cursor = self.head.get();
let mut seen = IdentityHashSet::default();
let mut cohort = 0u8;
while let Some(ptr) = cursor {
let id = ptr.as_ptr() as *const () as usize;
if !seen.insert(id) {
break;
}
if Some(ptr) == reallyold {
cohort = 3;
} else if Some(ptr) == old1 {
cohort = 2;
} else if Some(ptr) == survival {
cohort = 1;
}
match cohort {
0 => stats.new += 1,
1 => stats.survival += 1,
2 => stats.old1 += 1,
_ => stats.old += 1,
}
cursor = self.header_from_ptr(ptr).next.get();
}
stats
}
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_list_header(
&self,
head: Option<NonNull<GcBox<dyn Trace>>>,
f: &mut impl FnMut(&GcHeader),
) {
let mut cursor = head;
while let Some(ptr) = cursor {
let header = self.header_from_ptr(ptr);
cursor = header.next.get();
f(header);
}
}
fn for_each_header(&self, mut f: impl FnMut(&GcHeader)) {
self.for_each_list_header(self.head.get(), &mut f);
self.for_each_list_header(self.finobj.get(), &mut f);
self.for_each_list_header(self.tobefnz.get(), &mut f);
}
fn header_from_ptr<'a>(&'a self, ptr: NonNull<GcBox<dyn Trace>>) -> &'a GcHeader {
unsafe { &(*ptr.as_ptr()).header }
}
fn clear_generation_cursors(&self) {
self.survival.set(None);
self.old1.set(None);
self.reallyold.set(None);
self.firstold1.set(None);
self.finobjsur.set(None);
self.finobjold1.set(None);
self.finobjrold.set(None);
self.clear_grayagain();
}
fn set_all_cursors_to_head(&self) {
let head = self.head.get();
self.survival.set(head);
self.old1.set(head);
self.reallyold.set(head);
self.firstold1.set(None);
let finobj = self.finobj.get();
self.finobjsur.set(finobj);
self.finobjold1.set(finobj);
self.finobjrold.set(finobj);
self.clear_grayagain();
}
fn correct_generation_pointers(
&self,
removed: NonNull<GcBox<dyn Trace>>,
next: Option<NonNull<GcBox<dyn Trace>>>,
) {
if self.survival.get() == Some(removed) {
self.survival.set(next);
}
if self.old1.get() == Some(removed) {
self.old1.set(next);
}
if self.reallyold.get() == Some(removed) {
self.reallyold.set(next);
}
if self.firstold1.get() == Some(removed) {
self.firstold1.set(next);
}
if self.finobjsur.get() == Some(removed) {
self.finobjsur.set(next);
}
if self.finobjold1.get() == Some(removed) {
self.finobjold1.set(next);
}
if self.finobjrold.get() == Some(removed) {
self.finobjrold.set(next);
}
if self.header_from_ptr(removed).gray_listed.get() {
self.unlink_grayagain(removed);
}
}
fn unlink_from_list(
&self,
list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>,
ptr: NonNull<GcBox<dyn Trace>>,
) -> bool {
let mut prev_cell = list;
loop {
let Some(current) = prev_cell.get() else {
return false;
};
let header = self.header_from_ptr(current);
let next = header.next.get();
if std::ptr::addr_eq(current.as_ptr(), ptr.as_ptr()) {
prev_cell.set(next);
let prev_next_ptr = NonNull::from(prev_cell);
let removed_next_ptr = NonNull::from(&self.header_from_ptr(ptr).next);
if self.sweep_prev_next.get() == Some(removed_next_ptr) {
self.sweep_prev_next.set(Some(prev_next_ptr));
}
self.correct_generation_pointers(ptr, next);
header.next.set(None);
return true;
}
prev_cell = &header.next;
}
}
fn link_to_head(
&self,
list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>,
ptr: NonNull<GcBox<dyn Trace>>,
) {
let header = self.header_from_ptr(ptr);
header.next.set(list.get());
list.set(Some(ptr));
}
fn link_to_tail(
&self,
list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>,
ptr: NonNull<GcBox<dyn Trace>>,
) {
let mut last_cell = list;
loop {
let Some(current) = last_cell.get() else {
let header = self.header_from_ptr(ptr);
header.next.set(None);
last_cell.set(Some(ptr));
return;
};
last_cell = &self.header_from_ptr(current).next;
}
}
pub fn move_allgc_to_finobj(&self, ptr: NonNull<GcBox<dyn Trace>>) -> bool {
let header = self.header_from_ptr(ptr);
if !header.collected.get() {
return false;
}
if !self.unlink_from_list(&self.head, ptr) {
return false;
}
if self.state.get().is_sweep() {
header.color.set(self.current_white());
}
self.link_to_head(&self.finobj, ptr);
true
}
pub fn move_finobj_to_tobefnz(&self, ptr: NonNull<GcBox<dyn Trace>>) -> bool {
if !self.unlink_from_list(&self.finobj, ptr) {
return false;
}
self.link_to_tail(&self.tobefnz, ptr);
true
}
pub fn move_tobefnz_to_allgc(&self, ptr: NonNull<GcBox<dyn Trace>>) -> bool {
let header = self.header_from_ptr(ptr);
if !self.unlink_from_list(&self.tobefnz, ptr) {
return false;
}
if self.state.get().is_sweep() {
header.color.set(self.current_white());
}
self.link_to_head(&self.head, ptr);
if header.age.get() == GcAge::Old1 {
self.firstold1.set(Some(ptr));
}
true
}
fn remember_minor_revisit(&self, ptr: NonNull<GcBox<dyn Trace>>) {
let header = self.header_from_ptr(ptr);
if header.gray_listed.get() {
return;
}
header.gray_next.set(self.grayagain.get());
header.gray_listed.set(true);
self.grayagain.set(Some(ptr));
}
fn mark_minor_revisit_objects(&self, marker: &mut Marker) {
let mut cursor = self.grayagain.get();
while let Some(ptr) = cursor {
let header = self.header_from_ptr(ptr);
cursor = header.gray_next.get();
let id = ptr.as_ptr() as *const () as usize;
marker.mark_box(ptr, header, id);
}
}
fn clear_grayagain(&self) {
let mut cursor = self.grayagain.get();
self.grayagain.set(None);
while let Some(ptr) = cursor {
let header = self.header_from_ptr(ptr);
cursor = header.gray_next.get();
header.gray_next.set(None);
header.gray_listed.set(false);
}
}
fn take_grayagain(&self) -> Vec<NonNull<GcBox<dyn Trace>>> {
let mut objects = Vec::new();
let mut cursor = self.grayagain.get();
self.grayagain.set(None);
while let Some(ptr) = cursor {
let header = self.header_from_ptr(ptr);
cursor = header.gray_next.get();
header.gray_next.set(None);
header.gray_listed.set(false);
objects.push(ptr);
}
objects
}
fn replace_grayagain(&self, objects: Vec<NonNull<GcBox<dyn Trace>>>) {
self.clear_grayagain();
for ptr in objects.into_iter().rev() {
self.remember_minor_revisit(ptr);
}
}
fn unlink_grayagain(&self, removed: NonNull<GcBox<dyn Trace>>) {
let keep = self
.take_grayagain()
.into_iter()
.filter(|ptr| !std::ptr::addr_eq(ptr.as_ptr(), removed.as_ptr()))
.collect();
self.replace_grayagain(keep);
}
pub fn grayagain_count(&self) -> usize {
let mut count = 0usize;
let mut cursor = self.grayagain.get();
while let Some(ptr) = cursor {
count += 1;
cursor = self.header_from_ptr(ptr).gray_next.get();
}
count
}
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 barrier_back<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;
}
parent.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>> = parent.ptr;
m.gray_queue.push(ptr);
m.visited.insert(parent.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);
let ptr: NonNull<GcBox<dyn Trace>> = child.ptr;
self.remember_minor_revisit(ptr);
}
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);
let ptr: NonNull<GcBox<dyn Trace>> = parent.ptr;
self.remember_minor_revisit(ptr);
}
}
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_reserving(self.objects.get());
roots.trace(&mut marker);
marker.drain_gray_queue();
post_mark(&mut marker);
marker.drain_gray_queue();
self.last_mark_stats.set(marker.stats());
}
pub fn promote_all_to_old(&self) {
self.for_each_header(|header| {
header.age.set(GcAge::Old);
header.color.set(Color::Black);
});
self.set_all_cursors_to_head();
}
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);
});
self.clear_generation_cursors();
}
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_minor_reserving(self.objects.get());
self.last_sweep_stats.set(SweepStats::default());
self.mark_minor_revisit_objects(&mut marker);
roots.trace(&mut marker);
marker.drain_gray_queue();
self.state.set(GcState::EnterAtomic);
self.state.set(GcState::Atomic);
post_mark(&mut marker);
marker.drain_gray_queue();
self.last_mark_stats.set(marker.stats());
self.state.set(GcState::SweepAllGc);
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);
self.minor_collections.set(self.minor_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();
}
self.full_collections.set(self.full_collections.get() + 1);
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::EnterAtomic);
if stop_at == Some(GcState::EnterAtomic) {
return did_work;
}
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::EnterAtomic => {
self.state.set(GcState::Atomic);
budget.remaining_work -= 1;
did_work = true;
if stop_at == Some(GcState::Atomic) || budget.remaining_work <= 0 {
return did_work;
}
}
GcState::Atomic => {
self.run_atomic(post_mark);
self.state.set(GcState::SweepAllGc);
budget.remaining_work -= 1;
did_work = true;
if stop_at == Some(GcState::SweepAllGc) {
return did_work;
}
}
GcState::SweepAllGc => {
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::SweepFinObj);
self.sweep_prev_next.set(Some(NonNull::from(&self.finobj)));
if stop_at == Some(GcState::SweepFinObj) {
return did_work;
}
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::SweepFinObj => {
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::SweepToBeFnz);
self.sweep_prev_next.set(Some(NonNull::from(&self.tobefnz)));
if stop_at == Some(GcState::SweepToBeFnz) {
return did_work;
}
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::SweepToBeFnz => {
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::SweepEnd);
if stop_at == Some(GcState::SweepEnd) {
return did_work;
}
} else if budget.remaining_work <= 0 {
return did_work;
}
}
GcState::SweepEnd => {
self.state.set(GcState::CallFin);
budget.remaining_work -= 1;
did_work = true;
if stop_at == Some(GcState::CallFin) || budget.remaining_work <= 0 {
return did_work;
}
}
GcState::CallFin => {
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 dead_white = self.other_white();
self.for_each_header(|header| {
header.color.set(dead_white);
});
let mut marker = Marker::new_reserving(self.objects.get());
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();
marker.stats.traced += 1;
if bx.header.age.get().is_old() {
marker.stats.traced_old += 1;
} else {
marker.stats.traced_young += 1;
}
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)));
self.last_sweep_stats.set(SweepStats::default());
}
fn sweep_budgeted(&self, max_units: isize) -> usize {
let mut work = 0usize;
let mut budget = max_units;
let mut freed_bytes = 0usize;
let mut stats = SweepStats::default();
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 age = header.age.get();
stats.record_visit(age);
let color = header.color.get();
if color == dead_white {
prev_cell.set(next);
let size = header.size.get();
freed_bytes += size;
stats.record_free(size);
self.correct_generation_pointers(ptr, next);
self.allocation_tokens
.borrow_mut()
.remove(&(ptr.as_ptr() as *const () as usize));
self.objects.set(self.objects.get().saturating_sub(1));
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));
}
if stats.visited > 0 {
let mut total = self.last_sweep_stats.get();
total.add(stats);
self.last_sweep_stats.set(total);
}
work
}
fn push_next_revisit(
next_revisit: &mut Vec<NonNull<GcBox<dyn Trace>>>,
seen: &mut IdentityHashSet,
ptr: NonNull<GcBox<dyn Trace>>,
age: GcAge,
) {
if matches!(
age,
GcAge::Old0 | GcAge::Old1 | GcAge::Touched1 | GcAge::Touched2
) {
let id = ptr.as_ptr() as *const () as usize;
if seen.insert(id) {
next_revisit.push(ptr);
}
}
}
fn sweep_young_range(
&self,
mut prev_next_ptr: NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>,
limit: Option<NonNull<GcBox<dyn Trace>>>,
next_revisit: &mut Vec<NonNull<GcBox<dyn Trace>>>,
next_revisit_seen: &mut IdentityHashSet,
processed: &mut Option<OldRevisitTracker>,
firstold1: &mut Option<NonNull<GcBox<dyn Trace>>>,
freed_bytes: &mut usize,
stats: &mut SweepStats,
) -> (
NonNull<Cell<Option<NonNull<GcBox<dyn Trace>>>>>,
Option<NonNull<GcBox<dyn Trace>>>,
) {
let current_white = self.current_white();
loop {
let prev_cell = unsafe { prev_next_ptr.as_ref() };
let Some(ptr) = prev_cell.get() else {
return (prev_next_ptr, None);
};
if Some(ptr) == limit {
return (prev_next_ptr, Some(ptr));
}
let header = self.header_from_ptr(ptr);
let next = header.next.get();
let age = header.age.get();
stats.record_visit(age);
if let Some(processed) = processed.as_mut() {
processed.record_processed(ptr.as_ptr() as *const () as usize);
}
if header.color.get().is_white() && !age.is_old() {
prev_cell.set(next);
let size = header.size.get();
*freed_bytes += size;
stats.record_free(size);
self.correct_generation_pointers(ptr, next);
self.allocation_tokens
.borrow_mut()
.remove(&(ptr.as_ptr() as *const () as usize));
self.objects.set(self.objects.get().saturating_sub(1));
unsafe {
let _ = Box::from_raw(ptr.as_ptr());
}
continue;
}
if !header.color.get().is_white() {
let next_age = age.next_after_minor();
header.age.set(next_age);
if next_age == GcAge::Old1 && firstold1.is_none() {
*firstold1 = Some(ptr);
}
match age {
GcAge::New => header.color.set(current_white),
GcAge::Touched1 | GcAge::Touched2 => header.color.set(Color::Black),
_ => {}
}
Self::push_next_revisit(next_revisit, next_revisit_seen, ptr, next_age);
}
prev_next_ptr = unsafe { NonNull::from(&(*ptr.as_ptr()).header.next) };
}
}
fn sweep_young(&self) {
let mut freed_bytes = 0usize;
let mut next_revisit = Vec::new();
let mut next_revisit_seen = IdentityHashSet::default();
let mut firstold1 = None;
let mut stats = SweepStats::default();
let old_revisit = self.take_grayagain();
let mut processed = OldRevisitTracker::new(&old_revisit);
let survival = self.survival.get();
let old1 = self.old1.get();
let (psurvival, new_old1) = self.sweep_young_range(
NonNull::from(&self.head),
survival,
&mut next_revisit,
&mut next_revisit_seen,
&mut processed,
&mut firstold1,
&mut freed_bytes,
&mut stats,
);
self.sweep_young_range(
psurvival,
old1,
&mut next_revisit,
&mut next_revisit_seen,
&mut processed,
&mut firstold1,
&mut freed_bytes,
&mut stats,
);
let finobjsur = self.finobjsur.get();
let finobjold1 = self.finobjold1.get();
let mut dummy_firstold1 = None;
let (pfinobjsur, new_finobjold1) = self.sweep_young_range(
NonNull::from(&self.finobj),
finobjsur,
&mut next_revisit,
&mut next_revisit_seen,
&mut processed,
&mut dummy_firstold1,
&mut freed_bytes,
&mut stats,
);
self.sweep_young_range(
pfinobjsur,
finobjold1,
&mut next_revisit,
&mut next_revisit_seen,
&mut processed,
&mut dummy_firstold1,
&mut freed_bytes,
&mut stats,
);
self.sweep_young_range(
NonNull::from(&self.tobefnz),
None,
&mut next_revisit,
&mut next_revisit_seen,
&mut processed,
&mut dummy_firstold1,
&mut freed_bytes,
&mut stats,
);
if let Some(processed) = processed.as_mut() {
processed.finish();
}
for ptr in old_revisit {
let id = ptr.as_ptr() as *const () as usize;
if processed.as_ref().is_some_and(|processed| processed.was_processed(id)) {
continue;
}
stats.revisit += 1;
let header = self.header_from_ptr(ptr);
if header.color.get().is_white() {
continue;
}
let age = header.age.get();
let next_age = age.next_after_minor();
header.age.set(next_age);
if next_age == GcAge::Old1 && firstold1.is_none() {
firstold1 = Some(ptr);
}
if matches!(age, GcAge::Touched1 | GcAge::Touched2) {
header.color.set(Color::Black);
}
Self::push_next_revisit(&mut next_revisit, &mut next_revisit_seen, ptr, next_age);
}
if freed_bytes > 0 {
self.bytes.set(self.bytes.get().saturating_sub(freed_bytes));
}
self.replace_grayagain(next_revisit);
self.reallyold.set(old1);
self.old1.set(new_old1);
self.survival.set(self.head.get());
self.firstold1.set(firstold1);
self.finobjrold.set(finobjold1);
self.finobjold1.set(new_finobjold1);
self.finobjsur.set(self.finobj.get());
self.last_sweep_stats.set(stats);
}
fn finish_cycle(&self) {
let stats = self
.marker
.borrow()
.as_ref()
.map(|marker| marker.stats())
.unwrap_or_default();
self.last_mark_stats.set(stats);
*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);
}
pub fn finish_callfin_phase(&self) -> bool {
if self.state.get() != GcState::CallFin {
return false;
}
self.finish_cycle();
self.state.set(GcState::Pause);
true
}
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 {
self.objects.get()
}
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.clear_generation_cursors();
self.state.set(GcState::Pause);
self.allocation_tokens.borrow_mut().clear();
self.drop_list(&self.head);
self.drop_list(&self.finobj);
self.drop_list(&self.tobefnz);
self.bytes.set(0);
self.objects.set(0);
}
fn drop_list(&self, list: &Cell<Option<NonNull<GcBox<dyn Trace>>>>) {
let mut cursor = list.get();
list.set(None);
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;
}
}
}
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);
}
}
}
struct TwoRoots {
first: Option<Gc<Cell0>>,
second: Option<Gc<Cell0>>,
}
impl Trace for TwoRoots {
fn trace(&self, m: &mut Marker) {
if let Some(g) = self.first {
m.mark(g);
}
if let Some(g) = self.second {
m.mark(g);
}
}
}
#[derive(Clone)]
struct FinalizerCell {
id: usize,
age: GcAge,
finalized: std::rc::Rc<Cell<bool>>,
}
impl FinalizerCell {
fn new(id: usize) -> Self {
Self {
id,
age: GcAge::New,
finalized: std::rc::Rc::new(Cell::new(false)),
}
}
}
impl FinalizerEntry for FinalizerCell {
fn identity(&self) -> usize {
self.id
}
fn age(&self) -> GcAge {
self.age
}
fn is_finalized(&self) -> bool {
self.finalized.get()
}
fn set_finalized(&self, finalized: bool) {
self.finalized.set(finalized);
}
}
fn finalizer_ids(objects: &[FinalizerCell]) -> Vec<usize> {
objects.iter().map(|object| object.id).collect()
}
#[derive(Clone)]
struct WeakCell {
id: usize,
live: bool,
kind: WeakListKind,
}
impl WeakEntry for WeakCell {
type Strong = usize;
fn identity(&self) -> usize {
self.id
}
fn list_kind(&self) -> WeakListKind {
self.kind
}
fn upgrade(&self) -> Option<Self::Strong> {
self.live.then_some(self.id)
}
}
#[test]
fn weak_registry_dedups_snapshots_and_retains_live_ids() {
let mut registry = WeakRegistry::default();
registry.push_unique(WeakCell {
id: 1,
live: true,
kind: WeakListKind::WeakValues,
});
registry.push_unique(WeakCell {
id: 1,
live: true,
kind: WeakListKind::Ephemeron,
});
registry.push_unique(WeakCell {
id: 2,
live: false,
kind: WeakListKind::AllWeak,
});
registry.push_unique(WeakCell {
id: 3,
live: true,
kind: WeakListKind::WeakValues,
});
let stats = registry.stats();
assert_eq!(stats.weak_values, 1);
assert_eq!(stats.ephemeron, 1);
assert_eq!(stats.all_weak, 1);
let snapshot = registry.live_snapshot();
assert_eq!(snapshot, vec![3, 1]);
assert_eq!(
registry.stats(),
WeakRegistryStats {
tracked: 3,
snapshot_live: 2,
snapshot_dead: 1,
retained: 2,
weak_values: 1,
ephemeron: 1,
all_weak: 0,
}
);
let keep: std::collections::HashSet<usize> = [3usize].into_iter().collect();
registry.retain_identities(&keep);
assert_eq!(registry.len(), 1);
assert_eq!(registry.stats().retained, 1);
assert_eq!(registry.stats().weak_values, 1);
registry.remove_identity(3);
assert_eq!(registry.len(), 0);
}
#[test]
fn finalizer_registry_tracks_generational_cohorts() {
let mut registry = FinalizerRegistry::default();
registry.push_pending_unique(FinalizerCell::new(1));
registry.push_pending_unique(FinalizerCell::new(2));
let stats = registry.stats();
assert_eq!(stats.finobj_new, 2);
assert_eq!(stats.finobj_survival, 0);
assert_eq!(stats.finobj_old1, 0);
assert_eq!(stats.finobj_reallyold, 0);
assert_eq!(stats.finobj_minor_scan, 2);
registry.finish_minor_collection();
let stats = registry.stats();
assert_eq!(stats.finobj_new, 0);
assert_eq!(stats.finobj_survival, 2);
assert_eq!(stats.finobj_old1, 0);
assert_eq!(stats.finobj_reallyold, 0);
assert_eq!(stats.finobj_minor_scan, 2);
registry.push_pending_unique(FinalizerCell::new(3));
registry.finish_minor_collection();
let stats = registry.stats();
assert_eq!(stats.finobj_new, 0);
assert_eq!(stats.finobj_survival, 1);
assert_eq!(stats.finobj_old1, 2);
assert_eq!(stats.finobj_reallyold, 0);
assert_eq!(stats.finobj_minor_scan, 1);
registry.finish_minor_collection();
let stats = registry.stats();
assert_eq!(stats.finobj_new, 0);
assert_eq!(stats.finobj_survival, 0);
assert_eq!(stats.finobj_old1, 1);
assert_eq!(stats.finobj_reallyold, 2);
assert_eq!(stats.finobj_minor_scan, 0);
}
#[test]
fn finalizer_registry_minor_snapshot_uses_cohort_boundaries() {
let mut registry = FinalizerRegistry::default();
registry.push_pending_unique(FinalizerCell::new(1));
registry.push_pending_unique(FinalizerCell::new(2));
registry.push_pending_unique(FinalizerCell::new(3));
registry.finish_minor_collection();
registry.finish_minor_collection();
registry.push_pending_unique(FinalizerCell::new(4));
registry.push_pending_unique(FinalizerCell::new(5));
assert_eq!(
finalizer_ids(®istry.pending_minor_snapshot()),
vec![4, 5],
"minor finalizer scan must skip the old1/reallyold prefix"
);
registry.push_to_be_finalized(FinalizerCell::new(99));
registry.promote_pending_to_finalized(vec![
FinalizerCell::new(1),
FinalizerCell::new(2),
FinalizerCell::new(4),
]);
let stats = registry.stats();
assert_eq!(stats.finobj_old1, 1);
assert_eq!(stats.finobj_new, 1);
assert_eq!(stats.finobj_minor_scan, 1);
assert_eq!(finalizer_ids(registry.pending()), vec![3, 5]);
assert_eq!(
finalizer_ids(registry.to_be_finalized()),
vec![99, 4, 2, 1],
"new to-be-finalized batches append behind older queued finalizers"
);
}
#[test]
fn finalizer_registry_marks_and_clears_finalized_bit() {
let mut registry = FinalizerRegistry::default();
let object = FinalizerCell::new(1);
assert!(!object.is_finalized());
registry.push_pending_unique(object.clone());
assert!(object.is_finalized());
registry.push_pending_unique(object.clone());
assert_eq!(registry.pending_len(), 1);
registry.promote_pending_to_finalized(vec![object.clone()]);
assert!(object.is_finalized());
assert_eq!(registry.pending_len(), 0);
assert_eq!(registry.to_be_finalized_len(), 1);
let popped = registry.pop_to_be_finalized().unwrap();
assert_eq!(popped.id, 1);
assert!(!object.is_finalized());
registry.push_pending_unique(object.clone());
assert!(object.is_finalized());
assert_eq!(registry.pending_len(), 1);
}
#[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);
}
fn list_len(heap: &Heap, mut cursor: Option<NonNull<GcBox<dyn Trace>>>) -> usize {
let mut count = 0usize;
while let Some(ptr) = cursor {
count += 1;
cursor = heap.header_from_ptr(ptr).next.get();
}
count
}
#[test]
fn finalizer_intrusive_lists_sweep_and_drop() {
let heap = Heap::new();
heap.unpause();
let _normal = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let finobj = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
let tobefnz = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
assert!(heap.move_allgc_to_finobj(finobj.as_trace_ptr()));
assert!(heap.move_allgc_to_finobj(tobefnz.as_trace_ptr()));
assert!(heap.move_finobj_to_tobefnz(tobefnz.as_trace_ptr()));
assert_eq!(list_len(&heap, heap.head.get()), 1);
assert_eq!(list_len(&heap, heap.finobj.get()), 1);
assert_eq!(list_len(&heap, heap.tobefnz.get()), 1);
assert_eq!(heap.allgc_count(), 3);
heap.full_collect(&TwoRoots { first: Some(finobj), second: Some(tobefnz) });
assert_eq!(list_len(&heap, heap.head.get()), 0);
assert_eq!(list_len(&heap, heap.finobj.get()), 1);
assert_eq!(list_len(&heap, heap.tobefnz.get()), 1);
assert_eq!(heap.allgc_count(), 2);
assert!(heap.move_tobefnz_to_allgc(tobefnz.as_trace_ptr()));
heap.full_collect(&OneRoot(Some(tobefnz)));
assert_eq!(list_len(&heap, heap.head.get()), 1);
assert_eq!(list_len(&heap, heap.finobj.get()), 0);
assert_eq!(list_len(&heap, heap.tobefnz.get()), 0);
assert_eq!(heap.allgc_count(), 1);
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_run_until_state_with_post_mark(
&OneRoot(None),
GcState::SweepAllGc,
1024,
|_| {},
);
assert_eq!(outcome, StepOutcome::InProgress);
assert_eq!(heap.gc_state(), GcState::SweepAllGc);
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 minor_collect_skips_untouched_old_root_scan_work() {
let heap = Heap::new();
heap.unpause();
let old_root = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
old_root.set_age(GcAge::Old);
old_root.set_color(Color::Black);
let young_root = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.minor_collect_with_post_mark(
&TwoRoots {
first: Some(old_root),
second: Some(young_root),
},
|_| {},
);
let stats = heap.last_mark_stats();
assert_eq!(stats.marked, 2);
assert_eq!(stats.marked_old, 1);
assert_eq!(stats.marked_young, 1);
assert_eq!(stats.traced, 1);
assert_eq!(stats.traced_old, 0);
assert_eq!(stats.traced_young, 1);
assert_eq!(old_root.marker_calls.get(), 0);
assert_eq!(young_root.marker_calls.get(), 1);
assert_eq!(old_root.age(), GcAge::Old);
assert_eq!(young_root.age(), GcAge::Survival);
}
#[test]
fn minor_collect_traces_touched_old_parent() {
let heap = Heap::new();
heap.unpause();
let old_root = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
old_root.set_age(GcAge::Old);
old_root.set_color(Color::Black);
let young_child = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
old_root.next.set(Some(young_child));
heap.generational_backward_barrier(old_root);
heap.minor_collect_with_post_mark(&OneRoot(Some(old_root)), |_| {});
let stats = heap.last_mark_stats();
assert_eq!(stats.marked, 2);
assert_eq!(stats.marked_old, 1);
assert_eq!(stats.marked_young, 1);
assert_eq!(stats.traced, 2);
assert_eq!(stats.traced_old, 1);
assert_eq!(stats.traced_young, 1);
assert_eq!(old_root.marker_calls.get(), 1);
assert_eq!(young_child.marker_calls.get(), 1);
assert_eq!(old_root.age(), GcAge::Touched2);
assert_eq!(young_child.age(), GcAge::Survival);
}
#[test]
fn minor_sweep_uses_generation_cursors_to_skip_old_tail() {
let heap = Heap::new();
heap.unpause();
let mut old_objects = Vec::new();
for _ in 0..64 {
old_objects.push(heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
}));
}
heap.promote_all_to_old();
let young_root = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.minor_collect_with_post_mark(&OneRoot(Some(young_root)), |_| {});
let stats = heap.last_sweep_stats();
assert_eq!(stats.visited, 1);
assert_eq!(stats.visited_young, 1);
assert_eq!(stats.visited_old, 0);
assert_eq!(heap.allgc_count(), old_objects.len() + 1);
assert_eq!(young_root.age(), GcAge::Survival);
}
#[test]
fn full_sweep_corrects_generation_cursors_when_cursor_object_is_freed() {
let heap = Heap::new();
heap.unpause();
let _old = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.promote_all_to_old();
assert!(heap.survival.get().is_some());
assert!(heap.old1.get().is_some());
assert!(heap.reallyold.get().is_some());
heap.full_collect(&OneRoot(None));
assert_eq!(heap.allgc_count(), 0);
assert_eq!(heap.survival.get(), None);
assert_eq!(heap.old1.get(), None);
assert_eq!(heap.reallyold.get(), None);
assert_eq!(heap.firstold1.get(), None);
assert_eq!(heap.last_sweep_stats().freed, 1);
}
#[test]
fn full_sweep_unlinks_freed_grayagain_entries() {
let heap = Heap::new();
heap.unpause();
let parent = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.promote_all_to_old();
heap.generational_backward_barrier(parent);
assert_eq!(heap.grayagain_count(), 1);
heap.full_collect(&OneRoot(None));
assert_eq!(heap.allgc_count(), 0);
assert_eq!(heap.grayagain_count(), 0);
let young = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.minor_collect_with_post_mark(&OneRoot(Some(young)), |_| {});
assert_eq!(young.age(), GcAge::Survival);
}
#[test]
fn grayagain_links_object_once() {
let heap = Heap::new();
heap.unpause();
let parent = 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);
heap.generational_backward_barrier(parent);
assert_eq!(heap.grayagain_count(), 1);
}
#[test]
fn grayagain_list_carries_old1_until_old() {
let heap = Heap::new();
heap.unpause();
let survivor = heap.allocate(Cell0 {
next: Cell::new(None),
marker_calls: Cell::new(0),
});
heap.minor_collect_with_post_mark(&OneRoot(Some(survivor)), |_| {});
assert_eq!(survivor.age(), GcAge::Survival);
heap.minor_collect_with_post_mark(&OneRoot(Some(survivor)), |_| {});
assert_eq!(survivor.age(), GcAge::Old1);
heap.minor_collect_with_post_mark(&OneRoot(None), |_| {});
assert_eq!(survivor.age(), GcAge::Old);
assert_eq!(heap.allgc_count(), 1);
}
#[test]
fn grayagain_list_carries_touched2_until_old() {
let heap = Heap::new();
heap.unpause();
let parent = 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);
heap.minor_collect_with_post_mark(&OneRoot(None), |_| {});
assert_eq!(parent.age(), GcAge::Touched2);
heap.minor_collect_with_post_mark(&OneRoot(None), |_| {});
assert_eq!(parent.age(), GcAge::Old);
assert_eq!(parent.marker_calls.get(), 2);
}
#[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::SweepAllGc,
1024,
|_| atomic_calls.set(atomic_calls.get() + 1),
);
assert_eq!(outcome, StepOutcome::InProgress);
assert_eq!(heap.gc_state(), GcState::SweepAllGc);
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().is_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);
}
}