feat: add dependency
This commit is contained in:
426
javascript-engine/external/boa/boa_gc/src/lib.rs
vendored
Normal file
426
javascript-engine/external/boa/boa_gc/src/lib.rs
vendored
Normal file
@@ -0,0 +1,426 @@
|
||||
//! 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<GcBox<dyn Trace>>;
|
||||
|
||||
thread_local!(static EPHEMERON_QUEUE: Cell<Option<Vec<GcPointer>>> = Cell::new(None));
|
||||
thread_local!(static GC_DROPPING: Cell<bool> = Cell::new(false));
|
||||
thread_local!(static BOA_GC: RefCell<BoaGc> = 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<Option<GcPointer>>,
|
||||
}
|
||||
|
||||
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<T>` 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<T: Trace>(value: GcBox<T>) -> NonNull<GcBox<T>> {
|
||||
let _timer = Profiler::global().start_event("New Pointer", "BoaAlloc");
|
||||
let element_size = mem::size_of_val::<GcBox<T>>(&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<T>
|
||||
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<Option<NonNull<GcBox<dyn Trace>>>>) -> Vec<NonNull<GcBox<dyn Trace>>> {
|
||||
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<NonNull<GcBox<dyn Trace>>>,
|
||||
) -> Vec<NonNull<GcBox<dyn Trace>>> {
|
||||
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<NonNull<GcBox<dyn Trace>>>) {
|
||||
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<Option<NonNull<GcBox<dyn Trace>>>>,
|
||||
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::<GcBox<_>>(&*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;
|
||||
Reference in New Issue
Block a user