├── .npmignore ├── .gitignore ├── bin └── nperf ├── package.json ├── config.js.example ├── Readme.md └── client.js /.npmignore: -------------------------------------------------------------------------------- 1 | config.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | -------------------------------------------------------------------------------- /bin/nperf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../client.js'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-perf", 3 | "preferGlobal": true, 4 | "version": "0.0.5", 5 | "author": "Ilya Braude ", 6 | "description": "a simple utility to test an http server and get stats", 7 | "homepage": "https://github.com/zanchin/node-http-perf", 8 | "keywords": ["http", "performance", "testing", "load"], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/zanchin/node-http-perf.git" 12 | }, 13 | "directories": { 14 | "bin": "./bin" 15 | }, 16 | "bin": { 17 | "nperf": "./bin/nperf" 18 | }, 19 | "dependencies": { 20 | "optimist": "0.3.x", 21 | "colors": "0.6.x" 22 | }, 23 | "license": "MIT", 24 | "engines": { 25 | "node": ">=0.6.0" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /config.js.example: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | settings: { 3 | concurrency: 10, // -c 4 | max_requests: 200, // -n 5 | output_format: 'text' // -o 'text' or 'json' 6 | }, 7 | targets: { 8 | // can have multiple targets here 9 | // pick one using the --target commandline argument 10 | local: { 11 | host: 'localhost', 12 | port: 8080, 13 | path: '/path/to/http/resource', 14 | headers: { 15 | 'X-Optional-Header': "header value" 16 | } 17 | }, 18 | ssl: { 19 | host: 'www.google.com', 20 | protocol: 'https:', 21 | path: '/' 22 | }, 23 | google: { 24 | host: 'www.google.com', 25 | port: 80, 26 | path: '/' 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Node HTTP Server Performance Tool 2 | 3 | http-perf is a tool used to test HTTP/S server performance. It is basically an HTTP client that executes specified requests against a server and then measures and records response times and other metrics. 4 | 5 | Its function is similar to the popular [ab](http://httpd.apache.org/docs/2.0/programs/ab.html) tool, and in fact the basic usage is identical. However, this tool goes above what `ab` provides for my needs. For example, it parses the server-side request time (if reported in headers) and displays it along with the client's view of request time for each request. It can also output its data in JSON. 6 | 7 | ## Install 8 | 9 | via `npm`, preferrably globally (-g) 10 | 11 | $ npm install -g http-perf 12 | 13 | This installs an executable called `nperf`. 14 | 15 | You can run the tool directly 16 | 17 | $ node node_modules/http-perf/bin/nperf 18 | 19 | Or if installed globally 20 | 21 | $ nperf 22 | 23 | 24 | ## Quick Start 25 | 26 | Send 10 requests to google.com with 5 concurrent requests: 27 | 28 | $ nperf -c 5 -n 10 http://www.google.com/ 29 | [status] response# /request_id time: client time (ms) (server time (ms)) 30 | [200] 1 /1 time: 78 (-1) 31 | [200] 2 /0 time: 89 (-1) 32 | [200] 3 /3 time: 86 (-1) 33 | [200] 4 /4 time: 88 (-1) 34 | [200] 5 /2 time: 91 (-1) 35 | [200] 6 /6 time: 76 (-1) 36 | [200] 7 /5 time: 82 (-1) 37 | [200] 8 /7 time: 82 (-1) 38 | [200] 9 /9 time: 82 (-1) 39 | [200] 10 /8 time: 100 (-1) 40 | stats: 41 | { min: 76, 42 | max: 100, 43 | avg: 85.4, 44 | count: 10, 45 | rate: 50.76142131979695, 46 | start: 1337831509423, 47 | total_time: 197 } 48 | 49 | We see that 10 requests were sent to the server with the average response time being 85.4 ms. The server processed requests at a rate of about 50 requests per second. 50 | 51 | Server processing time is not available (-1) because Google does not return it in a header. Supported headers are: _X-Response-Time_ and _X-Runtime_. 52 | 53 | ## Usage 54 | 55 | Display usage: 56 | 57 | $ nperf -h 58 | Stress test an HTTP server. 59 | Usage: node ./bin/nperf [options] [target server] 60 | 61 | Options: 62 | --conf, --config Configuration file with targets 63 | --target, -t Target server name in config file 64 | -c Number of concurrent requests 65 | -n Max number of total requests 66 | -o Output format: [text|json]. Default: text 67 | -v, --verbose Verbose output 68 | --dry-run Read config, but don't run (can be used with -v) 69 | --help, -h Print this usage and exit 70 | 71 | 72 | One useful feature of the tool is that you can save all parameters and server targets in a config file and refer to it instead of specifying them on the commandline. All parameters specified on the commandline override their counterparts in the config file. 73 | 74 | Sample config file `config.js`: 75 | 76 | module.exports = { 77 | settings: { 78 | concurrency: 10, // -c 79 | max_requests: 200, // -n 80 | output_format: 'text' // -o 'text' or 'json' 81 | }, 82 | targets: { 83 | // can have multiple targets here 84 | // pick one using the --target commandline argument 85 | local: { 86 | host: 'localhost', 87 | port: 8080, 88 | path: '/path/to/http/resource', 89 | headers: { 90 | 'X-Optional-Header': "header value" 91 | } 92 | }, 93 | google: { 94 | host: 'www.google.com', 95 | port: 80, 96 | path: '/' 97 | } 98 | } 99 | }; 100 | 101 | Set the port to `443` for HTTPS. 102 | 103 | To use the config file and specify the `google` target, run: 104 | 105 | $ nperf --conf config.js -t google 106 | [status] response# /request_id time: client time (ms) (server time (ms)) 107 | [200] 1 /1 time: 161 (-1) 108 | [200] 2 /3 time: 164 (-1) 109 | [200] 3 /6 time: 165 (-1) 110 | ... output truncated ... 111 | [200] 198 /198 time: 67 (-1) 112 | [200] 199 /197 time: 81 (-1) 113 | [200] 200 /199 time: 71 (-1) 114 | stats: 115 | { min: 43, 116 | max: 722, 117 | avg: 110.34500000000004, 118 | count: 200, 119 | rate: 88.65248226950355, 120 | start: 1337832532680, 121 | total_time: 2256 } 122 | 123 | The number of requests and concurrency values are taken from the config file, as well as the details for the `google` target. Output above is truncated for brevity. 124 | 125 | 126 | ## More examples 127 | 128 | Override config with commandline parameters: 129 | 130 | $ nperf --config config.js --target google -c 1 -n 20 131 | 132 | JSON output: 133 | 134 | $ nperf -o json http://www.google.com -n 5 135 | {"status":"status","response_count":"response#","request_id":"request_id","client_time":"client time (ms)","server_time":"server time (ms)"} 136 | {"status":200,"request_id":2,"response_count":1,"client_time":467,"server_time":-1} 137 | {"status":200,"request_id":0,"response_count":2,"client_time":475,"server_time":-1} 138 | {"status":200,"request_id":4,"response_count":3,"client_time":475,"server_time":-1} 139 | {"status":200,"request_id":1,"response_count":4,"client_time":477,"server_time":-1} 140 | {"status":200,"request_id":3,"response_count":5,"client_time":486,"server_time":-1} 141 | stats: 142 | { min: 467, 143 | max: 486, 144 | avg: 476, 145 | count: 5, 146 | rate: 10.101010101010102, 147 | start: 1337833296687, 148 | total_time: 495 } 149 | 150 | 151 | ## Contributing 152 | 153 | I welcome pull requests! 154 | 155 | ## License 156 | 157 | This software is distributed under the MIT License. 158 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | // -*- js2-basic-offset: 4; javascript-basic-offset: 4 -*- 2 | /*global require, console, setTimeout */ 3 | 4 | var http = require('http'); 5 | var https = require('https'); 6 | var util = require('util'); 7 | var path = require('path'); 8 | var url = require('url'); 9 | var fs = require('fs'); 10 | 11 | var colors = require('colors'); 12 | var argv = require('optimist') 13 | .usage("Stress test an HTTP server.\nUsage: $0 [options] [target server]") 14 | .options('conf', { alias: 'config', describe: 'Configuration file with targets' }) 15 | .options('target', { alias: 't', describe: 'Target server name in config file' }) 16 | .options('c', { describe: 'Number of concurrent requests. Default: 20' }) 17 | .options('n', { describe: 'Max number of total requests. Default: 200' }) 18 | .options('o', { describe: 'Output format: [text|json]. Default: text' }) 19 | .boolean('v', { alias: "verbose", describe: 'Verbose output' }) 20 | .boolean('dry-run', { describe: "Read config, but don't run (can be used with -v)" }) 21 | .boolean('help', { alias: 'h', describe: 'Print this usage and exit' }) 22 | .argv; 23 | 24 | function usage(noexit){ 25 | console.log(require('optimist').help()); 26 | if(!noexit) process.exit(); 27 | } 28 | 29 | argv.h && usage(); 30 | 31 | 32 | /*********** CONFIGURATION *************/ 33 | var defaults = { 34 | concurrency: 20, 35 | max_requests: 200, 36 | output_format: 'text' // 'text' or 'json' 37 | }; 38 | 39 | var options = {}; 40 | 41 | var config_path; 42 | if( argv.conf ){ 43 | config_path = path.resolve(argv.conf); 44 | if(!fs.existsSync(config_path)){ 45 | console.error("Configuration file not found: ", config_path); 46 | process.exit(-1); 47 | } 48 | options = require(config_path); 49 | } 50 | 51 | // Specify which config to use 52 | var target = options.targets && options.targets[argv.target || argv.t]; 53 | 54 | // allow overriding/setting target options on the commandline 55 | if(argv._[0]){ 56 | target = url.parse(argv._[0]); 57 | } 58 | 59 | // establish concurrency settings 60 | var concurrency = argv.c || (options.settings && options.settings.concurrency) || defaults.concurrency; 61 | var max_requests = argv.n || (options.settings && options.settings.max_requests) || defaults.max_requests; 62 | var global_output_format = argv.o || (options.settings && options.settings.output_format) || defaults.output_format; 63 | 64 | /********** END CONFIGURATION ***********/ 65 | 66 | 67 | if( argv.v ){ 68 | console.log("config file:", config_path); 69 | console.log("options:", util.inspect(options)); 70 | console.log("target:", util.inspect(target)); 71 | console.log("concurrency:", concurrency); 72 | console.log("max_requests:", max_requests); 73 | console.log("output_format:", global_output_format); 74 | console.log(""); 75 | } 76 | 77 | if(!target){ 78 | console.error("Error: No target specified\n"); 79 | console.error("Available targets are:"); 80 | if( options.targets ){ 81 | for(var t in options.targets){ 82 | console.error("->", t); 83 | } 84 | } 85 | console.error(""); 86 | usage(true); 87 | process.exit(1); 88 | } 89 | 90 | if( argv['dry-run'] ){ 91 | process.exit(); 92 | } 93 | 94 | http.globalAgent.maxSockets = https.globalAgent.maxSockets = concurrency+5; 95 | 96 | var http_get = http.get; 97 | if(target.port == 443 || (target.protocol && target.protocol.indexOf('https') > -1)){ 98 | http_get = https.get; 99 | } 100 | 101 | 102 | var total_requests = 0; 103 | var requests = 0; 104 | var response_count = 1; 105 | 106 | var stats = { 107 | statuses: {}, 108 | min: 99999999999, 109 | max: -1, 110 | avg: -1, 111 | count: 0, 112 | rate: 0, 113 | start: false 114 | }; 115 | 116 | stats.start = stats.start || new Date().getTime(); 117 | var updateStats = function(time, status){ 118 | stats.statuses[status] = stats.statuses[status] || 0; 119 | stats.statuses[status]++; 120 | 121 | if( time < stats.min ) stats.min = time; 122 | if( time > stats.max ) stats.max = time; 123 | stats.avg = (stats.avg*stats.count + time)/++stats.count; 124 | stats.rate = stats.count / (new Date().getTime() - stats.start) * 1000; // per sec 125 | }; 126 | 127 | var log_request = function(r, format){ 128 | format = format || global_output_format || 'text'; 129 | 130 | var output; 131 | if(format === 'json'){ 132 | output = JSON.stringify(r); 133 | } else { 134 | var status_color = r.status == 200 ? "green" : "red"; 135 | output = util.format("[%s] %s /%s time: %s (%s)", 136 | r.status.toString()[status_color], 137 | r.response_count, 138 | r.request_id, 139 | r.client_time.toString().blue, 140 | r.server_time.toString().yellow); 141 | } 142 | console.log(output); 143 | }; 144 | 145 | var makeCall = function(done, req_id){ 146 | // console.log("making call"); 147 | 148 | var start = new Date().getTime(); 149 | 150 | http_get(target, function(res) { 151 | res.on('data', function(){/*Do nothing. Consume data so the connection ends.*/}); 152 | res.on('end', function(){ 153 | var client_time = new Date().getTime() - start; 154 | var status = res.statusCode; 155 | 156 | // show server-compute time if server reports it 157 | function get_server_time(res){ 158 | if(res.headers["x-response-time"]) return parseInt(res.headers["x-response-time"]); 159 | if(res.headers["x-runtime"]) return Math.floor(res.headers["x-runtime"]*1000); 160 | return -1; 161 | } 162 | var server_time = get_server_time(res); 163 | 164 | //console.error(util.inspect(res.headers)); 165 | 166 | var r = { 167 | status: status, 168 | request_id: req_id, 169 | response_count: response_count++, 170 | client_time: client_time, 171 | server_time: server_time 172 | }; 173 | 174 | log_request(r); 175 | 176 | done(client_time, status); 177 | }); 178 | }).on('error', function(e) { 179 | var time = new Date().getTime() - start; 180 | console.log("Got error: " + e.message); 181 | done(time, 0); 182 | }); 183 | }; 184 | 185 | function go(){ 186 | if( requests < concurrency ){ 187 | //console.log("requests: ", requests); 188 | //console.log("total_requests: ", total_requests); 189 | 190 | if( total_requests >= max_requests ){ 191 | // console.log("=== done sending requests! ==="); 192 | return; 193 | } 194 | 195 | makeCall(function(time, status){ 196 | updateStats(time, status); 197 | requests--; 198 | 199 | if( requests == 0 && total_requests >= max_requests ){ 200 | console.log("stats:"); 201 | stats.total_time = new Date().getTime() - stats.start; 202 | console.log(util.inspect(stats)); 203 | } 204 | 205 | go(); 206 | }, total_requests); 207 | 208 | requests++; 209 | total_requests++; 210 | } else { 211 | console.log("too busy"); 212 | } 213 | } 214 | 215 | log_request({status: "status", response_count: "response#", 216 | request_id: "request_id", 217 | client_time: "client time (ms)", 218 | server_time: "server time (ms)"}); 219 | 220 | 221 | // seed the right amount of requests 222 | for(var i = 0; i < concurrency; i++){ 223 | go(); 224 | } 225 | --------------------------------------------------------------------------------