Skip to content

Commit ad489ed

Browse files
committed
lib: add heap profile labels API to v8 module
JS API for heap profile label attribution: - withHeapProfileLabels(labels, fn): scoped labels via AsyncLocalStorage — just ALS.run with pre-flattened label array, zero C++ calls - setHeapProfileLabels(labels): enterWith semantics for frameworks where the handler runs after the middleware returns (e.g., Hapi) - startSamplingHeapProfiler with includeCollectedObjects option - stopSamplingHeapProfiler and getAllocationProfile with labels Labels are pre-flattened to [key, val, key, val, ...] arrays at set time for GC safety — the C++ callback runs during BuildSamples() iteration where V8 object allocation could invalidate the iterator. Signed-off-by: Rudolf Meijering <[email protected]>
1 parent 21702bb commit ad489ed

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

lib/v8.js

Lines changed: 91 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,9 @@ const {
2627
Int32Array,
2728
Int8Array,
2829
JSONParse,
30+
ObjectKeys,
2931
ObjectPrototypeToString,
32+
String,
3033
SymbolDispose,
3134
Uint16Array,
3235
Uint32Array,
@@ -38,7 +41,10 @@ const {
3841
} = primordials;
3942

4043
const { Buffer } = require('buffer');
44+
const { AsyncLocalStorage } = require('async_hooks');
4145
const {
46+
validateFunction,
47+
validateObject,
4248
validateString,
4349
validateUint32,
4450
validateOneOf,
@@ -156,6 +162,11 @@ const {
156162
heapSpaceStatisticsBuffer,
157163
getCppHeapStatistics: _getCppHeapStatistics,
158164
detailLevel,
165+
166+
startSamplingHeapProfiler: _startSamplingHeapProfiler,
167+
stopSamplingHeapProfiler: _stopSamplingHeapProfiler,
168+
getAllocationProfile: _getAllocationProfile,
169+
setHeapProfileLabelsStore: _setHeapProfileLabelsStore,
159170
} = binding;
160171

161172
const kNumberOfHeapSpaces = kHeapSpaces.length;
@@ -494,6 +505,81 @@ class GCProfiler {
494505
}
495506
}
496507

508+
// --- Heap profile labels API ---
509+
// Internal AsyncLocalStorage for propagating labels through async context.
510+
// Requires --experimental-async-context-frame (Node 22) or Node 24+.
511+
const _heapProfileLabelsALS = new AsyncLocalStorage();
512+
// Register the ALS instance with C++ so the V8 callback can look up
513+
// label values from stored CPED (AsyncContextFrame Map) at read time.
514+
_setHeapProfileLabelsStore(_heapProfileLabelsALS);
515+
516+
/**
517+
* Convert a labels object to a flat array [key1, val1, key2, val2, ...].
518+
* Pre-flattened at label-set time (not per allocation/sample) because the
519+
* C++ callback runs during BuildSamples() iteration where V8 Object property
520+
* access could allocate and trigger GC, invalidating the sample iterator.
521+
* @param {Record<string, string>} labels
522+
* @returns {string[]}
523+
*/
524+
function labelsToFlat(labels) {
525+
const keys = ObjectKeys(labels);
526+
const flat = [];
527+
for (let i = 0; i < keys.length; i++) {
528+
ArrayPrototypePush(flat, String(keys[i]), String(labels[keys[i]]));
529+
}
530+
return flat;
531+
}
532+
533+
/**
534+
* Starts the V8 sampling heap profiler.
535+
* @param {number} [sampleInterval] - Average bytes between samples (default 512 KB).
536+
* @param {number} [stackDepth] - Maximum stack depth for samples (default 16).
537+
* @param {object} [options] - Options object.
538+
* @param {boolean} [options.includeCollectedObjects] - If true, retain
539+
* samples for objects collected by GC (allocation-rate mode).
540+
*/
541+
function startSamplingHeapProfiler(sampleInterval, stackDepth, options) {
542+
if (sampleInterval !== undefined) validateUint32(sampleInterval, 'sampleInterval');
543+
if (stackDepth !== undefined) validateUint32(stackDepth, 'stackDepth');
544+
if (options !== undefined) validateObject(options, 'options');
545+
return _startSamplingHeapProfiler(sampleInterval, stackDepth, options);
546+
}
547+
548+
/**
549+
* Runs `fn` with the given heap profile labels active. Labels propagate
550+
* across `await` boundaries via AsyncLocalStorage. If `fn` returns a
551+
* Promise, labels remain active until the Promise settles.
552+
*
553+
* @param {Record<string, string>} labels
554+
* @param {Function} fn
555+
* @returns {*} The return value of `fn`.
556+
*/
557+
function withHeapProfileLabels(labels, fn) {
558+
validateObject(labels, 'labels');
559+
validateFunction(fn, 'fn');
560+
// Store the flat [key1, val1, key2, val2, ...] array in ALS.
561+
// Conversion happens once at label-set time (not per allocation).
562+
// Must stay pre-flattened because the C++ callback runs during
563+
// BuildSamples iteration — Object property access would allocate
564+
// V8 objects, potentially triggering GC and invalidating the iterator.
565+
const flat = labelsToFlat(labels);
566+
return _heapProfileLabelsALS.run(flat, fn);
567+
}
568+
569+
/**
570+
* Sets heap profile labels for the current async scope using
571+
* `enterWith` semantics. Labels persist until overwritten or the
572+
* async scope ends. Useful for frameworks (e.g. Hapi) where the
573+
* handler runs after the extension returns.
574+
*
575+
* @param {Record<string, string>} labels
576+
*/
577+
function setHeapProfileLabels(labels) {
578+
validateObject(labels, 'labels');
579+
const flat = labelsToFlat(labels);
580+
_heapProfileLabelsALS.enterWith(flat);
581+
}
582+
497583
module.exports = {
498584
cachedDataVersionTag,
499585
getHeapSnapshot,
@@ -518,4 +604,9 @@ module.exports = {
518604
GCProfiler,
519605
isStringOneByteRepresentation,
520606
startCpuProfile,
607+
startSamplingHeapProfiler,
608+
stopSamplingHeapProfiler: _stopSamplingHeapProfiler,
609+
getAllocationProfile: _getAllocationProfile,
610+
withHeapProfileLabels,
611+
setHeapProfileLabels,
521612
};

0 commit comments

Comments
 (0)