Skip to main content

gc_lite/
mark_sweep.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::{
7    GcHeap,
8    node::{GcHead, GcNode, GcRef, GcTriColor},
9    node_link::{GcNodeLink, NodeLinkIter},
10    partition::GcPartitionId,
11    trace::GcTraceCtx,
12};
13
14#[cfg(feature = "gc_arena")]
15use crate::node::GcNodeFlag;
16
17impl GcHeap {
18    pub fn add_gray_node(&mut self, node: NonNull<GcHead>) {
19        if unsafe { node.as_ref().color() } != GcTriColor::Black {
20            let pid = unsafe { node.as_ref().partition_id() };
21            self.partitions[pid.0 as usize].add_gray_node(node);
22        }
23    }
24
25    /// Check whether the partition that `node` belongs to is currently marking.
26    #[inline]
27    pub(crate) unsafe fn is_node_partition_marking<T: GcNode>(&self, node: GcRef<T>) -> bool {
28        let pid = unsafe { node.head_ptr.as_ref().partition_id() };
29        self.partitions[pid.0 as usize].is_marking()
30    }
31
32    pub fn mark_reset(&mut self, partition_id: GcPartitionId) {
33        let par = &mut self.partitions[partition_id.0 as usize];
34        par.set_marking(false);
35
36        // Drain gray list and clear flags so stale GRAY_LISTED bits
37        // don't affect the next marking cycle.
38        for mut n in par.gray_list.drain(..) {
39            unsafe {
40                n.as_mut().set_gray_listed(false);
41            }
42        }
43
44        for n in par.nodes_mut() {
45            n.set_color(GcTriColor::White);
46        }
47    }
48
49    pub fn mark_prepare(&mut self, partition_id: GcPartitionId) {
50        if (partition_id.0 as usize) >= self.partitions.len() {
51            return;
52        }
53
54        // If the target partition is already marking, no-op (its gray_list
55        // is being processed by mark_grays).
56        if self.partitions[partition_id.0 as usize].is_marking() {
57            return;
58        }
59
60        // Set ALL partitions to marking mode so that cross-partition
61        // references can be pushed into any partition's gray_list.
62        for par in &mut self.partitions {
63            par.set_marking(true);
64        }
65
66        // Seed roots from ALL partitions.
67        for i in 0..self.partitions.len() {
68            let has_pending_grays = !self.partitions[i].gray_list.is_empty();
69            let par = &mut self.partitions[i];
70
71            for mut n in par.nodes.iter() {
72                let node = unsafe { n.as_mut() };
73                if node.is_root_or_local() {
74                    node.set_color(GcTriColor::Gray);
75                    if !has_pending_grays {
76                        par.gray_list.push(n);
77                    }
78                } else if !has_pending_grays {
79                    node.set_color(GcTriColor::White);
80                }
81            }
82
83            if has_pending_grays {
84                // Pending gray nodes already exist; only add root/LOCAL
85                // nodes that are not already in the gray list.
86                for mut n in par.nodes.iter() {
87                    let node = unsafe { n.as_mut() };
88                    if node.is_root_or_local() {
89                        node.set_color(GcTriColor::Gray);
90                        if !node.is_gray_listed() {
91                            node.set_gray_listed(true);
92                            par.gray_list.push(n);
93                        }
94                    }
95                }
96            }
97        }
98    }
99
100    pub fn mark_grays(&mut self, partition_id: GcPartitionId, max_steps: usize) -> bool {
101        if max_steps == 0 {
102            return false;
103        }
104
105        // Pre-acquire the opaque pointer and type registry so we don't need &self
106        // while holding a mutable borrow on the partition.
107        let opaque = self.opaque();
108        let node_dtypes: *const crate::gctype::GcTypeRegistry = self.node_dtypes;
109
110        if !self.partitions[partition_id.0 as usize]
111            .gray_list
112            .is_empty()
113        {
114            let mut gcx = GcTraceCtx {
115                traced_nodes: Vec::with_capacity(64),
116                opaque,
117                _mark: std::marker::PhantomData,
118            };
119
120            // Buffer for children that need to be pushed to a different
121            // partition's gray_list (avoids borrow-checker conflicts when
122            // indexing self.partitions at different indices).
123            let mut cross_buffer: Vec<(u16, NonNull<GcHead>)> = Vec::with_capacity(8);
124            let mut cnt = 0;
125
126            while let Some(mut node_ptr) = self.partitions[partition_id.0 as usize].gray_list.pop()
127            {
128                let node = unsafe { node_ptr.as_mut() };
129                // Clear gray_listed flag when popping (O(1) instead of O(n) contains)
130                node.set_gray_listed(false);
131
132                if node.color() == GcTriColor::Gray {
133                    if cnt >= max_steps {
134                        self.partitions[partition_id.0 as usize]
135                            .gray_list
136                            .push(node_ptr);
137                        return false;
138                    }
139
140                    // SAFETY: node_dtypes is &'static, and we hold &mut self so the
141                    // registry is guaranteed to be alive.
142                    debug_assert!(
143                        gcx.traced_nodes.is_empty(),
144                        "trace context should be empty before tracing a new node"
145                    );
146                    gcx.traced_nodes.clear();
147
148                    unsafe {
149                        let dtype = node_ptr.as_ref().dtype() as usize;
150                        let info = &(*node_dtypes).type_info_list[dtype];
151                        (info.trace_fn)(node_ptr, &mut gcx);
152                    }
153
154                    while let Some(mut ch) = gcx.traced_nodes.pop() {
155                        let child = unsafe { ch.as_mut() };
156
157                        #[cfg(debug_assertions)]
158                        child.debug_assert_node_valid_simple();
159
160                        if matches!(child.color(), GcTriColor::White | GcTriColor::Gray) {
161                            child.set_color(GcTriColor::Gray);
162                            child.set_gray_listed(true);
163
164                            let child_pid = child.partition_id();
165                            if child_pid == partition_id {
166                                // Same partition: push directly
167                                self.partitions[partition_id.0 as usize].gray_list.push(ch);
168                            } else {
169                                // Cross-partition child: buffer and push after
170                                // the inner loop so we don't contend with the
171                                // while-let borrow on self.partitions[pid].
172                                cross_buffer.push((child_pid.0, ch));
173                            }
174                        }
175                    }
176
177                    // Flush cross-partition children to their own partitions
178                    for (cpid, node) in cross_buffer.drain(..) {
179                        self.partitions[cpid as usize].gray_list.push(node);
180                    }
181
182                    // Mark current node as black.
183                    node.set_color(GcTriColor::Black);
184
185                    cnt += 1;
186                }
187            }
188        }
189
190        true
191    }
192
193    pub fn mark(&mut self, partition_id: GcPartitionId, max_steps: usize) -> bool {
194        self.mark_prepare(partition_id);
195        if max_steps > 0 {
196            self.mark_grays(partition_id, max_steps)
197        } else {
198            false
199        }
200    }
201
202    /// dispose white nodes in the partition.
203    /// `on_dispose` is called BEFORE a node will be disposed.
204    pub fn sweep(
205        &mut self,
206        partition_id: GcPartitionId,
207        on_dispose: impl Fn(&GcHeap, &GcHead),
208    ) -> usize {
209        if (partition_id.0 as usize) >= self.partitions.len() {
210            return 0;
211        }
212        if let Some(link0) = {
213            let par = &mut self.partitions[partition_id.0 as usize];
214            if par.is_marking() && par.gray_list.is_empty() {
215                par.set_marking(false);
216                std::mem::take(&mut par.nodes).into_inner()
217            } else {
218                None // mark cycle not done
219            }
220        } {
221            #[cfg(debug_assertions)]
222            for n in NodeLinkIter::new(Some(link0)) {
223                unsafe {
224                    debug_assert!(
225                        matches!(n.as_ref().color(), GcTriColor::Black | GcTriColor::White),
226                        "sweep node must be either black or white: {:?}",
227                        n.as_ref()
228                    );
229                }
230            }
231
232            let call_on_dispose = !std::ptr::addr_eq(&on_dispose, &Self::DUMMY_DISPOSE_CALLBACK);
233            let mut link1 = Some(link0);
234            let mut freed_bytes = 0;
235
236            #[cfg(feature = "gc_arena")]
237            let mut holes: Vec<crate::arena::Hole> = Vec::new();
238
239            for &pass in self.node_dtypes.drop_passes {
240                let mut current = link1;
241                let mut prev: Option<NonNull<GcHead>> = None;
242
243                while let Some(mut this) = current {
244                    unsafe {
245                        #[cfg(debug_assertions)]
246                        this.as_ref().debug_assert_node_valid(self);
247
248                        current = this.as_mut().next;
249
250                        let drop_pass = self.node_dtypes.type_info_list
251                            [this.as_ref().dtype() as usize]
252                            .drop_pass;
253
254                        if drop_pass == pass
255                            && this.as_ref().color() == GcTriColor::White
256                            && !this.as_ref().is_root_or_local()
257                        {
258                            if let Some(mut p) = prev {
259                                p.as_mut().next = current;
260                            } else {
261                                link1 = current;
262                            }
263
264                            if call_on_dispose {
265                                on_dispose(self, this.as_ref());
266                            }
267
268                            // Cache arena info before dispose poisons GcHead
269                            #[cfg(feature = "gc_arena")]
270                            let arena_info = {
271                                let hd = this.as_ref();
272                                let is_arena = hd.contains_flag(GcNodeFlag::ARENA_ALLOC);
273                                let dtype = hd.dtype() as usize;
274                                let info = &self.node_dtypes.type_info_list[dtype];
275                                (is_arena, info.layout().size())
276                            };
277
278                            freed_bytes += self.dispose(this);
279
280                            // Arena hole collection (after dispose, which skips mem_dealloc)
281                            #[cfg(feature = "gc_arena")]
282                            if arena_info.0 {
283                                // SAFETY: an arena-allocated node exists only if the
284                                // partition has an arena — this is an invariant.
285                                self.partitions[partition_id.0 as usize]
286                                    .arena
287                                    .as_ref()
288                                    .unwrap()
289                                    .collect_hole(
290                                        &mut holes,
291                                        this.cast::<u8>(),
292                                        arena_info.1,
293                                    );
294                            }
295                        } else {
296                            prev = Some(this);
297                        }
298                    }
299                }
300
301                if link1.is_none() {
302                    break;
303                }
304            }
305
306            #[cfg(feature = "gc_arena")]
307            if let Some(ref arena) = self.partitions[partition_id.0 as usize].arena {
308                arena.finish_sweep(&mut holes);
309            }
310
311            debug_assert!(
312                self.partitions[partition_id.0 as usize]
313                    .gray_list
314                    .is_empty()
315            );
316
317            // update remainder node link of partition
318            if link1.is_some() {
319                #[cfg(debug_assertions)]
320                for n in NodeLinkIter::new(link1) {
321                    unsafe {
322                        debug_assert!(
323                            n.as_ref().color() == GcTriColor::Black
324                                || n.as_ref().is_root_or_local(),
325                            "live nodes should be black, root or protected"
326                        );
327                    }
328                }
329
330                self.partitions[partition_id.0 as usize].nodes =
331                    crate::node_link::GcNodeLink::new(link1);
332            }
333
334            // Memory accounting is already handled by dispose() which calls
335            // update_mem_use() for each individual node. No need to subtract
336            // freed_bytes again here.
337            freed_bytes
338        } else {
339            0
340        }
341    }
342
343    /// Collect garbage on given partition, call notify with node *BEFORE* it is disposed.
344    #[inline]
345    pub fn garbage_collect(
346        &mut self,
347        partition_id: GcPartitionId,
348        on_dispose: impl Fn(&GcHeap, &GcHead),
349    ) -> usize {
350        if self.partition(partition_id).is_none() {
351            return 0;
352        }
353
354        // Mark phase: incrementally process gray list until all reachable nodes are marked
355        while !self.mark(partition_id, 64) {}
356
357        // Sweep phase: reclaim unmarked (white) nodes
358        self.sweep(partition_id, on_dispose)
359    }
360
361    /// Dispose all nodes along chain
362    pub(crate) fn dispose_all_nodes(
363        &mut self,
364        mut link: GcNodeLink,
365        on_dispose: impl Fn(&GcHeap, &GcHead),
366    ) -> usize {
367        let call_on_dispose = !std::ptr::addr_eq(&on_dispose, &Self::DUMMY_DISPOSE_CALLBACK);
368        let mut freed_bytes = 0;
369
370        let pass_slice = self.node_dtypes.drop_passes;
371        for &pass in pass_slice {
372            log::trace!("[dipose_all] pass {pass}, count={}", link.len());
373
374            let self_ptr: *mut GcHeap = self;
375
376            link.filter_remove_with(
377                |node| {
378                    #[cfg(debug_assertions)]
379                    unsafe {
380                        node.debug_assert_node_valid(&*self_ptr);
381                    }
382
383                    let dtype = node.dtype() as usize;
384                    let info = unsafe { &(*self_ptr).node_dtypes.type_info_list[dtype] };
385                    info.drop_pass == pass
386                },
387                |node_ptr| unsafe {
388                    if call_on_dispose {
389                        on_dispose(&*self_ptr, node_ptr.as_ref());
390                    }
391
392                    freed_bytes += (&mut *self_ptr).dispose(node_ptr);
393                },
394            );
395
396            if link.head().is_none() {
397                break;
398            }
399        }
400
401        debug_assert!(
402            link.head().is_none(),
403            "dispose_all_nodes: link still has nodes after disposal",
404        );
405        log::trace!("[dipose_all] done, freed {} bytes", freed_bytes);
406
407        freed_bytes
408    }
409}
410
411#[cfg(test)]
412mod sweep_test {
413    use super::*;
414    use crate::{GcNode, GcRef};
415
416    use crate::trace::{GcTrace, GcTraceCtx};
417
418    /// Shorthand for creating a partition with default arena config.
419    fn create_default_partition(heap: &mut crate::GcHeap) -> GcPartitionId {
420        heap.create_partition(
421            crate::arena::ARENA_CAPACITY,
422            crate::arena::MAX_ARENA_ALLOC,
423        )
424    }
425
426    #[derive(Debug)]
427    struct MyI32(i32);
428
429    impl GcTrace for MyI32 {
430        fn trace(&self, _: &mut GcTraceCtx) {}
431    }
432
433    crate::gc_type_register! {
434        MyI32, drop_pass = 0;
435    }
436
437    /// Helper function to count nodes in a partition
438    fn count_nodes_in_partition(heap: &GcHeap, partition_id: GcPartitionId) -> usize {
439        heap.nodes(partition_id).count()
440    }
441
442    /// Helper function to get all node pointers in a partition
443    fn get_all_nodes_in_partition(
444        heap: &GcHeap,
445        partition_id: GcPartitionId,
446    ) -> Vec<NonNull<GcHead>> {
447        heap.nodes(partition_id).collect()
448    }
449
450    /// Test basic sweep functionality
451    #[test]
452    fn test_sweep_with_basic() {
453        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
454        let partition_id = create_default_partition(&mut heap);
455
456        let objects: Vec<GcRef<MyI32>> = (0..5)
457            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
458            .collect();
459
460        for (i, obj) in objects.iter().enumerate() {
461            if i % 2 == 1 {
462                unsafe {
463                    let head = obj.head_ptr.as_ptr();
464                    let attrs = (*head).attrs | crate::node::GcNodeFlag::ROOT.bits() as u32;
465                    std::ptr::write(&mut (*head).attrs, attrs);
466                }
467            }
468        }
469
470        assert_eq!(count_nodes_in_partition(&heap, partition_id), 5);
471
472        while !heap.mark(partition_id, 64) {}
473
474        let removed = heap.sweep(partition_id, |_, _| {});
475        assert!(removed > 0, "Should have freed some bytes");
476
477        assert_eq!(
478            count_nodes_in_partition(&heap, partition_id),
479            2,
480            "Only root nodes should remain"
481        );
482
483        let remaining_nodes = get_all_nodes_in_partition(&heap, partition_id);
484        let info = &GC_TYPE_REGISTRY.type_info_list[MyI32::GC_TYPE_ID as usize];
485        for node in remaining_nodes {
486            unsafe {
487                let payload_ptr = info.payload_ptr(node);
488                let value = *(payload_ptr.as_ptr() as *const i32);
489                assert_eq!(value % 2, 1, "Remaining nodes should have odd values");
490            }
491        }
492    }
493
494    /// Test removing chain head nodes (n个节点被剔除后)
495    #[test]
496    fn test_sweep_with_chain_head_removal() {
497        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
498        let partition_id = create_default_partition(&mut heap);
499
500        let objects: Vec<GcRef<MyI32>> = (0..5)
501            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
502            .collect();
503
504        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(3)) }.unwrap();
505        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(4)) }.unwrap();
506
507        while !heap.mark(partition_id, 64) {}
508
509        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
510
511        assert!(removed > 0, "Should have freed some bytes");
512
513        // Should have 2 nodes left (3 and 4)
514        assert_eq!(count_nodes_in_partition(&heap, partition_id), 2);
515
516        // Verify chain head is now the node with value 4
517        let head = heap.partitions[partition_id.0 as usize].nodes.head();
518        assert!(head.is_some(), "Chain head should exist");
519
520        unsafe {
521            let info = &GC_TYPE_REGISTRY.type_info_list[MyI32::GC_TYPE_ID as usize];
522            let payload_ptr = info.payload_ptr(head.unwrap());
523            let value = (*(payload_ptr.as_ptr() as *const MyI32)).0;
524            assert_eq!(
525                value, 4,
526                "Chain head should be value 4 (last allocated, first in chain)"
527            );
528        }
529
530        // Verify the chain is properly linked
531        let nodes = get_all_nodes_in_partition(&heap, partition_id);
532        assert_eq!(nodes.len(), 2);
533
534        unsafe {
535            let info = &GC_TYPE_REGISTRY.type_info_list[MyI32::GC_TYPE_ID as usize];
536            let payload_ptr1 = info.payload_ptr(nodes[0]).cast::<MyI32>();
537            let value1 = (*payload_ptr1.as_ptr()).0;
538            assert_eq!(value1, 4);
539
540            let payload_ptr2 = info.payload_ptr(nodes[1]).cast::<MyI32>();
541            let value2 = (*payload_ptr2.as_ptr()).0;
542            assert_eq!(value2, 3);
543        }
544    }
545
546    /// Test removing all chain head nodes (连续剔除所有链头节点)
547    #[test]
548    fn test_sweep_with_all_chain_head_removal() {
549        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
550        let partition_id = create_default_partition(&mut heap);
551
552        let _objects: Vec<GcRef<MyI32>> = (0..3)
553            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
554            .collect();
555
556        while !heap.mark(partition_id, 64) {}
557
558        let removed = heap.sweep(partition_id, |_, n| {
559            println!("dispose {n:?}");
560        });
561
562        assert!(removed > 0, "Should have freed some bytes");
563
564        // Should have 0 nodes left
565        assert_eq!(count_nodes_in_partition(&heap, partition_id), 0);
566
567        // Chain head should be None
568        let head = heap.partitions[partition_id.0 as usize].nodes.head();
569        assert!(
570            head.is_none(),
571            "Chain head should be None after removing all nodes"
572        );
573    }
574
575    /// Test removing middle nodes
576    #[test]
577    fn test_sweep_with_middle_node_removal() {
578        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
579        let partition_id = create_default_partition(&mut heap);
580
581        let _objects: Vec<GcRef<MyI32>> = (0..5)
582            .map(|i| {
583                if i != 2 {
584                    unsafe { heap.alloc_root_raw(partition_id, MyI32(i)) }.unwrap()
585                } else {
586                    unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap()
587                }
588            })
589            .collect();
590
591        while !heap.mark(partition_id, 64) {}
592
593        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
594
595        assert!(removed > 0, "Should have freed some bytes");
596
597        // Should have 4 nodes left
598        assert_eq!(count_nodes_in_partition(&heap, partition_id), 4);
599
600        // Verify chain is still properly linked
601        let nodes = get_all_nodes_in_partition(&heap, partition_id);
602        assert_eq!(nodes.len(), 4);
603
604        let expected_values = [4, 3, 1, 0];
605        let info = &GC_TYPE_REGISTRY.type_info_list[MyI32::GC_TYPE_ID as usize];
606        for (i, node) in nodes.iter().enumerate() {
607            unsafe {
608                let payload_ptr = info.payload_ptr(*node);
609                let value = (*(payload_ptr.as_ptr() as *const MyI32)).0;
610                assert_eq!(
611                    value, expected_values[i],
612                    "Node at position {} should have value {}",
613                    i, expected_values[i]
614                );
615            }
616        }
617    }
618
619    /// Test removing root nodes
620    #[test]
621    fn test_sweep_with_root_node_removal() {
622        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
623        let partition_id = create_default_partition(&mut heap);
624
625        let root_obj = unsafe { heap.alloc_raw(partition_id, MyI32(0)) }.unwrap();
626        let _objects: Vec<GcRef<MyI32>> = (1..3)
627            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
628            .collect();
629
630        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(1)) }.unwrap();
631
632        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(1)) }.unwrap();
633
634        while !heap.mark(partition_id, 64) {}
635
636        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
637
638        assert!(removed > 0, "Should have freed some bytes");
639
640        // Should have 2 nodes left
641        assert_eq!(count_nodes_in_partition(&heap, partition_id), 2);
642
643        let remaining = get_all_nodes_in_partition(&heap, partition_id);
644        assert!(!remaining.contains(&root_obj.head_ptr));
645    }
646
647    /// Test empty partition
648    #[test]
649    fn test_sweep_with_empty_partition() {
650        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
651        let partition_id = create_default_partition(&mut heap);
652
653        while !heap.mark(partition_id, 64) {}
654
655        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
656        assert_eq!(removed, 0, "Should return 0 for empty partition");
657    }
658
659    /// Test non-existent partition
660    #[test]
661    fn test_sweep_with_nonexistent_partition() {
662        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
663        let non_existent_partition = GcPartitionId(9999);
664
665        let removed = heap.sweep(non_existent_partition, GcHeap::DUMMY_DISPOSE_CALLBACK);
666        assert_eq!(removed, 0, "Should return 0 for non-existent partition");
667    }
668}