Skip to main content

gc_lite/
heap.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright (c) 2025-2026 John Ray <996351336@qq.com>
3
4use 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
13/// Minimum GC threshold: at least one GcHead plus the smallest possible
14/// aligned payload (1 byte, aligned to GcHead's alignment).
15const 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    /// Registered GC data type info
28    pub(super) node_dtypes: &'static GcTypeRegistry,
29
30    /// Partition storage (indexed by `GcPartitionId`)
31    pub(super) partitions: Vec<GcPartition>,
32    /// Global memory usage limit for the entire heap, 0 for unlimited
33    pub(super) memory_limit: usize,
34    /// Global automatic GC threshold for the entire heap, 0 for disabled
35    pub(super) gc_threshold: usize,
36    /// Total memory used across all partitions
37    pub(super) total_memory_used: usize,
38    /// Weak reference list, each slot stores (version, GcHeader).
39    /// The node pointer is wrapped in `Cell` to allow clearing through `&self`
40    /// during `finalize_partition` — preventing use-after-free when a Drop
41    /// callback upgrades a weak ref to a node whose payload was already dropped.
42    pub(super) weak_slots: Vec<(u16, Cell<Option<NonNull<GcHead>>>)>,
43    /// gc scope stack list
44    pub(crate) scope_stacks: Vec<ScopeStack>, // DON'T use SmallVec here
45
46    /// User provided opaque raw pointer
47    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        // heap world is gone, dealloc all nodes live in it, regardless their status.
60        log::trace!("[heap::drop]");
61
62        // Clear all scope caches first to remove LOCAL flags from nodes.
63        // This must happen BEFORE disposing partitions so that nodes are not
64        // protected by scope and can be reclaimed. GcScopeState::clear() only
65        // touches node flags and does not access any GcHeap fields, so it is
66        // safe to call during GcHeap::drop.
67        for stack in &mut self.scope_stacks {
68            for s in stack.list.drain(..) {
69                s.clear();
70            }
71        }
72
73        // Take all nodes from all partitions to avoid self-borrow conflicts
74        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    /// Create a new garbage collection heap with an explicit GC type registry.
97    ///
98    /// The heap starts with no partitions. Use [`create_partition`](GcHeap::create_partition)
99    /// to add partitions as needed.
100    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    /// Check if garbage collection is needed
167    #[inline(always)]
168    pub fn should_gc(&self) -> bool {
169        // If GC threshold > 0 and memory usage reaches threshold, trigger GC
170        // gc_threshold = 0 means automatic GC is disabled
171        self.gc_threshold > 0 && self.total_memory_used >= self.gc_threshold
172    }
173
174    /// Attach a node to partition's nodes chain.
175    ///
176    /// This method does NOT update memory accounting — the caller is responsible
177    /// for calling `update_mem_use` separately (typically done in `alloc_node_mem`).
178    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    /// Check if `node` was allocated in this heap
209    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    /// Protect node from being gc collected.
217    ///
218    /// 1. if node is local or root, it's protected, returns true
219    /// 1. otherwise if has current scope, add node to current scope and returns true
220    /// 1. can't protect, returns false
221    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    /// Protect nodes from being gc collected, for each node do following steps:
227    ///
228    /// 1. if node is local or root, do nothing
229    /// 1. if has current scope, add node to current scope
230    /// 1. can't protect, returns false
231    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    /// Protect nodes from being gc collected, for each node do following steps:
244    ///
245    /// 1. if node is local or root, do nothing
246    /// 1. if has current scope, add node to current scope
247    /// 1. can't protect, returns false
248    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    /// Update memory usage with rollup to parent partitions
253    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        // Use two-phase partition removal to cleanly dispose all nodes and reclaim memory
361        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        // Allocate 3 non-root nodes and 1 root node
378        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        // GC should collect the 3 non-root nodes
410        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        // Only the root node should remain
417        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        // Remove the partition to clean up remaining root node
422        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        // Normal add
434        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        // Normal subtract
439        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        // Subtract to zero
444        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}