wrk-node
Version:
wrk benchmarking http node.js
341 lines (281 loc) • 9.62 kB
JavaScript
;
var exec = require( 'child_process' ).exec;
var glob = require( 'glob' );
var path = require( 'path' );
// plug-ins for table and styling table
var color = require( 'colors' );
var Table = require( 'cli-table' );
var async = require( 'async' );
var fs = require( 'fs' );
var Promise = require( 'bluebird' );
function Benchmark ( config ) {
this.config = config;
this.data = [];
this.table = new Table( {
'head' : [ 'Path', 'Method', 'Specs', 'Latency', 'Transfer/sec', 'Req/sec', 'Threshold', 'Status' ],
'colWidths' : [ 50, 10, 35, 15, 15, 15, 13, 18 ],
'chars' : {
'top' : '═',
'top-mid' : '╤',
'top-left' : '╔',
'top-right' : '╗',
'bottom' : '═',
'bottom-mid' : '╧',
'bottom-left' : '╚',
'bottom-right' : '╝',
'left' : '║',
'left-mid' : '╟',
'mid' : '─',
'mid-mid' : '┼',
'right' : '║',
'right-mid' : '╢',
'middle' : '│'
},
'style' : {
'head' : [ 'white' ]
}
} );
this.host = [ config.server.host, config.server.port ].join( ':' );
// path to the LUA file that is going to be reused on every 'wrk' execution
this.reqPath = path.join( __dirname, './request.lua' );
// path for the request LUA template
this.reqTpl = path.join( __dirname, './requestTpl.txt' );
// overall status of the benchmark
this.benchmarkObj = {};
this.benchmarkObj.status = 'Success';
this.benchmarkObj.message = '. Reached the required request/sec threshold';
// path to the wrk bin
this.wrk = path.join( __dirname, '../src/wrk' );
}
Benchmark.prototype.exec = function ( cb ) {
var self = this;
async.waterfall( [
function ( callback ) {
if ( self.config.getAuth && ( typeof self.config.getAuth === 'function' ) ) {
self.config.getAuth( function () {
callback( null, 'done' );
} );
} else {
callback( null, 'done' );
}
} ],
function ( err ) {
if ( err ) {
cb( err, null );
} else {
self.execBenchmark( function ( error ) {
if ( cb && ( typeof cb ) === 'function' ) {
cb( error, self.data );
}
} );
}
} );
};
// cycle through the directory to get the js files
Benchmark.prototype.getFiles = function ( dir ) {
return new Promise( function ( resolve, reject ) {
glob( process.cwd() + dir, function ( err, files ) {
if ( err ) {
reject( 'Error in reading the file' );
}
resolve( files );
} );
} );
};
// replace the variables in the lua template with the data obtained form request info file
Benchmark.prototype.replaceAttr = function ( strReq, reqData ) {
var headerArr = [];
var content;
for ( var keys in reqData.headers ) {
if ( reqData.headers.hasOwnProperty( keys ) ) {
headerArr.push( 'wrk.headers["' + keys + '"] = "' + reqData.headers[ keys ] + '"' );
}
}
// replace template with the correct obj key value
content = strReq.replace( '[@path]', '"' + reqData.request.path + '"' )
.replace( '[@requestHeaders]', headerArr.join( '\n' ) )
.replace( '[@body]', reqData.body )
.replace( '[@method]', '"' + reqData.request.method + '"' );
/* eslint new-cap:0 */
return new Promise.resolve( content );
};
// read files obtained from getFiles fn
Benchmark.prototype.readFiles = function ( reqOptions ) {
var self = this;
return new Promise( function ( resolve, reject ) {
fs.readFile( self.reqTpl, 'utf8', function ( err, tplStr ) {
if ( err ) {
reject( 'Error in reading the file' );
}
var req = {};
req.headers = {};
req.request = reqOptions.request;
if ( Object.keys( reqOptions.customHeaders ).length ) {
req.headers = reqOptions.customHeaders;
} else {
req.headers = self.config.headers;
}
if ( reqOptions.request.body ) {
req.body = JSON.stringify( reqOptions.request.body ).replace( /"/g, '\\"' );
} else {
req.body = '';
}
var obj = {};
obj.tplStr = tplStr;
obj.req = req;
resolve( obj );
} );
} );
};
// write the final content to request.lua
Benchmark.prototype.writeFile = function ( newContent ) {
var self = this;
// delete the reusable file each time there is a new content written to it.
return new Promise( function ( resolve, reject ) {
fs.writeFile( self.reqPath, newContent, function ( err ) {
if ( err ) {
reject( 'error in Writing file : ' + err );
}
// execute the wrk with each options as arguments
resolve();
} );
} );
};
// executes the wrk
Benchmark.prototype.execWrk = function ( reqOptions ) {
var wrkCmd = this.wrk + ' -t ' + reqOptions.options.threads + ' -c ' + reqOptions.options.connections + ' -d ' + reqOptions.options.duration + ' -s ' + this.reqPath + ' ' + this.host;
return new Promise( function ( resolve, reject ) {
exec( wrkCmd, function ( err, stdout, stderr ) {
if ( err ) {
reject( err );
}
if ( stderr ) {
reject( err );
}
resolve( stdout );
} );
} );
};
// parses the result string returned from stdout of wrk
Benchmark.prototype.parseResult = function ( reqOptions, fileDir, stdout ) {
var arrElem = [];
// splits the data returned by the request
var dataStr = stdout.split( '\n' ).filter( Boolean );
var dataStats = {};
// .filter( Boolean ) removes empty string elements form the array
var threadStats = dataStr[ 3 ].trim().split( ' ' ).filter( Boolean );
// assign values to the final presentation object
dataStats.api = reqOptions.request.path;
dataStats.specs = dataStr[ 1 ].trim();
dataStats.latency = threadStats[ 1 ];
dataStats.reqRate = dataStr[ 6 ].split( ':' ).pop().trim();
dataStats.transferRate = dataStr[ 7 ].split( ':' ).pop().trim();
dataStats.status = color.green( 'Success' );
dataStats.statIcon = color.green( '✓' );
dataStats.keys = [];
if ( dataStr.length > 8 ) {
if ( !this.benchmarkObj.failures ) {
this.benchmarkObj.failures = {};
}
dataStats.status = color.red( 'Failed' );
this.benchmarkObj.status = 'Failed';
this.benchmarkObj.message = '. There are requests that did not respond to a 2xx or 3xx status';
dataStats.statIcon = color.red( '✗' );
dataStats.reqRate = dataStr[ 7 ].split( ':' ).pop().trim();
dataStats.transferRate = dataStr[ 8 ].split( ':' ).pop().trim();
this.benchmarkObj.failures[ '*' ] = 'There are requests that did not respond to a 2xx or 3xx status';
dataStats.keys.push( '*' );
}
if ( parseFloat( dataStats.reqRate ) < reqOptions.threshold.reqRate.min ) {
if ( !this.benchmarkObj.failures ) {
this.benchmarkObj.failures = {};
}
dataStats.status = color.red( 'Failed' );
this.benchmarkObj.status = 'Failed';
dataStats.statIcon = color.red( '✗' );
this.benchmarkObj.message = '. Should reach the request/sec minimum requirements';
this.benchmarkObj.failures[ '+' ] = 'Should reach the request/sec minimum requirements';
dataStats.keys.push( '+' );
}
if ( dataStats.keys.length > 0 ) {
dataStats.keys.forEach( function ( value ) {
dataStats.status = dataStats.status + value;
} );
}
arrElem = [ dataStats.api, reqOptions.request.method, dataStats.specs, dataStats.latency, dataStats.transferRate, dataStats.reqRate, reqOptions.threshold.reqRate.min, dataStats.status ];
// insert the results to the table
this.table.push( arrElem );
this.data.push( {
'Path' : dataStats.api,
'Method' : reqOptions.request.method,
'Specs' : dataStats.specs,
'Latency' : dataStats.latency,
'Transfer/sec' : dataStats.transferRate,
'Req/sec' : dataStats.reqRate,
'Threshold' : reqOptions.threshold.reqRate.min,
'Status' : dataStats.status
} );
console.log( '\t' + dataStats.statIcon + ' ' + fileDir.split( '/' ).pop() );
/* eslint new-cap:0 */
return new Promise.resolve();
};
// prints out the table
Benchmark.prototype.showTable = function ( fileLen, callback ) {
// show the result table
console.log( this.table.toString() );
var error;
if ( this.benchmarkObj.status === 'Failed' ) {
console.log( color.red( this.benchmarkObj.status ) );
var failures = this.benchmarkObj.failures;
Object.keys( failures ).forEach( function ( key ) {
console.log( ' ' + key + ' : ' + failures[ key ] );
} );
error = new Error( this.benchmarkObj.message );
} else {
console.log( color.green( this.benchmarkObj.status ) + this.benchmarkObj.message );
}
if ( callback ) {
callback( error );
}
};
// plugin to get files with specific pattern or extension
Benchmark.prototype.execBenchmark = function ( cb ) {
var fileLen;
var self = this;
this.getFiles( this.config.targetFolder )
.then( function ( files ) {
fileLen = files.length;
async.eachSeries( files, function ( fileDir, callback ) {
var reqOptions = require( fileDir );
self.readFiles( reqOptions )
.then( function ( obj ) {
return self.replaceAttr( obj.tplStr, obj.req );
} )
.then( function ( newContent ) {
return self.writeFile( newContent );
} )
.then( function () {
return self.execWrk( reqOptions );
} )
.then( function ( stdout ) {
return self.parseResult( reqOptions, fileDir, stdout );
} )
.then( function () {
callback( null );
} )
.catch( function ( err ) {
throw err;
} );
}, function ( err ) {
if ( err ) {
throw err;
}
self.showTable( fileLen, function ( error ) {
if ( cb && ( typeof cb ) === 'function' ) {
cb( error );
}
} );
} );
} );
};
module.exports = Benchmark;