diff --git a/.gitlab/benchmarks.yml b/.gitlab/benchmarks.yml index 1bb4e7fd..73bb6494 100644 --- a/.gitlab/benchmarks.yml +++ b/.gitlab/benchmarks.yml @@ -5,7 +5,10 @@ variables: tags: ["runner:apm-k8s-tweaked-metal"] image: $BASE_CI_IMAGE stage: benchmarks - when: on_success + rules: + - if: $CI_COMMIT_TAG + when: never + - when: on_success variables: UPSTREAM_PROJECT_ID: $CI_PROJECT_ID UPSTREAM_PROJECT_NAME: $CI_PROJECT_NAME diff --git a/binding.gyp b/binding.gyp index 8a6d3250..db93a543 100644 --- a/binding.gyp +++ b/binding.gyp @@ -15,7 +15,9 @@ "bindings/profilers/heap.cc", "bindings/profilers/wall.cc", "bindings/per-isolate-data.cc", + "bindings/profile-translator.cc", "bindings/thread-cpu-clock.cc", + "bindings/translate-heap-profile.cc", "bindings/translate-time-profile.cc", "bindings/binding.cc" ], @@ -38,6 +40,7 @@ "bindings/profilers/wall.cc", "bindings/per-isolate-data.cc", "bindings/thread-cpu-clock.cc", + "bindings/translate-heap-profile.cc", "bindings/translate-time-profile.cc", "bindings/test/binding.cc", ], diff --git a/bindings/contexts.hh b/bindings/contexts.hh index 3f5faf75..844fb621 100644 --- a/bindings/contexts.hh +++ b/bindings/contexts.hh @@ -16,7 +16,6 @@ #pragma once -#include #include #include diff --git a/bindings/general-regs-only.hh b/bindings/general-regs-only.hh new file mode 100644 index 00000000..cdcf8166 --- /dev/null +++ b/bindings/general-regs-only.hh @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#if defined(__linux__) && defined(__aarch64__) +#define GENERAL_REGS_ONLY __attribute__((target("general-regs-only"))) +#else +#define GENERAL_REGS_ONLY +#endif diff --git a/bindings/profile-translator.cc b/bindings/profile-translator.cc new file mode 100644 index 00000000..de2c18b4 --- /dev/null +++ b/bindings/profile-translator.cc @@ -0,0 +1,22 @@ +/* + * Copyright 2025 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "profile-translator.hh" +#include + +v8::Local dd::ProfileTranslator::NewNumber(int64_t x) { + return v8::Number::New(isolate, static_cast(x)); +} diff --git a/bindings/profile-translator.hh b/bindings/profile-translator.hh new file mode 100644 index 00000000..54429c72 --- /dev/null +++ b/bindings/profile-translator.hh @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace dd { +class ProfileTranslator { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Local context = isolate->GetCurrentContext(); + v8::Local emptyArray = v8::Array::New(isolate, 0); + + protected: + v8::Local NewObject() { return v8::Object::New(isolate); } + + v8::Local NewInteger(int x) { + return v8::Integer::New(isolate, x); + } + + v8::Local NewBoolean(bool x) { + return v8::Boolean::New(isolate, x); + } + + v8::Local NewNumber(int64_t x); + + v8::Local NewArray(int length) { + return length == 0 ? emptyArray : v8::Array::New(isolate, length); + } + + v8::Local NewString(const char* str) { + return v8::String::NewFromUtf8(isolate, str).ToLocalChecked(); + } + + v8::MaybeLocal Get(v8::Local arr, uint32_t index) { + return arr->Get(context, index); + } + + v8::Maybe Set(v8::Local arr, + uint32_t index, + v8::Local value) { + return arr->Set(context, index, value); + } + + v8::Maybe Set(v8::Local obj, + v8::Local key, + v8::Local value) { + return obj->Set(context, key, value); + } + + ProfileTranslator() = default; +}; +}; // namespace dd diff --git a/bindings/profilers/heap.cc b/bindings/profilers/heap.cc index a21007f1..0ad9a9de 100644 --- a/bindings/profilers/heap.cc +++ b/bindings/profilers/heap.cc @@ -18,6 +18,7 @@ #include "defer.hh" #include "per-isolate-data.hh" +#include "translate-heap-profile.hh" #include #include @@ -482,49 +483,6 @@ size_t NearHeapLimit(void* data, return new_heap_limit; } -v8::Local TranslateAllocationProfile( - v8::AllocationProfile::Node* node) { - v8::Local js_node = Nan::New(); - - Nan::Set(js_node, Nan::New("name").ToLocalChecked(), node->name); - Nan::Set(js_node, - Nan::New("scriptName").ToLocalChecked(), - node->script_name); - Nan::Set(js_node, - Nan::New("scriptId").ToLocalChecked(), - Nan::New(node->script_id)); - Nan::Set(js_node, - Nan::New("lineNumber").ToLocalChecked(), - Nan::New(node->line_number)); - Nan::Set(js_node, - Nan::New("columnNumber").ToLocalChecked(), - Nan::New(node->column_number)); - - v8::Local children = Nan::New(node->children.size()); - for (size_t i = 0; i < node->children.size(); i++) { - Nan::Set(children, i, TranslateAllocationProfile(node->children[i])); - } - Nan::Set( - js_node, Nan::New("children").ToLocalChecked(), children); - v8::Local allocations = - Nan::New(node->allocations.size()); - for (size_t i = 0; i < node->allocations.size(); i++) { - v8::AllocationProfile::Allocation alloc = node->allocations[i]; - v8::Local js_alloc = Nan::New(); - Nan::Set(js_alloc, - Nan::New("sizeBytes").ToLocalChecked(), - Nan::New(alloc.size)); - Nan::Set(js_alloc, - Nan::New("count").ToLocalChecked(), - Nan::New(alloc.count)); - Nan::Set(allocations, i, js_alloc); - } - Nan::Set(js_node, - Nan::New("allocations").ToLocalChecked(), - allocations); - return js_node; -} - NAN_METHOD(HeapProfiler::StartSamplingHeapProfiler) { if (info.Length() == 2) { if (!info[0]->IsUint32()) { diff --git a/bindings/profilers/heap.hh b/bindings/profilers/heap.hh index 5badc46c..01c897e3 100644 --- a/bindings/profilers/heap.hh +++ b/bindings/profilers/heap.hh @@ -17,6 +17,7 @@ #pragma once #include +#include "general-regs-only.hh" namespace dd { @@ -34,7 +35,7 @@ class HeapProfiler { // getAllocationProfile(): AllocationProfileNode static NAN_METHOD(GetAllocationProfile); - static NAN_METHOD(MonitorOutOfMemory); + static NAN_METHOD(MonitorOutOfMemory) GENERAL_REGS_ONLY; static NAN_MODULE_INIT(Init); }; diff --git a/bindings/profilers/wall.cc b/bindings/profilers/wall.cc index 7585fb1f..cd40da58 100644 --- a/bindings/profilers/wall.cc +++ b/bindings/profilers/wall.cc @@ -321,7 +321,9 @@ void SignalHandler::HandleProfilerSignal(int sig, auto time_from = Now(); old_handler(sig, info, context); auto time_to = Now(); - prof->PushContext(time_from, time_to, cpu_time); + int64_t async_id = + static_cast(node::AsyncHooksGetExecutionAsyncId(isolate)); + prof->PushContext(time_from, time_to, cpu_time, async_id); } #else class SignalHandler { @@ -368,10 +370,17 @@ static int64_t GetV8ToEpochOffset() { return V8toEpochOffset; } -ContextsByNode WallProfiler::GetContextsByNode(CpuProfile* profile, - ContextBuffer& contexts, - int64_t startCpuTime) { - ContextsByNode contextsByNode; +Local NewNumberFromInt64(Isolate* isolate, int64_t value) { + return Number::New(isolate, static_cast(value)); +} + +std::shared_ptr CreateContextsByNode() { + return std::make_shared(); +} + +std::shared_ptr WallProfiler::GetContextsByNode( + CpuProfile* profile, ContextBuffer& contexts, int64_t startCpuTime) { + auto contextsByNode = CreateContextsByNode(); auto sampleCount = profile->GetSamplesCount(); if (contexts.empty() || sampleCount == 0) { @@ -379,15 +388,17 @@ ContextsByNode WallProfiler::GetContextsByNode(CpuProfile* profile, } auto isolate = Isolate::GetCurrent(); + auto v8Context = isolate->GetCurrentContext(); auto contextIt = contexts.begin(); // deltaIdx is the offset of the sample to process compared to current // iteration index int deltaIdx = 0; - auto contextKey = Nan::New("context").ToLocalChecked(); - auto timestampKey = Nan::New("timestamp").ToLocalChecked(); - auto cpuTimeKey = Nan::New("cpuTime").ToLocalChecked(); + auto contextKey = String::NewFromUtf8Literal(isolate, "context"); + auto timestampKey = String::NewFromUtf8Literal(isolate, "timestamp"); + auto cpuTimeKey = String::NewFromUtf8Literal(isolate, "cpuTime"); + auto asyncIdKey = String::NewFromUtf8Literal(isolate, "asyncId"); auto V8toEpochOffset = GetV8ToEpochOffset(); auto lastCpuTime = startCpuTime; @@ -429,35 +440,46 @@ ContextsByNode WallProfiler::GetContextsByNode(CpuProfile* profile, break; } else { // This sample context is the closest to this sample. - auto it = contextsByNode.find(sample); + auto it = contextsByNode->find(sample); Local array; - if (it == contextsByNode.end()) { - array = Nan::New(); - contextsByNode[sample] = {array, 1}; + if (it == contextsByNode->end()) { + array = Array::New(isolate); + (*contextsByNode)[sample] = {array, 1}; } else { array = it->second.contexts; ++it->second.hitcount; } if (sampleContext.context) { // Conforms to TimeProfileNodeContext defined in v8-types.ts - v8::Local timedContext = Nan::New(); - Nan::Set(timedContext, - contextKey, - sampleContext.context.get()->Get(isolate)); - Nan::Set(timedContext, - timestampKey, - BigInt::New(isolate, sampleTimestamp + V8toEpochOffset)); + Local timedContext = Object::New(isolate); + timedContext + ->Set(v8Context, + contextKey, + sampleContext.context.get()->Get(isolate)) + .Check(); + timedContext + ->Set(v8Context, + timestampKey, + BigInt::New(isolate, sampleTimestamp + V8toEpochOffset)) + .Check(); // if current sample is idle/program, reports its cpu time to the next // sample if (collectCpuTime_ && !isIdleOrProgram(sample)) { - Nan::Set( - timedContext, - cpuTimeKey, - Nan::New(sampleContext.cpu_time - lastCpuTime)); + timedContext + ->Set(v8Context, + cpuTimeKey, + NewNumberFromInt64(isolate, + sampleContext.cpu_time - lastCpuTime)) + .Check(); lastCpuTime = sampleContext.cpu_time; } - Nan::Set(array, array->Length(), timedContext); + timedContext + ->Set(v8Context, + asyncIdKey, + NewNumberFromInt64(isolate, sampleContext.async_id)) + .Check(); + array->Set(v8Context, array->Length(), timedContext).Check(); } // Sample context was consumed, fetch the next one @@ -501,11 +523,7 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod, v8::Local jsArray = v8::Uint32Array::New(buffer, 0, kFieldCount); -#if (V8_MAJOR_VERSION >= 8) fields_ = static_cast(buffer->GetBackingStore()->Data()); -#else - fields_ = static_cast(buffer->GetContents().Data()); -#endif jsArray_ = v8::Global(isolate, jsArray); std::fill(fields_, fields_ + kFieldCount, 0); } @@ -867,7 +885,7 @@ Result WallProfiler::StopImpl(bool restart, v8::Local& profile) { profile = TranslateTimeProfile(v8_profile, includeLines_, - &contextsByNode, + contextsByNode, collectCpuTime_, nonJSThreadsCpuTime); @@ -1003,14 +1021,15 @@ NAN_METHOD(WallProfiler::Dispose) { void WallProfiler::PushContext(int64_t time_from, int64_t time_to, - int64_t cpu_time) { + int64_t cpu_time, + int64_t async_id) { // Be careful this is called in a signal handler context therefore all // operations must be async signal safe (in particular no allocations). // Our ring buffer avoids allocations. auto context = curContext_.load(std::memory_order_relaxed); std::atomic_signal_fence(std::memory_order_acquire); if (contexts_.size() < contexts_.capacity()) { - contexts_.push_back({*context, time_from, time_to, cpu_time}); + contexts_.push_back({*context, time_from, time_to, cpu_time, async_id}); std::atomic_fetch_add_explicit( reinterpret_cast*>(&fields_[kSampleCount]), 1U, diff --git a/bindings/profilers/wall.hh b/bindings/profilers/wall.hh index 91871b0f..0f1f3322 100644 --- a/bindings/profilers/wall.hh +++ b/bindings/profilers/wall.hh @@ -17,6 +17,7 @@ #pragma once #include "contexts.hh" +#include "general-regs-only.hh" #include "thread-cpu-clock.hh" #include @@ -82,6 +83,7 @@ class WallProfiler : public Nan::ObjectWrap { int64_t time_from; int64_t time_to; int64_t cpu_time; + int64_t async_id; }; using ContextBuffer = std::vector; @@ -94,9 +96,10 @@ class WallProfiler : public Nan::ObjectWrap { // to work around https://bugs.chromium.org/p/v8/issues/detail?id=11051. v8::CpuProfiler* CreateV8CpuProfiler(); - ContextsByNode GetContextsByNode(v8::CpuProfile* profile, - ContextBuffer& contexts, - int64_t startCpuTime); + std::shared_ptr GetContextsByNode(v8::CpuProfile* profile, + ContextBuffer& contexts, + int64_t startCpuTime) + GENERAL_REGS_ONLY; bool waitForSignal(uint64_t targetCallCount = 0); @@ -118,10 +121,14 @@ class WallProfiler : public Nan::ObjectWrap { v8::Local GetContext(v8::Isolate*); void SetContext(v8::Isolate*, v8::Local); - void PushContext(int64_t time_from, int64_t time_to, int64_t cpu_time); + void PushContext(int64_t time_from, + int64_t time_to, + int64_t cpu_time, + int64_t async_id); Result StartImpl(); std::string StartInternal(); - Result StopImpl(bool restart, v8::Local& profile); + Result StopImpl(bool restart, + v8::Local& profile) GENERAL_REGS_ONLY; CollectionMode collectionMode() { auto res = collectionMode_.load(std::memory_order_relaxed); @@ -142,12 +149,12 @@ class WallProfiler : public Nan::ObjectWrap { return threadCpuStopWatch_.GetAndReset(); } - static NAN_METHOD(New); + static NAN_METHOD(New) GENERAL_REGS_ONLY; static NAN_METHOD(Start); static NAN_METHOD(Stop); static NAN_METHOD(V8ProfilerStuckEventLoopDetected); static NAN_METHOD(Dispose); - static NAN_MODULE_INIT(Init); + static NAN_MODULE_INIT(Init) GENERAL_REGS_ONLY; static NAN_GETTER(GetContext); static NAN_SETTER(SetContext); static NAN_GETTER(SharedArrayGetter); diff --git a/bindings/translate-heap-profile.cc b/bindings/translate-heap-profile.cc new file mode 100644 index 00000000..b3eed7bc --- /dev/null +++ b/bindings/translate-heap-profile.cc @@ -0,0 +1,105 @@ +/* + * Copyright 2024 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "translate-heap-profile.hh" +#include "profile-translator.hh" + +namespace dd { + +namespace { +class HeapProfileTranslator : ProfileTranslator { +#define NODE_FIELDS \ + X(name) \ + X(scriptName) \ + X(scriptId) \ + X(lineNumber) \ + X(columnNumber) \ + X(children) \ + X(allocations) + +#define ALLOCATION_FIELDS \ + X(sizeBytes) \ + X(count) + +#define X(name) v8::Local str_##name = NewString(#name); + NODE_FIELDS + ALLOCATION_FIELDS +#undef X + + public: + v8::Local TranslateAllocationProfile( + v8::AllocationProfile::Node* node) GENERAL_REGS_ONLY { + v8::Local children = NewArray(node->children.size()); + for (size_t i = 0; i < node->children.size(); i++) { + Set(children, i, TranslateAllocationProfile(node->children[i])); + } + + v8::Local allocations = NewArray(node->allocations.size()); + for (size_t i = 0; i < node->allocations.size(); i++) { + auto alloc = node->allocations[i]; + Set(allocations, + i, + CreateAllocation(NewNumber(alloc.size), NewNumber(alloc.count))); + } + + return CreateNode(node->name, + node->script_name, + NewInteger(node->script_id), + NewInteger(node->line_number), + NewInteger(node->column_number), + children, + allocations); + } + + private: + v8::Local CreateNode(v8::Local name, + v8::Local scriptName, + v8::Local scriptId, + v8::Local lineNumber, + v8::Local columnNumber, + v8::Local children, + v8::Local allocations) + GENERAL_REGS_ONLY { + v8::Local js_node = NewObject(); +#define X(name) Set(js_node, str_##name, name); + NODE_FIELDS +#undef X +#undef NODE_FIELDS + return js_node; + } + + v8::Local CreateAllocation(v8::Local count, + v8::Local sizeBytes) + GENERAL_REGS_ONLY { + v8::Local js_alloc = NewObject(); +#define X(name) Set(js_alloc, str_##name, name); + ALLOCATION_FIELDS +#undef X +#undef ALLOCATION_FIELDS + return js_alloc; + } + + public: + explicit HeapProfileTranslator() {} +}; +} // namespace + +v8::Local TranslateAllocationProfile( + v8::AllocationProfile::Node* node) { + return HeapProfileTranslator().TranslateAllocationProfile(node); +} + +} // namespace dd diff --git a/bindings/translate-heap-profile.hh b/bindings/translate-heap-profile.hh new file mode 100644 index 00000000..5493fba2 --- /dev/null +++ b/bindings/translate-heap-profile.hh @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "general-regs-only.hh" + +namespace dd { + +v8::Local TranslateAllocationProfile( + v8::AllocationProfile::Node* node) GENERAL_REGS_ONLY; + +} // namespace dd diff --git a/bindings/translate-time-profile.cc b/bindings/translate-time-profile.cc index 4803f7a9..3772ebe1 100644 --- a/bindings/translate-time-profile.cc +++ b/bindings/translate-time-profile.cc @@ -15,16 +15,14 @@ */ #include "translate-time-profile.hh" - -#include +#include "profile-translator.hh" namespace dd { namespace { -class ProfileTranslator { +class TimeProfileTranslator : ProfileTranslator { private: - ContextsByNode* contextsByNode; - v8::Isolate* isolate = v8::Isolate::GetCurrent(); + std::shared_ptr contextsByNode; v8::Local emptyArray = NewArray(0); v8::Local zero = NewInteger(0); @@ -42,8 +40,8 @@ class ProfileTranslator { FIELDS #undef X - v8::Local getContextsForNode(const v8::CpuProfileNode* node, - uint32_t& hitcount) { + v8::Local getContextsForNode( + const v8::CpuProfileNode* node, uint32_t& hitcount) GENERAL_REGS_ONLY { hitcount = node->GetHitCount(); if (!contextsByNode) { // custom contexts are not enabled, keep the node hitcount and return @@ -72,29 +70,18 @@ class ProfileTranslator { v8::Local columnNumber, v8::Local hitCount, v8::Local children, - v8::Local contexts) { - v8::Local js_node = Nan::New(); -#define X(name) Nan::Set(js_node, str_##name, name); + v8::Local contexts) + GENERAL_REGS_ONLY { + v8::Local js_node = NewObject(); +#define X(name) Set(js_node, str_##name, name); FIELDS #undef X #undef FIELDS return js_node; } - v8::Local NewInteger(int32_t x) { - return v8::Integer::New(isolate, x); - } - - v8::Local NewArray(int length) { - return v8::Array::New(isolate, length); - } - - v8::Local NewString(const char* str) { - return Nan::New(str).ToLocalChecked(); - } - v8::Local GetLineNumberTimeProfileChildren( - const v8::CpuProfileNode* node) { + const v8::CpuProfileNode* node) GENERAL_REGS_ONLY { unsigned int index = 0; v8::Local children; int32_t count = node->GetChildrenCount(); @@ -107,46 +94,47 @@ class ProfileTranslator { node->GetLineTicks(&entries[0], hitLineCount); children = NewArray(count + hitLineCount); for (const v8::CpuProfileNode::LineTick entry : entries) { - Nan::Set(children, - index++, - CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), - scriptId, - NewInteger(entry.line), - zero, - NewInteger(entry.hit_count), - emptyArray, - emptyArray)); + Set(children, + index++, + CreateTimeNode(node->GetFunctionName(), + node->GetScriptResourceName(), + scriptId, + NewInteger(entry.line), + zero, + NewInteger(entry.hit_count), + emptyArray, + emptyArray)); } } else if (hitCount > 0) { // Handle nodes for pseudo-functions like "process" and "garbage // collection" which do not have hit line counts. children = NewArray(count + 1); - Nan::Set(children, - index++, - CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), - scriptId, - NewInteger(node->GetLineNumber()), - NewInteger(node->GetColumnNumber()), - NewInteger(hitCount), - emptyArray, - emptyArray)); + Set(children, + index++, + CreateTimeNode(node->GetFunctionName(), + node->GetScriptResourceName(), + scriptId, + NewInteger(node->GetLineNumber()), + NewInteger(node->GetColumnNumber()), + NewInteger(hitCount), + emptyArray, + emptyArray)); } else { children = NewArray(count); } for (int32_t i = 0; i < count; i++) { - Nan::Set(children, - index++, - TranslateLineNumbersTimeProfileNode(node, node->GetChild(i))); + Set(children, + index++, + TranslateLineNumbersTimeProfileNode(node, node->GetChild(i))); }; return children; } v8::Local TranslateLineNumbersTimeProfileNode( - const v8::CpuProfileNode* parent, const v8::CpuProfileNode* node) { + const v8::CpuProfileNode* parent, + const v8::CpuProfileNode* node) GENERAL_REGS_ONLY { return CreateTimeNode(parent->GetFunctionName(), parent->GetScriptResourceName(), NewInteger(parent->GetScriptId()), @@ -161,7 +149,7 @@ class ProfileTranslator { // and column number refer to the line/column from which the function was // called. v8::Local TranslateLineNumbersTimeProfileRoot( - const v8::CpuProfileNode* node) { + const v8::CpuProfileNode* node) GENERAL_REGS_ONLY { int32_t count = node->GetChildrenCount(); std::vector> childrenArrs(count); int32_t childCount = 0; @@ -177,7 +165,7 @@ class ProfileTranslator { for (int32_t i = 0; i < count; i++) { v8::Local arr = childrenArrs[i]; for (uint32_t j = 0; j < arr->Length(); j++) { - Nan::Set(children, idx, Nan::Get(arr, j).ToLocalChecked()); + Set(children, idx, Get(arr, j).ToLocalChecked()); idx++; } } @@ -192,12 +180,12 @@ class ProfileTranslator { emptyArray); } - v8::Local TranslateTimeProfileNode( - const v8::CpuProfileNode* node) { + v8::Local TranslateTimeProfileNode(const v8::CpuProfileNode* node) + GENERAL_REGS_ONLY { int32_t count = node->GetChildrenCount(); - v8::Local children = Nan::New(count); + v8::Local children = NewArray(count); for (int32_t i = 0; i < count; i++) { - Nan::Set(children, i, TranslateTimeProfileNode(node->GetChild(i))); + Set(children, i, TranslateTimeProfileNode(node->GetChild(i))); } uint32_t hitcount = 0; @@ -214,47 +202,44 @@ class ProfileTranslator { } public: - explicit ProfileTranslator(ContextsByNode* nls = nullptr) + explicit TimeProfileTranslator(std::shared_ptr nls = nullptr) : contextsByNode(nls) {} v8::Local TranslateTimeProfile(const v8::CpuProfile* profile, bool includeLineInfo, bool hasCpuTime, - int64_t nonJSThreadsCpuTime) { - v8::Local js_profile = Nan::New(); + int64_t nonJSThreadsCpuTime) + GENERAL_REGS_ONLY { + v8::Local js_profile = NewObject(); if (includeLineInfo) { - Nan::Set(js_profile, - NewString("topDownRoot"), - TranslateLineNumbersTimeProfileRoot(profile->GetTopDownRoot())); + Set(js_profile, + NewString("topDownRoot"), + TranslateLineNumbersTimeProfileRoot(profile->GetTopDownRoot())); } else { - Nan::Set(js_profile, - NewString("topDownRoot"), - TranslateTimeProfileNode(profile->GetTopDownRoot())); + Set(js_profile, + NewString("topDownRoot"), + TranslateTimeProfileNode(profile->GetTopDownRoot())); } - Nan::Set(js_profile, - NewString("startTime"), - Nan::New(profile->GetStartTime())); - Nan::Set(js_profile, - NewString("endTime"), - Nan::New(profile->GetEndTime())); - Nan::Set( - js_profile, NewString("hasCpuTime"), Nan::New(hasCpuTime)); + Set(js_profile, NewString("startTime"), NewNumber(profile->GetStartTime())); + Set(js_profile, NewString("endTime"), NewNumber(profile->GetEndTime())); + Set(js_profile, NewString("hasCpuTime"), NewBoolean(hasCpuTime)); - Nan::Set(js_profile, - NewString("nonJSThreadsCpuTime"), - Nan::New(nonJSThreadsCpuTime)); + Set(js_profile, + NewString("nonJSThreadsCpuTime"), + NewNumber(nonJSThreadsCpuTime)); return js_profile; } }; } // namespace -v8::Local TranslateTimeProfile(const v8::CpuProfile* profile, - bool includeLineInfo, - ContextsByNode* contextsByNode, - bool hasCpuTime, - int64_t nonJSThreadsCpuTime) { - return ProfileTranslator(contextsByNode) +v8::Local TranslateTimeProfile( + const v8::CpuProfile* profile, + bool includeLineInfo, + std::shared_ptr contextsByNode, + bool hasCpuTime, + int64_t nonJSThreadsCpuTime) { + return TimeProfileTranslator(contextsByNode) .TranslateTimeProfile( profile, includeLineInfo, hasCpuTime, nonJSThreadsCpuTime); } diff --git a/bindings/translate-time-profile.hh b/bindings/translate-time-profile.hh index 00d0352b..94e46d86 100644 --- a/bindings/translate-time-profile.hh +++ b/bindings/translate-time-profile.hh @@ -16,18 +16,17 @@ #pragma once -#include "contexts.hh" - -#include #include +#include "contexts.hh" +#include "general-regs-only.hh" namespace dd { v8::Local TranslateTimeProfile( const v8::CpuProfile* profile, bool includeLineInfo, - ContextsByNode* contextsByNode = nullptr, + std::shared_ptr contextsByNode = nullptr, bool hasCpuTime = false, - int64_t nonJSThreadsCpuTime = 0); + int64_t nonJSThreadsCpuTime = 0) GENERAL_REGS_ONLY; } // namespace dd diff --git a/package-lock.json b/package-lock.json index 4429bd46..ae5abcca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@datadog/pprof", - "version": "5.4.1", + "version": "5.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@datadog/pprof", - "version": "5.4.1", + "version": "5.5.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index b580ccb6..5f35389e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/pprof", - "version": "5.4.1", + "version": "5.5.0", "description": "pprof support for Node.js", "repository": "datadog/pprof-nodejs", "main": "out/src/index.js", diff --git a/ts/src/profile-serializer.ts b/ts/src/profile-serializer.ts index 4e24460a..9eed3153 100644 --- a/ts/src/profile-serializer.ts +++ b/ts/src/profile-serializer.ts @@ -40,7 +40,8 @@ import { TimeProfileNode, } from './v8-types'; -export const NON_JS_THREADS_FUNCTION_NAME = '(non-JS threads)'; +export const NON_JS_THREADS_FUNCTION_NAME = 'Non JS threads activity'; +export const GARBAGE_COLLECTION_FUNCTION_NAME = 'Garbage Collection'; /** * A stack of function IDs. @@ -116,7 +117,6 @@ function serialize( if (ignoreSamplesPath && node.scriptName.indexOf(ignoreSamplesPath) > -1) { continue; } - if (node.name === '(idle)' || node.name === '(program)') continue; const stack = entry.stack; const location = getLocation(node, sourceMapper); stack.unshift(location.id as number); @@ -268,6 +268,76 @@ function computeTotalHitCount(root: TimeProfileNode): number { ); } +/** Perform some modifications on time profile: + * - Add non-JS thread activity node if available + * - Remove `(idle)` and `(program)` nodes + * - Convert `(garbage collector)` node to `Garbage Collection` + * - Put `non-JS thread activity` node and `Garbage Collection` under a top level `Node.js` node + * This function does not change the input profile. + */ +function updateTimeProfile(prof: TimeProfile): TimeProfile { + const newTopLevelChildren: TimeProfileNode[] = []; + + let runtimeNode: TimeProfileNode | undefined; + + function getRuntimeNode(): TimeProfileNode { + if (!runtimeNode) { + runtimeNode = { + name: 'Node.js', + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + children: [], + hitCount: 0, + }; + newTopLevelChildren.push(runtimeNode); + } + return runtimeNode; + } + + for (const child of prof.topDownRoot.children as TimeProfileNode[]) { + if (child.name === '(idle)' || child.name === '(program)') { + continue; + } + if (child.name === '(garbage collector)') { + // Create a new node to avoid modifying the input one + const newChild: TimeProfileNode = { + ...child, + name: GARBAGE_COLLECTION_FUNCTION_NAME, + }; + getRuntimeNode().children.push(newChild); + } else { + newTopLevelChildren.push(child); + } + } + + if (prof.hasCpuTime && prof.nonJSThreadsCpuTime) { + const node: TimeProfileNode = { + name: NON_JS_THREADS_FUNCTION_NAME, + scriptName: '', + scriptId: 0, + lineNumber: 0, + columnNumber: 0, + children: [], + hitCount: 0, // 0 because this should not be accounted for wall time + contexts: [ + { + context: {}, + timestamp: BigInt(0), + cpuTime: prof.nonJSThreadsCpuTime, + asyncId: -1, + }, + ], + }; + getRuntimeNode().children.push(node); + } + return { + ...prof, + topDownRoot: {...prof.topDownRoot, children: newTopLevelChildren}, + }; +} + /** * Converts v8 time profile into into a profile proto. * (https://github.com/google/pprof/blob/master/proto/profile.proto) @@ -366,28 +436,11 @@ export function serializeTimeProfile( period: intervalNanos, }; - if (prof.hasCpuTime && prof.nonJSThreadsCpuTime) { - const node: TimeProfileNode = { - name: NON_JS_THREADS_FUNCTION_NAME, - scriptName: '', - scriptId: 0, - lineNumber: 0, - columnNumber: 0, - children: [], - hitCount: 0, // 0 because this should not be accounted for wall time - contexts: [ - { - context: {}, - timestamp: BigInt(0), - cpuTime: prof.nonJSThreadsCpuTime, - }, - ], - }; - prof.topDownRoot.children.push(node); - } + const updatedProf = updateTimeProfile(prof); + serialize( profile, - prof.topDownRoot, + updatedProf.topDownRoot, appendTimeEntryToSamples, stringTable, undefined, diff --git a/ts/src/time-profiler.ts b/ts/src/time-profiler.ts index 6aa242a4..5d1defd1 100644 --- a/ts/src/time-profiler.ts +++ b/ts/src/time-profiler.ts @@ -18,6 +18,7 @@ import delay from 'delay'; import { serializeTimeProfile, + GARBAGE_COLLECTION_FUNCTION_NAME, NON_JS_THREADS_FUNCTION_NAME, } from './profile-serializer'; import {SourceMapper} from './sourcemapper/sourcemapper'; @@ -171,5 +172,9 @@ export function v8ProfilerStuckEventLoopDetected() { return gV8ProfilerStuckEventLoopDetected; } -export const constants = {kSampleCount, NON_JS_THREADS_FUNCTION_NAME}; +export const constants = { + kSampleCount, + GARBAGE_COLLECTION_FUNCTION_NAME, + NON_JS_THREADS_FUNCTION_NAME, +}; export {getNativeThreadId}; diff --git a/ts/src/v8-types.ts b/ts/src/v8-types.ts index af81922e..28e15a98 100644 --- a/ts/src/v8-types.ts +++ b/ts/src/v8-types.ts @@ -41,6 +41,7 @@ export interface TimeProfileNodeContext { context: object; timestamp: bigint; // end of sample taking; in microseconds since epoch cpuTime: number; // cpu time in nanoseconds + asyncId: number; // async_hooks.executionAsyncId() at the time of sample taking } export interface TimeProfileNode extends ProfileNode { diff --git a/ts/test/profiles-for-tests.ts b/ts/test/profiles-for-tests.ts index 23dae818..7320bbde 100644 --- a/ts/test/profiles-for-tests.ts +++ b/ts/test/profiles-for-tests.ts @@ -84,7 +84,7 @@ const timeNode2 = { children: [timeLeaf3], }; -const timeRoot = { +const timeRoot = Object.freeze({ name: '(root)', scriptName: 'root', scriptId: 0, @@ -92,7 +92,7 @@ const timeRoot = { columnNumber: 0, hitCount: 0, children: [timeNode1, timeNode2], -}; +}); export const v8TimeProfile: TimeProfile = Object.freeze({ startTime: 0, diff --git a/ts/test/test-profile-serializer.ts b/ts/test/test-profile-serializer.ts index 9673198a..c93619e2 100644 --- a/ts/test/test-profile-serializer.ts +++ b/ts/test/test-profile-serializer.ts @@ -192,7 +192,7 @@ describe('profile-serializer', () => { const heapProfileOut = serializeHeapProfile(v8HeapProfile, 0, 512 * 1024); assert.deepEqual(heapProfileOut, heapProfile); }); - it('should produce expected profile when there is anyonmous function', () => { + it('should produce expected profile when there is anonymous function', () => { const heapProfileOut = serializeHeapProfile( v8AnonymousFunctionHeapProfile, 0, diff --git a/ts/test/test-time-profiler.ts b/ts/test/test-time-profiler.ts index 607e1a6f..26e7664b 100644 --- a/ts/test/test-time-profiler.ts +++ b/ts/test/test-time-profiler.ts @@ -135,6 +135,10 @@ describe('Time Profiler', () => { if (!context) { return {}; } + assert( + typeof context.asyncId === 'number', + 'context.asyncId should be a number' + ); const labels: LabelSet = {}; for (const [key, value] of Object.entries(context.context)) { if (typeof value === 'string') {