diff --git a/src/binding.cc b/src/binding.cc index c5ff4fd33f..1c84d6f341 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -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) { diff --git a/src/gc.rs b/src/gc.rs new file mode 100644 index 0000000000..59069bfb4d --- /dev/null +++ b/src/gc.rs @@ -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) + } +} diff --git a/src/isolate.rs b/src/isolate.rs index ec01ee6aa5..0e1ce39834 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -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; @@ -302,6 +304,13 @@ where pub type HostCreateShadowRealmContextCallback = for<'s> fn(scope: &mut HandleScope<'s>) -> Option>; +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); @@ -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, @@ -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. diff --git a/src/lib.rs b/src/lib.rs index 30e9cd8f45..fe94e541f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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; diff --git a/tests/test_api.rs b/tests/test_api.rs index b96184b4c0..39b35c5e8a 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -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()); @@ -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()); } @@ -6377,6 +6379,7 @@ 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 })); @@ -6384,6 +6387,7 @@ fn clear_kept_objects() { 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"; @@ -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()); @@ -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(); } @@ -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()); @@ -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(); } @@ -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()); @@ -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()); } @@ -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()); @@ -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); @@ -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) }; @@ -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(); } @@ -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()); } @@ -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); + } +}