Skip to content

Commit 9589661

Browse files
committed
lib: add withHeapProfileLabels and setHeapProfileLabels to v8 module
JS API for heap profile label attribution: - withHeapProfileLabels(labels, fn): scoped labels via AsyncLocalStorage with automatic cleanup on sync return, promise settle, or exception - setHeapProfileLabels(labels): enterWith semantics for frameworks where the handler runs after the extension returns (e.g., Hapi) - Expose startSamplingHeapProfiler, stopSamplingHeapProfiler, getAllocationProfile from the v8 binding Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
1 parent 0e1454b commit 9589661

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

lib/v8.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
const {
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

4044
const { Buffer } = require('buffer');
45+
const { AsyncLocalStorage } = require('async_hooks');
4146
const {
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');
5058
const {
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

161175
const 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+
497596
module.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

Comments
 (0)