Skip to content

Commit e0e3084

Browse files
committed
inspector: implement --cpu-prof[-path]
This patch introduces a CLI flag --cpu-prof that starts the V8 CPU profiler on start up, and ends the profiler then writes the CPU profile before the Node.js instance (on the main thread or the worker thread) exits. By default the profile is written to `${cwd}/CPU.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.cpuprofile`. The patch also introduces a --cpu-prof-path flag for the user to specify the path the profile will be written to. Refs: #26878 PR-URL: #27147 Reviewed-By: Anna Henningsen <[email protected]>
1 parent 57ab3b5 commit e0e3084

File tree

15 files changed

+480
-3
lines changed

15 files changed

+480
-3
lines changed

‎doc/api/cli.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,37 @@ $ node --completion-bash > node_bash_completion
7575
$ source node_bash_completion
7676
```
7777

78+
### `--cpu-prof`
79+
<!-- YAML
80+
added: REPLACEME
81+
-->
82+
83+
> Stability: 1 - Experimental
84+
85+
Starts the V8 CPU profiler on start up, and writes the CPU profile to disk
86+
before exit. If `--cpu-prof-path` is not specified, the profile will be
87+
written to `${cwd}/CPU.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.cpuprofile`.
88+
89+
```console
90+
$ node --cpu-prof index.js
91+
$ ls *.cpuprofile
92+
CPU.20190409.202950.15293.0.0.cpuprofile
93+
```
94+
95+
### `--cpu-prof-path`
96+
<!-- YAML
97+
added: REPLACEME
98+
-->
99+
100+
> Stability: 1 - Experimental
101+
102+
Location where the CPU profile generated by `--cpu-prof`
103+
should be written to. When used alone, it implies `--cpu-prof`.
104+
105+
```console
106+
$ node --cpu-prof-path /tmp/test.cpuprofile index.js
107+
```
108+
78109
### `--diagnostic-report-directory=directory`
79110
<!-- YAML
80111
added: v11.8.0

‎doc/node.1‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ Aborting instead of exiting causes a core file to be generated for analysis.
7878
.ItFl-completion-bash
7979
Print source-able bash completion script for Node.js.
8080
.
81+
.ItFl-cpu-prof
82+
Start the V8 CPU profiler on start up, and write the CPU profile to disk
83+
before exit. If
84+
.Fl-cpu-prof-path
85+
is not specified, the profile will be written to the current working directory.
86+
.
87+
.ItFl-cpu-prof-path
88+
Path the V8 CPU profile generated with
89+
.Fl-cpu-prof
90+
will be written to. When used alone, it implies
91+
.Fl-cpu-prof
92+
.
8193
.ItFl-diagnostic-report-directory
8294
Location at which the
8395
.Sydiagnosticreport

‎src/env-inl.h‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,25 @@ inline profiler::V8CoverageConnection* Environment::coverage_connection(){
659659
inlineconst std::string& Environment::coverage_directory() const{
660660
return coverage_directory_;
661661
}
662+
663+
inlinevoidEnvironment::set_cpu_profiler_connection(
664+
std::unique_ptr<profiler::V8CpuProfilerConnection> connection){
665+
CHECK_NULL(cpu_profiler_connection_);
666+
std::swap(cpu_profiler_connection_, connection);
667+
}
668+
669+
inline profiler::V8CpuProfilerConnection*
670+
Environment::cpu_profiler_connection(){
671+
return cpu_profiler_connection_.get();
672+
}
673+
674+
inlinevoidEnvironment::set_cpu_profile_path(const std::string& path){
675+
cpu_profile_path_ = path;
676+
}
677+
678+
inlineconst std::string& Environment::cpu_profile_path() const{
679+
return cpu_profile_path_;
680+
}
662681
#endif// HAVE_INSPECTOR
663682

664683
inline std::shared_ptr<HostPort> Environment::inspector_host_port(){

‎src/env.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class AgentWriterHandle;
7171
#if HAVE_INSPECTOR
7272
namespaceprofiler{
7373
classV8CoverageConnection;
74+
classV8CpuProfilerConnection;
7475
} // namespace profiler
7576
#endif// HAVE_INSPECTOR
7677

@@ -1129,6 +1130,13 @@ class Environment : public MemoryRetainer{
11291130

11301131
inlinevoidset_coverage_directory(constchar* directory);
11311132
inlineconst std::string& coverage_directory() const;
1133+
1134+
voidset_cpu_profiler_connection(
1135+
std::unique_ptr<profiler::V8CpuProfilerConnection> connection);
1136+
profiler::V8CpuProfilerConnection* cpu_profiler_connection();
1137+
1138+
inlinevoidset_cpu_profile_path(const std::string& path);
1139+
inlineconst std::string& cpu_profile_path() const;
11321140
#endif// HAVE_INSPECTOR
11331141

11341142
private:
@@ -1163,7 +1171,9 @@ class Environment : public MemoryRetainer{
11631171

11641172
#if HAVE_INSPECTOR
11651173
std::unique_ptr<profiler::V8CoverageConnection> coverage_connection_;
1174+
std::unique_ptr<profiler::V8CpuProfilerConnection> cpu_profiler_connection_;
11661175
std::string coverage_directory_;
1176+
std::string cpu_profile_path_;
11671177
#endif// HAVE_INSPECTOR
11681178

11691179
std::shared_ptr<EnvironmentOptions> options_;

‎src/inspector_profiler.cc‎

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ using v8::Value;
2323
using v8_inspector::StringBuffer;
2424
using v8_inspector::StringView;
2525

26-
#ifdef __POSIX__
27-
constchar* constkPathSeparator = "/";
28-
#else
26+
#ifdef _WIN32
2927
constchar* constkPathSeparator = "\\/";
28+
/* MAX_PATH is in characters, not bytes. Make sure we have enough headroom. */
29+
#defineCWD_BUFSIZE (MAX_PATH * 4)
30+
#else
31+
#include<climits>// PATH_MAX on Solaris.
32+
constchar* constkPathSeparator = "/";
33+
#defineCWD_BUFSIZE (PATH_MAX)
3034
#endif
3135

3236
std::unique_ptr<StringBuffer> ToProtocolString(Isolate* isolate,
@@ -180,6 +184,116 @@ void V8CoverageConnection::End(){
180184
DispatchMessage(end);
181185
}
182186

187+
voidV8CpuProfilerConnection::OnMessage(
188+
const v8_inspector::StringView& message){
189+
Debug(env(),
190+
DebugCategory::INSPECTOR_PROFILER,
191+
"Receive cpu profiling message, ending = %s\n",
192+
ending_ ? "true" : "false");
193+
if (!ending_){
194+
return;
195+
}
196+
Isolate* isolate = env()->isolate();
197+
HandleScope handle_scope(isolate);
198+
Local<Context> context = env()->context();
199+
Context::Scope context_scope(context);
200+
Local<String> result;
201+
if (!String::NewFromTwoByte(isolate,
202+
message.characters16(),
203+
NewStringType::kNormal,
204+
message.length())
205+
.ToLocal(&result)){
206+
fprintf(stderr, "Failed to convert profiling message\n");
207+
}
208+
WriteCpuProfile(result);
209+
}
210+
211+
voidV8CpuProfilerConnection::WriteCpuProfile(Local<String> message){
212+
const std::string& path = env()->cpu_profile_path();
213+
CHECK(!path.empty());
214+
std::string directory = path.substr(0, path.find_last_of(kPathSeparator));
215+
if (directory != path){
216+
uv_fs_t req;
217+
int ret = fs::MKDirpSync(nullptr, &req, directory, 0777, nullptr);
218+
uv_fs_req_cleanup(&req);
219+
if (ret < 0 && ret != UV_EEXIST){
220+
char err_buf[128];
221+
uv_err_name_r(ret, err_buf, sizeof(err_buf));
222+
fprintf(stderr,
223+
"%s: Failed to create cpu profile directory %s\n",
224+
err_buf,
225+
directory.c_str());
226+
return;
227+
}
228+
}
229+
MaybeLocal<String> result = GetResult(message);
230+
if (!result.IsEmpty()){
231+
WriteResult(path.c_str(), result.ToLocalChecked());
232+
}
233+
}
234+
235+
MaybeLocal<String> V8CpuProfilerConnection::GetResult(Local<String> message){
236+
Local<Context> context = env()->context();
237+
Isolate* isolate = env()->isolate();
238+
Local<Value> parsed;
239+
if (!v8::JSON::Parse(context, message).ToLocal(&parsed) ||
240+
!parsed->IsObject()){
241+
fprintf(stderr, "Failed to parse CPU profile result as JSON object\n");
242+
return MaybeLocal<String>();
243+
}
244+
245+
Local<Value> result_v;
246+
if (!parsed.As<Object>()
247+
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result"))
248+
.ToLocal(&result_v)){
249+
fprintf(stderr, "Failed to get result from CPU profile message\n");
250+
return MaybeLocal<String>();
251+
}
252+
253+
if (!result_v->IsObject()){
254+
fprintf(stderr, "'result' from CPU profile message is not an object\n");
255+
return MaybeLocal<String>();
256+
}
257+
258+
Local<Value> profile_v;
259+
if (!result_v.As<Object>()
260+
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "profile"))
261+
.ToLocal(&profile_v)){
262+
fprintf(stderr, "'profile' from CPU profile result is undefined\n");
263+
return MaybeLocal<String>();
264+
}
265+
266+
Local<String> result_s;
267+
if (!v8::JSON::Stringify(context, profile_v).ToLocal(&result_s)){
268+
fprintf(stderr, "Failed to stringify CPU profile result\n");
269+
return MaybeLocal<String>();
270+
}
271+
272+
return result_s;
273+
}
274+
275+
voidV8CpuProfilerConnection::Start(){
276+
Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.start\n");
277+
Isolate* isolate = env()->isolate();
278+
Local<String> enable = FIXED_ONE_BYTE_STRING(
279+
isolate, R"({"id": 1, "method": "Profiler.enable"})");
280+
Local<String> start = FIXED_ONE_BYTE_STRING(
281+
isolate, R"({"id": 2, "method": "Profiler.start"})");
282+
DispatchMessage(enable);
283+
DispatchMessage(start);
284+
}
285+
286+
voidV8CpuProfilerConnection::End(){
287+
CHECK_EQ(ending_, false);
288+
ending_ = true;
289+
Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.stop\n");
290+
Isolate* isolate = env()->isolate();
291+
HandleScope scope(isolate);
292+
Local<String> end =
293+
FIXED_ONE_BYTE_STRING(isolate, R"({"id": 3, "method": "Profiler.stop"})");
294+
DispatchMessage(end);
295+
}
296+
183297
// For now, we only support coverage profiling, but we may add more
184298
// in the future.
185299
voidEndStartedProfilers(Environment* env){
@@ -190,6 +304,12 @@ void EndStartedProfilers(Environment* env){
190304
env, DebugCategory::INSPECTOR_PROFILER, "Ending coverage collection\n");
191305
connection->End();
192306
}
307+
308+
connection = env->cpu_profiler_connection();
309+
if (connection != nullptr && !connection->ending()){
310+
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending cpu profiling\n");
311+
connection->End();
312+
}
193313
}
194314

195315
voidStartCoverageCollection(Environment* env){
@@ -198,6 +318,26 @@ void StartCoverageCollection(Environment* env){
198318
env->coverage_connection()->Start();
199319
}
200320

321+
voidStartCpuProfiling(Environment* env, const std::string& profile_path){
322+
std::string path;
323+
if (profile_path.empty()){
324+
char cwd[CWD_BUFSIZE];
325+
size_t size = CWD_BUFSIZE;
326+
int err = uv_cwd(cwd, &size);
327+
// TODO(joyeecheung): fallback to exec path / argv[0]
328+
CHECK_EQ(err, 0);
329+
CHECK_GT(size, 0);
330+
DiagnosticFilename filename(env, "CPU", "cpuprofile");
331+
path = cwd + std::string(kPathSeparator) + (*filename);
332+
} else{
333+
path = profile_path;
334+
}
335+
env->set_cpu_profile_path(std::move(path));
336+
env->set_cpu_profiler_connection(
337+
std::make_unique<V8CpuProfilerConnection>(env));
338+
env->cpu_profiler_connection()->Start();
339+
}
340+
201341
staticvoidSetCoverageDirectory(const FunctionCallbackInfo<Value>& args){
202342
CHECK(args[0]->IsString());
203343
Environment* env = Environment::GetCurrent(args);

‎src/inspector_profiler.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ class V8CoverageConnection : public V8ProfilerConnection{
6868
bool ending_ = false;
6969
};
7070

71+
classV8CpuProfilerConnection : publicV8ProfilerConnection{
72+
public:
73+
explicitV8CpuProfilerConnection(Environment* env)
74+
: V8ProfilerConnection(env){}
75+
76+
voidStart() override;
77+
voidEnd() override;
78+
voidOnMessage(const v8_inspector::StringView& message) override;
79+
boolending() constoverride{return ending_}
80+
81+
private:
82+
voidWriteCpuProfile(v8::Local<v8::String> message);
83+
v8::MaybeLocal<v8::String> GetResult(v8::Local<v8::String> message);
84+
85+
std::unique_ptr<inspector::InspectorSession> session_;
86+
bool ending_ = false;
87+
};
88+
7189
} // namespace profiler
7290
} // namespace node
7391

‎src/node.cc‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ MaybeLocal<Value> RunBootstrapping(Environment* env){
237237
#endif// HAVE_INSPECTOR
238238
}
239239

240+
#if HAVE_INSPECTOR
241+
if (env->options()->cpu_prof){
242+
profiler::StartCpuProfiling(env, env->options()->cpu_prof_path);
243+
}
244+
#endif// HAVE_INSPECTOR
245+
240246
// Add a reference to the global object
241247
Local<Object> global = context->Global();
242248

‎src/node_internals.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ void MarkBootstrapComplete(const v8::FunctionCallbackInfo<v8::Value>& args);
314314
#if HAVE_INSPECTOR
315315
namespaceprofiler{
316316
voidStartCoverageCollection(Environment* env);
317+
voidStartCpuProfiling(Environment* env, const std::string& profile_name);
317318
voidEndStartedProfilers(Environment* env);
318319
}
319320
#endif// HAVE_INSPECTOR

‎src/node_options.cc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,18 @@ EnvironmentOptionsParser::EnvironmentOptionsParser(){
332332
&EnvironmentOptions::prof_process);
333333
// Options after --prof-process are passed through to the prof processor.
334334
AddAlias("--prof-process",{"--prof-process", "--" });
335+
#if HAVE_INSPECTOR
336+
AddOption("--cpu-prof",
337+
"Start the V8 CPU profiler on start up, and write the CPU profile "
338+
"to disk before exit. If --cpu-prof-path is not specified, write "
339+
"the profile to the current working directory.",
340+
&EnvironmentOptions::cpu_prof);
341+
AddOption("--cpu-prof-path",
342+
"Path the V8 CPU profile generated with --cpu-prof will be "
343+
"written to.",
344+
&EnvironmentOptions::cpu_prof_path);
345+
Implies("--cpu-prof-path", "--cpu-prof");
346+
#endif// HAVE_INSPECTOR
335347
AddOption("--redirect-warnings",
336348
"write warnings to file instead of stderr",
337349
&EnvironmentOptions::redirect_warnings,

‎src/node_options.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ class EnvironmentOptions : public Options{
109109
bool preserve_symlinks = false;
110110
bool preserve_symlinks_main = false;
111111
bool prof_process = false;
112+
#if HAVE_INSPECTOR
113+
std::string cpu_prof_path;
114+
bool cpu_prof = false;
115+
#endif// HAVE_INSPECTOR
112116
std::string redirect_warnings;
113117
bool throw_deprecation = false;
114118
bool trace_deprecation = false;

0 commit comments

Comments
(0)