pub mod allocator;
pub mod barrier;
pub mod fixup;
pub mod generations;
pub mod header;
pub mod marker;
pub mod platform;
pub mod ptr;
pub mod region;
pub mod relocator;
pub mod roots;
pub mod safepoint;
pub mod scheduler;
pub mod trap_handler;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use allocator::BumpAllocator;
use barrier::SatbBuffer;
use generations::GenerationalCollector;
use marker::{MarkPhase, Marker};
use safepoint::SafepointState;
use scheduler::{AdaptiveScheduler, CollectionType, HeapMetrics};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CollectResult {
InProgress,
Complete(SweepStats),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SweepStats {
pub bytes_collected: usize,
pub objects_collected: usize,
pub bytes_retained: usize,
}
#[cfg(feature = "gc-stress")]
#[derive(Debug, Clone)]
pub struct StressConfig {
pub collect_interval: usize,
}
#[cfg(feature = "gc-stress")]
impl Default for StressConfig {
fn default() -> Self {
Self {
collect_interval: 16,
}
}
}
pub struct GcHeap {
allocator: BumpAllocator,
marker: Marker,
generations: GenerationalCollector,
safepoint: SafepointState,
satb_buffer: SatbBuffer,
bytes_since_gc: AtomicUsize,
gc_threshold: usize,
stats: GcHeapStats,
scheduler: Option<AdaptiveScheduler>,
#[cfg(feature = "gc-stress")]
stress_counter: usize,
#[cfg(feature = "gc-stress")]
stress_interval: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct GcHeapStats {
pub collections: u64,
pub young_collections: u64,
pub old_collections: u64,
pub total_collected_bytes: u64,
pub total_collection_time: Duration,
pub peak_heap_bytes: usize,
pub current_heap_bytes: usize,
pub last_collection: Option<Instant>,
}
impl GcHeap {
pub fn new() -> Self {
Self::with_threshold(4 * 1024 * 1024) }
pub fn with_threshold(gc_threshold: usize) -> Self {
Self {
allocator: BumpAllocator::new(),
marker: Marker::new(),
generations: GenerationalCollector::new(),
safepoint: SafepointState::new(),
satb_buffer: SatbBuffer::new(256),
bytes_since_gc: AtomicUsize::new(0),
gc_threshold,
stats: GcHeapStats::default(),
scheduler: None,
#[cfg(feature = "gc-stress")]
stress_counter: 0,
#[cfg(feature = "gc-stress")]
stress_interval: None,
}
}
#[cfg(feature = "gc-stress")]
pub fn enable_stress(&mut self, config: StressConfig) {
self.stress_interval = Some(config.collect_interval);
self.stress_counter = 0;
}
#[cfg(feature = "gc-stress")]
pub fn alloc_stressed<T>(
&mut self,
value: T,
trace_roots: &mut dyn FnMut(&mut dyn FnMut(*mut u8)),
) -> *mut T {
if let Some(interval) = self.stress_interval {
self.stress_counter += 1;
if self.stress_counter % interval == 0 {
self.collect(trace_roots);
}
}
self.alloc(value)
}
pub fn alloc<T>(&self, value: T) -> *mut T {
let layout = std::alloc::Layout::new::<T>();
let ptr = self.allocator.alloc(layout);
let typed = ptr as *mut T;
unsafe { typed.write(value) };
self.bytes_since_gc.fetch_add(
layout.size() + std::mem::size_of::<header::GcHeader>(),
Ordering::Relaxed,
);
typed
}
pub fn alloc_raw(&self, layout: std::alloc::Layout) -> *mut u8 {
let ptr = self.allocator.alloc(layout);
self.bytes_since_gc.fetch_add(
layout.size() + std::mem::size_of::<header::GcHeader>(),
Ordering::Relaxed,
);
ptr
}
pub fn should_collect(&self) -> bool {
self.bytes_since_gc.load(Ordering::Relaxed) >= self.gc_threshold
}
pub fn collect(&mut self, trace_roots: &mut dyn FnMut(&mut dyn FnMut(*mut u8))) {
let start = Instant::now();
self.marker.reset();
trace_roots(&mut |ptr| {
self.marker.mark_root(ptr);
});
self.marker.mark_all();
let collected = self.allocator.sweep(&self.marker);
let elapsed = start.elapsed();
self.stats.collections += 1;
self.stats.total_collected_bytes += collected as u64;
self.stats.total_collection_time += elapsed;
self.stats.last_collection = Some(Instant::now());
self.bytes_since_gc.store(0, Ordering::Relaxed);
}
pub fn collect_incremental(
&mut self,
mark_budget: usize,
trace_roots: &mut dyn FnMut(&mut dyn FnMut(*mut u8)),
) -> CollectResult {
if self.marker.phase() == MarkPhase::Idle {
self.marker.start_marking();
trace_roots(&mut |ptr| {
self.marker.mark_root(ptr);
});
}
let worklist_empty = self.marker.mark_step(mark_budget);
if worklist_empty {
let terminated = self.marker.terminate_marking(&mut self.satb_buffer);
if terminated {
let sweep_stats = self.sweep_regions();
self.marker.finish_marking();
self.stats.collections += 1;
self.stats.total_collected_bytes += sweep_stats.bytes_collected as u64;
self.stats.last_collection = Some(Instant::now());
self.bytes_since_gc.store(0, Ordering::Relaxed);
return CollectResult::Complete(sweep_stats);
}
}
CollectResult::InProgress
}
#[inline(always)]
pub fn write_barrier(&mut self, old_ref: *mut u8) {
if self.marker.is_marking() {
self.satb_buffer.enqueue(old_ref);
}
}
#[inline(always)]
pub fn write_barrier_combined(&mut self, slot_addr: usize, _new_val: *mut u8) {
if let Some(ref mut ct) = self.generations.card_table_mut() {
ct.mark_dirty(slot_addr);
}
if self.marker.is_marking() {
let old_ptr = unsafe { *(slot_addr as *const *mut u8) };
self.satb_buffer.enqueue(old_ptr);
}
}
pub fn generations_mut(&mut self) -> &mut GenerationalCollector {
&mut self.generations
}
pub fn generations(&self) -> &GenerationalCollector {
&self.generations
}
fn sweep_regions(&self) -> SweepStats {
self.allocator.flush_tlab_for_sweep();
let regions = self.allocator.regions_mut();
let mut stats = SweepStats::default();
for region in regions.iter_mut() {
let mut live_bytes = 0;
region.for_each_object_mut(|hdr, _obj_ptr| {
if hdr.color() == header::GcColor::White {
stats.bytes_collected += hdr.size as usize;
stats.objects_collected += 1;
} else {
let size = hdr.size as usize;
live_bytes += size;
stats.bytes_retained += size;
hdr.set_color(header::GcColor::White);
}
});
region.set_live_bytes(live_bytes);
}
stats
}
pub fn is_marking(&self) -> bool {
self.marker.is_marking()
}
pub fn satb_buffer_mut(&mut self) -> &mut SatbBuffer {
&mut self.satb_buffer
}
pub fn mark_phase(&self) -> MarkPhase {
self.marker.phase()
}
pub fn stats(&self) -> &GcHeapStats {
&self.stats
}
pub fn safepoint(&self) -> &SafepointState {
&self.safepoint
}
pub fn allocator(&self) -> &BumpAllocator {
&self.allocator
}
pub fn heap_size(&self) -> usize {
self.allocator.total_region_bytes()
}
pub fn enable_adaptive_scheduling(&mut self) {
self.scheduler = Some(AdaptiveScheduler::new());
}
pub fn enable_adaptive_scheduling_with_config(
&mut self,
young_utilization_threshold: f64,
headroom_factor: f64,
) {
self.scheduler = Some(AdaptiveScheduler::with_config(
young_utilization_threshold,
headroom_factor,
));
}
pub fn scheduler(&self) -> Option<&AdaptiveScheduler> {
self.scheduler.as_ref()
}
pub fn scheduler_mut(&mut self) -> Option<&mut AdaptiveScheduler> {
self.scheduler.as_mut()
}
pub fn should_collect_adaptive(&self) -> CollectionType {
if let Some(ref sched) = self.scheduler {
let metrics = HeapMetrics {
young_utilization: self.generations.young_utilization(),
old_free_bytes: self.generations.old_free_bytes(),
bytes_since_gc: self.bytes_since_gc.load(Ordering::Relaxed),
gc_threshold: self.gc_threshold,
avg_gc_pause_secs: self.avg_gc_pause_secs(),
};
sched.should_collect(&metrics)
} else {
if self.bytes_since_gc.load(Ordering::Relaxed) >= self.gc_threshold {
CollectionType::Full
} else {
CollectionType::None
}
}
}
pub fn avg_gc_pause_secs(&self) -> f64 {
if self.stats.collections == 0 {
return 0.0;
}
self.stats.total_collection_time.as_secs_f64() / self.stats.collections as f64
}
pub fn collect_young(&mut self, roots: &[*mut u8]) -> SweepStats {
let start = Instant::now();
let stats = self.generations.collect_young(&mut self.marker, roots);
let elapsed = start.elapsed();
self.stats.collections += 1;
self.stats.young_collections += 1;
self.stats.total_collected_bytes += stats.bytes_collected as u64;
self.stats.total_collection_time += elapsed;
self.stats.last_collection = Some(Instant::now());
self.bytes_since_gc.store(0, Ordering::Relaxed);
if let Some(ref mut sched) = self.scheduler {
sched.record_young_gc(elapsed);
}
stats
}
pub fn collect_old(&mut self, roots: &[*mut u8]) -> SweepStats {
let start = Instant::now();
let stats = self.generations.collect_old(&mut self.marker, roots);
let elapsed = start.elapsed();
self.stats.collections += 1;
self.stats.old_collections += 1;
self.stats.total_collected_bytes += stats.bytes_collected as u64;
self.stats.total_collection_time += elapsed;
self.stats.last_collection = Some(Instant::now());
self.bytes_since_gc.store(0, Ordering::Relaxed);
if let Some(ref mut sched) = self.scheduler {
sched.record_old_gc(elapsed);
}
stats
}
pub fn record_allocation(&mut self, bytes: usize) {
if let Some(ref mut sched) = self.scheduler {
sched.record_allocation(bytes);
}
}
pub fn young_gen_utilization(&self) -> f64 {
self.generations.young_utilization()
}
pub fn old_gen_free_bytes(&self) -> usize {
self.generations.old_free_bytes()
}
}
impl Default for GcHeap {
fn default() -> Self {
Self::new()
}
}
thread_local! {
static THREAD_GC_HEAP: std::cell::Cell<*mut GcHeap> = const { std::cell::Cell::new(std::ptr::null_mut()) };
}
pub unsafe fn set_thread_gc_heap(heap: *mut GcHeap) {
THREAD_GC_HEAP.with(|cell| cell.set(heap));
}
pub fn thread_gc_heap() -> &'static GcHeap {
THREAD_GC_HEAP.with(|cell| {
let ptr = cell.get();
assert!(!ptr.is_null(), "GC heap not initialized for this thread");
unsafe { &*ptr }
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::header::{GcColor, GcHeader};
fn header_of(ptr: *mut u8) -> &'static GcHeader {
unsafe {
let header_ptr = ptr.sub(std::mem::size_of::<GcHeader>()) as *const GcHeader;
&*header_ptr
}
}
fn header_of_mut(ptr: *mut u8) -> &'static mut GcHeader {
unsafe {
let header_ptr = ptr.sub(std::mem::size_of::<GcHeader>()) as *mut GcHeader;
&mut *header_ptr
}
}
#[test]
fn test_alloc_returns_valid_pointer() {
let heap = GcHeap::new();
let ptr = heap.alloc(42u64);
assert!(!ptr.is_null());
let val = unsafe { *ptr };
assert_eq!(val, 42u64);
}
#[test]
fn test_alloc_multiple_distinct() {
let heap = GcHeap::new();
let a = heap.alloc(1u64);
let b = heap.alloc(2u64);
let c = heap.alloc(3u64);
assert_ne!(a, b);
assert_ne!(b, c);
assert_ne!(a, c);
unsafe {
assert_eq!(*a, 1);
assert_eq!(*b, 2);
assert_eq!(*c, 3);
}
}
#[test]
fn test_collect_with_empty_roots_clears_all() {
let mut heap = GcHeap::new();
let _a = heap.alloc(100u64);
let _b = heap.alloc(200u64);
heap.collect(&mut |_visitor| {
});
assert!(heap.stats().collections == 1);
assert!(heap.stats().total_collected_bytes > 0);
}
#[test]
fn test_live_object_survives_collection() {
let mut heap = GcHeap::new();
let ptr = heap.alloc(12345u64);
heap.collect(&mut |visitor| {
visitor(ptr as *mut u8);
});
let val = unsafe { *ptr };
assert_eq!(val, 12345);
let header = header_of(ptr as *mut u8);
assert_eq!(header.color(), GcColor::White);
}
#[test]
fn test_multiple_live_objects_survive() {
let mut heap = GcHeap::new();
let a = heap.alloc(111u64);
let b = heap.alloc(222u64);
let c = heap.alloc(333u64);
heap.collect(&mut |visitor| {
visitor(a as *mut u8);
visitor(b as *mut u8);
visitor(c as *mut u8);
});
unsafe {
assert_eq!(*a, 111);
assert_eq!(*b, 222);
assert_eq!(*c, 333);
}
}
#[test]
fn test_unreachable_objects_collected() {
let mut heap = GcHeap::with_threshold(1024); let live = heap.alloc(1u64);
let _dead1 = heap.alloc(2u64);
let _dead2 = heap.alloc(3u64);
heap.collect(&mut |visitor| {
visitor(live as *mut u8); });
let stats = heap.stats();
assert_eq!(stats.collections, 1);
assert!(stats.total_collected_bytes >= 2 * std::mem::size_of::<u64>() as u64);
assert_eq!(unsafe { *live }, 1);
}
#[test]
fn test_alloc_collect_cycle_repeated() {
let mut heap = GcHeap::with_threshold(1024);
for cycle in 0..5 {
let mut live_ptrs = Vec::new();
for i in 0..10u64 {
let ptr = heap.alloc(cycle * 100 + i);
live_ptrs.push(ptr);
}
for _ in 0..10 {
let _ = heap.alloc(0xDEADu64);
}
heap.collect(&mut |visitor| {
for ptr in &live_ptrs {
visitor(*ptr as *mut u8);
}
});
for (i, ptr) in live_ptrs.iter().enumerate() {
let val = unsafe { **ptr };
assert_eq!(val, cycle * 100 + i as u64);
}
}
let stats = heap.stats();
assert_eq!(stats.collections, 5);
}
#[test]
fn test_should_collect_threshold() {
let heap = GcHeap::with_threshold(256);
assert!(!heap.should_collect());
for _ in 0..20 {
let _ = heap.alloc([0u8; 64]);
}
assert!(heap.should_collect());
}
#[test]
fn test_collect_resets_bytes_counter() {
let mut heap = GcHeap::with_threshold(256);
for _ in 0..20 {
let _ = heap.alloc([0u8; 64]);
}
assert!(heap.should_collect());
heap.collect(&mut |_visitor| {});
assert!(!heap.should_collect());
}
#[test]
fn test_large_allocation() {
let heap = GcHeap::new();
let ptr = heap.alloc([0u8; 64 * 1024]);
assert!(!ptr.is_null());
let val = unsafe { &*ptr };
assert_eq!(val.len(), 64 * 1024);
}
#[test]
fn test_stats_initial_state() {
let heap = GcHeap::new();
let stats = heap.stats();
assert_eq!(stats.collections, 0);
assert_eq!(stats.total_collected_bytes, 0);
assert!(stats.last_collection.is_none());
}
#[test]
fn test_stats_after_collection() {
let mut heap = GcHeap::new();
let _dead = heap.alloc(42u64);
heap.collect(&mut |_| {});
let stats = heap.stats();
assert_eq!(stats.collections, 1);
assert!(stats.last_collection.is_some());
}
#[test]
fn test_thread_local_gc_heap() {
let mut heap = GcHeap::new();
unsafe { set_thread_gc_heap(&mut heap as *mut _) };
let tl = thread_gc_heap();
let ptr = tl.alloc(999u64);
assert!(!ptr.is_null());
assert_eq!(unsafe { *ptr }, 999);
unsafe { set_thread_gc_heap(std::ptr::null_mut()) };
}
#[test]
fn test_partial_liveness() {
let mut heap = GcHeap::new();
let mut ptrs = Vec::new();
for i in 0..20u64 {
ptrs.push(heap.alloc(i));
}
let live_ptrs: Vec<*mut u64> = ptrs.iter().copied().step_by(2).collect();
heap.collect(&mut |visitor| {
for ptr in &live_ptrs {
visitor(*ptr as *mut u8);
}
});
for (idx, ptr) in live_ptrs.iter().enumerate() {
let val = unsafe { **ptr };
assert_eq!(val, (idx * 2) as u64);
}
let stats = heap.stats();
assert!(stats.total_collected_bytes >= 10 * std::mem::size_of::<u64>() as u64);
}
#[test]
fn test_double_collection_safe() {
let mut heap = GcHeap::new();
let ptr = heap.alloc(42u64);
heap.collect(&mut |visitor| {
visitor(ptr as *mut u8);
});
assert_eq!(unsafe { *ptr }, 42);
heap.collect(&mut |visitor| {
visitor(ptr as *mut u8);
});
assert_eq!(unsafe { *ptr }, 42);
assert_eq!(heap.stats().collections, 2);
}
#[test]
fn test_heap_size_grows_with_allocation() {
let heap = GcHeap::new();
let size_before = heap.heap_size();
let _ = heap.alloc(0u64);
let size_after = heap.heap_size();
assert!(size_after >= size_before);
assert!(size_after >= crate::region::REGION_SIZE);
}
#[test]
fn test_trace_nanboxed_bits_heap_tag() {
use crate::roots::trace_nanboxed_bits;
let fake_ptr: u64 = 0x0000_1234_5678_0000;
let tagged = 0xFFF8_0000_0000_0000u64 | fake_ptr;
let mut found_ptrs = Vec::new();
trace_nanboxed_bits(tagged, &mut |ptr| {
found_ptrs.push(ptr as u64);
});
assert_eq!(found_ptrs.len(), 1);
assert_eq!(found_ptrs[0], fake_ptr);
}
#[test]
fn test_trace_nanboxed_bits_int_tag_is_noop() {
use crate::roots::trace_nanboxed_bits;
let int_tagged = 0xFFF8_0000_0000_0000u64 | (0b001u64 << 48) | 42;
let mut found = false;
trace_nanboxed_bits(int_tagged, &mut |_| {
found = true;
});
assert!(!found);
}
#[cfg(feature = "gc-stress")]
#[test]
fn test_stress_mode_triggers_collection() {
let mut heap = GcHeap::with_threshold(1024 * 1024); heap.enable_stress(StressConfig {
collect_interval: 4,
});
let mut root: Option<*mut u64> = None;
for i in 0..12u64 {
let ptr = heap.alloc_stressed(i, &mut |visitor| {
if let Some(r) = root {
visitor(r as *mut u8);
}
});
root = Some(ptr);
}
assert_eq!(heap.stats().collections, 3);
}
#[cfg(feature = "gc-stress")]
#[test]
fn test_stress_mode_live_objects_survive() {
let mut heap = GcHeap::with_threshold(1024 * 1024);
heap.enable_stress(StressConfig {
collect_interval: 2,
});
let root = heap.alloc(42u64);
for i in 0..10u64 {
let _garbage = heap.alloc_stressed(i * 100, &mut |visitor| {
visitor(root as *mut u8);
});
}
assert_eq!(unsafe { *root }, 42);
assert!(heap.stats().collections >= 5); }
#[test]
fn test_incremental_marking_small_budget() {
let mut heap = GcHeap::new();
let mut ptrs = Vec::new();
for i in 0..10u64 {
ptrs.push(heap.alloc(i));
}
let ptrs_clone = ptrs.clone();
let mut steps = 0;
loop {
let result = heap.collect_incremental(2, &mut |visitor| {
for &ptr in &ptrs_clone {
visitor(ptr as *mut u8);
}
});
steps += 1;
if let CollectResult::Complete(_stats) = result {
break;
}
assert!(steps < 100, "incremental marking did not converge");
}
assert!(steps >= 2, "expected multiple steps, got {}", steps);
for (i, &ptr) in ptrs.iter().enumerate() {
let val = unsafe { *ptr };
assert_eq!(val, i as u64);
}
assert_eq!(heap.stats().collections, 1);
}
#[test]
fn test_satb_barrier_captures_during_marking() {
let mut heap = GcHeap::new();
let live = heap.alloc(100u64);
let overwritten = heap.alloc(200u64);
let first = heap.collect_incremental(0, &mut |visitor| {
visitor(live as *mut u8);
});
assert_eq!(first, CollectResult::InProgress);
assert!(heap.is_marking());
heap.write_barrier(overwritten as *mut u8);
loop {
let result = heap.collect_incremental(100, &mut |visitor| {
visitor(live as *mut u8);
});
if let CollectResult::Complete(stats) = result {
assert_eq!(
stats.objects_collected, 0,
"SATB should have saved the overwritten reference"
);
break;
}
}
}
#[test]
fn test_mark_termination_drains_satb() {
let mut heap = GcHeap::new();
let root = heap.alloc(1u64);
let saved = heap.alloc(2u64);
let _ = heap.collect_incremental(0, &mut |visitor| {
visitor(root as *mut u8);
});
heap.satb_buffer_mut().enqueue(saved as *mut u8);
loop {
let result = heap.collect_incremental(100, &mut |visitor| {
visitor(root as *mut u8);
});
if let CollectResult::Complete(_) = result {
break;
}
}
assert_eq!(unsafe { *root }, 1);
assert_eq!(unsafe { *saved }, 2);
}
#[test]
fn test_incremental_sweep_preserves_live() {
let mut heap = GcHeap::new();
let live = heap.alloc(42u64);
let _dead = heap.alloc(99u64);
loop {
let result = heap.collect_incremental(100, &mut |visitor| {
visitor(live as *mut u8);
});
if let CollectResult::Complete(stats) = result {
assert!(stats.bytes_collected > 0);
assert_eq!(stats.objects_collected, 1);
break;
}
}
assert_eq!(unsafe { *live }, 42);
}
#[test]
fn test_full_incremental_cycle() {
let mut heap = GcHeap::new();
let mut live_ptrs = Vec::new();
for i in 0..5u64 {
live_ptrs.push(heap.alloc(i * 10));
}
for _ in 0..5 {
let _ = heap.alloc(0xDEADu64);
}
let roots = live_ptrs.clone();
let result = loop {
let r = heap.collect_incremental(2, &mut |visitor| {
for &ptr in &roots {
visitor(ptr as *mut u8);
}
});
if let CollectResult::Complete(stats) = r {
break stats;
}
};
assert_eq!(result.objects_collected, 5);
assert!(result.bytes_collected >= 5 * std::mem::size_of::<u64>());
assert!(result.bytes_retained >= 5 * std::mem::size_of::<u64>());
for (i, &ptr) in live_ptrs.iter().enumerate() {
assert_eq!(unsafe { *ptr }, (i as u64) * 10);
}
assert_eq!(heap.stats().collections, 1);
assert!(!heap.is_marking());
assert_eq!(heap.mark_phase(), MarkPhase::Idle);
}
#[test]
fn test_write_barrier_noop_when_not_marking() {
let mut heap = GcHeap::new();
assert!(!heap.is_marking());
heap.write_barrier(0x1000 as *mut u8);
assert!(heap.satb_buffer_mut().is_empty());
}
#[test]
fn test_repeated_incremental_cycles() {
let mut heap = GcHeap::new();
let root = heap.alloc(42u64);
for cycle in 0..3 {
for _ in 0..5 {
let _ = heap.alloc(0xDEADu64);
}
loop {
let result = heap.collect_incremental(10, &mut |visitor| {
visitor(root as *mut u8);
});
if let CollectResult::Complete(_) = result {
break;
}
}
assert_eq!(heap.stats().collections, cycle + 1);
assert_eq!(unsafe { *root }, 42);
}
}
#[test]
fn test_combined_write_barrier_card_table_dirty() {
let mut heap = GcHeap::new();
let base_addr = 0x1_0000usize;
let size = 4096;
heap.generations_mut().init_card_table(base_addr, size);
let slot_addr = base_addr + 100;
assert!(!heap.generations().card_table().unwrap().is_dirty(slot_addr));
heap.write_barrier_combined(slot_addr, 0xBEEF as *mut u8);
assert!(heap.generations().card_table().unwrap().is_dirty(slot_addr));
}
#[test]
fn test_combined_write_barrier_satb_enqueue_during_marking() {
let mut heap = GcHeap::new();
let live = heap.alloc(10u64);
let _ = heap.collect_incremental(0, &mut |visitor| {
visitor(live as *mut u8);
});
assert!(heap.is_marking());
let mut slot: *mut u8 = 0xABCD_0000 as *mut u8;
let slot_addr = &mut slot as *mut *mut u8 as usize;
heap.write_barrier_combined(slot_addr, 0x5678 as *mut u8);
assert!(!heap.satb_buffer_mut().is_empty());
let drained = heap.satb_buffer_mut().drain();
assert_eq!(drained.len(), 1);
assert_eq!(drained[0], 0xABCD_0000 as *mut u8);
}
#[test]
fn test_combined_write_barrier_satb_noop_when_not_marking() {
let mut heap = GcHeap::new();
assert!(!heap.is_marking());
let base_addr = 0x2_0000usize;
heap.generations_mut().init_card_table(base_addr, 4096);
let slot_addr = base_addr + 256;
heap.write_barrier_combined(slot_addr, 0x1234 as *mut u8);
assert!(heap.generations().card_table().unwrap().is_dirty(slot_addr));
assert!(heap.satb_buffer_mut().is_empty());
}
#[test]
fn test_combined_write_barrier_no_card_table() {
let mut heap = GcHeap::new();
let slot_addr = 0x3_0000usize;
heap.write_barrier_combined(slot_addr, 0x1234 as *mut u8);
assert!(heap.generations().card_table().is_none());
}
#[test]
fn test_combined_write_barrier_both_card_and_satb() {
let mut heap = GcHeap::new();
let live = heap.alloc(42u64);
let _ = heap.collect_incremental(0, &mut |visitor| {
visitor(live as *mut u8);
});
assert!(heap.is_marking());
let mut slot: *mut u8 = 0xDEAD_BEEF as *mut u8;
let slot_addr = &mut slot as *mut *mut u8 as usize;
let card_base = slot_addr & !0xFFF; heap.generations_mut().init_card_table(card_base, 8192);
heap.write_barrier_combined(slot_addr, 0x1111 as *mut u8);
assert!(!heap.satb_buffer_mut().is_empty());
let drained = heap.satb_buffer_mut().drain();
assert_eq!(drained[0], 0xDEAD_BEEF as *mut u8);
assert!(heap.generations().card_table().unwrap().is_dirty(slot_addr));
}
}