version app + node.exe + nw.exe

This commit is contained in:
Antoine WEBER
2015-10-13 21:47:32 +02:00
parent 858c5bd713
commit ca00dcd224
2642 changed files with 388899 additions and 6 deletions

230
node_modules/cluster/lib/plugins/cli.js generated vendored Normal file
View File

@@ -0,0 +1,230 @@
/*!
* Cluster - cli
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, Log = require('log');
/**
* Commands.
*/
var commands = [];
/**
* Adds a command-line interface to your cluster.
*
* This plugin requires that you use `pidfiles()`
* above `cli()`, so that the pidfile directory
* is exposed.
*
* Examples:
*
* cluster(server)
* .use(cluster.pidfiles())
* .use(cluster.cli())
* .listen(3000);
*
* Once set up our server script serves as both
* the master, and the CLI. For example we may
* still launch the server(s) as shown below.
*
* $ nohup node server.js &
*
* However now we may also utilize commands
* provided by this plugin.
*
* $ node server.js status
*
* master 3281 dead
* worker 0 3282 dead
*
* For more command information use `--help`.
*
* $ node server.js --help
*
* @return {Function}
* @api public
*/
exports = module.exports = function(){
return function(master){
requirePIDs(master);
// augment master
master.killall = function(sig){
var pid = master.pidof('master');
try {
// signal master
process.kill(pid, sig);
} catch (err) {
if ('ESRCH' != err.code) throw err;
// signal children individually
master.workerpids().forEach(function(pid){
try {
process.kill(pid, sig);
} catch (err) {
if ('ESRCH' != err.code) throw err;
}
});
}
};
var args = process.argv.slice(2)
, len = commands.length
, command
, arg;
// parse arguments
while (args.length) {
arg = args.shift();
for (var i = 0; i < len; ++i) {
command = commands[i];
if (~command.flags.indexOf(arg)) {
command.callback(master);
master.preventDefault = true;
}
}
}
}
};
/**
* Define command `name` with the given callback `fn(master)`
* and a short `desc`.
*
* @param {String} name
* @param {Function} fn
* @param {String} desc
* @return {Object} exports for chaining
* @api public
*/
var define = exports.define = function(name, fn, desc){
commands.push({
flags: name.split(/ *, */)
, desc: desc
, callback: fn
});
return exports;
};
/**
* Report master / worker status based on
* the PID files saved by the pidfiles()
* plugin.
*/
define('-s, --status, status', function(master){
var dir = master.pidfiles
, files = fs.readdirSync(dir);
// null signal failed previous
// to this release
if (process.version < 'v0.4.1') {
console.log('status will not work with node < 0.4.1');
console.log('due to SIGTERM globbering the null signal');
process.exit(1);
}
console.log();
// only pids
files.filter(function(file){
return file.match(/\.pid$/);
// check status
}).forEach(function(file){
var name = file.replace('.pid', '')
, pid = master.pidof(name)
, name = name.replace('.', ' ')
, color
, status;
try {
process.kill(pid, 0);
status = 'alive';
color = '36';
} catch (err) {
if ('ESRCH' == err.code) {
color = '31';
status = 'dead';
} else {
throw err;
}
}
console.log(' %s\033[90m %d\033[0m \033[' + color + 'm%s\033[0m'
, name
, pid
, status);
});
console.log();
}, 'Output cluster status');
/**
* Restart workers.
*/
define('-r, --restart, restart', function(master){
master.killall('SIGUSR2');
}, 'Restart master by sending the SIGUSR2 signal');
/**
* Graceful shutdown.
*/
define('-g, --shutdown, shutdown', function(master){
master.killall('SIGQUIT');
}, 'Graceful shutdown by sending the SIGQUIT signal');
/**
* Hard shutdown.
*/
define('-S, --stop, stop', function(master){
master.killall('SIGTERM');
}, 'Hard shutdown by sending the SIGTERM signal');
/**
* Display help information.
*/
define('-h, --help, help', function(master){
console.log('\n Usage: node <file> <command>\n');
commands.forEach(function(command){
console.log(' '
+ command.flags.join(', ')
+ '\n '
+ '\033[90m' + command.desc + '\033[0m'
+ '\n');
});
console.log();
}, 'Show help information');
/**
* Output cluster version.
*/
define('-v, --version', function(master){
console.log(require('../cluster').version);
}, 'Output cluster version');
/**
* Require `pidfiles()` plugin.
*
* @param {Master} master
* @api private
*/
function requirePIDs(master) {
if (master.pidfiles) return;
throw new Error('cli() plugin requires pidfiles(), please add pidfiles() before cli()');
}

125
node_modules/cluster/lib/plugins/debug.js generated vendored Normal file
View File

@@ -0,0 +1,125 @@
/*!
* Cluster - debug
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Enable verbose debugging output.
*
* @return {Function}
* @api public
*/
module.exports = function(options){
options = options || {};
// strip colors
function color(text) {
if (options.colors === false) return text.replace(/\033\[\d+m/g, '');
return text
}
// logger
var log = {
debug: function(str){
console.error(color(' \033[90mdebug - %s\033[0m'), str);
},
info: function(str){
console.error(color(' info \033[90m- %s\033[0m'), str);
},
warning: function(str){
console.error(color(' \033[33mwarning\033[0m \033[90m- %s\033[0m'), str);
},
error: function(str){
console.error(color(' \033[31merror\033[0m \033[90m- %s\033[0m'), str);
}
};
return function(master){
// start
master.on('start', function(){
log.info('master started');
});
// closing
master.on('closing', function(){
log.info('shutting down');
});
// close
master.on('close', function(){
log.info('shutdown complete');
});
// killing workers
master.on('kill', function(sig){
log.warning('kill(' + (sig || 'SIGTERM') + ')');
});
// worker died
master.on('worker killed', function(worker){
if ('restarting' == master.state) return;
log.warning('worker ' + worker.id + ' died');
});
// worker exception
master.on('worker exception', function(worker, err){
log.error('worker ' + worker.id + ' uncaught exception ' + err.message);
});
// worker is waiting on connections to be closed
master.on('worker waiting', function(worker, connections){
log.warning('worker ' + worker.id + ' waiting on ' + connections + ' connections');
});
// worker has timed out
master.on('worker timeout', function(worker, timeout){
log.warning('worker ' + worker.id + ' timed out after ' + timeout + 'ms');
});
// connection
master.on('worker connected', function(worker){
log.info('worker ' + worker.id + ' connected');
});
// removed
master.on('worker removed', function(worker){
log.info('worker ' + worker.id + ' removed');
});
// worker
master.on('worker', function(worker){
log.info('worker ' + worker.id + ' spawned');
});
// listening
master.on('listening', function(){
log.info('listening for connections');
});
// cyclic or immediate restart
master.on('cyclic restart', function(){
log.warning('cyclic restart detected, restarting in ' + master.options['restart timeout'] + 'ms');
});
// restart requested
master.on('restarting', function(){
log.info('restart requested');
});
// restart complete
master.on('restart', function(){
log.info('restart complete');
});
// exit
process.on('exit', function(){
log.debug('exit');
});
}
};

150
node_modules/cluster/lib/plugins/logger.js generated vendored Normal file
View File

@@ -0,0 +1,150 @@
/*!
* Cluster - logger
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, Log = require('log')
, mkdir = require('mkdirp').mkdirp;
/**
* Enable stdout / stderr logs for both the master
* process, as well as workers.
*
* These output to the given `dir`, or `./logs`
* relative to the server's file.
*
* Examples:
*
* // log to ./logs
* engine(server)
* .use(engine.logger())
* .listen(3000);
*
* // log to ./app/logs
* engine(server)
* .use(engine.logger('./app/logs'))
* .listen(3000);
*
* // log to /var/log/node
* engine(server)
* .use(engine.logger('/var/log/node'))
* .listen(3000);
*
* @param {String} dir
* @param {Number} level
* @return {Function}
* @api public
*/
module.exports = function(dir, level){
return function(master){
dir = master.resolve(dir || 'logs');
mkdir(dir, 0755, function(err){
if (err) throw err;
// master log
var stream = fs.createWriteStream(dir + '/master.log', { flags: 'a' });
var log = master.log = new Log(level || Log.INFO, stream);
// master events
master.on('start', function(){
log.info('master started');
});
// master is shutting down
master.on('closing', function(){
log.warning('shutting down master');
});
// master has closed and performed cleanup
master.on('close', function(){
log.info('shutdown complete');
});
// sending signal to all workers
master.on('kill', function(sig){
log.warning('sent kill(%s) to all workers', sig);
});
// worker was killed
master.on('worker killed', function(worker){
if ('restarting' == master.state) return;
log.error('worker %s died', worker.id);
});
// worker exception
master.on('worker exception', function(worker, err){
log.error('worker %s uncaught exception %s', worker.id, err.message);
});
// worker is waiting on connections to be closed
master.on('worker waiting', function(worker, connections){
log.info('worker %s waiting on %s connections', worker.id, connections);
});
// worker has timed out
master.on('worker timeout', function(worker, timeout){
log.warning('worker %s timed out after %sms', worker.id, timeout);
});
// worker connected to master
master.on('worker connected', function(worker){
log.debug('worker %s connected', worker.id);
});
// cyclic or immediate restart
master.on('cyclic restart', function(){
log.warning('cyclic restart detected, restarting in %sms'
, master.options['restart timeout']);
});
// restart requested
master.on('restarting', function(){
log.info('restart requested');
});
// restart complete
master.on('restart', function(){
log.info('restart complete');
});
// repl socket connection established
master.on('repl socket', function(sock){
var from = sock.remoteAddress
? 'from ' + sock.remoteAddress
: '';
sock.on('connect', function(){
log.info('repl connection %s', from);
});
sock.on('close', function(){
log.info('repl disconnect %s', from);
});
});
// override fds
master.customFds = [-1, -1];
// children
master.on('worker', function(worker){
var proc = worker.proc;
log.info('spawned worker ' + worker.id);
// worker log streams
var access = fs.createWriteStream(dir + '/workers.access.log', { flags: 'a' })
, error = fs.createWriteStream(dir + '/workers.error.log', { flags: 'a' });
// redirect stdout / stderr
proc.stdout.pipe(access);
proc.stderr.pipe(error);
});
});
}
};

83
node_modules/cluster/lib/plugins/pidfiles.js generated vendored Normal file
View File

@@ -0,0 +1,83 @@
/*!
* Cluster - pidfiles
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, mkdir = require('mkdirp').mkdirp;
/**
* Save pidfiles to the given `dir` or `./pids`.
*
* Examples:
*
* // save to ./pids
* cluster(server)
* .use(cluster.pidfiles())
* .listen(3000);
*
* // save to /tmp
* cluster(server)
* .use(cluster.pidfiles('/tmp'))
* .listen(3000);
*
* // save to /var/run/node
* cluster(server)
* .use(cluster.logger('/var/run/node'))
* .listen(3000);
*
* @param {String} dir
* @return {Function}
* @api public
*/
module.exports = function(dir){
return function(master){
dir = master.pidfiles = master.resolve(dir || 'pids');
// augment master
master.pidof = function(name){
var dir = master.pidfiles
, path = dir + '/' + name + '.pid'
, pid = fs.readFileSync(path, 'ascii');
return parseInt(pid, 10);
};
master.workerpids = function(){
var dir = master.pidfiles;
return fs.readdirSync(dir).filter(function(file){
return file.match(/^worker\./);
}).map(function(file){
return parseInt(fs.readFileSync(dir + '/' + file, 'ascii'), 10);
});
};
mkdir(dir, 0755, function(err){
if (err) throw err;
// save worker pids
master.on('worker', function(worker){
var path = dir + '/worker.' + worker.id + '.pid';
fs.writeFile(path, worker.proc.pid.toString(), 'ascii', function(err){
if (err) throw err;
master.emit('worker pidfile');
});
});
master.on('listening', function(){
// save master pid
fs.writeFile(dir + '/master.pid', process.pid.toString(), 'ascii', function(err){
if (err) throw err;
master.emit('pidfile');
});
});
});
}
};

118
node_modules/cluster/lib/plugins/reload.js generated vendored Normal file
View File

@@ -0,0 +1,118 @@
/*!
* Cluster - reload
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, basename = require('path').basename
, extname = require('path').extname;
/**
* Restart the server the given js `files` have changed.
* `files` may be several directories, filenames, etc,
* defaulting to the server's root directory.
*
* Options:
*
* - `signal` Signal defaulting to __SIGTERM__
* - `interval` Watcher interval, defaulting to `100`
* - `extensions` File extensions to watch, defaults to ['.js']
*
* Examples:
*
* cluster(server)
* .use(cluster.reload())
* .listen(3000);
*
* cluster(server)
* .use(cluster.reload('lib'))
* .listen(3000);
*
* cluster(server)
* .use(cluster.reload(['lib', 'tests', 'index.js']))
* .listen(3000);
*
* cluster(server)
* .use(cluster.reload('lib', { interval: 60000 }))
* .listen(3000);
*
* cluster(server)
* .use(cluster.reload('lib', { extensions: ['.js', '.coffee'] }))
* .listen(3000);
*
* Ignore Directories:
*
* By default `reload()` will ignore the following directories:
*
* - node_modules
* - support
* - examples
* - test
* - bin
*
* Alter with `reload.ignoreDirectories`
*
* cluster.reload.ignoreDirectories.push('src');
*
* @param {String|Array} files
* @param {Options} options
* @return {Function}
* @api public
*/
exports = module.exports = function(files, options){
options = options || {};
// defaults
var interval = options.interval || 100
, extensions = options.extensions || ['.js']
, signal = options.signal || 'SIGTERM';
return function(master){
if (!files) files = master.dir;
if (!Array.isArray(files)) files = [files];
files.forEach(traverse);
// traverse file if it is a directory
// otherwise setup the watcher
function traverse(file) {
file = master.resolve(file);
fs.stat(file, function(err, stat){
if (!err) {
if (stat.isDirectory()) {
if (~exports.ignoreDirectories.indexOf(basename(file))) return;
fs.readdir(file, function(err, files){
files.map(function(f){
return file + '/' + f;
}).forEach(traverse);
});
} else {
watch(file);
}
}
});
}
// watch file for changes
function watch(file) {
if (!~extensions.indexOf(extname(file))) return;
fs.watchFile(file, { interval: interval }, function(curr, prev){
if (curr.mtime > prev.mtime) {
console.log(' \033[36mchanged\033[0m \033[90m- %s\033[0m', file);
master.restartWorkers(signal);
}
});
}
}
};
/**
* Directories to ignore.
*/
exports.ignoreDirectories = ['node_modules', 'support', 'test', 'bin'];

196
node_modules/cluster/lib/plugins/repl.js generated vendored Normal file
View File

@@ -0,0 +1,196 @@
/*!
* Cluster - repl
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var net = require('net')
, repl = require('repl');
/**
* Enable REPL with all arguments passed to `net.Server#listen()`.
*
* Examples:
*
* cluster(server)
* .use(cluster.stats())
* .use(cluster.repl('/var/run/cluster'))
* .listen();
*
* In the terminal:
*
* $ sudo telnet /var/run/cluster
*
* @return {Function}
* @api public
*/
exports = module.exports = function(){
var args = arguments;
if (!args.length) throw new Error('repl() plugin requires port/host or path');
return function(master){
var server
, sockets = [];
// start repl
function start(){
// TCP or unix-domain socket repl
server = net.createServer(function(sock){
sockets.push(sock);
var ctx = repl.start('cluster> ', sock).context;
master.emit('repl socket', sock);
// augment socket to provide some formatting methods
sock.title = function(str){ this.write('\n \033[36m' + str + '\033[0m\n'); }
sock.row = function(key, val){ this.write(' \033[90m' + key + ':\033[0m ' + val + '\n'); }
// merge commands into context
// executing in context of master
Object.keys(exports).forEach(function(cmd){
ctx[cmd] = function(){
var args = Array.prototype.slice.call(arguments);
args.unshift(master, sock);
return exports[cmd].apply(master, args);
};
});
});
// Apply all arguments given
server.listen.apply(server, args);
}
// initial master starts immediately
// replacements starts when the previous
// has closed
master.on(master.isChild
? 'restart'
: 'start', start);
// restart notification
master.on('restarting', function(){
sockets.forEach(function(sock){
if (sock.fd) {
sock.write('\n\033[33mrestarting\033[0m - closing connection soon\n');
}
});
});
// close
master.on('close', function(){
sockets.forEach(function(sock){
sock.fd && sock.end();
});
if (server && server.fd) server.close();
});
}
};
/**
* Define function `name`, with the given callback
* `fn(master, sock, ...)` and `description`.
*
* @param {String} name
* @param {Function} fn
* @param {String} desc
* @return {Object} exports for chaining
* @api public
*/
var define = exports.define = function(name, fn, desc){
(exports[name] = fn).description = desc;
return exports;
};
/**
* Display commmand help.
*/
define('help', function(master, sock){
sock.title('Commands');
Object.keys(exports).forEach(function(cmd){
if ('define' == cmd) return;
var fn = exports[cmd]
, params = fn.toString().match(/^function +\((.*?)\)/)[1]
, params = params.split(/ *, */).slice(2);
sock.row(
cmd + '(' + params.join(', ') + ')'
, fn.description);
});
sock.write('\n');
}, 'Display help information');
/**
* Spawn `n` additional workers with the given `signal`.
*/
define('spawn', function(master, sock, n, signal){
n = n || 1;
if (n < 0) {
n = Math.abs(n);
sock.write('removing ' + n + ' worker' + (n > 1 ? 's' : '')
+ ' with ' + (signal || 'SIGQUIT') + '\n');
master.remove(n, signal);
} else {
sock.write('spawning ' + n + ' worker' + (n > 1 ? 's' : '') + '\n');
master.spawn(n);
}
}, 'Spawn one or more additional workers, or remove one or more');
/**
* Output process ids.
*/
define('pids', function(master, sock){
sock.title('pids');
sock.row('master', process.pid);
master.children.forEach(function(worker){
sock.row('worker #' + worker.id, worker.proc.pid);
});
sock.write('\n');
}, 'Output process ids');
/**
* Kill the given worker by `id` and `signal`.
*/
define('kill', function(master, sock, id, signal){
var worker = master.children[id];
if (worker) {
worker.proc.kill(signal);
sock.write('sent \033[36m' + (signal || 'SIGTERM') + '\033[0m to worker #' + id + '\n');
} else {
sock.write('invalid worker id\n');
}
}, 'Send signal or SIGTERM to the given worker');
/**
* Gracefully shutdown.
*/
define('shutdown', function(master, sock){
master.close();
}, 'Gracefully shutdown server');
/**
* Hard shutdown.
*/
define('stop', function(master, sock){
master.destroy();
}, 'Hard shutdown');
/**
* Gracefully restart all workers.
*/
define('restart', function(master, sock){
master.restart();
}, 'Gracefully restart all workers');

250
node_modules/cluster/lib/plugins/stats.js generated vendored Normal file
View File

@@ -0,0 +1,250 @@
/*!
* Cluster - stats
* Copyright (c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, Log = require('log')
, repl = require('./repl')
, utils = require('../utils')
, os = require('os');
/**
* Enable stat tracking with the given `options`.
*
* Options:
*
* - `connections` enable connection statistics
* - `requests` enable request statistics
* - `lightRequests` enable light-weight request statistics
*
* Real-time applications should utilize `lightRequests` for reporting
* when possible, although less data is available.
*
* TODO: UDP
*
* @param {Object} options
* @return {Function}
* @api public
*/
module.exports = function(options){
options = options || {};
stats.enableInWorker = options.connections || options.requests;
function stats(master){
var server = master.server;
master.connectionStats = options.connections;
master.requestStats = options.requests;
master.lightRequestStats = options.lightRequests;
// worker stats
if (master.isWorker) {
var id = 0;
// connections
if (options.connections) {
server.on('connection', function(sock){
var data = { remoteAddress: sock.remoteAddress };
master.call('reportStats', 'connection', data);
sock.on('close', function(){
master.call('reportStats', 'disconnection', data);
});
});
}
// light-weight requests
if (options.lightRequests) {
utils.unshiftListener(server, 'request', function(req, res){
master.call('reportStats', 'light request', res.id = ++id);
var end = res.end;
res.end = function(str, encoding){
res.end = end;
res.end(str, encoding);
master.call('reportStats', 'light request complete', res.id);
};
});
}
// requests
if (options.requests) {
utils.unshiftListener(server, 'request', function(req, res){
var data = {
remoteAddress: req.socket.remoteAddress
, headers: req.headers
, httpVersion: req.httpVersion
, method: req.method
, url: req.url
, id: ++id
};
master.call('reportStats', 'request', data);
var end = res.end;
res.end = function(str, encoding){
res.end = end;
res.end(str, encoding);
master.call('reportStats', 'request complete', data);
};
});
}
// master stats
} else {
master.stats = {
start: new Date
, restarts: 0
, workersSpawned: 0
, workersKilled: 0
};
// 0.4.x
if (os) {
master.stats.totalmem = os.totalmem();
master.stats.freemem = os.freemem();
}
// worker stats
master.reportStats = function(worker, type, data){
master.emit('client ' + type, worker, data);
switch (type) {
case 'connection':
worker.stats.connectionsTotal++;
worker.stats.connectionsActive++;
break;
case 'disconnection':
worker.stats.connectionsActive--;
break;
case 'light request':
case 'request':
worker.stats.requestsTotal++;
}
};
// total workers spawned
master.on('worker', function(worker){
++master.stats.workersSpawned;
worker.stats = {
start: new Date
, connectionsTotal: 0
, connectionsActive: 0
, requestsTotal: 0
};
});
// total worker deaths
master.on('worker killed', function(worker){
++master.stats.workersKilled;
});
// restarting
master.on('restarting', function(data){
++master.stats.restarts;
data.stats = master.stats;
});
// restart
master.on('restart', function(data){
master.stats = data.stats;
master.stats.start = new Date(master.stats.start);
});
}
}
return stats;
};
/**
* REPL statistics command.
*/
repl.define('stats', function(master, sock){
var active = master.children.length
, total = master.stats.workersSpawned
, deaths = master.stats.workersKilled
, restarts = master.stats.restarts;
// master stats
sock.title('Master');
if (os) sock.row('os', os.type() + ' ' + os.release());
sock.row('state', master.state);
sock.row('started', master.stats.start.toUTCString());
sock.row('uptime', utils.formatDateRange(new Date, master.stats.start));
sock.row('restarts', restarts);
sock.row('workers', active);
sock.row('deaths', deaths);
// resources
if (os) {
sock.title('Resources');
sock.row('load average', os.loadavg().map(function(n){ return n.toFixed(2); }).join(' '));
sock.row('cores utilized', active + ' / ' + os.cpus().length);
var free = utils.formatBytes(master.stats.freemem);
var total = utils.formatBytes(master.stats.totalmem);
sock.row('memory at boot (free / total)', free + ' / ' + total);
var free = utils.formatBytes(os.freemem());
var total = utils.formatBytes(os.totalmem());
sock.row('memory now (free / total)', free + ' / ' + total);
}
// worker stats
sock.title('Workers');
// connections
if (master.connectionStats) {
sock.row('connections total', sum(master.children, 'connectionsTotal'));
sock.row('connections active', sum(master.children, 'connectionsActive'));
}
// requests
if (master.requestStats) {
sock.row('requests total', sum(master.children, 'requestsTotal'));
}
master.children.forEach(function(worker){
var stats = ''
, piped = [];
// uptime
stats += utils.formatDateRange(new Date, worker.stats.start);
// connections
if (master.connectionStats) {
piped.push(worker.stats.connectionsActive);
piped.push(worker.stats.connectionsTotal);
}
// requests
if (master.requestStats) {
piped.push(worker.stats.requestsTotal);
}
if (piped.length) {
stats += ' ' + piped.join('\033[90m|\033[0m');
}
sock.row(worker.id, stats);
});
sock.write('\n');
}, 'Display server statistics');
/**
* Return sum of each `prop` in `arr`.
*
* @param {Array} arr
* @param {String} prop
* @return {Number}
* @api private
*/
function sum(arr, prop){
return arr.reduce(function(sum, obj){
return sum + obj.stats[prop];
}, 0);
};