use std::cell::{Cell, RefCell};
use std::marker::PhantomData;
use std::num::NonZeroUsize;
use std::ptr::NonNull;
use std::sync::OnceLock;
use crate::Tracer;
use crate::descriptor::{Payload, TypeDescriptor};
use crate::gc::{GcHeader, GcRef, HeapId};
use crate::page::{self, NUM_CLASSES, PageHeader, SizeClass};
use crate::roots::{RootSet, RuntimeRoots, WeakSet};
#[must_use = "a Safepoint is the permission to allocate; dropping it wasted a pacing check"]
pub struct Safepoint<'a>(PhantomData<&'a Heap>);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PacingOffsets {
heap_offset: usize,
bytes_since_collect_offset: usize,
collect_threshold_offset: usize,
}
impl PacingOffsets {
const fn new() -> PacingOffsets {
PacingOffsets {
heap_offset: core::mem::offset_of!(crate::RuntimeContext, heap),
bytes_since_collect_offset: Heap::BYTES_SINCE_COLLECT_OFFSET,
collect_threshold_offset: Heap::COLLECT_THRESHOLD_OFFSET,
}
}
#[must_use]
pub const fn heap_offset(self) -> usize {
self.heap_offset
}
#[must_use]
pub const fn bytes_since_collect_offset(self) -> usize {
self.bytes_since_collect_offset
}
#[must_use]
pub const fn collect_threshold_offset(self) -> usize {
self.collect_threshold_offset
}
}
#[derive(Clone, Copy, Debug)]
pub struct InlineInternSite {
pacing: PacingOffsets,
table_offset: usize,
min: i64,
span: u64,
stride_shift: u8,
}
impl InlineInternSite {
pub(crate) const fn new(
table_offset: usize,
min: i64,
max: i64,
stride: usize,
) -> InlineInternSite {
assert!(min <= max, "an intern table's range runs upwards");
assert!(stride.is_power_of_two(), "the index scale must be a shift");
InlineInternSite {
pacing: PacingOffsets::new(),
table_offset,
min,
span: max.wrapping_sub(min) as u64,
stride_shift: stride.trailing_zeros() as u8,
}
}
#[must_use]
pub const fn pacing(self) -> PacingOffsets {
self.pacing
}
#[must_use]
pub const fn table_offset(self) -> usize {
self.table_offset
}
#[must_use]
pub const fn min(self) -> i64 {
self.min
}
#[must_use]
pub const fn span(self) -> u64 {
self.span
}
#[must_use]
pub const fn stride_shift(self) -> u8 {
self.stride_shift
}
}
#[derive(Clone, Copy, Debug)]
pub struct InlineClaimSite {
pacing: PacingOffsets,
heap_id_offset: usize,
heap_live_count_offset: usize,
partial_head_offset: usize,
page_cursor_offset: usize,
page_last_word_offset: usize,
page_allocated_offset: usize,
page_live_count_offset: usize,
header_descriptor_offset: usize,
header_payload_offset_offset: usize,
header_heap_id_offset: usize,
first_block: usize,
stride: usize,
payload_offset: usize,
}
impl InlineClaimSite {
pub(crate) const fn of(descriptor: &'static TypeDescriptor) -> Option<InlineClaimSite> {
if descriptor.owned_bytes.is_some() {
return None;
}
let (payload_offset, block) = BlockLayout::of(descriptor);
let Some(class) = SizeClass::of(block) else {
return None;
};
let stride = class.block_size();
Some(InlineClaimSite {
pacing: PacingOffsets::new(),
heap_id_offset: core::mem::offset_of!(Heap, id),
heap_live_count_offset: core::mem::offset_of!(Heap, live_count),
partial_head_offset: core::mem::offset_of!(Heap, partial)
+ class.index() * core::mem::size_of::<Cell<*mut PageHeader>>(),
page_cursor_offset: PageHeader::CURSOR_OFFSET,
page_last_word_offset: PageHeader::LAST_WORD_OFFSET,
page_allocated_offset: PageHeader::ALLOCATED_OFFSET,
page_live_count_offset: PageHeader::LIVE_COUNT_OFFSET,
header_descriptor_offset: GcHeader::DESCRIPTOR_OFFSET,
header_payload_offset_offset: GcHeader::PAYLOAD_OFFSET_FIELD_OFFSET,
header_heap_id_offset: GcHeader::HEAP_ID_OFFSET,
first_block: PageHeader::first_block_of(stride),
stride,
payload_offset,
})
}
#[must_use]
pub const fn pacing(self) -> PacingOffsets {
self.pacing
}
#[must_use]
pub const fn heap_id_offset(self) -> usize {
self.heap_id_offset
}
#[must_use]
pub const fn heap_live_count_offset(self) -> usize {
self.heap_live_count_offset
}
#[must_use]
pub const fn partial_head_offset(self) -> usize {
self.partial_head_offset
}
#[must_use]
pub const fn page_cursor_offset(self) -> usize {
self.page_cursor_offset
}
#[must_use]
pub const fn page_last_word_offset(self) -> usize {
self.page_last_word_offset
}
#[must_use]
pub const fn page_allocated_offset(self) -> usize {
self.page_allocated_offset
}
#[must_use]
pub const fn page_live_count_offset(self) -> usize {
self.page_live_count_offset
}
#[must_use]
pub const fn header_descriptor_offset(self) -> usize {
self.header_descriptor_offset
}
#[must_use]
pub const fn header_payload_offset_offset(self) -> usize {
self.header_payload_offset_offset
}
#[must_use]
pub const fn header_heap_id_offset(self) -> usize {
self.header_heap_id_offset
}
#[must_use]
pub const fn first_block(self) -> usize {
self.first_block
}
#[must_use]
pub const fn stride(self) -> usize {
self.stride
}
#[must_use]
pub const fn payload_offset(self) -> usize {
self.payload_offset
}
}
#[repr(C)]
pub struct Heap {
id: HeapId,
live_count: Cell<usize>,
bytes_since_collect: Cell<usize>,
collect_threshold: Cell<usize>,
pages: Cell<*mut PageHeader>,
partial: [Cell<*mut PageHeader>; NUM_CLASSES],
empty: Cell<*mut PageHeader>,
empty_large: Cell<*mut PageHeader>,
immortal_pages: Cell<*mut PageHeader>,
live_bytes: Cell<usize>,
pacer: Pacer,
mark_worklist: RefCell<Vec<GcRef>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Trigger {
Paced,
Explicit,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct BlockLayout {
pub(crate) size: usize,
pub(crate) align: usize,
}
impl BlockLayout {
pub(crate) const fn of(descriptor: &TypeDescriptor) -> (usize, BlockLayout) {
let payload_align = descriptor.align();
let payload_offset = GcHeader::payload_offset_for(payload_align);
let size = match payload_offset.checked_add(descriptor.size()) {
Some(size) => size,
None => panic!("allocation size overflow"),
};
let header_align = std::mem::align_of::<GcHeader>();
let align = if payload_align > header_align {
payload_align
} else {
header_align
};
(payload_offset, BlockLayout { size, align })
}
}
pub const INITIAL_COLLECT_THRESHOLD: usize = 1 << 16;
pub const MAX_COLLECT_THRESHOLD: usize = INITIAL_COLLECT_THRESHOLD << 6;
pub const LIVE_HEADROOM: usize = 2;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Pacer {
Doubling,
Bounded {
ceiling: NonZeroUsize,
live_factor: NonZeroUsize,
},
}
impl Pacer {
pub const DEFAULT: Pacer = Pacer::bounded(MAX_COLLECT_THRESHOLD, LIVE_HEADROOM);
pub const fn bounded(ceiling: usize, live_factor: usize) -> Pacer {
let ceiling = if ceiling < INITIAL_COLLECT_THRESHOLD {
INITIAL_COLLECT_THRESHOLD
} else {
ceiling
};
let live_factor = if live_factor < 1 { 1 } else { live_factor };
Pacer::Bounded {
ceiling: match NonZeroUsize::new(ceiling) {
Some(ceiling) => ceiling,
None => panic!("clamped to at least INITIAL_COLLECT_THRESHOLD, which is non-zero"),
},
live_factor: match NonZeroUsize::new(live_factor) {
Some(factor) => factor,
None => panic!("clamped to at least 1, which is non-zero"),
},
}
}
pub fn next_threshold(self, previous: usize, live: usize) -> usize {
match self {
Pacer::Doubling => previous.saturating_mul(2).max(INITIAL_COLLECT_THRESHOLD),
Pacer::Bounded {
ceiling,
live_factor,
} => previous
.saturating_mul(2)
.min(ceiling.get())
.max(live.saturating_mul(live_factor.get()))
.max(INITIAL_COLLECT_THRESHOLD),
}
}
fn from_env() -> Pacer {
static PACER: OnceLock<Pacer> = OnceLock::new();
*PACER.get_or_init(|| Pacer::from_spec(std::env::var("PRAXIS_GC_PACER").ok().as_deref()))
}
fn from_spec(spec: Option<&str>) -> Pacer {
let Some(spec) = spec else {
return Pacer::DEFAULT;
};
match Pacer::parse(spec) {
Ok(pacer) => pacer,
Err(reason) => {
eprintln!(
"praxis: ignoring PRAXIS_GC_PACER={spec:?} ({reason}); \
using the default pacer {:?}",
Pacer::DEFAULT
);
Pacer::DEFAULT
}
}
}
fn parse(spec: &str) -> Result<Pacer, String> {
let mut parts = spec.trim().split(':');
let head = parts.next().unwrap_or_default();
let pacer = match head {
"doubling" => Pacer::Doubling,
"bounded" => {
let ceiling = match parts.next() {
Some(text) => {
parse_bytes(text).ok_or_else(|| format!("{text:?} is not a byte count"))?
}
None => MAX_COLLECT_THRESHOLD,
};
let factor = match parts.next() {
Some(text) => text
.parse::<usize>()
.map_err(|_| format!("{text:?} is not a live-set factor"))?,
None => LIVE_HEADROOM,
};
Pacer::bounded(ceiling, factor)
}
other => return Err(format!("{other:?} is not a pacer")),
};
match parts.next() {
Some(extra) => Err(format!("trailing {extra:?}")),
None => Ok(pacer),
}
}
}
fn parse_bytes(text: &str) -> Option<usize> {
let (digits, scale) = match text.as_bytes().last()? {
b'k' | b'K' => (&text[..text.len() - 1], 1_usize << 10),
b'm' | b'M' => (&text[..text.len() - 1], 1_usize << 20),
b'g' | b'G' => (&text[..text.len() - 1], 1_usize << 30),
_ => (text, 1),
};
digits.parse::<usize>().ok()?.checked_mul(scale)
}
unsafe impl Send for Heap {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeapStats {
pub live_count: usize,
pub live_bytes: usize,
}
impl Heap {
pub const BYTES_SINCE_COLLECT_OFFSET: usize = core::mem::offset_of!(Heap, bytes_since_collect);
pub const COLLECT_THRESHOLD_OFFSET: usize = core::mem::offset_of!(Heap, collect_threshold);
pub fn new() -> Self {
Heap::with_pacer(Pacer::from_env())
}
pub fn with_pacer(pacer: Pacer) -> Self {
Heap {
id: HeapId::mint(),
live_count: Cell::new(0),
bytes_since_collect: Cell::new(0),
collect_threshold: Cell::new(INITIAL_COLLECT_THRESHOLD),
pages: Cell::new(std::ptr::null_mut()),
partial: std::array::from_fn(|_| Cell::new(std::ptr::null_mut())),
empty: Cell::new(std::ptr::null_mut()),
empty_large: Cell::new(std::ptr::null_mut()),
immortal_pages: Cell::new(std::ptr::null_mut()),
live_bytes: Cell::new(0),
pacer,
mark_worklist: RefCell::new(Vec::new()),
}
}
pub fn committed_bytes(&self) -> usize {
self.walk_pages().map(|page| page.page_bytes()).sum()
}
pub fn page_count(&self) -> usize {
self.walk_pages().count()
}
fn walk_pages(&self) -> impl Iterator<Item = &PageHeader> {
let mut next = self.pages.get();
std::iter::from_fn(move || {
if next.is_null() {
return None;
}
let page = unsafe { &*next };
next = page.next();
Some(page)
})
}
fn adopt(&self, page: *mut PageHeader) {
unsafe { (*page).set_next(self.pages.get()) };
self.pages.set(page);
}
#[inline]
fn claim_block(
&self,
descriptor: &'static TypeDescriptor,
payload_offset: usize,
block: BlockLayout,
) -> (*mut u8, usize) {
let Some(class) = SizeClass::of(block) else {
return (
self.claim_large_block(descriptor, payload_offset, block),
block.size,
);
};
let head_cell = self
.partial
.get(class.index())
.expect("SizeClass::of yields an index below NUM_CLASSES");
loop {
let head = head_cell.get();
if head.is_null() {
self.grow_class(class);
continue;
}
let page = unsafe { &*head };
match page.claim_free_block() {
Some(base) => return (base, class.block_size()),
None => head_cell.set(page.next_of_class()),
}
}
}
#[cold]
#[inline(never)]
fn grow_class(&self, class: SizeClass) {
let page = match self.pop_empty() {
Some(page) => {
unsafe { (*page).reclass(class) };
page
}
None => {
let page = PageHeader::new_small(class, self.id.get());
self.adopt(page);
page
}
};
unsafe { (*page).set_next_of_class(self.partial[class.index()].get()) };
self.partial[class.index()].set(page);
}
fn pop_empty(&self) -> Option<*mut PageHeader> {
let page = self.empty.get();
if page.is_null() {
return None;
}
self.empty.set(unsafe { (*page).next_of_class() });
Some(page)
}
#[cold]
#[inline(never)]
fn claim_large_block(
&self,
descriptor: &'static TypeDescriptor,
payload_offset: usize,
block: BlockLayout,
) -> *mut u8 {
let mut previous: *mut PageHeader = std::ptr::null_mut();
let mut current = self.empty_large.get();
while !current.is_null() {
let page = unsafe { &*current };
if page.fits_large(payload_offset, block) {
if previous.is_null() {
self.empty_large.set(page.next_of_class());
} else {
unsafe { (*previous).set_next_of_class(page.next_of_class()) };
}
page.set_next_of_class(std::ptr::null_mut());
page.rewind_cursor();
return page
.claim_free_block()
.expect("an empty large page has its block");
}
previous = current;
current = page.next_of_class();
}
let page = PageHeader::new_large(descriptor, payload_offset, block, self.id.get());
self.adopt(page);
unsafe { (*page).claim_free_block() }.expect("a fresh large page has its block")
}
fn relink_pages(&self) {
for head in &self.partial {
head.set(std::ptr::null_mut());
}
self.empty.set(std::ptr::null_mut());
self.empty_large.set(std::ptr::null_mut());
let mut current = self.pages.get();
while !current.is_null() {
let page = unsafe { &*current };
let next = page.next();
if !page.is_immortal() {
page.rewind_cursor();
let list = if page.live_count() == 0 {
match page.class() {
Some(_) => Some(&self.empty),
None => Some(&self.empty_large),
}
} else {
match page.class() {
Some(class) if (page.live_count() as usize) < page.block_count() => {
Some(&self.partial[class.index()])
}
_ => None,
}
};
match list {
Some(head) => {
page.set_next_of_class(head.get());
head.set(current);
}
None => page.set_next_of_class(std::ptr::null_mut()),
}
}
current = next;
}
}
pub fn id(&self) -> HeapId {
self.id
}
#[inline]
pub fn owns(&self, value: GcRef) -> bool {
value.header().heap_id() == Some(self.id)
}
pub fn stats(&self) -> HeapStats {
HeapStats {
live_count: self.live_count.get(),
live_bytes: self.live_bytes.get(),
}
}
#[cfg(test)]
pub(crate) fn bytes_since_collect(&self) -> usize {
self.bytes_since_collect.get()
}
pub fn charge_owned_growth(&self, bytes: usize) {
self.bytes_since_collect
.set(self.bytes_since_collect.get().saturating_add(bytes));
}
pub(crate) fn alloc_immortal<T: Copy>(
&self,
payload: Payload<T>,
value: T,
_witness: crate::immortal::ImmortalWitness,
) -> GcRef {
let descriptor = payload.descriptor();
let (payload_offset, block) = BlockLayout::of(descriptor);
let class = SizeClass::of(block).expect(
"an immortal payload is a scalar, and the size-class ladder holds every scalar",
);
let charged_before = self.bytes_since_collect.get();
let base = self.claim_immortal_block(class);
let r = unsafe {
self.occupy(
base,
class.block_size(),
descriptor,
payload_offset,
|payload| (payload as *mut T).write(value),
)
};
self.bytes_since_collect.set(charged_before);
r
}
#[cold]
#[inline(never)]
fn claim_immortal_block(&self, class: SizeClass) -> *mut u8 {
let mut current = self.immortal_pages.get();
while !current.is_null() {
let page = unsafe { &*current };
if page.class() == Some(class)
&& let Some(base) = page.claim_free_block()
{
return base;
}
current = page.next_of_class();
}
let page = PageHeader::new_small(class, self.id.get());
unsafe {
(*page).set_immortal();
(*page).set_next_of_class(self.immortal_pages.get());
}
self.adopt(page);
self.immortal_pages.set(page);
unsafe { (*page).claim_free_block() }.expect("a fresh page has room")
}
pub fn pace(&self, roots: &RuntimeRoots<'_>) -> Safepoint<'_> {
self.maybe_collect(roots);
Safepoint(PhantomData)
}
pub fn alloc<T: Copy>(
&self,
_safepoint: Safepoint<'_>,
payload: Payload<T>,
value: T,
) -> GcRef {
self.alloc_unpaced(payload, value)
}
pub unsafe fn alloc_with(
&self,
_safepoint: Safepoint<'_>,
descriptor: &'static TypeDescriptor,
size: usize,
align: usize,
init: impl FnOnce(*mut u8),
) -> GcRef {
unsafe { self.alloc_with_unpaced(descriptor, size, align, init) }
}
pub unsafe fn alloc_payload<P>(
&self,
safepoint: Safepoint<'_>,
descriptor: &'static TypeDescriptor,
payload: P,
) -> GcRef {
unsafe {
self.alloc_with(
safepoint,
descriptor,
std::mem::size_of::<P>(),
std::mem::align_of::<P>(),
|p| (p as *mut P).write(payload),
)
}
}
pub(crate) fn alloc_unpaced<T: Copy>(&self, payload: Payload<T>, value: T) -> GcRef {
unsafe { self.alloc_raw(payload.descriptor(), |p| (p as *mut T).write(value)) }
}
pub(crate) unsafe fn alloc_with_unpaced(
&self,
descriptor: &'static TypeDescriptor,
size: usize,
align: usize,
init: impl FnOnce(*mut u8),
) -> GcRef {
assert_eq!(
size,
descriptor.size(),
"payload size mismatch for descriptor {}",
descriptor.name
);
assert_eq!(
align,
descriptor.align(),
"payload align mismatch for descriptor {}",
descriptor.name
);
unsafe { self.alloc_raw(descriptor, init) }
}
pub(crate) unsafe fn alloc_payload_unpaced<P>(
&self,
descriptor: &'static TypeDescriptor,
payload: P,
) -> GcRef {
unsafe {
self.alloc_with_unpaced(
descriptor,
std::mem::size_of::<P>(),
std::mem::align_of::<P>(),
|p| (p as *mut P).write(payload),
)
}
}
unsafe fn alloc_raw(
&self,
descriptor: &'static TypeDescriptor,
init: impl FnOnce(*mut u8),
) -> GcRef {
let (payload_offset, block) = BlockLayout::of(descriptor);
let (base, stride) = self.claim_block(descriptor, payload_offset, block);
self.live_count.set(self.live_count.get() + 1);
unsafe { self.occupy(base, stride, descriptor, payload_offset, init) }
}
unsafe fn occupy(
&self,
base: *mut u8,
stride: usize,
descriptor: &'static TypeDescriptor,
payload_offset: usize,
init: impl FnOnce(*mut u8),
) -> GcRef {
let recorded_offset = u16::try_from(payload_offset).unwrap_or_else(|_| {
panic!(
"payload alignment {} of descriptor {} exceeds the \
largest offset a GcHeader can record",
descriptor.align(),
descriptor.name
)
});
let header_ptr = base as *mut GcHeader;
let payload_ptr = unsafe { base.add(payload_offset) };
unsafe {
std::ptr::write(
header_ptr,
GcHeader::new(descriptor, recorded_offset, self.id),
);
}
init(payload_ptr);
let owned = unsafe { descriptor.owned_bytes_of(payload_ptr) };
self.bytes_since_collect
.set(self.bytes_since_collect.get() + stride.saturating_add(owned));
unsafe { GcRef::from_non_null(NonNull::new_unchecked(header_ptr)) }
}
pub fn collect(&self, roots: &RuntimeRoots<'_>) {
self.collect_inner(roots, roots, Trigger::Explicit);
}
#[cfg(test)]
pub fn collect_with(&self, roots: &dyn RootSet) {
self.collect_inner(roots, &(), Trigger::Explicit);
}
#[cfg(test)]
pub fn collect_with_weak(&self, roots: &dyn RootSet, weak: &dyn WeakSet) {
self.collect_inner(roots, weak, Trigger::Explicit);
}
#[cfg(test)]
pub fn maybe_collect_with(&self, roots: &dyn RootSet) -> bool {
let should = self.collection_is_due();
if should {
self.collect_inner(roots, &(), Trigger::Paced);
}
should
}
fn collect_inner(&self, roots: &dyn RootSet, weak: &dyn WeakSet, trigger: Trigger) {
self.mark(roots);
self.sweep();
weak.clear_reclaimed();
self.bytes_since_collect.set(0);
if trigger == Trigger::Paced {
self.collect_threshold.set(
self.pacer
.next_threshold(self.collect_threshold.get(), self.live_bytes.get()),
);
}
}
pub fn maybe_collect(&self, roots: &RuntimeRoots<'_>) -> bool {
let should = self.collection_is_due();
if should {
self.collect_inner(roots, roots, Trigger::Paced);
}
should
}
#[inline]
#[must_use]
pub fn collection_is_due(&self) -> bool {
self.bytes_since_collect.get() >= self.collect_threshold.get()
}
fn mark(&self, roots: &dyn RootSet) {
let mut worklist = self.mark_worklist.borrow_mut();
worklist.clear();
roots.push_roots(&mut worklist);
struct Enqueuer<'a>(&'a mut Vec<GcRef>);
impl Tracer for Enqueuer<'_> {
fn trace(&mut self, reference: GcRef) {
self.0.push(reference);
}
}
while let Some(r) = worklist.pop() {
let header = r.header();
if header.heap_id() != Some(self.id) {
continue;
}
let address = r.as_ptr() as *const u8;
let page = unsafe { &*page::page_of(address) };
debug_assert_eq!(page.heap_id(), self.id.get());
let index = page.block_index(address);
debug_assert!(page.is_allocated(index), "a live header on a free block");
if page.test_and_set_mark(index) {
continue;
}
let desc = header.descriptor();
let payload = r.payload::<u8>();
let mut enq = Enqueuer(&mut worklist);
unsafe { (desc.trace)(payload, &mut enq) };
}
}
fn sweep(&self) {
let mut reclaimed = 0usize;
let mut live_bytes = 0usize;
for page in self.walk_pages() {
let words = page.words();
if page.is_immortal() {
for word in 0..words {
if page.mark_word(word) != 0 {
page.clear_mark_word(word);
}
}
continue;
}
let mut freed = 0u32;
for word in 0..words {
let alive = page.allocated_word(word);
let marked = page.mark_word(word);
let mut dead = alive & !marked;
if dead != 0 {
while dead != 0 {
let index = page.block_index_in(word, dead.trailing_zeros());
dead &= dead - 1;
unsafe { Self::finalize_block(page, index) };
freed += 1;
}
page.set_allocated_word(word, alive & marked);
}
if marked != 0 {
page.clear_mark_word(word);
}
}
if freed != 0 {
page.release_blocks(freed);
reclaimed += freed as usize;
}
live_bytes += page.live_count() as usize * page.block_size();
}
self.live_count.set(self.live_count.get() - reclaimed);
self.live_bytes.set(live_bytes);
self.relink_pages();
}
unsafe fn finalize_block(page: &PageHeader, index: usize) {
let header = unsafe { &*(page.block_ptr(index) as *const GcHeader) };
let desc = header.descriptor();
unsafe { (desc.drop_value)(header.payload::<u8>()) };
header.poison();
}
fn finalize_all(&self) {
for page in self.walk_pages() {
if page.is_immortal() {
continue;
}
for word in 0..page.words() {
let mut alive = page.allocated_word(word);
while alive != 0 {
let index = page.block_index_in(word, alive.trailing_zeros());
alive &= alive - 1;
unsafe { Self::finalize_block(page, index) };
}
}
page.clear_bitmaps();
}
self.live_count.set(0);
self.live_bytes.set(0);
self.relink_pages();
}
pub fn reset(&mut self) {
self.finalize_all();
let id = HeapId::mint();
for page in self.walk_pages() {
page.clear_bitmaps();
page.clear_immortal();
page.set_heap_id(id.get());
}
self.immortal_pages.set(std::ptr::null_mut());
self.relink_pages();
self.bytes_since_collect.set(0);
self.collect_threshold.set(INITIAL_COLLECT_THRESHOLD);
self.id = id;
}
fn release_pages(&mut self) {
let mut current = self.pages.get();
while !current.is_null() {
let next = unsafe { (*current).next() };
unsafe { PageHeader::release(current) };
current = next;
}
self.pages.set(std::ptr::null_mut());
for head in &self.partial {
head.set(std::ptr::null_mut());
}
self.empty.set(std::ptr::null_mut());
self.empty_large.set(std::ptr::null_mut());
self.immortal_pages.set(std::ptr::null_mut());
}
}
impl Drop for Heap {
fn drop(&mut self) {
self.finalize_all();
self.release_pages();
}
}
impl Default for Heap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collections::{VEC, VecPayload};
use crate::descriptor::TypeDescriptor;
use crate::roots::RootScope;
use crate::scalars::{INT, INT_PAYLOAD, UNIT_PAYLOAD};
use crate::{GcRef, Tracer};
use std::cell::Cell;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
#[repr(C)]
struct DropProbe(Arc<AtomicUsize>);
impl Drop for DropProbe {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
unsafe fn probe_trace(_: *mut u8, _: &mut dyn Tracer) {}
unsafe fn probe_drop(payload: *mut u8) {
unsafe { std::ptr::drop_in_place(payload as *mut DropProbe) };
}
unsafe fn probe_format(_: *const u8, _: &mut crate::FormatSink<'_>) {}
static DROP_PROBE: TypeDescriptor = TypeDescriptor::for_test::<DropProbe>(
1,
"DropProbe",
probe_trace,
probe_drop,
probe_format,
None,
None,
None,
);
#[repr(C, align(64))]
struct Overaligned(u8);
unsafe fn overaligned_drop(_: *mut u8) {}
static OVERALIGNED: TypeDescriptor = TypeDescriptor::for_test::<Overaligned>(
0,
"Overaligned",
probe_trace,
overaligned_drop,
probe_format,
None,
None,
None,
);
#[test]
fn alloc_int_round_trips_payload() {
let heap = Heap::new();
let r = heap.alloc_unpaced(INT_PAYLOAD, 42_i64);
assert_eq!(r.descriptor().name, "Int");
let v = unsafe { *r.payload::<i64>() };
assert_eq!(v, 42);
assert_eq!(heap.stats().live_count, 1);
}
#[test]
fn collect_reclaims_unrooted_allocation() {
let heap = Heap::new();
let _ = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
assert_eq!(heap.stats().live_count, 1);
let roots = RootScope::new(); heap.collect_with(&roots);
assert_eq!(
heap.stats().live_count,
0,
"unrooted Int should be reclaimed"
);
}
#[test]
fn collect_preserves_rooted_allocation() {
let heap = Heap::new();
let mut scope = RootScope::new();
let r = heap.alloc_unpaced(INT_PAYLOAD, 7_i64);
scope.root(r);
assert_eq!(heap.stats().live_count, 1);
heap.collect_with(&scope);
assert_eq!(heap.stats().live_count, 1, "rooted Int survives");
let v = unsafe { *r.payload::<i64>() };
assert_eq!(v, 7);
}
#[test]
fn collect_preserves_nested_references() {
let heap = Heap::new();
let mut scope = RootScope::new();
let elems: Vec<GcRef> = [10_i64, 20, 30]
.iter()
.map(|&v| heap.alloc_unpaced(INT_PAYLOAD, v))
.collect();
let vec_ref = unsafe {
heap.alloc_payload_unpaced(
&VEC,
VecPayload {
element_descriptor: &INT,
items: elems.into(),
},
)
};
scope.root(vec_ref);
for i in 0..5_i64 {
let _ = heap.alloc_unpaced(INT_PAYLOAD, 1000 + i);
}
assert_eq!(heap.stats().live_count, 9);
heap.collect_with(&scope);
assert_eq!(heap.stats().live_count, 4);
let mut out = String::new();
let desc = vec_ref.descriptor();
unsafe {
(desc.format)(
vec_ref.payload::<u8>() as *const u8,
&mut crate::FormatSink::display(&mut out),
)
};
assert_eq!(out, "[10, 20, 30]");
}
#[test]
fn collect_handles_vec_of_vec() {
let heap = Heap::new();
let mut scope = RootScope::new();
let inner_alloc = |ints: &[i64]| -> GcRef {
let elems: Vec<GcRef> = ints
.iter()
.map(|&v| heap.alloc_unpaced(INT_PAYLOAD, v))
.collect();
unsafe {
heap.alloc_payload_unpaced(
&VEC,
VecPayload {
element_descriptor: &INT,
items: elems.into(),
},
)
}
};
let inner0 = inner_alloc(&[1, 2]);
let inner1 = inner_alloc(&[3]);
let outer = unsafe {
heap.alloc_payload_unpaced(
&VEC,
VecPayload {
element_descriptor: &VEC,
items: vec![inner0, inner1].into(),
},
)
};
scope.root(outer);
let _ = heap.alloc_unpaced(UNIT_PAYLOAD, ());
heap.collect_with(&scope);
assert_eq!(heap.stats().live_count, 6);
let mut out = String::new();
unsafe {
(outer.descriptor().format)(
outer.payload::<u8>() as *const u8,
&mut crate::FormatSink::display(&mut out),
)
};
assert_eq!(out, "[[1, 2], [3]]");
}
#[test]
fn collect_finalizes_unreachable_owned_payload_exactly_once() {
let drops = Arc::new(AtomicUsize::new(0));
let heap = Heap::new();
unsafe {
heap.alloc_payload_unpaced(&DROP_PROBE, DropProbe(Arc::clone(&drops)));
}
let roots = RootScope::new();
heap.collect_with(&roots);
assert_eq!(drops.load(Ordering::SeqCst), 1);
heap.collect_with(&roots);
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"a swept payload must never be finalized twice"
);
}
#[test]
fn dropping_heap_finalizes_live_owned_payloads() {
let drops = Arc::new(AtomicUsize::new(0));
{
let heap = Heap::new();
unsafe {
heap.alloc_payload_unpaced(&DROP_PROBE, DropProbe(Arc::clone(&drops)));
}
assert_eq!(drops.load(Ordering::SeqCst), 0);
}
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"tearing down a heap must run descriptor finalizers for live payloads"
);
}
#[test]
fn dropping_heap_finalizes_reachable_payloads_too() {
let drops = Arc::new(AtomicUsize::new(0));
{
let heap = Heap::new();
let mut scope = RootScope::new();
let probe =
unsafe { heap.alloc_payload_unpaced(&DROP_PROBE, DropProbe(Arc::clone(&drops))) };
scope.root(probe);
heap.collect_with(&scope);
assert_eq!(drops.load(Ordering::SeqCst), 0, "a rooted probe survives");
}
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[test]
fn resetting_then_dropping_finalizes_each_payload_once() {
let drops = Arc::new(AtomicUsize::new(0));
{
let mut heap = Heap::new();
unsafe {
heap.alloc_payload_unpaced(&DROP_PROBE, DropProbe(Arc::clone(&drops)));
}
heap.reset();
assert_eq!(drops.load(Ordering::SeqCst), 1, "reset finalizes");
}
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"the drop after a reset must find nothing left to finalize"
);
}
#[test]
fn overaligned_payload_accessor_matches_initialized_address() {
let initialized_at = Cell::new(std::ptr::null_mut());
let heap = Heap::new();
let value = unsafe {
heap.alloc_with_unpaced(
&OVERALIGNED,
std::mem::size_of::<Overaligned>(),
std::mem::align_of::<Overaligned>(),
|payload| {
initialized_at.set(payload);
(payload as *mut Overaligned).write(Overaligned(7));
},
)
};
assert_eq!(
value.payload::<Overaligned>() as *mut u8,
initialized_at.get(),
"GcHeader::payload must account for alignment padding inserted by Heap::alloc_raw"
);
}
#[test]
fn foreign_heap_root_cannot_delay_reclamation() {
let first = Heap::new();
let second = Heap::new();
let value = first.alloc_unpaced(INT_PAYLOAD, 1_i64);
let mut foreign_roots = RootScope::new();
foreign_roots.root(value);
second.collect_with(&foreign_roots);
first.collect_with(&RootScope::new());
assert_eq!(
first.stats().live_count,
0,
"a collection on another heap must not mutate this heap's mark colors"
);
}
fn allocate_until_paced(heap: &Heap, limit: usize) -> bool {
for i in 0..limit {
let _ = heap.alloc_unpaced(INT_PAYLOAD, i as i64);
if heap.maybe_collect_with(&RootScope::new()) {
return true;
}
}
false
}
#[test]
fn the_pacing_predicate_is_one_unsigned_compare_of_the_two_exported_words() {
let heap = Heap::new();
for (since, threshold) in [
(0_usize, 0_usize),
(0, 1),
(0, INITIAL_COLLECT_THRESHOLD),
(INITIAL_COLLECT_THRESHOLD - 1, INITIAL_COLLECT_THRESHOLD),
(INITIAL_COLLECT_THRESHOLD, INITIAL_COLLECT_THRESHOLD),
(INITIAL_COLLECT_THRESHOLD + 1, INITIAL_COLLECT_THRESHOLD),
(usize::MAX, INITIAL_COLLECT_THRESHOLD),
(INITIAL_COLLECT_THRESHOLD, usize::MAX),
] {
heap.bytes_since_collect.set(since);
heap.collect_threshold.set(threshold);
let base = std::ptr::from_ref(&heap).cast::<u8>();
let (read_since, read_threshold) = unsafe {
(
*base.add(Heap::BYTES_SINCE_COLLECT_OFFSET).cast::<usize>(),
*base.add(Heap::COLLECT_THRESHOLD_OFFSET).cast::<usize>(),
)
};
assert_eq!(read_since, since, "BYTES_SINCE_COLLECT_OFFSET names it");
assert_eq!(read_threshold, threshold, "COLLECT_THRESHOLD_OFFSET does");
assert_eq!(
read_since >= read_threshold,
heap.collection_is_due(),
"the predicate generated code emits (since={since}, \
threshold={threshold}) is no longer the predicate \
`collection_is_due` applies — see its doc: the backend's \
`emit_inline_intern` owes the same change, or generated code \
answers from the intern table on a branch where the collector \
was due"
);
}
}
#[test]
fn an_inline_intern_site_carries_the_heaps_own_pacing_offsets() {
let pacing = crate::small_int::INLINE_INTERN_SITE.pacing();
assert_eq!(
pacing.bytes_since_collect_offset(),
Heap::BYTES_SINCE_COLLECT_OFFSET
);
assert_eq!(
pacing.collect_threshold_offset(),
Heap::COLLECT_THRESHOLD_OFFSET
);
assert_ne!(
pacing.bytes_since_collect_offset(),
pacing.collect_threshold_offset(),
"two distinct fields, or the compare is `x >= x`"
);
assert_eq!(
pacing.heap_offset(),
core::mem::offset_of!(crate::RuntimeContext, heap),
"and the base those two are relative to is the context's `heap`"
);
assert_eq!(
crate::scalars::INT_CLAIM_SITE.pacing(),
pacing,
"and the claim site's are the same three, because they are the same \
value — one predicate, one authority"
);
}
#[test]
fn the_claim_site_displacements_name_the_fields_they_claim_to() {
let site = crate::scalars::INT_CLAIM_SITE;
let heap = Heap::new();
let value = heap.alloc_unpaced(INT_PAYLOAD, 7_i64);
let heap_base = std::ptr::from_ref(&heap).cast::<u8>();
let (read_id, read_live, read_head) = unsafe {
(
*heap_base.add(site.heap_id_offset()).cast::<u32>(),
*heap_base.add(site.heap_live_count_offset()).cast::<usize>(),
*heap_base
.add(site.partial_head_offset())
.cast::<*mut PageHeader>(),
)
};
assert_eq!(read_id, heap.id.get(), "heap_id_offset names `Heap::id`");
assert_eq!(
read_live,
heap.live_count.get(),
"heap_live_count_offset names `Heap::live_count`"
);
assert!(
!read_head.is_null(),
"partial_head_offset names the `Int` class's list head, and the \
allocation above put a page on it"
);
let page = unsafe { &*read_head };
let page_base = std::ptr::from_ref(page).cast::<u8>();
let (read_cursor, read_last, read_page_live, read_word) = unsafe {
(
*page_base.add(site.page_cursor_offset()).cast::<u32>(),
*page_base.add(site.page_last_word_offset()).cast::<u32>(),
*page_base.add(site.page_live_count_offset()).cast::<u32>(),
*page_base.add(site.page_allocated_offset()).cast::<u64>(),
)
};
assert_eq!(
read_cursor, 0,
"the first claim leaves the cursor at word 0"
);
assert_eq!(
read_last,
page.words() as u32 - 1,
"page_last_word_offset names `PageHeader::last_word`"
);
assert!(
read_last >= 1,
"a claimable class must have more than one bitmap word, or the \
tail-word bail-out cedes the whole page to the wrapper"
);
assert_eq!(
read_page_live,
page.live_count(),
"page_live_count_offset names `PageHeader::live_count`"
);
assert_eq!(
read_word,
page.allocated_word(0),
"page_allocated_offset names the base of the `allocated` bitmap"
);
assert_eq!(
site.stride(),
page.block_size(),
"the stride the pacer is charged is the page's own"
);
assert_eq!(
site.first_block(),
page.first_block(),
"the folded `first_block` is the page's own — see \
`PageHeader::first_block_of`"
);
assert_eq!(
site.payload_offset(),
page.payload_offset(),
"and the payload displacement the header will record is the one \
the page was laid out with (ADR-039 decision 1)"
);
let header_base = value.as_ptr().cast::<u8>();
let (read_desc, read_payload_offset, read_header_id) = unsafe {
(
*header_base
.add(site.header_descriptor_offset())
.cast::<*const TypeDescriptor>(),
*header_base
.add(site.header_payload_offset_offset())
.cast::<u16>(),
*header_base.add(site.header_heap_id_offset()).cast::<u32>(),
)
};
assert!(
std::ptr::eq(read_desc, &crate::scalars::INT),
"header_descriptor_offset names the descriptor pointer"
);
assert_eq!(
read_payload_offset as usize,
site.payload_offset(),
"header_payload_offset_offset names the recorded displacement, and \
it is the one the site carries"
);
assert_eq!(
read_header_id,
heap.id.get(),
"header_heap_id_offset names the provenance word"
);
}
#[test]
fn only_a_descriptor_with_no_owned_bytes_charge_has_a_claim_site() {
for descriptor in crate::descriptor::BUILTINS {
let claimable = InlineClaimSite::of(descriptor).is_some();
let charges_outside = descriptor.owned_bytes.is_some();
let on_the_ladder = SizeClass::of(BlockLayout::of(descriptor).1).is_some();
assert_eq!(
claimable,
!charges_outside && on_the_ladder,
"{}: a claim site exists exactly when the pacing charge is the \
stride alone and the block is on the ladder",
descriptor.name
);
}
assert!(
InlineClaimSite::of(&crate::scalars::INT).is_some(),
"and `Int` is on the claimable side, which is the whole package"
);
assert!(
InlineClaimSite::of(&crate::text::TEXT).is_none(),
"…and `Text` is not: its `owned_bytes` is the `Box<str>` the \
sequence has no way to measure"
);
}
#[test]
fn reset_restores_collection_pacing() {
let mut heap = Heap::new();
assert!(allocate_until_paced(&heap, 100_000));
let _ = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
assert_ne!(heap.bytes_since_collect.get(), 0);
assert_ne!(heap.collect_threshold.get(), INITIAL_COLLECT_THRESHOLD);
heap.reset();
assert_eq!(heap.bytes_since_collect.get(), 0);
assert_eq!(heap.collect_threshold.get(), INITIAL_COLLECT_THRESHOLD);
}
#[test]
fn an_explicit_collection_does_not_grow_the_pacing_threshold() {
let heap = Heap::new();
for _ in 0..8 {
heap.collect_with(&RootScope::new());
}
assert_eq!(
heap.collect_threshold.get(),
INITIAL_COLLECT_THRESHOLD,
"an explicit collection must leave the automatic threshold alone"
);
assert!(allocate_until_paced(&heap, 100_000));
assert_eq!(
heap.collect_threshold.get(),
INITIAL_COLLECT_THRESHOLD * 2,
"a paced collection is what grows it"
);
}
#[test]
fn an_explicit_collection_does_not_grow_a_bounded_pacers_threshold() {
let heap = Heap::with_pacer(Pacer::bounded(1 << 20, LIVE_HEADROOM));
for _ in 0..8 {
heap.collect_with(&RootScope::new());
}
assert_eq!(
heap.collect_threshold.get(),
INITIAL_COLLECT_THRESHOLD,
"an explicit collection must leave the automatic threshold alone"
);
assert!(allocate_until_paced(&heap, 100_000));
assert_eq!(
heap.collect_threshold.get(),
INITIAL_COLLECT_THRESHOLD * 2,
"with nothing rooted the ratchet term is what grows it, exactly as before"
);
}
fn int_stride() -> usize {
let (_, block) = BlockLayout::of(&INT);
SizeClass::of(block)
.expect("an Int is on the ladder")
.block_size()
}
#[test]
fn sweep_measures_the_live_set_in_bytes() {
const ROOTED: usize = 100;
let heap = Heap::with_pacer(Pacer::Doubling);
let mut roots = RootScope::new();
for i in 0..1_000_i64 {
let r = heap.alloc_unpaced(INT_PAYLOAD, i);
if (i as usize) < ROOTED {
roots.root(r);
}
}
heap.collect_with(&roots);
assert_eq!(heap.stats().live_count, ROOTED);
assert_eq!(
heap.stats().live_bytes,
ROOTED * int_stride(),
"the live set is the survivors' blocks and nothing else"
);
}
#[test]
fn an_immortal_is_not_counted_in_the_live_set() {
let heap = Heap::with_pacer(Pacer::Doubling);
let immortals = crate::immortal::Immortals::new(&heap);
assert!(immortals.small_int(7).is_some(), "7 is interned");
let mut roots = RootScope::new();
roots.root(heap.alloc_unpaced(INT_PAYLOAD, 1_i64));
heap.collect_with(&roots);
assert_eq!(
heap.stats().live_bytes,
int_stride(),
"the immortal tables are on pages sweep never walks, so they are not live bytes"
);
}
#[test]
fn a_bounded_pacer_stops_doubling_at_the_ceiling() {
const CEILING: usize = 1 << 18; let heap = Heap::with_pacer(Pacer::bounded(CEILING, LIVE_HEADROOM));
for round in 0..40 {
assert!(
allocate_until_paced(&heap, 100_000),
"round {round} did not reach the threshold"
);
assert!(
heap.collect_threshold.get() <= CEILING,
"round {round} left the threshold at {} above the {CEILING}-byte ceiling",
heap.collect_threshold.get()
);
}
assert_eq!(
heap.collect_threshold.get(),
CEILING,
"and it ratchets all the way up to it rather than oscillating below"
);
}
#[test]
fn a_bounded_pacer_gives_a_large_live_set_its_headroom() {
const CEILING: usize = 1 << 20; let heap = Heap::with_pacer(Pacer::bounded(CEILING, LIVE_HEADROOM));
let mut roots = RootScope::new();
for i in 0..(2 * CEILING / int_stride()) as i64 {
roots.root(heap.alloc_unpaced(INT_PAYLOAD, i));
}
assert!(
drive_one_paced_collection(&heap, &roots, 200_000),
"the rooted fixture is already past the threshold"
);
let live = heap.stats().live_bytes;
assert!(
live > CEILING,
"the fixture must hold more than the ceiling, and holds {live}"
);
assert_eq!(
heap.collect_threshold.get(),
live * LIVE_HEADROOM,
"the mandatory term must be allowed to exceed the ceiling"
);
}
#[test]
fn a_shrinking_live_set_does_not_lower_the_threshold_below_the_ceiling() {
const CEILING: usize = 1 << 20; let heap = Heap::with_pacer(Pacer::bounded(CEILING, LIVE_HEADROOM));
{
let mut roots = RootScope::new();
for i in 0..(2 * CEILING / int_stride()) as i64 {
roots.root(heap.alloc_unpaced(INT_PAYLOAD, i));
}
assert!(drive_one_paced_collection(&heap, &roots, 200_000));
assert!(heap.collect_threshold.get() > CEILING);
}
assert!(drive_one_paced_collection(
&heap,
&RootScope::new(),
1_000_000
));
assert_eq!(heap.stats().live_bytes, 0);
assert_eq!(
heap.collect_threshold.get(),
CEILING,
"an empty live set must leave the threshold at the ceiling, not at INITIAL"
);
}
#[test]
fn a_bounded_heap_stops_growing() {
const CEILING: usize = 1 << 18; const RETAINED: usize = 1_024;
const CHURN: i64 = 512 * 1_024;
fn churn(pacer: Pacer) -> (usize, usize) {
let heap = Heap::with_pacer(pacer);
let mut roots = RootScope::new();
for i in 0..RETAINED as i64 {
roots.root(heap.alloc_unpaced(INT_PAYLOAD, i));
}
for i in 0..CHURN {
let _ = heap.alloc_unpaced(INT_PAYLOAD, i);
heap.maybe_collect_with(&roots);
}
(heap.committed_bytes(), heap.stats().live_bytes)
}
let slack = 14 * page::PAGE_SIZE;
let (bounded_bytes, live) = churn(Pacer::bounded(CEILING, LIVE_HEADROOM));
assert_eq!(live, RETAINED * int_stride());
assert!(
bounded_bytes <= live + CEILING + slack,
"a bounded pacer left {bounded_bytes} bytes committed against a {live}-byte \
live set and a {CEILING}-byte ceiling"
);
let (doubling_bytes, _) = churn(Pacer::Doubling);
assert!(
doubling_bytes > live + CEILING + slack,
"the doubling rule is supposed to fail this bound, and committed only \
{doubling_bytes} bytes — the fixture is no longer measuring anything"
);
}
fn drive_one_paced_collection(heap: &Heap, roots: &dyn RootSet, limit: usize) -> bool {
for i in 0..limit {
let _ = heap.alloc_unpaced(INT_PAYLOAD, i as i64);
if heap.maybe_collect_with(roots) {
return true;
}
}
false
}
#[test]
fn reset_repudiates_the_measured_live_set() {
let mut heap = Heap::with_pacer(Pacer::bounded(1 << 20, LIVE_HEADROOM));
{
let mut roots = RootScope::new();
for i in 0..8_192_i64 {
roots.root(heap.alloc_unpaced(INT_PAYLOAD, i));
}
assert!(drive_one_paced_collection(&heap, &roots, 200_000));
assert_ne!(heap.stats().live_bytes, 0);
}
heap.reset();
assert_eq!(heap.stats().live_bytes, 0);
assert_eq!(heap.collect_threshold.get(), INITIAL_COLLECT_THRESHOLD);
}
#[test]
fn the_default_pacer_is_bounded_at_the_measured_ceiling() {
assert_eq!(Pacer::from_spec(None), Pacer::DEFAULT);
assert_eq!(Pacer::DEFAULT, Pacer::bounded(4 << 20, 2));
assert_eq!(
Pacer::DEFAULT.next_threshold(1 << 30, 0),
MAX_COLLECT_THRESHOLD
);
}
#[test]
fn a_bounded_pacer_cannot_be_built_with_a_ceiling_below_the_first_threshold() {
assert_eq!(
Pacer::bounded(0, 0),
Pacer::bounded(INITIAL_COLLECT_THRESHOLD, 1)
);
assert_eq!(
Pacer::bounded(1, 0).next_threshold(INITIAL_COLLECT_THRESHOLD, 1_000_000),
1_000_000,
"a clamped factor of one still gives the live set its own bytes"
);
}
#[test]
fn a_pacer_spec_parses_its_grammar_and_rejects_everything_else() {
assert_eq!(Pacer::parse("doubling"), Ok(Pacer::Doubling));
assert_eq!(
Pacer::parse("bounded"),
Ok(Pacer::bounded(MAX_COLLECT_THRESHOLD, LIVE_HEADROOM))
);
assert_eq!(
Pacer::parse("bounded:8M"),
Ok(Pacer::bounded(8 << 20, LIVE_HEADROOM))
);
assert_eq!(Pacer::parse("bounded:1G:3"), Ok(Pacer::bounded(1 << 30, 3)));
assert_eq!(
Pacer::parse("bounded:65536"),
Ok(Pacer::bounded(INITIAL_COLLECT_THRESHOLD, LIVE_HEADROOM))
);
for bad in [
"",
"bounde",
"bounded:huge",
"bounded:8M:x",
"bounded:8M:2:2",
] {
assert!(Pacer::parse(bad).is_err(), "{bad:?} must not parse");
assert_eq!(
Pacer::from_spec(Some(bad)),
Pacer::DEFAULT,
"{bad:?} must fall back to the default"
);
}
}
#[test]
fn pacing_charges_the_bytes_a_payload_owns() {
use crate::text::{TEXT, TextPayload};
let alloc_text = |heap: &Heap, len: usize| {
let owned: Box<str> = "x".repeat(len).into_boxed_str();
unsafe { heap.alloc_payload_unpaced(&TEXT, TextPayload::owned(owned)) }
};
let small = Heap::new();
alloc_text(&small, 8);
let big = Heap::new();
alloc_text(&big, 64 * 1024);
let charged_small = small.bytes_since_collect.get();
let charged_big = big.bytes_since_collect.get();
assert_eq!(
charged_big - charged_small,
64 * 1024 - 8,
"the Box<str> must be charged at its real length"
);
assert!(
big.maybe_collect_with(&RootScope::new()),
"a 64 KiB Text must reach the 64 KiB threshold on its own"
);
}
#[test]
fn the_pacer_is_charged_the_narrower_stride() {
const N: usize = 100;
let heap = Heap::new();
assert_eq!(
heap.bytes_since_collect.get(),
0,
"a fresh heap owes nothing"
);
for i in 0..N {
let _ = heap.alloc_unpaced(INT_PAYLOAD, i as i64);
}
assert_eq!(
heap.bytes_since_collect.get(),
N * 24,
"an Int must be charged its 24-byte block and nothing else"
);
}
#[test]
fn a_source_slice_text_is_charged_nothing_beyond_its_block() {
use crate::text::{TEXT, TextPayload};
let heap = Heap::new();
let owner: Box<str> = "x".repeat(4096).into_boxed_str();
let owner_ref = unsafe { heap.alloc_payload_unpaced(&TEXT, TextPayload::owned(owner)) };
let after_owner = heap.bytes_since_collect.get();
let slice = unsafe { crate::text::SourceSlice::new(owner_ref, 0, 4096) }
.expect("the whole owner is a valid slice of itself");
unsafe {
heap.alloc_payload_unpaced(&TEXT, TextPayload::Slice(slice));
}
let (_, block) = BlockLayout::of(&TEXT);
let stride = SizeClass::of(block)
.expect("a Text is on the ladder")
.block_size();
assert_eq!(
heap.bytes_since_collect.get() - after_owner,
stride,
"a slice owns no bytes of its own"
);
}
#[test]
fn repeated_collection_reuses_dead_object_storage() {
let heap = Heap::new();
const OBJECTS_PER_CYCLE: usize = 4_096;
for i in 0..OBJECTS_PER_CYCLE {
let _ = heap.alloc_unpaced(INT_PAYLOAD, i as i64);
}
heap.collect_with(&RootScope::new());
let first_cycle_bytes = heap.committed_bytes();
for cycle in 1..=8 {
for i in 0..OBJECTS_PER_CYCLE {
let _ = heap.alloc_unpaced(INT_PAYLOAD, (cycle * OBJECTS_PER_CYCLE + i) as i64);
}
heap.collect_with(&RootScope::new());
}
let final_bytes = heap.committed_bytes();
assert!(
final_bytes <= first_cycle_bytes.saturating_mul(2),
"reclaiming the same bounded working set repeatedly grew the heap \
from {first_cycle_bytes} to {final_bytes} bytes"
);
}
#[test]
fn an_emptied_page_is_reused_for_another_size_class() {
use crate::text::{TEXT, TextPayload};
let heap = Heap::new();
for _ in 0..8_000 {
unsafe {
heap.alloc_payload_unpaced(&TEXT, TextPayload::owned("x"));
}
}
heap.collect_with(&RootScope::new());
let after_texts = heap.page_count();
assert!(after_texts > 1, "the fixture must span several pages");
for i in 0..8_000_i64 {
let _ = heap.alloc_unpaced(INT_PAYLOAD, i);
}
assert_eq!(
heap.page_count(),
after_texts,
"the pages the `Text`s emptied must have been re-classed for the `Int`s, \
not left as dead capital beside fresh ones"
);
}
#[test]
fn an_immortal_is_invisible_to_sweep_and_to_finalize_all() {
let drops = Arc::new(AtomicUsize::new(0));
{
let heap = Heap::new();
let immortals = crate::immortal::Immortals::new(&heap);
let immortal = immortals.small_int(7).expect("7 is interned");
let address = immortal.as_ptr();
unsafe {
heap.alloc_payload_unpaced(&DROP_PROBE, DropProbe(Arc::clone(&drops)));
}
heap.collect_with(&RootScope::new());
assert_eq!(drops.load(Ordering::SeqCst), 1, "the probe was reclaimed");
assert_eq!(heap.stats().live_count, 0, "an immortal is not counted");
assert!(!immortal.header().is_poisoned(), "an immortal is not swept");
assert_eq!(immortal.header().heap_id(), Some(heap.id()));
assert_eq!(unsafe { *immortal.payload::<i64>() }, 7);
for i in 0..4_000_i64 {
assert_ne!(heap.alloc_unpaced(INT_PAYLOAD, i).as_ptr(), address);
}
}
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"teardown must not finalize anything twice"
);
}
#[test]
fn two_heaps_pages_do_not_alias() {
let first = Heap::new();
let second = Heap::new();
for i in 0..2_000_i64 {
let _ = first.alloc_unpaced(INT_PAYLOAD, i);
let _ = second.alloc_unpaced(INT_PAYLOAD, i);
}
let mine: Vec<usize> = first
.walk_pages()
.map(|page| page.base() as usize)
.collect();
for page in second.walk_pages() {
assert!(!mine.contains(&(page.base() as usize)));
}
assert!(mine.len() > 1);
}
#[test]
fn every_allocation_records_the_offset_it_was_laid_out_with() {
let heap = Heap::new();
let int = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
assert_eq!(
int.payload::<i64>() as usize - int.as_ptr() as usize,
GcHeader::payload_offset_for(INT.align())
);
let over = unsafe { heap.alloc_payload_unpaced(&OVERALIGNED, Overaligned(1)) };
assert_eq!(
over.payload::<Overaligned>() as usize - over.as_ptr() as usize,
GcHeader::payload_offset_for(OVERALIGNED.align())
);
assert_eq!(over.payload::<Overaligned>() as usize % 64, 0);
}
#[test]
fn allocations_carry_their_owning_heap() {
let first = Heap::new();
let second = Heap::new();
let mine = first.alloc_unpaced(INT_PAYLOAD, 1_i64);
assert_eq!(mine.header().heap_id(), Some(first.id()));
assert!(first.owns(mine));
assert!(!second.owns(mine));
}
#[test]
fn sweeping_poisons_the_reclaimed_header() {
let heap = Heap::new();
let doomed = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
assert!(!doomed.header().is_poisoned());
heap.collect_with(&RootScope::new());
assert_eq!(heap.stats().live_count, 0);
assert!(doomed.header().is_poisoned());
assert_eq!(doomed.header().heap_id(), None);
}
#[test]
fn a_swept_reference_is_not_traced_again() {
let heap = Heap::new();
let stale = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
heap.collect_with(&RootScope::new());
assert!(stale.header().is_poisoned());
let mut stale_roots = RootScope::new();
stale_roots.root(stale);
heap.collect_with(&stale_roots);
assert_eq!(
heap.stats().live_count,
0,
"a poisoned header must not be resurrected by rooting it"
);
}
struct WeakSlots {
slots: std::cell::RefCell<Vec<Option<GcRef>>>,
poisoned_at_scan: std::cell::RefCell<Vec<bool>>,
}
impl WeakSlots {
fn holding(refs: &[GcRef]) -> WeakSlots {
WeakSlots {
slots: std::cell::RefCell::new(refs.iter().copied().map(Some).collect()),
poisoned_at_scan: std::cell::RefCell::new(Vec::new()),
}
}
}
impl crate::roots::WeakSet for WeakSlots {
fn clear_reclaimed(&self) -> usize {
let mut cleared = 0;
let mut seen = self.poisoned_at_scan.borrow_mut();
for slot in self.slots.borrow_mut().iter_mut() {
let Some(r) = *slot else {
seen.push(false);
continue;
};
let poisoned = r.header().is_poisoned();
seen.push(poisoned);
if poisoned {
*slot = None;
cleared += 1;
}
}
cleared
}
}
#[test]
fn the_weak_scan_nulls_only_what_this_collection_reclaimed() {
let heap = Heap::new();
let doomed = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
let kept = heap.alloc_unpaced(INT_PAYLOAD, 2_i64);
let weak = WeakSlots::holding(&[doomed, kept]);
let mut scope = RootScope::new();
scope.root(kept);
heap.collect_with_weak(&scope, &weak);
assert_eq!(
*weak.poisoned_at_scan.borrow(),
vec![true, false],
"the scan ran before the sweep, or the sweep did not poison"
);
assert_eq!(
*weak.slots.borrow(),
vec![None, Some(kept)],
"exactly the reclaimed entry becomes an absence"
);
assert_eq!(heap.stats().live_count, 1);
}
#[test]
fn the_weak_scan_runs_after_the_sweep_and_before_the_block_is_reissued() {
use crate::scalars::FLOAT_PAYLOAD;
let heap = Heap::new();
let doomed = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
let address = doomed.as_ptr();
let weak = WeakSlots::holding(&[doomed]);
heap.collect_with_weak(&RootScope::new(), &weak);
assert_eq!(*weak.poisoned_at_scan.borrow(), vec![true]);
assert_eq!(*weak.slots.borrow(), vec![None]);
let reused = heap.alloc_unpaced(FLOAT_PAYLOAD, 2.5_f64);
assert_eq!(
reused.as_ptr(),
address,
"this test only says anything if the block really came back"
);
assert_eq!(reused.descriptor().name, "Float");
}
#[test]
fn a_reclaimed_block_is_reused_for_the_next_object_of_its_layout() {
use crate::scalars::FLOAT_PAYLOAD;
let heap = Heap::new();
let doomed = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
let address = doomed.as_ptr();
heap.collect_with(&RootScope::new());
assert!(doomed.header().is_poisoned());
let reused = heap.alloc_unpaced(FLOAT_PAYLOAD, 2.5_f64);
assert_eq!(
reused.as_ptr(),
address,
"a swept block must be handed back out, not left spent"
);
assert!(!reused.header().is_poisoned());
assert_eq!(reused.header().heap_id(), Some(heap.id()));
assert_eq!(reused.descriptor().name, "Float");
assert_eq!(unsafe { *reused.payload::<f64>() }, 2.5);
assert_eq!(heap.stats().live_count, 1);
}
#[test]
fn reset_repudiates_every_page_and_keeps_the_storage() {
let mut heap = Heap::new();
let doomed = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
heap.collect_with(&RootScope::new());
let live_ref = heap.alloc_unpaced(INT_PAYLOAD, 3_i64);
let committed = heap.committed_bytes();
assert!(committed > 0);
heap.reset();
assert_eq!(
heap.committed_bytes(),
committed,
"reset keeps every page, so a stale reference still masks to mapped storage"
);
for page in heap.walk_pages() {
assert_eq!(page.live_count(), 0, "no page may still claim a live block");
assert!(
!page.is_immortal(),
"reset repudiates the immortal pages too"
);
assert_eq!(page.heap_id(), heap.id().get());
}
assert!(!heap.owns(doomed));
assert!(!heap.owns(live_ref));
let fresh = heap.alloc_unpaced(INT_PAYLOAD, 2_i64);
assert_eq!(fresh.header().heap_id(), Some(heap.id()));
}
#[test]
fn an_overaligned_block_round_trips_through_its_own_page() {
let heap = Heap::new();
let doomed = unsafe { heap.alloc_payload_unpaced(&OVERALIGNED, Overaligned(1)) };
let address = doomed.as_ptr();
heap.collect_with(&RootScope::new());
let pages = heap.page_count();
let reused = unsafe { heap.alloc_payload_unpaced(&OVERALIGNED, Overaligned(2)) };
assert_eq!(
reused.as_ptr(),
address,
"an over-aligned block must be handed back out, not left spent"
);
assert_eq!(reused.payload::<Overaligned>() as usize % 64, 0);
assert_eq!(
heap.page_count(),
pages,
"the emptied large page must be reused, not left beside a fresh one"
);
}
#[repr(C)]
struct Aligned8([u64; 4]);
#[repr(C, align(16))]
struct Aligned16([u64; 4]);
static ALIGNED_8: TypeDescriptor = TypeDescriptor::for_test::<Aligned8>(
2,
"Aligned8",
probe_trace,
overaligned_drop,
probe_format,
None,
None,
None,
);
static ALIGNED_16: TypeDescriptor = TypeDescriptor::for_test::<Aligned16>(
3,
"Aligned16",
probe_trace,
overaligned_drop,
probe_format,
None,
None,
None,
);
#[test]
fn a_swept_block_is_never_handed_to_a_request_of_another_alignment() {
let (_, eight) = BlockLayout::of(&ALIGNED_8);
let (_, sixteen) = BlockLayout::of(&ALIGNED_16);
assert_eq!(
eight.size, sixteen.size,
"the fixtures must share a size or this test proves nothing"
);
assert_ne!(eight.align, sixteen.align);
let heap = Heap::new();
let doomed = unsafe { heap.alloc_payload_unpaced(&ALIGNED_8, Aligned8([1, 2, 3, 4])) };
let address = doomed.as_ptr();
heap.collect_with(&RootScope::new());
let other = unsafe { heap.alloc_payload_unpaced(&ALIGNED_16, Aligned16([5, 6, 7, 8])) };
assert_ne!(
other.as_ptr(),
address,
"a block filed under {{48, 8}} must not satisfy a {{48, 16}} request"
);
assert_eq!(other.payload::<Aligned16>() as usize % 16, 0);
}
#[test]
fn reset_mints_a_new_heap_identity() {
let mut heap = Heap::new();
let before = heap.id();
let _ = heap.alloc_unpaced(INT_PAYLOAD, 1_i64);
heap.reset();
assert_ne!(heap.id(), before);
assert_eq!(
heap.alloc_unpaced(INT_PAYLOAD, 2_i64).header().heap_id(),
Some(heap.id())
);
}
}