Skip to content

Commit a8ac88e

Browse files
committed
lib: add heap profile labels API to v8 module
Add JS API for labeling heap profiler samples via AsyncLocalStorage. Labels are pre-flattened at set time to avoid V8 property access during resolution. Signed-off-by: Rudolf Meijering <[email protected]>
1 parent a393276 commit a8ac88e

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

lib/v8.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4042
const { Buffer } = require('buffer');
4143
const {
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

161170
const 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+
497609
module.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

Comments
 (0)