1use std::{collections::HashMap, ptr::NonNull};
5
6use crate::{
7 gctype::GcTypeRegistry,
8 node::{GcHead, GcNodeFlag},
9 partition::{GcPartition, GcPartitionId},
10 scope::GcScopeState,
11};
12
13pub struct GcHeap {
14 pub(super) node_dtypes: &'static GcTypeRegistry,
16
17 pub(super) partitions: HashMap<GcPartitionId, GcPartition>,
19 pub(super) memory_limit: usize,
21 pub(super) gc_threshold: usize,
23 pub(super) total_memory_used: usize,
25 pub(crate) scope_stack: Vec<GcScopeState<'static>>, pub(super) weak_slots: Vec<(u16, Option<NonNull<GcHead>>)>,
28
29 opaque: *mut u8,
31
32 #[cfg(debug_assertions)]
33 pub(crate) dbg_dropping_root_partition: Option<GcPartitionId>,
34 #[cfg(debug_assertions)]
35 pub(crate) dbg_living_nodes: std::collections::HashSet<NonNull<GcHead>>,
36}
37
38impl Drop for GcHeap {
39 fn drop(&mut self) {
40 log::trace!("[heap::drop]");
42
43 for s in self.scope_stack.drain(..) {
44 unsafe {
45 s.abort();
46 }
47 }
48
49 let pars = std::mem::take(&mut self.partitions);
50 for (_, partition) in pars {
51 self.dispose_all_nodes(partition.nodes, Self::DUMMY_DISPOSE_CALLBACK);
52 }
53
54 #[cfg(debug_assertions)]
55 debug_assert!(
56 self.dbg_living_nodes.is_empty(),
57 "[O.o][heap drop] leaked nodes {:?}",
58 self.dbg_living_nodes
59 );
60 }
61}
62
63impl GcHeap {
64 pub const DUMMY_DISPOSE_CALLBACK: fn(&GcHeap, &GcHead) = |_, _| {};
65
66 pub fn new(registry: &'static GcTypeRegistry) -> Self {
68 Self {
69 partitions: HashMap::new(),
70 memory_limit: 0,
71 gc_threshold: 0,
72 total_memory_used: 0,
73 weak_slots: Vec::new(),
74 opaque: std::ptr::null_mut(),
75 node_dtypes: registry,
76 scope_stack: Vec::with_capacity(16),
77
78 #[cfg(debug_assertions)]
79 dbg_dropping_root_partition: None,
80 #[cfg(debug_assertions)]
81 dbg_living_nodes: std::collections::HashSet::with_capacity(128),
82 }
83 }
84
85 #[inline(always)]
86 pub const fn opaque(&self) -> *mut u8 {
87 self.opaque
88 }
89
90 pub const fn set_opaque(&mut self, opaque: *mut u8) {
91 self.opaque = opaque;
92 }
93
94 pub fn memory_limit(&self) -> usize {
95 self.memory_limit
96 }
97
98 pub fn set_memory_limit(&mut self, limit: usize) -> usize {
99 if limit == 0 {
100 self.memory_limit = 0;
101 } else {
102 let used = self.total_memory_used;
103 let applied = std::cmp::max(used, limit);
104 self.memory_limit = applied;
105
106 if self.gc_threshold > 0 && self.gc_threshold >= applied {
107 let adjusted = applied - (applied >> 2);
108 self.gc_threshold = adjusted;
109 }
110 }
111
112 self.memory_limit
113 }
114
115 pub fn gc_threshold(&self) -> usize {
116 self.gc_threshold
117 }
118
119 pub fn set_gc_threshold(&mut self, threshold: usize) -> usize {
120 if threshold > 0 && self.memory_limit > 0 {
121 let capped = self.memory_limit.saturating_mul(8).saturating_div(10);
122 self.gc_threshold = std::cmp::min(threshold, capped);
123 } else {
124 self.gc_threshold = threshold;
125 }
126
127 self.gc_threshold
128 }
129
130 #[inline(always)]
132 pub fn should_gc(&self) -> bool {
133 self.gc_threshold > 0 && self.total_memory_used >= self.gc_threshold
136 }
137
138 pub(crate) fn attach_node(&mut self, partition_id: GcPartitionId, mut node: NonNull<GcHead>) {
144 debug_assert!(!partition_id.is_null());
145
146 let n = unsafe { node.as_mut() };
147 debug_assert!(n.partition_id().is_null());
148 debug_assert!(n.next.is_none());
149 n.set_partition_id(partition_id);
150
151 let par = self.partitions.get_mut(&partition_id).unwrap();
152 par.nodes.prepend(node);
153 }
154
155 pub fn set_root_node(&mut self, mut node: NonNull<GcHead>) {
156 let n = unsafe { node.as_mut() };
157 if !n.is_root() {
158 let partition_id = n.partition_id();
159 if let Some(p) = self.partitions.get_mut(&partition_id) {
160 n.insert_flag(GcNodeFlag::ROOT);
161 if p.is_marking() {
162 p.add_gray_node(node);
163 }
164 }
165 }
166 }
167
168 pub fn contains(&self, node: NonNull<GcHead>) -> bool {
170 self.nodes(unsafe { node.as_ref().partition_id() })
171 .any(|p| p == node)
172 }
173
174 pub fn protect_node(&mut self, node: NonNull<GcHead>) -> bool {
180 self.current_scope().is_some_and(|s| s.add_non_local(node))
181 }
182
183 pub fn protect_nodes_iter(&mut self, nodes: impl Iterator<Item = NonNull<GcHead>>) {
189 if let Some(s) = self.current_scope() {
190 for n in nodes {
191 s.add_non_local(n);
192 }
193 }
194 }
195
196 pub fn protect_nodes(&mut self, nodes: &[NonNull<GcHead>]) {
202 self.protect_nodes_iter(nodes.iter().copied());
203 }
204
205 pub(crate) fn update_mem_use(&mut self, id: GcPartitionId, delta: i32) -> usize {
207 if id.is_null() {
208 return 0;
209 }
210
211 if let Some(par) = self.partitions.get_mut(&id) {
212 if delta >= 0 {
213 let d = delta as usize;
214 par.memory_used += d;
215 self.total_memory_used += d;
216 } else {
217 let d = (-delta) as usize;
218 debug_assert!(par.memory_used >= d);
219 debug_assert!(self.total_memory_used >= d);
220 par.memory_used -= d;
221 self.total_memory_used -= d;
222 }
223 par.memory_used
224 } else {
225 0
226 }
227 }
228
229 #[inline(always)]
230 pub const fn memory_used(&self) -> usize {
231 self.total_memory_used
232 }
233}
234
235#[cfg(test)]
236mod heap_tests {
237 use crate::{GcRef, GcTraceCtx, trace::GcTrace};
238
239 use super::*;
240
241 #[derive(Debug)]
242 struct Node {
243 next: Option<GcRef<Node>>,
244 value: i32,
245 }
246
247 impl GcTrace for Node {
248 fn trace(&self, tr: &mut GcTraceCtx) {
249 if let Some(next) = self.next {
250 tr.add(next);
251 }
252 }
253 }
254
255 crate::gc_type_register! {
256 Node, drop_pass = 0;
257 }
258
259 #[test]
260 fn test_heap_with_context_alloc_and_cleanup() {
261 let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
262 let partition_id = heap.create_partition();
263
264 let head = heap.with_new_scope(partition_id, |ctx| {
265 let node: GcRef<Node> = ctx
266 .alloc_local(Node {
267 next: None,
268 value: 1,
269 })
270 .unwrap();
271 ctx.flush();
272 node.head_ptr
273 });
274
275 while !heap.mark(partition_id, 64) {}
276 let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
277 assert!(removed_after > 0);
278 }
279}