pm2/lib/Interactor/Utility.js

235 lines
6.2 KiB
JavaScript

var path = require('path');
// EWMA = ExponentiallyWeightedMovingAverage from
// https://github.com/felixge/node-measured/blob/master/lib/util/ExponentiallyMovingWeightedAverage.js
// used to compute the nbr of time per minute that a variance is hit by a new trace
function EWMA () {
this._timePeriod = 60000
this._tickInterval = 5000
this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod)
this._count = 0
this._rate = 0
var self = this
this._interval = setInterval(function () {
self.tick()
}, this._tickInterval)
this._interval.unref()
}
EWMA.prototype.update = function (n) {
this._count += n || 1
}
EWMA.prototype.tick = function () {
var instantRate = this._count / this._tickInterval
this._count = 0
this._rate += (this._alpha * (instantRate - this._rate))
}
EWMA.prototype.rate = function (timeUnit) {
return (this._rate || 0) * timeUnit
}
var moment = require('moment');
/**
* Simple cache implementation
*
* @param {Object} opts cache options
* @param {Integer} opts.ttl time to live of all the keys
* @param {Function} opts.miss function called when a key isn't found in the cache
*/
function Cache (opts) {
this._cache = {};
this._miss = opts.miss;
this._ttl_time = opts.ttl;
this._ttl = {};
if (opts.ttl) {
setInterval(this._worker.bind(this), 1000);
}
}
/**
* Task running to check TTL and potentially remove older key
*/
Cache.prototype._worker = function () {
var keys = Object.keys(this._ttl);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = this._ttl[key];
if (moment().isAfter(value)) {
delete this._cache[key];
delete this._ttl[key];
}
}
};
/**
* Empty the cache
*/
Cache.prototype.reset = function () {
this._cache = null;
this._cache = {};
this._ttl = null;
this._ttl = {};
};
/**
* Get a value from the cache
*
* @param {String} key
*/
Cache.prototype.get = function (key) {
if (!key) return null;
var value = this._cache[key];
if (value) return value;
value = this._miss(key);
if (value) {
this.set(key, value);
}
return value;
};
/**
* Set a value in the cache
*
* @param {String} key
* @param {Mixed} value
*/
Cache.prototype.set = function (key, value) {
if (!key || !value) return false;
this._cache[key] = value;
if (this._ttl_time) {
this._ttl[key] = moment().add(this._ttl_time, 'seconds');
}
return true;
};
/**
* StackTraceParser is used to parse callsite from stacktrace
* and get from FS the context of the error (if available)
*
* @param {Cache} cache cache implementation used to query file from FS and get context
*/
function StackTraceParser (opts) {
this._cache = opts.cache;
this._context_size = opts.context;
}
StackTraceParser.prototype.attachContext = function (error) {
var self = this;
if (!error) return error;
// if pmx attached callsites we can parse them to retrieve the context
if (typeof (error.stackframes) === 'object') {
var result = self.parse(error.stackframes);
// no need to send it since there is already the stacktrace
delete error.stackframes;
delete error.__error_callsites;
if (result) {
error.callsite = result.callsite;
error.context = result.context;
}
}
// if the stack is here we can parse it directly from the stack string
// only if the context has been retrieved from elsewhere
if (typeof error.stack === 'string' && !error.callsite) {
var siteRegex = /(\/[^\\\n]*)/g;
var tmp;
var stack = [];
// find matching groups
while ((tmp = siteRegex.exec(error.stack))) {
stack.push(tmp[1]);
}
// parse each callsite to match the format used by the stackParser
stack = stack.map(function (callsite) {
// remove the trailing ) if present
if (callsite[callsite.length - 1] === ')') {
callsite = callsite.substr(0, callsite.length - 1);
}
var location = callsite.split(':');
return location.length < 3 ? callsite : {
file_name: location[0],
line_number: parseInt(location[1])
};
});
var finalCallsite = self.parse(stack);
if (finalCallsite) {
error.callsite = finalCallsite.callsite;
error.context = finalCallsite.context;
}
}
return error;
};
/**
* Parse the stacktrace and return callsite + context if available
*/
StackTraceParser.prototype.parse = function (stack) {
var self = this;
if (!stack || stack.length === 0) return false;
for (var i = 0, len = stack.length; i < len; i++) {
var callsite = stack[i];
// avoid null values
if (typeof callsite !== 'object') continue;
if (!callsite.file_name || !callsite.line_number) continue;
var type = path.isAbsolute(callsite.file_name) || callsite.file_name[0] === '.' ? 'user' : 'core';
// only use the callsite if its inside user space
if (!callsite || type === 'core' || callsite.file_name.indexOf('node_modules') > -1 ||
callsite.file_name.indexOf('vxx') > -1) {
continue;
}
// get the whole context (all lines) and cache them if necessary
var context = self._cache.get(callsite.file_name);
var source = [];
if (context && context.length > 0) {
// get line before the call
var preLine = callsite.line_number - self._context_size - 1;
var pre = context.slice(preLine > 0 ? preLine : 0, callsite.line_number - 1);
if (pre && pre.length > 0) {
pre.forEach(function (line) {
source.push(line.replace(/\t/g, ' '));
});
}
// get the line where the call has been made
if (context[callsite.line_number - 1]) {
source.push(context[callsite.line_number - 1].replace(/\t/g, ' ').replace(' ', '>>'));
}
// and get the line after the call
var postLine = callsite.line_number + self._context_size;
var post = context.slice(callsite.line_number, postLine);
if (post && post.length > 0) {
post.forEach(function (line) {
source.push(line.replace(/\t/g, ' '));
});
}
}
return {
context: source.length > 0 ? source.join('\n') : 'cannot retrieve source context',
callsite: [ callsite.file_name, callsite.line_number ].join(':')
};
}
return false;
};
module.exports = {
EWMA: EWMA,
Cache: Cache,
StackTraceParser: StackTraceParser
};