//! Boa's **`boa_gc`** crate implements a garbage collector. //! //! # Crate Overview //! **`boa_gc`** is a mark-sweep garbage collector that implements a Trace and Finalize trait //! for garbage collected values. //! //! # About Boa //! Boa is an open-source, experimental ECMAScript Engine written in Rust for lexing, parsing and executing ECMAScript/JavaScript. Currently, Boa //! supports some of the [language][boa-conformance]. More information can be viewed at [Boa's website][boa-web]. //! //! Try out the most recent release with Boa's live demo [playground][boa-playground]. //! //! # Boa Crates //! - **`boa_ast`** - Boa's ECMAScript Abstract Syntax Tree. //! - **`boa_engine`** - Boa's implementation of ECMAScript builtin objects and execution. //! - **`boa_gc`** - Boa's garbage collector. //! - **`boa_interner`** - Boa's string interner. //! - **`boa_parser`** - Boa's lexer and parser. //! - **`boa_profiler`** - Boa's code profiler. //! - **`boa_unicode`** - Boa's Unicode identifier. //! - **`boa_icu_provider`** - Boa's ICU4X data provider. //! //! [boa-conformance]: https://boa-dev.github.io/boa/test262/ //! [boa-web]: https://boa-dev.github.io/ //! [boa-playground]: https://boa-dev.github.io/boa/playground/ #![doc( html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg" )] #![cfg_attr(not(test), forbid(clippy::unwrap_used))] #![warn(missing_docs, clippy::dbg_macro)] #![deny( // rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html warnings, future_incompatible, let_underscore, nonstandard_style, rust_2018_compatibility, rust_2018_idioms, rust_2021_compatibility, unused, // rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html macro_use_extern_crate, meta_variable_misuse, missing_abi, missing_copy_implementations, missing_debug_implementations, non_ascii_idents, noop_method_call, single_use_lifetimes, trivial_casts, trivial_numeric_casts, unreachable_pub, unsafe_op_in_unsafe_fn, unused_crate_dependencies, unused_import_braces, unused_lifetimes, unused_qualifications, unused_tuple_struct_fields, variant_size_differences, // rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, rustdoc::missing_crate_level_docs, rustdoc::private_doc_tests, rustdoc::invalid_codeblock_attributes, rustdoc::invalid_rust_codeblocks, rustdoc::bare_urls, // clippy categories https://doc.rust-lang.org/clippy/ clippy::all, clippy::correctness, clippy::suspicious, clippy::style, clippy::complexity, clippy::perf, clippy::pedantic, clippy::nursery, )] #![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 boa_profiler::Profiler; use std::{ cell::{Cell, RefCell}, mem, ptr::NonNull, }; pub use crate::trace::{Finalize, Trace}; pub use boa_macros::{Finalize, Trace}; pub use cell::{GcCell, GcCellRef, GcCellRefMut}; pub use internals::GcBox; pub use pointers::{Ephemeron, Gc, WeakGc}; type GcPointer = NonNull>; thread_local!(static EPHEMERON_QUEUE: Cell>> = Cell::new(None)); thread_local!(static GC_DROPPING: Cell = Cell::new(false)); thread_local!(static BOA_GC: RefCell = RefCell::new( BoaGc { config: GcConfig::default(), runtime: GcRuntimeData::default(), adult_start: Cell::new(None), })); #[derive(Debug, Clone, Copy)] struct GcConfig { threshold: usize, used_space_percentage: usize, } // Setting the defaults to an arbitrary value currently. // // TODO: Add a configure later impl Default for GcConfig { fn default() -> Self { Self { threshold: 1024, used_space_percentage: 80, } } } #[derive(Default, Debug, Clone, Copy)] struct GcRuntimeData { collections: usize, bytes_allocated: usize, } #[derive(Debug)] struct BoaGc { config: GcConfig, runtime: GcRuntimeData, adult_start: Cell>, } impl Drop for BoaGc { fn drop(&mut self) { Collector::dump(self); } } // Whether or not the thread is currently in the sweep phase of garbage collection. // During this phase, attempts to dereference a `Gc` pointer will trigger a panic. /// `DropGuard` flags whether the Collector is currently running `Collector::sweep()` or `Collector::dump()` /// /// While the `DropGuard` is active, all `GcBox`s must not be dereferenced or accessed as it could cause Undefined Behavior #[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)); } } /// Returns `true` if it is safe for a type to run [`Finalize::finalize`]. #[must_use] #[inline] pub fn finalizer_safe() -> bool { GC_DROPPING.with(|dropping| !dropping.get()) } /// The Allocator handles allocation of garbage collected values. /// /// The allocator can trigger a garbage collection. #[derive(Debug, Clone, Copy)] struct Allocator; impl Allocator { /// Allocate a new garbage collected value to the Garbage Collector's heap. fn allocate(value: GcBox) -> NonNull> { let _timer = Profiler::global().start_event("New Pointer", "BoaAlloc"); let element_size = mem::size_of_val::>(&value); BOA_GC.with(|st| { let mut gc = st.borrow_mut(); Self::manage_state(&mut gc); value.header.next.set(gc.adult_start.take()); // Safety: Value Cannot be a null as it must be a GcBox let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::from(value))) }; gc.adult_start.set(Some(ptr)); gc.runtime.bytes_allocated += element_size; ptr }) } fn manage_state(gc: &mut BoaGc) { if gc.runtime.bytes_allocated > gc.config.threshold { Collector::run_full_collection(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; } } } } // This collector currently functions in four main phases // // Mark -> Finalize -> Mark -> Sweep // // Mark nodes as reachable then finalize the unreachable nodes. A remark phase // then needs to be retriggered as finalization can potentially resurrect dead // nodes. // // A better approach in a more concurrent structure may be to reorder. // // Mark -> Sweep -> Finalize struct Collector; impl Collector { /// Run a collection on the full heap. fn run_full_collection(gc: &mut BoaGc) { let _timer = Profiler::global().start_event("Gc Full Collection", "gc"); gc.runtime.collections += 1; let unreachable_adults = Self::mark_heap(&gc.adult_start); // Check if any unreachable nodes were found and finalize if !unreachable_adults.is_empty() { // SAFETY: Please see `Collector::finalize()` unsafe { Self::finalize(unreachable_adults) }; } let _final_unreachable_adults = Self::mark_heap(&gc.adult_start); // SAFETY: Please see `Collector::sweep()` unsafe { Self::sweep(&gc.adult_start, &mut gc.runtime.bytes_allocated); } } /// Walk the heap and mark any nodes deemed reachable fn mark_heap(head: &Cell>>>) -> Vec>> { let _timer = Profiler::global().start_event("Gc Marking", "gc"); // Walk the list, tracing and marking the nodes let mut finalize = Vec::new(); let mut ephemeron_queue = Vec::new(); let mut mark_head = head; while let Some(node) = mark_head.get() { // SAFETY: node must be valid as it is coming directly from the heap. let node_ref = unsafe { node.as_ref() }; if node_ref.header.is_ephemeron() { ephemeron_queue.push(node); } else if node_ref.header.roots() > 0 { // SAFETY: the reference to node must be valid as it is rooted. Passing // invalid references can result in Undefined Behavior unsafe { node_ref.trace_inner(); } } else { finalize.push(node); } mark_head = &node_ref.header.next; } // Ephemeron Evaluation if !ephemeron_queue.is_empty() { ephemeron_queue = Self::mark_ephemerons(ephemeron_queue); } // Any left over nodes in the ephemeron queue at this point are // unreachable and need to be notified/finalized. finalize.extend(ephemeron_queue); finalize } // Tracing Ephemerons/Weak is always requires tracing the inner nodes in case it ends up marking unmarked node // // Time complexity should be something like O(nd) where d is the longest chain of epehemerons /// Mark any ephemerons that are deemed live and trace their fields. fn mark_ephemerons( initial_queue: Vec>>, ) -> Vec>> { let mut ephemeron_queue = initial_queue; loop { // iterate through ephemeron queue, sorting nodes by whether they // are reachable or unreachable let (reachable, other): (Vec<_>, Vec<_>) = ephemeron_queue.into_iter().partition(|node| { // SAFETY: Any node on the eph_queue or the heap must be non null let node = unsafe { node.as_ref() }; if node.value.is_marked_ephemeron() { node.header.mark(); true } else { node.header.roots() > 0 } }); // Replace the old queue with the unreachable ephemeron_queue = other; // If reachable nodes is not empty, trace values. If it is empty, // break from the loop if reachable.is_empty() { break; } EPHEMERON_QUEUE.with(|state| state.set(Some(Vec::new()))); // iterate through reachable nodes and trace their values, // enqueuing any ephemeron that is found during the trace for node in reachable { // TODO: deal with fetch ephemeron_queue // SAFETY: Node must be a valid pointer or else it would not be deemed reachable. unsafe { node.as_ref().weak_trace_inner(); } } EPHEMERON_QUEUE.with(|st| { if let Some(found_nodes) = st.take() { ephemeron_queue.extend(found_nodes); } }); } ephemeron_queue } /// # Safety /// /// Passing a vec with invalid pointers will result in Undefined Behaviour. unsafe fn finalize(finalize_vec: Vec>>) { let _timer = Profiler::global().start_event("Gc Finalization", "gc"); for node in finalize_vec { // We double check that the unreachable nodes are actually unreachable // prior to finalization as they could have been marked by a different // trace after initially being added to the queue // // SAFETY: The caller must ensure all pointers inside `finalize_vec` are valid. let node = unsafe { node.as_ref() }; if !node.header.is_marked() { Trace::run_finalizer(&node.value); } } } /// # Safety /// /// - Providing an invalid pointer in the `heap_start` or in any of the headers of each /// node will result in Undefined Behaviour. /// - Providing a list of pointers that weren't allocated by `Box::into_raw(Box::new(..))` /// will result in Undefined Behaviour. unsafe fn sweep( heap_start: &Cell>>>, total_allocated: &mut usize, ) { let _timer = Profiler::global().start_event("Gc Sweeping", "gc"); let _guard = DropGuard::new(); let mut sweep_head = heap_start; while let Some(node) = sweep_head.get() { // SAFETY: The caller must ensure the validity of every node of `heap_start`. let node_ref = unsafe { node.as_ref() }; if node_ref.is_marked() { node_ref.header.unmark(); sweep_head = &node_ref.header.next; } else if node_ref.header.is_ephemeron() && node_ref.header.roots() > 0 { // Keep the ephemeron box's alive if rooted, but note that it's pointer is no longer safe Trace::run_finalizer(&node_ref.value); sweep_head = &node_ref.header.next; } else { // SAFETY: The algorithm ensures only unmarked/unreachable pointers are dropped. // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; let unallocated_bytes = mem::size_of_val::>(&*unmarked_node); *total_allocated -= unallocated_bytes; sweep_head.set(unmarked_node.header.next.take()); } } } // Clean up the heap when BoaGc is dropped fn dump(gc: &mut BoaGc) { // Not initializing a dropguard since this should only be invoked when BOA_GC is being dropped. let _guard = DropGuard::new(); let sweep_head = &gc.adult_start; while let Some(node) = sweep_head.get() { // SAFETY: // The `Allocator` must always ensure its start node is a valid, non-null pointer that // was allocated by `Box::from_raw(Box::new(..))`. let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; sweep_head.set(unmarked_node.header.next.take()); } } } /// Forcefully runs a garbage collection of all unaccessible nodes. pub fn force_collect() { BOA_GC.with(|current| { let mut gc = current.borrow_mut(); if gc.runtime.bytes_allocated > 0 { Collector::run_full_collection(&mut gc); } }); } #[cfg(test)] mod test;