use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use crate::gc::heap::{Heap, OldSpace};
use crate::gc::trace::{Tracer, trace_heap_object};
use crate::objects::heap_object::HeapObject;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkColour {
White,
Grey,
Black,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GcPhase {
Idle,
Marking,
Sweeping,
Compacting,
}
#[derive(Debug, Clone)]
pub struct GcMetrics {
pub heap_size_bytes: usize,
pub last_pause_us: u64,
pub minor_gc_count: u64,
pub major_gc_count: u64,
pub incremental_steps: u64,
}
impl GcMetrics {
pub fn new() -> Self {
Self {
heap_size_bytes: 0,
last_pause_us: 0,
minor_gc_count: 0,
major_gc_count: 0,
incremental_steps: 0,
}
}
}
impl Default for GcMetrics {
fn default() -> Self {
Self::new()
}
}
pub type SweepResult = Vec<(usize, usize)>;
const DEFAULT_MARK_BUDGET: usize = 256;
pub struct IncrementalGc {
phase: GcPhase,
mark_table: HashSet<usize>,
grey_stack: Vec<*mut HeapObject>,
black_set: HashSet<usize>,
mark_budget: usize,
sweep_result: Arc<Mutex<Option<SweepResult>>>,
metrics: GcMetrics,
}
unsafe impl Send for IncrementalGc {}
impl IncrementalGc {
pub fn new() -> Self {
Self {
phase: GcPhase::Idle,
mark_table: HashSet::new(),
grey_stack: Vec::new(),
black_set: HashSet::new(),
mark_budget: DEFAULT_MARK_BUDGET,
sweep_result: Arc::new(Mutex::new(None)),
metrics: GcMetrics::new(),
}
}
pub fn with_budget(budget: usize) -> Self {
let mut gc = Self::new();
gc.mark_budget = budget;
gc
}
pub fn phase(&self) -> GcPhase {
self.phase
}
pub fn metrics(&self) -> &GcMetrics {
&self.metrics
}
pub fn colour_of(&self, ptr: *mut HeapObject) -> MarkColour {
let addr = ptr as usize;
if self.black_set.contains(&addr) {
MarkColour::Black
} else if self.mark_table.contains(&addr) {
MarkColour::Grey
} else {
MarkColour::White
}
}
pub unsafe fn write_barrier(&mut self, host: *mut HeapObject, old_target: *mut HeapObject) {
if self.phase != GcPhase::Marking {
return;
}
if host.is_null() || old_target.is_null() {
return;
}
if self.black_set.contains(&(host as usize))
&& !self.mark_table.contains(&(old_target as usize))
&& !self.black_set.contains(&(old_target as usize))
{
self.shade_grey(old_target);
}
}
pub unsafe fn start_marking(&mut self, roots: &[*mut *mut HeapObject], old_space: &OldSpace) {
self.phase = GcPhase::Marking;
self.mark_table.clear();
self.grey_stack.clear();
self.black_set.clear();
for &slot in roots {
let ptr = unsafe { *slot };
if ptr.is_null() {
continue;
}
if old_space.contains(ptr as *mut u8) {
self.shade_grey(ptr);
}
}
}
pub unsafe fn mark_step(&mut self) -> bool {
if self.phase != GcPhase::Marking {
return true;
}
let start = Instant::now();
let mut budget = self.mark_budget;
while budget > 0 {
let Some(obj) = self.grey_stack.pop() else {
break;
};
let addr = obj as usize;
if self.black_set.contains(&addr) {
continue;
}
self.mark_table.remove(&addr);
self.black_set.insert(addr);
let mut tracer = Tracer::new();
unsafe { trace_heap_object(obj, &mut tracer) };
for child_raw in tracer.gray_stack {
let child_addr = child_raw as usize;
if !self.black_set.contains(&child_addr) && !self.mark_table.contains(&child_addr) {
self.mark_table.insert(child_addr);
self.grey_stack.push(child_raw as *mut HeapObject);
}
}
budget -= 1;
}
let elapsed = start.elapsed();
self.metrics.last_pause_us = elapsed.as_micros() as u64;
self.metrics.incremental_steps += 1;
let done = self.grey_stack.is_empty();
if done {
self.phase = GcPhase::Sweeping;
}
done
}
pub unsafe fn finish_marking(&mut self) {
while !unsafe { self.mark_step() } {}
}
pub unsafe fn sweep_sync(&self, old_space: &OldSpace) -> SweepResult {
let base = old_space.base_ptr();
let used = old_space.used();
let mut free_regions = Vec::new();
let mut offset = 0usize;
while offset < used {
let obj_ptr = unsafe { base.add(offset) } as *mut HeapObject;
let size = unsafe { (*obj_ptr).alloc_size() } as usize;
if size == 0 {
break;
}
if !self.black_set.contains(&(obj_ptr as usize)) {
free_regions.push((offset, size));
}
offset += size;
}
free_regions
}
pub unsafe fn launch_concurrent_sweep(
&mut self,
old_base: *mut u8,
old_used: usize,
black_set: HashSet<usize>,
) {
let result_handle = Arc::clone(&self.sweep_result);
let base_addr = old_base as usize;
std::thread::spawn(move || {
let mut free_regions = Vec::new();
let mut offset = 0usize;
while offset < old_used {
let obj_addr = base_addr + offset;
let obj_ptr = obj_addr as *mut HeapObject;
let size = unsafe { (*obj_ptr).alloc_size() } as usize;
if size == 0 {
break;
}
if !black_set.contains(&obj_addr) {
free_regions.push((offset, size));
}
offset += size;
}
if let Ok(mut guard) = result_handle.lock() {
*guard = Some(free_regions);
}
});
}
pub fn take_sweep_result(&mut self) -> Option<SweepResult> {
if let Ok(mut guard) = self.sweep_result.lock() {
guard.take()
} else {
None
}
}
pub fn minor_gc(&mut self, heap: &mut Heap) {
let start = Instant::now();
heap.collect();
let elapsed = start.elapsed();
self.metrics.last_pause_us = elapsed.as_micros() as u64;
self.metrics.minor_gc_count += 1;
self.update_heap_size(heap);
}
pub unsafe fn major_gc(&mut self, heap: &mut Heap, roots: &[*mut *mut HeapObject]) {
let start = Instant::now();
unsafe { self.start_marking(roots, &heap.old_space) };
unsafe { self.finish_marking() };
let _free_regions = unsafe { self.sweep_sync(&heap.old_space) };
self.phase = GcPhase::Idle;
let elapsed = start.elapsed();
self.metrics.last_pause_us = elapsed.as_micros() as u64;
self.metrics.major_gc_count += 1;
self.update_heap_size(heap);
}
pub unsafe fn notify_idle(&mut self, heap: &mut Heap, _deadline_us: u64) -> bool {
match self.phase {
GcPhase::Marking => {
unsafe { self.mark_step() };
true
}
GcPhase::Idle => {
let young_usage = heap.young_space.used() as f64;
let young_cap = heap.young_space.capacity() as f64;
if young_cap > 0.0 && young_usage / young_cap > 0.5 {
self.minor_gc(heap);
true
} else {
false
}
}
_ => false,
}
}
fn shade_grey(&mut self, ptr: *mut HeapObject) {
let addr = ptr as usize;
if self.mark_table.insert(addr) {
self.grey_stack.push(ptr);
}
}
fn update_heap_size(&mut self, heap: &Heap) {
self.metrics.heap_size_bytes = heap.young_space.used() + heap.old_space.used();
}
pub fn reset(&mut self) {
self.phase = GcPhase::Idle;
self.mark_table.clear();
self.grey_stack.clear();
self.black_set.clear();
}
}
impl Default for IncrementalGc {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gc::heap::{Heap, OldSpace};
use crate::objects::heap_object::HeapObject;
use std::alloc::Layout;
fn alloc_in_old(old: &mut OldSpace) -> *mut HeapObject {
let layout = Layout::new::<HeapObject>();
let raw = old.bump_alloc(layout).expect("old space has free space");
unsafe { std::ptr::write_bytes(raw, 0, layout.size()) };
let obj = raw as *mut HeapObject;
unsafe { (*obj).init_alloc_size(layout.size() as u32) };
obj
}
#[test]
fn test_new_starts_idle() {
let gc = IncrementalGc::new();
assert_eq!(gc.phase(), GcPhase::Idle);
}
#[test]
fn test_start_marking_transitions_to_marking() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
assert_eq!(gc.phase(), GcPhase::Marking);
}
#[test]
fn test_mark_step_completes_and_transitions_to_sweeping() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
let done = unsafe { gc.mark_step() };
assert!(done, "single root must be fully processed in one step");
assert_eq!(gc.phase(), GcPhase::Sweeping);
}
#[test]
fn test_colour_of_unmarked_is_white() {
let gc = IncrementalGc::new();
let mut obj = HeapObject::new_null();
assert_eq!(gc.colour_of(&raw mut obj), MarkColour::White);
}
#[test]
fn test_start_marking_shades_roots_grey() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
assert_eq!(gc.colour_of(obj), MarkColour::Grey);
}
#[test]
fn test_mark_step_promotes_grey_to_black() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
unsafe { gc.mark_step() };
assert_eq!(gc.colour_of(obj), MarkColour::Black);
}
#[test]
fn test_write_barrier_noop_when_idle() {
let mut gc = IncrementalGc::new();
let mut a = HeapObject::new_null();
let mut b = HeapObject::new_null();
unsafe { gc.write_barrier(&raw mut a, &raw mut b) };
assert_eq!(gc.colour_of(&raw mut b), MarkColour::White);
}
#[test]
fn test_write_barrier_shades_old_target_grey_when_host_is_black() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let host = alloc_in_old(&mut old);
let old_target = alloc_in_old(&mut old);
let mut root: *mut HeapObject = host;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
unsafe { gc.mark_step() };
assert_eq!(gc.phase(), GcPhase::Sweeping);
gc.phase = GcPhase::Marking;
assert_eq!(gc.colour_of(host), MarkColour::Black);
assert_eq!(gc.colour_of(old_target), MarkColour::White);
unsafe { gc.write_barrier(host, old_target) };
assert_eq!(
gc.colour_of(old_target),
MarkColour::Grey,
"write barrier must shade the white old target grey"
);
}
#[test]
fn test_write_barrier_no_shade_when_host_is_grey() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let host = alloc_in_old(&mut old);
let old_target = alloc_in_old(&mut old);
let mut root: *mut HeapObject = host;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
assert_eq!(gc.colour_of(host), MarkColour::Grey);
unsafe { gc.write_barrier(host, old_target) };
assert_eq!(
gc.colour_of(old_target),
MarkColour::White,
"barrier must not shade when host is grey"
);
}
#[test]
fn test_mark_step_respects_budget() {
let mut gc = IncrementalGc::with_budget(1);
let mut old = OldSpace::new(65536);
let obj1 = alloc_in_old(&mut old);
let obj2 = alloc_in_old(&mut old);
let mut r1: *mut HeapObject = obj1;
let mut r2: *mut HeapObject = obj2;
let roots = [
&raw mut r1 as *mut *mut HeapObject,
&raw mut r2 as *mut *mut HeapObject,
];
unsafe { gc.start_marking(&roots, &old) };
let done = unsafe { gc.mark_step() };
assert!(!done, "budget=1 must not finish 2 roots in one step");
let done = unsafe { gc.mark_step() };
assert!(done, "second step must drain the remaining grey object");
}
#[test]
fn test_sweep_sync_identifies_dead_objects() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let live = alloc_in_old(&mut old);
let _dead = alloc_in_old(&mut old);
let mut root: *mut HeapObject = live;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
unsafe { gc.finish_marking() };
let free = unsafe { gc.sweep_sync(&old) };
assert_eq!(free.len(), 1, "one dead object → one free region");
}
#[test]
fn test_sweep_sync_no_dead_when_all_rooted() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj1 = alloc_in_old(&mut old);
let obj2 = alloc_in_old(&mut old);
let mut r1: *mut HeapObject = obj1;
let mut r2: *mut HeapObject = obj2;
let roots = [
&raw mut r1 as *mut *mut HeapObject,
&raw mut r2 as *mut *mut HeapObject,
];
unsafe { gc.start_marking(&roots, &old) };
unsafe { gc.finish_marking() };
let free = unsafe { gc.sweep_sync(&old) };
assert!(free.is_empty(), "all rooted → no dead objects");
}
#[test]
fn test_concurrent_sweep_produces_result() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let live = alloc_in_old(&mut old);
let _dead = alloc_in_old(&mut old);
let mut root: *mut HeapObject = live;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
unsafe { gc.finish_marking() };
let black_set = gc.black_set.clone();
unsafe {
gc.launch_concurrent_sweep(old.base_ptr(), old.used(), black_set);
}
let mut result = None;
for _ in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(10));
if let Some(r) = gc.take_sweep_result() {
result = Some(r);
break;
}
}
let free = result.expect("concurrent sweep must produce a result");
assert_eq!(free.len(), 1, "one dead object → one free region");
}
#[test]
fn test_minor_gc_increments_counter() {
let mut gc = IncrementalGc::new();
let mut heap = Heap::new();
let layout = Layout::new::<HeapObject>();
heap.allocate(layout);
gc.minor_gc(&mut heap);
assert_eq!(gc.metrics().minor_gc_count, 1);
assert_eq!(heap.young_space.used(), 0, "minor GC must clear nursery");
}
#[test]
fn test_major_gc_increments_counter() {
let mut gc = IncrementalGc::new();
let mut heap = Heap::new();
let roots: [*mut *mut HeapObject; 0] = [];
unsafe { gc.major_gc(&mut heap, &roots) };
assert_eq!(gc.metrics().major_gc_count, 1);
assert_eq!(gc.phase(), GcPhase::Idle);
}
#[test]
fn test_notify_idle_triggers_minor_gc_when_nursery_half_full() {
let mut gc = IncrementalGc::new();
let mut heap = Heap::new();
let layout = Layout::new::<HeapObject>();
let obj_size = layout
.align_to(std::mem::align_of::<HeapObject>())
.unwrap()
.pad_to_align()
.size();
let half_cap = heap.young_space.capacity() / 2;
let count = (half_cap / obj_size) + 1;
for _ in 0..count {
heap.allocate(layout);
}
assert!(heap.young_space.used() > half_cap);
let did_work = unsafe { gc.notify_idle(&mut heap, 1_000) };
assert!(did_work, "idle notification must trigger minor GC");
assert_eq!(gc.metrics().minor_gc_count, 1);
}
#[test]
fn test_notify_idle_does_nothing_when_nursery_empty() {
let mut gc = IncrementalGc::new();
let mut heap = Heap::new();
let did_work = unsafe { gc.notify_idle(&mut heap, 1_000) };
assert!(!did_work, "idle must be no-op when nursery is empty");
}
#[test]
fn test_notify_idle_performs_mark_step_during_marking() {
let mut gc = IncrementalGc::with_budget(1);
let mut heap = Heap::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
assert_eq!(gc.phase(), GcPhase::Marking);
let did_work = unsafe { gc.notify_idle(&mut heap, 1_000) };
assert!(did_work, "idle must perform a mark step during marking");
assert!(gc.metrics().incremental_steps > 0);
}
#[test]
fn test_metrics_default_zeroed() {
let m = GcMetrics::new();
assert_eq!(m.heap_size_bytes, 0);
assert_eq!(m.last_pause_us, 0);
assert_eq!(m.minor_gc_count, 0);
assert_eq!(m.major_gc_count, 0);
assert_eq!(m.incremental_steps, 0);
}
#[test]
fn test_incremental_steps_counted() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
unsafe { gc.finish_marking() };
assert!(
gc.metrics().incremental_steps >= 1,
"at least one step must have been counted"
);
}
#[test]
fn test_reset_returns_to_idle() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { gc.start_marking(&roots, &old) };
gc.reset();
assert_eq!(gc.phase(), GcPhase::Idle);
}
#[test]
fn test_stress_allocate_mark_sweep_many_objects() {
let mut gc = IncrementalGc::new();
let mut old = OldSpace::new(1024 * 1024);
let mut ptrs = Vec::new();
for _ in 0..1000 {
ptrs.push(alloc_in_old(&mut old));
}
let mut roots_raw: Vec<*mut HeapObject> = ptrs.iter().step_by(2).copied().collect();
let root_slots: Vec<*mut *mut HeapObject> = roots_raw
.iter_mut()
.map(|p| p as *mut *mut HeapObject)
.collect();
unsafe { gc.start_marking(&root_slots, &old) };
unsafe { gc.finish_marking() };
let free = unsafe { gc.sweep_sync(&old) };
assert_eq!(free.len(), 500, "500 unrooted objects must be dead");
}
}