diff --git a/.gitignore b/.gitignore index 907da6c..6e0acce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ results*.log results*.html -src/*.tpl.js +*.tpl.js +.reporting.test-output.html tmp diff --git a/Makefile b/Makefile index d9ef580..5a65103 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,19 @@ .PHONY: clean templates compile -SOURCES = src/header.js src/utils.js src/config.js src/api.js src/evloops.js src/scheduler.js src/monitor.js src/remote.js src/stats.js src/statsmgr.js src/log.js src/report.js src/http.js src/summary.tpl.js deps/dygraph.js deps/template.js +PROCESS_TPL = scripts/process_tpl.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 + rm -rf ./lib-cov + rm -f ./nodeload.js ./lib/reporting/*.tpl.js rm -f results-*-err.log results-*-stats.log results-*-summary.html - rm -r src/summary.tpl.js templates: - echo "var `head -n1 src/summary.tpl` = '`awk '{ if (NR > 1) { printf \"%s\\\\\\\\n\", $$0 }}' src/summary.tpl`'" > src/summary.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 - mkdir -p ./lib - cat $(SOURCES) | ./deps/jsmin.js > ./lib/nodeloadlib.js - cp src/options.js src/nodeload.js lib/ + echo "#!/usr/bin/env node" > ./nodeload.js + cat $(SOURCES) | ./scripts/jsmin.js >> ./nodeload.js + chmod +x ./nodeload.js \ No newline at end of file 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..560a7c3 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,171 @@ -NODELOAD +INSTALLING ================ -`nodeload` is both a **standalone tool** and a **`node.js` library** for load testing HTTP services. +Make sure [node.js](http://nodejs.org/#download) is installed. Then install `nodeload`: + +1. 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. +2. 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/'). +3. 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 independent [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 by example and selectively use the parts of a 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. +Look for 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). Here are simple examples of each module: +### [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 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 [`examples/remotetesting.ex.js`](https://github.com/benschmaus/nodeload/tree/master/examples/remotetesting.ex.js) and [`examples/remote.ex.js`](https://github.com/benschmaus/nodeload/tree/master/examples/remote.ex.js) for examples or read 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 [`test/monitoring.test.js`](https://github.com/benschmaus/nodeload/tree/master/test/monitoring.test.js) for examples or read 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 graph values over time and present it in a auto-updating HTML page. See [`test/reporting.test.js`](https://github.com/benschmaus/nodeload/tree/master/test/reporting.test.js) for examples or read 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 timeout = 0; timeout < 5000; timeout+=500) { + setTimeout(function() { + chart.put({ + 'Pareto': stats.nextPareto(0, 100), + 'Gaussian': stats.nextGaussian() + }); + }, timeout); + } + +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 or 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 [`test/http.test.js`](https://github.com/benschmaus/nodeload/tree/master/test/http.test.js) for examples or read 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 +================ +File bugs on [github](https://github.com/benschmaus/nodeload/issues), email any of the authors, and fork away. [doc/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. Contributions are always welcome. \ No newline at end of file diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 0000000..f00a1e1 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,43 @@ +## v0.2.0 (2010/12/01) ## + +This release is a substantial, non-backwards-compatible rewrite of nodeload. The major features are: + +* [npm](http://npmjs.org/) compatibility +* Independently usable modules: loop, stats, monitoring, http, reporting, and remote +* Addition of load and user profiles + +Specific changes to note are: + +* npm should be used to build the source + + [~/nodeload]> curl http://npmjs.org/install.sh | sh # install npm if not already installed + [~/nodeload]> npm link + +* `nodeload` is renamed to `nl` and `nodeloadlib` to `nodeload`. + +* addTest() / addRamp() / runTest() is replaced by run(): + + var nl = require('nodeload'); + var loadtest = nl.run({ ... test specications ... }, ...); + +* remoteTest() / remoteStart() is replaced by LoadTestCluster.run: + + var nl = require('nodeload'); + var cluster = new nl.LoadTestCluster(master:port, [slaves:port, ...]); + cluster.run({ ... test specifications ...}); + +* Callbacks and most of the globals (except `HTTP_SERVER` and `REPORT_MANAGER`) have been removed. Instead EventEmitters are used throughout. For example, run() returns an instance of LoadTest, which emits 'update' and 'end' events, replacing the need for both `TEST_MONITOR` and the startTests() callback parameter. + +* Scheduler has been replaced by MultiLoop, which also understands load & concurrency profiles. + +* Statistics tracking works through event handlers now rather than by wrapping the loop function. See monitoring/monitor.js. + +## v0.100.0 (2010/10/06) ## + +This release adds nodeloadlib and moves to Dygraph for charting. + +## v0.1.0 to v0.1.2 (2010/02/27) ## + +Initial releases of nodeload. Tags correspond to node compatible versions. To find a version of node that's compatible with a tag release do `git show `. + + For example: git show v0.1.1 \ No newline at end of file diff --git a/THANKS b/THANKS deleted file mode 100644 index f43c173..0000000 --- a/THANKS +++ /dev/null @@ -1,5 +0,0 @@ -The following people have contributed to nodeload: - -Benjamin Schmaus -Jonathan Lee -Robert Newson diff --git a/TODO b/TODO index 72513a1..a0ae77b 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,3 @@ -- Real build system -- Proper node.js packaging with npm -- Fix remote.js: - - MASTER/SLAVE controllers (handle tests) + RemoteWorkerPool/RemoteWorker (pings) - - multiple TEST_MONITOR.on('...') on slaves across tests - - master can't handle multiple test runs -- Use git submodules - Console - Update charts & summary on real data - Update test spec on real data @@ -18,5 +11,11 @@ - Edit test spec and "restart" (stop existing jobs / start updated test) - Clean up removed nodes properly - Download data as csv -- Unit testing with a framework +- Console webpage (stats) +- Console webpage (node manager) +- Add mem, disk io read + write + wait monitoring remote.ex.js +- Remote testing should also aggregate summary-only stats (e.g. uniques) +- Use stats.StatsGroup in monitoring and remote +- Update READMEs - Write a DEVELOPERS doc that explains the components +- Add zipf number generator diff --git a/deps/dygraph-LICENSE.txt b/deps/dygraph-LICENSE.txt deleted file mode 100644 index 536c0a8..0000000 --- a/deps/dygraph-LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2009 Dan Vanderkam - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/deps/dygraph.js b/deps/dygraph.js deleted file mode 100644 index aa823b5..0000000 --- a/deps/dygraph.js +++ /dev/null @@ -1,2 +0,0 @@ -// This is the Dygraph library available at http://github.com/danvk/dygraphs -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};'; diff --git a/deps/flot/excanvas.js b/deps/flot/excanvas.js deleted file mode 100644 index c40d6f7..0000000 --- a/deps/flot/excanvas.js +++ /dev/null @@ -1,1427 +0,0 @@ -// Copyright 2006 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -// Known Issues: -// -// * Patterns only support repeat. -// * Radial gradient are not implemented. The VML version of these look very -// different from the canvas one. -// * Clipping paths are not implemented. -// * Coordsize. The width and height attribute have higher priority than the -// width and height style values which isn't correct. -// * Painting mode isn't implemented. -// * Canvas width/height should is using content-box by default. IE in -// Quirks mode will draw the canvas using border-box. Either change your -// doctype to HTML5 -// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) -// or use Box Sizing Behavior from WebFX -// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) -// * Non uniform scaling does not correctly scale strokes. -// * Filling very large shapes (above 5000 points) is buggy. -// * Optimize. There is always room for speed improvements. - -// Only add this code if we do not already have a canvas implementation -if (!document.createElement('canvas').getContext) { - -(function() { - - // alias some functions to make (compiled) code shorter - var m = Math; - var mr = m.round; - var ms = m.sin; - var mc = m.cos; - var abs = m.abs; - var sqrt = m.sqrt; - - // this is used for sub pixel precision - var Z = 10; - var Z2 = Z / 2; - - /** - * This funtion is assigned to the elements as element.getContext(). - * @this {HTMLElement} - * @return {CanvasRenderingContext2D_} - */ - function getContext() { - return this.context_ || - (this.context_ = new CanvasRenderingContext2D_(this)); - } - - var slice = Array.prototype.slice; - - /** - * Binds a function to an object. The returned function will always use the - * passed in {@code obj} as {@code this}. - * - * Example: - * - * g = bind(f, obj, a, b) - * g(c, d) // will do f.call(obj, a, b, c, d) - * - * @param {Function} f The function to bind the object to - * @param {Object} obj The object that should act as this when the function - * is called - * @param {*} var_args Rest arguments that will be used as the initial - * arguments when the function is called - * @return {Function} A new function that has bound this - */ - function bind(f, obj, var_args) { - var a = slice.call(arguments, 2); - return function() { - return f.apply(obj, a.concat(slice.call(arguments))); - }; - } - - function encodeHtmlAttribute(s) { - return String(s).replace(/&/g, '&').replace(/"/g, '"'); - } - - function addNamespacesAndStylesheet(doc) { - // create xmlns - if (!doc.namespaces['g_vml_']) { - doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml', - '#default#VML'); - - } - if (!doc.namespaces['g_o_']) { - doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office', - '#default#VML'); - } - - // Setup default CSS. Only add one style sheet per document - if (!doc.styleSheets['ex_canvas_']) { - var ss = doc.createStyleSheet(); - ss.owningElement.id = 'ex_canvas_'; - ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + - // default size is 300x150 in Gecko and Opera - 'text-align:left;width:300px;height:150px}'; - } - } - - // Add namespaces and stylesheet at startup. - addNamespacesAndStylesheet(document); - - var G_vmlCanvasManager_ = { - init: function(opt_doc) { - if (/MSIE/.test(navigator.userAgent) && !window.opera) { - var doc = opt_doc || document; - // Create a dummy element so that IE will allow canvas elements to be - // recognized. - doc.createElement('canvas'); - doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); - } - }, - - init_: function(doc) { - // find all canvas elements - var els = doc.getElementsByTagName('canvas'); - for (var i = 0; i < els.length; i++) { - this.initElement(els[i]); - } - }, - - /** - * Public initializes a canvas element so that it can be used as canvas - * element from now on. This is called automatically before the page is - * loaded but if you are creating elements using createElement you need to - * make sure this is called on the element. - * @param {HTMLElement} el The canvas element to initialize. - * @return {HTMLElement} the element that was created. - */ - initElement: function(el) { - if (!el.getContext) { - el.getContext = getContext; - - // Add namespaces and stylesheet to document of the element. - addNamespacesAndStylesheet(el.ownerDocument); - - // Remove fallback content. There is no way to hide text nodes so we - // just remove all childNodes. We could hide all elements and remove - // text nodes but who really cares about the fallback content. - el.innerHTML = ''; - - // do not use inline function because that will leak memory - el.attachEvent('onpropertychange', onPropertyChange); - el.attachEvent('onresize', onResize); - - var attrs = el.attributes; - if (attrs.width && attrs.width.specified) { - // TODO: use runtimeStyle and coordsize - // el.getContext().setWidth_(attrs.width.nodeValue); - el.style.width = attrs.width.nodeValue + 'px'; - } else { - el.width = el.clientWidth; - } - if (attrs.height && attrs.height.specified) { - // TODO: use runtimeStyle and coordsize - // el.getContext().setHeight_(attrs.height.nodeValue); - el.style.height = attrs.height.nodeValue + 'px'; - } else { - el.height = el.clientHeight; - } - //el.getContext().setCoordsize_() - } - return el; - } - }; - - function onPropertyChange(e) { - var el = e.srcElement; - - switch (e.propertyName) { - case 'width': - el.getContext().clearRect(); - el.style.width = el.attributes.width.nodeValue + 'px'; - // In IE8 this does not trigger onresize. - el.firstChild.style.width = el.clientWidth + 'px'; - break; - case 'height': - el.getContext().clearRect(); - el.style.height = el.attributes.height.nodeValue + 'px'; - el.firstChild.style.height = el.clientHeight + 'px'; - break; - } - } - - function onResize(e) { - var el = e.srcElement; - if (el.firstChild) { - el.firstChild.style.width = el.clientWidth + 'px'; - el.firstChild.style.height = el.clientHeight + 'px'; - } - } - - G_vmlCanvasManager_.init(); - - // precompute "00" to "FF" - var decToHex = []; - for (var i = 0; i < 16; i++) { - for (var j = 0; j < 16; j++) { - decToHex[i * 16 + j] = i.toString(16) + j.toString(16); - } - } - - function createMatrixIdentity() { - return [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1] - ]; - } - - function matrixMultiply(m1, m2) { - var result = createMatrixIdentity(); - - for (var x = 0; x < 3; x++) { - for (var y = 0; y < 3; y++) { - var sum = 0; - - for (var z = 0; z < 3; z++) { - sum += m1[x][z] * m2[z][y]; - } - - result[x][y] = sum; - } - } - return result; - } - - function copyState(o1, o2) { - o2.fillStyle = o1.fillStyle; - o2.lineCap = o1.lineCap; - o2.lineJoin = o1.lineJoin; - o2.lineWidth = o1.lineWidth; - o2.miterLimit = o1.miterLimit; - o2.shadowBlur = o1.shadowBlur; - o2.shadowColor = o1.shadowColor; - o2.shadowOffsetX = o1.shadowOffsetX; - o2.shadowOffsetY = o1.shadowOffsetY; - o2.strokeStyle = o1.strokeStyle; - o2.globalAlpha = o1.globalAlpha; - o2.font = o1.font; - o2.textAlign = o1.textAlign; - o2.textBaseline = o1.textBaseline; - o2.arcScaleX_ = o1.arcScaleX_; - o2.arcScaleY_ = o1.arcScaleY_; - o2.lineScale_ = o1.lineScale_; - } - - var colorData = { - aliceblue: '#F0F8FF', - antiquewhite: '#FAEBD7', - aquamarine: '#7FFFD4', - azure: '#F0FFFF', - beige: '#F5F5DC', - bisque: '#FFE4C4', - black: '#000000', - blanchedalmond: '#FFEBCD', - 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', - darkgrey: '#A9A9A9', - darkkhaki: '#BDB76B', - darkmagenta: '#8B008B', - darkolivegreen: '#556B2F', - darkorange: '#FF8C00', - darkorchid: '#9932CC', - darkred: '#8B0000', - darksalmon: '#E9967A', - darkseagreen: '#8FBC8F', - darkslateblue: '#483D8B', - darkslategray: '#2F4F4F', - darkslategrey: '#2F4F4F', - darkturquoise: '#00CED1', - darkviolet: '#9400D3', - deeppink: '#FF1493', - deepskyblue: '#00BFFF', - dimgray: '#696969', - dimgrey: '#696969', - dodgerblue: '#1E90FF', - firebrick: '#B22222', - floralwhite: '#FFFAF0', - forestgreen: '#228B22', - gainsboro: '#DCDCDC', - ghostwhite: '#F8F8FF', - gold: '#FFD700', - goldenrod: '#DAA520', - grey: '#808080', - 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', - lightgreen: '#90EE90', - lightgrey: '#D3D3D3', - lightpink: '#FFB6C1', - lightsalmon: '#FFA07A', - lightseagreen: '#20B2AA', - lightskyblue: '#87CEFA', - lightslategray: '#778899', - lightslategrey: '#778899', - lightsteelblue: '#B0C4DE', - lightyellow: '#FFFFE0', - limegreen: '#32CD32', - linen: '#FAF0E6', - magenta: '#FF00FF', - mediumaquamarine: '#66CDAA', - mediumblue: '#0000CD', - mediumorchid: '#BA55D3', - mediumpurple: '#9370DB', - mediumseagreen: '#3CB371', - mediumslateblue: '#7B68EE', - mediumspringgreen: '#00FA9A', - mediumturquoise: '#48D1CC', - mediumvioletred: '#C71585', - midnightblue: '#191970', - mintcream: '#F5FFFA', - mistyrose: '#FFE4E1', - moccasin: '#FFE4B5', - navajowhite: '#FFDEAD', - oldlace: '#FDF5E6', - olivedrab: '#6B8E23', - orange: '#FFA500', - orangered: '#FF4500', - orchid: '#DA70D6', - palegoldenrod: '#EEE8AA', - palegreen: '#98FB98', - paleturquoise: '#AFEEEE', - palevioletred: '#DB7093', - papayawhip: '#FFEFD5', - peachpuff: '#FFDAB9', - peru: '#CD853F', - pink: '#FFC0CB', - plum: '#DDA0DD', - powderblue: '#B0E0E6', - rosybrown: '#BC8F8F', - royalblue: '#4169E1', - saddlebrown: '#8B4513', - salmon: '#FA8072', - sandybrown: '#F4A460', - seagreen: '#2E8B57', - seashell: '#FFF5EE', - sienna: '#A0522D', - skyblue: '#87CEEB', - slateblue: '#6A5ACD', - slategray: '#708090', - slategrey: '#708090', - snow: '#FFFAFA', - springgreen: '#00FF7F', - steelblue: '#4682B4', - tan: '#D2B48C', - thistle: '#D8BFD8', - tomato: '#FF6347', - turquoise: '#40E0D0', - violet: '#EE82EE', - wheat: '#F5DEB3', - whitesmoke: '#F5F5F5', - yellowgreen: '#9ACD32' - }; - - - function getRgbHslContent(styleString) { - var start = styleString.indexOf('(', 3); - var end = styleString.indexOf(')', start + 1); - var parts = styleString.substring(start + 1, end).split(','); - // add alpha if needed - if (parts.length == 4 && styleString.substr(3, 1) == 'a') { - alpha = Number(parts[3]); - } else { - parts[3] = 1; - } - return parts; - } - - function percent(s) { - return parseFloat(s) / 100; - } - - function clamp(v, min, max) { - return Math.min(max, Math.max(min, v)); - } - - function hslToRgb(parts){ - var r, g, b; - h = parseFloat(parts[0]) / 360 % 360; - if (h < 0) - h++; - s = clamp(percent(parts[1]), 0, 1); - l = clamp(percent(parts[2]), 0, 1); - if (s == 0) { - r = g = b = l; // achromatic - } else { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hueToRgb(p, q, h + 1 / 3); - g = hueToRgb(p, q, h); - b = hueToRgb(p, q, h - 1 / 3); - } - - return '#' + decToHex[Math.floor(r * 255)] + - decToHex[Math.floor(g * 255)] + - decToHex[Math.floor(b * 255)]; - } - - function hueToRgb(m1, m2, h) { - if (h < 0) - h++; - if (h > 1) - h--; - - if (6 * h < 1) - return m1 + (m2 - m1) * 6 * h; - else if (2 * h < 1) - return m2; - else if (3 * h < 2) - return m1 + (m2 - m1) * (2 / 3 - h) * 6; - else - return m1; - } - - function processStyle(styleString) { - var str, alpha = 1; - - styleString = String(styleString); - if (styleString.charAt(0) == '#') { - str = styleString; - } else if (/^rgb/.test(styleString)) { - var parts = getRgbHslContent(styleString); - var str = '#', n; - for (var i = 0; i < 3; i++) { - if (parts[i].indexOf('%') != -1) { - n = Math.floor(percent(parts[i]) * 255); - } else { - n = Number(parts[i]); - } - str += decToHex[clamp(n, 0, 255)]; - } - alpha = parts[3]; - } else if (/^hsl/.test(styleString)) { - var parts = getRgbHslContent(styleString); - str = hslToRgb(parts); - alpha = parts[3]; - } else { - str = colorData[styleString] || styleString; - } - return {color: str, alpha: alpha}; - } - - var DEFAULT_STYLE = { - style: 'normal', - variant: 'normal', - weight: 'normal', - size: 10, - family: 'sans-serif' - }; - - // Internal text style cache - var fontStyleCache = {}; - - function processFontStyle(styleString) { - if (fontStyleCache[styleString]) { - return fontStyleCache[styleString]; - } - - var el = document.createElement('div'); - var style = el.style; - try { - style.font = styleString; - } catch (ex) { - // Ignore failures to set to invalid font. - } - - return fontStyleCache[styleString] = { - style: style.fontStyle || DEFAULT_STYLE.style, - variant: style.fontVariant || DEFAULT_STYLE.variant, - weight: style.fontWeight || DEFAULT_STYLE.weight, - size: style.fontSize || DEFAULT_STYLE.size, - family: style.fontFamily || DEFAULT_STYLE.family - }; - } - - function getComputedStyle(style, element) { - var computedStyle = {}; - - for (var p in style) { - computedStyle[p] = style[p]; - } - - // Compute the size - var canvasFontSize = parseFloat(element.currentStyle.fontSize), - fontSize = parseFloat(style.size); - - if (typeof style.size == 'number') { - computedStyle.size = style.size; - } else if (style.size.indexOf('px') != -1) { - computedStyle.size = fontSize; - } else if (style.size.indexOf('em') != -1) { - computedStyle.size = canvasFontSize * fontSize; - } else if(style.size.indexOf('%') != -1) { - computedStyle.size = (canvasFontSize / 100) * fontSize; - } else if (style.size.indexOf('pt') != -1) { - computedStyle.size = fontSize / .75; - } else { - computedStyle.size = canvasFontSize; - } - - // Different scaling between normal text and VML text. This was found using - // trial and error to get the same size as non VML text. - computedStyle.size *= 0.981; - - return computedStyle; - } - - function buildStyle(style) { - return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + - style.size + 'px ' + style.family; - } - - function processLineCap(lineCap) { - switch (lineCap) { - case 'butt': - return 'flat'; - case 'round': - return 'round'; - case 'square': - default: - return 'square'; - } - } - - /** - * This class implements CanvasRenderingContext2D interface as described by - * the WHATWG. - * @param {HTMLElement} surfaceElement The element that the 2D context should - * be associated with - */ - function CanvasRenderingContext2D_(surfaceElement) { - this.m_ = createMatrixIdentity(); - - this.mStack_ = []; - this.aStack_ = []; - this.currentPath_ = []; - - // Canvas context properties - this.strokeStyle = '#000'; - this.fillStyle = '#000'; - - this.lineWidth = 1; - this.lineJoin = 'miter'; - this.lineCap = 'butt'; - this.miterLimit = Z * 1; - this.globalAlpha = 1; - this.font = '10px sans-serif'; - this.textAlign = 'left'; - this.textBaseline = 'alphabetic'; - this.canvas = surfaceElement; - - var el = surfaceElement.ownerDocument.createElement('div'); - el.style.width = surfaceElement.clientWidth + 'px'; - el.style.height = surfaceElement.clientHeight + 'px'; - el.style.overflow = 'hidden'; - el.style.position = 'absolute'; - surfaceElement.appendChild(el); - - this.element_ = el; - this.arcScaleX_ = 1; - this.arcScaleY_ = 1; - this.lineScale_ = 1; - } - - var contextPrototype = CanvasRenderingContext2D_.prototype; - contextPrototype.clearRect = function() { - if (this.textMeasureEl_) { - this.textMeasureEl_.removeNode(true); - this.textMeasureEl_ = null; - } - this.element_.innerHTML = ''; - }; - - contextPrototype.beginPath = function() { - // TODO: Branch current matrix so that save/restore has no effect - // as per safari docs. - this.currentPath_ = []; - }; - - contextPrototype.moveTo = function(aX, aY) { - var p = this.getCoords_(aX, aY); - this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); - this.currentX_ = p.x; - this.currentY_ = p.y; - }; - - contextPrototype.lineTo = function(aX, aY) { - var p = this.getCoords_(aX, aY); - this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); - - this.currentX_ = p.x; - this.currentY_ = p.y; - }; - - contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, - aCP2x, aCP2y, - aX, aY) { - var p = this.getCoords_(aX, aY); - var cp1 = this.getCoords_(aCP1x, aCP1y); - var cp2 = this.getCoords_(aCP2x, aCP2y); - bezierCurveTo(this, cp1, cp2, p); - }; - - // Helper function that takes the already fixed cordinates. - function bezierCurveTo(self, cp1, cp2, p) { - self.currentPath_.push({ - type: 'bezierCurveTo', - cp1x: cp1.x, - cp1y: cp1.y, - cp2x: cp2.x, - cp2y: cp2.y, - x: p.x, - y: p.y - }); - self.currentX_ = p.x; - self.currentY_ = p.y; - } - - contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { - // the following is lifted almost directly from - // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes - - var cp = this.getCoords_(aCPx, aCPy); - var p = this.getCoords_(aX, aY); - - var cp1 = { - x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), - y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) - }; - var cp2 = { - x: cp1.x + (p.x - this.currentX_) / 3.0, - y: cp1.y + (p.y - this.currentY_) / 3.0 - }; - - bezierCurveTo(this, cp1, cp2, p); - }; - - contextPrototype.arc = function(aX, aY, aRadius, - aStartAngle, aEndAngle, aClockwise) { - aRadius *= Z; - var arcType = aClockwise ? 'at' : 'wa'; - - var xStart = aX + mc(aStartAngle) * aRadius - Z2; - var yStart = aY + ms(aStartAngle) * aRadius - Z2; - - var xEnd = aX + mc(aEndAngle) * aRadius - Z2; - var yEnd = aY + ms(aEndAngle) * aRadius - Z2; - - // IE won't render arches drawn counter clockwise if xStart == xEnd. - if (xStart == xEnd && !aClockwise) { - xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something - // that can be represented in binary - } - - var p = this.getCoords_(aX, aY); - var pStart = this.getCoords_(xStart, yStart); - var pEnd = this.getCoords_(xEnd, yEnd); - - this.currentPath_.push({type: arcType, - x: p.x, - y: p.y, - radius: aRadius, - xStart: pStart.x, - yStart: pStart.y, - xEnd: pEnd.x, - yEnd: pEnd.y}); - - }; - - contextPrototype.rect = function(aX, aY, aWidth, aHeight) { - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - }; - - contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { - var oldPath = this.currentPath_; - this.beginPath(); - - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - this.stroke(); - - this.currentPath_ = oldPath; - }; - - contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { - var oldPath = this.currentPath_; - this.beginPath(); - - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - this.fill(); - - this.currentPath_ = oldPath; - }; - - contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { - var gradient = new CanvasGradient_('gradient'); - gradient.x0_ = aX0; - gradient.y0_ = aY0; - gradient.x1_ = aX1; - gradient.y1_ = aY1; - return gradient; - }; - - contextPrototype.createRadialGradient = function(aX0, aY0, aR0, - aX1, aY1, aR1) { - var gradient = new CanvasGradient_('gradientradial'); - gradient.x0_ = aX0; - gradient.y0_ = aY0; - gradient.r0_ = aR0; - gradient.x1_ = aX1; - gradient.y1_ = aY1; - gradient.r1_ = aR1; - return gradient; - }; - - contextPrototype.drawImage = function(image, var_args) { - var dx, dy, dw, dh, sx, sy, sw, sh; - - // to find the original width we overide the width and height - var oldRuntimeWidth = image.runtimeStyle.width; - var oldRuntimeHeight = image.runtimeStyle.height; - image.runtimeStyle.width = 'auto'; - image.runtimeStyle.height = 'auto'; - - // get the original size - var w = image.width; - var h = image.height; - - // and remove overides - image.runtimeStyle.width = oldRuntimeWidth; - image.runtimeStyle.height = oldRuntimeHeight; - - if (arguments.length == 3) { - dx = arguments[1]; - dy = arguments[2]; - sx = sy = 0; - sw = dw = w; - sh = dh = h; - } else if (arguments.length == 5) { - dx = arguments[1]; - dy = arguments[2]; - dw = arguments[3]; - dh = arguments[4]; - sx = sy = 0; - sw = w; - sh = h; - } else if (arguments.length == 9) { - sx = arguments[1]; - sy = arguments[2]; - sw = arguments[3]; - sh = arguments[4]; - dx = arguments[5]; - dy = arguments[6]; - dw = arguments[7]; - dh = arguments[8]; - } else { - throw Error('Invalid number of arguments'); - } - - var d = this.getCoords_(dx, dy); - - var w2 = sw / 2; - var h2 = sh / 2; - - var vmlStr = []; - - var W = 10; - var H = 10; - - // For some reason that I've now forgotten, using divs didn't work - vmlStr.push(' ' , - '', - ''); - - this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join('')); - }; - - contextPrototype.stroke = function(aFill) { - var W = 10; - var H = 10; - // Divide the shape into chunks if it's too long because IE has a limit - // somewhere for how long a VML shape can be. This simple division does - // not work with fills, only strokes, unfortunately. - var chunkSize = 5000; - - var min = {x: null, y: null}; - var max = {x: null, y: null}; - - for (var j = 0; j < this.currentPath_.length; j += chunkSize) { - var lineStr = []; - var lineOpen = false; - - lineStr.push(''); - - if (!aFill) { - appendStroke(this, lineStr); - } else { - appendFill(this, lineStr, min, max); - } - - lineStr.push(''); - - this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); - } - }; - - function appendStroke(ctx, lineStr) { - var a = processStyle(ctx.strokeStyle); - var color = a.color; - var opacity = a.alpha * ctx.globalAlpha; - var lineWidth = ctx.lineScale_ * ctx.lineWidth; - - // VML cannot correctly render a line if the width is less than 1px. - // In that case, we dilute the color to make the line look thinner. - if (lineWidth < 1) { - opacity *= lineWidth; - } - - lineStr.push( - '' - ); - } - - function appendFill(ctx, lineStr, min, max) { - var fillStyle = ctx.fillStyle; - var arcScaleX = ctx.arcScaleX_; - var arcScaleY = ctx.arcScaleY_; - var width = max.x - min.x; - var height = max.y - min.y; - if (fillStyle instanceof CanvasGradient_) { - // TODO: Gradients transformed with the transformation matrix. - var angle = 0; - var focus = {x: 0, y: 0}; - - // additional offset - var shift = 0; - // scale factor for offset - var expansion = 1; - - if (fillStyle.type_ == 'gradient') { - var x0 = fillStyle.x0_ / arcScaleX; - var y0 = fillStyle.y0_ / arcScaleY; - var x1 = fillStyle.x1_ / arcScaleX; - var y1 = fillStyle.y1_ / arcScaleY; - var p0 = ctx.getCoords_(x0, y0); - var p1 = ctx.getCoords_(x1, y1); - var dx = p1.x - p0.x; - var dy = p1.y - p0.y; - angle = Math.atan2(dx, dy) * 180 / Math.PI; - - // The angle should be a non-negative number. - if (angle < 0) { - angle += 360; - } - - // Very small angles produce an unexpected result because they are - // converted to a scientific notation string. - if (angle < 1e-6) { - angle = 0; - } - } else { - var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_); - focus = { - x: (p0.x - min.x) / width, - y: (p0.y - min.y) / height - }; - - width /= arcScaleX * Z; - height /= arcScaleY * Z; - var dimension = m.max(width, height); - shift = 2 * fillStyle.r0_ / dimension; - expansion = 2 * fillStyle.r1_ / dimension - shift; - } - - // We need to sort the color stops in ascending order by offset, - // otherwise IE won't interpret it correctly. - var stops = fillStyle.colors_; - stops.sort(function(cs1, cs2) { - return cs1.offset - cs2.offset; - }); - - var length = stops.length; - var color1 = stops[0].color; - var color2 = stops[length - 1].color; - var opacity1 = stops[0].alpha * ctx.globalAlpha; - var opacity2 = stops[length - 1].alpha * ctx.globalAlpha; - - var colors = []; - for (var i = 0; i < length; i++) { - var stop = stops[i]; - colors.push(stop.offset * expansion + shift + ' ' + stop.color); - } - - // When colors attribute is used, the meanings of opacity and o:opacity2 - // are reversed. - lineStr.push(''); - } else if (fillStyle instanceof CanvasPattern_) { - if (width && height) { - var deltaLeft = -min.x; - var deltaTop = -min.y; - lineStr.push(''); - } - } else { - var a = processStyle(ctx.fillStyle); - var color = a.color; - var opacity = a.alpha * ctx.globalAlpha; - lineStr.push(''); - } - } - - contextPrototype.fill = function() { - this.stroke(true); - }; - - contextPrototype.closePath = function() { - this.currentPath_.push({type: 'close'}); - }; - - /** - * @private - */ - contextPrototype.getCoords_ = function(aX, aY) { - var m = this.m_; - return { - x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, - y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 - }; - }; - - contextPrototype.save = function() { - var o = {}; - copyState(this, o); - this.aStack_.push(o); - this.mStack_.push(this.m_); - this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); - }; - - contextPrototype.restore = function() { - if (this.aStack_.length) { - copyState(this.aStack_.pop(), this); - this.m_ = this.mStack_.pop(); - } - }; - - function matrixIsFinite(m) { - return isFinite(m[0][0]) && isFinite(m[0][1]) && - isFinite(m[1][0]) && isFinite(m[1][1]) && - isFinite(m[2][0]) && isFinite(m[2][1]); - } - - function setM(ctx, m, updateLineScale) { - if (!matrixIsFinite(m)) { - return; - } - ctx.m_ = m; - - if (updateLineScale) { - // Get the line scale. - // Determinant of this.m_ means how much the area is enlarged by the - // transformation. So its square root can be used as a scale factor - // for width. - var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; - ctx.lineScale_ = sqrt(abs(det)); - } - } - - contextPrototype.translate = function(aX, aY) { - var m1 = [ - [1, 0, 0], - [0, 1, 0], - [aX, aY, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), false); - }; - - contextPrototype.rotate = function(aRot) { - var c = mc(aRot); - var s = ms(aRot); - - var m1 = [ - [c, s, 0], - [-s, c, 0], - [0, 0, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), false); - }; - - contextPrototype.scale = function(aX, aY) { - this.arcScaleX_ *= aX; - this.arcScaleY_ *= aY; - var m1 = [ - [aX, 0, 0], - [0, aY, 0], - [0, 0, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), true); - }; - - contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { - var m1 = [ - [m11, m12, 0], - [m21, m22, 0], - [dx, dy, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), true); - }; - - contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { - var m = [ - [m11, m12, 0], - [m21, m22, 0], - [dx, dy, 1] - ]; - - setM(this, m, true); - }; - - /** - * The text drawing function. - * The maxWidth argument isn't taken in account, since no browser supports - * it yet. - */ - contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) { - var m = this.m_, - delta = 1000, - left = 0, - right = delta, - offset = {x: 0, y: 0}, - lineStr = []; - - var fontStyle = getComputedStyle(processFontStyle(this.font), - this.element_); - - var fontStyleString = buildStyle(fontStyle); - - var elementStyle = this.element_.currentStyle; - var textAlign = this.textAlign.toLowerCase(); - switch (textAlign) { - case 'left': - case 'center': - case 'right': - break; - case 'end': - textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left'; - break; - case 'start': - textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left'; - break; - default: - textAlign = 'left'; - } - - // 1.75 is an arbitrary number, as there is no info about the text baseline - switch (this.textBaseline) { - case 'hanging': - case 'top': - offset.y = fontStyle.size / 1.75; - break; - case 'middle': - break; - default: - case null: - case 'alphabetic': - case 'ideographic': - case 'bottom': - offset.y = -fontStyle.size / 2.25; - break; - } - - switch(textAlign) { - case 'right': - left = delta; - right = 0.05; - break; - case 'center': - left = right = delta / 2; - break; - } - - var d = this.getCoords_(x + offset.x, y + offset.y); - - lineStr.push(''); - - if (stroke) { - appendStroke(this, lineStr); - } else { - // TODO: Fix the min and max params. - appendFill(this, lineStr, {x: -left, y: 0}, - {x: right, y: fontStyle.size}); - } - - var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' + - m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0'; - - var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z); - - lineStr.push('', - '', - ''); - - this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); - }; - - contextPrototype.fillText = function(text, x, y, maxWidth) { - this.drawText_(text, x, y, maxWidth, false); - }; - - contextPrototype.strokeText = function(text, x, y, maxWidth) { - this.drawText_(text, x, y, maxWidth, true); - }; - - contextPrototype.measureText = function(text) { - if (!this.textMeasureEl_) { - var s = ''; - this.element_.insertAdjacentHTML('beforeEnd', s); - this.textMeasureEl_ = this.element_.lastChild; - } - var doc = this.element_.ownerDocument; - this.textMeasureEl_.innerHTML = ''; - this.textMeasureEl_.style.font = this.font; - // Don't use innerHTML or innerText because they allow markup/whitespace. - this.textMeasureEl_.appendChild(doc.createTextNode(text)); - return {width: this.textMeasureEl_.offsetWidth}; - }; - - /******** STUBS ********/ - contextPrototype.clip = function() { - // TODO: Implement - }; - - contextPrototype.arcTo = function() { - // TODO: Implement - }; - - contextPrototype.createPattern = function(image, repetition) { - return new CanvasPattern_(image, repetition); - }; - - // Gradient / Pattern Stubs - function CanvasGradient_(aType) { - this.type_ = aType; - this.x0_ = 0; - this.y0_ = 0; - this.r0_ = 0; - this.x1_ = 0; - this.y1_ = 0; - this.r1_ = 0; - this.colors_ = []; - } - - CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { - aColor = processStyle(aColor); - this.colors_.push({offset: aOffset, - color: aColor.color, - alpha: aColor.alpha}); - }; - - function CanvasPattern_(image, repetition) { - assertImageIsValid(image); - switch (repetition) { - case 'repeat': - case null: - case '': - this.repetition_ = 'repeat'; - break - case 'repeat-x': - case 'repeat-y': - case 'no-repeat': - this.repetition_ = repetition; - break; - default: - throwException('SYNTAX_ERR'); - } - - this.src_ = image.src; - this.width_ = image.width; - this.height_ = image.height; - } - - function throwException(s) { - throw new DOMException_(s); - } - - function assertImageIsValid(img) { - if (!img || img.nodeType != 1 || img.tagName != 'IMG') { - throwException('TYPE_MISMATCH_ERR'); - } - if (img.readyState != 'complete') { - throwException('INVALID_STATE_ERR'); - } - } - - function DOMException_(s) { - this.code = this[s]; - this.message = s +': DOM Exception ' + this.code; - } - var p = DOMException_.prototype = new Error; - p.INDEX_SIZE_ERR = 1; - p.DOMSTRING_SIZE_ERR = 2; - p.HIERARCHY_REQUEST_ERR = 3; - p.WRONG_DOCUMENT_ERR = 4; - p.INVALID_CHARACTER_ERR = 5; - p.NO_DATA_ALLOWED_ERR = 6; - p.NO_MODIFICATION_ALLOWED_ERR = 7; - p.NOT_FOUND_ERR = 8; - p.NOT_SUPPORTED_ERR = 9; - p.INUSE_ATTRIBUTE_ERR = 10; - p.INVALID_STATE_ERR = 11; - p.SYNTAX_ERR = 12; - p.INVALID_MODIFICATION_ERR = 13; - p.NAMESPACE_ERR = 14; - p.INVALID_ACCESS_ERR = 15; - p.VALIDATION_ERR = 16; - p.TYPE_MISMATCH_ERR = 17; - - // set up externs - G_vmlCanvasManager = G_vmlCanvasManager_; - CanvasRenderingContext2D = CanvasRenderingContext2D_; - CanvasGradient = CanvasGradient_; - CanvasPattern = CanvasPattern_; - DOMException = DOMException_; -})(); - -} // if diff --git a/deps/flot/excanvas.min.js b/deps/flot/excanvas.min.js deleted file mode 100644 index 12c74f7..0000000 --- a/deps/flot/excanvas.min.js +++ /dev/null @@ -1 +0,0 @@ -if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&").replace(/"/g,""")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AHAL.x){AL.x=Z.x}if(AG.y==null||Z.yAL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push("')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push("')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push('')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push('','','');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z='';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()}; \ No newline at end of file diff --git a/deps/flot/jquery.colorhelpers.js b/deps/flot/jquery.colorhelpers.js deleted file mode 100644 index fa44961..0000000 --- a/deps/flot/jquery.colorhelpers.js +++ /dev/null @@ -1,174 +0,0 @@ -/* Plugin for jQuery for working with colors. - * - * Version 1.0. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() work in-place instead of returning - * new objects. - */ - -(function() { - jQuery.color = {}; - - // construct color object with some convenient chainable helpers - jQuery.color.make = function (r, g, b, a) { - var o = {}; - o.r = r || 0; - o.g = g || 0; - o.b = b || 0; - o.a = a != null ? a : 1; - - o.add = function (c, d) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] += d; - return o.normalize(); - }; - - o.scale = function (c, f) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] *= f; - return o.normalize(); - }; - - o.toString = function () { - if (o.a >= 1.0) { - return "rgb("+[o.r, o.g, o.b].join(",")+")"; - } else { - return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; - } - }; - - o.normalize = function () { - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - o.r = clamp(0, parseInt(o.r), 255); - o.g = clamp(0, parseInt(o.g), 255); - o.b = clamp(0, parseInt(o.b), 255); - o.a = clamp(0, o.a, 1); - return o; - }; - - o.clone = function () { - return jQuery.color.make(o.r, o.b, o.g, o.a); - }; - - return o.normalize(); - } - - // extract CSS color property from element, going up in the DOM - // if it's "transparent" - jQuery.color.extract = function (elem, css) { - var c; - do { - c = elem.css(css).toLowerCase(); - // keep going until we find an element that has color, or - // we hit the body - if (c != '' && c != 'transparent') - break; - elem = elem.parent(); - } while (!jQuery.nodeName(elem.get(0), "body")); - - // catch Safari's way of signalling transparent - if (c == "rgba(0, 0, 0, 0)") - c = "transparent"; - - return jQuery.color.parse(c); - } - - // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), - // returns color object - jQuery.color.parse = function (str) { - var res, m = jQuery.color.make; - - // Look for rgb(num,num,num) - if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); - - // Look for rgba(num,num,num,num) - if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); - - // Look for rgb(num%,num%,num%) - if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); - - // Look for rgba(num%,num%,num%,num) - if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); - - // Look for #a0b1c2 - if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) - return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); - - // Look for #fff - if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) - return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); - - // Otherwise, we're most likely dealing with a named color - var name = jQuery.trim(str).toLowerCase(); - if (name == "transparent") - return m(255, 255, 255, 0); - else { - res = lookupColors[name]; - return m(res[0], res[1], res[2]); - } - } - - var lookupColors = { - aqua:[0,255,255], - azure:[240,255,255], - beige:[245,245,220], - black:[0,0,0], - blue:[0,0,255], - brown:[165,42,42], - cyan:[0,255,255], - darkblue:[0,0,139], - darkcyan:[0,139,139], - darkgrey:[169,169,169], - darkgreen:[0,100,0], - darkkhaki:[189,183,107], - darkmagenta:[139,0,139], - darkolivegreen:[85,107,47], - darkorange:[255,140,0], - darkorchid:[153,50,204], - darkred:[139,0,0], - darksalmon:[233,150,122], - darkviolet:[148,0,211], - fuchsia:[255,0,255], - gold:[255,215,0], - green:[0,128,0], - indigo:[75,0,130], - khaki:[240,230,140], - lightblue:[173,216,230], - lightcyan:[224,255,255], - lightgreen:[144,238,144], - lightgrey:[211,211,211], - lightpink:[255,182,193], - lightyellow:[255,255,224], - lime:[0,255,0], - magenta:[255,0,255], - maroon:[128,0,0], - navy:[0,0,128], - olive:[128,128,0], - orange:[255,165,0], - pink:[255,192,203], - purple:[128,0,128], - violet:[128,0,128], - red:[255,0,0], - silver:[192,192,192], - white:[255,255,255], - yellow:[255,255,0] - }; -})(); diff --git a/deps/flot/jquery.colorhelpers.min.js b/deps/flot/jquery.colorhelpers.min.js deleted file mode 100644 index fafe905..0000000 --- a/deps/flot/jquery.colorhelpers.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return JH?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(); \ No newline at end of file diff --git a/deps/flot/jquery.flot.crosshair.js b/deps/flot/jquery.flot.crosshair.js deleted file mode 100644 index 11be113..0000000 --- a/deps/flot/jquery.flot.crosshair.js +++ /dev/null @@ -1,156 +0,0 @@ -/* -Flot plugin for showing a crosshair, thin lines, when the mouse hovers -over the plot. - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a -vertical crosshair that lets you trace the values on the x axis, "y" -enables a horizontal crosshair and "xy" enables them both. "color" is -the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"), -"lineWidth" is the width of the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair(pos) - - Set the position of the crosshair. Note that this is cleared if - the user moves the mouse. "pos" should be on the form { x: xpos, - y: ypos } (or x2 and y2 if you're using the secondary axes), which - is coincidentally the same format as what you get from a "plothover" - event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer - updating if the user moves the mouse. Optionally supply a position - (passed on to setCrosshair()) to move it to. - - Example usage: - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind("plothover", function (evt, position, item) { - if (item) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] }); - } - else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var axes = plot.getAxes(); - - crosshair.x = Math.max(0, Math.min(pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plot.width())); - crosshair.y = Math.max(0, Math.min(pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - } - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(function () { - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - }); - - eventHolder.mousemove(function (e) { - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - if (crosshair.locked) - return; - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - }); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - ctx.moveTo(crosshair.x, 0); - ctx.lineTo(crosshair.x, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - ctx.moveTo(0, crosshair.y); - ctx.lineTo(plot.width(), crosshair.y); - } - ctx.stroke(); - } - ctx.restore(); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/deps/flot/jquery.flot.crosshair.min.js b/deps/flot/jquery.flot.crosshair.min.js deleted file mode 100644 index ce689b1..0000000 --- a/deps/flot/jquery.flot.crosshair.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(B){var A={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function C(G){var H={x:-1,y:-1,locked:false};G.setCrosshair=function D(J){if(!J){H.x=-1}else{var I=G.getAxes();H.x=Math.max(0,Math.min(J.x!=null?I.xaxis.p2c(J.x):I.x2axis.p2c(J.x2),G.width()));H.y=Math.max(0,Math.min(J.y!=null?I.yaxis.p2c(J.y):I.y2axis.p2c(J.y2),G.height()))}G.triggerRedrawOverlay()};G.clearCrosshair=G.setCrosshair;G.lockCrosshair=function E(I){if(I){G.setCrosshair(I)}H.locked=true};G.unlockCrosshair=function F(){H.locked=false};G.hooks.bindEvents.push(function(J,I){if(!J.getOptions().crosshair.mode){return }I.mouseout(function(){if(H.x!=-1){H.x=-1;J.triggerRedrawOverlay()}});I.mousemove(function(K){if(J.getSelection&&J.getSelection()){H.x=-1;return }if(H.locked){return }var L=J.offset();H.x=Math.max(0,Math.min(K.pageX-L.left,J.width()));H.y=Math.max(0,Math.min(K.pageY-L.top,J.height()));J.triggerRedrawOverlay()})});G.hooks.drawOverlay.push(function(K,I){var L=K.getOptions().crosshair;if(!L.mode){return }var J=K.getPlotOffset();I.save();I.translate(J.left,J.top);if(H.x!=-1){I.strokeStyle=L.color;I.lineWidth=L.lineWidth;I.lineJoin="round";I.beginPath();if(L.mode.indexOf("x")!=-1){I.moveTo(H.x,0);I.lineTo(H.x,K.height())}if(L.mode.indexOf("y")!=-1){I.moveTo(0,H.y);I.lineTo(K.width(),H.y)}I.stroke()}I.restore()})}B.plot.plugins.push({init:C,options:A,name:"crosshair",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/deps/flot/jquery.flot.image.js b/deps/flot/jquery.flot.image.js deleted file mode 100644 index 90babf6..0000000 --- a/deps/flot/jquery.flot.image.js +++ /dev/null @@ -1,237 +0,0 @@ -/* -Flot plugin for plotting images, e.g. useful for putting ticks on a -prerendered complex visualization. - -The data syntax is [[image, x1, y1, x2, y2], ...] where (x1, y1) and -(x2, y2) are where you intend the two opposite corners of the image to -end up in the plot. Image must be a fully loaded Javascript image (you -can make one with new Image()). If the image is not complete, it's -skipped when plotting. - -There are two helpers included for retrieving images. The easiest work -the way that you put in URLs instead of images in the data (like -["myimage.png", 0, 0, 10, 10]), then call $.plot.image.loadData(data, -options, callback) where data and options are the same as you pass in -to $.plot. This loads the images, replaces the URLs in the data with -the corresponding images and calls "callback" when all images are -loaded (or failed loading). In the callback, you can then call $.plot -with the data set. See the included example. - -A more low-level helper, $.plot.image.load(urls, callback) is also -included. Given a list of URLs, it calls callback with an object -mapping from URL to Image object when all images are loaded or have -failed loading. - -Options for the plugin are - - series: { - images: { - show: boolean - anchor: "corner" or "center" - alpha: [0,1] - } - } - -which can be specified for a specific series - - $.plot($("#placeholder"), [{ data: [ ... ], images: { ... } ]) - -Note that because the data format is different from usual data points, -you can't use images with anything else in a specific data series. - -Setting "anchor" to "center" causes the pixels in the image to be -anchored at the corner pixel centers inside of at the pixel corners, -effectively letting half a pixel stick out to each side in the plot. - - -A possible future direction could be support for tiling for large -images (like Google Maps). - -*/ - -(function ($) { - var options = { - series: { - images: { - show: false, - alpha: 1, - anchor: "corner" // or "center" - } - } - }; - - $.plot.image = {}; - - $.plot.image.loadDataImages = function (series, options, callback) { - var urls = [], points = []; - - var defaultShow = options.series.images.show; - - $.each(series, function (i, s) { - if (!(defaultShow || s.images.show)) - return; - - if (s.data) - s = s.data; - - $.each(s, function (i, p) { - if (typeof p[0] == "string") { - urls.push(p[0]); - points.push(p); - } - }); - }); - - $.plot.image.load(urls, function (loadedImages) { - $.each(points, function (i, p) { - var url = p[0]; - if (loadedImages[url]) - p[0] = loadedImages[url]; - }); - - callback(); - }); - } - - $.plot.image.load = function (urls, callback) { - var missing = urls.length, loaded = {}; - if (missing == 0) - callback({}); - - $.each(urls, function (i, url) { - var handler = function () { - --missing; - - loaded[url] = this; - - if (missing == 0) - callback(loaded); - }; - - $('').load(handler).error(handler).attr('src', url); - }); - } - - function draw(plot, ctx) { - var plotOffset = plot.getPlotOffset(); - - $.each(plot.getData(), function (i, series) { - var points = series.datapoints.points, - ps = series.datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var img = points[i], - x1 = points[i + 1], y1 = points[i + 2], - x2 = points[i + 3], y2 = points[i + 4], - xaxis = series.xaxis, yaxis = series.yaxis, - tmp; - - // actually we should check img.complete, but it - // appears to be a somewhat unreliable indicator in - // IE6 (false even after load event) - if (!img || img.width <= 0 || img.height <= 0) - continue; - - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - // if the anchor is at the center of the pixel, expand the - // image by 1/2 pixel in each direction - if (series.images.anchor == "center") { - tmp = 0.5 * (x2-x1) / (img.width - 1); - x1 -= tmp; - x2 += tmp; - tmp = 0.5 * (y2-y1) / (img.height - 1); - y1 -= tmp; - y2 += tmp; - } - - // clip - if (x1 == x2 || y1 == y2 || - x1 >= xaxis.max || x2 <= xaxis.min || - y1 >= yaxis.max || y2 <= yaxis.min) - continue; - - var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; - if (x1 < xaxis.min) { - sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); - x1 = xaxis.min; - } - - if (x2 > xaxis.max) { - sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); - x2 = xaxis.max; - } - - if (y1 < yaxis.min) { - sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); - y1 = yaxis.min; - } - - if (y2 > yaxis.max) { - sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); - y2 = yaxis.max; - } - - x1 = xaxis.p2c(x1); - x2 = xaxis.p2c(x2); - y1 = yaxis.p2c(y1); - y2 = yaxis.p2c(y2); - - // the transformation may have swapped us - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - tmp = ctx.globalAlpha; - ctx.globalAlpha *= series.images.alpha; - ctx.drawImage(img, - sx1, sy1, sx2 - sx1, sy2 - sy1, - x1 + plotOffset.left, y1 + plotOffset.top, - x2 - x1, y2 - y1); - ctx.globalAlpha = tmp; - } - }); - } - - function processRawData(plot, series, data, datapoints) { - if (!series.images.show) - return; - - // format is Image, x1, y1, x2, y2 (opposite corners) - datapoints.format = [ - { required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.draw.push(draw); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'image', - version: '1.1' - }); -})(jQuery); diff --git a/deps/flot/jquery.flot.image.min.js b/deps/flot/jquery.flot.image.min.js deleted file mode 100644 index eb16cb1..0000000 --- a/deps/flot/jquery.flot.image.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(D){var B={series:{images:{show:false,alpha:1,anchor:"corner"}}};D.plot.image={};D.plot.image.loadDataImages=function(G,F,K){var J=[],H=[];var I=F.series.images.show;D.each(G,function(L,M){if(!(I||M.images.show)){return }if(M.data){M=M.data}D.each(M,function(N,O){if(typeof O[0]=="string"){J.push(O[0]);H.push(O)}})});D.plot.image.load(J,function(L){D.each(H,function(N,O){var M=O[0];if(L[M]){O[0]=L[M]}});K()})};D.plot.image.load=function(H,I){var G=H.length,F={};if(G==0){I({})}D.each(H,function(K,J){var L=function(){--G;F[J]=this;if(G==0){I(F)}};D("").load(L).error(L).attr("src",J)})};function A(H,F){var G=H.getPlotOffset();D.each(H.getData(),function(O,P){var X=P.datapoints.points,I=P.datapoints.pointsize;for(var O=0;OK){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}if(P.images.anchor=="center"){N=0.5*(K-M)/(Q.width-1);M-=N;K+=N;N=0.5*(T-V)/(Q.height-1);V-=N;T+=N}if(M==K||V==T||M>=W.max||K<=W.min||V>=S.max||T<=S.min){continue}var L=0,U=0,J=Q.width,R=Q.height;if(MW.max){J+=(J-L)*(W.max-K)/(K-M);K=W.max}if(VS.max){U+=(U-R)*(S.max-T)/(T-V);T=S.max}M=W.p2c(M);K=W.p2c(K);V=S.p2c(V);T=S.p2c(T);if(M>K){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}N=F.globalAlpha;F.globalAlpha*=P.images.alpha;F.drawImage(Q,L,U,J-L,R-U,M+G.left,V+G.top,K-M,T-V);F.globalAlpha=N}})}function C(I,F,G,H){if(!F.images.show){return }H.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function E(F){F.hooks.processRawData.push(C);F.hooks.draw.push(A)}D.plot.plugins.push({init:E,options:B,name:"image",version:"1.1"})})(jQuery); \ No newline at end of file diff --git a/deps/flot/jquery.flot.js b/deps/flot/jquery.flot.js deleted file mode 100644 index 6534a46..0000000 --- a/deps/flot/jquery.flot.js +++ /dev/null @@ -1,2119 +0,0 @@ -/* Javascript plotting library for jQuery, v. 0.6. - * - * Released under the MIT license by IOLA, December 2007. - * - */ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.0. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() work in-place instead of returning - * new objects. - */ -(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return JH?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(); - -// the actual Flot code -(function($) { - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of colums in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85 // set to 0 to avoid background - }, - xaxis: { - mode: null, // null or "time" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - - // mode specific options - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null, // number or [number, "unit"] - monthNames: null, // list of names of months - timeformat: null, // format string to use - twelveHourClock: false // 12 or 24 time in time mode - }, - yaxis: { - autoscaleMargin: 0.02 - }, - x2axis: { - autoscaleMargin: null - }, - y2axis: { - autoscaleMargin: 0.02 - }, - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff" - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // or "center" - horizontal: false // when horizontal, left is now top - }, - shadowSize: 3 - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - tickColor: "rgba(0,0,0,0.15)", // color used for the ticks - labelMargin: 5, // in pixels - borderWidth: 2, // in pixels - borderColor: null, // set if different from the grid color - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - hooks: {} - }, - canvas = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} }, - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - canvasWidth = 0, canvasHeight = 0, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - draw: [], - bindEvents: [], - drawOverlay: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return canvas; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function() { return series; }; - plot.getAxes = function() { return axes; }; - plot.getOptions = function() { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left), - top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) }; - }; - - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - constructCanvas(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - $.extend(true, options, opts); - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize) - options.series.shadowSize = options.shadowSize; - - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisSpecToRealAxis(obj, attr) { - var a = obj[attr]; - if (!a || a == 1) - return axes[attr]; - if (typeof a == "number") - return axes[attr.charAt(0) + a + attr.slice(1)]; - return a; // assume it's OK - } - - function fillInSeriesOptions() { - var i; - - // collect what we already got of colors - var neededColors = series.length, - usedColors = [], - assignedColors = []; - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - --neededColors; - if (typeof sc == "number") - assignedColors.push(sc); - else - usedColors.push($.color.parse(series[i].color)); - } - } - - // we might need to generate more colors if higher indices - // are assigned - for (i = 0; i < assignedColors.length; ++i) { - neededColors = Math.max(neededColors, assignedColors[i] + 1); - } - - // produce colors as needed - var colors = [], variation = 0; - i = 0; - while (colors.length < neededColors) { - var c; - if (options.colors.length == i) // check degenerate case - c = $.color.make(100, 100, 100); - else - c = $.color.parse(options.colors[i]); - - // vary color if needed - var sign = variation % 2 == 1 ? -1 : 1; - c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2) - - // FIXME: if we're getting to close to something else, - // we should probably skip this one - colors.push(c); - - ++i; - if (i >= options.colors.length) { - i = 0; - ++variation; - } - } - - // fill in the options - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // setup axes - s.xaxis = axisSpecToRealAxis(s, "xaxis"); - s.yaxis = axisSpecToRealAxis(s, "yaxis"); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p; - - for (axis in axes) { - axes[axis].datamin = topSentry; - axes[axis].datamax = bottomSentry; - axes[axis].used = false; - } - - function updateAxis(axis, min, max) { - if (min < axis.datamin) - axis.datamin = min; - if (max > axis.datamax) - axis.datamax = max; - } - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - var data = s.data, format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show) - format.push({ y: true, number: true, required: false, defaultValue: 0 }); - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - if (s.datapoints.pointsize == null) - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.x) - updateAxis(s.xaxis, val, val); - if (f.y) - updateAxis(s.yaxis, val, val); - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points, - ps = s.datapoints.pointsize; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2; - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - for (axis in axes) { - if (axes[axis].datamin == topSentry) - axes[axis].datamin = null; - if (axes[axis].datamax == bottomSentry) - axes[axis].datamax = null; - } - } - - function constructCanvas() { - function makeCanvas(width, height) { - var c = document.createElement('canvas'); - c.width = width; - c.height = height; - if ($.browser.msie) // excanvas hack - c = window.G_vmlCanvasManager.initElement(c); - return c; - } - - canvasWidth = placeholder.width(); - canvasHeight = placeholder.height(); - placeholder.html(""); // clear placeholder - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - if (canvasWidth <= 0 || canvasHeight <= 0) - throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; - - if ($.browser.msie) // excanvas hack - window.G_vmlCanvasManager.init_(document); // make sure everything is setup - - // the canvas - canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0); - ctx = canvas.getContext("2d"); - - // overlay canvas for interactive features - overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0); - octx = overlay.getContext("2d"); - octx.stroke(); - } - - function bindEvents() { - // we include the canvas in the event holder too, because IE 7 - // sometimes has trouble with the stacking order - eventHolder = $([overlay, canvas]); - - // bind events - if (options.grid.hoverable) - eventHolder.mousemove(onMouseMove); - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function setupGrid() { - function setTransformationHelpers(axis, o) { - function identity(x) { return x; } - - var s, m, t = o.transform || identity, - it = o.inverseTransform; - - // add transformation helpers - if (axis == axes.xaxis || axis == axes.x2axis) { - // precompute how much the axis is scaling a point - // in canvas space - s = axis.scale = plotWidth / (t(axis.max) - t(axis.min)); - m = t(axis.min); - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - else { - s = axis.scale = plotHeight / (t(axis.max) - t(axis.min)); - m = t(axis.max); - - if (t == identity) - axis.p2c = function (p) { return (m - p) * s; }; - else - axis.p2c = function (p) { return (m - t(p)) * s; }; - if (!it) - axis.c2p = function (c) { return m - c / s; }; - else - axis.c2p = function (c) { return it(m - c / s); }; - } - } - - function measureLabels(axis, axisOptions) { - var i, labels = [], l; - - axis.labelWidth = axisOptions.labelWidth; - axis.labelHeight = axisOptions.labelHeight; - - if (axis == axes.xaxis || axis == axes.x2axis) { - // to avoid measuring the widths of the labels, we - // construct fixed-size boxes and put the labels inside - // them, we don't need the exact figures and the - // fixed-size box content is easy to center - if (axis.labelWidth == null) - axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1); - - // measure x label heights - if (axis.labelHeight == null) { - labels = []; - for (i = 0; i < axis.ticks.length; ++i) { - l = axis.ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } - - if (labels.length > 0) { - var dummyDiv = $('
' - + labels.join("") + '
').appendTo(placeholder); - axis.labelHeight = dummyDiv.height(); - dummyDiv.remove(); - } - } - } - else if (axis.labelWidth == null || axis.labelHeight == null) { - // calculate y label dimensions - for (i = 0; i < axis.ticks.length; ++i) { - l = axis.ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } - - if (labels.length > 0) { - var dummyDiv = $('
' - + labels.join("") + '
').appendTo(placeholder); - if (axis.labelWidth == null) - axis.labelWidth = dummyDiv.width(); - if (axis.labelHeight == null) - axis.labelHeight = dummyDiv.find("div").height(); - dummyDiv.remove(); - } - - } - - if (axis.labelWidth == null) - axis.labelWidth = 0; - if (axis.labelHeight == null) - axis.labelHeight = 0; - } - - function setGridSpacing() { - // get the most space needed around the grid for things - // that may stick out - var maxOutset = options.grid.borderWidth; - for (i = 0; i < series.length; ++i) - maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; - - var margin = options.grid.labelMargin + options.grid.borderWidth; - - if (axes.xaxis.labelHeight > 0) - plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin); - if (axes.yaxis.labelWidth > 0) - plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin); - if (axes.x2axis.labelHeight > 0) - plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin); - if (axes.y2axis.labelWidth > 0) - plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin); - - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - } - - var axis; - for (axis in axes) - setRange(axes[axis], options[axis]); - - if (options.grid.show) { - for (axis in axes) { - prepareTickGeneration(axes[axis], options[axis]); - setTicks(axes[axis], options[axis]); - measureLabels(axes[axis], options[axis]); - } - - setGridSpacing(); - } - else { - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; - plotWidth = canvasWidth; - plotHeight = canvasHeight; - } - - for (axis in axes) - setTransformationHelpers(axes[axis], options[axis]); - - if (options.grid.show) - insertLabels(); - - insertLegend(); - } - - function setRange(axis, axisOptions) { - var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin), - max = +(axisOptions.max != null ? axisOptions.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (axisOptions.min == null) - min -= widen; - // alway widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (axisOptions.max == null || axisOptions.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = axisOptions.autoscaleMargin; - if (margin != null) { - if (axisOptions.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (axisOptions.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function prepareTickGeneration(axis, axisOptions) { - // estimate number of ticks - var noTicks; - if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0) - noTicks = axisOptions.ticks; - else if (axis == axes.xaxis || axis == axes.x2axis) - // heuristic based on the model a*sqrt(x) fitted to - // some reasonable data points - noTicks = 0.3 * Math.sqrt(canvasWidth); - else - noTicks = 0.3 * Math.sqrt(canvasHeight); - - var delta = (axis.max - axis.min) / noTicks, - size, generator, unit, formatter, i, magn, norm; - - if (axisOptions.mode == "time") { - // pretty handling of time - - // map of app. size of time units in milliseconds - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - var spec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"], [3, "month"], [6, "month"], - [1, "year"] - ]; - - var minSize = 0; - if (axisOptions.minTickSize != null) { - if (typeof axisOptions.tickSize == "number") - minSize = axisOptions.tickSize; - else - minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]]; - } - - for (i = 0; i < spec.length - 1; ++i) - if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) - break; - size = spec[i][0]; - unit = spec[i][1]; - - // special-case the possibility of several years - if (unit == "year") { - magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); - norm = (delta / timeUnitSize.year) / magn; - if (norm < 1.5) - size = 1; - else if (norm < 3) - size = 2; - else if (norm < 7.5) - size = 5; - else - size = 10; - - size *= magn; - } - - if (axisOptions.tickSize) { - size = axisOptions.tickSize[0]; - unit = axisOptions.tickSize[1]; - } - - generator = function(axis) { - var ticks = [], - tickSize = axis.tickSize[0], unit = axis.tickSize[1], - d = new Date(axis.min); - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") - d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); - if (unit == "minute") - d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); - if (unit == "hour") - d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); - if (unit == "month") - d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); - if (unit == "year") - d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); - - // reset smaller components - d.setUTCMilliseconds(0); - if (step >= timeUnitSize.minute) - d.setUTCSeconds(0); - if (step >= timeUnitSize.hour) - d.setUTCMinutes(0); - if (step >= timeUnitSize.day) - d.setUTCHours(0); - if (step >= timeUnitSize.day * 4) - d.setUTCDate(1); - if (step >= timeUnitSize.year) - d.setUTCMonth(0); - - - var carry = 0, v = Number.NaN, prev; - do { - prev = v; - v = d.getTime(); - ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); - if (unit == "month") { - if (tickSize < 1) { - // a bit complicated - we'll divide the month - // up but we need to take care of fractions - // so we don't end up in the middle of a day - d.setUTCDate(1); - var start = d.getTime(); - d.setUTCMonth(d.getUTCMonth() + 1); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getUTCHours(); - d.setUTCHours(0); - } - else - d.setUTCMonth(d.getUTCMonth() + tickSize); - } - else if (unit == "year") { - d.setUTCFullYear(d.getUTCFullYear() + tickSize); - } - else - d.setTime(v + step); - } while (v < axis.max && v != prev); - - return ticks; - }; - - formatter = function (v, axis) { - var d = new Date(v); - - // first check global format - if (axisOptions.timeformat != null) - return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (axisOptions.twelveHourClock) ? " %p" : ""; - - if (t < timeUnitSize.minute) - fmt = "%h:%M:%S" + suffix; - else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) - fmt = "%h:%M" + suffix; - else - fmt = "%b %d %h:%M" + suffix; - } - else if (t < timeUnitSize.month) - fmt = "%b %d"; - else if (t < timeUnitSize.year) { - if (span < timeUnitSize.year) - fmt = "%b"; - else - fmt = "%b %y"; - } - else - fmt = "%y"; - - return $.plot.formatDate(d, fmt, axisOptions.monthNames); - }; - } - else { - // pretty rounding of base-10 numbers - var maxDec = axisOptions.tickDecimals; - var dec = -Math.floor(Math.log(delta) / Math.LN10); - if (maxDec != null && dec > maxDec) - dec = maxDec; - - magn = Math.pow(10, -dec); - norm = delta / magn; // norm is between 1.0 and 10.0 - - if (norm < 1.5) - size = 1; - else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } - else if (norm < 7.5) - size = 5; - else - size = 10; - - size *= magn; - - if (axisOptions.minTickSize != null && size < axisOptions.minTickSize) - size = axisOptions.minTickSize; - - if (axisOptions.tickSize != null) - size = axisOptions.tickSize; - - axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec); - - generator = function (axis) { - var ticks = []; - - // spew out all possible ticks - var start = floorInBase(axis.min, axis.tickSize), - i = 0, v = Number.NaN, prev; - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - formatter = function (v, axis) { - return v.toFixed(axis.tickDecimals); - }; - } - - axis.tickSize = unit ? [size, unit] : size; - axis.tickGenerator = generator; - if ($.isFunction(axisOptions.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); }; - else - axis.tickFormatter = formatter; - } - - function setTicks(axis, axisOptions) { - axis.ticks = []; - - if (!axis.used) - return; - - if (axisOptions.ticks == null) - axis.ticks = axis.tickGenerator(axis); - else if (typeof axisOptions.ticks == "number") { - if (axisOptions.ticks > 0) - axis.ticks = axis.tickGenerator(axis); - } - else if (axisOptions.ticks) { - var ticks = axisOptions.ticks; - - if ($.isFunction(ticks)) - // generate the ticks - ticks = ticks({ min: axis.min, max: axis.max }); - - // clean up the user-supplied ticks, copy them over - var i, v; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = t; - if (label == null) - label = axis.tickFormatter(v, axis); - axis.ticks[i] = { v: v, label: label }; - } - } - - if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) { - // snap to ticks - if (axisOptions.min == null) - axis.min = Math.min(axis.min, axis.ticks[0].v); - if (axisOptions.max == null && axis.ticks.length > 1) - axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v); - } - } - - function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - - var grid = options.grid; - - if (grid.show && !grid.aboveData) - drawGrid(); - - for (var i = 0; i < series.length; ++i) - drawSeries(series[i]); - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) - drawGrid(); - } - - function extractRange(ranges, coord) { - var firstAxis = coord + "axis", - secondaryAxis = coord + "2axis", - axis, from, to, reverse; - - if (ranges[firstAxis]) { - axis = axes[firstAxis]; - from = ranges[firstAxis].from; - to = ranges[firstAxis].to; - } - else if (ranges[secondaryAxis]) { - axis = axes[secondaryAxis]; - from = ranges[secondaryAxis].from; - to = ranges[secondaryAxis].to; - } - else { - // backwards-compat stuff - to be removed in future - axis = axes[firstAxis]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) - return { from: to, to: from, axis: axis }; - - return { from: from, to: to, axis: axis }; - } - - function drawGrid() { - var i; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw background, if any - if (options.grid.backgroundColor) { - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - } - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) - // xmin etc. are backwards-compatible, to be removed in future - markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis }); - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - if (xrange.from == xrange.to && yrange.from == yrange.to) - continue; - - // then draw - xrange.from = xrange.axis.p2c(xrange.from); - xrange.to = xrange.axis.p2c(xrange.to); - yrange.from = yrange.axis.p2c(yrange.from); - yrange.to = yrange.axis.p2c(yrange.to); - - if (xrange.from == xrange.to || yrange.from == yrange.to) { - // draw line - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; - //ctx.moveTo(Math.floor(xrange.from), yrange.from); - //ctx.lineTo(Math.floor(xrange.to), yrange.to); - ctx.moveTo(xrange.from, yrange.from); - ctx.lineTo(xrange.to, yrange.to); - ctx.stroke(); - } - else { - // fill area - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the inner grid - ctx.lineWidth = 1; - ctx.strokeStyle = options.grid.tickColor; - ctx.beginPath(); - var v, axis = axes.xaxis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axes.xaxis.max) - continue; // skip those lying on the axes - - ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0); - ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight); - } - - axis = axes.yaxis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axis.max) - continue; - - ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - } - - axis = axes.x2axis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axis.max) - continue; - - ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5); - ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5); - } - - axis = axes.y2axis; - for (i = 0; i < axis.ticks.length; ++i) { - v = axis.ticks[i].v; - if (v <= axis.min || v >= axis.max) - continue; - - ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); - } - - ctx.stroke(); - - if (options.grid.borderWidth) { - // draw border - var bw = options.grid.borderWidth; - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - - ctx.restore(); - } - - function insertLabels() { - placeholder.find(".tickLabels").remove(); - - var html = ['
']; - - function addLabels(axis, labelGenerator) { - for (var i = 0; i < axis.ticks.length; ++i) { - var tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - html.push(labelGenerator(tick, axis)); - } - } - - var margin = options.grid.labelMargin + options.grid.borderWidth; - - addLabels(axes.xaxis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - - addLabels(axes.yaxis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - addLabels(axes.x2axis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - addLabels(axes.y2axis, function (tick, axis) { - return '
' + tick.label + "
"; - }); - - html.push('
'); - - placeholder.append(html.join("")); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - top, lastX = 0, areaOpen = false; - - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (areaOpen && x1 != null && x2 == null) { - // close area - ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); - ctx.fill(); - areaOpen = false; - continue; - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - lastX = x2; - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - lastX = x2; - continue; - } - - // else it's a bit more complicated, there might - // be two rectangles and two triangles we need to fill - // in; to find these keep track of the current x values - var x1old = x1, x2old = x2; - - // and clip the y values, without shortcutting - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - if (y1 <= axisy.min) - top = axisy.min; - else - top = axisy.max; - - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top)); - ctx.lineTo(axisx.p2c(x1), axisy.p2c(top)); - } - - // fill the triangles - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - if (y2 <= axisy.min) - top = axisy.min; - else - top = axisy.max; - - ctx.lineTo(axisx.p2c(x2), axisy.p2c(top)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top)); - } - - lastX = Math.max(x2, x2old); - } - - if (areaOpen) { - ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); - ctx.fill(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false); - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.lines.lineWidth, - sw = series.shadowSize, - radius = series.points.radius; - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, Math.PI, - series.xaxis, series.yaxis); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, Math.PI, - series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, 2 * Math.PI, - series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.beginPath(); - c.moveTo(left, bottom); - c.lineTo(left, top); - c.lineTo(right, top); - c.lineTo(right, bottom); - c.fillStyle = fillStyleCallback(bottom, top); - c.fill(); - } - - // draw outline - if (drawLeft || drawRight || drawTop || drawBottom) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom + offset); - if (drawLeft) - c.lineTo(left, top + offset); - else - c.moveTo(left, top + offset); - if (drawTop) - c.lineTo(right, top + offset); - else - c.moveTo(right, top + offset); - if (drawRight) - c.lineTo(right, bottom + offset); - else - c.moveTo(right, bottom + offset); - if (drawBottom) - c.lineTo(left, bottom + offset); - else - c.moveTo(left, bottom + offset); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - placeholder.find(".legend").remove(); - - if (!options.legend.show) - return; - - var fragments = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - for (i = 0; i < series.length; ++i) { - s = series[i]; - label = s.label; - if (!label) - continue; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - if (lf) - label = lf(label, s); - - fragments.push( - '
' + - '' + label + ''); - } - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j; - - for (i = 0; i < series.length; ++i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - ps = s.datapoints.pointsize, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist <= smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2, - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - pos = { pageX: event.pageX, pageY: event.pageY }, - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top; - - if (axes.xaxis.used) - pos.x = axes.xaxis.c2p(canvasX); - if (axes.yaxis.used) - pos.y = axes.yaxis.c2p(canvasY); - if (axes.x2axis.used) - pos.x2 = axes.x2axis.c2p(canvasX); - if (axes.y2axis.used) - pos.y2 = axes.y2axis.c2p(canvasY); - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && h.point == item.datapoint)) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, 30); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - octx.clearRect(0, 0, canvasWidth, canvasHeight); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") - point = s.data[point]; - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") - point = s.data[point]; - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis; - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var radius = 1.5 * pointRadius; - octx.beginPath(); - octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - c = $.color.parse(defaultColor).scale('rgb', c.brightness); - c.a *= c.opacity; - c = c.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - $.plot = function(placeholder, data, options) { - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - /*var t0 = new Date(); - var t1 = new Date(); - var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime()) - if (window.console) - console.log(tstr); - else - alert(tstr);*/ - return plot; - }; - - $.plot.plugins = []; - - // returns a string with the date d formatted according to fmt - $.plot.formatDate = function(d, fmt, monthNames) { - var leftPad = function(n) { - n = "" + n; - return n.length == 1 ? "0" + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getUTCHours(); - var isAM = hours < 12; - if (monthNames == null) - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - - if (fmt.search(/%p|%P/) != -1) { - if (hours > 12) { - hours = hours - 12; - } else if (hours == 0) { - hours = 12; - } - } - for (var i = 0; i < fmt.length; ++i) { - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'h': c = "" + hours; break; - case 'H': c = leftPad(hours); break; - case 'M': c = leftPad(d.getUTCMinutes()); break; - case 'S': c = leftPad(d.getUTCSeconds()); break; - case 'd': c = "" + d.getUTCDate(); break; - case 'm': c = "" + (d.getUTCMonth() + 1); break; - case 'y': c = "" + d.getUTCFullYear(); break; - case 'b': c = "" + monthNames[d.getUTCMonth()]; break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - } - r.push(c); - escape = false; - } - else { - if (c == "%") - escape = true; - else - r.push(c); - } - } - return r.join(""); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/deps/flot/jquery.flot.min.js b/deps/flot/jquery.flot.min.js deleted file mode 100644 index 31f465b..0000000 --- a/deps/flot/jquery.flot.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){jQuery.color={};jQuery.color.make=function(G,H,J,I){var A={};A.r=G||0;A.g=H||0;A.b=J||0;A.a=I!=null?I:1;A.add=function(C,D){for(var E=0;E=1){return"rgb("+[A.r,A.g,A.b].join(",")+")"}else{return"rgba("+[A.r,A.g,A.b,A.a].join(",")+")"}};A.normalize=function(){function C(E,D,F){return DF?F:D)}A.r=C(0,parseInt(A.r),255);A.g=C(0,parseInt(A.g),255);A.b=C(0,parseInt(A.b),255);A.a=C(0,A.a,1);return A};A.clone=function(){return jQuery.color.make(A.r,A.b,A.g,A.a)};return A.normalize()};jQuery.color.extract=function(E,F){var A;do{A=E.css(F).toLowerCase();if(A!=""&&A!="transparent"){break}E=E.parent()}while(!jQuery.nodeName(E.get(0),"body"));if(A=="rgba(0, 0, 0, 0)"){A="transparent"}return jQuery.color.parse(A)};jQuery.color.parse=function(A){var F,H=jQuery.color.make;if(F=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(A)){return H(parseInt(F[1],10),parseInt(F[2],10),parseInt(F[3],10))}if(F=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(A)){return H(parseInt(F[1],10),parseInt(F[2],10),parseInt(F[3],10),parseFloat(F[4]))}if(F=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(A)){return H(parseFloat(F[1])*2.55,parseFloat(F[2])*2.55,parseFloat(F[3])*2.55)}if(F=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(A)){return H(parseFloat(F[1])*2.55,parseFloat(F[2])*2.55,parseFloat(F[3])*2.55,parseFloat(F[4]))}if(F=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(A)){return H(parseInt(F[1],16),parseInt(F[2],16),parseInt(F[3],16))}if(F=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(A)){return H(parseInt(F[1]+F[1],16),parseInt(F[2]+F[2],16),parseInt(F[3]+F[3],16))}var G=jQuery.trim(A).toLowerCase();if(G=="transparent"){return H(255,255,255,0)}else{F=B[G];return H(F[0],F[1],F[2])}};var B={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();(function(C){function B(l,W,X,E){var O=[],g={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{mode:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02},x2axis:{autoscaleMargin:null},y2axis:{autoscaleMargin:0.02},series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,tickColor:"rgba(0,0,0,0.15)",labelMargin:5,borderWidth:2,borderColor:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},P=null,AC=null,AD=null,Y=null,AJ=null,s={xaxis:{},yaxis:{},x2axis:{},y2axis:{}},e={left:0,right:0,top:0,bottom:0},y=0,Q=0,I=0,t=0,L={processOptions:[],processRawData:[],processDatapoints:[],draw:[],bindEvents:[],drawOverlay:[]},G=this;G.setData=f;G.setupGrid=k;G.draw=AH;G.getPlaceholder=function(){return l};G.getCanvas=function(){return P};G.getPlotOffset=function(){return e};G.width=function(){return I};G.height=function(){return t};G.offset=function(){var AK=AD.offset();AK.left+=e.left;AK.top+=e.top;return AK};G.getData=function(){return O};G.getAxes=function(){return s};G.getOptions=function(){return g};G.highlight=AE;G.unhighlight=x;G.triggerRedrawOverlay=q;G.pointOffset=function(AK){return{left:parseInt(T(AK,"xaxis").p2c(+AK.x)+e.left),top:parseInt(T(AK,"yaxis").p2c(+AK.y)+e.top)}};G.hooks=L;b(G);r(X);c();f(W);k();AH();AG();function Z(AM,AK){AK=[G].concat(AK);for(var AL=0;AL=g.colors.length){AP=0;++AO}}var AQ=0,AW;for(AP=0;APAl.datamax){Al.datamax=Aj}}for(Ac=0;Ac0&&Ab[AZ-AX]!=null&&Ab[AZ-AX]!=Ab[AZ]&&Ab[AZ-AX+1]!=Ab[AZ+1]){for(AV=0;AVAU){AU=Ai}}if(Af.y){if(AiAd){Ad=Ai}}}}if(AR.bars.show){var Ag=AR.bars.align=="left"?0:-AR.bars.barWidth/2;if(AR.bars.horizontal){AY+=Ag;Ad+=Ag+AR.bars.barWidth}else{AS+=Ag;AU+=Ag+AR.bars.barWidth}}AN(AR.xaxis,AS,AU);AN(AR.yaxis,AY,Ad)}for(AK in s){if(s[AK].datamin==AW){s[AK].datamin=null}if(s[AK].datamax==AQ){s[AK].datamax=null}}}function c(){function AK(AM,AL){var AN=document.createElement("canvas");AN.width=AM;AN.height=AL;if(C.browser.msie){AN=window.G_vmlCanvasManager.initElement(AN)}return AN}y=l.width();Q=l.height();l.html("");if(l.css("position")=="static"){l.css("position","relative")}if(y<=0||Q<=0){throw"Invalid dimensions for plot, width = "+y+", height = "+Q}if(C.browser.msie){window.G_vmlCanvasManager.init_(document)}P=C(AK(y,Q)).appendTo(l).get(0);Y=P.getContext("2d");AC=C(AK(y,Q)).css({position:"absolute",left:0,top:0}).appendTo(l).get(0);AJ=AC.getContext("2d");AJ.stroke()}function AG(){AD=C([AC,P]);if(g.grid.hoverable){AD.mousemove(D)}if(g.grid.clickable){AD.click(d)}Z(L.bindEvents,[AD])}function k(){function AL(AT,AU){function AP(AV){return AV}var AS,AO,AQ=AU.transform||AP,AR=AU.inverseTransform;if(AT==s.xaxis||AT==s.x2axis){AS=AT.scale=I/(AQ(AT.max)-AQ(AT.min));AO=AQ(AT.min);if(AQ==AP){AT.p2c=function(AV){return(AV-AO)*AS}}else{AT.p2c=function(AV){return(AQ(AV)-AO)*AS}}if(!AR){AT.c2p=function(AV){return AO+AV/AS}}else{AT.c2p=function(AV){return AR(AO+AV/AS)}}}else{AS=AT.scale=t/(AQ(AT.max)-AQ(AT.min));AO=AQ(AT.max);if(AQ==AP){AT.p2c=function(AV){return(AO-AV)*AS}}else{AT.p2c=function(AV){return(AO-AQ(AV))*AS}}if(!AR){AT.c2p=function(AV){return AO-AV/AS}}else{AT.c2p=function(AV){return AR(AO-AV/AS)}}}}function AN(AR,AT){var AQ,AS=[],AP;AR.labelWidth=AT.labelWidth;AR.labelHeight=AT.labelHeight;if(AR==s.xaxis||AR==s.x2axis){if(AR.labelWidth==null){AR.labelWidth=y/(AR.ticks.length>0?AR.ticks.length:1)}if(AR.labelHeight==null){AS=[];for(AQ=0;AQ'+AP+"")}}if(AS.length>0){var AO=C('
'+AS.join("")+'
').appendTo(l);AR.labelHeight=AO.height();AO.remove()}}}else{if(AR.labelWidth==null||AR.labelHeight==null){for(AQ=0;AQ'+AP+"")}}if(AS.length>0){var AO=C('
'+AS.join("")+"
").appendTo(l);if(AR.labelWidth==null){AR.labelWidth=AO.width()}if(AR.labelHeight==null){AR.labelHeight=AO.find("div").height()}AO.remove()}}}if(AR.labelWidth==null){AR.labelWidth=0}if(AR.labelHeight==null){AR.labelHeight=0}}function AM(){var AP=g.grid.borderWidth;for(i=0;i0){e.bottom=Math.max(AP,s.xaxis.labelHeight+AO)}if(s.yaxis.labelWidth>0){e.left=Math.max(AP,s.yaxis.labelWidth+AO)}if(s.x2axis.labelHeight>0){e.top=Math.max(AP,s.x2axis.labelHeight+AO)}if(s.y2axis.labelWidth>0){e.right=Math.max(AP,s.y2axis.labelWidth+AO)}I=y-e.left-e.right;t=Q-e.bottom-e.top}var AK;for(AK in s){K(s[AK],g[AK])}if(g.grid.show){for(AK in s){F(s[AK],g[AK]);p(s[AK],g[AK]);AN(s[AK],g[AK])}AM()}else{e.left=e.right=e.top=e.bottom=0;I=y;t=Q}for(AK in s){AL(s[AK],g[AK])}if(g.grid.show){h()}AI()}function K(AN,AQ){var AM=+(AQ.min!=null?AQ.min:AN.datamin),AK=+(AQ.max!=null?AQ.max:AN.datamax),AP=AK-AM;if(AP==0){var AL=AK==0?1:0.01;if(AQ.min==null){AM-=AL}if(AQ.max==null||AQ.min!=null){AK+=AL}}else{var AO=AQ.autoscaleMargin;if(AO!=null){if(AQ.min==null){AM-=AP*AO;if(AM<0&&AN.datamin!=null&&AN.datamin>=0){AM=0}}if(AQ.max==null){AK+=AP*AO;if(AK>0&&AN.datamax!=null&&AN.datamax<=0){AK=0}}}}AN.min=AM;AN.max=AK}function F(AP,AS){var AO;if(typeof AS.ticks=="number"&&AS.ticks>0){AO=AS.ticks}else{if(AP==s.xaxis||AP==s.x2axis){AO=0.3*Math.sqrt(y)}else{AO=0.3*Math.sqrt(Q)}}var AX=(AP.max-AP.min)/AO,AZ,AT,AV,AW,AR,AM,AL;if(AS.mode=="time"){var AU={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var AY=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var AN=0;if(AS.minTickSize!=null){if(typeof AS.tickSize=="number"){AN=AS.tickSize}else{AN=AS.minTickSize[0]*AU[AS.minTickSize[1]]}}for(AR=0;AR=AN){break}}AZ=AY[AR][0];AV=AY[AR][1];if(AV=="year"){AM=Math.pow(10,Math.floor(Math.log(AX/AU.year)/Math.LN10));AL=(AX/AU.year)/AM;if(AL<1.5){AZ=1}else{if(AL<3){AZ=2}else{if(AL<7.5){AZ=5}else{AZ=10}}}AZ*=AM}if(AS.tickSize){AZ=AS.tickSize[0];AV=AS.tickSize[1]}AT=function(Ac){var Ah=[],Af=Ac.tickSize[0],Ai=Ac.tickSize[1],Ag=new Date(Ac.min);var Ab=Af*AU[Ai];if(Ai=="second"){Ag.setUTCSeconds(A(Ag.getUTCSeconds(),Af))}if(Ai=="minute"){Ag.setUTCMinutes(A(Ag.getUTCMinutes(),Af))}if(Ai=="hour"){Ag.setUTCHours(A(Ag.getUTCHours(),Af))}if(Ai=="month"){Ag.setUTCMonth(A(Ag.getUTCMonth(),Af))}if(Ai=="year"){Ag.setUTCFullYear(A(Ag.getUTCFullYear(),Af))}Ag.setUTCMilliseconds(0);if(Ab>=AU.minute){Ag.setUTCSeconds(0)}if(Ab>=AU.hour){Ag.setUTCMinutes(0)}if(Ab>=AU.day){Ag.setUTCHours(0)}if(Ab>=AU.day*4){Ag.setUTCDate(1)}if(Ab>=AU.year){Ag.setUTCMonth(0)}var Ak=0,Aj=Number.NaN,Ad;do{Ad=Aj;Aj=Ag.getTime();Ah.push({v:Aj,label:Ac.tickFormatter(Aj,Ac)});if(Ai=="month"){if(Af<1){Ag.setUTCDate(1);var Aa=Ag.getTime();Ag.setUTCMonth(Ag.getUTCMonth()+1);var Ae=Ag.getTime();Ag.setTime(Aj+Ak*AU.hour+(Ae-Aa)*Af);Ak=Ag.getUTCHours();Ag.setUTCHours(0)}else{Ag.setUTCMonth(Ag.getUTCMonth()+Af)}}else{if(Ai=="year"){Ag.setUTCFullYear(Ag.getUTCFullYear()+Af)}else{Ag.setTime(Aj+Ab)}}}while(AjAK){AQ=AK}AM=Math.pow(10,-AQ);AL=AX/AM;if(AL<1.5){AZ=1}else{if(AL<3){AZ=2;if(AL>2.25&&(AK==null||AQ+1<=AK)){AZ=2.5;++AQ}}else{if(AL<7.5){AZ=5}else{AZ=10}}}AZ*=AM;if(AS.minTickSize!=null&&AZ0){AO.ticks=AO.tickGenerator(AO)}}else{if(AQ.ticks){var AP=AQ.ticks;if(C.isFunction(AP)){AP=AP({min:AO.min,max:AO.max})}var AN,AK;for(AN=0;AN1){AL=AM[1]}}else{AK=AM}if(AL==null){AL=AO.tickFormatter(AK,AO)}AO.ticks[AN]={v:AK,label:AL}}}}}if(AQ.autoscaleMargin!=null&&AO.ticks.length>0){if(AQ.min==null){AO.min=Math.min(AO.min,AO.ticks[0].v)}if(AQ.max==null&&AO.ticks.length>1){AO.max=Math.max(AO.max,AO.ticks[AO.ticks.length-1].v)}}}function AH(){Y.clearRect(0,0,y,Q);var AL=g.grid;if(AL.show&&!AL.aboveData){S()}for(var AK=0;AKAP){return{from:AP,to:AQ,axis:AN}}return{from:AQ,to:AP,axis:AN}}function S(){var AO;Y.save();Y.translate(e.left,e.top);if(g.grid.backgroundColor){Y.fillStyle=R(g.grid.backgroundColor,t,0,"rgba(255, 255, 255, 0)");Y.fillRect(0,0,I,t)}var AL=g.grid.markings;if(AL){if(C.isFunction(AL)){AL=AL({xmin:s.xaxis.min,xmax:s.xaxis.max,ymin:s.yaxis.min,ymax:s.yaxis.max,xaxis:s.xaxis,yaxis:s.yaxis,x2axis:s.x2axis,y2axis:s.y2axis})}for(AO=0;AOAQ.axis.max||AN.toAN.axis.max){continue}AQ.from=Math.max(AQ.from,AQ.axis.min);AQ.to=Math.min(AQ.to,AQ.axis.max);AN.from=Math.max(AN.from,AN.axis.min);AN.to=Math.min(AN.to,AN.axis.max);if(AQ.from==AQ.to&&AN.from==AN.to){continue}AQ.from=AQ.axis.p2c(AQ.from);AQ.to=AQ.axis.p2c(AQ.to);AN.from=AN.axis.p2c(AN.from);AN.to=AN.axis.p2c(AN.to);if(AQ.from==AQ.to||AN.from==AN.to){Y.beginPath();Y.strokeStyle=AK.color||g.grid.markingsColor;Y.lineWidth=AK.lineWidth||g.grid.markingsLineWidth;Y.moveTo(AQ.from,AN.from);Y.lineTo(AQ.to,AN.to);Y.stroke()}else{Y.fillStyle=AK.color||g.grid.markingsColor;Y.fillRect(AQ.from,AN.to,AQ.to-AQ.from,AN.from-AN.to)}}}Y.lineWidth=1;Y.strokeStyle=g.grid.tickColor;Y.beginPath();var AM,AP=s.xaxis;for(AO=0;AO=s.xaxis.max){continue}Y.moveTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,0);Y.lineTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,t)}AP=s.yaxis;for(AO=0;AO=AP.max){continue}Y.moveTo(0,Math.floor(AP.p2c(AM))+Y.lineWidth/2);Y.lineTo(I,Math.floor(AP.p2c(AM))+Y.lineWidth/2)}AP=s.x2axis;for(AO=0;AO=AP.max){continue}Y.moveTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,-5);Y.lineTo(Math.floor(AP.p2c(AM))+Y.lineWidth/2,5)}AP=s.y2axis;for(AO=0;AO=AP.max){continue}Y.moveTo(I-5,Math.floor(AP.p2c(AM))+Y.lineWidth/2);Y.lineTo(I+5,Math.floor(AP.p2c(AM))+Y.lineWidth/2)}Y.stroke();if(g.grid.borderWidth){var AR=g.grid.borderWidth;Y.lineWidth=AR;Y.strokeStyle=g.grid.borderColor;Y.strokeRect(-AR/2,-AR/2,I+AR,t+AR)}Y.restore()}function h(){l.find(".tickLabels").remove();var AK=['
'];function AM(AP,AQ){for(var AO=0;AOAP.max){continue}AK.push(AQ(AN,AP))}}var AL=g.grid.labelMargin+g.grid.borderWidth;AM(s.xaxis,function(AN,AO){return'
'+AN.label+"
"});AM(s.yaxis,function(AN,AO){return'
'+AN.label+"
"});AM(s.x2axis,function(AN,AO){return'
'+AN.label+"
"});AM(s.y2axis,function(AN,AO){return'
'+AN.label+"
"});AK.push("
");l.append(AK.join(""))}function AA(AK){if(AK.lines.show){a(AK)}if(AK.bars.show){n(AK)}if(AK.points.show){o(AK)}}function a(AN){function AM(AY,AZ,AR,Ad,Ac){var Ae=AY.points,AS=AY.pointsize,AW=null,AV=null;Y.beginPath();for(var AX=AS;AX=Aa&&Ab>Ac.max){if(Aa>Ac.max){continue}AU=(Ac.max-Ab)/(Aa-Ab)*(AT-AU)+AU;Ab=Ac.max}else{if(Aa>=Ab&&Aa>Ac.max){if(Ab>Ac.max){continue}AT=(Ac.max-Ab)/(Aa-Ab)*(AT-AU)+AU;Aa=Ac.max}}if(AU<=AT&&AU=AT&&AU>Ad.max){if(AT>Ad.max){continue}Ab=(Ad.max-AU)/(AT-AU)*(Aa-Ab)+Ab;AU=Ad.max}else{if(AT>=AU&&AT>Ad.max){if(AU>Ad.max){continue}Aa=(Ad.max-AU)/(AT-AU)*(Aa-Ab)+Ab;AT=Ad.max}}if(AU!=AW||Ab!=AV){Y.moveTo(Ad.p2c(AU)+AZ,Ac.p2c(Ab)+AR)}AW=AT;AV=Aa;Y.lineTo(Ad.p2c(AT)+AZ,Ac.p2c(Aa)+AR)}Y.stroke()}function AO(AX,Ae,Ac){var Af=AX.points,AR=AX.pointsize,AS=Math.min(Math.max(0,Ac.min),Ac.max),Aa,AV=0,Ad=false;for(var AW=AR;AW=AT&&AU>Ae.max){if(AT>Ae.max){continue}Ab=(Ae.max-AU)/(AT-AU)*(AZ-Ab)+Ab;AU=Ae.max}else{if(AT>=AU&&AT>Ae.max){if(AU>Ae.max){continue}AZ=(Ae.max-AU)/(AT-AU)*(AZ-Ab)+Ab;AT=Ae.max}}if(!Ad){Y.beginPath();Y.moveTo(Ae.p2c(AU),Ac.p2c(AS));Ad=true}if(Ab>=Ac.max&&AZ>=Ac.max){Y.lineTo(Ae.p2c(AU),Ac.p2c(Ac.max));Y.lineTo(Ae.p2c(AT),Ac.p2c(Ac.max));AV=AT;continue}else{if(Ab<=Ac.min&&AZ<=Ac.min){Y.lineTo(Ae.p2c(AU),Ac.p2c(Ac.min));Y.lineTo(Ae.p2c(AT),Ac.p2c(Ac.min));AV=AT;continue}}var Ag=AU,AY=AT;if(Ab<=AZ&&Ab=Ac.min){AU=(Ac.min-Ab)/(AZ-Ab)*(AT-AU)+AU;Ab=Ac.min}else{if(AZ<=Ab&&AZ=Ac.min){AT=(Ac.min-Ab)/(AZ-Ab)*(AT-AU)+AU;AZ=Ac.min}}if(Ab>=AZ&&Ab>Ac.max&&AZ<=Ac.max){AU=(Ac.max-Ab)/(AZ-Ab)*(AT-AU)+AU;Ab=Ac.max}else{if(AZ>=Ab&&AZ>Ac.max&&Ab<=Ac.max){AT=(Ac.max-Ab)/(AZ-Ab)*(AT-AU)+AU;AZ=Ac.max}}if(AU!=Ag){if(Ab<=Ac.min){Aa=Ac.min}else{Aa=Ac.max}Y.lineTo(Ae.p2c(Ag),Ac.p2c(Aa));Y.lineTo(Ae.p2c(AU),Ac.p2c(Aa))}Y.lineTo(Ae.p2c(AU),Ac.p2c(Ab));Y.lineTo(Ae.p2c(AT),Ac.p2c(AZ));if(AT!=AY){if(AZ<=Ac.min){Aa=Ac.min}else{Aa=Ac.max}Y.lineTo(Ae.p2c(AT),Ac.p2c(Aa));Y.lineTo(Ae.p2c(AY),Ac.p2c(Aa))}AV=Math.max(AT,AY)}if(Ad){Y.lineTo(Ae.p2c(AV),Ac.p2c(AS));Y.fill()}}Y.save();Y.translate(e.left,e.top);Y.lineJoin="round";var AP=AN.lines.lineWidth,AK=AN.shadowSize;if(AP>0&&AK>0){Y.lineWidth=AK;Y.strokeStyle="rgba(0,0,0,0.1)";var AQ=Math.PI/18;AM(AN.datapoints,Math.sin(AQ)*(AP/2+AK/2),Math.cos(AQ)*(AP/2+AK/2),AN.xaxis,AN.yaxis);Y.lineWidth=AK/2;AM(AN.datapoints,Math.sin(AQ)*(AP/2+AK/4),Math.cos(AQ)*(AP/2+AK/4),AN.xaxis,AN.yaxis)}Y.lineWidth=AP;Y.strokeStyle=AN.color;var AL=V(AN.lines,AN.color,0,t);if(AL){Y.fillStyle=AL;AO(AN.datapoints,AN.xaxis,AN.yaxis)}if(AP>0){AM(AN.datapoints,0,0,AN.xaxis,AN.yaxis)}Y.restore()}function o(AN){function AP(AU,AT,Ab,AR,AV,AZ,AY){var Aa=AU.points,AQ=AU.pointsize;for(var AS=0;ASAZ.max||AWAY.max){continue}Y.beginPath();Y.arc(AZ.p2c(AX),AY.p2c(AW)+AR,AT,0,AV,false);if(Ab){Y.fillStyle=Ab;Y.fill()}Y.stroke()}}Y.save();Y.translate(e.left,e.top);var AO=AN.lines.lineWidth,AL=AN.shadowSize,AK=AN.points.radius;if(AO>0&&AL>0){var AM=AL/2;Y.lineWidth=AM;Y.strokeStyle="rgba(0,0,0,0.1)";AP(AN.datapoints,AK,null,AM+AM/2,Math.PI,AN.xaxis,AN.yaxis);Y.strokeStyle="rgba(0,0,0,0.2)";AP(AN.datapoints,AK,null,AM/2,Math.PI,AN.xaxis,AN.yaxis)}Y.lineWidth=AO;Y.strokeStyle=AN.color;AP(AN.datapoints,AK,V(AN.points,AN.color),0,2*Math.PI,AN.xaxis,AN.yaxis);Y.restore()}function AB(AV,AU,Ad,AQ,AY,AN,AL,AT,AS,Ac,AZ){var AM,Ab,AR,AX,AO,AK,AW,AP,Aa;if(AZ){AP=AK=AW=true;AO=false;AM=Ad;Ab=AV;AX=AU+AQ;AR=AU+AY;if(AbAT.max||AXAS.max){return }if(AMAT.max){Ab=AT.max;AK=false}if(ARAS.max){AX=AS.max;AW=false}AM=AT.p2c(AM);AR=AS.p2c(AR);Ab=AT.p2c(Ab);AX=AS.p2c(AX);if(AL){Ac.beginPath();Ac.moveTo(AM,AR);Ac.lineTo(AM,AX);Ac.lineTo(Ab,AX);Ac.lineTo(Ab,AR);Ac.fillStyle=AL(AR,AX);Ac.fill()}if(AO||AK||AW||AP){Ac.beginPath();Ac.moveTo(AM,AR+AN);if(AO){Ac.lineTo(AM,AX+AN)}else{Ac.moveTo(AM,AX+AN)}if(AW){Ac.lineTo(Ab,AX+AN)}else{Ac.moveTo(Ab,AX+AN)}if(AK){Ac.lineTo(Ab,AR+AN)}else{Ac.moveTo(Ab,AR+AN)}if(AP){Ac.lineTo(AM,AR+AN)}else{Ac.moveTo(AM,AR+AN)}Ac.stroke()}}function n(AM){function AL(AS,AR,AU,AP,AT,AW,AV){var AX=AS.points,AO=AS.pointsize;for(var AQ=0;AQ")}AP.push("");AN=true}if(AV){AR=AV(AR,AU)}AP.push('
'+AR+"")}if(AN){AP.push("")}if(AP.length==0){return }var AT=''+AP.join("")+"
";if(g.legend.container!=null){C(g.legend.container).html(AT)}else{var AQ="",AL=g.legend.position,AM=g.legend.margin;if(AM[0]==null){AM=[AM,AM]}if(AL.charAt(0)=="n"){AQ+="top:"+(AM[1]+e.top)+"px;"}else{if(AL.charAt(0)=="s"){AQ+="bottom:"+(AM[1]+e.bottom)+"px;"}}if(AL.charAt(1)=="e"){AQ+="right:"+(AM[0]+e.right)+"px;"}else{if(AL.charAt(1)=="w"){AQ+="left:"+(AM[0]+e.left)+"px;"}}var AS=C('
'+AT.replace('style="','style="position:absolute;'+AQ+";")+"
").appendTo(l);if(g.legend.backgroundOpacity!=0){var AO=g.legend.backgroundColor;if(AO==null){AO=g.grid.backgroundColor;if(AO&&typeof AO=="string"){AO=C.color.parse(AO)}else{AO=C.color.extract(AS,"background-color")}AO.a=1;AO=AO.toString()}var AK=AS.children();C('
').prependTo(AS).css("opacity",g.legend.backgroundOpacity)}}}var w=[],J=null;function AF(AR,AP,AM){var AX=g.grid.mouseActiveRadius,Aj=AX*AX+1,Ah=null,Aa=false,Af,Ad;for(Af=0;AfAL||AT-AZ<-AL||AS-AW>AK||AS-AW<-AK){continue}var AV=Math.abs(AQ.p2c(AT)-AR),AU=Math.abs(AO.p2c(AS)-AP),Ab=AV*AV+AU*AU;if(Ab<=Aj){Aj=Ab;Ah=[Af,Ad/Ac]}}}if(AY.bars.show&&!Ah){var AN=AY.bars.align=="left"?0:-AY.bars.barWidth/2,Ag=AN+AY.bars.barWidth;for(Ad=0;Ad=Math.min(Ai,AT)&&AW>=AS+AN&&AW<=AS+Ag):(AZ>=AT+AN&&AZ<=AT+Ag&&AW>=Math.min(Ai,AS)&&AW<=Math.max(Ai,AS))){Ah=[Af,Ad/Ac]}}}}if(Ah){Af=Ah[0];Ad=Ah[1];Ac=O[Af].datapoints.pointsize;return{datapoint:O[Af].datapoints.points.slice(Ad*Ac,(Ad+1)*Ac),dataIndex:Ad,series:O[Af],seriesIndex:Af}}return null}function D(AK){if(g.grid.hoverable){H("plothover",AK,function(AL){return AL.hoverable!=false})}}function d(AK){H("plotclick",AK,function(AL){return AL.clickable!=false})}function H(AL,AK,AM){var AN=AD.offset(),AS={pageX:AK.pageX,pageY:AK.pageY},AQ=AK.pageX-AN.left-e.left,AO=AK.pageY-AN.top-e.top;if(s.xaxis.used){AS.x=s.xaxis.c2p(AQ)}if(s.yaxis.used){AS.y=s.yaxis.c2p(AO)}if(s.x2axis.used){AS.x2=s.x2axis.c2p(AQ)}if(s.y2axis.used){AS.y2=s.y2axis.c2p(AO)}var AT=AF(AQ,AO,AM);if(AT){AT.pageX=parseInt(AT.series.xaxis.p2c(AT.datapoint[0])+AN.left+e.left);AT.pageY=parseInt(AT.series.yaxis.p2c(AT.datapoint[1])+AN.top+e.top)}if(g.grid.autoHighlight){for(var AP=0;APAQ.max||ARAP.max){return }var AO=AN.points.radius+AN.points.lineWidth/2;AJ.lineWidth=AO;AJ.strokeStyle=C.color.parse(AN.color).scale("a",0.5).toString();var AK=1.5*AO;AJ.beginPath();AJ.arc(AQ.p2c(AL),AP.p2c(AR),AK,0,2*Math.PI,false);AJ.stroke()}function z(AN,AK){AJ.lineWidth=AN.bars.lineWidth;AJ.strokeStyle=C.color.parse(AN.color).scale("a",0.5).toString();var AM=C.color.parse(AN.color).scale("a",0.5).toString();var AL=AN.bars.align=="left"?0:-AN.bars.barWidth/2;AB(AK[0],AK[1],AK[2]||0,AL,AL+AN.bars.barWidth,0,function(){return AM},AN.xaxis,AN.yaxis,AJ,AN.bars.horizontal)}function R(AM,AL,AQ,AO){if(typeof AM=="string"){return AM}else{var AP=Y.createLinearGradient(0,AQ,0,AL);for(var AN=0,AK=AM.colors.length;AN12){K=K-12}else{if(K==0){K=12}}}for(var F=0;F0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY) zr[1]))) - return; - - axisOptions.min = min; - axisOptions.max = max; - } - - scaleAxis(x1, x2, 'xaxis'); - scaleAxis(x1, x2, 'x2axis'); - scaleAxis(y1, y2, 'yaxis'); - scaleAxis(y1, y2, 'y2axis'); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotzoom", [ plot ]); - } - - plot.pan = function (args) { - var l = +args.left, t = +args.top, - axes = plot.getAxes(), options = plot.getOptions(); - - if (isNaN(l)) - l = 0; - if (isNaN(t)) - t = 0; - - function panAxis(delta, name) { - var axis = axes[name], - axisOptions = options[name], - min, max; - - if (!axis.used) - return; - - min = axis.c2p(axis.p2c(axis.min) + delta), - max = axis.c2p(axis.p2c(axis.max) + delta); - - var pr = axisOptions.panRange; - if (pr) { - // check whether we hit the wall - if (pr[0] != null && pr[0] > min) { - delta = pr[0] - min; - min += delta; - max += delta; - } - - if (pr[1] != null && pr[1] < max) { - delta = pr[1] - max; - min += delta; - max += delta; - } - } - - axisOptions.min = min; - axisOptions.max = max; - } - - panAxis(l, 'xaxis'); - panAxis(l, 'x2axis'); - panAxis(t, 'yaxis'); - panAxis(t, 'y2axis'); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotpan", [ plot ]); - } - - plot.hooks.bindEvents.push(bindEvents); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'navigate', - version: '1.1' - }); -})(jQuery); diff --git a/deps/flot/jquery.flot.navigate.min.js b/deps/flot/jquery.flot.navigate.min.js deleted file mode 100644 index fb7814e..0000000 --- a/deps/flot/jquery.flot.navigate.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(R){R.fn.drag=function(A,B,C){if(B){this.bind("dragstart",A)}if(C){this.bind("dragend",C)}return !A?this.trigger("drag"):this.bind("drag",B?B:A)};var M=R.event,L=M.special,Q=L.drag={not:":input",distance:0,which:1,dragging:false,setup:function(A){A=R.extend({distance:Q.distance,which:Q.which,not:Q.not},A||{});A.distance=N(A.distance);M.add(this,"mousedown",O,A);if(this.attachEvent){this.attachEvent("ondragstart",J)}},teardown:function(){M.remove(this,"mousedown",O);if(this===Q.dragging){Q.dragging=Q.proxy=false}P(this,true);if(this.detachEvent){this.detachEvent("ondragstart",J)}}};L.dragstart=L.dragend={setup:function(){},teardown:function(){}};function O(A){var B=this,C,D=A.data||{};if(D.elem){B=A.dragTarget=D.elem;A.dragProxy=Q.proxy||B;A.cursorOffsetX=D.pageX-D.left;A.cursorOffsetY=D.pageY-D.top;A.offsetX=A.pageX-A.cursorOffsetX;A.offsetY=A.pageY-A.cursorOffsetY}else{if(Q.dragging||(D.which>0&&A.which!=D.which)||R(A.target).is(D.not)){return }}switch(A.type){case"mousedown":R.extend(D,R(B).offset(),{elem:B,target:A.target,pageX:A.pageX,pageY:A.pageY});M.add(document,"mousemove mouseup",O,D);P(B,false);Q.dragging=null;return false;case !Q.dragging&&"mousemove":if(N(A.pageX-D.pageX)+N(A.pageY-D.pageY)Z[1]))){return }a.min=X;a.max=T}K(G,F,"xaxis");K(G,F,"x2axis");K(P,O,"yaxis");K(P,O,"y2axis");D.setupGrid();D.draw();if(!M.preventEvent){D.getPlaceholder().trigger("plotzoom",[D])}};D.pan=function(I){var F=+I.left,J=+I.top,K=D.getAxes(),H=D.getOptions();if(isNaN(F)){F=0}if(isNaN(J)){J=0}function G(R,M){var O=K[M],Q=H[M],N,L;if(!O.used){return }N=O.c2p(O.p2c(O.min)+R),L=O.c2p(O.p2c(O.max)+R);var P=Q.panRange;if(P){if(P[0]!=null&&P[0]>N){R=P[0]-N;N+=R;L+=R}if(P[1]!=null&&P[1] max? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first? 0: plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first? 0: plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - function setSelection(ranges, preventEvent) { - var axis, range, axes = plot.getAxes(); - var o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - axis = ranges["xaxis"]? axes["xaxis"]: (ranges["x2axis"]? axes["x2axis"]: axes["xaxis"]); - range = ranges["xaxis"] || ranges["x2axis"] || { from:ranges["x1"], to:ranges["x2"] } - selection.first.x = axis.p2c(Math.min(range.from, range.to)); - selection.second.x = axis.p2c(Math.max(range.from, range.to)); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - axis = ranges["yaxis"]? axes["yaxis"]: (ranges["y2axis"]? axes["y2axis"]: axes["yaxis"]); - range = ranges["yaxis"] || ranges["y2axis"] || { from:ranges["y1"], to:ranges["y2"] } - selection.first.y = axis.p2c(Math.min(range.from, range.to)); - selection.second.y = axis.p2c(Math.max(range.from, range.to)); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = 5; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) - eventHolder.mousemove(onMouseMove); - - if (o.selection.mode != null) - eventHolder.mousedown(onMouseDown); - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = "round"; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x), - y = Math.min(selection.first.y, selection.second.y), - w = Math.abs(selection.second.x - selection.first.x), - h = Math.abs(selection.second.y - selection.first.y); - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac" - } - }, - name: 'selection', - version: '1.0' - }); -})(jQuery); diff --git a/deps/flot/jquery.flot.selection.min.js b/deps/flot/jquery.flot.selection.min.js deleted file mode 100644 index 2260e8c..0000000 --- a/deps/flot/jquery.flot.selection.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(A){function B(J){var O={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var L={};function D(Q){if(O.active){J.getPlaceholder().trigger("plotselecting",[F()]);K(Q)}}function M(Q){if(Q.which!=1){return }document.body.focus();if(document.onselectstart!==undefined&&L.onselectstart==null){L.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&L.ondrag==null){L.ondrag=document.ondrag;document.ondrag=function(){return false}}C(O.first,Q);O.active=true;A(document).one("mouseup",I)}function I(Q){if(document.onselectstart!==undefined){document.onselectstart=L.onselectstart}if(document.ondrag!==undefined){document.ondrag=L.ondrag}O.active=false;K(Q);if(E()){H()}else{J.getPlaceholder().trigger("plotunselected",[]);J.getPlaceholder().trigger("plotselecting",[null])}return false}function F(){if(!E()){return null}var R=Math.min(O.first.x,O.second.x),Q=Math.max(O.first.x,O.second.x),T=Math.max(O.first.y,O.second.y),S=Math.min(O.first.y,O.second.y);var U={};var V=J.getAxes();if(V.xaxis.used){U.xaxis={from:V.xaxis.c2p(R),to:V.xaxis.c2p(Q)}}if(V.x2axis.used){U.x2axis={from:V.x2axis.c2p(R),to:V.x2axis.c2p(Q)}}if(V.yaxis.used){U.yaxis={from:V.yaxis.c2p(T),to:V.yaxis.c2p(S)}}if(V.y2axis.used){U.y2axis={from:V.y2axis.c2p(T),to:V.y2axis.c2p(S)}}return U}function H(){var Q=F();J.getPlaceholder().trigger("plotselected",[Q]);var R=J.getAxes();if(R.xaxis.used&&R.yaxis.used){J.getPlaceholder().trigger("selected",[{x1:Q.xaxis.from,y1:Q.yaxis.from,x2:Q.xaxis.to,y2:Q.yaxis.to}])}}function G(R,S,Q){return SQ?Q:S)}function C(U,R){var T=J.getOptions();var S=J.getPlaceholder().offset();var Q=J.getPlotOffset();U.x=G(0,R.pageX-S.left-Q.left,J.width());U.y=G(0,R.pageY-S.top-Q.top,J.height());if(T.selection.mode=="y"){U.x=U==O.first?0:J.width()}if(T.selection.mode=="x"){U.y=U==O.first?0:J.height()}}function K(Q){if(Q.pageX==null){return }C(O.second,Q);if(E()){O.show=true;J.triggerRedrawOverlay()}else{P(true)}}function P(Q){if(O.show){O.show=false;J.triggerRedrawOverlay();if(!Q){J.getPlaceholder().trigger("plotunselected",[])}}}function N(R,Q){var T,S,U=J.getAxes();var V=J.getOptions();if(V.selection.mode=="y"){O.first.x=0;O.second.x=J.width()}else{T=R.xaxis?U.xaxis:(R.x2axis?U.x2axis:U.xaxis);S=R.xaxis||R.x2axis||{from:R.x1,to:R.x2};O.first.x=T.p2c(Math.min(S.from,S.to));O.second.x=T.p2c(Math.max(S.from,S.to))}if(V.selection.mode=="x"){O.first.y=0;O.second.y=J.height()}else{T=R.yaxis?U.yaxis:(R.y2axis?U.y2axis:U.yaxis);S=R.yaxis||R.y2axis||{from:R.y1,to:R.y2};O.first.y=T.p2c(Math.min(S.from,S.to));O.second.y=T.p2c(Math.max(S.from,S.to))}O.show=true;J.triggerRedrawOverlay();if(!Q){H()}}function E(){var Q=5;return Math.abs(O.second.x-O.first.x)>=Q&&Math.abs(O.second.y-O.first.y)>=Q}J.clearSelection=P;J.setSelection=N;J.getSelection=F;J.hooks.bindEvents.push(function(R,Q){var S=R.getOptions();if(S.selection.mode!=null){Q.mousemove(D)}if(S.selection.mode!=null){Q.mousedown(M)}});J.hooks.drawOverlay.push(function(T,Y){if(O.show&&E()){var R=T.getPlotOffset();var Q=T.getOptions();Y.save();Y.translate(R.left,R.top);var U=A.color.parse(Q.selection.color);Y.strokeStyle=U.scale("a",0.8).toString();Y.lineWidth=1;Y.lineJoin="round";Y.fillStyle=U.scale("a",0.4).toString();var W=Math.min(O.first.x,O.second.x),V=Math.min(O.first.y,O.second.y),X=Math.abs(O.second.x-O.first.x),S=Math.abs(O.second.y-O.first.y);Y.fillRect(W,V,X,S);Y.strokeRect(W,V,X,S);Y.restore()}})}A.plot.plugins.push({init:B,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/deps/flot/jquery.flot.stack.js b/deps/flot/jquery.flot.stack.js deleted file mode 100644 index 4dbd29f..0000000 --- a/deps/flot/jquery.flot.stack.js +++ /dev/null @@ -1,152 +0,0 @@ -/* -Flot plugin for stacking data sets, i.e. putting them on top of each -other, for accumulative graphs. Note that the plugin assumes the data -is sorted on x. Also note that stacking a mix of positive and negative -values in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to -the same key (which can be any number or string or just "true"). To -specify the default stack, you can set - - series: { - stack: null or true or key (number/string) - } - -or specify it for a specific series - - $.plot($("#placeholder"), [{ data: [ ... ], stack: true ]) - -The stacking order is determined by the order of the data series in -the array (later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding -an offset to the y value. For line series, extra data points are -inserted through interpolation. For bar charts, the second y value is -also adjusted. -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, withbars = s.bars.show, - withsteps = withlines && s.lines.steps, - i = 0, j = 0, l; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (j >= otherpoints.length - || otherpoints[j] == null - || points[i] == null) { - // degenerate cases - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else { - // cases where we actually got two points - px = points[i]; - py = points[i + 1]; - qx = otherpoints[j]; - qy = otherpoints[j + 1]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + 1] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px); - newpoints.push(qx); - newpoints.push(intery + qy) - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - ps] != null) - bottom = qy + (otherpoints[j - ps + 1] - qy) * (px - qx) / (otherpoints[j - ps] - qx); - - newpoints[l + 1] += bottom; - - i += ps; - } - - if (l != newpoints.length && withbars) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.0' - }); -})(jQuery); diff --git a/deps/flot/jquery.flot.stack.min.js b/deps/flot/jquery.flot.stack.min.js deleted file mode 100644 index b5b8943..0000000 --- a/deps/flot/jquery.flot.stack.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(B){var A={series:{stack:null}};function C(F){function D(J,I){var H=null;for(var G=0;G=Y.length){break}U=N.length;if(V>=S.length||S[V]==null||Y[X]==null){for(m=0;ma){if(O&&X>0&&Y[X-T]!=null){I=Q+(Y[X-T+1]-Q)*(a-R)/(Y[X-T]-R);N.push(a);N.push(I+Z);for(m=2;m0&&S[V-T]!=null){M=Z+(S[V-T+1]-Z)*(R-a)/(S[V-T]-a)}N[U+1]+=M;X+=T}}if(U!=N.length&&K){N[U+2]+=M}}if(J&&U!=N.length&&U>0&&N[U]!=null&&N[U]!=N[U-T]&&N[U+1]!=N[U-T+1]){for(m=0;m 0 && origpoints[i - ps] != null) { - var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x; - prevp.push(interx); - prevp.push(below); - for (m = 2; m < ps; ++m) - prevp.push(origpoints[i + m]); - - p.push(null); // start new segment - p.push(null); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - p.push(interx); - p.push(below); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - p.push(x); - p.push(y); - } - - datapoints.points = newpoints; - thresholded.datapoints.points = threspoints; - - if (thresholded.datapoints.points.length > 0) - plot.getData().push(thresholded); - - // FIXME: there are probably some edge cases left in bars - } - - plot.hooks.processDatapoints.push(thresholdData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'threshold', - version: '1.0' - }); -})(jQuery); diff --git a/deps/flot/jquery.flot.threshold.min.js b/deps/flot/jquery.flot.threshold.min.js deleted file mode 100644 index d8b79df..0000000 --- a/deps/flot/jquery.flot.threshold.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/deps/flot/jquery.js b/deps/flot/jquery.js deleted file mode 100644 index 9263574..0000000 --- a/deps/flot/jquery.js +++ /dev/null @@ -1,4376 +0,0 @@ -/*! - * jQuery JavaScript Library v1.3.2 - * http://jquery.com/ - * - * Copyright (c) 2009 John Resig - * Dual licensed under the MIT and GPL licenses. - * http://docs.jquery.com/License - * - * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) - * Revision: 6246 - */ -(function(){ - -var - // Will speed up references to window, and allows munging its name. - window = this, - // Will speed up references to undefined, and allows munging its name. - undefined, - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - // Map over the $ in case of overwrite - _$ = window.$, - - jQuery = window.jQuery = window.$ = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context ); - }, - - // A simple way to check for HTML strings or ID strings - // (both of which we optimize for) - quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, - // Is it a simple selector - isSimple = /^.[^:#\[\.,]*$/; - -jQuery.fn = jQuery.prototype = { - init: function( selector, context ) { - // Make sure that a selection was provided - selector = selector || document; - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this[0] = selector; - this.length = 1; - this.context = selector; - return this; - } - // Handle HTML strings - if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - var match = quickExpr.exec( selector ); - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) - selector = jQuery.clean( [ match[1] ], context ); - - // HANDLE: $("#id") - else { - var elem = document.getElementById( match[3] ); - - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem && elem.id != match[3] ) - return jQuery().find( selector ); - - // Otherwise, we inject the element directly into the jQuery object - var ret = jQuery( elem || [] ); - ret.context = document; - ret.selector = selector; - return ret; - } - - // HANDLE: $(expr, [context]) - // (which is just equivalent to: $(content).find(expr) - } else - return jQuery( context ).find( selector ); - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) - return jQuery( document ).ready( selector ); - - // Make sure that old selector state is passed along - if ( selector.selector && selector.context ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return this.setArray(jQuery.isArray( selector ) ? - selector : - jQuery.makeArray(selector)); - }, - - // Start with an empty selector - selector: "", - - // The current version of jQuery being used - jquery: "1.3.2", - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num === undefined ? - - // Return a 'clean' array - Array.prototype.slice.call( this ) : - - // Return just the object - this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = jQuery( elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - ret.context = this.context; - - if ( name === "find" ) - ret.selector = this.selector + (this.selector ? " " : "") + selector; - else if ( name ) - ret.selector = this.selector + "." + name + "(" + selector + ")"; - - // Return the newly-formed element set - return ret; - }, - - // Force the current matched set of elements to become - // the specified array of elements (destroying the stack in the process) - // You should use pushStack() in order to do this, but maintain the stack - setArray: function( elems ) { - // Resetting the length to 0, then using the native Array push - // is a super-fast way to populate an object with array-like properties - this.length = 0; - Array.prototype.push.apply( this, elems ); - - return this; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem && elem.jquery ? elem[0] : elem - , this ); - }, - - attr: function( name, value, type ) { - var options = name; - - // Look for the case where we're accessing a style value - if ( typeof name === "string" ) - if ( value === undefined ) - return this[0] && jQuery[ type || "attr" ]( this[0], name ); - - else { - options = {}; - options[ name ] = value; - } - - // Check to see if we're setting style values - return this.each(function(i){ - // Set all the styles - for ( name in options ) - jQuery.attr( - type ? - this.style : - this, - name, jQuery.prop( this, options[ name ], type, i, name ) - ); - }); - }, - - css: function( key, value ) { - // ignore negative width and height values - if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) - value = undefined; - return this.attr( key, value, "curCSS" ); - }, - - text: function( text ) { - if ( typeof text !== "object" && text != null ) - return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); - - var ret = ""; - - jQuery.each( text || this, function(){ - jQuery.each( this.childNodes, function(){ - if ( this.nodeType != 8 ) - ret += this.nodeType != 1 ? - this.nodeValue : - jQuery.fn.text( [ this ] ); - }); - }); - - return ret; - }, - - wrapAll: function( html ) { - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).clone(); - - if ( this[0].parentNode ) - wrap.insertBefore( this[0] ); - - wrap.map(function(){ - var elem = this; - - while ( elem.firstChild ) - elem = elem.firstChild; - - return elem; - }).append(this); - } - - return this; - }, - - wrapInner: function( html ) { - return this.each(function(){ - jQuery( this ).contents().wrapAll( html ); - }); - }, - - wrap: function( html ) { - return this.each(function(){ - jQuery( this ).wrapAll( html ); - }); - }, - - append: function() { - return this.domManip(arguments, true, function(elem){ - if (this.nodeType == 1) - this.appendChild( elem ); - }); - }, - - prepend: function() { - return this.domManip(arguments, true, function(elem){ - if (this.nodeType == 1) - this.insertBefore( elem, this.firstChild ); - }); - }, - - before: function() { - return this.domManip(arguments, false, function(elem){ - this.parentNode.insertBefore( elem, this ); - }); - }, - - after: function() { - return this.domManip(arguments, false, function(elem){ - this.parentNode.insertBefore( elem, this.nextSibling ); - }); - }, - - end: function() { - return this.prevObject || jQuery( [] ); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: [].push, - sort: [].sort, - splice: [].splice, - - find: function( selector ) { - if ( this.length === 1 ) { - var ret = this.pushStack( [], "find", selector ); - ret.length = 0; - jQuery.find( selector, this[0], ret ); - return ret; - } else { - return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ - return jQuery.find( selector, elem ); - })), "find", selector ); - } - }, - - clone: function( events ) { - // Do the clone - var ret = this.map(function(){ - if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { - // IE copies events bound via attachEvent when - // using cloneNode. Calling detachEvent on the - // clone will also remove the events from the orignal - // In order to get around this, we use innerHTML. - // Unfortunately, this means some modifications to - // attributes in IE that are actually only stored - // as properties will not be copied (such as the - // the name attribute on an input). - var html = this.outerHTML; - if ( !html ) { - var div = this.ownerDocument.createElement("div"); - div.appendChild( this.cloneNode(true) ); - html = div.innerHTML; - } - - return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; - } else - return this.cloneNode(true); - }); - - // Copy the events from the original to the clone - if ( events === true ) { - var orig = this.find("*").andSelf(), i = 0; - - ret.find("*").andSelf().each(function(){ - if ( this.nodeName !== orig[i].nodeName ) - return; - - var events = jQuery.data( orig[i], "events" ); - - for ( var type in events ) { - for ( var handler in events[ type ] ) { - jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); - } - } - - i++; - }); - } - - // Return the cloned set - return ret; - }, - - filter: function( selector ) { - return this.pushStack( - jQuery.isFunction( selector ) && - jQuery.grep(this, function(elem, i){ - return selector.call( elem, i ); - }) || - - jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ - return elem.nodeType === 1; - }) ), "filter", selector ); - }, - - closest: function( selector ) { - var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, - closer = 0; - - return this.map(function(){ - var cur = this; - while ( cur && cur.ownerDocument ) { - if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { - jQuery.data(cur, "closest", closer); - return cur; - } - cur = cur.parentNode; - closer++; - } - }); - }, - - not: function( selector ) { - if ( typeof selector === "string" ) - // test special case where just one selector is passed in - if ( isSimple.test( selector ) ) - return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); - else - selector = jQuery.multiFilter( selector, this ); - - var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; - return this.filter(function() { - return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; - }); - }, - - add: function( selector ) { - return this.pushStack( jQuery.unique( jQuery.merge( - this.get(), - typeof selector === "string" ? - jQuery( selector ) : - jQuery.makeArray( selector ) - ))); - }, - - is: function( selector ) { - return !!selector && jQuery.multiFilter( selector, this ).length > 0; - }, - - hasClass: function( selector ) { - return !!selector && this.is( "." + selector ); - }, - - val: function( value ) { - if ( value === undefined ) { - var elem = this[0]; - - if ( elem ) { - if( jQuery.nodeName( elem, 'option' ) ) - return (elem.attributes.value || {}).specified ? elem.value : elem.text; - - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type == "select-one"; - - // Nothing was selected - if ( index < 0 ) - return null; - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - if ( option.selected ) { - // Get the specifc value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if ( one ) - return value; - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - } - - // Everything else, we just grab the value - return (elem.value || "").replace(/\r/g, ""); - - } - - return undefined; - } - - if ( typeof value === "number" ) - value += ''; - - return this.each(function(){ - if ( this.nodeType != 1 ) - return; - - if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) - this.checked = (jQuery.inArray(this.value, value) >= 0 || - jQuery.inArray(this.name, value) >= 0); - - else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(value); - - jQuery( "option", this ).each(function(){ - this.selected = (jQuery.inArray( this.value, values ) >= 0 || - jQuery.inArray( this.text, values ) >= 0); - }); - - if ( !values.length ) - this.selectedIndex = -1; - - } else - this.value = value; - }); - }, - - html: function( value ) { - return value === undefined ? - (this[0] ? - this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : - null) : - this.empty().append( value ); - }, - - replaceWith: function( value ) { - return this.after( value ).remove(); - }, - - eq: function( i ) { - return this.slice( i, +i + 1 ); - }, - - slice: function() { - return this.pushStack( Array.prototype.slice.apply( this, arguments ), - "slice", Array.prototype.slice.call(arguments).join(",") ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function(elem, i){ - return callback.call( elem, i, elem ); - })); - }, - - andSelf: function() { - return this.add( this.prevObject ); - }, - - domManip: function( args, table, callback ) { - if ( this[0] ) { - var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), - scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), - first = fragment.firstChild; - - if ( first ) - for ( var i = 0, l = this.length; i < l; i++ ) - callback.call( root(this[i], first), this.length > 1 || i > 0 ? - fragment.cloneNode(true) : fragment ); - - if ( scripts ) - jQuery.each( scripts, evalScript ); - } - - return this; - - function root( elem, cur ) { - return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? - (elem.getElementsByTagName("tbody")[0] || - elem.appendChild(elem.ownerDocument.createElement("tbody"))) : - elem; - } - } -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -function evalScript( i, elem ) { - if ( elem.src ) - jQuery.ajax({ - url: elem.src, - async: false, - dataType: "script" - }); - - else - jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); - - if ( elem.parentNode ) - elem.parentNode.removeChild( elem ); -} - -function now(){ - return +new Date; -} - -jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) - target = {}; - - // extend jQuery itself if only one argument is passed - if ( length == i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) - // Extend the base object - for ( var name in options ) { - var src = target[ name ], copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) - continue; - - // Recurse if we're merging object values - if ( deep && copy && typeof copy === "object" && !copy.nodeType ) - target[ name ] = jQuery.extend( deep, - // Never move original objects, clone them - src || ( copy.length != null ? [ ] : { } ) - , copy ); - - // Don't bring in undefined values - else if ( copy !== undefined ) - target[ name ] = copy; - - } - - // Return the modified object - return target; -}; - -// exclude the following css properties to add px -var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, - // cache defaultView - defaultView = document.defaultView || {}, - toString = Object.prototype.toString; - -jQuery.extend({ - noConflict: function( deep ) { - window.$ = _$; - - if ( deep ) - window.jQuery = _jQuery; - - return jQuery; - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return toString.call(obj) === "[object Function]"; - }, - - isArray: function( obj ) { - return toString.call(obj) === "[object Array]"; - }, - - // check if an element is in a (or is an) XML document - isXMLDoc: function( elem ) { - return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || - !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); - }, - - // Evalulates a script in a global context - globalEval: function( data ) { - if ( data && /\S/.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - if ( jQuery.support.scriptEval ) - script.appendChild( document.createTextNode( data ) ); - else - script.text = data; - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); - } - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, length = object.length; - - if ( args ) { - if ( length === undefined ) { - for ( name in object ) - if ( callback.apply( object[ name ], args ) === false ) - break; - } else - for ( ; i < length; ) - if ( callback.apply( object[ i++ ], args ) === false ) - break; - - // A special, fast, case for the most common use of each - } else { - if ( length === undefined ) { - for ( name in object ) - if ( callback.call( object[ name ], name, object[ name ] ) === false ) - break; - } else - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} - } - - return object; - }, - - prop: function( elem, value, type, i, name ) { - // Handle executable functions - if ( jQuery.isFunction( value ) ) - value = value.call( elem, i ); - - // Handle passing in a number to a CSS property - return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? - value + "px" : - value; - }, - - className: { - // internal only, use addClass("class") - add: function( elem, classNames ) { - jQuery.each((classNames || "").split(/\s+/), function(i, className){ - if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) - elem.className += (elem.className ? " " : "") + className; - }); - }, - - // internal only, use removeClass("class") - remove: function( elem, classNames ) { - if (elem.nodeType == 1) - elem.className = classNames !== undefined ? - jQuery.grep(elem.className.split(/\s+/), function(className){ - return !jQuery.className.has( classNames, className ); - }).join(" ") : - ""; - }, - - // internal only, use hasClass("class") - has: function( elem, className ) { - return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; - } - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations - swap: function( elem, options, callback ) { - var old = {}; - // Remember the old values, and insert the new ones - for ( var name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - callback.call( elem ); - - // Revert the old values - for ( var name in options ) - elem.style[ name ] = old[ name ]; - }, - - css: function( elem, name, force, extra ) { - if ( name == "width" || name == "height" ) { - var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; - - function getWH() { - val = name == "width" ? elem.offsetWidth : elem.offsetHeight; - - if ( extra === "border" ) - return; - - jQuery.each( which, function() { - if ( !extra ) - val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; - if ( extra === "margin" ) - val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; - else - val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; - }); - } - - if ( elem.offsetWidth !== 0 ) - getWH(); - else - jQuery.swap( elem, props, getWH ); - - return Math.max(0, Math.round(val)); - } - - return jQuery.curCSS( elem, name, force ); - }, - - curCSS: function( elem, name, force ) { - var ret, style = elem.style; - - // We need to handle opacity special in IE - if ( name == "opacity" && !jQuery.support.opacity ) { - ret = jQuery.attr( style, "opacity" ); - - return ret == "" ? - "1" : - ret; - } - - // Make sure we're using the right name for getting the float value - if ( name.match( /float/i ) ) - name = styleFloat; - - if ( !force && style && style[ name ] ) - ret = style[ name ]; - - else if ( defaultView.getComputedStyle ) { - - // Only "float" is needed here - if ( name.match( /float/i ) ) - name = "float"; - - name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); - - var computedStyle = defaultView.getComputedStyle( elem, null ); - - if ( computedStyle ) - ret = computedStyle.getPropertyValue( name ); - - // We should always get a number back from opacity - if ( name == "opacity" && ret == "" ) - ret = "1"; - - } else if ( elem.currentStyle ) { - var camelCase = name.replace(/\-(\w)/g, function(all, letter){ - return letter.toUpperCase(); - }); - - ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { - // Remember the original values - var left = style.left, rsLeft = elem.runtimeStyle.left; - - // Put in the new values to get a computed value out - elem.runtimeStyle.left = elem.currentStyle.left; - style.left = ret || 0; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - elem.runtimeStyle.left = rsLeft; - } - } - - return ret; - }, - - clean: function( elems, context, fragment ) { - context = context || document; - - // !context.createElement fails in IE with an error but returns typeof 'object' - if ( typeof context.createElement === "undefined" ) - context = context.ownerDocument || context[0] && context[0].ownerDocument || document; - - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { - var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); - if ( match ) - return [ context.createElement( match[1] ) ]; - } - - var ret = [], scripts = [], div = context.createElement("div"); - - jQuery.each(elems, function(i, elem){ - if ( typeof elem === "number" ) - elem += ''; - - if ( !elem ) - return; - - // Convert html string into DOM nodes - if ( typeof elem === "string" ) { - // Fix "XHTML"-style tags in all browsers - elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ - return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? - all : - front + ">"; - }); - - // Trim whitespace, otherwise indexOf won't work as expected - var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); - - var wrap = - // option or optgroup - !tags.indexOf("", "" ] || - - !tags.indexOf("", "" ] || - - tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && - [ 1, "", "
" ] || - - !tags.indexOf("", "" ] || - - // matched above - (!tags.indexOf("", "" ] || - - !tags.indexOf("", "" ] || - - // IE can't serialize and - - - -

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/examples/test-server.js b/examples/test-server.js old mode 100644 new mode 100755 index 8600098..728b884 --- a/examples/test-server.js +++ b/examples/test-server.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node var sys = require('sys'), http = require('http'); http.createServer(function (req, res) { @@ -8,5 +9,5 @@ http.createServer(function (req, res) { res.write(delay+'\n'); res.end(); }, delay); -}).listen(8080); -sys.puts('Server running at http://127.0.0.1:8080/'); +}).listen(9000); +sys.puts('Server running at http://127.0.0.1:9000/'); diff --git a/src/config.js b/lib/config.js similarity index 57% rename from src/config.js rename to lib/config.js index 4f8a282..8f2bec5 100644 --- a/src/config.js +++ b/lib/config.js @@ -2,79 +2,90 @@ // Nodeload configuration // ------------------------------------ // -// The functions in this file control the behavior of the nodeload. They are called when the library -// is included: +// The functions in this file control the behavior of the nodeload globals, like HTTP_SERVER and +// REPORT_MANAGER. They should be called when the library is included: // -// var nl = require('./lib/nodeloadlib').quiet().usePort(10000); +// var nl = require('./lib/nodeload').quiet().usePort(10000); // nl.runTest(...); // +// Or, when using individual modules: +// +// var nlconfig = require('./lib/config').quiet().usePort(10000); +// var reporting = require('./lib/reporting'); +// +var BUILD_AS_SINGLE_FILE, NODELOAD_CONFIG; +if (!BUILD_AS_SINGLE_FILE) { +var EventEmitter = require('events').EventEmitter; +} /** Suppress all console output */ exports.quiet = function() { NODELOAD_CONFIG.QUIET = true; return exports; -} +}; /** Start the nodeload HTTP server on the given port */ exports.usePort = function(port) { NODELOAD_CONFIG.HTTP_PORT = port; return exports; -} +}; /** Do not start the nodeload HTTP server */ exports.disableServer = function() { NODELOAD_CONFIG.HTTP_ENABLED = false; return exports; -} +}; -/** Set the number of milliseconds between TEST_MONITOR 'update' events when tests are running */ +/** Set the default number of milliseconds between 'update' events from a LoadTest created by run(). */ exports.setMonitorIntervalMs = function(milliseconds) { NODELOAD_CONFIG.MONITOR_INTERVAL_MS = milliseconds; return exports; -} +}; /** Set the number of milliseconds between auto-refreshes for the summary webpage */ exports.setAjaxRefreshIntervalMs = function(milliseconds) { NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS = milliseconds; return exports; -} +}; /** Do not write any logs to disk */ exports.disableLogs = function() { NODELOAD_CONFIG.LOGS_ENABLED = false; return exports; -} +}; /** Set the number of milliseconds between pinging slaves when running distributed load tests */ -exports.setSlavePingIntervalMs = function(milliseconds) { - NODELOAD_CONFIG.SLAVE_PING_INTERVAL_MS = milliseconds; -} - +exports.setSlaveUpdateIntervalMs = function(milliseconds) { + NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS = milliseconds; +}; // ================= -// Private +// Singletons // ================= -var NODELOAD_CONFIG = { - QUIET: false, + +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, + HTTP_PORT: Number(process.env.HTTP_PORT) || 8000, MONITOR_INTERVAL_MS: 2000, AJAX_REFRESH_INTERVAL_MS: 2000, - LOGS_ENABLED: true, + LOGS_ENABLED: process.env.LOGS ? process.env.LOGS !== '0' : true, - SLAVE_PING_INTERVAL_MS: 3000, + SLAVE_UPDATE_INTERVAL_MS: 3000, - eventEmitter: new events.EventEmitter(), + eventEmitter: new EventEmitter(), on: function(event, fun) { this.eventEmitter.on(event, fun); }, apply: function() { this.eventEmitter.emit('apply'); } -} +}; -process.nextTick(function() { NODELOAD_CONFIG.apply() }); \ No newline at end of file +process.nextTick(function() { NODELOAD_CONFIG.apply(); }); \ No newline at end of file diff --git a/lib/header.js b/lib/header.js new file mode 100644 index 0000000..ff94eae --- /dev/null +++ b/lib/header.js @@ -0,0 +1,15 @@ +// ----------------------------------------- +// Header for single file build +// ----------------------------------------- + +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; diff --git a/lib/http.js b/lib/http.js new file mode 100644 index 0000000..9574758 --- /dev/null +++ b/lib/http.js @@ -0,0 +1,130 @@ +// ------------------------------------ +// HTTP Server +// ------------------------------------ +// +// This file defines HttpServer and the singleton HTTP_SERVER. +// +// This file defines a generic HTTP server that serves static files and that can be configured +// with new routes. It also starts the nodeload HTTP server unless require('nodeload/config') +// .disableServer() was called. +// +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var config = require('./config'); +var http = require('http'); +var fs = require('fs'); +var util = require('./util'); +var qputs = util.qputs; +var EventEmitter = require('events').EventEmitter; +var NODELOAD_CONFIG = config.NODELOAD_CONFIG; +} + +/** By default, HttpServer knows how to return static files from the current directory. Add new route +regexs using HttpServer.on(). */ +var HttpServer = exports.HttpServer = function HttpServer() { + this.routes = []; + this.running = false; +}; +util.inherits(HttpServer, EventEmitter); +/** Start the server listening on the given port */ +HttpServer.prototype.start = function(port, hostname) { + if (this.running) { return; } + this.running = true; + + var self = this; + port = port || 8000; + self.hostname = hostname || 'localhost'; + self.port = port; + self.connections = []; + + self.server = http.createServer(function(req, res) { self.route_(req, res); }); + self.server.on('connection', function(c) { + // We need to track incoming connections, beause Server.close() won't terminate active + // connections by default. + c.on('close', function() { + var idx = self.connections.indexOf(c); + if (idx !== -1) { + self.connections.splice(idx, 1); + } + }); + self.connections.push(c); + }); + self.server.listen(port, hostname); + + self.emit('start', self.hostname, self.port); + return self; +}; +/** Terminate the server */ +HttpServer.prototype.stop = function() { + if (!this.running) { return; } + this.running = false; + this.connections.forEach(function(c) { c.destroy(); }); + this.server.close(); + this.server = null; + this.emit('end'); +}; +/** When an incoming request matches a given regex, route it to the provided handler: +function(url, ServerRequest, ServerResponse) */ +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; i < this.routes.length; i++) { + if (req.url.match(this.routes[i].regex)) { + this.routes[i].handler(req.url, req, res); + return; + } + } + if (req.method === 'GET') { + this.serveFile_('.' + req.url, res); + } else { + res.writeHead(405, {"Content-Length": "0"}); + res.end(); + } +}; +HttpServer.prototype.serveFile_ = function(file, response) { + fs.stat(file, function(err, stat) { + if (err) { + response.writeHead(404, {"Content-Type": "text/plain"}); + response.write("Cannot find file: " + file); + response.end(); + return; + } + + fs.readFile(file, "binary", function (err, data) { + if (err) { + response.writeHead(500, {"Content-Type": "text/plain"}); + response.write("Error opening file " + file + ": " + err); + } else { + response.writeHead(200, { 'Content-Length': data.length }); + response.write(data, "binary"); + } + response.end(); + }); + }); +}; + +// ================= +// Singletons +// ================= + +/** The global HTTP server used by nodeload */ +var HTTP_SERVER = exports.HTTP_SERVER = new HttpServer(); +NODELOAD_CONFIG.on('apply', function() { + if (NODELOAD_CONFIG.HTTP_ENABLED) { + HTTP_SERVER.on('start', function(hostname, port) { + qputs('Started HTTP server on ' + hostname + ':' + port + '.'); + }); + HTTP_SERVER.on('end', function() { + qputs('Shutdown HTTP server.'); + }); + HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT); + } +}); \ No newline at end of file diff --git a/lib/loadtesting.js b/lib/loadtesting.js new file mode 100644 index 0000000..282c818 --- /dev/null +++ b/lib/loadtesting.js @@ -0,0 +1,335 @@ +// ------------------------------------ +// Main HTTP load testing interface +// ------------------------------------ +// +// This file defines run(), LoadTest, createClient() and extendClient(). +// +// This file defines the main API for using nodeload to construct load tests. The main function for +// starting a load test is run(). Nodeload modules, such as monitoring.js and reporting.js, can also be +// used independently. +// +/*jslint laxbreak: true */ +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; +} + +/** TEST_OPTIONS defines all of the parameters that can be set in a test specifiction passed to +run(). By default (calling require('nodeload').run({});), will GET localhost:8080/ as fast as possible +with 10 users for 2 minutes. */ +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. +}; + +var LoadTest, generateConnection, requestGeneratorLoop; + +/** run(spec, ...) is the primary method for creating and executing load tests with nodeload. See +TEST_OPTIONS for a list of the configuration values in each specification. + +@return A LoadTest object with start() / stop() methods, emits 'start' / 'end', and holds statistics + in .interval and .stats. See LoadTest below. +*/ +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; +}; + +/** 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. Set loadtest.keepAlive=true to not shut down HTTP_SERVER when done. + +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 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', interval, stats: interval has stats since last update. stats contains overall stats. +- 'end': all tests finished + +*/ +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); + +/** Start running the load test. Starts HTTP_SERVER if it is stopped (unless disabled globally). */ +LoadTest.prototype.start = function(keepAlive) { + var self = this; + self.keepAlive = keepAlive; + + // clients can catch 'start' event even after calling start(). + 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; +}; + +/** Force the load test to stop. */ +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; } + + this.updateInterval = 0; + this.update(); + qputs('Done.'); + + if (!this.keepAlive) { + HTTP_SERVER.stop(); + } + + this.emit('end'); +}; + +/** extendClient extends an existing instance of http.Client by noting the request method and path. +Writing to new requests also emits the 'write' event. This client must be used when using nodeload to +when tracking 'uniques' and 'request-bytes'. */ +var extendClient = exports.extendClient = function(client) { + var wrappedRequest = client.request; + client.request = function(method, url) { + var request = wrappedRequest.apply(client, arguments), + wrappedWrite = request.write, + wrappedEnd = request.end, + track = function(data) { + if (data) { + request.emit('write', data); + request.body += data.toString(); + } + }; + request.method = method; + request.path = url; + request.body = ''; + request.write = function(data, encoding) { + track(data); + return wrappedWrite.apply(request, arguments); + }; + request.end = function(data, encoding) { + track(data); + return wrappedEnd.apply(request, arguments); + }; + return request; + }; + return client; +}; + +/** Same arguments as http.createClient. Returns an extended version of the object (see extendClient) */ +var createClient = exports.createClient = function() { + return extendClient(util.createReconnectingClient.apply(this, arguments)); +}; + +/** Creates a new HTTP connection. This is used as an argGenerator for LoadTest's MultiLoop, so each +"user" gets its own connection. If the load test is using requestGeneratorLoop to generate its requests, +then we also need to terminate pending requests when client errors occur. We emit a fake 'response' +event, so that requestGeneratorLoop can finish its iteration. */ +function generateConnection(host, port, detectClientErrors) { + return function() { + var client = createClient(port, host); + if (detectClientErrors) { + // we need to detect client errors if we're managing the request generation + client.on('error', function(err) { + qputs('WARN: Error during HTTP request: ' + (err ? err.toString() : 'unknown')); + }); + client.on('reconnect', function(oldclient) { + // For each pending outgoing request, simulate an empty response + if (oldclient._outgoing) { + oldclient._outgoing.forEach(function(req) { + if (req instanceof http.ClientRequest) { + req.emit('response', new EventEmitter()); + } + }); + } + }); + } + return client; + }; +} + +/** Wrapper for request generator function, generator + +@param generator A function: + + function(http.Client) -> http.ClientRequest + + The http.Client is provided by nodeload. The http.ClientRequest may contain an extra + .timeout field specifying the maximum milliseconds to wait for a response. + +@return A Loop compatible function, function(loopFun, http.Client). Each iteration makes an HTTP + request by calling generator. loopFun({req: http.ClientRequest, res: http.ClientResponse}) is + called when the HTTP response is received or the request times out. */ +function requestGeneratorLoop(generator) { + return function(finished, client) { + var running = true, timeoutId, request = generator(client); + var callFinished = function(response) { + if (running) { + running = false; + clearTimeout(timeoutId); + response.statusCode = response.statusCode || 0; + finished({req: request, res: response}); + } + }; + if (request) { + if (request.timeout > 0) { + timeoutId = setTimeout(function() { + callFinished(new EventEmitter()); + }, request.timeout); + } + request.on('response', function(response) { + callFinished(response); + }); + request.end(); + } else { + finished(null); + } + }; +} \ No newline at end of file diff --git a/lib/loop/index.js b/lib/loop/index.js new file mode 100644 index 0000000..7d97ddf --- /dev/null +++ b/lib/loop/index.js @@ -0,0 +1,2 @@ +exports.Loop = require('./loop').Loop; +exports.MultiLoop = require('./multiloop').MultiLoop; \ No newline at end of file diff --git a/lib/loop/loop.js b/lib/loop/loop.js new file mode 100644 index 0000000..8b945a3 --- /dev/null +++ b/lib/loop/loop.js @@ -0,0 +1,215 @@ +// ----------------------------------------- +// Event-based looping +// ----------------------------------------- +// +// This file defines Loop and MultiLoop. +// +// Nodeload uses the node.js event loop to repeatedly call a function. In order for this to work, the +// function cooperates by accepting a function, finished, as its first argument and calls finished() +// when it completes. This is refered to as "event-based looping" in nodeload. +// +/*jslint laxbreak: true, undef: true */ +/*global setTimeout: false */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var EventEmitter = require('events').EventEmitter; +} + +/** LOOP_OPTIONS defines all of the parameters that used with Loop.create(), MultiLoop() */ +var LOOP_OPTIONS = exports.LOOP_OPTIONS = { + fun: undefined, // A function to execute which accepts the parameters (finished, args). + // The value of args is the return value of argGenerator() or the args + // parameter if argGenerator is undefined. The function must call + // finished(results) when it completes. + argGenerator: undefined, // A function which is called once when the loop 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: undefined, // If argGenerator is NOT specified, then this is passed to the fun as + // "args". + rps: Infinity, // Target number of time per second to call fun() + duration: Infinity, // Maximum duration of this loop in seconds + numberOfTimes: Infinity, // Maximum number of times to call fun() + concurrency: 1, // (MultiLoop only) Number of concurrent calls of fun() + // + concurrencyProfile: undefined, // (MultiLoop only) array indicating concurrency over time: + // [[time (seconds), # users], [time 2, users], ...] + // For example, ramp up from 0 to 100 "threads" and back down to 0 over + // 20 seconds: + // [[0, 0], [10, 100], [20, 0]] + // + rpsProfile: undefined // (MultiLoop only) array indicating execution rate 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]] +}; + +/** Loop wraps an arbitrary function to be executed in a loop. Each iteration of the loop is scheduled +in the node.js event loop using process.nextTick(), which allows other events in the loop to be handled +as the loop executes. Loop emits the events 'start' (before the first iteration), 'end', 'startiteration' +and 'enditeration'. + +@param funOrSpec Either a loop specification object or a loop function. LOOP_OPTIONS lists all the + supported fields in a loop specification. + + A loop function is an asynchronous function that calls finished(result) when it + finishes: + + function(finished, args) { + ... + finished(result); + } + + use the static method Loop.funLoop(f) to wrap simple, non-asynchronous functions. +@param args passed as-is as the second argument to fun +@param conditions a list of functions that are called at the beginning of every loop. If any + function returns false, the loop terminates. Loop#timeLimit and Loop#maxExecutions + are conditions that can be used here. +@param rps max number of times per second this loop should execute */ +var Loop = exports.Loop = function Loop(funOrSpec, args, conditions, rps) { + EventEmitter.call(this); + + if (typeof funOrSpec === 'object') { + var spec = util.defaults(funOrSpec, LOOP_OPTIONS); + + funOrSpec = spec.fun; + args = spec.argGenerator ? spec.argGenerator() : spec.args; + conditions = []; + rps = spec.rps; + + if (spec.numberOfTimes > 0 && spec.numberOfTimes < Infinity) { + conditions.push(Loop.maxExecutions(spec.numberOfTimes)); + } + if (spec.duration > 0 && spec.duration < Infinity) { + conditions.push(Loop.timeLimit(spec.duration)); + } + } + + this.__defineGetter__('rps', function() { return rps; }); + this.__defineSetter__('rps', function(val) { + rps = (val >= 0) ? val : Infinity; + this.timeout_ = Math.floor(1/rps * 1000); + if (this.restart_ && this.timeout_ < Infinity) { + var oldRestart = this.restart_; + this.restart_ = null; + oldRestart(); + } + }); + + this.id = util.uid(); + this.fun = funOrSpec; + this.args = args; + this.conditions = conditions || []; + this.running = false; + this.rps = rps; +}; + +util.inherits(Loop, EventEmitter); + +/** Start executing this.fun with the arguments, this.args, until any condition in this.conditions +returns false. When the loop completes the 'end' event is emitted. */ +Loop.prototype.start = function() { + var self = this, + startLoop = function() { + self.emit('start'); + self.loop_(); + }; + + if (self.running) { return; } + self.running = true; + process.nextTick(startLoop); + return this; +}; + +Loop.prototype.stop = function() { + this.running = false; +}; + +/** Calls each function in Loop.conditions. Returns false if any function returns false */ +Loop.prototype.checkConditions_ = function() { + return this.running && this.conditions.every(function(c) { return c(); }); +}; + +/** Checks conditions and schedules the next loop iteration. 'startiteration' is emitted before each +iteration and 'enditeration' is emitted after. */ +Loop.prototype.loop_ = function() { + + var self = this, result, active, lagging, + callfun = function() { + if (self.timeout_ === Infinity) { + self.restart_ = callfun; + return; + } + + result = null; active = true; lagging = (self.timeout_ <= 0); + if (!lagging) { + setTimeout(function() { + lagging = active; + if (!lagging) { self.loop_(); } + }, self.timeout_); + } + self.emit('startiteration', self.args); + var start = new Date(); + self.fun(function(res) { + active = false; + result = res; + self.emit('enditeration', result); + if (lagging) { self.loop_(); } + }, self.args); + }; + + if (self.checkConditions_()) { + process.nextTick(callfun); + } else { + self.running = false; + self.emit('end'); + } +}; + + +// Predefined functions that can be used in Loop.conditions + +/** Returns false after a given number of seconds */ +Loop.timeLimit = function(seconds) { + var start = new Date(); + return function() { + return (seconds === Infinity) || ((new Date() - start) < (seconds * 1000)); + }; +}; +/** Returns false after a given number of iterations */ +Loop.maxExecutions = function(numberOfTimes) { + var counter = 0; + return function() { + return (numberOfTimes === Infinity) || (counter++ < numberOfTimes); + }; +}; + + +// Helpers for dealing with loop functions + +/** A wrapper for any existing function so it can be used by Loop. e.g.: + myfun = function(x) { return x+1; } + new Loop(Loop.funLoop(myfun), args, [Loop.timeLimit(10)], 0) */ +Loop.funLoop = function(fun) { + return function(finished, args) { + finished(fun(args)); + }; +}; +/** Wrap a loop function. For each iteration, calls startRes = start(args) before calling fun(), and +calls finish(result-from-fun, startRes) when fun() finishes. */ +Loop.loopWrapper = function(fun, start, finish) { + return function(finished, args) { + var startRes = start && start(args), + finishFun = function(result) { + if (result === undefined) { + util.qputs('Function result is null; did you forget to call finished(result)?'); + } + + if (finish) { finish(result, startRes); } + + finished(result); + }; + fun(finishFun, args); + }; +}; \ No newline at end of file diff --git a/lib/loop/multiloop.js b/lib/loop/multiloop.js new file mode 100644 index 0000000..0e5d277 --- /dev/null +++ b/lib/loop/multiloop.js @@ -0,0 +1,144 @@ +// ----------------------------------------- +// MultiLoop +// ----------------------------------------- +// +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var loop = require('./loop'); +var EventEmitter = require('events').EventEmitter; +var Loop = loop.Loop; +var LOOP_OPTIONS = loop.LOOP_OPTIONS; +} + +/** MultiLoop accepts a single loop specification, but allows it to be executed concurrently by creating +multiple Loop instances. The execution rate and concurrency are changed over time using profiles. +LOOP_OPTIONS lists the supported specification parameters. */ +var MultiLoop = exports.MultiLoop = function MultiLoop(spec) { + EventEmitter.call(this); + + this.spec = util.extend({}, util.defaults(spec, LOOP_OPTIONS)); + this.loops = []; + this.concurrencyProfile = spec.concurrencyProfile || [[0, spec.concurrency]]; + this.rpsProfile = spec.rpsProfile || [[0, spec.rps]]; + this.updater_ = this.update_.bind(this); + this.finishedChecker_ = this.checkFinished_.bind(this); +}; + +util.inherits(MultiLoop, EventEmitter); + +/** Start all scheduled Loops. When the loops complete, 'end' event is emitted. */ +MultiLoop.prototype.start = function() { + if (this.running) { return; } + this.running = true; + this.startTime = new Date(); + this.rps = 0; + this.concurrency = 0; + this.loops = []; + this.loopConditions_ = []; + + if (this.spec.numberOfTimes > 0 && this.spec.numberOfTimes < Infinity) { + this.loopConditions_.push(Loop.maxExecutions(this.spec.numberOfTimes)); + } + + if (this.spec.duration > 0 && this.spec.duration < Infinity) { + this.endTimeoutId = setTimeout(this.stop.bind(this), this.spec.duration * 1000); + } + + process.nextTick(this.emit.bind(this, 'start')); + this.update_(); + return this; +}; + +/** Force all loops to finish */ +MultiLoop.prototype.stop = function() { + if (!this.running) { return; } + clearTimeout(this.endTimeoutId); + clearTimeout(this.updateTimeoutId); + this.running = false; + this.loops.forEach(function(l) { l.stop(); }); + this.emit('remove', this.loops); + this.emit('end'); + this.loops = []; +}; + +/** Given a profile in the format [[time, value], [time, value], ...], return the value corresponding +to the given time. Transitions between points are currently assumed to be linear, and value=0 at time=0 +unless otherwise specified in the profile. */ +MultiLoop.prototype.getProfileValue_ = function(profile, time) { + if (!profile || profile.length === 0) { return 0; } + if (time < 0) { return profile[0][0]; } + + var lastval = [0,0]; + for (var i = 0; i < profile.length; i++) { + if (profile[i][0] === time) { + return profile[i][1]; + } else if (profile[i][0] > time) { + var dx = profile[i][0]-lastval[0], dy = profile[i][1]-lastval[1]; + return Math.floor((time-lastval[0]) / dx * dy + lastval[1]); + } + lastval = profile[i]; + } + return profile[profile.length-1][1]; +}; + +/** Given a profile in the format [[time, value], [time, value], ...], and the current time, return the +number of milliseconds before the profile value will change by 1. */ +MultiLoop.prototype.getProfileNextTimeout_ = function(profile, time) { + if (time < 0) { return -time; } + + var MIN_TIMEOUT = 1000, lastval = [0,0]; + for (var i = 0; i < profile.length; i++) { + if (profile[i][0] > time) { + var dt = profile[i][0]-lastval[0], + millisecondsPerUnitChange = dt / (profile[i][1]-lastval[1]) * 1000; + return Math.max(MIN_TIMEOUT, Math.min(dt, millisecondsPerUnitChange)); + } + lastval = profile[i]; + } + return Infinity; +}; + +MultiLoop.prototype.update_ = function() { + var i, now = Math.floor((new Date() - this.startTime) / 1000), + concurrency = this.getProfileValue_(this.concurrencyProfile, now), + rps = this.getProfileValue_(this.rpsProfile, now), + timeout = Math.min(this.getProfileNextTimeout_(this.concurrencyProfile, now), this.getProfileNextTimeout_(this.rpsProfile, now)); + + if (concurrency < this.concurrency) { + var removed = this.loops.splice(concurrency); + removed.forEach(function(l) { l.stop(); }); + this.emit('remove', removed); + } else if (concurrency > this.concurrency) { + var loops = []; + for (i = 0; i < concurrency-this.concurrency; i++) { + var args = this.spec.argGenerator ? this.spec.argGenerator() : this.spec.args, + loop = new Loop(this.spec.fun, args, this.loopConditions_, 0).start(); + loop.on('end', this.finishedChecker_); + loops.push(loop); + } + this.loops = this.loops.concat(loops); + this.emit('add', loops); + } + + if (concurrency !== this.concurrency || rps !== this.rps) { + var rpsPerLoop = (rps / concurrency); + this.loops.forEach(function(l) { l.rps = rpsPerLoop; }); + this.emit('rps', rps); + } + + this.concurrency = concurrency; + this.rps = rps; + + if (timeout < Infinity) { + this.updateTimeoutId = setTimeout(this.updater_, timeout); + } +}; + +MultiLoop.prototype.checkFinished_ = function() { + if (!this.running) { return true; } + if (this.loops.some(function (l) { return l.running; })) { return false; } + this.running = false; + this.emit('end'); + return true; +}; diff --git a/lib/monitoring/collectors.js b/lib/monitoring/collectors.js new file mode 100644 index 0000000..7cdccdf --- /dev/null +++ b/lib/monitoring/collectors.js @@ -0,0 +1,168 @@ +// +// Define new statistics that Monitor can track by adding to this file. Each class should have: +// +// - stats, a member which implements the standard interface found in stats.js +// - start(context, args), optional, called when execution of the instrumented code is about to start +// - end(context, result), optional, called when the instrumented code finishes executing +// +// Defining .disableIntervalCollection and .disableCumulativeCollection to the collection of per-interval +// and overall statistics respectively. +// + +/*jslint sub:true */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var stats = require('../stats'); +var Histogram = stats.Histogram; +var Peak = stats.Peak; +var ResultsCounter = stats.ResultsCounter; +var Uniques = stats.Uniques; +var Accumulator = stats.Accumulator; +var LogFile = stats.LogFile; +var StatsCollectors = exports; +} else { +var StatsCollectors = {}; +} + +/** Track the runtime of an operation, storing stats in a stats.js#Histogram */ +StatsCollectors['runtime'] = StatsCollectors['latency'] = function RuntimeCollector(params) { + var self = this; + self.stats = new Histogram(params); + self.start = function(context) { context.start = new Date(); }; + self.end = function(context) { self.stats.put(new Date() - context.start); }; +}; + +/** Track HTTP response codes, storing stats in a stats.js#ResultsCounter object. The client must call +.end({res: http.ClientResponse}). */ +StatsCollectors['result-codes'] = function ResultCodesCollector() { + var self = this; + self.stats = new ResultsCounter(); + self.end = function(context, http) { self.stats.put(http.res.statusCode); }; +}; + +/** Track the concurrent executions (ie. stuff between calls to .start() and .end()), storing in a +stats.js#Peak. */ +StatsCollectors['concurrency'] = function ConcurrencyCollector() { + var self = this, c = 0; + self.stats = new Peak(); + self.start = function() { c++; }; + self.end = function() { self.stats.put(c--); }; +}; + +/** Track the size of HTTP request bodies sent by adding up the content-length headers. This function +doesn't really work as you'd hope right now, since it doesn't work for chunked encoding messages and +doesn't return actual bytes over the wire (headers, etc). */ +StatsCollectors['request-bytes'] = function RequestBytesCollector() { + var self = this; + self.stats = new Accumulator(); + self.end = function(context, http) { + if (http && http.req) { + if (http.req._header) { self.stats.put(http.req._header.length); } + if (http.req.body) { self.stats.put(http.req.body.length); } + } + }; +}; + +/** Track the size of HTTP response bodies. It doesn't account for headers! */ +StatsCollectors['response-bytes'] = function ResponseBytesCollector() { + var self = this; + self.stats = new Accumulator(); + self.end = function(context, http) { + if (http && http.res) { + http.res.on('data', function(chunk) { + self.stats.put(chunk.length); + }); + } + }; +}; + +/** Track unique URLs requested, storing stats in a stats.js#Uniques object. The client must call +Monitor.start({req: http.ClientRequest}). */ +StatsCollectors['uniques'] = function UniquesCollector() { + var self = this; + self.stats = new Uniques(); + self.end = function(context, http) { + if (http && http.req) { self.stats.put(http.req.path); } + }; +}; +StatsCollectors['uniques'].disableIntervalCollection = true; // Per-interval stats should be not be collected + +/** Track number HTTP response codes that are considered errors. Can also log request / response +information to disk when an error response is received. Specify the acceptable HTTP status codes in +params.successCodes. Specify the log file name in params.log, or leave undefined to disable logging. */ +StatsCollectors['http-errors'] = function HttpErrorsCollector(params) { + var self = this; + self.stats = new Accumulator(); + self.successCodes = params.successCodes || [200]; + self.logfile = (typeof params.log === 'string') ? new LogFile(params.log) : params.log; + self.logResBody = ( params.hasOwnProperty('logResBody') ) ? params.logResBody : true; + self.end = function(context, http) { + if (self.successCodes.indexOf(http.res.statusCode) < 0) { + self.stats.put(1); + + if (self.logfile) { + util.readStream(http.res, function(body) { + var logObj = { ts: new Date(), + req: { + headers: http.req._header, + body: http.req.body, + }, + res: { + statusCode: http.res.statusCode, + headers: http.res.headers + } + }; + if (self.logResBody) { + logObj.res.body = body; + } + self.logfile.put(JSON.stringify(logObj) + '\n'); + }); + } + } + }; +}; +StatsCollectors['http-errors'].disableIntervalCollection = true; // Per-interval stats should be not be collected + +/** Track number HTTP response codes that are considered errors. Can also log request / response +information to disk when an error response is received. Specify the acceptable HTTP status codes in +params.successCodes. Specify the log file name in params.log, or leave undefined to disable logging. */ +StatsCollectors['slow-responses'] = function HttpErrorsCollector(params) { + var self = this; + self.stats = new Accumulator(); + self.threshold = params.threshold || 1000; + self.logfile = (typeof params.log === 'string') ? new LogFile(params.log) : params.log; + self.logResBody = ( params.hasOwnProperty('logResBody') ) ? params.logResBody : true; + self.start = function(context) { context.start = new Date(); }; + self.end = function(context, http) { + var runTime = new Date() - context.start; + if (runTime > self.threshold) { + self.stats.put(1); + + if (self.logfile) { + util.readStream(http.res, function(body) { + var logObj = { ts: new Date(), + req: { + // Use the _header "private" member of http.ClientRequest, available as of + // node v0.2.2 (9/30/10). This is the only way to reliably get all request + // headers, since ClientRequest adds headers beyond what the user specifies + // in certain conditions, like Connection and Transfer-Encoding. + headers: http.req._header, + body: http.req.body, + }, + res: { + statusCode: http.res.statusCode, + headers: http.res.headers + }, + latency: runTime + }; + if (self.logResBody) { + logObj.res.body = body; + } + self.logfile.put(JSON.stringify(logObj) + '\n'); + }); + } + } + }; +}; +StatsCollectors['slow-responses'].disableIntervalCollection = true; // Per-interval stats should be not be collected \ No newline at end of file diff --git a/lib/monitoring/index.js b/lib/monitoring/index.js new file mode 100644 index 0000000..44623c0 --- /dev/null +++ b/lib/monitoring/index.js @@ -0,0 +1,4 @@ +exports.Monitor = require('./monitor').Monitor; +exports.MonitorGroup = require('./monitorgroup').MonitorGroup; +exports.StatsLogger = require('./statslogger').StatsLogger; +exports.StatsCollectors = require('./collectors'); \ No newline at end of file diff --git a/lib/monitoring/monitor.js b/lib/monitoring/monitor.js new file mode 100644 index 0000000..f288b80 --- /dev/null +++ b/lib/monitoring/monitor.js @@ -0,0 +1,163 @@ +// ----------------- +// Monitor +// ----------------- +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var StatsCollectors = require('./collectors'); +var StatsLogger = require('./statslogger').StatsLogger; +var EventEmitter = require('events').EventEmitter; +} + +/** Monitor is used to track code statistics of code that is run multiple times or concurrently: + + var monitor = new Monitor('runtime'); + function f() { + var m = monitor.start(); + doSomethingAsynchronous(..., function() { + m.end(); + }); + } + ... + console.log('f() median runtime (ms): ' + monitor.stats['runtime'].percentile(.5)); + +Look at monitoring.test.js for more examples. + +Monitor can also emits periodic 'update' events with overall and statistics since the last 'update'. This +allows the statistics to be introspected at regular intervals for things like logging and reporting. Set +Monitor.updateInterval to enable 'update' events. + +@param arguments contain names of the statistics to track. Add additional statistics to collectors.js. +*/ +var Monitor = exports.Monitor = function Monitor() { // arguments + EventEmitter.call(this); + util.PeriodicUpdater.call(this); // adds updateInterval property and calls update() + this.targets = []; + this.setStats.apply(this, arguments); +}; + +util.inherits(Monitor, EventEmitter); + +/** Set the statistics this monitor should gather. */ +Monitor.prototype.setStats = function(stats) { // arguments contains stats names + var self = this, + summarizeStats = function() { + var summary = {ts: new Date()}; + if (self.name) { summary.name = self.name; } + util.forEach(this, function(statName, stats) { + summary[statName] = stats.summary(); + }); + return summary; + }; + + self.collectors = []; + self.stats = {}; + self.interval = {}; + stats = (stats instanceof Array) ? stats : Array.prototype.slice.call(arguments); + stats.forEach(function(stat) { + var name = stat, params; + if (typeof stat === 'object') { + name = stat.name; + params = stat; + } + var Collector = StatsCollectors[name]; + if (!Collector) { + throw new Error('No collector for statistic: ' + name); + } + if (!Collector.disableIntervalCollection) { + var intervalCollector = new Collector(params); + self.collectors.push(intervalCollector); + self.interval[name] = intervalCollector.stats; + } + if (!Collector.disableCumulativeCollection) { + var cumulativeCollector = new Collector(params); + self.collectors.push(cumulativeCollector); + self.stats[name] = cumulativeCollector.stats; + } + }); + + Object.defineProperty(this.stats, 'summary', { + enumerable: false, + value: summarizeStats + }); + Object.defineProperty(this.interval, 'summary', { + enumerable: false, + value: summarizeStats + }); +}; + +/** Called by the instrumented code when it begins executing. Returns a monitoring context. Call +context.end() when the instrumented code completes. */ +Monitor.prototype.start = function(args) { + var self = this, + endFuns = [], + doStart = function(m, context) { + if (m.start) { m.start(context, args); } + if (m.end) { + endFuns.push(function(result) { return m.end(context, result); }); + } + }, + monitoringContext = { + end: function(result) { + endFuns.forEach(function(f) { f(result); }); + } + }; + + self.collectors.forEach(function(m) { doStart(m, {}); }); + return monitoringContext; +}; + +/** Monitor a set of EventEmitter objects, where each object is analogous to a thread. The objects +should emit 'start' and 'end' when they begin doing the operation being instrumented. This is useful +for monitoring concurrently executing instances of loop.js#Loop. + +Call either as monitorObjects(obj1, obj2, ...) or monitorObjects([obj1, obj2, ...], 'start', 'end') */ +Monitor.prototype.monitorObjects = function(objs, startEvent, endEvent) { + var self = this; + + if (!(objs instanceof Array)) { + objs = util.argarray(arguments); + startEvent = endEvent = null; + } + + startEvent = startEvent || 'start'; + endEvent = endEvent || 'end'; + + objs.forEach(function(o) { + var mon; + o.on(startEvent, function(args) { + mon = self.start(args); + }); + o.on(endEvent, function(result) { + mon.end(result); + }); + }); + + return self; +}; + +/** Set the file name or stats.js#LogFile object that statistics are logged to; null for default */ +Monitor.prototype.setLogFile = function(logNameOrObject) { + this.logNameOrObject = logNameOrObject; +}; + +/** Log statistics each time an 'update' event is emitted? */ +Monitor.prototype.setLoggingEnabled = function(enabled) { + if (enabled) { + this.logger = this.logger || new StatsLogger(this, this.logNameOrObject).start(); + } else if (this.logger) { + this.logger.stop(); + this.logger = null; + } + return this; +}; + +/** Emit the 'update' event and reset the statistics for the next window */ +Monitor.prototype.update = function() { + this.emit('update', this.interval, this.stats); + util.forEach(this.interval, function(name, stats) { + if (stats.length > 0) { + stats.clear(); + } + }); +}; \ No newline at end of file diff --git a/lib/monitoring/monitorgroup.js b/lib/monitoring/monitorgroup.js new file mode 100644 index 0000000..63628d8 --- /dev/null +++ b/lib/monitoring/monitorgroup.js @@ -0,0 +1,116 @@ +// ----------------- +// MonitorGroup +// ----------------- +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var Monitor = require('./monitor').Monitor; +var StatsLogger = require('./statslogger').StatsLogger; +var EventEmitter = require('events').EventEmitter; +} + +/** MonitorGroup represents a group of Monitor instances. Calling MonitorGroup('runtime').start('myfunction') +is equivalent to creating a Monitor('runtime') for myfunction and and calling start(). MonitorGroup can +also emit regular 'update' events as well as log the statistics from the interval to disk. + +@param arguments contain names of the statistics to track. Register more statistics by extending + Monitor.StatsCollectors. */ +var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { + EventEmitter.call(this); + util.PeriodicUpdater.call(this); + + var summarizeStats = function() { + var summary = {ts: new Date()}; + util.forEach(this, function(monitorName, stats) { + summary[monitorName] = {}; + util.forEach(stats, function(statName, stat) { + summary[monitorName][statName] = stat.summary(); + }); + }); + return summary; + }; + + this.statsNames = (statsNames instanceof Array) ? statsNames : Array.prototype.slice.call(arguments); + this.monitors = {}; + this.stats = {}; + this.interval = {}; + + Object.defineProperty(this.stats, 'summary', { + enumerable: false, + value: summarizeStats + }); + Object.defineProperty(this.interval, 'summary', { + enumerable: false, + value: summarizeStats + }); +}; + +util.inherits(MonitorGroup, EventEmitter); + +/** Pre-initialize monitors with the given names. This allows construction overhead to take place all at +once if desired. */ +MonitorGroup.prototype.initMonitors = function(monitorNames) { + var self = this; + monitorNames = (monitorNames instanceof Array) ? monitorNames : Array.prototype.slice.call(arguments); + monitorNames.forEach(function(name) { + self.monitors[name] = new Monitor(self.statsNames); + self.stats[name] = self.monitors[name].stats; + self.interval[name] = self.monitors[name].interval; + }); + return self; +}; + +/** Call .start() for the named monitor */ +MonitorGroup.prototype.start = function(monitorName, args) { + monitorName = monitorName || ''; + if (!this.monitors[monitorName]) { + this.initMonitors([monitorName]); + } + return this.monitors[monitorName].start(args); +}; + +/** Like Monitor.monitorObjects() except each object's 'start' event should include the monitor name as +its first argument. See monitoring.test.js for an example. */ +MonitorGroup.prototype.monitorObjects = function(objs, startEvent, endEvent) { + var self = this, ctxs = {}; + + if (!(objs instanceof Array)) { + objs = util.argarray(arguments); + startEvent = endEvent = null; + } + + startEvent = startEvent || 'start'; + endEvent = endEvent || 'end'; + + objs.forEach(function(o) { + o.on(startEvent, function(monitorName, args) { + ctxs[monitorName] = self.start(monitorName, args); + }); + o.on(endEvent, function(monitorName, result) { + if (ctxs[monitorName]) { ctxs[monitorName].end(result); } + }); + }); + return self; +}; + +/** Set the file name or stats.js#LogFile object that statistics are logged to; null for default */ +MonitorGroup.prototype.setLogFile = function(logNameOrObject) { + this.logNameOrObject = logNameOrObject; +}; + +/** Log statistics each time an 'update' event is emitted */ +MonitorGroup.prototype.setLoggingEnabled = function(enabled) { + if (enabled) { + this.logger = this.logger || new StatsLogger(this, this.logNameOrObject).start(); + } else if (this.logger) { + this.logger.stop(); + this.logger = null; + } + return this; +}; + +/** Emit the update event and reset the statistics for the next window */ +MonitorGroup.prototype.update = function() { + this.emit('update', this.interval, this.stats); + util.forEach(this.monitors, function (name, m) { m.update(); }); +}; \ No newline at end of file diff --git a/lib/monitoring/statslogger.js b/lib/monitoring/statslogger.js new file mode 100644 index 0000000..4381925 --- /dev/null +++ b/lib/monitoring/statslogger.js @@ -0,0 +1,33 @@ +// ----------------- +// StatsLogger +// ----------------- +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var START = require('../config').NODELOAD_CONFIG.START; +var LogFile = require('../stats').LogFile; +} + +/** StatsLogger writes interval stats from a Monitor or MonitorGroup to disk each time it emits 'update' */ +var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { + this.logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '-stats.log'); + this.monitor = monitor; + this.logger_ = this.log_.bind(this); +}; +StatsLogger.prototype.start = function() { + this.createdLog = (typeof this.logNameOrObject === 'string'); + this.log = this.createdLog ? new LogFile(this.logNameOrObject) : this.logNameOrObject; + this.monitor.on('update', this.logger_); + return this; +}; +StatsLogger.prototype.stop = function() { + if (this.createdLog) { + this.log.close(); + this.log = null; + } + this.monitor.removeListener('update', this.logger_); + return this; +}; +StatsLogger.prototype.log_ = function() { + var summary = this.monitor.interval.summary(); + this.log.put(JSON.stringify(summary) + ',\n'); +}; \ No newline at end of file diff --git a/src/options.js b/lib/nl/options.js similarity index 92% rename from src/options.js rename to lib/nl/options.js index 368a82b..1a9f979 100644 --- a/src/options.js +++ b/lib/nl/options.js @@ -27,13 +27,13 @@ var sys = require('sys'); var url = require('url'); var path = require('path'); -var optparse = require('../deps/optparse-js/lib/optparse'); +var optparse = require('./optparse'); // Default options var testConfig = { - url: null, + url: undefined, method: 'GET', - requestData: null, + requestData: undefined, host: '', port: 80, numClients: 1, @@ -41,7 +41,7 @@ var testConfig = { timeLimit: Infinity, targetRps: Infinity, path: '/', - requestGenerator: null, + requestGenerator: undefined, reportInterval: 10, }; var switches = [ @@ -57,6 +57,13 @@ var switches = [ [ '-h', '--help', 'Show usage info' ], ]; +var parser; + +var help = exports.help = function() { + sys.puts(parser); + process.exit(); +}; + // Create a new OptionParser. var parser = new optparse.OptionParser(switches); parser.banner = 'nodeload.js [options] :[]'; @@ -65,8 +72,9 @@ parser.on('help', function() { }); parser.on(2, function (value) { - if (value.search('^http://') == -1) + if (value.search('^http://') === -1) { value = 'http://' + value; + } testConfig.url = url.parse(value, false); testConfig.host = testConfig.url.hostname || testConfig.host; @@ -123,14 +131,8 @@ exports.get = function(option) { }; exports.process = function() { parser.parse(process.argv); - if ((testConfig.timeLimit == null) && (testConfig.numRequests == null)) { + if ((testConfig.timeLimit === undefined) && (testConfig.numRequests === undefined)) { testConfig.numRequests = testConfig.numClients; } }; -function help() { - sys.puts(parser); - process.exit(); -}; -exports.help = help; - 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/nl/optparse.js b/lib/nl/optparse.js new file mode 100644 index 0000000..37e3ee8 --- /dev/null +++ b/lib/nl/optparse.js @@ -0,0 +1,309 @@ +// Optparse.js 1.0.2 - Option Parser for Javascript +// +// Copyright (c) 2009 Johan Dahlberg +// +// See README.md for license. +// +var optparse = {}; +try{ optparse = exports } catch(e) {}; // Try to export the lib for node.js +(function(self) { +var VERSION = '1.0.2'; +var LONG_SWITCH_RE = /^--\w/; +var SHORT_SWITCH_RE = /^-\w/; +var NUMBER_RE = /^(0x[A-Fa-f0-9]+)|([0-9]+\.[0-9]+)|(\d+)$/; +var DATE_RE = /^\d{4}-(0[0-9]|1[0,1,2])-([0,1,2][0-9]|3[0,1])$/; +var EMAIL_RE = /^([0-9a-zA-Z]+([_.-]?[0-9a-zA-Z]+)*@[0-9a-zA-Z]+[0-9,a-z,A-Z,.,-]*(.){1}[a-zA-Z]{2,4})+$/; +var EXT_RULE_RE = /(\-\-[\w_-]+)\s+([\w\[\]_-]+)|(\-\-[\w_-]+)/; +var ARG_OPTIONAL_RE = /\[(.+)\]/; + +// The default switch argument filter to use, when argument name doesnt match +// any other names. +var DEFAULT_FILTER = '_DEFAULT'; +var PREDEFINED_FILTERS = {}; + +// The default switch argument filter. Parses the argument as text. +function filter_text(value) { + return value; +} + +// Switch argument filter that expects an integer, HEX or a decimal value. An +// exception is throwed if the criteria is not matched. +// Valid input formats are: 0xFFFFFFF, 12345 and 1234.1234 +function filter_number(value) { + var m = NUMBER_RE(value); + if(m == null) throw OptError('Expected a number representative'); + if(m[1]) { + // The number is in HEX format. Convert into a number, then return it + return parseInt(m[1], 16); + } else { + // The number is in regular- or decimal form. Just run in through + // the float caster. + return parseFloat(m[2] || m[3]); + } +}; + +// Switch argument filter that expects a Date expression. The date string MUST be +// formated as: "yyyy-mm-dd" An exception is throwed if the criteria is not +// matched. An DATE object is returned on success. +function filter_date(value) { + var m = DATE_RE(value); + if(m == null) throw OptError('Expected a date representation in the "yyyy-mm-dd" format.'); + return new Date(parseInt(m[0]), parseInt(m[1]), parseInt(m[2])); +}; + +// Switch argument filter that expects an email address. An exception is throwed +// if the criteria doesn`t match. +function filter_email(value) { + var m = EMAIL_RE(value); + if(m == null) throw OptError('Excpeted an email address.'); + return m[1]; +} + +// Register all predefined filters. This dict is used by each OptionParser +// instance, when parsing arguments. Custom filters can be added to the parser +// instance by calling the "add_filter" -method. +PREDEFINED_FILTERS[DEFAULT_FILTER] = filter_text; +PREDEFINED_FILTERS['TEXT'] = filter_text; +PREDEFINED_FILTERS['NUMBER'] = filter_number; +PREDEFINED_FILTERS['DATE'] = filter_date; +PREDEFINED_FILTERS['EMAIL'] = filter_email; + +// Buildes rules from a switches collection. The switches collection is defined +// when constructing a new OptionParser object. +function build_rules(filters, arr) { + var rules = []; + for(var i=0; i> value means that the switch does +// not take anargument. +function build_rule(filters, short, expr, desc) { + var optional, filter; + var m = expr.match(EXT_RULE_RE); + if(m == null) throw OptError('The switch is not well-formed.'); + var long = m[1] || m[3]; + if(m[2] != undefined) { + // A switch argument is expected. Check if the argument is optional, + // then find a filter that suites. + var optional_match = ARG_OPTIONAL_RE(m[2]); + var filter_name = optional_match === null ? m[2] : optional_match[1]; + optional = optional_match !== null; + filter = filters[filter_name]; + if(filter === undefined) filter = filters[DEFAULT_FILTER]; + } + return { + name: long.substr(2), + short: short, + long: long, + decl: expr, + desc: desc, + optional_arg: optional, + filter: filter + } +} + +// Loop's trough all elements of an array and check if there is valid +// options expression within. An valid option is a token that starts +// double dashes. E.G. --my_option +function contains_expr(arr) { + if(!arr || !arr.length) return false; + var l = arr.length; + while(l-- > 0) if(LONG_SWITCH_RE(arr[l])) return true; + return false; +} + +// Extends destination object with members of source object +function extend(dest, src) { + var result = dest; + for(var n in src) { + result[n] = src[n]; + } + return result; +} + +// Appends spaces to match specified number of chars +function spaces(arg1, arg2) { + var l, builder = []; + if(arg1.constructor === Number) { + l = arg1; + } else { + if(arg1.length == arg2) return arg1; + l = arg2 - arg1.length; + builder.push(arg1); + } + while(l-- > 0) builder.push(' '); + return builder.join(''); +} + +// Create a new Parser object that can be used to parse command line arguments. +// +// +function Parser(rules) { + return new OptionParser(rules); +} + +// Creates an error object with specified error message. +function OptError(msg) { + return new function() { + this.msg = msg; + this.toString = function() { + return this.msg; + } + } +} + +function OptionParser(rules) { + this.banner = 'Usage: [Options]'; + this.options_title = 'Available options:' + this._rules = rules; + this._halt = false; + this.filters = extend({}, PREDEFINED_FILTERS); + this.on_args = {}; + this.on_switches = {}; + this.on_halt = function() {}; + this.default_handler = function() {}; +} + +OptionParser.prototype = { + + // Adds args and switchs handler. + on: function(value, fn) { + if(value.constructor === Function ) { + this.default_handler = value; + } else if(value.constructor === Number) { + this.on_args[value] = fn; + } else { + this.on_switches[value] = fn; + } + }, + + // Adds a custom filter to the parser. It's possible to override the + // default filter by passing the value "_DEFAULT" to the ´´name´´ + // argument. The name of the filter is automatically transformed into + // upper case. + filter: function(name, fn) { + this.filters[name.toUpperCase()] = fn; + }, + + // Parses specified args. Returns remaining arguments. + parse: function(args) { + var result = [], callback; + var rules = build_rules(this.filters, this._rules); + var tokens = args.concat([]); + while((token = tokens.shift()) && this._halt == false) { + if(LONG_SWITCH_RE(token) || SHORT_SWITCH_RE(token)) { + var arg = undefined; + // The token is a long or a short switch. Get the corresponding + // rule, filter and handle it. Pass the switch to the default + // handler if no rule matched. + for(var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if(rule.long == token || rule.short == token) { + if(rule.filter !== undefined) { + arg = tokens.shift(); + if(!LONG_SWITCH_RE(arg) && !SHORT_SWITCH_RE(arg)) { + try { + arg = rule.filter(arg); + } catch(e) { + throw OptError(token + ': ' + e.toString()); + } + } else if(rule.optional_arg) { + tokens.unshift(arg); + } else { + throw OptError('Expected switch argument.'); + } + } + callback = this.on_switches[rule.name]; + if (!callback) callback = this.on_switches['*']; + if(callback) callback.apply(this, [rule.name, arg]); + break; + } + } + if(i == rules.length) this.default_handler.apply(this, [token]); + } else { + // Did not match long or short switch. Parse the token as a + // normal argument. + callback = this.on_args[result.length]; + result.push(token); + if(callback) callback.apply(this, [token]); + } + } + return this._halt ? this.on_halt.apply(this, []) : result; + }, + + // Returns an Array with all defined option rules + options: function() { + return build_rules(this.filters, this._rules); + }, + + // Add an on_halt callback if argument ´´fn´´ is specified. on_switch handlers can + // call instance.halt to abort the argument parsing. This can be useful when + // displaying help or version information. + halt: function(fn) { + this._halt = fn === undefined + if(fn) this.on_halt = fn; + }, + + // Returns a string representation of this OptionParser instance. + toString: function() { + var builder = [this.banner, '', this.options_title], + shorts = false, longest = 0, rule; + var rules = build_rules(this.filters, this._rules); + for(var i = 0; i < rules.length; i++) { + rule = rules[i]; + // Quick-analyze the options. + if(rule.short) shorts = true; + if(rule.decl.length > longest) longest = rule.decl.length; + } + for(var i = 0; i < rules.length; i++) { + var text; + rule = rules[i]; + if(shorts) { + if(rule.short) text = spaces(2) + rule.short + ', '; + else text = spaces(6); + } + text += spaces(rule.decl, longest) + spaces(3); + text += rule.desc; + builder.push(text); + } + return builder.join('\n'); + } +} + +self.VERSION = VERSION; +self.OptionParser = OptionParser; + +})(optparse); \ No newline at end of file diff --git a/lib/nodeload.js b/lib/nodeload.js deleted file mode 100755 index 1d9139d..0000000 --- a/lib/nodeload.js +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env node -/* - Copyright (c) 2010 Benjamin Schmaus - Copyright (c) 2010 Jonathan Lee - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ - -var options = require('./options'); -options.process(); - -if (!options.get('url')) - options.help(); - -var nl = require('../lib/nodeloadlib') - .quiet() - .setMonitorIntervalMs(options.get('reportInterval') * 1000); - -function puts(text) { if (!options.get('quiet')) console.log(text) } -function pad(str, width) { return str + (new Array(width-str.length)).join(" "); } -function printItem(name, val, padLength) { - if (padLength == undefined) padLength = 40; - puts(pad(name + ":", padLength) + " " + val); -} - -nl.TEST_MONITOR.on('start', function(tests) { testStart = new Date(); }); -nl.TEST_MONITOR.on('update', function(tests) { - puts(pad('Completed ' +tests[0].stats['result-codes'].cumulative.length+ ' requests', 40)); -}); -nl.TEST_MONITOR.on('end', function(tests) { - - var stats = tests[0].stats; - var elapsedSeconds = ((new Date()) - testStart)/1000; - - puts(''); - printItem('Server', options.get('host') + ":" + options.get('port')); - - if (options.get('requestGeneratorModule') == null) { - printItem('HTTP Method', options.get('method')) - printItem('Document Path', options.get('path')) - } else { - printItem('Request Generator', options.get('requestGeneratorModule')); - } - - printItem('Concurrency Level', options.get('numClients')); - printItem('Number of requests', stats['result-codes'].cumulative.length); - printItem('Body bytes transferred', stats['request-bytes'].cumulative.total + stats['response-bytes'].cumulative.total); - printItem('Elapsed time (s)', elapsedSeconds.toFixed(2)); - printItem('Requests per second', (stats['result-codes'].cumulative.length/elapsedSeconds).toFixed(2)); - printItem('Mean time per request (ms)', stats['latency'].cumulative.mean().toFixed(2)); - printItem('Time per request standard deviation', stats['latency'].cumulative.stddev().toFixed(2)); - - puts('\nPercentages of requests served within a certain time (ms)'); - printItem(" Min", stats['latency'].cumulative.min, 6); - printItem(" Avg", stats['latency'].cumulative.mean().toFixed(1), 6); - printItem(" 50%", stats['latency'].cumulative.percentile(.5), 6) - printItem(" 95%", stats['latency'].cumulative.percentile(.95), 6) - printItem(" 99%", stats['latency'].cumulative.percentile(.99), 6) - printItem(" Max", stats['latency'].cumulative.max, 6); -}); - -nl.runTest({ - name: options.get('host'), - host: options.get('host'), - port: options.get('port'), - requestGenerator: options.get('requestGenerator'), - method: options.get('method'), - path: options.get('path'), - requestData: options.get('requestData'), - numClients: options.get('numClients'), - numRequests: options.get('numRequests'), - timeLimit: options.get('timeLimit'), - targetRps: options.get('targetRps'), - stats: ['latency', 'result-codes', 'bytes'] -}); diff --git a/lib/nodeloadlib.js b/lib/nodeloadlib.js deleted file mode 100644 index e0909bc..0000000 --- a/lib/nodeloadlib.js +++ /dev/null @@ -1,149 +0,0 @@ -var sys=require('sys'),http=require('http'),fs=require('fs'),events=require('events'),querystring=require('querystring');var START=new Date().getTime();var qputs=exports.qputs=function(s){NODELOAD_CONFIG.QUIET||sys.puts(s);};var qprint=exports.qprint=function(s){NODELOAD_CONFIG.QUIET||sys.print(s);};var Utils=exports.Utils={uid:function(){this.lastUid=this.lastUid||0;return this.lastUid++;},defaults:function(obj,defaults){for(var i in defaults){if(obj[i]===undefined){obj[i]=defaults[i];}}},inherits:function(ctor,superCtor){var proto=ctor.prototype;sys.inherits(ctor,superCtor);for(var i in proto){ctor.prototype[i]=proto[i];}}};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.setSlavePingIntervalMs=function(milliseconds){NODELOAD_CONFIG.SLAVE_PING_INTERVAL_MS=milliseconds;} -var NODELOAD_CONFIG={QUIET:false,HTTP_ENABLED:true,HTTP_PORT:Number(process.env['HTTP_PORT'])||8000,MONITOR_INTERVAL_MS:2000,AJAX_REFRESH_INTERVAL_MS:2000,LOGS_ENABLED:true,SLAVE_PING_INTERVAL_MS:3000,eventEmitter:new events.EventEmitter(),on:function(event,fun){this.eventEmitter.on(event,fun);},apply:function(){this.eventEmitter.emit('apply');}} -process.nextTick(function(){NODELOAD_CONFIG.apply()});var TEST_DEFAULTS={name:'Debug test',host:'localhost',port:8080,requestGenerator:null,requestLoop:null,method:'GET',path:'/',requestData:null,numClients:10,numRequests:Infinity,timeLimit:120,targetRps:Infinity,delay:0,successCodes:null,stats:['latency','result-codes'],latencyConf:{percentiles:[0.95,0.99]}};var RAMP_DEFAULTS={test:null,numberOfSteps:10,timeLimit:10,rpsPerStep:10,clientsPerStep:1,delay:0};var addTest=exports.addTest=function(spec){Utils.defaults(spec,TEST_DEFAULTS);var req=function(client){if(spec.requestGenerator!==null){return spec.requestGenerator(client);} -return traceableRequest(client,spec.method,spec.path,{'host':spec.host},spec.requestData);},test={spec:spec,stats:{},jobs:[],fun:spec.requestLoop||LoopUtils.requestGeneratorLoop(req)};if(spec.stats.indexOf('latency')>=0){var l=new Reportable([Histogram,spec.latencyConf],spec.name+': Latency',true);test.fun=LoopUtils.monitorLatenciesLoop(l,test.fun);test.stats['latency']=l;} -if(spec.stats.indexOf('result-codes')>=0){var rc=new Reportable(ResultsCounter,spec.name+': Result codes',true);test.fun=LoopUtils.monitorResultsLoop(rc,test.fun);test.stats['result-codes']=rc;} -if(spec.stats.indexOf('concurrency')>=0){var conc=new Reportable(Peak,spec.name+': Concurrency',true);test.fun=LoopUtils.monitorConcurrencyLoop(conc,test.fun);test.stats['concurrency']=conc;} -if(spec.stats.indexOf('uniques')>=0){var uniq=new Reportable(Uniques,spec.name+': Uniques',false);test.fun=LoopUtils.monitorUniqueUrlsLoop(uniq,test.fun);test.stats['uniques']=uniq;} -if(spec.stats.indexOf('bytes')>=0){var reqbytes=new Reportable(Accumulator,spec.name+': Request Bytes',true);test.fun=LoopUtils.monitorByteSentLoop(reqbytes,test.fun);test.stats['request-bytes']=reqbytes;var resbytes=new Reportable(Accumulator,spec.name+': Response Bytes',true);test.fun=LoopUtils.monitorByteReceivedLoop(resbytes,test.fun);test.stats['response-bytes']=resbytes;} -if(spec.successCodes!==null){test.fun=LoopUtils.monitorHttpFailuresLoop(spec.successCodes,test.fun);} -test.jobs=SCHEDULER.schedule({fun:test.fun,argGenerator:function(){return http.createClient(spec.port,spec.host)},concurrency:spec.numClients,rps:spec.targetRps,duration:spec.timeLimit,numberOfTimes:spec.numRequests,delay:spec.delay});TEST_MONITOR.addTest(test);return test;};var addRamp=exports.addRamp=function(spec){Utils.defaults(spec,RAMP_DEFAULTS);var rampStep=LoopUtils.funLoop(function(){SCHEDULER.schedule({fun:spec.test.fun,argGenerator:function(){return http.createClient(spec.test.spec.port,spec.test.spec.host)},rps:spec.rpsPerStep,concurrency:spec.clientsPerStep,monitored:false})}),ramp={spec:spec,jobs:[],fun:rampStep};ramp.jobs=SCHEDULER.schedule({fun:rampStep,delay:spec.delay,duration:spec.timeLimit,rps:spec.numberOfSteps/spec.timeLimit,monitored:false});return ramp;};var startTests=exports.startTests=function(callback,stayAliveAfterDone){TEST_MONITOR.start();SCHEDULER.startAll(testsComplete(callback,stayAliveAfterDone));};var runTest=exports.runTest=function(spec,callback,stayAliveAfterDone){var t=addTest(spec);startTests(callback,stayAliveAfterDone);return t;};var traceableRequest=exports.traceableRequest=function(client,method,path,headers,body){headers=headers||{};body=body||'';headers['content-length']=headers['content-length']||body.length;var request=client.request(method,path,headers);request.headers=headers;request.path=path;request.body=body;request.write(body);return request;};function testsComplete(callback,stayAliveAfterDone){return function(){TEST_MONITOR.stop();callback&&callback();if(!stayAliveAfterDone&&!SLAVE_CONFIG){checkToExitProcess();}};} -function checkToExitProcess(){setTimeout(function(){if(!SCHEDULER.running){qputs('\nFinishing...');LOGS.close();HTTP_SERVER.stop();setTimeout(process.exit,500);}},3000);} -var ConditionalLoop=exports.ConditionalLoop=function(fun,args,conditions,delay){this.fun=fun;this.args=args;this.conditions=conditions||[];this.delay=delay;this.stopped=true;this.callback=null;} -ConditionalLoop.prototype={start:function(callback){this.callback=callback;this.stopped=false;if(this.delay&&this.delay>0){var loop=this;setTimeout(function(){loop.loop_()},this.delay*1000);}else{this.loop_();}},stop:function(){this.stopped=true;},checkConditions_:function(){return!this.stopped&&this.conditions.every(function(c){return c();});},loop_:function(){if(this.checkConditions_()){var loop=this;process.nextTick(function(){loop.fun(function(){loop.loop_()},loop.args)});}else{this.callback&&this.callback();}}} -var LoopConditions=exports.LoopConditions={timeLimit:function(seconds){var start=new Date();return function(){return(seconds===Infinity)||((new Date()-start)<(seconds*1000));};},maxExecutions:function(numberOfTimes){var counter=0;return function(){return(numberOfTimes===Infinity)||(counter++0){timeoutId=setTimeout(function(){timedOut=true;loopFun({req:request,res:{statusCode:0}});},request.timeout);} -request.on('response',function(response){if(!timedOut){if(timeoutId!==null){clearTimeout(timeoutId);} -loopFun({req:request,res:response});}});request.end();}}},monitorLatenciesLoop:function(latencies,fun){var start=function(){return new Date()} -var finish=function(result,start){latencies.put(new Date()-start)};return LoopUtils.loopWrapper(fun,start,finish);},monitorResultsLoop:function(results,fun){var finish=function(http){results.put(http.res.statusCode)};return LoopUtils.loopWrapper(fun,null,finish);},monitorByteReceivedLoop:function(bytesReceived,fun){var finish=function(http){http.res.on('data',function(chunk){bytesReceived.put(chunk.length);});};return LoopUtils.loopWrapper(fun,null,finish);},monitorByteSentLoop:function(bytesSent,fun){var finish=function(http){if(http.req.headers&&http.req.headers['content-length']){bytesSent.put(http.req.headers['content-length']);}};return LoopUtils.loopWrapper(fun,null,finish);},monitorConcurrencyLoop:function(concurrency,fun){var c=0;var start=function(){c++;};var finish=function(){concurrency.put(c--)};return LoopUtils.loopWrapper(fun,start,finish);},monitorRateLoop:function(rate,fun){var finish=function(){rate.put()};return LoopUtils.loopWrapper(fun,null,finish);},monitorHttpFailuresLoop:function(successCodes,fun,log){log=log||LOGS.ERROR_LOG;var finish=function(http){var body="";if(successCodes.indexOf(http.res.statusCode)<0){http.res.on('data',function(chunk){body+=chunk;});http.res.on('end',function(chunk){log.put(JSON.stringify({ts:new Date(),req:{headers:http.req._header,body:http.req.body,},res:{statusCode:http.res.statusCode,headers:http.res.headers,body:body}})+'\n');});}};return LoopUtils.loopWrapper(fun,null,finish);},monitorUniqueUrlsLoop:function(uniqs,fun){var finish=function(http){uniqs.put(http.req.path)};return LoopUtils.loopWrapper(fun,null,finish);}} -var JOB_DEFAULTS={fun:null,argGenerator:null,args:null,concurrency:1,rps:Infinity,duration:Infinity,numberOfTimes:Infinity,delay:0,monitored:true};var Scheduler=exports.Scheduler=function(){this.id=Utils.uid();this.jobs=[];this.running=false;this.callback=null;} -Scheduler.prototype={schedule:function(spec){Utils.defaults(spec,JOB_DEFAULTS);var scheduledJobs=[] -spec.numberOfTimes/=spec.concurrency;spec.rps/=spec.concurrency;for(var i=0;i0) -duration+=this.delay;conditions.push(LoopConditions.timeLimit(duration));} -this.args=this.argGenerator&&this.argGenerator();this.callback=callback;this.loop=new ConditionalLoop(fun,this.args,conditions,this.delay);this.loop.start(function(){job.done=true;if(job.callback){job.callback();}});this.started=true;},stop:function(){this.started=true;this.done=true;if(this.loop){this.loop.stop();}}} -function TestMonitor(intervalMs){events.EventEmitter.call(this);this.intervalMs=intervalMs||2000;this.tests=[];} -TestMonitor.prototype={addTest:function(test){this.tests.push(test);this.emit('test',test);},start:function(){this.emit('start',this.tests);monitor=this;process.nextTick(function(){SCHEDULER.schedule({fun:LoopUtils.funLoop(function(){monitor.update()}),rps:1000/monitor.intervalMs,delay:monitor.intervalMs/1000,monitored:false});});},update:function(){this.emit('update',this.tests);this.emit('afterUpdate',this.tests);},stop:function(){this.update();this.emit('end',this.tests);this.tests=[];}} -Utils.inherits(TestMonitor,events.EventEmitter);var TEST_MONITOR=exports.TEST_MONITOR=new TestMonitor();TEST_MONITOR.on('update',function(){qprint('.')});TEST_MONITOR.on('end',function(){qprint('done.')});NODELOAD_CONFIG.on('apply',function(){TEST_MONITOR.intervalMs=NODELOAD_CONFIG.MONITOR_INTERVAL_MS;});var remoteTest=exports.remoteTest=function(spec){return"(function() {\n"+" var remoteSpec = JSON.parse('"+JSON.stringify(spec)+"');\n"+" remoteSpec.requestGenerator = "+spec.requestGenerator+";\n"+" remoteSpec.requestLoop = "+spec.requestLoop+";\n"+" remoteSpec.reportFun = "+spec.reportFun+";\n"+" addTest(remoteSpec);\n"+"})();";} -var remoteStart=exports.remoteStart=function(master,slaves,tests,callback,stayAliveAfterDone){var remoteFun=tests.join('\n')+'\nstartTests();';remoteSubmit(master,slaves,remoteFun,callback,stayAliveAfterDone);} -var remoteStartFile=exports.remoteStartFile=function(master,slaves,filename,callback,stayAliveAfterDone){fs.readFile(filename,function(err,data){if(err!=null)throw err;data=data.toString().replace(/^#![^\n]+\n/,'// removed shebang directive from runnable script\n');remoteSubmit(master,slaves,data,callback,stayAliveAfterDone);});} -var SLAVE_CONFIG=null;var WORKER_POOL=null;var REMOTE_TESTS={};function remoteSubmit(master,slaves,fun,callback,stayAliveAfterDone){var finished=function(){SCHEDULER.stopAll();TEST_MONITOR.stop();callback&&callback();if(!stayAliveAfterDone&&!SLAVE_CONFIG){checkToExitProcess();}} -WORKER_POOL=new RemoteWorkerPool(master,slaves,fun);WORKER_POOL.start(finished,stayAliveAfterDone);TEST_MONITOR.start();SCHEDULER.startAll();} -function registerSlave(id,master){SLAVE_CONFIG=new RemoteSlave(id,master);TEST_MONITOR.on('test',function(test){SLAVE_CONFIG.addTest(test)});TEST_MONITOR.on('update',function(){SLAVE_CONFIG.reportProgress()});TEST_MONITOR.on('end',function(){SLAVE_CONFIG.clearTests()});} -function receiveTestCreate(report){if(WORKER_POOL.slaves[report.slaveId]===undefined){return;} -var localtest=REMOTE_TESTS[report.spec.name];if(localtest===undefined){localtest={spec:report.spec,stats:{},jobs:[],fun:null} -REMOTE_TESTS[report.spec.name]=localtest;TEST_MONITOR.addTest(localtest);}} -function receiveTestProgress(report){if(WORKER_POOL.slaves[report.slaveId]===undefined){return;} -WORKER_POOL.slaves[report.slaveId].state="running";for(var testname in report.data){var localtest=REMOTE_TESTS[testname];var remotetest=report.data[testname];if(localtest){for(var s in remotetest.stats){var remotestat=remotetest.stats[s];var localstat=localtest.stats[s];if(localstat===undefined){var backend=statsClassFromString(remotestat.interval.type);localstat=new Reportable([backend,remotestat.interval.params],remotestat.name,remotestat.trend);localtest.stats[s]=localstat;} -localstat.merge(remotestat.interval);}}else{qputs("WARN: received remote progress report from '"+report.slaveId+"' for unknown test: "+testname);}}} -function RemoteSlave(id,master){this.id=id;this.tests=[];if(master){master=master.split(':');this.master={host:master[0],port:master[1],client:http.createClient(master[1],master[0])};}} -RemoteSlave.prototype={addTest:function(test){this.tests.push(test);this.sendReport_('/remote/newTest',{slaveId:this.id,spec:test.spec});},clearTests:function(){this.tests=[];},reportProgress:function(){var reports={};for(var i in this.tests){var test=this.tests[i];var stats={};for(var s in test.stats){stats[s]={name:test.stats[s].name,trend:test.stats[s].trend,interval:test.stats[s].interval}} -reports[test.spec.name]={stats:stats};} -this.sendReport_('/remote/progress',{slaveId:this.id,data:reports});},sendReport_:function(url,object){if(this.master.client){var s=JSON.stringify(object);var req=this.master.client.request('POST',url,{'host':this.master.host,'content-length':s.length});req.write(s);req.end();}}} -function RemoteWorkerPool(master,slaves,fun){this.master=master;this.slaves={};this.fun=fun;this.callback=null;this.pingId=null;this.progressId=null;for(var i in slaves){var slave=slaves[i].split(":");this.slaves[slaves[i]]={id:slaves[i],state:"notstarted",host:slave[0],port:slave[1],client:http.createClient(slave[1],slave[0])};}} -RemoteWorkerPool.prototype={start:function(callback,stayAliveAfterDone){var fun="(function() {"+this.fun+"})();";for(var i in this.slaves){var slave=this.slaves[i],slaveFun='';if(this.master){slaveFun="registerSlave('"+i+"','"+this.master+"');\n"+fun;}else{slaveFun="registerSlave('"+i+"');\n"+fun;} -var r=slave.client.request('POST','/remote',{'host':slave.host,'content-length':slaveFun.length});r.write(slaveFun);r.end();slave.state="running";} -var worker=this;this.pingId=setInterval(function(){worker.sendPings()},NODELOAD_CONFIG.SLAVE_PING_INTERVAL_MS);this.callback=callback;},checkFinished_:function(){for(var i in this.slaves){if(this.slaves[i].state!="done"&&this.slaves[i].state!="error"){return;}} -qprint("\nRemote tests complete.");var callback=this.callback;clearInterval(this.pingId);this.callback=null;this.slaves={};callback&&callback();},sendPings:function(){var worker=this;var pong=function(slave){return function(response){if(slave.state=="ping"){if(response.statusCode==200){slave.state="running";}else if(response.statusCode==410){qprint("\n"+slave.id+" done.");slave.state="done";}}}} -var ping=function(slave){slave.state="ping";var r=slave.client.request('GET','/remote/state',{'host':slave.host,'content-length':0});r.on('response',pong(slave));r.end();} -for(var i in this.slaves){if(this.slaves[i].state=="ping"){qprint("\nWARN: slave "+i+" unresponsive.");this.slaves[i].state="error";}else if(this.slaves[i].state=="running"){ping(this.slaves[i]);}} -this.checkFinished_();}} -function serveRemote(url,req,res){var readBody=function(req,callback){var body='';req.on('data',function(chunk){body+=chunk});req.on('end',function(){callback(body)});} -var sendStatus=function(status){res.writeHead(status,{"Content-Length":0});res.end();} -if(req.method=="POST"&&url=="/remote"){readBody(req,function(remoteFun){qputs("\nReceived remote command:\n"+remoteFun);eval(remoteFun);sendStatus(200);});}else if(req.method=="GET"&&req.url=="/remote/hosts"){var hosts=[];if(SLAVE_CONFIG){hosts.push(SLAVE_CONFIG.master.host+':'+SLAVE_CONFIG.master.port);} -if(WORKER_POOL){hosts.push(WORKER_POOL.master);for(var i in WORKER_POOL.slaves){hosts.push(i);}} -var body=JSON.stringify(hosts);res.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Content-Length":body.length});res.write(body);res.end();}else if(req.method=="GET"&&req.url=="/remote/state"){if(SCHEDULER.running==true){sendStatus(200);}else{sendStatus(410);} -res.end();}else if(req.method=="POST"&&url=="/remote/newTest"){readBody(req,function(data){receiveTestCreate(JSON.parse(data));sendStatus(200);});}else if(req.method=="POST"&&url=="/remote/progress"){readBody(req,function(data){receiveTestProgress(JSON.parse(data));sendStatus(200);});}else{sendStatus(405);}} -var Histogram=exports.Histogram=function(params){var numBuckets=3000;var percentiles=[0.95,0.99];if(params!=null&¶ms.numBuckets!=null) -numBuckets=params.buckets;if(params!=null&¶ms.percentiles!=null) -percentiles=params.percentiles;this.type="Histogram";this.params=params;this.size=numBuckets;this.percentiles=percentiles;this.clear();} -Histogram.prototype={clear:function(){this.start=new Date();this.length=0;this.sum=0;this.min=-1;this.max=-1;this.items=new Array(this.size);this.extra=[];this.sorted=true;},put:function(item){this.length++;this.sum+=item;if(itemthis.max||this.max==-1)this.max=item;if(itemtarget){var idx=this.extra.length-target;if(!this.sorted){this.extra=this.extra.sort(function(a,b){return a-b});this.sorted=true;} -return this.extra[idx];}else{var sum=this.extra.length;for(var i=this.items.length-1;i>=0;i--){if(this.items[i]!=null){sum+=this.items[i];if(sum>=target){return i;}}} -return 0;}},stddev:function(){var mean=this.mean();var s=0;for(var i=0;ithis.max||this.max==-1)?other.max:this.max;for(var i=0;i0){var total=0;for(var i in item){total+=this.items[i];} -return total;}else{return this.items[item];}},clear:function(){this.start=new Date();this.items={};this.length=0;},summary:function(){this.items.total=this.length;this.items.rps=Number((this.length/((new Date()-this.start)/1000)).toFixed(1));return this.items;},merge:function(other){for(var i in other.items){if(this.items[i]!=null){this.items[i]+=other.items[i];}else{this.items[i]=other.items[i];}} -this.length+=other.length;}} -var Uniques=exports.Uniques=function(){this.type="Uniques";this.start=new Date();this.items={};this.uniques=0;this.length=0;} -Uniques.prototype={put:function(item){if(this.items[item]!=null){this.items[item]++;}else{this.items[item]=1;this.uniques++} -this.length++;},get:function(){return this.uniques;},clear:function(){this.items={};this.unqiues=0;this.length=0;},summary:function(){return{total:this.length,uniqs:this.uniques};},merge:function(other){for(var i in other.items){if(this.items[i]!=null){this.items[i]+=other.items[i];}else{this.items[i]=other.items[i];this.uniques++;}} -this.length+=other.length;}} -var Peak=exports.Peak=function(){this.type="Peak";this.peak=0;this.length=0;} -Peak.prototype={put:function(item){if(this.peak0){this.interval.clear();} -this.lastSummary=null;},summary:function(){if(this.lastSummary){return this.lastSummary} -return{interval:this.interval.summary(),cumulative:this.cumulative.summary()};},merge:function(other){this.interval.merge(other);this.cumulative.merge(other);}} -var roundRobin=exports.roundRobin=function(list){r=list.slice();r.rridx=-1;r.get=function(){this.rridx=(this.rridx+1)%this.length;return this[this.rridx];} -return r;} -var randomString=exports.randomString=function(length){var s="";for(var i=0;i=1){z0=2*Math.random()-1;z1=2*Math.random()-1;s=z0*z0+z1*z1;} -return z0*Math.sqrt(-2*Math.log(s)/s)*stddev+mean;} -var nextPareto=exports.nextPareto=function(min,max,shape){if(shape==null)shape=0.1;var l=1,h=Math.pow(1+max-min,shape),rnd=Math.random();while(rnd==0)rnd=Math.random();return Math.pow((rnd*(h-l)-h)/-(h*l),-1/shape)-1+min;} -function statsClassFromString(name){types={"Histogram":Histogram,"Accumulator":Accumulator,"ResultsCounter":ResultsCounter,"Uniques":Uniques,"Peak":Peak,"Rate":Rate,"LogFile":LogFile,"NullLog":NullLog,"Reportable":Reportable};return types[name];} -var STATS_MANAGER={statsSets:[],addStatsSet:function(stats){this.statsSets.push(stats);},logStats:function(){var out='{"ts": '+JSON.stringify(new Date());this.statsSets.forEach(function(statsSet){for(var i in statsSet){var stat=statsSet[i];out+=', "'+stat.name+'": '+JSON.stringify(stat.summary().interval);}});out+="}";LOGS.STATS_LOG.put(out+",\n");},prepareNextInterval:function(){this.statsSets.forEach(function(statsSet){for(var i in statsSet){statsSet[i].next();}});},reset:function(){this.statsSets=[];}} -TEST_MONITOR.on('test',function(test){if(test.stats)STATS_MANAGER.addStatsSet(test.stats)});TEST_MONITOR.on('update',function(){STATS_MANAGER.logStats()});TEST_MONITOR.on('afterUpdate',function(){STATS_MANAGER.prepareNextInterval()}) -TEST_MONITOR.on('end',function(){STATS_MANAGER.reset()});var LOGS=exports.LOGS={opened:false,STATS_LOG:new NullLog(),ERROR_LOG:new NullLog(),SUMMARY_HTML:new NullLog(),open:function(){if(this.opened){return};qputs("Opening log files.");this.STATS_LOG=new LogFile('results-'+START+'-stats.log');this.ERROR_LOG=new LogFile('results-'+START+'-err.log');this.SUMMARY_HTML=new LogFile('results-'+START+'-summary.html');this.STATS_LOG.put("[");},close:function(){this.STATS_LOG.put("]");this.STATS_LOG.close();this.ERROR_LOG.close();this.SUMMARY_HTML.close();if(this.opened){qputs("Closed log files.");} -this.opened=false;}} -NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.LOGS_ENABLED){LOGS.open();}});var Report=exports.Report=function(name,updater){this.name=name;this.uid=Utils.uid();this.summary={};this.charts={};this.updater=updater;} -Report.prototype={getChart:function(name){if(this.charts[name]==null) -this.charts[name]=new Chart(name);return this.charts[name];},update:function(){if(this.updater!=null){this.updater(this);}}} -var Chart=exports.Chart=function(name){this.name=name;this.uid=Utils.uid();this.columns=["time"];this.rows=[[timeFromTestStart()]];} -Chart.prototype={put:function(data){var row=[timeFromTestStart()];for(item in data){var col=this.columns.indexOf(item);if(col<0){col=this.columns.length;this.columns.push(item);this.rows[0].push(0);} -row[col]=data[item];} -this.rows.push(row);}} -var REPORT_MANAGER=exports.REPORT_MANAGER={reports:{},addReport:function(report){this.reports[report.name]=report;},getReport:function(name){return this.reports[name];},updateReports:function(){for(var r in this.reports){this.reports[r].update();} -LOGS.SUMMARY_HTML.clear(REPORT_MANAGER.getHtml());},reset:function(){this.reports={};},getHtml:function(){var t=template.create(REPORT_SUMMARY_TEMPLATE);return t({querystring:querystring,refreshPeriodMs:NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS,reports:this.reports});}} -function timeFromTestStart(){return(Math.floor((new Date().getTime()-START)/600)/100);} -function updateReportFromStats(stats){return function(report){for(var s in stats){var stat=stats[s];var summary=stat.summary();if(stat.trend){report.getChart(stat.name).put(summary.interval);} -for(var i in summary.cumulative){report.summary[stat.name+" "+i]=summary.cumulative[i];}}}} -function getChartAsJson(chart){return(chart==null)?null:JSON.stringify(chart.rows);} -function serveReport(url,req,res){if(req.method=="GET"&&url=="/"){var html=REPORT_MANAGER.getHtml();res.writeHead(200,{"Content-Type":"text/html","Content-Length":html.length});res.write(html);}else if(req.method=="GET"&&req.url.match("^/data/([^/]+)/([^/]+)")){var urlparts=querystring.unescape(req.url).split("/"),report=REPORT_MANAGER.getReport(urlparts[2]),retobj=null;if(report){var chartname=urlparts[3];if(chartname=="summary"){retobj=report.summary;}else if(report.charts[chartname]!=null){retobj=report.charts[chartname].rows;}} -if(retobj){var json=JSON.stringify(retobj);res.writeHead(200,{"Content-Type":"application/json","Content-Length":json.length});res.write(json);}else{res.writeHead(404,{"Content-Type":"text/html","Content-Length":0});}}else if(req.method=="GET"&&url=="/data/"){var json=JSON.stringify(REPORT_MANAGER.reports);res.writeHead(200,{"Access-Control-Allow-Origin":"*","Content-Type":"application/json","Content-Length":json.length});res.write(json);}else{res.writeHead(405,{"Content-Length":0});} -res.end();} -TEST_MONITOR.on('update',function(){REPORT_MANAGER.updateReports()});TEST_MONITOR.on('end',function(){for(var r in REPORT_MANAGER.reports){REPORT_MANAGER.reports[r].updater=null;}});TEST_MONITOR.on('test',function(test){if(test.stats){REPORT_MANAGER.addReport(new Report(test.spec.name,updateReportFromStats(test.stats)))}});var HTTP_SERVER=exports.HTTP_SERVER={server:null,start:function(port){if(this.server){return};var that=this;this.server=http.createServer(function(req,res){that.route_(req,res)});this.server.listen(port);qputs('Started HTTP server on port '+port+'.');},stop:function(){if(!this.server){return};this.server.close();this.server=null;qputs('Shutdown HTTP server.');},route_:function(req,res){if(req.url=="/"||req.url.match("^/data/")){serveReport(req.url,req,res)}else if(req.url.match("^/remote")){serveRemote(req.url,req,res);}else if(req.method=="GET"){this.serveFile_("."+req.url,res);}else{res.writeHead(405,{"Content-Length":"0"});res.end();}},serveFile_:function(file,response){fs.stat(file,function(err,stat){if(err!=null){response.writeHead(404,{"Content-Type":"text/plain"});response.write("Cannot find file: "+file);response.end();return;} -fs.readFile(file,"binary",function(err,data){if(err){response.writeHead(500,{"Content-Type":"text/plain"});response.write("Error opening file "+file+": "+err);}else{response.writeHead(200,{'Content-Length':data.length});response.write(data,"binary");} -response.end();});});}} -NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);}});var REPORT_SUMMARY_TEMPLATE='\n \n Test Results\n \n \n \n\n \n \n
\n
\n <% for (var i in reports) { %>\n <% for (var j in reports[i].charts) { %>\n <% var chart = reports[i].charts[j]; %>\n

<%=chart.name%>

\n
\n
\n
\n
\n
\n <% } %>\n <% } %>\n
\n \n
\n \n \n \n\n \n \n\n' -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};';template={cache:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)) -if(!callback) -fn=create(fs.readFileSync(str).toString('utf8'));else{fs.readFile(str,function(err,buffer){if(err)throw err;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;}} diff --git a/lib/options.js b/lib/options.js deleted file mode 100644 index 368a82b..0000000 --- a/lib/options.js +++ /dev/null @@ -1,136 +0,0 @@ -/* - Copyright (c) 2010 Benjamin Schmaus - Copyright (c) 2010 Jonathan Lee - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -*/ - -var sys = require('sys'); -var url = require('url'); -var path = require('path'); -var optparse = require('../deps/optparse-js/lib/optparse'); - -// Default options -var testConfig = { - url: null, - method: 'GET', - requestData: null, - host: '', - port: 80, - numClients: 1, - numRequests: Infinity, - timeLimit: Infinity, - targetRps: Infinity, - path: '/', - requestGenerator: null, - reportInterval: 10, -}; -var switches = [ - [ '-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.' ], - [ '-r', '--request-generator STRING', 'Path to module that exports getRequest function'], - [ '-i', '--report-interval NUMBER', 'Frequency in seconds to report statistics. Default is 10.'], - [ '-q', '--quiet', 'Supress display of progress count info.'], - [ '-h', '--help', 'Show usage info' ], -]; - -// Create a new OptionParser. -var parser = new optparse.OptionParser(switches); -parser.banner = 'nodeload.js [options] :[]'; -parser.on('help', function() { - help(); -}); - -parser.on(2, function (value) { - if (value.search('^http://') == -1) - value = 'http://' + value; - - testConfig.url = url.parse(value, false); - testConfig.host = testConfig.url.hostname || testConfig.host; - testConfig.port = Number(testConfig.url.port) || testConfig.port; - testConfig.path = testConfig.url.pathname || testConfig.path; -}); - -parser.on( - "quiet", function() { - testConfig.quiet = true; - } -); - -parser.on( - "data", function(opt, value) { - testConfig.requestData = value; - } -); - -parser.on('request-generator', function(opt, value) { - var moduleName = value.substring(0, value.lastIndexOf('.')); - testConfig.requestGeneratorModule = value; - testConfig.requestGenerator = require(moduleName).getRequest; -}); - -parser.on('report-interval', function(opt, value) { - testConfig.reportInterval = Number(value); -}); - -parser.on('concurrency', function(opt, value) { - testConfig.numClients = Number(value); -}); - -parser.on('request-rate', function(opt, value) { - testConfig.targetRps = Number(value); -}); - -parser.on('number', function(opt, value) { - testConfig.numRequests = Number(value); -}); - -parser.on( - 'time-limit', function(opt, value) { - testConfig.timeLimit = Number(value); - } -); - -parser.on('method', function(opt, value) { - testConfig.method = value; -}); - -exports.get = function(option) { - return testConfig[option]; -}; -exports.process = function() { - parser.parse(process.argv); - if ((testConfig.timeLimit == null) && (testConfig.numRequests == null)) { - testConfig.numRequests = testConfig.numClients; - } -}; - -function help() { - sys.puts(parser); - process.exit(); -}; -exports.help = help; - diff --git a/lib/remote/cluster.js b/lib/remote/cluster.js new file mode 100644 index 0000000..da6191d --- /dev/null +++ b/lib/remote/cluster.js @@ -0,0 +1,195 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var Endpoint = require('./endpoint').Endpoint; +var EventEmitter = require('events').EventEmitter; +var SlaveNode = require('./slavenode').SlaveNode; +var Slaves = require('./slaves').Slaves; +var qputs = util.qputs; +var HTTP_SERVER = require('../http').HTTP_SERVER; +var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; +} + +/** Main interface for creating a distributed nodeload cluster. Spec: +{ + master: { + host: 'host' or 'host:port' or undefined to extract from HttpServer + master_remote_function_1: function(slaves, slaveId, args...) { ... }, + }, + slaves: { + host: ['host:port', ...], + setup: function(master) { ... } + slave_remote_function_1: function(master, args...) { ... } + }, + pingInterval: 2000, + server: HttpServer instance (defaults to global HTTP_SERVER) +} + +Calling cluster.start() will register a master handler on the provided http.js#HttpServer. It will +connect to every slave, asking each slave to 1) execute the setup() function, 2) report its current +state to this host every pingInterval milliseconds. Calling cluster.slave_remote_function_1(), will +execute slave_remote_function_1 on every slave. + +Cluster emits the following events: + +- 'init': emitted when the cluster.start() can be called (the underlying HTTP server has been started). +- 'start': when connections to all the slave instances have been established +- 'end': when all the slaves have been terminated (e.g. by calling cluster.end()). The endpoint + installed in the underlying HTTP server has been removed. +- 'slaveError', slave, Error: The connection to the slave experienced an error. If error is null, the + slave has failed to send its state in the last 4 pingInterval periods. It should be considered + unresponsive. +- 'slaveError', slave, http.ClientResponse: A method call to this slave returned this non-200 response. +- 'running', 'done': when all the slaves that are not in an error state (haven't responded in the last 4 + pingIntervals) report that they are in a 'running' or 'done' state. To set a slave's the state, + install a slave function: + + cluster = new Cluster({ + slaves: { + slave_remote_function: function(master) { this.state = 'running'; } + }, + ... + }); + + and call it + + cluster.slave_remote_function(); + +Cluster.state can be: +- 'initializing': The cluster cannot be started yet -- it is waiting for the HTTP server to start. +- 'initialized': The cluster can be started. +- 'started': Connections to all the slaves have been established and the master endpoint is created. +- 'stopping': Attempting to terminate all slaves. +- 'stopped': All of the slaves have been properly shutdown and the master endpoint removed. +*/ +var Cluster = exports.Cluster = function Cluster(spec) { + EventEmitter.call(this); + util.PeriodicUpdater.call(this); + + var self = this, + masterSpec = spec.master || {}, + slavesSpec = spec.slaves || { hosts:[] }, + masterHost = spec.master && spec.master.host || 'localhost'; + + self.pingInterval = spec.pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; + self.server = spec.server || HTTP_SERVER; + self.masterEndpoint = new Endpoint(self.server, masterHost); + self.slaves = new Slaves(self.masterEndpoint, self.pingInterval); + self.slaveState_ = {}; + + // Define all master methods on the local endpoint + self.masterEndpoint.setStaticParams([self.slaves]); // 1st param to all master functions is slaves. 2nd will be slave id, which SlaveNode prepends to all requests. + self.masterEndpoint.defineMethod('updateSlaveState_', self.updateSlaveState_.bind(self)); // updateSlaveState_ is on every master and called by SlaveNode.update() to periodically send its state to the master. + util.forEach(masterSpec, function(method, val) { + if (typeof val === 'function') { + self.masterEndpoint.defineMethod(method, val); + } + }); + + // Send all slave methods definitions to the remote instances + slavesSpec.hosts.forEach(function(h) { self.slaves.add(h); }); + util.forEach(spec.slaves, function(method, val) { + if (typeof val === 'function') { + self.slaves.defineMethod(method, val); + self[method] = function() { self.slaves[method].apply(self.slaves, arguments); }; + } + }); + + // Store some other extra state for each slave so we can detect state changes and unresponsiveness + self.slaves.slaves.forEach(function(s) { + if (!self.slaveState_[s.id]) { + self.slaveState_[s.id] = { alive: true, aliveSinceLastCheck: false }; + } + }); + + // Cluster is started when slaves are alive, and ends when slaves are all shutdown + self.slaves.on('start', function() { + self.state = 'started'; + self.emit('start'); + }); + self.slaves.on('end', function() { + self.masterEndpoint.end(); + self.state = 'stopped'; + self.emit('end'); + }); + self.slaves.on('slaveError', function(slave, err) { + self.emit('slaveError', slave, err); + }); + + // Cluster is initialized (can be started) once server is started + if (self.server.running) { + self.state = 'initialized'; + process.nextTick(function() { self.emit('init'); }); + } else { + self.state = 'initializing'; + self.server.on('start', function() { + self.state = 'initialized'; + self.emit('init'); + }); + } +}; +util.inherits(Cluster, EventEmitter); +Cluster.prototype.started = function() { return this.state === 'started'; }; +/** Start cluster; install a route on the local HTTP server and send the slave definition to all the +slave instances. */ +Cluster.prototype.start = function() { + if (!this.server.running) { + throw new Error('A Cluster can only be started after it has emitted \'init\'.'); + } + this.masterEndpoint.start(); + this.slaves.start(); + this.updateInterval = this.pingInterval * 4; // call update() every 4 ping intervals to check for slave aliveness + // this.slaves 'start' event handler emits 'start' and updates state +}; +/** Stop the cluster; remove the route from the local HTTP server and uninstall and disconnect from all +the slave instances */ +Cluster.prototype.end = function() { + this.state = 'stopping'; + this.updateInterval = 0; + this.slaves.end(); + // this.slaves 'end' event handler emits 'end', destroys masterEndpoint & updates state +}; +/** Check for unresponsive slaves that haven't called updateSlaveState_ in the last 4 update intervals */ +Cluster.prototype.update = function() { + var self = this; + util.forEach(self.slaveState_, function(id, s) { + if (!s.aliveSinceLastCheck && s.alive) { + // this node has not sent us its state in the last four spec.pingInterval intervals -- mark as dead + s.alive = false; + self.emit('slaveError', self.slaves[id], null); + } else if (s.aliveSinceLastCheck) { + s.aliveSinceLastCheck = false; + s.alive = true; + } + }); +}; +/** Receive a periodic state update message from a slave. When all slaves enter the 'running' or 'done' +states, emit an event. */ +Cluster.prototype.updateSlaveState_ = function(slaves, slaveId, state) { + var slave = slaves[slaveId]; + if (slave) { + var previousState = this.slaveState_[slaveId].state; + this.slaveState_[slaveId].state = state; + this.slaveState_[slaveId].aliveSinceLastCheck = true; + if (previousState !== state) { + this.emit('slaveState', slave, state); + + if (state === 'running' || state === 'done') { + this.emitWhenAllSlavesInState_(state); + } + } + } else { + qputs('WARN: ignoring message from unexpected slave instance ' + slaveId); + } +}; +Cluster.prototype.emitWhenAllSlavesInState_ = function(state) { + var allSlavesInSameState = true; + util.forEach(this.slaveState_, function(id, s) { + if (s.state !== state && s.alive) { + allSlavesInSameState = false; + } + }); + if (allSlavesInSameState) { + this.emit(state); + } +}; \ No newline at end of file diff --git a/lib/remote/endpoint.js b/lib/remote/endpoint.js new file mode 100644 index 0000000..cd7d78c --- /dev/null +++ b/lib/remote/endpoint.js @@ -0,0 +1,154 @@ +/*jslint sub: true */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var url = require('url'); +var util = require('../util'); +var EventEmitter = require('events').EventEmitter; +} + +/** Endpoint represents an a collection of functions that can be executed by POSTing parameters to an +HTTP server. + +When Endpoint is started it adds the a unique route, /remote/{uid}/{method}, to server. +When a POST request is received, it calls method() with the request body as it's parameters. + +The available methods for this endpoint are defined by calling defineMethod(...). + +Endpoint emits the following events: +- 'start': A route has been installed on the HTTP server and setup(), if defined through defineMethod(), + has been called +- 'end': The route has been removed. No more defined methods will be called. + +Endpoint.state can be: +- 'initialized': This endpoint is ready to be started. +- 'started': This endpoint is listening for POST requests to dispatching to the corresponding methods +*/ +var Endpoint = exports.Endpoint = function Endpoint(server, hostAndPort) { + EventEmitter.call(this); + + var self = this, + parts = hostAndPort ? hostAndPort.split(':') : []; + + self.id = util.uid(); + self.server = server; + self.methodNames = []; + self.methods = {}; + self.setStaticParams([]); + self.state = 'initialized'; + self.__defineGetter__('url', function() { return self.url_; }); + + self.hostname_ = parts[0]; + self.port_ = parts[1]; + self.basepath_ = '/remote/' + self.id; + self.handler_ = self.handle.bind(self); +}; + +util.inherits(Endpoint, EventEmitter); + +/** Set values that are passed as the initial arguments to every handler method. For example, if you: + + var id = 123, name = 'myobject'; + endpoint.setStaticParams([id, name]); + +You should define methods: + + endpoint.defineMethod('method_1', function(id, name, arg1, arg2...) {...}); + +which are called by: + + endpoint.method_1(arg1, arg2...) + +*/ +Endpoint.prototype.setStaticParams = function(params) { + this.staticParams_ = params instanceof Array ? params : [params]; +}; + +/** Define a method that can be executed by POSTing to /basepath/method-name. For example: + + endpoint.defineMethod('method_1', function(data) { return data; }); + +then POSTing '[123]' to /{basepath}/method_1 will respond with a message with body 123. + +*/ +Endpoint.prototype.defineMethod = function(name, fun) { + this.methodNames.push(name); + this.methods[name] = fun; +}; + +/** Start responding to requests to this endpoint by adding the proper route to the HTTP server*/ +Endpoint.prototype.start = function() { + if (this.state !== 'initialized') { return; } + this.url_ = url.format({ + protocol: 'http', + hostname: this.hostname_ || this.server.hostname, + port: this.port_ || this.server.port, + pathname: this.basepath_ + }); + this.route_ = '^' + this.basepath_ + '/?'; + this.server.addRoute(this.route_, this.handler_); + this.context = {}; + if (this.methods['setup']) { + this.methods['setup'].apply(this.context, this.staticParams_); + } + this.state = 'started'; + this.emit('start'); +}; + +/** Remove the HTTP server route and stop responding to requests */ +Endpoint.prototype.end = function() { + if (this.state !== 'started') { return; } + this.server.removeRoute(this.route_, this.handler_); + this.state = 'initialized'; + this.emit('end'); +}; + +/** The main HTTP request handler. On DELETE /{basepath}, it will self-destruct this endpoint. POST +requests are routed to the function set by defineMethod(), applying the HTTP request body as parameters, +and sending return value back in the HTTP response. */ +Endpoint.prototype.handle = function(path, req, res) { + var self = this; + if (path === self.basepath_) { + if (req.method === 'DELETE') { + self.end(); + res.writeHead(204, {'Content-Length': 0}); + res.end(); + } else { + res.writeHead(405); + res.end(); + } + } else if (req.method === 'POST') { + var method = path.slice(this.basepath_.length+1); + if (self.methods[method]) { + util.readStream(req, function(params) { + var status = 200, ret; + + try { + params = JSON.parse(params); + } catch(e1) { + res.writeHead(400); + res.end(); + return; + } + + params = (params instanceof Array) ? params : [params]; + ret = self.methods[method].apply(self.context, self.staticParams_.concat(params)); + + try { + ret = (ret === undefined) ? '' : JSON.stringify(ret); + } catch(e2) { + ret = e2.toString(); + status = 500; + } + + res.writeHead(status, {'Content-Length': ret.length, 'Content-Type': 'application/json'}); + res.end(ret); + }); + } else { + res.writeHead(404); + res.end(); + } + } else { + res.writeHead(405); + res.end(); + } +}; \ No newline at end of file diff --git a/lib/remote/endpointclient.js b/lib/remote/endpointclient.js new file mode 100644 index 0000000..6b0f998 --- /dev/null +++ b/lib/remote/endpointclient.js @@ -0,0 +1,72 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var http = require('http'); +var util = require('../util'); +var EventEmitter = require('events').EventEmitter; +var qputs = util.qputs; +} + +var DEFAULT_RETRY_INTERVAL_MS = 2000; + +/** EndpointClient represents an HTTP connection to an Endpoint. The supported methods should be added +by calling defineMethod(...). For example, + + client = new EndpointClient('myserver', 8000, '/remote/0'); + client.defineMethod('method_1'); + client.on('connect', function() { + client.method_1(args); + }); + +will send a POST request to http://myserver:8000/remote/0/method_1 with the body [args], which causes +the Endpoint listening on myserver to execute method_1(args). + +EndpointClient emits the following events: +- 'connect': An HTTP connection to the remote endpoint has been established. Methods may now be called. +- 'clientError', Error: The underlying HTTP connection returned an error. The connection will be retried. +- 'clientError', http.ClientResponse: A call to a method on the endpoint returned this non-200 response. +- 'end': The underlying HTTP connect has been terminated. No more events will be emitted. +*/ +var EndpointClient = exports.EndpointClient = function EndpointClient(host, port, basepath) { + EventEmitter.call(this); + this.host = host; + this.port = port; + this.client = util.createReconnectingClient(port, host); + this.client.on('error', this.emit.bind(this, 'error')); + this.basepath = basepath || ''; + this.methodNames = []; + this.retryInterval = DEFAULT_RETRY_INTERVAL_MS; + this.setStaticParams([]); +}; +util.inherits(EndpointClient, EventEmitter); +/** Terminate the HTTP connection. */ +EndpointClient.prototype.destroy = function() { + this.client.destroy(); + this.emit('end'); +}; +/** Send an arbitrary HTTP request using the underlying http.Client. */ +EndpointClient.prototype.rawRequest = function() { + return this.client.request.apply(this.client, arguments); +}; +EndpointClient.prototype.setStaticParams = function(params) { + this.staticParams_ = params instanceof Array ? params : [params]; +}; +/** Add a method that the target server understands. The method can be executed by calling +endpointClient.method(args...). */ +EndpointClient.prototype.defineMethod = function(name) { + var self = this; + self[name] = function() { + var req = self.client.request('POST', self.basepath + '/' + name), + params = self.staticParams_.concat(util.argarray(arguments)); + + req.on('response', function(res) { + if (res.statusCode !== 200) { + self.emit('clientError', res); + } + }); + req.end(JSON.stringify(params)); + + return req; + }; + self.methodNames.push(name); + return self; +}; \ No newline at end of file diff --git a/lib/remote/httphandler.js b/lib/remote/httphandler.js new file mode 100644 index 0000000..ab379b9 --- /dev/null +++ b/lib/remote/httphandler.js @@ -0,0 +1,8 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var installRemoteHandler = require('./slavenode').installRemoteHandler; +var HTTP_SERVER = require('../http').HTTP_SERVER; +} + +// Install the handler for /remote for the global HTTP server +installRemoteHandler(HTTP_SERVER); \ No newline at end of file diff --git a/lib/remote/index.js b/lib/remote/index.js new file mode 100644 index 0000000..0fde3c3 --- /dev/null +++ b/lib/remote/index.js @@ -0,0 +1,12 @@ +var slave = require('./slave'); +var slavenode = require('./slavenode'); +exports.Cluster = require('./cluster').Cluster; +exports.LoadTestCluster = require('./remotetesting').LoadTestCluster; +exports.Slaves = slave.Slaves; +exports.Slave = slave.Slave; +exports.SlaveNode = slavenode.SlaveNode; +exports.installRemoteHandler = slavenode.installRemoteHandler; +exports.Endpoint = require('./endpoint').Endpoint; +exports.EndpointClient = require('./endpointclient').EndpointClient; + +require('./httphandler'); \ No newline at end of file diff --git a/lib/remote/remotetesting.js b/lib/remote/remotetesting.js new file mode 100644 index 0000000..332f9ac --- /dev/null +++ b/lib/remote/remotetesting.js @@ -0,0 +1,260 @@ +// ------------------------------------ +// Distributed Load Testing Interface +// ------------------------------------ +// +// This file defines LoadTestCluster. +// +// This file defines the interface for distributing a load test across multiple machines. Load tests are +// defined through a specification identical to those used by loadtesting.js#run(). To run a distributed +// test, first start nodeload on the slave machines, then initiate the test from the master. +// +// remote-slave-1> nodeload.js +// Started HTTP server on remote-slave-1:8000. +// +// remote-slave-2> nodeload.js +// Started HTTP server on remote-slave-2:8000. +// +// master> edit remote-test.js +// # var nl = require('nodeload'); +// # var cluster = new nl.LoadTestCluster('master:8000', ['remote-slave-1:8000', 'remote-slave-2:8000']); +// # cluster.run({ ... test specification ... }); +// +// See examples/remotetesting.ex.js for a full example. +// +/*jslint forin:true */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var stats = require('../stats'); +var reporting = require('../reporting'); +var run = require('../loadtesting').run; +var Cluster = require('./cluster').Cluster; +var EventEmitter = require('events').EventEmitter; +var StatsLogger = require('../monitoring/statslogger').StatsLogger; +var Report = reporting.Report; +var qputs = util.qputs; + +var REPORT_MANAGER = reporting.REPORT_MANAGER; +var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; +} + +/** A LoadTestCluster consists of a master and multiple slave instances of nodeload. Use +LoadTestCluster.run() accepts the same parameters as loadtesting.js#run(). It runs starts the load test +on each of slaves and aggregates statistics from each of them. + +@param masterHost 'host:port' to use for slaves to communicate with this nodeload instance. +@param slaveHosts ['host:port', ...] of slave nodeload instances +@param masterHttpServer the http.js#HttpServer instance that will receive mesages from slaves. Defaults + to global HTTP_SERVER. +@param slaveUpdateInterval Number of milliseconds between each 'update' event (which contains the latest + statistics) from this cluster and also the the interval at which each slaves should ping us to + let us know it is still alive. + +LoadTestCluster emits the following events: +- 'start': All of the slaves have started executing the load test after a call to run() +- 'update', interval, stats: Emitted periodically with aggregate stats from the last interval and overall stats +- 'end': All of the slaves have completed executing the load test +*/ +var LoadTestCluster = exports.LoadTestCluster = function LoadTestCluster(masterHost, slaveHosts, masterHttpServer, slaveUpdateInterval) { + EventEmitter.call(this); + util.PeriodicUpdater.call(this); + + var self = this; + self.masterHost = masterHost; + self.slaveHosts = slaveHosts; + self.masterHttpServer = self.masterHttpServer; + self.slaveUpdateInterval = slaveUpdateInterval || NODELOAD_CONFIG.MONITOR_INTERVAL_MS; +}; +util.inherits(LoadTestCluster, EventEmitter); +/** Same parameters as loadtesting.js#run(). Start a load test on each slave in this cluster */ +LoadTestCluster.prototype.run = function(specs) { + var self = this; + if (!specs) { throw new Error('No tests.'); } + if (self.cluster && self.cluster.started()) { throw new Error('Already started.'); } + + self.specs = (specs instanceof Array) ? specs : util.argarray(arguments); + self.cluster = new Cluster(self.getClusterSpec_()); + self.cluster.on('init', function() { + self.cluster.on('start', function() { + self.startTests_(); + self.updateInterval = self.slaveUpdateInterval; + self.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); + }); + self.cluster.start(); + }); + self.cluster.on('running', function() { + self.emit('start'); + }); + self.cluster.on('done', function() { + self.setLoggingEnabled(false); + self.updateInterval = 0; + self.update(); + self.end(); + }); + self.cluster.on('end', function() { + self.emit('end'); + }); +}; +/** Force all slaves to stop running tests */ +LoadTestCluster.prototype.end = function() { + this.cluster.stopTests(); + this.cluster.end(); +}; +/** Set the file name or stats.js#LogFile object that statistics are logged to; null for default */ +LoadTestCluster.prototype.setLogFile = function(logNameOrObject) { + this.logNameOrObject = logNameOrObject; +}; + +/** Log statistics each time an 'update' event is emitted */ +LoadTestCluster.prototype.setLoggingEnabled = function(enabled) { + if (enabled) { + this.logger = this.logger || new StatsLogger(this, this.logNameOrObject).start(); + } else if (this.logger) { + this.logger.stop(); + this.logger = null; + } + return this; +}; +/** Emit an 'update' event, add latest to the reports, and clear out stats for next interval */ +LoadTestCluster.prototype.update = function() { + var self = this; + self.emit('update', self.interval, self.stats); + util.forEach(self.stats, function(testName, stats) { + var report = self.reports[testName]; + var interval = self.interval[testName]; + util.forEach(stats, function(statName, stat) { + util.forEach(stat.summary(), function(name, val) { + report.summary[testName + ' ' + statName + ' ' + name] = val; + }); + report.getChart(statName).put(interval[statName].summary()); + }); + }); + util.forEach(self.interval, function(testName, stats) { + util.forEach(stats, function(statName, stat) { + stat.clear(); + }); + }); + util.qprint('.'); +}; +LoadTestCluster.prototype.startTests_ = function() { + var self = this, + summarizeStats = function() { + var summary = {ts: new Date()}; + util.forEach(this, function(testName, stats) { + summary[testName] = {}; + util.forEach(stats, function(statName, stat) { + summary[testName][statName] = stat.summary(); + }); + }); + return summary; + }; + + this.reports = {}; + this.interval = {}; + this.stats = {}; + this.cluster.runTests(this.stringify_(this.specs)); + + Object.defineProperty(this.stats, 'summary', { + enumerable: false, + value: summarizeStats + }); + Object.defineProperty(this.interval, 'summary', { + enumerable: false, + value: summarizeStats + }); +}; +/** A custom JSON stringifier that outputs node-compatible JSON which includes functions. */ +LoadTestCluster.prototype.stringify_ = function(obj) { + switch (typeof obj) { + case 'function': + return obj.toString(); + case 'object': + if (obj instanceof Array) { + var self = this; + return ['[', obj.map(function(x) { return self.stringify_(x); }), ']'].join(''); + } else if (obj === null) { + return 'null'; + } + var ret = ['{']; + for (var i in obj) { + ret.push(i + ':' + this.stringify_(obj[i]) + ','); + } + ret.push('}'); + return ret.join(''); + case 'number': + if (isFinite(obj)) { + return String(obj); + } + return 'Infinity'; + default: + return JSON.stringify(obj); + } +}; +/** Get an actual cluster.js#Cluster definition that will create an local master endpoint and be sent +to the slaves */ +LoadTestCluster.prototype.getClusterSpec_ = function() { + var self = this; + return { + master: { + host: self.masterHost, + sendStats: function(slaves, slaveId, interval) { + // slave sends interval = {"test-name": { "stats-name": StatsObject, ...}, ...} + util.forEach(interval, function(testName, remoteInterval) { + if (!self.stats[testName]) { + // First time seeing this test. Create cumulative and interval stats and a report. + self.stats[testName] = {}; + self.interval[testName] = {}; + self.reports[testName] = new Report(testName); + REPORT_MANAGER.addReport(self.reports[testName]); + } + + // Merge in data from each stat (e.g. latency, result-codes, etc) from this slave + stats.mergeStatsGroups(remoteInterval, self.interval[testName]); + stats.mergeStatsGroups(remoteInterval, self.stats[testName]); + }); + } + }, + slaves: { + hosts: self.slaveHosts, + setup: function() { + if (typeof BUILD_AS_SINGLE_FILE === 'undefined' || BUILD_AS_SINGLE_FILE === false) { + this.nlrun = require('../loadtesting').run; + } else { + this.nlrun = run; + } + }, + runTests: function(master, specsStr) { + var specs; + try { + eval('specs='+specsStr); + } catch(e) { + qputs('WARN: Ignoring invalid remote test specifications: ' + specsStr + ' - ' + e.toString()); + return; + } + + if (this.state === 'running') { + qputs('WARN: Already running -- ignoring new test specifications: ' + specsStr); + return; + } + + qputs('Received remote test specifications: ' + specsStr); + + var self = this; + self.state = 'running'; + self.loadtest = self.nlrun(specs); + self.loadtest.keepAlive = true; + self.loadtest.on('update', function(interval, stats) { + master.sendStats(interval); + }); + self.loadtest.on('end', function() { + self.state = 'done'; + }); + }, + stopTests: function(master) { + if (this.loadtest) { this.loadtest.stop(); } + } + }, + server: self.masterHttpServer, + pingInterval: self.slaveUpdateInterval + }; +}; \ No newline at end of file diff --git a/lib/remote/slave.js b/lib/remote/slave.js new file mode 100644 index 0000000..af13d9c --- /dev/null +++ b/lib/remote/slave.js @@ -0,0 +1,104 @@ +/*jslint sub: true */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var url = require('url'); +var util = require('../util'); +var EventEmitter = require('events').EventEmitter; +var EndpointClient = require('./endpointclient').EndpointClient; +var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; +} + +/** Slave represents a remote slave instance from the master server's perspective. It holds the slave +method defintions, defined by calling defineMethod(), as Javascript strings. When start() is called, +the definitions are POSTed to /remote on the remote instance which causes the instance to create a new +endpoint with those methods. Subsequent calls to Slave simply POST parameters to the remote instance: + + slave = new Slave(...); + slave.defineMethod('slave_method_1', function(master, name) { return 'hello ' + name }); + slave.start(); + slave.on('start', function() { + slave.method_1('tom'); + slave.end(); + }); + +will POST the definition of method_1 to /remote, followed by ['tom'] to /remote/.../method_1. + +Slave emits the following events: +- 'slaveError', error: The underlying HTTP connection returned an error. +- 'start': The remote instance accepted the slave definition and slave methods can now be called. +- 'end': The slave endpoint has been removed from the remote instance. + +Slave.state can be: +- 'initialized': The slave is ready to be started. +- 'connecting': The slave definition is being sent to the remote instance. +- 'started': The remote instance is running and methods defined through defineMethod can be called. */ +var Slave = exports.Slave = function Slave(id, host, port, masterEndpoint, pingInterval) { + EventEmitter.call(this); + this.id = id; + this.client = new EndpointClient(host, port); + this.client.on('error', this.emit.bind(this, 'slaveError')); + this.masterEndpoint = masterEndpoint; + this.pingInterval = pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; + this.methodDefs = []; + this.state = 'initialized'; +}; +util.inherits(Slave, EventEmitter); +/** POST method definitions and information about this instance (the slave's master) to /remote */ +Slave.prototype.start = function() { + if (this.masterEndpoint && this.masterEndpoint.state !== 'started') { + throw new Error('Slave must be started after its Master.'); + } + + var self = this, + masterUrl = self.masterEndpoint ? self.masterEndpoint.url : null, + masterMethods = self.masterEndpoint ? self.masterEndpoint.methodNames : [], + req = self.client.rawRequest('POST', '/remote'); + + req.end(JSON.stringify({ + id: self.id, + master: masterUrl, + masterMethods: masterMethods, + slaveMethods: self.methodDefs, + pingInterval: self.pingInterval + })); + req.on('response', function(res) { + if (!res.headers['location']) { + self.emit('error', new Error('Remote slave does not have proper /remote handler.')); + } + self.client.basepath = url.parse(res.headers['location']).pathname; + self.state = 'started'; + self.emit('start'); + }); + + self.state = 'connecting'; +}; +/** Stop this slave by sending a DELETE request to terminate the slave's endpoint. */ +Slave.prototype.end = function() { + var self = this, + req = self.client.rawRequest('DELETE', self.client.basepath), + done = function() { + self.client.destroy(); + self.client.basepath = ''; + self.state = 'initialized'; + self.emit('end'); + }; + + self.client.once('error', function(e) { + self.emit('slaveError', e); + done(); + }); + req.on('response', function(res) { + if (res.statusCode !== 204) { + self.emit('slaveError', new Error('Error stopping slave.'), res); + } + done(); + }); + req.end(); +}; +/** Define a method that will be sent to the slave instance */ +Slave.prototype.defineMethod = function(name, fun) { + var self = this; + self.client.defineMethod(name, fun); + self[name] = function() { return self.client[name].apply(self.client, arguments); }; + self.methodDefs.push({name: name, fun: fun.toString()}); +}; \ No newline at end of file diff --git a/lib/remote/slavenode.js b/lib/remote/slavenode.js new file mode 100644 index 0000000..45a0a47 --- /dev/null +++ b/lib/remote/slavenode.js @@ -0,0 +1,148 @@ +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; +} + +/** An instance of SlaveNode represents a slave from the perspective of a slave (as opposed to +slave.js#Slave, which represents a slave from the perspective of a master). When a slave.js#Slave object +is started, it sends a slave specification to the target machine, which uses the specification to create +a SlaveNode. The specification contains: + + { + id: master assigned id of this node, + master: 'base url of master endpoint, e.g. /remote/0', + masterMethods: ['list of method name supported by master'], + slaveMethods: [ + { name: 'method-name', fun: 'function() { valid Javascript in a string }' } + ], + pingInterval: milliseconds between sending the current execution state to master + } + +If the any of the slaveMethods contain invalid Javascript, this constructor will throw an exception. + +SlaveNode emits the following events: +- 'start': The endpoint has been installed on the HTTP server and connection to the master has been made +- 'masterError': The HTTP connection to the master node returned an error. +- 'end': The local endpoint has been removed and the connection to the master server terminated +*/ +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) { + // Add a new endpoint and route to the HttpServer + var endpoint = new Endpoint(server); + + // "Compile" the methods by eval()'ing the string in "fun", and add to the endpoint + 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); }); + } + + // send this slave's id as the first parameter for all method calls to master + masterClient.setStaticParams([this.id]); + + masterClient.on('error', this.emit.bind(this, 'masterError')); + return masterClient; +}; + + +/** Install the /remote URL handler, which creates a slave endpoint. On receiving a POST request to +/remote, a new route is added to HTTP_SERVER using the handler definition provided in the request body. +See #SlaveNode for a description of the handler defintion. */ +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; + + // Grab the slave endpoint definition from the HTTP request body; should be valid JSON + try { + body = JSON.parse(body); + slaveNode = new SlaveNode(server, body); + } catch(e) { + res.writeHead(400); + res.end(e.toString()); + return; + } + + slaveNodes.push(slaveNode); + slaveNode.on('end', function() { + var idx = slaveNodes.indexOf(slaveNode); + if (idx !== -1) { slaveNodes.splice(idx, 1); } + }); + + res.writeHead(201, { + 'Location': slaveNode.url, + 'Content-Length': 0, + }); + res.end(); + }); + } else if (req.method === 'GET') { + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(JSON.stringify(slaveNodes.map(function(s) { return s.url; }))); + } else { + res.writeHead(405); + res.end(); + } + }); +}; \ No newline at end of file diff --git a/lib/remote/slaves.js b/lib/remote/slaves.js new file mode 100644 index 0000000..a20ec90 --- /dev/null +++ b/lib/remote/slaves.js @@ -0,0 +1,68 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var Slave = require('./slave').Slave; +var EventEmitter = require('events').EventEmitter; +} + +/** A small wrapper for a collection of Slave instances. The instances are all started and stopped +together and method calls are sent to all the instances. + +Slaves emits the following events: +- 'slaveError', slave, error: The underlying HTTP connection for this slave returned an error. +- 'start': All of the slave instances are running. +- 'stopped': All of the slave instances have been stopped. */ + +var Slaves = exports.Slaves = function Slaves(masterEndpoint, pingInterval) { + EventEmitter.call(this); + this.masterEndpoint = masterEndpoint; + this.slaves = []; + this.pingInterval = pingInterval; +}; +util.inherits(Slaves, EventEmitter); +/** Add a remote instance in the format 'host:port' as a slave in this collection */ +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'); + }); +}; +/** Define a method on all the slaves */ +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); }); + }; +}; +/** Start all the slaves */ +Slaves.prototype.start = function() { + this.slaves.forEach(function(s) { s.start(); }); +}; +/** Terminate all the slaves */ +Slaves.prototype.end = function() { + this.slaves.forEach(function(s) { s.end(); }); +}; \ No newline at end of file diff --git a/lib/reporting/dygraph.tpl b/lib/reporting/dygraph.tpl new file mode 100644 index 0000000..4e38efc --- /dev/null +++ b/lib/reporting/dygraph.tpl @@ -0,0 +1 @@ +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}; \ No newline at end of file diff --git a/lib/reporting/index.js b/lib/reporting/index.js new file mode 100644 index 0000000..8fcafb7 --- /dev/null +++ b/lib/reporting/index.js @@ -0,0 +1,192 @@ +// ------------------------------------ +// Progress Reporting +// ------------------------------------ +// +// This file defines Report, Chart, and REPORT_MANAGER +// +// A Report contains a summary and a number of charts. Reports added to the global REPORT_MANAGER are +// served by the global HTTP_SERVER instance (defaults to http://localhost:8000/) and written to disk +// at regular intervals. +// +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; + +/** A Report contains a summary object and set of charts. It can be easily updated using the stats from +a monitor.js#Monitor or monitor.js#MonitorGroup using updateFromMonitor()/updateFromMonitorGroup(). + +@param name A name for the report. Generally corresponds to the test name. +@param updater A function(report) that should update the summary and chart data. */ +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]; + }, + /** Update this report automatically each time the Monitor emits an 'update' event */ + updateFromMonitor: function(monitor) { + monitor.on('update', this.doUpdateFromMonitor_.bind(this, monitor, '')); + return this; + }, + /** Update this report automatically each time the MonitorGroup emits an 'update' event */ + 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()); + } + }); + } +}; + +/** A Chart represents a collection of lines over time represented as: + + columns: ["x values", "line 1", "line 2", "line 3", ...] + rows: [[timestamp1, line1[0], line2[0], line3[0], ...], + [timestamp2, line1[1], line2[1], line3[1], ...], + [timestamp3, line1[2], line2[2], line3[2], ...], + ... + ] + +@param name A name for the chart */ +var Chart = exports.Chart = function(name) { + this.name = name; + this.uid = util.uid(); + this.columns = ["time"]; + this.rows = [[timeFromStart()]]; +}; +Chart.prototype = { + /** Put a row of data into the chart. The current time will be used as the x-value. The lines in the + chart are extracted from the "data". New lines can be added to the chart at any time by including it + in data. + + @param data An object representing one row of data: { + "line name 1": value1 + "line name 2": value2 + ... + } + */ + 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()); + } +}; + +// ================= +// Singletons +// ================= + +/** A global report manager used by nodeload to keep the summary webpage up to date during a load test */ +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(); +}); + +// ================= +// Private methods +// ================= + +/** current time from start of nodeload process in 100ths of a minute */ +function timeFromStart() { + return (Math.floor((new Date().getTime() - START) / 600) / 100); +} diff --git a/lib/reporting/summary.tpl b/lib/reporting/summary.tpl new file mode 100644 index 0000000..d073f9d --- /dev/null +++ b/lib/reporting/summary.tpl @@ -0,0 +1,113 @@ + + + Test Results + + + + + + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/deps/template.js b/lib/reporting/template.js similarity index 63% rename from deps/template.js rename to lib/reporting/template.js index 7694627..c8c1c30 100644 --- a/deps/template.js +++ b/lib/reporting/template.js @@ -1,35 +1,35 @@ /* - * node-template - * http://github.com/graphnode/node-template/ - * by Diogo Gomes - MIT Licensed - * * Based off of: * - Chad Etzel - http://github.com/jazzychad/template.node.js/ * - John Resig - http://ejohn.org/blog/javascript-micro-templating/ */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var fs = require('fs'); +} -template = { - cache: {}, +var template = { + cache_: {}, create: function(str, data, callback) { // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var fn; - if (!/[\t\r\n% ]/.test(str)) - if (!callback) - fn = create(fs.readFileSync(str).toString('utf8')); - else { + 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; - - create(buffer.toString('utf8'), data, callback); + if (err) { throw err; } + + this.create(buffer.toString('utf8'), data, callback); }); return; } - else { - if (this.cache[str]) - fn = this.cache[str]; - else { + } else { + if (this.cache_[str]) { + fn = this.cache_[str]; + } else { // Generate a reusable function that will serve as a template // generator (and which will be cached). fn = new Function("obj", @@ -44,16 +44,16 @@ template = { .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; + .split("%>").join("p.push('") + "');}return p.join('');"); + + this.cache_[str] = fn; } } // Provide some "basic" currying to the user - if (callback) callback(data ? fn( data ) : fn); - else return data ? fn( data ) : fn; + if (callback) { callback(data ? fn( data ) : fn); } + else { return data ? fn( data ) : fn; } } -} \ No newline at end of file +}; + +exports.create = template.create.bind(template); \ No newline at end of file diff --git a/src/stats.js b/lib/stats.js similarity index 59% rename from src/stats.js rename to lib/stats.js index cb740d9..2ab7537 100644 --- a/src/stats.js +++ b/lib/stats.js @@ -4,24 +4,22 @@ // // Defines various statistics classes and function. The classes implement the same consistent interface. // See NODELOADLIB.md for a complete description of the classes and functions. +// +/*jslint forin:true */ +var BUILD_AS_SINGLE_FILE, stats = {}; +if (BUILD_AS_SINGLE_FILE === undefined) { +var fs = require('fs'); +} -var Histogram = exports.Histogram = function(params) { +var Histogram = stats.Histogram = function Histogram(params) { // default histogram size of 3000: when tracking latency at ms resolution, this // lets us store latencies up to 3 seconds in the main array - var numBuckets = 3000; - var percentiles = [0.95, 0.99]; - - if (params != null && params.numBuckets != null) - numBuckets = params.buckets; - if (params != null && params.percentiles != null) - percentiles = params.percentiles; - - this.type = "Histogram"; + this.type = 'Histogram'; this.params = params; - this.size = numBuckets; - this.percentiles = percentiles; + this.size = params && params.buckets || 3000; + this.percentiles = params && params.percentiles || [0.95, 0.99]; this.clear(); -} +}; Histogram.prototype = { clear: function() { this.start = new Date(); @@ -36,11 +34,11 @@ Histogram.prototype = { put: function(item) { this.length++; this.sum += item; - if (item < this.min || this.min == -1) this.min = item; - if (item > this.max || this.max == -1) this.max = item; + if (item < this.min || this.min === -1) { this.min = item; } + if (item > this.max || this.max === -1) { this.max = item; } if (item < this.items.length) { - if (this.items[item] != null) { + if (this.items[item] !== undefined) { this.items[item]++; } else { this.items[item] = 1; @@ -56,7 +54,7 @@ Histogram.prototype = { } else { var count = 0; for (var i in this.extra) { - if (this.extra[i] == item) { + if (this.extra[i] === item) { count++; } } @@ -72,14 +70,14 @@ Histogram.prototype = { if (this.extra.length > target) { var idx = this.extra.length - target; if (!this.sorted) { - this.extra = this.extra.sort(function(a, b) { return a - b }); + this.extra = this.extra.sort(function(a, b) { return a - b; }); this.sorted = true; } return this.extra[idx]; } else { var sum = this.extra.length; for (var i = this.items.length - 1; i >= 0; i--) { - if (this.items[i] != null) { + if (this.items[i] > 0) { sum += this.items[i]; if (sum >= target) { return i; @@ -94,7 +92,7 @@ Histogram.prototype = { var s = 0; for (var i = 0; i < this.items.length; i++) { - if (this.items[i] != null) { + if (this.items[i] !== undefined) { s += this.items[i] * Math.pow(i - mean, 2); } } @@ -104,28 +102,29 @@ Histogram.prototype = { return Math.sqrt(s / this.length); }, summary: function() { - var s = { - min: this.min, - max: this.max, - avg: Number(this.mean().toFixed(1)), - median: this.percentile(.5) - }; - for (var i in this.percentiles) { - s[this.percentiles[i] * 100 + "%"] = this.percentile(this.percentiles[i]); - } + var self = this, + s = { + min: self.min, + max: self.max, + avg: Number(self.mean().toFixed(1)), + median: self.percentile(0.5) + }; + self.percentiles.forEach(function(percentile) { + s[percentile * 100 + "%"] = self.percentile(percentile); + }); return s; }, merge: function(other) { - if (this.items.length != other.items.length) { + if (this.items.length !== other.items.length) { throw "Incompatible histograms"; } this.length += other.length; this.sum += other.sum; - this.min = (other.min != -1 && (other.min < this.min || this.min == -1)) ? other.min : this.min; - this.max = (other.max > this.max || this.max == -1) ? other.max : this.max; + this.min = (other.min !== -1 && (other.min < this.min || this.min === -1)) ? other.min : this.min; + this.max = (other.max > this.max || this.max === -1) ? other.max : this.max; for (var i = 0; i < this.items.length; i++) { - if (this.items[i] != null) { + if (this.items[i] !== undefined) { this.items[i] += other.items[i]; } else { this.items[i] = other.items[i]; @@ -134,13 +133,13 @@ Histogram.prototype = { this.extra = this.extra.concat(other.extra); this.sorted = false; } -} +}; -var Accumulator = exports.Accumulator = function() { - this.type = "Accumulator"; +var Accumulator = stats.Accumulator = function Accumulator() { + this.type = 'Accumulator'; this.total = 0; this.length = 0; -} +}; Accumulator.prototype = { put: function(stat) { this.total += stat; @@ -160,18 +159,17 @@ Accumulator.prototype = { this.total += other.total; this.length += other.length; } -} +}; -var ResultsCounter = exports.ResultsCounter = function() { - this.type = "ResultsCounter"; +var ResultsCounter = stats.ResultsCounter = function ResultsCounter() { + this.type = 'ResultsCounter'; this.start = new Date(); this.items = {}; - this.items.total = 0; this.length = 0; -} +}; ResultsCounter.prototype = { put: function(item) { - if (this.items[item] != null) { + if (this.items[item] !== undefined) { this.items[item]++; } else { this.items[item] = 1; @@ -195,13 +193,17 @@ ResultsCounter.prototype = { this.length = 0; }, summary: function() { - this.items.total = this.length; - this.items.rps = Number((this.length / ((new Date() - this.start) / 1000)).toFixed(1)); - return this.items; + var items = {}; + for (var i in this.items) { + items[i] = this.items[i]; + } + items.total = this.length; + items.rps = Number((this.length / ((new Date() - this.start) / 1000)).toFixed(1)); + return items; }, merge: function(other) { for (var i in other.items) { - if (this.items[i] != null) { + if (this.items[i] !== undefined) { this.items[i] += other.items[i]; } else { this.items[i] = other.items[i]; @@ -209,22 +211,22 @@ ResultsCounter.prototype = { } this.length += other.length; } -} +}; -var Uniques = exports.Uniques = function() { - this.type = "Uniques"; +var Uniques = stats.Uniques = function Uniques() { + this.type = 'Uniques'; this.start = new Date(); this.items = {}; this.uniques = 0; this.length = 0; -} +}; Uniques.prototype = { put: function(item) { - if (this.items[item] != null) { + if (this.items[item] !== undefined) { this.items[item]++; } else { this.items[item] = 1; - this.uniques++ + this.uniques++; } this.length++; }, @@ -241,7 +243,7 @@ Uniques.prototype = { }, merge: function(other) { for (var i in other.items) { - if (this.items[i] != null) { + if (this.items[i] !== undefined) { this.items[i] += other.items[i]; } else { this.items[i] = other.items[i]; @@ -250,13 +252,13 @@ Uniques.prototype = { } this.length += other.length; } -} +}; -var Peak = exports.Peak = function() { - this.type = "Peak"; +var Peak = stats.Peak = function Peak() { + this.type = 'Peak'; this.peak = 0; this.length = 0; -} +}; Peak.prototype = { put: function(item) { if (this.peak < item) { @@ -279,13 +281,13 @@ Peak.prototype = { } this.length += other.length; } -} +}; -var Rate = exports.Rate = function() { - type = "Rate"; +var Rate = stats.Rate = function Rate() { + this.type = 'Rate'; this.start = new Date(); this.length = 0; -} +}; Rate.prototype = { put: function() { this.length++; @@ -303,15 +305,15 @@ Rate.prototype = { merge: function(other) { this.length += other.length; } -} +}; -var LogFile = exports.LogFile = function(filename) { - this.type = "LogFile"; +var LogFile = stats.LogFile = function LogFile(filename) { + this.type = 'LogFile'; this.writepos = null; this.length = 0; this.filename = filename; this.open(); -} +}; LogFile.prototype = { put: function(item) { var buf = new Buffer(item); @@ -321,19 +323,19 @@ LogFile.prototype = { }, get: function(item) { fs.statSync(this.filename, function(err, stats) { - if (err == null) item = stats; + if (!err) { item = stats; } }); return item; }, clear: function(text) { - logfile = this; - this.writepos = 0; - fs.truncate(this.fd, 0, function(err) { - if (text !== undefined) logfile.put(text); + var self = this; + self.writepos = 0; + fs.truncate(self.fd, 0, function(err) { + if (text !== undefined) { self.put(text); } }); }, open: function() { - this.fd = fs.openSync(this.filename, "w"); + this.fd = fs.openSync(this.filename, "a"); }, close: function() { fs.closeSync(this.fd); @@ -342,37 +344,29 @@ LogFile.prototype = { summary: function() { return { file: this.filename, written: this.length }; } -} +}; -var NullLog = exports.NullLog = function() { - this.type = "NullLog"; +var NullLog = stats.NullLog = function NullLog() { + this.type = 'NullLog'; this.length = 0; -} +}; NullLog.prototype = { put: function(item) { /* nop */ }, get: function(item) { return null; }, clear: function() { /* nop */ }, open: function() { /* nop */ }, close: function() { /* nop */ }, - summary: function() { return { file: 'null', written: 0 } } -} + summary: function() { return { file: 'null', written: 0 }; } +}; -var Reportable = exports.Reportable = function(backend, name, trend) { - var backendparams = null; - name = name || ""; - if (typeof backend == 'object') { - backendparams = backend[1]; - backend = backend[0]; - } - - this.type = "Reportable"; - this.name = name; +var Reportable = stats.Reportable = function Reportable(name, Backend, backendparams) { + this.type = 'Reportable'; + this.name = name || ''; this.length = 0; - this.interval = new backend(backendparams); - this.cumulative = new backend(backendparams); - this.trend = trend; + this.interval = new Backend(backendparams); + this.cumulative = new Backend(backendparams); this.lastSummary = null; -} +}; Reportable.prototype = { put: function(stat) { if (!this.disableIntervalReporting) { @@ -396,7 +390,7 @@ Reportable.prototype = { this.lastSummary = null; }, summary: function() { - if (this.lastSummary) { return this.lastSummary } + if (this.lastSummary) { return this.lastSummary; } return { interval: this.interval.summary(), cumulative: this.cumulative.summary() }; }, merge: function(other) { @@ -404,60 +398,115 @@ Reportable.prototype = { this.interval.merge(other); this.cumulative.merge(other); } -} +}; -var roundRobin = exports.roundRobin = function(list) { - r = list.slice(); +var StatsGroup = stats.StatsGroup = function StatsGroup() { + Object.defineProperty(this, 'name', { + enumerable: false, + writable: true, + }); + Object.defineProperty(this, 'put', { + enumerable: false, + value: function(statNameOrVal, val) { + if (arguments.length < 2) { + for (var i in this) { this[i].put(statNameOrVal); } + } else { + if (this[statNameOrVal]) { this[statNameOrVal].put(val); } + } + } + }); + Object.defineProperty(this, 'get', { + enumerable: false, + value: function(statName) { + if (arguments.length === 1) { + var val = {}; + for (var i in this) { + val[i] = this[i].get.apply(this[i], arguments); + } + return val; + } + if (!this[statName]) { + return undefined; + } + console.log(this[statName]); + var getArgs = Array.prototype.slice.call(arguments, 1); + return this[statName].get.apply(this[statName], getArgs); + } + }); + Object.defineProperty(this, 'clear', { + enumerable: false, + value: function(statName) { + if (statName) { + this[statName].clear(); + } else { + for (var i in this) { this[i].clear(); } + } + } + }); + Object.defineProperty(this, 'summary', { + enumerable: false, + value: function(statName) { + if (statName) { + return this[statName].summary(); + } + + var summary = {ts: new Date()}; + if (this.name) { summary.name = this.name; } + for (var i in this) { + summary[i] = this[i].summary(); + } + return summary; + } + }); +}; + +/** Merge all the stats from one group of stats, {"statistic-name": StatsObject, ...} */ +var mergeStatsGroups = stats.mergeStatsGroups = function(sourceGroup, targetGroup) { + for (var statName in sourceGroup) { + var sourceStats = sourceGroup[statName]; + if (targetGroup[statName] === undefined) { + targetGroup[statName] = new stats[sourceStats.type](sourceStats.params); + } + targetGroup[statName].merge(sourceStats); + } +}; + +var roundRobin = stats.roundRobin = function(list) { + var r = list.slice(); r.rridx = -1; r.get = function() { - this.rridx = (this.rridx+1) % this.length; - return this[this.rridx]; - } + r.rridx = (r.rridx+1) % r.length; + return r[r.rridx]; + }; return r; -} +}; -var randomString = exports.randomString = function(length) { +var randomString = stats.randomString = function(length) { var s = ""; for (var i = 0; i < length; i++) { s += '\\' + (Math.floor(Math.random() * 95) + 32).toString(8); // ascii chars between 32 and 126 } return eval("'" + s + "'"); -} +}; -var nextGaussian = exports.nextGaussian = function(mean, stddev) { - if (mean == null) mean = 0; - if (stddev == null) stddev = 1; +var nextGaussian = stats.nextGaussian = function(mean, stddev) { + mean = mean || 0; + stddev = stddev || 1; var s = 0, z0, z1; - while (s == 0 || s >= 1) { + while (s === 0 || s >= 1) { z0 = 2 * Math.random() - 1; z1 = 2 * Math.random() - 1; s = z0*z0 + z1*z1; } return z0 * Math.sqrt(-2 * Math.log(s) / s) * stddev + mean; -} +}; -var nextPareto = exports.nextPareto = function(min, max, shape) { - if (shape == null) shape = 0.1; +var nextPareto = stats.nextPareto = function(min, max, shape) { + shape = shape || 0.1; var l = 1, h = Math.pow(1+max-min, shape), rnd = Math.random(); - while (rnd == 0) rnd = Math.random(); + while (rnd === 0) { rnd = Math.random(); } return Math.pow((rnd*(h-l)-h) / -(h*l), -1/shape)-1+min; -} - -// ================= -// Private methods -// ================= +}; -function statsClassFromString(name) { - types = { - "Histogram": Histogram, - "Accumulator": Accumulator, - "ResultsCounter": ResultsCounter, - "Uniques": Uniques, - "Peak": Peak, - "Rate": Rate, - "LogFile": LogFile, - "NullLog": NullLog, - "Reportable": Reportable - }; - return types[name]; -} \ No newline at end of file +// Export everything in stats namespace +for (var i in stats) { exports[i] = stats[i]; } \ No newline at end of file diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..9eb0ccb --- /dev/null +++ b/lib/util.js @@ -0,0 +1,129 @@ +// ------------------------------------ +// Statistics Manager +// ------------------------------------ +// +// This file defines qputs, qprint, and extends the util namespace. +// +// Extends node.js util.js with other common functions. +// +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('util'); +var NODELOAD_CONFIG = require('./config').NODELOAD_CONFIG; +} + +// A few common global functions so we can access them with as few keystrokes as possible +// +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); } +}; + + +// Static utility methods +// +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]; + } + } + return obj; +}; +util.forEach = function(obj, f) { + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + f(i, obj[i]); + } + } +}; +util.every = function(obj, f) { + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + if (!f(i, obj[i])) { + return false; + } + } + } + return true; +}; +util.argarray = function(args) { + return Array.prototype.slice.call(args); +}; +util.readStream = function(stream, callback) { + var data = []; + stream.on('data', function(chunk) { + data.push(chunk.toString()); + }); + stream.on('end', function() { + callback(data.join('')); + }); +}; + +/** Make an object a PeriodicUpdater by adding PeriodicUpdater.call(this) to the constructor. +The object will call this.update() every interval. */ +util.PeriodicUpdater = function(updateIntervalMs) { + var self = this, updateTimeoutId; + this.__defineGetter__('updateInterval', function() { return updateIntervalMs; }); + this.__defineSetter__('updateInterval', function(milliseconds) { + clearInterval(updateTimeoutId); + if (milliseconds > 0 && milliseconds < Infinity) { + updateTimeoutId = setInterval(self.update.bind(self), milliseconds); + } + updateIntervalMs = milliseconds; + }); + this.updateInterval = updateIntervalMs; +}; + +/** Same arguments as http.createClient. Returns an wrapped http.Client object that will reconnect when +connection errors are detected. In the current implementation of http.Client (11/29/10), calls to +request() fail silently after the initial 'error' event. */ +util.createReconnectingClient = function() { + var http = require('http'), + clientArgs = arguments, events = {}, client, wrappedClient = {}, + clientMethod = function(method) { + return function() { return client[method].apply(client, arguments); }; + }, + clientGetter = function(member) { return function() { return client[member]; };}, + clientSetter = function(member) { return function(val) { client[member] = val; };}, + reconnect = function() { + var oldclient = client; + if (oldclient) { oldclient.destroy(); } + client = http.createClient.apply(http, clientArgs); + client._events = util.extend(events, client._events); // EventEmitter._events stores event handlers + client.emit('reconnect', oldclient); + }; + + // Create initial http.Client + reconnect(); + client.on('error', function(err) { reconnect(); }); + + // Wrap client so implementation can be swapped out when there are connection errors + for (var j in client) { + if (typeof client[j] === 'function') { + wrappedClient[j] = clientMethod(j); + } else { + wrappedClient.__defineGetter__(j, clientGetter(j)); + wrappedClient.__defineSetter__(j, clientSetter(j)); + } + } + wrappedClient.impl = client; + return wrappedClient; +}; + +util.extend(exports, util); \ No newline at end of file diff --git a/src/nodeload.js b/nl.js similarity index 54% rename from src/nodeload.js rename to nl.js index 1d9139d..d3b9096 100755 --- a/src/nodeload.js +++ b/nl.js @@ -25,70 +25,81 @@ OTHER DEALINGS IN THE SOFTWARE. */ -var options = require('./options'); +/*jslint sub:true */ +/*globals __dirname */ + +require.paths.unshift(__dirname); + +var options = require('./lib/nl/options'); options.process(); -if (!options.get('url')) +if (!options.get('url')) { options.help(); +} -var nl = require('../lib/nodeloadlib') +var nl = require('./nodeload') .quiet() .setMonitorIntervalMs(options.get('reportInterval') * 1000); -function puts(text) { if (!options.get('quiet')) console.log(text) } -function pad(str, width) { return str + (new Array(width-str.length)).join(" "); } +function puts(text) { if (!options.get('quiet')) { console.log(text); } } +function pad(str, width) { return str + (new Array(width-str.length)).join(' '); } function printItem(name, val, padLength) { - if (padLength == undefined) padLength = 40; - puts(pad(name + ":", padLength) + " " + val); + if (padLength === undefined) { padLength = 40; } + puts(pad(name + ':', padLength) + ' ' + val); } -nl.TEST_MONITOR.on('start', function(tests) { testStart = new Date(); }); -nl.TEST_MONITOR.on('update', function(tests) { - puts(pad('Completed ' +tests[0].stats['result-codes'].cumulative.length+ ' requests', 40)); +var testStart; +var host = options.get('host'); +var test = nl.run({ + name: host, + host: options.get('host'), + port: options.get('port'), + requestGenerator: options.get('requestGenerator'), + method: options.get('method'), + path: options.get('path'), + requestData: options.get('requestData'), + numUsers: options.get('numClients'), + numRequests: options.get('numRequests'), + timeLimit: options.get('timeLimit'), + targetRps: options.get('targetRps'), + stats: ['latency', 'result-codes', 'request-bytes', 'response-bytes'] +}); + +test.on('start', function(tests) { testStart = new Date(); }); +test.on('update', function(interval, stats) { + puts(pad('Completed ' +stats[host]['result-codes'].length+ ' requests', 40)); }); -nl.TEST_MONITOR.on('end', function(tests) { +test.on('end', function() { - var stats = tests[0].stats; + var stats = test.stats[host]; var elapsedSeconds = ((new Date()) - testStart)/1000; puts(''); - printItem('Server', options.get('host') + ":" + options.get('port')); + printItem('Server', options.get('host') + ':' + options.get('port')); - if (options.get('requestGeneratorModule') == null) { - printItem('HTTP Method', options.get('method')) - printItem('Document Path', options.get('path')) + if (options.get('requestGeneratorModule') === undefined) { + printItem('HTTP Method', options.get('method')); + printItem('Document Path', options.get('path')); } else { printItem('Request Generator', options.get('requestGeneratorModule')); } printItem('Concurrency Level', options.get('numClients')); - printItem('Number of requests', stats['result-codes'].cumulative.length); - printItem('Body bytes transferred', stats['request-bytes'].cumulative.total + stats['response-bytes'].cumulative.total); + printItem('Number of requests', stats['result-codes'].length); + printItem('Body bytes transferred', stats['request-bytes'].total + stats['response-bytes'].total); printItem('Elapsed time (s)', elapsedSeconds.toFixed(2)); - printItem('Requests per second', (stats['result-codes'].cumulative.length/elapsedSeconds).toFixed(2)); - printItem('Mean time per request (ms)', stats['latency'].cumulative.mean().toFixed(2)); - printItem('Time per request standard deviation', stats['latency'].cumulative.stddev().toFixed(2)); + printItem('Requests per second', (stats['result-codes'].length/elapsedSeconds).toFixed(2)); + printItem('Mean time per request (ms)', stats['latency'].mean().toFixed(2)); + printItem('Time per request standard deviation', stats['latency'].stddev().toFixed(2)); puts('\nPercentages of requests served within a certain time (ms)'); - printItem(" Min", stats['latency'].cumulative.min, 6); - printItem(" Avg", stats['latency'].cumulative.mean().toFixed(1), 6); - printItem(" 50%", stats['latency'].cumulative.percentile(.5), 6) - printItem(" 95%", stats['latency'].cumulative.percentile(.95), 6) - printItem(" 99%", stats['latency'].cumulative.percentile(.99), 6) - printItem(" Max", stats['latency'].cumulative.max, 6); -}); + printItem(' Min', stats['latency'].min, 6); + printItem(' Avg', stats['latency'].mean().toFixed(1), 6); + printItem(' 50%', stats['latency'].percentile(0.5), 6); + printItem(' 95%', stats['latency'].percentile(0.95), 6); + printItem(' 99%', stats['latency'].percentile(0.99), 6); + printItem(' Max', stats['latency'].max, 6); -nl.runTest({ - name: options.get('host'), - host: options.get('host'), - port: options.get('port'), - requestGenerator: options.get('requestGenerator'), - method: options.get('method'), - path: options.get('path'), - requestData: options.get('requestData'), - numClients: options.get('numClients'), - numRequests: options.get('numRequests'), - timeLimit: options.get('timeLimit'), - targetRps: options.get('targetRps'), - stats: ['latency', 'result-codes', 'bytes'] + process.exit(0); }); +test.start(); \ No newline at end of file diff --git a/nodeload.js b/nodeload.js new file mode 100755 index 0000000..2bb3c24 --- /dev/null +++ b/nodeload.js @@ -0,0 +1,137 @@ +#!/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 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];}} +return obj;};util.forEach=function(obj,f){for(var i in obj){if(obj.hasOwnProperty(i)){f(i,obj[i]);}}};util.every=function(obj,f){for(var i in obj){if(obj.hasOwnProperty(i)){if(!f(i,obj[i])){return false;}}} +return true;};util.argarray=function(args){return Array.prototype.slice.call(args);};util.readStream=function(stream,callback){var data=[];stream.on('data',function(chunk){data.push(chunk.toString());});stream.on('end',function(){callback(data.join(''));});};util.PeriodicUpdater=function(updateIntervalMs){var self=this,updateTimeoutId;this.__defineGetter__('updateInterval',function(){return updateIntervalMs;});this.__defineSetter__('updateInterval',function(milliseconds){clearInterval(updateTimeoutId);if(milliseconds>0&&millisecondsthis.max||this.max===-1){this.max=item;} +if(itemtarget){var idx=this.extra.length-target;if(!this.sorted){this.extra=this.extra.sort(function(a,b){return a-b;});this.sorted=true;} +return this.extra[idx];}else{var sum=this.extra.length;for(var i=this.items.length-1;i>=0;i--){if(this.items[i]>0){sum+=this.items[i];if(sum>=target){return i;}}} +return 0;}},stddev:function(){var mean=this.mean();var s=0;for(var i=0;ithis.max||this.max===-1)?other.max:this.max;for(var i=0;i0){var total=0;for(var i in item){total+=this.items[i];} +return total;}else{return this.items[item];}},clear:function(){this.start=new Date();this.items={};this.length=0;},summary:function(){var items={};for(var i in this.items){items[i]=this.items[i];} +items.total=this.length;items.rps=Number((this.length/((new Date()-this.start)/1000)).toFixed(1));return items;},merge:function(other){for(var i in other.items){if(this.items[i]!==undefined){this.items[i]+=other.items[i];}else{this.items[i]=other.items[i];}} +this.length+=other.length;}};var Uniques=stats.Uniques=function Uniques(){this.type='Uniques';this.start=new Date();this.items={};this.uniques=0;this.length=0;};Uniques.prototype={put:function(item){if(this.items[item]!==undefined){this.items[item]++;}else{this.items[item]=1;this.uniques++;} +this.length++;},get:function(){return this.uniques;},clear:function(){this.items={};this.unqiues=0;this.length=0;},summary:function(){return{total:this.length,uniqs:this.uniques};},merge:function(other){for(var i in other.items){if(this.items[i]!==undefined){this.items[i]+=other.items[i];}else{this.items[i]=other.items[i];this.uniques++;}} +this.length+=other.length;}};var Peak=stats.Peak=function Peak(){this.type='Peak';this.peak=0;this.length=0;};Peak.prototype={put:function(item){if(this.peak0){this.interval.clear();} +this.lastSummary=null;},summary:function(){if(this.lastSummary){return this.lastSummary;} +return{interval:this.interval.summary(),cumulative:this.cumulative.summary()};},merge:function(other){this.interval.merge(other);this.cumulative.merge(other);}};var mergeStatsGroups=stats.mergeStatsGroups=function(sourceGroup,targetGroup){for(var statName in sourceGroup){var sourceStats=sourceGroup[statName];if(targetGroup[statName]===undefined){targetGroup[statName]=new stats[sourceStats.type](sourceStats.params);} +targetGroup[statName].merge(sourceStats);}};var roundRobin=stats.roundRobin=function(list){var r=list.slice();r.rridx=-1;r.get=function(){r.rridx=(r.rridx+1)%r.length;return r[r.rridx];};return r;};var randomString=stats.randomString=function(length){var s="";for(var i=0;i=1){z0=2*Math.random()-1;z1=2*Math.random()-1;s=z0*z0+z1*z1;} +return z0*Math.sqrt(-2*Math.log(s)/s)*stddev+mean;};var nextPareto=stats.nextPareto=function(min,max,shape){shape=shape||0.1;var l=1,h=Math.pow(1+max-min,shape),rnd=Math.random();while(rnd===0){rnd=Math.random();} +return Math.pow((rnd*(h-l)-h)/-(h*l),-1/shape)-1+min;};for(var i in stats){exports[i]=stats[i];} +var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var EventEmitter=require('events').EventEmitter;} +var LOOP_OPTIONS=exports.LOOP_OPTIONS={fun:undefined,argGenerator:undefined,args:undefined,rps:Infinity,duration:Infinity,numberOfTimes:Infinity,concurrency:1,concurrencyProfile:undefined,rpsProfile:undefined};var Loop=exports.Loop=function Loop(funOrSpec,args,conditions,rps){EventEmitter.call(this);if(typeof funOrSpec==='object'){var spec=util.defaults(funOrSpec,LOOP_OPTIONS);funOrSpec=spec.fun;args=spec.argGenerator?spec.argGenerator():spec.args;conditions=[];rps=spec.rps;if(spec.numberOfTimes>0&&spec.numberOfTimes0&&spec.duration=0)?val:Infinity;this.timeout_=Math.floor(1/rps*1000);if(this.restart_&&this.timeout_0&&this.spec.numberOfTimes0&&this.spec.durationtime){var dx=profile[i][0]-lastval[0],dy=profile[i][1]-lastval[1];return Math.floor((time-lastval[0])/dx*dy+lastval[1]);} +lastval=profile[i];} +return profile[profile.length-1][1];};MultiLoop.prototype.getProfileNextTimeout_=function(profile,time){if(time<0){return-time;} +var MIN_TIMEOUT=1000,lastval=[0,0];for(var i=0;itime){var dt=profile[i][0]-lastval[0],millisecondsPerUnitChange=dt/(profile[i][1]-lastval[1])*1000;return Math.max(MIN_TIMEOUT,Math.min(dt,millisecondsPerUnitChange));} +lastval=profile[i];} +return Infinity;};MultiLoop.prototype.update_=function(){var i,now=Math.floor((new Date()-this.startTime)/1000),concurrency=this.getProfileValue_(this.concurrencyProfile,now),rps=this.getProfileValue_(this.rpsProfile,now),timeout=Math.min(this.getProfileNextTimeout_(this.concurrencyProfile,now),this.getProfileNextTimeout_(this.rpsProfile,now));if(concurrencythis.concurrency){var loops=[];for(i=0;iself.threshold){self.stats.put(1);if(self.logfile){util.readStream(http.res,function(body){var logObj={ts:new Date(),req:{headers:http.req._header,body:http.req.body,},res:{statusCode:http.res.statusCode,headers:http.res.headers},latency:runTime};if(self.logResBody){logObj.res.body=body;} +self.logfile.put(JSON.stringify(logObj)+'\n');});}}};};StatsCollectors['slow-responses'].disableIntervalCollection=true;var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var START=require('../config').NODELOAD_CONFIG.START;var LogFile=require('../stats').LogFile;} +var StatsLogger=exports.StatsLogger=function StatsLogger(monitor,logNameOrObject){this.logNameOrObject=logNameOrObject||('results-'+START.getTime()+'-stats.log');this.monitor=monitor;this.logger_=this.log_.bind(this);};StatsLogger.prototype.start=function(){this.createdLog=(typeof this.logNameOrObject==='string');this.log=this.createdLog?new LogFile(this.logNameOrObject):this.logNameOrObject;this.monitor.on('update',this.logger_);return this;};StatsLogger.prototype.stop=function(){if(this.createdLog){this.log.close();this.log=null;} +this.monitor.removeListener('update',this.logger_);return this;};StatsLogger.prototype.log_=function(){var summary=this.monitor.interval.summary();this.log.put(JSON.stringify(summary)+',\n');};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var StatsCollectors=require('./collectors');var StatsLogger=require('./statslogger').StatsLogger;var EventEmitter=require('events').EventEmitter;} +var Monitor=exports.Monitor=function Monitor(){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.targets=[];this.setStats.apply(this,arguments);};util.inherits(Monitor,EventEmitter);Monitor.prototype.setStats=function(stats){var self=this,summarizeStats=function(){var summary={ts:new Date()};if(self.name){summary.name=self.name;} +util.forEach(this,function(statName,stats){summary[statName]=stats.summary();});return summary;};self.collectors=[];self.stats={};self.interval={};stats=(stats instanceof Array)?stats:Array.prototype.slice.call(arguments);stats.forEach(function(stat){var name=stat,params;if(typeof stat==='object'){name=stat.name;params=stat;} +var Collector=StatsCollectors[name];if(!Collector){throw new Error('No collector for statistic: '+name);} +if(!Collector.disableIntervalCollection){var intervalCollector=new Collector(params);self.collectors.push(intervalCollector);self.interval[name]=intervalCollector.stats;} +if(!Collector.disableCumulativeCollection){var cumulativeCollector=new Collector(params);self.collectors.push(cumulativeCollector);self.stats[name]=cumulativeCollector.stats;}});Object.defineProperty(this.stats,'summary',{enumerable:false,value:summarizeStats});Object.defineProperty(this.interval,'summary',{enumerable:false,value:summarizeStats});};Monitor.prototype.start=function(args){var self=this,endFuns=[],doStart=function(m,context){if(m.start){m.start(context,args);} +if(m.end){endFuns.push(function(result){return m.end(context,result);});}},monitoringContext={end:function(result){endFuns.forEach(function(f){f(result);});}};self.collectors.forEach(function(m){doStart(m,{});});return monitoringContext;};Monitor.prototype.monitorObjects=function(objs,startEvent,endEvent){var self=this;if(!(objs instanceof Array)){objs=util.argarray(arguments);startEvent=endEvent=null;} +startEvent=startEvent||'start';endEvent=endEvent||'end';objs.forEach(function(o){var mon;o.on(startEvent,function(args){mon=self.start(args);});o.on(endEvent,function(result){mon.end(result);});});return self;};Monitor.prototype.setLogFile=function(logNameOrObject){this.logNameOrObject=logNameOrObject;};Monitor.prototype.setLoggingEnabled=function(enabled){if(enabled){this.logger=this.logger||new StatsLogger(this,this.logNameOrObject).start();}else if(this.logger){this.logger.stop();this.logger=null;} +return this;};Monitor.prototype.update=function(){this.emit('update',this.interval,this.stats);util.forEach(this.interval,function(name,stats){if(stats.length>0){stats.clear();}});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var Monitor=require('./monitor').Monitor;var StatsLogger=require('./statslogger').StatsLogger;var EventEmitter=require('events').EventEmitter;} +var MonitorGroup=exports.MonitorGroup=function MonitorGroup(statsNames){EventEmitter.call(this);util.PeriodicUpdater.call(this);var summarizeStats=function(){var summary={ts:new Date()};util.forEach(this,function(monitorName,stats){summary[monitorName]={};util.forEach(stats,function(statName,stat){summary[monitorName][statName]=stat.summary();});});return summary;};this.statsNames=(statsNames instanceof Array)?statsNames:Array.prototype.slice.call(arguments);this.monitors={};this.stats={};this.interval={};Object.defineProperty(this.stats,'summary',{enumerable:false,value:summarizeStats});Object.defineProperty(this.interval,'summary',{enumerable:false,value:summarizeStats});};util.inherits(MonitorGroup,EventEmitter);MonitorGroup.prototype.initMonitors=function(monitorNames){var self=this;monitorNames=(monitorNames instanceof Array)?monitorNames:Array.prototype.slice.call(arguments);monitorNames.forEach(function(name){self.monitors[name]=new Monitor(self.statsNames);self.stats[name]=self.monitors[name].stats;self.interval[name]=self.monitors[name].interval;});return self;};MonitorGroup.prototype.start=function(monitorName,args){monitorName=monitorName||'';if(!this.monitors[monitorName]){this.initMonitors([monitorName]);} +return this.monitors[monitorName].start(args);};MonitorGroup.prototype.monitorObjects=function(objs,startEvent,endEvent){var self=this,ctxs={};if(!(objs instanceof Array)){objs=util.argarray(arguments);startEvent=endEvent=null;} +startEvent=startEvent||'start';endEvent=endEvent||'end';objs.forEach(function(o){o.on(startEvent,function(monitorName,args){ctxs[monitorName]=self.start(monitorName,args);});o.on(endEvent,function(monitorName,result){if(ctxs[monitorName]){ctxs[monitorName].end(result);}});});return self;};MonitorGroup.prototype.setLogFile=function(logNameOrObject){this.logNameOrObject=logNameOrObject;};MonitorGroup.prototype.setLoggingEnabled=function(enabled){if(enabled){this.logger=this.logger||new StatsLogger(this,this.logNameOrObject).start();}else if(this.logger){this.logger.stop();this.logger=null;} +return this;};MonitorGroup.prototype.update=function(){this.emit('update',this.interval,this.stats);util.forEach(this.monitors,function(name,m){m.update();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var config=require('./config');var http=require('http');var fs=require('fs');var util=require('./util');var qputs=util.qputs;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;} +var HttpServer=exports.HttpServer=function HttpServer(){this.routes=[];this.running=false;};util.inherits(HttpServer,EventEmitter);HttpServer.prototype.start=function(port,hostname){if(this.running){return;} +this.running=true;var self=this;port=port||8000;self.hostname=hostname||'localhost';self.port=port;self.connections=[];self.server=http.createServer(function(req,res){self.route_(req,res);});self.server.on('connection',function(c){c.on('close',function(){var idx=self.connections.indexOf(c);if(idx!==-1){self.connections.splice(idx,1);}});self.connections.push(c);});self.server.listen(port,hostname);self.emit('start',self.hostname,self.port);return self;};HttpServer.prototype.stop=function(){if(!this.running){return;} +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){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;} +this.updateInterval=0;this.update();qputs('Done.');if(!this.keepAlive){HTTP_SERVER.stop();} +this.emit('end');};var extendClient=exports.extendClient=function(client){var wrappedRequest=client.request;client.request=function(method,url){var request=wrappedRequest.apply(client,arguments),wrappedWrite=request.write,wrappedEnd=request.end,track=function(data){if(data){request.emit('write',data);request.body+=data.toString();}};request.method=method;request.path=url;request.body='';request.write=function(data,encoding){track(data);return wrappedWrite.apply(request,arguments);};request.end=function(data,encoding){track(data);return wrappedEnd.apply(request,arguments);};return request;};return client;};var createClient=exports.createClient=function(){return extendClient(util.createReconnectingClient.apply(this,arguments));};function generateConnection(host,port,detectClientErrors){return function(){var client=createClient(port,host);if(detectClientErrors){client.on('error',function(err){qputs('WARN: Error during HTTP request: '+(err?err.toString():'unknown'));});client.on('reconnect',function(oldclient){if(oldclient._outgoing){oldclient._outgoing.forEach(function(req){if(req instanceof http.ClientRequest){req.emit('response',new EventEmitter());}});}});} +return client;};} +function requestGeneratorLoop(generator){return function(finished,client){var running=true,timeoutId,request=generator(client);var callFinished=function(response){if(running){running=false;clearTimeout(timeoutId);response.statusCode=response.statusCode||0;finished({req:request,res:response});}};if(request){if(request.timeout>0){timeoutId=setTimeout(function(){callFinished(new EventEmitter());},request.timeout);} +request.on('response',function(response){callFinished(response);});request.end();}else{finished(null);}};} +var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var EventEmitter=require('events').EventEmitter;} +var Endpoint=exports.Endpoint=function Endpoint(server,hostAndPort){EventEmitter.call(this);var self=this,parts=hostAndPort?hostAndPort.split(':'):[];self.id=util.uid();self.server=server;self.methodNames=[];self.methods={};self.setStaticParams([]);self.state='initialized';self.__defineGetter__('url',function(){return self.url_;});self.hostname_=parts[0];self.port_=parts[1];self.basepath_='/remote/'+self.id;self.handler_=self.handle.bind(self);};util.inherits(Endpoint,EventEmitter);Endpoint.prototype.setStaticParams=function(params){this.staticParams_=params instanceof Array?params:[params];};Endpoint.prototype.defineMethod=function(name,fun){this.methodNames.push(name);this.methods[name]=fun;};Endpoint.prototype.start=function(){if(this.state!=='initialized'){return;} +this.url_=url.format({protocol:'http',hostname:this.hostname_||this.server.hostname,port:this.port_||this.server.port,pathname:this.basepath_});this.route_='^'+this.basepath_+'/?';this.server.addRoute(this.route_,this.handler_);this.context={};if(this.methods['setup']){this.methods['setup'].apply(this.context,this.staticParams_);} +this.state='started';this.emit('start');};Endpoint.prototype.end=function(){if(this.state!=='started'){return;} +this.server.removeRoute(this.route_,this.handler_);this.state='initialized';this.emit('end');};Endpoint.prototype.handle=function(path,req,res){var self=this;if(path===self.basepath_){if(req.method==='DELETE'){self.end();res.writeHead(204,{'Content-Length':0});res.end();}else{res.writeHead(405);res.end();}}else if(req.method==='POST'){var method=path.slice(this.basepath_.length+1);if(self.methods[method]){util.readStream(req,function(params){var status=200,ret;try{params=JSON.parse(params);}catch(e1){res.writeHead(400);res.end();return;} +params=(params instanceof Array)?params:[params];ret=self.methods[method].apply(self.context,self.staticParams_.concat(params));try{ret=(ret===undefined)?'':JSON.stringify(ret);}catch(e2){ret=e2.toString();status=500;} +res.writeHead(status,{'Content-Length':ret.length,'Content-Type':'application/json'});res.end(ret);});}else{res.writeHead(404);res.end();}}else{res.writeHead(405);res.end();}};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var http=require('http');var util=require('../util');var EventEmitter=require('events').EventEmitter;var qputs=util.qputs;} +var DEFAULT_RETRY_INTERVAL_MS=2000;var EndpointClient=exports.EndpointClient=function EndpointClient(host,port,basepath){EventEmitter.call(this);this.host=host;this.port=port;this.client=util.createReconnectingClient(port,host);this.client.on('error',this.emit.bind(this,'error'));this.basepath=basepath||'';this.methodNames=[];this.retryInterval=DEFAULT_RETRY_INTERVAL_MS;this.setStaticParams([]);};util.inherits(EndpointClient,EventEmitter);EndpointClient.prototype.destroy=function(){this.client.destroy();this.emit('end');};EndpointClient.prototype.rawRequest=function(){return this.client.request.apply(this.client,arguments);};EndpointClient.prototype.setStaticParams=function(params){this.staticParams_=params instanceof Array?params:[params];};EndpointClient.prototype.defineMethod=function(name){var self=this;self[name]=function(){var req=self.client.request('POST',self.basepath+'/'+name),params=self.staticParams_.concat(util.argarray(arguments));req.on('response',function(res){if(res.statusCode!==200){self.emit('clientError',res);}});req.end(JSON.stringify(params));return req;};self.methodNames.push(name);return self;};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var EventEmitter=require('events').EventEmitter;var EndpointClient=require('./endpointclient').EndpointClient;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var Slave=exports.Slave=function Slave(id,host,port,masterEndpoint,pingInterval){EventEmitter.call(this);this.id=id;this.client=new EndpointClient(host,port);this.client.on('error',this.emit.bind(this,'slaveError'));this.masterEndpoint=masterEndpoint;this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;this.methodDefs=[];this.state='initialized';};util.inherits(Slave,EventEmitter);Slave.prototype.start=function(){if(this.masterEndpoint&&this.masterEndpoint.state!=='started'){throw new Error('Slave must be started after its Master.');} +var self=this,masterUrl=self.masterEndpoint?self.masterEndpoint.url:null,masterMethods=self.masterEndpoint?self.masterEndpoint.methodNames:[],req=self.client.rawRequest('POST','/remote');req.end(JSON.stringify({id:self.id,master:masterUrl,masterMethods:masterMethods,slaveMethods:self.methodDefs,pingInterval:self.pingInterval}));req.on('response',function(res){if(!res.headers['location']){self.emit('error',new Error('Remote slave does not have proper /remote handler.'));} +self.client.basepath=url.parse(res.headers['location']).pathname;self.state='started';self.emit('start');});self.state='connecting';};Slave.prototype.end=function(){var self=this,req=self.client.rawRequest('DELETE',self.client.basepath),done=function(){self.client.destroy();self.client.basepath='';self.state='initialized';self.emit('end');};self.client.once('error',function(e){self.emit('slaveError',e);done();});req.on('response',function(res){if(res.statusCode!==204){self.emit('slaveError',new Error('Error stopping slave.'),res);} +done();});req.end();};Slave.prototype.defineMethod=function(name,fun){var self=this;self.client.defineMethod(name,fun);self[name]=function(){return self.client[name].apply(self.client,arguments);};self.methodDefs.push({name:name,fun:fun.toString()});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var Slave=require('./slave').Slave;var EventEmitter=require('events').EventEmitter;} +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);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;} +slaveNodes.push(slaveNode);slaveNode.on('end',function(){var idx=slaveNodes.indexOf(slaveNode);if(idx!==-1){slaveNodes.splice(idx,1);}});res.writeHead(201,{'Location':slaveNode.url,'Content-Length':0,});res.end();});}else if(req.method==='GET'){res.writeHead(200,{'Content-Type':'application/json'});res.end(JSON.stringify(slaveNodes.map(function(s){return s.url;})));}else{res.writeHead(405);res.end();}});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EventEmitter=require('events').EventEmitter;var SlaveNode=require('./slavenode').SlaveNode;var Slaves=require('./slaves').Slaves;var qputs=util.qputs;var HTTP_SERVER=require('../http').HTTP_SERVER;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var Cluster=exports.Cluster=function Cluster(spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this,masterSpec=spec.master||{},slavesSpec=spec.slaves||{hosts:[]},masterHost=spec.master&&spec.master.host||'localhost';self.pingInterval=spec.pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;self.server=spec.server||HTTP_SERVER;self.masterEndpoint=new Endpoint(self.server,masterHost);self.slaves=new Slaves(self.masterEndpoint,self.pingInterval);self.slaveState_={};self.masterEndpoint.setStaticParams([self.slaves]);self.masterEndpoint.defineMethod('updateSlaveState_',self.updateSlaveState_.bind(self));util.forEach(masterSpec,function(method,val){if(typeof val==='function'){self.masterEndpoint.defineMethod(method,val);}});slavesSpec.hosts.forEach(function(h){self.slaves.add(h);});util.forEach(spec.slaves,function(method,val){if(typeof val==='function'){self.slaves.defineMethod(method,val);self[method]=function(){self.slaves[method].apply(self.slaves,arguments);};}});self.slaves.slaves.forEach(function(s){if(!self.slaveState_[s.id]){self.slaveState_[s.id]={alive:true,aliveSinceLastCheck:false};}});self.slaves.on('start',function(){self.state='started';self.emit('start');});self.slaves.on('end',function(){self.masterEndpoint.end();self.state='stopped';self.emit('end');});self.slaves.on('slaveError',function(slave,err){self.emit('slaveError',slave,err);});if(self.server.running){self.state='initialized';process.nextTick(function(){self.emit('init');});}else{self.state='initializing';self.server.on('start',function(){self.state='initialized';self.emit('init');});}};util.inherits(Cluster,EventEmitter);Cluster.prototype.started=function(){return this.state==='started';};Cluster.prototype.start=function(){if(!this.server.running){throw new Error('A Cluster can only be started after it has emitted \'init\'.');} +this.masterEndpoint.start();this.slaves.start();this.updateInterval=this.pingInterval*4;};Cluster.prototype.end=function(){this.state='stopping';this.updateInterval=0;this.slaves.end();};Cluster.prototype.update=function(){var self=this;util.forEach(self.slaveState_,function(id,s){if(!s.aliveSinceLastCheck&&s.alive){s.alive=false;self.emit('slaveError',self.slaves[id],null);}else if(s.aliveSinceLastCheck){s.aliveSinceLastCheck=false;s.alive=true;}});};Cluster.prototype.updateSlaveState_=function(slaves,slaveId,state){var slave=slaves[slaveId];if(slave){var previousState=this.slaveState_[slaveId].state;this.slaveState_[slaveId].state=state;this.slaveState_[slaveId].aliveSinceLastCheck=true;if(previousState!==state){this.emit('slaveState',slave,state);if(state==='running'||state==='done'){this.emitWhenAllSlavesInState_(state);}}}else{qputs('WARN: ignoring message from unexpected slave instance '+slaveId);}};Cluster.prototype.emitWhenAllSlavesInState_=function(state){var allSlavesInSameState=true;util.forEach(this.slaveState_,function(id,s){if(s.state!==state&&s.alive){allSlavesInSameState=false;}});if(allSlavesInSameState){this.emit(state);}};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var installRemoteHandler=require('./slavenode').installRemoteHandler;var HTTP_SERVER=require('../http').HTTP_SERVER;} +installRemoteHandler(HTTP_SERVER);var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var stats=require('../stats');var reporting=require('../reporting');var run=require('../loadtesting').run;var Cluster=require('./cluster').Cluster;var EventEmitter=require('events').EventEmitter;var StatsLogger=require('../monitoring/statslogger').StatsLogger;var Report=reporting.Report;var qputs=util.qputs;var REPORT_MANAGER=reporting.REPORT_MANAGER;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var LoadTestCluster=exports.LoadTestCluster=function LoadTestCluster(masterHost,slaveHosts,masterHttpServer,slaveUpdateInterval){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.masterHost=masterHost;self.slaveHosts=slaveHosts;self.masterHttpServer=self.masterHttpServer;self.slaveUpdateInterval=slaveUpdateInterval||NODELOAD_CONFIG.MONITOR_INTERVAL_MS;};util.inherits(LoadTestCluster,EventEmitter);LoadTestCluster.prototype.run=function(specs){var self=this;if(!specs){throw new Error('No tests.');} +if(self.cluster&&self.cluster.started()){throw new Error('Already started.');} +self.specs=(specs instanceof Array)?specs:util.argarray(arguments);self.cluster=new Cluster(self.getClusterSpec_());self.cluster.on('init',function(){self.cluster.on('start',function(){self.startTests_();self.updateInterval=self.slaveUpdateInterval;self.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED);});self.cluster.start();});self.cluster.on('running',function(){self.emit('start');});self.cluster.on('done',function(){self.setLoggingEnabled(false);self.updateInterval=0;self.update();self.end();});self.cluster.on('end',function(){self.emit('end');});};LoadTestCluster.prototype.end=function(){this.cluster.stopTests();this.cluster.end();};LoadTestCluster.prototype.setLogFile=function(logNameOrObject){this.logNameOrObject=logNameOrObject;};LoadTestCluster.prototype.setLoggingEnabled=function(enabled){if(enabled){this.logger=this.logger||new StatsLogger(this,this.logNameOrObject).start();}else if(this.logger){this.logger.stop();this.logger=null;} +return this;};LoadTestCluster.prototype.update=function(){var self=this;self.emit('update',self.interval,self.stats);util.forEach(self.stats,function(testName,stats){var report=self.reports[testName];var interval=self.interval[testName];util.forEach(stats,function(statName,stat){util.forEach(stat.summary(),function(name,val){report.summary[testName+' '+statName+' '+name]=val;});report.getChart(statName).put(interval[statName].summary());});});util.forEach(self.interval,function(testName,stats){util.forEach(stats,function(statName,stat){stat.clear();});});util.qprint('.');};LoadTestCluster.prototype.startTests_=function(){var self=this,summarizeStats=function(){var summary={ts:new Date()};util.forEach(this,function(testName,stats){summary[testName]={};util.forEach(stats,function(statName,stat){summary[testName][statName]=stat.summary();});});return summary;};this.reports={};this.interval={};this.stats={};this.cluster.runTests(this.stringify_(this.specs));Object.defineProperty(this.stats,'summary',{enumerable:false,value:summarizeStats});Object.defineProperty(this.interval,'summary',{enumerable:false,value:summarizeStats});};LoadTestCluster.prototype.stringify_=function(obj){switch(typeof obj){case'function':return obj.toString();case'object':if(obj instanceof Array){var self=this;return['[',obj.map(function(x){return self.stringify_(x);}),']'].join('');}else if(obj===null){return'null';} +var ret=['{'];for(var i in obj){ret.push(i+':'+this.stringify_(obj[i])+',');} +ret.push('}');return ret.join('');case'number':if(isFinite(obj)){return String(obj);} +return'Infinity';default:return JSON.stringify(obj);}};LoadTestCluster.prototype.getClusterSpec_=function(){var self=this;return{master:{host:self.masterHost,sendStats:function(slaves,slaveId,interval){util.forEach(interval,function(testName,remoteInterval){if(!self.stats[testName]){self.stats[testName]={};self.interval[testName]={};self.reports[testName]=new Report(testName);REPORT_MANAGER.addReport(self.reports[testName]);} +stats.mergeStatsGroups(remoteInterval,self.interval[testName]);stats.mergeStatsGroups(remoteInterval,self.stats[testName]);});}},slaves:{hosts:self.slaveHosts,setup:function(){if(typeof BUILD_AS_SINGLE_FILE==='undefined'||BUILD_AS_SINGLE_FILE===false){this.nlrun=require('../loadtesting').run;}else{this.nlrun=run;}},runTests:function(master,specsStr){var specs;try{eval('specs='+specsStr);}catch(e){qputs('WARN: Ignoring invalid remote test specifications: '+specsStr+' - '+e.toString());return;} +if(this.state==='running'){qputs('WARN: Already running -- ignoring new test specifications: '+specsStr);return;} +qputs('Received remote test specifications: '+specsStr);var self=this;self.state='running';self.loadtest=self.nlrun(specs);self.loadtest.keepAlive=true;self.loadtest.on('update',function(interval,stats){master.sendStats(interval);});self.loadtest.on('end',function(){self.state='done';});},stopTests:function(master){if(this.loadtest){this.loadtest.stop();}}},server:self.masterHttpServer,pingInterval:self.slaveUpdateInterval};}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb55e58 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "nodeload", + "version": "0.2.0", + "description": "Load testing library for node.js", + "url": "https://github.com/benschmaus/nodeload", + "engines": { + "node": ">=0.3.0" + }, + "contributors": [ + "Benjamin Schmaus ", + "Jonathan Lee ", + "Robert Newson ", + "Michael Mattozzi " + ], + "repository": { + "type": "git", + "url": "http://github.com/benschmaus/nodeload" + }, + "main": "./nodeload.js", + "bin": { + "nodeload.js": "./nodeload.js", + "nl.js": "./nl.js" + }, + "modules": { + "loop": "./lib/loop", + "stats": "./lib/stats", + "monitoring": "./lib/monitoring", + "http": "./lib/http", + "reporting": "./lib/reporting", + "remote": "./lib/remote" + }, + "scripts": { + "test": "expresso", + "preinstall": "make clean compile" + }, + "devDependencies": { + "expresso": ">=0.6.4" + }, + "dependencies": { + } +} \ No newline at end of file diff --git a/deps/jsmin.js b/scripts/jsmin.js similarity index 100% rename from deps/jsmin.js rename to scripts/jsmin.js diff --git a/scripts/process_tpl.js b/scripts/process_tpl.js new file mode 100755 index 0000000..4546eb6 --- /dev/null +++ b/scripts/process_tpl.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node +if (process.argv.length < 4) { + console.log('Usage: ./scripts/process_tpl '); + process.exit(1); +} + +var varname = process.argv[2], src = process.argv[3]; +var file = require('fs').readFileSync(src).toString(); +require('util').puts('var ' + varname + '= exports.' + varname + '=' + JSON.stringify(file) + ';'); \ No newline at end of file diff --git a/src/api.js b/src/api.js deleted file mode 100644 index 2ba7004..0000000 --- a/src/api.js +++ /dev/null @@ -1,235 +0,0 @@ -// ------------------------------------ -// Main HTTP load testing interface -// ------------------------------------ -// -// This file defines addTest, addRamp, startTests, runTest and traceableRequest. -// -// This file defines the public API for using nodeload to construct load tests. -// - -/** TEST_DEFAULTS defines all of the parameters that can be set in a test specifiction passed to -addTest(spec). By default, a test will GET localhost:8080/ as fast as possible with 10 users for 2 -minutes. */ -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_DEFAULTS defines all of the parameters that can be set in a ramp-up specifiction passed to -addRamp(spec). By default, a ramp will add 100 requests/sec over 10 seconds, adding 1 user each second. -*/ -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. -}; - -/** addTest(spec) is the primary method to create a load test with nodeloadlib. See TEST_DEFAULTS for a -list of the configuration values that can be provided in the test specification, spec. Remember to call -startTests() to kick off the tests defined though addTest(spec)/addRamp(spec). - -@return A test object: - { - spec: the spec passed to addTest() to create this test - stats: { - 'latency': Reportable(Histogram), - 'result-codes': Reportable(ResultsCounter}, - 'uniques': Reportable(Uniques), - 'concurrency': Reportable(Peak) - } - jobs: jobs scheduled in SCHEDULER for this test - fun: the function being run by each job in jobs - } -*/ -var addTest = exports.addTest = function(spec) { - Utils.defaults(spec, TEST_DEFAULTS); - - var req = function(client) { - if (spec.requestGenerator !== null) { - return spec.requestGenerator(client); - } - - return traceableRequest(client, spec.method, spec.path, { 'host': spec.host }, spec.requestData); - }, - test = { - spec: spec, - stats: {}, - jobs: [], - fun: spec.requestLoop || LoopUtils.requestGeneratorLoop(req) - }; - - if (spec.stats.indexOf('latency') >= 0) { - var l = new Reportable([Histogram, spec.latencyConf], spec.name + ': Latency', true); - test.fun = LoopUtils.monitorLatenciesLoop(l, test.fun); - test.stats['latency'] = l; - } - if (spec.stats.indexOf('result-codes') >= 0) { - var rc = new Reportable(ResultsCounter, spec.name + ': Result codes', true); - test.fun = LoopUtils.monitorResultsLoop(rc, test.fun); - test.stats['result-codes'] = rc; - } - if (spec.stats.indexOf('concurrency') >= 0) { - var conc = new Reportable(Peak, spec.name + ': Concurrency', true); - test.fun = LoopUtils.monitorConcurrencyLoop(conc, test.fun); - test.stats['concurrency'] = conc; - } - if (spec.stats.indexOf('uniques') >= 0) { - var uniq = new Reportable(Uniques, spec.name + ': Uniques', false); - test.fun = LoopUtils.monitorUniqueUrlsLoop(uniq, test.fun); - test.stats['uniques'] = uniq; - } - if (spec.stats.indexOf('bytes') >= 0) { - var reqbytes = new Reportable(Accumulator, spec.name + ': Request Bytes', true); - test.fun = LoopUtils.monitorByteSentLoop(reqbytes, test.fun); - test.stats['request-bytes'] = reqbytes; - - var resbytes = new Reportable(Accumulator, spec.name + ': Response Bytes', true); - test.fun = LoopUtils.monitorByteReceivedLoop(resbytes, test.fun); - test.stats['response-bytes'] = resbytes; - } - if (spec.successCodes !== null) { - test.fun = LoopUtils.monitorHttpFailuresLoop(spec.successCodes, test.fun); - } - - test.jobs = SCHEDULER.schedule({ - fun: test.fun, - argGenerator: function() { return http.createClient(spec.port, spec.host) }, - concurrency: spec.numClients, - rps: spec.targetRps, - duration: spec.timeLimit, - numberOfTimes: spec.numRequests, - delay: spec.delay - }); - - TEST_MONITOR.addTest(test); - return test; -}; - -/** addRamp(spec) defines a step-wise ramp-up of the load in a given test defined by a pervious -addTest(spec) call. See RAMP_DEFAULTS for a list of the parameters that can be specified in the ramp -specification, spec. */ -var addRamp = exports.addRamp = function(spec) { - Utils.defaults(spec, RAMP_DEFAULTS); - - var rampStep = LoopUtils.funLoop(function() { - SCHEDULER.schedule({ - fun: spec.test.fun, - argGenerator: function() { return http.createClient(spec.test.spec.port, spec.test.spec.host) }, - rps: spec.rpsPerStep, - concurrency: spec.clientsPerStep, - monitored: false - })}), - ramp = { - spec: spec, - jobs: [], - fun: rampStep - }; - - ramp.jobs = SCHEDULER.schedule({ - fun: rampStep, - delay: spec.delay, - duration: spec.timeLimit, - rps: spec.numberOfSteps / spec.timeLimit, - monitored: false - }); - - return ramp; -}; - -/** Start all tests were added via addTest(spec) and addRamp(spec). When all tests complete, callback -will be called. If stayAliveAfterDone is true, then the nodeload HTTP server will remain running. -Otherwise, the server will automatically terminate once the tests are finished. */ -var startTests = exports.startTests = function(callback, stayAliveAfterDone) { - TEST_MONITOR.start(); - SCHEDULER.startAll(testsComplete(callback, stayAliveAfterDone)); -}; - -/** A convenience function equivalent to addTest() followed by startTests() */ -var runTest = exports.runTest = function(spec, callback, stayAliveAfterDone) { - var t = addTest(spec); - startTests(callback, stayAliveAfterDone); - return t; -}; - -/** Use traceableRequest instead of built-in node.js `http.Client.request()` when tracking the 'uniques' -statistic. It allows URLs to be properly tracked. */ -var traceableRequest = exports.traceableRequest = function(client, method, path, headers, body) { - headers = headers || {}; - body = body || ''; - headers['content-length'] = headers['content-length'] || body.length; - - var request = client.request(method, path, headers); - request.headers = headers; - request.path = path; - request.body = body; - request.write(body); - - return request; -}; - - - -// ================= -// Private -// ================= -/** Returns a callback function that should be called at the end of the load test. It calls the user -specified callback function and sets a timer for terminating the nodeload process if no new tests are -started by the user specified callback. */ -function testsComplete(callback, stayAliveAfterDone) { - return function() { - TEST_MONITOR.stop(); - - callback && callback(); - - if (!stayAliveAfterDone && !SLAVE_CONFIG) { - checkToExitProcess(); - } - }; -} - -/** Wait 3 seconds and check if anyone has restarted SCHEDULER (i.e. more tests). End process if not. */ -function checkToExitProcess() { - setTimeout(function() { - if (!SCHEDULER.running) { - qputs('\nFinishing...'); - LOGS.close(); - HTTP_SERVER.stop(); - setTimeout(process.exit, 500); - } - }, 3000); -} \ No newline at end of file diff --git a/src/evloops.js b/src/evloops.js deleted file mode 100644 index 2776a03..0000000 --- a/src/evloops.js +++ /dev/null @@ -1,276 +0,0 @@ -// ----------------------------------------- -// Event-based looping -// ----------------------------------------- -// -// This file defines ConditionalLoop, LoopConditions, and LoopUtils. -// -// Nodeload uses the node.js event loop to schedule iterations of a particular function. In order for -// this to work, the function must cooperate by accepting a loopFun as its first argument and call -// loopFun() when it completes each iteration. This is refered to as "event-based looping" in nodeload. -// -// This file defines the generic ConditionalLoop class for looping on an arbitrary function, and a number -// of other event based loops for predefined tasks, such as tracking the latency of the loop body. -// - -/** ConditionalLoop wraps an arbitrary function to be executed in a loop. Each iteration of the loop is -scheduled in the node.js event loop using process.nextTick(), which allows other events in the loop to be -handled as the loop executes. - -@param fun a function: - - function(loopFun, args) { - ... - loopFun(result); - } - - that calls loopFun(result) when it finishes. Use LoopUtils.funLoop() to wrap a - function for use in a ConditionalLoop. -@param args passed as-is as the second argument to fun -@param conditions a list of functions that are called at the beginning of every loop. If any - function returns false, the loop terminates. See LoopConditions. -@param delay number of seconds before the first iteration of fun is executed */ -var ConditionalLoop = exports.ConditionalLoop = function(fun, args, conditions, delay) { - this.fun = fun; - this.args = args; - this.conditions = conditions || []; - this.delay = delay; - this.stopped = true; - this.callback = null; -} -ConditionalLoop.prototype = { - /** Start executing "ConditionalLoop.fun" with the arguments, "ConditionalLoop.args", until any - condition in "ConditionalLoop.conditions" returns false. The loop begins after a delay of - "ConditionalLoop.delay" seconds. When the loop completes, the user defined function, callback is - called. */ - start: function(callback) { - this.callback = callback; - this.stopped = false; - if (this.delay && this.delay > 0) { - var loop = this; - setTimeout(function() { loop.loop_() }, this.delay * 1000); - } else { - this.loop_(); - } - }, - stop: function() { - this.stopped = true; - }, - /** Calls each function in ConditionalLoop.conditions. Returns false if any function returns false */ - checkConditions_: function() { - return !this.stopped && this.conditions.every(function(c) { return c(); }); - }, - /** Checks conditions and schedules the next loop iteration */ - loop_: function() { - if (this.checkConditions_()) { - var loop = this; - process.nextTick(function() { loop.fun(function() { loop.loop_() }, loop.args) }); - } else { - this.callback && this.callback(); - } - } -} - - -/** LoopConditions contains predefined functions that can be used in ConditionalLoop.conditions */ -var LoopConditions = exports.LoopConditions = { - /** Returns false after a given number of seconds */ - timeLimit: function(seconds) { - var start = new Date(); - return function() { - return (seconds === Infinity) || ((new Date() - start) < (seconds * 1000)); - }; - }, - /** Returns false after a given number of iterations */ - maxExecutions: function(numberOfTimes) { - var counter = 0; - return function() { - return (numberOfTimes === Infinity) || (counter++ < numberOfTimes) - }; - } -}; - - -/** LoopUtils contains helpers for dealing with ConditionalLoop loop functions */ -var LoopUtils = exports.LoopUtils = { - /** A wrapper for any existing function so it can be used by ConditionalLoop. e.g.: - myfun = function(x) { return x+1; } - new ConditionalLoop(LoopUtils.funLoop(myfun), args, [LoopConditions.timeLimit(10)], 0) */ - funLoop: function(fun) { - return function(loopFun, args) { - loopFun(fun(args)); - } - }, - /** Wrap a loop function. For each iteration, calls startRes = start(args) before calling fun(), and - calls finish(result-from-fun, startRes) when fun() finishes. */ - loopWrapper: function(fun, start, finish) { - return function(loopFun, args) { - var startRes = start && start(args), - finishFun = function(result) { - if (result === undefined) { - qputs('Function result is null; did you forget to call loopFun(result)?'); - } - - finish && finish(result, startRes); - - loopFun(result); - } - fun(finishFun, args); - } - }, - /** Wrapper for executing a ConditionalLoop function rps times per second. */ - rpsLoop: function(rps, fun) { - var timeout = 1/rps * 1000, - finished = false, - lagging = false, - finishFun = function(loopFun) { - finished = true; - if (lagging) { - loopFun(); - } - }; - - return function(loopFun, args) { - finished = false; - lagging = (timeout <= 0); - if (!lagging) { - setTimeout(function() { - lagging = !finished; - if (!lagging) { - loopFun(); - } - }, timeout); - } - fun(function() { finishFun(loopFun) }, args); - } - }, - /** Wrapper for request generator function, "generator" - - @param generator A function: - - function(http.Client) -> http.ClientRequest - - The http.Client is provided by nodeload. The http.ClientRequest may contain an extra - .timeout field specifying the maximum milliseconds to wait for a response. - - @return A ConditionalLoop function, function(loopFun, http.Client). Each iteration makes an HTTP - request by calling generator. loopFun({req: http.ClientRequest, res: http.ClientResponse}) is - called when the HTTP response is received or the request times out. */ - requestGeneratorLoop: function(generator) { - return function(loopFun, client) { - var request = generator(client), - timedOut = false, - timeoutId = null; - - if (!request) { - qputs('WARN: HTTP request is null; did you forget to call return request?'); - loopfun(null); - } else { - if (request.timeout > 0) { - timeoutId = setTimeout(function() { - timedOut = true; - loopFun({req: request, res: {statusCode: 0}}); - }, request.timeout); - } - request.on('response', function(response) { - if (!timedOut) { - if (timeoutId !== null) { - clearTimeout(timeoutId); - } - loopFun({req: request, res: response}); - } - }); - request.end(); - } - } - }, - - // ------------------------------------ - // Monitoring loops - // ------------------------------------ - /** Time each call to fun and write the runtime information to latencies, which is generally a - stats.js#Histogram object. */ - monitorLatenciesLoop: function(latencies, fun) { - var start = function() { return new Date() } - var finish = function(result, start) { latencies.put(new Date() - start) }; - return LoopUtils.loopWrapper(fun, start, finish); - }, - /** Each call to fun should return an object {res: http.ClientResponse}. This function tracks the http - response codes and writes them to results, which is generally a stats.js#ResultsCounter object. */ - monitorResultsLoop: function(results, fun) { - var finish = function(http) { results.put(http.res.statusCode) }; - return LoopUtils.loopWrapper(fun, null, finish); - }, - /** Each call to fun should return an object {res: http.ClientResponse}. This function reads the http - response body and writes its size to bytesReceived, which is generally a stats.js#Accumlator object. */ - monitorByteReceivedLoop: function(bytesReceived, fun) { - var finish = function(http) { - http.res.on('data', function(chunk) { - bytesReceived.put(chunk.length); - }); - }; - return LoopUtils.loopWrapper(fun, null, finish); - }, - /** Each call to fun should return an object {res: http.ClientResponse}. This function reads the http - response body and writes its size to bytesSent, which is generally a stats.js#Accumlator object. */ - monitorByteSentLoop: function(bytesSent, fun) { - var finish = function(http) { - if (http.req.headers && http.req.headers['content-length']) { - bytesSent.put(http.req.headers['content-length']); - } - }; - return LoopUtils.loopWrapper(fun, null, finish); - }, - /** Tracks the concurrency of calls to fun and writes it to concurrency, which is generally a - stats.js#Peak object. */ - monitorConcurrencyLoop: function(concurrency, fun) { - var c = 0; - var start = function() { c++; }; - var finish = function() { concurrency.put(c--) }; - return LoopUtils.loopWrapper(fun, start, finish); - }, - /** Tracks the rate of calls to fun and writes it to rate, which is generally a stats.js#Rate object. */ - monitorRateLoop: function(rate, fun) { - var finish = function() { rate.put() }; - return LoopUtils.loopWrapper(fun, null, finish); - }, - /** Each call to fun should return an object {res: http.ClientResponse}. This function reads the http - response code and writes the full request and response to "log" if the response code is not in the - "successCodes" list. "log" is generally a stats.js#LogFile object. */ - monitorHttpFailuresLoop: function(successCodes, fun, log) { - log = log || LOGS.ERROR_LOG; - var finish = function(http) { - var body = ""; - if (successCodes.indexOf(http.res.statusCode) < 0) { - http.res.on('data', function(chunk) { - body += chunk; - }); - http.res.on('end', function(chunk) { - log.put(JSON.stringify({ - ts: new Date(), - req: { - // Use the _header "private" member of http.ClientRequest, which is available - // in the current node release (v0.2.2, 9/30/10). This is the only way to - // reliably get all of the request headers, since ClientRequest will actually - // add headers beyond what the user specifies in certain conditions, like - // Connection and Transfer-Encoding. - headers: http.req._header, - body: http.req.body, - }, - res: { - statusCode: http.res.statusCode, - headers: http.res.headers, - body: body - } - }) + '\n'); - }); - } - }; - return LoopUtils.loopWrapper(fun, null, finish); - }, - /** Each call to fun should return an object {req: http.ClientRequest}. This function writes the request - URL to uniqs which is generally a stats.js#Uniques object. */ - monitorUniqueUrlsLoop: function(uniqs, fun) { - var finish = function(http) { uniqs.put(http.req.path) }; - return LoopUtils.loopWrapper(fun, null, finish); - } -} \ No newline at end of file diff --git a/src/header.js b/src/header.js deleted file mode 100644 index bf1fcbd..0000000 --- a/src/header.js +++ /dev/null @@ -1,7 +0,0 @@ -var sys = require('sys'), - http = require('http'), - fs = require('fs'), - events = require('events'), - querystring = require('querystring'); - -var START = new Date().getTime(); diff --git a/src/http.js b/src/http.js deleted file mode 100644 index 899593b..0000000 --- a/src/http.js +++ /dev/null @@ -1,69 +0,0 @@ -// ------------------------------------ -// HTTP Server -// ------------------------------------ -// -// This file defines HTTP_SERVER. -// -// This file defines and starts the nodeload HTTP server. -// - -/** The global HTTP server. By default, HTTP_SERVER knows how to return static files from the current -directory. Add new routes to HTTP_SERVER.route_(). */ -var HTTP_SERVER = exports.HTTP_SERVER = { - server: null, - - start: function(port) { - if (this.server) { return }; - - var that = this; - this.server = http.createServer(function(req, res) { that.route_(req, res) }); - this.server.listen(port); - qputs('Started HTTP server on port ' + port + '.'); - }, - stop: function() { - if (!this.server) { return }; - this.server.close(); - this.server = null; - qputs('Shutdown HTTP server.'); - }, - route_: function(req, res) { - if (req.url == "/" || req.url.match("^/data/")) { - serveReport(req.url, req, res) - } else if (req.url.match("^/remote")) { - serveRemote(req.url, req, res); - } else if (req.method == "GET") { - this.serveFile_("." + req.url, res); - } else { - res.writeHead(405, {"Content-Length": "0"}); - res.end(); - } - }, - serveFile_: function(file, response) { - fs.stat(file, function(err, stat) { - if (err != null) { - response.writeHead(404, {"Content-Type": "text/plain"}); - response.write("Cannot find file: " + file); - response.end(); - return; - } - - fs.readFile(file, "binary", function (err, data) { - if (err) { - response.writeHead(500, {"Content-Type": "text/plain"}); - response.write("Error opening file " + file + ": " + err); - } else { - response.writeHead(200, { 'Content-Length': data.length }); - response.write(data, "binary"); - } - response.end(); - }); - }); - } -} - -// Start HTTP server -NODELOAD_CONFIG.on('apply', function() { - if (NODELOAD_CONFIG.HTTP_ENABLED) { - HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT); - } -}); \ No newline at end of file diff --git a/src/log.js b/src/log.js deleted file mode 100644 index ad2ac23..0000000 --- a/src/log.js +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------ -// Logs -// ------------------------------------ -// -// This file defines LOGS. -// -// Each time nodeloadlib is used, three result files are created: -// 1. results--stats.log: Contains a log of all the statistics in JSON format -// 2. results--err.log: Contains all failed HTTP request/responses -// 3. results--summary.html: A HTML summary page of the load test -// - -var LOGS = exports.LOGS = { - opened: false, - STATS_LOG: new NullLog(), - ERROR_LOG: new NullLog(), - SUMMARY_HTML: new NullLog(), - open: function() { - if (this.opened) { return }; - - qputs("Opening log files."); - this.STATS_LOG = new LogFile('results-' + START + '-stats.log'); - this.ERROR_LOG = new LogFile('results-' + START + '-err.log'); - this.SUMMARY_HTML = new LogFile('results-' + START + '-summary.html'); - - // stats log should be a proper JSON array: output initial "[" - this.STATS_LOG.put("["); - }, - close: function() { - // stats log should be a proper JSON array: output final "]" - this.STATS_LOG.put("]"); - - this.STATS_LOG.close(); - this.ERROR_LOG.close(); - this.SUMMARY_HTML.close(); - - if (this.opened) { - qputs("Closed log files."); - } - this.opened = false; - } -} - -// Open all log files -NODELOAD_CONFIG.on('apply', function() { - if (NODELOAD_CONFIG.LOGS_ENABLED) { - LOGS.open(); - } -}); \ No newline at end of file diff --git a/src/monitor.js b/src/monitor.js deleted file mode 100644 index 81dc526..0000000 --- a/src/monitor.js +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------ -// Test monitor -// ------------------------------------ -// -// This file defines TEST_MONITOR. -// -// TEST_MONITOR is an EventEmitter that emits periodic 'update' events. This allows tests to be -// introspected at regular intervals for things like gathering statistics, generating reports, etc. -// - -/** An event emitter sending these events: - - 'test', function(test): addTest() was called, which created the new test, 'test'. - - 'start', function(tests): startTests() was called and all of the tests, 'tests' will be started. - - 'end', function(tests): all tests, 'tests', finished. - - 'update', function(tests): emitted every TestMonitor.intervalMs while 'tests' are running. - - 'afterUpdate', function(tests): emitted after every 'update' event. -*/ -function TestMonitor(intervalMs) { - events.EventEmitter.call(this); - this.intervalMs = intervalMs || 2000; - this.tests = []; -} -TestMonitor.prototype = { - addTest: function(test) { - this.tests.push(test); - this.emit('test', test); - }, - start: function() { - this.emit('start', this.tests); - monitor = this; - - // schedule on next process tick so NODELOAD_CONFIG.on('apply') can happen - process.nextTick(function() { - SCHEDULER.schedule({ - fun: LoopUtils.funLoop(function() { monitor.update() }), - rps: 1000/monitor.intervalMs, - delay: monitor.intervalMs/1000, - monitored: false - }); - }); - }, - update: function() { - this.emit('update', this.tests); - this.emit('afterUpdate', this.tests); - }, - stop: function() { - this.update(); - this.emit('end', this.tests); - this.tests = []; - } -} -Utils.inherits(TestMonitor, events.EventEmitter); - -/** The global test monitor. Register functions here that should be run at regular intervals during - the load test, such as processing & logging statistics. */ -var TEST_MONITOR = exports.TEST_MONITOR = new TestMonitor(); -TEST_MONITOR.on('update', function() { qprint('.') }); -TEST_MONITOR.on('end', function() { qprint('done.') }); - -NODELOAD_CONFIG.on('apply', function() { - TEST_MONITOR.intervalMs = NODELOAD_CONFIG.MONITOR_INTERVAL_MS; -}); diff --git a/src/remote.js b/src/remote.js deleted file mode 100644 index 8d20610..0000000 --- a/src/remote.js +++ /dev/null @@ -1,358 +0,0 @@ -// ----------------------------------------- -// Distributed testing -// ----------------------------------------- -// -// This file defines remoteTest, remoteStart, and remoteStartFile. -// -// This file contains the API for distributing load tests across multiple nodeload instances. See -// NODELOADLIB.md for instructions on running a distributed test. -// -// Distributed tests work as follows: -// 1. One node is designated as master, and the others are slaves -// 2. The master node POSTs a string containing valid javascript to http://slave/remote on each slave -// 3. Each slave executes the javascript by calling eval(). -// 4. Each slave periodically POSTs statistics as a JSON string back to the master at http://master/remote/progress -// 5. The master aggregates these statistics and generates reports just like a regular, non-distributed -// nodeloadlib instance -// - -/** Returns a test that can be scheduled with `remoteStart(spec)` (See TEST_DEFAULTS in api.js for a list - of the configuration values supported in the test specification */ -var remoteTest = exports.remoteTest = function(spec) { - return "(function() {\n" + - " var remoteSpec = JSON.parse('" + JSON.stringify(spec) + "');\n" + - " remoteSpec.requestGenerator = " + spec.requestGenerator + ";\n" + - " remoteSpec.requestLoop = " + spec.requestLoop + ";\n" + - " remoteSpec.reportFun = " + spec.reportFun + ";\n" + - " addTest(remoteSpec);\n" + - "})();"; -} - -/** Run the list of tests, created by remoteTest(spec), on the specified slaves. Slaves periodically -report statistics to master. When tests complete, callback is called. If stayAliveAfterDone == true, the -nodeload HTTP server will remain running. Otherwise, the server will automatically terminate. */ -var remoteStart = exports.remoteStart = function(master, slaves, tests, callback, stayAliveAfterDone) { - var remoteFun = tests.join('\n') + '\nstartTests();'; - remoteSubmit(master, slaves, remoteFun, callback, stayAliveAfterDone); -} - -/** Same as remoteStart(...), except runs a .js script rather than tests created using remoteTest(spec). -The script should use `addTest()` and `startTests()` to create and start tests, as if it were to run on -the local machine, not remoteTest(). */ -var remoteStartFile = exports.remoteStartFile = function(master, slaves, filename, callback, stayAliveAfterDone) { - fs.readFile(filename, function (err, data) { - if (err != null) throw err; - data = data.toString().replace(/^#![^\n]+\n/, '// removed shebang directive from runnable script\n'); - remoteSubmit(master, slaves, data, callback, stayAliveAfterDone); - }); -} - -// ================= -// Private -// ================= -var SLAVE_CONFIG = null; -var WORKER_POOL = null; -var REMOTE_TESTS = {}; - -/** Creates a RemoteWorkerPool with the given master and slave and runs the specified code, fun, on every -slave node in the pool. fun is a string containing valid Javascript. callback and stayAliveAfterDone are -the same as for remoteStart(). */ -function remoteSubmit(master, slaves, fun, callback, stayAliveAfterDone) { - var finished = function() { - SCHEDULER.stopAll(); - TEST_MONITOR.stop(); - - callback && callback(); - - if (!stayAliveAfterDone && !SLAVE_CONFIG) { - checkToExitProcess(); - } - } - - WORKER_POOL = new RemoteWorkerPool(master, slaves, fun); - WORKER_POOL.start(finished, stayAliveAfterDone); - - // Start the master's scheduler so that received stats are processed by STATS_MANAGER - TEST_MONITOR.start(); - SCHEDULER.startAll(); -} - -/** Converts this nodeload instance into a slave node by defining the global variable SLAVE_CONFIG. A -slave node differ from normal (master) node because it sends statistics to a master node. */ -function registerSlave(id, master) { - SLAVE_CONFIG = new RemoteSlave(id, master); - TEST_MONITOR.on('test', function(test) { SLAVE_CONFIG.addTest(test) }); - TEST_MONITOR.on('update', function() { SLAVE_CONFIG.reportProgress() }); - TEST_MONITOR.on('end', function() { SLAVE_CONFIG.clearTests() }); -} - -/** Process data POSTed by a slave to http://master/remote/newTest indicating that addTest() was called -*/ -function receiveTestCreate(report) { - if (WORKER_POOL.slaves[report.slaveId] === undefined) { return; } - - var localtest = REMOTE_TESTS[report.spec.name]; - if (localtest === undefined) { - localtest = { spec: report.spec, stats: {}, jobs: [], fun: null } - REMOTE_TESTS[report.spec.name] = localtest; - TEST_MONITOR.addTest(localtest); - } -} - -/** Merge in the statistics data POSTed by a slave to http://master/remote/progress */ -function receiveTestProgress(report) { - if (WORKER_POOL.slaves[report.slaveId] === undefined) { return; } - - WORKER_POOL.slaves[report.slaveId].state = "running"; - - // Slave report contains {testname: { stats: { statsname -> stats data }}. See RemoteSlave.reportProgress(); - for (var testname in report.data) { - var localtest = REMOTE_TESTS[testname]; - var remotetest = report.data[testname]; - if (localtest) { - for (var s in remotetest.stats) { - var remotestat = remotetest.stats[s]; - var localstat = localtest.stats[s]; - if (localstat === undefined) { - var backend = statsClassFromString(remotestat.interval.type); - localstat = new Reportable([backend, remotestat.interval.params], remotestat.name, remotestat.trend); - localtest.stats[s] = localstat; - } - localstat.merge(remotestat.interval); - } - } else { - qputs("WARN: received remote progress report from '" + report.slaveId + "' for unknown test: " + testname); - } - } -} - -/** A RemoteSlave represents a slave nodeload instance. RemoteSlave.reportProgress() POSTs statistics as -a JSON object to http://master/remote/progress. The object contains: -{ - slaveId: my unique id assigned by the master node - report: { - test-name: { - stats: { - // mirrors the fields of Reportable - name: name of stat - trend: should history of this stat be tracked - interval: stats data from current interval - } - } - } -} -*/ -function RemoteSlave(id, master) { - this.id = id; - this.tests = []; - if (master) { - master = master.split(':'); - this.master = { - host: master[0], - port: master[1], - client: http.createClient(master[1], master[0]) - }; - } -} -RemoteSlave.prototype = { - addTest: function(test) { - this.tests.push(test); - this.sendReport_('/remote/newTest', {slaveId: this.id, spec: test.spec}); - }, - clearTests: function() { - this.tests = []; - }, - reportProgress: function() { - var reports = {}; - for (var i in this.tests) { - var test = this.tests[i]; - var stats = {}; - for (var s in test.stats) { - stats[s] = { - name: test.stats[s].name, - trend: test.stats[s].trend, - interval: test.stats[s].interval - } - } - reports[test.spec.name] = { stats: stats }; - } - this.sendReport_('/remote/progress', {slaveId: this.id, data: reports}); - }, - sendReport_: function(url, object) { - if (this.master.client) { - var s = JSON.stringify(object); - var req = this.master.client.request('POST', url, {'host': this.master.host, 'content-length': s.length}); - req.write(s); - req.end(); - } - } -} - - -/** Represents a pool of nodeload instances with one master and multiple slaves. master and each slave is -specified as a string "host:port". Each slave node executes the Javascript specified in the "fun" string. -A slave indicates that is has completed its task when http://slave/remote/state returns a 410 status. -When all slaves are done, "callback" is executed. */ -function RemoteWorkerPool(master, slaves, fun) { - this.master = master; - this.slaves = {}; - this.fun = fun; - this.callback = null; - this.pingId = null; - this.progressId = null; - - for (var i in slaves) { - var slave = slaves[i].split(":"); - this.slaves[slaves[i]] = { - id: slaves[i], - state: "notstarted", - host: slave[0], - port: slave[1], - client: http.createClient(slave[1], slave[0]) - }; - } -} -RemoteWorkerPool.prototype = { - /** Run the Javascript in the string RemoteWorkerPool.fun on each of the slave node and register a - periodic alive check for each slave. */ - start: function(callback, stayAliveAfterDone) { - // Construct a Javascript string which converts a nodeloadlib instance to a slave, executes the - // contents of "fun" by placing it in an anonymous function call: - // registerSlave(slave-id, master-host:port); - // (function() { - // contents of "fun", which usually contains calls to addTest(), startTests(), etc - // })() - var fun = "(function() {" + this.fun + "})();"; - for (var i in this.slaves) { - var slave = this.slaves[i], - slaveFun = ''; - - if (this.master) { - slaveFun = "registerSlave('" + i + "','" + this.master + "');\n" + fun; - } else { - slaveFun = "registerSlave('" + i + "');\n" + fun; - } - - // POST the Javascript string to each slave which will eval() it. - var r = slave.client.request('POST', '/remote', {'host': slave.host, 'content-length': slaveFun.length}); - r.write(slaveFun); - r.end(); - slave.state = "running"; - } - - // Register a period ping to make sure slave is still alive - var worker = this; - this.pingId = setInterval(function() { worker.sendPings() }, NODELOAD_CONFIG.SLAVE_PING_INTERVAL_MS); - this.callback = callback; - }, - /** Called after each round of slave pings to see if all the slaves have finished. A slave is "finished" - if it reports that it finished successfully, or if it fails to respond to a ping and flagged with - an error state. When all slaves are finished, the overall test is considered complete and the user - defined callback function is called. */ - checkFinished_: function() { - for (var i in this.slaves) { - if (this.slaves[i].state != "done" && this.slaves[i].state != "error") { - return; - } - } - qprint("\nRemote tests complete."); - - var callback = this.callback; - clearInterval(this.pingId); - this.callback = null; - this.slaves = {}; - - callback && callback(); - }, - /** Issue a GET request to each slave at "http://slave/remote/state". This function is called every - SLAVE_PING_PERIOD seconds. If a slave fails to respond in that amount of time, it is flagged with - an error state. A slave will report that it is "done" when its SCHEDULER is no longer running, i.e. - all its tests ran to completion (or no tests were started, because "fun" didn't call to startTests()). */ - sendPings: function() { - var worker = this; - // Read the response from ping() (GET /remote/state) - var pong = function(slave) { return function(response) { - if (slave.state == "ping") { - if (response.statusCode == 200) { - slave.state = "running"; - } else if (response.statusCode == 410) { - qprint("\n" + slave.id + " done."); - slave.state = "done"; - } - } - }} - // Send GET to /remote/state - var ping = function(slave) { - slave.state = "ping"; - var r = slave.client.request('GET', '/remote/state', {'host': slave.host, 'content-length': 0}); - r.on('response', pong(slave)); - r.end(); - } - - // Verify every slave responded to the last round of pings. Send ping to slave that are still alive. - for (var i in this.slaves) { - if (this.slaves[i].state == "ping") { - qprint("\nWARN: slave " + i + " unresponsive."); - this.slaves[i].state = "error"; - } else if (this.slaves[i].state == "running") { - ping(this.slaves[i]); - } - } - this.checkFinished_(); - } -} - -/** Handler for all the requests to /remote. See http.js#startHttpServer(). */ -function serveRemote(url, req, res) { - var readBody = function(req, callback) { - var body = ''; - req.on('data', function(chunk) { body += chunk }); - req.on('end', function() { callback(body) }); - } - var sendStatus = function(status) { - res.writeHead(status, {"Content-Length": 0}); - res.end(); - } - if (req.method == "POST" && url == "/remote") { - readBody(req, function(remoteFun) { - qputs("\nReceived remote command:\n" + remoteFun); - eval(remoteFun); - sendStatus(200); - }); - } else if (req.method == "GET" && req.url == "/remote/hosts") { - var hosts = []; - if (SLAVE_CONFIG) { - hosts.push(SLAVE_CONFIG.master.host + ':' + SLAVE_CONFIG.master.port); - } - if (WORKER_POOL) { - hosts.push(WORKER_POOL.master); - for (var i in WORKER_POOL.slaves) { - hosts.push(i); - } - } - var body = JSON.stringify(hosts); - res.writeHead(200, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Content-Length": body.length}); - res.write(body); - res.end(); - } else if (req.method == "GET" && req.url == "/remote/state") { - if (SCHEDULER.running == true) { - sendStatus(200); - } else { - sendStatus(410); - } - res.end(); - } else if (req.method == "POST" && url == "/remote/newTest") { - readBody(req, function(data) { - receiveTestCreate(JSON.parse(data)); - sendStatus(200); - }); - } else if (req.method == "POST" && url == "/remote/progress") { - readBody(req, function(data) { - receiveTestProgress(JSON.parse(data)); - sendStatus(200); - }); - } else { - sendStatus(405); - } -} - diff --git a/src/report.js b/src/report.js deleted file mode 100644 index d1cb0d8..0000000 --- a/src/report.js +++ /dev/null @@ -1,181 +0,0 @@ -// ------------------------------------ -// Progress Reporting -// ------------------------------------ -// -// This file defines Report, Chart, and REPORT_MANAGER -// -// This file listens for 'update' events from TEST_MONITOR and trends test statistics. The trends are -// summarized in HTML page file written to disk and available via the nodeload HTTP server. - -/** A Report contains a summary object and set of charts. - -@param name A name for the report. Generally corresponds to the test name. -@param updater A function(report) that should update the summary and chart data. */ -var Report = exports.Report = function(name, updater) { - this.name = name; - this.uid = Utils.uid(); - this.summary = {}; - this.charts = {}; - this.updater = updater; -} -Report.prototype = { - getChart: function(name) { - if (this.charts[name] == null) - this.charts[name] = new Chart(name); - return this.charts[name]; - }, - update: function() { - if (this.updater != null) { this.updater(this); } - } -} - -/** A Chart represents a collection of lines over time represented as: - - columns: ["x values", "line 1", "line 2", "line 3", ...] - rows: [[timestamp1, line1[0], line2[0], line3[0], ...], - [timestamp2, line1[1], line2[1], line3[1], ...], - [timestamp3, line1[2], line2[2], line3[2], ...], - ... - ] - -@param name A name for the chart */ -var Chart = exports.Chart = function(name) { - this.name = name; - this.uid = Utils.uid(); - this.columns = ["time"]; - this.rows = [[timeFromTestStart()]]; -} -Chart.prototype = { - /** Put a row of data into the chart. The current time will be used as the x-value. The lines in the - chart are extracted from the "data". New lines can be added to the chart at any time by including it - in data. - - @param data An object representing one row of data: { - "line name 1": value1 - "line name 2": value2 - ... - } - */ - put: function(data) { - var row = [timeFromTestStart()]; - for (item in data) { - var col = this.columns.indexOf(item); - if (col < 0) { - col = this.columns.length; - this.columns.push(item); - this.rows[0].push(0); - } - row[col] = data[item]; - } - this.rows.push(row); - } -} - -/** The global report manager that keeps the summary webpage up to date during a load test */ -var REPORT_MANAGER = exports.REPORT_MANAGER = { - reports: {}, - addReport: function(report) { - this.reports[report.name] = report; - }, - getReport: function(name) { - return this.reports[name]; - }, - updateReports: function() { - for (var r in this.reports) { - this.reports[r].update(); - } - - LOGS.SUMMARY_HTML.clear(REPORT_MANAGER.getHtml()); - }, - reset: function() { - this.reports = {}; - }, - getHtml: function() { - var t = template.create(REPORT_SUMMARY_TEMPLATE); - return t({ - querystring: querystring, - refreshPeriodMs: NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS, - reports: this.reports - }); - } -} - - -// ================= -// Private methods -// ================= - -/** current time from start of nodeload process in 100ths of a minute */ -function timeFromTestStart() { - return (Math.floor((new Date().getTime() - START) / 600) / 100); -} - -/** Returns an updater function that cna be used with the Report() constructor. This updater write the -current state of "stats" to the report summary and charts. */ -function updateReportFromStats(stats) { - return function(report) { - for (var s in stats) { - var stat = stats[s]; - var summary = stat.summary(); - if (stat.trend) { - report.getChart(stat.name).put(summary.interval); - } - for (var i in summary.cumulative) { - report.summary[stat.name + " " + i] = summary.cumulative[i]; - } - } - } -} - -function getChartAsJson(chart) { - return (chart == null) ? null : JSON.stringify(chart.rows); -} - -/** Handler for all the requests to / and /data/main. See http.js#startHttpServer(). */ -function serveReport(url, req, res) { - if (req.method == "GET" && url == "/") { - var html = REPORT_MANAGER.getHtml(); - res.writeHead(200, {"Content-Type": "text/html", "Content-Length": html.length}); - res.write(html); - } else if (req.method == "GET" && req.url.match("^/data/([^/]+)/([^/]+)")) { - var urlparts = querystring.unescape(req.url).split("/"), - report = REPORT_MANAGER.getReport(urlparts[2]), - retobj = null; - if (report) { - var chartname = urlparts[3]; - if (chartname == "summary") { - retobj = report.summary; - } else if (report.charts[chartname] != null) { - retobj = report.charts[chartname].rows; - } - } - if (retobj) { - var json = JSON.stringify(retobj); - res.writeHead(200, {"Content-Type": "application/json", "Content-Length": json.length}); - res.write(json); - } else { - res.writeHead(404, {"Content-Type": "text/html", "Content-Length": 0}); - } - } else if (req.method == "GET" && url == "/data/") { - var json = JSON.stringify(REPORT_MANAGER.reports); - res.writeHead(200, {"Access-Control-Allow-Origin": "*", "Content-Type": "application/json", "Content-Length": json.length}); - res.write(json); - } else { - res.writeHead(405, {"Content-Length": 0}); - } - res.end(); -} - -// Register report manager with test monitor -TEST_MONITOR.on('update', function() { REPORT_MANAGER.updateReports() }); -TEST_MONITOR.on('end', function() { - for (var r in REPORT_MANAGER.reports) { - REPORT_MANAGER.reports[r].updater = null; - } -}); -TEST_MONITOR.on('test', function(test) { - // when a new test is created, add a report that contains all the test stats - if (test.stats) { - REPORT_MANAGER.addReport(new Report(test.spec.name, updateReportFromStats(test.stats))) - } -}); \ No newline at end of file diff --git a/src/scheduler.js b/src/scheduler.js deleted file mode 100644 index 450e39d..0000000 --- a/src/scheduler.js +++ /dev/null @@ -1,185 +0,0 @@ -// ----------------------------------------- -// Scheduler for event-based loops -// ----------------------------------------- -// -// This file defines SCHEDULER, Scheduler, and Job. -// -// This file provides a convenient way to define and group sets of Jobs. A Job is an event-based loop -// that runs at a certain rate with a set of termination conditions. A Scheduler groups a set of Jobs and -// starts and stops them together. - -/** JOB_DEFAULTS defines all of the parameters that can be set in a job specifiction passed to - Scheduler.schedule(spec). */ -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? -}; - -/** A scheduler starts and monitors a group of Jobs. Jobs can be monitored or unmonitored. When all -monitored jobs complete, Scheduler considers the entire job group to be complete. Scheduler automatically -stops all unmonitored jobs in the same group. See the Job class below. */ -var Scheduler = exports.Scheduler = function() { - this.id = Utils.uid(); - this.jobs = []; - this.running = false; - this.callback = null; -} -Scheduler.prototype = { - /** Primary function for defining and adding new Jobs. Start all scheduled jobs by calling - startAll(). If the scheduler is already startd, the jobs are started immediately upon scheduling. */ - schedule: function(spec) { - Utils.defaults(spec, JOB_DEFAULTS); - - // concurrency is handled by creating multiple jobs with portions of the load - var scheduledJobs = [] - spec.numberOfTimes /= spec.concurrency; - spec.rps /= spec.concurrency; - for (var i = 0; i < spec.concurrency; i++) { - var j = new Job(spec); - this.addJob(j); - scheduledJobs.push(j); - - // If the scheduler is running (startAll() was already called), start new jobs immediately - if (this.running) { - this.startJob_(j); - } - } - - return scheduledJobs; - }, - addJob: function(job) { - this.jobs.push(job); - }, - /** Start all scheduled Jobs. When the jobs complete, the user defined function, callback is called. */ - startAll: function(callback) { - if (this.running) return; - - this.callback = callback; - this.running = true; - for (var i in this.jobs) { - if (!this.jobs[i].started) { - this.startJob_(this.jobs[i]); - } - }; - }, - /** Force all jobs to finish. The user defined callback will still be called. */ - stopAll: function() { - this.jobs.forEach(function(j) { j.stop() }); - }, - startJob_: function(job) { - var scheduler = this; - job.start(function() { scheduler.checkFinished_() }); - }, - /** Iterate all jobs and see if any are still running. If all jobs are complete, then call the user - defined callback function. */ - checkFinished_: function() { - var foundRunningJob = false, - foundMonitoredJob = false; - - for (var i in this.jobs) { - foundMonitoredJob = foundMonitoredJob || this.jobs[i].monitored; - foundRunningJob = foundRunningJob || (this.jobs[i].started && !this.jobs[i].done); - if (this.jobs[i].monitored && this.jobs[i].started && !this.jobs[i].done) { - return false; - } - } - if (!foundMonitoredJob && foundRunningJob) { - return false; - } - - this.running = false; - this.stopAll(); - this.jobs = []; - - if (this.callback != null) { - // Clear out callback before calling it since function may actually call startAll() again. - var oldCallback = this.callback; - this.callback = null; - oldCallback(); - } - - return true; - } -} - -var SCHEDULER = exports.SCHEDULER = new Scheduler(); - -/** At a high level, a Job is analogous to a thread. A Job instance represents a function that is being -executed at a certain rate for a set number of times or duration. See JOB_DEFAULTS for a list of the -configuration values that can be provided in the job specification, spec. */ -var Job = exports.Job = function(spec) { - this.id = Utils.uid(); - this.fun = spec.fun; - this.args = spec.args; - this.argGenerator = spec.argGenerator; - this.rps = spec.rps; - this.duration = spec.duration; - this.numberOfTimes = spec.numberOfTimes; - this.delay = spec.delay; - this.monitored = spec.monitored; - - this.callback = null; - this.started = false; - this.done = false; - - var job = this; - this.warningTimeoutId = setTimeout(function() { qputs("WARN: a job" + job.id + " was not started; Job.start() called?") }, 3000); -} -Job.prototype = { - /** Scheduler calls this method to start the job. The user defined function, callback, is called when - the job completes. This function basically creates and starts a ConditionalLoop instance (which is an - "event based loop"). */ - start: function(callback) { - if (this.started) { return }; - - clearTimeout(this.warningTimeoutId); // Cancel "didn't start job" warning - - var job = this, - fun = this.fun, - conditions = []; - - if (this.rps && this.rps < Infinity) { - fun = LoopUtils.rpsLoop(this.rps, fun); - } - if (this.numberOfTimes !== null && this.numberOfTimes < Infinity) { - conditions.push(LoopConditions.maxExecutions(this.numberOfTimes)); - } - if (this.duration !== null && this.duration < Infinity) { - var duration = this.duration; - if (this.delay !== null && this.delay > 0) - duration += this.delay; - conditions.push(LoopConditions.timeLimit(duration)); - } - - this.args = this.argGenerator && this.argGenerator(); - this.callback = callback; - this.loop = new ConditionalLoop(fun, this.args, conditions, this.delay); - this.loop.start(function() { - job.done = true; - if (job.callback) { - job.callback(); - } - }); - - this.started = true; - }, - stop: function() { - this.started = true; - this.done = true; - if (this.loop) { - this.loop.stop(); - } - } -} \ No newline at end of file diff --git a/src/statsmgr.js b/src/statsmgr.js deleted file mode 100644 index dc213b3..0000000 --- a/src/statsmgr.js +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------ -// Statistics Manager -// ------------------------------------ -// -// This file defines STATS_MANAGER. -// - -/** The global statistics manager. Periodically process test statistics and logs them to disk during a -load test run. */ -var STATS_MANAGER = { - statsSets: [], - addStatsSet: function(stats) { - this.statsSets.push(stats); - }, - logStats: function() { - var out = '{"ts": ' + JSON.stringify(new Date()); - this.statsSets.forEach(function(statsSet) { - for (var i in statsSet) { - var stat = statsSet[i]; - out += ', "' + stat.name + '": ' + JSON.stringify(stat.summary().interval); - } - }); - out += "}"; - LOGS.STATS_LOG.put(out + ",\n"); - }, - prepareNextInterval: function() { - this.statsSets.forEach(function(statsSet) { - for (var i in statsSet) { - statsSet[i].next(); - } - }); - }, - reset: function() { - this.statsSets = []; - } -} -TEST_MONITOR.on('test', function(test) { if (test.stats) STATS_MANAGER.addStatsSet(test.stats) }); -TEST_MONITOR.on('update', function() { STATS_MANAGER.logStats() }); -TEST_MONITOR.on('afterUpdate', function() { STATS_MANAGER.prepareNextInterval() }) -TEST_MONITOR.on('end', function() { STATS_MANAGER.reset() }); \ No newline at end of file diff --git a/src/summary.tpl b/src/summary.tpl deleted file mode 100644 index 5231dd3..0000000 --- a/src/summary.tpl +++ /dev/null @@ -1,102 +0,0 @@ -REPORT_SUMMARY_TEMPLATE - - - Test Results - - - - - - -
-
- <% for (var i in reports) { %> - <% for (var j in reports[i].charts) { %> - <% var chart = reports[i].charts[j]; %> -

<%=chart.name%>

-
-
-
-
-
- <% } %> - <% } %> -
- -
- - - - - - - \ No newline at end of file diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 28f6fb9..0000000 --- a/src/utils.js +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------ -// Statistics Manager -// ------------------------------------ -// -// This file defines qputs, qprint, and Utils. -// -// Common utility functions. - -// A few common global functions so we can access them with as few keystrokes as possible -// -var qputs = exports.qputs = function(s) { - NODELOAD_CONFIG.QUIET || sys.puts(s); -}; - -var qprint = exports.qprint = function(s) { - NODELOAD_CONFIG.QUIET || sys.print(s); -}; - - -// Static utility methods -// -var Utils = exports.Utils = { - uid: function() { - this.lastUid = this.lastUid || 0; - return this.lastUid++; - }, - defaults: function(obj, defaults) { - for (var i in defaults) { - if (obj[i] === undefined) { - obj[i] = defaults[i]; - } - } - }, - inherits: function(ctor, superCtor) { - var proto = ctor.prototype; - sys.inherits(ctor, superCtor); - for (var i in proto) { - ctor.prototype[i] = proto[i]; - } - } -}; \ No newline at end of file diff --git a/test/http.test.js b/test/http.test.js new file mode 100644 index 0000000..75e9a84 --- /dev/null +++ b/test/http.test.js @@ -0,0 +1,39 @@ +var http = require('http'), + nlconfig = require('../lib/config').disableServer(), + HttpServer = require('../lib/http').HttpServer; + +var server = new HttpServer().start(9020); +setTimeout(function() { server.stop(); }, 1500); + +module.exports = { + 'example: add a new route': function(assert, beforeExit) { + var done = false; + server.addRoute('^/route', function() { + done = true; + }); + + var client = http.createClient(9020, '127.0.0.1'), + req = client.request('GET', '/route/item'); + req.end(); + + beforeExit(function() { + assert.ok(done, 'Never got request to /route'); + }); + }, + 'test file server finds package.json': function(assert, beforeExit) { + var done = false; + var client = http.createClient(9020, '127.0.0.1'), + req = client.request('GET', '/package.json'); + req.end(); + req.on('response', function(res) { + assert.equal(res.statusCode, 200); + res.on('data', function(chunk) { + done = true; + }); + }); + + beforeExit(function() { + assert.ok(done, 'Never got response data from /package.json'); + }); + }, +}; diff --git a/test/loop.test.js b/test/loop.test.js new file mode 100644 index 0000000..fbb7969 --- /dev/null +++ b/test/loop.test.js @@ -0,0 +1,154 @@ +var loop = require('../lib/loop'), + Loop = loop.Loop, + MultiLoop = loop.MultiLoop; + +module.exports = { + 'example: a basic rps loop with set duration': function(assert, beforeExit) { + var i = 0, start = new Date(), lasttime = start, duration, + l = new Loop({ + fun: function(finished) { + var now = new Date(); + assert.ok(Math.abs(now - lasttime) < 210, (now - lasttime).toString()); + lasttime = now; + + i++; + finished(); + }, + rps: 5, // times per second (every 200ms) + duration: 1 // second + }).start(); + + l.on('end', function() { duration = new Date() - start; }); + + 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) <= 60, '1000 == ' + duration); + }); + }, + 'example: use Scheduler to vary execution rate and concurrency': function (assert, beforeExit) { + var i = 0, c = 0, start = new Date(), duration, + l = new MultiLoop({ + fun: function(finished) { i++; finished(); }, + rpsProfile: [[2,10], [3,0]], + concurrencyProfile: [[1,5], [2,10]], + duration: 3.5 + }).start(); + + l.on('end', function() { duration = new Date() - start; }); + + beforeExit(function() { + assert.equal(i, 15, 'loop executed incorrect number of times: ' + i); + assert.ok(l.loops.every(function(l) { return !l.running; }), 'loops still flagged as running'); + assert.ok(Math.abs(duration - 3500) < 500, '3500 == ' + duration); + }); + }, + 'test numberOfTimes loop': function(assert, beforeExit) { + var i = 0, + l = new Loop({ + fun: function(finished) { i++; finished(); }, + rps: 5, + numberOfTimes: 3 + }).start(); + + beforeExit(function() { + assert.equal(3, i, 'loop executed incorrect number of times'); + }); + }, + 'test emits start and stop events': function(assert, beforeExit) { + var started, ended, + l = new Loop({ + fun: function(finished) { finished(); }, + rps: 10, + numberOfTimes: 3 + }).start(); + + l.on('start', function() { started = true; }); + l.on('end', function() { ended = true; }); + + beforeExit(function() { + assert.ok(started, 'start never emitted'); + assert.ok(ended, 'end never emitted'); + }); + }, + + 'test concurrency': function(assert, beforeExit) { + var i = 0, start = new Date(), duration, + l = new MultiLoop({ + fun: function(finished) { i++; finished(); }, + rps: 10, + duration: 1, + concurrency: 5 + }).start(); + + l.on('end', function() { duration = new Date() - start; }); + + assert.equal(l.loops.length, 5); + beforeExit(function() { + assert.equal(i, 10, 'loop executed incorrect number of times'); + assert.ok(l.loops.every(function(l){ return !l.running; }), 'loops still flagged as running'); + assert.ok(Math.abs(duration - 1000) < 30, '1000 == ' + duration); + }); + }, + 'MultiLoop emits events': function(assert, beforeExit) { + var started = false, ended = false, + l = new MultiLoop({ + fun: function(finished) { finished(); }, + numberOfTimes: 3 + }).start(); + + l.on('start', function() { started = true; }); + l.on('end', function() { ended = true; }); + + beforeExit(function() { + assert.ok(started, 'start never emitted'); + assert.ok(ended, 'end never emitted'); + }); + }, + 'change loop rate': function(assert, beforeExit) { + var i = 0, start = new Date(), duration, + l = new Loop({ + fun: function(finished) { + i++; + finished(); + }, + rps: 5, + duration: 2 + }).start(); + + l.on('end', function() { duration = new Date() - start; }); + setTimeout(function() { l.rps = 10; }, 1000); + setTimeout(function() { l.rps = 20; }, 1500); + + beforeExit(function() { + assert.equal(i, 20, 'loop executed incorrect number of times: ' + i); // 5+10/2+20/2 == 20 + assert.ok(!l.running, 'loop still flagged as running'); + assert.ok(Math.abs(duration - 2000) <= 50, '2000 == ' + duration); + }); + }, + 'test MultiLoop.getProfileValue_ works': function(assert) { + var getProfileValue = loop.MultiLoop.prototype.getProfileValue_; + assert.equal(getProfileValue(null, 10), 0); + assert.equal(getProfileValue(null, 10), 0); + assert.equal(getProfileValue([], 10), 0); + + assert.equal(getProfileValue([[0,0]], 0), 0); + assert.equal(getProfileValue([[0,10]], 0), 10); + assert.equal(getProfileValue([[0,0],[10,0]], 5), 0); + assert.equal(getProfileValue([[0,0],[10,100]], 5), 50); + assert.equal(getProfileValue([[0,0],[11,100]], 5), 45); + + var profile = [[0,0],[10,100],[15,100],[22,500],[30,500],[32,0]]; + assert.equal(getProfileValue(profile, -1), 0); + assert.equal(getProfileValue(profile, 0), 0); + assert.equal(getProfileValue(profile, 1), 10); + assert.equal(getProfileValue(profile, 1.5), 15); + assert.equal(getProfileValue(profile, 10), 100); + assert.equal(getProfileValue(profile, 14), 100); + assert.equal(getProfileValue(profile, 21), 442); + assert.equal(getProfileValue(profile, 30), 500); + assert.equal(getProfileValue(profile, 31), 250); + assert.equal(getProfileValue(profile, 32), 0); + assert.equal(getProfileValue(profile, 35), 0); + } +}; \ No newline at end of file diff --git a/test/monitoring.test.js b/test/monitoring.test.js new file mode 100644 index 0000000..e175c3f --- /dev/null +++ b/test/monitoring.test.js @@ -0,0 +1,209 @@ +/*jslint sub:true */ + +var http = require('http'), + EventEmitter = require('events').EventEmitter, + util = require('../lib/util'), + monitoring = require('../lib/monitoring'), + Monitor = monitoring.Monitor, + MonitorGroup = monitoring.MonitorGroup; + +var svr = http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(req.url); +}); +svr.listen(9000); +setTimeout(function() { svr.close(); }, 1000); + +function mockConnection(callback) { + var conn = { + operation: function(opcallback) { + setTimeout(function() { opcallback(); }, 25); + } + }; + setTimeout(function() { callback(conn); }, 75); +} + +module.exports = { + 'example: track runtime of a function': function(assert, beforeExit) { + var m = new Monitor('runtime'), + f = function() { + var ctx = m.start(), runtime = Math.floor(Math.random() * 100); + setTimeout(function() { ctx.end(); }, runtime); + }; + + for (var i = 0; i < 20; i++) { + f(); + } + + beforeExit(function() { + var summary = m.stats['runtime'] && m.stats['runtime'].summary(); + assert.ok(summary); + assert.equal(m.stats['runtime'].length, 20); + assert.ok(summary.min >= 0 && summary.min < 100); + assert.ok(summary.max > 0 && summary.max <= 100); + assert.ok(summary.median > 0 && summary.median < 100); + }); + }, + 'example: use a MonitorGroup to organize multiple Monitors': function(assert, beforeExit) { + var m = new MonitorGroup('runtime'), + f = function() { + var transactionCtx = m.start('transaction'); + mockConnection(function(conn) { + var operationCtx = m.start('operation'); + conn.operation(function() { + operationCtx.end(); + transactionCtx.end(); + }); + }); + }; + + for (var i = 0; i < 10; i++) { + f(); + } + + beforeExit(function() { + var summary = m.interval.summary(); + assert.ok(summary); + assert.ok(summary['transaction'] && summary['transaction']['runtime']); + assert.ok(summary['operation'] && summary['operation']['runtime']); + assert.ok(Math.abs(summary['transaction']['runtime'].median - 100) <= 10, summary['transaction']['runtime'].median.toString()); + assert.ok(Math.abs(summary['operation']['runtime'].median - 25) <= 5); + }); + }, + 'example: use EventEmitter objects instead of interacting with MonitorGroup directly': function(assert, beforeExit) { + function MonitoredObject() { + EventEmitter.call(this); + var self = this; + self.run = function() { + self.emit('start', 'transaction'); + mockConnection(function(conn) { + self.emit('start', 'operation'); + conn.operation(function() { + self.emit('end', 'operation'); + self.emit('end', 'transaction'); + }); + }); + }; + } + util.inherits(MonitoredObject, EventEmitter); + + var m = new MonitorGroup('runtime'); + for (var i = 0; i < 5; i++) { + var obj = new MonitoredObject(); + m.monitorObjects(obj); + setTimeout(obj.run, i * 100); + } + + beforeExit(function() { + var trSummary = m.stats['transaction'] && m.stats['transaction']['runtime'] && m.stats['transaction']['runtime'].summary(); + var opSummary = m.stats['operation'] && m.stats['operation']['runtime'] && m.stats['operation']['runtime'].summary(); + assert.ok(trSummary); + assert.ok(opSummary); + assert.equal(m.stats['transaction']['runtime'].length, 5); + assert.ok(Math.abs(trSummary.median - 100) <= 5, '100 == ' + trSummary.median); + assert.ok(Math.abs(opSummary.median - 25) <= 5, '25 == ' + opSummary.median); + }); + }, + 'use EventEmitter objects with Monitor': function(assert, beforeExit) { + function MonitoredObject() { + EventEmitter.call(this); + var self = this; + self.run = function() { + self.emit('start'); + setTimeout(function() { self.emit('end'); }, Math.floor(Math.random() * 100)); + }; + } + util.inherits(MonitoredObject, EventEmitter); + + var m = new Monitor('runtime'); + for (var i = 0; i < 5; i++) { + var obj = new MonitoredObject(); + m.monitorObjects(obj); + setTimeout(obj.run, i * 100); + } + + beforeExit(function() { + var summary = m.stats['runtime'] && m.stats['runtime'].summary(); + assert.ok(summary); + assert.equal(m.stats['runtime'].length, 5); + assert.ok(summary.min >= 0 && summary.min < 100, summary.min.toString()); + assert.ok(summary.max > 0 && summary.max <= 100, summary.max.toString()); + assert.ok(summary.median > 0 && summary.median < 100, summary.median.toString()); + }); + }, + 'HTTP specific monitors': function(assert, beforeExit) { + var q = 0, + m = new Monitor('result-codes', 'uniques', 'request-bytes', 'response-bytes'), + client = http.createClient(9000, 'localhost'), + f = function() { + var ctx = m.start(), + path = '/search?q=' + q++, + req = client.request('GET', path, {'host': 'localhost'}); + req.path = path; + req.end(); + req.on('response', function(res) { + ctx.end({req: req, res: res}); + }); + }; + + for (var i = 0; i < 2; i++) { + f(); + } + + beforeExit(function() { + var resultCodesSummary = m.stats['result-codes'] && m.stats['result-codes'].summary(), + uniquesSummary = m.stats['uniques'] && m.stats['uniques'].summary(), + requestBytesSummary = m.stats['request-bytes'] && m.stats['request-bytes'].summary(), + responseBytesSummary = m.stats['response-bytes'] && m.stats['response-bytes'].summary(); + + assert.ok(resultCodesSummary); + assert.ok(uniquesSummary); + assert.ok(requestBytesSummary); + assert.ok(responseBytesSummary); + + assert.equal(resultCodesSummary.total, 2); + assert.ok(resultCodesSummary.rps >= 0); + assert.equal(resultCodesSummary['200'], 2); + + assert.equal(uniquesSummary.total, 2); + assert.equal(uniquesSummary.uniqs, 2); + + assert.ok(requestBytesSummary.total > 0); + + assert.ok(responseBytesSummary.total > 20); + }); + }, + 'monitor generates update events with interval and overall stats': function(assert, beforeExit) { + var m = new Monitor('runtime'), + intervals = 0, + f = function() { + var ctx = m.start(), runtime = Math.floor(Math.random() * 10); + setTimeout(function() { ctx.end(); }, runtime); + }; + + m.updateInterval = 220; + + // Call to f every 100ms for a total runtime >500ms + for (var i = 1; i <= 5; i++) { + setTimeout(f, i*100); + } + + // Disable 'update' events after 500ms so that this test can complete + setTimeout(function() { m.updateInterval = 0; }, 510); + + m.on('update', function(interval, overall) { + assert.strictEqual(overall, m.stats); + + assert.ok(interval['runtime']); + assert.equal(interval['runtime'].length, 2); + assert.ok(interval['runtime'].mean() > 0 && interval['runtime'].mean() < 10); + assert.ok(interval['runtime'].mean() > 0 && interval['runtime'].mean() < 10); + intervals++; + }); + + beforeExit(function() { + assert.equal(intervals, 2, 'Got incorrect number of update events: ' + intervals); + assert.equal(m.stats['runtime'].length, 5); + }); + } +}; \ No newline at end of file diff --git a/test/remote.test.js b/test/remote.test.js new file mode 100755 index 0000000..fd1a1fe --- /dev/null +++ b/test/remote.test.js @@ -0,0 +1,81 @@ +var http = require('http'), + remote = require('../lib/remote'), + nlconfig = require('../lib/config').disableServer(), + HttpServer = require('../lib/http').HttpServer, + Cluster = remote.Cluster; + +module.exports = { + 'basic end-to-end cluster test': function(assert, beforeExit) { + var testTimeout, cluster, + masterSetupCalled, slaveSetupCalled = [], slaveFunCalled = [], + master = new HttpServer().start(9030), + slave1 = new HttpServer().start(9031), + slave2 = new HttpServer().start(9032), + stopAll = function() { + cluster.on('end', function() { + master.stop(); + slave1.stop(); + slave2.stop(); + }); + cluster.end(); + }; + + remote.installRemoteHandler(master); + remote.installRemoteHandler(slave1); + remote.installRemoteHandler(slave2); + + cluster = new Cluster({ + master: { + setup: function(slaves) { + assert.ok(slaves); + masterSetupCalled = true; + }, + slaveSetupCalled: function(slaves, slaveId) { + assert.ok(slaves); + assert.ok(slaveId); + slaveSetupCalled.push(slaveId); + }, + slaveFunCalled: function(slaves, slaveId, data) { + assert.ok(slaves); + assert.ok(slaveId); + assert.equal(data, 'data for master'); + slaveFunCalled.push(slaveId); + }, + }, + slaves: { + hosts: ['localhost:9031', 'localhost:9032'], + setup: function(master) { + this.assert = require('assert'); + this.assert.ok(master); + master.slaveSetupCalled(); + }, + slaveFun: function(master, data) { + this.assert.ok(master); + this.assert.equal(data, 'data for slaves'); + master.slaveFunCalled('data for master'); + } + }, + pingInterval: 250, + server: master + }); + + cluster.on('init', function() { + cluster.on('start', function() { + cluster.slaveFun('data for slaves'); + }); + cluster.start(); + }); + + testTimeout = setTimeout(stopAll, 500); + + beforeExit(function() { + assert.ok(masterSetupCalled); + assert.equal(slaveSetupCalled.length, 2); + assert.ok(slaveSetupCalled.indexOf('localhost:9031') > -1); + assert.ok(slaveSetupCalled.indexOf('localhost:9032') > -1); + assert.equal(slaveFunCalled.length, 2); + assert.ok(slaveFunCalled.indexOf('localhost:9031') > -1); + assert.ok(slaveFunCalled.indexOf('localhost:9032') > -1); + }); + }, +}; \ No newline at end of file diff --git a/test/reporting.test.js b/test/reporting.test.js new file mode 100644 index 0000000..1e4388e --- /dev/null +++ b/test/reporting.test.js @@ -0,0 +1,82 @@ +/*jslint sub:true */ + +var nlconfig = require('../lib/config').disableServer(), + reporting = require('../lib/reporting'), + monitoring = require('../lib/monitoring'), + REPORT_MANAGER = reporting.REPORT_MANAGER; + +REPORT_MANAGER.refreshIntervalMs = 500; +REPORT_MANAGER.setLogFile('.reporting.test-output.html'); +setTimeout(function() { REPORT_MANAGER.setLoggingEnabled(false); }, 1000); + +function mockConnection(callback) { + var conn = { + operation: function(opcallback) { + setTimeout(function() { opcallback(); }, 25); + } + }; + setTimeout(function() { callback(conn); }, 75); +} + +module.exports = { + 'example: add a chart to test summary webpage': function(assert, beforeExit) { + var report = REPORT_MANAGER.addReport('My Report'), + chart1 = report.getChart('Chart 1'), + chart2 = report.getChart('Chart 2'); + + chart1.put({'line 1': 1, 'line 2': -1}); + chart1.put({'line 1': 2, 'line 2': -2}); + chart1.put({'line 1': 3, 'line 2': -3}); + + chart2.put({'line 1': 10, 'line 2': -10}); + chart2.put({'line 1': 11, 'line 2': -11}); + chart2.put({'line 1': 12, 'line 2': -12}); + + report.summary = { + "statistic 1" : 500, + "statistic 2" : 'text', + }; + + var html = REPORT_MANAGER.getHtml(); + assert.isNotNull(html.match('name":"'+chart1.name)); + assert.isNotNull(html.match('name":"'+chart2.name)); + assert.isNotNull(html.match('summary":')); + }, + 'example: update reports from Monitor and MonitorGroup stats': function(assert, beforeExit) { + var m = new monitoring.MonitorGroup('runtime') + .initMonitors('transaction', 'operation'), + f = function() { + var trmon = m.start('transaction'); + mockConnection(function(conn) { + var opmon = m.start('operation'); + conn.operation(function() { + opmon.end(); + trmon.end(); + }); + }); + }; + + m.updateInterval = 200; + + REPORT_MANAGER.addReport('All Monitors').updateFromMonitorGroup(m); + REPORT_MANAGER.addReport('Transaction').updateFromMonitor(m.monitors['transaction']); + REPORT_MANAGER.addReport('Operation').updateFromMonitor(m.monitors['operation']); + + for (var i = 1; i <= 10; i++) { + setTimeout(f, i*50); + } + + // Disable 'update' events after 500ms so that this test can complete + setTimeout(function() { m.updateInterval = 0; }, 510); + + beforeExit(function() { + var trReport = REPORT_MANAGER.reports.filter(function(r) { return r.name === 'Transaction'; })[0]; + var opReport = REPORT_MANAGER.reports.filter(function(r) { return r.name === 'Operation'; })[0]; + assert.ok(trReport && (trReport.name === 'Transaction') && trReport.charts['runtime']); + assert.ok(opReport && (opReport.name === 'Operation') && opReport.charts['runtime']); + assert.equal(trReport.charts['runtime'].rows.length, 3); // 1+2, since first row is [[0,...]] + assert.equal(opReport.charts['runtime'].rows.length, 3); + assert.ok(Math.abs(trReport.charts['runtime'].rows[2][3] - 100) < 10); // third column is 'median' + }); + }, +}; \ No newline at end of file diff --git a/test/stats.test.js b/test/stats.test.js new file mode 100644 index 0000000..f6cb805 --- /dev/null +++ b/test/stats.test.js @@ -0,0 +1,50 @@ +var stats = require('../lib/stats'); + +module.exports = { + 'StatsGroup functions are non-enumerable': function(assert, beforeExit) { + var s = new stats.StatsGroup(); + s.latency = {}; + assert.ok(s.get); + assert.ok(s.put); + assert.ok(s.clear); + assert.ok(s.summary); + for (var i in s) { + if (i !== 'latency') { + assert.fail('Found enumerable property: ' + i); + } + } + }, + 'test StatsGroup methods': function(assert, beforeExit) { + var s = new stats.StatsGroup(); + s.latency = new stats.Histogram(); + s.results = new stats.ResultsCounter(); + + // name property + s.name = 'test'; + assert.equal(s.name, 'test'); + + // get()/put() + s.put(1); + assert.equal(s.latency.get(1), 1); + assert.equal(s.results.get(1), 1); + assert.eql(s.get(1), {latency: 1, results: 1}); + + // summary() + var summary = s.summary(); + assert.ok(summary.latency); + assert.isDefined(summary.latency.median); + assert.equal(s.summary('latency')['95%'], s.latency.summary()['95%']); + assert.ok(summary.results); + assert.equal(summary.results.total, 1); + assert.eql(s.summary('results'), s.results.summary()); + assert.equal(summary.name, 'test'); + assert.ok(summary.ts); + + // clear() + s.clear('latency'); + assert.equal(s.latency.length, 0); + assert.equal(s.results.length, 1); + s.clear(); + assert.equal(s.results.length, 0); + } +}; diff --git a/test/util.test.js b/test/util.test.js new file mode 100644 index 0000000..44f5cc5 --- /dev/null +++ b/test/util.test.js @@ -0,0 +1,52 @@ +var http = require('http'), + util = require('../lib/util'); + +module.exports = { + 'ReconnectingClient tolerates connection failures': function(assert, beforeExit) { + var PORT = 9010, + simpleResponse = function (req, res) { res.writeHead(200); res.end(); }, + svr = http.createServer(simpleResponse), + client = util.createReconnectingClient(PORT, 'localhost'), + numResponses = 0, + clientErrorsDetected = 0, + req, testTimeout; + + // reconnecting client should work like a normal client and get a response from our server + svr.listen(PORT); + req = client.request('GET', '/'); + assert.isNotNull(req); + req.on('response', function(res) { + numResponses++; + res.on('end', function() { + // once the server is terminated, request() should cause a clientError event (below) + svr = svr.close(); + req = client.request('GET','/'); + + client.once('reconnect', function() { + // restart server, and request() should work again + svr = http.createServer(simpleResponse); + svr.listen(PORT); + + req = client.request('GET','/'); + req.end(); + req.on('response', function(res) { + clearTimeout(testTimeout); + + numResponses++; + svr = svr.close(); + }); + }); + }); + }); + client.on('error', function(err) { clientErrorsDetected++; }); + req.end(); + + // Maximum timeout for this test is 1 second + testTimeout = setTimeout(function() { if (svr) { svr.close(); } }, 2000); + + beforeExit(function() { + assert.equal(clientErrorsDetected, 1); + assert.equal(numResponses, 2); + }); + }, +};