├── .npmignore ├── lib ├── reporting │ ├── jmxstat │ ├── index.js │ ├── reportmanager.js │ ├── template.js │ ├── external.js │ ├── summary.tpl │ └── report.js ├── loop │ ├── index.js │ ├── multiuserloop.js │ ├── userloop.js │ ├── multiloop.js │ └── loop.js ├── monitoring │ ├── index.js │ ├── statslogger.js │ ├── monitorgroup.js │ ├── monitor.js │ └── collectors.js ├── remote │ ├── httphandler.js │ ├── index.js │ ├── slaves.js │ ├── endpointclient.js │ ├── slave.js │ ├── endpoint.js │ ├── slavenode.js │ └── cluster.js ├── header.js ├── user │ ├── request_container.js │ ├── program.js │ └── http_program.js ├── config.js ├── http.js ├── nl │ └── options.js └── util.js ├── .gitignore ├── jmxstat └── jmxstat.jar ├── .gitmodules ├── console ├── css │ ├── pepper-grinder │ │ └── images │ │ │ ├── ui-icons_222222_256x240.png │ │ │ ├── ui-icons_3572ac_256x240.png │ │ │ ├── ui-icons_8c291d_256x240.png │ │ │ ├── ui-icons_b83400_256x240.png │ │ │ ├── ui-icons_fbdb93_256x240.png │ │ │ ├── ui-icons_ffffff_256x240.png │ │ │ ├── ui-bg_fine-grain_10_eceadf_60x60.png │ │ │ ├── ui-bg_fine-grain_10_f8f7f6_60x60.png │ │ │ ├── ui-bg_fine-grain_15_eceadf_60x60.png │ │ │ ├── ui-bg_fine-grain_15_f7f3de_60x60.png │ │ │ ├── ui-bg_fine-grain_15_ffffff_60x60.png │ │ │ ├── ui-bg_fine-grain_65_654b24_60x60.png │ │ │ ├── ui-bg_fine-grain_68_b83400_60x60.png │ │ │ ├── ui-bg_diagonal-maze_20_6e4f1c_10x10.png │ │ │ └── ui-bg_diagonal-maze_40_000000_10x10.png │ └── console.css ├── js │ ├── jquery.console.js │ ├── console.data.js │ └── jquery.hotkeys.js └── console.html ├── nodeload.js ├── scripts └── process_tpl.js ├── examples ├── test-server.js ├── test-generator.js ├── simpletest.ex.js ├── program.ex.js ├── graphjmx.ex.js ├── remotetesting.ex.js ├── nodeload.ex.js ├── riaktest.ex.js └── remote.ex.js ├── doc ├── developers.md ├── remote.md ├── tips.md ├── reporting.md ├── nl.md ├── stats.md ├── monitoring.md └── loop.md ├── Makefile ├── TODO ├── LICENSE ├── test ├── http.test.js ├── stats.test.js ├── util.test.js ├── remote.test.js ├── reporting.test.js └── monitoring.test.js ├── package.json ├── RELEASE-NOTES.md └── nl.js /.npmignore: -------------------------------------------------------------------------------- 1 | ./results-* 2 | -------------------------------------------------------------------------------- /lib/reporting/jmxstat: -------------------------------------------------------------------------------- 1 | ../../jmxstat -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | results-* 2 | *.tpl.js 3 | .reporting.test-output.html 4 | tmp 5 | node_modules -------------------------------------------------------------------------------- /jmxstat/jmxstat.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/jmxstat/jmxstat.jar -------------------------------------------------------------------------------- /lib/loop/index.js: -------------------------------------------------------------------------------- 1 | exports.Loop = require('./loop').Loop; 2 | exports.MultiLoop = require('./multiloop').MultiLoop; -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/optparse-js"] 2 | path = deps/optparse-js 3 | url = git://github.com/jfd/optparse-js 4 | -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-icons_3572ac_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-icons_3572ac_256x240.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-icons_8c291d_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-icons_8c291d_256x240.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-icons_b83400_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-icons_b83400_256x240.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-icons_fbdb93_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-icons_fbdb93_256x240.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_10_eceadf_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_10_eceadf_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_10_f8f7f6_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_10_f8f7f6_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_15_eceadf_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_15_eceadf_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_15_f7f3de_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_15_f7f3de_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_15_ffffff_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_15_ffffff_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_65_654b24_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_65_654b24_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_fine-grain_68_b83400_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_fine-grain_68_b83400_60x60.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_diagonal-maze_20_6e4f1c_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_diagonal-maze_20_6e4f1c_10x10.png -------------------------------------------------------------------------------- /console/css/pepper-grinder/images/ui-bg_diagonal-maze_40_000000_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamechanger/nodeload/HEAD/console/css/pepper-grinder/images/ui-bg_diagonal-maze_40_000000_10x10.png -------------------------------------------------------------------------------- /console/js/jquery.console.js: -------------------------------------------------------------------------------- 1 | // JQuery extensions for the console 2 | jQuery.fn.exists = function() { 3 | return (this.length !== 0); 4 | }; 5 | jQuery.extend(jQuery.expr[':'], { 6 | focus: function() { return this == document.activeElement; } 7 | }); -------------------------------------------------------------------------------- /lib/monitoring/index.js: -------------------------------------------------------------------------------- 1 | exports.Monitor = require('./monitor').Monitor; 2 | exports.MonitorGroup = require('./monitorgroup').MonitorGroup; 3 | exports.StatsLogger = require('./statslogger').StatsLogger; 4 | exports.StatsCollectors = require('./collectors'); -------------------------------------------------------------------------------- /nodeload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var util = require('./lib/util'); 4 | 5 | var include = function(name) { 6 | util.extend(module.exports, require(name)); 7 | } 8 | 9 | include('./lib/config'); 10 | include('./lib/loadtesting'); 11 | include('./lib/remote') 12 | -------------------------------------------------------------------------------- /lib/remote/httphandler.js: -------------------------------------------------------------------------------- 1 | var BUILD_AS_SINGLE_FILE; 2 | if (!BUILD_AS_SINGLE_FILE) { 3 | var installRemoteHandler = require('./slavenode').installRemoteHandler; 4 | var HTTP_SERVER = require('../http').HTTP_SERVER; 5 | } 6 | 7 | // Install the handler for /remote for the global HTTP server 8 | installRemoteHandler(HTTP_SERVER); -------------------------------------------------------------------------------- /lib/reporting/index.js: -------------------------------------------------------------------------------- 1 | var report = require('./report'); 2 | exports.Report = report.Report; 3 | exports.Chart = report.Chart; 4 | exports.ReportGroup = report.ReportGroup; 5 | exports.REPORT_MANAGER= require('./reportmanager').REPORT_MANAGER; 6 | exports.graphProcess = require('./external').graphProcess; 7 | exports.graphJmx = require('./external').graphJmx; -------------------------------------------------------------------------------- /scripts/process_tpl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | if (process.argv.length < 4) { 3 | console.log('Usage: ./scripts/process_tpl '); 4 | process.exit(1); 5 | } 6 | 7 | var varname = process.argv[2], src = process.argv[3]; 8 | var file = require('fs').readFileSync(src).toString(); 9 | require('util').puts('var ' + varname + '= exports.' + varname + '=' + JSON.stringify(file) + ';'); -------------------------------------------------------------------------------- /examples/test-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('http').createServer(function (req, res) { 3 | var maxDelayMs = 500; 4 | var delay = Math.round(Math.random()*maxDelayMs) + 1000; 5 | setTimeout(function () { 6 | res.writeHead(200, {'Content-Type': 'text/plain'}); 7 | res.write(delay+'\n'); 8 | res.end(); 9 | }, delay); 10 | }).listen(9000); 11 | console.log('Server running at http://127.0.0.1:9000/'); 12 | -------------------------------------------------------------------------------- /examples/test-generator.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var sys = require('sys'); 3 | exports.getRequest = function(client) { 4 | //sys.debug('running request generator'); 5 | // Generate a request for the server being server being tested. 6 | // (Normally you'd do something more interesting here, like generating 7 | // a request path for a range of objects.) 8 | var req = client.request('GET', '/', { 'host': 'localhost' }); 9 | return req; 10 | } 11 | -------------------------------------------------------------------------------- /lib/header.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------- 2 | // Header for single file build 3 | // ----------------------------------------- 4 | 5 | var util = require('util'), 6 | http = require('http'), 7 | url = require('url'), 8 | fs = require('fs'), 9 | path = require('path'), 10 | events = require('events'), 11 | querystring = require('querystring'), 12 | child_process = require('child_process'); 13 | 14 | var EventEmitter = events.EventEmitter; 15 | 16 | var START = new Date(); 17 | var BUILD_AS_SINGLE_FILE = true; 18 | -------------------------------------------------------------------------------- /lib/remote/index.js: -------------------------------------------------------------------------------- 1 | var slave = require('./slave'); 2 | var slavenode = require('./slavenode'); 3 | exports.Cluster = require('./cluster').Cluster; 4 | exports.LoadTestCluster = require('./remotetesting').LoadTestCluster; 5 | exports.Slaves = slave.Slaves; 6 | exports.Slave = slave.Slave; 7 | exports.SlaveNode = slavenode.SlaveNode; 8 | exports.installRemoteHandler = slavenode.installRemoteHandler; 9 | exports.Endpoint = require('./endpoint').Endpoint; 10 | exports.EndpointClient = require('./endpointclient').EndpointClient; 11 | 12 | require('./httphandler'); -------------------------------------------------------------------------------- /doc/developers.md: -------------------------------------------------------------------------------- 1 | # Setting up 2 | 3 | First, it's recommended that [`npm`](http://npmjs.org/) is installed. Just run: 4 | 5 | [~/]> curl http://npmjs.org/install.sh | sh 6 | 7 | The clone nodeload and run `npm link` 8 | 9 | [~/]> git clone git://github.com/benschmaus/nodeload.git 10 | [~/]> cd nodeload 11 | [~/nodeload]> npm link 12 | 13 | which will installs the unit testing framework [expresso](http://visionmedia.github.com/expresso) and puts a symlink to `nodeload` in the node library path. 14 | 15 | Use expresso to run the tests under test/: 16 | 17 | [~/nodeload]> expresso 18 | 19 | 100% 20 tests 20 | -------------------------------------------------------------------------------- /lib/user/request_container.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | _ = require('underscore'), 3 | temp = require('temp'), 4 | path = require('path'); 5 | 6 | var RequestContainer = module.exports = function() { 7 | 8 | this.reqDir = temp.mkdirSync('requests'); 9 | 10 | this.nextIndex = 0; 11 | }; 12 | 13 | RequestContainer.prototype.add = function(request) { 14 | fs.writeFileSync(path.join(this.reqDir, this.nextIndex + ".json"), JSON.stringify(request), 'utf8'); 15 | this.nextIndex++; 16 | }; 17 | 18 | RequestContainer.prototype.get = function(index) { 19 | if (index >= this.nextIndex) 20 | return null; 21 | 22 | return JSON.parse(fs.readFileSync(path.join(this.reqDir, index + ".json"), 'utf8')); 23 | }; 24 | 25 | RequestContainer.prototype.length = function() { 26 | return this.nextIndex; 27 | }; -------------------------------------------------------------------------------- /examples/simpletest.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Self contained node.js HTTP server and a load test against it. Just run: 4 | // 5 | // $ examples/simpletest.ex.js 6 | // 7 | var http = require('http'); 8 | var nl = require('../nodeload'); 9 | console.log("Test server on localhost:9000."); 10 | http.createServer(function (req, res) { 11 | res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); 12 | res.end('foo\n'); 13 | }).listen(9000); 14 | 15 | nl.run({ 16 | name: "Read", 17 | host: 'localhost', 18 | port: 9000, 19 | numUsers: 10, 20 | timeLimit: 600, 21 | targetRps: 500, 22 | stats: [ 23 | 'result-codes', 24 | { name: 'latency', percentiles: [0.9, 0.99] }, 25 | 'concurrency', 26 | 'rps', 27 | 'uniques', 28 | { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' } 29 | ], 30 | requestGenerator: function(client) { 31 | return client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); 32 | } 33 | }); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean templates compile 2 | PROCESS_TPL = scripts/process_tpl.js 3 | SOURCES = lib/header.js lib/config.js lib/util.js lib/stats.js lib/user/program.js lib/user/http_program.js lib/loop/loop.js lib/loop/multiloop.js lib/loop/userloop.js lib/loop/multiuserloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting/*.tpl.js lib/reporting/template.js lib/reporting/report.js lib/reporting/reportmanager.js lib/reporting/external.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slaves.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/httphandler.js lib/remote/remotetesting.js 4 | 5 | all: compile 6 | 7 | clean: 8 | rm -rf ./lib-cov 9 | rm -f ./lib/reporting/*.tpl.js 10 | rm -f results-*-err.log results-*-stats.log results-*-summary.html 11 | 12 | templates: 13 | $(PROCESS_TPL) REPORT_SUMMARY_TEMPLATE lib/reporting/summary.tpl > lib/reporting/summary.tpl.js 14 | $(PROCESS_TPL) DYGRAPH_SOURCE lib/reporting/dygraph.tpl > lib/reporting/dygraph.tpl.js 15 | 16 | compile: templates 17 | -------------------------------------------------------------------------------- /examples/program.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Self contained node.js HTTP server and a load test against it. Just run: 4 | // 5 | // $ examples/simpletest.ex.js 6 | // 7 | var http = require('http'); 8 | var nl = require('../nodeload'); 9 | console.log("Test server on localhost:9000."); 10 | http.createServer(function (req, res) { 11 | res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); 12 | res.end('foo\n'); 13 | }).listen(9000); 14 | 15 | var user = function(prog) { 16 | prog.host('localhost', 9000); 17 | for (var i = 0; i < 50; i++) { 18 | prog.get('/') 19 | .wait(5000); 20 | } 21 | }; 22 | 23 | nl.run({ 24 | name: "Read", 25 | host: 'localhost', 26 | port: 9000, 27 | userProfile: [[0, 0], [400, 400]], 28 | timeLimit: 600, 29 | targetRps: 500, 30 | stats: [ 31 | 'result-codes', 32 | { name: 'latency', percentiles: [0.9, 0.99] }, 33 | 'concurrency', 34 | 'rps', 35 | 'uniques', 36 | { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' } 37 | ], 38 | userProgram: user 39 | }); 40 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Console 2 | - Update test spec on real data 3 | - Adding a master node should add its slaves 4 | - Really "add" a node (ping and connect to it) 5 | - Running / stopped indicators for each node 6 | - Start & stop any node 7 | - Fire up nodeload on a new machine 8 | - Send a test script to run on any node 9 | - Interactive console for each node 10 | - Edit test spec and "restart" (stop existing jobs / start updated test) 11 | - Clean up removed nodes properly 12 | - Download data as csv 13 | - Console webpage (stats) 14 | - Console webpage (node manager) 15 | - Add mem, disk io read + write + wait monitoring 16 | - Remote testing should also aggregate summary-only stats (e.g. uniques) 17 | - Use stats.StatsGroup in monitoring and remote 18 | - Update READMEs 19 | - Write a DEVELOPERS doc that explains the components 20 | - Add zipf number generator 21 | - Download/copy data in CSV 22 | - Allow output directory to be customized 23 | - Add support for bar graphs 24 | - Methods for graphing histograms 25 | - Allow graphs to be overlayed 26 | - Use agents for concurrent connections 27 | - Label Y axes 28 | - Add customizable notes section to summary page -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Benjamin Schmaus 2 | Copyright (c) 2010 Jonathan Lee 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /lib/monitoring/statslogger.js: -------------------------------------------------------------------------------- 1 | // ----------------- 2 | // StatsLogger 3 | // ----------------- 4 | var BUILD_AS_SINGLE_FILE; 5 | if (!BUILD_AS_SINGLE_FILE) { 6 | var START = require('../config').NODELOAD_CONFIG.START; 7 | var LogFile = require('../stats').LogFile; 8 | } 9 | 10 | /** StatsLogger writes interval stats from a Monitor or MonitorGroup to disk each time it emits 'update' */ 11 | var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { 12 | this.logNameOrObject = logNameOrObject || ('results-' + START.toISOString() + '-stats.log'); 13 | this.monitor = monitor; 14 | this.logger_ = this.log_.bind(this); 15 | }; 16 | StatsLogger.prototype.start = function() { 17 | this.createdLog = (typeof this.logNameOrObject === 'string'); 18 | this.log = this.createdLog ? new LogFile(this.logNameOrObject) : this.logNameOrObject; 19 | this.monitor.on('update', this.logger_); 20 | return this; 21 | }; 22 | StatsLogger.prototype.stop = function() { 23 | if (this.createdLog) { 24 | this.log.close(); 25 | this.log = null; 26 | } 27 | this.monitor.removeListener('update', this.logger_); 28 | return this; 29 | }; 30 | StatsLogger.prototype.log_ = function() { 31 | var summary = this.monitor.interval.summary(); 32 | this.log.put(JSON.stringify(summary) + ',\n'); 33 | }; -------------------------------------------------------------------------------- /examples/graphjmx.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*jslint forin:true */ 4 | 5 | var assert = require('assert'), 6 | child_process = require('child_process'), 7 | reporting = require('../lib/reporting'), 8 | REPORT_MANAGER = reporting.REPORT_MANAGER; 9 | 10 | REPORT_MANAGER.setLogFile('.reporting.test-output.html'); 11 | 12 | var hostAndPort = 'localhost:9999', 13 | refreshInterval = 2; 14 | 15 | var jmx = reporting.graphJmx({ 16 | host: 'localhost:9999', 17 | reportName: 'Monitors', 18 | chartName: 'Heap', 19 | mbeans: { 20 | 'Used': 'java.lang:type=Memory[HeapMemoryUsage.used]', 21 | 'Committed': 'java.lang:type=Memory[HeapMemoryUsage.committed]' 22 | }, 23 | dataFormatter: function(data) { 24 | return { 25 | Used: data.Used / 1024, 26 | Committed: data.Committed /= 1024 27 | }; 28 | }, 29 | interval: refreshInterval 30 | }); 31 | 32 | reporting.graphProcess({ 33 | reportName: 'Monitors', 34 | chartName: 'CPU (iostat)', 35 | command: 'iostat -C ' + refreshInterval, 36 | columns: [null, null, null, 'tps', 'MB/s'], 37 | }); 38 | 39 | jmx.stderr.on('data', function(data) { 40 | console.log(data.toString()); 41 | }); 42 | 43 | jmx.on('exit', function(code) { 44 | if (code !== 0) { 45 | console.log('JMX monitor died with code ' + code); 46 | } 47 | process.exit(code); 48 | }); -------------------------------------------------------------------------------- /test/http.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | http = require('http'), 3 | nlconfig = require('../lib/config').disableServer(), 4 | HttpServer = require('../lib/http').HttpServer; 5 | 6 | var server = new HttpServer().start(9020); 7 | setTimeout(function() { server.stop(); }, 1500); 8 | 9 | module.exports = { 10 | 'example: add a new route': function(beforeExit) { 11 | var done = false; 12 | server.addRoute('^/route', function(url, req, res) { 13 | done = true; 14 | res.end(); 15 | }); 16 | 17 | var client = http.createClient(9020, '127.0.0.1'), 18 | req = client.request('GET', '/route/item'); 19 | req.end(); 20 | 21 | beforeExit(function() { 22 | assert.ok(done, 'Never got request to /route'); 23 | }); 24 | }, 25 | 'test file server finds package.json': function(beforeExit) { 26 | var done = false; 27 | var client = http.createClient(9020, '127.0.0.1'), 28 | req = client.request('GET', '/package.json'); 29 | req.end(); 30 | req.on('response', function(res) { 31 | assert.equal(res.statusCode, 200); 32 | res.on('data', function(chunk) { 33 | done = true; 34 | }); 35 | }); 36 | 37 | beforeExit(function() { 38 | assert.ok(done, 'Never got response data from /package.json'); 39 | }); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /lib/reporting/reportmanager.js: -------------------------------------------------------------------------------- 1 | // This file defines REPORT_MANAGER 2 | // 3 | // Reports added to the global REPORT_MANAGER are served by the global HTTP_SERVER instance (defaults to 4 | // http://localhost:8000/) and written to disk at regular intervals. 5 | 6 | var BUILD_AS_SINGLE_FILE; 7 | if (!BUILD_AS_SINGLE_FILE) { 8 | var ReportGroup = require('./report').ReportGroup; 9 | var config = require('../config'); 10 | 11 | var NODELOAD_CONFIG = config.NODELOAD_CONFIG; 12 | var HTTP_SERVER = require('../http').HTTP_SERVER; 13 | } 14 | 15 | /** A global report manager used by nodeload to keep the summary webpage up to date during a load test */ 16 | var REPORT_MANAGER = exports.REPORT_MANAGER = new ReportGroup(); 17 | NODELOAD_CONFIG.on('apply', function() { 18 | REPORT_MANAGER.refreshIntervalMs = REPORT_MANAGER.refreshIntervalMs || NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS; 19 | REPORT_MANAGER.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); 20 | }); 21 | 22 | HTTP_SERVER.addRoute('^/$', function(url, req, res) { 23 | var html = REPORT_MANAGER.getHtml(); 24 | res.writeHead(200, {"Content-Type": "text/html", "Content-Length": html.length}); 25 | res.write(html); 26 | res.end(); 27 | }); 28 | HTTP_SERVER.addRoute('^/reports$', function(url, req, res) { 29 | var json = JSON.stringify(REPORT_MANAGER.reports); 30 | res.writeHead(200, {"Content-Type": "application/json", "Content-Length": json.length}); 31 | res.write(json); 32 | res.end(); 33 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodeload", 3 | "version": "0.4.2", 4 | "description": "Load testing library for node.js", 5 | "keywords": [ 6 | "testing", 7 | "load testing", 8 | "http" 9 | ], 10 | "homepage": "https://github.com/benschmaus/nodeload", 11 | "engines": { 12 | "node": ">=0.4" 13 | }, 14 | "contributors": [ 15 | "Benjamin Schmaus ", 16 | "Jonathan Lee ", 17 | "Robert Newson ", 18 | "Michael Mattozzi " 19 | ], 20 | "bugs": { 21 | "web": "https://github.com/benschmaus/nodeload/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "http://github.com/benschmaus/nodeload" 26 | }, 27 | "main": "./nodeload.js", 28 | "bin": { 29 | "nodeload.js": "./nodeload.js", 30 | "nl.js": "./nl.js" 31 | }, 32 | "directories": { 33 | "doc": "./doc", 34 | "lib": "./lib", 35 | "example": "./examples" 36 | }, 37 | "scripts": { 38 | "test": "expresso", 39 | "preinstall": "make clean compile" 40 | }, 41 | "devDependencies": { 42 | "expresso": ">=0.7.7" 43 | }, 44 | "dependencies": { 45 | "optparse": "1.0.3", 46 | "underscore": "~1.4.3", 47 | "underscore.string": "~2.3.1", 48 | "traverse": "~0.6.3", 49 | "superagent": "~0.12.1", 50 | "temp": "~0.5.0", 51 | "async": "~0.1.22" 52 | }, 53 | "licenses": [ 54 | { 55 | "type": "MIT", 56 | "url": "https://github.com/benschmaus/nodeload/raw/master/LICENSE" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /console/js/console.data.js: -------------------------------------------------------------------------------- 1 | /*jslint forin:true */ 2 | /*globals window document $ */ 3 | 4 | var nodes = {}; 5 | var selectedNode = null; 6 | 7 | function refreshReportsData(node) { 8 | if (!node) { return; } 9 | $.getJSON('http://' + node.name + '/reports', function(data, status) { 10 | if (data) { node.reports = data; } 11 | }); 12 | } 13 | function initData() { 14 | var refreshReports = function() { 15 | for (var i in nodes) { refreshReportsData(nodes[i]); } 16 | setTimeout(function() { refreshReports(); }, 2000); 17 | }; 18 | refreshReports(); 19 | } 20 | function getIdFromString(str) { 21 | return str.replace(/[^a-zA-Z0-9\-]/g,'-'); 22 | } 23 | function getNodeId(name) { 24 | var parts = name.split(':'); 25 | if (parts.length === 1) { 26 | return getIdFromString(name) + "-8000"; 27 | } 28 | return getIdFromString(name); 29 | } 30 | function getNodeObject(name) { 31 | var nodeId = getNodeId(name); 32 | 33 | if (nodes[nodeId]) { return nodes[nodeId]; } 34 | var node = nodes[nodeId] = { 35 | id: nodeId, 36 | name: name, 37 | reports: {} 38 | }; 39 | 40 | return node; 41 | } 42 | function deleteNodeObject(node) { 43 | if (!node) { return; } 44 | nodes[node.id] = null; 45 | } 46 | 47 | // Stubs 48 | function getTests(nodeId) { 49 | return ['Read', 'Read+Write']; 50 | } 51 | function randomize(list) { 52 | return list.map(function(x) { 53 | var rnd = [x[0]]; 54 | for (var i = 1; i < x.length; i++) { 55 | rnd[i] = Math.random() * x[i]; 56 | } 57 | return rnd; 58 | }); 59 | } -------------------------------------------------------------------------------- /lib/user/program.js: -------------------------------------------------------------------------------- 1 | var util = require('../util'), 2 | EventEmitter = require('events').EventEmitter; 3 | 4 | 5 | var Program = exports.Program = function(fun, args) { 6 | this._plan = []; 7 | this.runData = {}; 8 | this.argv = args; 9 | this.attrs = util.extend({}, Program.default_attrs); 10 | fun(this, args); 11 | }; 12 | 13 | util.inherits(Program, EventEmitter); 14 | 15 | // Registration 16 | 17 | Program.interpreters = {}; 18 | Program.default_attrs = {}; 19 | 20 | Program.registerInterpreter = function(type, fn, isRequest) { 21 | Program.prototype[type] = function() { 22 | return this.addStep(type, Array.prototype.slice.call(arguments)); 23 | }; 24 | Program.interpreters[type] = { 25 | fn: fn, 26 | isRequest: isRequest 27 | }; 28 | }; 29 | 30 | 31 | // Planning 32 | 33 | Program.prototype.addStep = function(type, args) { 34 | this._plan.push({type: type, args: args}); 35 | return this; 36 | }; 37 | 38 | // Execution 39 | Program.prototype.pendingIsRequest = function() { 40 | return Program.interpreters[this._plan[0].type].isRequest; 41 | }; 42 | 43 | Program.prototype.next = function(cb) { 44 | var nextStep = this._plan.shift(); 45 | nextStep.args.push(cb); 46 | var interpreter = Program.interpreters[nextStep.type].fn; 47 | return interpreter.apply(this, nextStep.args); 48 | }; 49 | 50 | Program.prototype.finished = function() { 51 | return this._plan.length === 0; 52 | }; 53 | 54 | // We always want "wait" 55 | Program.registerInterpreter('wait', function(duration, splay, next) { 56 | if (arguments.length === 3) { 57 | duration = duration + ((Math.random() - 0.5) * 2 * splay); 58 | } else { 59 | next = splay; 60 | } 61 | 62 | setTimeout(next, duration); 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /test/stats.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | stats = require('../lib/stats'); 3 | 4 | module.exports = { 5 | 'StatsGroup functions are non-enumerable': function(beforeExit) { 6 | var s = new stats.StatsGroup(); 7 | s.latency = {}; 8 | assert.ok(s.get); 9 | assert.ok(s.put); 10 | assert.ok(s.clear); 11 | assert.ok(s.summary); 12 | for (var i in s) { 13 | if (i !== 'latency') { 14 | assert.fail('Found enumerable property: ' + i); 15 | } 16 | } 17 | }, 18 | 'test StatsGroup methods': function(beforeExit) { 19 | var s = new stats.StatsGroup(); 20 | s.latency = new stats.Histogram(); 21 | s.results = new stats.ResultsCounter(); 22 | 23 | // name property 24 | s.name = 'test'; 25 | assert.equal(s.name, 'test'); 26 | 27 | // get()/put() 28 | s.put(1); 29 | assert.equal(s.latency.get(1), 1); 30 | assert.equal(s.results.get(1), 1); 31 | assert.eql(s.get(1), {latency: 1, results: 1}); 32 | 33 | // summary() 34 | var summary = s.summary(); 35 | assert.ok(summary.latency); 36 | assert.isDefined(summary.latency.median); 37 | assert.equal(s.summary('latency')['95%'], s.latency.summary()['95%']); 38 | assert.ok(summary.results); 39 | assert.equal(summary.results.total, 1); 40 | assert.eql(s.summary('results'), s.results.summary()); 41 | assert.equal(summary.name, 'test'); 42 | assert.ok(summary.ts); 43 | 44 | // clear() 45 | s.clear('latency'); 46 | assert.equal(s.latency.length, 0); 47 | assert.equal(s.results.length, 1); 48 | s.clear(); 49 | assert.equal(s.results.length, 0); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /examples/remotetesting.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var http = require('http'), 4 | nl = require('../nodeload'); 5 | 6 | // Start a local HTTP server that we can load test 7 | var svr = http.createServer(function (req, res) { 8 | res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); 9 | res.end(req.url); 10 | }); 11 | svr.listen(9000); 12 | console.log('Started test server.'); 13 | 14 | // Define the tests and nodeload cluster 15 | var i = 0, 16 | readtest = { 17 | name: "Read", 18 | host: 'localhost', 19 | port: 9000, 20 | timeLimit: 40, 21 | loadProfile: [[0,0], [10, 100], [30, 100], [39, 0]], 22 | userProfile: [[0,0], [20, 10]], 23 | stats: ['result-codes', {name: 'latency', percentiles: [0.95, 0.999]}, 'concurrency', 'uniques', 'request-bytes', 'response-bytes'], 24 | requestGenerator: function(client) { 25 | var request = client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); 26 | request.end(); 27 | return request; 28 | } 29 | }, 30 | writetest = { 31 | name: "Write", 32 | host: 'localhost', 33 | port: 9000, 34 | numUsers: 10, 35 | timeLimit: 40, 36 | targetRps: 20, 37 | stats: ['result-codes', 'latency', 'uniques'], 38 | requestGenerator: function(client) { 39 | var request = client.request('PUT', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); 40 | request.end('foo'); 41 | return request; 42 | } 43 | }, 44 | cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8002', 'localhost:8001']); 45 | 46 | // Start the cluster 47 | cluster.run(readtest, writetest); 48 | cluster.on('end', function() { 49 | console.log('All tests complete.'); 50 | process.exit(0); 51 | }); -------------------------------------------------------------------------------- /console/css/console.css: -------------------------------------------------------------------------------- 1 | body{ 2 | font: 62.5% "Trebuchet MS", sans-serif; 3 | } 4 | 5 | /* Base elements: background, header, main content panels 6 | ----------------------------------------------------------*/ 7 | .clsDarkBackground{ 8 | background: #6e4f1c url(pepper-grinder/images/ui-bg_diagonal-maze_20_6e4f1c_10x10.png) 50% 50% repeat; 9 | } 10 | #pnlBackground{ 11 | padding: 0.5%; 12 | height: 100%; 13 | } 14 | 15 | #pnlHeader{ 16 | -webkit-transform: rotate(-90deg); 17 | -webkit-transform-origin: top left; 18 | -moz-transform: rotate(-90deg); 19 | position: absolute; 20 | top: 11em; 21 | font-size: 2em; font-weight: bold; 22 | width:auto; 23 | } 24 | #pnlMain{ 25 | float: left; 26 | margin-left: 3em; 27 | height: 100%; 28 | width: 95%; 29 | padding: 1em; 30 | background: #ffffff; 31 | } 32 | .clsMainRow{ 33 | display: inline-block; 34 | margin-bottom: 1em; 35 | width:100%; 36 | } 37 | 38 | /* Nodes header bar 39 | ----------------------------------------------------------*/ 40 | .clsToolbar{ 41 | padding: 0px 6px; 42 | } 43 | #frmAddNode{ 44 | position: absolute; 45 | display: none; 46 | padding: 5px; 47 | z-index: 1; 48 | } 49 | 50 | /* Tabs area 51 | ----------------------------------------------------------*/ 52 | #pnlCharts{ 53 | float: left; 54 | width: 75%; height: 100%; 55 | } 56 | .clsChartContainer{ 57 | position:relative; 58 | overflow:hidden; 59 | margin-bottom:30px; 60 | } 61 | .clsChartLegend{ 62 | position:absolute; 63 | display:block; 64 | top:0px; 65 | right: 0px; 66 | } 67 | 68 | /* Summary and details area (right column) 69 | ----------------------------------------------------------*/ 70 | #pnlRightColumn{ 71 | float: right; 72 | width: 24%; height: 100%; 73 | } 74 | #pnlSummary{ 75 | font-size: 1.3em; 76 | padding: 0.5em; 77 | } 78 | #pnlSummary table{ 79 | font-variant: small-caps; 80 | border-spacing: 10px 1px; 81 | } 82 | 83 | 84 | /* Miscellaneous 85 | ----------------------------------------------------------*/ 86 | .clsShortcutKeys { 87 | float: right; 88 | color: gray; 89 | margin: 2px 5px 0px 0px; 90 | } -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | http = require('http'), 3 | util = require('../lib/util'); 4 | 5 | module.exports = { 6 | 'ReconnectingClient tolerates connection failures': function(beforeExit) { 7 | var PORT = 9010, 8 | simpleResponse = function (req, res) { res.writeHead(200); res.end(); }, 9 | svr = http.createServer(simpleResponse), 10 | client = util.createReconnectingClient(PORT, 'localhost'), 11 | numResponses = 0, 12 | clientErrorsDetected = 0, 13 | req, testTimeout; 14 | 15 | // reconnecting client should work like a normal client and get a response from our server 16 | svr.listen(PORT); 17 | req = client.request('GET', '/'); 18 | assert.isNotNull(req); 19 | req.on('response', function(res) { 20 | numResponses++; 21 | res.on('end', function() { 22 | // once the server is terminated, request() should cause a clientError event (below) 23 | svr = svr.close(); 24 | req = client.request('GET','/'); 25 | 26 | client.once('reconnect', function() { 27 | // restart server, and request() should work again 28 | svr = http.createServer(simpleResponse); 29 | svr.listen(PORT); 30 | 31 | req = client.request('GET','/'); 32 | req.end(); 33 | req.on('response', function(res) { 34 | clearTimeout(testTimeout); 35 | 36 | numResponses++; 37 | svr = svr.close(); 38 | }); 39 | }); 40 | }); 41 | }); 42 | client.on('error', function(err) { clientErrorsDetected++; }); 43 | req.end(); 44 | 45 | // Maximum timeout for this test is 1 second 46 | testTimeout = setTimeout(function() { if (svr) { svr.close(); } }, 2000); 47 | 48 | beforeExit(function() { 49 | assert.equal(clientErrorsDetected, 1); 50 | assert.equal(numResponses, 2); 51 | }); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /examples/nodeload.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*jslint sub:true */ 4 | 5 | var http = require('http'), 6 | nl = require('../nodeload'); 7 | 8 | var svr = http.createServer(function (req, res) { 9 | res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); 10 | res.end(req.url); 11 | }); 12 | svr.listen(9000); 13 | console.log('Started test server.'); 14 | 15 | var i = 0, 16 | readtest = { 17 | name: "Read", 18 | host: 'localhost', 19 | port: 9000, 20 | timeLimit: 40, 21 | loadProfile: [[0,0], [10, 100], [30, 100], [39, 0]], 22 | userProfile: [[0,0], [20, 10]], 23 | stats: ['result-codes', {name: 'latency', percentiles: [0.95, 0.999]}, 'concurrency', 'uniques', 'request-bytes', 'response-bytes'], 24 | requestGenerator: function(client) { 25 | return client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost', 'connection': 'keep-alive' }); 26 | } 27 | }, 28 | writetest = { 29 | name: "Write", 30 | host: 'localhost', 31 | port: 9000, 32 | numUsers: 10, 33 | timeLimit: 40, 34 | targetRps: 20, 35 | stats: ['result-codes', 'latency', 'uniques'], 36 | requestGenerator: function(client) { 37 | var request = client.request('PUT', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost', 'connection': 'keep-alive' }); 38 | request.end('foo'); 39 | return request; 40 | } 41 | }, 42 | cleanup = { 43 | name: "Cleanup", 44 | host: 'localhost', 45 | port: 9000, 46 | numUsers: 50, 47 | numRequests: 8001, 48 | stats: ['result-codes'], 49 | requestGenerator: function(client) { 50 | return client.request('DELETE', "/" + i++, { 'host': 'localhost', 'connection': 'keep-alive' }); 51 | } 52 | }, 53 | loadtest = nl.run(readtest, writetest); 54 | 55 | loadtest.updateInterval = 1000; 56 | loadtest.on('end', function() { 57 | loadtest = nl.run(cleanup); 58 | loadtest.on('end', function() { 59 | console.log('Closing test server.'); 60 | svr.close(); 61 | process.exit(0); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /doc/remote.md: -------------------------------------------------------------------------------- 1 | **This document is out-of-date. See [`lib/remote/remotetesting.js`](https://github.com/benschmaus/nodeload/tree/master/lib/remote/remotetesting.js) and [`lib/remote/cluster.js`](https://github.com/benschmaus/nodeload/tree/master/lib/remote/cluster.js).** 2 | 3 | ## Distributed Testing ## 4 | 5 | Functions to distribute tests across multiple slave `nodeload` instances. See `remote.js`. 6 | 7 | **Functions:** 8 | 9 | * `remoteTest(spec)`: Return a test to be scheduled with `remoteStart(...)` (`spec` uses same format as `addTest(spec)`). 10 | * `remoteStart(master, slaves, tests, callback, stayAliveAfterDone)`: Run tests on specified slaves. 11 | * `remoteStartFile(master, slaves, filename, callback, stayAliveAfterDone)`: Execute a `.js` file on specified slaves. 12 | 13 | **Usage**: 14 | 15 | First, start `nodeloadlib.js` on each slave instances. 16 | 17 | $ node dist/nodeloadlib.js # Run on each slave machine 18 | 19 | Then, create tests using `remoteTest(spec)` with the same `spec` fields in the **Test Definition** section above. Pass the created tests as a list to `remoteStart(...)` to execute them on slave `nodeload` instances. `master` must be the `"host:port"` of the `nodeload` which runs `remoteStart(...)`. It will receive and aggregate statistics from the slaves, so the address should be reachable by the slaves. Or, use `master=null` to disable reports from the slaves. 20 | 21 | // This script must be run on master:8000, which will aggregate results. Each slave 22 | // will GET http://internal-service:8080/ at 100 rps. 23 | var t1 = nl.remoteTest({ 24 | name: "Distributed test", 25 | host: 'internal-service', 26 | port: 8080, 27 | timeLimit: 20, 28 | targetRps: 100 29 | }); 30 | nl.remoteStart('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], [t1]); 31 | 32 | Alternatively, an existing `nodeload` script file can be used: 33 | 34 | // The file /path/to/load-test.js should contain valid javascript and can use any nodeloadlib functions 35 | nl.remoteStartFile('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], '/path/to/load-test.js'); 36 | 37 | When the remote tests complete, the master instance will call the `callback` parameter if non-null. It then automatically terminates after 3 seconds unless the parameter `stayAliveAfterDone==true`. -------------------------------------------------------------------------------- /doc/tips.md: -------------------------------------------------------------------------------- 1 | **This page is out of date** 2 | 3 | TIPS AND TRICKS 4 | ================ 5 | 6 | Some handy features of `nodeload` worth mentioning. 7 | 8 | 1. **Examine and add to stats to the HTML page:** 9 | 10 | addTest().stats and runTest().stats are maps: 11 | 12 | { 'latency': Reportable(Histogram), 13 | 'result-codes': Reportable(ResultsCounter}, 14 | 'uniques': Reportable(Uniques), 15 | 'concurrency': Reportable(Peak) } 16 | 17 | Put `Reportable` instances to this map to have it automatically updated each reporting interval and added to the summary webpage. 18 | 19 | 2. **Post-process statistics:** 20 | 21 | Use a `startTests()` callback to examine the final statistics in `test.stats[name].cumulative` at test completion. 22 | 23 | // GET random URLs of the form localhost:8080/data/object-#### for 10 seconds, then 24 | // print out all the URLs that were hit. 25 | var t = addTest({ 26 | timeLimit: 10, 27 | targetRps: 10, 28 | stats: ['uniques'], 29 | requestGenerator: function(client) { 30 | return traceableRequest(client, 'GET', '/data/object-' + Math.floor(Math.random()*100));; 31 | } 32 | }); 33 | function printAllUrls() { 34 | console.log(JSON.stringify(t.stats['uniques'].cumulative)); 35 | } 36 | startTests(printAllUrls); 37 | 38 | 39 | 3. **Out-of-the-box file server:** 40 | 41 | Just start `nodeloadlib.js` and it will serve files in the current directory. 42 | 43 | $ node lib/nodeloadlib.js 44 | $ curl -i localhost:8000/lib/nodeloadlib.js # executed in a separate terminal 45 | HTTP/1.1 200 OK 46 | Content-Length: 50763 47 | Connection: keep-alive 48 | 49 | var sys = require('sys'); 50 | var http = require('http'); 51 | ... 52 | 53 | 4. **Run arbitrary Javascript:** 54 | 55 | POST any valid Javascript to `/remote` to have it `eval()`'d. 56 | 57 | $ node dist/nodeloadlib.js 58 | Serving progress report on port 8000. 59 | Opening log files. 60 | Received remote command: 61 | sys.puts("hello!") 62 | hello! 63 | 64 | $ curl -i -d 'sys.puts("hello!")' localhost:8000/remote # executed in a separate terminal 65 | -------------------------------------------------------------------------------- /lib/reporting/template.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Based off of: 3 | * - Chad Etzel - http://github.com/jazzychad/template.node.js/ 4 | * - John Resig - http://ejohn.org/blog/javascript-micro-templating/ 5 | */ 6 | var BUILD_AS_SINGLE_FILE; 7 | if (!BUILD_AS_SINGLE_FILE) { 8 | var fs = require('fs'); 9 | } 10 | 11 | var template = { 12 | cache_: {}, 13 | create: function(str, data, callback) { 14 | // Figure out if we're getting a template, or if we need to 15 | // load the template - and be sure to cache the result. 16 | var fn; 17 | 18 | if (!/[\t\r\n% ]/.test(str)) { 19 | if (!callback) { 20 | fn = this.create(fs.readFileSync(str).toString('utf8')); 21 | } else { 22 | fs.readFile(str, function(err, buffer) { 23 | if (err) { throw err; } 24 | 25 | this.create(buffer.toString('utf8'), data, callback); 26 | }); 27 | return; 28 | } 29 | } else { 30 | if (this.cache_[str]) { 31 | fn = this.cache_[str]; 32 | } else { 33 | // Generate a reusable function that will serve as a template 34 | // generator (and which will be cached). 35 | fn = new Function("obj", 36 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 37 | "obj=obj||{};" + 38 | // Introduce the data as local variables using with(){} 39 | "with(obj){p.push('" + 40 | 41 | // Convert the template into pure JavaScript 42 | str.split("'").join("\\'") 43 | .split("\n").join("\\n") 44 | .replace(/<%([\s\S]*?)%>/mg, function(m, t) { return '<%' + t.split("\\'").join("'").split("\\n").join("\n") + '%>'; }) 45 | .replace(/<%=(.+?)%>/g, "',$1,'") 46 | .split("<%").join("');") 47 | .split("%>").join("p.push('") + "');}return p.join('');"); 48 | 49 | this.cache_[str] = fn; 50 | } 51 | } 52 | 53 | // Provide some "basic" currying to the user 54 | if (callback) { callback(data ? fn( data ) : fn); } 55 | else { return data ? fn( data ) : fn; } 56 | } 57 | }; 58 | 59 | exports.create = template.create.bind(template); -------------------------------------------------------------------------------- /doc/reporting.md: -------------------------------------------------------------------------------- 1 | **This document is out-of-date. See [`lib/reporting.js`](https://github.com/benschmaus/nodeload/tree/master/lib/reporting.js).** 2 | 3 | ## Web-based Reports ## 4 | 5 | Functions for manipulating the report that is available during the test at http://localhost:8000/ and that is written to `results-{timestamp}-summary.html`. 6 | 7 | **Interface:** 8 | 9 | * `REPORT_MANAGER.reports`: All of the reports that are displayed in the summary webpage. 10 | * `REPORT_MANAGER.addReport(Report)`: Add a report object to the webpage. 11 | * `Report(name, updater(Report))`: A report consists of a set of charts, displayed in the main body of the webpage, and a summary object displayed on the right side bar. A report has a name and an updater function. Calling `updater(Report)` should update the report's chart and summary. When tests are running, REPORT_MANAGER calls each report's `updater` periodically. 12 | * `Report.summary`: A JSON object displayed in table form in the summary webpage right side bar. 13 | * `Report.getChart(name)`: Gets or creates a chart with the title `name` to the report and returns a `Chart` object. See `Chart.put(data)` below. 14 | * `Chart.put(data)`: Add the data, which is a map of { 'trend-1': value, 'trend-2': value, ... }, to the chart, which tracks the values for each trend over time. 15 | 16 | **Usage:** 17 | 18 | An HTTP server is started on port 8000 by default. Use: 19 | 20 | `var nl = require('./lib/nodeloadlib).disableServer()` 21 | 22 | to disable the HTTP server, or 23 | 24 | `var nl = require('./lib/nodeloadlib).usePort(port)` 25 | 26 | to change the port binding. The file `results-{timestamp}-summary.html` is written to the current directory. Use 27 | 28 | `var nl = require('./lib/nodeloadlib).disableLogs()` 29 | 30 | to disable creation of this file. 31 | 32 | A report is automatically added for each test created by `addTest()` or `runTest()`. To add additional charts to the summary webpage: 33 | 34 | var mycounter = 0; 35 | REPORT_MANAGER.addReport(new Report("My Report", function(report) { 36 | chart = report.getChart("My Chart"); 37 | chart.put({ 'counter': mycounter++ }); 38 | chart.summary = { 'Total increments': mycounter }; 39 | })); 40 | 41 | The webpage automatically issues an AJAX request to refresh the text and chart data every 2 seconds by default. Change the refresh period using: 42 | 43 | `var nl = require('./lib/nodeloadlib).setAjaxRefreshIntervalMs(milliseconds)` 44 | -------------------------------------------------------------------------------- /lib/remote/slaves.js: -------------------------------------------------------------------------------- 1 | var BUILD_AS_SINGLE_FILE; 2 | if (!BUILD_AS_SINGLE_FILE) { 3 | var util = require('../util'); 4 | var Slave = require('./slave').Slave; 5 | var EventEmitter = require('events').EventEmitter; 6 | } 7 | 8 | /** A small wrapper for a collection of Slave instances. The instances are all started and stopped 9 | together and method calls are sent to all the instances. 10 | 11 | Slaves emits the following events: 12 | - 'slaveError', slave, error: The underlying HTTP connection for this slave returned an error. 13 | - 'start': All of the slave instances are running. 14 | - 'stopped': All of the slave instances have been stopped. */ 15 | 16 | var Slaves = exports.Slaves = function Slaves(masterEndpoint, pingInterval) { 17 | EventEmitter.call(this); 18 | this.masterEndpoint = masterEndpoint; 19 | this.slaves = []; 20 | this.pingInterval = pingInterval; 21 | }; 22 | util.inherits(Slaves, EventEmitter); 23 | /** Add a remote instance in the format 'host:port' as a slave in this collection */ 24 | Slaves.prototype.add = function(hostAndPort) { 25 | var self = this, 26 | parts = hostAndPort.split(':'), 27 | host = parts[0], 28 | port = Number(parts[1]) || 8000, 29 | id = host + ':' + port, 30 | slave = new Slave(id, host, port, self.masterEndpoint, self.pingInterval); 31 | 32 | self.slaves.push(slave); 33 | self[id] = slave; 34 | self[id].on('slaveError', function(err) { 35 | self.emit('slaveError', slave, err); 36 | }); 37 | self[id].on('start', function() { 38 | var allStarted = util.every(self.slaves, function(id, s) { return s.state === 'started'; }); 39 | if (!allStarted) { return; } 40 | self.emit('start'); 41 | }); 42 | self[id].on('end', function() { 43 | var allStopped = util.every(self.slaves, function(id, s) { return s.state !== 'started'; }); 44 | if (!allStopped) { return; } 45 | self.emit('end'); 46 | }); 47 | }; 48 | /** Define a method on all the slaves */ 49 | Slaves.prototype.defineMethod = function(name, fun) { 50 | var self = this; 51 | 52 | self.slaves.forEach(function(slave) { 53 | slave.defineMethod(name, fun); 54 | }); 55 | 56 | self[name] = function() { 57 | var args = arguments; 58 | return self.slaves.map(function(s) { return s[name].apply(s, args); }); 59 | }; 60 | }; 61 | /** Start all the slaves */ 62 | Slaves.prototype.start = function() { 63 | this.slaves.forEach(function(s) { s.start(); }); 64 | }; 65 | /** Terminate all the slaves */ 66 | Slaves.prototype.end = function() { 67 | this.slaves.forEach(function(s) { s.end(); }); 68 | }; -------------------------------------------------------------------------------- /doc/nl.md: -------------------------------------------------------------------------------- 1 | NAME 2 | ---- 3 | 4 | nl - Load test tool for HTTP APIs. Generates result charts and has hooks 5 | for generating requests. 6 | 7 | SYNOPSIS 8 | -------- 9 | 10 | nl.js [options] :[] 11 | 12 | DESCRIPTION 13 | ----------- 14 | 15 | nl is for generating lots of requests to send to an HTTP API. It is 16 | inspired by Apache's ab benchmark tool and is designed to let programmers 17 | develop load tests and get informative reports without having to learn a 18 | big and complicated framework. 19 | 20 | OPTIONS 21 | ------- 22 | 23 | -n, --number NUMBER Number of requests to make. Defaults to 24 | value of --concurrency unless a time limit is specified. 25 | -c, --concurrency NUMBER Concurrent number of connections. Defaults to 1. 26 | -t, --time-limit NUMBER Number of seconds to spend running test. No timelimit by default. 27 | -e, --request-rate NUMBER Target number of requests per seconds. Infinite by default 28 | -m, --method STRING HTTP method to use. 29 | -d, --data STRING Data to send along with PUT or POST request. 30 | -r, --request-generator STRING Path to module that exports getRequest function 31 | -i, --report-interval NUMBER Frequency in seconds to report statistics. Default is 10. 32 | -q, --quiet Supress display of progress count info. 33 | -h, --help Show usage info 34 | 35 | 36 | ENVIRONMENT 37 | ----------- 38 | 39 | nl requires node to be installed somewhere on your path. Get it 40 | from http://nodejs.org/#download. 41 | 42 | To get a known working combination of nodeload + node, be sure 43 | to install using npm: 44 | 45 | $ curl http://npmjs.org/install.sh | sh # installs npm 46 | $ npm install nodeload 47 | 48 | QUICKSTART 49 | ---------- 50 | 51 | nodeload contains a toy server that you can use for a quick demo. 52 | Try the following: 53 | 54 | $ examples/test-server.js & 55 | [1] 2756 56 | $ Server running at http://127.0.0.1:9000/ 57 | $ nl.js -f -c 10 -n 10000 -i 1 -r examples/test-generator.js localhost:9000 58 | 59 | You should now see some test output in your console. The generated HTML 60 | report contains a graphical chart of test results. 61 | 62 | THANKS 63 | ------ 64 | 65 | Thanks to Orlando Vazquez for the original proof of concept app. 66 | 67 | SEE ALSO 68 | -------- 69 | 70 | `ab(1)`, [NODELOADLIB.md](http://github.com/benschmaus/nodeload/blob/master/NODELOADLIB.md) 71 | -------------------------------------------------------------------------------- /doc/stats.md: -------------------------------------------------------------------------------- 1 | ## Statistics ## 2 | 3 | Implementations of various statistics. See [`lib/stats.js`](https://github.com/benschmaus/nodeload/tree/master/lib/stats.js). 4 | 5 | **Classes:** 6 | 7 | * `Histogram(numBuckets)`: A histogram of integers. If most of the items are between 0 and `numBuckets`, calculating percentiles and stddev is fast. 8 | * `Accumulator`: Calculates the sum of the numbers put in. 9 | * `ResultsCounter`: Tracks results which are be limited to a small set of possible choices. Tracks the total number of results, number of results by value, and results added per second. 10 | * `Uniques`: Tracks the number of unique items added. 11 | * `Peak`: Tracks the max of the numbers put in. 12 | * `Rate`: Tracks the rate at which items are added. 13 | * `LogFile`: Outputs to a file on disk. 14 | * `NullLog`: Ignores all items put in. 15 | * `Reportable`: Wraps any other statistic to store an interval and cumulative version of it. 16 | 17 | **Functions:** 18 | 19 | * `randomString(length)`: Returns a random string of ASCII characters between 32 and 126 of the requested length. 20 | * `nextGaussian(mean, stddev)`: Returns a normally distributed number using the provided mean and standard deviation. 21 | * `nextPareto(min, max, shape)`: Returns a Pareto distributed number between `min` and `max` inclusive using the provided shape. 22 | * `roundRobin(list)`: Returns a copy of the list with a `get()` method. `get()` returns list entries round robin. 23 | 24 | **Usage:** 25 | 26 | All of the statistics classes support the methods: 27 | 28 | * `.length`: The total number of items `put()` into this object. 29 | * `put(item)`: Include an item in the statistic. 30 | * `get()`: Get a specific value from the object, which varies depending on the object. 31 | * `clear()`: Clear out all items. 32 | * `summary()`: Get a object containing a summary of the object, which varies depending on the object. The fields returned are used to generate the trends of the HTML report graphs. 33 | 34 | In addition, these other methods are supported: 35 | 36 | * `Histogram.mean()`: Calculate the mean of the numbers in the histogram. 37 | * `Histogram.percentile(percentile)`: Calculate the given `percentile`, between 0 and 1, of the numbers in the histogram. 38 | * `Histogram.stddev()`: Standard deviation of the numbers in the histogram. 39 | * `LogFile.open()`: Open the file. 40 | * `LogFile.clear(text)`: Truncate the file, and write `text` if specified. 41 | * `LogFile.close()`: Close the file. 42 | * `Reportable.next()`: clear out the interval statistic for the next window. 43 | 44 | Refer to the [`lib/stats.js`](https://github.com/benschmaus/nodeload/tree/master/lib/stats.js) for the return value of the `get()` and `summary()` functions for the different classes. -------------------------------------------------------------------------------- /lib/remote/endpointclient.js: -------------------------------------------------------------------------------- 1 | var BUILD_AS_SINGLE_FILE; 2 | if (!BUILD_AS_SINGLE_FILE) { 3 | var http = require('http'); 4 | var util = require('../util'); 5 | var EventEmitter = require('events').EventEmitter; 6 | var qputs = util.qputs; 7 | } 8 | 9 | var DEFAULT_RETRY_INTERVAL_MS = 2000; 10 | 11 | /** EndpointClient represents an HTTP connection to an Endpoint. The supported methods should be added 12 | by calling defineMethod(...). For example, 13 | 14 | client = new EndpointClient('myserver', 8000, '/remote/0'); 15 | client.defineMethod('method_1'); 16 | client.on('connect', function() { 17 | client.method_1(args); 18 | }); 19 | 20 | will send a POST request to http://myserver:8000/remote/0/method_1 with the body [args], which causes 21 | the Endpoint listening on myserver to execute method_1(args). 22 | 23 | EndpointClient emits the following events: 24 | - 'connect': An HTTP connection to the remote endpoint has been established. Methods may now be called. 25 | - 'clientError', Error: The underlying HTTP connection returned an error. The connection will be retried. 26 | - 'clientError', http.ClientResponse: A call to a method on the endpoint returned this non-200 response. 27 | - 'end': The underlying HTTP connect has been terminated. No more events will be emitted. 28 | */ 29 | var EndpointClient = exports.EndpointClient = function EndpointClient(host, port, basepath) { 30 | EventEmitter.call(this); 31 | this.host = host; 32 | this.port = port; 33 | this.client = util.createReconnectingClient(port, host); 34 | this.client.on('error', this.emit.bind(this, 'error')); 35 | this.basepath = basepath || ''; 36 | this.methodNames = []; 37 | this.retryInterval = DEFAULT_RETRY_INTERVAL_MS; 38 | this.setStaticParams([]); 39 | }; 40 | util.inherits(EndpointClient, EventEmitter); 41 | /** Send an arbitrary HTTP request using the underlying http.Client. */ 42 | EndpointClient.prototype.rawRequest = function() { 43 | return this.client.request.apply(this.client, arguments); 44 | }; 45 | EndpointClient.prototype.setStaticParams = function(params) { 46 | this.staticParams_ = params instanceof Array ? params : [params]; 47 | }; 48 | /** Add a method that the target server understands. The method can be executed by calling 49 | endpointClient.method(args...). */ 50 | EndpointClient.prototype.defineMethod = function(name) { 51 | var self = this; 52 | self[name] = function() { 53 | var req = self.client.request('POST', self.basepath + '/' + name), 54 | params = self.staticParams_.concat(util.argarray(arguments)); 55 | 56 | req.on('response', function(res) { 57 | if (res.statusCode !== 200) { 58 | self.emit('clientError', res); 59 | } 60 | }); 61 | req.end(JSON.stringify(params)); 62 | 63 | return req; 64 | }; 65 | self.methodNames.push(name); 66 | return self; 67 | }; 68 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Nodeload configuration 3 | // ------------------------------------ 4 | // 5 | // The functions in this file control the behavior of the nodeload globals, like HTTP_SERVER and 6 | // REPORT_MANAGER. They should be called when the library is included: 7 | // 8 | // var nl = require('./lib/nodeload').quiet().usePort(10000); 9 | // nl.runTest(...); 10 | // 11 | // Or, when using individual modules: 12 | // 13 | // var nlconfig = require('./lib/config').quiet().usePort(10000); 14 | // var reporting = require('./lib/reporting'); 15 | // 16 | var BUILD_AS_SINGLE_FILE, NODELOAD_CONFIG; 17 | if (!BUILD_AS_SINGLE_FILE) { 18 | var EventEmitter = require('events').EventEmitter; 19 | } 20 | 21 | /** Suppress all console output */ 22 | exports.quiet = function() { 23 | NODELOAD_CONFIG.QUIET = true; 24 | return exports; 25 | }; 26 | 27 | /** Start the nodeload HTTP server on the given port */ 28 | exports.usePort = function(port) { 29 | NODELOAD_CONFIG.HTTP_PORT = port; 30 | return exports; 31 | }; 32 | 33 | /** Do not start the nodeload HTTP server */ 34 | exports.disableServer = function() { 35 | NODELOAD_CONFIG.HTTP_ENABLED = false; 36 | return exports; 37 | }; 38 | 39 | /** Set the default number of milliseconds between 'update' events from a LoadTest created by run(). */ 40 | exports.setMonitorIntervalMs = function(milliseconds) { 41 | NODELOAD_CONFIG.MONITOR_INTERVAL_MS = milliseconds; 42 | return exports; 43 | }; 44 | 45 | /** Set the number of milliseconds between auto-refreshes for the summary webpage */ 46 | exports.setAjaxRefreshIntervalMs = function(milliseconds) { 47 | NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS = milliseconds; 48 | return exports; 49 | }; 50 | 51 | /** Do not write any logs to disk */ 52 | exports.disableLogs = function() { 53 | NODELOAD_CONFIG.LOGS_ENABLED = false; 54 | return exports; 55 | }; 56 | 57 | /** Set the number of milliseconds between pinging slaves when running distributed load tests */ 58 | exports.setSlaveUpdateIntervalMs = function(milliseconds) { 59 | NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS = milliseconds; 60 | }; 61 | 62 | // ================= 63 | // Singletons 64 | // ================= 65 | 66 | var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { 67 | START: new Date(), 68 | 69 | QUIET: Boolean(process.env.QUIET) || false, 70 | 71 | HTTP_ENABLED: true, 72 | HTTP_PORT: Number(process.env.HTTP_PORT) || 8000, 73 | 74 | MONITOR_INTERVAL_MS: 2000, 75 | 76 | AJAX_REFRESH_INTERVAL_MS: 2000, 77 | 78 | LOGS_ENABLED: process.env.LOGS ? process.env.LOGS !== '0' : true, 79 | 80 | SLAVE_UPDATE_INTERVAL_MS: 3000, 81 | 82 | eventEmitter: new EventEmitter(), 83 | on: function(event, fun) { 84 | this.eventEmitter.on(event, fun); 85 | }, 86 | apply: function() { 87 | this.eventEmitter.emit('apply'); 88 | } 89 | }; 90 | 91 | process.nextTick(function() { NODELOAD_CONFIG.apply(); }); -------------------------------------------------------------------------------- /lib/loop/multiuserloop.js: -------------------------------------------------------------------------------- 1 | var BUILD_AS_SINGLE_FILE; 2 | if (!BUILD_AS_SINGLE_FILE) { 3 | var util = require('../util'); 4 | var userloop = require('./userloop'); 5 | var MultiLoop = require('./multiloop').MultiLoop; 6 | var EventEmitter = require('events').EventEmitter; 7 | var UserLoop = userloop.UserLoop; 8 | var USER_LOOP_OPTIONS = UserLoop.USER_LOOP_OPTIONS; 9 | } 10 | 11 | var MultiUserLoop = exports.MultiUserLoop = function MultiUserLoop(spec) { 12 | EventEmitter.call(this); 13 | this.spec = util.extend({}, util.defaults(spec, USER_LOOP_OPTIONS)); 14 | this.loops = []; 15 | this.concurrencyProfile = spec.concurrencyProfile || [[0, spec.concurrency]]; 16 | this.updater_ = this.update_.bind(this); 17 | this.finishedChecker_ = this.checkFinished_.bind(this); 18 | } 19 | util.inherits(MultiUserLoop, MultiLoop); 20 | 21 | /** Start all scheduled Loops. When the loops complete, 'end' event is emitted. */ 22 | MultiUserLoop.prototype.start = function() { 23 | if (this.running) { return; } 24 | this.running = true; 25 | this.startTime = new Date(); 26 | this.concurrency = 0; 27 | this.loops = []; 28 | this.loopConditions_ = []; 29 | 30 | if (this.spec.numberOfTimes > 0 && this.spec.numberOfTimes < Infinity) { 31 | this.loopConditions_.push(Loop.maxExecutions(this.spec.numberOfTimes)); 32 | } 33 | 34 | if (this.spec.duration > 0 && this.spec.duration < Infinity) { 35 | this.endTimeoutId = setTimeout(this.stop.bind(this), this.spec.duration * 1000); 36 | } 37 | 38 | process.nextTick(this.emit.bind(this, 'start')); 39 | this.update_(); 40 | return this; 41 | }; 42 | 43 | /** Force all loops to finish */ 44 | MultiUserLoop.prototype.stop = function() { 45 | if (!this.running) { return; } 46 | clearTimeout(this.endTimeoutId); 47 | clearTimeout(this.updateTimeoutId); 48 | this.running = false; 49 | this.loops.forEach(function(l) { l.stop(); }); 50 | this.emit('remove', this.loops); 51 | this.emit('end'); 52 | this.loops = []; 53 | }; 54 | 55 | 56 | MultiUserLoop.prototype.update_ = function() { 57 | var i, now = Math.floor((new Date() - this.startTime) / 1000), 58 | concurrency = this.getProfileValue_(this.concurrencyProfile, now), 59 | timeout = this.getProfileTimeToNextValue_(this.concurrencyProfile, now) * 1000; 60 | 61 | if (concurrency < this.concurrency) { 62 | var removed = this.loops.splice(concurrency); 63 | removed.forEach(function(l) { l.stop(); }); 64 | this.emit('remove', removed); 65 | } else if (concurrency > this.concurrency) { 66 | var loops = []; 67 | for (i = 0; i < concurrency-this.concurrency; i++) { 68 | var loop = new UserLoop(this.spec.userProgram, this.spec.programArguments, this.loopConditions_).start(); 69 | loop.on('end', this.finishedChecker_); 70 | loops.push(loop); 71 | } 72 | this.loops = this.loops.concat(loops); 73 | this.emit('add', loops); 74 | } 75 | 76 | this.concurrency = concurrency; 77 | 78 | if (timeout < Infinity) { 79 | this.updateTimeoutId = setTimeout(this.updater_, timeout); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /console/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nodeload Console 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 27 | 28 |
29 |
NODELOAD CONSOLE
30 |
31 |
32 | 33 | 34 | 45 |

Nodes:

35 | 36 | 37 | 38 |
39 | Host: 40 | 41 | 42 |
43 |
< K    J >
44 |
46 |
47 |
48 |
49 |
50 |
51 |

Overall Statistics

52 |
53 |
54 |
55 |

Test Details

56 |
57 | spec: { 58 | 59 | } 60 |
61 |
62 |
63 |

Node Details

64 |
Last ping:
65 |
66 |
67 |
68 |
69 |
70 | 71 | -------------------------------------------------------------------------------- /console/js/jquery.hotkeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Hotkeys Plugin 3 | * Copyright 2010, John Resig 4 | * Dual licensed under the MIT or GPL Version 2 licenses. 5 | * 6 | * Based upon the plugin by Tzury Bar Yochay: 7 | * http://github.com/tzuryby/hotkeys 8 | * 9 | * Original idea by: 10 | * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ 11 | */ 12 | 13 | (function(jQuery){ 14 | 15 | jQuery.hotkeys = { 16 | version: "0.8", 17 | 18 | specialKeys: { 19 | 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", 20 | 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 21 | 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 22 | 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 23 | 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 24 | 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 25 | 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" 26 | }, 27 | 28 | shiftNums: { 29 | "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", 30 | "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", 31 | ".": ">", "/": "?", "\\": "|" 32 | } 33 | }; 34 | 35 | function keyHandler( handleObj ) { 36 | // Only care when a possible input has been specified 37 | if ( typeof handleObj.data !== "string" ) { 38 | return; 39 | } 40 | 41 | var origHandler = handleObj.handler, 42 | keys = handleObj.data.toLowerCase().split(" "); 43 | 44 | handleObj.handler = function( event ) { 45 | // Don't fire in text-accepting inputs that we didn't directly bind to 46 | if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || 47 | event.target.type === "text") ) { 48 | return; 49 | } 50 | 51 | // Keypress represents characters, not special keys 52 | var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], 53 | character = String.fromCharCode( event.which ).toLowerCase(), 54 | key, modif = "", possible = {}; 55 | 56 | // check combinations (alt|ctrl|shift+anything) 57 | if ( event.altKey && special !== "alt" ) { 58 | modif += "alt+"; 59 | } 60 | 61 | if ( event.ctrlKey && special !== "ctrl" ) { 62 | modif += "ctrl+"; 63 | } 64 | 65 | // TODO: Need to make sure this works consistently across platforms 66 | if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { 67 | modif += "meta+"; 68 | } 69 | 70 | if ( event.shiftKey && special !== "shift" ) { 71 | modif += "shift+"; 72 | } 73 | 74 | if ( special ) { 75 | possible[ modif + special ] = true; 76 | 77 | } else { 78 | possible[ modif + character ] = true; 79 | possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; 80 | 81 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" 82 | if ( modif === "shift+" ) { 83 | possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; 84 | } 85 | } 86 | 87 | for ( var i = 0, l = keys.length; i < l; i++ ) { 88 | if ( possible[ keys[i] ] ) { 89 | return origHandler.apply( this, arguments ); 90 | } 91 | } 92 | }; 93 | } 94 | 95 | jQuery.each([ "keydown", "keyup", "keypress" ], function() { 96 | jQuery.event.special[ this ] = { add: keyHandler }; 97 | }); 98 | 99 | })( jQuery ); -------------------------------------------------------------------------------- /test/remote.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | http = require('http'), 3 | remote = require('../lib/remote'), 4 | nlconfig = require('../lib/config').disableServer(), 5 | HttpServer = require('../lib/http').HttpServer, 6 | Cluster = remote.Cluster; 7 | 8 | module.exports = { 9 | 'basic end-to-end cluster test': function(beforeExit) { 10 | var testTimeout, cluster, 11 | masterSetupCalled, slaveSetupCalled = [], slaveFunCalled = [], 12 | master = new HttpServer().start(9030), 13 | slave1 = new HttpServer().start(9031), 14 | slave2 = new HttpServer().start(9032), 15 | stopAll = function() { 16 | cluster.on('end', function() { 17 | master.stop(); 18 | slave1.stop(); 19 | slave2.stop(); 20 | }); 21 | cluster.end(); 22 | }; 23 | 24 | remote.installRemoteHandler(master); 25 | remote.installRemoteHandler(slave1); 26 | remote.installRemoteHandler(slave2); 27 | 28 | cluster = new Cluster({ 29 | master: { 30 | setup: function(slaves) { 31 | assert.ok(slaves); 32 | masterSetupCalled = true; 33 | }, 34 | slaveSetupCalled: function(slaves, slaveId) { 35 | assert.ok(slaves); 36 | assert.ok(slaveId); 37 | slaveSetupCalled.push(slaveId); 38 | }, 39 | slaveFunCalled: function(slaves, slaveId, data) { 40 | assert.ok(slaves); 41 | assert.ok(slaveId); 42 | assert.equal(data, 'data for master'); 43 | slaveFunCalled.push(slaveId); 44 | }, 45 | }, 46 | slaves: { 47 | hosts: ['localhost:9031', 'localhost:9032'], 48 | setup: function(master) { 49 | this.assert = require('assert'); 50 | this.assert.ok(master); 51 | master.slaveSetupCalled(); 52 | }, 53 | slaveFun: function(master, data) { 54 | this.assert.ok(master); 55 | this.assert.equal(data, 'data for slaves'); 56 | master.slaveFunCalled('data for master'); 57 | } 58 | }, 59 | pingInterval: 250, 60 | server: master 61 | }); 62 | 63 | cluster.on('init', function() { 64 | cluster.on('start', function() { 65 | cluster.slaveFun('data for slaves'); 66 | }); 67 | cluster.start(); 68 | }); 69 | 70 | testTimeout = setTimeout(stopAll, 500); 71 | 72 | beforeExit(function() { 73 | assert.ok(masterSetupCalled); 74 | assert.equal(slaveSetupCalled.length, 2); 75 | assert.ok(slaveSetupCalled.indexOf('localhost:9031') > -1); 76 | assert.ok(slaveSetupCalled.indexOf('localhost:9032') > -1); 77 | assert.equal(slaveFunCalled.length, 2); 78 | assert.ok(slaveFunCalled.indexOf('localhost:9031') > -1); 79 | assert.ok(slaveFunCalled.indexOf('localhost:9032') > -1); 80 | }); 81 | }, 82 | }; -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | ## v0.4.0 (HEAD) ## 2 | 3 | Compatible with node v0.4.x 4 | 5 | Features: 6 | 7 | * Add graphJmx() and graphProcess(); deprecate spawnAndMonitor(). These provide an easy way to graph JMX attributes as well as output from external processes, such as iostat. 8 | * More readable date strings are used in log files names 9 | * rps is a separate stat from result codes 10 | * Test Results page timestamp does not auto-update on open 11 | * X-axes now use real timestamps rather than minutes since test start 12 | 13 | ## v0.3.0 (2011/06/16) ## 14 | 15 | Compatible with node v0.3.x 16 | 17 | Features: 18 | 19 | * Add /console/console.html, a jQuery based UI for connecting to multiple nodeload instances simultaneously 20 | * jmxstat/jmxstat.jar allows command line polling of JMX attributes. Combined with reporting.spawnAndMonitor(), Java processes can be monitored during load tests. 21 | * Add 'header-code' statistic which counts number of responses with different values for a given header. For instance, this can be used to graph cache misses/hits from Squid responses using the X-Cache header. 22 | 23 | Bug Fixes: 24 | 25 | * config: Add 'nodeload/config' module for configuring global parameters 26 | * multiloop: polling time for next change in load or user profiles was always 1 second 27 | * stats: Fix one-off error in Histogram.percentile wouldn't return the greatest number if it is greater than the number of buckets (i.e. in extra[]). Fix Uniques.clear() to actually reset count. 28 | * nl.js: #issue/5: nl.js discarded URL query string and hash 29 | 30 | ## v0.2.0 (2010/12/01) ## 31 | 32 | This release is a substantial, non-backwards-compatible rewrite of nodeload. The major features are: 33 | 34 | * [npm](http://npmjs.org/) compatibility 35 | * Independently usable modules: loop, stats, monitoring, http, reporting, and remote 36 | * Addition of load and user profiles 37 | 38 | Specific changes to note are: 39 | 40 | * npm should be used to build the source 41 | 42 | [~/nodeload]> curl http://npmjs.org/install.sh | sh # install npm if not already installed 43 | [~/nodeload]> npm link 44 | 45 | * `nodeload` is renamed to `nl` and `nodeloadlib` to `nodeload`. 46 | 47 | * addTest() / addRamp() / runTest() is replaced by run(): 48 | 49 | var nl = require('nodeload'); 50 | var loadtest = nl.run({ ... test specications ... }, ...); 51 | 52 | * remoteTest() / remoteStart() is replaced by LoadTestCluster.run: 53 | 54 | var nl = require('nodeload'); 55 | var cluster = new nl.LoadTestCluster(master:port, [slaves:port, ...]); 56 | cluster.run({ ... test specifications ...}); 57 | 58 | * Callbacks and most of the globals (except `HTTP_SERVER` and `REPORT_MANAGER`) have been removed. Instead EventEmitters are used throughout. For example, run() returns an instance of LoadTest, which emits 'update' and 'end' events, replacing the need for both `TEST_MONITOR` and the startTests() callback parameter. 59 | 60 | * Scheduler has been replaced by MultiLoop, which also understands load & concurrency profiles. 61 | 62 | * Statistics tracking works through event handlers now rather than by wrapping the loop function. See monitoring/monitor.js. 63 | 64 | ## v0.100.0 (2010/10/06) ## 65 | 66 | This release adds nodeloadlib and moves to Dygraph for charting. 67 | 68 | ## v0.1.0 to v0.1.2 (2010/02/27) ## 69 | 70 | Initial releases of nodeload. Tags correspond to node compatible versions. To find a version of node that's compatible with a tag release do `git show `. 71 | 72 | For example: git show v0.1.1 73 | -------------------------------------------------------------------------------- /examples/riaktest.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Instructions: 4 | // 5 | // 1. Get node (http://nodejs.org/#download) 6 | // 2. git clone http://github.com/benschmaus/nodeload.git 7 | // 3. examples/riaktest.ex.js 8 | // 9 | // This example performs a micro-benchmark of Riak (http://riak.basho.com/), a key-value store, 10 | // running on localhost:8098/riak. First, it first loads 2000 objects into the store as quickly 11 | // as possible. Then, it performs a 90% read + 10% update test at total request rate of 300 rps. 12 | // From minutes 5-8, the read load is increased by 100 rps. The test runs for 10 minutes. 13 | 14 | var sys = require('sys'), 15 | nl = require('../nodeload'); 16 | 17 | function riakUpdate(loopFun, client, url, body) { 18 | var req = client.request('GET', url, { 'host': 'localhost' }); 19 | req.on('response', function(res) { 20 | if (res.statusCode !== 200 && res.statusCode !== 404) { 21 | loopFun({req: req, res: res}); 22 | } else { 23 | var headers = { 24 | 'host': 'localhost', 25 | 'content-type': 'text/plain', 26 | 'x-riak-client-id': 'bmxpYg==' 27 | }; 28 | if (res.headers['x-riak-vclock']) { 29 | headers['x-riak-vclock'] = res.headers['x-riak-vclock']; 30 | } 31 | 32 | req = client.request('PUT', url, headers); 33 | req.on('response', function(res) { 34 | loopFun({req: req, res: res}); 35 | }); 36 | req.end(body); 37 | } 38 | }); 39 | req.end(); 40 | } 41 | 42 | var i=0; 43 | var loadData = nl.run({ 44 | name: "Load Data", 45 | host: 'localhost', 46 | port: 8098, 47 | numUsers: 20, 48 | numRequests: 2000, 49 | timeLimit: Infinity, 50 | stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [204], log: 'http-errors.log' }], 51 | requestLoop: function(loopFun, client) { 52 | riakUpdate(loopFun, client, '/riak/b/o' + i++, 'original value'); 53 | } 54 | }); 55 | 56 | loadData.on('end', function() { 57 | console.log("Running read + update test."); 58 | 59 | var reads = { 60 | name: "Read", 61 | host: 'localhost', 62 | port: 8098, 63 | numUsers: 30, 64 | loadProfile: [[0,0],[20,270],[300,270],[480,370],[590,400],[599,0]], // Ramp up to 270, then up to 370, then down to 0 65 | timeLimit: 600, 66 | stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' }], 67 | requestGenerator: function(client) { 68 | var url = '/riak/b/o' + Math.floor(Math.random()*8000); 69 | return client.request('GET', url, { 'host': 'localhost' }); 70 | } 71 | }, 72 | writes = { 73 | name: "Write", 74 | host: 'localhost', 75 | port: 8098, 76 | numUsers: 5, 77 | timeLimit: 600, 78 | targetRps: 30, 79 | reportInterval: 2, 80 | stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [204], log: 'http-errors.log' }], 81 | requestLoop: function(loopFun, client) { 82 | var url = '/riak/b/o' + Math.floor(Math.random()*8000); 83 | riakUpdate(loopFun, client, url, 'updated value'); 84 | } 85 | }; 86 | 87 | nl.run(reads, writes); 88 | }); -------------------------------------------------------------------------------- /lib/reporting/external.js: -------------------------------------------------------------------------------- 1 | /*jslint forin:true */ 2 | 3 | var BUILD_AS_SINGLE_FILE; 4 | if (!BUILD_AS_SINGLE_FILE) { 5 | var child_process = require('child_process'); 6 | var REPORT_MANAGER = require('./reportmanager').REPORT_MANAGER; 7 | var util = require('../util'); 8 | var path = require('path'); 9 | } 10 | 11 | var monitorProcess; 12 | 13 | var monitorJmx = exports.monitorJmx = function(options) { 14 | // Verify that java & jmxstat jar can be found. Search for jmxstat/jmxstat.jar located next to the 15 | // current module or a parent module that included it. 16 | var m = module; 17 | var jmxstat, found = false; 18 | while (m && !found) { 19 | jmxstat = path.join(path.dirname(m.filename), 'jmxstat/jmxstat.jar'); 20 | found = path.existsSync(jmxstat); 21 | m = m.parent; 22 | } 23 | if (!found) { 24 | throw new Error('jmxstat/jmxstat.jar not found.'); 25 | } 26 | 27 | // Build command line args, output regex, and field labels 28 | var regex = '\\d{2}:\\d{2}:\\d{2}', columns = [], mbeans = []; 29 | for (var mbean in options.mbeans) { 30 | regex += '\\t([^\\t]*)'; 31 | columns.push(mbean); 32 | mbeans.push(options.mbeans[mbean]); 33 | } 34 | 35 | // Start jmxstat 36 | var interval = options.interval || ''; 37 | return monitorProcess({ 38 | command: 'java -jar ' + jmxstat + ' ' + options.host + ' ' + mbeans.join(' ') + ' ' + interval, 39 | columns: columns, 40 | regex: regex, 41 | dataFormatter: options.dataFormatter 42 | }); 43 | }; 44 | var graphJmx = exports.graphJmx = function(options) { 45 | var report = REPORT_MANAGER.getReport(options.reportName || options.host || 'Monitor'), 46 | graph = report.getChart(options.chartName || 'JMX'), 47 | jmx = monitorJmx(options); 48 | 49 | jmx.on('data', function (data) { graph.put(data); }); 50 | return jmx; 51 | }; 52 | 53 | /** Spawn a child process, extract data using a regex, and graph the results on the summary report. 54 | Returns a standard ChildProcess object. 55 | */ 56 | var monitorProcess = exports.monitorProcess = function(options) { 57 | var delimiter = options.delimiter || ' +', 58 | columns = options.columns || [], 59 | fieldRegex = columns.map(function() { return '([0-9.e+-]+)'; }).join(delimiter), // e.g. ([0-9.e+-]*) +([0-9.e+-]*) +... 60 | regex = options.regex || ('^ *' + fieldRegex), 61 | splitIdx = columns.indexOf(options.splitBy) + 1; 62 | 63 | var valuesToNumber = function(o) { 64 | for (var i in o) { 65 | o[i] = Number(Number(o[i]).toFixed(2)); 66 | } 67 | return o; 68 | }; 69 | 70 | var format = options.dataFormatter || valuesToNumber; 71 | var proc = child_process.spawn('/bin/bash', ['-c', options.command], options.spawnOptions), 72 | lr = new util.LineReader(proc.stdout); 73 | 74 | lr.on('data', function (line) { 75 | var vals = line.match(regex); 76 | if (vals) { 77 | var obj = {}, prefix = ''; 78 | if (splitIdx > 0 && vals[splitIdx]) { 79 | prefix = vals[splitIdx] + ' '; 80 | } 81 | for (var i = 1; i < vals.length; i++) { 82 | if (columns[i-1]) { 83 | obj[prefix + columns[i-1]] = vals[i]; 84 | } 85 | } 86 | obj = format(obj); 87 | if (obj) { proc.emit('data', obj); } 88 | } 89 | }); 90 | 91 | return proc; 92 | }; 93 | var graphProcess = exports.graphProcess = function(options) { 94 | var report = REPORT_MANAGER.getReport(options.reportName || 'Monitor'), 95 | graph = report.getChart(options.chartName || options.command), 96 | proc = monitorProcess(options); 97 | 98 | proc.on('data', function (data) { graph.put(data); }); 99 | return proc; 100 | }; -------------------------------------------------------------------------------- /test/reporting.test.js: -------------------------------------------------------------------------------- 1 | /*jslint sub:true */ 2 | 3 | var assert = require('assert'), 4 | nlconfig = require('../lib/config').disableServer(), 5 | reporting = require('../lib/reporting'), 6 | monitoring = require('../lib/monitoring'), 7 | REPORT_MANAGER = reporting.REPORT_MANAGER; 8 | 9 | REPORT_MANAGER.refreshIntervalMs = 500; 10 | REPORT_MANAGER.setLogFile('.reporting.test-output.html'); 11 | setTimeout(function() { REPORT_MANAGER.setLoggingEnabled(false); }, 1000); 12 | 13 | function mockConnection(callback) { 14 | var conn = { 15 | operation: function(opcallback) { 16 | setTimeout(function() { opcallback(); }, 25); 17 | } 18 | }; 19 | setTimeout(function() { callback(conn); }, 75); 20 | } 21 | 22 | module.exports = { 23 | 'example: add a chart to test summary webpage': function(beforeExit) { 24 | var report = REPORT_MANAGER.addReport('My Report'), 25 | chart1 = report.getChart('Chart 1'), 26 | chart2 = report.getChart('Chart 2'); 27 | 28 | chart1.put({'line 1': 1, 'line 2': -1}); 29 | chart1.put({'line 1': 2, 'line 2': -2}); 30 | chart1.put({'line 1': 3, 'line 2': -3}); 31 | 32 | chart2.put({'line 1': 10, 'line 2': -10}); 33 | chart2.put({'line 1': 11, 'line 2': -11}); 34 | chart2.put({'line 1': 12, 'line 2': -12}); 35 | 36 | report.summary = { 37 | "statistic 1" : 500, 38 | "statistic 2" : 'text', 39 | }; 40 | 41 | var html = REPORT_MANAGER.getHtml(); 42 | assert.isNotNull(html.match('name":"'+chart1.name)); 43 | assert.isNotNull(html.match('name":"'+chart2.name)); 44 | assert.isNotNull(html.match('summary":')); 45 | }, 46 | 'example: update reports from Monitor and MonitorGroup stats': function(beforeExit) { 47 | var m = new monitoring.MonitorGroup('runtime') 48 | .initMonitors('transaction', 'operation'), 49 | f = function() { 50 | var trmon = m.start('transaction'); 51 | mockConnection(function(conn) { 52 | var opmon = m.start('operation'); 53 | conn.operation(function() { 54 | opmon.end(); 55 | trmon.end(); 56 | }); 57 | }); 58 | }; 59 | 60 | m.updateInterval = 200; 61 | 62 | REPORT_MANAGER.addReport('All Monitors').updateFromMonitorGroup(m); 63 | REPORT_MANAGER.addReport('Transaction').updateFromMonitor(m.monitors['transaction']); 64 | REPORT_MANAGER.addReport('Operation').updateFromMonitor(m.monitors['operation']); 65 | 66 | for (var i = 1; i <= 10; i++) { 67 | setTimeout(f, i*50); 68 | } 69 | 70 | // Disable 'update' events after 500ms so that this test can complete 71 | setTimeout(function() { m.updateInterval = 0; }, 510); 72 | 73 | beforeExit(function() { 74 | var trReport = REPORT_MANAGER.reports.filter(function(r) { return r.name === 'Transaction'; })[0]; 75 | var opReport = REPORT_MANAGER.reports.filter(function(r) { return r.name === 'Operation'; })[0]; 76 | assert.ok(trReport && (trReport.name === 'Transaction') && trReport.charts['runtime']); 77 | assert.ok(opReport && (opReport.name === 'Operation') && opReport.charts['runtime']); 78 | assert.equal(trReport.charts['runtime'].rows.length, 3); // 1+2, since first row is [[0,...]] 79 | assert.equal(opReport.charts['runtime'].rows.length, 3); 80 | assert.ok(Math.abs(trReport.charts['runtime'].rows[2][3] - 100) < 10); // third column is 'median' 81 | }); 82 | }, 83 | }; -------------------------------------------------------------------------------- /doc/monitoring.md: -------------------------------------------------------------------------------- 1 | **This document is out-of-date. See [`lib/monitoring/monitor.js`](https://github.com/benschmaus/nodeload/tree/master/lib/monitoring/monitor.js), [`lib/monitoring/monitorgroup.js`](https://github.com/benschmaus/nodeload/tree/master/lib/monitoring/monitorgroup.js), and [`lib/monitoring/collectors.js`](https://github.com/benschmaus/nodeload/tree/master/lib/monitoring/collectors.js).** 2 | 3 | ## Monitoring ## 4 | 5 | `TEST_MONITOR` is an EventEmitter that emits 'update' events at regular intervals. This allows tests to be introspected for things like statistics gathering, report generation, etc. See `monitor.js`. 6 | 7 | To set the interval between 'update' events: 8 | 9 | var nl = require('./lib/nodeloadlib').setMonitorIntervalMs(seconds) 10 | 11 | **Events:** 12 | 13 | * `TEST_MONITOR.on('test', callback(test))`: `addTest()` was called. The newly created test is passed to `callback`. 14 | * `TEST_MONITOR.on('start', callback(tests))`: `startTests()` was called. The list of tests being started is passed to `callback`. 15 | * `TEST_MONITOR.on('end', callback(tests))`: All tests finished. 16 | * `TEST_MONITOR.on('update', callback(tests))`: Emitted at regular intervals while tests are running. Default is every 2 seconds. `nodeloadlib` uses this event internally to track statistics and generate the summary webpage. 17 | * `TEST_MONITOR.on('afterUpdate', callback(tests))`: Emitted after the 'update' event. 18 | 19 | **Usage**: 20 | 21 | nl.TEST_MONITOR.on('update', function(tests) { 22 | for (var i in tests) { 23 | console.log(JSON.stringify(tests[i].stats['latency'].summary())) 24 | } 25 | }); 26 | 27 | ## HTTP-specific Monitors ## 28 | 29 | A collection of wrappers for `requestLoop` functions that record statistics for HTTP requests. These functions can be run scheduled with `SCHEDULER` or run with a `ConditionalLoop`. See `evloops.js`. 30 | 31 | **Functions:** 32 | 33 | * `monitorLatenciesLoop(latencies, fun)`: Call `fun()` and put the execution duration in `latencies`, which should be a `Histogram`. 34 | * `monitorResultsLoop(results, fun)`: Call `fun()` and put the HTTP response code in `results`, which should be a `ResultsCounter`. 35 | * `monitorByteReceivedLoop(bytesReceived, fun)`: Call `fun()` and put the number of bytes received in `bytesReceived`, usually an `Accumulator`. 36 | * `monitorConcurrencyLoop(concurrency, fun)`: Call `fun()` and put the number of "threads" currently executing it into `concurrency`, usually a `Peak`. 37 | * `monitorRateLoop(rate, fun)`: Call `fun()` and notify `rate`, which should be a `Rate`, that it was called. 38 | * `monitorHttpFailuresLoop(successCodes, fun, log)`: Call `fun()` and put the HTTP request and response into `log`, which should be a `LogFile`, for every request that does not return an HTTP status code included in the list `successCodes`. 39 | * `monitorUniqueUrlsLoop(uniqs, fun)`: Call `fun()` and put the HTTP request path into `uniqs`, which should be a `Uniques`. 40 | * `loopWrapper(fun, start, finish)`: Create a custom loop wrapper by specifying a functions to execute before and after calling `fun()`. 41 | 42 | **Usage:** 43 | 44 | All of these wrappers return a `function(loopFun, args)` which can be used by `SCHEDULER` and `ConditionalLoop`. The underlying function should have the same signature and execute an HTTP request. It must call `loopFun({req: http.ClientRequest, res: http.ClientResponse})` when it completes the request. 45 | 46 | Example: 47 | 48 | // Issue GET requests to random objects at localhost:8080/data/obj-{0-1000} for 1 minute and 49 | // track the number of unique URLs 50 | var uniq = new nl.Reportable(Uniques, 'Uniques'); 51 | var loop = nl.LoopUtils.monitorUniqueUrlsLoop(uniq, function(loopFun, client) { 52 | var req = nl.traceableRequest(client, 'GET', '/data/obj-' + Math.floor(Math.random()*1000)); 53 | req.on('response', function(res) { 54 | loopFun({req: req, res: res}); 55 | }); 56 | req.end(); 57 | }); 58 | SCHEDULER.schedule({ 59 | fun: loop, 60 | args: http.createClient(8080, 'localhost'), 61 | duration: 60 62 | }).start(function() { 63 | console.log(JSON.stringify(uniq.summary())); 64 | }); -------------------------------------------------------------------------------- /nl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Copyright (c) 2010 Benjamin Schmaus 4 | Copyright (c) 2010 Jonathan Lee 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | /*jslint sub:true */ 29 | 30 | var options = require('./lib/nl/options'); 31 | options.process(); 32 | 33 | if (!options.get('url')) { 34 | options.help(); 35 | } 36 | 37 | var nl = require('./nodeload') 38 | .quiet() 39 | .setMonitorIntervalMs(options.get('reportInterval') * 1000); 40 | 41 | function puts(text) { if (!options.get('quiet')) { console.log(text); } } 42 | function pad(str, width) { return str + (new Array(width-str.length)).join(' '); } 43 | function printItem(name, val, padLength) { 44 | if (padLength === undefined) { padLength = 40; } 45 | puts(pad(name + ':', padLength) + ' ' + val); 46 | } 47 | 48 | var testStart; 49 | var host = options.get('host'); 50 | var test = nl.run({ 51 | name: host, 52 | host: options.get('host'), 53 | port: options.get('port'), 54 | requestGenerator: options.get('requestGenerator'), 55 | method: options.get('method'), 56 | path: options.get('path'), 57 | requestData: options.get('requestData'), 58 | numUsers: options.get('numClients'), 59 | numRequests: options.get('numRequests'), 60 | timeLimit: options.get('timeLimit'), 61 | targetRps: options.get('targetRps'), 62 | stats: ['latency', 'result-codes', 'request-bytes', 'response-bytes'] 63 | }); 64 | 65 | test.on('start', function(tests) { testStart = new Date(); }); 66 | test.on('update', function(interval, stats) { 67 | puts(pad('Completed ' +stats[host]['result-codes'].length+ ' requests', 40)); 68 | }); 69 | test.on('end', function() { 70 | 71 | var stats = test.stats[host]; 72 | var elapsedSeconds = ((new Date()) - testStart)/1000; 73 | 74 | puts(''); 75 | printItem('Server', options.get('host') + ':' + options.get('port')); 76 | 77 | if (options.get('requestGeneratorModule') === undefined) { 78 | printItem('HTTP Method', options.get('method')); 79 | printItem('Document Path', options.get('path')); 80 | } else { 81 | printItem('Request Generator', options.get('requestGeneratorModule')); 82 | } 83 | 84 | printItem('Concurrency Level', options.get('numClients')); 85 | printItem('Number of requests', stats['result-codes'].length); 86 | printItem('Body bytes transferred', stats['request-bytes'].total + stats['response-bytes'].total); 87 | printItem('Elapsed time (s)', elapsedSeconds.toFixed(2)); 88 | printItem('Requests per second', (stats['result-codes'].length/elapsedSeconds).toFixed(2)); 89 | printItem('Mean time per request (ms)', stats['latency'].mean().toFixed(2)); 90 | printItem('Time per request standard deviation', stats['latency'].stddev().toFixed(2)); 91 | 92 | puts('\nPercentages of requests served within a certain time (ms)'); 93 | printItem(' Min', stats['latency'].min, 6); 94 | printItem(' Avg', stats['latency'].mean().toFixed(1), 6); 95 | printItem(' 50%', stats['latency'].percentile(0.5), 6); 96 | printItem(' 95%', stats['latency'].percentile(0.95), 6); 97 | printItem(' 99%', stats['latency'].percentile(0.99), 6); 98 | printItem(' Max', stats['latency'].max, 6); 99 | 100 | process.exit(0); 101 | }); 102 | test.start(); -------------------------------------------------------------------------------- /lib/remote/slave.js: -------------------------------------------------------------------------------- 1 | /*jslint sub: true */ 2 | var BUILD_AS_SINGLE_FILE; 3 | if (!BUILD_AS_SINGLE_FILE) { 4 | var url = require('url'); 5 | var util = require('../util'); 6 | var EventEmitter = require('events').EventEmitter; 7 | var EndpointClient = require('./endpointclient').EndpointClient; 8 | var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; 9 | } 10 | 11 | /** Slave represents a remote slave instance from the master server's perspective. It holds the slave 12 | method defintions, defined by calling defineMethod(), as Javascript strings. When start() is called, 13 | the definitions are POSTed to /remote on the remote instance which causes the instance to create a new 14 | endpoint with those methods. Subsequent calls to Slave simply POST parameters to the remote instance: 15 | 16 | slave = new Slave(...); 17 | slave.defineMethod('slave_method_1', function(master, name) { return 'hello ' + name }); 18 | slave.start(); 19 | slave.on('start', function() { 20 | slave.method_1('tom'); 21 | slave.end(); 22 | }); 23 | 24 | will POST the definition of method_1 to /remote, followed by ['tom'] to /remote/.../method_1. 25 | 26 | Slave emits the following events: 27 | - 'slaveError', error: The underlying HTTP connection returned an error. 28 | - 'start': The remote instance accepted the slave definition and slave methods can now be called. 29 | - 'end': The slave endpoint has been removed from the remote instance. 30 | 31 | Slave.state can be: 32 | - 'initialized': The slave is ready to be started. 33 | - 'connecting': The slave definition is being sent to the remote instance. 34 | - 'started': The remote instance is running and methods defined through defineMethod can be called. */ 35 | var Slave = exports.Slave = function Slave(id, host, port, masterEndpoint, pingInterval) { 36 | EventEmitter.call(this); 37 | this.id = id; 38 | this.client = new EndpointClient(host, port); 39 | this.client.on('error', this.emit.bind(this, 'slaveError')); 40 | this.masterEndpoint = masterEndpoint; 41 | this.pingInterval = pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; 42 | this.methodDefs = []; 43 | this.state = 'initialized'; 44 | }; 45 | util.inherits(Slave, EventEmitter); 46 | /** POST method definitions and information about this instance (the slave's master) to /remote */ 47 | Slave.prototype.start = function() { 48 | if (this.masterEndpoint && this.masterEndpoint.state !== 'started') { 49 | throw new Error('Slave must be started after its Master.'); 50 | } 51 | 52 | var self = this, 53 | masterUrl = self.masterEndpoint ? self.masterEndpoint.url : null, 54 | masterMethods = self.masterEndpoint ? self.masterEndpoint.methodNames : [], 55 | req = self.client.rawRequest('POST', '/remote'); 56 | 57 | req.end(JSON.stringify({ 58 | id: self.id, 59 | master: masterUrl, 60 | masterMethods: masterMethods, 61 | slaveMethods: self.methodDefs, 62 | pingInterval: self.pingInterval 63 | })); 64 | req.on('response', function(res) { 65 | if (!res.headers['location']) { 66 | self.emit('error', new Error('Remote slave does not have proper /remote handler.')); 67 | } 68 | self.client.basepath = url.parse(res.headers['location']).pathname; 69 | self.state = 'started'; 70 | self.emit('start'); 71 | }); 72 | 73 | self.state = 'connecting'; 74 | }; 75 | /** Stop this slave by sending a DELETE request to terminate the slave's endpoint. */ 76 | Slave.prototype.end = function() { 77 | var self = this, 78 | req = self.client.rawRequest('DELETE', self.client.basepath), 79 | done = function() { 80 | self.client.basepath = ''; 81 | self.state = 'initialized'; 82 | self.emit('end'); 83 | }; 84 | 85 | self.client.once('error', function(e) { 86 | self.emit('slaveError', e); 87 | done(); 88 | }); 89 | req.on('response', function(res) { 90 | if (res.statusCode !== 204) { 91 | self.emit('slaveError', new Error('Error stopping slave.'), res); 92 | } 93 | done(); 94 | }); 95 | req.end(); 96 | }; 97 | /** Define a method that will be sent to the slave instance */ 98 | Slave.prototype.defineMethod = function(name, fun) { 99 | var self = this; 100 | self.client.defineMethod(name, fun); 101 | self[name] = function() { return self.client[name].apply(self.client, arguments); }; 102 | self.methodDefs.push({name: name, fun: fun.toString()}); 103 | }; 104 | -------------------------------------------------------------------------------- /lib/monitoring/monitorgroup.js: -------------------------------------------------------------------------------- 1 | // ----------------- 2 | // MonitorGroup 3 | // ----------------- 4 | var BUILD_AS_SINGLE_FILE; 5 | if (!BUILD_AS_SINGLE_FILE) { 6 | var util = require('../util'); 7 | var Monitor = require('./monitor').Monitor; 8 | var StatsLogger = require('./statslogger').StatsLogger; 9 | var EventEmitter = require('events').EventEmitter; 10 | } 11 | 12 | /** MonitorGroup represents a group of Monitor instances. Calling MonitorGroup('runtime').start('myfunction') 13 | is equivalent to creating a Monitor('runtime') for myfunction and and calling start(). MonitorGroup can 14 | also emit regular 'update' events as well as log the statistics from the interval to disk. 15 | 16 | @param arguments contain names of the statistics to track. Register more statistics by extending 17 | Monitor.StatsCollectors. */ 18 | var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { 19 | EventEmitter.call(this); 20 | util.PeriodicUpdater.call(this); 21 | 22 | var summarizeStats = function() { 23 | var summary = {ts: new Date()}; 24 | util.forEach(this, function(monitorName, stats) { 25 | summary[monitorName] = {}; 26 | util.forEach(stats, function(statName, stat) { 27 | summary[monitorName][statName] = stat.summary(); 28 | }); 29 | }); 30 | return summary; 31 | }; 32 | 33 | this.statsNames = (statsNames instanceof Array) ? statsNames : Array.prototype.slice.call(arguments); 34 | this.monitors = {}; 35 | this.stats = {}; 36 | this.interval = {}; 37 | 38 | Object.defineProperty(this.stats, 'summary', { 39 | enumerable: false, 40 | value: summarizeStats 41 | }); 42 | Object.defineProperty(this.interval, 'summary', { 43 | enumerable: false, 44 | value: summarizeStats 45 | }); 46 | }; 47 | 48 | util.inherits(MonitorGroup, EventEmitter); 49 | 50 | /** Pre-initialize monitors with the given names. This allows construction overhead to take place all at 51 | once if desired. */ 52 | MonitorGroup.prototype.initMonitors = function(monitorNames) { 53 | var self = this; 54 | monitorNames = (monitorNames instanceof Array) ? monitorNames : Array.prototype.slice.call(arguments); 55 | monitorNames.forEach(function(name) { 56 | self.monitors[name] = new Monitor(self.statsNames); 57 | self.stats[name] = self.monitors[name].stats; 58 | self.interval[name] = self.monitors[name].interval; 59 | }); 60 | return self; 61 | }; 62 | 63 | /** Call .start() for the named monitor */ 64 | MonitorGroup.prototype.start = function(monitorName, args) { 65 | monitorName = monitorName || ''; 66 | if (!this.monitors[monitorName]) { 67 | this.initMonitors([monitorName]); 68 | } 69 | return this.monitors[monitorName].start(args); 70 | }; 71 | 72 | /** Like Monitor.monitorObjects() except each object's 'start' event should include the monitor name as 73 | its first argument. See monitoring.test.js for an example. */ 74 | MonitorGroup.prototype.monitorObjects = function(objs, startEvent, endEvent) { 75 | var self = this, ctxs = {}; 76 | 77 | if (!(objs instanceof Array)) { 78 | objs = util.argarray(arguments); 79 | startEvent = endEvent = null; 80 | } 81 | 82 | startEvent = startEvent || 'start'; 83 | endEvent = endEvent || 'end'; 84 | 85 | objs.forEach(function(o) { 86 | o.on(startEvent, function(monitorName, args) { 87 | ctxs[monitorName] = self.start(monitorName, args); 88 | }); 89 | o.on(endEvent, function(monitorName, result) { 90 | if (ctxs[monitorName]) { ctxs[monitorName].end(result); } 91 | }); 92 | }); 93 | return self; 94 | }; 95 | 96 | /** Set the file name or stats.js#LogFile object that statistics are logged to; null for default */ 97 | MonitorGroup.prototype.setLogFile = function(logNameOrObject) { 98 | this.logNameOrObject = logNameOrObject; 99 | }; 100 | 101 | /** Log statistics each time an 'update' event is emitted */ 102 | MonitorGroup.prototype.setLoggingEnabled = function(enabled) { 103 | if (enabled) { 104 | this.logger = this.logger || new StatsLogger(this, this.logNameOrObject).start(); 105 | } else if (this.logger) { 106 | this.logger.stop(); 107 | this.logger = null; 108 | } 109 | return this; 110 | }; 111 | 112 | /** Emit the update event and reset the statistics for the next window */ 113 | MonitorGroup.prototype.update = function() { 114 | this.emit('update', this.interval, this.stats); 115 | util.forEach(this.monitors, function (name, m) { m.update(); }); 116 | }; -------------------------------------------------------------------------------- /lib/loop/userloop.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------- 2 | // UserLoop 3 | // ----------------------------------------- 4 | // 5 | var util = require('../util'); 6 | var EventEmitter = require('events').EventEmitter; 7 | var Loop = require('./loop').Loop; 8 | var Program = require('../user/program').Program; 9 | var _ = require('underscore'); 10 | 11 | var USER_LOOP_OPTIONS = exports.USER_LOOP_OPTIONS = { 12 | program: undefined, 13 | duration: Infinity, 14 | numberOfTimes: Infinity, 15 | concurrency: 1, 16 | concurrencyProfile: undefined 17 | }; 18 | 19 | var UserLoop = exports.UserLoop = function UserLoop(programOrSpec, args, conditions) { 20 | console.log('new user coming online'); 21 | EventEmitter.call(this); 22 | 23 | if (_(programOrSpec).isString()) { 24 | programOrSpec = require(programOrSpec); 25 | } 26 | 27 | 28 | this.id = util.uid(); 29 | this.programFn = programOrSpec; 30 | this.programArgs = args || {}; 31 | this.restartProgram(); 32 | this.conditions = conditions || []; 33 | this.running = false; 34 | }; 35 | 36 | util.inherits(UserLoop, Loop); 37 | 38 | /** Start executing this.fun until any condition in this.conditions 39 | returns false. When the loop completes the 'end' event is emitted. */ 40 | UserLoop.prototype.start = function() { 41 | var self = this, 42 | startLoop = function() { 43 | self.emit('start'); 44 | console.log("Starting main program: " + self.programFn.program.name); 45 | self.loop_(self.headProgram); 46 | }; 47 | 48 | if (self.running) { return; } 49 | self.running = true; 50 | process.nextTick(startLoop); 51 | return this; 52 | }; 53 | 54 | UserLoop.prototype.stop = function() { 55 | this.running = false; 56 | }; 57 | 58 | UserLoop.prototype.wireInDependentPrograms = function(parentProgram, dependents) { 59 | var self = this; 60 | _(dependents).each(function(dependent) { 61 | parentProgram.on(dependent.onEvent, function(eventArgs) { 62 | console.log('Starting dependent program: ' + dependent.program.name); 63 | 64 | // Creates and starts the child program 65 | var startChild = function() { 66 | var child = new Program(dependent.program.program, _({}).extend(self.programArgs, eventArgs)); 67 | if (dependent.dependents) { 68 | self.wireInDependentPrograms(child, dependent.dependents); 69 | } 70 | 71 | self.loop_(child); 72 | }; 73 | 74 | if (!dependent.cardinality) 75 | dependent.cardinality = 1; 76 | 77 | // Support only triggering dependent programs some of the time 78 | if (dependent.cardinality < 1) { 79 | if (Number.random() < dependent.cardinality) { 80 | startChild(); 81 | } 82 | } else { 83 | _(dependent.cardinality).times(function() { 84 | startChild(); 85 | }); 86 | } 87 | 88 | }); 89 | }); 90 | }; 91 | 92 | UserLoop.prototype.restartProgram = function() { 93 | if (_(this.programFn).isFunction()) { 94 | this.headProgram = new Program(this.programFn, this.programArgs); 95 | } else { 96 | this.headProgram = new Program(this.programFn.program, this.programArgs); 97 | this.wireInDependentPrograms(this.headProgram, this.programFn.dependents); 98 | } 99 | 100 | }; 101 | 102 | /** Checks conditions and schedules the next loop iteration. 'startiteration' is emitted before each 103 | iteration and 'enditeration' is emitted after. */ 104 | UserLoop.prototype.loop_ = function(program) { 105 | var self = this, result, active, 106 | callfun = function() { 107 | result = null; active = true; 108 | if (program.finished()) { 109 | // TODO: How's this gonna work with dependent programs? 110 | // self.restartProgram(); 111 | return; 112 | } 113 | 114 | var isRequest = program.pendingIsRequest(); 115 | if (isRequest) { 116 | self.emit('startiteration'); 117 | } 118 | program.next(function(res) { 119 | if (isRequest) { 120 | self.emit('enditeration', res); 121 | } 122 | self.loop_(program); 123 | }); 124 | }; 125 | 126 | if (self.checkConditions_()) { 127 | process.nextTick(callfun); 128 | } else { 129 | self.running = false; 130 | self.emit('end'); 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // HTTP Server 3 | // ------------------------------------ 4 | // 5 | // This file defines HttpServer and the singleton HTTP_SERVER. 6 | // 7 | // This file defines a generic HTTP server that serves static files and that can be configured 8 | // with new routes. It also starts the nodeload HTTP server unless require('nodeload/config') 9 | // .disableServer() was called. 10 | // 11 | var BUILD_AS_SINGLE_FILE; 12 | if (!BUILD_AS_SINGLE_FILE) { 13 | var config = require('./config'); 14 | var http = require('http'); 15 | var fs = require('fs'); 16 | var util = require('./util'); 17 | var qputs = util.qputs; 18 | var EventEmitter = require('events').EventEmitter; 19 | var NODELOAD_CONFIG = config.NODELOAD_CONFIG; 20 | } 21 | 22 | /** By default, HttpServer knows how to return static files from the current directory. Add new route 23 | regexs using HttpServer.on(). */ 24 | var HttpServer = exports.HttpServer = function HttpServer() { 25 | this.routes = []; 26 | this.running = false; 27 | }; 28 | util.inherits(HttpServer, EventEmitter); 29 | /** Start the server listening on the given port */ 30 | HttpServer.prototype.start = function(port, hostname) { 31 | if (this.running) { return; } 32 | this.running = true; 33 | 34 | var self = this; 35 | port = port || 8000; 36 | self.hostname = hostname || 'localhost'; 37 | self.port = port; 38 | self.connections = []; 39 | 40 | self.server = http.createServer(function(req, res) { self.route_(req, res); }); 41 | self.server.on('connection', function(c) { 42 | // We need to track incoming connections, beause Server.close() won't terminate active 43 | // connections by default. 44 | c.on('close', function() { 45 | var idx = self.connections.indexOf(c); 46 | if (idx !== -1) { 47 | self.connections.splice(idx, 1); 48 | } 49 | }); 50 | self.connections.push(c); 51 | }); 52 | self.server.listen(port, hostname); 53 | 54 | self.emit('start', self.hostname, self.port); 55 | return self; 56 | }; 57 | /** Terminate the server */ 58 | HttpServer.prototype.stop = function() { 59 | if (!this.running) { return; } 60 | this.running = false; 61 | this.connections.forEach(function(c) { c.destroy(); }); 62 | this.server.close(); 63 | this.server = null; 64 | this.emit('end'); 65 | }; 66 | /** When an incoming request matches a given regex, route it to the provided handler: 67 | function(url, ServerRequest, ServerResponse) */ 68 | HttpServer.prototype.addRoute = function(regex, handler) { 69 | this.routes.unshift({regex: regex, handler: handler}); 70 | return this; 71 | }; 72 | HttpServer.prototype.removeRoute = function(regex, handler) { 73 | this.routes = this.routes.filter(function(r) { 74 | return !((regex === r.regex) && (!handler || handler === r.handler)); 75 | }); 76 | return this; 77 | }; 78 | HttpServer.prototype.route_ = function(req, res) { 79 | for (var i = 0; i < this.routes.length; i++) { 80 | if (req.url.match(this.routes[i].regex)) { 81 | this.routes[i].handler(req.url, req, res); 82 | return; 83 | } 84 | } 85 | if (req.method === 'GET') { 86 | this.serveFile_('.' + req.url, res); 87 | } else { 88 | res.writeHead(405, {"Content-Length": "0"}); 89 | res.end(); 90 | } 91 | }; 92 | HttpServer.prototype.serveFile_ = function(file, response) { 93 | fs.stat(file, function(err, stat) { 94 | if (err) { 95 | response.writeHead(404, {"Content-Type": "text/plain"}); 96 | response.write("Cannot find file: " + file); 97 | response.end(); 98 | return; 99 | } 100 | 101 | fs.readFile(file, "binary", function (err, data) { 102 | if (err) { 103 | response.writeHead(500, {"Content-Type": "text/plain"}); 104 | response.write("Error opening file " + file + ": " + err); 105 | } else { 106 | response.writeHead(200, { 'Content-Length': data.length }); 107 | response.write(data, "binary"); 108 | } 109 | response.end(); 110 | }); 111 | }); 112 | }; 113 | 114 | // ================= 115 | // Singletons 116 | // ================= 117 | 118 | /** The global HTTP server used by nodeload */ 119 | var HTTP_SERVER = exports.HTTP_SERVER = new HttpServer(); 120 | HTTP_SERVER.on('start', function(hostname, port) { 121 | qputs('Started HTTP server on ' + hostname + ':' + port + '.'); 122 | }); 123 | HTTP_SERVER.on('end', function() { 124 | qputs('Shutdown HTTP server.'); 125 | }); 126 | NODELOAD_CONFIG.on('apply', function() { 127 | if (NODELOAD_CONFIG.HTTP_ENABLED) { 128 | HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT); 129 | } 130 | }); -------------------------------------------------------------------------------- /lib/nl/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Benjamin Schmaus 3 | Copyright (c) 2010 Jonathan Lee 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | var util = require('util'); 28 | var url = require('url'); 29 | var path = require('path'); 30 | var optparse = require('optparse'); 31 | 32 | // Default options 33 | var testConfig = { 34 | url: undefined, 35 | method: 'GET', 36 | requestData: undefined, 37 | host: '', 38 | port: 80, 39 | numClients: 1, 40 | numRequests: Infinity, 41 | timeLimit: Infinity, 42 | targetRps: Infinity, 43 | path: '/', 44 | requestGenerator: undefined, 45 | reportInterval: 10, 46 | }; 47 | var switches = [ 48 | [ '-n', '--number NUMBER', 'Number of requests to make. Defaults to value of --concurrency unless a time limit is specified.' ], 49 | [ '-c', '--concurrency NUMBER', 'Concurrent number of connections. Defaults to 1.' ], 50 | [ '-t', '--time-limit NUMBER', 'Number of seconds to spend running test. No timelimit by default.' ], 51 | [ '-e', '--request-rate NUMBER', 'Target number of requests per seconds. Infinite by default' ], 52 | [ '-m', '--method STRING', 'HTTP method to use.' ], 53 | [ '-d', '--data STRING', 'Data to send along with PUT or POST request.' ], 54 | [ '-r', '--request-generator STRING', 'Path to module that exports getRequest function'], 55 | [ '-i', '--report-interval NUMBER', 'Frequency in seconds to report statistics. Default is 10.'], 56 | [ '-q', '--quiet', 'Supress display of progress count info.'], 57 | [ '-h', '--help', 'Show usage info' ], 58 | ]; 59 | 60 | var parser; 61 | 62 | var help = exports.help = function() { 63 | util.log(parser); 64 | process.exit(); 65 | }; 66 | 67 | // Create a new OptionParser. 68 | var parser = new optparse.OptionParser(switches); 69 | parser.banner = 'nodeload.js [options] :[]'; 70 | parser.on('help', function() { 71 | help(); 72 | }); 73 | 74 | parser.on(2, function (value) { 75 | if (value.search('^http://') === -1) { 76 | value = 'http://' + value; 77 | } 78 | 79 | testConfig.url = url.parse(value, false); 80 | testConfig.host = testConfig.url.hostname || testConfig.host; 81 | testConfig.port = Number(testConfig.url.port) || testConfig.port; 82 | testConfig.path = testConfig.url.pathname || testConfig.path; 83 | testConfig.path += testConfig.url.search || ''; 84 | testConfig.path += testConfig.url.hash || ''; 85 | }); 86 | 87 | parser.on( 88 | "quiet", function() { 89 | testConfig.quiet = true; 90 | } 91 | ); 92 | 93 | parser.on( 94 | "data", function(opt, value) { 95 | testConfig.requestData = value; 96 | } 97 | ); 98 | 99 | parser.on('request-generator', function(opt, value) { 100 | var moduleName = value.substring(0, value.lastIndexOf('.')); 101 | testConfig.requestGeneratorModule = value; 102 | testConfig.requestGenerator = require(moduleName).getRequest; 103 | }); 104 | 105 | parser.on('report-interval', function(opt, value) { 106 | testConfig.reportInterval = Number(value); 107 | }); 108 | 109 | parser.on('concurrency', function(opt, value) { 110 | testConfig.numClients = Number(value); 111 | }); 112 | 113 | parser.on('request-rate', function(opt, value) { 114 | testConfig.targetRps = Number(value); 115 | }); 116 | 117 | parser.on('number', function(opt, value) { 118 | testConfig.numRequests = Number(value); 119 | }); 120 | 121 | parser.on( 122 | 'time-limit', function(opt, value) { 123 | testConfig.timeLimit = Number(value); 124 | } 125 | ); 126 | 127 | parser.on('method', function(opt, value) { 128 | testConfig.method = value; 129 | }); 130 | 131 | exports.get = function(option) { 132 | return testConfig[option]; 133 | }; 134 | exports.process = function() { 135 | parser.parse(process.argv); 136 | if ((testConfig.timeLimit === undefined) && (testConfig.numRequests === undefined)) { 137 | testConfig.numRequests = testConfig.numClients; 138 | } 139 | }; 140 | 141 | -------------------------------------------------------------------------------- /examples/remote.ex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // This example uses 'iostat' to gather load average from a remote machine and graph it. To run this 4 | // example, first start nodeload on a remote machine: 5 | // 6 | // remote-machine> nodeload.js 7 | // Started HTTP server on remote-machine:8000. 8 | // 9 | // local-machine> examples/remote.ex.js remote-machine:8000 10 | // 11 | // This example expects a machine with a single disk, so iostat output looks like: 12 | // 13 | // disk0 cpu load average 14 | // KB/t tps MB/s us sy id 1m 5m 15m 15 | // 36.73 2 0.08 8 4 89 0.39 0.43 0.41 16 | // 0.00 0 0.00 10 4 86 0.39 0.43 0.41 17 | // 18 | var http = require('http'), 19 | util = require('util'), 20 | nlhttp = require('../lib/http'), 21 | remote = require('../lib/remote'), 22 | REPORT_MANAGER = require('../lib/reporting').REPORT_MANAGER, 23 | HTTP_SERVER = nlhttp.HTTP_SERVER, 24 | HttpServer = nlhttp.HttpServer, 25 | Cluster = remote.Cluster; 26 | 27 | // Parse remote host from command line arguments 28 | // 29 | var slave, remoteHost; 30 | if (process.argv.length < 3) { 31 | console.log([ 32 | 'No remote host specified, starting slave host locally. To use a separate slave machine, run:\n', 33 | '\n', 34 | ' $ examples/remote.ex.js .\n', 35 | '\n', 36 | 'To start a second host, just run nodeload.js on another machine, optionally specifying the port:\n', 37 | '\n', 38 | ' $ HTTP_PORT=8001 nodeload.js\n', 39 | ].join('')); 40 | 41 | slave = new HttpServer().start(8001); 42 | remote.installRemoteHandler(slave); 43 | remoteHost = 'localhost:8001'; 44 | } else { 45 | remoteHost = process.argv[2]; 46 | } 47 | 48 | 49 | // Initialize the HTML report 50 | var report = REPORT_MANAGER.addReport(remoteHost), 51 | cpuChart = report.getChart('CPU usage'); 52 | 53 | 54 | // Create the Cluster... 55 | // 56 | var cluster = new Cluster({ 57 | master: { 58 | sendOutput: function(slaves, slaveId, output) { 59 | util.print(output); 60 | 61 | // grab fields 4-6 from the iostat output, which assumes output looks like: 62 | // 63 | // disk0 cpu load average 64 | // KB/t tps MB/s us sy id 1m 5m 15m 65 | // 36.73 2 0.08 8 4 89 0.39 0.43 0.41 66 | // 67 | // not so portable... 68 | var parts = output.trim().split(/\s+/); 69 | if (parts.length > 5) { 70 | cpuChart.put({ 71 | user: parseFloat(parts[3]), 72 | system: parseFloat(parts[4]), 73 | idle: parseFloat(parts[5]) 74 | }); 75 | } 76 | } 77 | }, 78 | slaves: { 79 | hosts: [remoteHost], 80 | setup: function(master) { 81 | this.spawn = require("child_process").spawn; 82 | master.on('error', function(err) { 83 | console.log('Error communicating with master: ' + err.toString()); 84 | }); 85 | }, 86 | exec: function(master, cmd, params) { 87 | var self = this, 88 | child = self.spawn(cmd, params); 89 | 90 | self.state = 'running'; 91 | child.stdout.on('data', function(data) { 92 | master.sendOutput(data.toString()); 93 | }); 94 | child.on('exit', function(data) { 95 | self.state = 'done'; 96 | }); 97 | } 98 | } 99 | }); 100 | 101 | // ...and start it 102 | // 103 | console.log('Browse to http://localhost:8000 to HTML report'); 104 | console.log('Press ^C to exit.'); 105 | cluster.on('init', function() { 106 | cluster.on('start', function() { 107 | console.log('----------------- Slave Output -----------------'); 108 | cluster.exec('iostat', ['-w1']); 109 | }); 110 | cluster.on('end', function(slaves) { 111 | console.log('All slaves terminated.'); 112 | if (slave) { 113 | slave.stop(); 114 | } 115 | process.exit(0); 116 | }); 117 | cluster.on('running', function() { 118 | console.log('All slaves running'); 119 | }); 120 | cluster.on('done', function() { 121 | console.log('All slaves done. Stopping cluster...'); 122 | cluster.end(); 123 | }); 124 | cluster.on('slaveError', function(slave, err) { 125 | if (err === null) { 126 | console.log('Unresponsive slave detected: ' + slave.id); 127 | } else { 128 | console.log('Slave error from ' + slave.id + ': ' + err.toString()); 129 | if (cluster.state === 'stopping') { 130 | process.exit(1); 131 | } 132 | } 133 | }); 134 | cluster.on('slaveState', function(slave, state) { 135 | if (state === 'error') { 136 | console.log('Slave "' + slave.id + '" encountered an error.'); 137 | } 138 | }); 139 | cluster.start(); 140 | }); 141 | process.on('SIGINT', function () { 142 | cluster.end(); 143 | }); -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Statistics Manager 3 | // ------------------------------------ 4 | // 5 | // This file defines qputs, qprint, and extends the util namespace. 6 | // 7 | // Extends node.js util.js with other common functions. 8 | // 9 | var BUILD_AS_SINGLE_FILE; 10 | if (!BUILD_AS_SINGLE_FILE) { 11 | var util = require('util'); 12 | var EventEmitter = require('events').EventEmitter; 13 | var NODELOAD_CONFIG = require('./config').NODELOAD_CONFIG; 14 | } 15 | 16 | // A few common global functions so we can access them with as few keystrokes as possible 17 | // 18 | var qputs = util.qputs = function(s) { 19 | if (!NODELOAD_CONFIG.QUIET) { util.puts(s); } 20 | }; 21 | 22 | var qprint = util.qprint = function(s) { 23 | if (!NODELOAD_CONFIG.QUIET) { util.print(s); } 24 | }; 25 | 26 | 27 | // Static utility methods 28 | // 29 | util.uid = function() { 30 | exports.lastUid_ = exports.lastUid_ || 0; 31 | return exports.lastUid_++; 32 | }; 33 | util.defaults = function(obj, defaults) { 34 | for (var i in defaults) { 35 | if (obj[i] === undefined) { 36 | obj[i] = defaults[i]; 37 | } 38 | } 39 | return obj; 40 | }; 41 | util.extend = function(obj, extension) { 42 | for (var i in extension) { 43 | if (extension.hasOwnProperty(i)) { 44 | obj[i] = extension[i]; 45 | } 46 | } 47 | return obj; 48 | }; 49 | util.forEach = function(obj, f) { 50 | for (var i in obj) { 51 | if (obj.hasOwnProperty(i)) { 52 | f(i, obj[i]); 53 | } 54 | } 55 | }; 56 | util.every = function(obj, f) { 57 | for (var i in obj) { 58 | if (obj.hasOwnProperty(i)) { 59 | if (!f(i, obj[i])) { 60 | return false; 61 | } 62 | } 63 | } 64 | return true; 65 | }; 66 | util.argarray = function(args) { 67 | return Array.prototype.slice.call(args); 68 | }; 69 | util.readStream = function(stream, callback) { 70 | var data = []; 71 | stream.on('data', function(chunk) { 72 | data.push(chunk.toString()); 73 | }); 74 | stream.on('end', function() { 75 | callback(data.join('')); 76 | }); 77 | }; 78 | 79 | /** Make an object a PeriodicUpdater by adding PeriodicUpdater.call(this) to the constructor. 80 | The object will call this.update() every interval. */ 81 | util.PeriodicUpdater = function(updateIntervalMs) { 82 | var self = this, updateTimeoutId; 83 | this.__defineGetter__('updateInterval', function() { return updateIntervalMs; }); 84 | this.__defineSetter__('updateInterval', function(milliseconds) { 85 | clearInterval(updateTimeoutId); 86 | if (milliseconds > 0 && milliseconds < Infinity) { 87 | updateTimeoutId = setInterval(self.update.bind(self), milliseconds); 88 | } 89 | updateIntervalMs = milliseconds; 90 | }); 91 | this.updateInterval = updateIntervalMs; 92 | }; 93 | 94 | /** Same arguments as http.createClient. Returns an wrapped http.Client object that will reconnect when 95 | connection errors are detected. In the current implementation of http.Client (11/29/10), calls to 96 | request() fail silently after the initial 'error' event. */ 97 | util.createReconnectingClient = function() { 98 | var http = require('http'), 99 | clientArgs = arguments, events = {}, client, wrappedClient = {}, 100 | clientMethod = function(method) { 101 | return function() { return client[method].apply(client, arguments); }; 102 | }, 103 | clientGetter = function(member) { return function() { return client[member]; };}, 104 | clientSetter = function(member) { return function(val) { client[member] = val; };}, 105 | reconnect = function() { 106 | var oldclient = client; 107 | //if (oldclient) { oldclient.destroy(); } 108 | client = http.createClient.apply(http, clientArgs); 109 | client._events = util.extend(events, client._events); // EventEmitter._events stores event handlers 110 | client.emit('reconnect', oldclient); 111 | }; 112 | 113 | // Create initial http.Client 114 | reconnect(); 115 | client.on('error', function(err) { reconnect(); }); 116 | 117 | // Wrap client so implementation can be swapped out when there are connection errors 118 | for (var j in client) { 119 | if (typeof client[j] === 'function') { 120 | wrappedClient[j] = clientMethod(j); 121 | } else { 122 | wrappedClient.__defineGetter__(j, clientGetter(j)); 123 | wrappedClient.__defineSetter__(j, clientSetter(j)); 124 | } 125 | } 126 | wrappedClient.impl = client; 127 | return wrappedClient; 128 | }; 129 | 130 | /** Accepts an EventEmitter object that emits text data. LineReader buffers the text and emits a 'data' 131 | event each time a newline is encountered. For example, */ 132 | util.LineReader = function(eventEmitter, event) { 133 | EventEmitter.call(this); 134 | event = event || 'data'; 135 | 136 | var self = this, buffer = ''; 137 | 138 | var emitLine = function(buffer) { 139 | var lineEnd = buffer.indexOf("\n"); 140 | var line = (lineEnd === -1) ? buffer : buffer.substring(0, lineEnd); 141 | if (line) { self.emit('data', line); } 142 | return buffer.substring(line.length + 1, buffer.length); 143 | }; 144 | 145 | var readloop = function(data) { 146 | if (data) { buffer += data.toString(); } 147 | if (buffer.indexOf("\n") > -1) { 148 | buffer = emitLine(buffer); 149 | process.nextTick(readloop.bind(this)); 150 | } 151 | }; 152 | 153 | eventEmitter.on(event, readloop.bind(this)); 154 | } 155 | util.inherits(util.LineReader, EventEmitter); 156 | 157 | util.extend(exports, util); -------------------------------------------------------------------------------- /lib/remote/endpoint.js: -------------------------------------------------------------------------------- 1 | /*jslint sub: true */ 2 | var BUILD_AS_SINGLE_FILE; 3 | if (!BUILD_AS_SINGLE_FILE) { 4 | var url = require('url'); 5 | var util = require('../util'); 6 | var EventEmitter = require('events').EventEmitter; 7 | } 8 | 9 | /** Endpoint represents an a collection of functions that can be executed by POSTing parameters to an 10 | HTTP server. 11 | 12 | When Endpoint is started it adds the a unique route, /remote/{uid}/{method}, to server. 13 | When a POST request is received, it calls method() with the request body as it's parameters. 14 | 15 | The available methods for this endpoint are defined by calling defineMethod(...). 16 | 17 | Endpoint emits the following events: 18 | - 'start': A route has been installed on the HTTP server and setup(), if defined through defineMethod(), 19 | has been called 20 | - 'end': The route has been removed. No more defined methods will be called. 21 | 22 | Endpoint.state can be: 23 | - 'initialized': This endpoint is ready to be started. 24 | - 'started': This endpoint is listening for POST requests to dispatching to the corresponding methods 25 | */ 26 | var Endpoint = exports.Endpoint = function Endpoint(server, hostAndPort) { 27 | EventEmitter.call(this); 28 | 29 | var self = this, 30 | parts = hostAndPort ? hostAndPort.split(':') : []; 31 | 32 | self.id = util.uid(); 33 | self.server = server; 34 | self.methodNames = []; 35 | self.methods = {}; 36 | self.setStaticParams([]); 37 | self.state = 'initialized'; 38 | self.__defineGetter__('url', function() { return self.url_; }); 39 | 40 | self.hostname_ = parts[0]; 41 | self.port_ = parts[1]; 42 | self.basepath_ = '/remote/' + self.id; 43 | self.handler_ = self.handle.bind(self); 44 | }; 45 | 46 | util.inherits(Endpoint, EventEmitter); 47 | 48 | /** Set values that are passed as the initial arguments to every handler method. For example, if you: 49 | 50 | var id = 123, name = 'myobject'; 51 | endpoint.setStaticParams([id, name]); 52 | 53 | You should define methods: 54 | 55 | endpoint.defineMethod('method_1', function(id, name, arg1, arg2...) {...}); 56 | 57 | which are called by: 58 | 59 | endpoint.method_1(arg1, arg2...) 60 | 61 | */ 62 | Endpoint.prototype.setStaticParams = function(params) { 63 | this.staticParams_ = params instanceof Array ? params : [params]; 64 | }; 65 | 66 | /** Define a method that can be executed by POSTing to /basepath/method-name. For example: 67 | 68 | endpoint.defineMethod('method_1', function(data) { return data; }); 69 | 70 | then POSTing '[123]' to /{basepath}/method_1 will respond with a message with body 123. 71 | 72 | */ 73 | Endpoint.prototype.defineMethod = function(name, fun) { 74 | this.methodNames.push(name); 75 | this.methods[name] = fun; 76 | }; 77 | 78 | /** Start responding to requests to this endpoint by adding the proper route to the HTTP server*/ 79 | Endpoint.prototype.start = function() { 80 | if (this.state !== 'initialized') { return; } 81 | this.url_ = url.format({ 82 | protocol: 'http', 83 | hostname: this.hostname_ || this.server.hostname, 84 | port: this.port_ || this.server.port, 85 | pathname: this.basepath_ 86 | }); 87 | this.route_ = '^' + this.basepath_ + '/?'; 88 | this.server.addRoute(this.route_, this.handler_); 89 | this.context = {}; 90 | if (this.methods['setup']) { 91 | this.methods['setup'].apply(this.context, this.staticParams_); 92 | } 93 | this.state = 'started'; 94 | this.emit('start'); 95 | }; 96 | 97 | /** Remove the HTTP server route and stop responding to requests */ 98 | Endpoint.prototype.end = function() { 99 | if (this.state !== 'started') { return; } 100 | this.server.removeRoute(this.route_, this.handler_); 101 | this.state = 'initialized'; 102 | this.emit('end'); 103 | }; 104 | 105 | /** The main HTTP request handler. On DELETE /{basepath}, it will self-destruct this endpoint. POST 106 | requests are routed to the function set by defineMethod(), applying the HTTP request body as parameters, 107 | and sending return value back in the HTTP response. */ 108 | Endpoint.prototype.handle = function(path, req, res) { 109 | var self = this; 110 | if (path === self.basepath_) { 111 | if (req.method === 'DELETE') { 112 | self.end(); 113 | res.writeHead(204, {'Content-Length': 0}); 114 | res.end(); 115 | } else { 116 | res.writeHead(405); 117 | res.end(); 118 | } 119 | } else if (req.method === 'POST') { 120 | var method = path.slice(this.basepath_.length+1); 121 | if (self.methods[method]) { 122 | util.readStream(req, function(params) { 123 | var status = 200, ret; 124 | 125 | try { 126 | params = JSON.parse(params); 127 | } catch(e1) { 128 | res.writeHead(400); 129 | res.end(); 130 | return; 131 | } 132 | 133 | params = (params instanceof Array) ? params : [params]; 134 | ret = self.methods[method].apply(self.context, self.staticParams_.concat(params)); 135 | 136 | try { 137 | ret = (ret === undefined) ? '' : JSON.stringify(ret); 138 | } catch(e2) { 139 | ret = e2.toString(); 140 | status = 500; 141 | } 142 | 143 | res.writeHead(status, {'Content-Length': ret.length, 'Content-Type': 'application/json'}); 144 | res.end(ret); 145 | }); 146 | } else { 147 | res.writeHead(404); 148 | res.end(); 149 | } 150 | } else { 151 | res.writeHead(405); 152 | res.end(); 153 | } 154 | }; -------------------------------------------------------------------------------- /lib/loop/multiloop.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------- 2 | // MultiLoop 3 | // ----------------------------------------- 4 | // 5 | var BUILD_AS_SINGLE_FILE; 6 | if (!BUILD_AS_SINGLE_FILE) { 7 | var util = require('../util'); 8 | var loop = require('./loop'); 9 | var EventEmitter = require('events').EventEmitter; 10 | var Loop = loop.Loop; 11 | var LOOP_OPTIONS = loop.LOOP_OPTIONS; 12 | } 13 | 14 | /** MultiLoop accepts a single loop specification, but allows it to be executed concurrently by creating 15 | multiple Loop instances. The execution rate and concurrency are changed over time using profiles. 16 | LOOP_OPTIONS lists the supported specification parameters. */ 17 | var MultiLoop = exports.MultiLoop = function MultiLoop(spec) { 18 | EventEmitter.call(this); 19 | 20 | this.spec = util.extend({}, util.defaults(spec, LOOP_OPTIONS)); 21 | this.loops = []; 22 | this.concurrencyProfile = spec.concurrencyProfile || [[0, spec.concurrency]]; 23 | this.rpsProfile = spec.rpsProfile || [[0, spec.rps]]; 24 | this.updater_ = this.update_.bind(this); 25 | this.finishedChecker_ = this.checkFinished_.bind(this); 26 | }; 27 | 28 | util.inherits(MultiLoop, EventEmitter); 29 | 30 | /** Start all scheduled Loops. When the loops complete, 'end' event is emitted. */ 31 | MultiLoop.prototype.start = function() { 32 | if (this.running) { return; } 33 | this.running = true; 34 | this.startTime = new Date(); 35 | this.rps = 0; 36 | this.concurrency = 0; 37 | this.loops = []; 38 | this.loopConditions_ = []; 39 | 40 | if (this.spec.numberOfTimes > 0 && this.spec.numberOfTimes < Infinity) { 41 | this.loopConditions_.push(Loop.maxExecutions(this.spec.numberOfTimes)); 42 | } 43 | 44 | if (this.spec.duration > 0 && this.spec.duration < Infinity) { 45 | this.endTimeoutId = setTimeout(this.stop.bind(this), this.spec.duration * 1000); 46 | } 47 | 48 | process.nextTick(this.emit.bind(this, 'start')); 49 | this.update_(); 50 | return this; 51 | }; 52 | 53 | /** Force all loops to finish */ 54 | MultiLoop.prototype.stop = function() { 55 | if (!this.running) { return; } 56 | clearTimeout(this.endTimeoutId); 57 | clearTimeout(this.updateTimeoutId); 58 | this.running = false; 59 | this.loops.forEach(function(l) { l.stop(); }); 60 | this.emit('remove', this.loops); 61 | this.emit('end'); 62 | this.loops = []; 63 | }; 64 | 65 | /** Given a profile in the format [[time, value], [time, value], ...], return the value corresponding 66 | to the given time. Transitions between points are currently assumed to be linear, and value=0 at time=0 67 | unless otherwise specified in the profile. */ 68 | MultiLoop.prototype.getProfileValue_ = function(profile, time) { 69 | if (!profile || profile.length === 0) { return 0; } 70 | if (time < 0) { return profile[0][0]; } 71 | 72 | var lastval = [0,0]; 73 | for (var i = 0; i < profile.length; i++) { 74 | if (profile[i][0] === time) { 75 | return profile[i][1]; 76 | } else if (profile[i][0] > time) { 77 | var dx = profile[i][0]-lastval[0], dy = profile[i][1]-lastval[1]; 78 | return Math.floor((time-lastval[0]) / dx * dy + lastval[1]); 79 | } 80 | lastval = profile[i]; 81 | } 82 | return profile[profile.length-1][1]; 83 | }; 84 | 85 | /** Given a profile in the format [[time, value], [time, value], ...], and the current time, return the 86 | time (rounded up to the nearest whole unit) before the profile value will change by 1. */ 87 | MultiLoop.prototype.getProfileTimeToNextValue_ = function(profile, time) { 88 | if (!profile || profile.length === 0) { return Infinity; } 89 | if (time < 0) { return -time; } 90 | 91 | var MIN_TIMEOUT = 1, lastval = [0,0]; 92 | for (var i = 0; i < profile.length; i++) { 93 | if (profile[i][0] > time) { 94 | var dt = (profile[i][0]-time), 95 | timePerUnitChange = dt / Math.abs(profile[i][1]-lastval[1]); 96 | return Math.ceil(Math.max(MIN_TIMEOUT, Math.min(dt, timePerUnitChange))); 97 | } 98 | lastval = profile[i]; 99 | } 100 | return Infinity; 101 | }; 102 | 103 | MultiLoop.prototype.update_ = function() { 104 | var i, now = Math.floor((new Date() - this.startTime) / 1000), 105 | concurrency = this.getProfileValue_(this.concurrencyProfile, now), 106 | rps = this.getProfileValue_(this.rpsProfile, now), 107 | timeout = Math.min( 108 | this.getProfileTimeToNextValue_(this.concurrencyProfile, now), 109 | this.getProfileTimeToNextValue_(this.rpsProfile, now)) * 1000; 110 | 111 | if (concurrency < this.concurrency) { 112 | var removed = this.loops.splice(concurrency); 113 | removed.forEach(function(l) { l.stop(); }); 114 | this.emit('remove', removed); 115 | } else if (concurrency > this.concurrency) { 116 | var loops = []; 117 | for (i = 0; i < concurrency-this.concurrency; i++) { 118 | var args = this.spec.argGenerator ? this.spec.argGenerator() : this.spec.args, 119 | loop = new Loop(this.spec.fun, args, this.loopConditions_, 0).start(); 120 | loop.on('end', this.finishedChecker_); 121 | loops.push(loop); 122 | } 123 | this.loops = this.loops.concat(loops); 124 | this.emit('add', loops); 125 | } 126 | 127 | if (concurrency !== this.concurrency || rps !== this.rps) { 128 | var rpsPerLoop = (rps / concurrency); 129 | this.loops.forEach(function(l) { l.rps = rpsPerLoop; }); 130 | this.emit('rps', rps); 131 | } 132 | 133 | this.concurrency = concurrency; 134 | this.rps = rps; 135 | 136 | if (timeout < Infinity) { 137 | this.updateTimeoutId = setTimeout(this.updater_, timeout); 138 | } 139 | }; 140 | 141 | MultiLoop.prototype.checkFinished_ = function() { 142 | if (!this.running) { return true; } 143 | if (this.loops.some(function (l) { return l.running; })) { return false; } 144 | this.running = false; 145 | this.emit('end'); 146 | return true; 147 | }; 148 | -------------------------------------------------------------------------------- /lib/monitoring/monitor.js: -------------------------------------------------------------------------------- 1 | // ----------------- 2 | // Monitor 3 | // ----------------- 4 | var BUILD_AS_SINGLE_FILE; 5 | if (!BUILD_AS_SINGLE_FILE) { 6 | var util = require('../util'); 7 | var StatsCollectors = require('./collectors'); 8 | var StatsLogger = require('./statslogger').StatsLogger; 9 | var EventEmitter = require('events').EventEmitter; 10 | } 11 | 12 | /** Monitor is used to track code statistics of code that is run multiple times or concurrently: 13 | 14 | var monitor = new Monitor('runtime'); 15 | function f() { 16 | var m = monitor.start(); 17 | doSomethingAsynchronous(..., function() { 18 | m.end(); 19 | }); 20 | } 21 | ... 22 | console.log('f() median runtime (ms): ' + monitor.stats['runtime'].percentile(.5)); 23 | 24 | Look at monitoring.test.js for more examples. 25 | 26 | Monitor can also emits periodic 'update' events with overall and statistics since the last 'update'. This 27 | allows the statistics to be introspected at regular intervals for things like logging and reporting. Set 28 | Monitor.updateInterval to enable 'update' events. 29 | 30 | @param arguments contain names of the statistics to track. Add additional statistics to collectors.js. 31 | */ 32 | var Monitor = exports.Monitor = function Monitor() { // arguments 33 | EventEmitter.call(this); 34 | util.PeriodicUpdater.call(this); // adds updateInterval property and calls update() 35 | this.targets = []; 36 | this.setStats.apply(this, arguments); 37 | }; 38 | 39 | util.inherits(Monitor, EventEmitter); 40 | 41 | /** Set the statistics this monitor should gather. */ 42 | Monitor.prototype.setStats = function(stats) { // arguments contains stats names 43 | var self = this, 44 | summarizeStats = function() { 45 | var summary = {ts: new Date()}; 46 | if (self.name) { summary.name = self.name; } 47 | util.forEach(this, function(statName, stats) { 48 | summary[statName] = stats.summary(); 49 | }); 50 | return summary; 51 | }; 52 | 53 | self.collectors = []; 54 | self.stats = {}; 55 | self.interval = {}; 56 | stats = (stats instanceof Array) ? stats : Array.prototype.slice.call(arguments); 57 | stats.forEach(function(stat) { 58 | var name = stat, params; 59 | if (typeof stat === 'object') { 60 | name = stat.name; 61 | params = stat; 62 | } 63 | var Collector = StatsCollectors[name]; 64 | if (!Collector) { 65 | throw new Error('No collector for statistic: ' + name); 66 | } 67 | if (!Collector.disableIntervalCollection) { 68 | var intervalCollector = new Collector(params); 69 | self.collectors.push(intervalCollector); 70 | self.interval[name] = intervalCollector.stats; 71 | } 72 | if (!Collector.disableCumulativeCollection) { 73 | var cumulativeCollector = new Collector(params); 74 | self.collectors.push(cumulativeCollector); 75 | self.stats[name] = cumulativeCollector.stats; 76 | } 77 | }); 78 | 79 | Object.defineProperty(this.stats, 'summary', { 80 | enumerable: false, 81 | value: summarizeStats 82 | }); 83 | Object.defineProperty(this.interval, 'summary', { 84 | enumerable: false, 85 | value: summarizeStats 86 | }); 87 | }; 88 | 89 | /** Called by the instrumented code when it begins executing. Returns a monitoring context. Call 90 | context.end() when the instrumented code completes. */ 91 | Monitor.prototype.start = function(args) { 92 | var self = this, 93 | endFuns = [], 94 | doStart = function(m, context) { 95 | if (m.start) { m.start(context, args); } 96 | if (m.end) { 97 | endFuns.push(function(result) { return m.end(context, result); }); 98 | } 99 | }, 100 | monitoringContext = { 101 | end: function(result) { 102 | endFuns.forEach(function(f) { f(result); }); 103 | } 104 | }; 105 | 106 | self.collectors.forEach(function(m) { doStart(m, {}); }); 107 | return monitoringContext; 108 | }; 109 | 110 | /** Monitor a set of EventEmitter objects, where each object is analogous to a thread. The objects 111 | should emit 'start' and 'end' when they begin doing the operation being instrumented. This is useful 112 | for monitoring concurrently executing instances of loop.js#Loop. 113 | 114 | Call either as monitorObjects(obj1, obj2, ...) or monitorObjects([obj1, obj2, ...], 'start', 'end') */ 115 | Monitor.prototype.monitorObjects = function(objs, startEvent, endEvent) { 116 | var self = this; 117 | 118 | if (!(objs instanceof Array)) { 119 | objs = util.argarray(arguments); 120 | startEvent = endEvent = null; 121 | } 122 | 123 | startEvent = startEvent || 'start'; 124 | endEvent = endEvent || 'end'; 125 | 126 | objs.forEach(function(o) { 127 | var mon; 128 | o.on(startEvent, function(args) { 129 | mon = self.start(args); 130 | }); 131 | o.on(endEvent, function(result) { 132 | mon.end(result); 133 | }); 134 | }); 135 | 136 | return self; 137 | }; 138 | 139 | /** Set the file name or stats.js#LogFile object that statistics are logged to; null for default */ 140 | Monitor.prototype.setLogFile = function(logNameOrObject) { 141 | this.logNameOrObject = logNameOrObject; 142 | }; 143 | 144 | /** Log statistics each time an 'update' event is emitted? */ 145 | Monitor.prototype.setLoggingEnabled = function(enabled) { 146 | if (enabled) { 147 | this.logger = this.logger || new StatsLogger(this, this.logNameOrObject).start(); 148 | } else if (this.logger) { 149 | this.logger.stop(); 150 | this.logger = null; 151 | } 152 | return this; 153 | }; 154 | 155 | /** Emit the 'update' event and reset the statistics for the next window */ 156 | Monitor.prototype.update = function() { 157 | this.emit('update', this.interval, this.stats); 158 | util.forEach(this.interval, function(name, stats) { 159 | if (stats.length > 0) { 160 | stats.clear(); 161 | } 162 | }); 163 | }; -------------------------------------------------------------------------------- /lib/user/http_program.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | request = require('superagent'), 3 | url = require('url'), 4 | fs = require('fs'), 5 | async = require('async'), 6 | temp = require('temp'), 7 | zlib = require('zlib'), 8 | RequestContainer = require('./request_container'), 9 | querystring = require('querystring'), 10 | traverse = require('traverse'); 11 | 12 | _.str = require('underscore.string'); 13 | _.mixin(_.str); 14 | 15 | exports.request = function(options, next) { 16 | var self = this; 17 | options.headers = options.headers || {}; 18 | if (this.attrs.headers) { 19 | _(options.headers).extend(this.attrs.headers); 20 | } 21 | 22 | // Build a static JSON object from the options hash by invoking 23 | // any dynamic value functions. 24 | options = traverse(options).clone(); 25 | traverse(options).forEach(function(value) { 26 | if (_(value).isFunction()) { 27 | this.update(value.call(self, options)); 28 | } 29 | }); 30 | 31 | options.protocol = 'http'; 32 | options.hostname = options.hostname || this.attrs.hostname; 33 | options.port = options.port || this.attrs.port; 34 | var uri = 'http://' + options.hostname + ':' + options.port + options.path; 35 | var req = request(options.method, uri).set(options.headers); 36 | if (options.body) { 37 | var content; 38 | if (options.body.json) { 39 | // This is a total hack to deal with the fact that the API accepts this 40 | // bizarre urlencoded json string within a json object format. I'm sorry. 41 | var isV2 = options.path.indexOf('/v2') > 1, 42 | payload = JSON.stringify(options.body.json); 43 | content = querystring.stringify(isV2? {data: payload} : {json: payload}); 44 | 45 | } else { 46 | content = options.body; // wouldn't it be nice if it worked this way... 47 | } 48 | req.send(content); 49 | } 50 | req.end(function(res) { 51 | // Deal with the fact that sometimes, we get text/html as the 52 | // content type... sigh... 53 | if (res.type === 'text/html') { 54 | try { 55 | res.body = JSON.parse(res.text); 56 | } catch (e) { 57 | // sometimes it isn't JSON, sigh... 58 | res.body = res.text; 59 | } 60 | } 61 | 62 | // Deal with the possibility of receiving errors via 200 63 | // response and fudge the status code to give us meaningful 64 | // data in our reports. 65 | if (res.status == 200 && res.body.error) { 66 | res.status = 500; 67 | res.res.statusCode = 500; 68 | } 69 | 70 | if (res.status >= 400) { // TODO: Probably get ride of this 71 | console.log("Error: " + res.status + " from " + options.path); 72 | console.log(res.body); 73 | } 74 | 75 | options.response = res.body; 76 | 77 | if (!self.runData.requests) { 78 | self.runData.requests = new RequestContainer(); 79 | } 80 | self.runData.requests.add(options); 81 | 82 | next(res); 83 | }); 84 | req.on('error', function(err) { 85 | // might as well do something. 86 | next({res: {statusCode: 499}}); 87 | }); 88 | }; 89 | 90 | // Sets the hostname and port (port optional) 91 | exports.host = function(hostname, port, next) { 92 | if (arguments.length === 2) { 93 | next = port; 94 | port = 80; 95 | } 96 | 97 | this.attrs.hostname = hostname; 98 | this.attrs.port = port; 99 | next(); 100 | }; 101 | 102 | exports.headers = function(headers, next) { 103 | this.attrs.headers = this.attrs.headers || {}; 104 | _(this.attrs.headers).extend(headers); 105 | next(); 106 | }; 107 | 108 | 109 | _(['get', 'post', 'put', 'delete']).each(function(verb) { 110 | exports[verb] = function(path, next) { 111 | // Sort out args - we also accept path inserts and an options hash 112 | // The last arg is always the "next" callback 113 | var options = {}; 114 | var pathInserts = []; 115 | _.chain(arguments).rest().initial().each(function(arg) { 116 | if (_(arg).isFunction()) { 117 | pathInserts.push(arg); 118 | } else { 119 | options = arg; 120 | } 121 | }); 122 | next = _(arguments).last(); 123 | if (_(options).isString()) { 124 | var modulePath = process.cwd() + "/" + options; 125 | options = require(modulePath).load(this, this.argv); 126 | delete require.cache[modulePath]; // this might help keep the memory footprint low 127 | } 128 | 129 | options.method = verb.toUpperCase(); 130 | 131 | // If there were path inserts, we need to make path a function. 132 | if (pathInserts.length > 0) { 133 | options.path = function() { 134 | var args = [path]; 135 | // Allow path inserts to be functions 136 | args = args.concat(_(pathInserts).map(function(insert) { 137 | if (_(insert).isFunction()) { 138 | return insert(); 139 | } 140 | return insert; 141 | })); 142 | return _.sprintf.apply(this, args); 143 | }; 144 | } else { 145 | options.path = path; 146 | } 147 | exports.request.call(this, options, next); 148 | }; 149 | }); 150 | 151 | exports.install = function(Program) { 152 | 153 | Program.prototype.resolveValues = function(attrs) { 154 | var self = this; 155 | working = traverse(attrs).clone(); 156 | traverse(working).forEach(function(value) { 157 | if (_(value).isFunction()) { 158 | this.update(value.call(self, working)); 159 | } 160 | }); 161 | return working; 162 | }; 163 | 164 | _(['get', 'post', 'put', 'delete']).each(function(method) { 165 | Program.registerInterpreter(method, exports[method], true); 166 | }); 167 | _(['headers', 'host']).each(function(method) { 168 | Program.registerInterpreter(method, exports[method], false); 169 | }); 170 | }; 171 | -------------------------------------------------------------------------------- /lib/remote/slavenode.js: -------------------------------------------------------------------------------- 1 | var BUILD_AS_SINGLE_FILE; 2 | if (!BUILD_AS_SINGLE_FILE) { 3 | var url = require('url'); 4 | var util = require('../util'); 5 | var qputs = util.qputs; 6 | var Endpoint = require('./endpoint').Endpoint; 7 | var EndpointClient = require('./endpointclient').EndpointClient; 8 | var EventEmitter = require('events').EventEmitter; 9 | var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; 10 | var Program = require('../user/program.js').Program; 11 | } 12 | 13 | /** An instance of SlaveNode represents a slave from the perspective of a slave (as opposed to 14 | slave.js#Slave, which represents a slave from the perspective of a master). When a slave.js#Slave object 15 | is started, it sends a slave specification to the target machine, which uses the specification to create 16 | a SlaveNode. The specification contains: 17 | 18 | { 19 | id: master assigned id of this node, 20 | master: 'base url of master endpoint, e.g. /remote/0', 21 | masterMethods: ['list of method name supported by master'], 22 | slaveMethods: [ 23 | { name: 'method-name', fun: 'function() { valid Javascript in a string }' } 24 | ], 25 | pingInterval: milliseconds between sending the current execution state to master 26 | } 27 | 28 | If the any of the slaveMethods contain invalid Javascript, this constructor will throw an exception. 29 | 30 | SlaveNode emits the following events: 31 | - 'start': The endpoint has been installed on the HTTP server and connection to the master has been made 32 | - 'masterError': The HTTP connection to the master node returned an error. 33 | - 'end': The local endpoint has been removed and the connection to the master server terminated 34 | */ 35 | var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { 36 | EventEmitter.call(this); 37 | util.PeriodicUpdater.call(this); 38 | 39 | var self = this, slaveState = 'initialized'; 40 | this.id = spec.id; 41 | this.masterClient_ = spec.master ? this.createMasterClient_(spec.master, spec.masterMethods) : null; 42 | this.slaveEndpoint_ = this.createEndpoint_(server, spec.slaveMethods); 43 | this.slaveEndpoint_.setStaticParams([this.masterClient_]); 44 | this.slaveEndpoint_.on('start', function() { this.emit.bind(this, 'start'); }); 45 | this.slaveEndpoint_.on('end', this.end.bind(this)); 46 | 47 | this.slaveEndpoint_.start(); 48 | this.slaveEndpoint_.context.id = this.id; 49 | this.slaveEndpoint_.context.__defineGetter__('state', function() { return slaveState; }); 50 | this.slaveEndpoint_.context.__defineSetter__('state', function(val) { 51 | slaveState = val; 52 | self.update(); 53 | }); 54 | this.url = this.slaveEndpoint_.url; 55 | 56 | this.updateInterval = (spec.pingInterval >= 0) ? spec.pingInterval : NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; 57 | }; 58 | util.inherits(SlaveNode, EventEmitter); 59 | SlaveNode.prototype.end = function() { 60 | this.updateInterval = 0; 61 | this.slaveEndpoint_.end(); 62 | this.emit('end'); 63 | }; 64 | SlaveNode.prototype.update = function() { 65 | if (this.masterClient_) { 66 | this.masterClient_.updateSlaveState_(this.slaveEndpoint_.context.state); 67 | } 68 | }; 69 | SlaveNode.prototype.createEndpoint_ = function(server, methods) { 70 | // Add a new endpoint and route to the HttpServer 71 | var endpoint = new Endpoint(server); 72 | 73 | // "Compile" the methods by eval()'ing the string in "fun", and add to the endpoint 74 | if (methods) { 75 | try { 76 | methods.forEach(function(m) { 77 | var fun; 78 | eval('fun=' + m.fun); 79 | endpoint.defineMethod(m.name, fun); 80 | }); 81 | } catch (e) { 82 | endpoint.end(); 83 | endpoint = null; 84 | throw e; 85 | } 86 | } 87 | 88 | return endpoint; 89 | }; 90 | SlaveNode.prototype.createMasterClient_ = function(masterUrl, methods) { 91 | var parts = url.parse(masterUrl), 92 | masterClient = new EndpointClient(parts.hostname, Number(parts.port) || 8000, parts.pathname); 93 | 94 | masterClient.defineMethod('updateSlaveState_'); 95 | if (methods && methods instanceof Array) { 96 | methods.forEach(function(m) { masterClient.defineMethod(m); }); 97 | } 98 | 99 | // send this slave's id as the first parameter for all method calls to master 100 | masterClient.setStaticParams([this.id]); 101 | 102 | masterClient.on('error', this.emit.bind(this, 'masterError')); 103 | return masterClient; 104 | }; 105 | 106 | 107 | /** Install the /remote URL handler, which creates a slave endpoint. On receiving a POST request to 108 | /remote, a new route is added to HTTP_SERVER using the handler definition provided in the request body. 109 | See #SlaveNode for a description of the handler defintion. */ 110 | var installRemoteHandler = exports.installRemoteHandler = function(server) { 111 | var slaveNodes = []; 112 | server.addRoute('^/remote/?$', function(path, req, res) { 113 | if (req.method === 'POST') { 114 | util.readStream(req, function(body) { 115 | var slaveNode; 116 | 117 | // Grab the slave endpoint definition from the HTTP request body; should be valid JSON 118 | try { 119 | body = JSON.parse(body); 120 | slaveNode = new SlaveNode(server, body); 121 | } catch(e) { 122 | res.writeHead(400); 123 | res.end(e.toString()); 124 | return; 125 | } 126 | 127 | slaveNodes.push(slaveNode); 128 | slaveNode.on('end', function() { 129 | var idx = slaveNodes.indexOf(slaveNode); 130 | if (idx !== -1) { slaveNodes.splice(idx, 1); } 131 | }); 132 | 133 | res.writeHead(201, { 134 | 'Location': slaveNode.url, 135 | 'Content-Length': 0, 136 | }); 137 | res.end(); 138 | }); 139 | } else if (req.method === 'GET') { 140 | res.writeHead(200, {'Content-Type': 'application/json'}); 141 | res.end(JSON.stringify(slaveNodes.map(function(s) { return s.url; }))); 142 | } else { 143 | res.writeHead(405); 144 | res.end(); 145 | } 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /lib/reporting/summary.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test Results 4 | 13 | 34 | 35 | 36 | 37 | 42 |
43 |
44 | 50 |
51 | 52 | 53 | 54 | 121 | -------------------------------------------------------------------------------- /doc/loop.md: -------------------------------------------------------------------------------- 1 | **This document is out-of-date. See [`lib/loop/loop.js`](https://github.com/benschmaus/nodeload/tree/master/lib/loop/loop.js) and [`lib/loop/multiloop.js`](https://github.com/benschmaus/nodeload/tree/master/lib/loop/multiloop.js).** 2 | 3 | ## Function Scheduler ## 4 | 5 | The `SCHEDULER` object allows a function to be called at a desired rate and concurrency level. See `scheduler.js`. 6 | **Functions:** 7 | 8 | * `SCHEDULER.schedule(spec)`: Schedule a function to be executed (see the **Schedule Definition** below) 9 | * `SCHEDULER.startAll(callback)`: Start running all the scheduled functions and execute callback when they complete 10 | * `SCHEDULER.startSchedule(callback)`: Start a single scheduled function and execute callback when it completes 11 | * `funLoop(fun)`: Wrap functions that do not perform IO so they can be used with SCHEDULER 12 | 13 | **Usage**: 14 | 15 | Call `SCHEDULER.schedule(spec)` to add a job. `spec.fun` must be a `function(loopFun, args)` and call `loopFun(results)` when it completes. Call `SCHEDULER.startAll()` to start running all scheduled jobs. 16 | 17 | If `spec.argGenerator` is non-null, it is called `spec.concurrency` times on startup. One return value is passed as the second parameter to each concurrent execution of `spec.fun`. If null, the value of `spec.args` is passed to all executions of `spec.fun` instead. 18 | 19 | A scheduled job finishes after its target duration or it has been called the maximum number of times. `SCHEDULER` stops *all* jobs once all *monitored* jobs finish. For example, 1 monitored job is scheduled for 5 seconds, and 2 unmonitored jobs are scheduled with no time limits. `SCHEDULER` will start all 3 jobs when `SCHEDULER.startAll()` is called, and stop all 3 jobs 5 seconds later. Unmonitored jobs are useful for running side processes such as statistics gathering and reporting. 20 | 21 | Example: 22 | 23 | var t = 1; 24 | nl.SCHEDULER.schedule({ 25 | fun: nl.LoopUtils.funLoop(function(i) { console.log("Thread " + i) }), 26 | argGenerator: function() { return t++; }, 27 | concurrency: 5, 28 | rps: 10, 29 | duration: 10 30 | }); 31 | nl.SCHEDULER.startAll(function() { sys.puts("Done.") }); 32 | 33 | Alternatively, a Job can started independently. A Job instance is analogous to a single thread, and does not understand the `concurrency` parameter. 34 | 35 | var i = 0; 36 | var job = new nl.Job({ 37 | fun: nl.LoopUtils.funLoop(function() { console.log(i++) }), 38 | rps: 10, 39 | duration: 10 40 | }).start(); 41 | 42 | **Job Definition**: The following object defines the parameters and defaults for a job run by `SCHEDULER`: 43 | 44 | var JOB_DEFAULTS = { 45 | fun: null, // A function to execute which accepts the parameters (loopFun, args). 46 | // The value of args is the return value of argGenerator() or the args 47 | // parameter if argGenerator is null. The function must call 48 | // loopFun(results) when it completes. 49 | argGenerator: null, // A function which is called once when the job is started. The return 50 | // value is passed to fun as the "args" parameter. This is useful when 51 | // concurrency > 1, and each "thread" should have its own args. 52 | args: null, // If argGenerator is NOT specified, then this is passed to the fun as "args". 53 | concurrency: 1, // Number of concurrent calls of fun() 54 | rps: Infinity, // Target number of time per second to call fun() 55 | duration: Infinity, // Maximum duration of this job in seconds 56 | numberOfTimes: Infinity, // Maximum number of times to call fun() 57 | delay: 0, // Seconds to wait before calling fun() for the first time 58 | monitored: true // Does this job need to finish in order for SCHEDULER.startAll() to end? 59 | }; 60 | 61 | 62 | ## Event-based loops ## 63 | 64 | The `ConditionalLoop` class provides a generic way to write a loop where each iteration is scheduled using `process.nextTick()`. This allows many long running "loops" to be executed concurrently by `node.js`. See `evloops.js`. 65 | 66 | **Functions:** 67 | 68 | * `ConditionalLoop(fun, args, conditions, delay):` Defines a loop (see **Loop Definition** below) 69 | * `ConditionalLoop.start(callback):` Starts executing and call `callback` on termination 70 | * `ConditionalLoop.stop():` Terminate the loop 71 | * `LoopConditions.timeLimit(seconds)`, `LoopConditions.maxExecutions(numberOfTimes)`: useful ConditionalLoop conditions 72 | * `LoopUtils.rpsLoop(rps, fun)`: Wrap a `function(loopFun, args)` so ConditionalLoop calls it a set rate 73 | * `LoopUtils.funLoop(fun)`: Wrap a linearly executing `function(args)` so it can be used with a ConditionalLoop 74 | 75 | **Usage:** 76 | 77 | Create a `ConditionalLoop` instance and call `ConditionalLoop.start()` to execute the loop. A function given to `ConditionalLoop` must be a `function(loopFun, args)` which ends by calling `loopFun()`. 78 | 79 | The `conditions` parameter is a list of functions. When any function returns `false`, the loop terminates. For example, the functions `LoopConditions.timeLimit(seconds)` and `LoopConditions.maxExecutions(numberOfTimes)` are conditions that limit the duration and number of iterations of a loop respectively. 80 | 81 | The loop also terminates if `ConditionalLoop.stop()` is called. 82 | 83 | Example: 84 | 85 | var fun = function(loopFun, startTime) { 86 | console.log("It's been " + (new Date() - startTime) / 1000 + " seconds"); 87 | loopFun(); 88 | }; 89 | var stopOnFriday = function() { 90 | return (new Date()).getDay() < 5; 91 | } 92 | var loop = new nl.ConditionalLoop(nl.LoopUtils.rpsLoop(1, fun), new Date(), [stopOnFriday, nl.LoopConditions.timeLimit(604800 /*1 week*/)], 1); 93 | loop.start(function() { console.log("It's Friday!") }); 94 | 95 | **Loop Definition:** 96 | 97 | The `ConditionalLoop` constructor arguments are: 98 | 99 | fun: Function that takes parameters (loopFun, args) and calls loopFun() after each iteration 100 | args: The args parameter to pass to fun 101 | conditions: A list of functions representing termination conditions. Terminate when any function returns `false`. 102 | delay: Seconds to wait before starting the first iteration -------------------------------------------------------------------------------- /lib/reporting/report.js: -------------------------------------------------------------------------------- 1 | // This file defines Report, Chart, and ReportGroup 2 | // 3 | // A Report contains a summary and a number of charts. 4 | // 5 | var BUILD_AS_SINGLE_FILE; 6 | if (!BUILD_AS_SINGLE_FILE) { 7 | var util = require('../util'); 8 | var querystring = require('querystring'); 9 | var LogFile = require('../stats').LogFile; 10 | var template = require('./template'); 11 | var config = require('../config'); 12 | 13 | var REPORT_SUMMARY_TEMPLATE = require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE; 14 | var NODELOAD_CONFIG = config.NODELOAD_CONFIG; 15 | var START = NODELOAD_CONFIG.START; 16 | var DYGRAPH_SOURCE = require('./dygraph.tpl.js').DYGRAPH_SOURCE; 17 | } 18 | var Chart; 19 | 20 | /** A Report contains a summary object and set of charts. It can be easily updated using the stats from 21 | a monitor.js#Monitor or monitor.js#MonitorGroup using updateFromMonitor()/updateFromMonitorGroup(). 22 | 23 | @param name A name for the report. Generally corresponds to the test name. 24 | @param updater A function(report) that should update the summary and chart data. */ 25 | var Report = exports.Report = function(name) { 26 | this.name = name; 27 | this.uid = util.uid(); 28 | this.summary = {}; 29 | this.charts = {}; 30 | }; 31 | Report.prototype = { 32 | getChart: function(name) { 33 | if (!this.charts[name]) { 34 | this.charts[name] = new Chart(name); 35 | } 36 | return this.charts[name]; 37 | }, 38 | /** Update this report automatically each time the Monitor emits an 'update' event */ 39 | updateFromMonitor: function(monitor) { 40 | monitor.on('update', this.doUpdateFromMonitor_.bind(this, monitor, '')); 41 | return this; 42 | }, 43 | /** Update this report automatically each time the MonitorGroup emits an 'update' event */ 44 | updateFromMonitorGroup: function(monitorGroup) { 45 | var self = this; 46 | monitorGroup.on('update', function() { 47 | util.forEach(monitorGroup.monitors, function(monitorname, monitor) { 48 | self.doUpdateFromMonitor_(monitor, monitorname); 49 | }); 50 | }); 51 | return self; 52 | }, 53 | doUpdateFromMonitor_: function(monitor, monitorname) { 54 | var self = this; 55 | monitorname = monitorname ? monitorname + ' ' : ''; 56 | util.forEach(monitor.stats, function(statname, stat) { 57 | util.forEach(stat.summary(), function(name, val) { 58 | self.summary[self.name + ' ' + monitorname + statname + ' ' + name] = val; 59 | }); 60 | if (monitor.interval[statname]) { 61 | self.getChart(monitorname + statname) 62 | .put(monitor.interval[statname].summary()); 63 | } 64 | }); 65 | } 66 | }; 67 | 68 | /** A Chart represents a collection of lines over time represented as: 69 | 70 | columns: ["x values", "line 1", "line 2", "line 3", ...] 71 | rows: [[timestamp1, line1[0], line2[0], line3[0], ...], 72 | [timestamp2, line1[1], line2[1], line3[1], ...], 73 | [timestamp3, line1[2], line2[2], line3[2], ...], 74 | ... 75 | ] 76 | 77 | @param name A name for the chart */ 78 | var Chart = exports.Chart = function(name) { 79 | this.name = name; 80 | this.uid = util.uid(); 81 | this.columns = ["time"]; 82 | this.rows = [[Date.now()]]; 83 | }; 84 | Chart.prototype = { 85 | /** Put a row of data into the chart. The current time will be used as the x-value. The lines in the 86 | chart are extracted from the "data". New lines can be added to the chart at any time by including it 87 | in data. 88 | 89 | @param data An object representing one row of data: { 90 | "line name 1": value1 91 | "line name 2": value2 92 | ... 93 | } 94 | */ 95 | put: function(data) { 96 | var self = this, row = [Date.now()]; 97 | util.forEach(data, function(column, val) { 98 | var col = self.columns.indexOf(column); 99 | if (col < 0) { 100 | col = self.columns.length; 101 | self.columns.push(column); 102 | self.rows[0].push(0); 103 | } 104 | row[col] = val; 105 | }); 106 | self.rows.push(row); 107 | }, 108 | /** Update chart using data from event emitter each time it emits an event. 'eventEmitter' should 109 | emit the given 'event' (defaults to 'data') with a single object. 'fields' are read from the object 110 | and added to the chart. For example, a chart can track the output form a child process output using 111 | 112 | chart.updateFromEventEmitter(spawnAndMonitor('cmd', ['args'], /val: (.*)/, ['val']), ['val']) 113 | 114 | */ 115 | updateFromEventEmitter: function(eventEmitter, fields, event) { 116 | var self = this; 117 | eventEmitter.on(event || 'data', function(data) { 118 | var row = {}; 119 | fields.forEach(function(i) { 120 | if (data[i] !== undefined) { row[i] = data[i]; } 121 | }); 122 | self.put(row); 123 | }); 124 | } 125 | }; 126 | 127 | var ReportGroup = exports.ReportGroup = function() { 128 | this.reports = []; 129 | this.logNameOrObject = 'results-' + START.toISOString() + '.html'; 130 | }; 131 | ReportGroup.prototype = { 132 | addReport: function(report) { 133 | report = (typeof report === 'string') ? new Report(report) : report; 134 | this.reports.push(report); 135 | return report; 136 | }, 137 | getReport: function(report) { 138 | var reports = this.reports.filter(function(r) { return r.name === report; }); 139 | return reports[0] || this.addReport(report); 140 | }, 141 | setLogFile: function(logNameOrObject) { 142 | this.logNameOrObject = logNameOrObject; 143 | }, 144 | setLoggingEnabled: function(enabled) { 145 | clearTimeout(this.loggingTimeoutId); 146 | if (enabled) { 147 | this.logger = this.logger || (typeof this.logNameOrObject === 'string') ? new LogFile(this.logNameOrObject) : this.logNameOrObject; 148 | this.loggingTimeoutId = setTimeout(this.writeToLog_.bind(this), this.refreshIntervalMs); 149 | } else if (this.logger) { 150 | this.logger.close(); 151 | this.logger = null; 152 | } 153 | return this; 154 | }, 155 | reset: function() { 156 | this.reports = {}; 157 | }, 158 | getHtml: function() { 159 | var self = this, 160 | t = template.create(REPORT_SUMMARY_TEMPLATE); 161 | return t({ 162 | DYGRAPH_SOURCE: DYGRAPH_SOURCE, 163 | querystring: querystring, 164 | refreshPeriodMs: self.refreshIntervalMs, 165 | reports: self.reports 166 | }); 167 | }, 168 | writeToLog_: function() { 169 | this.loggingTimeoutId = setTimeout(this.writeToLog_.bind(this), this.refreshIntervalMs); 170 | this.logger.clear(this.getHtml()); 171 | } 172 | }; -------------------------------------------------------------------------------- /lib/remote/cluster.js: -------------------------------------------------------------------------------- 1 | var BUILD_AS_SINGLE_FILE; 2 | if (!BUILD_AS_SINGLE_FILE) { 3 | var util = require('../util'); 4 | var Endpoint = require('./endpoint').Endpoint; 5 | var EventEmitter = require('events').EventEmitter; 6 | var SlaveNode = require('./slavenode').SlaveNode; 7 | var Slaves = require('./slaves').Slaves; 8 | var qputs = util.qputs; 9 | var HTTP_SERVER = require('../http').HTTP_SERVER; 10 | var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; 11 | } 12 | 13 | /** Main interface for creating a distributed nodeload cluster. Spec: 14 | { 15 | master: { 16 | host: 'host' or 'host:port' or undefined to extract from HttpServer 17 | master_remote_function_1: function(slaves, slaveId, args...) { ... }, 18 | }, 19 | slaves: { 20 | host: ['host:port', ...], 21 | setup: function(master) { ... } 22 | slave_remote_function_1: function(master, args...) { ... } 23 | }, 24 | pingInterval: 2000, 25 | server: HttpServer instance (defaults to global HTTP_SERVER) 26 | } 27 | 28 | Calling cluster.start() will register a master handler on the provided http.js#HttpServer. It will 29 | connect to every slave, asking each slave to 1) execute the setup() function, 2) report its current 30 | state to this host every pingInterval milliseconds. Calling cluster.slave_remote_function_1(), will 31 | execute slave_remote_function_1 on every slave. 32 | 33 | Cluster emits the following events: 34 | 35 | - 'init': emitted when the cluster.start() can be called (the underlying HTTP server has been started). 36 | - 'start': when connections to all the slave instances have been established 37 | - 'end': when all the slaves have been terminated (e.g. by calling cluster.end()). The endpoint 38 | installed in the underlying HTTP server has been removed. 39 | - 'slaveError', slave, Error: The connection to the slave experienced an error. If error is null, the 40 | slave has failed to send its state in the last 4 pingInterval periods. It should be considered 41 | unresponsive. 42 | - 'slaveError', slave, http.ClientResponse: A method call to this slave returned this non-200 response. 43 | - 'running', 'done': when all the slaves that are not in an error state (haven't responded in the last 4 44 | pingIntervals) report that they are in a 'running' or 'done' state. To set a slave's the state, 45 | install a slave function: 46 | 47 | cluster = new Cluster({ 48 | slaves: { 49 | slave_remote_function: function(master) { this.state = 'running'; } 50 | }, 51 | ... 52 | }); 53 | 54 | and call it 55 | 56 | cluster.slave_remote_function(); 57 | 58 | Cluster.state can be: 59 | - 'initializing': The cluster cannot be started yet -- it is waiting for the HTTP server to start. 60 | - 'initialized': The cluster can be started. 61 | - 'started': Connections to all the slaves have been established and the master endpoint is created. 62 | - 'stopping': Attempting to terminate all slaves. 63 | - 'stopped': All of the slaves have been properly shutdown and the master endpoint removed. 64 | */ 65 | var Cluster = exports.Cluster = function Cluster(spec) { 66 | EventEmitter.call(this); 67 | util.PeriodicUpdater.call(this); 68 | 69 | var self = this, 70 | masterSpec = spec.master || {}, 71 | slavesSpec = spec.slaves || { hosts:[] }, 72 | masterHost = spec.master && spec.master.host || 'localhost'; 73 | 74 | self.pingInterval = spec.pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; 75 | self.server = spec.server || HTTP_SERVER; 76 | self.masterEndpoint = new Endpoint(self.server, masterHost); 77 | self.slaves = new Slaves(self.masterEndpoint, self.pingInterval); 78 | self.slaveState_ = {}; 79 | 80 | // Define all master methods on the local endpoint 81 | self.masterEndpoint.setStaticParams([self.slaves]); // 1st param to all master functions is slaves. 2nd will be slave id, which SlaveNode prepends to all requests. 82 | self.masterEndpoint.defineMethod('updateSlaveState_', self.updateSlaveState_.bind(self)); // updateSlaveState_ is on every master and called by SlaveNode.update() to periodically send its state to the master. 83 | util.forEach(masterSpec, function(method, val) { 84 | if (typeof val === 'function') { 85 | self.masterEndpoint.defineMethod(method, val); 86 | } 87 | }); 88 | 89 | // Send all slave methods definitions to the remote instances 90 | slavesSpec.hosts.forEach(function(h) { self.slaves.add(h); }); 91 | util.forEach(spec.slaves, function(method, val) { 92 | if (typeof val === 'function') { 93 | self.slaves.defineMethod(method, val); 94 | self[method] = function() { self.slaves[method].apply(self.slaves, arguments); }; 95 | } 96 | }); 97 | 98 | // Store some other extra state for each slave so we can detect state changes and unresponsiveness 99 | self.slaves.slaves.forEach(function(s) { 100 | if (!self.slaveState_[s.id]) { 101 | self.slaveState_[s.id] = { alive: true, aliveSinceLastCheck: false }; 102 | } 103 | }); 104 | 105 | // Cluster is started when slaves are alive, and ends when slaves are all shutdown 106 | self.slaves.on('start', function() { 107 | self.state = 'started'; 108 | self.emit('start'); 109 | }); 110 | self.slaves.on('end', function() { 111 | self.masterEndpoint.end(); 112 | self.state = 'stopped'; 113 | self.emit('end'); 114 | }); 115 | self.slaves.on('slaveError', function(slave, err) { 116 | self.emit('slaveError', slave, err); 117 | }); 118 | 119 | // Cluster is initialized (can be started) once server is started 120 | if (self.server.running) { 121 | self.state = 'initialized'; 122 | process.nextTick(function() { self.emit('init'); }); 123 | } else { 124 | self.state = 'initializing'; 125 | self.server.on('start', function() { 126 | self.state = 'initialized'; 127 | self.emit('init'); 128 | }); 129 | } 130 | }; 131 | util.inherits(Cluster, EventEmitter); 132 | Cluster.prototype.started = function() { return this.state === 'started'; }; 133 | /** Start cluster; install a route on the local HTTP server and send the slave definition to all the 134 | slave instances. */ 135 | Cluster.prototype.start = function() { 136 | if (!this.server.running) { 137 | throw new Error('A Cluster can only be started after it has emitted \'init\'.'); 138 | } 139 | this.masterEndpoint.start(); 140 | this.slaves.start(); 141 | this.updateInterval = this.pingInterval * 4; // call update() every 4 ping intervals to check for slave aliveness 142 | // this.slaves 'start' event handler emits 'start' and updates state 143 | }; 144 | /** Stop the cluster; remove the route from the local HTTP server and uninstall and disconnect from all 145 | the slave instances */ 146 | Cluster.prototype.end = function() { 147 | this.state = 'stopping'; 148 | this.updateInterval = 0; 149 | this.slaves.end(); 150 | // this.slaves 'end' event handler emits 'end', destroys masterEndpoint & updates state 151 | }; 152 | /** Check for unresponsive slaves that haven't called updateSlaveState_ in the last 4 update intervals */ 153 | Cluster.prototype.update = function() { 154 | var self = this; 155 | util.forEach(self.slaveState_, function(id, s) { 156 | if (!s.aliveSinceLastCheck && s.alive) { 157 | // this node has not sent us its state in the last four spec.pingInterval intervals -- mark as dead 158 | s.alive = false; 159 | self.emit('slaveError', self.slaves[id], null); 160 | } else if (s.aliveSinceLastCheck) { 161 | s.aliveSinceLastCheck = false; 162 | s.alive = true; 163 | } 164 | }); 165 | }; 166 | /** Receive a periodic state update message from a slave. When all slaves enter the 'running' or 'done' 167 | states, emit an event. */ 168 | Cluster.prototype.updateSlaveState_ = function(slaves, slaveId, state) { 169 | var slave = slaves[slaveId]; 170 | if (slave) { 171 | var previousState = this.slaveState_[slaveId].state; 172 | this.slaveState_[slaveId].state = state; 173 | this.slaveState_[slaveId].aliveSinceLastCheck = true; 174 | if (previousState !== state) { 175 | this.emit('slaveState', slave, state); 176 | 177 | if (state === 'running' || state === 'done') { 178 | this.emitWhenAllSlavesInState_(state); 179 | } 180 | } 181 | } else { 182 | qputs('WARN: ignoring message from unexpected slave instance ' + slaveId); 183 | } 184 | }; 185 | Cluster.prototype.emitWhenAllSlavesInState_ = function(state) { 186 | var allSlavesInSameState = true; 187 | util.forEach(this.slaveState_, function(id, s) { 188 | if (s.state !== state && s.alive) { 189 | allSlavesInSameState = false; 190 | } 191 | }); 192 | if (allSlavesInSameState) { 193 | this.emit(state); 194 | } 195 | }; -------------------------------------------------------------------------------- /lib/monitoring/collectors.js: -------------------------------------------------------------------------------- 1 | // 2 | // Define new statistics that Monitor can track by adding to this file. Each class should have: 3 | // 4 | // - stats, a member which implements the standard interface found in stats.js 5 | // - start(context, args), optional, called when execution of the instrumented code is about to start 6 | // - end(context, result), optional, called when the instrumented code finishes executing 7 | // 8 | // Defining .disableIntervalCollection and .disableCumulativeCollection to the collection of per-interval 9 | // and overall statistics respectively. 10 | // 11 | 12 | /*jslint sub:true */ 13 | var BUILD_AS_SINGLE_FILE; 14 | if (!BUILD_AS_SINGLE_FILE) { 15 | var util = require('../util'); 16 | var stats = require('../stats'); 17 | var Histogram = stats.Histogram; 18 | var Peak = stats.Peak; 19 | var ResultsCounter = stats.ResultsCounter; 20 | var Rate = stats.Rate; 21 | var Uniques = stats.Uniques; 22 | var Accumulator = stats.Accumulator; 23 | var LogFile = stats.LogFile; 24 | var StatsCollectors = exports; 25 | } else { 26 | var StatsCollectors = {}; 27 | } 28 | 29 | /** Track the runtime of an operation, storing stats in a stats.js#Histogram */ 30 | StatsCollectors['runtime'] = StatsCollectors['latency'] = function RuntimeCollector(params) { 31 | var self = this; 32 | self.stats = new Histogram(params); 33 | self.start = function(context) { context.start = new Date(); }; 34 | self.end = function(context) { self.stats.put(new Date() - context.start); }; 35 | }; 36 | 37 | /** Track HTTP response codes, storing stats in a stats.js#ResultsCounter object. The client must call 38 | .end({res: http.ClientResponse}). */ 39 | StatsCollectors['result-codes'] = function ResultCodesCollector() { 40 | var self = this; 41 | self.stats = new ResultsCounter(); 42 | self.end = function(context, http) { self.stats.put(http.res.statusCode); }; 43 | }; 44 | 45 | /** Track requests per second, storing stats in a stats.js#Rate object. The client must call 46 | .end({res: http.ClientResponse}). */ 47 | StatsCollectors['rps'] = function RpsCollector() { 48 | var self = this; 49 | self.stats = new Rate(); 50 | self.end = function(context, http) { self.stats.put(); }; 51 | }; 52 | 53 | /** Track a status code that is returned in an HTTP header, storing stats in a stats.js#ResultsCounter 54 | object. The client must call .end({res: http.ClientResponse}). */ 55 | StatsCollectors['header-code'] = function HeaderCodeCollector(params) { 56 | if (!params.header) { throw new Error('"header" is a required parameter for header-code'); } 57 | var self = this, header = params.header.toLowerCase(), regex = params.regex; 58 | self.stats = new ResultsCounter(); 59 | self.end = function(context, http) { 60 | var val = http.res.headers[header]; 61 | if (regex && val !== undefined) { 62 | val = val.match(regex); 63 | val = val && val[1] || undefined; 64 | } 65 | if (val !== undefined) { self.stats.put(val); } 66 | }; 67 | }; 68 | 69 | /** Track the concurrent executions (ie. stuff between calls to .start() and .end()), storing in a 70 | stats.js#Peak. */ 71 | StatsCollectors['concurrency'] = function ConcurrencyCollector() { 72 | var self = this, c = 0; 73 | self.stats = new Peak(); 74 | self.start = function() { c++; }; 75 | self.end = function() { self.stats.put(c--); }; 76 | }; 77 | 78 | /** Track the size of HTTP request bodies sent by adding up the content-length headers. This function 79 | doesn't really work as you'd hope right now, since it doesn't work for chunked encoding messages and 80 | doesn't return actual bytes over the wire (headers, etc). */ 81 | StatsCollectors['request-bytes'] = function RequestBytesCollector() { 82 | var self = this; 83 | self.stats = new Accumulator(); 84 | self.end = function(context, http) { 85 | if (http && http.req) { 86 | if (http.req._header) { self.stats.put(http.req._header.length); } 87 | if (http.req.body) { self.stats.put(http.req.body.length); } 88 | } 89 | }; 90 | }; 91 | 92 | /** Track the size of HTTP response bodies. It doesn't account for headers! */ 93 | StatsCollectors['response-bytes'] = function ResponseBytesCollector() { 94 | var self = this; 95 | self.stats = new Accumulator(); 96 | self.end = function(context, http) { 97 | if (http && http.res) { 98 | http.res.on('data', function(chunk) { 99 | self.stats.put(chunk.length); 100 | }); 101 | } 102 | }; 103 | }; 104 | 105 | /** Track unique URLs requested, storing stats in a stats.js#Uniques object. The client must call 106 | Monitor.start({req: http.ClientRequest}). */ 107 | StatsCollectors['uniques'] = function UniquesCollector() { 108 | var self = this; 109 | self.stats = new Uniques(); 110 | self.end = function(context, http) { 111 | if (http && http.req) { self.stats.put(http.req.path); } 112 | }; 113 | }; 114 | StatsCollectors['uniques'].disableIntervalCollection = true; // Per-interval stats should be not be collected 115 | 116 | /** Track number HTTP response codes that are considered errors. Can also log request / response 117 | information to disk when an error response is received. Specify the acceptable HTTP status codes in 118 | params.successCodes. Specify the log file name in params.log, or leave undefined to disable logging. */ 119 | StatsCollectors['http-errors'] = function HttpErrorsCollector(params) { 120 | var self = this; 121 | self.stats = new Accumulator(); 122 | self.successCodes = params.successCodes || [200]; 123 | self.logfile = (typeof params.log === 'string') ? new LogFile(params.log) : params.log; 124 | self.logResBody = ( params.hasOwnProperty('logResBody') ) ? params.logResBody : true; 125 | self.end = function(context, http) { 126 | if (self.successCodes.indexOf(http.res.statusCode) < 0) { 127 | self.stats.put(1); 128 | 129 | if (self.logfile) { 130 | util.readStream(http.res, function(body) { 131 | var logObj = { ts: new Date(), 132 | req: { 133 | headers: http.req._header, 134 | body: http.req.body, 135 | }, 136 | res: { 137 | statusCode: http.res.statusCode, 138 | headers: http.res.headers 139 | } 140 | }; 141 | if (self.logResBody) { 142 | logObj.res.body = body; 143 | } 144 | self.logfile.put(JSON.stringify(logObj) + '\n'); 145 | }); 146 | } 147 | } 148 | }; 149 | }; 150 | StatsCollectors['http-errors'].disableIntervalCollection = true; // Per-interval stats should be not be collected 151 | 152 | /** Track number HTTP response codes that are considered errors. Can also log request / response 153 | information to disk when an error response is received. Specify the acceptable HTTP status codes in 154 | params.successCodes. Specify the log file name in params.log, or leave undefined to disable logging. */ 155 | StatsCollectors['slow-responses'] = function HttpErrorsCollector(params) { 156 | var self = this; 157 | self.stats = new Accumulator(); 158 | self.threshold = params.threshold || 1000; 159 | self.logfile = (typeof params.log === 'string') ? new LogFile(params.log) : params.log; 160 | self.logResBody = ( params.hasOwnProperty('logResBody') ) ? params.logResBody : true; 161 | self.start = function(context) { context.start = new Date(); }; 162 | self.end = function(context, http) { 163 | var runTime = new Date() - context.start; 164 | if (runTime > self.threshold) { 165 | self.stats.put(1); 166 | 167 | if (self.logfile) { 168 | util.readStream(http.res, function(body) { 169 | var logObj = { ts: new Date(), 170 | req: { 171 | // Use the _header "private" member of http.ClientRequest, available as of 172 | // node v0.2.2 (9/30/10). This is the only way to reliably get all request 173 | // headers, since ClientRequest adds headers beyond what the user specifies 174 | // in certain conditions, like Connection and Transfer-Encoding. 175 | headers: http.req._header, 176 | body: http.req.body, 177 | }, 178 | res: { 179 | statusCode: http.res.statusCode, 180 | headers: http.res.headers 181 | }, 182 | latency: runTime 183 | }; 184 | if (self.logResBody) { 185 | logObj.res.body = body; 186 | } 187 | self.logfile.put(JSON.stringify(logObj) + '\n'); 188 | }); 189 | } 190 | } 191 | }; 192 | }; 193 | StatsCollectors['slow-responses'].disableIntervalCollection = true; // Per-interval stats should be not be collected -------------------------------------------------------------------------------- /test/monitoring.test.js: -------------------------------------------------------------------------------- 1 | /*jslint sub:true */ 2 | 3 | var assert = require('assert'), 4 | http = require('http'), 5 | EventEmitter = require('events').EventEmitter, 6 | util = require('../lib/util'), 7 | monitoring = require('../lib/monitoring'), 8 | Monitor = monitoring.Monitor, 9 | MonitorGroup = monitoring.MonitorGroup; 10 | 11 | var svr = http.createServer(function (req, res) { 12 | res.writeHead(200, {'Content-Type': 'text/plain'}); 13 | res.end(req.url); 14 | }); 15 | svr.listen(9000); 16 | setTimeout(function() { svr.close(); }, 1000); 17 | 18 | function mockConnection(callback) { 19 | var conn = { 20 | operation: function(opcallback) { 21 | setTimeout(function() { opcallback(); }, 25); 22 | } 23 | }; 24 | setTimeout(function() { callback(conn); }, 75); 25 | } 26 | 27 | module.exports = { 28 | 'example: track runtime of a function': function(beforeExit) { 29 | var m = new Monitor('runtime'), 30 | f = function() { 31 | var ctx = m.start(), runtime = Math.floor(Math.random() * 100); 32 | setTimeout(function() { ctx.end(); }, runtime); 33 | }; 34 | 35 | for (var i = 0; i < 20; i++) { 36 | f(); 37 | } 38 | 39 | beforeExit(function() { 40 | var summary = m.stats['runtime'] && m.stats['runtime'].summary(); 41 | assert.ok(summary); 42 | assert.equal(m.stats['runtime'].length, 20); 43 | assert.ok(summary.min >= 0 && summary.min < 100); 44 | assert.ok(summary.max > 0 && summary.max <= 100); 45 | assert.ok(summary.median > 0 && summary.median < 100); 46 | }); 47 | }, 48 | 'example: use a MonitorGroup to organize multiple Monitors': function(beforeExit) { 49 | var m = new MonitorGroup('runtime'), 50 | f = function() { 51 | var transactionCtx = m.start('transaction'); 52 | mockConnection(function(conn) { 53 | var operationCtx = m.start('operation'); 54 | conn.operation(function() { 55 | operationCtx.end(); 56 | transactionCtx.end(); 57 | }); 58 | }); 59 | }; 60 | 61 | for (var i = 0; i < 10; i++) { 62 | f(); 63 | } 64 | 65 | beforeExit(function() { 66 | var summary = m.interval.summary(); 67 | assert.ok(summary); 68 | assert.ok(summary['transaction'] && summary['transaction']['runtime']); 69 | assert.ok(summary['operation'] && summary['operation']['runtime']); 70 | assert.ok(Math.abs(summary['transaction']['runtime'].median - 100) <= 10, summary['transaction']['runtime'].median.toString()); 71 | assert.ok(Math.abs(summary['operation']['runtime'].median - 25) <= 5); 72 | }); 73 | }, 74 | 'example: use EventEmitter objects instead of interacting with MonitorGroup directly': function(beforeExit) { 75 | function MonitoredObject() { 76 | EventEmitter.call(this); 77 | var self = this; 78 | self.run = function() { 79 | self.emit('start', 'transaction'); 80 | mockConnection(function(conn) { 81 | self.emit('start', 'operation'); 82 | conn.operation(function() { 83 | self.emit('end', 'operation'); 84 | self.emit('end', 'transaction'); 85 | }); 86 | }); 87 | }; 88 | } 89 | util.inherits(MonitoredObject, EventEmitter); 90 | 91 | var m = new MonitorGroup('runtime'); 92 | for (var i = 0; i < 5; i++) { 93 | var obj = new MonitoredObject(); 94 | m.monitorObjects(obj); 95 | setTimeout(obj.run, i * 100); 96 | } 97 | 98 | beforeExit(function() { 99 | var trSummary = m.stats['transaction'] && m.stats['transaction']['runtime'] && m.stats['transaction']['runtime'].summary(); 100 | var opSummary = m.stats['operation'] && m.stats['operation']['runtime'] && m.stats['operation']['runtime'].summary(); 101 | assert.ok(trSummary); 102 | assert.ok(opSummary); 103 | assert.equal(m.stats['transaction']['runtime'].length, 5); 104 | assert.ok(Math.abs(trSummary.median - 100) <= 5, '100 == ' + trSummary.median); 105 | assert.ok(Math.abs(opSummary.median - 25) <= 5, '25 == ' + opSummary.median); 106 | }); 107 | }, 108 | 'use EventEmitter objects with Monitor': function(beforeExit) { 109 | function MonitoredObject() { 110 | EventEmitter.call(this); 111 | var self = this; 112 | self.run = function() { 113 | self.emit('start'); 114 | setTimeout(function() { self.emit('end'); }, Math.floor(Math.random() * 100)); 115 | }; 116 | } 117 | util.inherits(MonitoredObject, EventEmitter); 118 | 119 | var m = new Monitor('runtime'); 120 | for (var i = 0; i < 5; i++) { 121 | var obj = new MonitoredObject(); 122 | m.monitorObjects(obj); 123 | setTimeout(obj.run, i * 100); 124 | } 125 | 126 | beforeExit(function() { 127 | var summary = m.stats['runtime'] && m.stats['runtime'].summary(); 128 | assert.ok(summary); 129 | assert.equal(m.stats['runtime'].length, 5); 130 | assert.ok(summary.min >= 0 && summary.min < 100, summary.min.toString()); 131 | assert.ok(summary.max > 0 && summary.max <= 100, summary.max.toString()); 132 | assert.ok(summary.median > 0 && summary.median < 100, summary.median.toString()); 133 | }); 134 | }, 135 | 'HTTP specific monitors': function(beforeExit) { 136 | var q = 0, 137 | m = new Monitor('result-codes', 'uniques', 'request-bytes', 'response-bytes', {name: 'header-code', header: 'content-type'}), 138 | client = http.createClient(9000, 'localhost'), 139 | f = function() { 140 | var ctx = m.start(), 141 | path = '/search?q=' + q++, 142 | req = client.request('GET', path, {'host': 'localhost'}); 143 | req.path = path; 144 | req.end(); 145 | req.on('response', function(res) { 146 | ctx.end({req: req, res: res}); 147 | }); 148 | }; 149 | 150 | for (var i = 0; i < 2; i++) { 151 | f(); 152 | } 153 | 154 | beforeExit(function() { 155 | var resultCodesSummary = m.stats['result-codes'] && m.stats['result-codes'].summary(), 156 | uniquesSummary = m.stats['uniques'] && m.stats['uniques'].summary(), 157 | requestBytesSummary = m.stats['request-bytes'] && m.stats['request-bytes'].summary(), 158 | responseBytesSummary = m.stats['response-bytes'] && m.stats['response-bytes'].summary(), 159 | headerCodeSummary = m.stats['header-code'] && m.stats['header-code'].summary(); 160 | 161 | assert.ok(resultCodesSummary); 162 | assert.ok(uniquesSummary); 163 | assert.ok(requestBytesSummary); 164 | assert.ok(responseBytesSummary); 165 | 166 | assert.equal(resultCodesSummary.total, 2); 167 | assert.equal(resultCodesSummary['200'], 2); 168 | 169 | assert.equal(headerCodeSummary['text/plain'], 2); 170 | 171 | assert.equal(uniquesSummary.total, 2); 172 | assert.equal(uniquesSummary.uniqs, 2); 173 | 174 | assert.ok(requestBytesSummary.total > 0); 175 | 176 | assert.ok(responseBytesSummary.total > 20); 177 | }); 178 | }, 179 | 'monitor generates update events with interval and overall stats': function(beforeExit) { 180 | var m = new Monitor('runtime'), 181 | intervals = 0, 182 | f = function() { 183 | var ctx = m.start(), runtime = Math.floor(Math.random() * 10); 184 | setTimeout(function() { ctx.end(); }, runtime); 185 | }; 186 | 187 | m.updateInterval = 220; 188 | 189 | // Call to f every 100ms for a total runtime >500ms 190 | for (var i = 1; i <= 5; i++) { 191 | setTimeout(f, i*100); 192 | } 193 | 194 | // Disable 'update' events after 500ms so that this test can complete 195 | setTimeout(function() { m.updateInterval = 0; }, 510); 196 | 197 | m.on('update', function(interval, overall) { 198 | assert.strictEqual(overall, m.stats); 199 | 200 | assert.ok(interval['runtime']); 201 | assert.equal(interval['runtime'].length, 2); 202 | assert.ok(interval['runtime'].mean() > 0 && interval['runtime'].mean() < 10); 203 | assert.ok(interval['runtime'].mean() > 0 && interval['runtime'].mean() < 10); 204 | intervals++; 205 | }); 206 | 207 | beforeExit(function() { 208 | assert.equal(intervals, 2, 'Got incorrect number of update events: ' + intervals); 209 | assert.equal(m.stats['runtime'].length, 5); 210 | }); 211 | } 212 | }; 213 | 214 | process.setMaxListeners(20); -------------------------------------------------------------------------------- /lib/loop/loop.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------- 2 | // Event-based looping 3 | // ----------------------------------------- 4 | // 5 | // This file defines Loop and MultiLoop. 6 | // 7 | // Nodeload uses the node.js event loop to repeatedly call a function. In order for this to work, the 8 | // function cooperates by accepting a function, finished, as its first argument and calls finished() 9 | // when it completes. This is refered to as "event-based looping" in nodeload. 10 | // 11 | /*jslint laxbreak: true, undef: true */ 12 | /*global setTimeout: false */ 13 | var BUILD_AS_SINGLE_FILE; 14 | if (!BUILD_AS_SINGLE_FILE) { 15 | var util = require('../util'); 16 | var EventEmitter = require('events').EventEmitter; 17 | } 18 | 19 | /** LOOP_OPTIONS defines all of the parameters that used with Loop.create(), MultiLoop() */ 20 | var LOOP_OPTIONS = exports.LOOP_OPTIONS = { 21 | fun: undefined, // A function to execute which accepts the parameters (finished, args). 22 | // The value of args is the return value of argGenerator() or the args 23 | // parameter if argGenerator is undefined. The function must call 24 | // finished(results) when it completes. 25 | argGenerator: undefined, // A function which is called once when the loop is started. The return 26 | // value is passed to fun as the "args" parameter. This is useful when 27 | // concurrency > 1, and each "thread" should have its own args. 28 | args: undefined, // If argGenerator is NOT specified, then this is passed to the fun as 29 | // "args". 30 | rps: Infinity, // Target number of time per second to call fun() 31 | duration: Infinity, // Maximum duration of this loop in seconds 32 | numberOfTimes: Infinity, // Maximum number of times to call fun() 33 | concurrency: 1, // (MultiLoop only) Number of concurrent calls of fun() 34 | // 35 | concurrencyProfile: undefined, // (MultiLoop only) array indicating concurrency over time: 36 | // [[time (seconds), # users], [time 2, users], ...] 37 | // For example, ramp up from 0 to 100 "threads" and back down to 0 over 38 | // 20 seconds: 39 | // [[0, 0], [10, 100], [20, 0]] 40 | // 41 | rpsProfile: undefined // (MultiLoop only) array indicating execution rate over time: 42 | // [[time (seconds), rps], [time 2, rps], ...] 43 | // For example, ramp up from 100 to 500 rps and then down to 0 over 20 44 | // seconds: 45 | // [[0, 100], [10, 500], [20, 0]] 46 | }; 47 | 48 | /** Loop wraps an arbitrary function to be executed in a loop. Each iteration of the loop is scheduled 49 | in the node.js event loop using process.nextTick(), which allows other events in the loop to be handled 50 | as the loop executes. Loop emits the events 'start' (before the first iteration), 'end', 'startiteration' 51 | and 'enditeration'. 52 | 53 | @param funOrSpec Either a loop specification object or a loop function. LOOP_OPTIONS lists all the 54 | supported fields in a loop specification. 55 | 56 | A loop function is an asynchronous function that calls finished(result) when it 57 | finishes: 58 | 59 | function(finished, args) { 60 | ... 61 | finished(result); 62 | } 63 | 64 | use the static method Loop.funLoop(f) to wrap simple, non-asynchronous functions. 65 | @param args passed as-is as the second argument to fun 66 | @param conditions a list of functions that are called at the beginning of every loop. If any 67 | function returns false, the loop terminates. Loop#timeLimit and Loop#maxExecutions 68 | are conditions that can be used here. 69 | @param rps max number of times per second this loop should execute */ 70 | var Loop = exports.Loop = function Loop(funOrSpec, args, conditions, rps) { 71 | EventEmitter.call(this); 72 | 73 | if (typeof funOrSpec === 'object') { 74 | var spec = util.defaults(funOrSpec, LOOP_OPTIONS); 75 | 76 | funOrSpec = spec.fun; 77 | args = spec.argGenerator ? spec.argGenerator() : spec.args; 78 | conditions = []; 79 | rps = spec.rps; 80 | 81 | if (spec.numberOfTimes > 0 && spec.numberOfTimes < Infinity) { 82 | conditions.push(Loop.maxExecutions(spec.numberOfTimes)); 83 | } 84 | if (spec.duration > 0 && spec.duration < Infinity) { 85 | conditions.push(Loop.timeLimit(spec.duration)); 86 | } 87 | } 88 | 89 | this.__defineGetter__('rps', function() { return rps; }); 90 | this.__defineSetter__('rps', function(val) { 91 | rps = (val >= 0) ? val : Infinity; 92 | this.timeout_ = Math.floor(1/rps * 1000); 93 | if (this.restart_ && this.timeout_ < Infinity) { 94 | var oldRestart = this.restart_; 95 | this.restart_ = null; 96 | oldRestart(); 97 | } 98 | }); 99 | 100 | this.id = util.uid(); 101 | this.fun = funOrSpec; 102 | this.args = args; 103 | this.conditions = conditions || []; 104 | this.running = false; 105 | this.rps = rps; 106 | }; 107 | 108 | util.inherits(Loop, EventEmitter); 109 | 110 | /** Start executing this.fun with the arguments, this.args, until any condition in this.conditions 111 | returns false. When the loop completes the 'end' event is emitted. */ 112 | Loop.prototype.start = function() { 113 | var self = this, 114 | startLoop = function() { 115 | self.emit('start'); 116 | self.loop_(); 117 | }; 118 | 119 | if (self.running) { return; } 120 | self.running = true; 121 | process.nextTick(startLoop); 122 | return this; 123 | }; 124 | 125 | Loop.prototype.stop = function() { 126 | this.running = false; 127 | }; 128 | 129 | /** Calls each function in Loop.conditions. Returns false if any function returns false */ 130 | Loop.prototype.checkConditions_ = function() { 131 | var self = this; 132 | return this.running && this.conditions.every(function(c) { return c.bind(self)(); }); 133 | }; 134 | 135 | /** Checks conditions and schedules the next loop iteration. 'startiteration' is emitted before each 136 | iteration and 'enditeration' is emitted after. */ 137 | Loop.prototype.loop_ = function() { 138 | 139 | var self = this, result, active, lagging, 140 | callfun = function() { 141 | if (self.timeout_ === Infinity) { 142 | self.restart_ = callfun; 143 | return; 144 | } 145 | 146 | result = null; active = true; lagging = (self.timeout_ <= 0); 147 | if (!lagging) { 148 | setTimeout(function() { 149 | lagging = active; 150 | if (!lagging) { self.loop_(); } 151 | }, self.timeout_); 152 | } 153 | self.emit('startiteration', self.args); 154 | var start = new Date(); 155 | self.fun(function(res) { 156 | active = false; 157 | result = res; 158 | self.emit('enditeration', result); 159 | if (lagging) { self.loop_(); } 160 | }, self.args); 161 | }; 162 | 163 | if (self.checkConditions_()) { 164 | process.nextTick(callfun); 165 | } else { 166 | self.running = false; 167 | self.emit('end'); 168 | } 169 | }; 170 | 171 | 172 | // Predefined functions that can be used in Loop.conditions 173 | 174 | /** Returns false after a given number of seconds */ 175 | Loop.timeLimit = function(seconds) { 176 | var start = new Date(); 177 | return function() { 178 | return (seconds === Infinity) || ((new Date() - start) < (seconds * 1000)); 179 | }; 180 | }; 181 | /** Returns false after a given number of iterations */ 182 | Loop.maxExecutions = function(numberOfTimes) { 183 | var counter = 0; 184 | return function() { 185 | return (numberOfTimes === Infinity) || (counter++ < numberOfTimes); 186 | }; 187 | }; 188 | 189 | 190 | // Helpers for dealing with loop functions 191 | 192 | /** A wrapper for any existing function so it can be used by Loop. e.g.: 193 | myfun = function(x) { return x+1; } 194 | new Loop(Loop.funLoop(myfun), args, [Loop.timeLimit(10)], 0) */ 195 | Loop.funLoop = function(fun) { 196 | return function(finished, args) { 197 | finished(fun(args)); 198 | }; 199 | }; 200 | /** Wrap a loop function. For each iteration, calls startRes = start(args) before calling fun(), and 201 | calls finish(result-from-fun, startRes) when fun() finishes. */ 202 | Loop.loopWrapper = function(fun, start, finish) { 203 | return function(finished, args) { 204 | var startRes = start && start(args), 205 | finishFun = function(result) { 206 | if (result === undefined) { 207 | util.qputs('Function result is null; did you forget to call finished(result)?'); 208 | } 209 | 210 | if (finish) { finish(result, startRes); } 211 | 212 | finished(result); 213 | }; 214 | fun(finishFun, args); 215 | }; 216 | }; --------------------------------------------------------------------------------