1use crate::sys;
2use crate::value::Value;
3use crate::{Artichoke, Error};
4
5pub mod arena;
6
7use arena::{ArenaIndex, ArenaSavepointError};
8
9pub trait MrbGarbageCollection {
11 fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, ArenaSavepointError>;
24
25 fn live_object_count(&mut self) -> usize;
29
30 fn mark_value(&mut self, value: &Value) -> Result<(), Error>;
32
33 fn incremental_gc(&mut self) -> Result<(), Error>;
42
43 fn full_gc(&mut self) -> Result<(), Error>;
51
52 fn enable_gc(&mut self) -> Result<State, Error>;
56
57 fn disable_gc(&mut self) -> Result<State, Error>;
61}
62
63impl MrbGarbageCollection for Artichoke {
64 fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, ArenaSavepointError> {
65 ArenaIndex::new(self)
66 }
67
68 fn live_object_count(&mut self) -> usize {
69 let live_objects = unsafe { self.with_ffi_boundary(|mrb| sys::mrb_sys_gc_live_objects(mrb)) };
70 live_objects.unwrap_or(0)
71 }
72
73 fn mark_value(&mut self, value: &Value) -> Result<(), Error> {
74 unsafe {
75 self.with_ffi_boundary(|mrb| sys::mrb_sys_safe_gc_mark(mrb, value.inner()))?;
76 }
77 Ok(())
78 }
79
80 fn incremental_gc(&mut self) -> Result<(), Error> {
81 unsafe {
82 self.with_ffi_boundary(|mrb| sys::mrb_incremental_gc(mrb))?;
83 }
84 Ok(())
85 }
86
87 fn full_gc(&mut self) -> Result<(), Error> {
88 unsafe {
89 self.with_ffi_boundary(|mrb| sys::mrb_full_gc(mrb))?;
90 }
91 Ok(())
92 }
93
94 fn enable_gc(&mut self) -> Result<State, Error> {
95 unsafe {
96 let state = self.with_ffi_boundary(|mrb| sys::mrb_sys_gc_enable(mrb).into())?;
97 Ok(state)
98 }
99 }
100
101 fn disable_gc(&mut self) -> Result<State, Error> {
102 unsafe {
103 let state = self.with_ffi_boundary(|mrb| sys::mrb_sys_gc_disable(mrb).into())?;
104 Ok(state)
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
110pub enum State {
111 Disabled,
112 Enabled,
113}
114
115impl From<bool> for State {
116 fn from(state: bool) -> Self {
117 if state { Self::Enabled } else { Self::Disabled }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::State;
124 use crate::test::prelude::*;
125
126 #[test]
127 fn arena_restore_on_explicit_restore() {
128 let mut interp = interpreter();
129 let baseline_object_count = interp.live_object_count();
130 let mut arena = interp.create_arena_savepoint().unwrap();
131 for _ in 0..2000 {
132 let value = arena.eval(b"'a'").unwrap();
133 let _display = value.to_s(&mut arena);
134 }
135 arena.restore();
136 interp.full_gc().unwrap();
137 assert_eq!(
138 interp.live_object_count(),
139 baseline_object_count + 1,
142 "Arena restore + full GC should free unreachable objects",
143 );
144 }
145
146 #[test]
147 fn arena_restore_on_drop() {
148 let mut interp = interpreter();
149 let baseline_object_count = interp.live_object_count();
150 {
151 let mut arena = interp.create_arena_savepoint().unwrap();
152 for _ in 0..2000 {
153 let value = arena.eval(b"'a'").unwrap();
154 let _display = value.to_s(&mut arena);
155 }
156 }
157 interp.full_gc().unwrap();
158 assert_eq!(
159 interp.live_object_count(),
160 baseline_object_count + 1,
163 "Arena restore + full GC should free unreachable objects",
164 );
165 }
166
167 #[test]
168 fn gc_state() {
169 let mut interp = interpreter();
170 assert_eq!(interp.enable_gc().unwrap(), State::Enabled);
171 assert_eq!(interp.enable_gc().unwrap(), State::Enabled);
172
173 assert_eq!(interp.disable_gc().unwrap(), State::Enabled);
174 assert_eq!(interp.disable_gc().unwrap(), State::Disabled);
175 assert_eq!(interp.disable_gc().unwrap(), State::Disabled);
176
177 assert_eq!(interp.enable_gc().unwrap(), State::Disabled);
178 assert_eq!(interp.enable_gc().unwrap(), State::Enabled);
179 }
180
181 #[test]
182 fn enable_disable_gc() {
183 let mut interp = interpreter();
184 interp.disable_gc().unwrap();
185 let mut arena = interp.create_arena_savepoint().unwrap();
186 arena
187 .interp()
188 .eval(
189 br#"
190 # this value will be garbage collected because it is eventually
191 # shadowed and becomes unreachable
192 a = []
193 # this value will not be garbage collected because it is a local
194 # variable in top self
195 a = []
196 # this value will not be garbage collected because it is a local
197 # variable in top self
198 b = []
199 # this value will not be garbage collected because the last value
200 # returned by eval is retained with "stack keep"
201 []
202 "#,
203 )
204 .unwrap();
205 let live = arena.live_object_count();
206 arena.full_gc().unwrap();
207 assert_eq!(
208 arena.live_object_count(),
209 live,
210 "GC is disabled. No objects should be collected"
211 );
212 arena.restore();
213 interp.enable_gc().unwrap();
214 interp.full_gc().unwrap();
215 assert_eq!(
216 interp.live_object_count(),
217 live - 2,
218 "Arrays should be collected after enabling GC and running a full GC"
219 );
220 }
221
222 #[test]
223 fn gc_after_empty_eval() {
224 let mut interp = interpreter();
225 let mut arena = interp.create_arena_savepoint().unwrap();
226 let baseline_object_count = arena.live_object_count();
227 arena.eval(b"").unwrap();
228 arena.restore();
229 interp.full_gc().unwrap();
230 assert_eq!(interp.live_object_count(), baseline_object_count);
231 }
232
233 #[test]
234 fn gc_functional_test() {
235 let mut interp = interpreter();
236 let baseline_object_count = interp.live_object_count();
237 let mut initial_arena = interp.create_arena_savepoint().unwrap();
238 for _ in 0..2000 {
239 let mut arena = initial_arena.create_arena_savepoint().unwrap();
240 let result = arena.eval(b"'gc test'");
241 let value = result.unwrap();
242 assert!(!value.is_dead(&mut arena));
243 arena.restore();
244 initial_arena.incremental_gc().unwrap();
245 }
246 initial_arena.restore();
247 interp.full_gc().unwrap();
248 assert_eq!(
249 interp.live_object_count(),
250 baseline_object_count + 1,
253 "Started with {} live objects, ended with {}. Potential memory leak!",
254 baseline_object_count,
255 interp.live_object_count()
256 );
257 }
258}