|
15 | 15 | #include "node_realm-inl.h" |
16 | 16 | #include "node_shadow_realm.h" |
17 | 17 | #include "node_snapshot_builder.h" |
| 18 | +#include "node_v8.h" |
18 | 19 | #include "node_v8_platform-inl.h" |
19 | 20 | #include "node_wasm_web_api.h" |
20 | 21 | #include "uv.h" |
@@ -191,11 +192,159 @@ void DebuggingArrayBufferAllocator::RegisterPointerInternal(void* data, |
191 | 192 | allocations_[data] = size; |
192 | 193 | } |
193 | 194 |
|
| 195 | +void* ProfilingArrayBufferAllocator::Allocate(size_t size) { |
| 196 | + void* ret = NodeArrayBufferAllocator::Allocate(size); |
| 197 | + if (ret != nullptr && enabled_.load(std::memory_order_acquire)) { |
| 198 | + LabelPairs labels = FindCurrentLabels(); |
| 199 | + if (!labels.empty()) { |
| 200 | + std::string key = SerializeLabels(labels); |
| 201 | + Mutex::ScopedLock lock(mutex_); |
| 202 | + allocations_[ret] = {key, size}; |
| 203 | + auto& entry = per_label_bytes_[key]; |
| 204 | + if (entry.labels.empty()) entry.labels = std::move(labels); |
| 205 | + entry.bytes += static_cast<int64_t>(size); |
| 206 | + } |
| 207 | + } |
| 208 | + return ret; |
| 209 | +} |
| 210 | + |
| 211 | +void* ProfilingArrayBufferAllocator::AllocateUninitialized(size_t size) { |
| 212 | + void* ret = NodeArrayBufferAllocator::AllocateUninitialized(size); |
| 213 | + if (ret != nullptr && enabled_.load(std::memory_order_acquire)) { |
| 214 | + LabelPairs labels = FindCurrentLabels(); |
| 215 | + if (!labels.empty()) { |
| 216 | + std::string key = SerializeLabels(labels); |
| 217 | + Mutex::ScopedLock lock(mutex_); |
| 218 | + allocations_[ret] = {key, size}; |
| 219 | + auto& entry = per_label_bytes_[key]; |
| 220 | + if (entry.labels.empty()) entry.labels = std::move(labels); |
| 221 | + entry.bytes += static_cast<int64_t>(size); |
| 222 | + } |
| 223 | + } |
| 224 | + return ret; |
| 225 | +} |
| 226 | + |
| 227 | +void ProfilingArrayBufferAllocator::Free(void* data, size_t size) { |
| 228 | + if (enabled_.load(std::memory_order_acquire)) { |
| 229 | + Mutex::ScopedLock lock(mutex_); |
| 230 | + auto it = allocations_.find(data); |
| 231 | + if (it != allocations_.end()) { |
| 232 | + auto label_it = per_label_bytes_.find(it->second.first); |
| 233 | + if (label_it != per_label_bytes_.end()) { |
| 234 | + label_it->second.bytes -= static_cast<int64_t>(it->second.second); |
| 235 | + } |
| 236 | + allocations_.erase(it); |
| 237 | + } |
| 238 | + } |
| 239 | + NodeArrayBufferAllocator::Free(data, size); |
| 240 | +} |
| 241 | + |
| 242 | +void ProfilingArrayBufferAllocator::Enable( |
| 243 | + v8::Isolate* isolate, v8::Global<v8::Value>* als_key) { |
| 244 | + Mutex::ScopedLock lock(mutex_); |
| 245 | + isolate_ = isolate; |
| 246 | + als_key_ = als_key; |
| 247 | + main_thread_id_ = std::this_thread::get_id(); |
| 248 | + enabled_.store(true, std::memory_order_release); |
| 249 | +} |
| 250 | + |
| 251 | +void ProfilingArrayBufferAllocator::Disable() { |
| 252 | + enabled_.store(false, std::memory_order_release); |
| 253 | + Mutex::ScopedLock lock(mutex_); |
| 254 | + allocations_.clear(); |
| 255 | + per_label_bytes_.clear(); |
| 256 | + isolate_ = nullptr; |
| 257 | + als_key_ = nullptr; |
| 258 | +} |
| 259 | + |
| 260 | +std::vector<ProfilingArrayBufferAllocator::LabeledBytes> |
| 261 | +ProfilingArrayBufferAllocator::GetPerLabelBytes() const { |
| 262 | + Mutex::ScopedLock lock(mutex_); |
| 263 | + std::vector<LabeledBytes> result; |
| 264 | + for (const auto& [key, entry] : per_label_bytes_) { |
| 265 | + if (entry.bytes > 0) { |
| 266 | + result.push_back(entry); |
| 267 | + } |
| 268 | + } |
| 269 | + return result; |
| 270 | +} |
| 271 | + |
| 272 | +std::string ProfilingArrayBufferAllocator::SerializeLabels( |
| 273 | + const LabelPairs& labels) { |
| 274 | + std::string key; |
| 275 | + for (const auto& [k, v] : labels) { |
| 276 | + if (!key.empty()) key += '\0'; |
| 277 | + key += k; |
| 278 | + key += '\0'; |
| 279 | + key += v; |
| 280 | + } |
| 281 | + return key; |
| 282 | +} |
| 283 | + |
| 284 | +ProfilingArrayBufferAllocator::LabelPairs |
| 285 | +ProfilingArrayBufferAllocator::FindCurrentLabels() { |
| 286 | + // Skip non-main-thread allocations (SharedArrayBuffer from workers). |
| 287 | + if (std::this_thread::get_id() != main_thread_id_) return {}; |
| 288 | + if (isolate_ == nullptr || als_key_ == nullptr || als_key_->IsEmpty()) { |
| 289 | + return {}; |
| 290 | + } |
| 291 | + |
| 292 | + // Read CPED via public V8 API. This is safe because: |
| 293 | + // 1. ArrayBuffer allocator runs in normal JS context, not during GC |
| 294 | + // 2. HandleScope is always active during JS execution |
| 295 | + v8::Local<v8::Value> cped = |
| 296 | + isolate_->GetContinuationPreservedEmbedderData(); |
| 297 | + if (cped.IsEmpty() || !cped->IsMap()) return {}; |
| 298 | + |
| 299 | + v8::HandleScope handle_scope(isolate_); |
| 300 | + v8::Local<v8::Context> context = isolate_->GetCurrentContext(); |
| 301 | + v8::Local<v8::Map> frame = cped.As<v8::Map>(); |
| 302 | + v8::Local<v8::Value> als_key = als_key_->Get(isolate_); |
| 303 | + |
| 304 | + // Cannot use Map::Get() here — it calls a JS builtin which is not safe |
| 305 | + // in DisallowJavascriptExecution (ArrayBuffer allocator is called from |
| 306 | + // BackingStore::Allocate inside the ArrayBuffer constructor). |
| 307 | + // Use AsArray() which reads the internal backing store directly without |
| 308 | + // calling JS builtins, then iterate entries by identity comparison. |
| 309 | + v8::Local<v8::Array> entries = frame->AsArray(); |
| 310 | + uint32_t entries_len = entries->Length(); |
| 311 | + for (uint32_t i = 0; i + 1 < entries_len; i += 2) { |
| 312 | + v8::Local<v8::Value> entry_key; |
| 313 | + if (!entries->Get(context, i).ToLocal(&entry_key)) continue; |
| 314 | + if (!entry_key->StrictEquals(als_key)) continue; |
| 315 | + |
| 316 | + // Found the labels ALS entry — value is the flat array. |
| 317 | + v8::Local<v8::Value> val; |
| 318 | + if (!entries->Get(context, i + 1).ToLocal(&val) || !val->IsArray()) { |
| 319 | + return {}; |
| 320 | + } |
| 321 | + |
| 322 | + // Convert flat [key1, val1, key2, val2, ...] array to string pairs. |
| 323 | + v8::Local<v8::Array> flat = val.As<v8::Array>(); |
| 324 | + uint32_t len = flat->Length(); |
| 325 | + LabelPairs result; |
| 326 | + for (uint32_t j = 0; j + 1 < len; j += 2) { |
| 327 | + v8::Local<v8::Value> k, v; |
| 328 | + if (!flat->Get(context, j).ToLocal(&k)) return {}; |
| 329 | + if (!flat->Get(context, j + 1).ToLocal(&v)) return {}; |
| 330 | + v8::String::Utf8Value key_str(isolate_, k); |
| 331 | + v8::String::Utf8Value val_str(isolate_, v); |
| 332 | + result.emplace_back(*key_str, *val_str); |
| 333 | + } |
| 334 | + return result; |
| 335 | + } |
| 336 | + return {}; |
| 337 | +} |
| 338 | + |
194 | 339 | std::unique_ptr<ArrayBufferAllocator> ArrayBufferAllocator::Create(bool debug) { |
195 | 340 | if (debug || per_process::cli_options->debug_arraybuffer_allocations) |
196 | 341 | return std::make_unique<DebuggingArrayBufferAllocator>(); |
197 | | - else |
198 | | - return std::make_unique<NodeArrayBufferAllocator>(); |
| 342 | + // Always use ProfilingArrayBufferAllocator so that per-label external memory |
| 343 | + // tracking is available when the sampling heap profiler is started via |
| 344 | + // v8.startSamplingHeapProfiler(). When profiling is disabled (the default) |
| 345 | + // the only overhead is a single atomic load (enabled_.load()) on each |
| 346 | + // Allocate/Free — no hash-map lookups or CPED reads occur. |
| 347 | + return std::make_unique<ProfilingArrayBufferAllocator>(); |
199 | 348 | } |
200 | 349 |
|
201 | 350 | ArrayBufferAllocator* CreateArrayBufferAllocator() { |
|
0 commit comments