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, GcTriColor},
9    node_link::{GcNodeLink, NodeLinkIter},
10    partition::GcPartitionId,
11};
12
13impl GcHeap {
14    pub fn add_gray_node(&mut self, node: NonNull<GcHead>) {
15        if unsafe { node.as_ref().color() } != GcTriColor::Black {
16            self.partition_mut(unsafe { node.as_ref().partition_id() })
17                .unwrap()
18                .add_gray_node(node);
19        }
20    }
21
22    pub fn mark_reset(&mut self, partition_id: GcPartitionId) {
23        if let Some(par) = self.partition_mut(partition_id) {
24            par.set_marking(false);
25            par.gray_list.clear();
26            for n in par.nodes_mut() {
27                n.set_color(GcTriColor::White);
28            }
29        }
30    }
31
32    /// ensure marking is in progress,
33    /// if marking is in progress, exit do nothing;
34    /// if marking cycle is not started, start new cycle.
35    pub fn mark_prepare(&mut self, partition_id: GcPartitionId) {
36        if let Some(par) = self.partitions.get_mut(&partition_id)
37            && !par.is_marking()
38        {
39            debug_assert!(par.gray_list.is_empty());
40
41            par.set_marking(true);
42
43            for mut n in par.nodes.iter() {
44                let node = unsafe { n.as_mut() };
45                if node.is_root_or_local() {
46                    node.set_color(GcTriColor::Gray);
47                    par.gray_list.push(n);
48                } else {
49                    node.set_color(GcTriColor::White);
50                }
51            }
52        }
53    }
54
55    pub fn mark_grays(&mut self, partition_id: GcPartitionId, max_steps: usize) -> bool {
56        if max_steps == 0 {
57            return false;
58        }
59
60        let heap_ptr = self as *mut Self;
61
62        if let Some(par) = self.partitions.get_mut(&partition_id)
63            && !par.gray_list.is_empty()
64        {
65            let mut gcx = unsafe { (*heap_ptr).create_trace_ctx(64) };
66            let mut cnt = 0;
67
68            while let Some(mut node_ptr) = par.gray_list.pop() {
69                let node = unsafe { node_ptr.as_mut() };
70                debug_assert_eq!(node.partition_id(), partition_id);
71
72                if node.color() == GcTriColor::Gray {
73                    if cnt >= max_steps {
74                        par.gray_list.push(node_ptr);
75                        return false;
76                    }
77
78                    unsafe {
79                        (*heap_ptr).trace_node(node_ptr, &mut gcx);
80                    }
81
82                    while let Some(mut ch) = gcx.traced_nodes.pop() {
83                        let child = unsafe { ch.as_mut() };
84
85                        #[cfg(debug_assertions)]
86                        child.debug_assert_node_valid_simple();
87
88                        let pid = child.partition_id();
89                        if pid == partition_id {
90                            if matches!(child.color(), GcTriColor::White | GcTriColor::Gray) {
91                                child.set_color(GcTriColor::Gray);
92                                par.gray_list.push(ch);
93                            }
94                        } else {
95                            let p2 = unsafe { (*heap_ptr).partition(pid).unwrap() };
96                            if p2.is_marking() {
97                                unsafe {
98                                    (*heap_ptr).add_gray_node(ch);
99                                }
100                            }
101                        }
102                    }
103
104                    // Mark current node as black.
105                    node.set_color(GcTriColor::Black);
106
107                    cnt += 1;
108                }
109            }
110        }
111
112        true
113    }
114
115    pub fn mark(&mut self, partition_id: GcPartitionId, max_steps: usize) -> bool {
116        self.mark_prepare(partition_id);
117        if max_steps > 0 {
118            self.mark_grays(partition_id, max_steps)
119        } else {
120            false
121        }
122    }
123
124    /// dispose white nodes in the partition
125    pub fn sweep(
126        &mut self,
127        partition_id: GcPartitionId,
128        on_dispose: impl Fn(&GcHeap, &GcHead),
129    ) -> usize {
130        if let Some(link0) = self.partition_mut(partition_id).and_then(|p| {
131            if p.is_marking() && p.gray_list.is_empty() {
132                p.set_marking(false);
133                std::mem::take(&mut p.nodes).into_inner()
134            } else {
135                None // mark cycle not done
136            }
137        }) {
138            #[cfg(debug_assertions)]
139            for n in NodeLinkIter::new(Some(link0)) {
140                unsafe {
141                    debug_assert!(
142                        matches!(n.as_ref().color(), GcTriColor::Black | GcTriColor::White),
143                        "sweep node must be either black or white: {:?}",
144                        n.as_ref()
145                    );
146                }
147            }
148
149            let call_on_dispose = !std::ptr::addr_eq(&on_dispose, &Self::DUMMY_DISPOSE_CALLBACK);
150            let mut link1 = Some(link0);
151            let mut freed_bytes = 0;
152
153            for &pass in self.node_dtypes.drop_passes {
154                let mut current = link1;
155                let mut prev: Option<NonNull<GcHead>> = None;
156
157                while let Some(mut this) = current {
158                    unsafe {
159                        #[cfg(debug_assertions)]
160                        this.as_ref().debug_assert_node_valid(self);
161
162                        current = this.as_mut().next;
163
164                        let drop_pass = self.node_dtypes.type_info_list
165                            [this.as_ref().dtype() as usize]
166                            .drop_pass;
167
168                        if drop_pass == pass
169                            && this.as_ref().color() == GcTriColor::White
170                            && !this.as_ref().is_root_or_local()
171                        {
172                            if let Some(mut p) = prev {
173                                p.as_mut().next = current;
174                            } else {
175                                link1 = current;
176                            }
177
178                            if call_on_dispose {
179                                on_dispose(self, this.as_ref());
180                            }
181                            freed_bytes += self.dispose(this);
182                        } else {
183                            prev = Some(this);
184                        }
185                    }
186                }
187
188                if link1.is_none() {
189                    break;
190                }
191            }
192
193            debug_assert!(
194                self.partition_mut(partition_id)
195                    .unwrap()
196                    .gray_list
197                    .is_empty()
198            );
199
200            // update remainder node link of partition
201            if link1.is_some() {
202                #[cfg(debug_assertions)]
203                for n in NodeLinkIter::new(link1) {
204                    unsafe {
205                        debug_assert!(
206                            n.as_ref().color() == GcTriColor::Black
207                                || n.as_ref().is_root_or_local(),
208                            "live nodes should be black, root or protected"
209                        );
210                    }
211                }
212
213                let p = self.partition_mut(partition_id).unwrap();
214                p.nodes = crate::node_link::GcNodeLink::new(link1);
215            }
216
217            // Decrease partitions memory usage
218            if freed_bytes != 0 {
219                self.update_mem_use(partition_id, -(freed_bytes as i32));
220            }
221
222            freed_bytes
223        } else {
224            0
225        }
226    }
227
228    /// Collect garbage on given partition, call notify with node *BEFORE* it is disposed.
229    #[inline]
230    pub fn garbage_collect(
231        &mut self,
232        partition_id: GcPartitionId,
233        on_dispose: impl Fn(&GcHeap, &GcHead),
234    ) -> usize {
235        if self.partition(partition_id).is_none() {
236            return 0;
237        }
238
239        // Mark phase: incrementally process gray list until all reachable nodes are marked
240        while !self.mark(partition_id, 64) {}
241
242        // Sweep phase: reclaim unmarked (white) nodes
243        self.sweep(partition_id, on_dispose)
244    }
245
246    /// Dispose all nodes along chain
247    pub(crate) fn dispose_all_nodes(
248        &mut self,
249        mut link: GcNodeLink,
250        on_dispose: impl Fn(&GcHeap, &GcHead),
251    ) -> usize {
252        let call_on_dispose = !std::ptr::addr_eq(&on_dispose, &Self::DUMMY_DISPOSE_CALLBACK);
253        let mut freed_bytes = 0;
254
255        let pass_slice = self.node_dtypes.drop_passes;
256        for &pass in pass_slice {
257            log::trace!("[dipose_all] pass {pass}, count={}", link.len());
258
259            let self_ptr: *mut GcHeap = self;
260
261            link.filter_remove_with(
262                |node| {
263                    #[cfg(debug_assertions)]
264                    unsafe {
265                        node.debug_assert_node_valid(&*self_ptr);
266                    }
267
268                    let dtype = node.dtype() as usize;
269                    let info = unsafe { &(*self_ptr).node_dtypes.type_info_list[dtype] };
270                    info.drop_pass == pass
271                },
272                |node_ptr| unsafe {
273                    if call_on_dispose {
274                        on_dispose(&*self_ptr, node_ptr.as_ref());
275                    }
276
277                    freed_bytes += (&mut *self_ptr).dispose(node_ptr);
278                },
279            );
280
281            if link.head().is_none() {
282                break;
283            }
284        }
285
286        debug_assert!(link.head().is_none());
287        log::trace!("[dipose_all] done, freed {} bytes", freed_bytes);
288
289        freed_bytes
290    }
291}
292
293#[cfg(test)]
294mod sweep_test {
295    use super::*;
296    use crate::GcRef;
297
298    use crate::trace::{GcTrace, GcTraceCtx};
299
300    #[derive(Debug)]
301    struct MyI32(i32);
302
303    impl GcTrace for MyI32 {
304        fn trace(&self, _: &mut GcTraceCtx) {}
305    }
306
307    crate::gc_type_register! {
308        MyI32, drop_pass = 0;
309    }
310
311    /// Helper function to count nodes in a partition
312    fn count_nodes_in_partition(heap: &GcHeap, partition_id: GcPartitionId) -> usize {
313        heap.nodes(partition_id).count()
314    }
315
316    /// Helper function to get all node pointers in a partition
317    fn get_all_nodes_in_partition(
318        heap: &GcHeap,
319        partition_id: GcPartitionId,
320    ) -> Vec<NonNull<GcHead>> {
321        heap.nodes(partition_id).collect()
322    }
323
324    /// Test basic sweep functionality
325    #[test]
326    fn test_sweep_with_basic() {
327        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
328        let partition_id = heap.create_partition();
329
330        let objects: Vec<GcRef<MyI32>> = (0..5)
331            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
332            .collect();
333
334        for (i, obj) in objects.iter().enumerate() {
335            if i % 2 == 1 {
336                unsafe {
337                    let head = obj.head_ptr.as_ptr();
338                    let attrs = (*head).attrs | crate::node::GcNodeFlag::ROOT.bits() as u32;
339                    std::ptr::write(&mut (*head).attrs, attrs);
340                }
341            }
342        }
343
344        assert_eq!(count_nodes_in_partition(&heap, partition_id), 5);
345
346        while !heap.mark(partition_id, 64) {}
347
348        let removed = heap.sweep(partition_id, |_, _| {});
349        assert!(removed > 0, "Should have freed some bytes");
350
351        assert_eq!(
352            count_nodes_in_partition(&heap, partition_id),
353            2,
354            "Only root nodes should remain"
355        );
356
357        let remaining_nodes = get_all_nodes_in_partition(&heap, partition_id);
358        for node in remaining_nodes {
359            unsafe {
360                let payload_ptr = (node.as_ptr() as *mut u8).add(std::mem::size_of::<GcHead>());
361                let value = *(payload_ptr as *const i32);
362                assert_eq!(value % 2, 1, "Remaining nodes should have odd values");
363            }
364        }
365    }
366
367    /// Test removing chain head nodes (n个节点被剔除后)
368    #[test]
369    fn test_sweep_with_chain_head_removal() {
370        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
371        let partition_id = heap.create_partition();
372
373        let objects: Vec<GcRef<MyI32>> = (0..5)
374            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
375            .collect();
376
377        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(3)) }.unwrap();
378        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(4)) }.unwrap();
379
380        while !heap.mark(partition_id, 64) {}
381
382        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
383
384        assert!(removed > 0, "Should have freed some bytes");
385
386        // Should have 2 nodes left (3 and 4)
387        assert_eq!(count_nodes_in_partition(&heap, partition_id), 2);
388
389        // Verify chain head is now the node with value 4
390        let head = heap
391            .partitions
392            .get(&partition_id)
393            .and_then(|p| p.nodes.head());
394        assert!(head.is_some(), "Chain head should exist");
395
396        unsafe {
397            let payload_ptr =
398                (head.unwrap().as_ptr() as *mut u8).add(std::mem::size_of::<GcHead>());
399            let value = (*(payload_ptr as *const MyI32)).0;
400            assert_eq!(
401                value, 4,
402                "Chain head should be value 4 (last allocated, first in chain)"
403            );
404        }
405
406        // Verify the chain is properly linked
407        let nodes = get_all_nodes_in_partition(&heap, partition_id);
408        assert_eq!(nodes.len(), 2);
409
410        unsafe {
411            let payload_ptr1 = nodes[0].as_ref().payload().cast::<MyI32>();
412            let value1 = (*payload_ptr1.as_ptr()).0;
413            assert_eq!(value1, 4);
414
415            let payload_ptr2 = nodes[1].as_ref().payload().cast::<MyI32>();
416            let value2 = (*payload_ptr2.as_ptr()).0;
417            assert_eq!(value2, 3);
418        }
419    }
420
421    /// Test removing all chain head nodes (连续剔除所有链头节点)
422    #[test]
423    fn test_sweep_with_all_chain_head_removal() {
424        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
425        let partition_id = heap.create_partition();
426
427        let _objects: Vec<GcRef<MyI32>> = (0..3)
428            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
429            .collect();
430
431        while !heap.mark(partition_id, 64) {}
432
433        let removed = heap.sweep(partition_id, |_, n| {
434            println!("dispose {n:?}");
435        });
436
437        assert!(removed > 0, "Should have freed some bytes");
438
439        // Should have 0 nodes left
440        assert_eq!(count_nodes_in_partition(&heap, partition_id), 0);
441
442        // Chain head should be None
443        let head = heap
444            .partitions
445            .get(&partition_id)
446            .and_then(|p| p.nodes.head());
447        assert!(
448            head.is_none(),
449            "Chain head should be None after removing all nodes"
450        );
451    }
452
453    /// Test removing middle nodes
454    #[test]
455    fn test_sweep_with_middle_node_removal() {
456        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
457        let partition_id = heap.create_partition();
458
459        let _objects: Vec<GcRef<MyI32>> = (0..5)
460            .map(|i| {
461                if i != 2 {
462                    unsafe { heap.alloc_root_raw(partition_id, MyI32(i)) }.unwrap()
463                } else {
464                    unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap()
465                }
466            })
467            .collect();
468
469        while !heap.mark(partition_id, 64) {}
470
471        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
472
473        assert!(removed > 0, "Should have freed some bytes");
474
475        // Should have 4 nodes left
476        assert_eq!(count_nodes_in_partition(&heap, partition_id), 4);
477
478        // Verify chain is still properly linked
479        let nodes = get_all_nodes_in_partition(&heap, partition_id);
480        assert_eq!(nodes.len(), 4);
481
482        let expected_values = [4, 3, 1, 0];
483        for (i, node) in nodes.iter().enumerate() {
484            unsafe {
485                let payload_ptr = (node.as_ptr() as *mut u8).add(std::mem::size_of::<GcHead>());
486                let value = (*(payload_ptr as *const MyI32)).0;
487                assert_eq!(
488                    value, expected_values[i],
489                    "Node at position {} should have value {}",
490                    i, expected_values[i]
491                );
492            }
493        }
494    }
495
496    /// Test removing root nodes
497    #[test]
498    fn test_sweep_with_root_node_removal() {
499        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
500        let partition_id = heap.create_partition();
501
502        let root_obj = unsafe { heap.alloc_raw(partition_id, MyI32(0)) }.unwrap();
503        let _objects: Vec<GcRef<MyI32>> = (1..3)
504            .map(|i| unsafe { heap.alloc_raw(partition_id, MyI32(i)) }.unwrap())
505            .collect();
506
507        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(1)) }.unwrap();
508
509        let _ = unsafe { heap.alloc_root_raw(partition_id, MyI32(1)) }.unwrap();
510
511        while !heap.mark(partition_id, 64) {}
512
513        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
514
515        assert!(removed > 0, "Should have freed some bytes");
516
517        // Should have 2 nodes left
518        assert_eq!(count_nodes_in_partition(&heap, partition_id), 2);
519
520        let remaining = get_all_nodes_in_partition(&heap, partition_id);
521        assert!(!remaining.contains(&root_obj.head_ptr));
522    }
523
524    /// Test empty partition
525    #[test]
526    fn test_sweep_with_empty_partition() {
527        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
528        let partition_id = heap.create_partition();
529
530        while !heap.mark(partition_id, 64) {}
531
532        let removed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
533        assert_eq!(removed, 0, "Should return 0 for empty partition");
534    }
535
536    /// Test non-existent partition
537    #[test]
538    fn test_sweep_with_nonexistent_partition() {
539        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
540        let non_existent_partition = GcPartitionId(9999);
541
542        let removed = heap.sweep(non_existent_partition, GcHeap::DUMMY_DISPOSE_CALLBACK);
543        assert_eq!(removed, 0, "Should return 0 for non-existent partition");
544    }
545}