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

15 files changed

+480
-3
lines changed

doc/api/cli.md

+31
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

+12
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

+19
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,25 @@ inline profiler::V8CoverageConnection* Environment::coverage_connection() {
659659
inline const std::string& Environment::coverage_directory() const {
660660
return coverage_directory_;
661661
}
662+
663+
inline void Environment::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+
inline void Environment::set_cpu_profile_path(const std::string& path) {
675+
cpu_profile_path_ = path;
676+
}
677+
678+
inline const 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

+10
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

+143-3
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,116 @@ 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+
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+
void 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;
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+
void V8CpuProfilerConnection::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+
void V8CpuProfilerConnection::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
void EndStartedProfilers(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
void StartCoverageCollection(Environment* env) {
@@ -198,6 +318,26 @@ void StartCoverageCollection(Environment* env) {
198318
env->coverage_connection()->Start();
199319
}
200320

321+
void StartCpuProfiling(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
static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
202342
CHECK(args[0]->IsString());
203343
Environment* env = Environment::GetCurrent(args);

src/inspector_profiler.h

+18
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+
void 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

+6
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

+1
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
namespace profiler {
316316
void StartCoverageCollection(Environment* env);
317+
void StartCpuProfiling(Environment* env, const std::string& profile_name);
317318
void EndStartedProfilers(Environment* env);
318319
}
319320
#endif // HAVE_INSPECTOR

src/node_options.cc

+12
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

+4
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;
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
function fib(n) {
3+
if (n === 0 || n === 1) return n;
4+
return fib(n - 1) + fib(n - 2);
5+
}
6+
fib(parseInt(process.argv[2]) || 35);
7+
process.exit(55);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
function fib(n) {
3+
if (n === 0 || n === 1) return n;
4+
return fib(n - 1) + fib(n - 2);
5+
}
6+
fib(parseInt(process.argv[2]) || 35);
7+
process.kill(process.pid, "SIGINT");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
const { Worker } = require('worker_threads');
4+
const path = require('path');
5+
new Worker(path.join(__dirname, 'fibonacci.js'), {
6+
execArgv: ['--cpu-prof']
7+
});

test/fixtures/workload/fibonacci.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
function fib(n) {
3+
if (n === 0 || n === 1) return n;
4+
return fib(n - 1) + fib(n - 2);
5+
}
6+
7+
const n = parseInt(process.env.FIB) || 35;
8+
process.stdout.write(`${fib(n)}\n`);

0 commit comments

Comments
 (0)