Skip to content

Commit 2601e53

Browse files
committed
Added CPU profile functionality
Added CPU profile functionality
1 parent 9081a42 commit 2601e53

11 files changed

+295
-54
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,5 @@ typings/
5757
# dotenv environment variables file
5858
.env
5959

60-
*.heapsnapshot
60+
*.heapsnapshot
61+
*.cpuprofile

README.md

+31-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# node-oom-heapdump
22
Node module which will create a V8 heap snapshot right before an "Out of Memory" error occurs.
3-
It can also create heapdumps on request like 'v8-profiler', but does this off-process so it doesn't interfere with execution of the main process.
3+
It can also create heapdumps and CPU profiles on request like 'v8-profiler', but does this off-process so it doesn't interfere with execution of the main process.
44

55
Tested on Node.js 8.x, but should also work fine using Node.js 6.3 upwards (According to: https://chromedevtools.github.io/devtools-protocol/v8/).
66

@@ -54,8 +54,8 @@ These might impact performance though.
5454
* port - optionally, the alternative DevTools protocol port. Defaults to 9229. Should map on the port given to the --inspect arg.
5555

5656
# API
57-
Besides creating heapdumps when an out of memory error occurs, there also is an API for creating heapdumps on request.
58-
See below for the currently available API.
57+
Besides creating heapdumps when an out of memory error occurs, there also is an API for creating heapdumps and CPU profiles on request. See below for the currently available API.
58+
Notice that you cannot create a heapdump while a CPU profile is being generated and vice versa; an Error will be thrown if this is the case.
5959

6060
```javascript
6161
let nodeOomHeapdump = require("node-oom-heapdump")({
@@ -67,7 +67,7 @@ let nodeOomHeapdump = require("node-oom-heapdump")({
6767
* @param {String} snapshotPath - path of the snapshot
6868
* @return {Promise} Promise containing the heap snapshot path on success or error on rejection
6969
*/
70-
nodeOomHeapdump.createHeapSnapshot("mypath").then((snapshotPath) => {
70+
nodeOomHeapdump.createHeapSnapshot("myheapsnapshotpath").then((snapshotPath) => {
7171
// do something with heap snapshot
7272

7373
// and delete again from disk
@@ -87,4 +87,31 @@ nodeOomHeapdump.deleteAllHeapSnapshots();
8787
* @return {Promise}
8888
*/
8989
nodeOomHeapdump.deleteHeapSnapshot(snapshotPath);
90+
91+
/**
92+
* Returns the path to the created CPU profile in a promise, or rejects on error
93+
* @param {String} cpuProfilePath - path of the CPU profile
94+
* @param {number} duration - the duration of the CPU profile in ms (default: 30000ms)
95+
* @return {Promise} the CPU profile path on success or error on rejection
96+
*/
97+
nodeOomHeapdump.createCpuProfile("mycpuprofilepath", 10000).then((cpuProfilePath) => {
98+
// do something with CPU profile
99+
100+
// and delete again from disk
101+
nodeOomHeapdump.deleteCpuProfile(cpuProfilePath);
102+
}).catch((err) => {
103+
// handle error
104+
});
105+
106+
/**
107+
* Deletes all previously created CPU profiles from disk
108+
*/
109+
nodeOomHeapdump.deleteAllCpuProfiles();
110+
111+
/**
112+
* Deletes a particular CPU profile from disk
113+
* @param {String} cpuProfilePath - path to the CPU profile to delete from disk
114+
* @return {Promise}
115+
*/
116+
nodeOomHeapdump.deleteCpuProfile(cpuProfilePath);
90117
```

index.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,38 @@ class NodeOomHeapdumpAPI {
4343
deleteHeapSnapshot(snapshotPath) {
4444
return this._impl.deleteHeapSnapshot(snapshotPath);
4545
}
46-
}
4746

47+
/**
48+
* Returns the path to the created CPU profile in a promise, or rejects on error
49+
* @param {String} cpuProfilePath - path of the CPU profile
50+
* @param {number} duration - the duration of the CPU profile in ms
51+
* @return {Promise} the CPU profile path on success or error on rejection
52+
*/
53+
createCpuProfile(cpuProfilePath, duration) {
54+
if (duration === undefined) {
55+
duration = 30000;
56+
} else {
57+
duration = parseInt(duration);
58+
}
59+
return this._impl.createCpuProfile(cpuProfilePath, duration);
60+
}
61+
62+
/**
63+
* Deletes all previously created CPU profiles from disk
64+
*/
65+
deleteAllCpuProfiles() {
66+
this._impl.deleteAllCpuProfiles();
67+
}
68+
69+
/**
70+
* Deletes a particular CPU profile from disk
71+
* @param {String} cpuProfilePath - path to the CPU profile to delete from disk
72+
* @return {Promise}
73+
*/
74+
deleteCpuProfile(cpuProfilePath) {
75+
return this._impl.deleteCpuProfile(cpuProfilePath);
76+
}
77+
}
4878

4979
// utility functions
5080
function parseOptions(options) {

lib/cpuProfileWorker.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
let fs = require('fs');
2+
3+
// set global variables based on args passed on to this oomWorker
4+
let devToolsPort = process.argv[2];
5+
let path = process.argv[3];
6+
let duration = process.argv[4];
7+
8+
console.error('Started CPU profile (duration: %sms) %s worker on DevTools port \'%s\'.', duration, path, devToolsPort);
9+
10+
let CDP = require('chrome-remote-interface');
11+
let writeStream = fs.createWriteStream(path);
12+
let handleError = function () {
13+
console.error(arguments);
14+
writeStream.end();
15+
process.exit(-1);
16+
};
17+
writeStream.on('error', (err) => {
18+
handleError("CPU profile path not valid or writable", err);
19+
});
20+
21+
CDP({
22+
host: 'localhost',
23+
port: devToolsPort,
24+
}, (debugInstance) => {
25+
let cpuProfiler = debugInstance.Profiler;
26+
cpuProfiler.enable();
27+
cpuProfiler.start();
28+
29+
setTimeout(() => {
30+
let Profile = cpuProfiler.stop();
31+
Profile.then((p) => {
32+
writeStream.write(JSON.stringify(p.profile));
33+
writeStream.end();
34+
35+
cpuProfiler.disable();
36+
37+
console.error('CPU profile created in \'%s\'. Exiting worker now.', path);
38+
39+
// were done, exit normally
40+
process.exit(0);
41+
}).catch((err) => {
42+
handleError(err);
43+
});
44+
}, duration);
45+
}).on('error', (err) => {
46+
// cannot connect to the remote endpoint
47+
handleError(err);
48+
});

lib/heapdumpWorker.js

+11-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ let logPrefix = (process.argv[4]) ? process.argv[4] + " " : "";
88
console.error('Started heapdump %sworker for \'%s\' on DevTools port \'%s\'.', logPrefix, path, devToolsPort);
99

1010
let CDP = require('chrome-remote-interface');
11-
11+
let writeStream = fs.createWriteStream(path);
12+
let handleError = function () {
13+
console.error(arguments);
14+
writeStream.end();
15+
process.exit(-1);
16+
};
17+
writeStream.on('error', (err) => {
18+
handleError("Heapdump path not valid or writable", err);
19+
});
1220
CDP({
1321
host: 'localhost',
1422
port: devToolsPort,
@@ -19,12 +27,6 @@ CDP({
1927
let heapProfiler = debugInstance.HeapProfiler;
2028
heapProfiler.enable();
2129

22-
let writeStream = fs.createWriteStream(path);
23-
writeStream.on('error', (err) => {
24-
console.error("Heapdump path not valid or writable", err);
25-
process.exit(-1);
26-
});
27-
2830
debugInstance.on('HeapProfiler.addHeapSnapshotChunk', function (evt) {
2931
writeStream.write(evt.chunk);
3032
});
@@ -42,8 +44,5 @@ CDP({
4244
process.exit(0);
4345
});
4446
}).on('error', (err) => {
45-
// cannot connect to the remote endpoint
46-
console.error(err);
47-
48-
process.exit(-1);
49-
});
47+
handleError(err);
48+
});

lib/index.js

+107-33
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ let path = require("path");
55
class NodeOomHeapDumpImpl {
66
constructor(options) {
77
this._opts = options;
8-
this._heapSnapshots = [];
8+
this._files = [];
99
this._busy = false;
1010
this._count = 0;
1111
this._limitReached = false;
@@ -48,31 +48,22 @@ class NodeOomHeapDumpImpl {
4848
}
4949

5050
/**
51-
* Returns the path to the created heap snapshot in a promise, or rejects on error
52-
* @param {String} snapshotPath - path of the snapshot
53-
* @param {String} logPrefix - optional log prefix message when heapdumo is created
54-
* @return {Promise} the heap snapshot path on success or error on rejection
51+
* Calls the designated worker and returns a promise
52+
* @param {String} workerPath - path of the worker
53+
* @param {String[]} workerArgs - arguments to worker
54+
* @return {Promise} resolve on success, reject on error
5555
*/
56-
createHeapSnapshot(snapshotPath, logPrefix) {
57-
if (!snapshotPath) {
58-
snapshotPath = path.resolve(__dirname, "./heapsnapshot");
59-
}
60-
if (logPrefix === "OoM" && this._opts.addTimestamp){
61-
if (snapshotPath.endsWith(".heapsnapshot")) {
62-
snapshotPath = snapshotPath.replace(".heapsnapshot", "");
63-
}
64-
// in case of OoM error, add if timestamp if needed
65-
snapshotPath += "-" + Date.now();
66-
}
67-
if (!snapshotPath.endsWith(".heapsnapshot")) {
68-
snapshotPath += ".heapsnapshot";
56+
_callWorker(workerPath, workerArgs) {
57+
if (this._busy) {
58+
return new Promise((resolve, reject) => {
59+
reject(new Error("A CPU profile or heapdump is already being created, please retry later."));
60+
});
6961
}
7062

71-
// resolve to absolute file path
72-
snapshotPath = path.resolve(snapshotPath);
63+
var args = [path.resolve(__dirname, workerPath)].concat(workerArgs);
7364

74-
// start OoMworker to create heapdump
75-
let child = cp.spawn('node', [path.resolve(__dirname, './heapdumpWorker.js'), this._opts.port, snapshotPath, logPrefix || ""], {
65+
// start worker
66+
let child = cp.spawn('node', args, {
7667
cmd: path.dirname(require.main.filename),
7768
stdio: 'inherit'
7869
});
@@ -87,23 +78,77 @@ class NodeOomHeapDumpImpl {
8778
});
8879
child.on('exit', () => {
8980
if (!error) {
90-
this._count++;
91-
if (!this._heapSnapshots.includes(snapshotPath)) {
92-
this._heapSnapshots.push(snapshotPath);
93-
}
94-
resolve(snapshotPath);
81+
resolve();
9582
}
9683
this._busy = false;
9784
});
9885
});
9986
}
10087

88+
/**
89+
* Returns the path to the created heap snapshot in a promise, or rejects on error
90+
* @param {String} snapshotPath - path of the snapshot
91+
* @param {String} logPrefix - optional log prefix message when heapdump is created
92+
* @return {Promise} the heap snapshot path on success or error on rejection
93+
*/
94+
createHeapSnapshot(snapshotPath, logPrefix) {
95+
if (!snapshotPath) {
96+
snapshotPath = path.resolve(__dirname, "../heapsnapshot-" + Date.now());
97+
}
98+
if (logPrefix === "OoM" && this._opts.addTimestamp) {
99+
if (snapshotPath.endsWith(".heapsnapshot")) {
100+
snapshotPath = snapshotPath.replace(".heapsnapshot", "");
101+
}
102+
// in case of OoM error, add timestamp if needed
103+
snapshotPath += "-" + Date.now();
104+
}
105+
if (!snapshotPath.endsWith(".heapsnapshot")) {
106+
snapshotPath += ".heapsnapshot";
107+
}
108+
109+
// start OoMworker to create heapdump
110+
return this._callWorker('./heapdumpWorker.js', [this._opts.port, snapshotPath, logPrefix || ""]).then(() => {
111+
if (logPrefix === "OoM") {
112+
this._count++;
113+
}
114+
if (!this._files.includes(snapshotPath)) {
115+
this._files.push(snapshotPath);
116+
}
117+
return snapshotPath;
118+
});
119+
}
120+
121+
/**
122+
* Returns the path to the created CPU profile in a promise, or rejects on error
123+
* @param {String} cpuProfilePath - path of the CPU profile
124+
* @param {number} duration - the duration of the cpu profile (in ms)
125+
* @return {Promise} the CPU profile path on success or error on rejection
126+
*/
127+
createCpuProfile(cpuProfilePath, duration) {
128+
if (!cpuProfilePath) {
129+
cpuProfilePath = path.resolve(__dirname, "../cpuprofile-" + Date.now());
130+
}
131+
if (!cpuProfilePath.endsWith(".cpuprofile")) {
132+
cpuProfilePath += ".cpuprofile";
133+
}
134+
135+
// start OoMworker to create heapdump
136+
return this._callWorker('./cpuProfileWorker.js', [this._opts.port, cpuProfilePath, duration]).then(() => {
137+
if (!this._files.includes(cpuProfilePath)) {
138+
this._files.push(cpuProfilePath);
139+
}
140+
return cpuProfilePath;
141+
});
142+
}
143+
101144
/**
102145
* Delete all created heap snapshots
103146
*/
104147
deleteAllHeapSnapshots() {
105-
this._heapSnapshots.forEach((snapshotPath) => {
106-
this.deleteHeapSnapshot(snapshotPath);
148+
this._files.forEach((snapshotPath) => {
149+
if (snapshotPath.endsWith(".heapsnapshot")) {
150+
this.deleteHeapSnapshot(snapshotPath);
151+
}
107152
});
108153
}
109154

@@ -113,17 +158,46 @@ class NodeOomHeapDumpImpl {
113158
* @return {Promise}
114159
*/
115160
deleteHeapSnapshot(snapshotPath) {
161+
return this._deleteFile(snapshotPath);
162+
}
163+
164+
/**
165+
* Delete all created CPU profiles
166+
*/
167+
deleteAllCpuProfiles() {
168+
this._files.forEach((path) => {
169+
if (path.endsWith(".cpuprofile")) {
170+
this.deleteCpuProfile(path);
171+
}
172+
});
173+
}
174+
175+
/**
176+
* Deletes a particular CPU profile from disk
177+
* @param {String} cpuProfilePath - path of the CPU profile to delete
178+
* @return {Promise}
179+
*/
180+
deleteCpuProfile(cpuProfilePath) {
181+
return this._deleteFile(cpuProfilePath);
182+
}
183+
184+
/**
185+
* Deletes a given CPU profile or heapsnapshot from disk
186+
* @param {String} path - path to the file to delete
187+
* @return {Promise}
188+
*/
189+
_deleteFile(path) {
116190
return new Promise((resolve, reject) => {
117-
if (this._heapSnapshots.includes(snapshotPath)) {
118-
fs.unlink(snapshotPath, (err) => {
191+
if (this._files.includes(path)) {
192+
fs.unlink(path, (err) => {
119193
if (err) {
120194
reject(err);
121195
} else {
122-
resolve(snapshotPath);
196+
resolve(path);
123197
}
124198
});
125199
} else {
126-
reject(new Error("File not found:" + snapshotPath));
200+
reject(new Error("File not found:" + path));
127201
}
128202
});
129203
}

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)