diff --git a/src/nsolid.cc b/src/nsolid.cc index 1ad216b8ef..131e9b2e85 100644 --- a/src/nsolid.cc +++ b/src/nsolid.cc @@ -483,6 +483,14 @@ int CpuProfiler::get_cpu_profile_(SharedEnvInst envinst, deleter); } +int Snapshot::start_tracking_heap_objects_(SharedEnvInst envinst, + bool trackAllocations, + void* data) { + if (envinst == nullptr) return UV_ESRCH; + + return NSolidHeapSnapshot::Inst()->StartTrackingHeapObjects( + envinst, trackAllocations, data); +} int Snapshot::get_snapshot_(SharedEnvInst envinst, bool redacted, diff --git a/src/nsolid.h b/src/nsolid.h index 25411b66f1..5e4ee73211 100644 --- a/src/nsolid.h +++ b/src/nsolid.h @@ -6,6 +6,7 @@ #include #include +#include /** * @file nsolid.h @@ -948,6 +949,7 @@ class NODE_EXTERN Snapshot { * concurrent snapshot per thread can be taken. * * @param envinst SharedEnvInst of thread to take the snapshot from. + * @param redacted to get redacted strings in the heap snapshot. * @param cb callback function with the following signature: * `cb(int status, std::string snapshot, ...Data)`. It will be called from the * NSolid thread. @@ -961,7 +963,45 @@ class NODE_EXTERN Snapshot { Cb&& cb, Data&&... data); + /** + * @brief activates the heap profiler tracking objects from a specific JS + * thread. + * + * @param envinst SharedEnvInst of thread to take the snapshot from. + * @param redacted to get redacted strings in the heap snapshot. + * @param data variable number of arguments to be propagated to the callback. + * @return NSOLID_E_SUCCESS in case of success or a different NSOLID_E_ + * error value otherwise. + */ + template + static int StartTrackingHeapObjects(SharedEnvInst envinst, + bool trackAllocations, + Data&&... data); + + /** + * @brief same as TakeSnapshot but stops tracking heap objects in the heap + * profiler. + * + * @param envinst SharedEnvInst of thread to take the snapshot from. + * @param redacted to get redacted strings in the heap snapshot. + * @param cb callback function with the following signature: + * `cb(int status, std::string snapshot, ...Data)`. It will be called from the + * NSolid thread. + * @param data variable number of arguments to be propagated to the callback. + * @return NSOLID_E_SUCCESS in case of success or a different NSOLID_E_ + * error value otherwise. + */ + template + static int StopTrackingHeapObjects(SharedEnvInst envinst, + bool redacted, + Cb&& cb, + Data&&... data); + private: + static int start_tracking_heap_objects_(SharedEnvInst envinst, + bool trackAllocations, + void* data); + static int get_snapshot_(SharedEnvInst envinst, bool redacted, void* data, @@ -1133,6 +1173,38 @@ void CpuProfiler::cpu_profiler_proxy_(int status, (*static_cast(data))(status, json); } +template +int Snapshot::StartTrackingHeapObjects(SharedEnvInst envinst, + bool trackAllocations, + Data&&... data) { + // NOLINTNEXTLINE(build/namespaces) + using namespace std::placeholders; + using UserData = decltype(std::bind(std::forward(data)...)); + + UserData* user_data = + new (std::nothrow) UserData(std::bind(std::forward(data)...)); + + if (user_data == nullptr) { + return UV_ENOMEM; + } + + int er = start_tracking_heap_objects_(envinst, trackAllocations, user_data); + if (er) { + delete user_data; + } + + return er; +} + +template +int Snapshot::StopTrackingHeapObjects(SharedEnvInst envinst, + bool redacted, + Cb&& cb, + Data&&... data) { + int er = TakeSnapshot( + envinst, redacted, std::forward(cb), std::forward(data)...); + return er; +} template int Snapshot::TakeSnapshot(SharedEnvInst envinst, diff --git a/src/nsolid/nsolid_heap_snapshot.cc b/src/nsolid/nsolid_heap_snapshot.cc index f9672594e6..ef3e84dc86 100644 --- a/src/nsolid/nsolid_heap_snapshot.cc +++ b/src/nsolid/nsolid_heap_snapshot.cc @@ -12,6 +12,36 @@ NSolidHeapSnapshot::NSolidHeapSnapshot() { ASSERT_EQ(0, in_progress_heap_snapshots_.init(true)); } +int NSolidHeapSnapshot::StartTrackingHeapObjects(SharedEnvInst envinst, + bool trackAllocations, + void* data) { + uint64_t thread_id = envinst->thread_id(); + nsuv::ns_mutex::scoped_lock lock(&in_progress_heap_snapshots_); + // We can not trigger this command if there is already a snapshot in progress + auto it = threads_running_snapshots_.find(thread_id); + if (it != threads_running_snapshots_.end()) { + return UV_EEXIST; + } + + int status = RunCommand(envinst, + CommandType::Interrupt, + start_tracking_heapobjects, + trackAllocations, + this); + + return status; +} + +int NSolidHeapSnapshot::StopTrackingHeapObjects( + SharedEnvInst envinst, + bool redacted, + void* data, + Snapshot::snapshot_proxy_sig proxy, + internal::deleter_sig deleter) { + int status = GetHeapSnapshot(envinst, redacted, data, proxy, deleter); + return status; +} + int NSolidHeapSnapshot::GetHeapSnapshot(SharedEnvInst envinst, bool redacted, void* data, @@ -38,6 +68,16 @@ int NSolidHeapSnapshot::GetHeapSnapshot(SharedEnvInst envinst, return status; } +void NSolidHeapSnapshot::start_tracking_heapobjects( + SharedEnvInst envinst, + bool trackAllocations, + NSolidHeapSnapshot* snapshotter) { + ASSERT_EQ(snapshotter->is_tracking_heapobjects_, false); + envinst->isolate()->GetHeapProfiler()->StartTrackingHeapObjects( + trackAllocations); + snapshotter->is_tracking_heapobjects_ = true; +} + void NSolidHeapSnapshot::take_snapshot(SharedEnvInst envinst, NSolidHeapSnapshot* snapshotter) { v8::Isolate* isolate = envinst->isolate(); @@ -75,6 +115,12 @@ void NSolidHeapSnapshot::take_snapshot(SharedEnvInst envinst, snapshot->Serialize(&stream); } + // A snapshot requested via `StopTrackingHeapObjects` + if (snapshotter->is_tracking_heapobjects_) { + profiler->StopTrackingHeapObjects(); + snapshotter->is_tracking_heapobjects_ = false; + } + // Work around a deficiency in the API. The HeapSnapshot object is const // but we cannot call HeapProfiler::DeleteAllHeapSnapshots() because that // invalidates _all_ snapshots, including those created by other tools. diff --git a/src/nsolid/nsolid_heap_snapshot.h b/src/nsolid/nsolid_heap_snapshot.h index d295aad27b..e606387731 100644 --- a/src/nsolid/nsolid_heap_snapshot.h +++ b/src/nsolid/nsolid_heap_snapshot.h @@ -34,15 +34,30 @@ class NSolidHeapSnapshot { Snapshot::snapshot_proxy_sig proxy, internal::deleter_sig deleter); + int StartTrackingHeapObjects(SharedEnvInst envinst, + bool trackAllocations, + void* data); + + int StopTrackingHeapObjects(SharedEnvInst envinst, + bool redacted, + void* data, + Snapshot::snapshot_proxy_sig proxy, + internal::deleter_sig deleter); + private: NSolidHeapSnapshot(); + static void start_tracking_heapobjects(SharedEnvInst envinst, + bool trackAllocations, + NSolidHeapSnapshot*); + static void take_snapshot(SharedEnvInst envinst_sp, NSolidHeapSnapshot*); static void snapshot_cb(uint64_t thread_id, int status, const std::string& snapshot); + bool is_tracking_heapobjects_; std::map threads_running_snapshots_; nsuv::ns_mutex in_progress_heap_snapshots_; }; diff --git a/test/addons/nsolid-track-heap-objects/binding.cc b/test/addons/nsolid-track-heap-objects/binding.cc new file mode 100644 index 0000000000..f5f2f5b261 --- /dev/null +++ b/test/addons/nsolid-track-heap-objects/binding.cc @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +#include +#include + +using v8::FunctionCallbackInfo; +using v8::Uint32; +using v8::Value; + +std::map snapshots; + +static void got_snapshot(int status, + std::string snapshot, + uint64_t thread_id) { + assert(status == 0); + snapshots[thread_id] += snapshot; +} + +static void StartTrackingHeapObjects(const FunctionCallbackInfo& args) { + assert(args[0]->IsUint32()); + // Track allocations + assert(args[1]->IsBoolean()); + + uint64_t thread_id = args[0].As()->Value(); + bool track_allocations = args[1]->BooleanValue(args.GetIsolate()); + + node::nsolid::Snapshot::StartTrackingHeapObjects( + node::nsolid::GetEnvInst(thread_id), track_allocations, thread_id); +} + +static void StopTrackingHeapObjects(const FunctionCallbackInfo& args) { + assert(args[0]->IsUint32()); + // Redacted heap snapshot + assert(args[1]->IsBoolean()); + + uint64_t thread_id = args[0].As()->Value(); + bool redacted = args[1]->BooleanValue(args.GetIsolate()); + + node::nsolid::Snapshot::StopTrackingHeapObjects( + node::nsolid::GetEnvInst(thread_id), redacted, got_snapshot, thread_id); +} + +static void at_exit_cb() { + for (const auto& pair : snapshots) { + assert(pair.second.size() > 0); + } +} + +NODE_MODULE_INIT(/* exports, module, context */) { + NODE_SET_METHOD( + exports, "startTrackingHeapObjects", StartTrackingHeapObjects); + NODE_SET_METHOD(exports, "stopTrackingHeapObjects", StopTrackingHeapObjects); + node::nsolid::SharedEnvInst envinst = node::nsolid::GetLocalEnvInst(context); + if (node::nsolid::IsMainThread(envinst)) { + atexit(at_exit_cb); + } +} diff --git a/test/addons/nsolid-track-heap-objects/binding.gyp b/test/addons/nsolid-track-heap-objects/binding.gyp new file mode 100644 index 0000000000..a0b2d6e749 --- /dev/null +++ b/test/addons/nsolid-track-heap-objects/binding.gyp @@ -0,0 +1,16 @@ +{ + 'targets': [{ + 'target_name': 'binding', + 'sources': [ 'binding.cc' ], + 'includes': ['../common.gypi'], + 'target_defaults': { + 'default_configuration': 'Release', + 'configurations': { + 'Debug': { + 'defines': [ 'DEBUG', '_DEBUG' ], + 'cflags': [ '-g', '-O0', '-fstandalone-debug' ], + } + }, + }, + }], +} diff --git a/test/addons/nsolid-track-heap-objects/nsolid-track-heap-objects.js b/test/addons/nsolid-track-heap-objects/nsolid-track-heap-objects.js new file mode 100644 index 0000000000..7055b74c05 --- /dev/null +++ b/test/addons/nsolid-track-heap-objects/nsolid-track-heap-objects.js @@ -0,0 +1,43 @@ +'use strict'; + +const { buildType, mustCall, skip } = require('../../common'); +const assert = require('assert'); +const bindingPath = require.resolve(`./build/${buildType}/binding`); +const binding = require(bindingPath); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +if (process.env.NSOLID_COMMAND) + skip('required to run without the Console'); + +if (!isMainThread && +process.argv[2] !== process.pid) + skip('Test must first run as the main thread'); + +if (!isMainThread) { + parentPort.postMessage('hi'); + if (process.argv[3] === 'spin') { + const t = 0; + while (Date.now() - t < 500); + } else if (process.argv[3] === 'timeout') { + setTimeout(() => {}, 500); + } + return; +} + +process.on('beforeExit', mustCall()); + +createWorker('spin', () => { + createWorker('timeout'); +}); + +function createWorker(arg, cb) { + const worker = new Worker(__filename, { argv: [process.pid, arg] }); + worker.once('message', mustCall((msg) => { + assert.strictEqual(msg, 'hi'); + binding.startTrackingHeapObjects(worker.threadId, false); + binding.stopTrackingHeapObjects(worker.threadId, false); + })); + worker.on('exit', mustCall(() => { + if (cb) + setImmediate(cb); + })); +}