Skip to content

Commit aed11ed

Browse files
committed
cli: 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. Fixes: #26878
1 parent bf766c1 commit aed11ed

File tree

15 files changed

+471
-3
lines changed

15 files changed

+471
-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 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
.It Fl -completion-bash
7979
Print source-able bash completion script for Node.js.
8080
.
81+
.It Fl -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+
.It Fl -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
.It Fl -diagnostic-report-directory
8294
Location at which the
8395
.Sy diagnostic report

src/env-inl.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,25 @@ inline profiler::V8CoverageConnection* Environment::coverage_connection() {
656656
inline const std::string& Environment::coverage_directory() const {
657657
return coverage_directory_;
658658
}
659+
660+
inline void Environment::set_cpu_profiler_connection(
661+
std::unique_ptr<profiler::V8CpuProfilerConnection> connection) {
662+
CHECK_NULL(cpu_profiler_connection_);
663+
std::swap(cpu_profiler_connection_, connection);
664+
}
665+
666+
inline profiler::V8CpuProfilerConnection*
667+
Environment::cpu_profiler_connection() {
668+
return cpu_profiler_connection_.get();
669+
}
670+
671+
inline void Environment::set_cpu_profile_path(const std::string& path) {
672+
cpu_profile_path_ = path;
673+
}
674+
675+
inline const std::string& Environment::cpu_profile_path() const {
676+
return cpu_profile_path_;
677+
}
659678
#endif // HAVE_INSPECTOR
660679

661680
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
namespace profiler {
7373
class V8CoverageConnection;
74+
class V8CpuProfilerConnection;
7475
} // namespace profiler
7576
#endif // HAVE_INSPECTOR
7677

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

11301131
inline void set_coverage_directory(const char* directory);
11311132
inline const std::string& coverage_directory() const;
1133+
1134+
void set_cpu_profiler_connection(
1135+
std::unique_ptr<profiler::V8CpuProfilerConnection> connection);
1136+
profiler::V8CpuProfilerConnection* cpu_profiler_connection();
1137+
1138+
inline void set_cpu_profile_path(const std::string& path);
1139+
inline const 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: 144 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-
const char* const kPathSeparator = "/";
28-
#else
26+
#ifdef _WIN32
2927
const char* const kPathSeparator = "\\/";
28+
/* MAX_PATH is in characters, not bytes. Make sure we have enough headroom. */
29+
#define CWD_BUFSIZE (MAX_PATH * 4)
30+
#else
31+
#include <climits> // PATH_MAX on Solaris.
32+
const char* const kPathSeparator = "/";
33+
#define CWD_BUFSIZE (PATH_MAX)
3034
#endif
3135

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

187+
void V8CpuProfilerConnection::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+
Local<Context> context = env()->context();
198+
HandleScope handle_scope(isolate);
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 covert profiling message\n");
207+
}
208+
WriteCpuProfile(result);
209+
}
210+
211+
bool V8CpuProfilerConnection::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 false;
227+
}
228+
}
229+
MaybeLocal<String> result = GetResult(message);
230+
if (result.IsEmpty()) {
231+
return false;
232+
}
233+
return WriteResult(path.c_str(), result.ToLocalChecked());
234+
}
235+
236+
MaybeLocal<String> V8CpuProfilerConnection::GetResult(Local<String> message) {
237+
Local<Context> context = env()->context();
238+
Isolate* isolate = env()->isolate();
239+
Local<Value> parsed;
240+
if (!v8::JSON::Parse(context, message).ToLocal(&parsed) ||
241+
!parsed->IsObject()) {
242+
fprintf(stderr, "Failed to parse CPU profile result as JSON object\n");
243+
return MaybeLocal<String>();
244+
}
245+
246+
Local<Value> result_v;
247+
if (!parsed.As<Object>()
248+
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result"))
249+
.ToLocal(&result_v)) {
250+
fprintf(stderr, "Failed to get result from CPU profile message\n");
251+
return MaybeLocal<String>();
252+
}
253+
254+
if (!result_v->IsObject()) {
255+
fprintf(stderr, "'result' from CPU profile message is not an object\n");
256+
return MaybeLocal<String>();
257+
}
258+
259+
Local<Value> profile_v;
260+
if (!result_v.As<Object>()
261+
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "profile"))
262+
.ToLocal(&profile_v)) {
263+
fprintf(stderr, "'profile' from CPU profile result is undefined\n");
264+
return MaybeLocal<String>();
265+
}
266+
267+
Local<String> result_s;
268+
if (!v8::JSON::Stringify(context, profile_v).ToLocal(&result_s)) {
269+
fprintf(stderr, "Failed to stringify CPU profile result\n");
270+
return MaybeLocal<String>();
271+
}
272+
273+
return result_s;
274+
}
275+
276+
void V8CpuProfilerConnection::Start() {
277+
Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.start\n");
278+
Isolate* isolate = env()->isolate();
279+
Local<String> enable = FIXED_ONE_BYTE_STRING(
280+
isolate, R"({"id": 1, "method": "Profiler.enable"})");
281+
Local<String> start = FIXED_ONE_BYTE_STRING(
282+
isolate, R"({"id": 2, "method": "Profiler.start"})");
283+
DispatchMessage(enable);
284+
DispatchMessage(start);
285+
}
286+
287+
void V8CpuProfilerConnection::End() {
288+
CHECK_EQ(ending_, false);
289+
ending_ = true;
290+
Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.stop\n");
291+
Isolate* isolate = env()->isolate();
292+
HandleScope scope(isolate);
293+
Local<String> end =
294+
FIXED_ONE_BYTE_STRING(isolate, R"({"id": 3, "method": "Profiler.stop"})");
295+
DispatchMessage(end);
296+
}
297+
183298
// For now, we only support coverage profiling, but we may add more
184299
// in the future.
185300
void EndStartedProfilers(Environment* env) {
@@ -190,6 +305,12 @@ void EndStartedProfilers(Environment* env) {
190305
env, DebugCategory::INSPECTOR_PROFILER, "Ending coverage collection\n");
191306
connection->End();
192307
}
308+
309+
connection = env->cpu_profiler_connection();
310+
if (connection != nullptr && !connection->ending()) {
311+
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending cpu profiling\n");
312+
connection->End();
313+
}
193314
}
194315

195316
void StartCoverageCollection(Environment* env) {
@@ -198,6 +319,26 @@ void StartCoverageCollection(Environment* env) {
198319
env->coverage_connection()->Start();
199320
}
200321

322+
void StartCpuProfiling(Environment* env, const std::string& profile_path) {
323+
std::string path;
324+
if (profile_path.empty()) {
325+
char cwd[CWD_BUFSIZE];
326+
size_t size = CWD_BUFSIZE;
327+
int err = uv_cwd(cwd, &size);
328+
// TODO(joyeecheung): fallback to exec path / argv[0]
329+
CHECK_EQ(err, 0);
330+
CHECK_GT(size, 0);
331+
DiagnosticFilename filename(env, "CPU", "cpuprofile");
332+
path = cwd + std::string(kPathSeparator) + (*filename);
333+
} else {
334+
path = profile_path;
335+
}
336+
env->set_cpu_profile_path(std::move(path));
337+
env->set_cpu_profiler_connection(
338+
std::make_unique<V8CpuProfilerConnection>(env));
339+
env->cpu_profiler_connection()->Start();
340+
}
341+
201342
static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
202343
CHECK(args[0]->IsString());
203344
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+
class V8CpuProfilerConnection : public V8ProfilerConnection {
72+
public:
73+
explicit V8CpuProfilerConnection(Environment* env)
74+
: V8ProfilerConnection(env) {}
75+
76+
void Start() override;
77+
void End() override;
78+
void OnMessage(const v8_inspector::StringView& message) override;
79+
bool ending() const override { return ending_; }
80+
81+
private:
82+
bool WriteCpuProfile(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
@@ -252,6 +252,12 @@ MaybeLocal<Value> RunBootstrapping(Environment* env) {
252252
#endif // HAVE_INSPECTOR
253253
}
254254

255+
#if HAVE_INSPECTOR
256+
if (env->options()->cpu_prof) {
257+
profiler::StartCpuProfiling(env, env->options()->cpu_prof_path);
258+
}
259+
#endif // HAVE_INSPECTOR
260+
255261
// Add a reference to the global object
256262
Local<Object> global = context->Global();
257263

src/node_internals.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ void MarkBootstrapComplete(const v8::FunctionCallbackInfo<v8::Value>& args);
312312
#if HAVE_INSPECTOR
313313
namespace profiler {
314314
void StartCoverageCollection(Environment* env);
315+
void StartCpuProfiling(Environment* env, const std::string& profile_name);
315316
void EndStartedProfilers(Environment* env);
316317
}
317318
#endif // HAVE_INSPECTOR

src/node_options.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,18 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
317317
&EnvironmentOptions::prof_process);
318318
// Options after --prof-process are passed through to the prof processor.
319319
AddAlias("--prof-process", { "--prof-process", "--" });
320+
#if HAVE_INSPECTOR
321+
AddOption("--cpu-prof",
322+
"Start the V8 CPU profiler on start up, and write the CPU profile "
323+
"to disk before exit. If --cpu-prof-path is not specified, write "
324+
"the profile to the current working directory.",
325+
&EnvironmentOptions::cpu_prof);
326+
AddOption("--cpu-prof-path",
327+
"Path the V8 CPU profile generated with --cpu-prof will be "
328+
"written to.",
329+
&EnvironmentOptions::cpu_prof_path);
330+
Implies("--cpu-prof-path", "--cpu-prof");
331+
#endif // HAVE_INSPECTOR
320332
AddOption("--redirect-warnings",
321333
"write warnings to file instead of stderr",
322334
&EnvironmentOptions::redirect_warnings,

src/node_options.h

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

0 commit comments

Comments
 (0)