Skip to content

Commit

Permalink
agents: add support for start/stopTrackingHeapObjects
Browse files Browse the repository at this point in the history
This patch implements triggers for `startTrackingHeapObjects`
and `stopTrackingHeapObjects` in the heap profiler.

The implementation is based on what the inspector does:

The `startTrackingHeapObjects` will simply activate that in the
HeapProfiler and; the `stopTrackingHeapObjects` will create a heap
snapshot and deactivate the `TrackingHeapObjects` in the Heap
profiler.

This patch also includes the `trackAllocations` option supported.

Signed-off-by: Juan José Arboleda <soyjuanarbol@gmail.com>
  • Loading branch information
juanarbol committed Dec 14, 2023
1 parent 21459cb commit 6615347
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/nsolid.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
71 changes: 71 additions & 0 deletions src/nsolid.h
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,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.
Expand All @@ -961,7 +962,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 <typename... Data>
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 <typename Cb, typename... Data>
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,
Expand Down Expand Up @@ -1133,6 +1172,38 @@ void CpuProfiler::cpu_profiler_proxy_(int status,
(*static_cast<G*>(data))(status, json);
}

template <typename... Data>
int Snapshot::StartTrackingHeapObjects(SharedEnvInst envinst,
bool trackAllocations,
Data&&... data) {
// NOLINTNEXTLINE(build/namespaces)
using namespace std::placeholders;
using UserData = decltype(std::bind(std::forward<Data>(data)...));

UserData* user_data =
new (std::nothrow) UserData(std::bind(std::forward<Data>(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 <typename Cb, typename... Data>
int Snapshot::StopTrackingHeapObjects(SharedEnvInst envinst,
bool redacted,
Cb&& cb,
Data&&... data) {
int er = TakeSnapshot(
envinst, redacted, std::forward<Cb>(cb), std::forward<Data>(data)...);
return er;
}

template <typename Cb, typename... Data>
int Snapshot::TakeSnapshot(SharedEnvInst envinst,
Expand Down
46 changes: 46 additions & 0 deletions src/nsolid/nsolid_heap_snapshot.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions src/nsolid/nsolid_heap_snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint64_t, HeapSnapshotStor> threads_running_snapshots_;
nsuv::ns_mutex in_progress_heap_snapshots_;
};
Expand Down
60 changes: 60 additions & 0 deletions test/addons/nsolid-track-heap-objects/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <node.h>
#include <v8.h>
#include <uv.h>
#include <nsolid.h>

#include <assert.h>
#include <map>

using v8::FunctionCallbackInfo;
using v8::Uint32;
using v8::Value;

std::map<uint64_t, std::string> 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<Value>& args) {
assert(args[0]->IsUint32());
// Track allocations
assert(args[1]->IsBoolean());

uint64_t thread_id = args[0].As<Uint32>()->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<Value>& args) {
assert(args[0]->IsUint32());
// Redacted heap snapshot
assert(args[1]->IsBoolean());

uint64_t thread_id = args[0].As<Uint32>()->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);
}
}
16 changes: 16 additions & 0 deletions test/addons/nsolid-track-heap-objects/binding.gyp
Original file line number Diff line number Diff line change
@@ -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' ],
}
},
},
}],
}
43 changes: 43 additions & 0 deletions test/addons/nsolid-track-heap-objects/nsolid-track-heap-objects.js
Original file line number Diff line number Diff line change
@@ -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);
}));
}

0 comments on commit 6615347

Please sign in to comment.