2727#include " node.h"
2828#include " node_external_reference.h"
2929#include " util-inl.h"
30+ #include " v8-container.h"
3031#include " v8-profiler.h"
3132#include " v8.h"
3233
3334namespace node {
3435namespace v8_utils {
36+
37+ // V8's default sampling interval for the sampling heap profiler (512 KB).
38+ static constexpr uint64_t kDefaultSamplingInterval = 512 * 1024 ;
39+ using v8::AllocationProfile;
3540using v8::Array;
3641using v8::BigInt;
3742using v8::CFunction;
@@ -44,12 +49,14 @@ using v8::FunctionCallbackInfo;
4449using v8::FunctionTemplate;
4550using v8::HandleScope;
4651using v8::HeapCodeStatistics;
52+ using v8::HeapProfiler;
4753using v8::HeapSpaceStatistics;
4854using v8::HeapStatistics;
4955using v8::Integer;
5056using v8::Isolate;
5157using v8::Local;
5258using v8::LocalVector;
59+ using v8::Map;
5360using v8::MaybeLocal;
5461using v8::Number;
5562using v8::Object;
@@ -59,6 +66,63 @@ using v8::Uint32;
5966using v8::V8;
6067using v8::Value;
6168
69+ #ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
70+ // V8 callback invoked at profile-read time (BuildSamples) for each sample
71+ // that has a stored CPED. Receives the CPED (AsyncContextFrame = JS Map),
72+ // looks up the heap profile labels ALS store value, and converts the
73+ // pre-flattened [key1, val1, key2, val2, ...] array to string pairs.
74+ //
75+ // GC safety: this callback is invoked from GetAllocationProfile() BEFORE
76+ // BuildSamples() iteration, in a context where GC is allowed. The caller
77+ // iterates a snapshot of ALS values (not the live samples_ map), so
78+ // GC-triggered weak callbacks that erase from samples_ cannot cause
79+ // iterator invalidation. Array::Get() may allocate on the V8 heap — that
80+ // is safe in this pre-resolution phase.
81+ //
82+ // The |context| parameter is the ALS value (flat [key1, val1, key2, val2, ...]
83+ // array) extracted by V8 at allocation time via OrderedHashMap::FindEntry.
84+ // If no ALS value was found for a sample, context will be empty or undefined.
85+ static bool HeapProfileLabelsCallback (
86+ void * data, v8::Local<v8::Value> context,
87+ std::vector<std::pair<std::string, std::string>>* out_labels) {
88+ auto * binding_data = static_cast <BindingData*>(data);
89+ if (!binding_data) return false ;
90+
91+ // context is the ALS value (flat label array) — no Map lookup needed.
92+ if (context.IsEmpty () || !context->IsArray ()) return false ;
93+
94+ Isolate* isolate = binding_data->env ()->isolate ();
95+ HandleScope handle_scope (isolate);
96+ Local<v8::Context> v8_context = isolate->GetCurrentContext ();
97+
98+ // Convert flat [key1, val1, key2, val2, ...] array to string pairs.
99+ Local<Array> flat = context.As <Array>();
100+ uint32_t len = flat->Length ();
101+ std::vector<std::pair<std::string, std::string>> result;
102+ for (uint32_t j = 0 ; j + 1 < len; j += 2 ) {
103+ Local<Value> k, v;
104+ if (!flat->Get (v8_context, j).ToLocal (&k)) return false ;
105+ if (!flat->Get (v8_context, j + 1 ).ToLocal (&v)) return false ;
106+ String::Utf8Value key_str (isolate, k);
107+ String::Utf8Value val_str (isolate, v);
108+ if (*key_str == nullptr || *val_str == nullptr ) continue ;
109+ result.emplace_back (*key_str, *val_str);
110+ }
111+
112+ *out_labels = std::move (result);
113+ return !out_labels->empty ();
114+ }
115+
116+ // C++ binding: store the AsyncLocalStorage instance used for heap profile
117+ // labels. V8 uses this key at allocation time to extract the ALS value from
118+ // the CPED (AsyncContextFrame) via OrderedHashMap::FindEntry.
119+ void SetHeapProfileLabelsStore (const FunctionCallbackInfo<Value>& args) {
120+ Isolate* isolate = args.GetIsolate ();
121+ BindingData* binding_data = Realm::GetBindingData<BindingData>(args);
122+ binding_data->heap_profile_labels_als_key .Reset (isolate, args[0 ]);
123+ }
124+ #endif // V8_HEAP_PROFILER_SAMPLE_LABELS
125+
62126#define HEAP_STATISTICS_PROPERTIES (V ) \
63127 V (0 , total_heap_size, kTotalHeapSizeIndex ) \
64128 V (1 , total_heap_size_executable, kTotalHeapSizeExecutableIndex ) \
@@ -103,6 +167,9 @@ static const size_t kHeapCodeStatisticsPropertiesCount =
103167 HEAP_CODE_STATISTICS_PROPERTIES (V);
104168#undef V
105169
170+ // Forward declaration for the env cleanup hook (used by ~BindingData).
171+ static void CleanupHeapProfiling (void * data);
172+
106173BindingData::BindingData (Realm* realm,
107174 Local<Object> obj,
108175 InternalFieldInfo* info)
@@ -144,6 +211,25 @@ BindingData::BindingData(Realm* realm,
144211 heap_code_statistics_buffer.MakeWeak ();
145212}
146213
214+ BindingData::~BindingData () {
215+ // BindingData is destroyed during Realm::RunCleanup() (via
216+ // binding_data_store_.reset()), which runs BEFORE the environment cleanup
217+ // queue is drained. At this point the Isolate and Environment are
218+ // guaranteed to still be alive (Realm::RunCleanup runs inside
219+ // Environment::RunCleanup, before the Environment is deleted and long
220+ // before the Isolate is disposed).
221+ //
222+ // If profiling was active, DoCleanup() stops the sampler, clears V8's
223+ // callback pointer into this (about-to-be-destroyed) BindingData, and
224+ // disables the allocator tracker — preventing use-after-free.
225+ if (heap_profiling_cleanup_ != nullptr ) {
226+ heap_profiling_cleanup_->DoCleanup ();
227+ env ()->RemoveCleanupHook (CleanupHeapProfiling, heap_profiling_cleanup_);
228+ delete heap_profiling_cleanup_;
229+ heap_profiling_cleanup_ = nullptr ;
230+ }
231+ }
232+
147233bool BindingData::PrepareForSerialization (Local<Context> context,
148234 v8::SnapshotCreator* creator) {
149235 DCHECK_NULL (internal_field_info_);
@@ -186,6 +272,9 @@ void BindingData::MemoryInfo(MemoryTracker* tracker) const {
186272 heap_space_statistics_buffer);
187273 tracker->TrackField (" heap_code_statistics_buffer" ,
188274 heap_code_statistics_buffer);
275+ tracker->TrackFieldWithSize (" heap_profile_labels_als_key" ,
276+ heap_profile_labels_als_key.IsEmpty () ? 0 :
277+ sizeof (v8::Global<v8::Value>));
189278}
190279
191280void CachedDataVersionTag (const FunctionCallbackInfo<Value>& args) {
@@ -673,6 +762,180 @@ void GCProfiler::Stop(const FunctionCallbackInfo<v8::Value>& args) {
673762 }
674763}
675764
765+ // Data captured when heap profiling starts, used by the cleanup hook to
766+ // safely tear down profiler state if the Environment is destroyed while
767+ // profiling is still active (e.g. worker thread termination).
768+ //
769+ // Raw pointers are safe here because Environment cleanup hooks are
770+ // guaranteed to run before the Isolate is disposed: FreeEnvironment()
771+ // calls env->RunCleanup() (which drains the cleanup queue) while still
772+ // inside Isolate::Scope, and the ArrayBufferAllocator that owns
773+ // ProfilingArrayBufferAllocator outlives the Isolate.
774+ //
775+ // We intentionally do NOT store a BindingData* here — Realm::RunCleanup()
776+ // destroys BindingData (via binding_data_store_.reset()) before the
777+ // environment cleanup queue is drained, so any BindingData* would be
778+ // dangling by the time this hook fires.
779+ struct HeapProfilingCleanup {
780+ Isolate* isolate;
781+ bool cleaned_up = false ;
782+
783+ // Idempotent: stops the sampling profiler and clears the labels callback.
784+ // Safe to call multiple times — only the first call has effect.
785+ void DoCleanup () {
786+ if (cleaned_up) return ;
787+ cleaned_up = true ;
788+
789+ HeapProfiler* profiler = isolate->GetHeapProfiler ();
790+ profiler->StopSamplingHeapProfiler ();
791+ #ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
792+ profiler->SetHeapProfileSampleLabelsCallback (nullptr , nullptr );
793+ profiler->SetHeapProfileSampleLabelsKey (Local<Value>());
794+ #endif // V8_HEAP_PROFILER_SAMPLE_LABELS
795+ isolate = nullptr ;
796+ }
797+ };
798+
799+ static void CleanupHeapProfiling (void * data) {
800+ auto * ctx = static_cast <HeapProfilingCleanup*>(data);
801+ ctx->DoCleanup ();
802+ delete ctx;
803+ }
804+
805+ void StartSamplingHeapProfiler (const FunctionCallbackInfo<Value>& args) {
806+ Isolate* isolate = args.GetIsolate ();
807+ Local<Context> context = isolate->GetCurrentContext ();
808+ HeapProfiler* profiler = isolate->GetHeapProfiler ();
809+ BindingData* binding_data = Realm::GetBindingData<BindingData>(args);
810+ uint64_t interval = kDefaultSamplingInterval ;
811+ if (args.Length () > 0 && args[0 ]->IsNumber ()) {
812+ interval = static_cast <uint64_t >(args[0 ].As <Number>()->Value ());
813+ }
814+ int stack_depth = 16 ; // Default stack depth
815+ if (args.Length () > 1 && args[1 ]->IsNumber ()) {
816+ stack_depth = static_cast <int >(args[1 ].As <Number>()->Value ());
817+ }
818+ #ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
819+ profiler->SetHeapProfileSampleLabelsCallback (
820+ HeapProfileLabelsCallback, binding_data);
821+ if (!binding_data->heap_profile_labels_als_key .IsEmpty ()) {
822+ profiler->SetHeapProfileSampleLabelsKey (
823+ binding_data->heap_profile_labels_als_key .Get (isolate));
824+ }
825+ #endif // V8_HEAP_PROFILER_SAMPLE_LABELS
826+ // By default, GC'd samples are removed from the profile (live-memory mode).
827+ // When includeCollectedObjects is true, retain GC'd samples so allocation
828+ // attribution reflects total allocations (allocation-rate mode).
829+ v8::HeapProfiler::SamplingFlags flags =
830+ static_cast <v8::HeapProfiler::SamplingFlags>(
831+ v8::HeapProfiler::kSamplingNoFlags );
832+ if (args.Length () > 2 && args[2 ]->IsObject ()) {
833+ Local<Object> options = args[2 ].As <Object>();
834+ Local<String> key = String::NewFromUtf8Literal (isolate,
835+ " includeCollectedObjects" );
836+ Local<Value> val;
837+ if (options->Get (context, key).ToLocal (&val) && val->IsTrue ()) {
838+ flags = static_cast <v8::HeapProfiler::SamplingFlags>(
839+ v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMajorGC |
840+ v8::HeapProfiler::kSamplingIncludeObjectsCollectedByMinorGC );
841+ }
842+ }
843+ profiler->StartSamplingHeapProfiler (interval, stack_depth, flags);
844+
845+ // Register a cleanup hook so that if the Environment is torn down while
846+ // profiling is active (e.g. a worker thread is terminated), V8's callback
847+ // pointer into the now-destroyed BindingData is cleared. Remove any prior
848+ // hook first (handles repeated Start calls without an intervening Stop).
849+ Environment* env = Environment::GetCurrent (args);
850+ if (binding_data->heap_profiling_cleanup_ != nullptr ) {
851+ binding_data->heap_profiling_cleanup_ ->DoCleanup ();
852+ env->RemoveCleanupHook (
853+ CleanupHeapProfiling, binding_data->heap_profiling_cleanup_ );
854+ delete binding_data->heap_profiling_cleanup_ ;
855+ }
856+ auto * cleanup = new HeapProfilingCleanup{isolate};
857+ env->AddCleanupHook (CleanupHeapProfiling, cleanup);
858+ binding_data->heap_profiling_cleanup_ = cleanup;
859+ }
860+
861+ void StopSamplingHeapProfiler (const FunctionCallbackInfo<Value>& args) {
862+ Environment* env = Environment::GetCurrent (args);
863+ BindingData* binding_data = Realm::GetBindingData<BindingData>(args);
864+
865+ if (binding_data->heap_profiling_cleanup_ != nullptr ) {
866+ binding_data->heap_profiling_cleanup_ ->DoCleanup ();
867+ env->RemoveCleanupHook (
868+ CleanupHeapProfiling, binding_data->heap_profiling_cleanup_ );
869+ delete binding_data->heap_profiling_cleanup_ ;
870+ binding_data->heap_profiling_cleanup_ = nullptr ;
871+ }
872+ }
873+
874+ void GetAllocationProfile (const FunctionCallbackInfo<Value>& args) {
875+ Isolate* isolate = args.GetIsolate ();
876+ HeapProfiler* profiler = isolate->GetHeapProfiler ();
877+ HandleScope scope (isolate);
878+ Local<Context> context = isolate->GetCurrentContext ();
879+
880+ std::unique_ptr<AllocationProfile> profile (profiler->GetAllocationProfile ());
881+ if (!profile) {
882+ return ; // Returns undefined if profiler not started
883+ }
884+
885+ const std::vector<AllocationProfile::Sample>& samples = profile->GetSamples ();
886+ Local<Array> js_samples = Array::New (isolate, samples.size ());
887+
888+ for (size_t i = 0 ; i < samples.size (); i++) {
889+ const AllocationProfile::Sample& sample = samples[i];
890+ Local<Object> js_sample = Object::New (isolate);
891+
892+ if (js_sample->Set (context,
893+ FIXED_ONE_BYTE_STRING (isolate, " nodeId" ),
894+ Integer::NewFromUnsigned (isolate, sample.node_id ))
895+ .IsNothing ()) return ;
896+ if (js_sample->Set (context,
897+ FIXED_ONE_BYTE_STRING (isolate, " size" ),
898+ Number::New (isolate, static_cast <double >(sample.size )))
899+ .IsNothing ()) return ;
900+ if (js_sample->Set (context,
901+ FIXED_ONE_BYTE_STRING (isolate, " count" ),
902+ Integer::NewFromUnsigned (isolate, sample.count ))
903+ .IsNothing ()) return ;
904+ if (js_sample->Set (context,
905+ FIXED_ONE_BYTE_STRING (isolate, " sampleId" ),
906+ Number::New (isolate, static_cast <double >(sample.sample_id )))
907+ .IsNothing ()) return ;
908+
909+ #ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
910+ // Always emit labels field (empty {} when no labels captured)
911+ Local<Object> js_labels = Object::New (isolate);
912+ for (const auto & label : sample.labels ) {
913+ Local<String> key;
914+ if (!String::NewFromUtf8 (isolate, label.first .c_str (),
915+ v8::NewStringType::kNormal )
916+ .ToLocal (&key)) return ;
917+ Local<String> value;
918+ if (!String::NewFromUtf8 (isolate, label.second .c_str (),
919+ v8::NewStringType::kNormal )
920+ .ToLocal (&value)) return ;
921+ if (js_labels->Set (context, key, value).IsNothing ()) return ;
922+ }
923+ if (js_sample->Set (context,
924+ FIXED_ONE_BYTE_STRING (isolate, " labels" ),
925+ js_labels).IsNothing ()) return ;
926+ #endif // V8_HEAP_PROFILER_SAMPLE_LABELS
927+
928+ if (js_samples->Set (context, i, js_sample).IsNothing ()) return ;
929+ }
930+
931+ Local<Object> result = Object::New (isolate);
932+ if (result->Set (context,
933+ FIXED_ONE_BYTE_STRING (isolate, " samples" ),
934+ js_samples).IsNothing ()) return ;
935+
936+ args.GetReturnValue ().Set (result);
937+ }
938+
676939void Initialize (Local<Object> target,
677940 Local<Value> unused,
678941 Local<Context> context,
@@ -741,6 +1004,18 @@ void Initialize(Local<Object> target,
7411004 SetMethod (context, target, " startCpuProfile" , StartCpuProfile);
7421005 SetMethod (context, target, " stopCpuProfile" , StopCpuProfile);
7431006
1007+ // Sampling heap profiler with context support
1008+ SetMethod (context, target, " startSamplingHeapProfiler" ,
1009+ StartSamplingHeapProfiler);
1010+ SetMethod (context, target, " stopSamplingHeapProfiler" ,
1011+ StopSamplingHeapProfiler);
1012+ SetMethod (context, target, " getAllocationProfile" ,
1013+ GetAllocationProfile);
1014+ #ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
1015+ SetMethod (context, target, " setHeapProfileLabelsStore" ,
1016+ SetHeapProfileLabelsStore);
1017+ #endif // V8_HEAP_PROFILER_SAMPLE_LABELS
1018+
7441019 // Export symbols used by v8.isStringOneByteRepresentation()
7451020 SetFastMethodNoSideEffect (context,
7461021 target,
@@ -787,6 +1062,12 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
7871062 registry->Register (fast_is_string_one_byte_representation_);
7881063 registry->Register (StartCpuProfile);
7891064 registry->Register (StopCpuProfile);
1065+ registry->Register (StartSamplingHeapProfiler);
1066+ registry->Register (StopSamplingHeapProfiler);
1067+ registry->Register (GetAllocationProfile);
1068+ #ifdef V8_HEAP_PROFILER_SAMPLE_LABELS
1069+ registry->Register (SetHeapProfileLabelsStore);
1070+ #endif // V8_HEAP_PROFILER_SAMPLE_LABELS
7901071}
7911072
7921073} // namespace v8_utils
0 commit comments