From ea6121858b2f25ec5a39c0f495dd0dc8ccd8400a Mon Sep 17 00:00:00 2001 From: Brett Langdon Date: Sun, 21 Oct 2012 09:47:56 -0700 Subject: [PATCH] initial commit --- .npmignore | 4 + Readme.md | 29 +++++++ bin/tommygun | 6 ++ lib/index.js | 218 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/tommygun.js | 0 package.json | 23 +++++ 6 files changed, 280 insertions(+) create mode 100644 .npmignore create mode 100644 Readme.md create mode 100755 bin/tommygun create mode 100644 lib/index.js create mode 100644 lib/tommygun.js create mode 100644 package.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f1250e5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +support +test +examples +*.sock diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..ff551f7 --- /dev/null +++ b/Readme.md @@ -0,0 +1,29 @@ + +# tommygun + + HTTP Benchmarking Tool + +## License + +(The MIT License) + +Copyright (c) 2012 Brett Langdon <brett@blangdon.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/bin/tommygun b/bin/tommygun new file mode 100755 index 0000000..d1e33fa --- /dev/null +++ b/bin/tommygun @@ -0,0 +1,6 @@ +#!/usr/bin/env node +try{ + require('tommygun'); +} catch(e){ + require('../lib/index.js'); +} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..27e92bf --- /dev/null +++ b/lib/index.js @@ -0,0 +1,218 @@ +var gauss = require('gauss'); +var url_parse = require('url').parse; +var iniparser = require('iniparser'); +var extend = require('xtend'); +var http = require('http'); +var fs = require('fs'); +var async = require('async'); +var argv = require('optimist') + .usage('Usage: $0 [config] [config]...') + .options('c', { + alias: 'concurrency', + default: 1, + describe: 'Number of Concurrent Clients to Use' + }) + .options('r', { + alias: 'requests', + describe: 'Total Number Of Requests To Make' + }) + .options('k', { + alias: 'keepalive', + describe: 'Whether Or Not To Enable Keep Alive Support', + boolean: true, + default: false, + }) + .options('l', { + alias: 'log', + describe: 'Log File To Write All Request Data To', + string: true, + }) + .options('s', { + alias: 'stats', + describe: 'Whether or Not Print Result Stats', + boolean: true, + default: true + }) + .options('q', { + alias: 'quiet', + default: false, + describe: 'Whether Or Not To Silence stdout', + boolean: true, + }) + .options('h', { + alias: 'help', + describe: 'Show This Help Text', + boolean: true + }) + .demand(1) + .argv; + +if( argv.help ){ + require('optimist').showHelp(); + process.exit(); +} + +if( argv.quiet ){ + console.log = function(msg){}; +} + +if( argv.log ){ + argv.log = fs.openSync(argv.log, 'w'); +} + +var config = { + random: false, + urls: {}, +}; +var defaults = { + num: 1, + port: 80, +}; + +argv._.forEach(function(file){ + try{ + ini = iniparser.parseSync(file); + for( var key in ini ){ + if( typeof(ini[key]) === 'object'){ + var url = key; + if(url.substring(0, 4) !== 'http'){ + url = 'http://' + url; + } + parts = url_parse(url, true, true); + ini[key] = extend({}, defaults, ini[key], parts); + } else{ + config[key] = ini[key]; + delete ini[key]; + } + } + config.urls = extend(config.urls, ini); + }catch(e){ + console.error('Could Not Process ' + file + ' ini File'); + console.error(e.stack); + process.exit(1); + } + }); + +var urls = []; +for( var key in config.urls ){ + var url = config.urls[key]; + for( var i = 0; i < url.num; ++i ){ + urls.push(url); + } +} + +if( typeof(config.random) !== 'boolean' ){ + config.random = config.random.toLowerCase() === 'true'; +} +if( config.random ){ + console.log('Randomizing Url List'); + var i = urls.length; + if ( i == 0 ) return false; + while ( --i ) { + var j = Math.floor( Math.random() * ( i + 1 ) ); + var tempi = urls[i]; + var tempj = urls[j]; + urls[i] = tempj; + urls[j] = tempi; + } +} + +if( !argv.requests ){ + argv.requests = urls.length; +} + +var results = []; +var errors = []; + +var save_result = function(result){ + results.push(result); + if( argv.log ){ + var tmp = []; + for( var i in result ){ + var val = parseInt(result[i]); + if( isNaN(val) ){ + tmp.push('"' + result[i] + '"'); + } else{ + tmp.push(val); + } + } + fs.writeSync(argv.log, tmp.join() + '\r\n'); + } +} + +var run = function(options, done){ + var start = new Date().getTime(); + req = http.request(options, function(res){ + var stop = new Date().getTime(); + save_result({ + ms: (stop - start), + status: res.statusCode, + url: options.href + }); + done(); + }); + req.on('error', function(err){ + save_error({ + error: err, + url: options.href + }); + done(); + }); + if( options.data ){ + req.write(options.data); + } + req.setSocketKeepAlive(argv.keepalive); + req.end(); +}; + +console.log('Starting Up ' + argv.concurrency + ' Clients'); +var queue = async.queue(run, argv.concurrency); + +var percentiles = [.7, .8, .85, .9, .94, .96, .99]; +queue.drain = function(){ + console.log('Finished'); + + if( argv.stats ){ + console.log('Processing Results'); + var tmp = results; + results = {}; + tmp.forEach( function(result){ + var key = result.url; + delete result.url; + if( results[key] == undefined ){ + results[key] = [] + } + results[key].push(result); + }); + + for( var url in results ){ + console.log('Results For: ' + url); + console.log('\tTotal Requests: ' + results[url].length); + var ms_data = results[url].map( function(a){ return a.ms; } ); + ms_data = ms_data.sort(); + var ms_set = new gauss.Vector(ms_data); + console.log('\tMean Time (ms): ' + ms_set.mean()); + console.log('\tMin Time (ms): ' + ms_set.min()); + console.log('\tMax Time (ms): ' + ms_set.max()); + console.log('\tTotal Time (ms): ' + ms_set.sum()); + percentiles.forEach( function(percent){ + console.log('\t' + (percent*100) + 'th Percentile: ' + ms_set.percentile(percent)); + }); + } + } + + if( argv.log ){ + fs.closeSync(argv.log); + } + process.exit(0); +}; + +console.log('Queuing Up ' + argv.requests + ' Requests'); +var offset = 0; +for( var i = 0; i < argv.requests; ++i ){ + if( offset >= urls.length ){ + offset = 0; + } + queue.push(urls[offset]); + offset += 1; +} \ No newline at end of file diff --git a/lib/tommygun.js b/lib/tommygun.js new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..4bb28e9 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "tommygun", + "version": "0.0.1", + "description": "HTTP Benchmarking Tool", + "keywords": [ + "http", + "benchmark" + ], + "author": "Brett Langdon ", + "dependencies": { + "optimist": "*", + "gauss": "*", + "xtend": "*", + "iniparser": "*", + "async": "*" + }, + "devDependencies": { + }, + "main": "lib/index.js", + "bin": { + "tommygun": "bin/tommygun" + } +} \ No newline at end of file