#![doc = include_str!("../ABOUT.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
)]
#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
#![allow(
clippy::module_name_repetitions,
clippy::redundant_pub_crate,
clippy::let_unit_value
)]
extern crate self as boa_gc;
mod cell;
mod pointers;
mod trace;
pub(crate) mod internals;
use internals::{EphemeronBox, ErasedEphemeronBox, ErasedWeakMapBox, WeakMapBox};
use pointers::{NonTraceable, RawWeakMap};
use std::{
cell::{Cell, RefCell},
mem,
ptr::NonNull,
};
pub use crate::trace::{Finalize, Trace, Tracer};
pub use boa_macros::{Finalize, Trace};
pub use cell::{GcRef, GcRefCell, GcRefMut};
pub use internals::GcBox;
pub use pointers::{Ephemeron, Gc, GcErased, WeakGc, WeakMap};
type GcErasedPointer = NonNull<GcBox<NonTraceable>>;
type EphemeronPointer = NonNull<dyn ErasedEphemeronBox>;
type ErasedWeakMapBoxPointer = NonNull<dyn ErasedWeakMapBox>;
thread_local!(static GC_DROPPING: Cell<bool> = const { Cell::new(false) });
thread_local!(static BOA_GC: RefCell<BoaGc> = RefCell::new( BoaGc {
config: GcConfig::default(),
runtime: GcRuntimeData::default(),
strongs: Vec::default(),
weaks: Vec::default(),
weak_maps: Vec::default(),
}));
#[derive(Debug, Clone, Copy)]
struct GcConfig {
threshold: usize,
used_space_percentage: usize,
}
impl Default for GcConfig {
fn default() -> Self {
Self {
threshold: 1_048_576,
used_space_percentage: 70,
}
}
}
#[derive(Default, Debug, Clone, Copy)]
struct GcRuntimeData {
collections: usize,
bytes_allocated: usize,
}
#[derive(Debug)]
struct BoaGc {
config: GcConfig,
runtime: GcRuntimeData,
strongs: Vec<GcErasedPointer>,
weaks: Vec<EphemeronPointer>,
weak_maps: Vec<ErasedWeakMapBoxPointer>,
}
impl Drop for BoaGc {
fn drop(&mut self) {
Collector::dump(self);
}
}
#[derive(Debug, Clone)]
struct DropGuard;
impl DropGuard {
fn new() -> Self {
GC_DROPPING.with(|dropping| dropping.set(true));
Self
}
}
impl Drop for DropGuard {
fn drop(&mut self) {
GC_DROPPING.with(|dropping| dropping.set(false));
}
}
#[must_use]
#[inline]
pub fn finalizer_safe() -> bool {
GC_DROPPING.with(|dropping| !dropping.get())
}
#[derive(Debug, Clone, Copy)]
struct Allocator;
impl Allocator {
fn alloc_gc<T: Trace>(value: GcBox<T>) -> NonNull<GcBox<T>> {
let element_size = size_of_val::<GcBox<T>>(&value);
BOA_GC.with(|st| {
let mut gc = st.borrow_mut();
Self::manage_state(&mut gc);
let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) };
let erased: NonNull<GcBox<NonTraceable>> = ptr.cast();
gc.strongs.push(erased);
gc.runtime.bytes_allocated += element_size;
ptr
})
}
fn alloc_ephemeron<K: Trace + ?Sized, V: Trace>(
value: EphemeronBox<K, V>,
) -> NonNull<EphemeronBox<K, V>> {
let element_size = size_of_val::<EphemeronBox<K, V>>(&value);
BOA_GC.with(|st| {
let mut gc = st.borrow_mut();
Self::manage_state(&mut gc);
let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) };
let erased: NonNull<dyn ErasedEphemeronBox> = ptr;
gc.weaks.push(erased);
gc.runtime.bytes_allocated += element_size;
ptr
})
}
fn alloc_weak_map<K: Trace + ?Sized, V: Trace + Clone>() -> WeakMap<K, V> {
let weak_map = WeakMap {
inner: Gc::new(GcRefCell::new(RawWeakMap::new())),
};
let weak = WeakGc::new(&weak_map.inner);
BOA_GC.with(|st| {
let mut gc = st.borrow_mut();
let weak_box = WeakMapBox { map: weak };
let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(weak_box))) };
let erased: ErasedWeakMapBoxPointer = ptr;
gc.weak_maps.push(erased);
weak_map
})
}
fn manage_state(gc: &mut BoaGc) {
if gc.runtime.bytes_allocated > gc.config.threshold {
Collector::collect(gc);
if gc.runtime.bytes_allocated
> gc.config.threshold / 100 * gc.config.used_space_percentage
{
gc.config.threshold =
gc.runtime.bytes_allocated / gc.config.used_space_percentage * 100;
}
}
}
}
struct Unreachables {
strong: Vec<GcErasedPointer>,
weak: Vec<NonNull<dyn ErasedEphemeronBox>>,
}
struct Collector;
impl Collector {
fn collect(gc: &mut BoaGc) {
gc.runtime.collections += 1;
Self::trace_non_roots(gc);
let mut tracer = Tracer::new();
let unreachables = Self::mark_heap(&mut tracer, &gc.strongs, &gc.weaks, &gc.weak_maps);
assert!(tracer.is_empty(), "The queue should be empty");
if !unreachables.strong.is_empty() || !unreachables.weak.is_empty() {
unsafe { Self::finalize(unreachables) };
let _final_unreachables =
Self::mark_heap(&mut tracer, &gc.strongs, &gc.weaks, &gc.weak_maps);
}
unsafe {
Self::sweep(
&mut gc.strongs,
&mut gc.weaks,
&mut gc.runtime.bytes_allocated,
);
}
gc.weak_maps.retain(|w| {
let node_ref = unsafe { w.as_ref() };
if node_ref.is_live() {
node_ref.clear_dead_entries();
true
} else {
let _unmarked_node = unsafe { Box::from_raw(w.as_ptr()) };
false
}
});
gc.strongs.shrink_to(gc.strongs.len() >> 2);
gc.weaks.shrink_to(gc.weaks.len() >> 2);
gc.weak_maps.shrink_to(gc.weak_maps.len() >> 2);
}
fn trace_non_roots(gc: &BoaGc) {
for node in &gc.strongs {
let trace_non_roots_fn = unsafe { node.as_ref() }.trace_non_roots_fn();
unsafe {
trace_non_roots_fn(*node);
}
}
for eph in &gc.weaks {
let eph_ref = unsafe { eph.as_ref() };
eph_ref.trace_non_roots();
}
}
fn mark_heap(
tracer: &mut Tracer,
strongs: &[GcErasedPointer],
weaks: &[EphemeronPointer],
weak_maps: &[ErasedWeakMapBoxPointer],
) -> Unreachables {
let mut strong_dead = Vec::new();
let mut pending_ephemerons = Vec::new();
for node in strongs {
let node_ref = unsafe { node.as_ref() };
if node_ref.is_rooted() {
tracer.enqueue(*node);
unsafe {
tracer.trace_until_empty();
}
} else if !node_ref.is_marked() {
strong_dead.push(*node);
}
}
if weaks.is_empty() {
strong_dead.retain_mut(|node| {
unsafe { !node.as_ref().is_marked() }
});
return Unreachables {
strong: strong_dead,
weak: Vec::new(),
};
}
for eph in weaks {
let eph_ref = unsafe { eph.as_ref() };
let header = eph_ref.header();
if header.is_rooted() {
header.mark();
}
if unsafe { !eph_ref.trace(tracer) } {
pending_ephemerons.push(*eph);
}
unsafe {
tracer.trace_until_empty();
}
}
for w in weak_maps {
let node_ref = unsafe { w.as_ref() };
unsafe { node_ref.trace(tracer) };
unsafe {
tracer.trace_until_empty();
}
}
let mut previous_len = pending_ephemerons.len();
loop {
pending_ephemerons.retain_mut(|eph| {
let eph_ref = unsafe { eph.as_ref() };
let is_key_marked = unsafe { !eph_ref.trace(tracer) };
unsafe {
tracer.trace_until_empty();
}
is_key_marked
});
if previous_len == pending_ephemerons.len() {
break;
}
previous_len = pending_ephemerons.len();
}
strong_dead.retain_mut(|node| {
unsafe { !node.as_ref().is_marked() }
});
Unreachables {
strong: strong_dead,
weak: pending_ephemerons,
}
}
unsafe fn finalize(unreachables: Unreachables) {
for node in unreachables.strong {
let node_ref = unsafe { node.as_ref() };
let run_finalizer_fn = node_ref.run_finalizer_fn();
unsafe {
run_finalizer_fn(node);
}
}
for node in unreachables.weak {
let node = unsafe { node.as_ref() };
node.finalize_and_clear();
}
}
unsafe fn sweep(
strong: &mut Vec<GcErasedPointer>,
weak: &mut Vec<EphemeronPointer>,
total_allocated: &mut usize,
) {
let _guard = DropGuard::new();
strong.retain(|node| {
let node_ref = unsafe { node.as_ref() };
if node_ref.is_marked() {
node_ref.header.unmark();
node_ref.reset_non_root_count();
true
} else {
let drop_fn = node_ref.drop_fn();
let size = node_ref.size();
*total_allocated -= size;
unsafe {
drop_fn(*node);
}
false
}
});
weak.retain(|eph| {
let eph_ref = unsafe { eph.as_ref() };
let header = eph_ref.header();
if header.is_marked() {
header.unmark();
header.reset_non_root_count();
true
} else {
let unmarked_eph = unsafe { Box::from_raw(eph.as_ptr()) };
let unallocated_bytes = size_of_val(&*unmarked_eph);
*total_allocated -= unallocated_bytes;
false
}
});
}
fn dump(gc: &mut BoaGc) {
for node in mem::take(&mut gc.weak_maps) {
let _unmarked_node = unsafe { Box::from_raw(node.as_ptr()) };
}
let _guard = DropGuard::new();
for node in mem::take(&mut gc.strongs) {
let drop_fn = unsafe { node.as_ref() }.drop_fn();
unsafe {
drop_fn(node);
}
}
for node in mem::take(&mut gc.weaks) {
let _unmarked_node = unsafe { Box::from_raw(node.as_ptr()) };
}
}
}
pub fn force_collect() {
BOA_GC.with(|current| {
let mut gc = current.borrow_mut();
if gc.runtime.bytes_allocated > 0 {
Collector::collect(&mut gc);
}
});
}
#[cfg(test)]
mod test;
#[cfg(test)]
#[must_use]
pub fn has_weak_maps() -> bool {
BOA_GC.with(|current| {
let gc = current.borrow();
!gc.weak_maps.is_empty()
})
}