Skip to content

Commit 9fa16a3

Browse files
committed
Allow use of CPED to store sampling context
1 parent f972f10 commit 9fa16a3

File tree

3 files changed

+185
-41
lines changed

3 files changed

+185
-41
lines changed

bindings/profilers/wall.cc

Lines changed: 133 additions & 28 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 {
@@ -509,8 +510,10 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,
509510
bool workaroundV8Bug,
510511
bool collectCpuTime,
511512
bool collectAsyncId,
512-
bool isMainThread)
513+
bool isMainThread,
514+
bool useCPED)
513515
: samplingPeriod_(samplingPeriod),
516+
useCPED_(useCPED),
514517
includeLines_(includeLines),
515518
withContexts_(withContexts),
516519
isMainThread_(isMainThread) {
@@ -526,7 +529,6 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,
526529
contexts_.reserve(duration * 2 / samplingPeriod);
527530
}
528531

529-
curContext_.store(&context1_, std::memory_order_relaxed);
530532
collectionMode_.store(CollectionMode::kNoCollect, std::memory_order_relaxed);
531533
gcCount.store(0, std::memory_order_relaxed);
532534

@@ -541,10 +543,18 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,
541543
jsArray_ = v8::Global<v8::Uint32Array>(isolate, jsArray);
542544
std::fill(fields_, fields_ + kFieldCount, 0);
543545

544-
if (collectAsyncId_) {
546+
if (collectAsyncId_ || useCPED_) {
545547
isolate->AddGCPrologueCallback(&GCPrologueCallback, this);
546548
isolate->AddGCEpilogueCallback(&GCEpilogueCallback, this);
547549
}
550+
551+
if (useCPED_) {
552+
cpedSymbol_.Reset(
553+
isolate,
554+
v8::Symbol::ForApi(isolate,
555+
v8::String::NewFromUtf8Literal(
556+
isolate, "dd::WallProfiler::cpedSymbol_")));
557+
}
548558
}
549559

550560
WallProfiler::~WallProfiler() {
@@ -617,6 +627,7 @@ NAN_METHOD(WallProfiler::New) {
617627
DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(collectCpuTime);
618628
DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(collectAsyncId);
619629
DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(isMainThread);
630+
DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(useCPED);
620631

621632
if (withContexts && !DD_WALL_USE_SIGPROF) {
622633
return Nan::ThrowTypeError("Contexts are not supported.");
@@ -656,7 +667,8 @@ NAN_METHOD(WallProfiler::New) {
656667
workaroundV8Bug,
657668
collectCpuTime,
658669
collectAsyncId,
659-
isMainThread);
670+
isMainThread,
671+
useCPED);
660672
obj->Wrap(info.This());
661673
info.GetReturnValue().Set(info.This());
662674
} else {
@@ -971,28 +983,111 @@ v8::CpuProfiler* WallProfiler::CreateV8CpuProfiler() {
971983
}
972984

973985
v8::Local<v8::Value> WallProfiler::GetContext(Isolate* isolate) {
974-
auto context = *curContext_.load(std::memory_order_relaxed);
986+
auto context = GetContextPtr(isolate);
975987
if (!context) return v8::Undefined(isolate);
976988
return context->Get(isolate);
977989
}
978990

991+
class PersistentContextPtr : AtomicContextPtr {
992+
Persistent<Object> per;
993+
994+
void BindLifecycleTo(Isolate* isolate, Local<Object>& obj) {
995+
// Register a callback to delete this object when the object is GCed
996+
per.Reset(isolate, obj);
997+
per.SetWeak(
998+
this,
999+
[](const WeakCallbackInfo<PersistentContextPtr>& data) {
1000+
auto& per = data.GetParameter()->per;
1001+
if (!per.IsEmpty()) {
1002+
per.ClearWeak();
1003+
per.Reset();
1004+
}
1005+
// Using SetSecondPassCallback as shared_ptr can trigger ~Global and
1006+
// any V8 API use needs to be in the second pass
1007+
data.SetSecondPassCallback(
1008+
[](const WeakCallbackInfo<PersistentContextPtr>& data) {
1009+
delete data.GetParameter();
1010+
});
1011+
},
1012+
WeakCallbackType::kParameter);
1013+
}
1014+
1015+
friend class WallProfiler;
1016+
};
1017+
9791018
void WallProfiler::SetContext(Isolate* isolate, Local<Value> value) {
980-
// Need to be careful here, because we might be interrupted by a
981-
// signal handler that will make use of curContext_.
982-
// Update of shared_ptr is not atomic, so instead we use a pointer
983-
// (curContext_) that points on two shared_ptr (context1_ and context2_),
984-
// update the shared_ptr that is not currently in use and then atomically
985-
// update curContext_.
986-
auto newCurContext = curContext_.load(std::memory_order_relaxed) == &context1_
987-
? &context2_
988-
: &context1_;
989-
if (!value->IsNullOrUndefined()) {
990-
*newCurContext = std::make_shared<Global<Value>>(isolate, value);
1019+
if (!useCPED_) {
1020+
curContext_.Set(isolate, value);
1021+
return;
1022+
}
1023+
1024+
auto cped = isolate->GetContinuationPreservedEmbedderData();
1025+
// No Node AsyncContextFrame in this continuation yet
1026+
if (!cped->IsObject()) return;
1027+
1028+
auto cpedObj = cped.As<Object>();
1029+
auto localSymbol = cpedSymbol_.Get(isolate);
1030+
auto v8Ctx = isolate->GetCurrentContext();
1031+
auto maybeProfData = cpedObj->Get(v8Ctx, localSymbol);
1032+
if (maybeProfData.IsEmpty()) return;
1033+
auto profData = maybeProfData.ToLocalChecked();
1034+
1035+
PersistentContextPtr* contextPtr = nullptr;
1036+
if (profData->IsUndefined()) {
1037+
contextPtr = new PersistentContextPtr();
1038+
1039+
auto maybeSetResult =
1040+
cpedObj->Set(v8Ctx, localSymbol, External::New(isolate, contextPtr));
1041+
if (maybeSetResult.IsNothing()) {
1042+
delete contextPtr;
1043+
return;
1044+
}
1045+
contextPtr->BindLifecycleTo(isolate, cpedObj);
9911046
} else {
992-
newCurContext->reset();
1047+
contextPtr =
1048+
static_cast<PersistentContextPtr*>(profData.As<External>()->Value());
9931049
}
994-
std::atomic_signal_fence(std::memory_order_release);
995-
curContext_.store(newCurContext, std::memory_order_relaxed);
1050+
1051+
contextPtr->Set(isolate, value);
1052+
}
1053+
1054+
ContextPtr WallProfiler::GetContextPtrSignalSafe(Isolate* isolate) {
1055+
if (!useCPED_) {
1056+
// Not strictly necessary but we can avoid HandleScope creation for this
1057+
// case.
1058+
return curContext_.Get();
1059+
}
1060+
1061+
auto curGcCount = gcCount.load(std::memory_order_relaxed);
1062+
std::atomic_signal_fence(std::memory_order_acquire);
1063+
if (curGcCount > 0) {
1064+
return gcContext;
1065+
} else if (isolate->InContext()) {
1066+
auto handleScope = HandleScope(isolate);
1067+
return GetContextPtr(isolate);
1068+
}
1069+
// not in a V8 Context
1070+
return std::shared_ptr<Global<Value>>();
1071+
}
1072+
1073+
ContextPtr WallProfiler::GetContextPtr(Isolate* isolate) {
1074+
if (!useCPED_) {
1075+
return curContext_.Get();
1076+
}
1077+
1078+
auto cped = isolate->GetContinuationPreservedEmbedderData();
1079+
if (!cped->IsObject()) return std::shared_ptr<Global<Value>>();
1080+
1081+
auto cpedObj = cped.As<Object>();
1082+
auto localSymbol = cpedSymbol_.Get(isolate);
1083+
auto maybeProfData = cpedObj->Get(isolate->GetCurrentContext(), localSymbol);
1084+
if (maybeProfData.IsEmpty()) return std::shared_ptr<Global<Value>>();
1085+
auto profData = maybeProfData.ToLocalChecked();
1086+
1087+
if (profData->IsUndefined()) return std::shared_ptr<Global<Value>>();
1088+
1089+
return static_cast<PersistentContextPtr*>(profData.As<External>()->Value())
1090+
->Get();
9961091
}
9971092

9981093
NAN_GETTER(WallProfiler::GetContext) {
@@ -1041,8 +1136,13 @@ void WallProfiler::OnGCStart(v8::Isolate* isolate) {
10411136
auto curCount = gcCount.load(std::memory_order_relaxed);
10421137
std::atomic_signal_fence(std::memory_order_acquire);
10431138
if (curCount == 0) {
1044-
gcAsyncId = GetAsyncIdNoGC(isolate);
1045-
}
1139+
if (collectAsyncId_) {
1140+
gcAsyncId = GetAsyncIdNoGC(isolate);
1141+
}
1142+
if (useCPED_) {
1143+
gcContext = GetContextPtrSignalSafe(isolate);
1144+
}
1145+
}
10461146
gcCount.store(curCount + 1, std::memory_order_relaxed);
10471147
std::atomic_signal_fence(std::memory_order_release);
10481148
}
@@ -1051,23 +1151,28 @@ void WallProfiler::OnGCEnd() {
10511151
auto newCount = gcCount.load(std::memory_order_relaxed) - 1;
10521152
std::atomic_signal_fence(std::memory_order_acquire);
10531153
gcCount.store(newCount, std::memory_order_relaxed);
1054-
std::atomic_signal_fence(std::memory_order_release);
10551154
if (newCount == 0) {
10561155
gcAsyncId = -1;
1156+
if (useCPED_) {
1157+
gcContext.reset();
1158+
}
10571159
}
1160+
std::atomic_signal_fence(std::memory_order_release);
10581161
}
10591162

10601163
void WallProfiler::PushContext(int64_t time_from,
10611164
int64_t time_to,
10621165
int64_t cpu_time,
1063-
double async_id) {
1166+
Isolate* isolate) {
10641167
// Be careful this is called in a signal handler context therefore all
10651168
// operations must be async signal safe (in particular no allocations).
10661169
// Our ring buffer avoids allocations.
1067-
auto context = curContext_.load(std::memory_order_relaxed);
1068-
std::atomic_signal_fence(std::memory_order_acquire);
10691170
if (contexts_.size() < contexts_.capacity()) {
1070-
contexts_.push_back({*context, time_from, time_to, cpu_time, async_id});
1171+
contexts_.push_back({GetContextPtrSignalSafe(isolate),
1172+
time_from,
1173+
time_to,
1174+
cpu_time,
1175+
GetAsyncId(isolate)});
10711176
std::atomic_fetch_add_explicit(
10721177
reinterpret_cast<std::atomic<uint32_t>*>(&fields_[kSampleCount]),
10731178
1U,

bindings/profilers/wall.hh

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,61 @@ struct Result {
3636
std::string msg;
3737
};
3838

39+
using ContextPtr = std::shared_ptr<v8::Global<v8::Value>>;
40+
41+
/**
42+
* Class that allows atomic updates to a ContextPtr. Since update of shared_ptr
43+
* is not atomic, we use a pointer that alternates between pointing to one of
44+
* two shared_ptrs instead, and we use atomic operations to update the pointer.
45+
*/
46+
class AtomicContextPtr {
47+
ContextPtr ptr1;
48+
ContextPtr ptr2;
49+
std::atomic<ContextPtr*> currentPtr = &ptr1;
50+
51+
void Set(v8::Isolate* isolate, v8::Local<v8::Value> value) {
52+
auto oldPtr = currentPtr.load(std::memory_order_relaxed);
53+
std::atomic_signal_fence(std::memory_order_acquire);
54+
auto newPtr = oldPtr == &ptr1 ? &ptr2 : &ptr1;
55+
if (!value->IsNullOrUndefined()) {
56+
*newPtr = std::make_shared<v8::Global<v8::Value>>(isolate, value);
57+
} else {
58+
newPtr->reset();
59+
}
60+
std::atomic_signal_fence(std::memory_order_release);
61+
currentPtr.store(newPtr, std::memory_order_relaxed);
62+
std::atomic_signal_fence(std::memory_order_release);
63+
oldPtr->reset();
64+
}
65+
66+
ContextPtr Get() {
67+
auto ptr = currentPtr.load(std::memory_order_relaxed);
68+
std::atomic_signal_fence(std::memory_order_acquire);
69+
return ptr ? *ptr : std::shared_ptr<v8::Global<v8::Value>>();
70+
}
71+
72+
friend class WallProfiler;
73+
};
74+
3975
class WallProfiler : public Nan::ObjectWrap {
4076
public:
4177
enum class CollectionMode { kNoCollect, kPassThrough, kCollectContexts };
4278

4379
private:
4480
enum Fields { kSampleCount, kFieldCount };
4581

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

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

6091
std::atomic<int> gcCount = 0;
6192
double gcAsyncId;
93+
ContextPtr gcContext;
6294

6395
std::atomic<CollectionMode> collectionMode_;
6496
std::atomic<uint64_t> noCollectCallCount_;
@@ -104,6 +136,8 @@ class WallProfiler : public Nan::ObjectWrap {
104136
int64_t startCpuTime);
105137

106138
bool waitForSignal(uint64_t targetCallCount = 0);
139+
ContextPtr GetContextPtr(v8::Isolate* isolate);
140+
ContextPtr GetContextPtrSignalSafe(v8::Isolate* isolate);
107141

108142
public:
109143
/**
@@ -112,6 +146,10 @@ class WallProfiler : public Nan::ObjectWrap {
112146
* parameter is informative; it is up to the caller to call the Stop method
113147
* every period. The parameter is used to preallocate data structures that
114148
* should not be reallocated in async signal safe code.
149+
* @param useCPED whether to use the V8 ContinuationPreservingEmbedderData
150+
* to store the current sampling context. It can be used if AsyncLocalStorage
151+
* uses the AsyncContextFrame implementation (experimental in Node 23, default
152+
* in Node 24.)
115153
*/
116154
explicit WallProfiler(std::chrono::microseconds samplingPeriod,
117155
std::chrono::microseconds duration,
@@ -120,14 +158,15 @@ class WallProfiler : public Nan::ObjectWrap {
120158
bool workaroundV8bug,
121159
bool collectCpuTime,
122160
bool collectAsyncId,
123-
bool isMainThread);
161+
bool isMainThread,
162+
bool useCPED);
124163

125164
v8::Local<v8::Value> GetContext(v8::Isolate*);
126165
void SetContext(v8::Isolate*, v8::Local<v8::Value>);
127166
void PushContext(int64_t time_from,
128167
int64_t time_to,
129168
int64_t cpu_time,
130-
double async_id);
169+
v8::Isolate* isolate);
131170
Result StartImpl();
132171
std::string StartInternal();
133172
Result StopImpl(bool restart, v8::Local<v8::Value>& profile);

ts/src/time-profiler.ts

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

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

0 commit comments

Comments
 (0)