@@ -26,7 +26,9 @@ const {
2626 Int32Array,
2727 Int8Array,
2828 JSONParse,
29+ ObjectKeys,
2930 ObjectPrototypeToString,
31+ ReflectGet,
3032 SymbolDispose,
3133 Uint16Array,
3234 Uint32Array,
@@ -39,6 +41,8 @@ const {
3941
4042const { Buffer } = require ( 'buffer' ) ;
4143const {
44+ validateFunction,
45+ validateObject,
4246 validateString,
4347 validateUint32,
4448 validateOneOf,
@@ -156,6 +160,11 @@ const {
156160 heapSpaceStatisticsBuffer,
157161 getCppHeapStatistics : _getCppHeapStatistics ,
158162 detailLevel,
163+
164+ startSamplingHeapProfiler : _startSamplingHeapProfiler ,
165+ stopSamplingHeapProfiler : _stopSamplingHeapProfiler ,
166+ getAllocationProfile : _getAllocationProfile ,
167+ setHeapProfileLabelsStore : _setHeapProfileLabelsStore ,
159168} = binding ;
160169
161170const kNumberOfHeapSpaces = kHeapSpaces . length ;
@@ -494,6 +503,109 @@ class GCProfiler {
494503 }
495504}
496505
506+ // --- Heap profile labels API ---
507+ // Internal AsyncLocalStorage for propagating labels through async context.
508+ // Requires --experimental-async-context-frame (Node 22) or Node 24+.
509+ // Lazily initialized on first use so processes that never use heap profiling
510+ // pay zero cost (no ALS instance, no async_hooks require).
511+ let _heapProfileLabelsALS ;
512+
513+ function ensureHeapProfileLabelsALS ( ) {
514+ if ( _heapProfileLabelsALS === undefined ) {
515+ // When V8_HEAP_PROFILER_SAMPLE_LABELS is compiled out, the C++ binding
516+ // for _setHeapProfileLabelsStore is not registered — labels are a no-op.
517+ if ( typeof _setHeapProfileLabelsStore !== 'function' ) return ;
518+ const { AsyncLocalStorage } = require ( 'async_hooks' ) ;
519+ _heapProfileLabelsALS = new AsyncLocalStorage ( ) ;
520+ // The ALS instance is passed to C++ as the key used to look up labels in
521+ // the AsyncContextFrame map (via ContinuationPreservedEmbedderData).
522+ // This relies on the async-context-frame implementation storing ALS
523+ // instances as Map keys — if that internal representation changes, the
524+ // C++ label-resolution code in node_v8.cc must be updated to match.
525+ _setHeapProfileLabelsStore ( _heapProfileLabelsALS ) ;
526+ }
527+ return _heapProfileLabelsALS ;
528+ }
529+
530+ /**
531+ * Convert a labels object to a flat array [key1, val1, key2, val2, ...].
532+ * Pre-flattened at label-set time (not per allocation/sample) because the
533+ * C++ callback runs in GetAllocationProfile() BEFORE BuildSamples() where
534+ * it resolves labels from CPED captured at allocation time.
535+ * @param {Record<string, string> } labels
536+ * @returns {string[] }
537+ */
538+ function labelsToFlat ( labels ) {
539+ const keys = ObjectKeys ( labels ) ;
540+ const len = keys . length ;
541+ const flat = new Array ( len * 2 ) ;
542+ for ( let i = 0 ; i < len ; i ++ ) {
543+ const key = keys [ i ] ;
544+ const val = ReflectGet ( labels , key ) ;
545+ validateString ( val , `labels.${ key } ` ) ;
546+ flat [ i * 2 ] = key ;
547+ flat [ i * 2 + 1 ] = val ;
548+ }
549+ return flat ;
550+ }
551+
552+ /**
553+ * Starts the V8 sampling heap profiler.
554+ * @param {number } [sampleInterval] - Average bytes between samples (default 512 KB).
555+ * @param {number } [stackDepth] - Maximum stack depth for samples (default 16).
556+ * @param {object } [options] - Options object.
557+ * @param {boolean } [options.includeCollectedObjects] - If true, retain
558+ * samples for objects collected by GC (allocation-rate mode).
559+ */
560+ function startSamplingHeapProfiler ( sampleInterval , stackDepth , options ) {
561+ if ( sampleInterval !== undefined ) validateUint32 ( sampleInterval , 'sampleInterval' , true ) ;
562+ if ( stackDepth !== undefined ) validateUint32 ( stackDepth , 'stackDepth' ) ;
563+ if ( options !== undefined ) validateObject ( options , 'options' ) ;
564+ return _startSamplingHeapProfiler ( sampleInterval , stackDepth , options ) ;
565+ }
566+
567+ /**
568+ * Runs `fn` with the given heap profile labels active. Labels propagate
569+ * across `await` boundaries via AsyncLocalStorage. If `fn` returns a
570+ * Promise, labels remain active until the Promise settles.
571+ *
572+ * @param {Record<string, string> } labels
573+ * @param {Function } fn
574+ * @returns {* } The return value of `fn`.
575+ */
576+ function withHeapProfileLabels ( labels , fn ) {
577+ validateObject ( labels , 'labels' ) ;
578+ validateFunction ( fn , 'fn' ) ;
579+ // Store the flat [key1, val1, key2, val2, ...] array in ALS.
580+ // Conversion happens once at label-set time (not per allocation).
581+ // The C++ callback resolves labels from CPED in GetAllocationProfile()
582+ // before BuildSamples() — pre-flattening avoids V8 Object property
583+ // access during label resolution.
584+ const flat = labelsToFlat ( labels ) ;
585+ const als = ensureHeapProfileLabelsALS ( ) ;
586+ // When labels are compiled out, still run the callback — just without
587+ // label tracking.
588+ if ( als === undefined ) return fn ( ) ;
589+ return als . run ( flat , fn ) ;
590+ }
591+
592+ /**
593+ * Sets heap profile labels for the current async scope using
594+ * `enterWith` semantics. Labels persist until overwritten or the
595+ * async scope ends. Useful for frameworks (e.g. Hapi) where the
596+ * handler runs after the extension returns.
597+ *
598+ * @param {Record<string, string> } labels
599+ */
600+ function setHeapProfileLabels ( labels ) {
601+ validateObject ( labels , 'labels' ) ;
602+ const flat = labelsToFlat ( labels ) ;
603+ const als = ensureHeapProfileLabelsALS ( ) ;
604+ // When labels are compiled out, setHeapProfileLabels is a no-op.
605+ if ( als === undefined ) return ;
606+ als . enterWith ( flat ) ;
607+ }
608+
497609module . exports = {
498610 cachedDataVersionTag,
499611 getHeapSnapshot,
@@ -518,4 +630,9 @@ module.exports = {
518630 GCProfiler,
519631 isStringOneByteRepresentation,
520632 startCpuProfile,
633+ startSamplingHeapProfiler,
634+ stopSamplingHeapProfiler : _stopSamplingHeapProfiler ,
635+ getAllocationProfile : _getAllocationProfile ,
636+ withHeapProfileLabels,
637+ setHeapProfileLabels,
521638} ;
0 commit comments