-
Notifications
You must be signed in to change notification settings - Fork 7
Only collect async ID when it's safe to do so #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -291,6 +291,9 @@ void SignalHandler::HandleProfilerSignal(int sig, | |
| return; | ||
| } | ||
| auto isolate = Isolate::GetCurrent(); | ||
| if (!isolate || isolate->IsDead()) { | ||
| return; | ||
| } | ||
| WallProfiler* prof = g_profilers.GetProfiler(isolate); | ||
|
|
||
| if (!prof) { | ||
|
|
@@ -315,9 +318,7 @@ void SignalHandler::HandleProfilerSignal(int sig, | |
| auto time_from = Now(); | ||
| old_handler(sig, info, context); | ||
| auto time_to = Now(); | ||
| int64_t async_id = -1; | ||
| // don't capture for now until we work out the issues with GC and thread start | ||
| // static_cast<int64_t>(node::AsyncHooksGetExecutionAsyncId(isolate)); | ||
| auto async_id = prof->GetAsyncId(isolate); | ||
| prof->PushContext(time_from, time_to, cpu_time, async_id); | ||
| } | ||
| #else | ||
|
|
@@ -473,11 +474,13 @@ std::shared_ptr<ContextsByNode> WallProfiler::GetContextsByNode( | |
| sampleContext.context.get()->Get(isolate)) | ||
| .Check(); | ||
| } | ||
| timedContext | ||
| ->Set(v8Context, | ||
| asyncIdKey, | ||
| NewNumberFromInt64(isolate, sampleContext.async_id)) | ||
| .Check(); | ||
| if (collectAsyncId_) { | ||
| timedContext | ||
| ->Set(v8Context, | ||
| asyncIdKey, | ||
| NewNumberFromInt64(isolate, sampleContext.async_id)) | ||
| .Check(); | ||
| } | ||
| } | ||
| } | ||
| array->Set(v8Context, array->Length(), timedContext).Check(); | ||
|
|
@@ -492,12 +495,27 @@ std::shared_ptr<ContextsByNode> WallProfiler::GetContextsByNode( | |
| return contextsByNode; | ||
| } | ||
|
|
||
| void GCPrologueCallback(Isolate* isolate, | ||
| GCType type, | ||
| GCCallbackFlags flags, | ||
| void* data) { | ||
| static_cast<WallProfiler*>(data)->OnGCStart(isolate); | ||
| } | ||
|
|
||
| void GCEpilogueCallback(Isolate* isolate, | ||
| GCType type, | ||
| GCCallbackFlags flags, | ||
| void* data) { | ||
| static_cast<WallProfiler*>(data)->OnGCEnd(); | ||
| } | ||
|
|
||
| WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod, | ||
| std::chrono::microseconds duration, | ||
| bool includeLines, | ||
| bool withContexts, | ||
| bool workaroundV8Bug, | ||
| bool collectCpuTime, | ||
| bool collectAsyncId, | ||
| bool isMainThread) | ||
| : samplingPeriod_(samplingPeriod), | ||
| includeLines_(includeLines), | ||
|
|
@@ -509,14 +527,17 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod, | |
| // event just after triggers the issue. | ||
| workaroundV8Bug_ = workaroundV8Bug && DD_WALL_USE_SIGPROF && detectV8Bug_; | ||
| collectCpuTime_ = collectCpuTime && withContexts; | ||
| collectAsyncId_ = collectAsyncId && withContexts; | ||
|
|
||
| if (withContexts_) { | ||
| contexts_.reserve(duration * 2 / samplingPeriod); | ||
| } | ||
|
|
||
| curContext_.store(&context1_, std::memory_order_relaxed); | ||
| collectionMode_.store(CollectionMode::kNoCollect, std::memory_order_relaxed); | ||
| gcCount.store(0, std::memory_order_relaxed); | ||
|
|
||
| // TODO: bind to this isolate? Would fix the Dispose(nullptr) issue. | ||
| auto isolate = v8::Isolate::GetCurrent(); | ||
| v8::Local<v8::ArrayBuffer> buffer = | ||
| v8::ArrayBuffer::New(isolate, sizeof(uint32_t) * kFieldCount); | ||
|
|
@@ -526,6 +547,11 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod, | |
| fields_ = static_cast<uint32_t*>(buffer->GetBackingStore()->Data()); | ||
| jsArray_ = v8::Global<v8::Uint32Array>(isolate, jsArray); | ||
| std::fill(fields_, fields_ + kFieldCount, 0); | ||
|
|
||
| if (collectAsyncId_) { | ||
| isolate->AddGCPrologueCallback(&GCPrologueCallback, this); | ||
| isolate->AddGCEpilogueCallback(&GCEpilogueCallback, this); | ||
| } | ||
| } | ||
|
|
||
| WallProfiler::~WallProfiler() { | ||
|
|
@@ -538,9 +564,22 @@ void WallProfiler::Dispose(Isolate* isolate) { | |
| cpuProfiler_ = nullptr; | ||
|
|
||
| g_profilers.RemoveProfiler(isolate, this); | ||
|
|
||
| if (isolate != nullptr && collectAsyncId_) { | ||
| isolate->RemoveGCPrologueCallback(&GCPrologueCallback, this); | ||
| isolate->RemoveGCEpilogueCallback(&GCEpilogueCallback, this); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #define DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(name) \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No a big fan of macros, but here this nicely simplifies the code. |
||
| auto name##Value = \ | ||
| Nan::Get(arg, Nan::New<v8::String>(#name).ToLocalChecked()); \ | ||
| if (name##Value.IsEmpty() || !name##Value.ToLocalChecked()->IsBoolean()) { \ | ||
| return Nan::ThrowTypeError(#name " must be a boolean."); \ | ||
| } \ | ||
| bool name = name##Value.ToLocalChecked().As<v8::Boolean>()->Value(); | ||
|
|
||
| NAN_METHOD(WallProfiler::New) { | ||
| if (info.Length() != 1 || !info[0]->IsObject()) { | ||
| return Nan::ThrowTypeError("WallProfiler must have one object argument."); | ||
|
|
@@ -579,50 +618,12 @@ NAN_METHOD(WallProfiler::New) { | |
| return Nan::ThrowTypeError("Duration must not be less than sample rate."); | ||
| } | ||
|
|
||
| auto lineNumbersValue = | ||
| Nan::Get(arg, Nan::New<v8::String>("lineNumbers").ToLocalChecked()); | ||
| if (lineNumbersValue.IsEmpty() || | ||
| !lineNumbersValue.ToLocalChecked()->IsBoolean()) { | ||
| return Nan::ThrowTypeError("lineNumbers must be a boolean."); | ||
| } | ||
| bool lineNumbers = | ||
| lineNumbersValue.ToLocalChecked().As<v8::Boolean>()->Value(); | ||
|
|
||
| auto withContextsValue = | ||
| Nan::Get(arg, Nan::New<v8::String>("withContexts").ToLocalChecked()); | ||
| if (withContextsValue.IsEmpty() || | ||
| !withContextsValue.ToLocalChecked()->IsBoolean()) { | ||
| return Nan::ThrowTypeError("withContext must be a boolean."); | ||
| } | ||
| bool withContexts = | ||
| withContextsValue.ToLocalChecked().As<v8::Boolean>()->Value(); | ||
|
|
||
| auto workaroundV8BugValue = | ||
| Nan::Get(arg, Nan::New<v8::String>("workaroundV8Bug").ToLocalChecked()); | ||
| if (workaroundV8BugValue.IsEmpty() || | ||
| !workaroundV8BugValue.ToLocalChecked()->IsBoolean()) { | ||
| return Nan::ThrowTypeError("workaroundV8Bug must be a boolean."); | ||
| } | ||
| bool workaroundV8Bug = | ||
| workaroundV8BugValue.ToLocalChecked().As<v8::Boolean>()->Value(); | ||
|
|
||
| auto collectCpuTimeValue = | ||
| Nan::Get(arg, Nan::New<v8::String>("collectCpuTime").ToLocalChecked()); | ||
| if (collectCpuTimeValue.IsEmpty() || | ||
| !collectCpuTimeValue.ToLocalChecked()->IsBoolean()) { | ||
| return Nan::ThrowTypeError("collectCpuTime must be a boolean."); | ||
| } | ||
| bool collectCpuTime = | ||
| collectCpuTimeValue.ToLocalChecked().As<v8::Boolean>()->Value(); | ||
|
|
||
| auto isMainThreadValue = | ||
| Nan::Get(arg, Nan::New<v8::String>("isMainThread").ToLocalChecked()); | ||
| if (isMainThreadValue.IsEmpty() || | ||
| !isMainThreadValue.ToLocalChecked()->IsBoolean()) { | ||
| return Nan::ThrowTypeError("isMainThread must be a boolean."); | ||
| } | ||
| bool isMainThread = | ||
| isMainThreadValue.ToLocalChecked().As<v8::Boolean>()->Value(); | ||
| DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(lineNumbers); | ||
| DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(withContexts); | ||
| DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(workaroundV8Bug); | ||
| DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(collectCpuTime); | ||
| DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(collectAsyncId); | ||
| DD_WALL_PROFILER_GET_BOOLEAN_CONFIG(isMainThread); | ||
|
|
||
| if (withContexts && !DD_WALL_USE_SIGPROF) { | ||
| return Nan::ThrowTypeError("Contexts are not supported."); | ||
|
|
@@ -632,6 +633,10 @@ NAN_METHOD(WallProfiler::New) { | |
| return Nan::ThrowTypeError("Cpu time collection requires contexts."); | ||
| } | ||
|
|
||
| if (collectAsyncId && !withContexts) { | ||
| return Nan::ThrowTypeError("Async ID collection requires contexts."); | ||
| } | ||
|
|
||
| if (lineNumbers && withContexts) { | ||
| // Currently custom contexts are not compatible with caller line | ||
| // information, because it's not possible to associate context with line | ||
|
|
@@ -657,6 +662,7 @@ NAN_METHOD(WallProfiler::New) { | |
| withContexts, | ||
| workaroundV8Bug, | ||
| collectCpuTime, | ||
| collectAsyncId, | ||
| isMainThread); | ||
| obj->Wrap(info.This()); | ||
| info.GetReturnValue().Set(info.This()); | ||
|
|
@@ -668,6 +674,8 @@ NAN_METHOD(WallProfiler::New) { | |
| } | ||
| } | ||
|
|
||
| #undef DD_WALL_PROFILER_GET_BOOLEAN_CONFIG | ||
|
|
||
| NAN_METHOD(WallProfiler::Start) { | ||
| WallProfiler* wallProfiler = | ||
| Nan::ObjectWrap::Unwrap<WallProfiler>(info.Holder()); | ||
|
|
@@ -1019,6 +1027,45 @@ NAN_METHOD(WallProfiler::Dispose) { | |
| delete profiler; | ||
| } | ||
|
|
||
| int64_t GetAsyncIdNoGC(v8::Isolate* isolate) { | ||
| return isolate->InContext() | ||
| ? static_cast<int64_t>( | ||
| node::AsyncHooksGetExecutionAsyncId(isolate)) | ||
| : -1; | ||
| } | ||
|
|
||
| int64_t WallProfiler::GetAsyncId(v8::Isolate* isolate) { | ||
| if (!collectAsyncId_) { | ||
| return -1; | ||
| } | ||
| auto curGcCount = gcCount.load(std::memory_order_relaxed); | ||
| std::atomic_signal_fence(std::memory_order_acquire); | ||
| if (curGcCount > 0) { | ||
| return gcAsyncId; | ||
| } | ||
| return GetAsyncIdNoGC(isolate); | ||
| } | ||
|
|
||
| void WallProfiler::OnGCStart(v8::Isolate* isolate) { | ||
| auto curCount = gcCount.load(std::memory_order_relaxed); | ||
| std::atomic_signal_fence(std::memory_order_acquire); | ||
| if (curCount == 0) { | ||
| gcAsyncId = GetAsyncIdNoGC(isolate); | ||
| } | ||
| gcCount.store(curCount + 1, std::memory_order_relaxed); | ||
| std::atomic_signal_fence(std::memory_order_release); | ||
| } | ||
|
|
||
| void WallProfiler::OnGCEnd() { | ||
| auto newCount = gcCount.load(std::memory_order_relaxed) - 1; | ||
| std::atomic_signal_fence(std::memory_order_acquire); | ||
| gcCount.store(newCount, std::memory_order_relaxed); | ||
| std::atomic_signal_fence(std::memory_order_release); | ||
| if (newCount == 0) { | ||
| gcAsyncId = -1; | ||
| } | ||
| } | ||
|
|
||
| void WallProfiler::PushContext(int64_t time_from, | ||
| int64_t time_to, | ||
| int64_t cpu_time, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,7 @@ export interface TimeProfilerOptions { | |
| withContexts?: boolean; | ||
| workaroundV8Bug?: boolean; | ||
| collectCpuTime?: boolean; | ||
| collectAsyncId?: boolean; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, that's new. I'm inclined to ignore it, seeing how existing boolean properties also don't follow this format. |
||
| } | ||
|
|
||
| const DEFAULT_OPTIONS: TimeProfilerOptions = { | ||
|
|
@@ -75,6 +76,7 @@ const DEFAULT_OPTIONS: TimeProfilerOptions = { | |
| withContexts: false, | ||
| workaroundV8Bug: true, | ||
| collectCpuTime: false, | ||
| collectAsyncId: false, | ||
| }; | ||
|
|
||
| export async function profile(options: TimeProfilerOptions = {}) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which cases does
GetCurrentreturns null ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly don't know – I'm just trying to be pedantically defensive and not unconditionally dereference a pointer. I can envision a race condition between signal delivery and an isolate going away? I know the logic is different, but even the V8 sampler manager checks for isolate on a sampler not being null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could be more lenient and still invoke
old_handler?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's ok as this.