Skip to content

Commit 2febdb4

Browse files
committed
deps: add heap profile sample labels to V8 profiler
Add V8 API for attaching embedder-defined labels to sampling heap profiler samples. Labels propagate through async context via CPED and are resolved at profile-read time. Signed-off-by: Rudolf Meijering <[email protected]>
1 parent 511a57a commit 2febdb4

7 files changed

Lines changed: 637 additions & 16 deletions

File tree

deps/v8/include/v8-profiler.h

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
#include <unordered_set>
1212
#include <vector>
1313

14+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
15+
#include <string>
16+
#include <utility>
17+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
18+
1419
#include "cppgc/common.h" // NOLINT(build/include_directory)
1520
#include "v8-local-handle.h" // NOLINT(build/include_directory)
1621
#include "v8-message.h" // NOLINT(build/include_directory)
@@ -791,26 +796,42 @@ class V8_EXPORT AllocationProfile {
791796
* Represent a single sample recorded for an allocation.
792797
*/
793798
struct Sample {
794-
/**
795-
* id of the node in the profile tree.
796-
*/
799+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
800+
Sample(uint32_t node_id, size_t size, unsigned int count,
801+
uint64_t sample_id,
802+
std::vector<std::pair<std::string, std::string>> labels = {})
803+
: node_id(node_id),
804+
size(size),
805+
count(count),
806+
sample_id(sample_id),
807+
labels(std::move(labels)) {}
808+
#else
809+
Sample(uint32_t node_id, size_t size, unsigned int count,
810+
uint64_t sample_id)
811+
: node_id(node_id), size(size), count(count), sample_id(sample_id) {}
812+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
813+
814+
/** id of the node in the profile tree. */
797815
uint32_t node_id;
798-
799-
/**
800-
* Size of the sampled allocation object.
801-
*/
816+
/** Size of the sampled allocation object. */
802817
size_t size;
803-
804-
/**
805-
* The number of objects of such size that were sampled.
806-
*/
818+
/** The number of objects of such size that were sampled. */
807819
unsigned int count;
808-
809820
/**
810821
* Unique time-ordered id of the allocation sample. Can be used to track
811822
* what samples were added or removed between two snapshots.
812823
*/
813824
uint64_t sample_id;
825+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
826+
/**
827+
* Embedder-provided labels resolved by the HeapProfileSampleLabelsCallback
828+
* during GetAllocationProfile(). The ALS value is captured at allocation
829+
* time; labels are parsed from it when the profile is read. Each pair is
830+
* (key, value). Empty if no callback is registered or the callback
831+
* returned no labels for this sample.
832+
*/
833+
std::vector<std::pair<std::string, std::string>> labels;
834+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
814835
};
815836

816837
/**
@@ -1001,6 +1022,31 @@ class V8_EXPORT HeapProfiler {
10011022
v8::Isolate* isolate, const v8::Local<v8::Value>& v8_value,
10021023
uint16_t class_id, void* data);
10031024

1025+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
1026+
/**
1027+
* Callback invoked during GetAllocationProfile() to resolve labels from
1028+
* the ALS value captured at allocation time.
1029+
*
1030+
* |data| is the opaque pointer passed to SetHeapProfileSampleLabelsCallback.
1031+
* |context| is the ALS value (e.g. a flat [key, val, ...] array) that was
1032+
* extracted from the ContinuationPreservedEmbedderData (CPED) Map at
1033+
* allocation time using the ALS key set via SetHeapProfileSampleLabelsKey.
1034+
* The extraction uses OrderedHashMap::FindEntry (zero-allocation, GC-safe).
1035+
* The callback parses this value directly to produce labels — no Map lookup
1036+
* is needed at callback time.
1037+
*
1038+
* Only invoked for samples that had an ALS value at allocation time (i.e.
1039+
* the ALS key was found in the CPED Map).
1040+
*
1041+
* Write labels to |out_labels| and return true, or return false if no
1042+
* labels apply. The caller provides a stack-local vector; returning false
1043+
* avoids any heap allocation on the hot path.
1044+
*/
1045+
using HeapProfileSampleLabelsCallback = bool (*)(
1046+
void* data, v8::Local<v8::Value> context,
1047+
std::vector<std::pair<std::string, std::string>>* out_labels);
1048+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
1049+
10041050
/** Returns the number of snapshots taken. */
10051051
int GetSnapshotCount();
10061052

@@ -1261,6 +1307,31 @@ class V8_EXPORT HeapProfiler {
12611307

12621308
void SetGetDetachednessCallback(GetDetachednessCallback callback, void* data);
12631309

1310+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
1311+
/**
1312+
* Registers a callback invoked during GetAllocationProfile() to resolve
1313+
* embedder-defined labels from the ALS value extracted at allocation time.
1314+
* The labels are stored on AllocationProfile::Sample::labels.
1315+
*
1316+
* Pass nullptr to clear the callback.
1317+
*/
1318+
void SetHeapProfileSampleLabelsCallback(
1319+
HeapProfileSampleLabelsCallback callback, void* data = nullptr);
1320+
1321+
/**
1322+
* Sets the AsyncLocalStorage (ALS) key used to extract the label value
1323+
* from the ContinuationPreservedEmbedderData (CPED) Map at allocation
1324+
* time. When a heap sample is taken, SampleObject uses this key with
1325+
* OrderedHashMap::FindEntry to look up the corresponding ALS value and
1326+
* store it on the sample. The stored value is later passed to the
1327+
* HeapProfileSampleLabelsCallback as the |context| parameter.
1328+
*
1329+
* Must be called before starting the sampling profiler.
1330+
* Pass an empty handle to clear.
1331+
*/
1332+
void SetHeapProfileSampleLabelsKey(Local<Value> key);
1333+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
1334+
12641335
/**
12651336
* Returns whether the heap profiler is currently taking a snapshot.
12661337
*/

deps/v8/src/api/api.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11829,6 +11829,19 @@ void HeapProfiler::SetGetDetachednessCallback(GetDetachednessCallback callback,
1182911829
data);
1183011830
}
1183111831

11832+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
11833+
void HeapProfiler::SetHeapProfileSampleLabelsCallback(
11834+
HeapProfileSampleLabelsCallback callback, void* data) {
11835+
reinterpret_cast<i::HeapProfiler*>(this)
11836+
->SetHeapProfileSampleLabelsCallback(callback, data);
11837+
}
11838+
11839+
void HeapProfiler::SetHeapProfileSampleLabelsKey(Local<Value> key) {
11840+
reinterpret_cast<i::HeapProfiler*>(this)
11841+
->SetHeapProfileSampleLabelsKey(key);
11842+
}
11843+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
11844+
1183211845
bool HeapProfiler::IsTakingSnapshot() {
1183311846
return reinterpret_cast<i::HeapProfiler*>(this)->IsTakingSnapshot();
1183411847
}

deps/v8/src/profiler/heap-profiler.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,34 @@ class HeapProfiler : public HeapObjectAllocationTracker {
7979
bool is_sampling_allocations() { return !!sampling_heap_profiler_; }
8080
AllocationProfile* GetAllocationProfile();
8181

82+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
83+
void SetHeapProfileSampleLabelsCallback(
84+
v8::HeapProfiler::HeapProfileSampleLabelsCallback callback,
85+
void* data) {
86+
sample_labels_callback_ = callback;
87+
sample_labels_data_ = data;
88+
}
89+
90+
void SetHeapProfileSampleLabelsKey(v8::Local<v8::Value> key) {
91+
if (key.IsEmpty()) {
92+
sample_labels_als_key_.Reset();
93+
} else {
94+
sample_labels_als_key_.Reset(
95+
reinterpret_cast<v8::Isolate*>(isolate()), key);
96+
}
97+
}
98+
99+
v8::HeapProfiler::HeapProfileSampleLabelsCallback
100+
sample_labels_callback() const {
101+
return sample_labels_callback_;
102+
}
103+
void* sample_labels_data() const { return sample_labels_data_; }
104+
105+
const v8::Global<v8::Value>& sample_labels_als_key() const {
106+
return sample_labels_als_key_;
107+
}
108+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
109+
82110
void StartHeapObjectsTracking(bool track_allocations);
83111
void StopHeapObjectsTracking();
84112
AllocationTracker* allocation_tracker() const {
@@ -176,6 +204,18 @@ class HeapProfiler : public HeapObjectAllocationTracker {
176204
std::pair<v8::HeapProfiler::GetDetachednessCallback, void*>
177205
get_detachedness_callback_;
178206
std::unique_ptr<HeapProfilerNativeMoveListener> native_move_listener_;
207+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
208+
// All three fields are written only from the main thread via
209+
// SetHeapProfileSampleLabelsCallback() / SetHeapProfileSampleLabelsKey()
210+
// and read during GetAllocationProfile() or SampleObject() (also main
211+
// thread). No cross-thread access, so no synchronization is required.
212+
v8::HeapProfiler::HeapProfileSampleLabelsCallback sample_labels_callback_ =
213+
nullptr;
214+
void* sample_labels_data_ = nullptr;
215+
// The ALS key used at allocation time to extract the label value from
216+
// the CPED Map via OrderedHashMap::FindEntry.
217+
v8::Global<v8::Value> sample_labels_als_key_;
218+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
179219
};
180220

181221
} // namespace internal

deps/v8/src/profiler/sampling-heap-profiler.cc

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
#include "src/execution/isolate.h"
1616
#include "src/heap/heap-layout-inl.h"
1717
#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"
1821
#include "src/profiler/strings-storage.h"
1922

2023
namespace v8 {
@@ -96,6 +99,47 @@ void SamplingHeapProfiler::SampleObject(Address soon_object, size_t size) {
9699
node->allocations_[size]++;
97100
auto sample =
98101
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+
99143
sample->global.SetWeak(sample.get(), OnWeakCallback,
100144
WeakCallbackType::kParameter);
101145
samples_.emplace(sample.get(), std::move(sample));
@@ -299,22 +343,103 @@ v8::AllocationProfile* SamplingHeapProfiler::GetAllocationProfile() {
299343
scripts[script->id()] = handle(script, isolate_);
300344
}
301345
}
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+
302405
auto profile = new v8::internal::AllocationProfile();
303406
TranslateAllocationNode(profile, &profile_root_, scripts);
407+
408+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
409+
profile->samples_ = BuildSamples(std::move(resolved_labels));
410+
#else
304411
profile->samples_ = BuildSamples();
412+
#endif
305413

306414
return profile;
307415
}
308416

417+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
418+
const std::vector<v8::AllocationProfile::Sample>
419+
SamplingHeapProfiler::BuildSamples(ResolvedLabelsMap resolved_labels) const {
420+
#else
309421
const std::vector<v8::AllocationProfile::Sample>
310422
SamplingHeapProfiler::BuildSamples() const {
423+
#endif
311424
std::vector<v8::AllocationProfile::Sample> samples;
312425
samples.reserve(samples_.size());
426+
313427
for (const auto& it : samples_) {
314428
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
318443
}
319444
return samples;
320445
}

deps/v8/src/profiler/sampling-heap-profiler.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ class SamplingHeapProfiler {
114114
Global<Value> global;
115115
SamplingHeapProfiler* const profiler;
116116
const uint64_t sample_id;
117+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
118+
// ALS value extracted from the CPED Map at allocation time via
119+
// OrderedHashMap::FindEntry. Storing only the ALS value (the flat
120+
// label array) instead of the full CPED avoids retaining all ALS
121+
// stores for the lifetime of the sample. Labels are resolved from
122+
// this at read time (in GetAllocationProfile) via the callback.
123+
Global<Value> label_value;
124+
#endif // V8_HEAP_PROFILER_SAMPLE_LABELS
117125
};
118126

119127
SamplingHeapProfiler(Heap* heap, StringsStorage* names, uint64_t rate,
@@ -160,7 +168,14 @@ class SamplingHeapProfiler {
160168

161169
void SampleObject(Address soon_object, size_t size);
162170

171+
#ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
172+
using ResolvedLabelsMap = std::unordered_map<
173+
uint64_t, std::vector<std::pair<std::string, std::string>>>;
174+
const std::vector<v8::AllocationProfile::Sample> BuildSamples(
175+
ResolvedLabelsMap resolved_labels) const;
176+
#else
163177
const std::vector<v8::AllocationProfile::Sample> BuildSamples() const;
178+
#endif
164179

165180
AllocationNode* FindOrAddChildNode(AllocationNode* parent, const char* name,
166181
int script_id, int start_position);

0 commit comments

Comments
 (0)