node-oom-heapdump
Version:
Create a V8 heap snapshot when an "Out of Memory" error occurs, or create a heap snapshot or CPU profile on request.
194 lines (174 loc) • 5.87 kB
JavaScript
let cp = require("child_process");
let fs = require("fs");
let path = require("path");
class NodeOomHeapDumpImpl {
constructor(options) {
this._opts = options;
this._files = [];
this._busy = false;
this._count = 0;
if (this._opts.heapdumpOnOOM) {
if (options.OOMImplementation === "NATIVE_HOOK") {
require('bindings')('node_oom_heapdump_native.node').call(this._getHeapSnapshotPath(this._opts.path), this._opts.addTimestamp);
}
}
}
/**
* Calls the designated worker and returns a promise
* @param {String} workerPath - path of the worker
* @param {String[]} workerArgs - arguments to worker
* @return {Promise} resolve on success, reject on error
*/
_callWorker(workerPath, workerArgs) {
if (this._busy) {
return new Promise((resolve, reject) => {
reject(new Error("A CPU profile or heapdump is already being created, please retry later."));
});
}
var args = [path.resolve(__dirname, workerPath)].concat(workerArgs);
// use 'require-main-filename' module instead of require.main.filename, see https://github.com/blueconic/node-oom-heapdump/issues/3
let mainFilename = require('require-main-filename')();
// start worker
let child = cp.spawn('node', args, {
cmd: path.dirname(mainFilename),
stdio: 'inherit'
});
return new Promise((resolve, reject) => {
this._busy = true;
var error = null;
child.on('error', (err) => {
error = err;
reject(err);
});
child.on('exit', (code) => {
if (!error) {
if (code === 0) {
resolve();
} else {
reject(new Error("Worker exited with statusCode: " + code));
}
}
this._busy = false;
});
});
}
/**
* Returns the path to the created heap snapshot in a promise, or rejects on error
* @param {String} snapshotPath - path of the snapshot
* @param {String} logPrefix - optional log prefix message when heapdump is created
* @return {Promise} the heap snapshot path on success or error on rejection
*/
createHeapSnapshot(snapshotPath, logPrefix) {
snapshotPath = this._getHeapSnapshotPath(snapshotPath, logPrefix);
// start OoMworker to create heapdump
return this._callWorker('./heapdumpWorker.js', [this._opts.port, snapshotPath, logPrefix || ""]).then(() => {
if (logPrefix === "OoM") {
this._count++;
}
if (!this._files.includes(snapshotPath)) {
this._files.push(snapshotPath);
}
return snapshotPath;
});
}
/**
* Returns the path to the created CPU profile in a promise, or rejects on error
* @param {String} cpuProfilePath - path of the CPU profile
* @param {number} duration - the duration of the cpu profile (in ms)
* @return {Promise} the CPU profile path on success or error on rejection
*/
createCpuProfile(cpuProfilePath, duration) {
if (!cpuProfilePath) {
cpuProfilePath = path.resolve(__dirname, "../cpuprofile-" + Date.now());
}
if (!cpuProfilePath.endsWith(".cpuprofile")) {
cpuProfilePath += ".cpuprofile";
}
// start OoMworker to create heapdump
return this._callWorker('./cpuProfileWorker.js', [this._opts.port, cpuProfilePath, duration]).then(() => {
if (!this._files.includes(cpuProfilePath)) {
this._files.push(cpuProfilePath);
}
return cpuProfilePath;
});
}
/**
* Delete all created heap snapshots
*/
deleteAllHeapSnapshots() {
this._files.forEach((snapshotPath) => {
if (snapshotPath.endsWith(".heapsnapshot")) {
this.deleteHeapSnapshot(snapshotPath);
}
});
}
/**
* Deletes a particular snapshot from disk
* @param {String} snapshotPath - path of the heap snapshot to delete
* @return {Promise}
*/
deleteHeapSnapshot(snapshotPath) {
return this._deleteFile(snapshotPath);
}
/**
* Delete all created CPU profiles
*/
deleteAllCpuProfiles() {
this._files.forEach((path) => {
if (path.endsWith(".cpuprofile")) {
this.deleteCpuProfile(path);
}
});
}
/**
* Deletes a particular CPU profile from disk
* @param {String} cpuProfilePath - path of the CPU profile to delete
* @return {Promise}
*/
deleteCpuProfile(cpuProfilePath) {
return this._deleteFile(cpuProfilePath);
}
/**
* Deletes a given CPU profile or heapsnapshot from disk
* @param {String} path - path to the file to delete
* @return {Promise}
*/
_deleteFile(path) {
return new Promise((resolve, reject) => {
if (this._files.includes(path)) {
fs.unlink(path, (err) => {
if (err) {
reject(err);
} else {
resolve(path);
}
});
} else {
reject(new Error("File not found:" + path));
}
});
}
/**
* Returns a proper snapshot path, based on the given parameter
* @param {String} snapshotPath
* @param {String} logPrefix
* @return {String} path
*/
_getHeapSnapshotPath(snapshotPath, logPrefix) {
if (!snapshotPath) {
snapshotPath = path.resolve(__dirname, "../heapsnapshot-" + Date.now());
}
if (logPrefix === "OoM" && this._opts.addTimestamp) {
if (snapshotPath.endsWith(".heapsnapshot")) {
snapshotPath = snapshotPath.replace(".heapsnapshot", "");
}
// in case of OoM error, add timestamp if needed
snapshotPath += "-" + Date.now();
}
if (!snapshotPath.endsWith(".heapsnapshot")) {
snapshotPath += ".heapsnapshot";
}
return snapshotPath;
}
}
module.exports = NodeOomHeapDumpImpl;