From a5e3f73c0c520e3459ddd5f5f651bf803f2667a8 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 22:26:07 -0500 Subject: [PATCH] Update a bunch of docs and examples Move nl.js stuff under lib/nl Move reporting stuff under lib/reporting --- Makefile | 8 +- NODELOADLIB.md | 569 ------------------ README.md | 214 ++++--- doc/developers.md | 19 + doc/loop.md | 102 ++++ doc/monitoring.md | 64 ++ NODELOAD.md => doc/nl.md | 54 +- doc/nodeload.md | 202 +++++++ doc/remote.md | 37 ++ doc/reporting.md | 43 ++ doc/stats.md | 44 ++ doc/tips.md | 64 ++ .../{loadtesting.ex.js => nodeload.ex.js} | 4 +- examples/nodeloadlib-ex.js | 102 ---- examples/nodeloadlib-ex2.js | 33 - examples/riaktest.ex.js | 88 +++ examples/sample-report.html | 141 ++++- examples/simpletest.ex.js | 32 + lib/loadtesting.js | 9 +- lib/monitoring/index.js | 9 +- lib/{ => nl}/options.js | 0 lib/nl/optparse-README.md | 161 +++++ lib/{ => nl}/optparse.js | 0 lib/{ => reporting}/dygraph.tpl | 0 lib/{reporting.js => reporting/index.js} | 8 +- lib/{ => reporting}/summary.tpl | 0 lib/{ => reporting}/template.js | 0 nl.js | 7 +- nodeload.js | 18 +- test/loop.test.js | 2 +- 30 files changed, 1165 insertions(+), 869 deletions(-) delete mode 100644 NODELOADLIB.md create mode 100644 doc/developers.md create mode 100644 doc/loop.md create mode 100644 doc/monitoring.md rename NODELOAD.md => doc/nl.md (53%) create mode 100644 doc/nodeload.md create mode 100644 doc/remote.md create mode 100644 doc/reporting.md create mode 100644 doc/stats.md create mode 100644 doc/tips.md rename examples/{loadtesting.ex.js => nodeload.ex.js} (91%) delete mode 100755 examples/nodeloadlib-ex.js delete mode 100755 examples/nodeloadlib-ex2.js create mode 100755 examples/riaktest.ex.js mode change 100755 => 100644 examples/sample-report.html create mode 100755 examples/simpletest.ex.js rename lib/{ => nl}/options.js (100%) create mode 100644 lib/nl/optparse-README.md rename lib/{ => nl}/optparse.js (100%) rename lib/{ => reporting}/dygraph.tpl (100%) rename lib/{reporting.js => reporting/index.js} (97%) rename lib/{ => reporting}/summary.tpl (100%) rename lib/{ => reporting}/template.js (100%) diff --git a/Makefile b/Makefile index a4612f2..5a65103 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,17 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.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 +SOURCES = lib/header.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.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/index.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 all: compile clean: rm -rf ./lib-cov - rm -f ./nodeload.js ./lib/*.tpl.js + rm -f ./nodeload.js ./lib/reporting/*.tpl.js rm -f results-*-err.log results-*-stats.log results-*-summary.html templates: - $(PROCESS_TPL) REPORT_SUMMARY_TEMPLATE lib/summary.tpl > lib/summary.tpl.js - $(PROCESS_TPL) DYGRAPH_SOURCE lib/dygraph.tpl > lib/dygraph.tpl.js + $(PROCESS_TPL) REPORT_SUMMARY_TEMPLATE lib/reporting/summary.tpl > lib/reporting/summary.tpl.js + $(PROCESS_TPL) DYGRAPH_SOURCE lib/reporting/dygraph.tpl > lib/reporting/dygraph.tpl.js compile: templates echo "#!/usr/bin/env node" > ./nodeload.js diff --git a/NODELOADLIB.md b/NODELOADLIB.md deleted file mode 100644 index 0f5bb4c..0000000 --- a/NODELOADLIB.md +++ /dev/null @@ -1,569 +0,0 @@ -OVERVIEW -================ - -`nodeloadlib` is a [node.js](http://nodejs.org/) library containing building blocks to programmatically create load tests for HTTP services. The components are: - -* High-level load testing interface -* Test monitoring -* Distributed testing -* A scheduler which executes functions at a given rate -* Event-based loops -* Statistics classes -* HTTP-specific monitors -* Web-based reporting - -QUICKSTART -================ - -* **Write a load test** - - $ vi example.js ## Add the following text to example.js - - // This test will hit localhost:8080 with 20 concurrent connections for 10 minutes. - var http = require('http'), - nl = require('./lib/nodeloadlib'); - - http.createServer(function (req, res) { res.writeHead(200); res.end(); }).listen(8080); - console.log("Server to load test listening on 8080.") - - nl.runTest({ - host: 'localhost', - port: 8080, - numClients: 20, - timeLimit: 600, - successCodes: [200], - targetRps: 200, - requestLoop: function(loopFun, client) { - var url = '/data/object-' + Math.floor(Math.random()*10000), - req = nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - req.on('response', function(res) { - loopFun({req: req, res: res}); - }); - req.end(); - } - }); - - $ node example.js ## while running, browse to http://localhost:8000 - Listening on 8080. - Opening log files. - Started HTTP server on port 8000. - ......done. - Finishing... - Shutdown HTTP server. - - Browse to http://localhost:8000 during the test for graphs. Non-200 responses are logged to `results-{timestamp}-err.log`, `results-{timestamp}-stats.log` contains statistics, and the summary web page is written to `results-{timestamp}-summary.html`. Check out [examples/nodeloadlib-ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/nodeloadlib-ex.js) for a example of a full read+write test. - -* **Run a function at given rate:** - - // Print 0..19 over 10 seconds - var nl = require('./lib/nodeloadlib').disableServer(); - var i = 0; - - new nl.Job({ - rps: 2, // run 2 times/sec - duration: 10, // run for 10 seconds - fun: function(loopFun) { - console.log(i++); - loopFun(); - } - }).start(); - - -CONFIGURATION -================ - -Use the following functions when calling `require()` to alter nodeload's default behavior: - - var nl = require('./lib/nodeloadlib') - .quiet() // disable all console output - .usePort(10000) // start HTTP server on port 10000. Default: 8000 - .disableServer() // don't start the HTTP server - .setMonitorIntervalMs(1000) // emit 'update' events every second. Default: 2000 - .setAjaxRefreshIntervalMs(1000) // HTML page should update every second. Default: 2000 - .disableLogs() // don't log anything to disk - - -COMPONENTS -================ - -## Load Test Functions ## - -High-level functions useful for quickly building up complex load tests. See `api.js`. - -**Functions:** - -* `runTest(spec, callback, stayAliveAfterDone)`: Run a single test and call `callback` (see **Test Definition** below). -* `addTest(spec)`: Add a test to be run on `startTests()`. Tests are run concurrently. -* `addRamp(rampSpec)`: Gradually ramp up the load generated by a test (see **Ramp Definition** below). -* `startTests(callback, stayAliveAfterDone)`: Run tests added by `addTest()` and `addRamp()` and call `callback`. -* `traceableRequest(...)`: Used instead of built-in node.js `http.Client.request()` to allows proper tracking of unique URLs. - -**Usage**: - -A "test" represents requests being sent at a fixed rate over concurrent connections. Tests are run by calling `runTest()` or calling `addTest()` followed by `startTests()`. The parameters defining a test are detailed in **Test Definition** section. Issue requests using one of three methods: - -* Define `method`, `path`, and `requestData`, leaving `requestGenerator` and `requestLoop` as `null`. - -* Set `requestGenerator` to a `function(http.Client) -> http.ClientRequest`. Requests returned by this function are executed by `nodeloadlib`. For example, you can GET random URLs using a `requestGenerator`: - - nl.addTest({ - requestGenerator: function(client) { - return nl.traceableRequest(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); - } - }); - -* Set `requestLoop` to a `function(loopFun, http.Client)` which calls `loopFun({req: http.ClientRequest, res: http.ClientResponse})` after each request completes. This is the most flexibile, but the function must be sure to call `loopFun()`. For example, issue `PUT` requests with proper `If-Match` headers using a `requestLoop`: - - nl.addTest({ - requestLoop: function(loopFun, client) { - var req = nl.traceableRequest(client, 'GET', '/resource'); - req.on('response', function(response) { - if (response.statusCode != 200 && response.statusCode != 404) { - loopFun({req: req, res: response}); - } else { - var headers = { }; - if (response.headers['etag'] != null) - headers['if-match'] = response.headers['etag']; - req = nl.traceableRequest(client, 'PUT', '/resource', headers, "new value"); - req.on('response', function(response) { - loopFun({req: req, res: response}); - }); - req.end(); - } - }); - req.end(); - } - }); - -A "ramp" increases the load of a particular test over some period of time. Schedule a ramp after scheduling a test by calling `addRamp()`: - - var test1 = nl.addTest({ - targetRps: 100, - requestGenerator: function(client) { - return nl.traceableRequest(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); - } - }); - - // Add 100 requests / second using 10 concurrent connections to test1 between minutes 1 and 2 - nl.addRamp({ - test: test1, - numberOfSteps: 10, - timeLimit: 60, - rpsPerStep: 10, - clientsPerStep: 1, - delay: 60 - }); - -Start all the tests by calling `startTests(...)`. The script terminates 3 seconds after the tests complete unless the parameter `stayAliveAfterDone==true`. - -Check out [examples/nodeloadlib-ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/nodeloadlib-ex.js) for an example of a full read+write test. - -**Test Definition:** The following object defines the parameters and defaults for a test, which is used by `addTest()` or `runTest()`: - - var TEST_DEFAULTS = { - name: 'Debug test', // A descriptive name for the test - - host: 'localhost', // host and port specify where to connect - port: 8080, // - requestGenerator: null, // Specify one of: - // 1. requestGenerator: a function - // function(http.Client) -> http.ClientRequest - requestLoop: null, // 2. requestLoop: is a function - // function(loopFun, http.Client) - // It must call - // loopFun({ - // req: http.ClientRequest, - // res: http.ClientResponse}); - // after each transaction to finishes to schedule the - // next iteration of requestLoop. - method: 'GET', // 3. (method + path + requestData) specify a single URL to - path: '/', // test - requestData: null, // - // - numClients: 10, // Maximum number of concurrent executions of request loop - numRequests: Infinity, // Maximum number of iterations of request loop - timeLimit: 120, // Maximum duration of test in seconds - targetRps: Infinity, // Number of times per second to execute request loop - delay: 0, // Seconds before starting test - // - successCodes: null, // List of success HTTP response codes. Non-success responses - // are logged to the error log. - stats: ['latency', // Specify list of: 'latency', 'result-codes', 'uniques', - 'result-codes'], // 'concurrency'. Note that 'uniques' only shows up in - // Cumulative section of the report. traceableRequest() must - // be used for requets or only 2 uniques will be detected. - latencyConf: { // Set latencyConf.percentiles to percentiles to report for - percentiles: [0.95,0.99] // the 'latency' stat. - } // - }; - -**Ramp Definition:** The following object defines the parameters and defaults for a ramp, which is used by `addRamp()`: - - var RAMP_DEFAULTS = { - test: null, // The test to ramp up, returned from from addTest() - numberOfSteps: 10, // Number of steps in ramp - timeLimit: 10, // The total number of seconds to ramp up - rpsPerStep: 10, // The rps to add to the test at each step - clientsPerStep: 1, // The number of connections to add to the test at each step. - delay: 0 // Number of seconds to wait before ramping up. - } - - - -## Test Monitoring ## - -`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`. - -To set the interval between 'update' events: - - var nl = require('./lib/nodeloadlib').setMonitorIntervalMs(seconds) - -**Events:** - -* `TEST_MONITOR.on('test', callback(test))`: `addTest()` was called. The newly created test is passed to `callback`. -* `TEST_MONITOR.on('start', callback(tests))`: `startTests()` was called. The list of tests being started is passed to `callback`. -* `TEST_MONITOR.on('end', callback(tests))`: All tests finished. -* `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. -* `TEST_MONITOR.on('afterUpdate', callback(tests))`: Emitted after the 'update' event. - -**Usage**: - - nl.TEST_MONITOR.on('update', function(tests) { - for (var i in tests) { - console.log(JSON.stringify(tests[i].stats['latency'].summary())) - } - }); - -## Distributed Testing ## - -Functions to distribute tests across multiple slave `nodeload` instances. See `remote.js`. - -**Functions:** - -* `remoteTest(spec)`: Return a test to be scheduled with `remoteStart(...)` (`spec` uses same format as `addTest(spec)`). -* `remoteStart(master, slaves, tests, callback, stayAliveAfterDone)`: Run tests on specified slaves. -* `remoteStartFile(master, slaves, filename, callback, stayAliveAfterDone)`: Execute a `.js` file on specified slaves. - -**Usage**: - -First, start `nodeloadlib.js` on each slave instances. - - $ node dist/nodeloadlib.js # Run on each slave machine - -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. - - // This script must be run on master:8000, which will aggregate results. Each slave - // will GET http://internal-service:8080/ at 100 rps. - var t1 = nl.remoteTest({ - name: "Distributed test", - host: 'internal-service', - port: 8080, - timeLimit: 20, - targetRps: 100 - }); - nl.remoteStart('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], [t1]); - -Alternatively, an existing `nodeload` script file can be used: - - // The file /path/to/load-test.js should contain valid javascript and can use any nodeloadlib functions - nl.remoteStartFile('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], '/path/to/load-test.js'); - -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`. - - - -## Function Scheduler ## - -The `SCHEDULER` object allows a function to be called at a desired rate and concurrency level. See `scheduler.js`. -**Functions:** - -* `SCHEDULER.schedule(spec)`: Schedule a function to be executed (see the **Schedule Definition** below) -* `SCHEDULER.startAll(callback)`: Start running all the scheduled functions and execute callback when they complete -* `SCHEDULER.startSchedule(callback)`: Start a single scheduled function and execute callback when it completes -* `funLoop(fun)`: Wrap functions that do not perform IO so they can be used with SCHEDULER - -**Usage**: - -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. - -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. - -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. - -Example: - - var t = 1; - nl.SCHEDULER.schedule({ - fun: nl.LoopUtils.funLoop(function(i) { console.log("Thread " + i) }), - argGenerator: function() { return t++; }, - concurrency: 5, - rps: 10, - duration: 10 - }); - nl.SCHEDULER.startAll(function() { sys.puts("Done.") }); - -Alternatively, a Job can started independently. A Job instance is analogous to a single thread, and does not understand the `concurrency` parameter. - - var i = 0; - var job = new nl.Job({ - fun: nl.LoopUtils.funLoop(function() { console.log(i++) }), - rps: 10, - duration: 10 - }).start(); - -**Job Definition**: The following object defines the parameters and defaults for a job run by `SCHEDULER`: - - var JOB_DEFAULTS = { - fun: null, // A function to execute which accepts the parameters (loopFun, args). - // The value of args is the return value of argGenerator() or the args - // parameter if argGenerator is null. The function must call - // loopFun(results) when it completes. - argGenerator: null, // A function which is called once when the job is started. The return - // value is passed to fun as the "args" parameter. This is useful when - // concurrency > 1, and each "thread" should have its own args. - args: null, // If argGenerator is NOT specified, then this is passed to the fun as "args". - concurrency: 1, // Number of concurrent calls of fun() - rps: Infinity, // Target number of time per second to call fun() - duration: Infinity, // Maximum duration of this job in seconds - numberOfTimes: Infinity, // Maximum number of times to call fun() - delay: 0, // Seconds to wait before calling fun() for the first time - monitored: true // Does this job need to finish in order for SCHEDULER.startAll() to end? - }; - - -## Event-based loops ## - -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`. - -**Functions:** - -* `ConditionalLoop(fun, args, conditions, delay):` Defines a loop (see **Loop Definition** below) -* `ConditionalLoop.start(callback):` Starts executing and call `callback` on termination -* `ConditionalLoop.stop():` Terminate the loop -* `LoopConditions.timeLimit(seconds)`, `LoopConditions.maxExecutions(numberOfTimes)`: useful ConditionalLoop conditions -* `LoopUtils.rpsLoop(rps, fun)`: Wrap a `function(loopFun, args)` so ConditionalLoop calls it a set rate -* `LoopUtils.funLoop(fun)`: Wrap a linearly executing `function(args)` so it can be used with a ConditionalLoop - -**Usage:** - -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()`. - -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. - -The loop also terminates if `ConditionalLoop.stop()` is called. - -Example: - - var fun = function(loopFun, startTime) { - console.log("It's been " + (new Date() - startTime) / 1000 + " seconds"); - loopFun(); - }; - var stopOnFriday = function() { - return (new Date()).getDay() < 5; - } - var loop = new nl.ConditionalLoop(nl.LoopUtils.rpsLoop(1, fun), new Date(), [stopOnFriday, nl.LoopConditions.timeLimit(604800 /*1 week*/)], 1); - loop.start(function() { console.log("It's Friday!") }); - -**Loop Definition:** - -The `ConditionalLoop` constructor arguments are: - - fun: Function that takes parameters (loopFun, args) and calls loopFun() after each iteration - args: The args parameter to pass to fun - conditions: A list of functions representing termination conditions. Terminate when any function returns `false`. - delay: Seconds to wait before starting the first iteration - - -## Statistics ## - -Implementations of various statistics. See `stats.js`. - -**Classes:** - -* `Histogram(numBuckets)`: A histogram of integers. If most of the items are between 0 and `numBuckets`, calculating percentiles and stddev is fast. -* `Accumulator`: Calculates the sum of the numbers put in. -* `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. -* `Uniques`: Tracks the number of unique items added. -* `Peak`: Tracks the max of the numbers put in. -* `Rate`: Tracks the rate at which items are added. -* `LogFile`: Outputs to a file on disk. -* `NullLog`: Ignores all items put in. -* `Reportable`: Wraps any other statistic to store an interval and cumulative version of it. - -**Functions:** - -* `randomString(length)`: Returns a random string of ASCII characters between 32 and 126 of the requested length. -* `nextGaussian(mean, stddev)`: Returns a normally distributed number using the provided mean and standard deviation. -* `nextPareto(min, max, shape)`: Returns a Pareto distributed number between `min` and `max` inclusive using the provided shape. -* `roundRobin(list)`: Returns a copy of the list with a `get()` method. `get()` returns list entries round robin. - -**Usage:** - -All of the statistics classes support the methods: - -* `.length`: The total number of items `put()` into this object. -* `put(item)`: Include an item in the statistic. -* `get()`: Get a specific value from the object, which varies depending on the object. -* `clear()`: Clear out all items. -* `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. - -In addition, these other methods are supported: - -* `Histogram.mean()`: Calculate the mean of the numbers in the histogram. -* `Histogram.percentile(percentile)`: Calculate the given `percentile`, between 0 and 1, of the numbers in the histogram. -* `Histogram.stddev()`: Standard deviation of the numbers in the histogram. -* `LogFile.open()`: Open the file. -* `LogFile.clear(text)`: Truncate the file, and write `text` if specified. -* `LogFile.close()`: Close the file. -* `Reportable.next()`: clear out the interval statistic for the next window. - -Refer to the `stats.js` for the return value of the `get()` and `summary()` functions for the different classes. - - - -## HTTP-specific Monitors ## - -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`. - -**Functions:** - -* `monitorLatenciesLoop(latencies, fun)`: Call `fun()` and put the execution duration in `latencies`, which should be a `Histogram`. -* `monitorResultsLoop(results, fun)`: Call `fun()` and put the HTTP response code in `results`, which should be a `ResultsCounter`. -* `monitorByteReceivedLoop(bytesReceived, fun)`: Call `fun()` and put the number of bytes received in `bytesReceived`, usually an `Accumulator`. -* `monitorConcurrencyLoop(concurrency, fun)`: Call `fun()` and put the number of "threads" currently executing it into `concurrency`, usually a `Peak`. -* `monitorRateLoop(rate, fun)`: Call `fun()` and notify `rate`, which should be a `Rate`, that it was called. -* `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`. -* `monitorUniqueUrlsLoop(uniqs, fun)`: Call `fun()` and put the HTTP request path into `uniqs`, which should be a `Uniques`. -* `loopWrapper(fun, start, finish)`: Create a custom loop wrapper by specifying a functions to execute before and after calling `fun()`. - -**Usage:** - -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. - -Example: - - // Issue GET requests to random objects at localhost:8080/data/obj-{0-1000} for 1 minute and - // track the number of unique URLs - var uniq = new nl.Reportable(Uniques, 'Uniques'); - var loop = nl.LoopUtils.monitorUniqueUrlsLoop(uniq, function(loopFun, client) { - var req = nl.traceableRequest(client, 'GET', '/data/obj-' + Math.floor(Math.random()*1000)); - req.on('response', function(res) { - loopFun({req: req, res: res}); - }); - req.end(); - }); - SCHEDULER.schedule({ - fun: loop, - args: http.createClient(8080, 'localhost'), - duration: 60 - }).start(function() { - console.log(JSON.stringify(uniq.summary())); - }); - - - -## Web-based Reports ## - -Functions for manipulating the report that is available during the test at http://localhost:8000/ and that is written to `results-{timestamp}-summary.html`. - -**Interface:** - -* `REPORT_MANAGER.reports`: All of the reports that are displayed in the summary webpage. -* `REPORT_MANAGER.addReport(Report)`: Add a report object to the webpage. -* `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. -* `Report.summary`: A JSON object displayed in table form in the summary webpage right side bar. -* `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. -* `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. - -**Usage:** - -An HTTP server is started on port 8000 by default. Use: - - `var nl = require('./lib/nodeloadlib).disableServer()` - -to disable the HTTP server, or - - `var nl = require('./lib/nodeloadlib).usePort(port)` - -to change the port binding. The file `results-{timestamp}-summary.html` is written to the current directory. Use - - `var nl = require('./lib/nodeloadlib).disableLogs()` - -to disable creation of this file. - -A report is automatically added for each test created by `addTest()` or `runTest()`. To add additional charts to the summary webpage: - - var mycounter = 0; - REPORT_MANAGER.addReport(new Report("My Report", function(report) { - chart = report.getChart("My Chart"); - chart.put({ 'counter': mycounter++ }); - chart.summary = { 'Total increments': mycounter }; - })); - -The webpage automatically issues an AJAX request to refresh the text and chart data every 2 seconds by default. Change the refresh period using: - - `var nl = require('./lib/nodeloadlib).setAjaxRefreshIntervalMs(milliseconds)` - - -TIPS AND TRICKS -================ - -Some handy features worth mentioning. - -1. **Examine and add to stats to the HTML page:** - - addTest().stats and runTest().stats are maps: - - { 'latency': Reportable(Histogram), - 'result-codes': Reportable(ResultsCounter}, - 'uniques': Reportable(Uniques), - 'concurrency': Reportable(Peak) } - - Put `Reportable` instances to this map to have it automatically updated each reporting interval and added to the summary webpage. - -2. **Post-process statistics:** - - Use a `startTests()` callback to examine the final statistics in `test.stats[name].cumulative` at test completion. - - // GET random URLs of the form localhost:8080/data/object-#### for 10 seconds, then - // print out all the URLs that were hit. - var t = addTest({ - timeLimit: 10, - targetRps: 10, - stats: ['uniques'], - requestGenerator: function(client) { - return traceableRequest(client, 'GET', '/data/object-' + Math.floor(Math.random()*100));; - } - }); - function printAllUrls() { - console.log(JSON.stringify(t.stats['uniques'].cumulative)); - } - startTests(printAllUrls); - - -3. **Out-of-the-box file server:** - - Just start `nodeloadlib.js` and it will serve files in the current directory. - - $ node lib/nodeloadlib.js - $ curl -i localhost:8000/lib/nodeloadlib.js # executed in a separate terminal - HTTP/1.1 200 OK - Content-Length: 50763 - Connection: keep-alive - - var sys = require('sys'); - var http = require('http'); - ... - -4. **Run arbitrary Javascript:** - - POST any valid Javascript to `/remote` to have it `eval()`'d. - - $ node dist/nodeloadlib.js - Serving progress report on port 8000. - Opening log files. - Received remote command: - sys.puts("hello!") - hello! - - $ curl -i -d 'sys.puts("hello!")' localhost:8000/remote # executed in a separate terminal diff --git a/README.md b/README.md index 756f6f4..acdd977 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,165 @@ -NODELOAD +INSTALLING ================ -`nodeload` is both a **standalone tool** and a **`node.js` library** for load testing HTTP services. +Using [npm](http://npmjs.org/): + + curl http://npmjs.org/install.sh | sh # install npm if not already installed + npm install nodeload -See [NODELOADLIB.md](http://github.com/benschmaus/nodeload/blob/master/NODELOADLIB.md) for using `nodeload` as a `node.js` library. +From source: -See [NODELOAD.md](http://github.com/benschmaus/nodeload/blob/master/NODELOAD.md) for instructions on using the standalone load test tool. + git clone git://github.com/benschmaus/nodeload.git + cd nodeload + npm link # optional. enables require('nodeload/') instead of require('./lib/'). +Or as a single file (this does not install the `nl.js` tool): + wget https://github.com/benschmaus/nodeload/raw/master/nodeload.js -NODELOAD QUICKSTART +NODELOAD ================ -1. Install node.js. -2. Clone nodeload. -3. cd into nodeload working copy. -4. git submodule update --init -5. Start testing! +`nodeload` is collection of [node.js](http://nodejs.org/) modules for load testing HTTP services. -nodeload contains a toy server that you can use for a quick demo. -Try the following: +As a developer, you should be able write load tests and get informative reports without having to learn another framework. You should be able to build tests by example and selectively use the parts of the tool that fit your task. Being a library means that you can use as much or as little of `nodeload` as makes sense, and you can create load tests with the power of a full programming language. For example, if you need to execute some function at a given rate, just use the [`'nodeload/loop'`](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) module, and write the rest yourself. - [~/code/nodeload] node examples/test-server.js & - [1] 2756 - [~/code/nodeload] Server running at http://127.0.0.1:8080/ - [~/code/nodeload] ./dist/nodeload.js -f -c 10 -n 10000 -i 1 -r ../examples/test-generator.js localhost:8080 +In addition, `nodeload` is built for operability. It can always be deployed by simply copying the single file, `nodeload.js`. -You should now see some test output in your console. The generated webpage contains a graphical chart of test results. +Here are examples of each module, which can be used separately. Look for more examples in the [`examples/`](https://github.com/benschmaus/nodeload/tree/master/examples) directory and in test cases prefixed with "example" in [`test/`](https://github.com/benschmaus/nodeload/tree/master/test): +### [nl](https://github.com/benschmaus/nodeload/tree/master/doc/nl.md) +`nl` is an [Apache Bench (ab)](http://httpd.apache.org/docs/2.0/programs/ab.html) like command line tool for running tests quickly. See the [nl documentation](https://github.com/benschmaus/nodeload/tree/master/doc/nl.md) for details. -NODELOADLIB QUICKSTART -================ + $ examples/test-server.js & # starts a simple server on port 9000 to load test + $ ./nl.js -c 10 -n 10000 -i 2 localhost:9000 + +will send 10,000 queries to http://localhost:9000 using 10 connections. Statistics are printed to the console and graphs can be seen at . + +### [nodeload](https://github.com/benschmaus/nodeload/tree/master/doc/nodeload.md) + +The `nodeload` module is the primary interface for creating load tests. It includes all of the other modules described below, so if you `require('nodeload')`, you don't need to `require()` any of the other ones. Look at the examples in [`examples/loadtesting.ex.js`](https://github.com/benschmaus/nodeload/tree/master/examples/loadtesting.ex.js) and [`examples/riaktest.ex.js`]((https://github.com/benschmaus/nodeload/tree/master/examples/riaktest.ex.js) or read the [nodeload module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/nodeload.md). + + var nl = require('nodeload'); + var loadtest = nl.run({ + host: 'localhost', + port: 9000, + timeLimit: 60, + targetRps: 500, + requestGenerator: function(client) { + var request = client.request('GET', "/" + Math.floor(Math.random()*10000)); + request.end(); + return request; + } + }); + loadtest.on('end', function() { console.log('Load test done.'); }); + +### [remote](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md) -* **Write a load test:** +The `remote` module provides a mechanism for running a distributed load test. See the [remote module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md). - $ vi example.js ## Add the following text to example.js +Start slave instances: + + $ HTTP_PORT=10001 ./nodeload.js # start a local slave instance on :10001 + $ HTTP_PORT=10002 ./nodeload.js # start a 2nd slave instance on :10002 - // This test will hit localhost:8080 with 20 concurrent connections for 10 minutes. - var http = require('http'), - nl = require('./lib/nodeloadlib'); - - http.createServer(function (req, res) { res.writeHead(200); res.end(); }).listen(8080); - console.log("Server to load test listening on 8080.") - - nl.runTest({ - host: 'localhost', - port: 8080, - numClients: 20, - timeLimit: 600, - successCodes: [200], - targetRps: 200, - requestLoop: function(loopFun, client) { - var url = '/data/object-' + Math.floor(Math.random()*10000), - req = nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - req.on('response', function(res) { - loopFun({req: req, res: res}); - }); - req.end(); - } - }); - - $ node example.js ## while running, browse to http://localhost:8000 - Listening on 8080. - Opening log files. - Started HTTP server on port 8000. - ......done. - Finishing... - Shutdown HTTP server. - - Browse to http://localhost:8000 during the test for graphs. Non-200 responses are logged to `results-{timestamp}-err.log`, `results-{timestamp}-stats.log` contains statistics, and the summary web page is written to `results-{timestamp}-summary.html`. Check out [examples/nodeloadlib-ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/nodeloadlib-ex.js) for a example of a full read+write test. - -* **Run a function at given rate:** - - // Print 0..19 over 10 seconds - var nl = require('./lib/nodeloadlib').disableServer(); - var i = 0; - - new nl.Job({ - rps: 2, // run 2 times/sec - duration: 10, // run for 10 seconds - fun: function(loopFun) { - console.log(i++); - loopFun(); - } +Create the distributed load test: + + var nl = require('nodeload/remote'); + var cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8002', 'localhost:8001']); + cluster.run({ + host: 'localhost', + port: 9000, + timeLimit: 60, + targetRps: 500, + requestGenerator: function(client) { + var request = client.request('GET', "/" + Math.floor(Math.random()*10000)); + request.end(); + return request; + } + }); + cluster.on('end', function() { console.log('Load test done.'); }); + +### [stats](https://github.com/benschmaus/nodeload/tree/master/doc/stats.md) + +The `stats` module provides implementations of various statistics objects, like Histograms and Accumulators, and functions, like randomString(), and nextGaussian(). See the [stats module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/stats.md). + + var stats = require('nodeload/stats'); + var histogram = new stats.Histogram(); + for (var i = 0; i < 1000; i++) + histogram.put(Math.abs(Math.floor(stats.nextGaussian()))); + console.log('Mean: ' + histogram.mean() + ', 99%: ' + histogram.percentile(0.99)); + +will output "`Mean: 0.852, 99%: 3`". + +### [monitoring](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md) + +The `monitoring` module provides a way to track runtime statistics for code that is run concurrently. See the [monitoring module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md). + + var monitoring = require('nodeload/monitoring'); + var monitor = new monitoring.Monitor('runtime'); + function asyncFunction() { + var m = monitor.start(); + setTimeout(function() { m.end(); }, Math.floor(Math.random()*1000)); + } + for (var i = 0; i < 1000; i++) { asyncFunction(); } + process.on('exit', function() { + console.log('Median runtime (ms): ' + monitor.stats['runtime'].percentile(0.5)); + }); + +will output "`Median runtime (ms): 497`". + +### [reporting](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md) + +The `reporting` module provides a way to produce HTML graphs from code. See the [reporting module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md). + + var reporting = require('nodeload/reporting'), stats = require('nodeload/stats'), + report = reporting.REPORT_MANAGER.addReport('Random Numbers'), + chart = report.getChart('Gaussian / Pareto vs. Time (minutes)'); + for (var i = 0; i < 10; i++) { + setTimeout(function() { + chart.put({'Pareto': stats.nextPareto(0, 100), 'Gaussian': stats.nextGaussian()}); + }, i * 500); + } + +will display a graph on http://localhost:8000/ and save it to an HTML file in the local directory. + +### [loop](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) + +The `loop` module provides a way to execute a function at a set rate and concurrency. See [`test/loop.test.js`](https://github.com/benschmaus/nodeload/tree/master/test/loop.test.js) for examples and read the [loop module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) for details. + + var http = require('http'), + loop = require('nodeload/loop'), + requests = 0, + client = http.createClient(80, 'www.google.com'), + l = new loop.MultiLoop({ + fun: function(finished) { + client.request('GET', '/').end(); + requests++; + finished(); + }, + rps: 10, + duration: 3, + concurrency: 5 }).start(); + l.on('end', function() { console.log('Total requests: ' + requests) }); + +will output "`Total requests: 30`". + +### [http](https://github.com/benschmaus/nodeload/tree/master/doc/http.md) + +The `http` module provides a generic HTTP server that serves static files and that can be configured with new routes. See the [http module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/http.md). + + var http = require('nodeload/http'); + var server = new http.HttpServer().start(10000); + server.addRoute('^/hello$', function(url, req, res) { + res.writeHead(200); + res.end("Hello"); + }); + + will output the contents of `./package.json`, and will display "Hello". + + +CONTRIBUTING +================ +Contributions are always welcome. File bugs on [github](https://github.com/benschmaus/nodeload/issues), email any of the authors, and fork away! [developers.md](https://github.com/benschmaus/nodeload/tree/master/doc/developers.md) has brief instructions on getting tests up and running, and will hold more design details in the future. \ No newline at end of file diff --git a/doc/developers.md b/doc/developers.md new file mode 100644 index 0000000..05c2e1a --- /dev/null +++ b/doc/developers.md @@ -0,0 +1,19 @@ +# Setting up + +First, it's recommended that [`npm`](http://npmjs.org/) is installed. Just run: + + [~/]> curl http://npmjs.org/install.sh | sh + +The clone nodeload and run `npm link` + + [~/]> git clone git://github.com/benschmaus/nodeload.git + [~/]> cd nodeload + [~/nodeload]> npm link + +which will installs the unit testing framework [expresso](http://visionmedia.github.com/expresso) and puts a symlink to `nodeload` in the node library path. + +Use expresso to run the tests under test/: + + [~/nodeload]> expresso + + 100% 20 tests diff --git a/doc/loop.md b/doc/loop.md new file mode 100644 index 0000000..fac1818 --- /dev/null +++ b/doc/loop.md @@ -0,0 +1,102 @@ +**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).** + +## Function Scheduler ## + +The `SCHEDULER` object allows a function to be called at a desired rate and concurrency level. See `scheduler.js`. +**Functions:** + +* `SCHEDULER.schedule(spec)`: Schedule a function to be executed (see the **Schedule Definition** below) +* `SCHEDULER.startAll(callback)`: Start running all the scheduled functions and execute callback when they complete +* `SCHEDULER.startSchedule(callback)`: Start a single scheduled function and execute callback when it completes +* `funLoop(fun)`: Wrap functions that do not perform IO so they can be used with SCHEDULER + +**Usage**: + +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. + +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. + +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. + +Example: + + var t = 1; + nl.SCHEDULER.schedule({ + fun: nl.LoopUtils.funLoop(function(i) { console.log("Thread " + i) }), + argGenerator: function() { return t++; }, + concurrency: 5, + rps: 10, + duration: 10 + }); + nl.SCHEDULER.startAll(function() { sys.puts("Done.") }); + +Alternatively, a Job can started independently. A Job instance is analogous to a single thread, and does not understand the `concurrency` parameter. + + var i = 0; + var job = new nl.Job({ + fun: nl.LoopUtils.funLoop(function() { console.log(i++) }), + rps: 10, + duration: 10 + }).start(); + +**Job Definition**: The following object defines the parameters and defaults for a job run by `SCHEDULER`: + + var JOB_DEFAULTS = { + fun: null, // A function to execute which accepts the parameters (loopFun, args). + // The value of args is the return value of argGenerator() or the args + // parameter if argGenerator is null. The function must call + // loopFun(results) when it completes. + argGenerator: null, // A function which is called once when the job is started. The return + // value is passed to fun as the "args" parameter. This is useful when + // concurrency > 1, and each "thread" should have its own args. + args: null, // If argGenerator is NOT specified, then this is passed to the fun as "args". + concurrency: 1, // Number of concurrent calls of fun() + rps: Infinity, // Target number of time per second to call fun() + duration: Infinity, // Maximum duration of this job in seconds + numberOfTimes: Infinity, // Maximum number of times to call fun() + delay: 0, // Seconds to wait before calling fun() for the first time + monitored: true // Does this job need to finish in order for SCHEDULER.startAll() to end? + }; + + +## Event-based loops ## + +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`. + +**Functions:** + +* `ConditionalLoop(fun, args, conditions, delay):` Defines a loop (see **Loop Definition** below) +* `ConditionalLoop.start(callback):` Starts executing and call `callback` on termination +* `ConditionalLoop.stop():` Terminate the loop +* `LoopConditions.timeLimit(seconds)`, `LoopConditions.maxExecutions(numberOfTimes)`: useful ConditionalLoop conditions +* `LoopUtils.rpsLoop(rps, fun)`: Wrap a `function(loopFun, args)` so ConditionalLoop calls it a set rate +* `LoopUtils.funLoop(fun)`: Wrap a linearly executing `function(args)` so it can be used with a ConditionalLoop + +**Usage:** + +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()`. + +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. + +The loop also terminates if `ConditionalLoop.stop()` is called. + +Example: + + var fun = function(loopFun, startTime) { + console.log("It's been " + (new Date() - startTime) / 1000 + " seconds"); + loopFun(); + }; + var stopOnFriday = function() { + return (new Date()).getDay() < 5; + } + var loop = new nl.ConditionalLoop(nl.LoopUtils.rpsLoop(1, fun), new Date(), [stopOnFriday, nl.LoopConditions.timeLimit(604800 /*1 week*/)], 1); + loop.start(function() { console.log("It's Friday!") }); + +**Loop Definition:** + +The `ConditionalLoop` constructor arguments are: + + fun: Function that takes parameters (loopFun, args) and calls loopFun() after each iteration + args: The args parameter to pass to fun + conditions: A list of functions representing termination conditions. Terminate when any function returns `false`. + delay: Seconds to wait before starting the first iteration \ No newline at end of file diff --git a/doc/monitoring.md b/doc/monitoring.md new file mode 100644 index 0000000..ead2c83 --- /dev/null +++ b/doc/monitoring.md @@ -0,0 +1,64 @@ +**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).** + +## Monitoring ## + +`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`. + +To set the interval between 'update' events: + + var nl = require('./lib/nodeloadlib').setMonitorIntervalMs(seconds) + +**Events:** + +* `TEST_MONITOR.on('test', callback(test))`: `addTest()` was called. The newly created test is passed to `callback`. +* `TEST_MONITOR.on('start', callback(tests))`: `startTests()` was called. The list of tests being started is passed to `callback`. +* `TEST_MONITOR.on('end', callback(tests))`: All tests finished. +* `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. +* `TEST_MONITOR.on('afterUpdate', callback(tests))`: Emitted after the 'update' event. + +**Usage**: + + nl.TEST_MONITOR.on('update', function(tests) { + for (var i in tests) { + console.log(JSON.stringify(tests[i].stats['latency'].summary())) + } + }); + +## HTTP-specific Monitors ## + +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`. + +**Functions:** + +* `monitorLatenciesLoop(latencies, fun)`: Call `fun()` and put the execution duration in `latencies`, which should be a `Histogram`. +* `monitorResultsLoop(results, fun)`: Call `fun()` and put the HTTP response code in `results`, which should be a `ResultsCounter`. +* `monitorByteReceivedLoop(bytesReceived, fun)`: Call `fun()` and put the number of bytes received in `bytesReceived`, usually an `Accumulator`. +* `monitorConcurrencyLoop(concurrency, fun)`: Call `fun()` and put the number of "threads" currently executing it into `concurrency`, usually a `Peak`. +* `monitorRateLoop(rate, fun)`: Call `fun()` and notify `rate`, which should be a `Rate`, that it was called. +* `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`. +* `monitorUniqueUrlsLoop(uniqs, fun)`: Call `fun()` and put the HTTP request path into `uniqs`, which should be a `Uniques`. +* `loopWrapper(fun, start, finish)`: Create a custom loop wrapper by specifying a functions to execute before and after calling `fun()`. + +**Usage:** + +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. + +Example: + + // Issue GET requests to random objects at localhost:8080/data/obj-{0-1000} for 1 minute and + // track the number of unique URLs + var uniq = new nl.Reportable(Uniques, 'Uniques'); + var loop = nl.LoopUtils.monitorUniqueUrlsLoop(uniq, function(loopFun, client) { + var req = nl.traceableRequest(client, 'GET', '/data/obj-' + Math.floor(Math.random()*1000)); + req.on('response', function(res) { + loopFun({req: req, res: res}); + }); + req.end(); + }); + SCHEDULER.schedule({ + fun: loop, + args: http.createClient(8080, 'localhost'), + duration: 60 + }).start(function() { + console.log(JSON.stringify(uniq.summary())); + }); \ No newline at end of file diff --git a/NODELOAD.md b/doc/nl.md similarity index 53% rename from NODELOAD.md rename to doc/nl.md index 6c85092..ff60581 100644 --- a/NODELOAD.md +++ b/doc/nl.md @@ -1,76 +1,64 @@ NAME ---- - nodeload - Load test tool for HTTP APIs. Generates result charts and has hooks for generating requests. + nl - Load test tool for HTTP APIs. Generates result charts and has hooks + for generating requests. SYNOPSIS -------- - nodeload.js [options] :[] + nl.js [options] :[] DESCRIPTION ----------- - nodeload is for generating lots of requests to send to an HTTP API. It is + nl is for generating lots of requests to send to an HTTP API. It is inspired by Apache's ab benchmark tool and is designed to let programmers develop load tests and get informative reports without having to learn a - big and complicated framework.. + big and complicated framework. OPTIONS ------- - -n, --number NUMBER Number of requests to make. Defaults to - value of --concurrency unless a time limit - is specified. + -n, --number NUMBER Number of requests to make. Defaults to + value of --concurrency unless a time limit is specified. -c, --concurrency NUMBER Concurrent number of connections. Defaults to 1. -t, --time-limit NUMBER Number of seconds to spend running test. No timelimit by default. -e, --request-rate NUMBER Target number of requests per seconds. Infinite by default -m, --method STRING HTTP method to use. -d, --data STRING Data to send along with PUT or POST request. - -f, --flot-chart If set, generate an HTML page with a Flot chart of results. -r, --request-generator STRING Path to module that exports getRequest function - -i, --report-interval NUMBER Frequency in seconds to report statistics + -i, --report-interval NUMBER Frequency in seconds to report statistics. Default is 10. -q, --quiet Supress display of progress count info. - -u, --usage Show usage info + -h, --help Show usage info + ENVIRONMENT ----------- - nodeload requires node to be installed somewhere on your path. - - To get a known working combination of nodeload + node grab a release - download or checkout a release tag. - - To find a version of node that's compatible with a tag release do - git show . + nl requires node to be installed somewhere on your path. Get it + from http://nodejs.org/#download. - For example: git show v0.1.1 + To get a known working combination of nodeload + node, be sure + to install using npm: + $ curl http://npmjs.org/install.sh | sh # installs npm + $ npm install nodeload + QUICKSTART ---------- - 1. Install node.js. - 2. Clone nodeload. - 3. cd into nodeload working copy. - 4. git submodule update --init - 5. Start testing! nodeload contains a toy server that you can use for a quick demo. Try the following: - [~/code/nodeload] node examples/test-server.js & - [1] 2756 - [~/code/nodeload] Server running at http://127.0.0.1:8000/ - [~/code/nodeload] ./nodeload.js -f -c 10 -n 10000 -i 1 -r ./examples/test-generator.js localhost:8000 + $ examples/test-server.js & + [1] 2756 + $ Server running at http://127.0.0.1:9000/ + $ nl.js -f -c 10 -n 10000 -i 1 -r examples/test-generator.js localhost:9000 You should now see some test output in your console. The generated HTML report contains a graphical chart of test results. -AUTHORS -------- - - Benjamin Schmaus - Jonathan Lee - THANKS ------ diff --git a/doc/nodeload.md b/doc/nodeload.md new file mode 100644 index 0000000..2850873 --- /dev/null +++ b/doc/nodeload.md @@ -0,0 +1,202 @@ +The `nodeload` module contains a high level interface for constructing load tests for HTTP services. It also includes all of the other modules: [remote](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md), [stats](https://github.com/benschmaus/nodeload/tree/master/doc/stats.md), [monitoring](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md), [reporting](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md), [loop](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md), and [http](https://github.com/benschmaus/nodeload/tree/master/doc/http.md). + +# Quickstart + + $ cat > example.js < http.ClientRequest`. Requests returned by this function are executed by `nodeload`. For example, you can GET random URLs using a `requestGenerator`: + + nl.run({ + requestGenerator: function(client) { + client.request(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); + } + }); + +* Set `requestLoop` to a `function(finished, http.Client)` which calls `finished({req: http.ClientRequest, res: http.ClientResponse})` after each request completes. This is the most flexibile, but the function must be sure to call `finished()`. For example, issue `PUT` requests with proper `If-Match` headers using a `requestLoop`: + + nl.run({ + requestLoop: function(finished, client) { + var req = client.request('GET', '/resource'); + req.on('response', function(res) { + if (res.statusCode !== 200 && res.statusCode !== 404) { + finished({req: req, res: res}); + } else { + var headers = res.headers['etag'] ? {'if-match': res.headers['etag']} : {}; + req = client.request('PUT', '/resource', headers); + req.on('response', function(res) { + finished({req: req, res: res}); + }); + req.end("new value"); + } + }); + req.end(); + } + }); + +Check out [examples/riaktest.ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/riaktest.ex.js) for an example of a full read+write test. + +### Events: + +`run()` returns a `nl.LoadTest` object, which emits these events: + +* `'update', interval, stats`: + + `interval` and `stats` both contains { 'test-name': { 'statistic-name': StatsObject } }. e.g. + + { + 'Read': { + 'latency': [object stats.Histogram], + 'result-codes': [object stats.ResultsCounter] + } + } + + `interval` contains the statistics gathered since the last `'update'` event. `stats` contains cumulative statistics since the beginning of the test. + + Set the frequency of 'update' events in milliseconds by changing loadtest.updateInterval. + +* 'end': all tests finished + + +### Load and User Profiles: + +Profiles can be used to adjust the load and number of users (concurrency) during a load test. The following will linearly ramp up from 0 to 100 req/sec over the first 10 seconds and ramp back down to 0 in the last 10 seconds. It will also ramp up from 0 to 10 users over the first 10 seconds. + + nl.run({ + timeLimit: 40, + loadProfile: [[0,0], [10, 100], [30, 100], [39, 0]], + userProfile: [[0,0], [20, 10]], + }); + + +### Other options: + +The global HTTP server will automatically shutdown after `run(...)` finishes and emits the `'end'` event. This allows the process to terminate after the load test finishes if nothing else is running. To keep the server running, set stayAlive: + + var loadtest = nl.run(...); + loadtest.stayAlive = true; + +### Test Definition: + +The following object defines the parameters and defaults for a test, which is used by `run()`: + + var TEST_OPTIONS = { + name: 'Debug test', // A descriptive name for the test + + // Specify one of: + host: 'localhost', // 1. (host, port) to connect to via HTTP + port: 8080, // + // + connectionGenerator: undefined, // 2. connectionGenerator(), called once for each user. + // The return value is passed as-is to requestGenerator, + // requestLoop, or used internally to generate requests + // when using (method + path + requestData). + + // Specify one of: + requestGenerator: undefined, // 1. requestGenerator: a function + // function(http.Client) -> http.ClientRequest + requestLoop: undefined, // 2. requestLoop: is a function + // function(loopFun, http.Client) + method: 'GET', // If must call: + path: '/', // loopFun({ + requestData: undefined, // req: http.ClientRequest, + // res: http.ClientResponse}); + // after each transaction to finishes to schedule the + // next iteration of requestLoop. + // 3. (method + path + requestData) specify a single URL to + // test + // + + // Specify one of: + numUsers: 10, // 1. numUsers: number of virtual users concurrently + // executing therequest loop + loadProfile: undefined, // 2. loadProfile: array with requests/sec over time: + // [[time (seconds), rps], [time 2, rps], ...] + // For example, ramp up from 100 to 500 rps and then + // down to 0 over 20 seconds: + // [[0, 100], [10, 500], [20, 0]] + + // Specify one of: + targetRps: Infinity, // 1. targetRps: times per second to execute request loop + userProfile: undefined, // 2. userProfile: array with number of users over time: + // [[time (seconds), # users], [time 2, users], ...] + // For example, ramp up from 0 to 100 users and back + // down to 0 over 20 seconds: + // [[0, 0], [10, 100], [20, 0]] + + numRequests: Infinity, // Maximum number of iterations of request loop + timeLimit: 120, // Maximum duration of test in seconds + delay: 0, // Seconds before starting test + + stats: ['latency', // Specify list of: 'latency', 'result-codes', 'uniques', + 'result-codes'], // 'concurrency', 'http-errors'. These following statistics + // may also be specified with parameters: + // + // { name: 'latency', percentiles: [0.9, 0.99] } + // { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' } + // + // Extend this list of statistics by adding to the + // monitor.js#Monitor.Monitors object. + // + // Note: + // - for 'uniques', traceableRequest() must be used + // to create the ClientRequest or only 2 will be detected. + }; \ No newline at end of file diff --git a/doc/remote.md b/doc/remote.md new file mode 100644 index 0000000..d7529a9 --- /dev/null +++ b/doc/remote.md @@ -0,0 +1,37 @@ +**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).** + +## Distributed Testing ## + +Functions to distribute tests across multiple slave `nodeload` instances. See `remote.js`. + +**Functions:** + +* `remoteTest(spec)`: Return a test to be scheduled with `remoteStart(...)` (`spec` uses same format as `addTest(spec)`). +* `remoteStart(master, slaves, tests, callback, stayAliveAfterDone)`: Run tests on specified slaves. +* `remoteStartFile(master, slaves, filename, callback, stayAliveAfterDone)`: Execute a `.js` file on specified slaves. + +**Usage**: + +First, start `nodeloadlib.js` on each slave instances. + + $ node dist/nodeloadlib.js # Run on each slave machine + +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. + + // This script must be run on master:8000, which will aggregate results. Each slave + // will GET http://internal-service:8080/ at 100 rps. + var t1 = nl.remoteTest({ + name: "Distributed test", + host: 'internal-service', + port: 8080, + timeLimit: 20, + targetRps: 100 + }); + nl.remoteStart('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], [t1]); + +Alternatively, an existing `nodeload` script file can be used: + + // The file /path/to/load-test.js should contain valid javascript and can use any nodeloadlib functions + nl.remoteStartFile('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], '/path/to/load-test.js'); + +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`. \ No newline at end of file diff --git a/doc/reporting.md b/doc/reporting.md new file mode 100644 index 0000000..6b265da --- /dev/null +++ b/doc/reporting.md @@ -0,0 +1,43 @@ +**This document is out-of-date. See [`lib/reporting.js`](https://github.com/benschmaus/nodeload/tree/master/lib/reporting.js).** + +## Web-based Reports ## + +Functions for manipulating the report that is available during the test at http://localhost:8000/ and that is written to `results-{timestamp}-summary.html`. + +**Interface:** + +* `REPORT_MANAGER.reports`: All of the reports that are displayed in the summary webpage. +* `REPORT_MANAGER.addReport(Report)`: Add a report object to the webpage. +* `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. +* `Report.summary`: A JSON object displayed in table form in the summary webpage right side bar. +* `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. +* `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. + +**Usage:** + +An HTTP server is started on port 8000 by default. Use: + + `var nl = require('./lib/nodeloadlib).disableServer()` + +to disable the HTTP server, or + + `var nl = require('./lib/nodeloadlib).usePort(port)` + +to change the port binding. The file `results-{timestamp}-summary.html` is written to the current directory. Use + + `var nl = require('./lib/nodeloadlib).disableLogs()` + +to disable creation of this file. + +A report is automatically added for each test created by `addTest()` or `runTest()`. To add additional charts to the summary webpage: + + var mycounter = 0; + REPORT_MANAGER.addReport(new Report("My Report", function(report) { + chart = report.getChart("My Chart"); + chart.put({ 'counter': mycounter++ }); + chart.summary = { 'Total increments': mycounter }; + })); + +The webpage automatically issues an AJAX request to refresh the text and chart data every 2 seconds by default. Change the refresh period using: + + `var nl = require('./lib/nodeloadlib).setAjaxRefreshIntervalMs(milliseconds)` diff --git a/doc/stats.md b/doc/stats.md new file mode 100644 index 0000000..cdc888c --- /dev/null +++ b/doc/stats.md @@ -0,0 +1,44 @@ +## Statistics ## + +Implementations of various statistics. See [`lib/stats.js`](https://github.com/benschmaus/nodeload/tree/master/lib/stats.js). + +**Classes:** + +* `Histogram(numBuckets)`: A histogram of integers. If most of the items are between 0 and `numBuckets`, calculating percentiles and stddev is fast. +* `Accumulator`: Calculates the sum of the numbers put in. +* `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. +* `Uniques`: Tracks the number of unique items added. +* `Peak`: Tracks the max of the numbers put in. +* `Rate`: Tracks the rate at which items are added. +* `LogFile`: Outputs to a file on disk. +* `NullLog`: Ignores all items put in. +* `Reportable`: Wraps any other statistic to store an interval and cumulative version of it. + +**Functions:** + +* `randomString(length)`: Returns a random string of ASCII characters between 32 and 126 of the requested length. +* `nextGaussian(mean, stddev)`: Returns a normally distributed number using the provided mean and standard deviation. +* `nextPareto(min, max, shape)`: Returns a Pareto distributed number between `min` and `max` inclusive using the provided shape. +* `roundRobin(list)`: Returns a copy of the list with a `get()` method. `get()` returns list entries round robin. + +**Usage:** + +All of the statistics classes support the methods: + +* `.length`: The total number of items `put()` into this object. +* `put(item)`: Include an item in the statistic. +* `get()`: Get a specific value from the object, which varies depending on the object. +* `clear()`: Clear out all items. +* `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. + +In addition, these other methods are supported: + +* `Histogram.mean()`: Calculate the mean of the numbers in the histogram. +* `Histogram.percentile(percentile)`: Calculate the given `percentile`, between 0 and 1, of the numbers in the histogram. +* `Histogram.stddev()`: Standard deviation of the numbers in the histogram. +* `LogFile.open()`: Open the file. +* `LogFile.clear(text)`: Truncate the file, and write `text` if specified. +* `LogFile.close()`: Close the file. +* `Reportable.next()`: clear out the interval statistic for the next window. + +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. \ No newline at end of file diff --git a/doc/tips.md b/doc/tips.md new file mode 100644 index 0000000..74e8e44 --- /dev/null +++ b/doc/tips.md @@ -0,0 +1,64 @@ +**This page is out of date** + +TIPS AND TRICKS +================ + +Some handy features of `nodeload` worth mentioning. + +1. **Examine and add to stats to the HTML page:** + + addTest().stats and runTest().stats are maps: + + { 'latency': Reportable(Histogram), + 'result-codes': Reportable(ResultsCounter}, + 'uniques': Reportable(Uniques), + 'concurrency': Reportable(Peak) } + + Put `Reportable` instances to this map to have it automatically updated each reporting interval and added to the summary webpage. + +2. **Post-process statistics:** + + Use a `startTests()` callback to examine the final statistics in `test.stats[name].cumulative` at test completion. + + // GET random URLs of the form localhost:8080/data/object-#### for 10 seconds, then + // print out all the URLs that were hit. + var t = addTest({ + timeLimit: 10, + targetRps: 10, + stats: ['uniques'], + requestGenerator: function(client) { + return traceableRequest(client, 'GET', '/data/object-' + Math.floor(Math.random()*100));; + } + }); + function printAllUrls() { + console.log(JSON.stringify(t.stats['uniques'].cumulative)); + } + startTests(printAllUrls); + + +3. **Out-of-the-box file server:** + + Just start `nodeloadlib.js` and it will serve files in the current directory. + + $ node lib/nodeloadlib.js + $ curl -i localhost:8000/lib/nodeloadlib.js # executed in a separate terminal + HTTP/1.1 200 OK + Content-Length: 50763 + Connection: keep-alive + + var sys = require('sys'); + var http = require('http'); + ... + +4. **Run arbitrary Javascript:** + + POST any valid Javascript to `/remote` to have it `eval()`'d. + + $ node dist/nodeloadlib.js + Serving progress report on port 8000. + Opening log files. + Received remote command: + sys.puts("hello!") + hello! + + $ curl -i -d 'sys.puts("hello!")' localhost:8000/remote # executed in a separate terminal diff --git a/examples/loadtesting.ex.js b/examples/nodeload.ex.js similarity index 91% rename from examples/loadtesting.ex.js rename to examples/nodeload.ex.js index 8a00e93..fbb54c3 100755 --- a/examples/loadtesting.ex.js +++ b/examples/nodeload.ex.js @@ -22,9 +22,7 @@ var i = 0, userProfile: [[0,0], [20, 10]], stats: ['result-codes', {name: 'latency', percentiles: [0.95, 0.999]}, 'concurrency', 'uniques', 'request-bytes', 'response-bytes'], requestGenerator: function(client) { - var request = client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); - request.end(); - return request; + return client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); } }, writetest = { diff --git a/examples/nodeloadlib-ex.js b/examples/nodeloadlib-ex.js deleted file mode 100755 index fedf833..0000000 --- a/examples/nodeloadlib-ex.js +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node - -// Instructions: -// -// 1. Get node (http://nodejs.org/#download) -// 2. git clone http://github.com/benschmaus/nodeload.git -// 3. node nodeload/examples/nodeloadlib-ex.js -// -// This example performs a micro-benchmark of Riak (http://riak.basho.com/), a key-value store, -// running on localhost:8098/riak. First, it first loads 2000 objects into the store as quickly -// as possible. Then, it performs a 90% read + 10% update test at total request rate of 300 rps. -// From minutes 5-8, the read load is increased by 100 rps. The test runs for 10 minutes. - -var sys = require('sys'), - nl = require('../dist/nodeloadlib'); - -function riakUpdate(loopFun, client, url, body) { - var req = nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - req.on('response', function(response) { - if (response.statusCode != 200 && response.statusCode != 404) { - loopFun({req: req, res: response}); - } else { - var headers = { - 'host': 'localhost', - 'content-type': 'text/plain', - 'x-riak-client-id': 'bmxpYg==' - }; - if (response.headers['x-riak-vclock'] != null) - headers['x-riak-vclock'] = response.headers['x-riak-vclock']; - - req = nl.traceableRequest(client, 'PUT', url, headers, body); - req.on('response', function(response) { - loopFun({req: req, res: response}); - }); - req.end(); - } - }); - req.end(); -} - -var i=0; -nl.runTest({ - name: "Load Data", - host: 'localhost', - port: 8098, - numClients: 20, - numRequests: 2000, - timeLimit: Infinity, - successCodes: [204], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - requestLoop: function(loopFun, client) { - riakUpdate(loopFun, client, '/riak/b/o' + i++, 'original value'); - } -}, startRWTest); - -function startRWTest() { - console.log("Running read + update test."); - - var reads = nl.addTest({ - name: "Read", - host: 'localhost', - port: 8098, - numClients: 30, - timeLimit: 600, - targetRps: 270, - successCodes: [200,404], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - requestGenerator: function(client) { - var url = '/riak/b/o' + Math.floor(Math.random()*8000); - return nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - } - }); - var writes = nl.addTest({ - name: "Write", - host: 'localhost', - port: 8098, - numClients: 5, - timeLimit: 600, - targetRps: 30, - successCodes: [204], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - requestLoop: function(loopFun, client) { - var url = '/riak/b/o' + Math.floor(Math.random()*8000); - riakUpdate(loopFun, client, url, 'updated value'); - } - }); - - // From minute 5, schedule 10x 10 read requests per second in 3 minutes = adding 100 requests/sec - nl.addRamp({ - test: reads, - numberOfSteps: 10, - rpsPerStep: 10, - clientsPerStep: 2, - timeLimit: 180, - delay: 300 - }); - - nl.startTests(); -} diff --git a/examples/nodeloadlib-ex2.js b/examples/nodeloadlib-ex2.js deleted file mode 100755 index a4be4b7..0000000 --- a/examples/nodeloadlib-ex2.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node - -// Self contained node.js HTTP server and a load test against it. Just run: -// -// node examples/nodeloadlib-ex2.js -// -var http = require('http'); -var sys = require('sys'); -var nl = require('../lib/nodeloadlib'); -sys.puts("Test server on localhost:9000."); -http.createServer(function (req, res) { - res.writeHead((Math.random() < .8) ? 200 : 404, {'Content-Type': 'text/plain'}); - res.write('foo\n'); - res.end(); -}).listen(9000); - -var test = nl.addTest({ - name: "Read", - host: 'localhost', - port: 9000, - numClients: 10, - timeLimit: 600, - targetRps: 500, - successCodes: [200,404], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - latencyConf: {percentiles: [.90, .999]}, - requestGenerator: function(client) { - return nl.traceableRequest(client, 'GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); - } -}); - -nl.startTests(); diff --git a/examples/riaktest.ex.js b/examples/riaktest.ex.js new file mode 100755 index 0000000..e9d26a7 --- /dev/null +++ b/examples/riaktest.ex.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +// Instructions: +// +// 1. Get node (http://nodejs.org/#download) +// 2. git clone http://github.com/benschmaus/nodeload.git +// 3. examples/riaktest.ex.js +// +// This example performs a micro-benchmark of Riak (http://riak.basho.com/), a key-value store, +// running on localhost:8098/riak. First, it first loads 2000 objects into the store as quickly +// as possible. Then, it performs a 90% read + 10% update test at total request rate of 300 rps. +// From minutes 5-8, the read load is increased by 100 rps. The test runs for 10 minutes. + +var sys = require('sys'), + nl = require('../nodeload'); + +function riakUpdate(loopFun, client, url, body) { + var req = client.request('GET', url, { 'host': 'localhost' }); + req.on('response', function(res) { + if (res.statusCode !== 200 && res.statusCode !== 404) { + loopFun({req: req, res: res}); + } else { + var headers = { + 'host': 'localhost', + 'content-type': 'text/plain', + 'x-riak-client-id': 'bmxpYg==' + }; + if (res.headers['x-riak-vclock']) { + headers['x-riak-vclock'] = res.headers['x-riak-vclock']; + } + + req = client.request('PUT', url, headers); + req.on('response', function(res) { + loopFun({req: req, res: res}); + }); + req.end(body); + } + }); + req.end(); +} + +var i=0; +var loadData = nl.run({ + name: "Load Data", + host: 'localhost', + port: 8098, + numUsers: 20, + numRequests: 2000, + timeLimit: Infinity, + stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [204], log: 'http-errors.log' }], + requestLoop: function(loopFun, client) { + riakUpdate(loopFun, client, '/riak/b/o' + i++, 'original value'); + } +}); + +loadData.on('end', function() { + console.log("Running read + update test."); + + var reads = { + name: "Read", + host: 'localhost', + port: 8098, + numUsers: 30, + 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 + timeLimit: 600, + stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' }], + requestGenerator: function(client) { + var url = '/riak/b/o' + Math.floor(Math.random()*8000); + return client.request('GET', url, { 'host': 'localhost' }); + } + }, + writes = { + name: "Write", + host: 'localhost', + port: 8098, + numUsers: 5, + timeLimit: 600, + targetRps: 30, + reportInterval: 2, + stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [204], log: 'http-errors.log' }], + requestLoop: function(loopFun, client) { + var url = '/riak/b/o' + Math.floor(Math.random()*8000); + riakUpdate(loopFun, client, url, 'updated value'); + } + }; + + nl.run(reads, writes); +}); \ No newline at end of file diff --git a/examples/sample-report.html b/examples/sample-report.html old mode 100755 new mode 100644 index 1fff25b..253f524 --- a/examples/sample-report.html +++ b/examples/sample-report.html @@ -1,32 +1,113 @@ -Response Times over Time - - - - -

Test Results from Fri Feb 12 2010 16:48:44 GMT-0500 (EST)

-
Server Hostname:                        localhost
-Server Port:                            8000
-Request Generator:                      ./examples/test-generator.js
-Concurrency Level:                      5
-Number of requests:                     25
-Body bytes transferred:                 50
-Elapsed time (s):                       0.04
-Requests per second:                    641.03
-Mean time per request (ms):             6.40
-Time per request standard deviation:    3.31
+    
+        Test Results
+        
+        
+    
 
-Percentages of requests served within a certain time (ms)
-  Min: 1
-  50%: 6
-  90%: 11
-  95%: 11
-  99%: 11
-  Max: 11
-

x = number of requests, y = response times (ms)

-
- - - + + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/examples/simpletest.ex.js b/examples/simpletest.ex.js new file mode 100755 index 0000000..da9bc15 --- /dev/null +++ b/examples/simpletest.ex.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +// Self contained node.js HTTP server and a load test against it. Just run: +// +// $ examples/simpletest.ex.js +// +var http = require('http'); +var nl = require('../nodeload'); +console.log("Test server on localhost:9000."); +http.createServer(function (req, res) { + res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); + res.end('foo\n'); +}).listen(9000); + +nl.run({ + name: "Read", + host: 'localhost', + port: 9000, + numUsers: 10, + timeLimit: 600, + targetRps: 500, + stats: [ + 'result-codes', + { name: 'latency', percentiles: [0.9, 0.99] }, + 'concurrency', + 'uniques', + { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' } + ], + requestGenerator: function(client) { + return client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); + } +}); \ No newline at end of file diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 61fad2c..282c818 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -106,6 +106,7 @@ TEST_OPTIONS for a list of the configuration values in each specification. var run = exports.run = function(specs) { specs = (specs instanceof Array) ? specs : util.argarray(arguments); var tests = specs.map(function(spec) { + spec = util.defaults(spec, TEST_OPTIONS); var generateRequest = function(client) { if (spec.requestGenerator) { return spec.requestGenerator(client); } var request = client.request(spec.method, spec.path, { 'host': spec.host }); @@ -148,23 +149,23 @@ var run = exports.run = function(specs) { /** LoadTest can be started & stopped. Starting it will fire up the global HTTP_SERVER if it is not started. Stopping LoadTest will shut HTTP_SERVER down. The expectation is that only one LoadTest instance is normally running at a time, and when the test finishes, you usually want to let the process end, which -requires stopping HTTP_SERVER. Use start(keepAlive=true) to not shut down HTTP_SERVER when done. +requires stopping HTTP_SERVER. Set loadtest.keepAlive=true to not shut down HTTP_SERVER when done. -LoadTest contains members: +LoadTest contains the members: - tests: a list of the test objects created by run() from each spec, which contains: spec: original specification used by run to create this test object loop: a MultiLoop instance that represents all the "vusers" for this job monitor: a Monitor instance tracking stats from the MultiLoop instance, loop report: a Report which is tracked by REPORT_MANAGER holding a chart for every stat in monitor - - interval: statistics from this current interval + - interval: statistics gathered since the last 'update' event - stats: cumulative statistics - updateInterval: milliseconds between 'update' events, which includes statistics from the previous interval as well as overall statistics. Defaults to 2 seconds. LoadTest emits these events: -- 'update', stats since last update, overall stats: set the frequency of these events using setWindowSizeSeconds(). +- 'update', interval, stats: interval has stats since last update. stats contains overall stats. - 'end': all tests finished */ diff --git a/lib/monitoring/index.js b/lib/monitoring/index.js index 0718bfc..44623c0 100644 --- a/lib/monitoring/index.js +++ b/lib/monitoring/index.js @@ -1,9 +1,4 @@ -// ------------------------------------ -// Monitoring -// ------------------------------------ -// -// This file defines Monitor and MonitorGroup, and StatsLogger -// exports.Monitor = require('./monitor').Monitor; exports.MonitorGroup = require('./monitorgroup').MonitorGroup; -exports.StatsLogger = require('./statslogger').StatsLogger; \ No newline at end of file +exports.StatsLogger = require('./statslogger').StatsLogger; +exports.StatsCollectors = require('./collectors'); \ No newline at end of file diff --git a/lib/options.js b/lib/nl/options.js similarity index 100% rename from lib/options.js rename to lib/nl/options.js diff --git a/lib/nl/optparse-README.md b/lib/nl/optparse-README.md new file mode 100644 index 0000000..d08ccf1 --- /dev/null +++ b/lib/nl/optparse-README.md @@ -0,0 +1,161 @@ +optparse-js +=========== + +Optparse-js is a command line option parser for Javascript. It's slightly based on Ruby's implementation optparse but with some differences (different languages has different needs) such as custom parsers. + +All examples in this readme is using [Node.js](http://nodejs.org/). How ever, the library works with all kinds of Javascript implementations. + + +QUICK START +----------- + +The library defines one class, the OptionParser class. The class constructor takes one single argument, a list with a set of rules. Here is a quick example: + + // Import the sys library + var sys = require('sys'); + + // Import the optparse library. + var optparse = require('optparse'); + + // Define an option called ´´help´´. We give it a quick alias named ´´-h´´ + // and a quick help text. + var switches = [ + ['-h', '--help', 'Shows help sections'] + ]; + + // Create a new OptionParser. + var parser = new optparse.OptionParser(switches); + + // Hook the help option. The callback will be executed when the OptionParser + // hits the switch ´´-h´´ or ´´--help´´. Each representatio + parser.on('help', function() { + sys.puts('Help'); + }); + + + +DEFINING RULES +-------------- +The OptionParser constructor takes an Array with rules. Each rule is represented by an array (tuple) of two or three values. A typical rule definition may look like this: + + ['-h', '--help', 'Print this help'] + + +The first value is optional, and represents an alias for the long-named switch (the second value, in this case ´´--help´´). + +The second argument is the actual rule. The rule must start with a double dash followed by a switch name (in this case ´help´). The OptionParser also supports special option arguments. Define an option argument in the rule by adding a named argument after the leading double dash and switch name (E.G '--port-number PORT_NUMBER'). The argument is then parsed to the option handler. To define an optional option argument, just add a braces around argument in the rule (E.G '--port-number [PORT_NUMBER]). The OptionParser also supports filter. More on that in in the section called ´Option Filters´. + +The third argument is an optional rule description. + + +OPTION FILTERS +-------------- +Filters is a neat feature that let you filter option arguments. The OptionParser itself as already a set of built-in common filter's. These are: + +- NUMBER, supports both decimal and hexadecimal numbers. +- DATE, filters arguments that matches YYYY-MM-DD. +- EMAIL, filters arguments that matches my@email.com. + +It's simple to use any of the filter above in your rule-set. Here is a quick example how to filter number: + + var rules = [ + ['--first-option NUMBER', 'Takes a number as argument'], + ['--second-option [NUMBER]', 'Takes an optional number as argument'] + ] + +You can add your own set of filter by calling the *parser_instance.filter* method: + + parser.filter('single_char', function(value) { + if(value.length != 1) throw "Filter mismatch."; + return value; + }); + + +OPTION PARSER +------------- +The OptionParser class has the following properties and methods: + +### string banner +An optional usage banner. This text is included when calling ´´toString´´. Default value is: "Usage: [Options]". + + +### string options_title +An optional title for the options list. This text is included when calling ´´toString´´. Default value is: "Available options:". + + +### function on(switch_or_arg_index, callback) +Add's a callback for a switch or an argument (defined by index). Switch hooks MUST be typed witout the leading ´´--´´. This example show how to hook a switch: + + parser.on('help', function(optional_argument) { + // Show help section + }); + +And this example show how to hook an argument (an option without the leading - or --): + + parser.on(0, function(opt) { + puts('The first non-switch option is:' + opt); + }); + +It's also possible to define a default handler. The default handler is called when no rule's are meet. Here is an example how to add a ´default handler´: + + parser.on(function(opt) { + puts('No handler was defined for option:' + opt); + }); + +Use the wildcard handler to build a custom ´´on´´ handler. + + parser.on('*', function(opt, value) { + puts('option=' + opt + ', value=' + value); + }); + +### function filter(name, callback) +Adds a new filter extension to the OptionParser instance. The first argument is the name of the filter (trigger). The second argument is the actual filter See the ´OPTION FILTERS´ section for more info. + +It's possible to override the default filters by passing the value "_DEFAULT" to the ´´name´´ argument. The name of the filter is automatically transformed into +upper case. + + +### function halt([callback]) +Interrupt's further parsing. This function should be called from an ´on´ -callbacks, to cancel the parsing. This can be useful when the program should ignore all other arguments (when displaying help or version information). + +The function also takes an optional callback argument. If the callback argument is specified, a ´halt´ callback will be added (instead of executing the ´halt´ command). + +Here is an example how to add an ´on_halt´ callback: + + parser.halt(function() { + puts('An option callback interupted the parser'); + }); + + +### function parse(arguments) +Start's parsing of arguments. This should be the last thing you do. + + +### function options() +Returns an Array with all defined option rules + + +### function toString() +Returns a string representation of this OptionParser instance (a formatted help section). + + +MORE EXAMPLES +------------- +See examples/nodejs-test.js and examples/browser-test-html for more info how to +use the script. + + +SUGGESTIONS +----------- +All comments in how to improve this library is very welcome. Feel free post suggestions to the [Issue tracker](http://github.com/jfd/optparse-js/issues), or even better, fork the repository to implement your own features. + + +LICENSE +------- +Released under a MIT-style license. + + +COPYRIGHT +--------- +Copyright (c) 2009 Johan Dahlberg + diff --git a/lib/optparse.js b/lib/nl/optparse.js similarity index 100% rename from lib/optparse.js rename to lib/nl/optparse.js diff --git a/lib/dygraph.tpl b/lib/reporting/dygraph.tpl similarity index 100% rename from lib/dygraph.tpl rename to lib/reporting/dygraph.tpl diff --git a/lib/reporting.js b/lib/reporting/index.js similarity index 97% rename from lib/reporting.js rename to lib/reporting/index.js index 0e0f1ea..8fcafb7 100644 --- a/lib/reporting.js +++ b/lib/reporting/index.js @@ -10,17 +10,17 @@ // var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { -var util = require('./util'); +var util = require('../util'); var querystring = require('querystring'); -var LogFile = require('./stats').LogFile; +var LogFile = require('../stats').LogFile; var template = require('./template'); -var config = require('./config'); +var config = require('../config'); var REPORT_SUMMARY_TEMPLATE = require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE; var NODELOAD_CONFIG = config.NODELOAD_CONFIG; var START = NODELOAD_CONFIG.START; var DYGRAPH_SOURCE = require('./dygraph.tpl.js').DYGRAPH_SOURCE; -var HTTP_SERVER = require('./http').HTTP_SERVER; +var HTTP_SERVER = require('../http').HTTP_SERVER; } var Chart, timeFromStart; diff --git a/lib/summary.tpl b/lib/reporting/summary.tpl similarity index 100% rename from lib/summary.tpl rename to lib/reporting/summary.tpl diff --git a/lib/template.js b/lib/reporting/template.js similarity index 100% rename from lib/template.js rename to lib/reporting/template.js diff --git a/nl.js b/nl.js index 54f9745..d3b9096 100755 --- a/nl.js +++ b/nl.js @@ -26,15 +26,18 @@ */ /*jslint sub:true */ +/*globals __dirname */ -var options = require('./lib/options'); +require.paths.unshift(__dirname); + +var options = require('./lib/nl/options'); options.process(); if (!options.get('url')) { options.help(); } -var nl = require('./lib/nodeload') +var nl = require('./nodeload') .quiet() .setMonitorIntervalMs(options.get('reportInterval') * 1000); diff --git a/nodeload.js b/nodeload.js index 04835e7..2bb3c24 100755 --- a/nodeload.js +++ b/nodeload.js @@ -1,11 +1,6 @@ #!/usr/bin/env node -var util=require('util'),http=require('http'),url=require('url'),fs=require('fs'),events=require('events'),querystring=require('querystring');var EventEmitter=events.EventEmitter;var START=new Date();var BUILD_AS_SINGLE_FILE=true;var DYGRAPH_SOURCE=exports.DYGRAPH_SOURCE="DygraphLayout=function(b,a){this.dygraph_=b;this.options={};Dygraph.update(this.options,a?a:{});this.datasets=new Array()};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.options.dateWindow){this.minxval=this.options.dateWindow[0];this.maxxval=this.options.dateWindow[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var d=this.datasets[c];var b=d[0][0];if(!this.minxval||bthis.maxxval){this.maxxval=a}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!=0?1/this.xrange:1);this.minyval=this.options.yAxis[0];this.maxyval=this.options.yAxis[1];this.yrange=this.maxyval-this.minyval;this.yscale=(this.yrange!=0?1/this.yrange:1)};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=new Array();for(var e in this.datasets){if(!this.datasets.hasOwnProperty(e)){continue}var d=this.datasets[e];for(var b=0;b=1){a.y=1}this.points.push(a)}}};DygraphLayout.prototype._evaluateLineTicks=function(){this.xticks=new Array();for(var c=0;c=0)&&(d<=1)){this.xticks.push([d,a])}}this.yticks=new Array();for(var c=0;c=0)&&(d<=1)){this.yticks.push([d,a])}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!this.options.errorBars){return}var d=0;for(var g in this.datasets){if(!this.datasets.hasOwnProperty(g)){continue}var c=0;var f=this.datasets[g];for(var c=0;c0){for(var e=0;ethis.height){k.style.bottom=\"0px\"}else{k.style.top=h+\"px\"}k.style.left=\"0px\";k.style.textAlign=\"right\";k.style.width=this.options.yAxisLabelWidth+\"px\";this.container.appendChild(k);this.ylabels.push(k)}var m=this.ylabels[0];var n=this.options.axisLabelFontSize;var a=parseInt(m.style.top)+n;if(a>this.height-n){m.style.top=(parseInt(m.style.top)-n/2)+\"px\"}}b.beginPath();b.moveTo(this.area.x,this.area.y);b.lineTo(this.area.x,this.area.y+this.area.h);b.closePath();b.stroke()}if(this.options.drawXAxis){if(this.layout.xticks){for(var e=0;ethis.width){c=this.width-this.options.xAxisLabelWidth;k.style.textAlign=\"right\"}if(c<0){c=0;k.style.textAlign=\"left\"}k.style.left=c+\"px\";k.style.width=this.options.xAxisLabelWidth+\"px\";this.container.appendChild(k);this.xlabels.push(k)}}b.beginPath();b.moveTo(this.area.x,this.area.y+this.area.h);b.lineTo(this.area.x+this.area.w,this.area.y+this.area.h);b.closePath();b.stroke()}b.restore()};DygraphCanvasRenderer.prototype._renderLineChart=function(){var b=this.element.getContext(\"2d\");var d=this.options.colorScheme.length;var n=this.options.colorScheme;var x=this.options.fillAlpha;var C=this.layout.options.errorBars;var q=this.layout.options.fillGraph;var E=[];for(var F in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(F)){E.push(F)}}var y=E.length;this.colors={};for(var A=0;A0){r=E[A-1]}var v=this.colors[g];s.save();s.strokeStyle=v;s.lineWidth=this.options.strokeWidth;var h=NaN;var f=[-1,-1];var k=0;var B=this.layout.yscale;var a=new RGBColor(v);var D=\"rgba(\"+a.r+\",\"+a.g+\",\"+a.b+\",\"+x+\")\";s.fillStyle=D;s.beginPath();for(var w=0;w1){e=1}}var p=[t.y,e];p[0]=this.area.h*p[0]+this.area.y;p[1]=this.area.h*p[1]+this.area.y;if(!isNaN(h)){s.moveTo(h,f[0]);s.lineTo(t.canvasx,p[0]);s.lineTo(t.canvasx,p[1]);s.lineTo(h,f[1]);s.closePath()}f[0]=p[0];f[1]=p[1];h=t.canvasx}}s.fill()}}}for(var A=0;A0){if(arguments.length==4){this.warn(\"Using deprecated four-argument dygraph constructor\");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME=\"Dygraph\";Dygraph.VERSION=\"1.2\";Dygraph.__repr__=function(){return\"[\"+this.NAME+\" \"+this.VERSION+\"]\"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.AXIS_LINE_WIDTH=0.3;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,pixelsPerXLabel:60,pixelsPerYLabel:30,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,yValueFormatter:function(a){return Dygraph.round_(a,2)},strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueFormatter:Dygraph.dateString_,xValueParser:Dygraph.dateParser,xTicker:Dygraph.dateTicker,delimiter:\",\",logScale:false,sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!=null){var a=[\"Date\"];for(var c=0;c=10){n.doZoom_(Math.min(b,m),Math.max(b,m))}else{n.canvas_.getContext(\"2d\").clearRect(0,0,n.canvas_.width,n.canvas_.height)}b=null;a=null}if(e){e=false;l=null;j=null}});Dygraph.addEvent(this.hidden_,\"dblclick\",function(o){if(n.dateWindow_==null){return}n.dateWindow_=null;n.drawGraph_(n.rawData_);var p=n.rawData_[0][0];var q=n.rawData_[n.rawData_.length-1][0];if(n.attr_(\"zoomCallback\")){n.attr_(\"zoomCallback\")(p,q)}})};Dygraph.prototype.drawZoomRect_=function(c,d,b){var a=this.canvas_.getContext(\"2d\");if(b){a.clearRect(Math.min(c,b),0,Math.abs(c-b),this.height_)}if(d&&c){a.fillStyle=\"rgba(128,128,128,0.33)\";a.fillRect(Math.min(c,d),0,Math.abs(d-c),this.height_)}};Dygraph.prototype.doZoom_=function(d,a){var b=this.toDataCoords(d,null);var c=b[0];b=this.toDataCoords(a,null);var e=b[0];this.dateWindow_=[c,e];this.drawGraph_(this.rawData_);if(this.attr_(\"zoomCallback\")){this.attr_(\"zoomCallback\")(c,e)}};Dygraph.prototype.mouseMove_=function(b){var a=Dygraph.pageX(b)-Dygraph.findPosX(this.hidden_);var s=this.layout_.points;var m=-1;var j=-1;var q=1e+100;var r=-1;for(var f=0;fq){continue}q=h;r=f}if(r>=0){m=s[r].xval}if(a>s[s.length-1].canvasx){m=s[s.length-1].xval}this.selPoints_=[];var g=0;var d=s.length;var o=this.attr_(\"stackedGraph\");if(!this.attr_(\"stackedGraph\")){for(var f=0;f=0;f--){if(s[f].xval==m){var c={};for(var e in s[f]){c[e]=s[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}}if(this.attr_(\"highlightCallback\")){var n=this.lastHighlightCallbackX;if(n!==null&&m!=n){this.lastHighlightCallbackX=m;this.attr_(\"highlightCallback\")(b,m,this.selPoints_)}}this.lastx_=m;this.updateSelection_()};Dygraph.prototype.updateSelection_=function(){var a=this.attr_(\"highlightCircleSize\");var n=this.canvas_.getContext(\"2d\");if(this.previousVerticalX_>=0){var l=this.previousVerticalX_;n.clearRect(l-a-1,0,2*a+2,this.height_)}var m=function(c){return c&&!isNaN(c)};if(this.selPoints_.length>0){var b=this.selPoints_[0].canvasx;var d=this.attr_(\"xValueFormatter\")(this.lastx_,this)+\":\";var e=this.attr_(\"yValueFormatter\");var j=this.colors_.length;if(this.attr_(\"showLabelsOnHighlight\")){for(var f=0;f\"}var k=this.selPoints_[f];var h=new RGBColor(this.colors_[f%j]);var g=e(k.yval);d+=\" \"+k.name+\":\"+g}this.attr_(\"labelsDiv\").innerHTML=d}n.save();for(var f=0;f=0){for(var a in this.layout_.datasets){if(b=Dygraph.DAILY){y.push({v:l,label:new Date(l+3600*1000).strftime(u)})}else{y.push({v:l,label:this.hmsString_(l)})}}}else{var f;var o=1;if(a==Dygraph.MONTHLY){f=[0,1,2,3,4,5,6,7,8,9,10,11,12]}else{if(a==Dygraph.QUARTERLY){f=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){f=[0,6]}else{if(a==Dygraph.ANNUAL){f=[0]}else{if(a==Dygraph.DECADAL){f=[0];o=10}}}}}var r=new Date(n).getFullYear();var p=new Date(k).getFullYear();var c=Dygraph.zeropad;for(var s=r;s<=p;s++){if(s%o!=0){continue}for(var q=0;qk){continue}y.push({v:l,label:new Date(l).strftime(\"%b %y\")})}}}return y};Dygraph.dateTicker=function(a,f,d){var b=-1;for(var e=0;e=d.attr_(\"pixelsPerXLabel\")){b=e;break}}if(b>=0){return d.GetXAxis(a,f,b)}else{}};Dygraph.numericTicks=function(v,u,l){if(l.attr_(\"labelsKMG2\")){var f=[1,2,4,8]}else{var f=[1,2,5]}var x,p,a,q;var h=l.attr_(\"pixelsPerYLabel\");for(var t=-10;t<50;t++){if(l.attr_(\"labelsKMG2\")){var c=Math.pow(16,t)}else{var c=Math.pow(10,t)}for(var s=0;sh){break}}if(d>h){break}}var w=[];var r;var o=[];if(l.attr_(\"labelsKMB\")){r=1000;o=[\"K\",\"M\",\"B\",\"T\"]}if(l.attr_(\"labelsKMG2\")){if(r){l.warn(\"Setting both labelsKMB and labelsKMG2. Pick one!\")}r=1024;o=[\"k\",\"M\",\"G\",\"T\"]}if(p>a){x*=-1}for(var t=0;t=0;s--,m/=r){if(b>=m){e=Dygraph.round_(g/m,1)+o[s];break}}}w.push({label:e,v:g})}return w};Dygraph.prototype.addYTicks_=function(c,b){var a=Dygraph.numericTicks(c,b,this);this.layout_.updateOptions({yAxis:[c,b],yTicks:a})};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null;var b=this.attr_(\"errorBars\")||this.attr_(\"customBars\");if(b){for(var c=0;cg){a=g}if(ef){f=e}if(h==null||af){f=g}if(h==null||g=E&&e===null){e=t}if(g[t][0]<=f){D=t}}if(e===null){e=0}if(e>0){e--}if(D===null){D=g.length-1}if(Dx){x=o}if(p){var m=[];for(var u=0;ux){x=d[g[u][0]]}}h.push([this.attr_(\"labels\")[w],m])}else{this.layout_.addDataset(this.attr_(\"labels\")[w],g)}}}if(h.length>0){for(var w=(h.length-1);w>=0;w--){this.layout_.addDataset(h[w][0],h[w][1])}}if(this.valueRange_!=null){this.addYTicks_(this.valueRange_[0],this.valueRange_[1]);this.displayedYRange_=this.valueRange_}else{if(this.attr_(\"includeZero\")&&y>0){y=0}var v=x-y;if(v==0){v=x}var c=x+0.1*v;var B=y-0.1*v;if(B<0&&y>=0){B=0}if(c>0&&x<=0){c=0}if(this.attr_(\"includeZero\")){if(x<0){c=0}if(y>0){B=0}}this.addYTicks_(B,c);this.displayedYRange_=[B,c]}this.addXTicks_();this.layout_.updateOptions({dateWindow:this.dateWindow_});this.layout_.evaluateWithError();this.plotter_.clear();this.plotter_.render();this.canvas_.getContext(\"2d\").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_(\"drawCallback\")!==null){this.attr_(\"drawCallback\")(this,n)}};Dygraph.prototype.rollingAverage=function(m,d){if(m.length<2){return m}var d=Math.min(d,m.length-1);var b=[];var s=this.attr_(\"sigma\");if(this.fractions_){var k=0;var h=0;var e=100;for(var x=0;x=0){k-=m[x-d][1][0];h-=m[x-d][1][1]}var B=m[x][0];var v=h?k/h:0;if(this.attr_(\"errorBars\")){if(this.wilsonInterval_){if(h){var t=v<0?0:v,u=h;var A=s*Math.sqrt(t*(1-t)/u+s*s/(4*u*u));var a=1+s*s/h;var F=(t+s*s/(2*h)-A)/a;var o=(t+s*s/(2*h)+A)/a;b[x]=[B,[t*e,(t-F)*e,(o-t)*e]]}else{b[x]=[B,[0,0,0]]}}else{var z=h?s*Math.sqrt(v*(1-v)/h):1;b[x]=[B,[e*v,e*z,e*z]]}}else{b[x]=[B,e*v]}}}else{if(this.attr_(\"customBars\")){var F=0;var C=0;var o=0;var g=0;for(var x=0;x=0){var r=m[x-d];if(r[1][1]!=null&&!isNaN(r[1][1])){F-=r[1][0];C-=r[1][1];o-=r[1][2];g-=1}}b[x]=[m[x][0],[1*C/g,1*(C-F)/g,1*(o-C)/g]]}}else{var q=Math.min(d-1,m.length-2);if(!this.attr_(\"errorBars\")){if(d==1){return m}for(var x=0;x=0||b.indexOf(\"/\")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>\"19700101\"&&b<\"20371231\"){a=true}}if(a){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker}else{this.attrs_.xValueFormatter=function(c){return c};this.attrs_.xValueParser=function(c){return parseFloat(c)};this.attrs_.xTicker=Dygraph.numericTicks}};Dygraph.prototype.parseCSV_=function(h){var m=[];var q=h.split(\"\\n\");var b=this.attr_(\"delimiter\");if(q[0].indexOf(b)==-1&&q[0].indexOf(\"\\t\")>=0){b=\"\\t\"}var a=0;if(this.labelsFromCSV_){a=1;this.attrs_.labels=q[0].split(b)}var c;var o=false;var d=this.attr_(\"labels\").length;var l=false;for(var g=a;g0&&k[0]0&&k[0]=0){this.loadedEvent_(this.file_)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status==200){a.loadedEvent_(b.responseText)}}};b.open(\"GET\",this.file_,true);b.send(null)}}else{this.error(\"Unknown data format: \"+(typeof this.file_))}}}}};Dygraph.prototype.updateOptions=function(a){if(a.rollPeriod){this.rollPeriod_=a.rollPeriod}if(a.dateWindow){this.dateWindow_=a.dateWindow}if(a.valueRange){this.valueRange_=a.valueRange}Dygraph.update(this.user_attrs_,a);this.labelsFromCSV_=(this.attr_(\"labels\")==null);this.layout_.updateOptions({errorBars:this.attr_(\"errorBars\")});if(a.file){this.file_=a.file;this.start_()}else{this.drawGraph_(this.rawData_)}};Dygraph.prototype.resize=function(b,a){if((b===null)!=(a===null)){this.warn(\"Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.\");b=a=null}this.maindiv_.innerHTML=\"\";this.attrs_.labelsDiv=null;if(b){this.maindiv_.style.width=b+\"px\";this.maindiv_.style.height=a+\"px\";this.width_=b;this.height_=a}else{this.width_=this.maindiv_.offsetWidth;this.height_=this.maindiv_.offsetHeight}this.createInterface_();this.drawGraph_(this.rawData_)};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.drawGraph_(this.rawData_)};Dygraph.prototype.visibility=function(){if(!this.attr_(\"visibility\")){this.attrs_.visibility=[]}while(this.attr_(\"visibility\").length=a.length){this.warn(\"invalid series number in setVisibility: \"+b)}else{a[b]=c;this.drawGraph_(this.rawData_)}};Dygraph.createCanvas=function(){var a=document.createElement(\"canvas\");isIE=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(isIE){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML=\"\";this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var c=this.date_graph.getSelection();if(c<0){return b}col=1;for(var a in this.date_graph.layout_.datasets){b.push({row:c,column:col});col++}return b};DateGraph=Dygraph;function RGBColor(g){this.ok=false;if(g.charAt(0)==\"#\"){g=g.substr(1,6)}g=g.replace(/ /g,\"\");g=g.toLowerCase();var a={aliceblue:\"f0f8ff\",antiquewhite:\"faebd7\",aqua:\"00ffff\",aquamarine:\"7fffd4\",azure:\"f0ffff\",beige:\"f5f5dc\",bisque:\"ffe4c4\",black:\"000000\",blanchedalmond:\"ffebcd\",blue:\"0000ff\",blueviolet:\"8a2be2\",brown:\"a52a2a\",burlywood:\"deb887\",cadetblue:\"5f9ea0\",chartreuse:\"7fff00\",chocolate:\"d2691e\",coral:\"ff7f50\",cornflowerblue:\"6495ed\",cornsilk:\"fff8dc\",crimson:\"dc143c\",cyan:\"00ffff\",darkblue:\"00008b\",darkcyan:\"008b8b\",darkgoldenrod:\"b8860b\",darkgray:\"a9a9a9\",darkgreen:\"006400\",darkkhaki:\"bdb76b\",darkmagenta:\"8b008b\",darkolivegreen:\"556b2f\",darkorange:\"ff8c00\",darkorchid:\"9932cc\",darkred:\"8b0000\",darksalmon:\"e9967a\",darkseagreen:\"8fbc8f\",darkslateblue:\"483d8b\",darkslategray:\"2f4f4f\",darkturquoise:\"00ced1\",darkviolet:\"9400d3\",deeppink:\"ff1493\",deepskyblue:\"00bfff\",dimgray:\"696969\",dodgerblue:\"1e90ff\",feldspar:\"d19275\",firebrick:\"b22222\",floralwhite:\"fffaf0\",forestgreen:\"228b22\",fuchsia:\"ff00ff\",gainsboro:\"dcdcdc\",ghostwhite:\"f8f8ff\",gold:\"ffd700\",goldenrod:\"daa520\",gray:\"808080\",green:\"008000\",greenyellow:\"adff2f\",honeydew:\"f0fff0\",hotpink:\"ff69b4\",indianred:\"cd5c5c\",indigo:\"4b0082\",ivory:\"fffff0\",khaki:\"f0e68c\",lavender:\"e6e6fa\",lavenderblush:\"fff0f5\",lawngreen:\"7cfc00\",lemonchiffon:\"fffacd\",lightblue:\"add8e6\",lightcoral:\"f08080\",lightcyan:\"e0ffff\",lightgoldenrodyellow:\"fafad2\",lightgrey:\"d3d3d3\",lightgreen:\"90ee90\",lightpink:\"ffb6c1\",lightsalmon:\"ffa07a\",lightseagreen:\"20b2aa\",lightskyblue:\"87cefa\",lightslateblue:\"8470ff\",lightslategray:\"778899\",lightsteelblue:\"b0c4de\",lightyellow:\"ffffe0\",lime:\"00ff00\",limegreen:\"32cd32\",linen:\"faf0e6\",magenta:\"ff00ff\",maroon:\"800000\",mediumaquamarine:\"66cdaa\",mediumblue:\"0000cd\",mediumorchid:\"ba55d3\",mediumpurple:\"9370d8\",mediumseagreen:\"3cb371\",mediumslateblue:\"7b68ee\",mediumspringgreen:\"00fa9a\",mediumturquoise:\"48d1cc\",mediumvioletred:\"c71585\",midnightblue:\"191970\",mintcream:\"f5fffa\",mistyrose:\"ffe4e1\",moccasin:\"ffe4b5\",navajowhite:\"ffdead\",navy:\"000080\",oldlace:\"fdf5e6\",olive:\"808000\",olivedrab:\"6b8e23\",orange:\"ffa500\",orangered:\"ff4500\",orchid:\"da70d6\",palegoldenrod:\"eee8aa\",palegreen:\"98fb98\",paleturquoise:\"afeeee\",palevioletred:\"d87093\",papayawhip:\"ffefd5\",peachpuff:\"ffdab9\",peru:\"cd853f\",pink:\"ffc0cb\",plum:\"dda0dd\",powderblue:\"b0e0e6\",purple:\"800080\",red:\"ff0000\",rosybrown:\"bc8f8f\",royalblue:\"4169e1\",saddlebrown:\"8b4513\",salmon:\"fa8072\",sandybrown:\"f4a460\",seagreen:\"2e8b57\",seashell:\"fff5ee\",sienna:\"a0522d\",silver:\"c0c0c0\",skyblue:\"87ceeb\",slateblue:\"6a5acd\",slategray:\"708090\",snow:\"fffafa\",springgreen:\"00ff7f\",steelblue:\"4682b4\",tan:\"d2b48c\",teal:\"008080\",thistle:\"d8bfd8\",tomato:\"ff6347\",turquoise:\"40e0d0\",violet:\"ee82ee\",violetred:\"d02090\",wheat:\"f5deb3\",white:\"ffffff\",whitesmoke:\"f5f5f5\",yellow:\"ffff00\",yellowgreen:\"9acd32\"};for(var c in a){if(g==c){g=a[c]}}var h=[{re:/^rgb\\((\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d{1,3})\\)$/,example:[\"rgb(123, 234, 45)\",\"rgb(255,234,245)\"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\\w{2})(\\w{2})(\\w{2})$/,example:[\"#00ff00\",\"336699\"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\\w{1})(\\w{1})(\\w{1})$/,example:[\"#fb0\",\"f0f\"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var b=0;b255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return\"rgb(\"+this.r+\", \"+this.g+\", \"+this.b+\")\"};this.toHex=function(){var k=this.r.toString(16);var j=this.g.toString(16);var i=this.b.toString(16);if(k.length==1){k=\"0\"+k}if(j.length==1){j=\"0\"+j}if(i.length==1){i=\"0\"+i}return\"#\"+k+j+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)==\"undefined\"){b=10}for(;parseInt(a,10)1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale=\"en-GB\";if(document.getElementsByTagName(\"html\")&&document.getElementsByTagName(\"html\")[0].lang){Date.prototype.locale=document.getElementsByTagName(\"html\")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],A:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],b:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],B:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],c:\"%a %d %b %Y %T %Z\",p:[\"AM\",\"PM\"],P:[\"am\",\"pm\"],x:\"%d/%m/%y\",X:\"%T\"};Date.ext.locales[\"en-US\"]=Date.ext.locales.en;Date.ext.locales[\"en-US\"].c=\"%a %d %b %Y %r %Z\";Date.ext.locales[\"en-US\"].x=\"%D\";Date.ext.locales[\"en-US\"].X=\"%r\";Date.ext.locales[\"en-GB\"]=Date.ext.locales.en;Date.ext.locales[\"en-AU\"]=Date.ext.locales[\"en-GB\"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:\"toLocaleString\",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:[\"getDate\",\"0\"],e:[\"getDate\",\" \"],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:[\"getHours\",\"0\"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(\"\"+c.getFullYear()+\"/1/1 GMT\");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:[\"getMinutes\",\"0\"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:[\"getSeconds\",\"0\"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(\"\"+e.getFullYear()+\"/1/1\")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(\"\"+e.getFullYear()+\"/12/31\")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(\"\"+(e.getFullYear()-1)+\"/12/31\"))}}return Date.ext.util.xPad(b,0)},w:\"getDay\",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:\"getFullYear\",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?\"-\":\"+\")+a+e},Z:function(a){return a.toString().replace(/^.*\\(([^)]+)\\)$/,\"$1\")},\"%\":function(a){return\"%\"}};Date.ext.aggregates={c:\"locale\",D:\"%m/%d/%y\",h:\"%b\",n:\"\\n\",r:\"%I:%M:%S %p\",R:\"%H:%M\",t:\"\\t\",T:\"%H:%M:%S\",x:\"locale\",X:\"locale\"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,\"\") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,\"\")}else{this.locale=\"en-GB\"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g==\"locale\"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)==\"string\"){return c[g]()}else{if(typeof(g)==\"function\"){return g.call(c,c)}else{if(typeof(g)==\"object\"&&typeof(g[0])==\"string\"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};";var REPORT_SUMMARY_TEMPLATE=exports.REPORT_SUMMARY_TEMPLATE="\n \n Test Results\n \n \n \n\n \n
\n

Test Results

\n

<%=new Date()%>

\n
\n
\n
\n
\n
\n

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

\n \n\n \n";var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var fs=require('fs');} -var template={cache_:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)){if(!callback){fn=this.create(fs.readFileSync(str).toString('utf8'));}else{fs.readFile(str,function(err,buffer){if(err){throw err;} -this.create(buffer.toString('utf8'),data,callback);});return;}}else{if(this.cache_[str]){fn=this.cache_[str];}else{fn=new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};"+"obj=obj||{};"+"with(obj){p.push('"+ -str.split("'").join("\\'").split("\n").join("\\n").replace(/<%([\s\S]*?)%>/mg,function(m,t){return'<%'+t.split("\\'").join("'").split("\\n").join("\n")+'%>';}).replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');");this.cache_[str]=fn;}} -if(callback){callback(data?fn(data):fn);} -else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);var BUILD_AS_SINGLE_FILE,NODELOAD_CONFIG;if(!BUILD_AS_SINGLE_FILE){var EventEmitter=require('events').EventEmitter;} +var util=require('util'),http=require('http'),url=require('url'),fs=require('fs'),events=require('events'),querystring=require('querystring');var EventEmitter=events.EventEmitter;var START=new Date();var BUILD_AS_SINGLE_FILE=true;var BUILD_AS_SINGLE_FILE,NODELOAD_CONFIG;if(!BUILD_AS_SINGLE_FILE){var EventEmitter=require('events').EventEmitter;} exports.quiet=function(){NODELOAD_CONFIG.QUIET=true;return exports;};exports.usePort=function(port){NODELOAD_CONFIG.HTTP_PORT=port;return exports;};exports.disableServer=function(){NODELOAD_CONFIG.HTTP_ENABLED=false;return exports;};exports.setMonitorIntervalMs=function(milliseconds){NODELOAD_CONFIG.MONITOR_INTERVAL_MS=milliseconds;return exports;};exports.setAjaxRefreshIntervalMs=function(milliseconds){NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS=milliseconds;return exports;};exports.disableLogs=function(){NODELOAD_CONFIG.LOGS_ENABLED=false;return exports;};exports.setSlaveUpdateIntervalMs=function(milliseconds){NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS=milliseconds;};var NODELOAD_CONFIG=exports.NODELOAD_CONFIG={START:new Date(),QUIET:Boolean(process.env.QUIET)||false,HTTP_ENABLED:true,HTTP_PORT:Number(process.env.HTTP_PORT)||8000,MONITOR_INTERVAL_MS:2000,AJAX_REFRESH_INTERVAL_MS:2000,LOGS_ENABLED:process.env.LOGS?process.env.LOGS!=='0':true,SLAVE_UPDATE_INTERVAL_MS:3000,eventEmitter:new EventEmitter(),on:function(event,fun){this.eventEmitter.on(event,fun);},apply:function(){this.eventEmitter.emit('apply');}};process.nextTick(function(){NODELOAD_CONFIG.apply();});var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('util');var NODELOAD_CONFIG=require('./config').NODELOAD_CONFIG;} var qputs=util.qputs=function(s){if(!NODELOAD_CONFIG.QUIET){util.puts(s);}};var qprint=util.qprint=function(s){if(!NODELOAD_CONFIG.QUIET){util.print(s);}};util.uid=function(){exports.lastUid_=exports.lastUid_||0;return exports.lastUid_++;};util.defaults=function(obj,defaults){for(var i in defaults){if(obj[i]===undefined){obj[i]=defaults[i];}} return obj;};util.extend=function(obj,extension){for(var i in extension){if(extension.hasOwnProperty(i)){obj[i]=extension[i];}} @@ -87,13 +82,18 @@ this.running=true;var self=this;port=port||8000;self.hostname=hostname||'localho this.running=false;this.connections.forEach(function(c){c.destroy();});this.server.close();this.server=null;this.emit('end');};HttpServer.prototype.addRoute=function(regex,handler){this.routes.unshift({regex:regex,handler:handler});return this;};HttpServer.prototype.removeRoute=function(regex,handler){this.routes=this.routes.filter(function(r){return!((regex===r.regex)&&(!handler||handler===r.handler));});return this;};HttpServer.prototype.route_=function(req,res){for(var i=0;ithis.maxxval){this.maxxval=a}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!=0?1/this.xrange:1);this.minyval=this.options.yAxis[0];this.maxyval=this.options.yAxis[1];this.yrange=this.maxyval-this.minyval;this.yscale=(this.yrange!=0?1/this.yrange:1)};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=new Array();for(var e in this.datasets){if(!this.datasets.hasOwnProperty(e)){continue}var d=this.datasets[e];for(var b=0;b=1){a.y=1}this.points.push(a)}}};DygraphLayout.prototype._evaluateLineTicks=function(){this.xticks=new Array();for(var c=0;c=0)&&(d<=1)){this.xticks.push([d,a])}}this.yticks=new Array();for(var c=0;c=0)&&(d<=1)){this.yticks.push([d,a])}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!this.options.errorBars){return}var d=0;for(var g in this.datasets){if(!this.datasets.hasOwnProperty(g)){continue}var c=0;var f=this.datasets[g];for(var c=0;c0){for(var e=0;ethis.height){k.style.bottom=\"0px\"}else{k.style.top=h+\"px\"}k.style.left=\"0px\";k.style.textAlign=\"right\";k.style.width=this.options.yAxisLabelWidth+\"px\";this.container.appendChild(k);this.ylabels.push(k)}var m=this.ylabels[0];var n=this.options.axisLabelFontSize;var a=parseInt(m.style.top)+n;if(a>this.height-n){m.style.top=(parseInt(m.style.top)-n/2)+\"px\"}}b.beginPath();b.moveTo(this.area.x,this.area.y);b.lineTo(this.area.x,this.area.y+this.area.h);b.closePath();b.stroke()}if(this.options.drawXAxis){if(this.layout.xticks){for(var e=0;ethis.width){c=this.width-this.options.xAxisLabelWidth;k.style.textAlign=\"right\"}if(c<0){c=0;k.style.textAlign=\"left\"}k.style.left=c+\"px\";k.style.width=this.options.xAxisLabelWidth+\"px\";this.container.appendChild(k);this.xlabels.push(k)}}b.beginPath();b.moveTo(this.area.x,this.area.y+this.area.h);b.lineTo(this.area.x+this.area.w,this.area.y+this.area.h);b.closePath();b.stroke()}b.restore()};DygraphCanvasRenderer.prototype._renderLineChart=function(){var b=this.element.getContext(\"2d\");var d=this.options.colorScheme.length;var n=this.options.colorScheme;var x=this.options.fillAlpha;var C=this.layout.options.errorBars;var q=this.layout.options.fillGraph;var E=[];for(var F in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(F)){E.push(F)}}var y=E.length;this.colors={};for(var A=0;A0){r=E[A-1]}var v=this.colors[g];s.save();s.strokeStyle=v;s.lineWidth=this.options.strokeWidth;var h=NaN;var f=[-1,-1];var k=0;var B=this.layout.yscale;var a=new RGBColor(v);var D=\"rgba(\"+a.r+\",\"+a.g+\",\"+a.b+\",\"+x+\")\";s.fillStyle=D;s.beginPath();for(var w=0;w1){e=1}}var p=[t.y,e];p[0]=this.area.h*p[0]+this.area.y;p[1]=this.area.h*p[1]+this.area.y;if(!isNaN(h)){s.moveTo(h,f[0]);s.lineTo(t.canvasx,p[0]);s.lineTo(t.canvasx,p[1]);s.lineTo(h,f[1]);s.closePath()}f[0]=p[0];f[1]=p[1];h=t.canvasx}}s.fill()}}}for(var A=0;A0){if(arguments.length==4){this.warn(\"Using deprecated four-argument dygraph constructor\");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME=\"Dygraph\";Dygraph.VERSION=\"1.2\";Dygraph.__repr__=function(){return\"[\"+this.NAME+\" \"+this.VERSION+\"]\"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.AXIS_LINE_WIDTH=0.3;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,pixelsPerXLabel:60,pixelsPerYLabel:30,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,yValueFormatter:function(a){return Dygraph.round_(a,2)},strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueFormatter:Dygraph.dateString_,xValueParser:Dygraph.dateParser,xTicker:Dygraph.dateTicker,delimiter:\",\",logScale:false,sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!=null){var a=[\"Date\"];for(var c=0;c=10){n.doZoom_(Math.min(b,m),Math.max(b,m))}else{n.canvas_.getContext(\"2d\").clearRect(0,0,n.canvas_.width,n.canvas_.height)}b=null;a=null}if(e){e=false;l=null;j=null}});Dygraph.addEvent(this.hidden_,\"dblclick\",function(o){if(n.dateWindow_==null){return}n.dateWindow_=null;n.drawGraph_(n.rawData_);var p=n.rawData_[0][0];var q=n.rawData_[n.rawData_.length-1][0];if(n.attr_(\"zoomCallback\")){n.attr_(\"zoomCallback\")(p,q)}})};Dygraph.prototype.drawZoomRect_=function(c,d,b){var a=this.canvas_.getContext(\"2d\");if(b){a.clearRect(Math.min(c,b),0,Math.abs(c-b),this.height_)}if(d&&c){a.fillStyle=\"rgba(128,128,128,0.33)\";a.fillRect(Math.min(c,d),0,Math.abs(d-c),this.height_)}};Dygraph.prototype.doZoom_=function(d,a){var b=this.toDataCoords(d,null);var c=b[0];b=this.toDataCoords(a,null);var e=b[0];this.dateWindow_=[c,e];this.drawGraph_(this.rawData_);if(this.attr_(\"zoomCallback\")){this.attr_(\"zoomCallback\")(c,e)}};Dygraph.prototype.mouseMove_=function(b){var a=Dygraph.pageX(b)-Dygraph.findPosX(this.hidden_);var s=this.layout_.points;var m=-1;var j=-1;var q=1e+100;var r=-1;for(var f=0;fq){continue}q=h;r=f}if(r>=0){m=s[r].xval}if(a>s[s.length-1].canvasx){m=s[s.length-1].xval}this.selPoints_=[];var g=0;var d=s.length;var o=this.attr_(\"stackedGraph\");if(!this.attr_(\"stackedGraph\")){for(var f=0;f=0;f--){if(s[f].xval==m){var c={};for(var e in s[f]){c[e]=s[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}}if(this.attr_(\"highlightCallback\")){var n=this.lastHighlightCallbackX;if(n!==null&&m!=n){this.lastHighlightCallbackX=m;this.attr_(\"highlightCallback\")(b,m,this.selPoints_)}}this.lastx_=m;this.updateSelection_()};Dygraph.prototype.updateSelection_=function(){var a=this.attr_(\"highlightCircleSize\");var n=this.canvas_.getContext(\"2d\");if(this.previousVerticalX_>=0){var l=this.previousVerticalX_;n.clearRect(l-a-1,0,2*a+2,this.height_)}var m=function(c){return c&&!isNaN(c)};if(this.selPoints_.length>0){var b=this.selPoints_[0].canvasx;var d=this.attr_(\"xValueFormatter\")(this.lastx_,this)+\":\";var e=this.attr_(\"yValueFormatter\");var j=this.colors_.length;if(this.attr_(\"showLabelsOnHighlight\")){for(var f=0;f\"}var k=this.selPoints_[f];var h=new RGBColor(this.colors_[f%j]);var g=e(k.yval);d+=\" \"+k.name+\":\"+g}this.attr_(\"labelsDiv\").innerHTML=d}n.save();for(var f=0;f=0){for(var a in this.layout_.datasets){if(b=Dygraph.DAILY){y.push({v:l,label:new Date(l+3600*1000).strftime(u)})}else{y.push({v:l,label:this.hmsString_(l)})}}}else{var f;var o=1;if(a==Dygraph.MONTHLY){f=[0,1,2,3,4,5,6,7,8,9,10,11,12]}else{if(a==Dygraph.QUARTERLY){f=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){f=[0,6]}else{if(a==Dygraph.ANNUAL){f=[0]}else{if(a==Dygraph.DECADAL){f=[0];o=10}}}}}var r=new Date(n).getFullYear();var p=new Date(k).getFullYear();var c=Dygraph.zeropad;for(var s=r;s<=p;s++){if(s%o!=0){continue}for(var q=0;qk){continue}y.push({v:l,label:new Date(l).strftime(\"%b %y\")})}}}return y};Dygraph.dateTicker=function(a,f,d){var b=-1;for(var e=0;e=d.attr_(\"pixelsPerXLabel\")){b=e;break}}if(b>=0){return d.GetXAxis(a,f,b)}else{}};Dygraph.numericTicks=function(v,u,l){if(l.attr_(\"labelsKMG2\")){var f=[1,2,4,8]}else{var f=[1,2,5]}var x,p,a,q;var h=l.attr_(\"pixelsPerYLabel\");for(var t=-10;t<50;t++){if(l.attr_(\"labelsKMG2\")){var c=Math.pow(16,t)}else{var c=Math.pow(10,t)}for(var s=0;sh){break}}if(d>h){break}}var w=[];var r;var o=[];if(l.attr_(\"labelsKMB\")){r=1000;o=[\"K\",\"M\",\"B\",\"T\"]}if(l.attr_(\"labelsKMG2\")){if(r){l.warn(\"Setting both labelsKMB and labelsKMG2. Pick one!\")}r=1024;o=[\"k\",\"M\",\"G\",\"T\"]}if(p>a){x*=-1}for(var t=0;t=0;s--,m/=r){if(b>=m){e=Dygraph.round_(g/m,1)+o[s];break}}}w.push({label:e,v:g})}return w};Dygraph.prototype.addYTicks_=function(c,b){var a=Dygraph.numericTicks(c,b,this);this.layout_.updateOptions({yAxis:[c,b],yTicks:a})};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null;var b=this.attr_(\"errorBars\")||this.attr_(\"customBars\");if(b){for(var c=0;cg){a=g}if(ef){f=e}if(h==null||af){f=g}if(h==null||g=E&&e===null){e=t}if(g[t][0]<=f){D=t}}if(e===null){e=0}if(e>0){e--}if(D===null){D=g.length-1}if(Dx){x=o}if(p){var m=[];for(var u=0;ux){x=d[g[u][0]]}}h.push([this.attr_(\"labels\")[w],m])}else{this.layout_.addDataset(this.attr_(\"labels\")[w],g)}}}if(h.length>0){for(var w=(h.length-1);w>=0;w--){this.layout_.addDataset(h[w][0],h[w][1])}}if(this.valueRange_!=null){this.addYTicks_(this.valueRange_[0],this.valueRange_[1]);this.displayedYRange_=this.valueRange_}else{if(this.attr_(\"includeZero\")&&y>0){y=0}var v=x-y;if(v==0){v=x}var c=x+0.1*v;var B=y-0.1*v;if(B<0&&y>=0){B=0}if(c>0&&x<=0){c=0}if(this.attr_(\"includeZero\")){if(x<0){c=0}if(y>0){B=0}}this.addYTicks_(B,c);this.displayedYRange_=[B,c]}this.addXTicks_();this.layout_.updateOptions({dateWindow:this.dateWindow_});this.layout_.evaluateWithError();this.plotter_.clear();this.plotter_.render();this.canvas_.getContext(\"2d\").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_(\"drawCallback\")!==null){this.attr_(\"drawCallback\")(this,n)}};Dygraph.prototype.rollingAverage=function(m,d){if(m.length<2){return m}var d=Math.min(d,m.length-1);var b=[];var s=this.attr_(\"sigma\");if(this.fractions_){var k=0;var h=0;var e=100;for(var x=0;x=0){k-=m[x-d][1][0];h-=m[x-d][1][1]}var B=m[x][0];var v=h?k/h:0;if(this.attr_(\"errorBars\")){if(this.wilsonInterval_){if(h){var t=v<0?0:v,u=h;var A=s*Math.sqrt(t*(1-t)/u+s*s/(4*u*u));var a=1+s*s/h;var F=(t+s*s/(2*h)-A)/a;var o=(t+s*s/(2*h)+A)/a;b[x]=[B,[t*e,(t-F)*e,(o-t)*e]]}else{b[x]=[B,[0,0,0]]}}else{var z=h?s*Math.sqrt(v*(1-v)/h):1;b[x]=[B,[e*v,e*z,e*z]]}}else{b[x]=[B,e*v]}}}else{if(this.attr_(\"customBars\")){var F=0;var C=0;var o=0;var g=0;for(var x=0;x=0){var r=m[x-d];if(r[1][1]!=null&&!isNaN(r[1][1])){F-=r[1][0];C-=r[1][1];o-=r[1][2];g-=1}}b[x]=[m[x][0],[1*C/g,1*(C-F)/g,1*(o-C)/g]]}}else{var q=Math.min(d-1,m.length-2);if(!this.attr_(\"errorBars\")){if(d==1){return m}for(var x=0;x=0||b.indexOf(\"/\")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>\"19700101\"&&b<\"20371231\"){a=true}}if(a){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker}else{this.attrs_.xValueFormatter=function(c){return c};this.attrs_.xValueParser=function(c){return parseFloat(c)};this.attrs_.xTicker=Dygraph.numericTicks}};Dygraph.prototype.parseCSV_=function(h){var m=[];var q=h.split(\"\\n\");var b=this.attr_(\"delimiter\");if(q[0].indexOf(b)==-1&&q[0].indexOf(\"\\t\")>=0){b=\"\\t\"}var a=0;if(this.labelsFromCSV_){a=1;this.attrs_.labels=q[0].split(b)}var c;var o=false;var d=this.attr_(\"labels\").length;var l=false;for(var g=a;g0&&k[0]0&&k[0]=0){this.loadedEvent_(this.file_)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status==200){a.loadedEvent_(b.responseText)}}};b.open(\"GET\",this.file_,true);b.send(null)}}else{this.error(\"Unknown data format: \"+(typeof this.file_))}}}}};Dygraph.prototype.updateOptions=function(a){if(a.rollPeriod){this.rollPeriod_=a.rollPeriod}if(a.dateWindow){this.dateWindow_=a.dateWindow}if(a.valueRange){this.valueRange_=a.valueRange}Dygraph.update(this.user_attrs_,a);this.labelsFromCSV_=(this.attr_(\"labels\")==null);this.layout_.updateOptions({errorBars:this.attr_(\"errorBars\")});if(a.file){this.file_=a.file;this.start_()}else{this.drawGraph_(this.rawData_)}};Dygraph.prototype.resize=function(b,a){if((b===null)!=(a===null)){this.warn(\"Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.\");b=a=null}this.maindiv_.innerHTML=\"\";this.attrs_.labelsDiv=null;if(b){this.maindiv_.style.width=b+\"px\";this.maindiv_.style.height=a+\"px\";this.width_=b;this.height_=a}else{this.width_=this.maindiv_.offsetWidth;this.height_=this.maindiv_.offsetHeight}this.createInterface_();this.drawGraph_(this.rawData_)};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.drawGraph_(this.rawData_)};Dygraph.prototype.visibility=function(){if(!this.attr_(\"visibility\")){this.attrs_.visibility=[]}while(this.attr_(\"visibility\").length=a.length){this.warn(\"invalid series number in setVisibility: \"+b)}else{a[b]=c;this.drawGraph_(this.rawData_)}};Dygraph.createCanvas=function(){var a=document.createElement(\"canvas\");isIE=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(isIE){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML=\"\";this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var c=this.date_graph.getSelection();if(c<0){return b}col=1;for(var a in this.date_graph.layout_.datasets){b.push({row:c,column:col});col++}return b};DateGraph=Dygraph;function RGBColor(g){this.ok=false;if(g.charAt(0)==\"#\"){g=g.substr(1,6)}g=g.replace(/ /g,\"\");g=g.toLowerCase();var a={aliceblue:\"f0f8ff\",antiquewhite:\"faebd7\",aqua:\"00ffff\",aquamarine:\"7fffd4\",azure:\"f0ffff\",beige:\"f5f5dc\",bisque:\"ffe4c4\",black:\"000000\",blanchedalmond:\"ffebcd\",blue:\"0000ff\",blueviolet:\"8a2be2\",brown:\"a52a2a\",burlywood:\"deb887\",cadetblue:\"5f9ea0\",chartreuse:\"7fff00\",chocolate:\"d2691e\",coral:\"ff7f50\",cornflowerblue:\"6495ed\",cornsilk:\"fff8dc\",crimson:\"dc143c\",cyan:\"00ffff\",darkblue:\"00008b\",darkcyan:\"008b8b\",darkgoldenrod:\"b8860b\",darkgray:\"a9a9a9\",darkgreen:\"006400\",darkkhaki:\"bdb76b\",darkmagenta:\"8b008b\",darkolivegreen:\"556b2f\",darkorange:\"ff8c00\",darkorchid:\"9932cc\",darkred:\"8b0000\",darksalmon:\"e9967a\",darkseagreen:\"8fbc8f\",darkslateblue:\"483d8b\",darkslategray:\"2f4f4f\",darkturquoise:\"00ced1\",darkviolet:\"9400d3\",deeppink:\"ff1493\",deepskyblue:\"00bfff\",dimgray:\"696969\",dodgerblue:\"1e90ff\",feldspar:\"d19275\",firebrick:\"b22222\",floralwhite:\"fffaf0\",forestgreen:\"228b22\",fuchsia:\"ff00ff\",gainsboro:\"dcdcdc\",ghostwhite:\"f8f8ff\",gold:\"ffd700\",goldenrod:\"daa520\",gray:\"808080\",green:\"008000\",greenyellow:\"adff2f\",honeydew:\"f0fff0\",hotpink:\"ff69b4\",indianred:\"cd5c5c\",indigo:\"4b0082\",ivory:\"fffff0\",khaki:\"f0e68c\",lavender:\"e6e6fa\",lavenderblush:\"fff0f5\",lawngreen:\"7cfc00\",lemonchiffon:\"fffacd\",lightblue:\"add8e6\",lightcoral:\"f08080\",lightcyan:\"e0ffff\",lightgoldenrodyellow:\"fafad2\",lightgrey:\"d3d3d3\",lightgreen:\"90ee90\",lightpink:\"ffb6c1\",lightsalmon:\"ffa07a\",lightseagreen:\"20b2aa\",lightskyblue:\"87cefa\",lightslateblue:\"8470ff\",lightslategray:\"778899\",lightsteelblue:\"b0c4de\",lightyellow:\"ffffe0\",lime:\"00ff00\",limegreen:\"32cd32\",linen:\"faf0e6\",magenta:\"ff00ff\",maroon:\"800000\",mediumaquamarine:\"66cdaa\",mediumblue:\"0000cd\",mediumorchid:\"ba55d3\",mediumpurple:\"9370d8\",mediumseagreen:\"3cb371\",mediumslateblue:\"7b68ee\",mediumspringgreen:\"00fa9a\",mediumturquoise:\"48d1cc\",mediumvioletred:\"c71585\",midnightblue:\"191970\",mintcream:\"f5fffa\",mistyrose:\"ffe4e1\",moccasin:\"ffe4b5\",navajowhite:\"ffdead\",navy:\"000080\",oldlace:\"fdf5e6\",olive:\"808000\",olivedrab:\"6b8e23\",orange:\"ffa500\",orangered:\"ff4500\",orchid:\"da70d6\",palegoldenrod:\"eee8aa\",palegreen:\"98fb98\",paleturquoise:\"afeeee\",palevioletred:\"d87093\",papayawhip:\"ffefd5\",peachpuff:\"ffdab9\",peru:\"cd853f\",pink:\"ffc0cb\",plum:\"dda0dd\",powderblue:\"b0e0e6\",purple:\"800080\",red:\"ff0000\",rosybrown:\"bc8f8f\",royalblue:\"4169e1\",saddlebrown:\"8b4513\",salmon:\"fa8072\",sandybrown:\"f4a460\",seagreen:\"2e8b57\",seashell:\"fff5ee\",sienna:\"a0522d\",silver:\"c0c0c0\",skyblue:\"87ceeb\",slateblue:\"6a5acd\",slategray:\"708090\",snow:\"fffafa\",springgreen:\"00ff7f\",steelblue:\"4682b4\",tan:\"d2b48c\",teal:\"008080\",thistle:\"d8bfd8\",tomato:\"ff6347\",turquoise:\"40e0d0\",violet:\"ee82ee\",violetred:\"d02090\",wheat:\"f5deb3\",white:\"ffffff\",whitesmoke:\"f5f5f5\",yellow:\"ffff00\",yellowgreen:\"9acd32\"};for(var c in a){if(g==c){g=a[c]}}var h=[{re:/^rgb\\((\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d{1,3})\\)$/,example:[\"rgb(123, 234, 45)\",\"rgb(255,234,245)\"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\\w{2})(\\w{2})(\\w{2})$/,example:[\"#00ff00\",\"336699\"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\\w{1})(\\w{1})(\\w{1})$/,example:[\"#fb0\",\"f0f\"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var b=0;b255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return\"rgb(\"+this.r+\", \"+this.g+\", \"+this.b+\")\"};this.toHex=function(){var k=this.r.toString(16);var j=this.g.toString(16);var i=this.b.toString(16);if(k.length==1){k=\"0\"+k}if(j.length==1){j=\"0\"+j}if(i.length==1){i=\"0\"+i}return\"#\"+k+j+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)==\"undefined\"){b=10}for(;parseInt(a,10)1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale=\"en-GB\";if(document.getElementsByTagName(\"html\")&&document.getElementsByTagName(\"html\")[0].lang){Date.prototype.locale=document.getElementsByTagName(\"html\")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],A:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],b:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],B:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],c:\"%a %d %b %Y %T %Z\",p:[\"AM\",\"PM\"],P:[\"am\",\"pm\"],x:\"%d/%m/%y\",X:\"%T\"};Date.ext.locales[\"en-US\"]=Date.ext.locales.en;Date.ext.locales[\"en-US\"].c=\"%a %d %b %Y %r %Z\";Date.ext.locales[\"en-US\"].x=\"%D\";Date.ext.locales[\"en-US\"].X=\"%r\";Date.ext.locales[\"en-GB\"]=Date.ext.locales.en;Date.ext.locales[\"en-AU\"]=Date.ext.locales[\"en-GB\"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:\"toLocaleString\",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:[\"getDate\",\"0\"],e:[\"getDate\",\" \"],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:[\"getHours\",\"0\"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(\"\"+c.getFullYear()+\"/1/1 GMT\");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:[\"getMinutes\",\"0\"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:[\"getSeconds\",\"0\"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(\"\"+e.getFullYear()+\"/1/1\")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(\"\"+e.getFullYear()+\"/12/31\")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(\"\"+(e.getFullYear()-1)+\"/12/31\"))}}return Date.ext.util.xPad(b,0)},w:\"getDay\",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:\"getFullYear\",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?\"-\":\"+\")+a+e},Z:function(a){return a.toString().replace(/^.*\\(([^)]+)\\)$/,\"$1\")},\"%\":function(a){return\"%\"}};Date.ext.aggregates={c:\"locale\",D:\"%m/%d/%y\",h:\"%b\",n:\"\\n\",r:\"%I:%M:%S %p\",R:\"%H:%M\",t:\"\\t\",T:\"%H:%M:%S\",x:\"locale\",X:\"locale\"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,\"\") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,\"\")}else{this.locale=\"en-GB\"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g==\"locale\"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)==\"string\"){return c[g]()}else{if(typeof(g)==\"function\"){return g.call(c,c)}else{if(typeof(g)==\"object\"&&typeof(g[0])==\"string\"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};";var REPORT_SUMMARY_TEMPLATE=exports.REPORT_SUMMARY_TEMPLATE="\n \n Test Results\n \n \n \n\n \n
\n

Test Results

\n

<%=new Date()%>

\n
\n
\n
\n
\n
\n

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

\n \n\n \n";var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var fs=require('fs');} +var template={cache_:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)){if(!callback){fn=this.create(fs.readFileSync(str).toString('utf8'));}else{fs.readFile(str,function(err,buffer){if(err){throw err;} +this.create(buffer.toString('utf8'),data,callback);});return;}}else{if(this.cache_[str]){fn=this.cache_[str];}else{fn=new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};"+"obj=obj||{};"+"with(obj){p.push('"+ +str.split("'").join("\\'").split("\n").join("\\n").replace(/<%([\s\S]*?)%>/mg,function(m,t){return'<%'+t.split("\\'").join("'").split("\\n").join("\n")+'%>';}).replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');");this.cache_[str]=fn;}} +if(callback){callback(data?fn(data):fn);} +else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var querystring=require('querystring');var LogFile=require('../stats').LogFile;var template=require('./template');var config=require('../config');var REPORT_SUMMARY_TEMPLATE=require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;var START=NODELOAD_CONFIG.START;var DYGRAPH_SOURCE=require('./dygraph.tpl.js').DYGRAPH_SOURCE;var HTTP_SERVER=require('../http').HTTP_SERVER;} var Chart,timeFromStart;var Report=exports.Report=function(name){this.name=name;this.uid=util.uid();this.summary={};this.charts={};};Report.prototype={getChart:function(name){if(!this.charts[name]){this.charts[name]=new Chart(name);} return this.charts[name];},updateFromMonitor:function(monitor){monitor.on('update',this.doUpdateFromMonitor_.bind(this,monitor,''));return this;},updateFromMonitorGroup:function(monitorGroup){var self=this;monitorGroup.on('update',function(){util.forEach(monitorGroup.monitors,function(monitorname,monitor){self.doUpdateFromMonitor_(monitor,monitorname);});});return self;},doUpdateFromMonitor_:function(monitor,monitorname){var self=this;monitorname=monitorname?monitorname+' ':'';util.forEach(monitor.stats,function(statname,stat){util.forEach(stat.summary(),function(name,val){self.summary[self.name+' '+monitorname+statname+' '+name]=val;});if(monitor.interval[statname]){self.getChart(monitorname+statname).put(monitor.interval[statname].summary());}});}};var Chart=exports.Chart=function(name){this.name=name;this.uid=util.uid();this.columns=["time"];this.rows=[[timeFromStart()]];};Chart.prototype={put:function(data){var self=this,row=[timeFromStart()];util.forEach(data,function(column,val){var col=self.columns.indexOf(column);if(col<0){col=self.columns.length;self.columns.push(column);self.rows[0].push(0);} row[col]=val;});self.rows.push(row);}};var ReportGroup=exports.ReportGroup=function(){this.reports=[];this.logNameOrObject='results-'+START.getTime()+'.html';};ReportGroup.prototype={addReport:function(report){report=(typeof report==='string')?new Report(report):report;this.reports.push(report);return report;},setLogFile:function(logNameOrObject){this.logNameOrObject=logNameOrObject;},setLoggingEnabled:function(enabled){clearTimeout(this.loggingTimeoutId);if(enabled){this.logger=this.logger||(typeof this.logNameOrObject==='string')?new LogFile(this.logNameOrObject):this.logNameOrObject;this.loggingTimeoutId=setTimeout(this.writeToLog_.bind(this),this.refreshIntervalMs);}else if(this.logger){this.logger.close();this.logger=null;} return this;},reset:function(){this.reports={};},getHtml:function(){var self=this,t=template.create(REPORT_SUMMARY_TEMPLATE);return t({DYGRAPH_SOURCE:DYGRAPH_SOURCE,querystring:querystring,refreshPeriodMs:self.refreshIntervalMs,reports:self.reports});},writeToLog_:function(){this.loggingTimeoutId=setTimeout(this.writeToLog_.bind(this),this.refreshIntervalMs);this.logger.clear(this.getHtml());}};var REPORT_MANAGER=exports.REPORT_MANAGER=new ReportGroup();NODELOAD_CONFIG.on('apply',function(){REPORT_MANAGER.refreshIntervalMs=REPORT_MANAGER.refreshIntervalMs||NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS;REPORT_MANAGER.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED);});HTTP_SERVER.addRoute('^/$',function(url,req,res){var html=REPORT_MANAGER.getHtml();res.writeHead(200,{"Content-Type":"text/html","Content-Length":html.length});res.write(html);res.end();});HTTP_SERVER.addRoute('^/reports$',function(url,req,res){var json=JSON.stringify(REPORT_MANAGER.reports);res.writeHead(200,{"Content-Type":"application/json","Content-Length":json.length});res.write(json);res.end();});function timeFromStart(){return(Math.floor((new Date().getTime()-START)/600)/100);} var BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var http=require('http');var util=require('./util');var stats=require('./stats');var reporting=require('./reporting');var qputs=util.qputs;var qprint=util.qprint;var EventEmitter=require('events').EventEmitter;var MultiLoop=require('./loop').MultiLoop;var Monitor=require('./monitoring').Monitor;var Report=reporting.Report;var LogFile=stats.LogFile;var NODELOAD_CONFIG=require('./config').NODELOAD_CONFIG;var START=NODELOAD_CONFIG.START;var REPORT_MANAGER=reporting.REPORT_MANAGER;var HTTP_SERVER=require('./http').HTTP_SERVER;} -var TEST_OPTIONS={name:'Debug test',host:'localhost',port:8080,connectionGenerator:undefined,requestGenerator:undefined,requestLoop:undefined,method:'GET',path:'/',requestData:undefined,numUsers:10,loadProfile:undefined,targetRps:Infinity,userProfile:undefined,numRequests:Infinity,timeLimit:120,delay:0,stats:['latency','result-codes'],};var LoadTest,generateConnection,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} +var TEST_OPTIONS={name:'Debug test',host:'localhost',port:8080,connectionGenerator:undefined,requestGenerator:undefined,requestLoop:undefined,method:'GET',path:'/',requestData:undefined,numUsers:10,loadProfile:undefined,targetRps:Infinity,userProfile:undefined,numRequests:Infinity,timeLimit:120,delay:0,stats:['latency','result-codes'],};var LoadTest,generateConnection,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){spec=util.defaults(spec,TEST_OPTIONS);var generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} var request=client.request(spec.method,spec.path,{'host':spec.host});if(spec.requestData){request.write(spec.requestData);} return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection(spec.host,spec.port,!spec.requestLoop),concurrencyProfile:spec.userProfile||[[0,spec.numUsers]],rpsProfile:spec.loadProfile||[[0,spec.targetRps]],duration:spec.timeLimit,numberOfTimes:spec.numRequests,delay:spec.delay}),monitor=new Monitor(spec.stats),report=new Report(spec.name).updateFromMonitor(monitor);loop.on('add',function(loops){monitor.monitorObjects(loops,'startiteration','enditeration');});REPORT_MANAGER.addReport(report);monitor.name=spec.name;monitor.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED);return{spec:spec,loop:loop,monitor:monitor,report:report,};});var loadtest=new LoadTest(tests).start();return loadtest;};var LoadTest=exports.LoadTest=function LoadTest(tests){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.tests=tests;self.updateInterval=NODELOAD_CONFIG.MONITOR_INTERVAL_MS;self.interval={};self.stats={};self.tests.forEach(function(test){self.interval[test.spec.name]=test.monitor.interval;self.stats[test.spec.name]=test.monitor.stats;});self.finishChecker_=this.checkFinished_.bind(this);};util.inherits(LoadTest,EventEmitter);LoadTest.prototype.start=function(keepAlive){var self=this;self.keepAlive=keepAlive;process.nextTick(self.emit.bind(self,'start'));self.tests.forEach(function(test){test.loop.start();test.loop.on('end',self.finishChecker_);});if(!HTTP_SERVER.running&&NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);} return self;};LoadTest.prototype.stop=function(){this.tests.forEach(function(t){t.loop.stop();});return this;};LoadTest.prototype.update=function(){this.emit('update',this.interval,this.stats);this.tests.forEach(function(t){t.monitor.update();});qprint('.');};LoadTest.prototype.checkFinished_=function(){if(this.tests.some(function(t){return t.loop.running;})){return;} @@ -117,7 +117,7 @@ done();});req.end();};Slave.prototype.defineMethod=function(name,fun){var self=t var Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval;};util.inherits(Slaves,EventEmitter);Slaves.prototype.add=function(hostAndPort){var self=this,parts=hostAndPort.split(':'),host=parts[0],port=Number(parts[1])||8000,id=host+':'+port,slave=new Slave(id,host,port,self.masterEndpoint,self.pingInterval);self.slaves.push(slave);self[id]=slave;self[id].on('slaveError',function(err){self.emit('slaveError',slave,err);});self[id].on('start',function(){var allStarted=util.every(self.slaves,function(id,s){return s.state==='started';});if(!allStarted){return;} self.emit('start');});self[id].on('end',function(){var allStopped=util.every(self.slaves,function(id,s){return s.state!=='started';});if(!allStopped){return;} self.emit('end');});};Slaves.prototype.defineMethod=function(name,fun){var self=this;self.slaves.forEach(function(slave){slave.defineMethod(name,fun);});self[name]=function(){var args=arguments;return self.slaves.map(function(s){return s[name].apply(s,args);});};};Slaves.prototype.start=function(){this.slaves.forEach(function(s){s.start();});};Slaves.prototype.end=function(){this.slaves.forEach(function(s){s.end();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} -var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;this.masterClient_=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.slaveEndpoint_=this.createEndpoint_(server,spec.slaveMethods);this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.slaveEndpoint_.start();this.slaveEndpoint_.context.id=this.id;this.slaveEndpoint_.context.state='initialized';this.url=this.slaveEndpoint_.url;this.updateInterval=(spec.pingInterval>=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.updateInterval=0;this.slaveEndpoint_.end();if(this.masterClient_){this.masterClient_.destroy();} +var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this,slaveState='initialized';this.id=spec.id;this.masterClient_=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.slaveEndpoint_=this.createEndpoint_(server,spec.slaveMethods);this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.slaveEndpoint_.start();this.slaveEndpoint_.context.id=this.id;this.slaveEndpoint_.context.__defineGetter__('state',function(){return slaveState;});this.slaveEndpoint_.context.__defineSetter__('state',function(val){slaveState=val;self.update();});this.url=this.slaveEndpoint_.url;this.updateInterval=(spec.pingInterval>=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.updateInterval=0;this.slaveEndpoint_.end();if(this.masterClient_){this.masterClient_.destroy();} this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_){this.masterClient_.updateSlaveState_(this.slaveEndpoint_.context.state);}};SlaveNode.prototype.createEndpoint_=function(server,methods){var endpoint=new Endpoint(server);if(methods){try{methods.forEach(function(m){var fun;eval('fun='+m.fun);endpoint.defineMethod(m.name,fun);});}catch(e){endpoint.end();endpoint=null;throw e;}} return endpoint;};SlaveNode.prototype.createMasterClient_=function(masterUrl,methods){var parts=url.parse(masterUrl),masterClient=new EndpointClient(parts.hostname,Number(parts.port)||8000,parts.pathname);masterClient.defineMethod('updateSlaveState_');if(methods&&methods instanceof Array){methods.forEach(function(m){masterClient.defineMethod(m);});} masterClient.setStaticParams([this.id]);masterClient.on('error',this.emit.bind(this,'masterError'));return masterClient;};var installRemoteHandler=exports.installRemoteHandler=function(server){var slaveNodes=[];server.addRoute('^/remote/?$',function(path,req,res){if(req.method==='POST'){util.readStream(req,function(body){var slaveNode;try{body=JSON.parse(body);slaveNode=new SlaveNode(server,body);}catch(e){res.writeHead(400);res.end(e.toString());return;} diff --git a/test/loop.test.js b/test/loop.test.js index d5c0d96..fbb7969 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -23,7 +23,7 @@ module.exports = { beforeExit(function() { assert.equal(i, 5, 'loop executed incorrect number of times: ' + i); assert.ok(!l.running, 'loop still flagged as running'); - assert.ok(Math.abs(duration - 1000) <= 50, '1000 == ' + duration); + assert.ok(Math.abs(duration - 1000) <= 60, '1000 == ' + duration); }); }, 'example: use Scheduler to vary execution rate and concurrency': function (assert, beforeExit) {