use std::alloc::{self, Layout};
use std::cell::RefCell;
use std::ptr::{self, NonNull};
#[cfg(not(feature = "no-gc"))]
use crate::gc_header::GcBoxHeader;
use crate::{GcBox, GcPtr, Trace};
const DEFAULT_CHUNK_SIZE: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegionLimitExceeded {
pub limit: usize,
pub used: usize,
pub requested: usize,
}
struct Chunk {
data: NonNull<u8>,
layout: Layout,
}
impl Chunk {
fn new(size: usize, align: usize) -> Self {
let layout =
Layout::from_size_align(size, align.max(16)).expect("Region: invalid chunk layout");
let data =
unsafe { NonNull::new(alloc::alloc(layout)).expect("Region: chunk allocation failed") };
Self { data, layout }
}
}
struct DropEntry {
ptr: *mut u8,
drop_fn: unsafe fn(*mut u8),
}
unsafe fn drop_gcbox_in_place<T: Trace + 'static>(ptr: *mut u8) {
unsafe { ptr::drop_in_place(ptr as *mut GcBox<T>) };
}
pub struct Region {
chunks: Vec<Chunk>,
ptr: usize,
end: usize,
drops: Vec<DropEntry>,
bytes_used: usize,
accounted_bytes: usize,
byte_limit: Option<usize>,
object_count: usize,
}
impl Region {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CHUNK_SIZE)
}
pub fn with_capacity(cap: usize) -> Self {
let cap = cap.max(64); let chunk = Chunk::new(cap, 16);
let base = chunk.data.as_ptr() as usize;
Self {
chunks: vec![chunk],
ptr: base,
end: base + cap,
drops: Vec::new(),
bytes_used: 0,
accounted_bytes: 0,
byte_limit: None,
object_count: 0,
}
}
pub fn with_limit(limit: usize) -> Self {
assert!(limit >= 64, "Region: byte limit must be at least 64");
let chunk = Chunk::new(limit, 16);
let base = chunk.data.as_ptr() as usize;
Self {
chunks: vec![chunk],
ptr: base,
end: base + limit,
drops: Vec::new(),
bytes_used: 0,
accounted_bytes: 0,
byte_limit: Some(limit),
object_count: 0,
}
}
pub fn alloc<T: Trace + 'static>(&mut self, value: T) -> GcPtr<T> {
let layout = Layout::new::<GcBox<T>>();
let requested = layout.size().saturating_add(value.gc_size_extra());
self.charge(requested);
let raw = self.bump_alloc(layout);
let gc_box = raw as *mut GcBox<T>;
#[cfg(not(feature = "no-gc"))]
unsafe {
ptr::write(
gc_box,
GcBox {
header: GcBoxHeader::new::<T>(0),
value,
},
);
}
#[cfg(feature = "no-gc")]
unsafe {
ptr::write(gc_box, GcBox { value });
}
self.drops.push(DropEntry {
ptr: raw,
drop_fn: drop_gcbox_in_place::<T>,
});
self.object_count += 1;
crate::stats::GC_STATS.record_region_alloc(layout.size());
#[cfg(not(feature = "no-gc"))]
{
unsafe { GcPtr::from_region_raw(gc_box) }
}
#[cfg(feature = "no-gc")]
{
GcPtr(unsafe { NonNull::new_unchecked(gc_box) })
}
}
#[cfg(not(feature = "no-gc"))]
pub(crate) fn trace_live(&self, visitor: &mut crate::MarkVisitor) {
for entry in &self.drops {
let header = entry.ptr as *const GcBoxHeader;
unsafe { ((*header).trace_fn)(header, visitor) };
}
}
pub fn reset(&mut self) {
for entry in self.drops.drain(..).rev() {
unsafe { (entry.drop_fn)(entry.ptr) };
}
while self.chunks.len() > 1 {
let chunk = self.chunks.pop().unwrap();
unsafe { alloc::dealloc(chunk.data.as_ptr(), chunk.layout) };
}
if let Some(first) = self.chunks.first() {
let base = first.data.as_ptr() as usize;
self.ptr = base;
self.end = base + first.layout.size();
}
self.bytes_used = 0;
self.accounted_bytes = 0;
self.object_count = 0;
}
pub fn bytes_used(&self) -> usize {
self.bytes_used
}
pub fn object_count(&self) -> usize {
self.object_count
}
pub fn accounted_bytes(&self) -> usize {
self.accounted_bytes
}
pub fn byte_limit(&self) -> Option<usize> {
self.byte_limit
}
fn bump_alloc(&mut self, layout: Layout) -> *mut u8 {
let align = layout.align();
let size = layout.size();
let aligned = (self.ptr + align - 1) & !(align - 1);
let new_ptr = aligned + size;
if new_ptr <= self.end {
self.ptr = new_ptr;
self.bytes_used += size;
aligned as *mut u8
} else {
self.grow_and_alloc(layout)
}
}
fn charge(&mut self, requested: usize) {
if let Some(limit) = self.byte_limit
&& self.accounted_bytes.saturating_add(requested) > limit
{
std::panic::panic_any(RegionLimitExceeded {
limit,
used: self.accounted_bytes,
requested,
});
}
self.accounted_bytes = self.accounted_bytes.saturating_add(requested);
}
fn grow_and_alloc(&mut self, layout: Layout) -> *mut u8 {
let size = layout.size();
if let Some(limit) = self.byte_limit {
std::panic::panic_any(RegionLimitExceeded {
limit,
used: self.accounted_bytes.saturating_sub(size),
requested: size,
});
}
let chunk_size = DEFAULT_CHUNK_SIZE.max(size * 2);
let chunk = Chunk::new(chunk_size, layout.align());
let base = chunk.data.as_ptr() as usize;
let aligned = (base + layout.align() - 1) & !(layout.align() - 1);
self.ptr = aligned + size;
self.end = base + chunk_size;
self.bytes_used += size;
self.chunks.push(chunk);
aligned as *mut u8
}
}
impl Default for Region {
fn default() -> Self {
Self::new()
}
}
impl Drop for Region {
fn drop(&mut self) {
for entry in self.drops.drain(..).rev() {
unsafe { (entry.drop_fn)(entry.ptr) };
}
for chunk in self.chunks.drain(..) {
unsafe { alloc::dealloc(chunk.data.as_ptr(), chunk.layout) };
}
}
}
thread_local! {
static REGION_STACK: RefCell<Vec<*mut Region>> = const { RefCell::new(Vec::new()) };
}
pub struct RegionGuard {
_not_send: std::marker::PhantomData<*mut ()>, }
impl RegionGuard {
pub unsafe fn new(region: &mut Region) -> Self {
let ptr = region as *mut Region;
REGION_STACK.with(|stack| stack.borrow_mut().push(ptr));
Self {
_not_send: std::marker::PhantomData,
}
}
}
impl Drop for RegionGuard {
fn drop(&mut self) {
REGION_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
}
#[cfg(not(feature = "no-gc"))]
thread_local! {
static RETIRED_REGIONS: RefCell<Vec<Region>> = const { RefCell::new(Vec::new()) };
static POISON_WATERMARK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(not(feature = "no-gc"))]
pub fn poison_active_regions() {
let depth = region_stack_depth();
if depth == 0 {
return;
}
POISON_WATERMARK.with(|w| w.set(w.get().max(depth)));
crate::stats::GC_STATS.record_region_poison();
}
#[cfg(feature = "no-gc")]
pub fn poison_active_regions() {}
#[cfg(not(feature = "no-gc"))]
pub fn close_region(region: Box<Region>) {
let depth = region_stack_depth();
pop_region_guard();
let poisoned = POISON_WATERMARK.with(|w| {
if depth != 0 && depth <= w.get() {
w.set(depth - 1);
true
} else {
false
}
});
if poisoned {
tracing::debug!(
target: "gc",
"retiring poisoned region ({} objects, {} bytes)",
region.object_count(),
region.bytes_used()
);
RETIRED_REGIONS.with(|r| r.borrow_mut().push(*region));
}
}
#[cfg(feature = "no-gc")]
pub fn close_region(region: Box<Region>) {
pop_region_guard();
drop(region);
}
#[cfg(not(feature = "no-gc"))]
pub(crate) fn trace_retired_regions(visitor: &mut crate::MarkVisitor) {
RETIRED_REGIONS.with(|r| {
for region in r.borrow().iter() {
region.trace_live(visitor);
}
});
}
#[cfg(not(feature = "no-gc"))]
pub(crate) fn trace_active_regions(visitor: &mut crate::MarkVisitor) {
REGION_STACK.with(|stack| {
for ®ion_ptr in stack.borrow().iter() {
let region = unsafe { &*region_ptr };
region.trace_live(visitor);
}
});
}
pub unsafe fn try_alloc_in_region<T: Trace + 'static>(value: T) -> Option<GcPtr<T>> {
REGION_STACK.with(|stack| {
let stack = stack.borrow();
if let Some(®ion_ptr) = stack.last() {
let region = unsafe { &mut *region_ptr };
Some(region.alloc(value))
} else {
None
}
})
}
pub fn pop_region_guard() {
REGION_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
pub fn region_is_active() -> bool {
REGION_STACK.with(|stack| !stack.borrow().is_empty())
}
pub fn region_stack_depth() -> usize {
REGION_STACK.with(|stack| stack.borrow().len())
}
pub fn unwind_region_stack_to(target_depth: usize) {
REGION_STACK.with(|stack| {
let mut stack = stack.borrow_mut();
while stack.len() > target_depth {
stack.pop();
}
});
}
pub unsafe fn push_region_raw(region: *mut Region) {
REGION_STACK.with(|stack| stack.borrow_mut().push(region));
}
#[cfg(all(test, not(feature = "no-gc")))]
mod tests {
use super::*;
use crate::MarkVisitor;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct Tracked {
id: i32,
dropped: Arc<Mutex<Vec<i32>>>,
}
impl Drop for Tracked {
fn drop(&mut self) {
self.dropped.lock().unwrap().push(self.id);
}
}
impl Trace for Tracked {
fn trace(&self, _: &mut MarkVisitor) {}
}
#[derive(Debug)]
struct Parent {
child: GcPtr<Tracked>,
}
impl Trace for Parent {
fn trace(&self, visitor: &mut MarkVisitor) {
use crate::GcVisitor as _;
visitor.visit(&self.child);
}
}
#[test]
fn basic_alloc_and_read() {
let mut region = Region::new();
let p = region.alloc(42i64);
assert_eq!(*p.get(), 42);
assert_eq!(region.object_count(), 1);
}
#[test]
fn multiple_allocs() {
let mut region = Region::new();
let a = region.alloc(10i64);
let b = region.alloc(20i64);
let c = region.alloc(30i64);
assert_eq!(*a.get(), 10);
assert_eq!(*b.get(), 20);
assert_eq!(*c.get(), 30);
assert_eq!(region.object_count(), 3);
}
#[test]
fn drop_runs_on_region_drop() {
let dropped = Arc::new(Mutex::new(Vec::new()));
{
let mut region = Region::new();
region.alloc(Tracked {
id: 1,
dropped: dropped.clone(),
});
region.alloc(Tracked {
id: 2,
dropped: dropped.clone(),
});
region.alloc(Tracked {
id: 3,
dropped: dropped.clone(),
});
}
let order = dropped.lock().unwrap();
assert_eq!(*order, vec![3, 2, 1]);
}
#[test]
fn reset_drops_and_reuses() {
let dropped = Arc::new(Mutex::new(Vec::new()));
let mut region = Region::new();
region.alloc(Tracked {
id: 1,
dropped: dropped.clone(),
});
region.alloc(Tracked {
id: 2,
dropped: dropped.clone(),
});
region.reset();
{
let order = dropped.lock().unwrap();
assert_eq!(*order, vec![2, 1]);
}
assert_eq!(region.object_count(), 0);
let p = region.alloc(99i64);
assert_eq!(*p.get(), 99);
assert_eq!(region.object_count(), 1);
}
#[test]
fn large_alloc_triggers_new_chunk() {
let mut region = Region::with_capacity(128);
for i in 0..100 {
let p = region.alloc(i);
assert_eq!(*p.get(), i);
}
assert_eq!(region.object_count(), 100);
assert!(region.chunks.len() > 1);
}
#[test]
fn region_objects_not_in_gc_heap() {
let heap = crate::GcHeap::new();
let heap_before = heap.count();
let mut region = Region::new();
let _p = region.alloc(42i64);
let _q = region.alloc(99i64);
assert_eq!(heap.count(), heap_before);
}
#[test]
fn gc_skips_region_objects_from_heap_parent() {
let dropped = Arc::new(Mutex::new(Vec::new()));
let mut region = Region::new();
let child = region.alloc(Tracked {
id: 1,
dropped: dropped.clone(),
});
assert!(child.is_region_alloc(), "region alloc must be tagged");
let heap = crate::GcHeap::new();
let parent = heap.alloc(Parent {
child: child.clone(),
});
heap.collect(|vis| {
use crate::GcVisitor as _;
vis.visit(&parent);
});
assert_eq!(heap.count(), 1);
assert!(dropped.lock().unwrap().is_empty());
}
#[test]
fn gc_does_not_follow_reset_region_pointer() {
let dropped = Arc::new(Mutex::new(Vec::new()));
let heap = crate::GcHeap::new();
let mut region = Region::new();
let region_child = region.alloc(Tracked {
id: 1,
dropped: dropped.clone(),
});
let parent = heap.alloc(Parent {
child: region_child,
});
region.reset();
heap.collect(|vis| {
use crate::GcVisitor as _;
vis.visit(&parent);
});
assert_eq!(
heap.count(),
1,
"parent survives; no crash on dangling region ptr"
);
}
#[test]
fn active_region_keeps_heap_child_alive() {
let dropped = Arc::new(Mutex::new(Vec::new()));
let heap = crate::GcHeap::new();
let heap_child = heap.alloc(Tracked {
id: 9,
dropped: dropped.clone(),
});
let mut region = Region::new();
let _parent = region.alloc(Parent { child: heap_child });
let _guard = unsafe { RegionGuard::new(&mut region) };
heap.collect(|_vis| {});
heap.collect(|_vis| {});
assert!(
dropped.lock().unwrap().is_empty(),
"heap object reachable only through an active region must survive GC"
);
}
#[test]
fn thread_local_region_guard() {
assert!(!region_is_active());
let mut region = Region::new();
{
let _guard = unsafe { RegionGuard::new(&mut region) };
assert!(region_is_active());
let p: GcPtr<i64> = unsafe { try_alloc_in_region(42i64) }.unwrap();
assert_eq!(*p.get(), 42);
}
assert!(!region_is_active());
}
#[test]
fn try_alloc_returns_none_without_region() {
assert!(!region_is_active());
let result: Option<GcPtr<i64>> = unsafe { try_alloc_in_region(42i64) };
assert!(result.is_none());
}
#[test]
fn nested_region_guards() {
let mut r1 = Region::new();
let mut r2 = Region::new();
let _g1 = unsafe { RegionGuard::new(&mut r1) };
assert!(region_is_active());
{
let _g2 = unsafe { RegionGuard::new(&mut r2) };
assert!(region_is_active());
unsafe { try_alloc_in_region(1i64) };
assert_eq!(r2.object_count(), 1);
assert_eq!(r1.object_count(), 0);
}
unsafe { try_alloc_in_region(2i64) };
assert_eq!(r1.object_count(), 1);
}
#[test]
fn bytes_used_tracking() {
let mut region = Region::new();
let size = std::mem::size_of::<GcBox<i64>>();
region.alloc(1i64);
region.alloc(2i64);
assert!(region.bytes_used() >= size * 2);
}
#[test]
fn close_region_resets_when_not_poisoned() {
let dropped = Arc::new(Mutex::new(Vec::new()));
let mut region = Box::new(Region::new());
region.alloc(Tracked {
id: 1,
dropped: dropped.clone(),
});
unsafe { push_region_raw(region.as_mut() as *mut Region) };
close_region(region);
assert_eq!(*dropped.lock().unwrap(), vec![1], "destructor must run");
assert!(!region_is_active());
}
#[test]
fn poisoned_region_is_retired_not_reset() {
let dropped = Arc::new(Mutex::new(Vec::new()));
let mut region = Box::new(Region::new());
let p = region.alloc(Tracked {
id: 7,
dropped: dropped.clone(),
});
unsafe { push_region_raw(region.as_mut() as *mut Region) };
poison_active_regions();
close_region(region);
assert!(
dropped.lock().unwrap().is_empty(),
"poisoned region must not run destructors"
);
assert_eq!(p.get().id, 7);
assert!(!region_is_active());
let dropped2 = Arc::new(Mutex::new(Vec::new()));
let mut r2 = Box::new(Region::new());
r2.alloc(Tracked {
id: 9,
dropped: dropped2.clone(),
});
unsafe { push_region_raw(r2.as_mut() as *mut Region) };
close_region(r2);
assert_eq!(*dropped2.lock().unwrap(), vec![9]);
}
#[test]
fn poison_with_no_active_region_is_a_no_op() {
poison_active_regions();
let dropped = Arc::new(Mutex::new(Vec::new()));
let mut region = Box::new(Region::new());
region.alloc(Tracked {
id: 3,
dropped: dropped.clone(),
});
unsafe { push_region_raw(region.as_mut() as *mut Region) };
close_region(region);
assert_eq!(*dropped.lock().unwrap(), vec![3]);
}
#[test]
fn alloc_throughput_region_vs_heap() {
const N: usize = 10_000;
let region_start = std::time::Instant::now();
let mut region = Region::with_capacity(N * std::mem::size_of::<GcBox<i64>>() + 1024);
for i in 0..N as i64 {
let p = region.alloc(i);
std::hint::black_box(p.get());
}
let region_dur = region_start.elapsed();
drop(region);
let heap = crate::GcHeap::new();
let heap_start = std::time::Instant::now();
for i in 0..N as i64 {
let p = heap.alloc(i);
std::hint::black_box(p.get());
}
let heap_dur = heap_start.elapsed();
eprintln!(
"Region: {:?} ({:.0} ns/alloc), Heap: {:?} ({:.0} ns/alloc), speedup: {:.1}x",
region_dur,
region_dur.as_nanos() as f64 / N as f64,
heap_dur,
heap_dur.as_nanos() as f64 / N as f64,
heap_dur.as_nanos() as f64 / region_dur.as_nanos().max(1) as f64,
);
}
}