Skip to main content

gc_lite/
partition.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright (c) 2025-2026 John Ray <996351336@qq.com>
3
4use std::ptr::NonNull;
5
6use crate::{GcHead, GcHeap, node::GcTriColor, node_link::GcNodeLink};
7
8#[cfg(feature = "gc_arena")]
9use crate::arena::GcArena;
10
11/// Partition ID, used as index into `GcHeap::partitions`.
12///
13/// Every node belongs to exactly one partition (index >= 0).
14/// There is no "null" partition ID.
15#[derive(Clone, Copy, PartialEq, Eq, Hash)]
16#[repr(transparent)]
17pub struct GcPartitionId(pub u16);
18
19impl std::fmt::Debug for GcPartitionId {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(f, "{}", self.0)
22    }
23}
24
25#[derive(Debug)]
26pub struct GcPartition {
27    /// link of nodes in this partition
28    pub(crate) nodes: GcNodeLink,
29    /// gray nodes to be traced in this partition
30    pub(crate) gray_list: Vec<NonNull<GcHead>>,
31    /// Is in a marking cycle
32    marking: bool,
33
34    pub(crate) memory_used: usize,
35
36    /// bump + free-list arena allocator (`None` = arena disabled for this partition)
37    #[cfg(feature = "gc_arena")]
38    pub(crate) arena: Option<GcArena>,
39    /// Arena capacity used when creating / resetting (`0` = disabled)
40    #[cfg(feature = "gc_arena")]
41    arena_capacity: usize,
42    /// Allocations larger than this skip the arena (`0` = all skip)
43    #[cfg(feature = "gc_arena")]
44    pub(crate) arena_max_alloc: usize,
45}
46
47impl GcPartition {
48    /// Create a new partition.
49    ///
50    /// # Parameters
51    ///
52    /// - `capacity`: arena size in bytes. `0` = disable arena for this partition
53    ///   (all allocations go through system malloc directly).
54    /// - `max_alloc`: allocations larger than this skip the arena.
55    ///   When `capacity == 0` this is forced to `0`.
56    #[allow(unused_variables)]
57    pub fn new(capacity: usize, max_alloc: usize) -> Self {
58        #[cfg(feature = "gc_arena")]
59        let (arena, arena_max_alloc) = if capacity > 0 {
60            (
61                Some(GcArena::new(capacity).expect("arena alloc failed")),
62                max_alloc,
63            )
64        } else {
65            (None, 0)
66        };
67
68        Self {
69            memory_used: 0,
70            nodes: GcNodeLink::default(),
71            gray_list: Vec::new(),
72            marking: false,
73
74            #[cfg(feature = "gc_arena")]
75            arena,
76            #[cfg(feature = "gc_arena")]
77            arena_capacity: capacity,
78            #[cfg(feature = "gc_arena")]
79            arena_max_alloc,
80        }
81    }
82
83    /// Reset the arena with the same capacity used at creation time.
84    /// Used by `dealloc_partition` after freeing all nodes.
85    #[cfg(feature = "gc_arena")]
86    pub(super) fn reset_arena(&mut self) {
87        self.arena = if self.arena_capacity > 0 {
88            Some(GcArena::new(self.arena_capacity).expect("arena alloc failed"))
89        } else {
90            None
91        };
92    }
93
94    #[inline(always)]
95    pub fn memory_used(&self) -> usize {
96        self.memory_used
97    }
98
99    #[inline(always)]
100    pub const fn is_marking(&self) -> bool {
101        self.marking
102    }
103
104    #[inline(always)]
105    pub(crate) const fn set_marking(&mut self, marking: bool) {
106        self.marking = marking;
107    }
108
109    pub(crate) fn add_gray_node(&mut self, mut node: NonNull<GcHead>) {
110        debug_assert!(
111            self.is_marking(),
112            "add_gray_node called when partition is not marking"
113        );
114
115        match unsafe { node.as_ref().color() } {
116            GcTriColor::White => unsafe {
117                node.as_mut().set_color(GcTriColor::Gray);
118            },
119            GcTriColor::Gray => {}
120            GcTriColor::Black => {
121                return;
122            }
123        }
124
125        unsafe {
126            if !node.as_ref().is_gray_listed() {
127                node.as_mut().set_gray_listed(true);
128                self.gray_list.push(node);
129            }
130        }
131    }
132}
133
134impl GcHeap {
135    /// Create a new partition and return its ID.
136    ///
137    /// # Parameters
138    ///
139    /// - `arena_capacity`: arena size in bytes. `0` = disable arena for this partition.
140    /// - `arena_max_alloc`: allocations larger than this skip the arena.
141    pub fn create_partition(
142        &mut self,
143        arena_capacity: usize,
144        arena_max_alloc: usize,
145    ) -> GcPartitionId {
146        let id = GcPartitionId(self.partitions.len() as u16);
147        self.partitions
148            .push(GcPartition::new(arena_capacity, arena_max_alloc));
149        id
150    }
151
152    // ── Two-phase partition removal ──────────────────────────────────────
153    //
154    //  Phase 1 (finalize): takes &self, calls Drop on all node payloads without
155    //    deallocating memory. Drop implementations can safely access GcHeap
156    //    through a shared reference (e.g. &GcHeap), avoiding the aliasing
157    //    problem that existed in the single-phase remove_partition.
158    //
159    //  Phase 2 (dealloc):   takes &mut self, frees the memory of all finalized
160    //    nodes and resets the partition state. No Drop callbacks run
161    //    at this point, so there is no risk of reentrant &mut self access.
162    //
163    //  remove_partition remains for backward compatibility and calls both
164    //    phases in sequence.
165
166    /// Phase 1: Finalize — call `Drop` on all node payloads without freeing memory.
167    ///
168    /// Only takes `&self`, so `Drop` implementations can safely access `GcHeap`
169    /// via a shared reference. Returns the node link containing all finalized
170    /// nodes, which must be passed to [`dealloc_partition`] to reclaim memory.
171    ///
172    /// Scope caches associated with this partition are cleared before any drops
173    /// are called, so that `GcScopeState::clear()` (which only touches node flags)
174    /// runs before payload drops.
175    pub fn finalize_partition(&self, partition_id: GcPartitionId) -> Option<GcNodeLink> {
176        // Check that the partition exists
177        self.partitions.get(partition_id.0 as usize)?;
178
179        // Clear scope caches associated with this partition BEFORE dropping nodes.
180        // GcScopeState::clear() only touches node flags and does not access any
181        // GcHeap fields, so it is safe to call during finalize_partition.
182        for stack in &self.scope_stacks {
183            if stack.partition == Some(partition_id) {
184                for s in &stack.list {
185                    s.clear();
186                }
187            }
188        }
189
190        log::trace!("[finalize_partition] {partition_id:?}");
191
192        // Clone the partition's node link so we can iterate without borrowing &self.
193        let link = self.partitions[partition_id.0 as usize].nodes.clone();
194
195        // Pre-clear all weak_slots BEFORE dropping any payloads.
196        // This prevents use-after-free when a Drop callback in a later node
197        // calls GcWeak::upgrade() on a node whose payload was already dropped
198        // earlier in this same pass. The node pointer is Cell-wrapped so we
199        // can mutate it through &self.
200        for node in link.iter() {
201            let hd = unsafe { node.as_ref() };
202            if !hd.weak_id.is_null() {
203                self.weak_slots[hd.weak_id.index() as usize].1.set(None);
204            }
205        }
206
207        // Process nodes by drop pass order.
208        // We iterate all nodes for each pass to respect inter-pass dependencies.
209        for &pass in self.node_dtypes.drop_passes {
210            for node in link.iter() {
211                let dtype = unsafe { node.as_ref().dtype() } as usize;
212                let info = &self.node_dtypes.type_info_list[dtype];
213                if info.drop_pass == pass {
214                    self.drop_node_payload_without_dealloc(node);
215                }
216            }
217        }
218
219        Some(link)
220    }
221
222    /// Phase 2: Dealloc — free memory of all finalized nodes and reset the partition.
223    ///
224    /// Takes `&mut self` — no `Drop` callbacks run at this point, so there is
225    /// no risk of reentrant `&mut self` access. The `link` must be the value
226    /// returned by [`finalize_partition`] for the same partition.
227    ///
228    /// Returns the total number of bytes freed.
229    pub fn dealloc_partition(&mut self, partition_id: GcPartitionId, link: GcNodeLink) -> usize {
230        let partition_mem = self.partitions[partition_id.0 as usize].memory_used;
231        let mut freed_bytes = 0;
232
233        // Deallocate all finalized nodes in the link.
234        for node in link.iter() {
235            // Handle weak reference cleanup.
236            unsafe {
237                if !node.as_ref().weak_id.is_null() {
238                    let widx = node.as_ref().weak_id.index();
239                    debug_assert!(
240                        (widx as usize) < self.weak_slots.len(),
241                        "dealloc_partition: weak slot index {} out of bounds (len {})",
242                        widx,
243                        self.weak_slots.len(),
244                    );
245                    self.weak_slots.get_unchecked_mut(widx as usize).1.set(None);
246                }
247            }
248
249            let dtype = unsafe { node.as_ref().dtype() } as usize;
250            let info = &self.node_dtypes.type_info_list[dtype];
251            let layout = info.layout();
252            let gross_size = layout.size();
253
254            #[cfg(debug_assertions)]
255            unsafe {
256                // Poison GcHead fields so any subsequent use-after-free is caught.
257                (*node.as_ptr()).attrs = 0xDEAD_BEEF;
258                (*node.as_ptr()).next = None;
259            }
260
261            #[cfg(feature = "gc_arena")]
262            {
263                let head = unsafe { node.as_ref() };
264                if head.contains_flag(crate::node::GcNodeFlag::ARENA_ALLOC) {
265                    // Arena-allocated node: memory is owned by GcArena,
266                    // which will be dropped when the partition is reset below.
267                    // Do NOT individually dealloc — that would be a double-free.
268                    #[cfg(debug_assertions)]
269                    {
270                        self.dbg_living_nodes.remove(&node.cast());
271                    }
272
273                    freed_bytes += gross_size;
274                    continue;
275                }
276            }
277
278            self.mem_dealloc(node.cast::<u8>(), layout);
279            freed_bytes += gross_size;
280        }
281
282        debug_assert!(
283            self.total_memory_used >= partition_mem,
284            "dealloc_partition: global memory underflow ({} < {})",
285            self.total_memory_used,
286            partition_mem,
287        );
288        self.total_memory_used -= partition_mem;
289
290        // Reset partition to fresh state (all nodes have been freed).
291        // Use reset_arena to preserve the original arena configuration.
292        #[cfg(feature = "gc_arena")]
293        self.partitions[partition_id.0 as usize].reset_arena();
294        self.partitions[partition_id.0 as usize].memory_used = 0;
295        self.partitions[partition_id.0 as usize].nodes = GcNodeLink::default();
296        self.partitions[partition_id.0 as usize].gray_list.clear();
297        self.partitions[partition_id.0 as usize].marking = false;
298
299        log::trace!("[dealloc_partition] freed {freed_bytes} bytes");
300
301        freed_bytes
302    }
303
304    /// Remove a partition and reclaim all its memory.
305    ///
306    /// ⚠️ **DEPRECATED** — This method is inherently unsound. It holds `&mut self`
307    /// while `Drop` callbacks run inside [`finalize_partition`], and those
308    /// callbacks can re-enter `GcHeap` through raw pointers, creating aliasing
309    /// `&mut` references (UB).
310    ///
311    /// **Use the two-phase API instead:**
312    ///
313    /// ```ignore
314    /// let link = gc_heap.finalize_partition(pid);
315    /// // ... (Drop callbacks that access GcHeap are safe here) ...
316    /// if let Some(link) = link {
317    ///     gc_heap.dealloc_partition(pid, link);
318    /// }
319    /// ```
320    ///
321    /// See [`finalize_partition`] (takes `&self`) and [`dealloc_partition`]
322    /// (takes `&mut self`) for details.
323    ///
324    /// The `on_dispose` parameter is kept for API compatibility but is no
325    /// longer called — `Drop` is handled internally by `finalize_partition`.
326    #[deprecated(
327        note = "unsound — use finalize_partition(&self) + dealloc_partition(&mut self) instead"
328    )]
329    pub fn remove_partition(
330        &mut self,
331        partition_id: GcPartitionId,
332        _on_dispose: impl Fn(&GcHeap, &GcHead),
333    ) -> usize {
334        let link = self.finalize_partition(partition_id);
335        link.map_or(0, |link| {
336            #[cfg(debug_assertions)]
337            {
338                self.dbg_dropping_root_partition = Some(partition_id);
339            }
340            let result = self.dealloc_partition(partition_id, link);
341            #[cfg(debug_assertions)]
342            {
343                self.dbg_dropping_root_partition = None;
344            }
345            result
346        })
347    }
348
349    /// Get partition information
350    #[inline(always)]
351    pub fn partition(&self, partition_id: GcPartitionId) -> Option<&GcPartition> {
352        self.partitions.get(partition_id.0 as usize)
353    }
354
355    /// Get partition information
356    #[inline(always)]
357    pub fn partition_mut(&mut self, partition_id: GcPartitionId) -> Option<&mut GcPartition> {
358        self.partitions.get_mut(partition_id.0 as usize)
359    }
360
361    /// Get all partition IDs
362    pub fn partition_ids(&self) -> Vec<GcPartitionId> {
363        (0..self.partitions.len())
364            .map(|i| GcPartitionId(i as u16))
365            .collect()
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::arena::{ARENA_CAPACITY, MAX_ARENA_ALLOC};
373
374    #[derive(Debug)]
375    struct DummyType;
376    impl crate::trace::GcTrace for DummyType {
377        fn trace(&self, _: &mut crate::trace::GcTraceCtx) {}
378    }
379
380    crate::gc_type_register! {
381        DummyType, drop_pass = 0;
382    }
383
384    #[test]
385    fn test_partition_creation() {
386        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
387        let id = heap.create_partition(ARENA_CAPACITY, MAX_ARENA_ALLOC);
388
389        let partition = heap.partition(id).unwrap();
390        assert_eq!(partition.memory_used(), 0);
391
392        // Clean up partition using two-phase API
393        let link = heap.finalize_partition(id).unwrap();
394        heap.dealloc_partition(id, link);
395        // After dealloc, partition is reset to a fresh state
396        assert_eq!(heap.partition(id).unwrap().memory_used(), 0);
397    }
398
399    #[test]
400    fn test_gc_threshold() {
401        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
402        heap.set_memory_limit(1024);
403
404        assert_eq!(heap.gc_threshold(), 0);
405
406        heap.set_gc_threshold(512);
407        assert_eq!(heap.gc_threshold(), 512);
408
409        heap.set_gc_threshold(2048);
410        assert_eq!(heap.gc_threshold(), 819);
411
412        heap.set_gc_threshold(0);
413        assert_eq!(heap.gc_threshold(), 0);
414    }
415
416    #[test]
417    fn test_memory_limit() {
418        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
419        let id = heap.create_partition(0, 0);
420
421        assert_eq!(heap.memory_limit(), 0);
422
423        // Simulate some allocations in a single partition
424        heap.update_mem_use(id, 100);
425        assert_eq!(heap.memory_limit(), 0);
426
427        // Set limit larger than used memory
428        let applied = heap.set_memory_limit(512);
429        assert_eq!(applied, 512);
430        assert_eq!(heap.memory_limit(), 512);
431
432        // Set limit smaller than used memory should clamp to used
433        let applied_small = heap.set_memory_limit(80);
434        assert_eq!(applied_small, 100);
435        assert_eq!(heap.memory_limit(), 100);
436
437        // Set unlimited
438        let applied_zero = heap.set_memory_limit(0);
439        assert_eq!(applied_zero, 0);
440        assert_eq!(heap.memory_limit(), 0);
441    }
442
443    #[test]
444    fn test_is_ancestor_of() {
445        // 已删除 ancestor 相关 API,此测试不再适用,保留空壳确保编译通过
446    }
447
448    #[test]
449    fn test_common_parent() {
450        // 已删除 common_parent 相关 API,此测试不再适用,保留空壳确保编译通过
451    }
452
453    #[test]
454    fn test_update_mem_use() {
455        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
456        let id = heap.create_partition(0, 0);
457
458        heap.update_mem_use(id, 100);
459        assert_eq!(heap.partition(id).unwrap().memory_used(), 100);
460        assert_eq!(heap.memory_used(), 100);
461
462        heap.update_mem_use(id, -20);
463        assert_eq!(heap.partition(id).unwrap().memory_used(), 80);
464        assert_eq!(heap.memory_used(), 80);
465
466        // Clean up using two-phase API
467        let link = heap.finalize_partition(id).unwrap();
468        heap.dealloc_partition(id, link);
469    }
470
471    #[test]
472    fn test_partition_id_serial_and_range() {
473        let id = GcPartitionId(10);
474        assert_eq!(id.0, 10);
475    }
476
477    #[test]
478    fn test_remove_partition_reclaims_memory_stats() {
479        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
480        let id = heap.create_partition(ARENA_CAPACITY, MAX_ARENA_ALLOC);
481
482        // Allocate some nodes
483        let _n1 = unsafe { heap.alloc_raw(id, DummyType) }.unwrap();
484        let _n2 = unsafe { heap.alloc_raw(id, DummyType) }.unwrap();
485
486        let used_before = heap.memory_used();
487        assert!(
488            used_before > 0,
489            "memory_used should be > 0 after allocations"
490        );
491
492        let p_used = heap.partition(id).unwrap().memory_used();
493        assert!(p_used > 0, "partition memory_used should be > 0");
494
495        // Remove partition — all its nodes should be disposed and memory reclaimed
496        let link = heap.finalize_partition(id).unwrap();
497        let freed = heap.dealloc_partition(id, link);
498        assert!(freed > 0, "should free some bytes");
499
500        // Partition is reset to fresh state
501        assert_eq!(heap.partition(id).unwrap().memory_used(), 0);
502        assert_eq!(heap.memory_used(), 0, "all memory should be reclaimed");
503    }
504}