Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitlab/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand All @@ -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",
],
Expand Down
1 change: 0 additions & 1 deletion bindings/contexts.hh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

#pragma once

#include <nan.h>
#include <v8-profiler.h>
#include <unordered_map>

Expand Down
23 changes: 23 additions & 0 deletions bindings/general-regs-only.hh
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions bindings/profile-translator.cc
Original file line number Diff line number Diff line change
@@ -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.h>

v8::Local<v8::Number> dd::ProfileTranslator::NewNumber(int64_t x) {
return v8::Number::New(isolate, static_cast<double>(x));
}
64 changes: 64 additions & 0 deletions bindings/profile-translator.hh
Original file line number Diff line number Diff line change
@@ -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 <v8.h>

namespace dd {
class ProfileTranslator {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Array> emptyArray = v8::Array::New(isolate, 0);

protected:
v8::Local<v8::Object> NewObject() { return v8::Object::New(isolate); }

v8::Local<v8::Integer> NewInteger(int x) {
return v8::Integer::New(isolate, x);
}

v8::Local<v8::Boolean> NewBoolean(bool x) {
return v8::Boolean::New(isolate, x);
}

v8::Local<v8::Number> NewNumber(int64_t x);

v8::Local<v8::Array> NewArray(int length) {
return length == 0 ? emptyArray : v8::Array::New(isolate, length);
}

v8::Local<v8::String> NewString(const char* str) {
return v8::String::NewFromUtf8(isolate, str).ToLocalChecked();
}

v8::MaybeLocal<v8::Value> Get(v8::Local<v8::Array> arr, uint32_t index) {
return arr->Get(context, index);
}

v8::Maybe<bool> Set(v8::Local<v8::Array> arr,
uint32_t index,
v8::Local<v8::Value> value) {
return arr->Set(context, index, value);
}

v8::Maybe<bool> Set(v8::Local<v8::Object> obj,
v8::Local<v8::Value> key,
v8::Local<v8::Value> value) {
return obj->Set(context, key, value);
}

ProfileTranslator() = default;
};
}; // namespace dd
44 changes: 1 addition & 43 deletions bindings/profilers/heap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "defer.hh"
#include "per-isolate-data.hh"
#include "translate-heap-profile.hh"

#include <chrono>
#include <memory>
Expand Down Expand Up @@ -482,49 +483,6 @@ size_t NearHeapLimit(void* data,
return new_heap_limit;
}

v8::Local<v8::Value> TranslateAllocationProfile(
v8::AllocationProfile::Node* node) {
v8::Local<v8::Object> js_node = Nan::New<v8::Object>();

Nan::Set(js_node, Nan::New<v8::String>("name").ToLocalChecked(), node->name);
Nan::Set(js_node,
Nan::New<v8::String>("scriptName").ToLocalChecked(),
node->script_name);
Nan::Set(js_node,
Nan::New<v8::String>("scriptId").ToLocalChecked(),
Nan::New<v8::Integer>(node->script_id));
Nan::Set(js_node,
Nan::New<v8::String>("lineNumber").ToLocalChecked(),
Nan::New<v8::Integer>(node->line_number));
Nan::Set(js_node,
Nan::New<v8::String>("columnNumber").ToLocalChecked(),
Nan::New<v8::Integer>(node->column_number));

v8::Local<v8::Array> children = Nan::New<v8::Array>(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<v8::String>("children").ToLocalChecked(), children);
v8::Local<v8::Array> allocations =
Nan::New<v8::Array>(node->allocations.size());
for (size_t i = 0; i < node->allocations.size(); i++) {
v8::AllocationProfile::Allocation alloc = node->allocations[i];
v8::Local<v8::Object> js_alloc = Nan::New<v8::Object>();
Nan::Set(js_alloc,
Nan::New<v8::String>("sizeBytes").ToLocalChecked(),
Nan::New<v8::Number>(alloc.size));
Nan::Set(js_alloc,
Nan::New<v8::String>("count").ToLocalChecked(),
Nan::New<v8::Number>(alloc.count));
Nan::Set(allocations, i, js_alloc);
}
Nan::Set(js_node,
Nan::New<v8::String>("allocations").ToLocalChecked(),
allocations);
return js_node;
}

NAN_METHOD(HeapProfiler::StartSamplingHeapProfiler) {
if (info.Length() == 2) {
if (!info[0]->IsUint32()) {
Expand Down
3 changes: 2 additions & 1 deletion bindings/profilers/heap.hh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#pragma once

#include <nan.h>
#include "general-regs-only.hh"

namespace dd {

Expand All @@ -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);
};
Expand Down
81 changes: 50 additions & 31 deletions bindings/profilers/wall.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<int64_t>(node::AsyncHooksGetExecutionAsyncId(isolate));
prof->PushContext(time_from, time_to, cpu_time, async_id);
}
#else
class SignalHandler {
Expand Down Expand Up @@ -368,26 +370,35 @@ static int64_t GetV8ToEpochOffset() {
return V8toEpochOffset;
}

ContextsByNode WallProfiler::GetContextsByNode(CpuProfile* profile,
ContextBuffer& contexts,
int64_t startCpuTime) {
ContextsByNode contextsByNode;
Local<Number> NewNumberFromInt64(Isolate* isolate, int64_t value) {
return Number::New(isolate, static_cast<double>(value));
}

std::shared_ptr<ContextsByNode> CreateContextsByNode() {
return std::make_shared<ContextsByNode>();
}

std::shared_ptr<ContextsByNode> WallProfiler::GetContextsByNode(
CpuProfile* profile, ContextBuffer& contexts, int64_t startCpuTime) {
auto contextsByNode = CreateContextsByNode();

auto sampleCount = profile->GetSamplesCount();
if (contexts.empty() || sampleCount == 0) {
return contextsByNode;
}

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<v8::String>("context").ToLocalChecked();
auto timestampKey = Nan::New<v8::String>("timestamp").ToLocalChecked();
auto cpuTimeKey = Nan::New<v8::String>("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;

Expand Down Expand Up @@ -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> array;
if (it == contextsByNode.end()) {
array = Nan::New<Array>();
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<v8::Object> timedContext = Nan::New<v8::Object>();
Nan::Set(timedContext,
contextKey,
sampleContext.context.get()->Get(isolate));
Nan::Set(timedContext,
timestampKey,
BigInt::New(isolate, sampleTimestamp + V8toEpochOffset));
Local<Object> 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<v8::Number>(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
Expand Down Expand Up @@ -501,11 +523,7 @@ WallProfiler::WallProfiler(std::chrono::microseconds samplingPeriod,

v8::Local<v8::Uint32Array> jsArray =
v8::Uint32Array::New(buffer, 0, kFieldCount);
#if (V8_MAJOR_VERSION >= 8)
fields_ = static_cast<uint32_t*>(buffer->GetBackingStore()->Data());
#else
fields_ = static_cast<uint32_t*>(buffer->GetContents().Data());
#endif
jsArray_ = v8::Global<v8::Uint32Array>(isolate, jsArray);
std::fill(fields_, fields_ + kFieldCount, 0);
}
Expand Down Expand Up @@ -867,7 +885,7 @@ Result WallProfiler::StopImpl(bool restart, v8::Local<v8::Value>& profile) {

profile = TranslateTimeProfile(v8_profile,
includeLines_,
&contextsByNode,
contextsByNode,
collectCpuTime_,
nonJSThreadsCpuTime);

Expand Down Expand Up @@ -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<std::atomic<uint32_t>*>(&fields_[kSampleCount]),
1U,
Expand Down
Loading