diff --git a/src/nsolid.cc b/src/nsolid.cc index 2406a221d7..9c611e1bc0 100644 --- a/src/nsolid.cc +++ b/src/nsolid.cc @@ -508,6 +508,22 @@ int CpuProfiler::get_cpu_profile_(SharedEnvInst envinst, deleter); } +int Snapshot::start_tracking_heap_objects_(SharedEnvInst envinst, + bool redacted, + bool trackAllocations, + uint64_t duration, + internal::user_data data, + snapshot_proxy_sig proxy) { + return NSolidHeapSnapshot::Inst()->StartTrackingHeapObjects( + envinst, redacted, trackAllocations, duration, std::move(data), proxy); +} + +int Snapshot::StopTrackingHeapObjects(SharedEnvInst envinst) { + if (envinst == nullptr) + return UV_ESRCH; + + return NSolidHeapSnapshot::Inst()->StopTrackingHeapObjects(envinst); +} int Snapshot::get_snapshot_(SharedEnvInst envinst, bool redacted, diff --git a/src/nsolid.h b/src/nsolid.h index 7b42865fe4..ec7807c836 100644 --- a/src/nsolid.h +++ b/src/nsolid.h @@ -954,6 +954,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. @@ -967,7 +968,50 @@ 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 trackAllLocations record stack traces of allocations, set this as + * as true will add a significant overhead. + * @param duration duration in milliseconds of the heap profiler after which + * the heap snapshot will be returned in the callback. + * @param data variable number of arguments to be propagated to the callback. + * @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 StartTrackingHeapObjects(SharedEnvInst envinst, + bool redacted, + bool trackAllocations, + uint64_t duration, + Cb&& cb, + 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. + * @return NSOLID_E_SUCCESS in case of success or a different NSOLID_E_ + * error value otherwise. + */ + static int StopTrackingHeapObjects(SharedEnvInst envinst); + private: + static int start_tracking_heap_objects_(SharedEnvInst envinst, + bool redacted, + bool trackAllocations, + uint64_t duration, + internal::user_data data, + snapshot_proxy_sig proxy); + static int get_snapshot_(SharedEnvInst envinst, bool redacted, void* data, @@ -1133,6 +1177,36 @@ void CpuProfiler::cpu_profiler_proxy_(int status, (*static_cast(data))(status, json); } +template +int Snapshot::StartTrackingHeapObjects(SharedEnvInst envinst, + bool redacted, + bool trackAllocations, + uint64_t duration, + Cb&& cb, + Data&&... data) { + if (envinst == nullptr) { + return UV_ESRCH; + } + + // NOLINTNEXTLINE(build/namespaces) + using namespace std::placeholders; + using UserData = decltype(std::bind( + std::forward(cb), _1, _2, std::forward(data)...)); + + auto user_data = internal::user_data(new (std::nothrow) UserData( + std::bind(std::forward(cb), _1, _2, std::forward(data)...)), + internal::delete_proxy_); + if (user_data == nullptr) { + return UV_ENOMEM; + } + + return start_tracking_heap_objects_(envinst, + redacted, + trackAllocations, + duration, + std::move(user_data), + snapshot_proxy_); +} template int Snapshot::TakeSnapshot(SharedEnvInst envinst, diff --git a/src/nsolid/nsolid_api.cc b/src/nsolid/nsolid_api.cc index 39b6ba1757..756b9c6128 100644 --- a/src/nsolid/nsolid_api.cc +++ b/src/nsolid/nsolid_api.cc @@ -1,4 +1,5 @@ #include "nsolid_api.h" +#include "nsolid/nsolid_heap_snapshot.h" #include "nsolid_bindings.h" #include "node_buffer.h" #include "nsolid_cpu_profiler.h" @@ -966,6 +967,7 @@ void EnvList::RemoveEnv(Environment* env) { // End any pending CPU profiles. This has to be done before removing the // EnvList from env_map_ so the checks in StopProfilingSync() pass. NSolidCpuProfiler::Inst()->StopProfilingSync(envinst_sp); + NSolidHeapSnapshot::Inst()->StopTrackingHeapObjects(envinst_sp); // Remove the GC prologue and epilogue callbacks just to be safe. envinst_sp->env()->isolate()->RemoveGCPrologueCallback( diff --git a/src/nsolid/nsolid_heap_snapshot.cc b/src/nsolid/nsolid_heap_snapshot.cc index f9672594e6..1e222862b4 100644 --- a/src/nsolid/nsolid_heap_snapshot.cc +++ b/src/nsolid/nsolid_heap_snapshot.cc @@ -12,6 +12,35 @@ NSolidHeapSnapshot::NSolidHeapSnapshot() { ASSERT_EQ(0, in_progress_heap_snapshots_.init(true)); } +int NSolidHeapSnapshot::StartTrackingHeapObjects( + SharedEnvInst envinst, + bool redacted, + bool trackAllocations, + uint64_t duration, + internal::user_data data, + Snapshot::snapshot_proxy_sig proxy) { + 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_.emplace( + thread_id, + HeapSnapshotStor{ redacted, true, proxy, std::move(data) }); + if (it.second == false) { + return UV_EEXIST; + } + int status = RunCommand(envinst, + CommandType::Interrupt, + start_tracking_heapobjects, + trackAllocations, + duration, + this); + if (status != 0) { + // Now we are tracking heap objects in this thread + threads_running_snapshots_.erase(it.first); + } + return status; +} + int NSolidHeapSnapshot::GetHeapSnapshot(SharedEnvInst envinst, bool redacted, void* data, @@ -31,13 +60,147 @@ int NSolidHeapSnapshot::GetHeapSnapshot(SharedEnvInst envinst, if (status == 0) { threads_running_snapshots_.emplace( - thread_id, - HeapSnapshotStor{ redacted, proxy, internal::user_data(data, deleter) }); + thread_id, + HeapSnapshotStor{ + redacted, false, proxy, internal::user_data(data, deleter)}); } return status; } +int NSolidHeapSnapshot::StopTrackingHeapObjects(SharedEnvInst envinst) { + uint64_t thread_id = envinst->thread_id(); + nsuv::ns_mutex::scoped_lock lock(&in_progress_heap_snapshots_); + auto it = threads_running_snapshots_.find(thread_id); + // Make sure there is a snapshot running + if (it == threads_running_snapshots_.end()) + return UV_ENOENT; + + int er = RunCommand(envinst, + CommandType::Interrupt, + stop_tracking_heap_objects, + this); + return er; +} + +void NSolidHeapSnapshot::stop_tracking_heap_objects( + SharedEnvInst envinst, + NSolidHeapSnapshot* snapshotter) { + uint64_t thread_id = envinst->thread_id(); + nsuv::ns_mutex::scoped_lock lock(&snapshotter->in_progress_heap_snapshots_); + auto it = snapshotter->threads_running_snapshots_.find(thread_id); + if (it == snapshotter->threads_running_snapshots_.end()) { + // This might happen if snapshot_cb is called before RemoveEnv. Just return; + return; + } + + HeapSnapshotStor& stor = it->second; + // If this condition is reached. This was called by EnvList::RemoveEnv. + // It wants to stop any pending snapshot w/ tracking heap object. + if (!stor.is_tracking_heapobjects_) { + // If no pending trackers, just do nothing + // There are not peding snapshots with trackers + return; + } + + v8::Isolate* isolate = envinst->isolate(); + + v8::HeapProfiler* profiler = isolate->GetHeapProfiler(); + ASSERT_NOT_NULL(profiler); + + v8::HandleScope scope(isolate); + + const v8::HeapSnapshot* snapshot = profiler->TakeHeapSnapshot(); + if (snapshot == nullptr) { + stor.cb(heap_profiler::HEAP_SNAPSHOT_FAILURE, + std::string(), + stor.data.get()); + } else { + std::string snapshot_str; + DataOutputStream stream(&snapshot_str, + snapshot, + &thread_id); + + if (stor.redacted) { + const v8::RedactedHeapSnapshot snapshot_redact(snapshot); + snapshot_redact.Serialize(&stream); + } else { + snapshot->Serialize(&stream); + } + + ASSERT_EQ(stor.is_tracking_heapobjects_, true); + profiler->StopTrackingHeapObjects(); + stor.is_tracking_heapobjects_ = false; + + + // At this point, the snapshot is fully serialized + stor.cb(0, snapshot_str.c_str(), stor.data.get()); + // Tell ZMQ that the snapshot is done + stor.cb(0, std::string(), stor.data.get()); + + // Don't leak the snapshot string + snapshot_str.clear(); + + // 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. + const_cast(snapshot)->Delete(); + } + // Delete the snapshot from the map + snapshotter->threads_running_snapshots_.erase(it); +} + +void NSolidHeapSnapshot::start_tracking_heapobjects( + SharedEnvInst envinst, + bool trackAllocations, + uint64_t duration, + NSolidHeapSnapshot* snapshotter) { + uint64_t thread_id = envinst->thread_id(); + + nsuv::ns_mutex::scoped_lock lock(&snapshotter->in_progress_heap_snapshots_); + auto it = snapshotter->threads_running_snapshots_.find(thread_id); + ASSERT(it != snapshotter->threads_running_snapshots_.end()); + + HeapSnapshotStor& stor = it->second; + ASSERT_EQ(stor.is_tracking_heapobjects_, true); + + envinst->isolate()->GetHeapProfiler()->StartTrackingHeapObjects( + trackAllocations); + if (duration > 0) { + // Schedule a timer to take the snapshot + int er = QueueCallback(duration, take_snapshot_timer, envinst, snapshotter); + + if (er) { + // In case the the thread is already gone, the cpu profile will be stopped + // on RemoveEnv, so do nothing here. + } + } +} + +void NSolidHeapSnapshot::take_snapshot_timer(SharedEnvInst envinst, + NSolidHeapSnapshot* snapshotter) { + uint64_t thread_id = envinst->thread_id(); + nsuv::ns_mutex::scoped_lock lock(&snapshotter->in_progress_heap_snapshots_); + auto it = snapshotter->threads_running_snapshots_.find(thread_id); + // The snapshot was stopped before the timer was triggered + if (it == snapshotter->threads_running_snapshots_.end()) + return; + + HeapSnapshotStor& stor = it->second; + ASSERT_EQ(stor.is_tracking_heapobjects_, true); + + // Give control back to the V8 thread + int er = RunCommand(envinst, + CommandType::Interrupt, + take_snapshot, + snapshotter); + // NO BUENO??? + if (er) { + // In case the the thread is already gone, the cpu profile will be stopped + // on RemoveEnv, so do nothing here. + } +} + void NSolidHeapSnapshot::take_snapshot(SharedEnvInst envinst, NSolidHeapSnapshot* snapshotter) { v8::Isolate* isolate = envinst->isolate(); @@ -75,6 +238,12 @@ void NSolidHeapSnapshot::take_snapshot(SharedEnvInst envinst, snapshot->Serialize(&stream); } + // A snapshot requested via `StopTrackingHeapObjects` or timer + if (stor.is_tracking_heapobjects_) { + profiler->StopTrackingHeapObjects(); + stor.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 c5c91e9a12..a66374ca4f 100644 --- a/src/nsolid/nsolid_heap_snapshot.h +++ b/src/nsolid/nsolid_heap_snapshot.h @@ -21,6 +21,7 @@ class NSolidHeapSnapshot { public: struct HeapSnapshotStor { bool redacted; + bool is_tracking_heapobjects_; Snapshot::snapshot_proxy_sig cb; internal::user_data data; }; @@ -36,11 +37,31 @@ class NSolidHeapSnapshot { Snapshot::snapshot_proxy_sig proxy, internal::deleter_sig deleter); + int StartTrackingHeapObjects(SharedEnvInst envinst, + bool redacted, + bool trackAllocations, + uint64_t duration, + internal::user_data data, + Snapshot::snapshot_proxy_sig proxy); + + int StopTrackingHeapObjects(SharedEnvInst envinst); + private: NSolidHeapSnapshot(); + static void start_tracking_heapobjects(SharedEnvInst envinst, + bool trackAllocations, + uint64_t duration, + NSolidHeapSnapshot*); + + static void stop_tracking_heap_objects(SharedEnvInst envinst_sp, + NSolidHeapSnapshot*); + static void take_snapshot(SharedEnvInst envinst_sp, NSolidHeapSnapshot*); + static void take_snapshot_timer(SharedEnvInst envinst_sp, + NSolidHeapSnapshot*); + static void snapshot_cb(uint64_t thread_id, int status, const std::string& snapshot); 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..5d8ba5f883 --- /dev/null +++ b/test/addons/nsolid-track-heap-objects/binding.cc @@ -0,0 +1,81 @@ +#include +#include +#include +#include + +#include +#include + +using v8::FunctionCallbackInfo; +using v8::Integer; +using v8::Number; +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 started_profiler(int status, std::string json, uint64_t thread_id) { + assert(status == 0); +} + +static void StartTrackingHeapObjectsBinding( + const FunctionCallbackInfo& args) { + v8::HandleScope handle_scope(args.GetIsolate()); + // thread_id + assert(args[0]->IsUint32()); + // Redacted heap snapshot + assert(args[1]->IsBoolean()); + // Track allocations + assert(args[2]->IsBoolean()); + // Stop after this many milliseconds + assert(args[3]->IsNumber()); + + uint64_t thread_id = args[0].As()->Value(); + bool redacted = args[1]->BooleanValue(args.GetIsolate()); + bool track_allocations = args[2]->BooleanValue(args.GetIsolate()); + uint64_t duration = static_cast( + args[3]->NumberValue(args.GetIsolate()->GetCurrentContext()).FromJust()); + + int ret = node::nsolid::Snapshot::StartTrackingHeapObjects( + node::nsolid::GetEnvInst(thread_id), + redacted, + track_allocations, + duration, + started_profiler, + thread_id); + args.GetReturnValue().Set(Integer::New(args.GetIsolate(), ret)); +} + +static void StopTrackingHeapObjects(const FunctionCallbackInfo& args) { + v8::HandleScope handle_scope(args.GetIsolate()); + // thread_id + assert(args[0]->IsUint32()); + + uint64_t thread_id = args[0].As()->Value(); + + int ret = node::nsolid::Snapshot::StopTrackingHeapObjects( + node::nsolid::GetEnvInst(thread_id)); + args.GetReturnValue().Set(ret); +} + +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", StartTrackingHeapObjectsBinding); + 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..81dc7e199f --- /dev/null +++ b/test/addons/nsolid-track-heap-objects/nsolid-track-heap-objects.js @@ -0,0 +1,83 @@ +'use strict'; +// Flags: --expose-internals + +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, threadId } = require('worker_threads'); +const { internalBinding } = require('internal/test/binding'); +const { UV_EEXIST, UV_ENOENT } = internalBinding('uv'); + +let er; + +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) { + // Starting the profile from this thread. + er = binding.startTrackingHeapObjects(threadId, false, false, 5000); + assert.strictEqual(er, 0); + parentPort.postMessage('hi'); + setTimeout(() => {}, 2000); + return; +} + +process.on('beforeExit', mustCall(() => { + er = binding.stopTrackingHeapObjects(0); + // It could be peding or not. + assert.ok(er === 0 || er === UV_ENOENT); +})); + +// Normal usage check. +er = binding.startTrackingHeapObjects(threadId, false, false, 10000); +assert.strictEqual(er, 0); +er = binding.stopTrackingHeapObjects(threadId); +assert.strictEqual(er, 0); +setTimeout(() => { + // Check error codes for invalid calls. + er = binding.startTrackingHeapObjects(threadId, false, false, 10000); + assert.strictEqual(er, 0); + er = binding.startTrackingHeapObjects(threadId, false, false, 10); + assert.strictEqual(er, UV_EEXIST); + + er = binding.stopTrackingHeapObjects(threadId); + assert.strictEqual(er, 0); + setTimeout(() => { + er = binding.stopTrackingHeapObjects(threadId); + assert.strictEqual(er, UV_ENOENT); + // Test getting profile + er = binding.startTrackingHeapObjects(threadId, false, false, 10); + assert.strictEqual(er, 0); + + setTimeout(() => { + // The CPU profile should have ended by now. + er = binding.stopTrackingHeapObjects(threadId); + assert.strictEqual(er, UV_ENOENT); + testWorker(); + }, 500); + }, 500); +}, 500); + +function testWorker() { + const worker = new Worker(__filename, { argv: [process.pid] }); + worker.on('exit', mustCall((code) => { + assert.strictEqual(code, 0); + })); + worker.once('message', mustCall((msg) => { + assert.strictEqual(msg, 'hi'); + + er = binding.startTrackingHeapObjects(worker.threadId, false, false, 500); + assert.strictEqual(er, UV_EEXIST); + + er = binding.stopTrackingHeapObjects(worker.threadId); + assert.strictEqual(er, 0); + setTimeout(() => { + er = binding.startTrackingHeapObjects(threadId, false, false, 2000); + assert.strictEqual(er, 0); + }, 2000); + })); +}