From d360041006a71fb9bebacd2008823bc4f0bf56f0 Mon Sep 17 00:00:00 2001 From: ishabi Date: Fri, 10 Apr 2026 22:52:49 +0200 Subject: [PATCH] v8: add cpu profile options --- doc/api/v8.md | 8 ++- doc/api/worker_threads.md | 8 ++- lib/internal/v8/cpu_profiler.js | 56 ++++++++++++++++++++ lib/internal/worker.js | 16 ++++-- lib/v8.js | 14 ++++- src/env.cc | 12 +++-- src/env.h | 2 +- src/node_v8.cc | 3 +- src/node_worker.cc | 7 +-- src/util.cc | 14 +++++ src/util.h | 8 +++ test/parallel/test-v8-cpu-profile.js | 67 +++++++++++++++++++++--- test/parallel/test-worker-cpu-profile.js | 13 +++++ 13 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 lib/internal/v8/cpu_profiler.js diff --git a/doc/api/v8.md b/doc/api/v8.md index bf868d5e74491d..da225a333ddcac 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1771,7 +1771,7 @@ writeString('hello'); writeString('你好'); ``` -## `v8.startCpuProfile()` +## `v8.startCpuProfile([options])` +* `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); ``` diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 6fee0d8368a914..b482725ee0fac1 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -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])` +* `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 @@ -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(); diff --git a/lib/internal/v8/cpu_profiler.js b/lib/internal/v8/cpu_profiler.js new file mode 100644 index 00000000000000..4bfda1bced3e42 --- /dev/null +++ b/lib/internal/v8/cpu_profiler.js @@ -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, +}; diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 0b0d91171f0ba5..9c0dd03cac6726 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -71,7 +71,6 @@ const { validateObject, validateNumber, } = require('internal/validators'); -let normalizeHeapProfileOptions; const { throwIfBuildingSnapshot, } = require('internal/v8/startup_snapshot'); @@ -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(); @@ -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) => { diff --git a/lib/v8.js b/lib/v8.js index 4bea3a9f1d3fbb..bb174f8d524305 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -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) { @@ -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); } diff --git a/src/env.cc b/src/env.cc index 04807ffae13437..c8f8ff066cd1d3 100644 --- a/src/env.cc +++ b/src/env.cc @@ -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); } diff --git a/src/env.h b/src/env.h index 5fabc366c6e68b..5414edb1aea8ef 100644 --- a/src/env.h +++ b/src/env.h @@ -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_ diff --git a/src/node_v8.cc b/src/node_v8.cc index 350e70cbc8663b..fc0f16d340bcd5 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -250,7 +250,8 @@ void SetFlagsFromString(const FunctionCallbackInfo& args) { void StartCpuProfile(const FunctionCallbackInfo& 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"); diff --git a/src/node_worker.cc b/src/node_worker.cc index d49b278b19031d..7d131f61cf97a2 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -923,6 +923,7 @@ void Worker::StartCpuProfile(const FunctionCallbackInfo& 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 wrap; @@ -935,9 +936,9 @@ void Worker::StartCpuProfile(const FunctionCallbackInfo& args) { BaseObjectPtr taker = MakeDetachedBaseObject(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(); diff --git a/src/util.cc b/src/util.cc index 6d08d8e71145a1..4bc7ddea92905a 100644 --- a/src/util.cc +++ b/src/util.cc @@ -894,4 +894,18 @@ HeapProfileOptions ParseHeapProfileOptions( return options; } +CpuProfileOptions ParseCpuProfileOptions( + const v8::FunctionCallbackInfo& args) { + CpuProfileOptions options; + CHECK_LE(args.Length(), 2); + if (args.Length() > 0) { + CHECK(args[0]->IsInt32()); + options.sampling_interval_us = args[0].As()->Value(); + } + if (args.Length() > 1) { + CHECK(args[1]->IsUint32()); + options.max_samples = args[1].As()->Value(); + } + return options; +} } // namespace node diff --git a/src/util.h b/src/util.h index ac4686c9b2d57b..cbda68d6c96a20 100644 --- a/src/util.h +++ b/src/util.h @@ -1078,6 +1078,14 @@ struct HeapProfileOptions { HeapProfileOptions ParseHeapProfileOptions( const v8::FunctionCallbackInfo& args); +struct CpuProfileOptions { + int sampling_interval_us = 0; + uint32_t max_samples = v8::CpuProfilingOptions::kNoSampleLimit; +}; + +CpuProfileOptions ParseCpuProfileOptions( + const v8::FunctionCallbackInfo& args); + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/parallel/test-v8-cpu-profile.js b/test/parallel/test-v8-cpu-profile.js index 3925e2a44fef6c..5a7953e9852818 100644 --- a/test/parallel/test-v8-cpu-profile.js +++ b/test/parallel/test-v8-cpu-profile.js @@ -1,12 +1,65 @@ '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 handle = v8.startCpuProfile(); + burnBlocks(150, 1500); + const profile = JSON.parse(handle.stop()); + assert.ok(typeof profile === 'object'); + assert.ok(Array.isArray(profile.samples)); + assert.ok(profile.samples.length > 0); +} + +{ + const profile = collectProfile({ sampleInterval: 0.001 }, () => burnBlocks(400, 2000)); + assert.ok(profile.samples.length >= 1); +} + +{ + const fastSampling = collectProfile({ sampleInterval: 0.001 }, () => burnBlocks(400, 2000)); + const slowSampling = collectProfile({ sampleInterval: 50 }, () => burnBlocks(400, 2000)); + assert.ok(fastSampling.samples.length > slowSampling.samples.length); + assert.ok(slowSampling.samples.length > 0); +} + +{ + 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' })); +} diff --git a/test/parallel/test-worker-cpu-profile.js b/test/parallel/test-worker-cpu-profile.js index dbbe6adad21c39..f7b790b6eb89f3 100644 --- a/test/parallel/test-worker-cpu-profile.js +++ b/test/parallel/test-worker-cpu-profile.js @@ -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(), @@ -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' });