artichoke_backend/
gc.rs

1use crate::sys;
2use crate::value::Value;
3use crate::{Artichoke, Error};
4
5pub mod arena;
6
7use arena::{ArenaIndex, ArenaSavepointError};
8
9/// Garbage collection primitives for an mruby interpreter.
10pub trait MrbGarbageCollection {
11    /// Create a savepoint in the GC arena.
12    ///
13    /// Savepoints allow mruby to deallocate all the objects created via the
14    /// C API.
15    ///
16    /// Normally objects created via the C API are marked as permanently alive
17    /// ("white" GC color) with a call to [`mrb_gc_protect`].
18    ///
19    /// The returned [`ArenaIndex`] implements [`Drop`], so it is sufficient to
20    /// let it go out of scope to ensure objects are eventually collected.
21    ///
22    /// [`mrb_gc_protect`]: sys::mrb_gc_protect
23    fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, ArenaSavepointError>;
24
25    /// Retrieve the number of live objects on the interpreter heap.
26    ///
27    /// A live object is reachable via top self, the stack, or the arena.
28    fn live_object_count(&mut self) -> usize;
29
30    /// Mark a [`Value`] as reachable in the mruby garbage collector.
31    fn mark_value(&mut self, value: &Value) -> Result<(), Error>;
32
33    /// Perform an incremental garbage collection.
34    ///
35    /// An incremental GC is less computationally expensive than a [full GC],
36    /// but does not guarantee that all dead objects will be reaped. You may
37    /// wish to use an incremental GC if you are operating with an interpreter
38    /// in a loop.
39    ///
40    /// [full GC]: MrbGarbageCollection::full_gc
41    fn incremental_gc(&mut self) -> Result<(), Error>;
42
43    /// Perform a full garbage collection.
44    ///
45    /// A full GC guarantees that all dead objects will be reaped, so it is more
46    /// expensive than an [incremental GC]. You may wish to use a full GC if you
47    /// are memory constrained.
48    ///
49    /// [incremental GC]: MrbGarbageCollection::incremental_gc
50    fn full_gc(&mut self) -> Result<(), Error>;
51
52    /// Enable garbage collection.
53    ///
54    /// Returns the prior GC enabled state.
55    fn enable_gc(&mut self) -> Result<State, Error>;
56
57    /// Disable garbage collection.
58    ///
59    /// Returns the prior GC enabled state.
60    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            // plus 1 because stack keep is enabled in eval which marks the last
140            // returned value as live.
141            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            // plus 1 because stack keep is enabled in eval which marks the last
161            // returned value as live.
162            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            // plus 1 because stack keep is enabled in eval which marks the
251            // last returned value as live.
252            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}