Skip to content

Commit

Permalink
feat: v8::Isolate::{add,remove}_gc_prologue_callback (denoland#1142)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartlomieju committed Nov 30, 2022
1 parent 3783a5c commit caa2ef4
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,19 @@ bool v8__Isolate__AddMessageListener(v8::Isolate* isolate,
return isolate->AddMessageListener(callback);
}

void v8__Isolate__AddGCPrologueCallback(v8::Isolate* isolate,
v8::Isolate::GCCallbackWithData callback,
void* data,
v8::GCType gc_type_filter) {
isolate->AddGCPrologueCallback(callback, data, gc_type_filter);
}

void v8__Isolate__RemoveGCPrologueCallback(v8::Isolate* isolate,
v8::Isolate::GCCallbackWithData callback,
void* data) {
isolate->RemoveGCPrologueCallback(callback, data);
}

void v8__Isolate__AddNearHeapLimitCallback(v8::Isolate* isolate,
v8::NearHeapLimitCallback callback,
void* data) {
Expand Down
73 changes: 73 additions & 0 deletions src/gc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/// Applications can register callback functions which will be called before and
/// after certain garbage collection operations. Allocations are not allowed in
/// the callback functions, you therefore cannot manipulate objects (set or
/// delete properties for example) since it is possible such operations will
/// result in the allocation of objects.
#[repr(C)]
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct GCType(u32);

pub const GC_TYPE_TYPE_SCAVENGE: GCType = GCType(1);

pub const GC_TYPE_MINOR_MARK_COMPACT: GCType = GCType(2);

pub const GC_TYPE_MARK_SWEEP_COMPACT: GCType = GCType(4);

pub const GC_TYPE_INCREMENTAL_MARKING: GCType = GCType(8);

pub const GC_TYPE_PROCESS_WEAK_CALLBACK: GCType = GCType(16);

pub const GC_TYPE_ALL: GCType = GCType(31);

impl std::ops::BitOr for GCType {
type Output = Self;

fn bitor(self, Self(rhs): Self) -> Self {
let Self(lhs) = self;
Self(lhs | rhs)
}
}

/// GCCallbackFlags is used to notify additional information about the GC
/// callback.
/// - GCCallbackFlagConstructRetainedObjectInfos: The GC callback is for
/// constructing retained object infos.
/// - GCCallbackFlagForced: The GC callback is for a forced GC for testing.
/// - GCCallbackFlagSynchronousPhantomCallbackProcessing: The GC callback
/// is called synchronously without getting posted to an idle task.
/// - GCCallbackFlagCollectAllAvailableGarbage: The GC callback is called
/// in a phase where V8 is trying to collect all available garbage
/// (e.g., handling a low memory notification).
/// - GCCallbackScheduleIdleGarbageCollection: The GC callback is called to
/// trigger an idle garbage collection.
#[repr(C)]
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct GCCallbackFlags(u32);

pub const GC_CALLBACK_FLAGS_NO_FLAGS: GCCallbackFlags = GCCallbackFlags(0);

pub const GC_CALLBACK_FLAGS_CONSTRUCT_RETAINED_OBJECT_INFOS: GCCallbackFlags =
GCCallbackFlags(2);

pub const GC_CALLBACK_FLAGS_FORCED: GCCallbackFlags = GCCallbackFlags(4);

pub const GC_CALLBACK_FLAGS_SYNCHRONOUS_PHANTOM_CALLBACK_PROCESSING:
GCCallbackFlags = GCCallbackFlags(8);

pub const GC_CALLBACK_FLAGS_COLLECT_ALL_AVAILABLE_GARBAGE: GCCallbackFlags =
GCCallbackFlags(16);

pub const GC_CALLBACK_FLAGS_COLLECT_ALL_EXTERNAL_MEMORY: GCCallbackFlags =
GCCallbackFlags(32);

pub const GC_CALLBACK_FLAGS_SCHEDULE_IDLE_GARBAGE_COLLECTION: GCCallbackFlags =
GCCallbackFlags(64);

impl std::ops::BitOr for GCCallbackFlags {
type Output = Self;

fn bitor(self, Self(rhs): Self) -> Self {
let Self(lhs) = self;
Self(lhs | rhs)
}
}
52 changes: 52 additions & 0 deletions src/isolate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license.
use crate::function::FunctionCallbackInfo;
use crate::gc::GCCallbackFlags;
use crate::gc::GCType;
use crate::handle::FinalizerCallback;
use crate::handle::FinalizerMap;
use crate::isolate_create_params::raw;
Expand Down Expand Up @@ -302,6 +304,13 @@ where
pub type HostCreateShadowRealmContextCallback =
for<'s> fn(scope: &mut HandleScope<'s>) -> Option<Local<'s, Context>>;

pub type GcCallbackWithData = extern "C" fn(
isolate: *mut Isolate,
r#type: GCType,
flags: GCCallbackFlags,
data: *mut c_void,
);

pub type InterruptCallback =
extern "C" fn(isolate: &mut Isolate, data: *mut c_void);

Expand Down Expand Up @@ -373,6 +382,17 @@ extern "C" {
isolate: *mut Isolate,
callback: MessageCallback,
) -> bool;
fn v8__Isolate__AddGCPrologueCallback(
isolate: *mut Isolate,
callback: GcCallbackWithData,
data: *mut c_void,
gc_type_filter: GCType,
);
fn v8__Isolate__RemoveGCPrologueCallback(
isolate: *mut Isolate,
callback: GcCallbackWithData,
data: *mut c_void,
);
fn v8__Isolate__AddNearHeapLimitCallback(
isolate: *mut Isolate,
callback: NearHeapLimitCallback,
Expand Down Expand Up @@ -974,6 +994,38 @@ impl Isolate {
}
}

/// Enables the host application to receive a notification before a
/// garbage collection. Allocations are allowed in the callback function,
/// but the callback is not re-entrant: if the allocation inside it will
/// trigger the garbage collection, the callback won't be called again.
/// It is possible to specify the GCType filter for your callback. But it is
/// not possible to register the same callback function two times with
/// different GCType filters.
#[allow(clippy::not_unsafe_ptr_arg_deref)] // False positive.
#[inline(always)]
pub fn add_gc_prologue_callback(
&mut self,
callback: GcCallbackWithData,
data: *mut c_void,
gc_type_filter: GCType,
) {
unsafe {
v8__Isolate__AddGCPrologueCallback(self, callback, data, gc_type_filter)
}
}

/// This function removes callback which was installed by
/// AddGCPrologueCallback function.
#[allow(clippy::not_unsafe_ptr_arg_deref)] // False positive.
#[inline(always)]
pub fn remove_gc_prologue_callback(
&mut self,
callback: GcCallbackWithData,
data: *mut c_void,
) {
unsafe { v8__Isolate__RemoveGCPrologueCallback(self, callback, data) }
}

/// Add a callback to invoke in case the heap size is close to the heap limit.
/// If multiple callbacks are added, only the most recently added callback is
/// invoked.
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod external_references;
pub mod fast_api;
mod fixed_array;
mod function;
mod gc;
mod get_property_names_args_builder;
mod handle;
pub mod icu;
Expand Down Expand Up @@ -90,6 +91,7 @@ pub use exception::*;
pub use external_references::ExternalReference;
pub use external_references::ExternalReferences;
pub use function::*;
pub use gc::*;
pub use get_property_names_args_builder::*;
pub use handle::Global;
pub use handle::Handle;
Expand Down
86 changes: 86 additions & 0 deletions tests/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ fn global_from_into_raw() {
(raw, weak)
};

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(!weak.is_empty());

Expand All @@ -171,6 +172,7 @@ fn global_from_into_raw() {
assert_eq!(global_from_weak, reconstructed);
}

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(weak.is_empty());
}
Expand Down Expand Up @@ -6377,13 +6379,15 @@ fn clear_kept_objects() {
let context = v8::Context::new(scope);
let scope = &mut v8::ContextScope::new(scope, context);

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
let step1 = r#"
var weakrefs = [];
for (let i = 0; i < 424242; i++) weakrefs.push(new WeakRef({ i }));
gc();
if (weakrefs.some(w => !w.deref())) throw "fail";
"#;

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
let step2 = r#"
gc();
if (weakrefs.every(w => w.deref())) throw "fail";
Expand Down Expand Up @@ -7416,6 +7420,7 @@ fn weak_handle() {

let scope = &mut v8::HandleScope::new(scope);

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();

assert!(weak.is_empty());
Expand Down Expand Up @@ -7445,6 +7450,7 @@ fn finalizers() {
}

let scope = &mut v8::HandleScope::new(scope);
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
}

Expand Down Expand Up @@ -7480,6 +7486,7 @@ fn finalizers() {
};

let scope = &mut v8::HandleScope::new(scope);
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(weak.is_empty());
assert!(finalizer_called.get());
Expand Down Expand Up @@ -7514,6 +7521,7 @@ fn guaranteed_finalizers() {
}

let scope = &mut v8::HandleScope::new(scope);
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
}

Expand Down Expand Up @@ -7549,6 +7557,7 @@ fn guaranteed_finalizers() {
};

let scope = &mut v8::HandleScope::new(scope);
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(weak.is_empty());
assert!(finalizer_called.get());
Expand All @@ -7574,6 +7583,7 @@ fn weak_from_global() {
assert_eq!(weak.to_global(scope).unwrap(), global);

drop(global);
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(weak.is_empty());
}
Expand Down Expand Up @@ -7623,6 +7633,7 @@ fn weak_from_into_raw() {
assert!(!finalizer_called.get());
(weak1, weak2)
};
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(weak1.is_empty());
assert!(weak2.is_empty());
Expand All @@ -7637,6 +7648,7 @@ fn weak_from_into_raw() {
v8::Weak::new(scope, local)
};
assert!(!weak.is_empty());
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(weak.is_empty());
assert_eq!(weak.into_raw(), None);
Expand Down Expand Up @@ -7668,6 +7680,7 @@ fn weak_from_into_raw() {
let raw2 = weak_with_finalizer.into_raw();
assert!(raw1.is_some());
assert!(raw2.is_some());
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(finalizer_called.get());
let weak1 = unsafe { v8::Weak::from_raw(scope, raw1) };
Expand All @@ -7682,8 +7695,10 @@ fn weak_from_into_raw() {
let local = v8::Object::new(scope);
v8::Weak::new(scope, local).into_raw();
v8::Weak::with_finalizer(scope, local, Box::new(|_| {})).into_raw();
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
}
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
}

Expand Down Expand Up @@ -7724,6 +7739,7 @@ fn drop_weak_from_raw_in_finalizer() {
}

assert!(!finalized.get());
// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert!(finalized.get());
}
Expand Down Expand Up @@ -8681,3 +8697,73 @@ fn test_fast_calls_onebytestring() {
eval(scope, source).unwrap();
assert_eq!("fast", unsafe { WHO });
}

#[test]
fn gc_callbacks() {
let _setup_guard = setup();

#[derive(Default)]
struct GCCallbackState {
mark_sweep_calls: u64,
incremental_marking_calls: u64,
}

extern "C" fn callback(
_isolate: *mut v8::Isolate,
r#type: v8::GCType,
_flags: v8::GCCallbackFlags,
data: *mut c_void,
) {
// We should get a mark-sweep GC here.
assert_eq!(r#type, v8::GC_TYPE_MARK_SWEEP_COMPACT);
let state = unsafe { &mut *(data as *mut GCCallbackState) };
state.mark_sweep_calls += 1;
}

extern "C" fn callback2(
_isolate: *mut v8::Isolate,
r#type: v8::GCType,
_flags: v8::GCCallbackFlags,
data: *mut c_void,
) {
// We should get a mark-sweep GC here.
assert_eq!(r#type, v8::GC_TYPE_INCREMENTAL_MARKING);
let state = unsafe { &mut *(data as *mut GCCallbackState) };
state.incremental_marking_calls += 1;
}

let mut state = GCCallbackState::default();
let state_ptr = &mut state as *mut _ as *mut c_void;
let isolate = &mut v8::Isolate::new(Default::default());
isolate.add_gc_prologue_callback(callback, state_ptr, v8::GC_TYPE_ALL);
isolate.add_gc_prologue_callback(
callback2,
state_ptr,
v8::GC_TYPE_INCREMENTAL_MARKING | v8::GC_TYPE_PROCESS_WEAK_CALLBACK,
);

{
let scope = &mut v8::HandleScope::new(isolate);
let context = v8::Context::new(scope);
let scope = &mut v8::ContextScope::new(scope, context);

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
assert_eq!(state.mark_sweep_calls, 1);
assert_eq!(state.incremental_marking_calls, 0);
}

isolate.remove_gc_prologue_callback(callback, state_ptr);
isolate.remove_gc_prologue_callback(callback2, state_ptr);
{
let scope = &mut v8::HandleScope::new(isolate);
let context = v8::Context::new(scope);
let scope = &mut v8::ContextScope::new(scope, context);

// TODO use binding to Isolate::RequestGarbageCollectionForTesting instead of gc()
eval(scope, "gc()").unwrap();
// Assert callback was removed and not called again.
assert_eq!(state.mark_sweep_calls, 1);
assert_eq!(state.incremental_marking_calls, 0);
}
}

0 comments on commit caa2ef4

Please sign in to comment.