|
15 | 15 | #include "src/execution/isolate.h" |
16 | 16 | #include "src/heap/heap-layout-inl.h" |
17 | 17 | #include "src/heap/heap.h" |
| 18 | +#include "src/objects/js-collection-inl.h" |
| 19 | +#include "src/objects/ordered-hash-table.h" |
| 20 | +#include "src/profiler/heap-profiler.h" |
18 | 21 | #include "src/profiler/strings-storage.h" |
19 | 22 |
|
20 | 23 | namespace v8 { |
@@ -96,6 +99,47 @@ void SamplingHeapProfiler::SampleObject(Address soon_object, size_t size) { |
96 | 99 | node->allocations_[size]++; |
97 | 100 | auto sample = |
98 | 101 | std::make_unique<Sample>(size, node, loc, this, next_sample_id()); |
| 102 | + |
| 103 | +#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS |
| 104 | + // Extract the ALS value from the CPED Map at allocation time. |
| 105 | + // The CPED (ContinuationPreservedEmbedderData) is a JSMap containing |
| 106 | + // ALL ALS stores. We look up only our ALS key via |
| 107 | + // OrderedHashMap::FindEntry (zero-allocation, GC-safe) and store just |
| 108 | + // the resulting value (the flat label array). This avoids retaining |
| 109 | + // the entire CPED for the lifetime of each sample. |
| 110 | + // Global::Reset is safe inside DisallowGC (uses malloc, not V8 heap). |
| 111 | + { |
| 112 | + HeapProfiler* hp = isolate_->heap()->heap_profiler(); |
| 113 | + if (hp->sample_labels_callback()) { |
| 114 | + const v8::Global<v8::Value>& als_key_global = |
| 115 | + hp->sample_labels_als_key(); |
| 116 | + if (!als_key_global.IsEmpty()) { |
| 117 | + v8::Isolate* v8_isolate = reinterpret_cast<v8::Isolate*>(isolate_); |
| 118 | + v8::Local<v8::Value> context = |
| 119 | + v8_isolate->GetContinuationPreservedEmbedderData(); |
| 120 | + if (!context.IsEmpty() && !context->IsUndefined()) { |
| 121 | + Tagged<Object> cped_obj = *Utils::OpenDirectHandle(*context); |
| 122 | + if (IsJSMap(cped_obj)) { |
| 123 | + Tagged<JSMap> js_map = Cast<JSMap>(cped_obj); |
| 124 | + Tagged<OrderedHashMap> table = |
| 125 | + Cast<OrderedHashMap>(js_map->table()); |
| 126 | + v8::Local<v8::Value> als_key_local = |
| 127 | + als_key_global.Get(v8_isolate); |
| 128 | + Tagged<Object> key_obj = *Utils::OpenDirectHandle(*als_key_local); |
| 129 | + InternalIndex entry = table->FindEntry(isolate_, key_obj); |
| 130 | + if (entry.is_found()) { |
| 131 | + Tagged<Object> value = table->ValueAt(entry); |
| 132 | + v8::Local<v8::Value> value_local = |
| 133 | + Utils::ToLocal(direct_handle(value, isolate_)); |
| 134 | + sample->label_value.Reset(v8_isolate, value_local); |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | +#endif // V8_HEAP_PROFILER_SAMPLE_LABELS |
| 142 | + |
99 | 143 | sample->global.SetWeak(sample.get(), OnWeakCallback, |
100 | 144 | WeakCallbackType::kParameter); |
101 | 145 | samples_.emplace(sample.get(), std::move(sample)); |
@@ -299,22 +343,103 @@ v8::AllocationProfile* SamplingHeapProfiler::GetAllocationProfile() { |
299 | 343 | scripts[script->id()] = handle(script, isolate_); |
300 | 344 | } |
301 | 345 | } |
| 346 | + |
| 347 | +#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS |
| 348 | + // Pre-resolve labels while GC is still allowed. |
| 349 | + // The labels callback may allocate on the V8 heap. These MUST NOT run |
| 350 | + // during BuildSamples() iteration — GC would fire weak callbacks that |
| 351 | + // erase from samples_, causing iterator invalidation / UAF. |
| 352 | + // |
| 353 | + // Two-phase approach: |
| 354 | + // Phase 1: snapshot sample IDs and ALS values (Global::Get + Global |
| 355 | + // ctor use malloc, not V8 heap — no GC risk during iteration). |
| 356 | + // Phase 2: invoke the callback for each snapshot entry. GC may fire |
| 357 | + // here but we iterate our snapshot vector, not samples_. |
| 358 | + ResolvedLabelsMap resolved_labels; |
| 359 | + { |
| 360 | + HeapProfiler* hp = heap_->heap_profiler(); |
| 361 | + auto callback = hp->sample_labels_callback(); |
| 362 | + void* callback_data = hp->sample_labels_data(); |
| 363 | + if (callback) { |
| 364 | + v8::Isolate* v8_isolate = reinterpret_cast<v8::Isolate*>(isolate_); |
| 365 | + |
| 366 | + // Phase 1: snapshot ALS values keyed by sample_id. |
| 367 | + // Global ctor (GlobalizeReference) and Global::Get use global handle |
| 368 | + // storage (malloc), not the V8 heap — safe under DisallowGarbageCollection. |
| 369 | + // Locals from Get() are immediately consumed by Global ctor, so a single |
| 370 | + // HandleScope outside the loop suffices (no per-iteration scope churn). |
| 371 | + struct LabelValueSnapshot { |
| 372 | + uint64_t sample_id; |
| 373 | + v8::Global<v8::Value> label_value; |
| 374 | + }; |
| 375 | + std::vector<LabelValueSnapshot> snapshot; |
| 376 | + snapshot.reserve(samples_.size()); |
| 377 | + { |
| 378 | + HandleScope handle_scope(isolate_); |
| 379 | + DisallowGarbageCollection no_gc; |
| 380 | + for (const auto& [ptr, sample] : samples_) { |
| 381 | + if (!sample->label_value.IsEmpty()) { |
| 382 | + snapshot.push_back( |
| 383 | + {sample->sample_id, |
| 384 | + v8::Global<v8::Value>( |
| 385 | + v8_isolate, sample->label_value.Get(v8_isolate))}); |
| 386 | + } |
| 387 | + } |
| 388 | + } |
| 389 | + |
| 390 | + // Phase 2: resolve labels via callback (GC allowed). |
| 391 | + // The callback receives the ALS value (flat label array) directly — |
| 392 | + // the CPED Map lookup was already done at allocation time. |
| 393 | + for (auto& entry : snapshot) { |
| 394 | + HandleScope scope(isolate_); |
| 395 | + v8::Local<v8::Value> value_local = entry.label_value.Get(v8_isolate); |
| 396 | + std::vector<std::pair<std::string, std::string>> labels; |
| 397 | + if (callback(callback_data, value_local, &labels) && !labels.empty()) { |
| 398 | + resolved_labels[entry.sample_id] = std::move(labels); |
| 399 | + } |
| 400 | + } |
| 401 | + } |
| 402 | + } |
| 403 | +#endif |
| 404 | + |
302 | 405 | auto profile = new v8::internal::AllocationProfile(); |
303 | 406 | TranslateAllocationNode(profile, &profile_root_, scripts); |
| 407 | + |
| 408 | +#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS |
| 409 | + profile->samples_ = BuildSamples(std::move(resolved_labels)); |
| 410 | +#else |
304 | 411 | profile->samples_ = BuildSamples(); |
| 412 | +#endif |
305 | 413 |
|
306 | 414 | return profile; |
307 | 415 | } |
308 | 416 |
|
| 417 | +#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS |
| 418 | +const std::vector<v8::AllocationProfile::Sample> |
| 419 | +SamplingHeapProfiler::BuildSamples(ResolvedLabelsMap resolved_labels) const { |
| 420 | +#else |
309 | 421 | const std::vector<v8::AllocationProfile::Sample> |
310 | 422 | SamplingHeapProfiler::BuildSamples() const { |
| 423 | +#endif |
311 | 424 | std::vector<v8::AllocationProfile::Sample> samples; |
312 | 425 | samples.reserve(samples_.size()); |
| 426 | + |
313 | 427 | for (const auto& it : samples_) { |
314 | 428 | const Sample* sample = it.second.get(); |
315 | | - samples.emplace_back(v8::AllocationProfile::Sample{ |
316 | | - sample->owner->id_, sample->size, ScaleSample(sample->size, 1).count, |
317 | | - sample->sample_id}); |
| 429 | +#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS |
| 430 | + std::vector<std::pair<std::string, std::string>> labels; |
| 431 | + auto label_it = resolved_labels.find(sample->sample_id); |
| 432 | + if (label_it != resolved_labels.end()) { |
| 433 | + labels = std::move(label_it->second); |
| 434 | + } |
| 435 | + samples.emplace_back(sample->owner->id_, sample->size, |
| 436 | + ScaleSample(sample->size, 1).count, |
| 437 | + sample->sample_id, std::move(labels)); |
| 438 | +#else |
| 439 | + samples.emplace_back(sample->owner->id_, sample->size, |
| 440 | + ScaleSample(sample->size, 1).count, |
| 441 | + sample->sample_id); |
| 442 | +#endif |
318 | 443 | } |
319 | 444 | return samples; |
320 | 445 | } |
|
0 commit comments