Skip to content

Commit 49747a5

Browse files
theanarkhtargos
authored andcommitted
worker: add heap profile API
PR-URL: #59846 Refs: #59428 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 1e723f9 commit 49747a5

File tree

12 files changed

+346
-0
lines changed

12 files changed

+346
-0
lines changed

doc/api/v8.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,33 @@ added: v24.8.0
14211421

14221422
Stopping collecting the profile and the profile will be discarded.
14231423

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

14261453
<!-- YAML

doc/api/worker_threads.md

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

lib/internal/worker.js

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

167+
class HeapProfileHandle {
168+
#worker = null;
169+
#promise = null;
170+
171+
constructor(worker) {
172+
this.#worker = worker;
173+
}
174+
175+
stop() {
176+
if (this.#promise) {
177+
return this.#promise;
178+
}
179+
const stopTaker = this.#worker[kHandle]?.stopHeapProfile();
180+
return this.#promise = new Promise((resolve, reject) => {
181+
if (!stopTaker) return reject(new ERR_WORKER_NOT_RUNNING());
182+
stopTaker.ondone = (err, profile) => {
183+
if (err) {
184+
return reject(err);
185+
}
186+
resolve(profile);
187+
};
188+
});
189+
};
190+
191+
async [SymbolAsyncDispose]() {
192+
await this.stop();
193+
}
194+
}
195+
167196
class Worker extends EventEmitter {
168197
constructor(filename, options = kEmptyObject) {
169198
throwIfBuildingSnapshot('Creating workers');
@@ -559,6 +588,19 @@ class Worker extends EventEmitter {
559588
};
560589
});
561590
}
591+
592+
startHeapProfile() {
593+
const startTaker = this[kHandle]?.startHeapProfile();
594+
return new Promise((resolve, reject) => {
595+
if (!startTaker) return reject(new ERR_WORKER_NOT_RUNNING());
596+
startTaker.ondone = (err) => {
597+
if (err) {
598+
return reject(err);
599+
}
600+
resolve(new HeapProfileHandle(this));
601+
};
602+
});
603+
}
562604
}
563605

564606
/**

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
@@ -458,6 +458,7 @@
458458
V(write_wrap_template, v8::ObjectTemplate) \
459459
V(worker_cpu_profile_taker_template, v8::ObjectTemplate) \
460460
V(worker_cpu_usage_taker_template, v8::ObjectTemplate) \
461+
V(worker_heap_profile_taker_template, v8::ObjectTemplate) \
461462
V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
462463
V(worker_heap_statistics_taker_template, v8::ObjectTemplate) \
463464
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: 183 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,169 @@ void Worker::StopCpuProfile(const FunctionCallbackInfo<Value>& args) {
10311033
}
10321034
}
10331035

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

13321499
SetConstructorFunction(isolate, target, "Worker", w);
13331500
}
@@ -1387,6 +1554,20 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
13871554
wst->InstanceTemplate());
13881555
}
13891556

1557+
{
1558+
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);
1559+
1560+
wst->InstanceTemplate()->SetInternalFieldCount(
1561+
WorkerHeapProfileTaker::kInternalFieldCount);
1562+
wst->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));
1563+
1564+
Local<String> wst_string =
1565+
FIXED_ONE_BYTE_STRING(isolate, "WorkerHeapProfileTaker");
1566+
wst->SetClassName(wst_string);
1567+
isolate_data->set_worker_heap_profile_taker_template(
1568+
wst->InstanceTemplate());
1569+
}
1570+
13901571
SetMethod(isolate, target, "getEnvMessagePort", GetEnvMessagePort);
13911572
}
13921573

@@ -1466,6 +1647,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
14661647
registry->Register(Worker::CpuUsage);
14671648
registry->Register(Worker::StartCpuProfile);
14681649
registry->Register(Worker::StopCpuProfile);
1650+
registry->Register(Worker::StartHeapProfile);
1651+
registry->Register(Worker::StopHeapProfile);
14691652
}
14701653

14711654
} // anonymous namespace

0 commit comments

Comments
 (0)