Skip to content

Commit f8b79a1

Browse files
authored
v8: add heap profile API
PR-URL: #62273 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 79671cf commit f8b79a1

12 files changed

Lines changed: 518 additions & 70 deletions

File tree

doc/api/v8.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,32 @@ added:
16171617
16181618
Stopping collecting the profile and the profile will be discarded.
16191619
1620+
## Class: `SyncHeapProfileHandle`
1621+
1622+
<!-- YAML
1623+
added: REPLACEME
1624+
-->
1625+
1626+
> Stability: 1 - Experimental
1627+
1628+
### `syncHeapProfileHandle.stop()`
1629+
1630+
<!-- YAML
1631+
added: REPLACEME
1632+
-->
1633+
1634+
* Returns: {string}
1635+
1636+
Stopping collecting the profile and return the profile data.
1637+
1638+
### `syncHeapProfileHandle[Symbol.dispose]()`
1639+
1640+
<!-- YAML
1641+
added: REPLACEME
1642+
-->
1643+
1644+
Stopping collecting the profile and the profile will be discarded.
1645+
16201646
## Class: `CPUProfileHandle`
16211647
16221648
<!-- YAML
@@ -1764,6 +1790,72 @@ const profile = handle.stop();
17641790
console.log(profile);
17651791
```
17661792
1793+
## `v8.startHeapProfile([options])`
1794+
1795+
<!-- YAML
1796+
added: REPLACEME
1797+
-->
1798+
1799+
* `options` {Object}
1800+
* `sampleInterval` {number} The average sampling interval in bytes.
1801+
**Default:** `524288` (512 KiB).
1802+
* `stackDepth` {integer} The maximum stack depth for samples.
1803+
**Default:** `16`.
1804+
* `forceGC` {boolean} Force garbage collection before taking the profile.
1805+
**Default:** `false`.
1806+
* `includeObjectsCollectedByMajorGC` {boolean} Include objects collected
1807+
by major GC. **Default:** `false`.
1808+
* `includeObjectsCollectedByMinorGC` {boolean} Include objects collected
1809+
by minor GC. **Default:** `false`.
1810+
* Returns: {SyncHeapProfileHandle}
1811+
1812+
Starting a heap profile then return a `SyncHeapProfileHandle` object.
1813+
This API supports `using` syntax.
1814+
1815+
```cjs
1816+
const v8 = require('node:v8');
1817+
1818+
const handle = v8.startHeapProfile();
1819+
const profile = handle.stop();
1820+
console.log(profile);
1821+
```
1822+
1823+
```mjs
1824+
import v8 from 'node:v8';
1825+
1826+
const handle = v8.startHeapProfile();
1827+
const profile = handle.stop();
1828+
console.log(profile);
1829+
```
1830+
1831+
With custom parameters:
1832+
1833+
```cjs
1834+
const v8 = require('node:v8');
1835+
1836+
const handle = v8.startHeapProfile({
1837+
sampleInterval: 1024,
1838+
stackDepth: 8,
1839+
forceGC: true,
1840+
includeObjectsCollectedByMajorGC: true,
1841+
});
1842+
const profile = handle.stop();
1843+
console.log(profile);
1844+
```
1845+
1846+
```mjs
1847+
import v8 from 'node:v8';
1848+
1849+
const handle = v8.startHeapProfile({
1850+
sampleInterval: 1024,
1851+
stackDepth: 8,
1852+
forceGC: true,
1853+
includeObjectsCollectedByMajorGC: true,
1854+
});
1855+
const profile = handle.stop();
1856+
console.log(profile);
1857+
```
1858+
17671859
[CppHeap]: https://v8docs.nodesource.com/node-22.4/d9/dc4/classv8_1_1_cpp_heap.html
17681860
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
17691861
[Hook Callbacks]: #hook-callbacks

doc/api/worker_threads.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2005,14 +2005,25 @@ w.on('online', async () => {
20052005
});
20062006
```
20072007
2008-
### `worker.startHeapProfile()`
2008+
### `worker.startHeapProfile([options])`
20092009
20102010
<!-- YAML
20112011
added:
20122012
- v24.9.0
20132013
- v22.20.0
20142014
-->
20152015
2016+
* `options` {Object}
2017+
* `sampleInterval` {number} The average sampling interval in bytes.
2018+
**Default:** `524288` (512 KiB).
2019+
* `stackDepth` {integer} The maximum stack depth for samples.
2020+
**Default:** `16`.
2021+
* `forceGC` {boolean} Force garbage collection before taking the profile.
2022+
**Default:** `false`.
2023+
* `includeObjectsCollectedByMajorGC` {boolean} Include objects collected
2024+
by major GC. **Default:** `false`.
2025+
* `includeObjectsCollectedByMinorGC` {boolean} Include objects collected
2026+
by minor GC. **Default:** `false`.
20162027
* Returns: {Promise}
20172028
20182029
Starting a Heap profile then return a Promise that fulfills with an error
@@ -2034,6 +2045,22 @@ worker.on('online', async () => {
20342045
});
20352046
```
20362047
2048+
```mjs
2049+
import { Worker } from 'node:worker_threads';
2050+
2051+
const worker = new Worker(`
2052+
const { parentPort } = require('node:worker_threads');
2053+
parentPort.on('message', () => {});
2054+
`, { eval: true });
2055+
2056+
worker.on('online', async () => {
2057+
const handle = await worker.startHeapProfile();
2058+
const profile = await handle.stop();
2059+
console.log(profile);
2060+
worker.terminate();
2061+
});
2062+
```
2063+
20372064
`await using` example.
20382065
20392066
```cjs
@@ -2050,6 +2077,20 @@ w.on('online', async () => {
20502077
});
20512078
```
20522079
2080+
```mjs
2081+
import { Worker } from 'node:worker_threads';
2082+
2083+
const w = new Worker(`
2084+
const { parentPort } = require('node:worker_threads');
2085+
parentPort.on('message', () => {});
2086+
`, { eval: true });
2087+
2088+
w.on('online', async () => {
2089+
// Stop profile automatically when return and profile will be discarded
2090+
await using handle = await w.startHeapProfile();
2091+
});
2092+
```
2093+
20532094
### `worker.stderr`
20542095
20552096
<!-- YAML

lib/internal/v8/heap_profile.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
const {
4+
validateBoolean,
5+
validateInteger,
6+
validateInt32,
7+
validateObject,
8+
} = require('internal/validators');
9+
10+
const {
11+
kSamplingNoFlags,
12+
kSamplingForceGC,
13+
kSamplingIncludeObjectsCollectedByMajorGC,
14+
kSamplingIncludeObjectsCollectedByMinorGC,
15+
} = internalBinding('v8');
16+
17+
function normalizeHeapProfileOptions(options = {}) {
18+
validateObject(options, 'options');
19+
const {
20+
sampleInterval = 512 * 1024,
21+
stackDepth = 16,
22+
forceGC = false,
23+
includeObjectsCollectedByMajorGC = false,
24+
includeObjectsCollectedByMinorGC = false,
25+
} = options;
26+
27+
validateInteger(sampleInterval, 'options.sampleInterval', 1);
28+
validateInt32(stackDepth, 'options.stackDepth', 0);
29+
validateBoolean(forceGC, 'options.forceGC');
30+
validateBoolean(includeObjectsCollectedByMajorGC,
31+
'options.includeObjectsCollectedByMajorGC');
32+
validateBoolean(includeObjectsCollectedByMinorGC,
33+
'options.includeObjectsCollectedByMinorGC');
34+
35+
let flags = kSamplingNoFlags;
36+
if (forceGC) flags |= kSamplingForceGC;
37+
if (includeObjectsCollectedByMajorGC) {
38+
flags |= kSamplingIncludeObjectsCollectedByMajorGC;
39+
}
40+
if (includeObjectsCollectedByMinorGC) {
41+
flags |= kSamplingIncludeObjectsCollectedByMinorGC;
42+
}
43+
44+
return { sampleInterval, stackDepth, flags };
45+
}
46+
47+
module.exports = {
48+
normalizeHeapProfileOptions,
49+
};

lib/internal/worker.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ const {
6565
constructSharedArrayBuffer,
6666
kEmptyObject,
6767
} = require('internal/util');
68-
const { validateArray, validateString, validateObject, validateNumber } = require('internal/validators');
68+
const {
69+
validateArray,
70+
validateString,
71+
validateObject,
72+
validateNumber,
73+
} = require('internal/validators');
74+
let normalizeHeapProfileOptions;
6975
const {
7076
throwIfBuildingSnapshot,
7177
} = require('internal/v8/startup_snapshot');
@@ -582,8 +588,22 @@ class Worker extends EventEmitter {
582588
});
583589
}
584590

585-
startHeapProfile() {
586-
const startTaker = this[kHandle]?.startHeapProfile();
591+
/**
592+
* @param {object} [options]
593+
* @param {number} [options.sampleInterval]
594+
* @param {number} [options.stackDepth]
595+
* @param {boolean} [options.forceGC]
596+
* @param {boolean} [options.includeObjectsCollectedByMajorGC]
597+
* @param {boolean} [options.includeObjectsCollectedByMinorGC]
598+
* @returns {Promise}
599+
*/
600+
startHeapProfile(options) {
601+
normalizeHeapProfileOptions ??=
602+
require('internal/v8/heap_profile').normalizeHeapProfileOptions;
603+
const { sampleInterval, stackDepth, flags } =
604+
normalizeHeapProfileOptions(options);
605+
const startTaker = this[kHandle]?.startHeapProfile(
606+
sampleInterval, stackDepth, flags);
587607
return new Promise((resolve, reject) => {
588608
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
589609
startTaker.ondone = (err) => {

lib/v8.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ const {
4040
const { Buffer } = require('buffer');
4141
const {
4242
validateString,
43-
validateUint32,
4443
validateOneOf,
44+
validateUint32,
4545
} = require('internal/validators');
4646
const {
4747
Serializer,
@@ -50,6 +50,9 @@ const {
5050
const {
5151
namespace: startupSnapshot,
5252
} = require('internal/v8/startup_snapshot');
53+
const {
54+
normalizeHeapProfileOptions,
55+
} = require('internal/v8/heap_profile');
5356

5457
let profiler = {};
5558
if (internalBinding('config').hasInspector) {
@@ -115,6 +118,8 @@ const {
115118
setFlagsFromString: _setFlagsFromString,
116119
startCpuProfile: _startCpuProfile,
117120
stopCpuProfile: _stopCpuProfile,
121+
startHeapProfile: _startHeapProfile,
122+
stopHeapProfile: _stopHeapProfile,
118123
isStringOneByteRepresentation: _isStringOneByteRepresentation,
119124
updateHeapStatisticsBuffer,
120125
updateHeapSpaceStatisticsBuffer,
@@ -191,6 +196,22 @@ class SyncCPUProfileHandle {
191196
}
192197
}
193198

199+
class SyncHeapProfileHandle {
200+
#stopped = false;
201+
202+
stop() {
203+
if (this.#stopped) {
204+
return;
205+
}
206+
this.#stopped = true;
207+
return _stopHeapProfile();
208+
};
209+
210+
[SymbolDispose]() {
211+
this.stop();
212+
}
213+
}
214+
194215
/**
195216
* Starting CPU Profile.
196217
* @returns {SyncCPUProfileHandle}
@@ -200,6 +221,23 @@ function startCpuProfile() {
200221
return new SyncCPUProfileHandle(id);
201222
}
202223

224+
/**
225+
* Starting Heap Profile.
226+
* @param {object} [options]
227+
* @param {number} [options.sampleInterval]
228+
* @param {number} [options.stackDepth]
229+
* @param {boolean} [options.forceGC]
230+
* @param {boolean} [options.includeObjectsCollectedByMajorGC]
231+
* @param {boolean} [options.includeObjectsCollectedByMinorGC]
232+
* @returns {SyncHeapProfileHandle}
233+
*/
234+
function startHeapProfile(options) {
235+
const { sampleInterval, stackDepth, flags } =
236+
normalizeHeapProfileOptions(options);
237+
_startHeapProfile(sampleInterval, stackDepth, flags);
238+
return new SyncHeapProfileHandle();
239+
}
240+
203241
/**
204242
* Return whether this string uses one byte as underlying representation or not.
205243
* @param {string} content
@@ -518,4 +556,5 @@ module.exports = {
518556
GCProfiler,
519557
isStringOneByteRepresentation,
520558
startCpuProfile,
559+
startHeapProfile,
521560
};

0 commit comments

Comments
 (0)