diff --git a/binding.gyp b/binding.gyp index df68afe6..0018fc0d 100644 --- a/binding.gyp +++ b/binding.gyp @@ -22,9 +22,11 @@ "bindings/location.cc", "bindings/per-isolate-data.cc", "bindings/sample.cc", + "bindings/translate-time-profile.cc", "bindings/binding.cc" ], "include_dirs": [ + "bindings", " #include #include diff --git a/bindings/buffer.hh b/bindings/buffer.hh new file mode 100644 index 00000000..f3cae172 --- /dev/null +++ b/bindings/buffer.hh @@ -0,0 +1,86 @@ +/* + * Copyright 2023 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 + +namespace dd { +template +class RingBuffer { + public: + explicit RingBuffer(size_t capacity) + : buffer(std::make_unique(capacity)), + capacity_(capacity), + size_(0), + back_index_(0), + front_index_(0) {} + + size_t capacity() const { return capacity_; } + bool full() const { return size_ == capacity_; } + bool empty() const { return size_ == 0; } + size_t size() const { return size_; } + + T& front() { return buffer[front_index_]; } + const T& front() const { return buffer[front_index_]; } + + void push_back(const T& t) { push_back_(t); } + void push_back(T&& t) { push_back_(std::move(t)); } + + void clear() { + while (!empty()) { + pop_front(); + } + } + + T pop_front() { + auto idx = front_index_; + increment(front_index_); + --size_; + return std::move(buffer[idx]); + } + + private: + template + void push_back_(U&& t) { + const bool is_full = full(); + + if (is_full && empty()) { + return; + } + buffer[back_index_] = std::forward(t); + increment(back_index_); + + if (is_full) { + // move buffer head + front_index_ = back_index_; + } else { + ++size_; + } + } + + void increment(size_t& idx) const { + idx = idx + 1 == capacity_ ? 0 : idx + 1; + } + void decrement(size_t& idx) const { + idx = idx == 0 ? capacity_ - 1 : idx - 1; + } + + std::unique_ptr buffer; + size_t capacity_; + size_t size_; + size_t back_index_; + size_t front_index_; +}; +} // namespace dd diff --git a/bindings/code-event-record.cc b/bindings/code-event-record.cc index 78b7f436..08c00834 100644 --- a/bindings/code-event-record.cc +++ b/bindings/code-event-record.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include "code-event-record.hh" diff --git a/bindings/code-event-record.hh b/bindings/code-event-record.hh index 06076d8f..c417a0ef 100644 --- a/bindings/code-event-record.hh +++ b/bindings/code-event-record.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/code-map.cc b/bindings/code-map.cc index 822a32ea..3d1e9683 100644 --- a/bindings/code-map.cc +++ b/bindings/code-map.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include diff --git a/bindings/code-map.hh b/bindings/code-map.hh index 2f6f6d8a..25ca0a83 100644 --- a/bindings/code-map.hh +++ b/bindings/code-map.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/cpu-time.cc b/bindings/cpu-time.cc index 60acad53..e37932bc 100644 --- a/bindings/cpu-time.cc +++ b/bindings/cpu-time.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "cpu-time.hh" #ifdef __linux__ diff --git a/bindings/cpu-time.hh b/bindings/cpu-time.hh index cdfade90..6aad0dbf 100644 --- a/bindings/cpu-time.hh +++ b/bindings/cpu-time.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/labelsets.hh b/bindings/labelsets.hh new file mode 100644 index 00000000..13ef4780 --- /dev/null +++ b/bindings/labelsets.hh @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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 +#include + +namespace dd { + +struct NodeInfo { + v8::Local labelSets; + uint32_t hitcount; +}; + +using LabelSetsByNode = std::unordered_map; +} // namespace dd diff --git a/bindings/location.cc b/bindings/location.cc index ae5effb9..2c4ad5e9 100644 --- a/bindings/location.cc +++ b/bindings/location.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include diff --git a/bindings/location.hh b/bindings/location.hh index 07e4596b..c3849dde 100644 --- a/bindings/location.hh +++ b/bindings/location.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/per-isolate-data.cc b/bindings/per-isolate-data.cc index 04c94959..ea78dc65 100644 --- a/bindings/per-isolate-data.cc +++ b/bindings/per-isolate-data.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include #include diff --git a/bindings/per-isolate-data.hh b/bindings/per-isolate-data.hh index 2718c5b4..1f516912 100644 --- a/bindings/per-isolate-data.hh +++ b/bindings/per-isolate-data.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/profilers/cpu.hh b/bindings/profilers/cpu.hh index b915a432..9601fa49 100644 --- a/bindings/profilers/cpu.hh +++ b/bindings/profilers/cpu.hh @@ -7,6 +7,7 @@ #include #include +#include "../buffer.hh" #include "../code-map.hh" #include "../cpu-time.hh" #include "../sample.hh" @@ -14,59 +15,6 @@ namespace dd { -class SampleBuffer { - public: - using SamplePtr = std::unique_ptr; - - explicit SampleBuffer(size_t size) - : samples_(std::make_unique(size)), - capacity_(size), - size_(0), - back_index_(0), - front_index_(0) {} - - bool full() const { return size_ == capacity_; } - bool empty() const { return size_ == 0; } - - SamplePtr& front() { return samples_[front_index_]; } - - const SamplePtr& front() const { return samples_[front_index_]; } - - void push_back(SamplePtr ptr) { - if (full()) { - if (empty()) { - return; - } - // overwrite buffer head - samples_[back_index_] = std::move(ptr); - increment(back_index_); - // move buffer head - front_index_ = back_index_; - } else { - samples_[back_index_] = std::move(ptr); - increment(back_index_); - ++size_; - } - } - - SamplePtr pop_front() { - auto idx = front_index_; - increment(front_index_); - --size_; - return std::move(samples_[idx]); - } - - private: - void increment(size_t& idx) const { - idx = idx + 1 == capacity_ ? 0 : idx + 1; - } - std::unique_ptr samples_; - size_t capacity_; - size_t size_; - size_t back_index_; - size_t front_index_; -}; - class CpuProfiler : public Nan::ObjectWrap { friend class CodeMap; @@ -75,7 +23,7 @@ class CpuProfiler : public Nan::ObjectWrap { uv_async_t* async; std::shared_ptr code_map; CpuTime cpu_time; - SampleBuffer last_samples; + RingBuffer> last_samples; std::shared_ptr labels_; double frequency = 0; Nan::Global samples; diff --git a/bindings/profilers/defer.hh b/bindings/profilers/defer.hh index 1ba3750d..a07dd613 100644 --- a/bindings/profilers/defer.hh +++ b/bindings/profilers/defer.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/profilers/heap.cc b/bindings/profilers/heap.cc index c2ce16ec..c534d914 100644 --- a/bindings/profilers/heap.cc +++ b/bindings/profilers/heap.cc @@ -16,8 +16,8 @@ #include "heap.hh" -#include "../per-isolate-data.hh" #include "defer.hh" +#include "per-isolate-data.hh" #include #include diff --git a/bindings/profilers/wall.cc b/bindings/profilers/wall.cc index 5a8f03a0..7c510bdb 100644 --- a/bindings/profilers/wall.cc +++ b/bindings/profilers/wall.cc @@ -1,11 +1,11 @@ -/** - * Copyright 2018 Google Inc. All Rights Reserved. +/* + * Copyright 2023 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 + * 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, @@ -14,224 +14,409 @@ * limitations under the License. */ +#include +#include #include #include +#include #include #include #include #include -#include "../per-isolate-data.hh" +#include "per-isolate-data.hh" +#include "translate-time-profile.hh" #include "wall.hh" +#ifndef _WIN32 +#define DD_WALL_USE_SIGPROF + +// Declare v8::base::TimeTicks::Now. It is exported from the node executable so +// our addon will be able to dynamically link to the symbol when loaded. +namespace v8 { +namespace base { +struct TimeTicks { + static int64_t Now(); +}; +} // namespace base +} // namespace v8 + +static int64_t Now() { + return v8::base::TimeTicks::Now(); +}; +#else +static int64_t Now() { + return 0; +}; +#endif + using namespace v8; namespace dd { -Local CreateTimeNode(Local name, - Local scriptName, - Local scriptId, - Local lineNumber, - Local columnNumber, - Local hitCount, - Local children) { - Local js_node = Nan::New(); - Nan::Set(js_node, Nan::New("name").ToLocalChecked(), name); - Nan::Set( - js_node, Nan::New("scriptName").ToLocalChecked(), scriptName); - Nan::Set(js_node, Nan::New("scriptId").ToLocalChecked(), scriptId); - Nan::Set( - js_node, Nan::New("lineNumber").ToLocalChecked(), lineNumber); - Nan::Set( - js_node, Nan::New("columnNumber").ToLocalChecked(), columnNumber); - Nan::Set(js_node, Nan::New("hitCount").ToLocalChecked(), hitCount); - Nan::Set(js_node, Nan::New("children").ToLocalChecked(), children); - - return js_node; -} +class ProtectedProfilerMap { + public: + WallProfiler* GetProfiler(const Isolate* isolate) const { + // Prevent updates to profiler map by atomically setting g_profilers to null + auto prof_map = profilers_.exchange(nullptr, std::memory_order_acq_rel); + if (!prof_map) { + return nullptr; + } + auto prof_it = prof_map->find(isolate); + WallProfiler* profiler = nullptr; + if (prof_it != prof_map->end()) { + profiler = prof_it->second; + } + // Allow updates + profilers_.store(prof_map, std::memory_order_release); + return profiler; + } + + bool RemoveProfiler(const v8::Isolate* isolate, WallProfiler* profiler) { + return UpdateProfilers([isolate, profiler](auto map) { + if (isolate != nullptr) { + auto it = map->find(isolate); + if (it != map->end() && it->second == profiler) { + map->erase(it); + return true; + } + } else { + auto it = std::find_if(map->begin(), map->end(), [profiler](auto& x) { + return x.second == profiler; + }); + if (it != map->end()) { + map->erase(it); + return true; + } + } + return false; + }); + } -Local TranslateLineNumbersTimeProfileNode(const CpuProfileNode* parent, - const CpuProfileNode* node); - -Local GetLineNumberTimeProfileChildren(const CpuProfileNode* parent, - const CpuProfileNode* node) { - unsigned int index = 0; - Local children; - int32_t count = node->GetChildrenCount(); - - unsigned int hitLineCount = node->GetHitLineCount(); - unsigned int hitCount = node->GetHitCount(); - if (hitLineCount > 0) { - std::vector entries(hitLineCount); - node->GetLineTicks(&entries[0], hitLineCount); - children = Nan::New(count + hitLineCount); - for (const CpuProfileNode::LineTick entry : entries) { - Nan::Set(children, - index++, - CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), - Nan::New(node->GetScriptId()), - Nan::New(entry.line), - Nan::New(0), - Nan::New(entry.hit_count), - Nan::New(0))); + bool AddProfiler(const v8::Isolate* isolate, WallProfiler* profiler) { + return UpdateProfilers([isolate, profiler](auto map) { + return map->emplace(isolate, profiler).second; + }); + } + + private: + template + bool UpdateProfilers(F updateFn) { + // use mutex to prevent two isolates of updating profilers concurrently + std::lock_guard lock(update_mutex_); + + if (!init_) { + profilers_.store(new ProfilerMap(), std::memory_order_release); } - } else if (hitCount > 0) { - // Handle nodes for pseudo-functions like "process" and "garbage collection" - // which do not have hit line counts. - children = Nan::New(count + 1); - Nan::Set(children, - index++, - CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), - Nan::New(node->GetScriptId()), - Nan::New(node->GetLineNumber()), - Nan::New(node->GetColumnNumber()), - Nan::New(hitCount), - Nan::New(0))); - } else { - children = Nan::New(count); + + auto currProfilers = profilers_.load(std::memory_order_acquire); + // Wait until sighandler is done using the map + while (!currProfilers) { + currProfilers = profilers_.load(std::memory_order_relaxed); + } + auto newProfilers = new ProfilerMap(*currProfilers); + auto res = updateFn(newProfilers); + // Wait until sighandler is done using the map before installing a new map. + // The value in profilers is either nullptr or currProfilers. + for (;;) { + ProfilerMap* currProfilers2 = currProfilers; + if (profilers_.compare_exchange_weak( + currProfilers2, newProfilers, std::memory_order_acq_rel)) { + break; + } + } + delete currProfilers; + return res; } - for (int32_t i = 0; i < count; i++) { - Nan::Set(children, - index++, - TranslateLineNumbersTimeProfileNode(node, node->GetChild(i))); - }; + using ProfilerMap = std::unordered_map; + mutable std::atomic profilers_; + std::mutex update_mutex_; + bool init_ = false; + ; +}; - return children; -} +using ProfilerMap = std::unordered_map; -Local TranslateLineNumbersTimeProfileNode(const CpuProfileNode* parent, - const CpuProfileNode* node) { - return CreateTimeNode(parent->GetFunctionName(), - parent->GetScriptResourceName(), - Nan::New(parent->GetScriptId()), - Nan::New(node->GetLineNumber()), - Nan::New(node->GetColumnNumber()), - Nan::New(0), - GetLineNumberTimeProfileChildren(parent, node)); -} +static ProtectedProfilerMap g_profilers; +static std::mutex g_profilers_update_mtx; + +namespace { + +#ifdef DD_WALL_USE_SIGPROF +class SignalHandler { + public: + static void IncreaseUseCount() { + std::lock_guard lock(mutex_); + ++use_count_; + // Always reinstall the signal handler + Install(); + } -// In profiles with line level accurate line numbers, a node's line number -// and column number refer to the line/column from which the function was -// called. -Local TranslateLineNumbersTimeProfileRoot(const CpuProfileNode* node) { - int32_t count = node->GetChildrenCount(); - std::vector> childrenArrs(count); - int32_t childCount = 0; - for (int32_t i = 0; i < count; i++) { - Local c = GetLineNumberTimeProfileChildren(node, node->GetChild(i)); - childCount = childCount + c->Length(); - childrenArrs[i] = c; - } - - Local children = Nan::New(childCount); - int32_t idx = 0; - for (int32_t i = 0; i < count; i++) { - Local arr = childrenArrs[i]; - for (uint32_t j = 0; j < arr->Length(); j++) { - Nan::Set(children, idx, Nan::Get(arr, j).ToLocalChecked()); - idx++; + static void DecreaseUseCount() { + std::lock_guard lock(mutex_); + if (--use_count_ == 0) { + Restore(); } } - return CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), - Nan::New(node->GetScriptId()), - Nan::New(node->GetLineNumber()), - Nan::New(node->GetColumnNumber()), - Nan::New(0), - children); -} + static bool Installed() { + std::lock_guard lock(mutex_); + return installed_; + } -Local TranslateTimeProfileNode(const CpuProfileNode* node) { - int32_t count = node->GetChildrenCount(); - Local children = Nan::New(count); - for (int32_t i = 0; i < count; i++) { - Nan::Set(children, i, TranslateTimeProfileNode(node->GetChild(i))); + private: + static void Install() { + struct sigaction sa; + sa.sa_sigaction = &HandleProfilerSignal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK; + if (installed_) { + sigaction(SIGPROF, &sa, nullptr); + } else { + installed_ = (sigaction(SIGPROF, &sa, &old_handler_) == 0); + old_handler_func_.store(old_handler_.sa_sigaction, + std::memory_order_relaxed); + } } - return CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), - Nan::New(node->GetScriptId()), - Nan::New(node->GetLineNumber()), - Nan::New(node->GetColumnNumber()), - Nan::New(node->GetHitCount()), - children); -} + static void Restore() { + if (installed_) { + sigaction(SIGPROF, &old_handler_, nullptr); + installed_ = false; + old_handler_func_.store(nullptr, std::memory_order_relaxed); + } + } -Local TranslateTimeProfile(const CpuProfile* profile, - bool includeLineInfo) { - Local js_profile = Nan::New(); - Nan::Set(js_profile, - Nan::New("title").ToLocalChecked(), - profile->GetTitle()); - -#if NODE_MODULE_VERSION > NODE_11_0_MODULE_VERSION - if (includeLineInfo) { - Nan::Set(js_profile, - Nan::New("topDownRoot").ToLocalChecked(), - TranslateLineNumbersTimeProfileRoot(profile->GetTopDownRoot())); - } else { - Nan::Set(js_profile, - Nan::New("topDownRoot").ToLocalChecked(), - TranslateTimeProfileNode(profile->GetTopDownRoot())); + static void HandleProfilerSignal(int signal, siginfo_t* info, void* context); + + // Protects the process wide state below. + static std::mutex mutex_; + static int use_count_; + static bool installed_; + static struct sigaction old_handler_; + using HandlerFunc = void (*)(int, siginfo_t*, void*); + static std::atomic old_handler_func_; +}; + +std::mutex SignalHandler::mutex_; +int SignalHandler::use_count_ = 0; +struct sigaction SignalHandler::old_handler_; +bool SignalHandler::installed_ = false; +std::atomic SignalHandler::old_handler_func_; + +void SignalHandler::HandleProfilerSignal(int sig, + siginfo_t* info, + void* context) { + auto old_handler = old_handler_func_.load(std::memory_order_relaxed); + + if (!old_handler) { + return; + } + + auto isolate = Isolate::GetCurrent(); + WallProfiler* prof = g_profilers.GetProfiler(isolate); + + if (!prof) { + // no profiler found for current isolate, just pass the signal to old + // handler + old_handler(sig, info, context); + return; + } + + // Check if sampling is allowed + if (!prof->collectSampleAllowed()) { + return; } + + auto time_from = Now(); + old_handler(sig, info, context); + auto time_to = Now(); + prof->PushContext(time_from, time_to); +} #else - Nan::Set(js_profile, - Nan::New("topDownRoot").ToLocalChecked(), - TranslateTimeProfileNode(profile->GetTopDownRoot())); +class SignalHandler { + public: + static void IncreaseUseCount() {} + static void DecreaseUseCount() {} +}; #endif - Nan::Set(js_profile, - Nan::New("startTime").ToLocalChecked(), - Nan::New(profile->GetStartTime())); - Nan::Set(js_profile, - Nan::New("endTime").ToLocalChecked(), - Nan::New(profile->GetEndTime())); - return js_profile; -} +} // namespace -WallProfiler::WallProfiler(int interval) : samplingInterval(interval) {} +LabelSetsByNode WallProfiler::GetLabelSetsByNode(CpuProfile* profile, + ContextBuffer& contexts) { + LabelSetsByNode labelSetsByNode; -WallProfiler::~WallProfiler() { - Dispose(); -} + auto sampleCount = profile->GetSamplesCount(); + if (contexts.empty() || sampleCount == 0) { + return labelSetsByNode; + } + + auto isolate = Isolate::GetCurrent(); + // auto labelKey = Nan::New("label").ToLocalChecked(); + + auto contextIt = contexts.begin(); + + // deltaIdx is the offset of the sample to process compared to current + // iteration index + int deltaIdx = 0; + + // skip first sample because it's the one taken on profiler start, outside of + // signal handler + for (int i = 1; i < sampleCount; i++) { + // Handle out-of-order samples, hypothesis is that at most 2 consecutive + // samples can be out-of-order + if (deltaIdx == 1) { + // previous iteration was processing next sample, so this one should + // process previous sample + deltaIdx = -1; + } else if (deltaIdx == -1) { + // previous iteration was processing previous sample, returns to normal + // index + deltaIdx = 0; + } else if (i < sampleCount - 1 && profile->GetSampleTimestamp(i + 1) < + profile->GetSampleTimestamp(i)) { + // detected out-of-order sample, process next sample + deltaIdx = 1; + } -void WallProfiler::Dispose() { - if (cpuProfiler != nullptr) { - cpuProfiler->Dispose(); - cpuProfiler = nullptr; + auto sampleIdx = i + deltaIdx; + auto sample = profile->GetSample(sampleIdx); + + auto sampleTimestamp = profile->GetSampleTimestamp(sampleIdx); + + // This loop will drop all contexts that are too old to be associated with + // the current sample; association is done by matching each sample with + // context whose [time_from,time_to] interval encompasses sample timestamp. + while (contextIt != contexts.end()) { + auto& sampleContext = *contextIt; + if (sampleContext.time_to < sampleTimestamp) { + // Current sample context is too old, discard it and fetch the next one. + ++contextIt; + } else if (sampleContext.time_from > sampleTimestamp) { + // Current sample context is too recent, we'll try to match it to the + // next sample. + break; + } else { + // This sample context is the closest to this sample. + auto it = labelSetsByNode.find(sample); + Local array; + if (it == labelSetsByNode.end()) { + array = Nan::New(); + assert(labelSetsByNode.find(sample) == labelSetsByNode.end()); + labelSetsByNode[sample] = {array, 1}; + } else { + array = it->second.labelSets; + ++it->second.hitcount; + } + if (sampleContext.labels) { + Nan::Set( + array, array->Length(), sampleContext.labels.get()->Get(isolate)); + } + + // Sample context was consumed, fetch the next one + ++contextIt; + break; // don't match more than one context to one sample + } + } } + + return labelSetsByNode; } -NAN_METHOD(WallProfiler::Dispose) { - WallProfiler* wallProfiler = - Nan::ObjectWrap::Unwrap(info.Holder()); +WallProfiler::WallProfiler(int samplingPeriodMicros, + int durationMicros, + bool includeLines, + bool withLabels) + : samplingPeriodMicros_(samplingPeriodMicros), + includeLines_(includeLines), + withLabels_(withLabels) { + contexts_.reserve(durationMicros * 2 / samplingPeriodMicros); + curLabels_.store(&labels1_, std::memory_order_relaxed); + collectSamples_.store(false, std::memory_order_relaxed); +} - wallProfiler->Dispose(); +WallProfiler::~WallProfiler() { + Dispose(nullptr); +} + +void WallProfiler::Dispose(Isolate* isolate) { + if (cpuProfiler_ != nullptr) { + cpuProfiler_->Dispose(); + cpuProfiler_ = nullptr; + + g_profilers.RemoveProfiler(isolate, this); + } } NAN_METHOD(WallProfiler::New) { - if (info.Length() != 1) { - return Nan::ThrowTypeError("WallProfiler must have one argument."); + if (info.Length() != 4) { + return Nan::ThrowTypeError("WallProfiler must have four arguments."); } + if (!info[0]->IsNumber()) { - return Nan::ThrowTypeError("Sample rate must be a number."); + return Nan::ThrowTypeError("Sample period must be a number."); + } + if (!info[1]->IsNumber()) { + return Nan::ThrowTypeError("Duration must be a number."); + } + if (!info[2]->IsBoolean()) { + return Nan::ThrowTypeError("includeLines must be a boolean."); + } + if (!info[3]->IsBoolean()) { + return Nan::ThrowTypeError("withLabels must be a boolean."); } if (info.IsConstructCall()) { - int interval = Nan::MaybeLocal(info[0].As()) - .ToLocalChecked() - ->Value(); + int interval = info[0].As()->Value(); + int duration = info[1].As()->Value(); + + if (interval <= 0) { + return Nan::ThrowTypeError("Sample rate must be positive."); + } + if (duration <= 0) { + return Nan::ThrowTypeError("Duration must be positive."); + } + if (duration < interval) { + return Nan::ThrowTypeError("Duration must not be less than sample rate."); + } + + bool includeLines = info[2].As()->Value(); + bool withLabels = info[3].As()->Value(); + +#ifndef DD_WALL_USE_SIGPROF + if (withLabels) { + return Nan::ThrowTypeError("Labels are not supported."); + } +#endif + + if (includeLines && withLabels) { + // Currently custom labels are not compatible with caller line + // information, because it's not possible to associate labels with line + // ticks: + // labels are associated to sample which itself is associated with + // a CpuProfileNode, but this node has several line ticks, and we cannot + // determine labels <-> line ticks association. Note that line number is + // present in v8 internal sample struct and would allow mapping sample to + // line tick, and thus labels to line tick, but this information is not + // available in v8 public API. + // More over in caller line number mode, line number of a CpuProfileNode + // is not the line of the current function, but the line number where this + // function is called, therefore we don't access either to the line of the + // function (otherwise we could ignoree line ticks and replace them with + // single hitcount for the function). + return Nan::ThrowTypeError( + "Include line option is not compatible with labels."); + } - WallProfiler* obj = new WallProfiler(interval); + WallProfiler* obj = + new WallProfiler(interval, duration, includeLines, withLabels); obj->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } else { - const int argc = 1; - v8::Local argv[argc] = {info[0]}; + const int argc = 4; + v8::Local argv[argc] = {info[0], info[1], info[2], info[3]}; v8::Local cons = Nan::New( PerIsolateData::For(info.GetIsolate())->WallProfilerConstructor()); info.GetReturnValue().Set( @@ -243,62 +428,167 @@ NAN_METHOD(WallProfiler::Start) { WallProfiler* wallProfiler = Nan::ObjectWrap::Unwrap(info.Holder()); - if (info.Length() != 2) { - return Nan::ThrowTypeError("Start must have two arguments."); + if (info.Length() != 0) { + return Nan::ThrowTypeError("Start must not have any arguments."); } - if (!info[0]->IsString()) { - return Nan::ThrowTypeError("Profile name must be a string."); + + auto res = wallProfiler->StartImpl(); + if (!res.success) { + return Nan::ThrowTypeError(res.msg.c_str()); } - if (!info[1]->IsBoolean()) { - return Nan::ThrowTypeError("Include lines must be a boolean."); +} + +Result WallProfiler::StartImpl() { + if (started_) { + return Result{"Start called on already started profiler, stop it first."}; } - Local name = - Nan::MaybeLocal(info[0].As()).ToLocalChecked(); + profileIdx_ = 0; - bool includeLines = - Nan::MaybeLocal(info[1].As()).ToLocalChecked()->Value(); + if (!CreateV8CpuProfiler()) { + return Result{"Cannot start profiler: another profiler is already active."}; + } - auto profiler = wallProfiler->GetProfiler(); + profileId_ = StartInternal(); - // Sample counts and timestamps are not used, so we do not need to record - // samples. - const bool recordSamples = false; + collectSamples_.store(true, std::memory_order_relaxed); + started_ = true; + return {}; +} - if (includeLines) { - profiler->StartProfiling( - name, CpuProfilingMode::kCallerLineNumbers, recordSamples); - } else { - profiler->StartProfiling(name, recordSamples); +std::string WallProfiler::StartInternal() { + char buf[128]; + snprintf(buf, sizeof(buf), "pprof-%" PRId64, profileIdx_++); + v8::Local title = Nan::New(buf).ToLocalChecked(); + cpuProfiler_->StartProfiling(title, + includeLines_ + ? CpuProfilingMode::kCallerLineNumbers + : CpuProfilingMode::kLeafNodeLineNumbers, + withLabels_); + + // reinstall sighandler on each new upload period + if (withLabels_) { + SignalHandler::IncreaseUseCount(); } + + return buf; } NAN_METHOD(WallProfiler::Stop) { + if (info.Length() != 1) { + return Nan::ThrowTypeError("Stop must have one argument."); + } + if (!info[0]->IsBoolean()) { + return Nan::ThrowTypeError("Restart must be a boolean."); + } + + bool restart = info[0].As()->Value(); + WallProfiler* wallProfiler = Nan::ObjectWrap::Unwrap(info.Holder()); - if (info.Length() != 2) { - return Nan::ThrowTypeError("Start must have two arguments."); + v8::Local profile; +#if NODE_MODULE_VERSION < NODE_16_0_MODULE_VERSION + auto err = wallProfiler->StopImplOld(restart, profile); +#else + auto err = wallProfiler->StopImpl(restart, profile); +#endif + + if (!err.success) { + return Nan::ThrowTypeError(err.msg.c_str()); } - if (!info[0]->IsString()) { - return Nan::ThrowTypeError("Profile name must be a string."); + info.GetReturnValue().Set(profile); +} + +Result WallProfiler::StopImpl(bool restart, v8::Local& profile) { + if (!started_) { + return Result{"Stop called on not started profiler."}; + } + + auto oldProfileId = profileId_; + if (withLabels_) { + collectSamples_.store(false, std::memory_order_relaxed); + std::atomic_signal_fence(std::memory_order_release); + + // make sure timestamp changes to avoid having samples from previous profile + auto now = Now(); + while (Now() == now) { + } + } + + if (restart) { + profileId_ = StartInternal(); + } + + if (withLabels_) { + SignalHandler::DecreaseUseCount(); } - if (!info[1]->IsBoolean()) { - return Nan::ThrowTypeError("Include lines must be a boolean."); + auto v8_profile = cpuProfiler_->StopProfiling( + Nan::New(oldProfileId).ToLocalChecked()); + + ContextBuffer contexts; + if (withLabels_) { + contexts.reserve(contexts_.capacity()); + std::swap(contexts, contexts_); } - Local name = - Nan::MaybeLocal(info[0].As()).ToLocalChecked(); + if (restart && withLabels_) { + // make sure timestamp changes to avoid mixing sample taken upon start and a + // sample from signal handler + auto now = Now(); + while (Now() == now) { + } + collectSamples_.store(true, std::memory_order_relaxed); + std::atomic_signal_fence(std::memory_order_release); + } - bool includeLines = - Nan::MaybeLocal(info[1].As()).ToLocalChecked()->Value(); + if (withLabels_) { + auto labelSetsByNode = GetLabelSetsByNode(v8_profile, contexts); + profile = TranslateTimeProfile(v8_profile, includeLines_, &labelSetsByNode); - auto profiler = wallProfiler->GetProfiler(); - auto v8_profile = profiler->StopProfiling(name); - Local profile = TranslateTimeProfile(v8_profile, includeLines); + } else { + profile = TranslateTimeProfile(v8_profile, includeLines_); + } v8_profile->Delete(); - info.GetReturnValue().Set(profile); + if (!restart) { + Dispose(v8::Isolate::GetCurrent()); + } + started_ = restart; + + return {}; +} + +Result WallProfiler::StopImplOld(bool restart, v8::Local& profile) { + if (!started_) { + return Result{"Stop called on not started profiler."}; + } + + if (withLabels_) { + SignalHandler::DecreaseUseCount(); + } + auto v8_profile = cpuProfiler_->StopProfiling( + Nan::New(profileId_).ToLocalChecked()); + + if (withLabels_) { + auto labelSetsByNode = GetLabelSetsByNode(v8_profile, contexts_); + profile = TranslateTimeProfile(v8_profile, includeLines_, &labelSetsByNode); + + } else { + profile = TranslateTimeProfile(v8_profile, includeLines_); + } + contexts_.clear(); + v8_profile->Delete(); + Dispose(v8::Isolate::GetCurrent()); + + if (restart) { + CreateV8CpuProfiler(); + profileId_ = StartInternal(); + } else { + started_ = false; + } + + return {}; } NAN_MODULE_INIT(WallProfiler::Init) { @@ -307,8 +597,12 @@ NAN_MODULE_INIT(WallProfiler::Init) { tpl->SetClassName(className); tpl->InstanceTemplate()->SetInternalFieldCount(1); + Nan::SetAccessor(tpl->InstanceTemplate(), + Nan::New("labels").ToLocalChecked(), + GetLabels, + SetLabels); + Nan::SetPrototypeMethod(tpl, "start", Start); - Nan::SetPrototypeMethod(tpl, "dispose", Dispose); Nan::SetPrototypeMethod(tpl, "stop", Stop); PerIsolateData::For(Isolate::GetCurrent()) @@ -320,13 +614,66 @@ NAN_MODULE_INIT(WallProfiler::Init) { // A new CPU profiler object will be created each time profiling is started // to work around https://bugs.chromium.org/p/v8/issues/detail?id=11051. // TODO: Fixed in v16. Delete this hack when deprecating v14. -v8::CpuProfiler* WallProfiler::GetProfiler() { - if (cpuProfiler == nullptr) { +v8::CpuProfiler* WallProfiler::CreateV8CpuProfiler() { + if (cpuProfiler_ == nullptr) { v8::Isolate* isolate = v8::Isolate::GetCurrent(); - cpuProfiler = v8::CpuProfiler::New(isolate); - cpuProfiler->SetSamplingInterval(samplingInterval); + + bool inserted = g_profilers.AddProfiler(isolate, this); + + if (!inserted) { + // refuse to create a new profiler if one is already active + return nullptr; + } + cpuProfiler_ = v8::CpuProfiler::New(isolate); + cpuProfiler_->SetSamplingInterval(samplingPeriodMicros_); + } + return cpuProfiler_; +} + +v8::Local WallProfiler::GetLabels(Isolate* isolate) { + auto labels = *curLabels_.load(std::memory_order_relaxed); + if (!labels) return v8::Undefined(isolate); + return labels->Get(isolate); +} + +void WallProfiler::SetLabels(Isolate* isolate, Local value) { + // Need to be careful here, because we might be interrupted by a + // signal handler that will make use of curLabels_. + // Update of shared_ptr is not atomic, so instead we use a pointer + // (curLabels_) that points on two shared_ptr (labels1_ and labels2_), update + // the shared_ptr that is not currently in use and then atomically update + // curLabels_. + auto newCurLabels = curLabels_.load(std::memory_order_relaxed) == &labels1_ + ? &labels2_ + : &labels1_; + if (value->BooleanValue(isolate)) { + *newCurLabels = std::make_shared>(isolate, value); + } else { + newCurLabels->reset(); + } + std::atomic_signal_fence(std::memory_order_release); + curLabels_.store(newCurLabels, std::memory_order_relaxed); +} + +NAN_GETTER(WallProfiler::GetLabels) { + auto profiler = Nan::ObjectWrap::Unwrap(info.Holder()); + info.GetReturnValue().Set(profiler->GetLabels(info.GetIsolate())); +} + +NAN_SETTER(WallProfiler::SetLabels) { + auto profiler = Nan::ObjectWrap::Unwrap(info.Holder()); + profiler->SetLabels(info.GetIsolate(), value); +} + +void WallProfiler::PushContext(int64_t time_from, int64_t time_to) { + // 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 labels = curLabels_.load(std::memory_order_relaxed); + std::atomic_signal_fence(std::memory_order_acquire); + if (contexts_.size() < contexts_.capacity()) { + contexts_.push_back({*labels, time_from, time_to}); } - return cpuProfiler; } } // namespace dd diff --git a/bindings/profilers/wall.hh b/bindings/profilers/wall.hh index b79249f6..1ff9f807 100644 --- a/bindings/profilers/wall.hh +++ b/bindings/profilers/wall.hh @@ -1,29 +1,114 @@ +/* + * Copyright 2023 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 "labelsets.hh" + #include #include +#include +#include +#include namespace dd { +struct Result { + Result() = default; + explicit Result(const char* msg) : success{false}, msg{msg} {}; + + bool success = true; + std::string msg; +}; + class WallProfiler : public Nan::ObjectWrap { private: - int samplingInterval = 0; - v8::CpuProfiler* cpuProfiler = nullptr; + using ValuePtr = std::shared_ptr>; + + int samplingPeriodMicros_ = 0; + v8::CpuProfiler* cpuProfiler_ = nullptr; + // TODO: Investigate use of v8::Persistent instead of shared_ptr to + // avoid heap allocation. Need to figure out the right move/copy semantics in + // and out of the ring buffer. + + // We're using a pair of shared pointers and an atomic pointer-to-current as + // a way to ensure signal safety on update. + ValuePtr labels1_; + ValuePtr labels2_; + std::atomic curLabels_; + std::atomic collectSamples_; + std::string profileId_; + int64_t profileIdx_ = 0; + bool includeLines_ = false; + bool withLabels_ = false; + bool started_ = false; + + struct SampleContext { + ValuePtr labels; + int64_t time_from; + int64_t time_to; + }; + + using ContextBuffer = std::vector; + ContextBuffer contexts_; + ~WallProfiler(); - void Dispose(); + void Dispose(v8::Isolate* isolate); // A new CPU profiler object will be created each time profiling is started // to work around https://bugs.chromium.org/p/v8/issues/detail?id=11051. - v8::CpuProfiler* GetProfiler(); + v8::CpuProfiler* CreateV8CpuProfiler(); + + LabelSetsByNode GetLabelSetsByNode(v8::CpuProfile* profile, + ContextBuffer& contexts); public: - explicit WallProfiler(int interval); + /** + * @param samplingPeriodMicros sampling interval, in microseconds + * @param durationMicros the duration of sampling, in microseconds. This + * parameter is informative; it is up to the caller to call the Stop method + * every period. The parameter is used to preallocate data structures that + * should not be reallocated in async signal safe code. + */ + explicit WallProfiler(int samplingPeriodMicros, + int durationMicros, + bool includeLines, + bool withLabels); + + v8::Local GetLabels(v8::Isolate*); + void SetLabels(v8::Isolate*, v8::Local); + + void PushContext(int64_t time_from, int64_t time_to); + Result StartImpl(); + std::string StartInternal(); + Result StopImpl(bool restart, v8::Local& profile); + Result StopImplOld(bool restart, v8::Local& profile); + + bool collectSampleAllowed() const { + bool res = collectSamples_.load(std::memory_order_relaxed); + std::atomic_signal_fence(std::memory_order_acquire); + return res; + } static NAN_METHOD(New); - static NAN_METHOD(Dispose); static NAN_METHOD(Start); static NAN_METHOD(Stop); static NAN_MODULE_INIT(Init); + static NAN_GETTER(GetLabels); + static NAN_SETTER(SetLabels); }; } // namespace dd diff --git a/bindings/sample.cc b/bindings/sample.cc index 3d10ad04..57fb313b 100644 --- a/bindings/sample.cc +++ b/bindings/sample.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include #include diff --git a/bindings/sample.hh b/bindings/sample.hh index 47b15964..5ea84b70 100644 --- a/bindings/sample.hh +++ b/bindings/sample.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 diff --git a/bindings/test/binding.cc b/bindings/test/binding.cc index 1ed3854a..fa819de2 100644 --- a/bindings/test/binding.cc +++ b/bindings/test/binding.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include #include diff --git a/bindings/test/code-event-record.test.cc b/bindings/test/code-event-record.test.cc index b317425f..5776fe09 100644 --- a/bindings/test/code-event-record.test.cc +++ b/bindings/test/code-event-record.test.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include diff --git a/bindings/test/code-event-record.test.hh b/bindings/test/code-event-record.test.hh index 9cb3e7d9..12b6c0f7 100644 --- a/bindings/test/code-event-record.test.hh +++ b/bindings/test/code-event-record.test.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "tap.h" diff --git a/bindings/test/code-map.test.cc b/bindings/test/code-map.test.cc index 0cadcefa..bf9959ad 100644 --- a/bindings/test/code-map.test.cc +++ b/bindings/test/code-map.test.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "code-map.test.hh" #include "../code-map.hh" diff --git a/bindings/test/code-map.test.hh b/bindings/test/code-map.test.hh index 9ed7d5ee..eaa2592e 100644 --- a/bindings/test/code-map.test.hh +++ b/bindings/test/code-map.test.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "tap.h" diff --git a/bindings/test/cpu-time.test.cc b/bindings/test/cpu-time.test.cc index 0867c0e5..769e3813 100644 --- a/bindings/test/cpu-time.test.cc +++ b/bindings/test/cpu-time.test.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "cpu-time.test.hh" #include "../cpu-time.hh" diff --git a/bindings/test/cpu-time.test.hh b/bindings/test/cpu-time.test.hh index eb9403c9..df1ae55c 100644 --- a/bindings/test/cpu-time.test.hh +++ b/bindings/test/cpu-time.test.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "tap.h" diff --git a/bindings/test/location.test.cc b/bindings/test/location.test.cc index 7c4ad5bd..ae407d8b 100644 --- a/bindings/test/location.test.cc +++ b/bindings/test/location.test.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "location.test.hh" #include "../location.hh" diff --git a/bindings/test/location.test.hh b/bindings/test/location.test.hh index 0a591885..a38b4b53 100644 --- a/bindings/test/location.test.hh +++ b/bindings/test/location.test.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "tap.h" diff --git a/bindings/test/profilers/cpu.test.cc b/bindings/test/profilers/cpu.test.cc index 24cf68da..f4156358 100644 --- a/bindings/test/profilers/cpu.test.cc +++ b/bindings/test/profilers/cpu.test.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "cpu.test.hh" #include "../../location.hh" #include "../../profilers/cpu.hh" diff --git a/bindings/test/profilers/cpu.test.hh b/bindings/test/profilers/cpu.test.hh index 125ccab2..2a7e67ee 100644 --- a/bindings/test/profilers/cpu.test.hh +++ b/bindings/test/profilers/cpu.test.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "../tap.h" diff --git a/bindings/test/sample.test.cc b/bindings/test/sample.test.cc index 06e57f50..bceefe87 100644 --- a/bindings/test/sample.test.cc +++ b/bindings/test/sample.test.cc @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 #include "../sample.hh" diff --git a/bindings/test/sample.test.hh b/bindings/test/sample.test.hh index de1cfccb..b7d4267e 100644 --- a/bindings/test/sample.test.hh +++ b/bindings/test/sample.test.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 "tap.h" diff --git a/bindings/test/tap.h b/bindings/test/tap.h index 25d19e63..a6b20377 100644 --- a/bindings/test/tap.h +++ b/bindings/test/tap.h @@ -1,11 +1,17 @@ -/** - * @file tap.h - * @author Stephen Belanger - * @date Apr 13, 2022 - * @brief C and C++ API for TAP testing +/* + * Copyright 2023 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 * - * @todo TODO directives - * @todo YAML blocks? + * 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. */ #ifndef _INCLUDE_TAP_H_ diff --git a/bindings/translate-time-profile.cc b/bindings/translate-time-profile.cc new file mode 100644 index 00000000..c3ec3a7e --- /dev/null +++ b/bindings/translate-time-profile.cc @@ -0,0 +1,252 @@ +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * + * 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-time-profile.hh" + +#include + +namespace dd { + +namespace { +class ProfileTranslator { + private: + LabelSetsByNode* labelSetsByNode; + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Local emptyArray = NewArray(0); + v8::Local zero = NewInteger(0); + +#define FIELDS \ + X(name) \ + X(scriptName) \ + X(scriptId) \ + X(lineNumber) \ + X(columnNumber) \ + X(hitCount) \ + X(children) \ + X(labelSets) + +#define X(name) v8::Local str_##name = NewString(#name); + FIELDS +#undef X + + v8::Local getLabelSetsForNode(const v8::CpuProfileNode* node, + uint32_t& hitcount) { + hitcount = node->GetHitCount(); + if (!labelSetsByNode) { + // custom labels are not enabled, keep the node hitcount and return empty + // array + return emptyArray; + } + + auto it = labelSetsByNode->find(node); + auto labelSets = emptyArray; + if (it != labelSetsByNode->end()) { + hitcount = it->second.hitcount; + labelSets = it->second.labelSets; + } else { + // no context found for node, discard it since every sample taken from + // signal handler should have a matching context if it does not, it means + // sample was captured by a deopt event + hitcount = 0; + } + return labelSets; + } + + v8::Local CreateTimeNode(v8::Local name, + v8::Local scriptName, + v8::Local scriptId, + v8::Local lineNumber, + v8::Local columnNumber, + v8::Local hitCount, + v8::Local children, + v8::Local labelSets) { + v8::Local js_node = Nan::New(); +#define X(name) Nan::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) { + unsigned int index = 0; + v8::Local children; + int32_t count = node->GetChildrenCount(); + + unsigned int hitLineCount = node->GetHitLineCount(); + unsigned int hitCount = node->GetHitCount(); + auto scriptId = NewInteger(node->GetScriptId()); + if (hitLineCount > 0) { + std::vector entries(hitLineCount); + 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)); + } + } 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)); + } else { + children = NewArray(count); + } + + for (int32_t i = 0; i < count; i++) { + Nan::Set(children, + index++, + TranslateLineNumbersTimeProfileNode(node, node->GetChild(i))); + }; + + return children; + } + + v8::Local TranslateLineNumbersTimeProfileNode( + const v8::CpuProfileNode* parent, const v8::CpuProfileNode* node) { + return CreateTimeNode(parent->GetFunctionName(), + parent->GetScriptResourceName(), + NewInteger(parent->GetScriptId()), + NewInteger(node->GetLineNumber()), + NewInteger(node->GetColumnNumber()), + zero, + GetLineNumberTimeProfileChildren(node), + emptyArray); + } + + // In profiles with line level accurate line numbers, a node's line number + // and column number refer to the line/column from which the function was + // called. + v8::Local TranslateLineNumbersTimeProfileRoot( + const v8::CpuProfileNode* node) { + int32_t count = node->GetChildrenCount(); + std::vector> childrenArrs(count); + int32_t childCount = 0; + for (int32_t i = 0; i < count; i++) { + v8::Local c = + GetLineNumberTimeProfileChildren(node->GetChild(i)); + childCount = childCount + c->Length(); + childrenArrs[i] = c; + } + + v8::Local children = NewArray(childCount); + int32_t idx = 0; + 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()); + idx++; + } + } + + return CreateTimeNode(node->GetFunctionName(), + node->GetScriptResourceName(), + NewInteger(node->GetScriptId()), + NewInteger(node->GetLineNumber()), + NewInteger(node->GetColumnNumber()), + zero, + children, + emptyArray); + } + + v8::Local TranslateTimeProfileNode( + const v8::CpuProfileNode* node) { + int32_t count = node->GetChildrenCount(); + v8::Local children = Nan::New(count); + for (int32_t i = 0; i < count; i++) { + Nan::Set(children, i, TranslateTimeProfileNode(node->GetChild(i))); + } + + uint32_t hitcount = 0; + auto labels = getLabelSetsForNode(node, hitcount); + + return CreateTimeNode(node->GetFunctionName(), + node->GetScriptResourceName(), + NewInteger(node->GetScriptId()), + NewInteger(node->GetLineNumber()), + NewInteger(node->GetColumnNumber()), + NewInteger(hitcount), + children, + labels); + } + + public: + explicit ProfileTranslator(LabelSetsByNode* nls = nullptr) + : labelSetsByNode(nls) {} + + v8::Local TranslateTimeProfile(const v8::CpuProfile* profile, + bool includeLineInfo) { + v8::Local js_profile = Nan::New(); + + if (includeLineInfo) { + Nan::Set(js_profile, + NewString("topDownRoot"), + TranslateLineNumbersTimeProfileRoot(profile->GetTopDownRoot())); + } else { + Nan::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())); + + return js_profile; + } +}; +} // namespace + +v8::Local TranslateTimeProfile(const v8::CpuProfile* profile, + bool includeLineInfo, + LabelSetsByNode* labelSetsByNode) { + return ProfileTranslator(labelSetsByNode) + .TranslateTimeProfile(profile, includeLineInfo); +} + +} // namespace dd \ No newline at end of file diff --git a/bindings/translate-time-profile.hh b/bindings/translate-time-profile.hh new file mode 100644 index 00000000..ed96e41a --- /dev/null +++ b/bindings/translate-time-profile.hh @@ -0,0 +1,31 @@ +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * + * 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 "labelsets.hh" + +#include +#include + +namespace dd { + +v8::Local TranslateTimeProfile( + const v8::CpuProfile* profile, + bool includeLineInfo, + LabelSetsByNode* labelSetsByNode = nullptr); + +} // namespace dd diff --git a/bindings/wrap.hh b/bindings/wrap.hh index cbd65c32..0da587ac 100644 --- a/bindings/wrap.hh +++ b/bindings/wrap.hh @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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 // cppcheck-suppress missingIncludeSystem diff --git a/package.json b/package.json index 6856b442..f43f1540 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "scripts": { "install": "exit 0", "rebuild": "node-gyp rebuild --jobs=max", - "test:js": "nyc mocha out/test/test-*.js", + "test:js": "nyc mocha -r source-map-support/register out/test/test-*.js", "test:cpp": "node scripts/cctest.js", + "test:wall": "nyc mocha -r source-map-support/register out/test/test-time-profiler.js", "test": "npm run test:js && npm run test:cpp", "codecov": "nyc report --reporter=json && codecov -f coverage/*.json", "compile": "tsc -p .", diff --git a/ts/src/index.ts b/ts/src/index.ts index 0e50f994..89cdfb04 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -30,6 +30,9 @@ export const CpuProfiler = cpuProfiler; export const time = { profile: timeProfiler.profile, start: timeProfiler.start, + stop: timeProfiler.stop, + setLabels: timeProfiler.setLabels, + isStarted: timeProfiler.isStarted, }; export const heap = { @@ -44,11 +47,11 @@ export const heap = { // If loaded with --require, start profiling. if (module.parent && module.parent.id === 'internal/preload') { - const stop = time.start(); + time.start({}); process.on('exit', () => { // The process is going to terminate imminently. All work here needs to // be synchronous. - const profile = stop(); + const profile = time.stop(); const buffer = encodeSync(profile); writeFileSync(`pprof-profile-${process.pid}.pb.gz`, buffer); }); diff --git a/ts/src/profile-serializer.ts b/ts/src/profile-serializer.ts index 749897b4..d177be88 100644 --- a/ts/src/profile-serializer.ts +++ b/ts/src/profile-serializer.ts @@ -299,10 +299,22 @@ export function serializeTimeProfile( entry: Entry, samples: Sample[] ) => { - if (entry.node.hitCount > 0) { + let unlabelledHits = entry.node.hitCount; + for (const labelSet of entry.node.labelSets || []) { + if (Object.keys(labelSet).length > 0) { + const sample = new Sample({ + locationId: entry.stack, + value: [1, intervalNanos], + label: buildLabels(labelSet, stringTable), + }); + samples.push(sample); + unlabelledHits--; + } + } + if (unlabelledHits > 0) { const sample = new Sample({ locationId: entry.stack, - value: [entry.node.hitCount, entry.node.hitCount * intervalNanos], + value: [unlabelledHits, unlabelledHits * intervalNanos], }); samples.push(sample); } diff --git a/ts/src/time-profiler.ts b/ts/src/time-profiler.ts index b129f9aa..84356c35 100644 --- a/ts/src/time-profiler.ts +++ b/ts/src/time-profiler.ts @@ -19,21 +19,30 @@ import delay from 'delay'; import {serializeTimeProfile} from './profile-serializer'; import {SourceMapper} from './sourcemapper/sourcemapper'; import {TimeProfiler} from './time-profiler-bindings'; +import {LabelSet} from './v8-types'; const DEFAULT_INTERVAL_MICROS: Microseconds = 1000; - -const majorVersion = process.version.slice(1).split('.').map(Number)[0]; +const DEFAULT_DURATION_MILLIS: Milliseconds = 60000; type Microseconds = number; type Milliseconds = number; +let gProfiler: InstanceType | undefined; +let gSourceMapper: SourceMapper | undefined; +let gIntervalMicros: Microseconds; + +/** Make sure to stop profiler before node shuts down, otherwise profiling + * signal might cause a crash if it occurs during shutdown */ +process.once('exit', () => { + if (isStarted()) stop(); +}); + export interface TimeProfilerOptions { /** time in milliseconds for which to collect profile. */ - durationMillis: Milliseconds; + durationMillis?: Milliseconds; /** average time in microseconds between samples */ intervalMicros?: Microseconds; sourceMapper?: SourceMapper; - name?: string; /** * This configuration option is experimental. @@ -42,65 +51,76 @@ export interface TimeProfilerOptions { * This defaults to false. */ lineNumbers?: boolean; + customLabels?: boolean; } -export async function profile(options: TimeProfilerOptions) { - const stop = start( - options.intervalMicros || DEFAULT_INTERVAL_MICROS, - options.name, - options.sourceMapper, - options.lineNumbers - ); - await delay(options.durationMillis); +export async function profile({ + intervalMicros = DEFAULT_INTERVAL_MICROS, + durationMillis = DEFAULT_DURATION_MILLIS, + sourceMapper, + lineNumbers = false, + customLabels = false, +}: TimeProfilerOptions) { + start({ + intervalMicros, + durationMillis, + sourceMapper, + lineNumbers, + customLabels, + }); + await delay(durationMillis); return stop(); } -function ensureRunName(name?: string) { - return name || `pprof-${Date.now()}-${Math.random()}`; -} +// Temporarily retained for backwards compatibility with older tracer +export function start({ + intervalMicros = DEFAULT_INTERVAL_MICROS, + durationMillis = DEFAULT_DURATION_MILLIS, + sourceMapper, + lineNumbers = false, + customLabels = false, +}: TimeProfilerOptions) { + if (gProfiler) { + throw new Error('Wall profiler is already started'); + } -// NOTE: refreshing doesn't work if giving a profile name. -export function start( - intervalMicros: Microseconds = DEFAULT_INTERVAL_MICROS, - name?: string, - sourceMapper?: SourceMapper, - lineNumbers = true -) { - const profiler = new TimeProfiler(intervalMicros); - let runName = start(); - return majorVersion < 16 ? stopOld : stop; + gProfiler = new TimeProfiler( + intervalMicros, + durationMillis * 1000, + lineNumbers, + customLabels + ); + gProfiler.start(); + gSourceMapper = sourceMapper; + gIntervalMicros = intervalMicros; +} - function start() { - const runName = ensureRunName(name); - profiler.start(runName, lineNumbers); - return runName; +export function stop(restart = false) { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); } - // Node.js versions prior to v16 leak memory if not disposed and recreated - // between each profile. As disposing deletes current profile data too, - // we must stop then dispose then start. - function stopOld(restart = false) { - const result = profiler.stop(runName, lineNumbers); - profiler.dispose(); - if (restart) { - runName = start(); - } - return serializeTimeProfile(result, intervalMicros, sourceMapper, true); + const profile = gProfiler.stop(restart); + const serialized_profile = serializeTimeProfile( + profile, + gIntervalMicros, + gSourceMapper, + true + ); + if (!restart) { + gProfiler = undefined; + gSourceMapper = undefined; } + return serialized_profile; +} - // For Node.js v16+, we want to start the next profile before we stop the - // current one as otherwise the active profile count could reach zero which - // means V8 might tear down the symbolizer thread and need to start it again. - function stop(restart = false) { - let nextRunName; - if (restart) { - nextRunName = start(); - } - const result = profiler.stop(runName, lineNumbers); - if (nextRunName) { - runName = nextRunName; - } - if (!restart) profiler.dispose(); - return serializeTimeProfile(result, intervalMicros, sourceMapper, true); +export function setLabels(labels?: LabelSet) { + if (!gProfiler) { + throw new Error('Wall profiler is not started'); } + gProfiler.labels = labels; +} + +export function isStarted() { + return !!gProfiler; } diff --git a/ts/src/v8-types.ts b/ts/src/v8-types.ts index 4eef4885..2763ca13 100644 --- a/ts/src/v8-types.ts +++ b/ts/src/v8-types.ts @@ -36,6 +36,7 @@ export interface ProfileNode { export interface TimeProfileNode extends ProfileNode { hitCount: number; + labelSets?: LabelSet[]; // TODO: use LabelsCpu later } export interface AllocationProfileNode extends ProfileNode { diff --git a/ts/test/test-cpu-profiler.ts b/ts/test/test-cpu-profiler.ts deleted file mode 100644 index 043ad872..00000000 --- a/ts/test/test-cpu-profiler.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * 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. - */ - -import CpuProfiler from '../src/cpu-profiler'; -import {Profile, ValueType} from 'pprof-format'; - -const assert = require('assert'); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function str(profile: Profile, index: any) { - return profile.stringTable.strings![index as number]; -} - -function verifyValueType(profile: Profile, valueType: ValueType, name: string) { - const type = str(profile, valueType.type!); - const unit = str(profile, valueType.unit!); - - assert.strictEqual( - `${type}/${unit}`, - name, - 'has expected type and unit for value type' - ); -} - -function verifySampleType(profile: Profile, index: number, name: string) { - const sampleType = profile.sampleType![index]; - verifyValueType(profile, sampleType, name); -} - -function verifyPeriodType(profile: Profile, name: string) { - verifyValueType(profile, profile.periodType!, name); -} - -function verifyFunction(profile: Profile, index: number) { - const fn = profile.function![index]; - assert.ok(fn, 'has function matching function id'); - assert.ok(fn.id! >= 0, 'has id for function'); - - assert.strictEqual( - typeof str(profile, fn.name), - 'string', - 'has name in string table for function' - ); - assert.strictEqual( - typeof str(profile, fn.systemName), - 'string', - 'has systemName in string table for function' - ); - assert.strictEqual( - typeof str(profile, fn.filename), - 'string', - 'has filename in string table for function' - ); -} - -function verifyLocation(profile: Profile, index: number) { - const location = profile.location![index]; - assert.ok(location, 'has location matching location id'); - assert.ok(location.id! > 0, 'has id for location'); - - for (const line of location.line!) { - assert.ok(line.line! >= 0, 'has line number for line record'); - verifyFunction(profile, (line.functionId! as number) - 1); - } -} - -function verifySample(profile: Profile, index: number) { - const sample = profile.sample![index]; - for (const locationId of sample.locationId!) { - verifyLocation(profile, (locationId as number) - 1); - } - assert.strictEqual( - sample.value!.length, - 2, - 'has expected number of values in sample' - ); -} - -function busyWait(ms: number) { - return new Promise(resolve => { - let done = false; - function work() { - if (done) return; - let sum = 0; - for (let i = 0; i < 1e6; i++) { - sum += sum; - } - setImmediate(work, sum); - } - setImmediate(work); - setTimeout(() => { - done = true; - resolve(undefined); - }, ms); - }); -} - -describe('CPU Profiler', () => { - describe('profile', () => { - it('should have valid basic structure', async () => { - const data = {str: 'foo', num: 123}; - const cpu = new CpuProfiler(); - cpu.labels = data; - cpu.start(99); - await busyWait(100); - const profile = cpu.profile()!; - cpu.stop(); - - verifySampleType(profile, 0, 'sample/count'); - verifySampleType(profile, 1, 'cpu/nanoseconds'); - // verifySampleType(profile, 2, 'wall/nanoseconds'); - verifyPeriodType(profile, 'cpu/nanoseconds'); - - assert.strictEqual(profile.period, 1e9 / 99); - assert.ok(profile.durationNanos! > 0); - assert.ok(profile.timeNanos! > 0); - - assert.ok(profile.sample!.length > 0); - assert.ok(profile.location!.length > 0); - assert.ok(profile.function!.length > 0); - - verifySample(profile, 0); - - const {label = []} = profile.sample![0]; - assert.notEqual(label, null); - if (label === null) return; // Appease TypeScript. - assert.strictEqual(label.length, 2); - assert.strictEqual(str(profile, label[0].key! as number), 'str'); - assert.strictEqual(str(profile, label[0].str! as number), 'foo'); - assert.strictEqual(str(profile, label[1].key! as number), 'num'); - assert.strictEqual(label[1].num!, 123); - }); - - it('should have timeNanos gap that roughly matches durationNanos', async () => { - const wait = 100; - // Need 10% wiggle room due to precision loss in timeNanos - const minimumDuration = wait * 1e6 * 0.9; - const cpu = new CpuProfiler(); - cpu.start(99); - - await busyWait(wait); - const first = cpu.profile()!; - assert.ok(first.durationNanos! >= minimumDuration); - - await busyWait(wait); - const second = cpu.profile()!; - assert.ok( - second.timeNanos! >= (first.timeNanos! as number) + minimumDuration - ); - assert.ok(second.durationNanos! >= minimumDuration); - cpu.stop(); - }); - }); -}); diff --git a/ts/test/test-time-profiler.ts b/ts/test/test-time-profiler.ts index 0319c559..84d25b76 100644 --- a/ts/test/test-time-profiler.ts +++ b/ts/test/test-time-profiler.ts @@ -19,11 +19,12 @@ import * as sinon from 'sinon'; import * as time from '../src/time-profiler'; import * as v8TimeProfiler from '../src/time-profiler-bindings'; import {timeProfile, v8TimeProfile} from './profiles-for-tests'; +import {hrtime} from 'process'; +import {Label, Profile} from 'pprof-format'; +import {AssertionError} from 'assert'; const assert = require('assert'); -const majorVersion = process.version.slice(1).split('.').map(Number)[0]; - const PROFILE_OPTIONS = { durationMillis: 500, intervalMicros: 1000, @@ -42,6 +43,163 @@ describe('Time Profiler', () => { [-1, -1] ); }); + + it('should assign labels', function () { + if (process.platform !== 'darwin' && process.platform !== 'linux') { + this.skip(); + } + this.timeout(3000); + + const intervalNanos = PROFILE_OPTIONS.intervalMicros * 1_000; + time.start({ + intervalMicros: PROFILE_OPTIONS.intervalMicros, + durationMillis: PROFILE_OPTIONS.durationMillis, + customLabels: true, + lineNumbers: false, + }); + // By repeating the test few times, we also exercise the profiler + // start-stop overlap behavior. + const repeats = 3; + for (let i = 0; i < repeats; ++i) { + loop(); + validateProfile(time.stop(i < repeats - 1)); + } + + // Each of fn0, fn1, fn2 loops busily for one or two profiling intervals. + // fn0 resets the label; fn1 and fn2 don't. Label for fn1 + // is reset in the loop. This ensures the following invariants that we + // test for: + // label0 can be observed in loop or fn0 + // label1 can be observed in loop or fn1 + // fn0 might be observed with no label + // fn1 must always be observed with label1 + // fn2 must never be observed with a label + function fn0() { + const start = hrtime.bigint(); + while (hrtime.bigint() - start < intervalNanos); + time.setLabels(undefined); + } + + function fn1() { + const start = hrtime.bigint(); + while (hrtime.bigint() - start < intervalNanos); + } + + function fn2() { + const start = hrtime.bigint(); + while (hrtime.bigint() - start < intervalNanos); + } + + function loop() { + const label0 = {label: 'value0'}; + const label1 = {label: 'value1'}; + const durationNanos = PROFILE_OPTIONS.durationMillis * 1_000_000; + const start = hrtime.bigint(); + while (hrtime.bigint() - start < durationNanos) { + time.setLabels(label0); + fn0(); + time.setLabels(label1); + fn1(); + time.setLabels(undefined); + fn2(); + } + } + + function validateProfile(profile: Profile) { + // Get string table indices for strings we're interested in + const stringTable = profile.stringTable; + const [ + loopIdx, + fn0Idx, + fn1Idx, + fn2Idx, + labelIdx, + value0Idx, + value1Idx, + ] = ['loop', 'fn0', 'fn1', 'fn2', 'label', 'value0', 'value1'].map(x => + stringTable.dedup(x) + ); + function labelIs(l: Label, str: number) { + return l.key === labelIdx && l.str === str; + } + + function idx(n: number | bigint): number { + if (typeof n === 'number') { + // We want a 0-based array index, but IDs start from 1. + return n - 1; + } + throw new AssertionError({message: 'Expected a number'}); + } + + function labelStr(label: Label) { + return label ? stringTable.strings[idx(label.str) + 1] : 'undefined'; + } + + let fn0ObservedWithLabel0 = false; + let fn1ObservedWithLabel1 = false; + let fn2ObservedWithoutLabels = false; + profile.sample.forEach(sample => { + const locIdx = idx(sample.locationId[0]); + const loc = profile.location[locIdx]; + const fnIdx = idx(loc.line[0].functionId); + const fn = profile.function[fnIdx]; + const fnName = fn.name; + const labels = sample.label; + switch (fnName) { + case loopIdx: + assert(labels.length < 2, 'loop can have at most one label'); + labels.forEach(label => { + assert( + labelIs(label, value0Idx) || labelIs(label, value1Idx), + 'loop can be observed with value0 or value1' + ); + }); + break; + case fn0Idx: + assert(labels.length < 2, 'fn0 can have at most one label'); + labels.forEach(label => { + if (labelIs(label, value0Idx)) { + fn0ObservedWithLabel0 = true; + } else { + throw new AssertionError({ + message: + 'Only value0 can be observed with fn0. Observed instead ' + + labelStr(label), + }); + } + }); + break; + case fn1Idx: + assert(labels.length === 1, 'fn1 must be observed with a label'); + labels.forEach(label => { + assert( + labelIs(label, value1Idx), + 'Only value1 can be observed with fn1' + ); + }); + fn1ObservedWithLabel1 = true; + break; + case fn2Idx: + assert( + labels.length === 0, + 'fn2 must be observed with no labels. Observed instead with ' + + labelStr(labels[0]) + ); + fn2ObservedWithoutLabels = true; + break; + default: + // Make no assumptions about other functions; we can just as well + // capture internals of time-profiler.ts, GC, etc. + } + }); + assert(fn0ObservedWithLabel0, 'fn0 was not observed with value0'); + assert(fn1ObservedWithLabel1, 'fn1 was not observed with value1'); + assert( + fn2ObservedWithoutLabels, + 'fn2 was not observed without a label' + ); + } + }); }); describe('profile (w/ stubs)', () => { @@ -50,7 +208,6 @@ describe('Time Profiler', () => { const timeProfilerStub = { start: sinon.stub(), stop: sinon.stub().returns(v8TimeProfile), - dispose: sinon.stub(), }; before(() => { @@ -81,30 +238,22 @@ describe('Time Profiler', () => { }); it('should be able to restart when stopping', async () => { - const stop = time.start(PROFILE_OPTIONS.intervalMicros); + time.start({intervalMicros: PROFILE_OPTIONS.intervalMicros}); timeProfilerStub.start.resetHistory(); timeProfilerStub.stop.resetHistory(); - timeProfilerStub.dispose.resetHistory(); - assert.deepEqual(timeProfile, stop(true)); + assert.deepEqual(timeProfile, time.stop(true)); - sinon.assert.calledOnce(timeProfilerStub.start); + sinon.assert.notCalled(timeProfilerStub.start); sinon.assert.calledOnce(timeProfilerStub.stop); - if (majorVersion >= 16) { - sinon.assert.notCalled(timeProfilerStub.dispose); - } else { - sinon.assert.calledOnce(timeProfilerStub.dispose); - } timeProfilerStub.start.resetHistory(); timeProfilerStub.stop.resetHistory(); - timeProfilerStub.dispose.resetHistory(); - assert.deepEqual(timeProfile, stop()); + assert.deepEqual(timeProfile, time.stop()); sinon.assert.notCalled(timeProfilerStub.start); sinon.assert.calledOnce(timeProfilerStub.stop); - sinon.assert.calledOnce(timeProfilerStub.dispose); }); }); });