mirror of https://github.com/boa-dev/boa.git
Browse Source
This PR changes the following: - Modifies `EphemeronBox` to be more akin to `GcBox`, with its own header, roots and markers. This also makes it more similar to [Racket's](https://docs.racket-lang.org/reference/ephemerons.html) implementation. - Removes `EPHEMERON_QUEUE`. - Ephemerons are now tracked on a special `weak_start` linked list, instead of `strong_start` which is where all other GC boxes live. - Documents all unsafe blocks. - Documents our current garbage collection algorithm. I hope this'll clarify a bit what exactly are we doing on every garbage collection. - Renames/removes some functions.pull/2559/head
José Julián Espina
2 years ago
28 changed files with 838 additions and 577 deletions
@ -1,115 +1,210 @@
|
||||
use crate::{finalizer_safe, trace::Trace, Finalize, Gc, GcBox}; |
||||
use std::{cell::Cell, ptr::NonNull}; |
||||
use crate::{trace::Trace, Gc, GcBox}; |
||||
use std::{ |
||||
cell::Cell, |
||||
ptr::{self, NonNull}, |
||||
}; |
||||
|
||||
// Age and Weak Flags
|
||||
const MARK_MASK: usize = 1 << (usize::BITS - 1); |
||||
const ROOTS_MASK: usize = !MARK_MASK; |
||||
const ROOTS_MAX: usize = ROOTS_MASK; |
||||
|
||||
/// The `EphemeronBoxHeader` contains the `EphemeronBoxHeader`'s current state for the `Collector`'s
|
||||
/// Mark/Sweep as well as a pointer to the next ephemeron in the heap.
|
||||
///
|
||||
/// These flags include:
|
||||
/// - Root Count
|
||||
/// - Mark Flag Bit
|
||||
///
|
||||
/// The next node is set by the `Allocator` during initialization and by the
|
||||
/// `Collector` during the sweep phase.
|
||||
pub(crate) struct EphemeronBoxHeader { |
||||
roots: Cell<usize>, |
||||
pub(crate) next: Cell<Option<NonNull<dyn ErasedEphemeronBox>>>, |
||||
} |
||||
|
||||
impl EphemeronBoxHeader { |
||||
/// Creates a new `EphemeronBoxHeader` with a root of 1 and next set to None.
|
||||
pub(crate) fn new() -> Self { |
||||
Self { |
||||
roots: Cell::new(1), |
||||
next: Cell::new(None), |
||||
} |
||||
} |
||||
|
||||
/// The inner allocation of an [`Ephemeron`][crate::Ephemeron] pointer.
|
||||
pub(crate) struct EphemeronBox<K: Trace + ?Sized + 'static, V: Trace + ?Sized + 'static> { |
||||
key: Cell<Option<NonNull<GcBox<K>>>>, |
||||
value: V, |
||||
/// Returns the `EphemeronBoxHeader`'s current root count
|
||||
pub(crate) fn roots(&self) -> usize { |
||||
self.roots.get() & ROOTS_MASK |
||||
} |
||||
|
||||
impl<K: Trace + ?Sized, V: Trace> EphemeronBox<K, V> { |
||||
pub(crate) fn new(key: &Gc<K>, value: V) -> Self { |
||||
Self { |
||||
key: Cell::new(Some(key.inner_ptr())), |
||||
value, |
||||
/// Increments `EphemeronBoxHeader`'s root count.
|
||||
pub(crate) fn inc_roots(&self) { |
||||
let roots = self.roots.get(); |
||||
|
||||
if (roots & ROOTS_MASK) < ROOTS_MAX { |
||||
self.roots.set(roots + 1); |
||||
} else { |
||||
// TODO: implement a better way to handle root overload.
|
||||
panic!("roots counter overflow"); |
||||
} |
||||
} |
||||
|
||||
/// Decreases `EphemeronBoxHeader`'s current root count.
|
||||
pub(crate) fn dec_roots(&self) { |
||||
// Underflow check as a stop gap for current issue when dropping.
|
||||
if self.roots.get() > 0 { |
||||
self.roots.set(self.roots.get() - 1); |
||||
} |
||||
} |
||||
|
||||
impl<K: Trace + ?Sized, V: Trace + ?Sized> EphemeronBox<K, V> { |
||||
/// Checks if the key pointer is marked by Trace
|
||||
/// Returns a bool for whether `EphemeronBoxHeader`'s mark bit is 1.
|
||||
pub(crate) fn is_marked(&self) -> bool { |
||||
self.inner_key().map_or(false, GcBox::is_marked) |
||||
self.roots.get() & MARK_MASK != 0 |
||||
} |
||||
|
||||
/// Returns some pointer to the `key`'s `GcBox` or None
|
||||
/// # Panics
|
||||
/// This method will panic if called while the garbage collector is dropping.
|
||||
pub(crate) fn inner_key_ptr(&self) -> Option<*mut GcBox<K>> { |
||||
assert!(finalizer_safe()); |
||||
self.key.get().map(NonNull::as_ptr) |
||||
/// Sets `EphemeronBoxHeader`'s mark bit to 1.
|
||||
pub(crate) fn mark(&self) { |
||||
self.roots.set(self.roots.get() | MARK_MASK); |
||||
} |
||||
|
||||
/// Returns some reference to `key`'s `GcBox` or None
|
||||
pub(crate) fn inner_key(&self) -> Option<&GcBox<K>> { |
||||
// SAFETY: This is safe as `EphemeronBox::inner_key_ptr()` will
|
||||
// fetch either a live `GcBox` or None. The value of `key` is set
|
||||
// to None in the case where `EphemeronBox` and `key`'s `GcBox`
|
||||
// entered into `Collector::sweep()` as unmarked.
|
||||
unsafe { self.inner_key_ptr().map(|inner_key| &*inner_key) } |
||||
/// Sets `EphemeronBoxHeader`'s mark bit to 0.
|
||||
pub(crate) fn unmark(&self) { |
||||
self.roots.set(self.roots.get() & !MARK_MASK); |
||||
} |
||||
} |
||||
|
||||
/// Returns a reference to the value of `key`'s `GcBox`
|
||||
pub(crate) fn key(&self) -> Option<&K> { |
||||
self.inner_key().map(GcBox::value) |
||||
impl core::fmt::Debug for EphemeronBoxHeader { |
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
||||
f.debug_struct("EphemeronBoxHeader") |
||||
.field("roots", &self.roots()) |
||||
.field("marked", &self.is_marked()) |
||||
.finish() |
||||
} |
||||
} |
||||
|
||||
/// Returns a reference to `value`
|
||||
pub(crate) const fn value(&self) -> &V { |
||||
&self.value |
||||
/// The inner allocation of an [`Ephemeron`][crate::Ephemeron] pointer.
|
||||
pub(crate) struct EphemeronBox<K: Trace + ?Sized + 'static, V: Trace + 'static> { |
||||
pub(crate) header: EphemeronBoxHeader, |
||||
data: Cell<Option<NonNull<Data<K, V>>>>, |
||||
} |
||||
|
||||
/// Calls [`Trace::weak_trace()`][crate::Trace] on key
|
||||
fn weak_trace_key(&self) { |
||||
if let Some(key) = self.inner_key() { |
||||
key.weak_trace_inner(); |
||||
impl<K: Trace + ?Sized + 'static, V: Trace + 'static> Drop for EphemeronBox<K, V> { |
||||
fn drop(&mut self) { |
||||
if let Some(data) = self.data.take() { |
||||
// SAFETY: `data` comes from an `into_raw` call, so this pointer is safe to pass to
|
||||
// `from_raw`.
|
||||
drop(unsafe { Box::from_raw(data.as_ptr()) }); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Calls [`Trace::weak_trace()`][crate::Trace] on value
|
||||
fn weak_trace_value(&self) { |
||||
// SAFETY: Value is a sized element that must implement trace. The
|
||||
// operation is safe as EphemeronBox owns value and `Trace::weak_trace`
|
||||
// must be implemented on it
|
||||
unsafe { |
||||
self.value().weak_trace(); |
||||
struct Data<K: Trace + ?Sized + 'static, V: Trace + 'static> { |
||||
key: NonNull<GcBox<K>>, |
||||
value: V, |
||||
} |
||||
|
||||
impl<K: Trace + ?Sized, V: Trace> EphemeronBox<K, V> { |
||||
pub(crate) fn new(key: &Gc<K>, value: V) -> Self { |
||||
let data = Box::into_raw(Box::new(Data { |
||||
key: key.inner_ptr(), |
||||
value, |
||||
})); |
||||
// SAFETY: `Box::into_raw` must always return a non-null pointer.
|
||||
let data = unsafe { NonNull::new_unchecked(data) }; |
||||
Self { |
||||
header: EphemeronBoxHeader::new(), |
||||
data: Cell::new(Some(data)), |
||||
} |
||||
} |
||||
|
||||
// `EphemeronBox`'s Finalize is special in that if it is determined to be unreachable
|
||||
// and therefore so has the `GcBox` that `key`stores the pointer to, then we set `key`
|
||||
// to None to guarantee that we do not access freed memory.
|
||||
impl<K: Trace + ?Sized, V: Trace + ?Sized> Finalize for EphemeronBox<K, V> { |
||||
fn finalize(&self) { |
||||
self.key.set(None); |
||||
/// Returns `true` if the two references refer to the same `GcBox`.
|
||||
pub(crate) fn ptr_eq(this: &Self, other: &Self) -> bool { |
||||
// Use .header to ignore fat pointer vtables, to work around
|
||||
// https://github.com/rust-lang/rust/issues/46139
|
||||
ptr::eq(&this.header, &other.header) |
||||
} |
||||
|
||||
/// Returns a reference to the ephemeron's value or None.
|
||||
pub(crate) fn value(&self) -> Option<&V> { |
||||
// SAFETY: the garbage collector ensures `ptr` is valid as long as `data` is `Some`.
|
||||
unsafe { self.data.get().map(|ptr| &ptr.as_ref().value) } |
||||
} |
||||
|
||||
// SAFETY: EphemeronBox implements primarly two methods of trace `Trace::is_marked_ephemeron`
|
||||
// to determine whether the key field is stored and `Trace::weak_trace` which continues the `Trace::weak_trace()`
|
||||
// into `key` and `value`.
|
||||
unsafe impl<K: Trace + ?Sized, V: Trace + ?Sized> Trace for EphemeronBox<K, V> { |
||||
unsafe fn trace(&self) { |
||||
/* An ephemeron is never traced with Phase One Trace */ |
||||
/// Marks this `EphemeronBox` as live.
|
||||
///
|
||||
/// This doesn't mark the inner value of the ephemeron. [`ErasedEphemeronBox::trace`]
|
||||
/// does this, and it's called by the garbage collector on demand.
|
||||
pub(crate) unsafe fn mark(&self) { |
||||
self.header.mark(); |
||||
} |
||||
|
||||
/// Increases the root count on this `EphemeronBox`.
|
||||
///
|
||||
/// Roots prevent the `EphemeronBox` from being destroyed by the garbage collector.
|
||||
pub(crate) fn root(&self) { |
||||
self.header.inc_roots(); |
||||
} |
||||
|
||||
/// Decreases the root count on this `EphemeronBox`.
|
||||
///
|
||||
/// Roots prevent the `EphemeronBox` from being destroyed by the garbage collector.
|
||||
pub(crate) fn unroot(&self) { |
||||
self.header.dec_roots(); |
||||
} |
||||
} |
||||
|
||||
/// Checks if the `key`'s `GcBox` has been marked by `Trace::trace()` or `Trace::weak_trace`.
|
||||
fn is_marked_ephemeron(&self) -> bool { |
||||
self.is_marked() |
||||
pub(crate) trait ErasedEphemeronBox { |
||||
/// Gets the header of the `EphemeronBox`.
|
||||
fn header(&self) -> &EphemeronBoxHeader; |
||||
|
||||
/// Traces through the `EphemeronBox`'s held value, but only if it's marked and its key is also
|
||||
/// marked. Returns `true` if the ephemeron successfuly traced through its value. This also
|
||||
/// considers ephemerons that are marked but don't have their value anymore as
|
||||
/// "successfully traced".
|
||||
unsafe fn trace(&self) -> bool; |
||||
|
||||
/// Runs the finalization logic of the `EphemeronBox`'s held value, if the key is still live,
|
||||
/// and clears its contents.
|
||||
fn finalize_and_clear(&self); |
||||
} |
||||
|
||||
/// Checks if this `EphemeronBox` has already been determined reachable. If so, continue to trace
|
||||
/// value in `key` and `value`.
|
||||
unsafe fn weak_trace(&self) { |
||||
if self.is_marked() { |
||||
self.weak_trace_key(); |
||||
self.weak_trace_value(); |
||||
impl<K: Trace + ?Sized, V: Trace> ErasedEphemeronBox for EphemeronBox<K, V> { |
||||
fn header(&self) -> &EphemeronBoxHeader { |
||||
&self.header |
||||
} |
||||
|
||||
unsafe fn trace(&self) -> bool { |
||||
if !self.header.is_marked() { |
||||
return false; |
||||
} |
||||
|
||||
// EphemeronBox does not implement root.
|
||||
unsafe fn root(&self) {} |
||||
let Some(data) = self.data.get() else { |
||||
return true; |
||||
}; |
||||
|
||||
// EphemeronBox does not implement unroot
|
||||
unsafe fn unroot(&self) {} |
||||
// SAFETY: `data` comes from a `Box`, so it is safe to dereference.
|
||||
let data = unsafe { data.as_ref() }; |
||||
// SAFETY: `key` comes from a `Gc`, and the garbage collector only invalidates
|
||||
// `key` when it is unreachable, making `key` always valid.
|
||||
let key = unsafe { data.key.as_ref() }; |
||||
|
||||
// An `EphemeronBox`'s key is set to None once it has been finalized.
|
||||
//
|
||||
// NOTE: while it is possible for the `key`'s pointer value to be
|
||||
// resurrected, we should still consider the finalize the ephemeron
|
||||
// box and set the `key` to None.
|
||||
fn run_finalizer(&self) { |
||||
Finalize::finalize(self); |
||||
let is_key_marked = key.is_marked(); |
||||
|
||||
if is_key_marked { |
||||
// SAFETY: this is safe to call, since we want to trace all reachable objects
|
||||
// from a marked ephemeron that holds a live `key`.
|
||||
unsafe { data.value.trace() } |
||||
} |
||||
|
||||
is_key_marked |
||||
} |
||||
|
||||
fn finalize_and_clear(&self) { |
||||
if let Some(data) = self.data.take() { |
||||
// SAFETY: `data` comes from an `into_raw` call, so this pointer is safe to pass to
|
||||
// `from_raw`.
|
||||
let contents = unsafe { Box::from_raw(data.as_ptr()) }; |
||||
contents.value.finalize(); |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,5 +1,5 @@
|
||||
mod ephemeron_box; |
||||
mod gc_box; |
||||
|
||||
pub(crate) use self::ephemeron_box::EphemeronBox; |
||||
pub(crate) use self::ephemeron_box::{EphemeronBox, ErasedEphemeronBox}; |
||||
pub use self::gc_box::GcBox; |
||||
|
@ -0,0 +1,87 @@
|
||||
use std::ptr::{self, addr_of_mut, NonNull}; |
||||
|
||||
/// A [`NonNull`] pointer with a `rooted` tag.
|
||||
///
|
||||
/// This pointer can be created only from pointers that are 2-byte aligned. In other words,
|
||||
/// the pointer must point to an address that is a multiple of 2.
|
||||
pub(crate) struct Rootable<T: ?Sized> { |
||||
ptr: NonNull<T>, |
||||
} |
||||
|
||||
impl<T: ?Sized> Copy for Rootable<T> {} |
||||
|
||||
impl<T: ?Sized> Clone for Rootable<T> { |
||||
fn clone(&self) -> Self { |
||||
Self { ptr: self.ptr } |
||||
} |
||||
} |
||||
|
||||
impl<T: ?Sized> std::fmt::Debug for Rootable<T> { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.debug_struct("Rootable") |
||||
.field("ptr", &self.as_ptr()) |
||||
.field("is_rooted", &self.is_rooted()) |
||||
.finish() |
||||
} |
||||
} |
||||
|
||||
impl<T: ?Sized> Rootable<T> { |
||||
/// Creates a new `Rootable` without checking if the [`NonNull`] is properly aligned.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `ptr` must be 2-byte aligned.
|
||||
pub(crate) const unsafe fn new_unchecked(ptr: NonNull<T>) -> Self { |
||||
Self { ptr } |
||||
} |
||||
|
||||
/// Returns `true` if the pointer is rooted.
|
||||
pub(crate) fn is_rooted(self) -> bool { |
||||
self.ptr.as_ptr().cast::<u8>() as usize & 1 != 0 |
||||
} |
||||
|
||||
/// Returns a pointer with the same address as `self` but rooted.
|
||||
pub(crate) fn rooted(self) -> Self { |
||||
let ptr = self.ptr.as_ptr(); |
||||
let data = ptr.cast::<u8>(); |
||||
let addr = data as isize; |
||||
let ptr = set_data_ptr(ptr, data.wrapping_offset((addr | 1) - addr)); |
||||
// SAFETY: ptr must be a non null value.
|
||||
unsafe { Self::new_unchecked(NonNull::new_unchecked(ptr)) } |
||||
} |
||||
|
||||
/// Returns a pointer with the same address as `self` but unrooted.
|
||||
pub(crate) fn unrooted(self) -> Self { |
||||
let ptr = self.ptr.as_ptr(); |
||||
let data = ptr.cast::<u8>(); |
||||
let addr = data as isize; |
||||
let ptr = set_data_ptr(ptr, data.wrapping_offset((addr & !1) - addr)); |
||||
// SAFETY: ptr must be a non null value
|
||||
unsafe { Self::new_unchecked(NonNull::new_unchecked(ptr)) } |
||||
} |
||||
|
||||
/// Acquires the underlying `NonNull` pointer.
|
||||
pub(crate) fn as_ptr(self) -> NonNull<T> { |
||||
self.unrooted().ptr |
||||
} |
||||
|
||||
/// Returns a shared reference to the pointee.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// See [`NonNull::as_ref`].
|
||||
pub(crate) unsafe fn as_ref(&self) -> &T { |
||||
// SAFETY: it is the caller's job to ensure the safety of this operation.
|
||||
unsafe { self.as_ptr().as_ref() } |
||||
} |
||||
} |
||||
|
||||
// Technically, this function is safe, since we're just modifying the address of a pointer without
|
||||
// dereferencing it.
|
||||
fn set_data_ptr<T: ?Sized, U>(mut ptr: *mut T, data: *mut U) -> *mut T { |
||||
// SAFETY: this should be safe as ptr must be a valid nonnull
|
||||
unsafe { |
||||
ptr::write(addr_of_mut!(ptr).cast::<*mut u8>(), data.cast::<u8>()); |
||||
} |
||||
ptr |
||||
} |
Loading…
Reference in new issue