Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: implement --cpu-prof[-path] #27147

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ $ node --completion-bash > node_bash_completion
$ source node_bash_completion
```

### `--cpu-prof`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Starts the V8 CPU profiler on start up, and writes the CPU profile to disk
before exit. If `--cpu-prof-path` is not specified, the profile will be
written to `${cwd}/CPU.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.cpuprofile`.

```console
$ node --cpu-prof index.js
$ ls *.cpuprofile
CPU.20190409.202950.15293.0.0.cpuprofile
```

### `--cpu-prof-path`
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Location where the the CPU profile generated by `--cpu-prof`
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
should be written to. When used alone, it implies `--cpu-prof`.

```console
$ node --cpu-prof-path /tmp/test.cpuprofile index.js
```

### `--diagnostic-report-directory=directory`
<!-- YAML
added: v11.8.0
Expand Down
12 changes: 12 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ Aborting instead of exiting causes a core file to be generated for analysis.
.It Fl -completion-bash
Print source-able bash completion script for Node.js.
.
.It Fl -cpu-prof
Start the V8 CPU profiler on start up, and write the CPU profile to disk
before exit. If
.Fl -cpu-prof-path
is not specified, the profile will be written to the current working directory.
.
.It Fl -cpu-prof-path
Path the V8 CPU profile generated with
.Fl -cpu-prof
will be written to. When used alone, it implies
.Fl -cpu-prof
.
.It Fl -diagnostic-report-directory
Location at which the
.Sy diagnostic report
Expand Down
19 changes: 19 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,25 @@ inline profiler::V8CoverageConnection* Environment::coverage_connection() {
inline const std::string& Environment::coverage_directory() const {
return coverage_directory_;
}

inline void Environment::set_cpu_profiler_connection(
std::unique_ptr<profiler::V8CpuProfilerConnection> connection) {
CHECK_NULL(cpu_profiler_connection_);
std::swap(cpu_profiler_connection_, connection);
}

inline profiler::V8CpuProfilerConnection*
Environment::cpu_profiler_connection() {
return cpu_profiler_connection_.get();
}

inline void Environment::set_cpu_profile_path(const std::string& path) {
cpu_profile_path_ = path;
}

inline const std::string& Environment::cpu_profile_path() const {
return cpu_profile_path_;
}
#endif // HAVE_INSPECTOR

inline std::shared_ptr<HostPort> Environment::inspector_host_port() {
Expand Down
10 changes: 10 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class AgentWriterHandle;
#if HAVE_INSPECTOR
namespace profiler {
class V8CoverageConnection;
class V8CpuProfilerConnection;
} // namespace profiler
#endif // HAVE_INSPECTOR

Expand Down Expand Up @@ -1129,6 +1130,13 @@ class Environment : public MemoryRetainer {

inline void set_coverage_directory(const char* directory);
inline const std::string& coverage_directory() const;

void set_cpu_profiler_connection(
std::unique_ptr<profiler::V8CpuProfilerConnection> connection);
profiler::V8CpuProfilerConnection* cpu_profiler_connection();

inline void set_cpu_profile_path(const std::string& path);
inline const std::string& cpu_profile_path() const;
#endif // HAVE_INSPECTOR

private:
Expand Down Expand Up @@ -1163,7 +1171,9 @@ class Environment : public MemoryRetainer {

#if HAVE_INSPECTOR
std::unique_ptr<profiler::V8CoverageConnection> coverage_connection_;
std::unique_ptr<profiler::V8CpuProfilerConnection> cpu_profiler_connection_;
std::string coverage_directory_;
std::string cpu_profile_path_;
#endif // HAVE_INSPECTOR

std::shared_ptr<EnvironmentOptions> options_;
Expand Down
147 changes: 144 additions & 3 deletions src/inspector_profiler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ using v8::Value;
using v8_inspector::StringBuffer;
using v8_inspector::StringView;

#ifdef __POSIX__
const char* const kPathSeparator = "/";
#else
#ifdef _WIN32
const char* const kPathSeparator = "\\/";
/* MAX_PATH is in characters, not bytes. Make sure we have enough headroom. */
#define CWD_BUFSIZE (MAX_PATH * 4)
#else
#include <climits> // PATH_MAX on Solaris.
const char* const kPathSeparator = "/";
#define CWD_BUFSIZE (PATH_MAX)
#endif

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

void V8CpuProfilerConnection::OnMessage(
const v8_inspector::StringView& message) {
Debug(env(),
DebugCategory::INSPECTOR_PROFILER,
"Receive cpu profiling message, ending = %s\n",
ending_ ? "true" : "false");
if (!ending_) {
return;
}
Isolate* isolate = env()->isolate();
Local<Context> context = env()->context();
HandleScope handle_scope(isolate);
Context::Scope context_scope(context);
Local<String> result;
if (!String::NewFromTwoByte(isolate,
message.characters16(),
NewStringType::kNormal,
message.length())
.ToLocal(&result)) {
fprintf(stderr, "Failed to covert profiling message\n");
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
}
WriteCpuProfile(result);
}

bool V8CpuProfilerConnection::WriteCpuProfile(Local<String> message) {
const std::string& path = env()->cpu_profile_path();
CHECK(!path.empty());
std::string directory = path.substr(0, path.find_last_of(kPathSeparator));
if (directory != path) {
uv_fs_t req;
int ret = fs::MKDirpSync(nullptr, &req, directory, 0777, nullptr);
uv_fs_req_cleanup(&req);
if (ret < 0 && ret != UV_EEXIST) {
char err_buf[128];
uv_err_name_r(ret, err_buf, sizeof(err_buf));
fprintf(stderr,
"%s: Failed to create cpu profile directory %s\n",
err_buf,
directory.c_str());
return false;
}
}
MaybeLocal<String> result = GetResult(message);
if (result.IsEmpty()) {
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
return WriteResult(path.c_str(), result.ToLocalChecked());
}

MaybeLocal<String> V8CpuProfilerConnection::GetResult(Local<String> message) {
Local<Context> context = env()->context();
Isolate* isolate = env()->isolate();
Local<Value> parsed;
if (!v8::JSON::Parse(context, message).ToLocal(&parsed) ||
!parsed->IsObject()) {
fprintf(stderr, "Failed to parse CPU profile result as JSON object\n");
return MaybeLocal<String>();
}

Local<Value> result_v;
if (!parsed.As<Object>()
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "result"))
.ToLocal(&result_v)) {
fprintf(stderr, "Failed to get result from CPU profile message\n");
return MaybeLocal<String>();
}

if (!result_v->IsObject()) {
fprintf(stderr, "'result' from CPU profile message is not an object\n");
return MaybeLocal<String>();
}

Local<Value> profile_v;
if (!result_v.As<Object>()
->Get(context, FIXED_ONE_BYTE_STRING(isolate, "profile"))
.ToLocal(&profile_v)) {
fprintf(stderr, "'profile' from CPU profile result is undefined\n");
return MaybeLocal<String>();
}

Local<String> result_s;
if (!v8::JSON::Stringify(context, profile_v).ToLocal(&result_s)) {
fprintf(stderr, "Failed to stringify CPU profile result\n");
return MaybeLocal<String>();
}

return result_s;
}

void V8CpuProfilerConnection::Start() {
Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.start\n");
Isolate* isolate = env()->isolate();
Local<String> enable = FIXED_ONE_BYTE_STRING(
isolate, R"({"id": 1, "method": "Profiler.enable"})");
Local<String> start = FIXED_ONE_BYTE_STRING(
isolate, R"({"id": 2, "method": "Profiler.start"})");
DispatchMessage(enable);
DispatchMessage(start);
}

void V8CpuProfilerConnection::End() {
CHECK_EQ(ending_, false);
ending_ = true;
Debug(env(), DebugCategory::INSPECTOR_PROFILER, "Sending Profiler.stop\n");
Isolate* isolate = env()->isolate();
HandleScope scope(isolate);
Local<String> end =
FIXED_ONE_BYTE_STRING(isolate, R"({"id": 3, "method": "Profiler.stop"})");
DispatchMessage(end);
}

// For now, we only support coverage profiling, but we may add more
// in the future.
void EndStartedProfilers(Environment* env) {
Expand All @@ -190,6 +305,12 @@ void EndStartedProfilers(Environment* env) {
env, DebugCategory::INSPECTOR_PROFILER, "Ending coverage collection\n");
connection->End();
}

connection = env->cpu_profiler_connection();
if (connection != nullptr && !connection->ending()) {
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending cpu profiling\n");
connection->End();
}
}

void StartCoverageCollection(Environment* env) {
Expand All @@ -198,6 +319,26 @@ void StartCoverageCollection(Environment* env) {
env->coverage_connection()->Start();
}

void StartCpuProfiling(Environment* env, const std::string& profile_path) {
std::string path;
if (profile_path.empty()) {
char cwd[CWD_BUFSIZE];
size_t size = CWD_BUFSIZE;
int err = uv_cwd(cwd, &size);
// TODO(joyeecheung): fallback to exec path / argv[0]
CHECK_EQ(err, 0);
CHECK_GT(size, 0);
DiagnosticFilename filename(env, "CPU", "cpuprofile");
Copy link
Member Author

@joyeecheung joyeecheung Apr 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex This is where the file name is formatted.

Pending #27142 to be an exact match to the format described in the doc even for the first file generated.

path = cwd + std::string(kPathSeparator) + (*filename);
} else {
path = profile_path;
}
env->set_cpu_profile_path(std::move(path));
env->set_cpu_profiler_connection(
std::make_unique<V8CpuProfilerConnection>(env));
env->cpu_profiler_connection()->Start();
}

static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
Environment* env = Environment::GetCurrent(args);
Expand Down
18 changes: 18 additions & 0 deletions src/inspector_profiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ class V8CoverageConnection : public V8ProfilerConnection {
bool ending_ = false;
};

class V8CpuProfilerConnection : public V8ProfilerConnection {
public:
explicit V8CpuProfilerConnection(Environment* env)
: V8ProfilerConnection(env) {}

void Start() override;
void End() override;
void OnMessage(const v8_inspector::StringView& message) override;
bool ending() const override { return ending_; }

private:
bool WriteCpuProfile(v8::Local<v8::String> message);
v8::MaybeLocal<v8::String> GetResult(v8::Local<v8::String> message);

std::unique_ptr<inspector::InspectorSession> session_;
bool ending_ = false;
};

} // namespace profiler
} // namespace node

Expand Down
6 changes: 6 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ MaybeLocal<Value> RunBootstrapping(Environment* env) {
#endif // HAVE_INSPECTOR
}

#if HAVE_INSPECTOR
if (env->options()->cpu_prof) {
profiler::StartCpuProfiling(env, env->options()->cpu_prof_path);
}
#endif // HAVE_INSPECTOR

// Add a reference to the global object
Local<Object> global = context->Global();

Expand Down
1 change: 1 addition & 0 deletions src/node_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ void MarkBootstrapComplete(const v8::FunctionCallbackInfo<v8::Value>& args);
#if HAVE_INSPECTOR
namespace profiler {
void StartCoverageCollection(Environment* env);
void StartCpuProfiling(Environment* env, const std::string& profile_name);
void EndStartedProfilers(Environment* env);
}
#endif // HAVE_INSPECTOR
Expand Down
12 changes: 12 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,18 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::prof_process);
// Options after --prof-process are passed through to the prof processor.
AddAlias("--prof-process", { "--prof-process", "--" });
#if HAVE_INSPECTOR
AddOption("--cpu-prof",
"Start the V8 CPU profiler on start up, and write the CPU profile "
"to disk before exit. If --cpu-prof-path is not specified, write "
"the profile to the current working directory.",
&EnvironmentOptions::cpu_prof);
AddOption("--cpu-prof-path",
"Path the V8 CPU profile generated with --cpu-prof will be "
"written to.",
&EnvironmentOptions::cpu_prof_path);
Implies("--cpu-prof-path", "--cpu-prof");
#endif // HAVE_INSPECTOR
AddOption("--redirect-warnings",
"write warnings to file instead of stderr",
&EnvironmentOptions::redirect_warnings,
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ class EnvironmentOptions : public Options {
bool preserve_symlinks = false;
bool preserve_symlinks_main = false;
bool prof_process = false;
#if HAVE_INSPECTOR
std::string cpu_prof_path;
bool cpu_prof = false;
#endif // HAVE_INSPECTOR
std::string redirect_warnings;
bool throw_deprecation = false;
bool trace_deprecation = false;
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/workload/fibonacci-exit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';
function fib(n) {
if (n === 0 || n === 1) return n;
return fib(n - 1) + fib(n - 2);
}
fib(parseInt(process.argv[2]) || 35);
process.exit(55);
7 changes: 7 additions & 0 deletions test/fixtures/workload/fibonacci-sigint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';
function fib(n) {
if (n === 0 || n === 1) return n;
return fib(n - 1) + fib(n - 2);
}
fib(parseInt(process.argv[2]) || 35);
process.kill(process.pid, "SIGINT");
7 changes: 7 additions & 0 deletions test/fixtures/workload/fibonacci-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const { Worker } = require('worker_threads');
const path = require('path');
new Worker(path.join(__dirname, 'fibonacci.js'), {
execArgv: ['--cpu-prof']
});
8 changes: 8 additions & 0 deletions test/fixtures/workload/fibonacci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';
function fib(n) {
if (n === 0 || n === 1) return n;
return fib(n - 1) + fib(n - 2);
}

const n = parseInt(process.env.FIB) || 35;
process.stdout.write(`${fib(n)}\n`);
Loading