use crate::sys;
use crate::value::Value;
use crate::{Artichoke, Error};
pub mod arena;
use arena::{ArenaIndex, ArenaSavepointError};
pub trait MrbGarbageCollection {
fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, ArenaSavepointError>;
fn live_object_count(&mut self) -> usize;
fn mark_value(&mut self, value: &Value) -> Result<(), Error>;
fn incremental_gc(&mut self) -> Result<(), Error>;
fn full_gc(&mut self) -> Result<(), Error>;
fn enable_gc(&mut self) -> Result<State, Error>;
fn disable_gc(&mut self) -> Result<State, Error>;
}
impl MrbGarbageCollection for Artichoke {
fn create_arena_savepoint(&mut self) -> Result<ArenaIndex<'_>, ArenaSavepointError> {
ArenaIndex::new(self)
}
fn live_object_count(&mut self) -> usize {
let live_objects = unsafe { self.with_ffi_boundary(|mrb| sys::mrb_sys_gc_live_objects(mrb)) };
live_objects.unwrap_or(0)
}
fn mark_value(&mut self, value: &Value) -> Result<(), Error> {
unsafe {
self.with_ffi_boundary(|mrb| sys::mrb_sys_safe_gc_mark(mrb, value.inner()))?;
}
Ok(())
}
fn incremental_gc(&mut self) -> Result<(), Error> {
unsafe {
self.with_ffi_boundary(|mrb| sys::mrb_incremental_gc(mrb))?;
}
Ok(())
}
fn full_gc(&mut self) -> Result<(), Error> {
unsafe {
self.with_ffi_boundary(|mrb| sys::mrb_full_gc(mrb))?;
}
Ok(())
}
fn enable_gc(&mut self) -> Result<State, Error> {
unsafe {
let state = self.with_ffi_boundary(|mrb| sys::mrb_sys_gc_enable(mrb).into())?;
Ok(state)
}
}
fn disable_gc(&mut self) -> Result<State, Error> {
unsafe {
let state = self.with_ffi_boundary(|mrb| sys::mrb_sys_gc_disable(mrb).into())?;
Ok(state)
}
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum State {
Disabled,
Enabled,
}
impl From<bool> for State {
fn from(state: bool) -> Self {
if state {
Self::Enabled
} else {
Self::Disabled
}
}
}
#[cfg(test)]
mod tests {
use super::State;
use crate::test::prelude::*;
#[test]
fn arena_restore_on_explicit_restore() {
let mut interp = interpreter();
let baseline_object_count = interp.live_object_count();
let mut arena = interp.create_arena_savepoint().unwrap();
for _ in 0..2000 {
let value = arena.eval(b"'a'").unwrap();
let _display = value.to_s(&mut arena);
}
arena.restore();
interp.full_gc().unwrap();
assert_eq!(
interp.live_object_count(),
baseline_object_count + 1,
"Arena restore + full GC should free unreachable objects",
);
}
#[test]
fn arena_restore_on_drop() {
let mut interp = interpreter();
let baseline_object_count = interp.live_object_count();
{
let mut arena = interp.create_arena_savepoint().unwrap();
for _ in 0..2000 {
let value = arena.eval(b"'a'").unwrap();
let _display = value.to_s(&mut arena);
}
}
interp.full_gc().unwrap();
assert_eq!(
interp.live_object_count(),
baseline_object_count + 1,
"Arena restore + full GC should free unreachable objects",
);
}
#[test]
fn gc_state() {
let mut interp = interpreter();
assert_eq!(interp.enable_gc().unwrap(), State::Enabled);
assert_eq!(interp.enable_gc().unwrap(), State::Enabled);
assert_eq!(interp.disable_gc().unwrap(), State::Enabled);
assert_eq!(interp.disable_gc().unwrap(), State::Disabled);
assert_eq!(interp.disable_gc().unwrap(), State::Disabled);
assert_eq!(interp.enable_gc().unwrap(), State::Disabled);
assert_eq!(interp.enable_gc().unwrap(), State::Enabled);
}
#[test]
fn enable_disable_gc() {
let mut interp = interpreter();
interp.disable_gc().unwrap();
let mut arena = interp.create_arena_savepoint().unwrap();
arena
.interp()
.eval(
br#"
# this value will be garbage collected because it is eventually
# shadowed and becomes unreachable
a = []
# this value will not be garbage collected because it is a local
# variable in top self
a = []
# this value will not be garbage collected because it is a local
# variable in top self
b = []
# this value will not be garbage collected because the last value
# returned by eval is retained with "stack keep"
[]
"#,
)
.unwrap();
let live = arena.live_object_count();
arena.full_gc().unwrap();
assert_eq!(
arena.live_object_count(),
live,
"GC is disabled. No objects should be collected"
);
arena.restore();
interp.enable_gc().unwrap();
interp.full_gc().unwrap();
assert_eq!(
interp.live_object_count(),
live - 2,
"Arrays should be collected after enabling GC and running a full GC"
);
}
#[test]
fn gc_after_empty_eval() {
let mut interp = interpreter();
let mut arena = interp.create_arena_savepoint().unwrap();
let baseline_object_count = arena.live_object_count();
arena.eval(b"").unwrap();
arena.restore();
interp.full_gc().unwrap();
assert_eq!(interp.live_object_count(), baseline_object_count);
}
#[test]
fn gc_functional_test() {
let mut interp = interpreter();
let baseline_object_count = interp.live_object_count();
let mut initial_arena = interp.create_arena_savepoint().unwrap();
for _ in 0..2000 {
let mut arena = initial_arena.create_arena_savepoint().unwrap();
let result = arena.eval(b"'gc test'");
let value = result.unwrap();
assert!(!value.is_dead(&mut arena));
arena.restore();
initial_arena.incremental_gc().unwrap();
}
initial_arena.restore();
interp.full_gc().unwrap();
assert_eq!(
interp.live_object_count(),
baseline_object_count + 1,
"Started with {} live objects, ended with {}. Potential memory leak!",
baseline_object_count,
interp.live_object_count()
);
}
}