1use std::{cell::Cell, ptr::NonNull};
5
6use crate::{
7 gctype::GcTypeRegistry,
8 node::{GcHead, GcNodeFlag},
9 partition::{GcPartition, GcPartitionId},
10 scope::{GcScopeStackId, ScopeStack},
11};
12
13const MIN_GC_THRESHOLD: usize = {
16 const fn align_up(value: usize, align: usize) -> usize {
17 let mask = align - 1;
18 (value + mask) & !mask
19 }
20 let head_size = std::mem::size_of::<GcHead>();
21 let min_payload = 1;
22 let payload_align = std::mem::align_of::<GcHead>();
23 head_size + align_up(min_payload, payload_align)
24};
25
26pub struct GcHeap {
27 pub(super) node_dtypes: &'static GcTypeRegistry,
29
30 pub(super) partitions: Vec<GcPartition>,
32 pub(super) memory_limit: usize,
34 pub(super) gc_threshold: usize,
36 pub(super) total_memory_used: usize,
38 pub(super) weak_slots: Vec<(u16, Cell<Option<NonNull<GcHead>>>)>,
43 pub(crate) scope_stacks: Vec<ScopeStack>, opaque: *mut u8,
48
49 #[cfg(debug_assertions)]
50 pub(crate) dbg_dropping_root_partition: Option<GcPartitionId>,
51 #[cfg(debug_assertions)]
52 pub(crate) dbg_living_nodes: std::collections::HashSet<NonNull<GcHead>>,
53}
54
55impl GcHeap {}
56
57impl Drop for GcHeap {
58 fn drop(&mut self) {
59 log::trace!("[heap::drop]");
61
62 for stack in &mut self.scope_stacks {
68 for s in stack.list.drain(..) {
69 s.clear();
70 }
71 }
72
73 let all_nodes: Vec<_> = self
75 .partitions
76 .iter_mut()
77 .map(|par| std::mem::take(&mut par.nodes))
78 .collect();
79
80 for nodes in all_nodes {
81 self.dispose_all_nodes(nodes, Self::DUMMY_DISPOSE_CALLBACK);
82 }
83
84 #[cfg(debug_assertions)]
85 debug_assert!(
86 self.dbg_living_nodes.is_empty(),
87 "[O.o][heap drop] leaked nodes {:?}",
88 self.dbg_living_nodes
89 );
90 }
91}
92
93impl GcHeap {
94 pub const DUMMY_DISPOSE_CALLBACK: fn(&GcHeap, &GcHead) = |_, _| {};
95
96 pub fn new(registry: &'static GcTypeRegistry) -> Self {
101 Self {
102 partitions: Vec::with_capacity(1),
103 memory_limit: 0,
104 gc_threshold: 0,
105 total_memory_used: 0,
106 weak_slots: Vec::with_capacity(8),
107 opaque: std::ptr::null_mut(),
108 node_dtypes: registry,
109 scope_stacks: vec![ScopeStack::new(None)],
110
111 #[cfg(debug_assertions)]
112 dbg_dropping_root_partition: None,
113 #[cfg(debug_assertions)]
114 dbg_living_nodes: std::collections::HashSet::with_capacity(128),
115 }
116 }
117
118 #[inline(always)]
119 pub const fn opaque(&self) -> *mut u8 {
120 self.opaque
121 }
122
123 #[inline(always)]
124 pub const fn set_opaque(&mut self, opaque: *mut u8) {
125 self.opaque = opaque;
126 }
127
128 #[inline(always)]
129 pub fn memory_limit(&self) -> usize {
130 self.memory_limit
131 }
132
133 pub fn set_memory_limit(&mut self, limit: usize) -> usize {
134 if limit == 0 {
135 self.memory_limit = 0;
136 } else {
137 let used = self.total_memory_used;
138 let applied = std::cmp::max(used, limit);
139 self.memory_limit = applied;
140
141 if self.gc_threshold > 0 && self.gc_threshold >= applied {
142 let adjusted = std::cmp::max(applied - (applied >> 2), MIN_GC_THRESHOLD);
143 self.gc_threshold = adjusted;
144 }
145 }
146
147 self.memory_limit
148 }
149
150 #[inline(always)]
151 pub fn gc_threshold(&self) -> usize {
152 self.gc_threshold
153 }
154
155 pub fn set_gc_threshold(&mut self, threshold: usize) -> usize {
156 if threshold > 0 && self.memory_limit > 0 {
157 let capped = self.memory_limit.saturating_mul(8).saturating_div(10);
158 self.gc_threshold = std::cmp::min(threshold, capped);
159 } else {
160 self.gc_threshold = threshold;
161 }
162
163 self.gc_threshold
164 }
165
166 #[inline(always)]
168 pub fn should_gc(&self) -> bool {
169 self.gc_threshold > 0 && self.total_memory_used >= self.gc_threshold
172 }
173
174 pub(crate) fn attach_node(&mut self, partition_id: GcPartitionId, mut node: NonNull<GcHead>) {
179 let n = unsafe { node.as_mut() };
180 debug_assert!(n.next.is_none());
181 debug_assert_eq!(
182 n.partition_id(),
183 partition_id,
184 "attach_node: node partition_id doesn't match"
185 );
186
187 let par = &mut self.partitions[partition_id.0 as usize];
188 let mem_before = par.memory_used;
189 par.nodes.prepend(node);
190 debug_assert_eq!(
191 par.memory_used, mem_before,
192 "attach_node must not change memory accounting"
193 );
194 }
195
196 pub fn set_root_node(&mut self, mut node: NonNull<GcHead>) {
197 let n = unsafe { node.as_mut() };
198 if !n.is_root() {
199 n.insert_flag(GcNodeFlag::ROOT);
200 let pid = n.partition_id();
201 let par = &mut self.partitions[pid.0 as usize];
202 if par.is_marking() {
203 par.add_gray_node(node);
204 }
205 }
206 }
207
208 pub fn contains(&self, node: NonNull<GcHead>) -> bool {
210 let pid = unsafe { node.as_ref().partition_id() };
211 self.partitions
212 .get(pid.0 as usize)
213 .is_some_and(|par| par.nodes.iter().any(|p| p == node))
214 }
215
216 pub fn protect_node(&mut self, scope_stack_id: GcScopeStackId, node: NonNull<GcHead>) -> bool {
222 self.current_scope(scope_stack_id)
223 .is_some_and(|s| s.add_non_local(node))
224 }
225
226 pub fn protect_nodes_iter(
232 &mut self,
233 scope_stack_id: GcScopeStackId,
234 nodes: impl Iterator<Item = NonNull<GcHead>>,
235 ) {
236 if let Some(s) = self.current_scope(scope_stack_id) {
237 for n in nodes {
238 s.add_non_local(n);
239 }
240 }
241 }
242
243 pub fn protect_nodes(&mut self, scope_stack_id: GcScopeStackId, nodes: &[NonNull<GcHead>]) {
249 self.protect_nodes_iter(scope_stack_id, nodes.iter().copied());
250 }
251
252 pub(crate) fn update_mem_use(&mut self, id: GcPartitionId, delta: i32) -> usize {
254 let par = &mut self.partitions[id.0 as usize];
255 if delta >= 0 {
256 let d = delta as usize;
257 par.memory_used += d;
258 self.total_memory_used += d;
259 par.memory_used
260 } else {
261 let d = (-delta) as usize;
262 debug_assert!(
263 par.memory_used >= d,
264 "update_mem_use: partition memory underflow ({} < {})",
265 par.memory_used,
266 d,
267 );
268 debug_assert!(
269 self.total_memory_used >= d,
270 "update_mem_use: global memory underflow ({} < {})",
271 self.total_memory_used,
272 d,
273 );
274 par.memory_used -= d;
275 self.total_memory_used -= d;
276 par.memory_used
277 }
278 }
279
280 #[inline(always)]
281 pub const fn memory_used(&self) -> usize {
282 self.total_memory_used
283 }
284}
285
286#[cfg(test)]
287mod heap_tests {
288 use crate::arena::{ARENA_CAPACITY, MAX_ARENA_ALLOC};
289 use crate::{GcRef, GcTraceCtx, node::GcNode, trace::GcTrace};
290
291 use super::*;
292
293 #[derive(Debug)]
294 struct Node {
295 next: Option<GcRef<Node>>,
296 #[expect(dead_code)]
297 value: i32,
298 }
299
300 impl GcTrace for Node {
301 fn trace(&self, tr: &mut GcTraceCtx) {
302 if let Some(next) = self.next {
303 tr.add(next);
304 }
305 }
306 }
307
308 crate::gc_type_register! {
309 Node, drop_pass = 0;
310 }
311
312 #[test]
313 fn test_heap_with_context_alloc_and_cleanup() {
314 let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
315 let partition_id = heap.create_partition(ARENA_CAPACITY, MAX_ARENA_ALLOC);
316 let stack_id = heap.acquire_scope_stack(partition_id);
317
318 let _head = heap.with_new_scope(stack_id, |ctx| {
319 let node = ctx
320 .alloc_local(Node {
321 next: None,
322 value: 1,
323 })
324 .unwrap();
325 ctx.clear();
326 node.gc_head_ptr()
327 });
328
329 while !heap.mark(partition_id, 64) {}
330 let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
331 assert!(removed_after > 0);
332 }
333
334 #[test]
335 fn test_memory_used_symmetry_alloc_dispose() {
336 let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
337 let id = heap.create_partition(ARENA_CAPACITY, MAX_ARENA_ALLOC);
338
339 let mem_before = heap.memory_used();
340 let par_mem_before = heap.partition(id).unwrap().memory_used();
341
342 let node = unsafe {
343 heap.alloc_raw(
344 id,
345 Node {
346 next: None,
347 value: 42,
348 },
349 )
350 }
351 .unwrap();
352 let gross_size = heap.memory_used() - mem_before;
353
354 assert!(gross_size > 0);
355 assert_eq!(
356 heap.partition(id).unwrap().memory_used() - par_mem_before,
357 gross_size
358 );
359
360 let link = heap.finalize_partition(id).unwrap();
362 let freed = heap.dealloc_partition(id, link);
363 assert_eq!(freed, gross_size);
364
365 let mem_after = heap.memory_used();
366 assert_eq!(
367 mem_after, mem_before,
368 "global memory should return to original after partition removal"
369 );
370 }
371
372 #[test]
373 fn test_memory_used_symmetry_sweep() {
374 let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
375 let id = heap.create_partition(ARENA_CAPACITY, MAX_ARENA_ALLOC);
376
377 let mem_before = heap.memory_used();
379 let par_mem_before = heap.partition(id).unwrap().memory_used();
380
381 for i in 0..3 {
382 unsafe {
383 heap.alloc_raw(
384 id,
385 Node {
386 next: None,
387 value: i,
388 },
389 )
390 }
391 .unwrap();
392 }
393 let root = unsafe {
394 heap.alloc_root_raw(
395 id,
396 Node {
397 next: None,
398 value: 99,
399 },
400 )
401 }
402 .unwrap();
403
404 let mem_after_alloc = heap.memory_used();
405 let par_mem_after_alloc = heap.partition(id).unwrap().memory_used();
406 assert!(mem_after_alloc > mem_before);
407 assert!(par_mem_after_alloc > par_mem_before);
408
409 let freed = heap.garbage_collect(id, GcHeap::DUMMY_DISPOSE_CALLBACK);
411 assert!(freed > 0);
412
413 let mem_after_gc = heap.memory_used();
414 let par_mem_after_gc = heap.partition(id).unwrap().memory_used();
415
416 let root_size = mem_after_alloc - mem_before - freed;
418 assert_eq!(mem_after_gc, mem_before + root_size);
419 assert_eq!(par_mem_after_gc, par_mem_before + root_size);
420
421 let link = heap.finalize_partition(id).unwrap();
423 let freed_rem = heap.dealloc_partition(id, link);
424 assert_eq!(freed_rem, root_size);
425 assert_eq!(heap.memory_used(), mem_before);
426 }
427
428 #[test]
429 fn test_memory_used_update_mem_use_edge_cases() {
430 let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
431 let id = heap.create_partition(0, 0);
432
433 assert_eq!(heap.update_mem_use(id, 50), 50);
435 assert_eq!(heap.partition(id).unwrap().memory_used(), 50);
436 assert_eq!(heap.memory_used(), 50);
437
438 assert_eq!(heap.update_mem_use(id, -30), 20);
440 assert_eq!(heap.partition(id).unwrap().memory_used(), 20);
441 assert_eq!(heap.memory_used(), 20);
442
443 assert_eq!(heap.update_mem_use(id, -20), 0);
445 assert_eq!(heap.partition(id).unwrap().memory_used(), 0);
446 assert_eq!(heap.memory_used(), 0);
447 }
448}