Skip to content

Commit d6dc08e

Browse files
committed
worker: add cpu profile APIs for worker
1 parent 60a58f6 commit d6dc08e

File tree

9 files changed

+355
-1
lines changed

9 files changed

+355
-1
lines changed

doc/api/errors.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3291,6 +3291,24 @@ added: v18.1.0
32913291
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
32923292
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
32933293

3294+
<a id="ERR_WORKER_CPU_PROFILE_ALREADY_STARTED"></a>
3295+
3296+
### `ERR_WORKER_CPU_PROFILE_ALREADY_STARTED`
3297+
3298+
The `Worker` CPU profile with the given name is already started.
3299+
3300+
<a id="ERR_WORKER_CPU_PROFILE_NOT_STARTED"></a>
3301+
3302+
### `ERR_WORKER_CPU_PROFILE_NOT_STARTED`
3303+
3304+
The `Worker` CPU profile with the given name is not started.
3305+
3306+
<a id="ERR_WORKER_CPU_PROFILE_TOO_MANY"></a>
3307+
3308+
### `ERR_WORKER_CPU_PROFILE_TOO_MANY`
3309+
3310+
There are too many CPU profiles being collected in `Worker`.
3311+
32943312
<a id="ERR_WORKER_INIT_FAILED"></a>
32953313

32963314
### `ERR_WORKER_INIT_FAILED`

doc/api/worker_threads.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,6 +1957,18 @@ this matches its values.
19571957
19581958
If the worker has stopped, the return value is an empty object.
19591959
1960+
### `worker.startCpuProfile(name)`
1961+
1962+
<!-- YAML
1963+
added: REPLACEME
1964+
-->
1965+
1966+
* name: {string}
1967+
* Returns: {Promise}
1968+
1969+
Starting a CPU profile with the given `name` for the worker thread. The profile can be stopped
1970+
with [`worker.stopCpuProfile(name)`][].
1971+
19601972
### `worker.stderr`
19611973
19621974
<!-- YAML
@@ -1995,6 +2007,18 @@ inside the worker thread. If `stdout: true` was not passed to the
19952007
[`Worker`][] constructor, then data is piped to the parent thread's
19962008
[`process.stdout`][] stream.
19972009

2010+
### `worker.stopCpuProfile(name)`
2011+
2012+
<!-- YAML
2013+
added: REPLACEME
2014+
-->
2015+
2016+
* name: {string}
2017+
* Returns: {Promise}
2018+
2019+
Stopping a CPU profile with the given `name` which is passed to [`worker.startCpuProfile(name)`][]
2020+
for the worker thread. Returns a Promise that fulfills with the profile data or throws an error.
2021+
19982022
### `worker.terminate()`
19992023

20002024
<!-- YAML
@@ -2176,6 +2200,8 @@ thread spawned will spawn another until the application crashes.
21762200
[`worker.SHARE_ENV`]: #workershare_env
21772201
[`worker.on('message')`]: #event-message_1
21782202
[`worker.postMessage()`]: #workerpostmessagevalue-transferlist
2203+
[`worker.startCpuProfile(name)`]: #workerstartcpuprofilename
2204+
[`worker.stopCpuProfile(name)`]: #workerstopcpuprofilename
21792205
[`worker.terminate()`]: #workerterminate
21802206
[`worker.threadId`]: #workerthreadid_1
21812207
[`worker.threadName`]: #workerthreadname_1

lib/internal/errors.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,6 +1898,9 @@ E('ERR_VM_MODULE_NOT_MODULE',
18981898
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
18991899
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
19001900
E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError);
1901+
E('ERR_WORKER_CPU_PROFILE_ALREADY_STARTED', 'Worker CPU profile already started with name(%s)', Error);
1902+
E('ERR_WORKER_CPU_PROFILE_NOT_STARTED', 'Worker CPU profile not started with name(%s)', Error);
1903+
E('ERR_WORKER_CPU_PROFILE_TOO_MANY', 'Worker has too many CPU profiles', Error);
19011904
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
19021905
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
19031906
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,

lib/internal/worker.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ const {
3636

3737
const errorCodes = require('internal/errors').codes;
3838
const {
39+
ERR_WORKER_CPU_PROFILE_ALREADY_STARTED,
40+
ERR_WORKER_CPU_PROFILE_NOT_STARTED,
41+
ERR_WORKER_CPU_PROFILE_TOO_MANY,
3942
ERR_WORKER_NOT_RUNNING,
4043
ERR_WORKER_PATH,
4144
ERR_WORKER_UNSERIALIZABLE_ERROR,
@@ -506,6 +509,32 @@ class Worker extends EventEmitter {
506509
};
507510
});
508511
}
512+
513+
// TODO(theanarkh): add options
514+
startCpuProfile(name) {
515+
validateString(name, 'name');
516+
const startTaker = this[kHandle]?.startCpuProfile(name);
517+
return new Promise((resolve, reject) => {
518+
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
519+
startTaker.ondone = (status) => {
520+
if (status === 1) return reject(new ERR_WORKER_CPU_PROFILE_ALREADY_STARTED(name));
521+
if (status === 2) return reject(new ERR_WORKER_CPU_PROFILE_TOO_MANY());
522+
resolve();
523+
};
524+
});
525+
}
526+
527+
stopCpuProfile(name) {
528+
validateString(name, 'name');
529+
const stopTaker = this[kHandle]?.stopCpuProfile(name);
530+
return new Promise((resolve, reject) => {
531+
if (!stopTaker) return reject(new ERR_WORKER_NOT_RUNNING());
532+
stopTaker.ondone = (status, profile) => {
533+
if (status === 1) return reject(new ERR_WORKER_CPU_PROFILE_NOT_STARTED(name));
534+
resolve(profile);
535+
};
536+
});
537+
}
509538
}
510539

511540
/**

src/async_wrap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ namespace node {
7979
V(UDPWRAP) \
8080
V(SIGINTWATCHDOG) \
8181
V(WORKER) \
82+
V(WORKERCPUPROFILE) \
8283
V(WORKERCPUUSAGE) \
8384
V(WORKERHEAPSNAPSHOT) \
8485
V(WORKERHEAPSTATISTICS) \

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@
483483
V(tcp_constructor_template, v8::FunctionTemplate) \
484484
V(tty_constructor_template, v8::FunctionTemplate) \
485485
V(write_wrap_template, v8::ObjectTemplate) \
486+
V(worker_cpu_profile_taker_template, v8::ObjectTemplate) \
486487
V(worker_cpu_usage_taker_template, v8::ObjectTemplate) \
487488
V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
488489
V(worker_heap_statistics_taker_template, v8::ObjectTemplate) \

src/node_worker.cc

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ using v8::Array;
2323
using v8::ArrayBuffer;
2424
using v8::Boolean;
2525
using v8::Context;
26+
using v8::CpuProfile;
27+
using v8::CpuProfiler;
28+
using v8::CpuProfilingStatus;
2629
using v8::Float64Array;
2730
using v8::FunctionCallbackInfo;
2831
using v8::FunctionTemplate;
@@ -495,7 +498,10 @@ Worker::~Worker() {
495498
CHECK(stopped_);
496499
CHECK_NULL(env_);
497500
CHECK(!tid_.has_value());
498-
501+
if (!cpu_profiler_) {
502+
cpu_profiler_->Dispose();
503+
cpu_profiler_ = nullptr;
504+
}
499505
Debug(this, "Worker %llu destroyed", thread_id_.id);
500506
}
501507

@@ -897,6 +903,164 @@ void Worker::CpuUsage(const FunctionCallbackInfo<Value>& args) {
897903
}
898904
}
899905

906+
class WorkerCpuProfileTaker : public AsyncWrap {
907+
public:
908+
WorkerCpuProfileTaker(Environment* env, Local<Object> obj)
909+
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_WORKERCPUPROFILE) {}
910+
911+
SET_NO_MEMORY_INFO()
912+
SET_MEMORY_INFO_NAME(WorkerCpuProfileTaker)
913+
SET_SELF_SIZE(WorkerCpuProfileTaker)
914+
};
915+
916+
void Worker::StartCpuProfile(const FunctionCallbackInfo<Value>& args) {
917+
Worker* w;
918+
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
919+
Environment* env = w->env();
920+
921+
CHECK(args[0]->IsString());
922+
node::Utf8Value name(env->isolate(), args[0]);
923+
924+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
925+
Local<Object> wrap;
926+
if (!env->worker_cpu_profile_taker_template()
927+
->NewInstance(env->context())
928+
.ToLocal(&wrap)) {
929+
return;
930+
}
931+
932+
BaseObjectPtr<WorkerCpuProfileTaker> taker =
933+
MakeDetachedBaseObject<WorkerCpuProfileTaker>(env, wrap);
934+
935+
bool scheduled = w->RequestInterrupt([taker = std::move(taker),
936+
name = name.ToString(),
937+
env,
938+
w](Environment* worker_env) mutable {
939+
Isolate* isolate = worker_env->isolate();
940+
if (!w->cpu_profiler_) {
941+
w->cpu_profiler_ = CpuProfiler::New(isolate);
942+
}
943+
Local<String> title = String::NewFromUtf8(isolate,
944+
name.data(),
945+
NewStringType::kNormal,
946+
name.size()).ToLocalChecked();
947+
CpuProfilingStatus status = w->cpu_profiler_->StartProfiling(title, true);
948+
env->SetImmediateThreadsafe(
949+
[taker = std::move(taker),
950+
status
951+
](Environment* env) mutable {
952+
Isolate* isolate = env->isolate();
953+
HandleScope handle_scope(isolate);
954+
Context::Scope context_scope(env->context());
955+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(taker.get());
956+
Local<Value> argv[] = {
957+
Number::New(isolate, static_cast<double>(status)) // status
958+
};
959+
taker->MakeCallback(env->ondone_string(), arraysize(argv), argv);
960+
},
961+
CallbackFlags::kUnrefed);
962+
});
963+
964+
if (scheduled) {
965+
args.GetReturnValue().Set(wrap);
966+
}
967+
}
968+
969+
class JSONOutputStream : public v8::OutputStream {
970+
public:
971+
JSONOutputStream() {}
972+
973+
int GetChunkSize() override {
974+
return 65536;
975+
}
976+
977+
void EndOfStream() override {}
978+
979+
WriteResult WriteAsciiChunk(char* data, const int size) override {
980+
out_stream_.write(data, size);
981+
return kContinue;
982+
}
983+
984+
std::ostringstream& out_stream() { return out_stream_; }
985+
986+
private:
987+
std::ostringstream out_stream_;
988+
};
989+
990+
void Worker::StopCpuProfile(const FunctionCallbackInfo<Value>& args) {
991+
Worker* w;
992+
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
993+
994+
Environment* env = w->env();
995+
CHECK(args[0]->IsString());
996+
node::Utf8Value name(env->isolate(), args[0]);
997+
998+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
999+
Local<Object> wrap;
1000+
if (!env->worker_cpu_profile_taker_template()
1001+
->NewInstance(env->context())
1002+
.ToLocal(&wrap)) {
1003+
return;
1004+
}
1005+
1006+
BaseObjectPtr<WorkerCpuProfileTaker> taker =
1007+
MakeDetachedBaseObject<WorkerCpuProfileTaker>(env, wrap);
1008+
1009+
bool scheduled = w->RequestInterrupt([taker = std::move(taker),
1010+
name = name.ToString(),
1011+
env,
1012+
w](Environment* worker_env) mutable {
1013+
Local<String> title = String::NewFromUtf8(worker_env->isolate(),
1014+
name.data(),
1015+
NewStringType::kNormal,
1016+
name.size()).ToLocalChecked();
1017+
bool found = false;
1018+
auto json_out_stream = std::make_unique<JSONOutputStream>();
1019+
if (w->cpu_profiler_) {
1020+
CpuProfile* profile = w->cpu_profiler_->StopProfiling(title);
1021+
if (profile) {
1022+
profile->Serialize(json_out_stream.get(),
1023+
CpuProfile::SerializationFormat::kJSON);
1024+
profile->Delete();
1025+
found = true;
1026+
}
1027+
}
1028+
env->SetImmediateThreadsafe(
1029+
[taker = std::move(taker),
1030+
json_out_stream = std::move(json_out_stream),
1031+
found
1032+
](Environment* env) mutable {
1033+
Isolate* isolate = env->isolate();
1034+
HandleScope handle_scope(isolate);
1035+
Context::Scope context_scope(env->context());
1036+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(taker.get());
1037+
Local<Value> argv[] = {
1038+
Undefined(isolate), // status
1039+
Undefined(isolate), // profile
1040+
};
1041+
if (found) {
1042+
argv[0] = Number::New(isolate, 0);
1043+
Local<Value> result;
1044+
if (ToV8Value(env->context(),
1045+
json_out_stream->out_stream().str(),
1046+
isolate).ToLocal(&result)) {
1047+
argv[1] = result;
1048+
} else {
1049+
argv[1] = FIXED_ONE_BYTE_STRING(isolate, "{}");
1050+
}
1051+
} else {
1052+
argv[0] = Number::New(isolate, 1);
1053+
}
1054+
taker->MakeCallback(env->ondone_string(), arraysize(argv), argv);
1055+
},
1056+
CallbackFlags::kUnrefed);
1057+
});
1058+
1059+
if (scheduled) {
1060+
args.GetReturnValue().Set(wrap);
1061+
}
1062+
}
1063+
9001064
class WorkerHeapStatisticsTaker : public AsyncWrap {
9011065
public:
9021066
WorkerHeapStatisticsTaker(Environment* env, Local<Object> obj)
@@ -1189,6 +1353,8 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
11891353
SetProtoMethod(isolate, w, "loopStartTime", Worker::LoopStartTime);
11901354
SetProtoMethod(isolate, w, "getHeapStatistics", Worker::GetHeapStatistics);
11911355
SetProtoMethod(isolate, w, "cpuUsage", Worker::CpuUsage);
1356+
SetProtoMethod(isolate, w, "startCpuProfile", Worker::StartCpuProfile);
1357+
SetProtoMethod(isolate, w, "stopCpuProfile", Worker::StopCpuProfile);
11921358

11931359
SetConstructorFunction(isolate, target, "Worker", w);
11941360
}
@@ -1234,6 +1400,20 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
12341400
isolate_data->set_worker_cpu_usage_taker_template(wst->InstanceTemplate());
12351401
}
12361402

1403+
{
1404+
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);
1405+
1406+
wst->InstanceTemplate()->SetInternalFieldCount(
1407+
WorkerCpuProfileTaker::kInternalFieldCount);
1408+
wst->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));
1409+
1410+
Local<String> wst_string =
1411+
FIXED_ONE_BYTE_STRING(isolate, "WorkerCpuProfileTaker");
1412+
wst->SetClassName(wst_string);
1413+
isolate_data->set_worker_cpu_profile_taker_template(
1414+
wst->InstanceTemplate());
1415+
}
1416+
12371417
SetMethod(isolate, target, "getEnvMessagePort", GetEnvMessagePort);
12381418
}
12391419

@@ -1311,6 +1491,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
13111491
registry->Register(Worker::LoopStartTime);
13121492
registry->Register(Worker::GetHeapStatistics);
13131493
registry->Register(Worker::CpuUsage);
1494+
registry->Register(Worker::StartCpuProfile);
1495+
registry->Register(Worker::StopCpuProfile);
13141496
}
13151497

13161498
} // anonymous namespace

src/node_worker.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#include "node_exit_code.h"
99
#include "node_messaging.h"
1010
#include "uv.h"
11+
#include "v8-profiler.h"
12+
#include "json_utils.h"
1113

1214
namespace node {
1315

@@ -81,6 +83,8 @@ class Worker : public AsyncWrap {
8183
static void GetHeapStatistics(
8284
const v8::FunctionCallbackInfo<v8::Value>& args);
8385
static void CpuUsage(const v8::FunctionCallbackInfo<v8::Value>& args);
86+
static void StartCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
87+
static void StopCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
8488

8589
private:
8690
bool CreateEnvMessagePort(Environment* env);
@@ -107,6 +111,7 @@ class Worker : public AsyncWrap {
107111
uintptr_t stack_base_ = 0;
108112
// Optional name used for debugging in inspector and trace events.
109113
std::string name_;
114+
v8::CpuProfiler* cpu_profiler_ = nullptr;
110115

111116
// Custom resource constraints:
112117
double resource_limits_[kTotalResourceLimitCount];

0 commit comments

Comments
 (0)