Skip to content

Commit 0c1791e

Browse files
committed
Allow use of CPED to store sampling context
1 parent a2384e5 commit 0c1791e

File tree

3 files changed

+170
-37
lines changed

3 files changed

+170
-37
lines changed

bindings/profilers/wall.cc

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ using namespace v8;
5858

5959
namespace dd {
6060

61+
using ContextPtr = std::shared_ptr<Global<Value>>;
62+
6163
// Maximum number of rounds in the GetV8ToEpochOffset
6264
static constexpr int MAX_EPOCH_OFFSET_ATTEMPTS = 20;
6365

@@ -318,8 +320,7 @@ void SignalHandler::HandleProfilerSignal(int sig,
318320
auto time_from = Now();
319321
old_handler(sig, info, context);
320322
auto time_to = Now();
321-
auto async_id = prof->GetAsyncId(isolate);
322-
prof->PushContext(time_from, time_to, cpu_time, async_id);
323+
prof->PushContext(time_from, time_to, cpu_time, isolate);
323324
}
324325
#else
325326
class SignalHandler {
@@ -513,8 +514,10 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,
513514
bool withContexts,
514515
bool workaroundV8Bug,
515516
bool collectCpuTime,
516-
bool isMainThread)
517+
bool isMainThread,
518+
bool useCPED)
517519
: samplingPeriod_(samplingPeriod),
520+
useCPED_(useCPED),
518521
includeLines_(includeLines),
519522
withContexts_(withContexts),
520523
isMainThread_(isMainThread) {
@@ -529,7 +532,6 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,
529532
contexts_.reserve(duration * 2 / samplingPeriod);
530533
}
531534

532-
curContext_.store(&context1_, std::memory_order_relaxed);
533535
collectionMode_.store(CollectionMode::kNoCollect, std::memory_order_relaxed);
534536

535537
// TODO: bind to this isolate? Would fix the Dispose(nullptr) issue.
@@ -543,6 +545,9 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,
543545
jsArray_ = v8::Global<v8::Uint32Array>(isolate, jsArray);
544546
std::fill(fields_, fields_ + kFieldCount, 0);
545547

548+
if (useCPED_) {
549+
cpedSymbol_ = v8::Global<v8::Symbol>(isolate, v8::Symbol::New(isolate));
550+
}
546551
gcCount = 0;
547552
isolate->AddGCPrologueCallback(&GCPrologueCallback, this);
548553
isolate->AddGCEpilogueCallback(&GCEpilogueCallback, this);
@@ -676,13 +681,21 @@ NAN_METHOD(WallProfiler::New) {
676681
"Include line option is not compatible with contexts.");
677682
}
678683

684+
auto useCPEDValue =
685+
Nan::Get(arg, Nan::New<v8::String>("useCPED").ToLocalChecked());
686+
if (useCPEDValue.IsEmpty() || !useCPEDValue.ToLocalChecked()->IsBoolean()) {
687+
return Nan::ThrowTypeError("useCPED must be a boolean.");
688+
}
689+
bool useCPED = useCPEDValue.ToLocalChecked().As<v8::Boolean>()->Value();
690+
679691
WallProfiler* obj = new WallProfiler(interval,
680692
duration,
681693
lineNumbers,
682694
withContexts,
683695
workaroundV8Bug,
684696
collectCpuTime,
685-
isMainThread);
697+
isMainThread,
698+
useCPED);
686699
obj->Wrap(info.This());
687700
info.GetReturnValue().Set(info.This());
688701
} else {
@@ -995,28 +1008,109 @@ v8::CpuProfiler* WallProfiler::CreateV8CpuProfiler() {
9951008
}
9961009

9971010
v8::Local<v8::Value> WallProfiler::GetContext(Isolate* isolate) {
998-
auto context = *curContext_.load(std::memory_order_relaxed);
1011+
auto context = GetContextPtr(isolate);
9991012
if (!context) return v8::Undefined(isolate);
10001013
return context->Get(isolate);
10011014
}
10021015

1016+
class PersistentContextPtr : AtomicContextPtr {
1017+
Persistent<Object> per;
1018+
1019+
void BindLifecycleTo(Isolate* isolate, Local<Object>& obj) {
1020+
// Register a callback to delete this object when the object is GCed
1021+
per.Reset(isolate, obj);
1022+
per.SetWeak(
1023+
this,
1024+
[](const WeakCallbackInfo<PersistentContextPtr>& data) {
1025+
auto& per = data.GetParameter()->per;
1026+
if (!per.IsEmpty()) {
1027+
per.ClearWeak();
1028+
per.Reset();
1029+
}
1030+
// Using SetSecondPassCallback as shared_ptr can trigger ~Global and
1031+
// any V8 API use needs to be in the second pass
1032+
data.SetSecondPassCallback(
1033+
[](const WeakCallbackInfo<PersistentContextPtr>& data) {
1034+
delete data.GetParameter();
1035+
});
1036+
},
1037+
WeakCallbackType::kParameter);
1038+
}
1039+
1040+
friend class WallProfiler;
1041+
};
1042+
10031043
void WallProfiler::SetContext(Isolate* isolate, Local<Value> value) {
1004-
// Need to be careful here, because we might be interrupted by a
1005-
// signal handler that will make use of curContext_.
1006-
// Update of shared_ptr is not atomic, so instead we use a pointer
1007-
// (curContext_) that points on two shared_ptr (context1_ and context2_),
1008-
// update the shared_ptr that is not currently in use and then atomically
1009-
// update curContext_.
1010-
auto newCurContext = curContext_.load(std::memory_order_relaxed) == &context1_
1011-
? &context2_
1012-
: &context1_;
1013-
if (!value->IsNullOrUndefined()) {
1014-
*newCurContext = std::make_shared<Global<Value>>(isolate, value);
1044+
if (!useCPED_) {
1045+
curContext_.Set(isolate, value);
1046+
return;
1047+
}
1048+
1049+
auto cped = isolate->GetContinuationPreservedEmbedderData();
1050+
// No Node AsyncContextFrame in this continuation yet
1051+
if (!cped->IsObject()) return;
1052+
1053+
auto cpedObj = cped.As<Object>();
1054+
auto localSymbol = cpedSymbol_.Get(isolate);
1055+
auto v8Ctx = isolate->GetCurrentContext();
1056+
auto maybeProfData = cpedObj->Get(v8Ctx, localSymbol);
1057+
if (maybeProfData.IsEmpty()) return;
1058+
auto profData = maybeProfData.ToLocalChecked();
1059+
1060+
PersistentContextPtr* contextPtr = nullptr;
1061+
if (profData->IsUndefined()) {
1062+
contextPtr = new PersistentContextPtr();
1063+
1064+
auto maybeSetResult =
1065+
cpedObj->Set(v8Ctx, localSymbol, External::New(isolate, contextPtr));
1066+
if (maybeSetResult.IsNothing()) {
1067+
delete contextPtr;
1068+
return;
1069+
}
1070+
contextPtr->BindLifecycleTo(isolate, cpedObj);
10151071
} else {
1016-
newCurContext->reset();
1072+
contextPtr =
1073+
static_cast<PersistentContextPtr*>(profData.As<External>()->Value());
10171074
}
1018-
std::atomic_signal_fence(std::memory_order_release);
1019-
curContext_.store(newCurContext, std::memory_order_relaxed);
1075+
1076+
contextPtr->Set(isolate, value);
1077+
}
1078+
1079+
ContextPtr WallProfiler::GetContextPtrSignalSafe(Isolate* isolate) {
1080+
if (!useCPED_) {
1081+
// Not strictly necessary but we can avoid HandleScope creation for this
1082+
// case.
1083+
return curContext_.Get();
1084+
}
1085+
1086+
if (gcCount > 0) {
1087+
return gcContext;
1088+
} else if (isolate->InContext()) {
1089+
auto handleScope = HandleScope(isolate);
1090+
return GetContextPtr(isolate);
1091+
}
1092+
// not in a V8 Context
1093+
return std::shared_ptr<Global<Value>>();
1094+
}
1095+
1096+
ContextPtr WallProfiler::GetContextPtr(Isolate* isolate) {
1097+
if (!useCPED_) {
1098+
return curContext_.Get();
1099+
}
1100+
1101+
auto cped = isolate->GetContinuationPreservedEmbedderData();
1102+
if (!cped->IsObject()) return std::shared_ptr<Global<Value>>();
1103+
1104+
auto cpedObj = cped.As<Object>();
1105+
auto localSymbol = cpedSymbol_.Get(isolate);
1106+
auto maybeProfData = cpedObj->Get(isolate->GetCurrentContext(), localSymbol);
1107+
if (maybeProfData.IsEmpty()) return std::shared_ptr<Global<Value>>();
1108+
auto profData = maybeProfData.ToLocalChecked();
1109+
1110+
if (profData->IsUndefined()) return std::shared_ptr<Global<Value>>();
1111+
1112+
return static_cast<PersistentContextPtr*>(profData.As<External>()->Value())
1113+
->Get();
10201114
}
10211115

10221116
NAN_GETTER(WallProfiler::GetContext) {
@@ -1047,14 +1141,16 @@ NAN_METHOD(WallProfiler::Dispose) {
10471141
void WallProfiler::PushContext(int64_t time_from,
10481142
int64_t time_to,
10491143
int64_t cpu_time,
1050-
int64_t async_id) {
1144+
Isolate* isolate) {
10511145
// Be careful this is called in a signal handler context therefore all
10521146
// operations must be async signal safe (in particular no allocations).
10531147
// Our ring buffer avoids allocations.
1054-
auto context = curContext_.load(std::memory_order_relaxed);
1055-
std::atomic_signal_fence(std::memory_order_acquire);
10561148
if (contexts_.size() < contexts_.capacity()) {
1057-
contexts_.push_back({*context, time_from, time_to, cpu_time, async_id});
1149+
contexts_.push_back({GetContextPtrSignalSafe(isolate),
1150+
time_from,
1151+
time_to,
1152+
cpu_time,
1153+
GetAsyncId(isolate)});
10581154
std::atomic_fetch_add_explicit(
10591155
reinterpret_cast<std::atomic<uint32_t>*>(&fields_[kSampleCount]),
10601156
1U,

bindings/profilers/wall.hh

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,53 @@ struct Result {
3737
std::string msg;
3838
};
3939

40+
using ContextPtr = std::shared_ptr<v8::Global<v8::Value>>;
41+
42+
class AtomicContextPtr {
43+
ContextPtr ptr1;
44+
ContextPtr ptr2;
45+
std::atomic<ContextPtr*> currentPtr = &ptr1;
46+
47+
void Set(v8::Isolate* isolate, v8::Local<v8::Value> value) {
48+
auto newPtr =
49+
currentPtr.load(std::memory_order_relaxed) == &ptr1 ? &ptr2 : &ptr1;
50+
if (!value->IsNullOrUndefined()) {
51+
*newPtr = std::make_shared<v8::Global<v8::Value>>(isolate, value);
52+
} else {
53+
newPtr->reset();
54+
}
55+
std::atomic_signal_fence(std::memory_order_release);
56+
currentPtr.store(newPtr, std::memory_order_relaxed);
57+
}
58+
59+
ContextPtr Get() {
60+
auto ptr = currentPtr.load(std::memory_order_relaxed);
61+
std::atomic_signal_fence(std::memory_order_acquire);
62+
return ptr ? *ptr : std::shared_ptr<v8::Global<v8::Value>>();
63+
}
64+
65+
friend class WallProfiler;
66+
};
67+
4068
class WallProfiler : public Nan::ObjectWrap {
4169
public:
4270
enum class CollectionMode { kNoCollect, kPassThrough, kCollectContexts };
4371

4472
private:
4573
enum Fields { kSampleCount, kFieldCount };
4674

47-
using ContextPtr = std::shared_ptr<v8::Global<v8::Value>>;
48-
4975
std::chrono::microseconds samplingPeriod_{0};
5076
v8::CpuProfiler* cpuProfiler_ = nullptr;
51-
// TODO: Investigate use of v8::Persistent instead of shared_ptr<Global> to
52-
// avoid heap allocation. Need to figure out the right move/copy semantics in
53-
// and out of the ring buffer.
5477

55-
// We're using a pair of shared pointers and an atomic pointer-to-current as
56-
// a way to ensure signal safety on update.
57-
ContextPtr context1_;
58-
ContextPtr context2_;
59-
std::atomic<ContextPtr*> curContext_;
78+
bool useCPED_ = false;
79+
// If we aren't using the CPED, we use a single context ptr stored here.
80+
AtomicContextPtr curContext_;
81+
// Otherwise we'll use a private symbol to store the context in CPED objects.
82+
v8::Global<v8::Symbol> cpedSymbol_;
6083

6184
std::atomic<int> gcCount = 0;
6285
int64_t gcAsyncId;
86+
ContextPtr gcContext;
6387

6488
std::atomic<CollectionMode> collectionMode_;
6589
std::atomic<uint64_t> noCollectCallCount_;
@@ -105,6 +129,8 @@ class WallProfiler : public Nan::ObjectWrap {
105129
GENERAL_REGS_ONLY;
106130

107131
bool waitForSignal(uint64_t targetCallCount = 0);
132+
ContextPtr GetContextPtr(v8::Isolate* isolate);
133+
ContextPtr GetContextPtrSignalSafe(v8::Isolate* isolate);
108134

109135
public:
110136
/**
@@ -113,21 +139,26 @@ class WallProfiler : public Nan::ObjectWrap {
113139
* parameter is informative; it is up to the caller to call the Stop method
114140
* every period. The parameter is used to preallocate data structures that
115141
* should not be reallocated in async signal safe code.
142+
* @param useCPED whether to use the V8 ContinuationPreservingEmbedderData
143+
* to store the current sampling context. It can be used if AsyncLocalStorage
144+
* uses the AsyncContextFrame implementation (experimental in Node 23, default
145+
* in Node 24.)
116146
*/
117147
explicit WallProfiler(std::chrono::microseconds samplingPeriod,
118148
std::chrono::microseconds duration,
119149
bool includeLines,
120150
bool withContexts,
121151
bool workaroundV8bug,
122152
bool collectCpuTime,
123-
bool isMainThread);
153+
bool isMainThread,
154+
bool useCPED);
124155

125156
v8::Local<v8::Value> GetContext(v8::Isolate*);
126157
void SetContext(v8::Isolate*, v8::Local<v8::Value>);
127158
void PushContext(int64_t time_from,
128159
int64_t time_to,
129160
int64_t cpu_time,
130-
int64_t async_id);
161+
v8::Isolate* isolate);
131162
Result StartImpl();
132163
std::string StartInternal();
133164
Result StopImpl(bool restart,
@@ -164,6 +195,9 @@ class WallProfiler : public Nan::ObjectWrap {
164195
void OnGCStart(v8::Isolate* isolate) {
165196
if (gcCount == 0) {
166197
gcAsyncId = GetAsyncId(isolate);
198+
if (useCPED_) {
199+
gcContext = GetContextPtrSignalSafe(v8::Isolate::GetCurrent());
200+
}
167201
gcCount = 1;
168202
} else {
169203
++gcCount;
@@ -173,6 +207,9 @@ class WallProfiler : public Nan::ObjectWrap {
173207
void OnGCEnd() {
174208
if (--gcCount == 0) {
175209
gcAsyncId = -1;
210+
if (useCPED_) {
211+
gcContext.reset();
212+
}
176213
}
177214
}
178215

ts/src/time-profiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function start(options: TimeProfilerOptions = {}) {
9191
throw new Error('Wall profiler is already started');
9292
}
9393

94-
gProfiler = new TimeProfiler({...options, isMainThread});
94+
gProfiler = new TimeProfiler({...options, isMainThread, useCPED: false});
9595
gSourceMapper = options.sourceMapper;
9696
gIntervalMicros = options.intervalMicros!;
9797
gV8ProfilerStuckEventLoopDetected = 0;

0 commit comments

Comments
 (0)