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
8 changes: 6 additions & 2 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -1771,21 +1771,25 @@ writeString('hello');
writeString('你好');
```

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

<!-- YAML
added:
- v25.0.0
- v24.12.0
-->

* `options` {Object}
* `sampleInterval` {number} Requested sampling interval in milliseconds. **Default:** `0`.
* `maxBufferSize` {integer} Maximum number of samples to keep before older
entries are discarded. **Default:** `4294967295`.
* Returns: {SyncCPUProfileHandle}

Starting a CPU profile then return a `SyncCPUProfileHandle` object.
This API supports `using` syntax.

```cjs
const handle = v8.startCpuProfile();
const handle = v8.startCpuProfile({ sampleInterval: 1, maxBufferSize: 10_000 });
const profile = handle.stop();
console.log(profile);
```
Expand Down
8 changes: 6 additions & 2 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -1962,12 +1962,16 @@ this matches its values.

If the worker has stopped, the return value is an empty object.

### `worker.startCpuProfile()`
### `worker.startCpuProfile([options])`

<!-- YAML
added: v24.8.0
-->

* `options` {Object}
* `sampleInterval` {number} Requested sampling interval in milliseconds. **Default:** `0`.
* `maxBufferSize` {integer} Maximum number of samples to retain.
**Default:** `4294967295`.
* Returns: {Promise}

Starting a CPU profile then return a Promise that fulfills with an error
Expand All @@ -1982,7 +1986,7 @@ const worker = new Worker(`
`, { eval: true });

worker.on('online', async () => {
const handle = await worker.startCpuProfile();
const handle = await worker.startCpuProfile({ sampleInterval: 1 });
const profile = await handle.stop();
console.log(profile);
worker.terminate();
Expand Down
56 changes: 56 additions & 0 deletions lib/internal/v8/cpu_profiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const {
MathFloor,
} = primordials;

const {
validateNumber,
validateObject,
} = require('internal/validators');

const kMicrosPerMilli = 1_000;
const kMaxSamplingIntervalUs = 0x7FFFFFFF;
const kMaxSamplingIntervalMs = kMaxSamplingIntervalUs / kMicrosPerMilli;
const kMaxSamplesUnlimited = 0xFFFF_FFFF;

function normalizeCpuProfileOptions(options = {}) {
validateObject(options, 'options');

// TODO(ishabi): add support for 'mode' and 'filterContext' options
const {
sampleInterval,
maxBufferSize,
} = options;

let samplingIntervalMicros = 0;
if (sampleInterval !== undefined) {
validateNumber(sampleInterval,
'options.sampleInterval',
0,
kMaxSamplingIntervalMs);
samplingIntervalMicros = MathFloor(sampleInterval * kMicrosPerMilli);
if (sampleInterval > 0 && samplingIntervalMicros === 0) {
samplingIntervalMicros = 1;
}
}

const size = maxBufferSize;
let normalizedMaxSamples = kMaxSamplesUnlimited;
if (size !== undefined) {
validateNumber(size,
'options.maxBufferSize',
1,
kMaxSamplesUnlimited);
normalizedMaxSamples = MathFloor(size);
}

return {
samplingIntervalMicros,
maxSamples: normalizedMaxSamples,
};
}

module.exports = {
normalizeCpuProfileOptions,
};
16 changes: 12 additions & 4 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ const {
validateObject,
validateNumber,
} = require('internal/validators');
let normalizeHeapProfileOptions;
const {
throwIfBuildingSnapshot,
} = require('internal/v8/startup_snapshot');
Expand Down Expand Up @@ -111,6 +110,8 @@ const dc = require('diagnostics_channel');
const workerThreadsChannel = dc.channel('worker_threads');

let cwdCounter;
let normalizeHeapProfileOptions;
let normalizeCpuProfileOptions;

const environmentData = new SafeMap();

Expand Down Expand Up @@ -574,9 +575,16 @@ class Worker extends EventEmitter {
});
}

// TODO(theanarkh): add options, such as sample_interval, CpuProfilingMode
startCpuProfile() {
const startTaker = this[kHandle]?.startCpuProfile();
startCpuProfile(options) {
normalizeCpuProfileOptions ??=
require('internal/v8/cpu_profiler').normalizeCpuProfileOptions;
const {
samplingIntervalMicros,
maxSamples,
} = normalizeCpuProfileOptions(options);
const startTaker = this[kHandle]?.startCpuProfile(
samplingIntervalMicros,
maxSamples);
return new Promise((resolve, reject) => {
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
startTaker.ondone = (err, id) => {
Expand Down
14 changes: 12 additions & 2 deletions lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ const {
const {
normalizeHeapProfileOptions,
} = require('internal/v8/heap_profile');
const {
normalizeCpuProfileOptions,
} = require('internal/v8/cpu_profiler');

let profiler = {};
if (internalBinding('config').hasInspector) {
Expand Down Expand Up @@ -214,10 +217,17 @@ class SyncHeapProfileHandle {

/**
* Starting CPU Profile.
* @param {object} [options]
* @param {number} [options.sampleInterval]
* @param {number} [options.maxBufferSize]
* @returns {SyncCPUProfileHandle}
*/
function startCpuProfile() {
const id = _startCpuProfile();
function startCpuProfile(options) {
const {
samplingIntervalMicros,
maxSamples,
} = normalizeCpuProfileOptions(options);
const id = _startCpuProfile(samplingIntervalMicros, maxSamples);
return new SyncCPUProfileHandle(id);
}

Expand Down
12 changes: 8 additions & 4 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2261,14 +2261,18 @@ void Environment::RunWeakRefCleanup() {
isolate()->ClearKeptObjects();
}

v8::CpuProfilingResult Environment::StartCpuProfile() {
v8::CpuProfilingResult Environment::StartCpuProfile(
const CpuProfileOptions& options) {
HandleScope handle_scope(isolate());
if (!cpu_profiler_) {
cpu_profiler_ = v8::CpuProfiler::New(isolate());
}
v8::CpuProfilingResult result = cpu_profiler_->Start(
v8::CpuProfilingOptions{v8::CpuProfilingMode::kLeafNodeLineNumbers,
v8::CpuProfilingOptions::kNoSampleLimit});
v8::CpuProfilingOptions start_options(
v8::CpuProfilingMode::kLeafNodeLineNumbers,
options.max_samples,
options.sampling_interval_us);
v8::CpuProfilingResult result =
cpu_profiler_->Start(std::move(start_options));
if (result.status == v8::CpuProfilingStatus::kStarted) {
pending_profiles_.push_back(result.id);
}
Expand Down
2 changes: 1 addition & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@ class Environment final : public MemoryRetainer {

inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);

v8::CpuProfilingResult StartCpuProfile();
v8::CpuProfilingResult StartCpuProfile(const CpuProfileOptions& options);
v8::CpuProfile* StopCpuProfile(v8::ProfilerId profile_id);

// Field identifiers for exit_info_
Expand Down
3 changes: 2 additions & 1 deletion src/node_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ void SetFlagsFromString(const FunctionCallbackInfo<Value>& args) {
void StartCpuProfile(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
CpuProfilingResult result = env->StartCpuProfile();
CpuProfileOptions options = ParseCpuProfileOptions(args);
CpuProfilingResult result = env->StartCpuProfile(options);
if (result.status == CpuProfilingStatus::kErrorTooManyProfilers) {
return THROW_ERR_CPU_PROFILE_TOO_MANY(isolate,
"There are too many CPU profiles");
Expand Down
7 changes: 4 additions & 3 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ void Worker::StartCpuProfile(const FunctionCallbackInfo<Value>& args) {
Worker* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
Environment* env = w->env();
CpuProfileOptions options = ParseCpuProfileOptions(args);

AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
Local<Object> wrap;
Expand All @@ -935,9 +936,9 @@ void Worker::StartCpuProfile(const FunctionCallbackInfo<Value>& args) {
BaseObjectPtr<WorkerCpuProfileTaker> taker =
MakeDetachedBaseObject<WorkerCpuProfileTaker>(env, wrap);

bool scheduled = w->RequestInterrupt([taker = std::move(taker),
env](Environment* worker_env) mutable {
CpuProfilingResult result = worker_env->StartCpuProfile();
bool scheduled = w->RequestInterrupt([taker = std::move(taker), env, options](
Environment* worker_env) mutable {
CpuProfilingResult result = worker_env->StartCpuProfile(options);
env->SetImmediateThreadsafe(
[taker = std::move(taker), result = result](Environment* env) mutable {
Isolate* isolate = env->isolate();
Expand Down
14 changes: 14 additions & 0 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -894,4 +894,18 @@ HeapProfileOptions ParseHeapProfileOptions(
return options;
}

CpuProfileOptions ParseCpuProfileOptions(
const v8::FunctionCallbackInfo<v8::Value>& args) {
CpuProfileOptions options;
CHECK_LE(args.Length(), 2);
if (args.Length() > 0) {
CHECK(args[0]->IsInt32());
options.sampling_interval_us = args[0].As<v8::Int32>()->Value();
}
if (args.Length() > 1) {
CHECK(args[1]->IsUint32());
options.max_samples = args[1].As<v8::Uint32>()->Value();
}
return options;
}
} // namespace node
8 changes: 8 additions & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,14 @@ struct HeapProfileOptions {
HeapProfileOptions ParseHeapProfileOptions(
const v8::FunctionCallbackInfo<v8::Value>& args);

struct CpuProfileOptions {
int sampling_interval_us = 0;
uint32_t max_samples = v8::CpuProfilingOptions::kNoSampleLimit;
};

CpuProfileOptions ParseCpuProfileOptions(
const v8::FunctionCallbackInfo<v8::Value>& args);

} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
Expand Down
51 changes: 44 additions & 7 deletions test/parallel/test-v8-cpu-profile.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
'use strict';

require('../common');
const common = require('../common');
const assert = require('assert');
const v8 = require('v8');

const handle = v8.startCpuProfile();
const profile = handle.stop();
assert.ok(typeof profile === 'string');
assert.ok(profile.length > 0);
// Call stop() again
assert.ok(handle.stop() === undefined);
function busyLoop(iterations = 1000) {
let total = 0;
for (let i = 0; i < iterations; i++) {
total += Math.sqrt(i + total);
}
return total;
}

function burnBlocks(blocks = 200, iterations = 1000) {
let total = 0;
for (let i = 0; i < blocks; i++) {
total += busyLoop(iterations);
}
return total;
}

function collectProfile(options, workload) {
const handle = v8.startCpuProfile(options);
workload();
return JSON.parse(handle.stop());
}

{
const profile = collectProfile({ sampleInterval: 0.001 }, () => burnBlocks(400, 2000));
assert.ok(profile.samples.length >= 1);
}

{
const profile = collectProfile({ sampleInterval: 0.5, maxBufferSize: 3 }, () => burnBlocks(400, 2000));
assert.ok(profile.samples.length <= 3);
}

{
const profile = collectProfile({ sampleInterval: 0.1, maxBufferSize: 50 }, () => burnBlocks(600, 3000));
assert.ok(profile.samples.length >= 1);
assert.ok(profile.samples.length <= 50);
}

{
assert.throws(
() => v8.startCpuProfile({ sampleInterval: -1 }),
common.expectsError({ code: 'ERR_OUT_OF_RANGE' }));
}
13 changes: 13 additions & 0 deletions test/parallel/test-worker-cpu-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ worker.on('online', common.mustCall(async () => {
JSON.parse(await handle.stop());
}

{
const handle = await worker.startCpuProfile({
sampleInterval: 0.5,
maxBufferSize: 8,
});
JSON.parse(await handle.stop());
}

{
const [handle1, handle2] = await Promise.all([
worker.startCpuProfile(),
Expand All @@ -37,6 +45,11 @@ worker.on('online', common.mustCall(async () => {
}));

worker.once('exit', common.mustCall(async () => {
assert.throws(
() => worker.startCpuProfile({ maxBufferSize: 0 }),
common.expectsError({
code: 'ERR_OUT_OF_RANGE',
}));
await assert.rejects(worker.startCpuProfile(), {
code: 'ERR_WORKER_NOT_RUNNING'
});
Expand Down