Skip to content

Commit 0e1454b

Browse files
committed
src: add heap profile labels and ProfilingArrayBufferAllocator
C++ bindings for the V8 heap profile labels callback: - RegisterHeapProfileLabels/UnregisterHeapProfileLabels via CPED lookup - HeapProfileLabelsCallback with O(1) address map + GC-move slow path - ProfilingArrayBufferAllocator for per-label Buffer/ArrayBuffer tracking - GetAllocationProfile with labels and externalBytes - Cleanup hooks for environment teardown - Build flag conditioning on v8_enable_continuation_preserved_embedder_data Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
1 parent e0a47d9 commit 0e1454b

6 files changed

Lines changed: 587 additions & 2 deletions

File tree

node.gyp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'v8_trace_maps%': 0,
55
'v8_enable_pointer_compression%': 0,
66
'v8_enable_31bit_smis_on_64bit_arch%': 0,
7+
'v8_enable_continuation_preserved_embedder_data%': 1,
78
'force_dynamic_crt%': 0,
89
'node_builtin_modules_path%': '',
910
'node_core_target_name%': 'node',
@@ -937,6 +938,12 @@
937938
'msvs_disabled_warnings!': [4244],
938939

939940
'conditions': [
941+
[ 'v8_enable_continuation_preserved_embedder_data==1', {
942+
'defines': [
943+
# Enable heap profiler sample labels when CPED is available.
944+
'V8_HEAP_PROFILER_SAMPLE_LABELS',
945+
],
946+
}],
940947
[ 'openssl_default_cipher_list!=""', {
941948
'defines': [
942949
'NODE_OPENSSL_DEFAULT_CIPHER_LIST="<(openssl_default_cipher_list)"'
@@ -1322,6 +1329,11 @@
13221329
'sources': [ '<@(node_cctest_sources)' ],
13231330

13241331
'conditions': [
1332+
[ 'v8_enable_continuation_preserved_embedder_data==1', {
1333+
'defines': [
1334+
'V8_HEAP_PROFILER_SAMPLE_LABELS',
1335+
],
1336+
}],
13251337
[ 'node_shared_gtest=="false"', {
13261338
'dependencies': [
13271339
'deps/googletest/googletest.gyp:gtest',

src/api/environment.cc

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
#include "node_realm-inl.h"
1616
#include "node_shadow_realm.h"
1717
#include "node_snapshot_builder.h"
18+
#include "node_v8.h"
1819
#include "node_v8_platform-inl.h"
20+
#include "node_handle_address.h"
1921
#include "node_wasm_web_api.h"
2022
#include "uv.h"
2123
#ifdef NODE_ENABLE_VTUNE_PROFILING
@@ -191,11 +193,147 @@ void DebuggingArrayBufferAllocator::RegisterPointerInternal(void* data,
191193
allocations_[data] = size;
192194
}
193195

196+
void* ProfilingArrayBufferAllocator::Allocate(size_t size) {
197+
void* ret = NodeArrayBufferAllocator::Allocate(size);
198+
if (ret != nullptr && enabled_.load(std::memory_order_acquire)) {
199+
LabelPairs labels = FindCurrentLabels();
200+
if (!labels.empty()) {
201+
std::string key = SerializeLabels(labels);
202+
Mutex::ScopedLock lock(mutex_);
203+
allocations_[ret] = {key, size};
204+
auto& entry = per_label_bytes_[key];
205+
if (entry.labels.empty()) entry.labels = std::move(labels);
206+
entry.bytes += static_cast<int64_t>(size);
207+
}
208+
}
209+
return ret;
210+
}
211+
212+
void* ProfilingArrayBufferAllocator::AllocateUninitialized(size_t size) {
213+
void* ret = NodeArrayBufferAllocator::AllocateUninitialized(size);
214+
if (ret != nullptr && enabled_.load(std::memory_order_acquire)) {
215+
LabelPairs labels = FindCurrentLabels();
216+
if (!labels.empty()) {
217+
std::string key = SerializeLabels(labels);
218+
Mutex::ScopedLock lock(mutex_);
219+
allocations_[ret] = {key, size};
220+
auto& entry = per_label_bytes_[key];
221+
if (entry.labels.empty()) entry.labels = std::move(labels);
222+
entry.bytes += static_cast<int64_t>(size);
223+
}
224+
}
225+
return ret;
226+
}
227+
228+
void ProfilingArrayBufferAllocator::Free(void* data, size_t size) {
229+
if (enabled_.load(std::memory_order_acquire)) {
230+
Mutex::ScopedLock lock(mutex_);
231+
auto it = allocations_.find(data);
232+
if (it != allocations_.end()) {
233+
auto label_it = per_label_bytes_.find(it->second.first);
234+
if (label_it != per_label_bytes_.end()) {
235+
label_it->second.bytes -= static_cast<int64_t>(it->second.second);
236+
}
237+
allocations_.erase(it);
238+
}
239+
}
240+
NodeArrayBufferAllocator::Free(data, size);
241+
}
242+
243+
void ProfilingArrayBufferAllocator::Enable(
244+
v8::Isolate* isolate,
245+
std::unordered_map<uintptr_t,
246+
v8_utils::HeapProfileLabelEntry>* label_map) {
247+
Mutex::ScopedLock lock(mutex_);
248+
isolate_ = isolate;
249+
label_map_ = label_map;
250+
main_thread_id_ = std::this_thread::get_id();
251+
enabled_.store(true, std::memory_order_release);
252+
}
253+
254+
void ProfilingArrayBufferAllocator::Disable() {
255+
enabled_.store(false, std::memory_order_release);
256+
Mutex::ScopedLock lock(mutex_);
257+
allocations_.clear();
258+
per_label_bytes_.clear();
259+
isolate_ = nullptr;
260+
label_map_ = nullptr;
261+
}
262+
263+
std::vector<ProfilingArrayBufferAllocator::LabeledBytes>
264+
ProfilingArrayBufferAllocator::GetPerLabelBytes() const {
265+
Mutex::ScopedLock lock(mutex_);
266+
std::vector<LabeledBytes> result;
267+
for (const auto& [key, entry] : per_label_bytes_) {
268+
if (entry.bytes > 0) {
269+
result.push_back(entry);
270+
}
271+
}
272+
return result;
273+
}
274+
275+
std::string ProfilingArrayBufferAllocator::SerializeLabels(
276+
const LabelPairs& labels) {
277+
std::string key;
278+
for (const auto& [k, v] : labels) {
279+
if (!key.empty()) key += '\0';
280+
key += k;
281+
key += '\0';
282+
key += v;
283+
}
284+
return key;
285+
}
286+
287+
ProfilingArrayBufferAllocator::LabelPairs
288+
ProfilingArrayBufferAllocator::FindCurrentLabels() {
289+
// Skip non-main-thread allocations (SharedArrayBuffer from workers).
290+
if (std::this_thread::get_id() != main_thread_id_) return {};
291+
if (isolate_ == nullptr || label_map_ == nullptr) return {};
292+
293+
// Read CPED via public V8 API. This is safe because:
294+
// 1. ArrayBuffer allocator runs in normal JS context, not during GC
295+
// 2. HandleScope is always active during JS execution
296+
v8::Local<v8::Value> cped =
297+
isolate_->GetContinuationPreservedEmbedderData();
298+
if (cped.IsEmpty() || cped->IsUndefined() || cped->IsNull()) return {};
299+
300+
uintptr_t addr = GetLocalAddress(cped);
301+
if (addr == 0) return {}; // Smi::zero() — no ALS context
302+
303+
// O(1) lookup by tagged object address.
304+
auto it = label_map_->find(addr);
305+
if (it != label_map_->end() && !it->second.labels.empty()) {
306+
return it->second.labels;
307+
}
308+
309+
// Slow path: GC may have moved the object. Scan by identity.
310+
for (const auto& [key, entry] : *label_map_) {
311+
if (!entry.context_key.IsEmpty() && entry.context_key == cped) {
312+
if (!entry.labels.empty()) {
313+
// Copy labels before extract invalidates the reference.
314+
auto result = entry.labels;
315+
// Rehash for future fast-path hits.
316+
auto node = label_map_->extract(key);
317+
node.key() = addr;
318+
label_map_->insert(std::move(node));
319+
return result;
320+
}
321+
break;
322+
}
323+
}
324+
325+
return {};
326+
}
327+
194328
std::unique_ptr<ArrayBufferAllocator> ArrayBufferAllocator::Create(bool debug) {
195329
if (debug || per_process::cli_options->debug_arraybuffer_allocations)
196330
return std::make_unique<DebuggingArrayBufferAllocator>();
197-
else
198-
return std::make_unique<NodeArrayBufferAllocator>();
331+
// Always use ProfilingArrayBufferAllocator so that per-label external memory
332+
// tracking is available when the sampling heap profiler is started via
333+
// v8.startSamplingHeapProfiler(). When profiling is disabled (the default)
334+
// the only overhead is a single atomic load (enabled_.load()) on each
335+
// Allocate/Free — no hash-map lookups or CPED reads occur.
336+
return std::make_unique<ProfilingArrayBufferAllocator>();
199337
}
200338

201339
ArrayBufferAllocator* CreateArrayBufferAllocator() {

src/node_handle_address.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#ifndef SRC_NODE_HANDLE_ADDRESS_H_
2+
#define SRC_NODE_HANDLE_ADDRESS_H_
3+
4+
#include "v8-internal.h"
5+
#include "v8-local-handle.h"
6+
7+
namespace node {
8+
9+
// Extract the tagged V8 object address from a Local handle.
10+
// Uses V8's ValueHelper which correctly handles both indirect handles
11+
// (Address* location_) and direct handles (V8_ENABLE_DIRECT_LOCAL).
12+
//
13+
// v8::internal::ValueHelper::ValueAsAddress is declared in V8's semi-public
14+
// header <v8-internal.h>, which is distributed with V8 and intended for
15+
// embedder use (it backs the V8 API handle machinery). However, internal APIs
16+
// may change across V8 major versions. This is the recommended approach per
17+
// the V8 team for extracting tagged addresses from Local handles under both
18+
// direct and indirect handle modes. If V8 removes or renames ValueHelper in a
19+
// future update, this callsite will need to be adjusted accordingly.
20+
inline uintptr_t GetLocalAddress(v8::Local<v8::Value> handle) {
21+
return static_cast<uintptr_t>(
22+
v8::internal::ValueHelper::ValueAsAddress(*handle));
23+
}
24+
25+
} // namespace node
26+
27+
#endif // SRC_NODE_HANDLE_ADDRESS_H_

src/node_internals.h

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ v8::Maybe<void> InitializePrimordials(v8::Local<v8::Context> context,
123123
v8::MaybeLocal<v8::Object> InitializePrivateSymbols(
124124
v8::Local<v8::Context> context, IsolateData* isolate_data);
125125

126+
class ProfilingArrayBufferAllocator; // Forward declaration.
127+
126128
class NodeArrayBufferAllocator : public ArrayBufferAllocator {
127129
public:
128130
void* Allocate(size_t size) override; // Defined in src/node.cc
@@ -136,6 +138,9 @@ class NodeArrayBufferAllocator : public ArrayBufferAllocator {
136138
}
137139

138140
NodeArrayBufferAllocator* GetImpl() final { return this; }
141+
virtual ProfilingArrayBufferAllocator* GetProfilingAllocator() {
142+
return nullptr;
143+
}
139144
inline uint64_t total_mem_usage() const {
140145
return total_mem_usage_.load(std::memory_order_relaxed);
141146
}
@@ -164,6 +169,59 @@ class DebuggingArrayBufferAllocator final : public NodeArrayBufferAllocator {
164169
std::unordered_map<void*, size_t> allocations_;
165170
};
166171

172+
// Forward declarations for heap profile label tracking.
173+
namespace v8_utils {
174+
struct HeapProfileLabelEntry;
175+
}
176+
177+
// Subclass of NodeArrayBufferAllocator that tracks per-label external memory
178+
// (Buffer/ArrayBuffer backing stores) when heap profiling with labels is active.
179+
// When disabled (default), overhead is a single relaxed atomic load per alloc.
180+
class ProfilingArrayBufferAllocator : public NodeArrayBufferAllocator {
181+
public:
182+
using LabelPairs = std::vector<std::pair<std::string, std::string>>;
183+
184+
struct LabeledBytes {
185+
LabelPairs labels;
186+
int64_t bytes = 0;
187+
};
188+
189+
void* Allocate(size_t size) override;
190+
void* AllocateUninitialized(size_t size) override;
191+
void Free(void* data, size_t size) override;
192+
ProfilingArrayBufferAllocator* GetProfilingAllocator() override {
193+
return this;
194+
}
195+
196+
// Called from StartSamplingHeapProfiler/StopSamplingHeapProfiler.
197+
void Enable(
198+
v8::Isolate* isolate,
199+
std::unordered_map<uintptr_t,
200+
v8_utils::HeapProfileLabelEntry>* label_map);
201+
void Disable();
202+
203+
// Returns per-label live external bytes (for getAllocationProfile).
204+
std::vector<LabeledBytes> GetPerLabelBytes() const;
205+
206+
private:
207+
LabelPairs FindCurrentLabels();
208+
static std::string SerializeLabels(const LabelPairs& labels);
209+
210+
std::atomic<bool> enabled_{false};
211+
v8::Isolate* isolate_ = nullptr;
212+
// Borrowed pointer to BindingData::heap_profile_label_map (not owned).
213+
std::unordered_map<uintptr_t,
214+
v8_utils::HeapProfileLabelEntry>* label_map_ = nullptr;
215+
216+
std::thread::id main_thread_id_ = std::this_thread::get_id();
217+
218+
mutable Mutex mutex_;
219+
// Maps allocation pointer to {serialized_label_key, size}.
220+
std::unordered_map<void*, std::pair<std::string, size_t>> allocations_;
221+
// Per-serialized-label-key entry with full labels and live bytes.
222+
std::unordered_map<std::string, LabeledBytes> per_label_bytes_;
223+
};
224+
167225
namespace Buffer {
168226
v8::MaybeLocal<v8::Object> Copy(Environment* env, const char* data, size_t len);
169227
v8::MaybeLocal<v8::Object> New(Environment* env, size_t size);

0 commit comments

Comments
 (0)