use std::{collections::HashMap, ptr::NonNull};
use crate::{
gctype::GcTypeRegistry,
node::{GcHead, GcNodeFlag},
partition::{GcPartition, GcPartitionId},
scope::GcScopeState,
};
pub struct GcHeap {
pub(super) node_dtypes: &'static GcTypeRegistry,
pub(super) partitions: HashMap<GcPartitionId, GcPartition>,
pub(super) memory_limit: usize,
pub(super) gc_threshold: usize,
pub(super) total_memory_used: usize,
pub(crate) scope_stack: Vec<GcScopeState<'static>>, pub(super) weak_slots: Vec<(u16, Option<NonNull<GcHead>>)>,
opaque: *mut u8,
#[cfg(debug_assertions)]
pub(crate) dbg_dropping_root_partition: Option<GcPartitionId>,
#[cfg(debug_assertions)]
pub(crate) dbg_living_nodes: std::collections::HashSet<NonNull<GcHead>>,
}
impl Drop for GcHeap {
fn drop(&mut self) {
log::trace!("[heap::drop]");
for s in self.scope_stack.drain(..) {
unsafe {
s.abort();
}
}
let pars = std::mem::take(&mut self.partitions);
for (_, partition) in pars {
self.dispose_all_nodes(partition.nodes, Self::DUMMY_DISPOSE_CALLBACK);
}
#[cfg(debug_assertions)]
debug_assert!(
self.dbg_living_nodes.is_empty(),
"[O.o][heap drop] leaked nodes {:?}",
self.dbg_living_nodes
);
}
}
impl GcHeap {
pub const DUMMY_DISPOSE_CALLBACK: fn(&GcHeap, &GcHead) = |_, _| {};
pub fn new(registry: &'static GcTypeRegistry) -> Self {
Self {
partitions: HashMap::new(),
memory_limit: 0,
gc_threshold: 0,
total_memory_used: 0,
weak_slots: Vec::new(),
opaque: std::ptr::null_mut(),
node_dtypes: registry,
scope_stack: Vec::with_capacity(16),
#[cfg(debug_assertions)]
dbg_dropping_root_partition: None,
#[cfg(debug_assertions)]
dbg_living_nodes: std::collections::HashSet::with_capacity(128),
}
}
#[inline(always)]
pub const fn opaque(&self) -> *mut u8 {
self.opaque
}
pub const fn set_opaque(&mut self, opaque: *mut u8) {
self.opaque = opaque;
}
pub fn memory_limit(&self) -> usize {
self.memory_limit
}
pub fn set_memory_limit(&mut self, limit: usize) -> usize {
if limit == 0 {
self.memory_limit = 0;
} else {
let used = self.total_memory_used;
let applied = std::cmp::max(used, limit);
self.memory_limit = applied;
if self.gc_threshold > 0 && self.gc_threshold >= applied {
let adjusted = applied - (applied >> 2);
self.gc_threshold = adjusted;
}
}
self.memory_limit
}
pub fn gc_threshold(&self) -> usize {
self.gc_threshold
}
pub fn set_gc_threshold(&mut self, threshold: usize) -> usize {
if threshold > 0 && self.memory_limit > 0 {
let capped = self.memory_limit.saturating_mul(8).saturating_div(10);
self.gc_threshold = std::cmp::min(threshold, capped);
} else {
self.gc_threshold = threshold;
}
self.gc_threshold
}
#[inline(always)]
pub fn should_gc(&self) -> bool {
self.gc_threshold > 0 && self.total_memory_used >= self.gc_threshold
}
pub(crate) fn attach_node(&mut self, partition_id: GcPartitionId, mut node: NonNull<GcHead>) {
debug_assert!(!partition_id.is_null());
let n = unsafe { node.as_mut() };
debug_assert!(n.partition_id().is_null());
debug_assert!(n.next.is_none());
n.set_partition_id(partition_id);
let par = self.partitions.get_mut(&partition_id).unwrap();
par.nodes.prepend(node);
}
pub fn set_root_node(&mut self, mut node: NonNull<GcHead>) {
let n = unsafe { node.as_mut() };
if !n.is_root() {
let partition_id = n.partition_id();
if let Some(p) = self.partitions.get_mut(&partition_id) {
n.insert_flag(GcNodeFlag::ROOT);
if p.is_marking() {
p.add_gray_node(node);
}
}
}
}
pub fn contains(&self, node: NonNull<GcHead>) -> bool {
self.nodes(unsafe { node.as_ref().partition_id() })
.any(|p| p == node)
}
pub fn protect_node(&mut self, node: NonNull<GcHead>) -> bool {
self.current_scope().is_some_and(|s| s.add_non_local(node))
}
pub fn protect_nodes_iter(&mut self, nodes: impl Iterator<Item = NonNull<GcHead>>) {
if let Some(s) = self.current_scope() {
for n in nodes {
s.add_non_local(n);
}
}
}
pub fn protect_nodes(&mut self, nodes: &[NonNull<GcHead>]) {
self.protect_nodes_iter(nodes.iter().copied());
}
pub(crate) fn update_mem_use(&mut self, id: GcPartitionId, delta: i32) -> usize {
if id.is_null() {
return 0;
}
if let Some(par) = self.partitions.get_mut(&id) {
if delta >= 0 {
let d = delta as usize;
par.memory_used += d;
self.total_memory_used += d;
} else {
let d = (-delta) as usize;
debug_assert!(par.memory_used >= d);
debug_assert!(self.total_memory_used >= d);
par.memory_used -= d;
self.total_memory_used -= d;
}
par.memory_used
} else {
0
}
}
#[inline(always)]
pub const fn memory_used(&self) -> usize {
self.total_memory_used
}
}
#[cfg(test)]
mod heap_tests {
use crate::{GcRef, GcTraceCtx, trace::GcTrace};
use super::*;
#[derive(Debug)]
struct Node {
next: Option<GcRef<Node>>,
value: i32,
}
impl GcTrace for Node {
fn trace(&self, tr: &mut GcTraceCtx) {
if let Some(next) = self.next {
tr.add(next);
}
}
}
crate::gc_type_register! {
Node, drop_pass = 0;
}
#[test]
fn test_heap_with_context_alloc_and_cleanup() {
let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
let partition_id = heap.create_partition();
let head = heap.with_new_scope(partition_id, |ctx| {
let node: GcRef<Node> = ctx
.alloc_local(Node {
next: None,
value: 1,
})
.unwrap();
ctx.flush();
node.head_ptr
});
while !heap.mark(partition_id, 64) {}
let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
assert!(removed_after > 0);
}
}