Skip to content

Commit 71037df

Browse files
committed
doc: document heap profile labels
Document the labels option on v8.startHeapProfile, the getAllocationProfile() method on the handle, and the two functions that set labels. Document the limitations: labelled data is only available through getAllocationProfile() and never from stop(), external memory accounting needs Node's own ArrayBuffer allocator, small pooled Buffers are attributed to whichever label triggered the pool refill, and addons built outside node-gyp must define V8_HEAP_PROFILER_SAMPLE_LABELS to see the same struct layout as libnode. Signed-off-by: Rudolf Meijering <skaapgif@gmail.com>
1 parent 29a1273 commit 71037df

2 files changed

Lines changed: 163 additions & 1 deletion

File tree

‎doc/api/v8.md‎

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,90 @@ added:
14461446
14471447
Returns true if the Node.js instance is run to build a snapshot.
14481448
1449+
## Heap profile labels
1450+
1451+
<!-- YAML
1452+
added: REPLACEME
1453+
-->
1454+
1455+
> Stability: 1 - Experimental
1456+
1457+
Attach string labels to V8 sampling heap profiler allocation samples.
1458+
Combined with [`AsyncLocalStorage`][], labels propagate through `await`
1459+
boundaries for per-context memory attribution (e.g., per-HTTP-route).
1460+
1461+
### `v8.withHeapProfileLabels(labels, fn)`
1462+
1463+
<!-- YAML
1464+
added: REPLACEME
1465+
-->
1466+
1467+
* `labels` {Object} Key-value string pairs (e.g., `{ route: '/users/:id' }`).
1468+
* `fn` {Function} May be `async`.
1469+
* Returns: {\*} Return value of `fn`.
1470+
1471+
Runs `fn` with the given labels active. If `fn` returns a promise, labels
1472+
remain active until the promise settles.
1473+
1474+
```mjs
1475+
const handle = v8.startHeapProfile({ sampleInterval: 64, labels: true });
1476+
1477+
await v8.withHeapProfileLabels({ route: '/users' }, async () => {
1478+
const data = await fetchUsers();
1479+
return processData(data);
1480+
});
1481+
1482+
const profile = handle.getAllocationProfile();
1483+
handle.stop();
1484+
```
1485+
1486+
### `v8.setHeapProfileLabels(labels)`
1487+
1488+
<!-- YAML
1489+
added: REPLACEME
1490+
-->
1491+
1492+
* `labels` {Object} Key-value string pairs.
1493+
1494+
Sets labels for the current async scope using `enterWith` semantics.
1495+
Useful for frameworks where the handler runs after the extension returns.
1496+
1497+
Prefer [`v8.withHeapProfileLabels()`][] when possible for automatic cleanup.
1498+
1499+
### Limitations — what is measured
1500+
1501+
Heap samples cover V8 heap allocations (JS objects, strings, closures).
1502+
`externalBytes` covers `Buffer`/`ArrayBuffer` backing stores.
1503+
1504+
Not measured: native addon memory, JIT code space, OS-level allocations.
1505+
1506+
**Native addon ABI.** The `v8::AllocationProfile::Sample` struct in
1507+
`v8-profiler.h` includes the `label_id` field only when
1508+
`V8_HEAP_PROFILER_SAMPLE_LABELS` is defined at compile time. Node.js sets
1509+
this macro for its own builds and for addons compiled through node-gyp
1510+
(via `common.gypi`). Addons built with other build systems (cmake, meson,
1511+
Makefile) must define `-DV8_HEAP_PROFILER_SAMPLE_LABELS` themselves to match
1512+
libnode. The `label_id` field is appended after the pre-existing fields, so
1513+
their offsets never change. On the common 64-bit ABIs (and on 32-bit targets
1514+
whose ABI 8-aligns `uint64_t`, such as Windows and ARM), it lands in existing
1515+
tail padding, `sizeof(AllocationProfile::Sample)` is unchanged, and iterating
1516+
`GetSamples()` strides correctly whether or not the macro is defined; an addon
1517+
only needs the macro to name `label_id`. On i386 System V (32-bit x86 on
1518+
Linux and the BSDs), `uint64_t` is 4-aligned and the field grows the struct by
1519+
4 bytes, so an addon there must define the macro to stride `GetSamples()`
1520+
correctly. A `static_assert` in V8 enforces the no-growth invariant on the
1521+
ABIs that provide the padding. When the macro is not defined, the
1522+
label APIs (`withHeapProfileLabels()`, `setHeapProfileLabels()`) are no-ops
1523+
and `getAllocationProfile()` omits `samples[].labels` and `externalBytes`
1524+
entirely.
1525+
1526+
**Label availability.** Labels depend on async-context-frame, which propagates
1527+
the `AsyncLocalStorage` map through continuation callbacks via
1528+
`ContinuationPreservedEmbedderData`. The option is on by default; passing
1529+
`--no-async-context-frame` disables it, leaving every sample's `labels` object
1530+
empty. A one-time `process.emitWarning` fires at first label use when the
1531+
option is off.
1532+
14491533
## Class: `v8.GCProfiler`
14501534
14511535
<!-- YAML
@@ -1618,6 +1702,57 @@ added: v26.1.0
16181702
16191703
> Stability: 1 - Experimental
16201704
1705+
### `syncHeapProfileHandle.getAllocationProfile()`
1706+
1707+
<!-- YAML
1708+
added: REPLACEME
1709+
-->
1710+
1711+
* Returns: {Object | undefined}
1712+
1713+
Returns the current allocation profile without stopping the profiler, or
1714+
`undefined` if the handle has already been stopped or if its session was
1715+
superseded by a newer one started on the same binding (for example when
1716+
an inspector `HeapProfiler.stopSampling` call ended the handle's V8
1717+
session out of band and a new session was subsequently started). In the
1718+
superseded case the handle carries a stale session identity and will never
1719+
return a profile; call `stop()` on the new handle instead. The method is
1720+
always available regardless of the `labels` option. For sessions started with
1721+
`labels: true`, each sample's `labels` object is populated with the
1722+
active label context at allocation time and `externalBytes` is included
1723+
when labelled backing stores are live. For sessions started without
1724+
`labels: true`, each sample carries an empty `labels` object and
1725+
`externalBytes` is omitted.
1726+
1727+
```json
1728+
{
1729+
"samples": [
1730+
{ "nodeId": 1, "size": 128, "count": 4, "sampleId": 42,
1731+
"labels": { "route": "/users/:id" } }
1732+
],
1733+
"externalBytes": [
1734+
{ "labels": { "route": "/users/:id" }, "bytes": 1048576 }
1735+
]
1736+
}
1737+
```
1738+
1739+
* `samples[].labels` — key-value string pairs from the active label context
1740+
at allocation time. Empty object if no labels were active. The object is
1741+
**frozen** and **shared** across all samples captured under the same
1742+
active label context — mutating it throws in strict mode.
1743+
* `externalBytes[]` — live `Buffer`/`ArrayBuffer` backing-store bytes per
1744+
label context. Omitted when the profiling allocator is inactive, when no
1745+
labelled backing stores are live, or when all live stores were allocated
1746+
under an empty label set.
1747+
1748+
**Label memory model.** The V8 heap profiler retains one copy of each
1749+
distinct label set for the profiler's lifetime. In the default mode
1750+
(`includeObjectsCollectedByMajorGC: false`) samples are dropped when their
1751+
objects are GC'd. With `includeObjectsCollectedByMajorGC: true` or
1752+
`includeObjectsCollectedByMinorGC: true`, dead samples are kept and each
1753+
unique label set pins one internal array for the profiler's lifetime —
1754+
label cardinality should be bounded to avoid unbounded growth.
1755+
16211756
### `syncHeapProfileHandle.stop()`
16221757
16231758
<!-- YAML
@@ -1626,7 +1761,17 @@ added: v26.1.0
16261761
16271762
* Returns: {string}
16281763
1629-
Stopping collecting the profile and return the profile data.
1764+
Stops the profiler and returns the profile as a DevTools-compatible JSON
1765+
string. Each sample in the returned JSON contains `size`, `nodeId`, and
1766+
`ordinal` fields. The returned string does not include `labels` or
1767+
`externalBytes`; labelled allocation data is available mid-session through
1768+
`syncHeapProfileHandle.getAllocationProfile()`.
1769+
1770+
Returns `undefined` without stopping anything if `stop()` is called on a
1771+
handle whose session was superseded (see `getAllocationProfile()` above).
1772+
Throws `ERR_HEAP_PROFILE_NOT_STARTED` if `stop()` is called on a handle
1773+
whose V8 session was ended out of band and no subsequent session has yet
1774+
started.
16301775
16311776
### `syncHeapProfileHandle[Symbol.dispose]()`
16321777
@@ -1804,6 +1949,14 @@ added: v26.1.0
18041949
by major GC. **Default:** `false`.
18051950
* `includeObjectsCollectedByMinorGC` {boolean} Include objects collected
18061951
by minor GC. **Default:** `false`.
1952+
* `labels` {boolean} Enable per-allocation label attribution via
1953+
[`v8.withHeapProfileLabels()`][] and [`v8.setHeapProfileLabels()`][].
1954+
When `true`, each sample's `labels` object in `getAllocationProfile()`
1955+
is populated with the active label context at allocation time, and
1956+
`externalBytes` entries are included when labelled backing stores are
1957+
live. Labelled data is only available through `getAllocationProfile()`;
1958+
`stop()` always returns the unlabelled DevTools JSON string.
1959+
**Default:** `false`.
18071960
* Returns: {SyncHeapProfileHandle}
18081961
18091962
Starting a heap profile then return a `SyncHeapProfileHandle` object.
@@ -1887,7 +2040,9 @@ console.log(profile);
18872040
[`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer
18882041
[`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer
18892042
[`settled` callback]: #settledpromise
2043+
[`v8.setHeapProfileLabels()`]: #v8setheapprofilelabelslabels
18902044
[`v8.stopCoverage()`]: #v8stopcoverage
18912045
[`v8.takeCoverage()`]: #v8takecoverage
2046+
[`v8.withHeapProfileLabels()`]: #v8withheapprofilelabelslabels-fn
18922047
[`vm.Script`]: vm.md#new-vmscriptcode-options
18932048
[worker threads]: worker_threads.md

‎doc/api/worker_threads.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,12 @@ added:
19991999
Starting a Heap profile then return a Promise that fulfills with an error
20002000
or an `HeapProfileHandle` object. This API supports `await using` syntax.
20012001
2002+
The `labels` option from [`v8.startHeapProfile()`][] is not supported
2003+
when profiling a worker from the parent thread and throws
2004+
`ERR_INVALID_ARG_VALUE`. To use labelled heap profiling inside a worker,
2005+
call `v8.startHeapProfile({ labels: true })` from within the worker
2006+
script itself.
2007+
20022008
```cjs
20032009
const { Worker } = require('node:worker_threads');
20042010
@@ -2275,6 +2281,7 @@ thread spawned will spawn another until the application crashes.
22752281
[`trace_events`]: tracing.md
22762282
[`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshotoptions
22772283
[`v8.getHeapStatistics()`]: v8.md#v8getheapstatistics
2284+
[`v8.startHeapProfile()`]: v8.md#v8startheapprofileoptions
22782285
[`vm`]: vm.md
22792286
[`worker.SHARE_ENV`]: #worker_threadsshare_env
22802287
[`worker.on('message')`]: #event-message_1

0 commit comments

Comments
 (0)