Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ default = [
dhat = ["dep:dhat"]
fast-allocator = ["dep:mimalloc-safe", "dep:tikv-jemallocator"]
fetch = ["boa_runtime/fetch", "boa_runtime/reqwest-blocking"]
gc-stats = ["boa_gc/gc-stats"]

[target.x86_64-unknown-linux-gnu.dependencies]
tikv-jemallocator = { workspace = true, optional = true }
Expand Down
109 changes: 104 additions & 5 deletions cli/src/debug/gc.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(feature = "gc-stats")]
use boa_engine::property::Attribute;
use boa_engine::{
Context, JsObject, JsResult, JsValue, NativeFunction, js_string, object::ObjectInitializer,
};
Expand All @@ -8,12 +10,109 @@ fn collect(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}

/// Get the garbage collector statistics of the current thread.
///
/// Timings are reported in milliseconds.
#[cfg(feature = "gc-stats")]
fn stats(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let stats = boa_gc::stats();
let ms = |duration: std::time::Duration| duration.as_secs_f64() * 1000.0;

let object = ObjectInitializer::new(context)
.property(
js_string!("collections"),
stats.collections,
Attribute::all(),
)
.property(
js_string!("finalizingCollections"),
stats.finalizing_collections,
Attribute::all(),
)
.property(
js_string!("totalBytesAllocated"),
stats.total_bytes_allocated,
Attribute::all(),
)
.property(
js_string!("totalBytesFreed"),
stats.total_bytes_freed,
Attribute::all(),
)
.property(
js_string!("objectsAllocated"),
stats.objects_allocated,
Attribute::all(),
)
.property(
js_string!("objectsSwept"),
stats.objects_swept,
Attribute::all(),
)
.property(
js_string!("bytesAllocated"),
stats.bytes_allocated,
Attribute::all(),
)
.property(
js_string!("traceNonRootsMs"),
ms(stats.trace_non_roots_time),
Attribute::all(),
)
.property(js_string!("markMs"), ms(stats.mark_time), Attribute::all())
.property(
js_string!("finalizeMs"),
ms(stats.finalize_time),
Attribute::all(),
)
.property(
js_string!("remarkMs"),
ms(stats.remark_time),
Attribute::all(),
)
.property(
js_string!("sweepMs"),
ms(stats.sweep_time),
Attribute::all(),
)
.property(
js_string!("weakMapPruneMs"),
ms(stats.weak_map_prune_time),
Attribute::all(),
)
.property(
js_string!("totalMs"),
ms(stats.total_time),
Attribute::all(),
)
.build();

Ok(object.into())
}

/// Reset the garbage collector statistics of the current thread.
#[cfg(feature = "gc-stats")]
fn reset_stats(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
boa_gc::reset_stats();
Ok(JsValue::undefined())
}

pub(super) fn create_object(context: &mut Context) -> JsObject {
ObjectInitializer::new(context)
let mut object = ObjectInitializer::new(context);
object.function(
NativeFunction::from_fn_ptr(collect),
js_string!("collect"),
0,
);

#[cfg(feature = "gc-stats")]
object
.function(NativeFunction::from_fn_ptr(stats), js_string!("stats"), 0)
.function(
NativeFunction::from_fn_ptr(collect),
js_string!("collect"),
NativeFunction::from_fn_ptr(reset_stats),
js_string!("resetStats"),
0,
)
.build()
);

object.build()
}
2 changes: 2 additions & 0 deletions core/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ embedded_lz4 = ["boa_macros/embedded_lz4", "lz4_flex"]
jsvalue-enum = []
deser = ["boa_interner/serde", "boa_ast/serde"]
either = ["dep:either", "boa_gc/either"]
# Enable collection of GC statistics
gc-stats = ["boa_gc/gc-stats"]

# Enables the `Intl` builtin object and bundles a default ICU4X data provider.
# Prefer this over `intl` if you just want to enable `Intl` without dealing with the
Expand Down
2 changes: 2 additions & 0 deletions core/gc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ boa_string = ["dep:boa_string"]
either = ["dep:either"]
# Enable default implementations of trace and finalize for the arrayvec crate
arrayvec = ["dep:arrayvec"]
# Enable collection of GC statistics (allocation/free counters and per-phase timings)
gc-stats = []

[dependencies]
boa_macros.workspace = true
Expand Down
134 changes: 104 additions & 30 deletions core/gc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ extern crate self as boa_gc;

mod cell;
mod pointers;
#[cfg(feature = "gc-stats")]
mod stats;
mod trace;

pub(crate) mod internals;
Expand All @@ -36,6 +38,8 @@ pub use boa_macros::{Finalize, Trace};
pub use cell::{GcRef, GcRefCell, GcRefMut};
pub use internals::GcBox;
pub use pointers::{Ephemeron, Gc, GcErased, WeakGc, WeakMap};
#[cfg(feature = "gc-stats")]
pub use stats::{GcStats, reset_stats, stats};

type GcErasedPointer = NonNull<GcBox<NonTraceable>>;
type EphemeronPointer = NonNull<dyn ErasedEphemeronBox>;
Expand All @@ -48,6 +52,8 @@ thread_local!(static BOA_GC: RefCell<BoaGc> = RefCell::new( BoaGc {
strongs: Vec::default(),
weaks: Vec::default(),
weak_maps: Vec::default(),
#[cfg(feature = "gc-stats")]
stats: GcStats::default(),
}));

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -84,6 +90,8 @@ struct BoaGc {
strongs: Vec<GcErasedPointer>,
weaks: Vec<EphemeronPointer>,
weak_maps: Vec<ErasedWeakMapBoxPointer>,
#[cfg(feature = "gc-stats")]
stats: GcStats,
}

impl Drop for BoaGc {
Expand Down Expand Up @@ -140,6 +148,11 @@ impl Allocator {

gc.strongs.push(erased);
gc.runtime.bytes_allocated += element_size;
#[cfg(feature = "gc-stats")]
{
gc.stats.total_bytes_allocated += element_size as u64;
gc.stats.objects_allocated += 1;
}

ptr
})
Expand All @@ -159,6 +172,11 @@ impl Allocator {

gc.weaks.push(erased);
gc.runtime.bytes_allocated += element_size;
#[cfg(feature = "gc-stats")]
{
gc.stats.total_bytes_allocated += element_size as u64;
gc.stats.objects_allocated += 1;
}

ptr
})
Expand Down Expand Up @@ -206,6 +224,20 @@ struct Unreachables {
weak: Vec<NonNull<dyn ErasedEphemeronBox>>,
}

/// Runs `$body` adding its runtime to `$gc.stats.$field` when `gc-stats` is enabled
macro_rules! timed {
($gc:ident . $field:ident, $body:expr) => {{
#[cfg(feature = "gc-stats")]
let start = std::time::Instant::now();
let result = $body;
#[cfg(feature = "gc-stats")]
{
$gc.stats.$field += start.elapsed();
}
result
}};
}

/// This collector currently functions in four main phases
///
/// Mark -> Finalize -> Mark -> Sweep
Expand All @@ -223,58 +255,100 @@ struct Collector;
impl Collector {
/// Run a collection on the full heap.
fn collect(gc: &mut BoaGc) {
#[cfg(feature = "gc-stats")]
let total_start = std::time::Instant::now();

gc.runtime.collections += 1;

Self::trace_non_roots(gc);
#[cfg(feature = "gc-stats")]
{
gc.stats.collections += 1;
}

timed!(gc.trace_non_roots_time, Self::trace_non_roots(gc));

let mut tracer = Tracer::new();

let unreachables = Self::mark_heap(&mut tracer, &gc.strongs, &gc.weaks, &gc.weak_maps);
let unreachables = timed!(
gc.mark_time,
Self::mark_heap(&mut tracer, &gc.strongs, &gc.weaks, &gc.weak_maps)
);

assert!(tracer.is_empty(), "The queue should be empty");

// Only finalize if there are any unreachable nodes.
if !unreachables.strong.is_empty() || !unreachables.weak.is_empty() {
// Finalize all the unreachable nodes.
// SAFETY: All passed pointers are valid, since we won't deallocate until `Self::sweep`.
unsafe { Self::finalize(unreachables) };
#[cfg(feature = "gc-stats")]
{
gc.stats.finalizing_collections += 1;
}

// Reuse the tracer's already allocated capacity.
let _final_unreachables =
Self::mark_heap(&mut tracer, &gc.strongs, &gc.weaks, &gc.weak_maps);
}
timed!(gc.finalize_time, {
// Finalize all the unreachable nodes.
// SAFETY: All passed pointers are valid, since we won't deallocate until `Self::sweep`.
unsafe { Self::finalize(unreachables) }
});

// SAFETY: The head of our linked list is always valid per the invariants of our GC.
unsafe {
Self::sweep(
&mut gc.strongs,
&mut gc.weaks,
&mut gc.runtime.bytes_allocated,
// Reuse the tracer's already allocated capacity.
let _final_unreachables = timed!(
gc.remark_time,
Self::mark_heap(&mut tracer, &gc.strongs, &gc.weaks, &gc.weak_maps)
);
}

// Weak maps have to be cleared after the sweep, since the process dereferences GcBoxes.
gc.weak_maps.retain(|w| {
// SAFETY: The caller must ensure the validity of every node of `heap_start`.
let node_ref = unsafe { w.as_ref() };

if node_ref.is_live() {
node_ref.clear_dead_entries();
#[cfg(feature = "gc-stats")]
let (bytes_before, objects_before) = (
gc.runtime.bytes_allocated,
gc.strongs.len() + gc.weaks.len(),
);

true
} else {
// 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(w.as_ptr()) };

false
timed!(gc.sweep_time, {
// SAFETY: The head of our linked list is always valid per the invariants of our GC.
unsafe {
Self::sweep(
&mut gc.strongs,
&mut gc.weaks,
&mut gc.runtime.bytes_allocated,
);
}
});

#[cfg(feature = "gc-stats")]
{
gc.stats.total_bytes_freed += (bytes_before - gc.runtime.bytes_allocated) as u64;
gc.stats.objects_swept += (objects_before - gc.strongs.len() - gc.weaks.len()) as u64;
}

// Weak maps have to be cleared after the sweep, since the process dereferences GcBoxes.
timed!(
gc.weak_map_prune_time,
gc.weak_maps.retain(|w| {
// SAFETY: The caller must ensure the validity of every node of `heap_start`.
let node_ref = unsafe { w.as_ref() };

if node_ref.is_live() {
node_ref.clear_dead_entries();

true
} else {
// 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(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);

#[cfg(feature = "gc-stats")]
{
gc.stats.total_time += total_start.elapsed();
}
}

fn trace_non_roots(gc: &BoaGc) {
Expand Down
Loading
Loading