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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,36 @@ when an error occurs (and is caught) during the creation of the
context, for example, when the allocation fails or the maximum call stack
size is reached when the context is created.

<a id="ERR_CPU_PROFILE_ALREADY_STARTED"></a>

### `ERR_CPU_PROFILE_ALREADY_STARTED`

<!-- YAML
added: REPLACEME
-->

The CPU profile with the given name is already started.

<a id="ERR_CPU_PROFILE_NOT_STARTED"></a>

### `ERR_CPU_PROFILE_NOT_STARTED`

<!-- YAML
added: REPLACEME
-->

The CPU profile with the given name is not started.

<a id="ERR_CPU_PROFILE_TOO_MANY"></a>

### `ERR_CPU_PROFILE_TOO_MANY`

<!-- YAML
added: REPLACEME
-->

There are too many CPU profiles being collected.

<a id="ERR_CRYPTO_ARGON2_NOT_SUPPORTED"></a>

### `ERR_CRYPTO_ARGON2_NOT_SUPPORTED`
Expand Down
30 changes: 30 additions & 0 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,36 @@ this matches its values.

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

### `worker.startCpuProfile(name)`

<!-- YAML
added: REPLACEME
-->

* name: {string}
* Returns: {Promise}

Starting a CPU profile with the given `name`, then return a Promise that fulfills
with an error or an object which has a `stop` method. Calling the `stop` method will
Copy link
Member

@Qard Qard Aug 29, 2025

Choose a reason for hiding this comment

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

Might want to document an actual object for that with its own methods and such. That way if we decide to add more methods or method parameters in the future we would have somewhere to document those additional things. For example, I would like to add support for outputting pprof data in a buffer, or possibly the OTel profiling format (which is heavily based on pprof) in the future if/when that stabilizes at some point.

To that end, and thinking toward the future, it may also make sense for the stop() method to return a container object with a json() method on it instead, similar to fetch, so other format methods could be added in the future. This could probably also help with code-sharing with #59429 too as you could have a container that just receives the profile data object from wherever and then does the serialization logic internally.

stop collecting the profile, then return a Promise that fulfills with an error or the
profile data.

```cjs
const { Worker } = require('node:worker_threads');

const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.on('message', () => {});
`, { eval: true });

worker.on('online', async () => {
const handle = await worker.startCpuProfile('demo');
const profile = await handle.stop();
console.log(profile);
worker.terminate();
});
```

### `worker.stderr`

<!-- YAML
Expand Down
36 changes: 36 additions & 0 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,42 @@ class Worker extends EventEmitter {
};
});
}

// TODO(theanarkh): add options, such as sample_interval, CpuProfilingMode
startCpuProfile(name) {
validateString(name, 'name');
const startTaker = this[kHandle]?.startCpuProfile(name);
return new Promise((resolve, reject) => {
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
startTaker.ondone = (err) => {
if (err) {
return reject(err);
}
let promise = null;
const stop = () => {
if (promise) {
return promise;
}
const stopTaker = this[kHandle]?.stopCpuProfile(name);
return promise = new Promise((resolve, reject) => {
if (!stopTaker) return reject(new ERR_WORKER_NOT_RUNNING());
stopTaker.ondone = (status, profile) => {
if (err) {
return reject(err);
}
resolve(profile);
};
});
};
resolve({
stop,
async [SymbolAsyncDispose]() {
await stop();
},
Comment on lines +538 to +540
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't this just discard the profile then? What use is there in starting a profile only to discard the result? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes sure stop the profile in some scenarios. Like this:

const { Worker } = require('worker_threads'); const w = new Worker(`setTimeout(() => {  console.log("hello world"); }, 1000);`, { eval: true }); w.on("online", async () => { // call handle.stop() automatically when return await using handle = await w.startCpuProfile("profile"); // unexpected return here if (true) { return // or throw an error } await handle.stop(); }); process.on('uncaughtException', (err) => { console.log('uncaughtException', err); });
});
};
});
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/async_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ namespace node {
V(UDPWRAP) \
V(SIGINTWATCHDOG) \
V(WORKER) \
V(WORKERCPUPROFILE) \
V(WORKERCPUUSAGE) \
V(WORKERHEAPSNAPSHOT) \
V(WORKERHEAPSTATISTICS) \
Expand Down
36 changes: 36 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,13 @@ Environment::~Environment() {
}

delete external_memory_accounter_;
if (cpu_profiler_) {
for (auto& it : pending_profiles_) {
cpu_profiler_->Stop(it.second);
}
cpu_profiler_->Dispose();
cpu_profiler_ = nullptr;
}
}

void Environment::InitializeLibuv() {
Expand Down Expand Up @@ -2225,4 +2232,33 @@ void Environment::MemoryInfo(MemoryTracker* tracker) const {
void Environment::RunWeakRefCleanup() {
isolate()->ClearKeptObjects();
}

v8::CpuProfilingResult Environment::StartCpuProfile(std::string_view name) {
HandleScope handle_scope(isolate());
if (!cpu_profiler_) {
cpu_profiler_ = v8::CpuProfiler::New(isolate());
}
Local<Value> title =
node::ToV8Value(context(), name, isolate()).ToLocalChecked();
v8::CpuProfilingResult result =
cpu_profiler_->Start(title.As<String>(), true);
if (result.status == v8::CpuProfilingStatus::kStarted) {
pending_profiles_.emplace(name, result.id);
}
return result;
}

v8::CpuProfile* Environment::StopCpuProfile(std::string_view name) {
if (!cpu_profiler_) {
return nullptr;
}
auto it = pending_profiles_.find(std::string(name));
if (it == pending_profiles_.end()) {
return nullptr;
}
v8::CpuProfile* profile = cpu_profiler_->Stop(it->second);
pending_profiles_.erase(it);
return profile;
}

} // namespace node
7 changes: 7 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "util.h"
#include "uv.h"
#include "v8-external-memory-accounter.h"
#include "v8-profiler.h"
#include "v8.h"

#if HAVE_OPENSSL
Expand Down Expand Up @@ -1048,6 +1049,9 @@ class Environment final : public MemoryRetainer {

inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);

v8::CpuProfilingResult StartCpuProfile(std::string_view name);
v8::CpuProfile* StopCpuProfile(std::string_view name);

// Field identifiers for exit_info_
enum ExitInfoField {
kExiting = 0,
Expand Down Expand Up @@ -1244,6 +1248,9 @@ class Environment final : public MemoryRetainer {
// track of the BackingStore for a given pointer.
std::unordered_map<char*, std::unique_ptr<v8::BackingStore>>
released_allocated_buffers_;

v8::CpuProfiler* cpu_profiler_ = nullptr;
std::unordered_map<std::string, v8::ProfilerId> pending_profiles_;
};

} // namespace node
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@
V(tcp_constructor_template, v8::FunctionTemplate) \
V(tty_constructor_template, v8::FunctionTemplate) \
V(write_wrap_template, v8::ObjectTemplate) \
V(worker_cpu_profile_taker_template, v8::ObjectTemplate) \
V(worker_cpu_usage_taker_template, v8::ObjectTemplate) \
V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
V(worker_heap_statistics_taker_template, v8::ObjectTemplate) \
Expand Down
3 changes: 3 additions & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_CLOSED_MESSAGE_PORT, Error) \
V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \
V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \
V(ERR_CPU_PROFILE_ALREADY_STARTED, Error) \
V(ERR_CPU_PROFILE_NOT_STARTED, Error) \
V(ERR_CPU_PROFILE_TOO_MANY, Error) \
V(ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, Error) \
V(ERR_CRYPTO_INITIALIZATION_FAILED, Error) \
V(ERR_CRYPTO_INVALID_ARGON2_PARAMS, TypeError) \
Expand Down
Loading
Loading