From c516feebaa1269535d13570c3026f8748e3f3712 Mon Sep 17 00:00:00 2001 From: Santiago Gimeno Date: Mon, 9 Jun 2025 13:00:54 +0200 Subject: [PATCH 1/2] src: a couple of improvements in TSList API Adds a for_each method to TSList that passes the current list size and item to the callback. Updates erase to return the new size after removal. These changes improve thread-safe iteration and management of hooks. --- src/nsolid/thread_safe.h | 20 ++++++- test/cctest/test_nsolid_thread_safe.cc | 80 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/nsolid/thread_safe.h b/src/nsolid/thread_safe.h index a4d2e825a63..58483bbcdc1 100644 --- a/src/nsolid/thread_safe.h +++ b/src/nsolid/thread_safe.h @@ -146,9 +146,17 @@ struct TSList { nsuv::ns_mutex::scoped_lock lock(lock_); std::for_each(list_.begin(), list_.end(), fn); } - inline void erase(iterator it) { + inline void for_each(std::function fn) { + nsuv::ns_mutex::scoped_lock lock(lock_); + size_t current_size = list_.size(); + for (auto& item : list_) { + fn(item, current_size); + } + } + inline size_t erase(iterator it) { nsuv::ns_mutex::scoped_lock lock(lock_); list_.erase(it); + return list_.size(); } inline size_t size() { nsuv::ns_mutex::scoped_lock lock(lock_); @@ -178,9 +186,17 @@ struct TSList { nsuv::ns_mutex::scoped_lock lock(lock_); std::for_each(list_.begin(), list_.end(), fn); } - inline void erase(iterator it) { + inline void for_each(std::function fn) { + nsuv::ns_mutex::scoped_lock lock(lock_); + size_t current_size = list_.size(); + for (auto& item : list_) { + fn(item, current_size); + } + } + inline size_t erase(iterator it) { nsuv::ns_mutex::scoped_lock lock(lock_); list_.erase(it); + return list_.size(); } inline size_t size() { nsuv::ns_mutex::scoped_lock lock(lock_); diff --git a/test/cctest/test_nsolid_thread_safe.cc b/test/cctest/test_nsolid_thread_safe.cc index 364a7ece608..a5b8bea25af 100644 --- a/test/cctest/test_nsolid_thread_safe.cc +++ b/test/cctest/test_nsolid_thread_safe.cc @@ -50,6 +50,86 @@ TEST(TSListTest, ObjectIterator) { }); } +// Test TSList::for_each with size parameter (object specialization) +TEST(TSListTest, ObjectForEachWithSize) { + TSList list; + auto it1 = list.push_back(10); + auto it2 = list.push_back(20); + auto it3 = list.push_back(30); + std::vector values; + std::vector sizes; + list.for_each([&](const int& v, size_t size) { + values.push_back(v); + sizes.push_back(size); + }); + EXPECT_EQ(values.size(), 3u); + EXPECT_EQ(sizes[0], 3u); + EXPECT_EQ(sizes[1], 3u); + EXPECT_EQ(sizes[2], 3u); + EXPECT_EQ(values[0], 10); + EXPECT_EQ(values[1], 20); + EXPECT_EQ(values[2], 30); +} + +// Test TSList::erase returns new size (object specialization) +TEST(TSListTest, ObjectEraseReturnsSize) { + TSList list; + auto it1 = list.push_back(1); + auto it2 = list.push_back(2); + auto it3 = list.push_back(3); + EXPECT_EQ(list.erase(it2), 2u); + EXPECT_EQ(list.erase(it1), 1u); + EXPECT_EQ(list.erase(it3), 0u); +} + +// Test TSList::for_each with size parameter (pointer specialization) +TEST(TSListTest, PointerForEachWithSize) { + TSList list; + auto it1 = list.push_back(new int(100)); + auto it2 = list.push_back(new int(200)); + auto it3 = list.push_back(new int(300)); + std::vector values; + std::vector sizes; + list.for_each([&](int* v, size_t size) { + values.push_back(*v); + sizes.push_back(size); + }); + EXPECT_EQ(values.size(), 3u); + EXPECT_EQ(sizes[0], 3u); + EXPECT_EQ(sizes[1], 3u); + EXPECT_EQ(sizes[2], 3u); + EXPECT_EQ(values[0], 100); + EXPECT_EQ(values[1], 200); + EXPECT_EQ(values[2], 300); + // Clean up + int* tmp = *it1; + delete tmp; + tmp = *it2; + delete tmp; + tmp = *it3; + delete tmp; + list.erase(it1); + list.erase(it2); + list.erase(it3); +} + +// Test TSList::erase returns new size (pointer specialization) +TEST(TSListTest, PointerEraseReturnsSize) { + TSList list; + auto it1 = list.push_back(new int(1)); + auto it2 = list.push_back(new int(2)); + auto it3 = list.push_back(new int(3)); + int* p1 = *it1; + int* p2 = *it2; + int* p3 = *it3; + EXPECT_EQ(list.erase(it2), 2u); + EXPECT_EQ(list.erase(it1), 1u); + EXPECT_EQ(list.erase(it3), 0u); + delete p1; + delete p2; + delete p3; +} + TEST(TSListTest, PointerIterator) { TSList list; auto it1 = list.push_back(new int(1)); From cae6e4eef5fc78d5d85de9f5ef708201fd286119 Mon Sep 17 00:00:00 2001 From: Santiago Gimeno Date: Tue, 10 Jun 2025 11:23:15 +0200 Subject: [PATCH 2/2] src: add AddCodeEventHook API for code events Introduces a new API to register code event hooks in nsolid. This includes new handler classes, queueing, and infrastructure to allow extensible code event observation and callbacks. --- node.gyp | 1 + src/nsolid.cc | 47 ++++++ src/nsolid.h | 94 +++++++++++- src/nsolid/nsolid_api.cc | 105 +++++++++++++ src/nsolid/nsolid_api.h | 26 ++++ src/nsolid/nsolid_code_event_handler.cc | 44 ++++++ src/nsolid/nsolid_code_event_handler.h | 28 ++++ test/addons/nsolid-code-event-hook/binding.cc | 141 ++++++++++++++++++ .../addons/nsolid-code-event-hook/binding.gyp | 9 ++ .../nsolid-code-event-hook.js | 91 +++++++++++ 10 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 src/nsolid/nsolid_code_event_handler.cc create mode 100644 src/nsolid/nsolid_code_event_handler.h create mode 100644 test/addons/nsolid-code-event-hook/binding.cc create mode 100644 test/addons/nsolid-code-event-hook/binding.gyp create mode 100644 test/addons/nsolid-code-event-hook/nsolid-code-event-hook.js diff --git a/node.gyp b/node.gyp index 9c56b09ca6f..ed5797f1349 100644 --- a/node.gyp +++ b/node.gyp @@ -515,6 +515,7 @@ 'src/nsolid.cc', 'src/nsolid/continuous_profiler.cc', 'src/nsolid/nsolid_api.cc', + 'src/nsolid/nsolid_code_event_handler.cc', 'src/nsolid/nsolid_trace.cc', 'src/nsolid/nsolid_cpu_profiler.cc', 'src/nsolid/nsolid_heap_snapshot.cc', diff --git a/src/nsolid.cc b/src/nsolid.cc index 92e6d2be5fe..91555dfa4c3 100644 --- a/src/nsolid.cc +++ b/src/nsolid.cc @@ -597,6 +597,42 @@ int Snapshot::get_snapshot_(SharedEnvInst envinst, GetHeapSnapshot(envinst, redacted, data, proxy, deleter); } +class CodeEventHook::Impl { + public: + Impl() = default; + ~Impl() { + // Remove the hook from the TSList in EnvList + EnvList::Inst()->RemoveCodeEventHook(hook_); + } + + private: + friend class CodeEventHook; + void Setup(internal::code_event_hook_proxy_sig cb, + void(*deleter)(void*), + void* data) { + // Add hook to the TSList in EnvList + hook_ = EnvList::Inst()->AddCodeEventHook(data, cb, deleter); + } + + TSList::iterator hook_; +}; + +CodeEventHook::CodeEventHook(): impl_(std::make_unique()) { +} + +CodeEventHook::~CodeEventHook() = default; + +void CodeEventHook::Dispose() { + // This method transfers ownership to the callee and deletes the object. + // The caller must not use this object after calling Dispose(). + delete this; +} + +void CodeEventHook::DoSetup(internal::code_event_hook_proxy_sig cb, + internal::deleter_sig deleter, + void* data) { + impl_->Setup(cb, deleter, data); +} namespace internal { @@ -637,6 +673,17 @@ void thread_removed_hook_(void* data, EnvList::Inst()->EnvironmentDeletionHook(data, proxy, deleter); } +CodeEventHook* add_code_event_hook_(void* data, + code_event_hook_proxy_sig proxy, + deleter_sig deleter) { + CodeEventHook* hook = new (std::nothrow) CodeEventHook(); + if (hook == nullptr) { + return nullptr; + } + hook->DoSetup(proxy, deleter, data); + return hook; +} + int queue_callback_(void* data, queue_callback_proxy_sig proxy) { return EnvList::Inst()->QueueCallback(proxy, data); diff --git a/src/nsolid.h b/src/nsolid.h index 1b44a1ce907..1b4dd75971c 100644 --- a/src/nsolid.h +++ b/src/nsolid.h @@ -27,6 +27,8 @@ namespace nsolid { class EnvInst; struct LogWriteInfo; +class CodeEventHook; +struct CodeEventInfo; #define kNSByte "byte" #define kNSMhz "MHz" @@ -285,6 +287,7 @@ using on_log_write_hook_proxy_sig = void(*)(SharedEnvInst, LogWriteInfo, void*); using at_exit_hook_proxy_sig = void(*)(bool, bool, void*); using thread_added_hook_proxy_sig = void(*)(SharedEnvInst, void*); using thread_removed_hook_proxy_sig = thread_added_hook_proxy_sig; +using code_event_hook_proxy_sig = void(*)(SharedEnvInst, CodeEventInfo, void*); using deleter_sig = void(*)(void*); using user_data = std::unique_ptr; @@ -314,6 +317,8 @@ void thread_added_hook_proxy_(SharedEnvInst, void* data); template void thread_removed_hook_proxy_(SharedEnvInst, void* data); template +void code_event_hook_proxy_(SharedEnvInst, CodeEventInfo, void*); +template void delete_proxy_(void* g); @@ -348,7 +353,9 @@ NODE_EXTERN void thread_added_hook_(void*, NODE_EXTERN void thread_removed_hook_(void*, thread_removed_hook_proxy_sig, deleter_sig); - +NODE_EXTERN CodeEventHook* add_code_event_hook_(void*, + code_event_hook_proxy_sig, + deleter_sig); } // namespace internal /** @endcond */ @@ -647,6 +654,52 @@ NODE_EXTERN int ThreadAddedHook(Cb&& cb, Data&&... data); template NODE_EXTERN int ThreadRemovedHook(Cb&& cb, Data&&... data); +template +NODE_EXTERN CodeEventHook* AddCodeEventHook(Cb&& cb, Data&&... data); + +struct CodeEventInfo { + uint64_t thread_id; + uint64_t timestamp; + v8::CodeEventType type; + uintptr_t code_start; + uintptr_t prev_code_start; + size_t code_len; + std::string fn_name; + std::string script_name; + int script_line; + int script_column; + std::string comment; +}; + +/** + * @brief Opaque handle for a code event hook registered with AddCodeEventHook. + * + * Only Dispose() should be used to remove the hook and release resources. + */ +class NODE_EXTERN CodeEventHook { + public: + /** + * @brief Dispose and remove the registered code event hook. + * + * After calling Dispose(), this object must not be used again. + */ + void Dispose(); + + private: + CodeEventHook(); + ~CodeEventHook(); + friend CodeEventHook* internal::add_code_event_hook_( + void*, internal::code_event_hook_proxy_sig, internal::deleter_sig); + CodeEventHook(const CodeEventHook&) = delete; + CodeEventHook& operator=(const CodeEventHook&) = delete; + + void DoSetup(internal::code_event_hook_proxy_sig cb, + internal::deleter_sig deleter, + void* data); + + class Impl; + std::unique_ptr impl_; +}; /** * @brief Defines the types of metrics supported @@ -1776,6 +1829,38 @@ int ThreadRemovedHook(Cb&& cb, Data&&... data) { } +/** + * @brief Register a hook (function) to be called on code events. + * + * @tparam Cb Callback type. The callback will be invoked with (...Data) arguments. + * @tparam Data Variable argument types to be propagated to the callback. + * @param cb Hook function with signature: cb(...Data) + * @param data Variable number of arguments to be propagated to the callback. + * @return Pointer to CodeEventHook if successful, nullptr otherwise. + */ +template +inline CodeEventHook* AddCodeEventHook(Cb&& cb, Data&&... data) { + // NOLINTNEXTLINE(build/namespaces) + using namespace std::placeholders; + using UserData = decltype(std::bind( + std::forward(cb), _1, _2, std::forward(data)...)); + + // _1 - SharedEnvInst + // _2 - CodeEventInfo + UserData* user_data = new (std::nothrow) UserData(std::bind( + std::forward(cb), _1, _2, std::forward(data)...)); + if (user_data == nullptr) { + return nullptr; + } + + return internal::add_code_event_hook_( + user_data, + internal::code_event_hook_proxy_, + internal::delete_proxy_); +} + + + namespace internal { template @@ -1842,6 +1927,13 @@ void thread_removed_hook_proxy_(SharedEnvInst envinst, void* data) { (*static_cast(data))(envinst); } +template +void code_event_hook_proxy_(SharedEnvInst envinst, + CodeEventInfo info, + void* data) { + (*static_cast(data))(envinst, std::move(info)); +} + template void delete_proxy_(void* g) { delete static_cast(g); diff --git a/src/nsolid/nsolid_api.cc b/src/nsolid/nsolid_api.cc index 23d283ca6d1..c9a7b514258 100644 --- a/src/nsolid/nsolid_api.cc +++ b/src/nsolid/nsolid_api.cc @@ -1,4 +1,5 @@ #include "nsolid_api.h" +#include "nsolid/nsolid_code_event_handler.h" #include "nsolid/nsolid_heap_snapshot.h" #include "nsolid_bindings.h" #include "node_buffer.h" @@ -841,6 +842,19 @@ void EnvInst::StoreSourceCode(int script_id, } +void EnvInst::setup_code_event_handler() { + if (!code_event_handler_) { + code_event_handler_ = + std::make_unique(isolate_, thread_id_); + code_event_handler_->Enable(); + } +} + +void EnvInst::disable_code_event_handler() { + code_event_handler_.reset(); +} + + EnvList::EnvList(): info_(nlohmann::json()) { int er; // Create event loop and new thread to run EnvList commands. @@ -870,6 +884,13 @@ EnvList::EnvList(): info_(nlohmann::json()) { er = thread_.create(env_list_routine_, this); CHECK_EQ(er, 0); continuous_profiler_ = std::make_shared(&thread_loop_); + on_code_event_q_ = + AsyncTSQueue::create( + &thread_loop_, + +[](CodeEventInfo&& info, EnvList* envlist) { + envlist->got_code_event(std::move(info)); + }, + this); } @@ -979,6 +1000,58 @@ void EnvList::OnUnblockedLoopHook( { 0, proxy, nsolid::internal::user_data(data, deleter) }); } +TSList::iterator EnvList::AddCodeEventHook( + void* data, + internal::code_event_hook_proxy_sig proxy, + internal::deleter_sig deleter) { + + auto it = code_event_hook_list_.push_back( + { proxy, nsolid::internal::user_data(data, deleter) }); + + decltype(env_map_) env_map; + { + // Copy the envinst map so we don't need to keep it locked the entire time. + ns_mutex::scoped_lock lock(map_lock_); + env_map = env_map_; + } + + for (auto& entry : env_map) { + SharedEnvInst envinst = entry.second; + int er = RunCommand(envinst, + CommandType::InterruptOnly, + setup_code_event_handler); + if (er) { + // Nothing to do here, really. + } + } + + return it; +} + +void EnvList::RemoveCodeEventHook( + TSList::iterator it) { + size_t size = code_event_hook_list_.erase(it); + if (size == 0) { + // No hooks left, disable the code event handler. + decltype(env_map_) env_map; + { + // Copy the envinst map so we don't need to keep it locked the entire + // time. + ns_mutex::scoped_lock lock(map_lock_); + env_map = env_map_; + } + + for (auto& entry : env_map) { + SharedEnvInst envinst = entry.second; + int er = RunCommand(envinst, + CommandType::InterruptOnly, + disable_code_event_handler); + if (er) { + // Nothing to do here, really. + } + } + } +} int EnvList::QueueCallback(q_cb_sig cb, uint64_t timeout, void* data) { QCbTimeoutStor* stor = new (std::nothrow) QCbTimeoutStor({ @@ -1042,6 +1115,10 @@ NODE_PERFORMANCE_MILESTONES(V) env->isolate()->AddGCEpilogueCallback(EnvInst::v8_gc_epilogue_cb_, envinst_sp.get()); + if (code_event_hook_list_.size() > 0) { + envinst_sp->setup_code_event_handler(); + } + // Run EnvironmentCreationHook callbacks. env_creation_list_.for_each([envinst_sp](auto& stor) { stor.cb(envinst_sp, stor.data.get()); @@ -1416,6 +1493,18 @@ void EnvList::datapoint_cb_(std::queue&& q) { }); } +void EnvList::setup_code_event_handler(SharedEnvInst envinst_sp) { + DCHECK_EQ(envinst_sp->isolate(), v8::Isolate::TryGetCurrent()); + + envinst_sp->setup_code_event_handler(); +} + +void EnvList::disable_code_event_handler(SharedEnvInst envinst_sp) { + DCHECK_EQ(envinst_sp->isolate(), v8::Isolate::TryGetCurrent()); + + envinst_sp->disable_code_event_handler(); +} + void EnvInst::send_datapoint(MetricsStream::Type type, double value) { @@ -1618,9 +1707,11 @@ void EnvList::log_written_cb_(ns_async*, EnvList* envlist) { // registered users will need to receive their last set of metrics and a // notification that it'll be their last. void EnvList::removed_env_cb_(ns_async*, EnvList* envlist) { + envlist->on_code_event_q_.reset(); if (envlist->continuous_profiler_) { envlist->continuous_profiler_.reset(); } + envlist->removed_env_msg_.close(); envlist->process_callbacks_msg_.close(); envlist->log_written_msg_.close(); @@ -1882,6 +1973,20 @@ void EnvList::fill_trace_id_q() { } } +void EnvList::got_code_event(CodeEventInfo&& info) { + auto envinst_sp = EnvInst::GetInst(info.thread_id); + if (envinst_sp == nullptr) { + return; + } + + code_event_hook_list_.for_each([&info, &envinst_sp](auto& stor, size_t size) { + if (size == 1) { + stor.cb(envinst_sp, std::move(info), stor.data.get()); + } else { + stor.cb(envinst_sp, info, stor.data.get()); + } + }); +} void EnvInst::custom_command_(SharedEnvInst envinst_sp, const std::string req_id) { diff --git a/src/nsolid/nsolid_api.h b/src/nsolid/nsolid_api.h index 2d5b13a5873..e94989eb092 100644 --- a/src/nsolid/nsolid_api.h +++ b/src/nsolid/nsolid_api.h @@ -18,6 +18,7 @@ #include "node_snapshotable.h" #include "nsolid.h" #include "nsuv-inl.h" +#include "async_ts_queue.h" #include "nsolid_heap_snapshot.h" #include "nsolid_trace.h" #include "nsolid_util.h" @@ -33,6 +34,7 @@ namespace node { namespace nsolid { + #define NSOLID_JS_METRICS_COUNTERS(V) \ V(kHttpClientCount) \ V(kHttpServerCount) \ @@ -55,6 +57,7 @@ namespace nsolid { class EnvInst; class EnvList; class ContinuousProfiler; +class NSolidCodeEventHandler; template @@ -241,6 +244,9 @@ class EnvInst { void inc_fs_handles_closed() { fs_handles_closed_++; } void inc_fs_handles_opened() { fs_handles_opened_++; } + void setup_code_event_handler(); + void disable_code_event_handler(); + /* * Return a shared_ptr instead of a normal pointer because the * lifetime of the EnvInst instance depends on the state of several things @@ -416,6 +422,8 @@ class EnvInst { nsuv::ns_mutex source_files_lock_; std::map source_files_; + + std::unique_ptr code_event_handler_; }; /** @@ -492,6 +500,11 @@ class EnvList { nsolid::internal::user_data data; }; + struct CodeEventHookStor { + nsolid::internal::code_event_hook_proxy_sig cb; + nsolid::internal::user_data data; + }; + // Return the one true instance. NSOLID_EXTERN_PRIVATE static EnvList* Inst(); @@ -527,6 +540,11 @@ class EnvList { internal::on_unblock_loop_hook_proxy_sig proxy, internal::deleter_sig deleter); + TSList::iterator AddCodeEventHook(void* data, + internal::code_event_hook_proxy_sig proxy, + internal::deleter_sig deleter); + void RemoveCodeEventHook(TSList::iterator it); + // Queue callbacks to run on the EnvList thread without reference to a // specific EnvInst instance. NSOLID_EXTERN_PRIVATE int QueueCallback(q_cb_sig cb, void* data); @@ -612,6 +630,7 @@ class EnvList { private: friend class EnvInst; friend class Metrics; + friend class NSolidCodeEventHandler; friend class tracing::TracerImpl; EnvList(); @@ -632,6 +651,8 @@ class EnvList { void fill_trace_id_q(); + void got_code_event(CodeEventInfo&& info); + void update_continuous_profiler(bool enabled, uint64_t interval); #ifdef __POSIX__ @@ -655,6 +676,8 @@ class EnvList { static void fill_tracing_ids_cb_(nsuv::ns_async*, EnvList* envlist); static void q_cb_timeout_cb_(nsuv::ns_timer*, QCbTimeoutStor*); static void datapoint_cb_(std::queue&&); + static void setup_code_event_handler(SharedEnvInst envinst_sp); + static void disable_code_event_handler(SharedEnvInst envinst_sp); std::atomic is_alive_ = { true }; // unique agent id @@ -732,6 +755,9 @@ class EnvList { // ContinuousProfiler instance std::shared_ptr continuous_profiler_; + + std::shared_ptr> on_code_event_q_; + TSList code_event_hook_list_; }; diff --git a/src/nsolid/nsolid_code_event_handler.cc b/src/nsolid/nsolid_code_event_handler.cc new file mode 100644 index 00000000000..f78ff9627a9 --- /dev/null +++ b/src/nsolid/nsolid_code_event_handler.cc @@ -0,0 +1,44 @@ +#include "nsolid_code_event_handler.h" +#include "nsolid_api.h" + +namespace node { +namespace nsolid { + +using v8::CodeEvent; +using v8::Isolate; + +NSolidCodeEventHandler::NSolidCodeEventHandler(Isolate* isolate, + uint64_t thread_id): + CodeEventHandler(isolate), isolate_(isolate), thread_id_(thread_id) { +} + +NSolidCodeEventHandler::~NSolidCodeEventHandler() { +} + +void NSolidCodeEventHandler::Handle(CodeEvent* code_event) { + std::string fn_name = *Utf8Value(isolate_, code_event->GetFunctionName()); + std::string script_name = *Utf8Value(isolate_, code_event->GetScriptName()); + int script_line = code_event->GetScriptLine(); + int script_column = code_event->GetScriptColumn(); + v8::CodeEventType type = code_event->GetCodeType(); + uintptr_t prev = type == v8::CodeEventType::kRelocationType ? + code_event->GetPreviousCodeStartAddress() : 0; + CodeEventInfo info { + thread_id_, + uv_hrtime(), + type, + code_event->GetCodeStartAddress(), + prev, + code_event->GetCodeSize(), + fn_name, + script_name, + script_line, + script_column, + std::string(code_event->GetComment()) + }; + + EnvList::Inst()->on_code_event_q_->enqueue(std::move(info)); +} + +} // namespace nsolid +} // namespace node diff --git a/src/nsolid/nsolid_code_event_handler.h b/src/nsolid/nsolid_code_event_handler.h new file mode 100644 index 00000000000..7a7f641bc7a --- /dev/null +++ b/src/nsolid/nsolid_code_event_handler.h @@ -0,0 +1,28 @@ +#ifndef SRC_NSOLID_NSOLID_CODE_EVENT_HANDLER_H_ +#define SRC_NSOLID_NSOLID_CODE_EVENT_HANDLER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "v8.h" +#include "v8-profiler.h" + +namespace node { +namespace nsolid { + +class NSolidCodeEventHandler: public v8::CodeEventHandler { + public: + NSolidCodeEventHandler(v8::Isolate* isolate, uint64_t thread_id); + virtual ~NSolidCodeEventHandler(); + + void Handle(v8::CodeEvent* code_event) override; + + private: + v8::Isolate* isolate_; + uint64_t thread_id_; +}; +} // namespace nsolid +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NSOLID_NSOLID_CODE_EVENT_HANDLER_H_ diff --git a/test/addons/nsolid-code-event-hook/binding.cc b/test/addons/nsolid-code-event-hook/binding.cc new file mode 100644 index 00000000000..beb02a79888 --- /dev/null +++ b/test/addons/nsolid-code-event-hook/binding.cc @@ -0,0 +1,141 @@ +#include +#include +#include +#include "../../../deps/nsuv/include/nsuv-inl.h" +#include +#include +#include + +using node::nsolid::CodeEventHook; + +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::Global; +using v8::Isolate; +using v8::Local; +using v8::Number; +using v8::String; +using v8::Uint32; +using v8::Undefined; +using v8::Value; + +namespace { + +struct CustomDeleter { + void operator()(CodeEventHook* hook) const { + hook->Dispose(); + } +}; + +struct HookHolder { + std::unique_ptr hook; + std::map> js_callback_per_thread; +}; + +std::vector> hooks; +nsuv::ns_mutex hooks_mutex_; + +void CodeEventCallback(std::shared_ptr envinst, + const node::nsolid::CodeEventInfo& info, + HookHolder* holder) { + assert(0 == node::nsolid::RunCommand( + envinst, + node::nsolid::CommandType::EventLoop, + [holder](node::nsolid::SharedEnvInst envinst, + const node::nsolid::CodeEventInfo& info) { + // Call JS callback with function name + if (!holder->js_callback_per_thread[info.thread_id].IsEmpty()) { + Isolate* isolate = Isolate::GetCurrent(); + v8::HandleScope scope(isolate); + Local cb = + holder->js_callback_per_thread[info.thread_id].Get(isolate); + Local argv[2] = { + String::NewFromUtf8(isolate, info.fn_name.c_str()).ToLocalChecked(), + Number::New(isolate, info.thread_id), + }; + // Call the callback in the current context + cb->Call(isolate->GetCurrentContext(), + Undefined(isolate), 2, argv).ToLocalChecked(); + } + }, info)); +} + +void RegisterJSCodeEventCallback(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + assert(args[0]->IsUint32()); + uint32_t index = args[0].As()->Value(); + assert(args[1]->IsFunction()); + Local cb = Local::Cast(args[1]); + uint64_t thread_id = + node::nsolid::GetThreadId(node::nsolid::GetLocalEnvInst(isolate)); + + nsuv::ns_mutex::scoped_lock lock(hooks_mutex_); + assert(index < hooks.size()); + assert(hooks[index]); + HookHolder* holder = hooks[index].get(); + holder->js_callback_per_thread[thread_id].Reset(isolate, cb); +} + +void RegisterCodeEventHook(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + auto holder = std::make_unique(); + HookHolder* holder_ptr = holder.get(); + holder->hook.reset(node::nsolid::AddCodeEventHook(CodeEventCallback, + holder_ptr)); + assert(holder->hook); + nsuv::ns_mutex::scoped_lock lock(hooks_mutex_); + hooks.push_back(std::move(holder)); + + // Return index of the hook + args.GetReturnValue().Set(static_cast(hooks.size() - 1)); +} + +void UnregisterCodeEventHook(const FunctionCallbackInfo& args) { + assert(args[0]->IsUint32()); + uint32_t index = args[0].As()->Value(); + nsuv::ns_mutex::scoped_lock lock(hooks_mutex_); + assert(index < hooks.size()); + assert(hooks[index]); + HookHolder* holder = hooks[index].get(); + holder->hook.reset(); + holder->js_callback_per_thread.clear(); +} + +void TriggerCodeEvent(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Local src = + String::NewFromUtf8Literal(isolate, + "(function(){ function nsolidTestEvent(){}; " + "nsolidTestEvent(); })();"); + Local script = v8::Script::Compile(context, src).ToLocalChecked(); + script->Run(context).ToLocalChecked(); +} + +void UnregisterAllHooks(const v8::FunctionCallbackInfo&) { + nsuv::ns_mutex::scoped_lock lock(hooks_mutex_); + for (auto& hook : hooks) { + if (hook) { + hook->hook.reset(); + hook->js_callback_per_thread.clear(); + } + } + hooks.clear(); +} + +} // namespace + +NODE_MODULE_INIT(/* exports, module, context */) { + NODE_SET_METHOD(exports, + "registerJSCodeEventCallback", + RegisterJSCodeEventCallback); + NODE_SET_METHOD(exports, "registerCodeEventHook", RegisterCodeEventHook); + NODE_SET_METHOD(exports, "unregisterCodeEventHook", UnregisterCodeEventHook); + NODE_SET_METHOD(exports, "triggerCodeEvent", TriggerCodeEvent); + NODE_SET_METHOD(exports, "unregisterAllHooks", UnregisterAllHooks); + node::nsolid::SharedEnvInst envinst = node::nsolid::GetLocalEnvInst(context); + if (node::nsolid::IsMainThread(envinst)) { + assert(0 == hooks_mutex_.init(true)); + } +} diff --git a/test/addons/nsolid-code-event-hook/binding.gyp b/test/addons/nsolid-code-event-hook/binding.gyp new file mode 100644 index 00000000000..1f128e44676 --- /dev/null +++ b/test/addons/nsolid-code-event-hook/binding.gyp @@ -0,0 +1,9 @@ +{ + "targets": [ + { + "target_name": "binding", + "sources": [ "binding.cc" ], + "cflags": [ "-g" ] + } + ] +} diff --git a/test/addons/nsolid-code-event-hook/nsolid-code-event-hook.js b/test/addons/nsolid-code-event-hook/nsolid-code-event-hook.js new file mode 100644 index 00000000000..dc879447e1f --- /dev/null +++ b/test/addons/nsolid-code-event-hook/nsolid-code-event-hook.js @@ -0,0 +1,91 @@ +'use strict'; + +const { buildType, skip } = require('../../common'); +const assert = require('assert'); +const bindingPath = require.resolve(`./build/${buildType}/binding`); +const binding = require(bindingPath); +const { Worker, isMainThread, threadId } = require('worker_threads'); + +if (!isMainThread && +process.argv[2] !== process.pid) + skip('Test must first run as the main thread'); + +function registerHook(state) { + binding.registerJSCodeEventCallback(state.hookIndex, (fnName, tid) => { + state.called = true; + assert.strictEqual(typeof fnName, 'string'); + state.customFnNameReceived |= fnName.indexOf('nsolidTestEvent') !== -1; + assert.strictEqual(tid, threadId); + }); +} + +function resetState(state) { + state.called = false; + state.customFnNameReceived = false; +} + + +function doTest(hookIndex1, hookIndex2) { + const state1 = { + called: false, + customFnNameReceived: false, + hookIndex: hookIndex1, + }; + + const state2 = { + called: false, + customFnNameReceived: false, + hookIndex: hookIndex2, + }; + + registerHook(state1); + registerHook(state2); + + // Trigger a code event (function definition) + binding.triggerCodeEvent(); + + // Wait a tick to allow the hook to fire + setTimeout(() => { + assert.ok(state1.called, 'Code event hook was not called'); + assert.ok(state1.customFnNameReceived, 'Custom function name was not received'); + assert.ok(state2.called, 'Code event hook was not called'); + assert.ok(state2.customFnNameReceived, 'Custom function name was not received'); + + // Unregister the hooks + binding.unregisterCodeEventHook(hookIndex1); + + resetState(state1); + resetState(state2); + + // The hook should not be called after unregistering + binding.triggerCodeEvent(); + setTimeout(() => { + assert.ok(!state1.called, 'Code event hook was called after unregistering'); + assert.ok(state2.called, 'Code event hook was not called after unregistering'); + binding.unregisterCodeEventHook(hookIndex2); + resetState(state1); + resetState(state2); + setTimeout(() => { + assert.ok(!state1.called, 'Code event hook was called after unregistering'); + assert.ok(!state2.called, 'Code event hook was called after unregistering'); + binding.unregisterAllHooks(); + }, 500); + }, 500); + }, 500); +} + +let hookIndex1; +let hookIndex2; + +if (isMainThread) { + hookIndex1 = binding.registerCodeEventHook(); + hookIndex2 = binding.registerCodeEventHook(); + const worker = new Worker(__filename, { argv: [process.pid, hookIndex1, hookIndex2] }); + worker.on('exit', (code) => { + assert.strictEqual(code, 0); + }); +} else { + hookIndex1 = +process.argv[3]; + hookIndex2 = +process.argv[4]; +} + +doTest(hookIndex1, hookIndex2);