Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,30 @@ added:

Stopping collecting the profile and the profile will be discarded.

## Class: `SyncHeapProfileHandle`

<!-- YAML
added: REPLACEME
-->

### `syncHeapProfileHandle.stop()`

<!-- YAML
added: REPLACEME
-->

* Returns: {string}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to return a Buffer instead. It'd give us more flexibility to add binary formats in the future. I'd like to add support at some point in the future for the pprof-derived format being defined in OpenTelemetry at the moment, which is a binary format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC worker.stopHeapProfile() already returns a JSON since v22, so changing the return type to Buffer would be a breaking change. For now this PR keeps the main thread consistent with the worker.
We could add a format option { format: 'json' | 'pprof' } in a follow up PR when binary format support is needed. wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, going for parity with the worker API. We could probably do something similar to fs.readFile(path, encoding, cb) where the encoding value switches the result between a string and a buffer depending on the format. I think format: 'json' | 'pprof' seems a suitable way to deal with that. 🙂


Stopping collecting the profile and return the profile data.

### `syncHeapProfileHandle[Symbol.dispose]()`

<!-- YAML
added: REPLACEME
-->

Stopping collecting the profile and the profile will be discarded.

## Class: `CPUProfileHandle`

<!-- YAML
Expand Down Expand Up @@ -1764,6 +1788,72 @@ const profile = handle.stop();
console.log(profile);
```

## `v8.startHeapProfile([options])`

<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `sampleInterval` {number} The average sampling interval in bytes.
**Default:** `524288` (512 KiB).
* `stackDepth` {integer} The maximum stack depth for samples.
**Default:** `16`.
* `forceGC` {boolean} Force garbage collection before taking the profile.
**Default:** `false`.
* `includeObjectsCollectedByMajorGC` {boolean} Include objects collected
by major GC. **Default:** `false`.
* `includeObjectsCollectedByMinorGC` {boolean} Include objects collected
by minor GC. **Default:** `false`.
* Returns: {SyncHeapProfileHandle}

Starting a heap profile then return a `SyncHeapProfileHandle` object.
This API supports `using` syntax.

```cjs
const v8 = require('node:v8');

const handle = v8.startHeapProfile();
const profile = handle.stop();
console.log(profile);
```

```mjs
import v8 from 'node:v8';

const handle = v8.startHeapProfile();
const profile = handle.stop();
console.log(profile);
```

With custom parameters:

```cjs
const v8 = require('node:v8');

const handle = v8.startHeapProfile({
sampleInterval: 1024,
stackDepth: 8,
forceGC: true,
includeObjectsCollectedByMajorGC: true,
});
const profile = handle.stop();
console.log(profile);
```

```mjs
import v8 from 'node:v8';

const handle = v8.startHeapProfile({
sampleInterval: 1024,
stackDepth: 8,
forceGC: true,
includeObjectsCollectedByMajorGC: true,
});
const profile = handle.stop();
console.log(profile);
```

[CppHeap]: https://v8docs.nodesource.com/node-22.4/d9/dc4/classv8_1_1_cpp_heap.html
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[Hook Callbacks]: #hook-callbacks
Expand Down
43 changes: 42 additions & 1 deletion doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -2005,14 +2005,25 @@ w.on('online', async () => {
});
```
### `worker.startHeapProfile()`
### `worker.startHeapProfile([options])`
<!-- YAML
added:
- v24.9.0
- v22.20.0
-->
* `options` {Object}
* `sampleInterval` {number} The average sampling interval in bytes.
**Default:** `524288` (512 KiB).
* `stackDepth` {integer} The maximum stack depth for samples.
**Default:** `16`.
* `forceGC` {boolean} Force garbage collection before taking the profile.
**Default:** `false`.
* `includeObjectsCollectedByMajorGC` {boolean} Include objects collected
by major GC. **Default:** `false`.
* `includeObjectsCollectedByMinorGC` {boolean} Include objects collected
by minor GC. **Default:** `false`.
* Returns: {Promise}
Starting a Heap profile then return a Promise that fulfills with an error
Expand All @@ -2034,6 +2045,22 @@ worker.on('online', async () => {
});
```
```mjs
import { Worker } from 'node:worker_threads';
const worker = new Worker(`
const { parentPort } = require('node:worker_threads');
parentPort.on('message', () => {});
`, { eval: true });
worker.on('online', async () => {
const handle = await worker.startHeapProfile();
const profile = await handle.stop();
console.log(profile);
worker.terminate();
});
```
`await using` example.
```cjs
Expand All @@ -2050,6 +2077,20 @@ w.on('online', async () => {
});
```
```mjs
import { Worker } from 'node:worker_threads';
const w = new Worker(`
const { parentPort } = require('node:worker_threads');
parentPort.on('message', () => {});
`, { eval: true });
w.on('online', async () => {
// Stop profile automatically when return and profile will be discarded
await using handle = await w.startHeapProfile();
});
```
### `worker.stderr`
<!-- YAML
Expand Down
49 changes: 49 additions & 0 deletions lib/internal/v8/heap_profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const {
validateBoolean,
validateInteger,
validateInt32,
validateObject,
} = require('internal/validators');

const {
kSamplingNoFlags,
kSamplingForceGC,
kSamplingIncludeObjectsCollectedByMajorGC,
kSamplingIncludeObjectsCollectedByMinorGC,
} = internalBinding('v8');

function normalizeHeapProfileOptions(options = {}) {
validateObject(options, 'options');
const {
sampleInterval = 512 * 1024,
stackDepth = 16,
forceGC = false,
includeObjectsCollectedByMajorGC = false,
includeObjectsCollectedByMinorGC = false,
} = options;

validateInteger(sampleInterval, 'options.sampleInterval', 1);
validateInt32(stackDepth, 'options.stackDepth', 0);
validateBoolean(forceGC, 'options.forceGC');
validateBoolean(includeObjectsCollectedByMajorGC,
'options.includeObjectsCollectedByMajorGC');
validateBoolean(includeObjectsCollectedByMinorGC,
'options.includeObjectsCollectedByMinorGC');

let flags = kSamplingNoFlags;
if (forceGC) flags |= kSamplingForceGC;
if (includeObjectsCollectedByMajorGC) {
flags |= kSamplingIncludeObjectsCollectedByMajorGC;
}
if (includeObjectsCollectedByMinorGC) {
flags |= kSamplingIncludeObjectsCollectedByMinorGC;
}

return { sampleInterval, stackDepth, flags };
}

module.exports = {
normalizeHeapProfileOptions,
};
26 changes: 23 additions & 3 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ const {
constructSharedArrayBuffer,
kEmptyObject,
} = require('internal/util');
const { validateArray, validateString, validateObject, validateNumber } = require('internal/validators');
const {
validateArray,
validateString,
validateObject,
validateNumber,
} = require('internal/validators');
let normalizeHeapProfileOptions;
const {
throwIfBuildingSnapshot,
} = require('internal/v8/startup_snapshot');
Expand Down Expand Up @@ -582,8 +588,22 @@ class Worker extends EventEmitter {
});
}

startHeapProfile() {
const startTaker = this[kHandle]?.startHeapProfile();
/**
* @param {object} [options]
* @param {number} [options.sampleInterval]
* @param {number} [options.stackDepth]
* @param {boolean} [options.forceGC]
* @param {boolean} [options.includeObjectsCollectedByMajorGC]
* @param {boolean} [options.includeObjectsCollectedByMinorGC]
* @returns {Promise}
*/
startHeapProfile(options) {
normalizeHeapProfileOptions ??=
require('internal/v8/heap_profile').normalizeHeapProfileOptions;
const { sampleInterval, stackDepth, flags } =
normalizeHeapProfileOptions(options);
const startTaker = this[kHandle]?.startHeapProfile(
sampleInterval, stackDepth, flags);
return new Promise((resolve, reject) => {
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
startTaker.ondone = (err) => {
Expand Down
41 changes: 40 additions & 1 deletion lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const {
const { Buffer } = require('buffer');
const {
validateString,
validateUint32,
validateOneOf,
validateUint32,
} = require('internal/validators');
const {
Serializer,
Expand All @@ -50,6 +50,9 @@ const {
const {
namespace: startupSnapshot,
} = require('internal/v8/startup_snapshot');
const {
normalizeHeapProfileOptions,
} = require('internal/v8/heap_profile');

let profiler = {};
if (internalBinding('config').hasInspector) {
Expand Down Expand Up @@ -115,6 +118,8 @@ const {
setFlagsFromString: _setFlagsFromString,
startCpuProfile: _startCpuProfile,
stopCpuProfile: _stopCpuProfile,
startHeapProfile: _startHeapProfile,
stopHeapProfile: _stopHeapProfile,
isStringOneByteRepresentation: _isStringOneByteRepresentation,
updateHeapStatisticsBuffer,
updateHeapSpaceStatisticsBuffer,
Expand Down Expand Up @@ -191,6 +196,22 @@ class SyncCPUProfileHandle {
}
}

class SyncHeapProfileHandle {
#stopped = false;

stop() {
if (this.#stopped) {
return;
}
this.#stopped = true;
return _stopHeapProfile();
};

[SymbolDispose]() {
this.stop();
}
}

/**
* Starting CPU Profile.
* @returns {SyncCPUProfileHandle}
Expand All @@ -200,6 +221,23 @@ function startCpuProfile() {
return new SyncCPUProfileHandle(id);
}

/**
* Starting Heap Profile.
* @param {object} [options]
* @param {number} [options.sampleInterval]
* @param {number} [options.stackDepth]
* @param {boolean} [options.forceGC]
* @param {boolean} [options.includeObjectsCollectedByMajorGC]
* @param {boolean} [options.includeObjectsCollectedByMinorGC]
* @returns {SyncHeapProfileHandle}
*/
function startHeapProfile(options) {
const { sampleInterval, stackDepth, flags } =
normalizeHeapProfileOptions(options);
_startHeapProfile(sampleInterval, stackDepth, flags);
return new SyncHeapProfileHandle();
}

/**
* Return whether this string uses one byte as underlying representation or not.
* @param {string} content
Expand Down Expand Up @@ -518,4 +556,5 @@ module.exports = {
GCProfiler,
isStringOneByteRepresentation,
startCpuProfile,
startHeapProfile,
};
Loading
Loading