Skip to content

Commit c6bb954

Browse files
committed
worker: add heap profile API
1 parent 861d624 commit c6bb954

File tree

12 files changed

+353
-0
lines changed

12 files changed

+353
-0
lines changed

doc/api/v8.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,33 @@ added: REPLACEME
14251425

14261426
Stopping collecting the profile and the profile will be discarded.
14271427

1428+
## Class: `HeapProfileHandle`
1429+
1430+
<!-- YAML
1431+
added: REPLACEME
1432+
-->
1433+
1434+
### `heapProfileHandle.stop()`
1435+
1436+
<!-- YAML
1437+
added: REPLACEME
1438+
-->
1439+
1440+
* Returns: {Promise}
1441+
1442+
Stopping collecting the profile, then return a Promise that fulfills with an error or the
1443+
profile data.
1444+
1445+
### `heapProfileHandle[Symbol.asyncDispose]()`
1446+
1447+
<!-- YAML
1448+
added: REPLACEME
1449+
-->
1450+
1451+
* Returns: {Promise}
1452+
1453+
Stopping collecting the profile and the profile will be discarded.
1454+
14281455
## `v8.isStringOneByteRepresentation(content)`
14291456

14301457
<!-- YAML

doc/api/worker_threads.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,49 @@ w.on('online', async () => {
20012001
});
20022002
```
20032003
2004+
### `worker.startHeapProfile()`
2005+
2006+
<!-- YAML
2007+
added: REPLACEME
2008+
-->
2009+
2010+
* Returns: {Promise}
2011+
2012+
Starting a Heap profile then return a Promise that fulfills with an error
2013+
or an `HeapProfileHandle` object. This API supports `await using` syntax.
2014+
2015+
```cjs
2016+
const { Worker } = require('node:worker_threads');
2017+
2018+
const worker = new Worker(`
2019+
const { parentPort } = require('worker_threads');
2020+
parentPort.on('message', () => {});
2021+
`, { eval: true });
2022+
2023+
worker.on('online', async () => {
2024+
const handle = await worker.startHeapProfile();
2025+
const profile = await handle.stop();
2026+
console.log(profile);
2027+
worker.terminate();
2028+
});
2029+
```
2030+
2031+
`await using` example.
2032+
2033+
```cjs
2034+
const { Worker } = require('node::worker_threads');
2035+
2036+
const w = new Worker(`
2037+
const { parentPort } = require('worker_threads');
2038+
parentPort.on('message', () => {});
2039+
`, { eval: true });
2040+
2041+
w.on('online', async () => {
2042+
// Stop profile automatically when return and profile will be discarded
2043+
await using handle = await w.startHeapProfile();
2044+
});
2045+
```
2046+
20042047
### `worker.stderr`
20052048
20062049
<!-- YAML

lib/internal/worker.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,35 @@ class CPUProfileHandle {
165165
}
166166
}
167167

168+
class HeapProfileHandle {
169+
#worker = null;
170+
#promise = null;
171+
172+
constructor(worker) {
173+
this.#worker = worker;
174+
}
175+
176+
stop() {
177+
if (this.#promise) {
178+
return this.#promise;
179+
}
180+
const stopTaker = this.#worker[kHandle]?.stopHeapProfile();
181+
return this.#promise = new Promise((resolve, reject) => {
182+
if (!stopTaker) return reject(new ERR_WORKER_NOT_RUNNING());
183+
stopTaker.ondone = (err, profile) => {
184+
if (err) {
185+
return reject(err);
186+
}
187+
resolve(profile);
188+
};
189+
});
190+
};
191+
192+
async [SymbolAsyncDispose]() {
193+
await this.stop();
194+
}
195+
}
196+
168197
class Worker extends EventEmitter {
169198
constructor(filename, options = kEmptyObject) {
170199
throwIfBuildingSnapshot('Creating workers');
@@ -551,6 +580,19 @@ class Worker extends EventEmitter {
551580
};
552581
});
553582
}
583+
584+
startHeapProfile() {
585+
const startTaker = this[kHandle]?.startHeapProfile();
586+
return new Promise((resolve, reject) => {
587+
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
588+
startTaker.ondone = (err) => {
589+
if (err) {
590+
return reject(err);
591+
}
592+
resolve(new HeapProfileHandle(this));
593+
};
594+
});
595+
}
554596
}
555597

556598
/**

src/async_wrap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ namespace node {
8181
V(WORKER) \
8282
V(WORKERCPUPROFILE) \
8383
V(WORKERCPUUSAGE) \
84+
V(WORKERHEAPPROFILE) \
8485
V(WORKERHEAPSNAPSHOT) \
8586
V(WORKERHEAPSTATISTICS) \
8687
V(WRITEWRAP) \

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@
471471
V(write_wrap_template, v8::ObjectTemplate) \
472472
V(worker_cpu_profile_taker_template, v8::ObjectTemplate) \
473473
V(worker_cpu_usage_taker_template, v8::ObjectTemplate) \
474+
V(worker_heap_profile_taker_template, v8::ObjectTemplate) \
474475
V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
475476
V(worker_heap_statistics_taker_template, v8::ObjectTemplate) \
476477
V(x509_constructor_template, v8::FunctionTemplate)

src/node_errors.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
8787
V(ERR_FS_CP_SOCKET, Error) \
8888
V(ERR_FS_CP_FIFO_PIPE, Error) \
8989
V(ERR_FS_CP_UNKNOWN, Error) \
90+
V(ERR_HEAP_PROFILE_HAVE_BEEN_STARTED, Error) \
91+
V(ERR_HEAP_PROFILE_NOT_STARTED, Error) \
9092
V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
9193
V(ERR_INVALID_ADDRESS, Error) \
9294
V(ERR_INVALID_ARG_VALUE, TypeError) \

src/node_worker.cc

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
using node::kAllowedInEnvvar;
2222
using node::kDisallowedInEnvvar;
23+
using v8::AllocationProfile;
2324
using v8::Array;
2425
using v8::ArrayBuffer;
2526
using v8::Boolean;
@@ -32,6 +33,7 @@ using v8::Float64Array;
3233
using v8::FunctionCallbackInfo;
3334
using v8::FunctionTemplate;
3435
using v8::HandleScope;
36+
using v8::HeapProfiler;
3537
using v8::HeapStatistics;
3638
using v8::Integer;
3739
using v8::Isolate;
@@ -1031,6 +1033,176 @@ void Worker::StopCpuProfile(const FunctionCallbackInfo<Value>& args) {
10311033
}
10321034
}
10331035

1036+
1037+
class WorkerHeapProfileTaker final : public AsyncWrap {
1038+
public:
1039+
WorkerHeapProfileTaker(Environment* env, Local<Object> obj)
1040+
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_WORKERHEAPPROFILE) {}
1041+
1042+
SET_NO_MEMORY_INFO()
1043+
SET_MEMORY_INFO_NAME(WorkerHeapProfileTaker)
1044+
SET_SELF_SIZE(WorkerHeapProfileTaker)
1045+
};
1046+
1047+
void Worker::StartHeapProfile(const FunctionCallbackInfo<Value>& args) {
1048+
Worker* w;
1049+
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
1050+
Environment* env = w->env();
1051+
1052+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
1053+
Local<Object> wrap;
1054+
if (!env->worker_heap_profile_taker_template()
1055+
->NewInstance(env->context())
1056+
.ToLocal(&wrap)) {
1057+
return;
1058+
}
1059+
1060+
BaseObjectPtr<WorkerHeapProfileTaker> taker =
1061+
MakeDetachedBaseObject<WorkerHeapProfileTaker>(env, wrap);
1062+
1063+
bool scheduled = w->RequestInterrupt([taker = std::move(taker),
1064+
env](Environment* worker_env) mutable {
1065+
v8::HeapProfiler* profiler = worker_env->isolate()->GetHeapProfiler();
1066+
bool success = profiler->StartSamplingHeapProfiler();
1067+
env->SetImmediateThreadsafe(
1068+
[
1069+
taker = std::move(taker),
1070+
success = success
1071+
](Environment* env) mutable {
1072+
Isolate* isolate = env->isolate();
1073+
HandleScope handle_scope(isolate);
1074+
Context::Scope context_scope(env->context());
1075+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(taker.get());
1076+
Local<Value> argv[] = {
1077+
Null(isolate), // error
1078+
};
1079+
if (!success) {
1080+
argv[0] = ERR_HEAP_PROFILE_HAVE_BEEN_STARTED(
1081+
isolate, "heap profiler have been started");
1082+
}
1083+
taker->MakeCallback(env->ondone_string(), arraysize(argv), argv);
1084+
},
1085+
CallbackFlags::kUnrefed);
1086+
});
1087+
1088+
if (scheduled) {
1089+
args.GetReturnValue().Set(wrap);
1090+
}
1091+
}
1092+
1093+
static void buildHeapProfileNode(Isolate* isolate,
1094+
const AllocationProfile::Node* node,
1095+
JSONWriter& writer) {
1096+
size_t selfSize = 0;
1097+
for (const auto& allocation : node->allocations)
1098+
selfSize += allocation.size * allocation.count;
1099+
1100+
writer.json_keyvalue("selfSize", selfSize);
1101+
writer.json_keyvalue("id", node->node_id);
1102+
writer.json_objectstart("callFrame");
1103+
writer.json_keyvalue("scriptId", node->script_id);
1104+
writer.json_keyvalue("lineNumber", node->line_number-1);
1105+
writer.json_keyvalue("columnNumber", node->column_number-1);
1106+
node::Utf8Value name(isolate, node->name);
1107+
node::Utf8Value script_name(isolate, node->script_name);
1108+
writer.json_keyvalue("functionName", *name);
1109+
writer.json_keyvalue("url", *script_name);
1110+
writer.json_objectend();
1111+
1112+
writer.json_arraystart("children");
1113+
for (const auto* child : node->children) {
1114+
writer.json_start();
1115+
buildHeapProfileNode(isolate, child, writer);
1116+
writer.json_end();
1117+
}
1118+
writer.json_arrayend();
1119+
}
1120+
1121+
static bool serializeProfile(Isolate* isolate, std::ostringstream& out_stream) {
1122+
HandleScope scope(isolate);
1123+
HeapProfiler* profiler = isolate->GetHeapProfiler();
1124+
std::unique_ptr<AllocationProfile> profile(profiler->GetAllocationProfile());
1125+
if (!profile) {
1126+
return false;
1127+
}
1128+
JSONWriter writer(out_stream, false);
1129+
writer.json_start();
1130+
1131+
writer.json_arraystart("samples");
1132+
for (const auto& sample : profile->GetSamples()) {
1133+
writer.json_start();
1134+
writer.json_keyvalue("size", sample.size * sample.count);
1135+
writer.json_keyvalue("nodeId", sample.node_id);
1136+
writer.json_keyvalue("ordinal", static_cast<double>(sample.sample_id));
1137+
writer.json_end();
1138+
}
1139+
writer.json_arrayend();
1140+
1141+
writer.json_objectstart("head");
1142+
buildHeapProfileNode(isolate, profile->GetRootNode(), writer);
1143+
writer.json_objectend();
1144+
1145+
writer.json_end();
1146+
profiler->StopSamplingHeapProfiler();
1147+
return true;
1148+
}
1149+
1150+
void Worker::StopHeapProfile(const FunctionCallbackInfo<Value>& args) {
1151+
Worker* w;
1152+
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
1153+
1154+
Environment* env = w->env();
1155+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
1156+
Local<Object> wrap;
1157+
if (!env->worker_heap_profile_taker_template()
1158+
->NewInstance(env->context())
1159+
.ToLocal(&wrap)) {
1160+
return;
1161+
}
1162+
1163+
BaseObjectPtr<WorkerHeapProfileTaker> taker =
1164+
MakeDetachedBaseObject<WorkerHeapProfileTaker>(env, wrap);
1165+
1166+
bool scheduled = w->RequestInterrupt([taker = std::move(taker),
1167+
env](Environment* worker_env) mutable {
1168+
std::ostringstream out_stream;
1169+
bool success = serializeProfile(worker_env->isolate(), out_stream);
1170+
env->SetImmediateThreadsafe(
1171+
[taker = std::move(taker),
1172+
out_stream = std::move(out_stream),
1173+
success = success
1174+
](Environment* env) mutable {
1175+
Isolate* isolate = env->isolate();
1176+
HandleScope handle_scope(isolate);
1177+
Context::Scope context_scope(env->context());
1178+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(taker.get());
1179+
Local<Value> argv[] = {
1180+
Null(isolate), // error
1181+
Undefined(isolate), // profile
1182+
};
1183+
if (success) {
1184+
Local<Value> result;
1185+
if (!ToV8Value(env->context(),
1186+
out_stream.str(),
1187+
isolate)
1188+
.ToLocal(&result)) {
1189+
return;
1190+
}
1191+
argv[1] = result;
1192+
} else {
1193+
argv[0] =
1194+
ERR_HEAP_PROFILE_NOT_STARTED(isolate,
1195+
"heap profile not started");
1196+
}
1197+
taker->MakeCallback(env->ondone_string(), arraysize(argv), argv);
1198+
},
1199+
CallbackFlags::kUnrefed);
1200+
});
1201+
1202+
if (scheduled) {
1203+
args.GetReturnValue().Set(wrap);
1204+
}
1205+
}
10341206
class WorkerHeapStatisticsTaker : public AsyncWrap {
10351207
public:
10361208
WorkerHeapStatisticsTaker(Environment* env, Local<Object> obj)
@@ -1328,6 +1500,8 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
13281500
SetProtoMethod(isolate, w, "cpuUsage", Worker::CpuUsage);
13291501
SetProtoMethod(isolate, w, "startCpuProfile", Worker::StartCpuProfile);
13301502
SetProtoMethod(isolate, w, "stopCpuProfile", Worker::StopCpuProfile);
1503+
SetProtoMethod(isolate, w, "startHeapProfile", Worker::StartHeapProfile);
1504+
SetProtoMethod(isolate, w, "stopHeapProfile", Worker::StopHeapProfile);
13311505

13321506
SetConstructorFunction(isolate, target, "Worker", w);
13331507
}
@@ -1387,6 +1561,20 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
13871561
wst->InstanceTemplate());
13881562
}
13891563

1564+
{
1565+
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);
1566+
1567+
wst->InstanceTemplate()->SetInternalFieldCount(
1568+
WorkerHeapProfileTaker::kInternalFieldCount);
1569+
wst->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));
1570+
1571+
Local<String> wst_string =
1572+
FIXED_ONE_BYTE_STRING(isolate, "WorkerHeapProfileTaker");
1573+
wst->SetClassName(wst_string);
1574+
isolate_data->set_worker_heap_profile_taker_template(
1575+
wst->InstanceTemplate());
1576+
}
1577+
13901578
SetMethod(isolate, target, "getEnvMessagePort", GetEnvMessagePort);
13911579
}
13921580

@@ -1466,6 +1654,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
14661654
registry->Register(Worker::CpuUsage);
14671655
registry->Register(Worker::StartCpuProfile);
14681656
registry->Register(Worker::StopCpuProfile);
1657+
registry->Register(Worker::StartHeapProfile);
1658+
registry->Register(Worker::StopHeapProfile);
14691659
}
14701660

14711661
} // anonymous namespace

0 commit comments

Comments
 (0)