1616
1717const {
1818 Array,
19+ ArrayPrototypePush,
1920 BigInt64Array,
2021 BigUint64Array,
2122 DataView,
@@ -26,7 +27,10 @@ const {
2627 Int32Array,
2728 Int8Array,
2829 JSONParse,
30+ ObjectKeys,
2931 ObjectPrototypeToString,
32+ SafePromisePrototypeFinally,
33+ String,
3034 SymbolDispose,
3135 Uint16Array,
3236 Uint32Array,
@@ -38,7 +42,10 @@ const {
3842} = primordials ;
3943
4044const { Buffer } = require ( 'buffer' ) ;
45+ const { AsyncLocalStorage } = require ( 'async_hooks' ) ;
4146const {
47+ validateFunction,
48+ validateObject,
4249 validateString,
4350 validateUint32,
4451 validateOneOf,
@@ -47,6 +54,7 @@ const {
4754 Serializer,
4855 Deserializer,
4956} = internalBinding ( 'serdes' ) ;
57+ const { isPromise } = internalBinding ( 'types' ) ;
5058const {
5159 namespace : startupSnapshot ,
5260} = require ( 'internal/v8/startup_snapshot' ) ;
@@ -156,6 +164,12 @@ const {
156164 heapSpaceStatisticsBuffer,
157165 getCppHeapStatistics : _getCppHeapStatistics ,
158166 detailLevel,
167+
168+ startSamplingHeapProfiler : _startSamplingHeapProfiler ,
169+ stopSamplingHeapProfiler : _stopSamplingHeapProfiler ,
170+ getAllocationProfile : _getAllocationProfile ,
171+ registerHeapProfileLabels : _registerHeapProfileLabels ,
172+ unregisterHeapProfileLabels : _unregisterHeapProfileLabels ,
159173} = binding ;
160174
161175const kNumberOfHeapSpaces = kHeapSpaces . length ;
@@ -494,6 +508,91 @@ class GCProfiler {
494508 }
495509}
496510
511+ // --- Heap profile labels API ---
512+ // Internal AsyncLocalStorage for propagating labels through async context.
513+ // Requires --experimental-async-context-frame (Node 22) or Node 24+.
514+ const _heapProfileLabelsALS = new AsyncLocalStorage ( ) ;
515+
516+ /**
517+ * Convert a labels object to a flat array [key1, val1, key2, val2, ...].
518+ * @param {Record<string, string> } labels
519+ * @returns {string[] }
520+ */
521+ function labelsToFlat ( labels ) {
522+ const keys = ObjectKeys ( labels ) ;
523+ const flat = [ ] ;
524+ for ( let i = 0 ; i < keys . length ; i ++ ) {
525+ ArrayPrototypePush ( flat , String ( keys [ i ] ) , String ( labels [ keys [ i ] ] ) ) ;
526+ }
527+ return flat ;
528+ }
529+
530+ /**
531+ * Starts the V8 sampling heap profiler.
532+ * @param {number } [sampleInterval] - Average bytes between samples (default 512 KB).
533+ * @param {number } [stackDepth] - Maximum stack depth for samples (default 16).
534+ * @param {object } [options] - Options object.
535+ * @param {boolean } [options.includeCollectedObjects] - If true, retain
536+ * samples for objects collected by GC (allocation-rate mode).
537+ */
538+ function startSamplingHeapProfiler ( sampleInterval , stackDepth , options ) {
539+ if ( sampleInterval !== undefined ) validateUint32 ( sampleInterval , 'sampleInterval' ) ;
540+ if ( stackDepth !== undefined ) validateUint32 ( stackDepth , 'stackDepth' ) ;
541+ if ( options !== undefined ) validateObject ( options , 'options' ) ;
542+ return _startSamplingHeapProfiler ( sampleInterval , stackDepth , options ) ;
543+ }
544+
545+ /**
546+ * Runs `fn` with the given heap profile labels active. Labels propagate
547+ * across `await` boundaries via AsyncLocalStorage. If `fn` returns a
548+ * Promise, labels remain active until the Promise settles.
549+ *
550+ * @param {Record<string, string> } labels
551+ * @param {Function } fn
552+ * @returns {* } The return value of `fn`.
553+ */
554+ function withHeapProfileLabels ( labels , fn ) {
555+ validateObject ( labels , 'labels' ) ;
556+ validateFunction ( fn , 'fn' ) ;
557+ const flat = labelsToFlat ( labels ) ;
558+ return _heapProfileLabelsALS . run ( flat , ( ) => {
559+ _registerHeapProfileLabels ( flat ) ;
560+ try {
561+ const result = fn ( ) ;
562+ if ( isPromise ( result ) ) {
563+ return SafePromisePrototypeFinally (
564+ result ,
565+ ( ) => _unregisterHeapProfileLabels ( ) ,
566+ ) ;
567+ }
568+ _unregisterHeapProfileLabels ( ) ;
569+ return result ;
570+ } catch ( err ) {
571+ _unregisterHeapProfileLabels ( ) ;
572+ throw err ;
573+ }
574+ } ) ;
575+ }
576+
577+ /**
578+ * Sets heap profile labels for the current async scope using
579+ * `enterWith` semantics. Labels persist until overwritten or the
580+ * async scope ends. Useful for frameworks (e.g. Hapi) where the
581+ * handler runs after the extension returns.
582+ *
583+ * @param {Record<string, string> } labels
584+ */
585+ function setHeapProfileLabels ( labels ) {
586+ validateObject ( labels , 'labels' ) ;
587+ const flat = labelsToFlat ( labels ) ;
588+ // Unregister previous entry before enterWith creates a new CPED context,
589+ // otherwise the old entry leaks in the label map (enterWith creates a new
590+ // AsyncContextFrame each call, orphaning the previous CPED identity).
591+ _unregisterHeapProfileLabels ( ) ;
592+ _heapProfileLabelsALS . enterWith ( flat ) ;
593+ _registerHeapProfileLabels ( flat ) ;
594+ }
595+
497596module . exports = {
498597 cachedDataVersionTag,
499598 getHeapSnapshot,
@@ -518,4 +617,9 @@ module.exports = {
518617 GCProfiler,
519618 isStringOneByteRepresentation,
520619 startCpuProfile,
620+ startSamplingHeapProfiler,
621+ stopSamplingHeapProfiler : _stopSamplingHeapProfiler ,
622+ getAllocationProfile : _getAllocationProfile ,
623+ withHeapProfileLabels,
624+ setHeapProfileLabels,
521625} ;
0 commit comments