Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions bindings/profilers/wall.cc
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,25 @@ void SetContextPtr(ContextPtr& contextPtr,

class PersistentContextPtr : public node::ObjectWrap {
ContextPtr context;
std::unordered_set<PersistentContextPtr*>* live;
// Back-pointer to the WallProfiler that created this PCP. Guaranteed to be
// a valid pointer whenever pprev_ != nullptr — ~WallProfiler nulls pprev_
// on every live PCP before any of them can outlive the profiler.
WallProfiler* const profiler_;
// Intrusive doubly-linked list, threaded through the WallProfiler's
// liveContextPtrHead_. pprev_ points at the slot that currently holds *this
// (either the head field or another PCP's next_), enabling O(1) unlink in
// ~PCP. pprev_ == nullptr is the "detached" sentinel — set by
// ~WallProfiler before deleting us, so our unlink becomes a no-op and we
// don't poke at the dying profiler's memory.
PersistentContextPtr** pprev_ = nullptr;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the claim that pprev (double pointer) enables O(1) unlink is dubious. Maybe better explain the benefits vs using a single pointer.

PersistentContextPtr* next_ = nullptr;

friend class WallProfiler;

public:
PersistentContextPtr(std::unordered_set<PersistentContextPtr*>* live,
Local<Object> wrap)
: live(live) {
Wrap(wrap);
}
PersistentContextPtr(WallProfiler* profiler, Local<Object> wrap);

void Detach() { live = nullptr; }

~PersistentContextPtr() {
if (live) {
live->erase(this);
}
}
~PersistentContextPtr();

void Set(Isolate* isolate, const Local<Value>& value) {
SetContextPtr(context, isolate, value);
Expand All @@ -136,6 +139,32 @@ class PersistentContextPtr : public node::ObjectWrap {
}
};

PersistentContextPtr::PersistentContextPtr(WallProfiler* profiler,
Local<Object> wrap)
: profiler_(profiler) {
Wrap(wrap);
// Splice ourselves at the head of profiler's live list.
auto** headSlot = profiler->liveContextPtrHeadSlot();
next_ = *headSlot;
pprev_ = headSlot;
if (next_ != nullptr) next_->pprev_ = &next_;
*headSlot = this;
profiler->recordContextCreate();
}

PersistentContextPtr::~PersistentContextPtr() {
// pprev_ != nullptr means we're still on profiler_'s live list, which means
// ~WallProfiler hasn't run yet and the back-pointer is still valid. Unlink
// and release the slot in one step. If pprev_ is null, ~WallProfiler is
// taking care of us so don't do anything (don't unlink us, and leave its
// counter alone as it's about to go away).
if (pprev_ != nullptr) {
*pprev_ = next_;
if (next_ != nullptr) next_->pprev_ = pprev_;
profiler_->recordContextRelease();
}
}

inline void* GetAlignedPointerFromInternalField(Object* object, int index) {
#if NODE_MAJOR_VERSION >= 26
return object->GetAlignedPointerFromInternalField(
Expand Down Expand Up @@ -617,12 +646,21 @@ WallProfiler::~WallProfiler() {
}
g_profilers.Remove(this);

// Delete all live contexts
for (auto ptr : liveContextPtrs_) {
ptr->Detach(); // so it doesn't invalidate our iterator
delete ptr;
}
liveContextPtrs_.clear();
// Delete every PCP still live in the CPED map. ~PCP would normally unlink
// itself via pprev_/next_, but we're tearing down the list we point into —
// so null pprev_ first to signal "already detached" and let ~PCP skip the
// unlink. (~ObjectWrap will still clear V8's weak callback during delete,
// so the dangling internal-field pointer in the wrap object stays inert
// even if V8 later GCs the wrap.)
auto* p = liveContextPtrHead_;
while (p != nullptr) {
auto* next = p->next_;
p->pprev_ = nullptr;
p->next_ = nullptr;
delete p;
p = next;
}
liveContextPtrHead_ = nullptr;
}

void WallProfiler::Dispose(Isolate* isolate) {
Expand Down Expand Up @@ -1207,8 +1245,7 @@ Local<Object> WallProfiler::CreateContextHolder(Isolate* isolate,
// for easy access from JS when cpedKey is an ALS, it can do
// als.getStore()?.[0];
wrap->Set(v8Ctx, 0, value).Check();
auto contextPtr = new PersistentContextPtr(&liveContextPtrs_, wrap);
liveContextPtrs_.insert(contextPtr);
auto contextPtr = new PersistentContextPtr(this, wrap);
contextPtr->Set(isolate, value);
return wrap;
}
Expand Down Expand Up @@ -1273,7 +1310,7 @@ ContextPtr WallProfiler::GetContextPtr(Isolate* isolate) {
}

Local<Object> WallProfiler::GetMetrics(Isolate* isolate) {
auto usedAsyncContextCount = liveContextPtrs_.size();
auto usedAsyncContextCount = liveContextPtrCount();
auto context = isolate->GetCurrentContext();
auto metrics = Object::New(isolate);
metrics
Expand Down
32 changes: 29 additions & 3 deletions bindings/profilers/wall.hh
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,19 @@ class WallProfiler : public Nan::ObjectWrap {
int cpedKeyHash_ = 0;
v8::Global<v8::ObjectTemplate> wrapObjectTemplate_;

// We track live context pointers in a set to avoid memory leaks. They will
// be deleted when the profiler is disposed.
std::unordered_set<PersistentContextPtr*> liveContextPtrs_;
// The list lets ~WallProfiler delete any PCPs V8 hasn't GC'd yet, so they
// don't show up as leaks under LSAN at exit.
// PCPs link themselves in at construction and unlink in their destructor.
// This used to be an unordered set, but storing two pointers in the PCP is
// much cheaper memorywise than the hash set's overhead of buckets and nodes.
PersistentContextPtr* liveContextPtrHead_ = nullptr;

// Number of PersistentContextPtr instances created by this profiler that
// are still alive. Incremented in PersistentContextPtr's constructor;
// decremented in its destructor when it unlinks from the list (i.e., while
// the profiler is still alive). PCPs cleaned up by ~WallProfiler's walk
// skip the decrement — the counter is going away with the profiler anyway.
std::atomic<size_t> liveContextPtrCount_{0};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that liveContextPtrCount_ is only accessed by isolate thread and not from a signal handler, thus it could be a plain size_t.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only worry is GC callbacks, but I'll try to confirm from source code that they always run on the isolate thread (I know that some V8 GC stages can be multithreaded.)


std::atomic<int> gcCount = 0;
std::atomic<bool> setInProgress_ = false;
Expand Down Expand Up @@ -175,6 +185,22 @@ class WallProfiler : public Nan::ObjectWrap {

bool interceptSignal() const { return withContexts_ || workaroundV8Bug_; }

// Returns the address of the live-PCP list head so a newly constructed PCP
// can splice itself in. Only used by PersistentContextPtr's constructor.
PersistentContextPtr** liveContextPtrHeadSlot() {
return &liveContextPtrHead_;
}

void recordContextCreate() {
liveContextPtrCount_.fetch_add(1, std::memory_order_relaxed);
}
void recordContextRelease() {
liveContextPtrCount_.fetch_sub(1, std::memory_order_relaxed);
}
size_t liveContextPtrCount() const {
return liveContextPtrCount_.load(std::memory_order_relaxed);
}

int v8ProfilerStuckEventLoopDetected() const {
return v8ProfilerStuckEventLoopDetected_;
}
Expand Down
Loading