inspector: implement --heap-prof

In addition implements --heap-prof-name, --heap-prof-dir and
--heap-prof-interval.
These flags are similar to --cpu-prof flags but they are meant
for the V8 sampling heap profiler instead of the CPU profiler.

PR-URL: https://github.com/nodejs/node/pull/27596
Fixes: https://github.com/nodejs/node/issues/27421
Reviewed-By: Jan Krems <jan.krems@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
Joyee Cheung 2019-04-23 11:47:28 +08:00 committed by Anna Henningsen
parent e2c0c0c680
commit 4b74dae6b2
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
14 changed files with 682 additions and 0 deletions

View File

@ -245,6 +245,57 @@ new X();
added: v12.0.0
-->
### `--heap-prof`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Starts the V8 heap profiler on start up, and writes the heap profile to disk
before exit.
If `--heap-prof-dir` is not specified, the generated profile will be placed
in the current working directory.
If `--heap-prof-name` is not specified, the generated profile will be
named `Heap.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.heapprofile`.
```console
$ node --heap-prof index.js
$ ls *.heapprofile
Heap.20190409.202950.15293.0.001.heapprofile
```
### `--heap-prof-dir`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Specify the directory where the heap profiles generated by `--heap-prof` will
be placed.
### `--heap-prof-interval`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Specify the average sampling interval in bytes for the heap profiles generated
by `--heap-prof`. The default is 512 * 1024 bytes.
### `--heap-prof-name`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Specify the file name of the heap profile generated by `--heap-prof`.
Generates a heap snapshot each time the process receives the specified signal.
`signal` must be a valid signal name. Disabled by default.

View File

@ -139,6 +139,28 @@ Enable experimental frozen intrinsics support.
.It Fl -heapsnapshot-signal Ns = Ns Ar signal
Generate heap snapshot on specified signal.
.
.It Fl -heap-prof
Start the V8 heap profiler on start up, and write the heap profile to disk
before exit. If
.Fl -heap-prof-dir
is not specified, the profile will be written to the current working directory
with a generated file name.
.
.It Fl -heap-prof-dir
The directory where the heap profiles generated by
.Fl -heap-prof
will be placed.
.
.It Fl -heap-prof-interval
The average sampling interval in bytes for the heap profiles generated by
.Fl -heap-prof .
The default is
.Sy 512 * 1024 .
.
.It Fl -heap-prof-name
File name of the V8 heap profile generated with
.Fl -heap-prof
.
.It Fl -http-parser Ns = Ns Ar library
Chooses an HTTP parser library. Available values are
.Sy llhttp

View File

@ -689,6 +689,41 @@ inline const std::string& Environment::cpu_prof_dir() const {
return cpu_prof_dir_;
}
inline void Environment::set_heap_profiler_connection(
std::unique_ptr<profiler::V8HeapProfilerConnection> connection) {
CHECK_NULL(heap_profiler_connection_);
std::swap(heap_profiler_connection_, connection);
}
inline profiler::V8HeapProfilerConnection*
Environment::heap_profiler_connection() {
return heap_profiler_connection_.get();
}
inline void Environment::set_heap_prof_name(const std::string& name) {
heap_prof_name_ = name;
}
inline const std::string& Environment::heap_prof_name() const {
return heap_prof_name_;
}
inline void Environment::set_heap_prof_dir(const std::string& dir) {
heap_prof_dir_ = dir;
}
inline const std::string& Environment::heap_prof_dir() const {
return heap_prof_dir_;
}
inline void Environment::set_heap_prof_interval(uint64_t interval) {
heap_prof_interval_ = interval;
}
inline uint64_t Environment::heap_prof_interval() const {
return heap_prof_interval_;
}
#endif // HAVE_INSPECTOR
inline std::shared_ptr<HostPort> Environment::inspector_host_port() {

View File

@ -73,6 +73,7 @@ class AgentWriterHandle;
namespace profiler {
class V8CoverageConnection;
class V8CpuProfilerConnection;
class V8HeapProfilerConnection;
} // namespace profiler
#endif // HAVE_INSPECTOR
@ -1151,6 +1152,20 @@ class Environment : public MemoryRetainer {
inline void set_cpu_prof_dir(const std::string& dir);
inline const std::string& cpu_prof_dir() const;
void set_heap_profiler_connection(
std::unique_ptr<profiler::V8HeapProfilerConnection> connection);
profiler::V8HeapProfilerConnection* heap_profiler_connection();
inline void set_heap_prof_name(const std::string& name);
inline const std::string& heap_prof_name() const;
inline void set_heap_prof_dir(const std::string& dir);
inline const std::string& heap_prof_dir() const;
inline void set_heap_prof_interval(uint64_t interval);
inline uint64_t heap_prof_interval() const;
#endif // HAVE_INSPECTOR
private:
@ -1190,6 +1205,10 @@ class Environment : public MemoryRetainer {
std::string cpu_prof_dir_;
std::string cpu_prof_name_;
uint64_t cpu_prof_interval_;
std::unique_ptr<profiler::V8HeapProfilerConnection> heap_profiler_connection_;
std::string heap_prof_dir_;
std::string heap_prof_name_;
uint64_t heap_prof_interval_;
#endif // HAVE_INSPECTOR
std::shared_ptr<EnvironmentOptions> options_;

View File

@ -258,6 +258,44 @@ void V8CpuProfilerConnection::End() {
DispatchMessage("Profiler.stop");
}
std::string V8HeapProfilerConnection::GetDirectory() const {
return env()->heap_prof_dir();
}
std::string V8HeapProfilerConnection::GetFilename() const {
return env()->heap_prof_name();
}
MaybeLocal<Object> V8HeapProfilerConnection::GetProfile(Local<Object> result) {
Local<Value> profile_v;
if (!result
->Get(env()->context(),
FIXED_ONE_BYTE_STRING(env()->isolate(), "profile"))
.ToLocal(&profile_v)) {
fprintf(stderr, "'profile' from heap profile result is undefined\n");
return MaybeLocal<Object>();
}
if (!profile_v->IsObject()) {
fprintf(stderr, "'profile' from heap profile result is not an Object\n");
return MaybeLocal<Object>();
}
return profile_v.As<Object>();
}
void V8HeapProfilerConnection::Start() {
DispatchMessage("HeapProfiler.enable");
std::string params = R"({ "samplingInterval": )";
params += std::to_string(env()->heap_prof_interval());
params += " }";
DispatchMessage("HeapProfiler.startSampling", params.c_str());
}
void V8HeapProfilerConnection::End() {
CHECK_EQ(ending_, false);
ending_ = true;
DispatchMessage("HeapProfiler.stopSampling");
}
// For now, we only support coverage profiling, but we may add more
// in the future.
void EndStartedProfilers(Environment* env) {
@ -268,6 +306,12 @@ void EndStartedProfilers(Environment* env) {
connection->End();
}
connection = env->heap_profiler_connection();
if (connection != nullptr && !connection->ending()) {
Debug(env, DebugCategory::INSPECTOR_PROFILER, "Ending heap profiling\n");
connection->End();
}
connection = env->coverage_connection();
if (connection != nullptr && !connection->ending()) {
Debug(
@ -313,6 +357,20 @@ void StartProfilers(Environment* env) {
std::make_unique<V8CpuProfilerConnection>(env));
env->cpu_profiler_connection()->Start();
}
if (env->options()->heap_prof) {
const std::string& dir = env->options()->heap_prof_dir;
env->set_heap_prof_interval(env->options()->heap_prof_interval);
env->set_heap_prof_dir(dir.empty() ? GetCwd() : dir);
if (env->options()->heap_prof_name.empty()) {
DiagnosticFilename filename(env, "Heap", "heapprofile");
env->set_heap_prof_name(*filename);
} else {
env->set_heap_prof_name(env->options()->heap_prof_name);
}
env->set_heap_profiler_connection(
std::make_unique<profiler::V8HeapProfilerConnection>(env));
env->heap_profiler_connection()->Start();
}
}
static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) {

View File

@ -107,6 +107,26 @@ class V8CpuProfilerConnection : public V8ProfilerConnection {
bool ending_ = false;
};
class V8HeapProfilerConnection : public V8ProfilerConnection {
public:
explicit V8HeapProfilerConnection(Environment* env)
: V8ProfilerConnection(env) {}
void Start() override;
void End() override;
const char* type() const override { return "heap"; }
bool ending() const override { return ending_; }
std::string GetDirectory() const override;
std::string GetFilename() const override;
v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override;
private:
std::unique_ptr<inspector::InspectorSession> session_;
bool ending_ = false;
};
} // namespace profiler
} // namespace node

View File

@ -168,6 +168,19 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
}
}
if (!heap_prof) {
if (!heap_prof_name.empty()) {
errors->push_back("--heap-prof-name must be used with --heap-prof");
}
if (!heap_prof_dir.empty()) {
errors->push_back("--heap-prof-dir must be used with --heap-prof");
}
// We can't catch the case where the value passed is the default value,
// then the option just becomes a noop which is fine.
if (heap_prof_interval != kDefaultHeapProfInterval) {
errors->push_back("--heap-prof-interval must be used with --heap-prof");
}
}
debug_options_.CheckOptions(errors);
#endif // HAVE_INSPECTOR
}
@ -369,6 +382,24 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"Directory where the V8 profiles generated by --cpu-prof will be "
"placed. Does not affect --prof.",
&EnvironmentOptions::cpu_prof_dir);
AddOption(
"--heap-prof",
"Start the V8 heap profiler on start up, and write the heap profile "
"to disk before exit. If --heap-prof-dir is not specified, write "
"the profile to the current working directory.",
&EnvironmentOptions::heap_prof);
AddOption("--heap-prof-name",
"specified file name of the V8 CPU profile generated with "
"--heap-prof",
&EnvironmentOptions::heap_prof_name);
AddOption("--heap-prof-dir",
"Directory where the V8 heap profiles generated by --heap-prof "
"will be placed.",
&EnvironmentOptions::heap_prof_dir);
AddOption("--heap-prof-interval",
"specified sampling interval in bytes for the V8 heap "
"profile generated with --heap-prof. (default: 512 * 1024)",
&EnvironmentOptions::heap_prof_interval);
#endif // HAVE_INSPECTOR
AddOption("--redirect-warnings",
"write warnings to file instead of stderr",

View File

@ -115,6 +115,11 @@ class EnvironmentOptions : public Options {
uint64_t cpu_prof_interval = kDefaultCpuProfInterval;
std::string cpu_prof_name;
bool cpu_prof = false;
std::string heap_prof_dir;
std::string heap_prof_name;
static const uint64_t kDefaultHeapProfInterval = 512 * 1024;
uint64_t heap_prof_interval = kDefaultHeapProfInterval;
bool heap_prof = false;
#endif // HAVE_INSPECTOR
std::string redirect_warnings;
bool throw_deprecation = false;

View File

@ -0,0 +1,17 @@
'use strict';
const util = require('util');
const total = parseInt(process.env.TEST_ALLOCATION) || 100;
let count = 0;
let string = '';
function runAllocation() {
string += util.inspect(process.env);
if (count++ < total) {
setTimeout(runAllocation, 1);
} else {
console.log(string.length);
process.exit(55);
}
}
setTimeout(runAllocation, 1);

View File

@ -0,0 +1,17 @@
'use strict';
const util = require('util');
const total = parseInt(process.env.TEST_ALLOCATION) || 100;
let count = 0;
let string = '';
function runAllocation() {
string += util.inspect(process.env);
if (count++ < total) {
setTimeout(runAllocation, 1);
} else {
console.log(string.length);
process.kill(process.pid, "SIGINT");
}
}
setTimeout(runAllocation, 1);

View File

@ -0,0 +1,11 @@
'use strict';
const { Worker } = require('worker_threads');
const path = require('path');
new Worker(path.join(__dirname, 'allocation.js'), {
execArgv: [
'--heap-prof',
'--heap-prof-interval',
process.HEAP_PROF_INTERVAL || '128',
]
});

View File

@ -0,0 +1,5 @@
'use strict';
const { Worker } = require('worker_threads');
const path = require('path');
new Worker(path.join(__dirname, 'allocation.js'));

16
test/fixtures/workload/allocation.js vendored Normal file
View File

@ -0,0 +1,16 @@
'use strict';
const util = require('util');
const total = parseInt(process.env.TEST_ALLOCATION) || 100;
let count = 0;
let string = '';
function runAllocation() {
string += util.inspect(process.env);
if (count++ < total) {
setTimeout(runAllocation, 1);
} else {
console.log(string.length);
}
}
setTimeout(runAllocation, 1);

View File

@ -0,0 +1,375 @@
'use strict';
// This tests that --heap-prof, --heap-prof-dir and --heap-prof-name works.
const common = require('../common');
const fixtures = require('../common/fixtures');
common.skipIfInspectorDisabled();
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');
function getHeapProfiles(dir) {
const list = fs.readdirSync(dir);
return list
.filter((file) => file.endsWith('.heapprofile'))
.map((file) => path.join(dir, file));
}
function findFirstFrameInNode(root, func) {
const first = root.children.find(
(child) => child.callFrame.functionName === func
);
if (first) {
return first;
}
for (const child of root.children) {
const first = findFirstFrameInNode(child, func);
if (first) {
return first;
}
}
return undefined;
}
function findFirstFrame(file, func) {
const data = fs.readFileSync(file, 'utf8');
const profile = JSON.parse(data);
const first = findFirstFrameInNode(profile.head, func);
return { frame: first, roots: profile.head.children };
}
function verifyFrames(output, file, func) {
const { frame, roots } = findFirstFrame(file, func);
if (!frame) {
// Show native debug output and the profile for debugging.
console.log(output.stderr.toString());
console.log(roots);
}
assert.notDeepStrictEqual(frame, undefined);
}
// We need to set --heap-prof-interval to a small enough value to make
// sure we can find our workload in the samples, so we need to set
// TEST_ALLOCATION > kHeapProfInterval.
const kHeapProfInterval = 128;
const TEST_ALLOCATION = kHeapProfInterval * 2;
const env = {
...process.env,
TEST_ALLOCATION,
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER'
};
// Test --heap-prof without --heap-prof-interval. Here we just verify that
// we manage to generate a profile.
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof',
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
console.log(output);
}
assert.strictEqual(output.status, 0);
const profiles = getHeapProfiles(tmpdir.path);
assert.strictEqual(profiles.length, 1);
}
// Outputs heap profile when event loop is drained.
// TODO(joyeecheung): share the fixutres with v8 coverage tests
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
console.log(output);
}
assert.strictEqual(output.status, 0);
const profiles = getHeapProfiles(tmpdir.path);
assert.strictEqual(profiles.length, 1);
verifyFrames(output, profiles[0], 'runAllocation');
}
// Outputs heap profile when process.exit(55) exits process.
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation-exit.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 55) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 55);
const profiles = getHeapProfiles(tmpdir.path);
assert.strictEqual(profiles.length, 1);
verifyFrames(output, profiles[0], 'runAllocation');
}
// Outputs heap profile when process.kill(process.pid, "SIGINT"); exits process.
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation-sigint.js'),
], {
cwd: tmpdir.path,
env
});
if (!common.isWindows) {
if (output.signal !== 'SIGINT') {
console.log(output.stderr.toString());
}
assert.strictEqual(output.signal, 'SIGINT');
}
const profiles = getHeapProfiles(tmpdir.path);
assert.strictEqual(profiles.length, 1);
verifyFrames(output, profiles[0], 'runAllocation');
}
// Outputs heap profile from worker when execArgv is set.
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
fixtures.path('workload', 'allocation-worker-argv.js'),
], {
cwd: tmpdir.path,
env: {
...process.env,
HEAP_PROF_INTERVAL: '128'
}
});
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
const profiles = getHeapProfiles(tmpdir.path);
assert.strictEqual(profiles.length, 1);
verifyFrames(output, profiles[0], 'runAllocation');
}
// --heap-prof-name without --heap-prof
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof-name',
'test.heapprofile',
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
const stderr = output.stderr.toString().trim();
if (output.status !== 9) {
console.log(stderr);
}
assert.strictEqual(output.status, 9);
assert.strictEqual(
stderr,
`${process.execPath}: --heap-prof-name must be used with --heap-prof`);
}
// --heap-prof-dir without --heap-prof
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof-dir',
'prof',
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
const stderr = output.stderr.toString().trim();
if (output.status !== 9) {
console.log(stderr);
}
assert.strictEqual(output.status, 9);
assert.strictEqual(
stderr,
`${process.execPath}: --heap-prof-dir must be used with --heap-prof`);
}
// --heap-prof-interval without --heap-prof
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
const stderr = output.stderr.toString().trim();
if (output.status !== 9) {
console.log(stderr);
}
assert.strictEqual(output.status, 9);
assert.strictEqual(
stderr,
`${process.execPath}: ` +
'--heap-prof-interval must be used with --heap-prof');
}
// --heap-prof-name
{
tmpdir.refresh();
const file = path.join(tmpdir.path, 'test.heapprofile');
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-name',
'test.heapprofile',
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
const profiles = getHeapProfiles(tmpdir.path);
assert.deepStrictEqual(profiles, [file]);
verifyFrames(output, file, 'runAllocation');
}
// relative --heap-prof-dir
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-dir',
'prof',
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
const dir = path.join(tmpdir.path, 'prof');
assert(fs.existsSync(dir));
const profiles = getHeapProfiles(dir);
assert.strictEqual(profiles.length, 1);
verifyFrames(output, profiles[0], 'runAllocation');
}
// absolute --heap-prof-dir
{
tmpdir.refresh();
const dir = path.join(tmpdir.path, 'prof');
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-dir',
dir,
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
assert(fs.existsSync(dir));
const profiles = getHeapProfiles(dir);
assert.strictEqual(profiles.length, 1);
verifyFrames(output, profiles[0], 'runAllocation');
}
// --heap-prof-dir and --heap-prof-name
{
tmpdir.refresh();
const dir = path.join(tmpdir.path, 'prof');
const file = path.join(dir, 'test.heapprofile');
const output = spawnSync(process.execPath, [
'--heap-prof',
'--heap-prof-name',
'test.heapprofile',
'--heap-prof-dir',
dir,
'--heap-prof-interval',
kHeapProfInterval,
fixtures.path('workload', 'allocation.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
assert(fs.existsSync(dir));
const profiles = getHeapProfiles(dir);
assert.deepStrictEqual(profiles, [file]);
verifyFrames(output, file, 'runAllocation');
}
{
tmpdir.refresh();
const output = spawnSync(process.execPath, [
'--heap-prof-interval',
kHeapProfInterval,
'--heap-prof-dir',
'prof',
'--heap-prof',
fixtures.path('workload', 'allocation-worker.js'),
], {
cwd: tmpdir.path,
env
});
if (output.status !== 0) {
console.log(output.stderr.toString());
}
assert.strictEqual(output.status, 0);
const dir = path.join(tmpdir.path, 'prof');
assert(fs.existsSync(dir));
const profiles = getHeapProfiles(dir);
assert.strictEqual(profiles.length, 2);
const profile1 = findFirstFrame(profiles[0], 'runAllocation');
const profile2 = findFirstFrame(profiles[1], 'runAllocation');
if (!profile1.frame && !profile2.frame) {
// Show native debug output and the profile for debugging.
console.log(output.stderr.toString());
console.log('heap path: ', profiles[0]);
console.log(profile1.roots);
console.log('heap path: ', profiles[1]);
console.log(profile2.roots);
}
assert(profile1.frame || profile2.frame);
}