From d8ded3af1efbc9b6e042642ad5c053f8dda6107a Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 5 Nov 2010 18:45:33 -0400 Subject: [PATCH 01/41] Add basic npm support: - Move everything from deps & src to lib - Add process_tpl.js and use in Makefile for converting template files to variables - Rename a bunch of stuff to match npm module names --- .gitignore | 1 + Makefile | 15 +- deps/flot/dygraph-LICENSE.txt | 22 - deps/flot/excanvas.js | 1427 -------- deps/flot/excanvas.min.js | 1 - deps/flot/jquery.colorhelpers.js | 174 - deps/flot/jquery.colorhelpers.min.js | 1 - deps/flot/jquery.flot.crosshair.js | 156 - deps/flot/jquery.flot.crosshair.min.js | 1 - deps/flot/jquery.flot.image.js | 237 -- deps/flot/jquery.flot.image.min.js | 1 - deps/flot/jquery.flot.js | 2119 ------------ deps/flot/jquery.flot.min.js | 1 - deps/flot/jquery.flot.navigate.js | 272 -- deps/flot/jquery.flot.navigate.min.js | 1 - deps/flot/jquery.flot.selection.js | 299 -- deps/flot/jquery.flot.selection.min.js | 1 - deps/flot/jquery.flot.stack.js | 152 - deps/flot/jquery.flot.stack.min.js | 1 - deps/flot/jquery.flot.threshold.js | 103 - deps/flot/jquery.flot.threshold.min.js | 1 - deps/flot/jquery.js | 4376 ------------------------ deps/flot/jquery.min.js | 19 - deps/optparse-js | 1 - {src => lib}/config.js | 0 deps/dygraph.js => lib/dygraph.tpl | 0 {src => lib}/evloops.js | 0 {src => lib}/header.js | 0 {src => lib}/http.js | 0 {src => lib}/log.js | 0 lib/loop.js | 291 ++ src/monitor.js => lib/monitoring.js | 0 src/nodeload.js => lib/nl.js | 0 lib/nodeload.js | 228 +- lib/nodeloadlib.js | 147 - {src => lib}/remote.js | 0 src/report.js => lib/reporting.js | 0 {src => lib}/stats.js | 0 {src => lib}/statsmgr.js | 0 {src => lib}/summary.tpl | 0 {deps => lib}/template.js | 0 src/api.js => lib/testapi.js | 0 {src => lib}/utils.js | 0 package.json | 38 + {deps => scripts}/jsmin.js | 0 scripts/process_tpl.js | 9 + src/options.js | 136 - src/scheduler.js | 185 - 48 files changed, 482 insertions(+), 9934 deletions(-) delete mode 100644 deps/flot/dygraph-LICENSE.txt delete mode 100644 deps/flot/excanvas.js delete mode 100644 deps/flot/excanvas.min.js delete mode 100644 deps/flot/jquery.colorhelpers.js delete mode 100644 deps/flot/jquery.colorhelpers.min.js delete mode 100644 deps/flot/jquery.flot.crosshair.js delete mode 100644 deps/flot/jquery.flot.crosshair.min.js delete mode 100644 deps/flot/jquery.flot.image.js delete mode 100644 deps/flot/jquery.flot.image.min.js delete mode 100644 deps/flot/jquery.flot.js delete mode 100644 deps/flot/jquery.flot.min.js delete mode 100644 deps/flot/jquery.flot.navigate.js delete mode 100644 deps/flot/jquery.flot.navigate.min.js delete mode 100644 deps/flot/jquery.flot.selection.js delete mode 100644 deps/flot/jquery.flot.selection.min.js delete mode 100644 deps/flot/jquery.flot.stack.js delete mode 100644 deps/flot/jquery.flot.stack.min.js delete mode 100644 deps/flot/jquery.flot.threshold.js delete mode 100644 deps/flot/jquery.flot.threshold.min.js delete mode 100644 deps/flot/jquery.js delete mode 100644 deps/flot/jquery.min.js delete mode 160000 deps/optparse-js rename {src => lib}/config.js (100%) rename deps/dygraph.js => lib/dygraph.tpl (100%) rename {src => lib}/evloops.js (100%) rename {src => lib}/header.js (100%) rename {src => lib}/http.js (100%) rename {src => lib}/log.js (100%) create mode 100644 lib/loop.js rename src/monitor.js => lib/monitoring.js (100%) rename src/nodeload.js => lib/nl.js (100%) delete mode 100644 lib/nodeloadlib.js rename {src => lib}/remote.js (100%) rename src/report.js => lib/reporting.js (100%) rename {src => lib}/stats.js (100%) rename {src => lib}/statsmgr.js (100%) rename {src => lib}/summary.tpl (100%) rename {deps => lib}/template.js (100%) rename src/api.js => lib/testapi.js (100%) rename {src => lib}/utils.js (100%) create mode 100644 package.json rename {deps => scripts}/jsmin.js (100%) create mode 100755 scripts/process_tpl.js delete mode 100644 src/options.js delete mode 100644 src/scheduler.js diff --git a/.gitignore b/.gitignore index 71ec932..c2ef917 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ results*.html +*.tpl.js tmp diff --git a/Makefile b/Makefile index d9ef580..f418a9d 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,18 @@ .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/*.tpl.js lib/utils.js lib/config.js lib/testapi.js lib/job.js lib/monitoring.js lib/remote.js lib/stats.js lib/log.js lib/reporting.js lib/http.js lib/template.js all: compile clean: - rm -rf ./lib + rm -f ./lib/nodeload.js ./lib/*.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/summary.tpl > lib/summary.tpl.js + $(PROCESS_TPL) DYGRAPH_SOURCE lib/dygraph.tpl > lib/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" > ./lib/nodeload.js + cat $(SOURCES) | ./scripts/jsmin.js >> ./lib/nodeload.js + chmod +x ./lib/nodeload.js \ No newline at end of file diff --git a/deps/flot/dygraph-LICENSE.txt b/deps/flot/dygraph-LICENSE.txt deleted file mode 100644 index 536c0a8..0000000 --- a/deps/flot/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/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 \n \n \n\n \n

Test Results

<%=new Date()%>

\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
\" style=\"float:left;width:660px;height:200px;\">
\n
legend\" style=\"float:left;width:80px;height:200px;\">
\n
\n
\n <% } %>\n <% } %>\n
\n
\n

Cumulative

\n <% for (var i in reports) { %>\n

\"/>

\n \n <% } %>\n
\n
\n
\n \n \n \n\n

generated with nodeload

\n \n";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 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.masterhost=master[0];this.master=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){var s=JSON.stringify(object);var req=this.master.request('POST',url,{'host':this.masterhost,'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],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/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 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{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);}});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/nodeloadlib.js b/lib/nodeloadlib.js deleted file mode 100644 index c80ed32..0000000 --- a/lib/nodeloadlib.js +++ /dev/null @@ -1,147 +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.masterhost=master[0];this.master=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){var s=JSON.stringify(object);var req=this.master.request('POST',url,{'host':this.masterhost,'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],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/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{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/src/remote.js b/lib/remote.js similarity index 100% rename from src/remote.js rename to lib/remote.js diff --git a/src/report.js b/lib/reporting.js similarity index 100% rename from src/report.js rename to lib/reporting.js diff --git a/src/stats.js b/lib/stats.js similarity index 100% rename from src/stats.js rename to lib/stats.js diff --git a/src/statsmgr.js b/lib/statsmgr.js similarity index 100% rename from src/statsmgr.js rename to lib/statsmgr.js diff --git a/src/summary.tpl b/lib/summary.tpl similarity index 100% rename from src/summary.tpl rename to lib/summary.tpl diff --git a/deps/template.js b/lib/template.js similarity index 100% rename from deps/template.js rename to lib/template.js diff --git a/src/api.js b/lib/testapi.js similarity index 100% rename from src/api.js rename to lib/testapi.js diff --git a/src/utils.js b/lib/utils.js similarity index 100% rename from src/utils.js rename to lib/utils.js diff --git a/package.json b/package.json new file mode 100644 index 0000000..e4f0a65 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "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 ", + "Jeremy Volkman " + ], + "repository": { + "type": "git", + "url": "http://github.com/benschmaus/nodeload" + }, + "main": "nodeload", + "bin": { + "nodeload": "./lib/nodeload.js", + "nl": "./lib/nl.js" + }, + "modules": { + "job": "./lib/job", + "stats": "./lib/stats", + "monitoring": "./lib/monitoring", + "http": "./lib/http", + "reporting": "./lib/reporting", + "remote": "./lib/remote" + }, + "scripts": { + "preinstall": "make clean compile", + "test": "make test" + }, + "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..085989e --- /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 + '=' + JSON.stringify(file) + ';') \ No newline at end of file diff --git a/src/options.js b/src/options.js deleted file mode 100644 index 368a82b..0000000 --- a/src/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/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 From 0469ffb309e128c1cb8874c5188721e1c01717a3 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Thu, 11 Nov 2010 11:27:05 -0500 Subject: [PATCH 02/41] loop, monitoring, and http are implemented with tests. --- Makefile | 1 + lib/header.js | 2 +- lib/http.js | 130 +++++++----- lib/loop.js | 457 +++++++++++++++++++++------------------- lib/monitoring.js | 283 ++++++++++++++++++++----- lib/nl.js | 2 +- lib/nodeload.js | 103 ++++----- lib/stats.js | 173 +++++++-------- lib/util.js | 56 +++++ lib/utils.js | 41 ---- package.json | 3 +- test/http.test.js | 40 ++++ test/loop.test.js | 133 ++++++++++++ test/monitoring.test.js | 209 ++++++++++++++++++ 14 files changed, 1114 insertions(+), 519 deletions(-) create mode 100644 lib/util.js delete mode 100644 lib/utils.js create mode 100644 test/http.test.js create mode 100644 test/loop.test.js create mode 100644 test/monitoring.test.js diff --git a/Makefile b/Makefile index f418a9d..2f7f803 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ SOURCES = lib/header.js lib/*.tpl.js lib/utils.js lib/config.js lib/testapi.js l all: compile clean: + rm -rf ./lib-cov rm -f ./lib/nodeload.js ./lib/*.tpl.js rm -f results-*-err.log results-*-stats.log results-*-summary.html diff --git a/lib/header.js b/lib/header.js index bf1fcbd..403071b 100644 --- a/lib/header.js +++ b/lib/header.js @@ -4,4 +4,4 @@ var sys = require('sys'), events = require('events'), querystring = require('querystring'); -var START = new Date().getTime(); +var BUILD_AS_SINGLE_FILE = true; diff --git a/lib/http.js b/lib/http.js index 899593b..aa06539 100644 --- a/lib/http.js +++ b/lib/http.js @@ -7,63 +7,81 @@ // 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(); +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var http = require('http'); +var fs = require('fs'); +var util = require('./util'); +var qputs = util.qputs; +} + +/** 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 = []; +}; +HttpServer.prototype.start = function(port) { + if (this.server) { return; } + + var self = this; + port = port || 8000; + self.server = http.createServer(function(req, res) { self.route_(req, res); }); + self.server.listen(port); + qputs('Started HTTP server on port ' + port + '.'); + return self; +}; +HttpServer.prototype.stop = function() { + if (!this.server) { return; } + this.server.close(); + this.server = null; + qputs('Shutdown HTTP server.'); +}; +HttpServer.prototype.on = function(regex, handler) { + this.routes.push({regex: regex, handler: handler}); + return this; +}; +HttpServer.prototype.route_ = function(req, res) { + this.routes.forEach(function(r) { + if (req.url.match(r.regex)) { + r.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; } - }, - 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(); - }); + 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 +/** The global HTTP server used by nodeload */ +exports.HTTP_SERVER = new HttpServer(); + +// // 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/lib/loop.js b/lib/loop.js index 19769eb..25c35f6 100644 --- a/lib/loop.js +++ b/lib/loop.js @@ -1,121 +1,6 @@ -// // ----------------------------------------- -// // 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(); -// +/*jslint laxbreak: true, undef: true */ +/*global setTimeout: false */ + // ----------------------------------------- // Event-based looping // ----------------------------------------- @@ -128,31 +13,82 @@ // of other event based loops for predefined tasks, such as tracking the latency of the loop body. // +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('./util'); +var EventEmitter = require('events').EventEmitter; +} + +/** LOOP_DEFAULTS defines all of the parameters that used with Loop.create() and Scheduler.schedule() */ +var LOOP_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 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: null, // If argGenerator is NOT specified, then this is passed to the fun as + // "args". + concurrency: 1, // Number of concurrent calls of fun() (Scheduler only) + 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() + delay: 0, // Seconds to wait before calling fun() for the first time + monitored: true // Does this loop need to finish in order for a call to the containing + // Scheduler.startAll() to complete +}; + /** 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. -@param fun a function: +@param fun an asynchronous function that calls loopFun(result) when it finishes: function(loopFun, args) { ... loopFun(result); } - that calls loopFun(result) when it finishes. Use LoopUtils.funLoop() to wrap a - function for use in a Loop. + use LoopUtils.funLoop() to wrap a 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. See LoopConditions. + function returns false, the loop terminates. Loop#timeLimit and Loop#maxExecutions + are conditions that can be used here. @param delay number of seconds before the first iteration of fun is executed */ -var Loop = exports.Loop = function Loop(fun, args, conditions, delay) { +var Loop = exports.Loop = function Loop(fun, args, conditions, delay, monitored) { EventEmitter.call(this); + this.id = util.uid(); this.fun = fun; this.args = args; this.conditions = conditions || []; this.delay = delay; - this.stopped = true; -} + this.monitored = monitored; + this.running = false; +}; + +/** Static method to create a Loop from a spec object. LOOP_DEFAULTS lists the supported parameters. */ +Loop.create = function(spec) { + util.defaults(spec, LOOP_DEFAULTS); + + var fun = (spec.rps < Infinity) + ? Loop.rpsLoop(spec.rps, spec.fun) + : spec.fun, + args = spec.argGenerator && spec.argGenerator(), + conditions = []; + + if (spec.numberOfTimes > 0 && spec.numberOfTimes < Infinity) { + conditions.push(Loop.maxExecutions(spec.numberOfTimes)); + } + if (spec.duration > 0 && spec.duration < Infinity) { + var duration = (spec.delay && spec.delay > 0) + ? spec.duration + spec.delay + : spec.duration; + conditions.push(Loop.timeLimit(duration)); + } + + return new Loop(fun, args, conditions, spec.delay, spec.monitored); +}; util.inherits(Loop, EventEmitter); @@ -167,125 +103,202 @@ Loop.prototype.start = function() { self.loop_(); }; - if (!this.stopped) return; - self.stopped = false; + if (self.running) { return; } + self.running = true; if (self.delay && self.delay > 0) { - setTimeout(function() { startLoop() }, self.delay * 1000); + setTimeout(startLoop, self.delay * 1000); } else { - startLoop(); + process.nextTick(startLoop); } -} + + return this; +}; -Loop.prototype.stop =function() { - this.stopped = true; -}, +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.stopped && this.conditions.every(function(c) { return c(); }); -}, + return this.running && this.conditions.every(function(c) { return c(); }); +}; /** Checks conditions and schedules the next loop iteration */ Loop.prototype.loop_ = function() { - if (this.checkConditions_()) { - var self = this, - callback = function() { self.loop_() }; - process.nextTick(function() { self.fun(callback, self.args) }); + var self = this, + callback = function(result) { + self.emit('enditeration', result); + self.loop_(); + }, + callFun = function() { + self.emit('startiteration', self.args); + self.fun(callback, self.args); + }; + + if (self.checkConditions_()) { + process.nextTick(callFun); } else { + self.running = false; self.emit('end'); } -} +}; -/** Loop.Conditions contains predefined functions that can be used in Loop.conditions */ -Loop.Conditions = { - /** 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) - }; - } +// 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); + }; }; -/** Loop.Utils contains helpers for dealing with Loop loop functions */ -var Loop.Utils = { - getLoop: function(spec) { - var fun = (spec.rps && spec.rps < Infinity) - ? Loop.Utils.rpsLoop(spec.rps, spec.fun) - : spec.fun, - args = spec.argGenerator && spec.argGenerator(), - conditions = []; +// Helpers for dealing with Loop loop functions - if (spec.numberOfTimes && spec.numberOfTimes > 0 && spec.numberOfTimes < Infinity) { - conditions.push(Loop.Conditions.maxExecutions(spec.numberOfTimes)); - } - if (spec.duration && spec.duration > 0 && spec.duration < Infinity) { - var duration = (spec.delay && spec.delay > 0) - ? spec.duration + spec.delay - : spec.duration; - conditions.push(LoopConditions.timeLimit(duration)); - } +/** 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(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. */ +Loop.loopWrapper = function(fun, start, finish) { + return function(loopFun, args) { + var startRes = start && start(args), + finishFun = function(result) { + if (result === undefined) { + util.qputs('Function result is null; did you forget to call loopFun(result)?'); + } - return new Loop(fun, args, conditions, spec.delay); - }, - /** A wrapper for any existing function so it can be used by Loop. e.g.: - myfun = function(x) { return x+1; } - new Loop(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); + if (finish) { finish(result, startRes); } + + loopFun(result); + }; + fun(finishFun, args); + }; +}; +/** Wrapper for executing a Loop function rps times per second. */ +Loop.rpsLoop = function(rps, fun) { + var finished, lagging, + timeout = (rps && rps > 0) ? (1/rps * 1000) : 0, + 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(); } - fun(finishFun, args); + }, timeout); } - }, - /** Wrapper for executing a Loop function rps times per second. */ - rpsLoop: function(rps, fun) { - var finished, lagging, - timeout = (rps && rps > 0) ? (1/rps * 1000) : 0, - finishFun = function(loopFun) { - finished = true; - if (lagging) { - loopFun(); - } - }; + var callback = function() { finishFun(loopFun); }; + fun(callback, args); + }; +}; - return function(loopFun, args) { - finished = false; - lagging = (timeout <= 0); - if (!lagging) { - setTimeout(function() { - lagging = !finished; - if (!lagging) { - loopFun(); - } - }, timeout); - } - var callback = function() { finishFun(loopFun) } - fun(callback, args); + +// ----------------------------------------- +// Scheduler for event-based loops +// ----------------------------------------- +// +// Scheduler provides a way to define and group sets of Loops that are started and stopped together. + +/** A scheduler starts and monitors a group of Loops. Each Loop can be monitored or unmonitored. When all +monitored loops complete, Scheduler considers the entire group to be complete and stops all unmonitored +loops. */ +var Scheduler = exports.Scheduler = function Scheduler() { + this.id = util.uid(); + this.loops = []; + this.running = false; +}; + +util.inherits(Scheduler, EventEmitter); + +/** Primary function for adding new Loops. Start all scheduled loops by calling startAll(). If the +scheduler is already started, the loops are started immediately upon scheduling. */ +Scheduler.prototype.schedule = function(spec) { + util.defaults(spec, LOOP_DEFAULTS); + + // concurrency is handled by creating multiple loops with portions of the load + spec.numberOfTimes /= spec.concurrency; + spec.rps /= spec.concurrency; + for (var i = 0; i < spec.concurrency; i++) { + this.addLoop(Loop.create(spec)); + } + + return this; +}; +Scheduler.prototype.addLoop = function(loop) { + this.loops.push(loop); + // If the scheduler is running (startAll() was already called), start new loops immediately + if (this.running) { + this.startLoop_(loop); + } + return this; +}; +/** Start all scheduled Loops. When the loops complete, 'end' event is emitted. */ +Scheduler.prototype.startAll = function() { + var self = this; + if (self.running || self.loops.length === 0) { return; } + self.running = true; + process.nextTick(function() { self.emit('start'); }); + for (var i in self.loops) { + if (!self.loops[i].running) { + self.startLoop_(self.loops[i]); } } -} \ No newline at end of file + return self; +}; +/** Force all loops to finish */ +Scheduler.prototype.stopAll = function() { + this.loops.forEach(function(l) { l.stop(); }); + return this; +}; +Scheduler.prototype.startLoop_ = function(loop) { + var self = this; + loop.start(); + loop.on('end', function() { self.checkFinished_(); }); + return self; +}; +/** Iterate all loops and see if any are still running. If all loops are complete, then emit 'end'. */ +Scheduler.prototype.checkFinished_ = function() { + var isAllUnmonitoredLoops = true, + hasRunningLoop = false; + + this.loops.forEach(function (l) { + if (l.monitored && l.running) { + return false; + } + + isAllUnmonitoredLoops = isAllUnmonitoredLoops && !l.monitored; + hasRunningLoop = hasRunningLoop || l.running; + }); + if (isAllUnmonitoredLoops && hasRunningLoop) { + return false; + } + + this.running = false; + this.stopAll(); + this.emit('end'); + return true; +}; diff --git a/lib/monitoring.js b/lib/monitoring.js index 81dc526..7207145 100644 --- a/lib/monitoring.js +++ b/lib/monitoring.js @@ -1,5 +1,5 @@ // ------------------------------------ -// Test monitor +// Monitoring // ------------------------------------ // // This file defines TEST_MONITOR. @@ -7,56 +7,243 @@ // 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. // +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('./util'); +var EventEmitter = require('events').EventEmitter; +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; +} -/** 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 - }); +var UpdateEventGenerator; + +var Monitor = exports.Monitor = function Monitor() { // arguments contains stats names + EventEmitter.call(this); + this.targets = []; + this.setStats.apply(this, arguments); + this.updater = new UpdateEventGenerator(this); +}; + +util.inherits(Monitor, EventEmitter); + +Monitor.prototype.setStats = function(stats) { // arguments contains stats names + var self = this; + self.monitors = []; + self.stats = {}; + self.interval = {}; + stats = (stats instanceof Array) ? stats : [].concat.apply([], arguments); + stats.forEach(function(name) { + if (!Monitor.Monitors[name]) { throw new Error('No monitor for statistic: ' + name); } + var intervalmon = new Monitor.Monitors[name](); + var overallmon = new Monitor.Monitors[name](); + self.monitors.push(intervalmon); + self.monitors.push(overallmon); + self.interval[name] = intervalmon.stats; + self.stats[name] = overallmon.stats; + }); +}; + +Monitor.prototype.updateEvery = function(intervalMs) { + this.updater.updateEvery(intervalMs); + return this; +}; + +Monitor.prototype.disableUpdates = function() { + return this.updateEvery(0); +}; + +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); }); + } + }; + self.monitors.forEach(function(m) { doStart(m, {}); }); + return { + end: function(result) { + endFuns.forEach(function(f) { f(result); }); + } + }; +}; + +Monitor.prototype.monitor = function(objs) { + var self = this; + objs = (objs instanceof Array) ? objs : [].concat.apply([], arguments); + objs.forEach(function(o) { + var mon; + o.on('start', function(args) { + mon = self.start(args); + }); + o.on('end', function(result) { + mon.end(result); }); - }, - update: function() { - this.emit('update', this.tests); - this.emit('afterUpdate', this.tests); - }, - stop: function() { - this.update(); - this.emit('end', this.tests); - this.tests = []; + }); + return self; +}; + +Monitor.prototype.doUpdate = function() { + this.emit('update', this.interval, this.stats); + util.forEach(this.interval, function(name, stats) { + if (stats.length > 0) { + stats.clear(); + } + }); +}; + +function RuntimeMonitor() { + var self = this; + self.stats = new Histogram(); + self.start = function(context) { context.start = new Date(); }; + self.end = function(context) { self.stats.put(new Date() - context.start); }; +} + +function ResultCodeMonitor() { + var self = this; + self.stats = new ResultsCounter(); + self.end = function(context, http) { self.stats.put(http.res.statusCode); }; +} + +function ConcurrencyMonitor() { + var self = this, c = 0; + self.stats = new Peak(); + self.start = function() { c++; }; + self.end = function() { self.stats.put(c--); }; +} + +function RequestBytesMonitor() { + var self = this; + self.stats = new Accumulator(); + self.end = function(context, http) { + if (http && http.req && http.req.headers && http.req.headers['content-length']) { + self.stats.put(http.req.headers['content-length']); + } + }; +} + +function ResponseBytesMonitor() { + 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); + }); + } + }; +} + +function UniquesMonitor() { + var self = this; + self.stats = new Uniques(); + self.end = function(context, http) { + if (http && http.req) { self.stats.put(http.req.path); } + }; +} + +Monitor.Monitors = { + 'runtime': RuntimeMonitor, + 'latency': RuntimeMonitor, + 'result-codes': ResultCodeMonitor, + 'concurrency': ConcurrencyMonitor, + 'request-bytes': RequestBytesMonitor, + 'response-bytes': ResponseBytesMonitor, + 'uniques': UniquesMonitor, +}; + + +var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // arguments contains stats names + this.statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); + this.stats = {}; + this.interval = {}; + this.monitors = {}; + this.updater = new UpdateEventGenerator(this); +}; + +MonitorSet.prototype.init = function(monitorNames) { // arguments contains monitor names + var self = this; + monitorNames = (monitorNames instanceof Array) ? monitorNames : [].concat.apply([], 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; + }); +}; + +MonitorSet.prototype.updateEvery = function(intervalMs) { + this.monitors.forEach(function (m) { m.updateEvery(intervalMs); }); + this.updater.updateEvery(intervalMs); + return this; +}; + +MonitorSet.prototype.disableUpdates = function() { + return this.updateEvery(0); +}; + +MonitorSet.prototype.start = function(monitor, args) { + monitor = monitor || ''; + if (!this.monitors[monitor]) { + this.init([monitor]); } + return this.monitors[monitor].start(args); +}; + +MonitorSet.prototype.monitor = function(objs) { + var self = this, mons = {}; + objs = (objs instanceof Array) ? objs : [].concat.apply([], arguments); + objs.forEach(function(o) { + o.on('start', function(monitor, args) { + mons[monitor] = self.start(monitor, args); + }); + o.on('end', function(monitor, result) { + if (mons[monitor]) { mons[monitor].end(result); } + }); + }); + return self; +}; + +MonitorSet.prototype.doUpdate = function() { + this.emit('update', this.interval, this.stats); + util.forEach(this.interval, function(monitor, interval) { + util.forEach(interval, function(name, stats) { + if (stats.length > 0) { + stats.clear(); + } + }); + }); +}; + + +function UpdateEventGenerator(monitor) { + this.monitor = monitor; } -Utils.inherits(TestMonitor, events.EventEmitter); +UpdateEventGenerator.prototype.updateEvery = function(intervalMs) { + clearTimeout(this.updateTimeoutId); + this.scheduleUpdate_(intervalMs); +}; +UpdateEventGenerator.prototype.scheduleUpdate_ = function(intervalMs) { + var self = this; + if (intervalMs > 0) { + self.updateTimeoutId = setTimeout( + function() { + self.scheduleUpdate_(intervalMs); + self.monitor.doUpdate(); + }, + intervalMs); + } +}; -/** 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; -}); +var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { + logNameOrObject = logNameOrObject || ('results-' + new Date().getTime() + '-stats.log'); + this.log = (typeof logNameOrObject === 'string') ? new LogFile(logNameOrObject) : logNameOrObject; + this.monitor = monitor; + this.monitor.on('update'); +}; \ No newline at end of file diff --git a/lib/nl.js b/lib/nl.js index 1d9139d..e939f25 100755 --- a/lib/nl.js +++ b/lib/nl.js @@ -31,7 +31,7 @@ options.process(); if (!options.get('url')) options.help(); -var nl = require('../lib/nodeloadlib') +var nl = require('./nodeload') .quiet() .setMonitorIntervalMs(options.get('reportInterval') * 1000); diff --git a/lib/nodeload.js b/lib/nodeload.js index 82c6632..0fda63e 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -var sys=require('sys'),http=require('http'),fs=require('fs'),events=require('events'),querystring=require('querystring');var START=new Date().getTime();var DYGRAPH_SOURCE="// This is the Dygraph library available at http://github.com/danvk/dygraphs\nDYGRAPH_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};';\n";var REPORT_SUMMARY_TEMPLATE="REPORT_SUMMARY_TEMPLATE\n\n \n Test Results\n \n \n \n\n \n

Test Results

<%=new Date()%>

\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
\" style=\"float:left;width:660px;height:200px;\">
\n
legend\" style=\"float:left;width:80px;height:200px;\">
\n
\n
\n <% } %>\n <% } %>\n
\n
\n

Cumulative

\n <% for (var i in reports) { %>\n

\"/>

\n \n <% } %>\n
\n
\n
\n \n \n \n\n

generated with nodeload

\n \n";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;} +var sys=require('sys'),http=require('http'),fs=require('fs'),events=require('events'),querystring=require('querystring');var BUILD_AS_SINGLE_FILE=true;var DYGRAPH_SOURCE="// This is the Dygraph library available at http://github.com/danvk/dygraphs\nDYGRAPH_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};';\n";var REPORT_SUMMARY_TEMPLATE="REPORT_SUMMARY_TEMPLATE\n\n \n Test Results\n \n \n \n\n \n

Test Results

<%=new Date()%>

\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
\" style=\"float:left;width:660px;height:200px;\">
\n
legend\" style=\"float:left;width:80px;height:200px;\">
\n
\n
\n <% } %>\n <% } %>\n
\n
\n

Cumulative

\n <% for (var i in reports) { %>\n

\"/>

\n \n <% } %>\n
\n
\n
\n \n \n \n\n

generated with nodeload

\n \n";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;} @@ -16,22 +16,18 @@ if(spec.stats.indexOf('bytes')>=0){var reqbytes=new Reportable(Accumulator,spec. 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 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 BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var util=require('./util');var EventEmitter=require('events').EventEmitter;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 UpdateEventGenerator;var Monitor=exports.Monitor=function Monitor(){EventEmitter.call(this);this.targets=[];this.setStats.apply(this,arguments);this.updater=new UpdateEventGenerator(this);};util.inherits(Monitor,EventEmitter);Monitor.prototype.setStats=function(stats){var self=this;self.monitors=[];self.stats={};self.interval={};stats=(stats instanceof Array)?stats:[].concat.apply([],arguments);stats.forEach(function(name){if(!Monitor.Monitors[name]){throw new Error('No monitor for statistic: '+name);} +var intervalmon=new Monitor.Monitors[name]();var overallmon=new Monitor.Monitors[name]();self.monitors.push(intervalmon);self.monitors.push(overallmon);self.interval[name]=intervalmon.stats;self.stats[name]=overallmon.stats;});};Monitor.prototype.updateEvery=function(intervalMs){this.updater.updateEvery(intervalMs);return this;};Monitor.prototype.disableUpdates=function(){return this.updateEvery(0);};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);});}};self.monitors.forEach(function(m){doStart(m,{});});return{end:function(result){endFuns.forEach(function(f){f(result);});}};};Monitor.prototype.monitor=function(objs){var self=this;objs=(objs instanceof Array)?objs:[].concat.apply([],arguments);objs.forEach(function(o){var mon;o.on('start',function(args){mon=self.start(args);});o.on('end',function(result){mon.end(result);});});return self;};Monitor.prototype.doUpdate=function(){this.emit('update',this.interval,this.stats);util.forEach(this.interval,function(name,stats){if(stats.length>0){stats.clear();}});};function RuntimeMonitor(){var self=this;self.stats=new Histogram();self.start=function(context){context.start=new Date();};self.end=function(context){self.stats.put(new Date()-context.start);};} +function ResultCodeMonitor(){var self=this;self.stats=new ResultsCounter();self.end=function(context,http){self.stats.put(http.res.statusCode);};} +function ConcurrencyMonitor(){var self=this,c=0;self.stats=new Peak();self.start=function(){c++;};self.end=function(){self.stats.put(c--);};} +function RequestBytesMonitor(){var self=this;self.stats=new Accumulator();self.end=function(context,http){if(http&&http.req&&http.req.headers&&http.req.headers['content-length']){self.stats.put(http.req.headers['content-length']);}};} +function ResponseBytesMonitor(){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);});}};} +function UniquesMonitor(){var self=this;self.stats=new Uniques();self.end=function(context,http){if(http&&http.req){self.stats.put(http.req.path);}};} +Monitor.Monitors={'runtime':RuntimeMonitor,'latency':RuntimeMonitor,'result-codes':ResultCodeMonitor,'concurrency':ConcurrencyMonitor,'request-bytes':RequestBytesMonitor,'response-bytes':ResponseBytesMonitor,'uniques':UniquesMonitor,};var MonitorSet=exports.MonitorSet=function MonitorSet(statsNames){this.statsNames=(statsNames instanceof Array)?statsNames:[].concat.apply([],arguments);this.stats={};this.interval={};this.monitors={};this.updater=new UpdateEventGenerator(this);};MonitorSet.prototype.init=function(monitorNames){var self=this;monitorNames=(monitorNames instanceof Array)?monitorNames:[].concat.apply([],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;});};MonitorSet.prototype.updateEvery=function(intervalMs){this.monitors.forEach(function(m){m.updateEvery(intervalMs);});this.updater.updateEvery(intervalMs);return this;};MonitorSet.prototype.disableUpdates=function(){return this.updateEvery(0);};MonitorSet.prototype.start=function(monitor,args){monitor=monitor||'';if(!this.monitors[monitor]){this.init([monitor]);} +return this.monitors[monitor].start(args);};MonitorSet.prototype.monitor=function(objs){var self=this,mons={};objs=(objs instanceof Array)?objs:[].concat.apply([],arguments);objs.forEach(function(o){o.on('start',function(monitor,args){mons[monitor]=self.start(monitor,args);});o.on('end',function(monitor,result){if(mons[monitor]){mons[monitor].end(result);}});});return self;};MonitorSet.prototype.doUpdate=function(){this.emit('update',this.interval,this.stats);util.forEach(this.interval,function(monitor,interval){util.forEach(interval,function(name,stats){if(stats.length>0){stats.clear();}});});};function UpdateEventGenerator(monitor){this.monitor=monitor;} +UpdateEventGenerator.prototype.updateEvery=function(intervalMs){clearTimeout(this.updateTimeoutId);this.scheduleUpdate_(intervalMs);};UpdateEventGenerator.prototype.scheduleUpdate_=function(intervalMs){var self=this;if(intervalMs>0){self.updateTimeoutId=setTimeout(function(){self.scheduleUpdate_(intervalMs);self.monitor.doUpdate();},intervalMs);}};var StatsLogger=exports.StatsLogger=function StatsLogger(monitor,logNameOrObject){logNameOrObject=logNameOrObject||('results-'+new Date().getTime()+'-stats.log');this.log=(typeof logNameOrObject==='string')?new LogFile(logNameOrObject):logNameOrObject;this.monitor=monitor;this.monitor.on('update');};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();}} @@ -59,52 +55,31 @@ function serveRemote(url,req,res){var readBody=function(req,callback){var 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/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;ithis.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]!==undefined){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];} +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){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=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){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;};function statsClassFromString(name){var types={"Histogram":Histogram,"Accumulator":Accumulator,"ResultsCounter":ResultsCounter,"Uniques":Uniques,"Peak":Peak,"Rate":Rate,"LogFile":LogFile,"NullLog":NullLog,"Reportable":Reportable};return types[name];} 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;} @@ -123,10 +98,12 @@ function getChartAsJson(chart){return(chart==null)?null:JSON.stringify(chart.row 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{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;} +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 BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var http=require('http');var fs=require('fs');var util=require('./util');var qputs=util.qputs;} +var HttpServer=exports.HttpServer=function HttpServer(port){this.port=port||8000;this.routes=[];};HttpServer.prototype.start=function(){var self=this;if(self.server){return;} +self.server=http.createServer(function(req,res){self.route_(req,res)});self.server.listen(self.port);qputs('Started HTTP server on port '+port+'.');return self;};HttpServer.prototype.stop=function(){if(!this.server){return;} +this.server.close();this.server=null;qputs('Shutdown HTTP server.');};HttpServer.prototype.on=function(regex,handler){this.routes.push({regex:regex,handler:handler});};HttpServer.prototype.route_=function(req,res){this.routes.forEach(function(r){if(req.url.match(r.regex)){r.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!=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);}});template={cache:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)) +response.end();});});};NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);}});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]) diff --git a/lib/stats.js b/lib/stats.js index cb740d9..3861b77 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -1,27 +1,26 @@ +/*jslint forin:true */ + // ------------------------------------ // Statistics // ------------------------------------ // // 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. +// +var BUILD_AS_SINGLE_FILE; +if (BUILD_AS_SINGLE_FILE === undefined) { +var fs = require('fs'); +} var Histogram = exports.Histogram = function(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.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 +35,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 +55,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 +71,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] !== undefined) { sum += this.items[i]; if (sum >= target) { return i; @@ -94,7 +93,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 +103,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 +134,13 @@ Histogram.prototype = { this.extra = this.extra.concat(other.extra); this.sorted = false; } -} +}; var Accumulator = exports.Accumulator = function() { this.type = "Accumulator"; this.total = 0; this.length = 0; -} +}; Accumulator.prototype = { put: function(stat) { this.total += stat; @@ -160,18 +160,17 @@ Accumulator.prototype = { this.total += other.total; this.length += other.length; } -} +}; var ResultsCounter = exports.ResultsCounter = function() { 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 +194,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,7 +212,7 @@ ResultsCounter.prototype = { } this.length += other.length; } -} +}; var Uniques = exports.Uniques = function() { this.type = "Uniques"; @@ -217,14 +220,14 @@ var Uniques = exports.Uniques = function() { 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 +244,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 +253,13 @@ Uniques.prototype = { } 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.peak < item) { @@ -279,13 +282,13 @@ Peak.prototype = { } this.length += other.length; } -} +}; var Rate = exports.Rate = function() { - type = "Rate"; + this.type = "Rate"; this.start = new Date(); this.length = 0; -} +}; Rate.prototype = { put: function() { this.length++; @@ -303,7 +306,7 @@ Rate.prototype = { merge: function(other) { this.length += other.length; } -} +}; var LogFile = exports.LogFile = function(filename) { this.type = "LogFile"; @@ -311,7 +314,7 @@ var LogFile = exports.LogFile = function(filename) { this.length = 0; this.filename = filename; this.open(); -} +}; LogFile.prototype = { put: function(item) { var buf = new Buffer(item); @@ -321,15 +324,15 @@ 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() { @@ -342,37 +345,37 @@ LogFile.prototype = { summary: function() { return { file: this.filename, written: this.length }; } -} +}; var NullLog = exports.NullLog = function() { 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 Reportable = exports.Reportable = function(Backend, name, trend) { var backendparams = null; name = name || ""; - if (typeof backend == 'object') { - backendparams = backend[1]; - backend = backend[0]; + if (typeof Backend === 'object') { + backendparams = Backend[1]; + Backend = Backend[0]; } this.type = "Reportable"; this.name = name; this.length = 0; - this.interval = new backend(backendparams); - this.cumulative = new backend(backendparams); + this.interval = new Backend(backendparams); + this.cumulative = new Backend(backendparams); this.trend = trend; this.lastSummary = null; -} +}; Reportable.prototype = { put: function(stat) { if (!this.disableIntervalReporting) { @@ -396,7 +399,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,17 +407,17 @@ Reportable.prototype = { this.interval.merge(other); this.cumulative.merge(other); } -} +}; var roundRobin = exports.roundRobin = function(list) { - r = list.slice(); + 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 s = ""; @@ -422,33 +425,33 @@ var randomString = exports.randomString = function(length) { 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; + 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; + 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 = { + var types = { "Histogram": Histogram, "Accumulator": Accumulator, "ResultsCounter": ResultsCounter, diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..c3d8703 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,56 @@ +// ------------------------------------ +// Statistics Manager +// ------------------------------------ +// +// This file defines qputs, qprint, and Utils. +// +// Extends node.js util.js with other common functions. + +var util = require('util'); + +// A few common global functions so we can access them with as few keystrokes as possible +// +var NODELOAD_CONFIG; +NODELOAD_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); } +}; + + +// Static utility methods +// +util.uid = function() { + this.lastUid_ = this.lastUid_ || 0; + return this.lastUid_++; +}; +util.defaults = function(obj, defaults) { + for (var i in defaults) { + if (obj[i] === undefined) { + obj[i] = defaults[i]; + } + } +}; +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.argarray = function(args) { + return (args instanceof Array) ? args : [].concat.apply([], args); +}; + +util.extend(exports, util); \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index 28f6fb9..0000000 --- a/lib/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/package.json b/package.json index e4f0a65..739c4af 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,7 @@ "remote": "./lib/remote" }, "scripts": { - "preinstall": "make clean compile", - "test": "make test" + "preinstall": "make clean compile" }, "dependencies": { } diff --git a/test/http.test.js b/test/http.test.js new file mode 100644 index 0000000..7daef61 --- /dev/null +++ b/test/http.test.js @@ -0,0 +1,40 @@ +var http = require('http'), + nlhttp = require('../lib/http'), + HTTP_SERVER = nlhttp.HTTP_SERVER; + +module.exports = { + 'example: add a new route': function(assert, beforeExit) { + var done = false; + HTTP_SERVER.start(); + HTTP_SERVER.on('^/route', function() { + done = true; + HTTP_SERVER.stop(); + }); + + var client = http.createClient(8000, '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; + HTTP_SERVER.start(); + var client = http.createClient(8000, '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; + HTTP_SERVER.stop(); + }); + }); + + 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..5b48b73 --- /dev/null +++ b/test/loop.test.js @@ -0,0 +1,133 @@ +var loop = require('../lib/loop'), + Loop = loop.Loop, + Scheduler = loop.Scheduler; + +module.exports = { + 'example: a basic rps loop with set duration': function(assert, beforeExit) { + var i = 0, start = new Date(), lasttime = start, duration, + l = Loop.create({ + fun: function(loopFun) { + var now = new Date(); + assert.ok(Math.abs(now - lasttime) < 205, (now - lasttime).toString()); + lasttime = now; + + i++; + loopFun(); + }, + 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'); + assert.ok(!l.running, 'loop still flagged as running'); + assert.ok(Math.abs(duration - 1000) < 30, '1000 == ' + duration); + }); + }, + 'test numberOfTimes loop': function(assert, beforeExit) { + var i = 0, + l = Loop.create({ + fun: function(loopFun) { i++; loopFun(); }, + 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 = Loop.create({ + fun: function(loopFun) { loopFun(); }, + 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, s = new Scheduler(); + s.schedule({ + fun: function(loopFun) { i++; loopFun(); }, + rps: 10, + duration: 1, + concurrency: 5 + }).startAll(); + + s.on('end', function() { duration = new Date() - start; }); + + assert.equal(s.loops.length, 5); + beforeExit(function() { + assert.equal(i, 10, 'loop executed incorrect number of times'); + assert.ok(s.loops.every(function(l){ return !l.running; }), 'loops still flagged as running'); + assert.ok(Math.abs(duration - 1000) < 30, '1000 == ' + duration); + }); + }, + 'scheduler emits events': function(assert, beforeExit) { + var s = new Scheduler(), started = false, ended = false; + s.schedule({ + fun: function(loopFun) { loopFun(); }, + numberOfTimes: 3 + }).startAll(); + + s.on('start', function() { started = true; }); + s.on('end', function() { ended = true; }); + + beforeExit(function() { + assert.ok(started, 'start never emitted'); + assert.ok(ended, 'end never emitted'); + }); + }, + 'test mixed monitored and unmonitored loops': function(assert, beforeExit) { + var s = new Scheduler(); + s.schedule({ + fun: function(loopFun) { loopFun(); }, + numberOfTimes: 50, + concurrency: 5 + }); + s.schedule({ + fun: function(loopFun) { loopFun(); }, + rps: 1, + monitored: false + }); + s.startAll(); + + var unmonitoredLoops = s.loops.filter(function(l) { return !l.monitored; }); + assert.equal(s.loops.length, 6); + assert.equal(unmonitoredLoops.length, 1); + assert.ok(s.loops.every(function(l){ return l.running; }), 'not all loops started'); + beforeExit(function() { + assert.ok(s.loops.every(function(l){ return !l.running; }), 'loops still flagged as running'); + }); + }, + 'test all unmonitored loops': function(assert, beforeExit) { + var s = new Scheduler(), ended = false; + s.schedule({ + fun: function(loopFun) { loopFun(); }, + rps: 2, + concurrency: 2, + monitored: false + }); + s.startAll(); + s.on('end', function() { ended = true; }); + + var unmonitoredLoops = s.loops.filter(function(l) { return !l.monitored; }); + assert.equal(s.loops.length, unmonitoredLoops.length); + + s.loops.forEach(function(l) { l.stop(); }); + + beforeExit(function() { + assert.ok(ended, 'scheduler never finished'); + }); + } +}; \ No newline at end of file diff --git a/test/monitoring.test.js b/test/monitoring.test.js new file mode 100644 index 0000000..57ce270 --- /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, + MonitorSet = monitoring.MonitorSet; + +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 mon = m.start(), runtime = Math.floor(Math.random() * 100); + setTimeout( + function() { mon.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 MonitorSet to organize multiple Monitors': function(assert, beforeExit) { + var m = new MonitorSet('runtime'), + f = function() { + var trmon = m.start('transaction'); + mockConnection(function(conn) { + var opmon = m.start('operation'); + conn.operation(function() { + opmon.end(); + trmon.end(); + }); + }); + }; + + for (var i = 0; i < 10; i++) { + f(); + } + + 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.ok(Math.abs(trSummary.median - 100) <= 5, '100 == ' + trSummary.median); + assert.ok(Math.abs(opSummary.median - 25) <= 5); + }); + }, + 'example: use EventEmitter objects instead of interacting with MonitorSet 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 MonitorSet('runtime'); + for (var i = 0; i < 5; i++) { + var obj = new MonitoredObject(); + m.monitor(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.monitor(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(80, 'www.google.com'), + f = function() { + var mon = m.start(), + path = '/search?q=' + q++, + req = client.request( + 'GET', + path, + {'host': 'www.google.com'} + ); + req.path = path; + req.end(); + req.on('response', function(res) { + mon.end({req: req, res: res}); + }); + }; + + client.on('error', function(e) { assert.fail('This test requires internet connectivity: ' + e); }); + + 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 >= 1); + assert.equal(resultCodesSummary['200'], 2); + + assert.equal(uniquesSummary.total, 2); + assert.equal(uniquesSummary.uniqs, 2); + + assert.equal(requestBytesSummary.total, 0); + + assert.ok(responseBytesSummary.total > 1000); + }); + }, + 'monitor generates update events with interval and overall stats': function(assert, beforeExit) { + var m = new Monitor('runtime').updateEvery(220), + intervals = 0, + f = function() { + var mon = m.start(), runtime = Math.floor(Math.random() * 10); + setTimeout(function() { mon.end(); }, runtime); + }; + + // 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.disableUpdates(); }, 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 From 16f0658e0177239acf39779b37ddb778d3e56922 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Thu, 11 Nov 2010 13:13:24 -0500 Subject: [PATCH 03/41] Support logging from MonitorSet Minor fixes --- lib/monitoring.js | 54 ++++++++++++++++++++++++++++++++++++----- lib/stats.js | 2 +- scripts/process_tpl.js | 6 ++--- test/loop.test.js | 2 +- test/monitoring.test.js | 12 ++++----- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lib/monitoring.js b/lib/monitoring.js index 7207145..a149932 100644 --- a/lib/monitoring.js +++ b/lib/monitoring.js @@ -9,6 +9,7 @@ // var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { +var START = new Date(); var util = require('./util'); var EventEmitter = require('events').EventEmitter; var stats = require('./stats'); @@ -20,7 +21,7 @@ var Accumulator = stats.Accumulator; var LogFile = stats.LogFile; } -var UpdateEventGenerator; +var UpdateEventGenerator, StatsLogger; var Monitor = exports.Monitor = function Monitor() { // arguments contains stats names EventEmitter.call(this); @@ -160,13 +161,28 @@ Monitor.Monitors = { var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // arguments contains stats names + EventEmitter.call(this); + + var summaryFun = function() { + var summary = {}; + util.forEach(this, function(monitor, stats) { + if (monitor === 'summary') { return; } + summary[monitor] = {}; + util.forEach(stats, function(name, stat) { + summary[monitor][name] = stat.summary(); + }); + }); + return summary; + }; this.statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); - this.stats = {}; - this.interval = {}; this.monitors = {}; this.updater = new UpdateEventGenerator(this); + this.stats = { summary: summaryFun }; + this.interval = { summary: summaryFun }; }; +util.inherits(MonitorSet, EventEmitter); + MonitorSet.prototype.init = function(monitorNames) { // arguments contains monitor names var self = this; monitorNames = (monitorNames instanceof Array) ? monitorNames : [].concat.apply([], arguments); @@ -178,7 +194,7 @@ MonitorSet.prototype.init = function(monitorNames) { // arguments contains monit }; MonitorSet.prototype.updateEvery = function(intervalMs) { - this.monitors.forEach(function (m) { m.updateEvery(intervalMs); }); + util.forEach(this.monitors, function (name, m) { m.updateEvery(intervalMs); }); this.updater.updateEvery(intervalMs); return this; }; @@ -209,6 +225,19 @@ MonitorSet.prototype.monitor = function(objs) { return self; }; +MonitorSet.prototype.enableLogging = function(logNameOrObject) { + if (this.logger) { return; } + this.logger = new StatsLogger(this, logNameOrObject).start(); + return this; +}; + +MonitorSet.prototype.disableLogging = function() { + if (!this.logger) { return; } + this.logger.stop(); + this.logger = null; + return this; +}; + MonitorSet.prototype.doUpdate = function() { this.emit('update', this.interval, this.stats); util.forEach(this.interval, function(monitor, interval) { @@ -242,8 +271,21 @@ UpdateEventGenerator.prototype.scheduleUpdate_ = function(intervalMs) { var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { - logNameOrObject = logNameOrObject || ('results-' + new Date().getTime() + '-stats.log'); + logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '-stats.log'); this.log = (typeof logNameOrObject === 'string') ? new LogFile(logNameOrObject) : logNameOrObject; this.monitor = monitor; - this.monitor.on('update'); + this.logger_ = this.log_.bind(this); +}; +StatsLogger.prototype.start = function() { + this.monitor.on('update', this.logger_); + this.log.put('['); + return this; +}; +StatsLogger.prototype.stop = function() { + this.log.put(']'); + this.monitor.removeListener('update', this.logger_); + return this; +}; +StatsLogger.prototype.log_ = function(interval) { + this.log.put(JSON.stringify(interval.summary()) + ',\n'); }; \ No newline at end of file diff --git a/lib/stats.js b/lib/stats.js index 3861b77..3350277 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -336,7 +336,7 @@ LogFile.prototype = { }); }, open: function() { - this.fd = fs.openSync(this.filename, "w"); + this.fd = fs.openSync(this.filename, "a"); }, close: function() { fs.closeSync(this.fd); diff --git a/scripts/process_tpl.js b/scripts/process_tpl.js index 085989e..4546eb6 100755 --- a/scripts/process_tpl.js +++ b/scripts/process_tpl.js @@ -1,9 +1,9 @@ #!/usr/bin/env node if (process.argv.length < 4) { - console.log('Usage: ./scripts/process_tpl ') - process.exit(1) + 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 + '=' + JSON.stringify(file) + ';') \ No newline at end of file +require('util').puts('var ' + varname + '= exports.' + varname + '=' + JSON.stringify(file) + ';'); \ No newline at end of file diff --git a/test/loop.test.js b/test/loop.test.js index 5b48b73..bdc38f3 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -8,7 +8,7 @@ module.exports = { l = Loop.create({ fun: function(loopFun) { var now = new Date(); - assert.ok(Math.abs(now - lasttime) < 205, (now - lasttime).toString()); + assert.ok(Math.abs(now - lasttime) < 210, (now - lasttime).toString()); lasttime = now; i++; diff --git a/test/monitoring.test.js b/test/monitoring.test.js index 57ce270..47239de 100644 --- a/test/monitoring.test.js +++ b/test/monitoring.test.js @@ -58,12 +58,12 @@ module.exports = { } 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.ok(Math.abs(trSummary.median - 100) <= 5, '100 == ' + trSummary.median); - assert.ok(Math.abs(opSummary.median - 25) <= 5); + 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) <= 5, '100 == ' + summary['transaction']['runtime'].median); + assert.ok(Math.abs(summary['operation']['runtime'].median - 25) <= 5); }); }, 'example: use EventEmitter objects instead of interacting with MonitorSet directly': function(assert, beforeExit) { From 4131338c0f107158de3a2ba5094d5cbd81b9feac Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Thu, 11 Nov 2010 18:39:15 -0500 Subject: [PATCH 04/41] Add ability to configure while require()'ing Add http module Fix dygraph source Add reporting module template passes jslint --- .gitignore | 1 + lib/config.js | 32 +++--- lib/dygraph.tpl | 3 +- lib/http.js | 25 +++-- lib/monitoring.js | 63 +++++------ lib/reporting.js | 230 ++++++++++++++++++++++------------------ lib/summary.tpl | 66 ++++++------ lib/template.js | 50 ++++----- lib/util.js | 7 +- package.json | 3 +- test/http.test.js | 9 +- test/loop.test.js | 2 +- test/monitoring.test.js | 2 +- test/reporting.test.js | 82 ++++++++++++++ 14 files changed, 346 insertions(+), 229 deletions(-) create mode 100644 test/reporting.test.js diff --git a/.gitignore b/.gitignore index c2ef917..2255828 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ results*.html *.tpl.js +.reporting.test-output.html tmp diff --git a/lib/config.js b/lib/config.js index 4f8a282..3bc57fd 100644 --- a/lib/config.js +++ b/lib/config.js @@ -8,57 +8,61 @@ // var nl = require('./lib/nodeloadlib').quiet().usePort(10000); // nl.runTest(...); // +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 */ 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; -} - +}; // ================= -// Private +// Global stuff // ================= -var NODELOAD_CONFIG = { + +var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { 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, @@ -68,13 +72,13 @@ var NODELOAD_CONFIG = { SLAVE_PING_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/dygraph.tpl b/lib/dygraph.tpl index aa823b5..4e38efc 100644 --- a/lib/dygraph.tpl +++ b/lib/dygraph.tpl @@ -1,2 +1 @@ -// 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};'; +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/http.js b/lib/http.js index aa06539..9c6ad07 100644 --- a/lib/http.js +++ b/lib/http.js @@ -9,10 +9,17 @@ 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 NODELOAD_CONFIG = config.NODELOAD_CONFIG; + +exports.disableServer = function() { config.disableServer(); return exports; }; +exports.usePort = function() { config.usePort(); return exports; }; +exports.quiet = function() { config.quiet(); return exports; }; } /** By default, HttpServer knows how to return static files from the current directory. Add new route @@ -76,12 +83,14 @@ HttpServer.prototype.serveFile_ = function(file, response) { }); }; -/** The global HTTP server used by nodeload */ -exports.HTTP_SERVER = new HttpServer(); +// ================= +// Global stuff +// ================= -// // 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 +/** 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.start(NODELOAD_CONFIG.HTTP_PORT); + } +}); \ No newline at end of file diff --git a/lib/monitoring.js b/lib/monitoring.js index a149932..ac28baa 100644 --- a/lib/monitoring.js +++ b/lib/monitoring.js @@ -23,6 +23,9 @@ var LogFile = stats.LogFile; var UpdateEventGenerator, StatsLogger; +// ----------------- +// Monitor +// ----------------- var Monitor = exports.Monitor = function Monitor() { // arguments contains stats names EventEmitter.call(this); this.targets = []; @@ -160,6 +163,9 @@ Monitor.Monitors = { }; +// ----------------- +// MonitorSet +// ----------------- var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // arguments contains stats names EventEmitter.call(this); @@ -191,11 +197,12 @@ MonitorSet.prototype.init = function(monitorNames) { // arguments contains monit self.stats[name] = self.monitors[name].stats; self.interval[name] = self.monitors[name].interval; }); + return self; }; MonitorSet.prototype.updateEvery = function(intervalMs) { - util.forEach(this.monitors, function (name, m) { m.updateEvery(intervalMs); }); this.updater.updateEvery(intervalMs); + util.forEach(this.monitors, function (name, m) { m.updateEvery(intervalMs); }); return this; }; @@ -225,13 +232,13 @@ MonitorSet.prototype.monitor = function(objs) { return self; }; -MonitorSet.prototype.enableLogging = function(logNameOrObject) { +MonitorSet.prototype.startLogger = function(logNameOrObject) { if (this.logger) { return; } this.logger = new StatsLogger(this, logNameOrObject).start(); return this; }; -MonitorSet.prototype.disableLogging = function() { +MonitorSet.prototype.stopLogger = function() { if (!this.logger) { return; } this.logger.stop(); this.logger = null; @@ -240,36 +247,10 @@ MonitorSet.prototype.disableLogging = function() { MonitorSet.prototype.doUpdate = function() { this.emit('update', this.interval, this.stats); - util.forEach(this.interval, function(monitor, interval) { - util.forEach(interval, function(name, stats) { - if (stats.length > 0) { - stats.clear(); - } - }); - }); -}; - - -function UpdateEventGenerator(monitor) { - this.monitor = monitor; -} -UpdateEventGenerator.prototype.updateEvery = function(intervalMs) { - clearTimeout(this.updateTimeoutId); - this.scheduleUpdate_(intervalMs); -}; -UpdateEventGenerator.prototype.scheduleUpdate_ = function(intervalMs) { - var self = this; - if (intervalMs > 0) { - self.updateTimeoutId = setTimeout( - function() { - self.scheduleUpdate_(intervalMs); - self.monitor.doUpdate(); - }, - intervalMs); - } }; +/** StatsLogger writes interval stats from a Monitor or MonitorSet to disk each time it emits 'update' */ var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '-stats.log'); this.log = (typeof logNameOrObject === 'string') ? new LogFile(logNameOrObject) : logNameOrObject; @@ -288,4 +269,26 @@ StatsLogger.prototype.stop = function() { }; StatsLogger.prototype.log_ = function(interval) { this.log.put(JSON.stringify(interval.summary()) + ',\n'); +}; + +// ================= +// Private methods +// ================= +function UpdateEventGenerator(monitor) { + this.monitor = monitor; +} +UpdateEventGenerator.prototype.updateEvery = function(intervalMs) { + clearTimeout(this.updateTimeoutId); + this.scheduleUpdate_(intervalMs); +}; +UpdateEventGenerator.prototype.scheduleUpdate_ = function(intervalMs) { + var self = this; + if (intervalMs > 0) { + self.updateTimeoutId = setTimeout( + function() { + self.scheduleUpdate_(intervalMs); + self.monitor.doUpdate(); + }, + intervalMs); + } }; \ No newline at end of file diff --git a/lib/reporting.js b/lib/reporting.js index 4eecf1f..067edb5 100644 --- a/lib/reporting.js +++ b/lib/reporting.js @@ -7,27 +7,72 @@ // 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. +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var START = new Date(); +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 DYGRAPH_SOURCE = require('./dygraph.tpl.js').DYGRAPH_SOURCE; +var HTTP_SERVER = require('./http').HTTP_SERVER; + +exports.setAjaxRefreshIntervalMs = function(ms) { config.setAjaxRefreshIntervalMs(ms); return exports; }; +exports.disableLogs = function() { config.disableLogs(); return exports; }; +exports.disableServer = function() { config.disableServer(); return exports; }; +exports.usePort = function() { config.usePort(); return exports; }; +exports.quiet = function() { config.quiet(); return exports; }; +} +var Chart, timeFromStart; + +/** 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) { +var Report = exports.Report = function(name) { this.name = name; - this.uid = Utils.uid(); + this.uid = util.uid(); this.summary = {}; this.charts = {}; - this.updater = updater; -} +}; Report.prototype = { getChart: function(name) { - if (this.charts[name] == null) + if (!this.charts[name]) { this.charts[name] = new Chart(name); + } return this.charts[name]; }, - update: function() { - if (this.updater != null) { this.updater(this); } + /** 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 MonitorSet emits an 'update' event */ + updateFromMonitorSet: function(monitorset) { + var self = this; + monitorset.on('update', function() { + util.forEach(monitorset.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; + }); + self.getChart(monitorname + statname) + .put(monitor.interval[statname].summary()); + }); } -} +}; /** A Chart represents a collection of lines over time represented as: @@ -41,10 +86,10 @@ Report.prototype = { @param name A name for the chart */ var Chart = exports.Chart = function(name) { this.name = name; - this.uid = Utils.uid(); + this.uid = util.uid(); this.columns = ["time"]; - this.rows = [[timeFromTestStart()]]; -} + 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 @@ -57,121 +102,94 @@ Chart.prototype = { } */ put: function(data) { - var row = [timeFromTestStart()]; - for (item in data) { - var col = this.columns.indexOf(item); + var self = this, row = [timeFromStart()]; + util.forEach(data, function(column, val) { + var col = self.columns.indexOf(column); if (col < 0) { - col = this.columns.length; - this.columns.push(item); - this.rows[0].push(0); + col = self.columns.length; + self.columns.push(column); + self.rows[0].push(0); } - row[col] = data[item]; - } - this.rows.push(row); + row[col] = val; + }); + self.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: {}, +var ReportSet = exports.ReportSet = function() { + this.reports = []; + this.refreshIntervalMs = 2000; +}; +ReportSet.prototype = { addReport: function(report) { - this.reports[report.name] = report; + report = (typeof report === 'string') ? new Report(report) : report; + this.reports.push(report); + return report; }, - getReport: function(name) { - return this.reports[name]; + startLogger: function(logNameOrObject) { + if (this.logger) { return; } + logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '.html'); + this.logger = (typeof logNameOrObject === 'string') ? new LogFile(logNameOrObject) : logNameOrObject; + this.loggingTimeoutId = setTimeout(this.write_.bind(this), this.refreshIntervalMs); + return this; }, - updateReports: function() { - for (var r in this.reports) { - this.reports[r].update(); - } - - LOGS.SUMMARY_HTML.clear(REPORT_MANAGER.getHtml()); + stopLogger: function() { + if (!this.logger) { return; } + clearTimeout(this.loggingTimeoutId); + this.logger.close(); + this.logger = null; + return this; }, reset: function() { this.reports = {}; }, getHtml: function() { - var t = template.create(REPORT_SUMMARY_TEMPLATE); + var self = this, + t = template.create(REPORT_SUMMARY_TEMPLATE); return t({ - querystring: querystring, - refreshPeriodMs: NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS, - reports: this.reports + DYGRAPH_SOURCE: DYGRAPH_SOURCE, + querystring: querystring, + refreshPeriodMs: self.refreshIntervalMs, + reports: self.reports }); + }, + write_: function() { + this.loggingTimeoutId = setTimeout(this.write_.bind(this), this.refreshIntervalMs); + this.logger.clear(this.getHtml()); } -} +}; + +// ================= +// Global stuff +// ================= +/** 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 ReportSet(); +NODELOAD_CONFIG.on('apply', function() { + REPORT_MANAGER.refreshIntervalMs = NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS; + if (NODELOAD_CONFIG.LOGS_ENABLED) { + REPORT_MANAGER.startLogger(); + } +}); + +HTTP_SERVER.on('^/$', 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.on('^/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 timeFromTestStart() { +function timeFromStart() { 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 { - 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/lib/summary.tpl b/lib/summary.tpl index 5231dd3..6f70dd3 100644 --- a/lib/summary.tpl +++ b/lib/summary.tpl @@ -1,4 +1,3 @@ -REPORT_SUMMARY_TEMPLATE Test Results @@ -34,51 +33,39 @@ REPORT_SUMMARY_TEMPLATE - +
- <% for (var i in reports) { %> - <% for (var j in reports[i].charts) { %> - <% var chart = reports[i].charts[j]; %> -

<%=chart.name%>

+ <% reports.forEach(function(report) { %> + <% for (var j in report.charts) { %> + <% var chart = report.charts[j]; %> +

<%=report.name%>: <%=chart.name%>

<% } %> - <% } %> + <% }); %>
diff --git a/lib/template.js b/lib/template.js index 7694627..c8c1c30 100644 --- a/lib/template.js +++ b/lib/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/lib/util.js b/lib/util.js index c3d8703..4cfbf91 100644 --- a/lib/util.js +++ b/lib/util.js @@ -8,10 +8,13 @@ var util = require('util'); +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var NODELOAD_CONFIG = require('./config').NODELOAD_CONFIG; +} + // A few common global functions so we can access them with as few keystrokes as possible // -var NODELOAD_CONFIG; -NODELOAD_CONFIG = NODELOAD_CONFIG || {}; var qputs = util.qputs = function(s) { if (!NODELOAD_CONFIG.QUIET) { util.puts(s); } }; diff --git a/package.json b/package.json index 739c4af..c95b59f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "nl": "./lib/nl.js" }, "modules": { - "job": "./lib/job", + "loop": "./lib/loop", "stats": "./lib/stats", "monitoring": "./lib/monitoring", "http": "./lib/http", @@ -30,6 +30,7 @@ "remote": "./lib/remote" }, "scripts": { + "test": "expresso", "preinstall": "make clean compile" }, "dependencies": { diff --git a/test/http.test.js b/test/http.test.js index 7daef61..cf17e56 100644 --- a/test/http.test.js +++ b/test/http.test.js @@ -1,14 +1,15 @@ var http = require('http'), - nlhttp = require('../lib/http'), + nlhttp = require('../lib/http').quiet(), HTTP_SERVER = nlhttp.HTTP_SERVER; +HTTP_SERVER.start(); +setTimeout(function() { HTTP_SERVER.stop(); }, 1500); + module.exports = { 'example: add a new route': function(assert, beforeExit) { var done = false; - HTTP_SERVER.start(); HTTP_SERVER.on('^/route', function() { done = true; - HTTP_SERVER.stop(); }); var client = http.createClient(8000, '127.0.0.1'), @@ -21,7 +22,6 @@ module.exports = { }, 'test file server finds package.json': function(assert, beforeExit) { var done = false; - HTTP_SERVER.start(); var client = http.createClient(8000, '127.0.0.1'), req = client.request('GET', '/package.json'); req.end(); @@ -29,7 +29,6 @@ module.exports = { assert.equal(res.statusCode, 200); res.on('data', function(chunk) { done = true; - HTTP_SERVER.stop(); }); }); diff --git a/test/loop.test.js b/test/loop.test.js index bdc38f3..ad079fd 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -23,7 +23,7 @@ module.exports = { beforeExit(function() { assert.equal(i, 5, 'loop executed incorrect number of times'); assert.ok(!l.running, 'loop still flagged as running'); - assert.ok(Math.abs(duration - 1000) < 30, '1000 == ' + duration); + assert.ok(Math.abs(duration - 1000) <= 50, '1000 == ' + duration); }); }, 'test numberOfTimes loop': function(assert, beforeExit) { diff --git a/test/monitoring.test.js b/test/monitoring.test.js index 47239de..04e60f2 100644 --- a/test/monitoring.test.js +++ b/test/monitoring.test.js @@ -62,7 +62,7 @@ module.exports = { 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) <= 5, '100 == ' + summary['transaction']['runtime'].median); + 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); }); }, diff --git a/test/reporting.test.js b/test/reporting.test.js new file mode 100644 index 0000000..4963069 --- /dev/null +++ b/test/reporting.test.js @@ -0,0 +1,82 @@ +/*jslint sub:true */ + +var reporting = require('../lib/reporting').disableServer(), + monitoring = require('../lib/monitoring'), + REPORT_MANAGER = reporting.REPORT_MANAGER; + +REPORT_MANAGER.refreshIntervalMs = 500; +REPORT_MANAGER.startLogger('.reporting.test-output.html'); +setTimeout(function() { REPORT_MANAGER.stopLogger(); }, 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('id="chart'+chart1.uid)); + assert.isNotNull(html.match('id="chart'+chart2.uid)); + assert.isNotNull(html.match('graph'+chart1.uid+' = new Dygraph')); + assert.isNotNull(html.match('graph'+chart2.uid+' = new Dygraph')); + assert.isNotNull(html.match('id="reportSummary'+report.uid)); + }, + 'example: update reports from Monitor and MonitorSet stats': function(assert, beforeExit) { + var m = new monitoring.MonitorSet('runtime') + .init('transaction', 'operation') + .updateEvery(200), + f = function() { + var trmon = m.start('transaction'); + mockConnection(function(conn) { + var opmon = m.start('operation'); + conn.operation(function() { + opmon.end(); + trmon.end(); + }); + }); + }; + + REPORT_MANAGER.addReport('All Monitors').updateFromMonitorSet(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.disableUpdates(); }, 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 From 1a3e9d7c0ca15be2401d9ec80e2ce09967be01f3 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 15 Nov 2010 15:11:51 -0500 Subject: [PATCH 05/41] Code cleanup --- lib/config.js | 11 +- lib/evloops.js | 276 ----------------------------------------- lib/header.js | 4 + lib/http.js | 18 ++- lib/log.js | 49 -------- lib/loop.js | 35 +++--- lib/monitoring.js | 79 ++++++++++-- lib/reporting.js | 21 ++-- lib/stats.js | 45 +------ lib/util.js | 6 +- test/http.test.js | 3 +- test/reporting.test.js | 3 +- 12 files changed, 128 insertions(+), 422 deletions(-) delete mode 100644 lib/evloops.js delete mode 100644 lib/log.js diff --git a/lib/config.js b/lib/config.js index 3bc57fd..acaf93f 100644 --- a/lib/config.js +++ b/lib/config.js @@ -2,12 +2,17 @@ // 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. They should be called when the +// library is included: // // var nl = require('./lib/nodeloadlib').quiet().usePort(10000); // nl.runTest(...); // +// Or, if 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; @@ -55,7 +60,7 @@ exports.setSlavePingIntervalMs = function(milliseconds) { }; // ================= -// Global stuff +// Singletons // ================= var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { diff --git a/lib/evloops.js b/lib/evloops.js deleted file mode 100644 index 2776a03..0000000 --- a/lib/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/lib/header.js b/lib/header.js index 403071b..eb8a252 100644 --- a/lib/header.js +++ b/lib/header.js @@ -1,3 +1,7 @@ +// ----------------------------------------- +// Header for single file build +// ----------------------------------------- + var sys = require('sys'), http = require('http'), fs = require('fs'), diff --git a/lib/http.js b/lib/http.js index 9c6ad07..2d8b50c 100644 --- a/lib/http.js +++ b/lib/http.js @@ -2,11 +2,10 @@ // HTTP Server // ------------------------------------ // -// This file defines HTTP_SERVER. +// This file defines the singleton HTTP_SERVER. // // This file defines and starts the nodeload HTTP server. // - var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var config = require('./config'); @@ -14,12 +13,7 @@ var http = require('http'); var fs = require('fs'); var util = require('./util'); var qputs = util.qputs; - var NODELOAD_CONFIG = config.NODELOAD_CONFIG; - -exports.disableServer = function() { config.disableServer(); return exports; }; -exports.usePort = function() { config.usePort(); return exports; }; -exports.quiet = function() { config.quiet(); return exports; }; } /** By default, HttpServer knows how to return static files from the current directory. Add new route @@ -27,22 +21,26 @@ regexs using HttpServer.on(). */ var HttpServer = exports.HttpServer = function HttpServer() { this.routes = []; }; -HttpServer.prototype.start = function(port) { +/** Start the server listening on the given port */ +HttpServer.prototype.start = function(port, hostname) { if (this.server) { return; } var self = this; port = port || 8000; self.server = http.createServer(function(req, res) { self.route_(req, res); }); - self.server.listen(port); + self.server.listen(port, hostname); qputs('Started HTTP server on port ' + port + '.'); return self; }; +/** Terminate the server */ HttpServer.prototype.stop = function() { if (!this.server) { return; } this.server.close(); this.server = null; qputs('Shutdown HTTP server.'); }; +/** When an incoming request matches a given regex, route it to the provided handler: +function(url, ServerRequest, ServerResponse) */ HttpServer.prototype.on = function(regex, handler) { this.routes.push({regex: regex, handler: handler}); return this; @@ -84,7 +82,7 @@ HttpServer.prototype.serveFile_ = function(file, response) { }; // ================= -// Global stuff +// Singletons // ================= /** The global HTTP server used by nodeload */ diff --git a/lib/log.js b/lib/log.js deleted file mode 100644 index ad2ac23..0000000 --- a/lib/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/lib/loop.js b/lib/loop.js index 25c35f6..44865df 100644 --- a/lib/loop.js +++ b/lib/loop.js @@ -1,18 +1,16 @@ -/*jslint laxbreak: true, undef: true */ -/*global setTimeout: false */ - // ----------------------------------------- // Event-based looping // ----------------------------------------- // +// This file defines Loop (allows looping on an arbitrary function), and Scheduler (runs multiple +// Loop instances as a group). +// // 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 Loop 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. -// - +/*jslint laxbreak: true, undef: true */ +/*global setTimeout: false */ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var util = require('./util'); @@ -41,7 +39,8 @@ var LOOP_DEFAULTS = { /** 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. +handled as the loop executes. Loop emits the events 'start' (before the first iteration), 'end', +'startiteration' and 'enditeration'. @param fun an asynchronous function that calls loopFun(result) when it finishes: @@ -50,7 +49,7 @@ handled as the loop executes. loopFun(result); } - use LoopUtils.funLoop() to wrap a simple, non-asynchronous functions. + 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 @@ -92,10 +91,9 @@ Loop.create = function(spec) { util.inherits(Loop, EventEmitter); -/** Start executing "Loop.fun" with the arguments, "Loop.args", until any -condition in "Loop.conditions" returns false. The loop begins after a delay of -"Loop.delay" seconds. When the loop completes, the user defined function, callback is -called. */ +/** Start executing this.fun with the arguments, this.args, until any +condition in this.conditions returns false. The loop begins after a delay of +this.delay seconds. When the loop completes the 'end' event is emitted. */ Loop.prototype.start = function() { var self = this, startLoop = function() { @@ -124,7 +122,8 @@ Loop.prototype.checkConditions_ = function() { return this.running && this.conditions.every(function(c) { return c(); }); }; -/** Checks conditions and schedules the next loop iteration */ +/** 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, callback = function(result) { @@ -162,7 +161,7 @@ Loop.maxExecutions = function(numberOfTimes) { }; -// Helpers for dealing with Loop loop functions +// 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; } @@ -234,8 +233,9 @@ var Scheduler = exports.Scheduler = function Scheduler() { util.inherits(Scheduler, EventEmitter); -/** Primary function for adding new Loops. Start all scheduled loops by calling startAll(). If the -scheduler is already started, the loops are started immediately upon scheduling. */ +/** Primary function for adding a new Loop given a spec object. LOOP_DEFAULTS lists the supported +parameters. Start all scheduled loops by calling startAll(). If the scheduler is already started, the +loops are started immediately upon scheduling. */ Scheduler.prototype.schedule = function(spec) { util.defaults(spec, LOOP_DEFAULTS); @@ -274,6 +274,7 @@ Scheduler.prototype.stopAll = function() { this.loops.forEach(function(l) { l.stop(); }); return this; }; +/** Starts a single loop, installing a Loop 'end' event listener to check for completion. */ Scheduler.prototype.startLoop_ = function(loop) { var self = this; loop.start(); diff --git a/lib/monitoring.js b/lib/monitoring.js index ac28baa..a3528c0 100644 --- a/lib/monitoring.js +++ b/lib/monitoring.js @@ -2,10 +2,8 @@ // Monitoring // ------------------------------------ // -// This file defines TEST_MONITOR. +// This file defines Monitor and MonitorSet, and StatsLogger // -// 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. // var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { @@ -26,7 +24,27 @@ var UpdateEventGenerator, StatsLogger; // ----------------- // Monitor // ----------------- -var Monitor = exports.Monitor = function Monitor() { // arguments contains stats names +/** 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. + +@param arguments contain names of the statistics to track. Register more statistics by extending + Monitor.Monitors. +*/ +var Monitor = exports.Monitor = function Monitor() { // arguments EventEmitter.call(this); this.targets = []; this.setStats.apply(this, arguments); @@ -35,6 +53,7 @@ var Monitor = exports.Monitor = function Monitor() { // arguments contains stats util.inherits(Monitor, EventEmitter); +/** Set the statistics this monitor should gather. */ Monitor.prototype.setStats = function(stats) { // arguments contains stats names var self = this; self.monitors = []; @@ -52,15 +71,19 @@ Monitor.prototype.setStats = function(stats) { // arguments contains stats names }); }; +/** Monitor should gather statistics for each intervalMs period, and generate 'update' events */ Monitor.prototype.updateEvery = function(intervalMs) { this.updater.updateEvery(intervalMs); return this; }; +/** Stop generating 'update' events */ Monitor.prototype.disableUpdates = function() { return this.updateEvery(0); }; +/** Called by the instrumented code when it begins executing. Call .end() on the returned object +when complete. */ Monitor.prototype.start = function(args) { var self = this, endFuns = [], @@ -78,6 +101,9 @@ Monitor.prototype.start = function(args) { }; }; +/** 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. */ Monitor.prototype.monitor = function(objs) { var self = this; objs = (objs instanceof Array) ? objs : [].concat.apply([], arguments); @@ -93,6 +119,7 @@ Monitor.prototype.monitor = function(objs) { return self; }; +/** Emit the 'update' event and reset the statistics for the next window */ Monitor.prototype.doUpdate = function() { this.emit('update', this.interval, this.stats); util.forEach(this.interval, function(name, stats) { @@ -102,6 +129,7 @@ Monitor.prototype.doUpdate = function() { }); }; +/** Track the runtime of an operation, storing stats in a stats.js#Histogram */ function RuntimeMonitor() { var self = this; self.stats = new Histogram(); @@ -109,12 +137,16 @@ function RuntimeMonitor() { 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}). */ function ResultCodeMonitor() { 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. */ function ConcurrencyMonitor() { var self = this, c = 0; self.stats = new Peak(); @@ -122,6 +154,9 @@ function ConcurrencyMonitor() { 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). */ function RequestBytesMonitor() { var self = this; self.stats = new Accumulator(); @@ -132,6 +167,7 @@ function RequestBytesMonitor() { }; } +/** Track the size of HTTP response bodies. It doesn't account for headers! */ function ResponseBytesMonitor() { var self = this; self.stats = new Accumulator(); @@ -144,6 +180,8 @@ function ResponseBytesMonitor() { }; } +/** Track unique URLs requested, storing stats in a stats.js#Uniques object. The client must call +Monitor.start({req: http.ClientRequest}). */ function UniquesMonitor() { var self = this; self.stats = new Uniques(); @@ -152,6 +190,11 @@ function UniquesMonitor() { }; } +/** Define new statistics that Monitor can track by adding to Monitor.Monitors. Each entry should be a +class with: +- 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 */ Monitor.Monitors = { 'runtime': RuntimeMonitor, 'latency': RuntimeMonitor, @@ -166,7 +209,13 @@ Monitor.Monitors = { // ----------------- // MonitorSet // ----------------- -var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // arguments contains stats names +/** MonitorSet represents a group of Monitor instances. Calling MonitorSet('runtime').start('myfunction') +is equivalent to creating a Monitor('runtime') for myfunction and and calling start(). MonitorSet 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.Monitors. */ +var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { EventEmitter.call(this); var summaryFun = function() { @@ -189,6 +238,8 @@ var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // argum util.inherits(MonitorSet, EventEmitter); +/** Pre-initialize monitors with the given names. This allows construction overhead to take place all at +once if desired. */ MonitorSet.prototype.init = function(monitorNames) { // arguments contains monitor names var self = this; monitorNames = (monitorNames instanceof Array) ? monitorNames : [].concat.apply([], arguments); @@ -200,9 +251,10 @@ MonitorSet.prototype.init = function(monitorNames) { // arguments contains monit return self; }; +/** All the Monitors in this set should gather statistics for each intervalMs period. MonitorSet should +generate 'update' events */ MonitorSet.prototype.updateEvery = function(intervalMs) { this.updater.updateEvery(intervalMs); - util.forEach(this.monitors, function (name, m) { m.updateEvery(intervalMs); }); return this; }; @@ -210,6 +262,7 @@ MonitorSet.prototype.disableUpdates = function() { return this.updateEvery(0); }; +/** Call .start() for the named monitor */ MonitorSet.prototype.start = function(monitor, args) { monitor = monitor || ''; if (!this.monitors[monitor]) { @@ -218,6 +271,8 @@ MonitorSet.prototype.start = function(monitor, args) { return this.monitors[monitor].start(args); }; +/** Like Monitor.monitor() except each object's 'start' event should include the monitor name as its +first argument. See monitoring.test.js for an example. */ MonitorSet.prototype.monitor = function(objs) { var self = this, mons = {}; objs = (objs instanceof Array) ? objs : [].concat.apply([], arguments); @@ -232,6 +287,7 @@ MonitorSet.prototype.monitor = function(objs) { return self; }; +/** Log statistics to the given file or stats.js#LogFile object each time an 'update' event is emitted */ MonitorSet.prototype.startLogger = function(logNameOrObject) { if (this.logger) { return; } this.logger = new StatsLogger(this, logNameOrObject).start(); @@ -245,11 +301,16 @@ MonitorSet.prototype.stopLogger = function() { return this; }; +/** Emit the update event and reset the statistics for the next window */ MonitorSet.prototype.doUpdate = function() { this.emit('update', this.interval, this.stats); + util.forEach(this.monitors, function (name, m) { m.doUpdate(); }); }; +// ----------------- +// StatsLogger +// ----------------- /** StatsLogger writes interval stats from a Monitor or MonitorSet to disk each time it emits 'update' */ var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '-stats.log'); @@ -274,8 +335,8 @@ StatsLogger.prototype.log_ = function(interval) { // ================= // Private methods // ================= -function UpdateEventGenerator(monitor) { - this.monitor = monitor; +function UpdateEventGenerator(parent) { + this.parent = parent; } UpdateEventGenerator.prototype.updateEvery = function(intervalMs) { clearTimeout(this.updateTimeoutId); @@ -287,7 +348,7 @@ UpdateEventGenerator.prototype.scheduleUpdate_ = function(intervalMs) { self.updateTimeoutId = setTimeout( function() { self.scheduleUpdate_(intervalMs); - self.monitor.doUpdate(); + self.parent.doUpdate(); }, intervalMs); } diff --git a/lib/reporting.js b/lib/reporting.js index 067edb5..1c0f5a3 100644 --- a/lib/reporting.js +++ b/lib/reporting.js @@ -4,33 +4,30 @@ // // 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 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 START = new Date(); var util = require('./util'); var querystring = require('querystring'); var LogFile = require('./stats').LogFile; var template = require('./template'); var config = require('./config'); +var START = new Date(); var REPORT_SUMMARY_TEMPLATE = require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE; var NODELOAD_CONFIG = config.NODELOAD_CONFIG; var DYGRAPH_SOURCE = require('./dygraph.tpl.js').DYGRAPH_SOURCE; var HTTP_SERVER = require('./http').HTTP_SERVER; - -exports.setAjaxRefreshIntervalMs = function(ms) { config.setAjaxRefreshIntervalMs(ms); return exports; }; -exports.disableLogs = function() { config.disableLogs(); return exports; }; -exports.disableServer = function() { config.disableServer(); return exports; }; -exports.usePort = function() { config.usePort(); return exports; }; -exports.quiet = function() { config.quiet(); return exports; }; } var Chart, timeFromStart; -/** A Report contains a summary object and set of charts. +/** 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#MonitorSet using updateFromMonitor()/updateFromMonitorSet(). + @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) { @@ -160,7 +157,7 @@ ReportSet.prototype = { }; // ================= -// Global stuff +// Singletons // ================= /** A global report manager used by nodeload to keep the summary webpage up to date during a load test */ diff --git a/lib/stats.js b/lib/stats.js index 3350277..011ca66 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -1,5 +1,3 @@ -/*jslint forin:true */ - // ------------------------------------ // Statistics // ------------------------------------ @@ -7,6 +5,7 @@ // 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; if (BUILD_AS_SINGLE_FILE === undefined) { var fs = require('fs'); @@ -15,7 +14,6 @@ var fs = require('fs'); var Histogram = exports.Histogram = function(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 - this.type = "Histogram"; this.params = params; this.size = params && params.buckets || 3000; this.percentiles = params && params.percentiles || [0.95, 0.99]; @@ -137,7 +135,6 @@ Histogram.prototype = { }; var Accumulator = exports.Accumulator = function() { - this.type = "Accumulator"; this.total = 0; this.length = 0; }; @@ -163,7 +160,6 @@ Accumulator.prototype = { }; var ResultsCounter = exports.ResultsCounter = function() { - this.type = "ResultsCounter"; this.start = new Date(); this.items = {}; this.length = 0; @@ -215,7 +211,6 @@ ResultsCounter.prototype = { }; var Uniques = exports.Uniques = function() { - this.type = "Uniques"; this.start = new Date(); this.items = {}; this.uniques = 0; @@ -256,7 +251,6 @@ Uniques.prototype = { }; var Peak = exports.Peak = function() { - this.type = "Peak"; this.peak = 0; this.length = 0; }; @@ -285,7 +279,6 @@ Peak.prototype = { }; var Rate = exports.Rate = function() { - this.type = "Rate"; this.start = new Date(); this.length = 0; }; @@ -309,7 +302,6 @@ Rate.prototype = { }; var LogFile = exports.LogFile = function(filename) { - this.type = "LogFile"; this.writepos = null; this.length = 0; this.filename = filename; @@ -348,7 +340,6 @@ LogFile.prototype = { }; var NullLog = exports.NullLog = function() { - this.type = "NullLog"; this.length = 0; }; NullLog.prototype = { @@ -360,20 +351,11 @@ NullLog.prototype = { 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 = exports.Reportable = function(name, Backend, backendparams) { + this.name = name || ''; this.length = 0; this.interval = new Backend(backendparams); this.cumulative = new Backend(backendparams); - this.trend = trend; this.lastSummary = null; }; Reportable.prototype = { @@ -444,23 +426,4 @@ var nextPareto = exports.nextPareto = function(min, max, shape) { 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; -}; - -// ================= -// Private methods -// ================= - -function statsClassFromString(name) { - var 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 +}; \ No newline at end of file diff --git a/lib/util.js b/lib/util.js index 4cfbf91..105b6af 100644 --- a/lib/util.js +++ b/lib/util.js @@ -5,7 +5,7 @@ // This file defines qputs, qprint, and Utils. // // Extends node.js util.js with other common functions. - +// var util = require('util'); var BUILD_AS_SINGLE_FILE; @@ -27,8 +27,8 @@ var qprint = util.qprint = function(s) { // Static utility methods // util.uid = function() { - this.lastUid_ = this.lastUid_ || 0; - return this.lastUid_++; + exports.lastUid_ = exports.lastUid_ || 0; + return exports.lastUid_++; }; util.defaults = function(obj, defaults) { for (var i in defaults) { diff --git a/test/http.test.js b/test/http.test.js index cf17e56..3b6e8fc 100644 --- a/test/http.test.js +++ b/test/http.test.js @@ -1,5 +1,6 @@ var http = require('http'), - nlhttp = require('../lib/http').quiet(), + nlconfig = require('../lib/config').quiet(), + nlhttp = require('../lib/http'), HTTP_SERVER = nlhttp.HTTP_SERVER; HTTP_SERVER.start(); diff --git a/test/reporting.test.js b/test/reporting.test.js index 4963069..82ffdf0 100644 --- a/test/reporting.test.js +++ b/test/reporting.test.js @@ -1,6 +1,7 @@ /*jslint sub:true */ -var reporting = require('../lib/reporting').disableServer(), +var nlconfig = require('../lib/config').disableServer(), + reporting = require('../lib/reporting'), monitoring = require('../lib/monitoring'), REPORT_MANAGER = reporting.REPORT_MANAGER; From 7e32ba2b1a71189a5fcbebca7b7ef4405094bf09 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 15 Nov 2010 15:11:51 -0500 Subject: [PATCH 06/41] Code cleanup --- lib/config.js | 11 +- lib/evloops.js | 276 ---------------------------------------- lib/header.js | 4 + lib/http.js | 20 ++- lib/log.js | 49 ------- lib/loop.js | 82 ++++++------ lib/monitoring.js | 272 +++++++++++++++++++++++++-------------- lib/reporting.js | 66 +++++----- lib/stats.js | 45 +------ lib/testapi.js | 235 ---------------------------------- lib/util.js | 6 +- test/http.test.js | 5 +- test/loop.test.js | 18 +-- test/monitoring.test.js | 41 +++--- test/reporting.test.js | 19 +-- 15 files changed, 318 insertions(+), 831 deletions(-) delete mode 100644 lib/evloops.js delete mode 100644 lib/log.js delete mode 100644 lib/testapi.js diff --git a/lib/config.js b/lib/config.js index 3bc57fd..976a6ba 100644 --- a/lib/config.js +++ b/lib/config.js @@ -2,12 +2,17 @@ // 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); // nl.runTest(...); // +// Or, if 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; @@ -55,7 +60,7 @@ exports.setSlavePingIntervalMs = function(milliseconds) { }; // ================= -// Global stuff +// Singletons // ================= var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { diff --git a/lib/evloops.js b/lib/evloops.js deleted file mode 100644 index 2776a03..0000000 --- a/lib/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/lib/header.js b/lib/header.js index 403071b..eb8a252 100644 --- a/lib/header.js +++ b/lib/header.js @@ -1,3 +1,7 @@ +// ----------------------------------------- +// Header for single file build +// ----------------------------------------- + var sys = require('sys'), http = require('http'), fs = require('fs'), diff --git a/lib/http.js b/lib/http.js index 9c6ad07..bbbf879 100644 --- a/lib/http.js +++ b/lib/http.js @@ -2,11 +2,10 @@ // HTTP Server // ------------------------------------ // -// This file defines HTTP_SERVER. +// This file defines the singleton HTTP_SERVER. // // This file defines and starts the nodeload HTTP server. // - var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var config = require('./config'); @@ -14,12 +13,7 @@ var http = require('http'); var fs = require('fs'); var util = require('./util'); var qputs = util.qputs; - var NODELOAD_CONFIG = config.NODELOAD_CONFIG; - -exports.disableServer = function() { config.disableServer(); return exports; }; -exports.usePort = function() { config.usePort(); return exports; }; -exports.quiet = function() { config.quiet(); return exports; }; } /** By default, HttpServer knows how to return static files from the current directory. Add new route @@ -27,23 +21,27 @@ regexs using HttpServer.on(). */ var HttpServer = exports.HttpServer = function HttpServer() { this.routes = []; }; -HttpServer.prototype.start = function(port) { +/** Start the server listening on the given port */ +HttpServer.prototype.start = function(port, hostname) { if (this.server) { return; } var self = this; port = port || 8000; self.server = http.createServer(function(req, res) { self.route_(req, res); }); - self.server.listen(port); + self.server.listen(port, hostname); qputs('Started HTTP server on port ' + port + '.'); return self; }; +/** Terminate the server */ HttpServer.prototype.stop = function() { if (!this.server) { return; } this.server.close(); this.server = null; qputs('Shutdown HTTP server.'); }; -HttpServer.prototype.on = function(regex, handler) { +/** 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.push({regex: regex, handler: handler}); return this; }; @@ -84,7 +82,7 @@ HttpServer.prototype.serveFile_ = function(file, response) { }; // ================= -// Global stuff +// Singletons // ================= /** The global HTTP server used by nodeload */ diff --git a/lib/log.js b/lib/log.js deleted file mode 100644 index ad2ac23..0000000 --- a/lib/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/lib/loop.js b/lib/loop.js index 25c35f6..690956b 100644 --- a/lib/loop.js +++ b/lib/loop.js @@ -1,18 +1,15 @@ -/*jslint laxbreak: true, undef: true */ -/*global setTimeout: false */ - // ----------------------------------------- // Event-based looping // ----------------------------------------- // -// 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 Loop 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. +// This file defines Loop and Scheduler. +// +// 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'); @@ -21,10 +18,10 @@ var EventEmitter = require('events').EventEmitter; /** LOOP_DEFAULTS defines all of the parameters that used with Loop.create() and Scheduler.schedule() */ var LOOP_DEFAULTS = { - fun: null, // A function to execute which accepts the parameters (loopFun, args). + fun: null, // 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 null. The function must call - // loopFun(results) when it completes. + // finished(results) when it completes. argGenerator: null, // 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. @@ -39,18 +36,19 @@ var LOOP_DEFAULTS = { // Scheduler.startAll() to complete }; -/** 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 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 fun an asynchronous function that calls loopFun(result) when it finishes: +@param fun an asynchronous function that calls finished(result) when it finishes: - function(loopFun, args) { + function(finished, args) { ... - loopFun(result); + finished(result); } - use LoopUtils.funLoop() to wrap a simple, non-asynchronous functions. + 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 @@ -92,10 +90,9 @@ Loop.create = function(spec) { util.inherits(Loop, EventEmitter); -/** Start executing "Loop.fun" with the arguments, "Loop.args", until any -condition in "Loop.conditions" returns false. The loop begins after a delay of -"Loop.delay" seconds. When the loop completes, the user defined function, callback is -called. */ +/** Start executing this.fun with the arguments, this.args, until any +condition in this.conditions returns false. The loop begins after a delay of +this.delay seconds. When the loop completes the 'end' event is emitted. */ Loop.prototype.start = function() { var self = this, startLoop = function() { @@ -124,7 +121,8 @@ Loop.prototype.checkConditions_ = function() { return this.running && this.conditions.every(function(c) { return c(); }); }; -/** Checks conditions and schedules the next loop iteration */ +/** 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, callback = function(result) { @@ -162,56 +160,56 @@ Loop.maxExecutions = function(numberOfTimes) { }; -// Helpers for dealing with Loop loop functions +// 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(loopFun, args) { - loopFun(fun(args)); + 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(loopFun, args) { + 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 loopFun(result)?'); + util.qputs('Function result is null; did you forget to call finished(result)?'); } if (finish) { finish(result, startRes); } - loopFun(result); + finished(result); }; fun(finishFun, args); }; }; /** Wrapper for executing a Loop function rps times per second. */ Loop.rpsLoop = function(rps, fun) { - var finished, lagging, + var running, lagging, timeout = (rps && rps > 0) ? (1/rps * 1000) : 0, - finishFun = function(loopFun) { - finished = true; + finishFun = function(finished) { + running = false; if (lagging) { - loopFun(); + finished(); } }; - return function(loopFun, args) { - finished = false; + return function(finished, args) { + running = true; lagging = (timeout <= 0); if (!lagging) { setTimeout(function() { - lagging = !finished; + lagging = running; if (!lagging) { - loopFun(); + finished(); } }, timeout); } - var callback = function() { finishFun(loopFun); }; + var callback = function() { finishFun(finished); }; fun(callback, args); }; }; @@ -234,8 +232,9 @@ var Scheduler = exports.Scheduler = function Scheduler() { util.inherits(Scheduler, EventEmitter); -/** Primary function for adding new Loops. Start all scheduled loops by calling startAll(). If the -scheduler is already started, the loops are started immediately upon scheduling. */ +/** Primary function for adding a new Loop given a spec object. LOOP_DEFAULTS lists the supported +parameters. Start all scheduled loops by calling startAll(). If the scheduler is already started, the +loops are started immediately upon scheduling. */ Scheduler.prototype.schedule = function(spec) { util.defaults(spec, LOOP_DEFAULTS); @@ -274,6 +273,7 @@ Scheduler.prototype.stopAll = function() { this.loops.forEach(function(l) { l.stop(); }); return this; }; +/** Starts a single loop, installing a Loop 'end' event listener to check for completion. */ Scheduler.prototype.startLoop_ = function(loop) { var self = this; loop.start(); diff --git a/lib/monitoring.js b/lib/monitoring.js index ac28baa..1bbafdd 100644 --- a/lib/monitoring.js +++ b/lib/monitoring.js @@ -2,10 +2,8 @@ // Monitoring // ------------------------------------ // -// This file defines TEST_MONITOR. +// This file defines Monitor and MonitorGroup, and StatsLogger // -// 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. // var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { @@ -26,7 +24,27 @@ var UpdateEventGenerator, StatsLogger; // ----------------- // Monitor // ----------------- -var Monitor = exports.Monitor = function Monitor() { // arguments contains stats names +/** 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. + +@param arguments contain names of the statistics to track. Register more statistics by extending + Monitor.StatsCollectors. +*/ +var Monitor = exports.Monitor = function Monitor() { // arguments EventEmitter.call(this); this.targets = []; this.setStats.apply(this, arguments); @@ -35,32 +53,32 @@ var Monitor = exports.Monitor = function Monitor() { // arguments contains stats util.inherits(Monitor, EventEmitter); -Monitor.prototype.setStats = function(stats) { // arguments contains stats names +/** Set the statistics this monitor should gather. */ +Monitor.prototype.setStats = function(statsNames) { // arguments contains stats names var self = this; - self.monitors = []; + self.collectors = []; self.stats = {}; self.interval = {}; - stats = (stats instanceof Array) ? stats : [].concat.apply([], arguments); - stats.forEach(function(name) { - if (!Monitor.Monitors[name]) { throw new Error('No monitor for statistic: ' + name); } - var intervalmon = new Monitor.Monitors[name](); - var overallmon = new Monitor.Monitors[name](); - self.monitors.push(intervalmon); - self.monitors.push(overallmon); - self.interval[name] = intervalmon.stats; - self.stats[name] = overallmon.stats; + statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); + statsNames.forEach(function(name) { + if (!Monitor.StatsCollectors[name]) { throw new Error('No collector for statistic: ' + name); } + var intervalCollector = new Monitor.StatsCollectors[name](); + var overallCollector = new Monitor.StatsCollectors[name](); + self.collectors.push(intervalCollector); + self.collectors.push(overallCollector); + self.interval[name] = intervalCollector.stats; + self.stats[name] = overallCollector.stats; }); }; -Monitor.prototype.updateEvery = function(intervalMs) { - this.updater.updateEvery(intervalMs); +/** Monitor should gather statistics for each intervalMs period, and generate 'update' events */ +Monitor.prototype.setUpdateIntervalMs = function(milliseconds) { + this.updater.setUpdateIntervalMs(milliseconds); return this; }; -Monitor.prototype.disableUpdates = function() { - return this.updateEvery(0); -}; - +/** 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 = [], @@ -69,31 +87,48 @@ Monitor.prototype.start = function(args) { if (m.end) { endFuns.push(function(result) { return m.end(context, result); }); } + }, + monitoringContext = { + end: function(result) { + endFuns.forEach(function(f) { f(result); }); + } }; - self.monitors.forEach(function(m) { doStart(m, {}); }); - return { - end: function(result) { - endFuns.forEach(function(f) { f(result); }); - } - }; + + self.collectors.forEach(function(m) { doStart(m, {}); }); + return monitoringContext; }; -Monitor.prototype.monitor = function(objs) { +/** 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; - objs = (objs instanceof Array) ? objs : [].concat.apply([], arguments); + + 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('start', function(args) { + o.on(startEvent, function(args) { mon = self.start(args); }); - o.on('end', function(result) { + o.on(endEvent, function(result) { mon.end(result); }); }); + return self; }; -Monitor.prototype.doUpdate = function() { +/** 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) { @@ -102,27 +137,35 @@ Monitor.prototype.doUpdate = function() { }); }; -function RuntimeMonitor() { +/** Track the runtime of an operation, storing stats in a stats.js#Histogram */ +function RuntimeCollector() { var self = this; self.stats = new Histogram(); self.start = function(context) { context.start = new Date(); }; self.end = function(context) { self.stats.put(new Date() - context.start); }; } -function ResultCodeMonitor() { +/** Track HTTP response codes, storing stats in a stats.js#ResultsCounter object. The client must call +.end({res: http.ClientResponse}). */ +function ResultCodesCollector() { var self = this; self.stats = new ResultsCounter(); self.end = function(context, http) { self.stats.put(http.res.statusCode); }; } -function ConcurrencyMonitor() { +/** Track the concurrent executions (ie. stuff between calls to .start() and .end()), storing in a +stats.js#Peak. */ +function ConcurrencyCollector() { var self = this, c = 0; self.stats = new Peak(); self.start = function() { c++; }; self.end = function() { self.stats.put(c--); }; } -function RequestBytesMonitor() { +/** 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). */ +function RequestBytesCollector() { var self = this; self.stats = new Accumulator(); self.end = function(context, http) { @@ -132,7 +175,8 @@ function RequestBytesMonitor() { }; } -function ResponseBytesMonitor() { +/** Track the size of HTTP response bodies. It doesn't account for headers! */ +function ResponseBytesCollector() { var self = this; self.stats = new Accumulator(); self.end = function(context, http) { @@ -144,7 +188,9 @@ function ResponseBytesMonitor() { }; } -function UniquesMonitor() { +/** Track unique URLs requested, storing stats in a stats.js#Uniques object. The client must call +Monitor.start({req: http.ClientRequest}). */ +function UniquesCollector() { var self = this; self.stats = new Uniques(); self.end = function(context, http) { @@ -152,34 +198,46 @@ function UniquesMonitor() { }; } -Monitor.Monitors = { - 'runtime': RuntimeMonitor, - 'latency': RuntimeMonitor, - 'result-codes': ResultCodeMonitor, - 'concurrency': ConcurrencyMonitor, - 'request-bytes': RequestBytesMonitor, - 'response-bytes': ResponseBytesMonitor, - 'uniques': UniquesMonitor, +/** Define new statistics that Monitor can track by adding to Monitor.StatsCollectors. Each entry should +be a class with: +- 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 */ +Monitor.StatsCollectors = { + 'runtime': RuntimeCollector, + 'latency': RuntimeCollector, + 'result-codes': ResultCodesCollector, + 'concurrency': ConcurrencyCollector, + 'request-bytes': RequestBytesCollector, + 'response-bytes': ResponseBytesCollector, + 'uniques': UniquesCollector, }; // ----------------- -// MonitorSet +// MonitorGroup // ----------------- -var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // arguments contains stats names +/** 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); var summaryFun = function() { var summary = {}; - util.forEach(this, function(monitor, stats) { - if (monitor === 'summary') { return; } - summary[monitor] = {}; - util.forEach(stats, function(name, stat) { - summary[monitor][name] = stat.summary(); + util.forEach(this, function(monitorName, stats) { + if (monitorName === 'summary') { return; } + summary[monitorName] = {}; + util.forEach(stats, function(statName, stat) { + summary[monitorName][statName] = stat.summary(); }); }); return summary; }; + this.statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); this.monitors = {}; this.updater = new UpdateEventGenerator(this); @@ -187,9 +245,11 @@ var MonitorSet = exports.MonitorSet = function MonitorSet(statsNames) { // argum this.interval = { summary: summaryFun }; }; -util.inherits(MonitorSet, EventEmitter); +util.inherits(MonitorGroup, EventEmitter); -MonitorSet.prototype.init = function(monitorNames) { // arguments contains monitor names +/** 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 : [].concat.apply([], arguments); monitorNames.forEach(function(name) { @@ -200,70 +260,91 @@ MonitorSet.prototype.init = function(monitorNames) { // arguments contains monit return self; }; -MonitorSet.prototype.updateEvery = function(intervalMs) { - this.updater.updateEvery(intervalMs); - util.forEach(this.monitors, function (name, m) { m.updateEvery(intervalMs); }); +/** All the Monitors in this set should gather statistics for each intervalMs period. MonitorGroup should +generate 'update' events */ +MonitorGroup.prototype.setUpdateIntervalMs = function(interval) { + this.updater.setUpdateIntervalMs(interval); return this; }; -MonitorSet.prototype.disableUpdates = function() { - return this.updateEvery(0); +/** 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); }; -MonitorSet.prototype.start = function(monitor, args) { - monitor = monitor || ''; - if (!this.monitors[monitor]) { - this.init([monitor]); +/** 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; } - return this.monitors[monitor].start(args); -}; -MonitorSet.prototype.monitor = function(objs) { - var self = this, mons = {}; - objs = (objs instanceof Array) ? objs : [].concat.apply([], arguments); + startEvent = startEvent || 'start'; + endEvent = endEvent || 'end'; + objs.forEach(function(o) { - o.on('start', function(monitor, args) { - mons[monitor] = self.start(monitor, args); + o.on(startEvent, function(monitorName, args) { + ctxs[monitorName] = self.start(monitorName, args); }); - o.on('end', function(monitor, result) { - if (mons[monitor]) { mons[monitor].end(result); } + o.on(endEvent, function(monitorName, result) { + if (ctxs[monitorName]) { ctxs[monitorName].end(result); } }); }); return self; }; -MonitorSet.prototype.startLogger = function(logNameOrObject) { - if (this.logger) { return; } - this.logger = new StatsLogger(this, logNameOrObject).start(); - return this; +/** 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; }; -MonitorSet.prototype.stopLogger = function() { - if (!this.logger) { return; } - this.logger.stop(); - this.logger = null; +/** 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; }; -MonitorSet.prototype.doUpdate = function() { +/** 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(); }); }; -/** StatsLogger writes interval stats from a Monitor or MonitorSet to disk each time it emits 'update' */ +// ----------------- +// StatsLogger +// ----------------- +/** StatsLogger writes interval stats from a Monitor or MonitorGroup to disk each time it emits 'update' */ var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrObject) { - logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '-stats.log'); - this.log = (typeof logNameOrObject === 'string') ? new LogFile(logNameOrObject) : logNameOrObject; + this.logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '-stats.log'); this.monitor = monitor; this.logger_ = this.log_.bind(this); }; StatsLogger.prototype.start = function() { - this.monitor.on('update', this.logger_); + this.createdLog = (typeof this.logNameOrObject === 'string'); + this.log = this.createdLog ? new LogFile(this.logNameOrObject) : this.logNameOrObject; this.log.put('['); + this.monitor.on('update', this.logger_); return this; }; StatsLogger.prototype.stop = function() { this.log.put(']'); + if (this.createdLog) { + this.log.close(); + this.log = null; + } this.monitor.removeListener('update', this.logger_); return this; }; @@ -274,21 +355,22 @@ StatsLogger.prototype.log_ = function(interval) { // ================= // Private methods // ================= -function UpdateEventGenerator(monitor) { - this.monitor = monitor; +function UpdateEventGenerator(parent, updateIntervalMs) { + this.parent = parent; + this.setUpdateIntervalMs(updateIntervalMs); } -UpdateEventGenerator.prototype.updateEvery = function(intervalMs) { +UpdateEventGenerator.prototype.setUpdateIntervalMs = function(milliseconds) { clearTimeout(this.updateTimeoutId); - this.scheduleUpdate_(intervalMs); + this.scheduleUpdate_(milliseconds); }; -UpdateEventGenerator.prototype.scheduleUpdate_ = function(intervalMs) { +UpdateEventGenerator.prototype.scheduleUpdate_ = function(milliseconds) { var self = this; - if (intervalMs > 0) { + if (milliseconds > 0) { self.updateTimeoutId = setTimeout( function() { - self.scheduleUpdate_(intervalMs); - self.monitor.doUpdate(); + self.scheduleUpdate_(milliseconds); + self.parent.update(); }, - intervalMs); + milliseconds); } }; \ No newline at end of file diff --git a/lib/reporting.js b/lib/reporting.js index 067edb5..83e2e76 100644 --- a/lib/reporting.js +++ b/lib/reporting.js @@ -4,33 +4,30 @@ // // 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 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 START = new Date(); var util = require('./util'); var querystring = require('querystring'); var LogFile = require('./stats').LogFile; var template = require('./template'); var config = require('./config'); +var START = new Date(); var REPORT_SUMMARY_TEMPLATE = require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE; var NODELOAD_CONFIG = config.NODELOAD_CONFIG; var DYGRAPH_SOURCE = require('./dygraph.tpl.js').DYGRAPH_SOURCE; var HTTP_SERVER = require('./http').HTTP_SERVER; - -exports.setAjaxRefreshIntervalMs = function(ms) { config.setAjaxRefreshIntervalMs(ms); return exports; }; -exports.disableLogs = function() { config.disableLogs(); return exports; }; -exports.disableServer = function() { config.disableServer(); return exports; }; -exports.usePort = function() { config.usePort(); return exports; }; -exports.quiet = function() { config.quiet(); return exports; }; } var Chart, timeFromStart; -/** A Report contains a summary object and set of charts. +/** 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) { @@ -51,11 +48,11 @@ Report.prototype = { monitor.on('update', this.doUpdateFromMonitor_.bind(this, monitor, '')); return this; }, - /** Update this report automatically each time the MonitorSet emits an 'update' event */ - updateFromMonitorSet: function(monitorset) { + /** Update this report automatically each time the MonitorGroup emits an 'update' event */ + updateFromMonitorGroup: function(monitorGroup) { var self = this; - monitorset.on('update', function() { - util.forEach(monitorset.monitors, function(monitorname, monitor) { + monitorGroup.on('update', function() { + util.forEach(monitorGroup.monitors, function(monitorname, monitor) { self.doUpdateFromMonitor_(monitor, monitorname); }); }); @@ -116,28 +113,29 @@ Chart.prototype = { } }; -var ReportSet = exports.ReportSet = function() { +var ReportGroup = exports.ReportGroup = function() { this.reports = []; this.refreshIntervalMs = 2000; + this.logNameOrObject = 'results-' + START.getTime() + '.html'; }; -ReportSet.prototype = { +ReportGroup.prototype = { addReport: function(report) { report = (typeof report === 'string') ? new Report(report) : report; this.reports.push(report); return report; }, - startLogger: function(logNameOrObject) { - if (this.logger) { return; } - logNameOrObject = logNameOrObject || ('results-' + START.getTime() + '.html'); - this.logger = (typeof logNameOrObject === 'string') ? new LogFile(logNameOrObject) : logNameOrObject; - this.loggingTimeoutId = setTimeout(this.write_.bind(this), this.refreshIntervalMs); - return this; + setLogFile: function(logNameOrObject) { + this.logNameOrObject = logNameOrObject; }, - stopLogger: function() { - if (!this.logger) { return; } + setLoggingEnabled: function(enabled) { clearTimeout(this.loggingTimeoutId); - this.logger.close(); - this.logger = null; + 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() { @@ -153,32 +151,30 @@ ReportSet.prototype = { reports: self.reports }); }, - write_: function() { + writeToLog_: function() { this.loggingTimeoutId = setTimeout(this.write_.bind(this), this.refreshIntervalMs); this.logger.clear(this.getHtml()); } }; // ================= -// Global stuff +// 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 ReportSet(); +var REPORT_MANAGER = exports.REPORT_MANAGER = new ReportGroup(); NODELOAD_CONFIG.on('apply', function() { REPORT_MANAGER.refreshIntervalMs = NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS; - if (NODELOAD_CONFIG.LOGS_ENABLED) { - REPORT_MANAGER.startLogger(); - } + REPORT_MANAGER.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); }); -HTTP_SERVER.on('^/$', function(url, req, res) { +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.on('^/reports$', function(url, req, res) { +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); diff --git a/lib/stats.js b/lib/stats.js index 3350277..011ca66 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -1,5 +1,3 @@ -/*jslint forin:true */ - // ------------------------------------ // Statistics // ------------------------------------ @@ -7,6 +5,7 @@ // 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; if (BUILD_AS_SINGLE_FILE === undefined) { var fs = require('fs'); @@ -15,7 +14,6 @@ var fs = require('fs'); var Histogram = exports.Histogram = function(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 - this.type = "Histogram"; this.params = params; this.size = params && params.buckets || 3000; this.percentiles = params && params.percentiles || [0.95, 0.99]; @@ -137,7 +135,6 @@ Histogram.prototype = { }; var Accumulator = exports.Accumulator = function() { - this.type = "Accumulator"; this.total = 0; this.length = 0; }; @@ -163,7 +160,6 @@ Accumulator.prototype = { }; var ResultsCounter = exports.ResultsCounter = function() { - this.type = "ResultsCounter"; this.start = new Date(); this.items = {}; this.length = 0; @@ -215,7 +211,6 @@ ResultsCounter.prototype = { }; var Uniques = exports.Uniques = function() { - this.type = "Uniques"; this.start = new Date(); this.items = {}; this.uniques = 0; @@ -256,7 +251,6 @@ Uniques.prototype = { }; var Peak = exports.Peak = function() { - this.type = "Peak"; this.peak = 0; this.length = 0; }; @@ -285,7 +279,6 @@ Peak.prototype = { }; var Rate = exports.Rate = function() { - this.type = "Rate"; this.start = new Date(); this.length = 0; }; @@ -309,7 +302,6 @@ Rate.prototype = { }; var LogFile = exports.LogFile = function(filename) { - this.type = "LogFile"; this.writepos = null; this.length = 0; this.filename = filename; @@ -348,7 +340,6 @@ LogFile.prototype = { }; var NullLog = exports.NullLog = function() { - this.type = "NullLog"; this.length = 0; }; NullLog.prototype = { @@ -360,20 +351,11 @@ NullLog.prototype = { 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 = exports.Reportable = function(name, Backend, backendparams) { + this.name = name || ''; this.length = 0; this.interval = new Backend(backendparams); this.cumulative = new Backend(backendparams); - this.trend = trend; this.lastSummary = null; }; Reportable.prototype = { @@ -444,23 +426,4 @@ var nextPareto = exports.nextPareto = function(min, max, shape) { 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; -}; - -// ================= -// Private methods -// ================= - -function statsClassFromString(name) { - var 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 +}; \ No newline at end of file diff --git a/lib/testapi.js b/lib/testapi.js deleted file mode 100644 index 2ba7004..0000000 --- a/lib/testapi.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/lib/util.js b/lib/util.js index 4cfbf91..105b6af 100644 --- a/lib/util.js +++ b/lib/util.js @@ -5,7 +5,7 @@ // This file defines qputs, qprint, and Utils. // // Extends node.js util.js with other common functions. - +// var util = require('util'); var BUILD_AS_SINGLE_FILE; @@ -27,8 +27,8 @@ var qprint = util.qprint = function(s) { // Static utility methods // util.uid = function() { - this.lastUid_ = this.lastUid_ || 0; - return this.lastUid_++; + exports.lastUid_ = exports.lastUid_ || 0; + return exports.lastUid_++; }; util.defaults = function(obj, defaults) { for (var i in defaults) { diff --git a/test/http.test.js b/test/http.test.js index cf17e56..236c0f2 100644 --- a/test/http.test.js +++ b/test/http.test.js @@ -1,5 +1,6 @@ var http = require('http'), - nlhttp = require('../lib/http').quiet(), + nlconfig = require('../lib/config').quiet(), + nlhttp = require('../lib/http'), HTTP_SERVER = nlhttp.HTTP_SERVER; HTTP_SERVER.start(); @@ -8,7 +9,7 @@ setTimeout(function() { HTTP_SERVER.stop(); }, 1500); module.exports = { 'example: add a new route': function(assert, beforeExit) { var done = false; - HTTP_SERVER.on('^/route', function() { + HTTP_SERVER.addRoute('^/route', function() { done = true; }); diff --git a/test/loop.test.js b/test/loop.test.js index ad079fd..d8bc8fb 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -6,13 +6,13 @@ module.exports = { 'example: a basic rps loop with set duration': function(assert, beforeExit) { var i = 0, start = new Date(), lasttime = start, duration, l = Loop.create({ - fun: function(loopFun) { + fun: function(finished) { var now = new Date(); assert.ok(Math.abs(now - lasttime) < 210, (now - lasttime).toString()); lasttime = now; i++; - loopFun(); + finished(); }, rps: 5, // times per second (every 200ms) duration: 1 // second @@ -29,7 +29,7 @@ module.exports = { 'test numberOfTimes loop': function(assert, beforeExit) { var i = 0, l = Loop.create({ - fun: function(loopFun) { i++; loopFun(); }, + fun: function(finished) { i++; finished(); }, rps: 5, numberOfTimes: 3 }).start(); @@ -41,7 +41,7 @@ module.exports = { 'test emits start and stop events': function(assert, beforeExit) { var started, ended, l = Loop.create({ - fun: function(loopFun) { loopFun(); }, + fun: function(finished) { finished(); }, rps: 10, numberOfTimes: 3 }).start(); @@ -58,7 +58,7 @@ module.exports = { 'test concurrency': function(assert, beforeExit) { var i = 0, start = new Date(), duration, s = new Scheduler(); s.schedule({ - fun: function(loopFun) { i++; loopFun(); }, + fun: function(finished) { i++; finished(); }, rps: 10, duration: 1, concurrency: 5 @@ -76,7 +76,7 @@ module.exports = { 'scheduler emits events': function(assert, beforeExit) { var s = new Scheduler(), started = false, ended = false; s.schedule({ - fun: function(loopFun) { loopFun(); }, + fun: function(finished) { finished(); }, numberOfTimes: 3 }).startAll(); @@ -91,12 +91,12 @@ module.exports = { 'test mixed monitored and unmonitored loops': function(assert, beforeExit) { var s = new Scheduler(); s.schedule({ - fun: function(loopFun) { loopFun(); }, + fun: function(finished) { finished(); }, numberOfTimes: 50, concurrency: 5 }); s.schedule({ - fun: function(loopFun) { loopFun(); }, + fun: function(finished) { finished(); }, rps: 1, monitored: false }); @@ -113,7 +113,7 @@ module.exports = { 'test all unmonitored loops': function(assert, beforeExit) { var s = new Scheduler(), ended = false; s.schedule({ - fun: function(loopFun) { loopFun(); }, + fun: function(finished) { finished(); }, rps: 2, concurrency: 2, monitored: false diff --git a/test/monitoring.test.js b/test/monitoring.test.js index 04e60f2..ae94fd8 100644 --- a/test/monitoring.test.js +++ b/test/monitoring.test.js @@ -5,7 +5,7 @@ var http = require('http'), util = require('../lib/util'), monitoring = require('../lib/monitoring'), Monitor = monitoring.Monitor, - MonitorSet = monitoring.MonitorSet; + MonitorGroup = monitoring.MonitorGroup; function mockConnection(callback) { var conn = { @@ -20,11 +20,8 @@ module.exports = { 'example: track runtime of a function': function(assert, beforeExit) { var m = new Monitor('runtime'), f = function() { - var mon = m.start(), runtime = Math.floor(Math.random() * 100); - setTimeout( - function() { mon.end(); }, - runtime - ); + var ctx = m.start(), runtime = Math.floor(Math.random() * 100); + setTimeout(function() { ctx.end(); }, runtime); }; for (var i = 0; i < 20; i++) { @@ -40,15 +37,15 @@ module.exports = { assert.ok(summary.median > 0 && summary.median < 100); }); }, - 'example: use a MonitorSet to organize multiple Monitors': function(assert, beforeExit) { - var m = new MonitorSet('runtime'), + 'example: use a MonitorGroup to organize multiple Monitors': function(assert, beforeExit) { + var m = new MonitorGroup('runtime'), f = function() { - var trmon = m.start('transaction'); + var transactionCtx = m.start('transaction'); mockConnection(function(conn) { - var opmon = m.start('operation'); + var operationCtx = m.start('operation'); conn.operation(function() { - opmon.end(); - trmon.end(); + operationCtx.end(); + transactionCtx.end(); }); }); }; @@ -66,7 +63,7 @@ module.exports = { assert.ok(Math.abs(summary['operation']['runtime'].median - 25) <= 5); }); }, - 'example: use EventEmitter objects instead of interacting with MonitorSet directly': function(assert, beforeExit) { + 'example: use EventEmitter objects instead of interacting with MonitorGroup directly': function(assert, beforeExit) { function MonitoredObject() { EventEmitter.call(this); var self = this; @@ -83,10 +80,10 @@ module.exports = { } util.inherits(MonitoredObject, EventEmitter); - var m = new MonitorSet('runtime'); + var m = new MonitorGroup('runtime'); for (var i = 0; i < 5; i++) { var obj = new MonitoredObject(); - m.monitor(obj); + m.monitorObjects(obj); setTimeout(obj.run, i * 100); } @@ -114,7 +111,7 @@ module.exports = { var m = new Monitor('runtime'); for (var i = 0; i < 5; i++) { var obj = new MonitoredObject(); - m.monitor(obj); + m.monitorObjects(obj); setTimeout(obj.run, i * 100); } @@ -132,7 +129,7 @@ module.exports = { m = new Monitor('result-codes', 'uniques', 'request-bytes', 'response-bytes'), client = http.createClient(80, 'www.google.com'), f = function() { - var mon = m.start(), + var ctx = m.start(), path = '/search?q=' + q++, req = client.request( 'GET', @@ -142,7 +139,7 @@ module.exports = { req.path = path; req.end(); req.on('response', function(res) { - mon.end({req: req, res: res}); + ctx.end({req: req, res: res}); }); }; @@ -176,11 +173,11 @@ module.exports = { }); }, 'monitor generates update events with interval and overall stats': function(assert, beforeExit) { - var m = new Monitor('runtime').updateEvery(220), + var m = new Monitor('runtime').setUpdateIntervalMs(220), intervals = 0, f = function() { - var mon = m.start(), runtime = Math.floor(Math.random() * 10); - setTimeout(function() { mon.end(); }, runtime); + var ctx = m.start(), runtime = Math.floor(Math.random() * 10); + setTimeout(function() { ctx.end(); }, runtime); }; // Call to f every 100ms for a total runtime >500ms @@ -189,7 +186,7 @@ module.exports = { } // Disable 'update' events after 500ms so that this test can complete - setTimeout(function() { m.disableUpdates(); }, 510); + setTimeout(function() { m.setUpdateIntervalMs(0); }, 510); m.on('update', function(interval, overall) { assert.strictEqual(overall, m.stats); diff --git a/test/reporting.test.js b/test/reporting.test.js index 4963069..d1e7785 100644 --- a/test/reporting.test.js +++ b/test/reporting.test.js @@ -1,12 +1,13 @@ /*jslint sub:true */ -var reporting = require('../lib/reporting').disableServer(), +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.startLogger('.reporting.test-output.html'); -setTimeout(function() { REPORT_MANAGER.stopLogger(); }, 1000); +REPORT_MANAGER.setLogFile('.reporting.test-output.html'); +setTimeout(function() { REPORT_MANAGER.setLoggingEnabled(false); }, 1000); function mockConnection(callback) { var conn = { @@ -43,10 +44,10 @@ module.exports = { assert.isNotNull(html.match('graph'+chart2.uid+' = new Dygraph')); assert.isNotNull(html.match('id="reportSummary'+report.uid)); }, - 'example: update reports from Monitor and MonitorSet stats': function(assert, beforeExit) { - var m = new monitoring.MonitorSet('runtime') - .init('transaction', 'operation') - .updateEvery(200), + 'example: update reports from Monitor and MonitorGroup stats': function(assert, beforeExit) { + var m = new monitoring.MonitorGroup('runtime') + .initMonitors('transaction', 'operation') + .setUpdateIntervalMs(200), f = function() { var trmon = m.start('transaction'); mockConnection(function(conn) { @@ -58,7 +59,7 @@ module.exports = { }); }; - REPORT_MANAGER.addReport('All Monitors').updateFromMonitorSet(m); + REPORT_MANAGER.addReport('All Monitors').updateFromMonitorGroup(m); REPORT_MANAGER.addReport('Transaction').updateFromMonitor(m.monitors['transaction']); REPORT_MANAGER.addReport('Operation').updateFromMonitor(m.monitors['operation']); @@ -67,7 +68,7 @@ module.exports = { } // Disable 'update' events after 500ms so that this test can complete - setTimeout(function() { m.disableUpdates(); }, 510); + setTimeout(function() { m.setUpdateIntervalMs(0); }, 510); beforeExit(function() { var trReport = REPORT_MANAGER.reports.filter(function(r) { return r.name === 'Transaction'; })[0]; From e4044c77b68ac2a4dc5af8cc4efc29ab1c3f9512 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 00:22:13 -0500 Subject: [PATCH 07/41] Remove Scheduler and add MultiLoop Move UpdateEventGenerator to util.PeriodicUpdater Move stats collectors to their own file Other minor fixes --- lib/collectors.js | 123 ++++++++++++++++ lib/loop.js | 307 ++++++++++++++++++++++++---------------- lib/monitoring.js | 169 +++++----------------- lib/reporting.js | 5 +- lib/statsmgr.js | 40 ------ lib/util.js | 25 ++++ test/loop.test.js | 120 ++++++++-------- test/monitoring.test.js | 20 +-- test/reporting.test.js | 7 +- 9 files changed, 438 insertions(+), 378 deletions(-) create mode 100644 lib/collectors.js delete mode 100644 lib/statsmgr.js diff --git a/lib/collectors.js b/lib/collectors.js new file mode 100644 index 0000000..6909a0d --- /dev/null +++ b/lib/collectors.js @@ -0,0 +1,123 @@ +// +// 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 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 && http.req.headers && http.req.headers['content-length']) { + self.stats.put(http.req.headers['content-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 + +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.end = function(context, http) { + if (self.successCodes.indexOf(http.res.statusCode) < 0) { + self.stats.put(1); + + if (self.logfile) { + var body = ''; + http.res.on('data', function(chunk) { body += chunk; }); + http.res.on('end', function(chunk) { + self.logfile.put(JSON.stringify({ + 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, + body: body + } + }) + '\n'); + }); + } + } + }; +}; +StatsCollectors['http-errors'].disableIntervalCollection = true; // Per-interval stats should be not be collected diff --git a/lib/loop.js b/lib/loop.js index 690956b..9bea1b1 100644 --- a/lib/loop.js +++ b/lib/loop.js @@ -2,7 +2,7 @@ // Event-based looping // ----------------------------------------- // -// This file defines Loop and Scheduler. +// 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() @@ -13,10 +13,11 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var util = require('./util'); +var PeriodicUpdater = util.PeriodicUpdater; var EventEmitter = require('events').EventEmitter; } -/** LOOP_DEFAULTS defines all of the parameters that used with Loop.create() and Scheduler.schedule() */ +/** LOOP_DEFAULTS defines all of the parameters that used with Loop.create(), MultiLoop() */ var LOOP_DEFAULTS = { fun: null, // A function to execute which accepts the parameters (finished, args). // The value of args is the return value of argGenerator() or the args @@ -27,13 +28,42 @@ var LOOP_DEFAULTS = { // 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() (Scheduler only) 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() - delay: 0, // Seconds to wait before calling fun() for the first time - monitored: true // Does this loop need to finish in order for a call to the containing - // Scheduler.startAll() to complete + concurrency: 1, // (MultiLoop only) Number of concurrent calls of fun() + // + concurrencyProfile: null, // (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: null // (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]] +}; + +var Loop; + +/** Create a Loop from a specification object. LOOP_DEFAULTS lists the supported parameters. */ +exports.create = function(spec) { + util.defaults(spec, LOOP_DEFAULTS); + + var fun = Loop.rpsLoop(spec.rps, spec.fun), + args = spec.argGenerator ? spec.argGenerator() : spec.args, + conditions = []; + + 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)); + } + + return new Loop(fun, args, conditions); }; /** Loop wraps an arbitrary function to be executed in a loop. Each iteration of the loop is scheduled @@ -52,47 +82,20 @@ and 'enditeration'. @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 delay number of seconds before the first iteration of fun is executed */ -var Loop = exports.Loop = function Loop(fun, args, conditions, delay, monitored) { + are conditions that can be used here. */ +var Loop = exports.Loop = function Loop(fun, args, conditions) { EventEmitter.call(this); this.id = util.uid(); this.fun = fun; this.args = args; this.conditions = conditions || []; - this.delay = delay; - this.monitored = monitored; this.running = false; }; -/** Static method to create a Loop from a spec object. LOOP_DEFAULTS lists the supported parameters. */ -Loop.create = function(spec) { - util.defaults(spec, LOOP_DEFAULTS); - - var fun = (spec.rps < Infinity) - ? Loop.rpsLoop(spec.rps, spec.fun) - : spec.fun, - args = spec.argGenerator && spec.argGenerator(), - conditions = []; - - if (spec.numberOfTimes > 0 && spec.numberOfTimes < Infinity) { - conditions.push(Loop.maxExecutions(spec.numberOfTimes)); - } - if (spec.duration > 0 && spec.duration < Infinity) { - var duration = (spec.delay && spec.delay > 0) - ? spec.duration + spec.delay - : spec.duration; - conditions.push(Loop.timeLimit(duration)); - } - - return new Loop(fun, args, conditions, spec.delay, spec.monitored); -}; - util.inherits(Loop, EventEmitter); -/** Start executing this.fun with the arguments, this.args, until any -condition in this.conditions returns false. The loop begins after a delay of -this.delay seconds. When the loop completes the 'end' event is emitted. */ +/** 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() { @@ -102,13 +105,7 @@ Loop.prototype.start = function() { if (self.running) { return; } self.running = true; - - if (self.delay && self.delay > 0) { - setTimeout(startLoop, self.delay * 1000); - } else { - process.nextTick(startLoop); - } - + process.nextTick(startLoop); return this; }; @@ -142,6 +139,7 @@ Loop.prototype.loop_ = function() { } }; + // Predefined functions that can be used in Loop.conditions /** Returns false after a given number of seconds */ @@ -189,116 +187,175 @@ Loop.loopWrapper = function(fun, start, finish) { }; /** Wrapper for executing a Loop function rps times per second. */ Loop.rpsLoop = function(rps, fun) { - var running, lagging, - timeout = (rps && rps > 0) ? (1/rps * 1000) : 0, - finishFun = function(finished) { - running = false; - if (lagging) { - finished(); + var timeout, running, lagging, restart, + loop = function(finished, args) { + if (timeout === Infinity) { + restart = function() { loop(finished, args); }; + return; + } + running = true; + lagging = (timeout <= 0); + if (!lagging) { + setTimeout(function() { + lagging = running; + if (!lagging) { finished(); } + }, timeout); } + fun(function() { + running = false; + if (lagging) { finished(); } + }, args); }; - - return function(finished, args) { - running = true; - lagging = (timeout <= 0); - if (!lagging) { - setTimeout(function() { - lagging = running; - if (!lagging) { - finished(); - } - }, timeout); + + loop.__defineGetter__('rps', function() { return rps; }); + loop.__defineSetter__('rps', function(val) { + rps = (val >= 0) ? val : Infinity; + timeout = (rps >= 0) ? Math.floor(1/rps * 1000) : 0; + if (restart && timeout < Infinity) { + var oldRestart = restart; + restart = null; + oldRestart(); } - var callback = function() { finishFun(finished); }; - fun(callback, args); - }; + }); + loop.rps = rps; + + return loop; }; // ----------------------------------------- -// Scheduler for event-based loops +// MultiLoop // ----------------------------------------- // -// Scheduler provides a way to define and group sets of Loops that are started and stopped together. -/** A scheduler starts and monitors a group of Loops. Each Loop can be monitored or unmonitored. When all -monitored loops complete, Scheduler considers the entire group to be complete and stops all unmonitored -loops. */ -var Scheduler = exports.Scheduler = function Scheduler() { - this.id = util.uid(); +/** 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_DEFAULTS lists the supported specification parameters. */ +var MultiLoop = exports.MultiLoop = function MultiLoop(spec) { + EventEmitter.call(this); + + this.spec = util.extend({}, util.defaults(spec, LOOP_DEFAULTS)); this.loops = []; - this.running = false; + 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(Scheduler, EventEmitter); +util.inherits(MultiLoop, EventEmitter); -/** Primary function for adding a new Loop given a spec object. LOOP_DEFAULTS lists the supported -parameters. Start all scheduled loops by calling startAll(). If the scheduler is already started, the -loops are started immediately upon scheduling. */ -Scheduler.prototype.schedule = function(spec) { - util.defaults(spec, LOOP_DEFAULTS); +/** 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_ = []; - // concurrency is handled by creating multiple loops with portions of the load - spec.numberOfTimes /= spec.concurrency; - spec.rps /= spec.concurrency; - for (var i = 0; i < spec.concurrency; i++) { - this.addLoop(Loop.create(spec)); + if (this.spec.numberOfTimes > 0 && this.spec.numberOfTimes < Infinity) { + this.loopConditions_.push(Loop.maxExecutions(this.spec.numberOfTimes)); } - return this; -}; -Scheduler.prototype.addLoop = function(loop) { - this.loops.push(loop); - // If the scheduler is running (startAll() was already called), start new loops immediately - if (this.running) { - this.startLoop_(loop); + 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; }; -/** Start all scheduled Loops. When the loops complete, 'end' event is emitted. */ -Scheduler.prototype.startAll = function() { - var self = this; - if (self.running || self.loops.length === 0) { return; } - self.running = true; - process.nextTick(function() { self.emit('start'); }); - for (var i in self.loops) { - if (!self.loops[i].running) { - self.startLoop_(self.loops[i]); - } - } - return self; -}; + /** Force all loops to finish */ -Scheduler.prototype.stopAll = function() { +MultiLoop.prototype.stop = function() { + if (!this.running) { return; } + clearTimeout(this.endTimeoutId); + clearTimeout(this.updateTimeoutId); + this.running = false; this.loops.forEach(function(l) { l.stop(); }); - return this; + this.emit('remove', this.loops); + this.emit('end'); + this.loops = []; }; -/** Starts a single loop, installing a Loop 'end' event listener to check for completion. */ -Scheduler.prototype.startLoop_ = function(loop) { - var self = this; - loop.start(); - loop.on('end', function() { self.checkFinished_(); }); - return self; + +MultiLoop.prototype.getProfileValue_ = function(profile, time) { + // 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. + 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]; +}; + +MultiLoop.prototype.getProfileNextTimeout_ = function(profile, time) { + // 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. + 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; }; -/** Iterate all loops and see if any are still running. If all loops are complete, then emit 'end'. */ -Scheduler.prototype.checkFinished_ = function() { - var isAllUnmonitoredLoops = true, - hasRunningLoop = false; - - this.loops.forEach(function (l) { - if (l.monitored && l.running) { - return false; + +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 fun = Loop.rpsLoop(0, this.spec.fun), + args = this.spec.argGenerator ? this.spec.argGenerator() : this.spec.args, + loop = new Loop(fun, args, this.loopConditions_).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.fun.rps = rpsPerLoop; }); + this.emit('rps', rps); + } + + this.concurrency = concurrency; + this.rps = rps; - isAllUnmonitoredLoops = isAllUnmonitoredLoops && !l.monitored; - hasRunningLoop = hasRunningLoop || l.running; - }); - if (isAllUnmonitoredLoops && hasRunningLoop) { - return false; + 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.stopAll(); this.emit('end'); return true; }; diff --git a/lib/monitoring.js b/lib/monitoring.js index 1bbafdd..3016599 100644 --- a/lib/monitoring.js +++ b/lib/monitoring.js @@ -4,22 +4,18 @@ // // This file defines Monitor and MonitorGroup, and StatsLogger // -// + var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var START = new Date(); var util = require('./util'); +var PeriodicUpdater = util.PeriodicUpdater; +var StatsCollectors = require('./collectors'); var EventEmitter = require('events').EventEmitter; -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 LogFile = require('./stats').LogFile; } -var UpdateEventGenerator, StatsLogger; +var StatsLogger; // ----------------- // Monitor @@ -41,42 +37,47 @@ 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. -@param arguments contain names of the statistics to track. Register more statistics by extending - Monitor.StatsCollectors. +@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); + PeriodicUpdater.call(this); // adds updateInterval property and calls update() this.targets = []; this.setStats.apply(this, arguments); - this.updater = new UpdateEventGenerator(this); }; util.inherits(Monitor, EventEmitter); /** Set the statistics this monitor should gather. */ -Monitor.prototype.setStats = function(statsNames) { // arguments contains stats names +Monitor.prototype.setStats = function(stats) { // arguments contains stats names var self = this; self.collectors = []; self.stats = {}; self.interval = {}; - statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); - statsNames.forEach(function(name) { - if (!Monitor.StatsCollectors[name]) { throw new Error('No collector for statistic: ' + name); } - var intervalCollector = new Monitor.StatsCollectors[name](); - var overallCollector = new Monitor.StatsCollectors[name](); - self.collectors.push(intervalCollector); - self.collectors.push(overallCollector); - self.interval[name] = intervalCollector.stats; - self.stats[name] = overallCollector.stats; + stats = (stats instanceof Array) ? stats : [].concat.apply([], 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; + } }); }; -/** Monitor should gather statistics for each intervalMs period, and generate 'update' events */ -Monitor.prototype.setUpdateIntervalMs = function(milliseconds) { - this.updater.setUpdateIntervalMs(milliseconds); - return this; -}; - /** 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) { @@ -137,82 +138,6 @@ Monitor.prototype.update = function() { }); }; -/** Track the runtime of an operation, storing stats in a stats.js#Histogram */ -function RuntimeCollector() { - var self = this; - self.stats = new Histogram(); - 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}). */ -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. */ -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). */ -function RequestBytesCollector() { - var self = this; - self.stats = new Accumulator(); - self.end = function(context, http) { - if (http && http.req && http.req.headers && http.req.headers['content-length']) { - self.stats.put(http.req.headers['content-length']); - } - }; -} - -/** Track the size of HTTP response bodies. It doesn't account for headers! */ -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}). */ -function UniquesCollector() { - var self = this; - self.stats = new Uniques(); - self.end = function(context, http) { - if (http && http.req) { self.stats.put(http.req.path); } - }; -} - -/** Define new statistics that Monitor can track by adding to Monitor.StatsCollectors. Each entry should -be a class with: -- 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 */ -Monitor.StatsCollectors = { - 'runtime': RuntimeCollector, - 'latency': RuntimeCollector, - 'result-codes': ResultCodesCollector, - 'concurrency': ConcurrencyCollector, - 'request-bytes': RequestBytesCollector, - 'response-bytes': ResponseBytesCollector, - 'uniques': UniquesCollector, -}; - // ----------------- // MonitorGroup @@ -225,6 +150,7 @@ also emit regular 'update' events as well as log the statistics from the interva Monitor.StatsCollectors. */ var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { EventEmitter.call(this); + PeriodicUpdater.call(this); var summaryFun = function() { var summary = {}; @@ -240,7 +166,6 @@ var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { this.statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); this.monitors = {}; - this.updater = new UpdateEventGenerator(this); this.stats = { summary: summaryFun }; this.interval = { summary: summaryFun }; }; @@ -260,13 +185,6 @@ MonitorGroup.prototype.initMonitors = function(monitorNames) { return self; }; -/** All the Monitors in this set should gather statistics for each intervalMs period. MonitorGroup should -generate 'update' events */ -MonitorGroup.prototype.setUpdateIntervalMs = function(interval) { - this.updater.setUpdateIntervalMs(interval); - return this; -}; - /** Call .start() for the named monitor */ MonitorGroup.prototype.start = function(monitorName, args) { monitorName = monitorName || ''; @@ -349,28 +267,7 @@ StatsLogger.prototype.stop = function() { return this; }; StatsLogger.prototype.log_ = function(interval) { - this.log.put(JSON.stringify(interval.summary()) + ',\n'); + var summary = interval.summary(); + summary.ts = new Date(); + this.log.put(JSON.stringify(summary) + ',\n'); }; - -// ================= -// Private methods -// ================= -function UpdateEventGenerator(parent, updateIntervalMs) { - this.parent = parent; - this.setUpdateIntervalMs(updateIntervalMs); -} -UpdateEventGenerator.prototype.setUpdateIntervalMs = function(milliseconds) { - clearTimeout(this.updateTimeoutId); - this.scheduleUpdate_(milliseconds); -}; -UpdateEventGenerator.prototype.scheduleUpdate_ = function(milliseconds) { - var self = this; - if (milliseconds > 0) { - self.updateTimeoutId = setTimeout( - function() { - self.scheduleUpdate_(milliseconds); - self.parent.update(); - }, - milliseconds); - } -}; \ No newline at end of file diff --git a/lib/reporting.js b/lib/reporting.js index 83e2e76..a0699f9 100644 --- a/lib/reporting.js +++ b/lib/reporting.js @@ -115,7 +115,6 @@ Chart.prototype = { var ReportGroup = exports.ReportGroup = function() { this.reports = []; - this.refreshIntervalMs = 2000; this.logNameOrObject = 'results-' + START.getTime() + '.html'; }; ReportGroup.prototype = { @@ -152,7 +151,7 @@ ReportGroup.prototype = { }); }, writeToLog_: function() { - this.loggingTimeoutId = setTimeout(this.write_.bind(this), this.refreshIntervalMs); + this.loggingTimeoutId = setTimeout(this.writeToLog_.bind(this), this.refreshIntervalMs); this.logger.clear(this.getHtml()); } }; @@ -164,7 +163,7 @@ ReportGroup.prototype = { /** 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 = NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS; + REPORT_MANAGER.refreshIntervalMs = REPORT_MANAGER.refreshIntervalMs || NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS; REPORT_MANAGER.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); }); diff --git a/lib/statsmgr.js b/lib/statsmgr.js deleted file mode 100644 index dc213b3..0000000 --- a/lib/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/lib/util.js b/lib/util.js index 105b6af..b0eb076 100644 --- a/lib/util.js +++ b/lib/util.js @@ -36,6 +36,7 @@ util.defaults = function(obj, defaults) { obj[i] = defaults[i]; } } + return obj; }; util.extend = function(obj, extension) { for (var i in extension) { @@ -56,4 +57,28 @@ util.argarray = function(args) { return (args instanceof Array) ? args : [].concat.apply([], args); }; +/** Make an object an UpdateEventGenerator by adding UpdateEventGenerator.call(this) to the constructor. +Monitor should gather statistics for each intervalMs period, and generate 'update' events */ +util.PeriodicUpdater = function(updateIntervalMs) { + var self = this, updateTimeoutId, + scheduleUpdate = function(milliseconds) { + clearTimeout(updateTimeoutId); + if (milliseconds > 0) { + updateTimeoutId = setTimeout( + function() { + scheduleUpdate(milliseconds); + self.update(); + }, + milliseconds); + } + }; + + this.__defineGetter__('updateInterval', function() { return updateIntervalMs; }); + this.__defineSetter__('updateInterval', function(milliseconds) { + scheduleUpdate(milliseconds); + updateIntervalMs = milliseconds; + }); + this.updateInterval = updateIntervalMs; +}; + util.extend(exports, util); \ No newline at end of file diff --git a/test/loop.test.js b/test/loop.test.js index d8bc8fb..5607b92 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -1,11 +1,11 @@ var loop = require('../lib/loop'), Loop = loop.Loop, - Scheduler = loop.Scheduler; + 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 = Loop.create({ + l = loop.create({ fun: function(finished) { var now = new Date(); assert.ok(Math.abs(now - lasttime) < 210, (now - lasttime).toString()); @@ -21,14 +21,31 @@ module.exports = { l.on('end', function() { duration = new Date() - start; }); beforeExit(function() { - assert.equal(i, 5, 'loop executed incorrect number of times'); + assert.equal(i, 5, 'loop executed incorrect number of times: ' + i); assert.ok(!l.running, 'loop still flagged as running'); assert.ok(Math.abs(duration - 1000) <= 50, '1000 == ' + duration); }); }, + '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 = Loop.create({ + l = loop.create({ fun: function(finished) { i++; finished(); }, rps: 5, numberOfTimes: 3 @@ -40,7 +57,7 @@ module.exports = { }, 'test emits start and stop events': function(assert, beforeExit) { var started, ended, - l = Loop.create({ + l = loop.create({ fun: function(finished) { finished(); }, rps: 10, numberOfTimes: 3 @@ -56,78 +73,57 @@ module.exports = { }, 'test concurrency': function(assert, beforeExit) { - var i = 0, start = new Date(), duration, s = new Scheduler(); - s.schedule({ - fun: function(finished) { i++; finished(); }, - rps: 10, - duration: 1, - concurrency: 5 - }).startAll(); + var i = 0, start = new Date(), duration, + l = new MultiLoop({ + fun: function(finished) { i++; finished(); }, + rps: 10, + duration: 1, + concurrency: 5 + }).start(); - s.on('end', function() { duration = new Date() - start; }); + l.on('end', function() { duration = new Date() - start; }); - assert.equal(s.loops.length, 5); + assert.equal(l.loops.length, 5); beforeExit(function() { assert.equal(i, 10, 'loop executed incorrect number of times'); - assert.ok(s.loops.every(function(l){ return !l.running; }), 'loops still flagged as running'); + assert.ok(l.loops.every(function(l){ return !l.running; }), 'loops still flagged as running'); assert.ok(Math.abs(duration - 1000) < 30, '1000 == ' + duration); }); }, - 'scheduler emits events': function(assert, beforeExit) { - var s = new Scheduler(), started = false, ended = false; - s.schedule({ - fun: function(finished) { finished(); }, - numberOfTimes: 3 - }).startAll(); + 'LoopGroup emits events': function(assert, beforeExit) { + var started = false, ended = false, + l = new MultiLoop({ + fun: function(finished) { finished(); }, + numberOfTimes: 3 + }).start(); - s.on('start', function() { started = true; }); - s.on('end', function() { ended = true; }); + 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 mixed monitored and unmonitored loops': function(assert, beforeExit) { - var s = new Scheduler(); - s.schedule({ - fun: function(finished) { finished(); }, - numberOfTimes: 50, - concurrency: 5 - }); - s.schedule({ - fun: function(finished) { finished(); }, - rps: 1, - monitored: false - }); - s.startAll(); - - var unmonitoredLoops = s.loops.filter(function(l) { return !l.monitored; }); - assert.equal(s.loops.length, 6); - assert.equal(unmonitoredLoops.length, 1); - assert.ok(s.loops.every(function(l){ return l.running; }), 'not all loops started'); - beforeExit(function() { - assert.ok(s.loops.every(function(l){ return !l.running; }), 'loops still flagged as running'); - }); - }, - 'test all unmonitored loops': function(assert, beforeExit) { - var s = new Scheduler(), ended = false; - s.schedule({ - fun: function(finished) { finished(); }, - rps: 2, - concurrency: 2, - monitored: false - }); - s.startAll(); - s.on('end', function() { ended = true; }); - - var unmonitoredLoops = s.loops.filter(function(l) { return !l.monitored; }); - assert.equal(s.loops.length, unmonitoredLoops.length); + 'change loop rate': function(assert, beforeExit) { + var i = 0, start = new Date(), duration, + l = loop.create({ + fun: function(finished) { + i++; + finished(); + }, + rps: 5, + duration: 2 + }).start(); + + l.on('end', function() { duration = new Date() - start; }); + setTimeout(function() { l.fun.rps = 10; }, 1000); + setTimeout(function() { l.fun.rps = 20; }, 1500); - s.loops.forEach(function(l) { l.stop(); }); - beforeExit(function() { - assert.ok(ended, 'scheduler never finished'); + 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); }); - } + }, }; \ No newline at end of file diff --git a/test/monitoring.test.js b/test/monitoring.test.js index ae94fd8..8c4f632 100644 --- a/test/monitoring.test.js +++ b/test/monitoring.test.js @@ -142,51 +142,53 @@ module.exports = { ctx.end({req: req, res: res}); }); }; - + client.on('error', function(e) { assert.fail('This test requires internet connectivity: ' + e); }); - + 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 >= 1); + assert.ok(resultCodesSummary.rps >= 0); assert.equal(resultCodesSummary['200'], 2); assert.equal(uniquesSummary.total, 2); assert.equal(uniquesSummary.uniqs, 2); - + assert.equal(requestBytesSummary.total, 0); - + assert.ok(responseBytesSummary.total > 1000); }); }, 'monitor generates update events with interval and overall stats': function(assert, beforeExit) { - var m = new Monitor('runtime').setUpdateIntervalMs(220), + 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.setUpdateIntervalMs(0); }, 510); + setTimeout(function() { m.updateInterval = 0; }, 510); m.on('update', function(interval, overall) { assert.strictEqual(overall, m.stats); diff --git a/test/reporting.test.js b/test/reporting.test.js index d1e7785..5ee8b1b 100644 --- a/test/reporting.test.js +++ b/test/reporting.test.js @@ -46,8 +46,7 @@ module.exports = { }, 'example: update reports from Monitor and MonitorGroup stats': function(assert, beforeExit) { var m = new monitoring.MonitorGroup('runtime') - .initMonitors('transaction', 'operation') - .setUpdateIntervalMs(200), + .initMonitors('transaction', 'operation'), f = function() { var trmon = m.start('transaction'); mockConnection(function(conn) { @@ -58,6 +57,8 @@ module.exports = { }); }); }; + + m.updateInterval = 200; REPORT_MANAGER.addReport('All Monitors').updateFromMonitorGroup(m); REPORT_MANAGER.addReport('Transaction').updateFromMonitor(m.monitors['transaction']); @@ -68,7 +69,7 @@ module.exports = { } // Disable 'update' events after 500ms so that this test can complete - setTimeout(function() { m.setUpdateIntervalMs(0); }, 510); + setTimeout(function() { m.updateInterval = 0; }, 510); beforeExit(function() { var trReport = REPORT_MANAGER.reports.filter(function(r) { return r.name === 'Transaction'; })[0]; From 051ab2627c12c34b2ba9764fc6ab492d8d04f0fb Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 11:35:44 -0500 Subject: [PATCH 08/41] Combine loop.create into Loop constructor Combine rpsLoop directly into Loop Summary dynamically generates HTML elements from reports --- lib/http.js | 7 ++- lib/loop.js | 145 ++++++++++++++++++++++------------------------ lib/reporting.js | 6 +- lib/summary.tpl | 127 ++++++++++++++++++++++------------------ test/loop.test.js | 37 ++++++++++-- 5 files changed, 179 insertions(+), 143 deletions(-) diff --git a/lib/http.js b/lib/http.js index bbbf879..c9cac59 100644 --- a/lib/http.js +++ b/lib/http.js @@ -20,10 +20,12 @@ var NODELOAD_CONFIG = config.NODELOAD_CONFIG; regexs using HttpServer.on(). */ var HttpServer = exports.HttpServer = function HttpServer() { this.routes = []; + this.running = false; }; /** Start the server listening on the given port */ HttpServer.prototype.start = function(port, hostname) { - if (this.server) { return; } + if (this.running) { return; } + this.running = true; var self = this; port = port || 8000; @@ -34,7 +36,8 @@ HttpServer.prototype.start = function(port, hostname) { }; /** Terminate the server */ HttpServer.prototype.stop = function() { - if (!this.server) { return; } + if (!this.running) { return; } + this.running = false; this.server.close(); this.server = null; qputs('Shutdown HTTP server.'); diff --git a/lib/loop.js b/lib/loop.js index 9bea1b1..109f867 100644 --- a/lib/loop.js +++ b/lib/loop.js @@ -17,8 +17,8 @@ var PeriodicUpdater = util.PeriodicUpdater; var EventEmitter = require('events').EventEmitter; } -/** LOOP_DEFAULTS defines all of the parameters that used with Loop.create(), MultiLoop() */ -var LOOP_DEFAULTS = { +/** LOOP_OPTIONS defines all of the parameters that used with Loop.create(), MultiLoop() */ +var LOOP_OPTIONS = { fun: null, // 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 null. The function must call @@ -46,32 +46,16 @@ var LOOP_DEFAULTS = { // [[0, 100], [10, 500], [20, 0]] }; -var Loop; - -/** Create a Loop from a specification object. LOOP_DEFAULTS lists the supported parameters. */ -exports.create = function(spec) { - util.defaults(spec, LOOP_DEFAULTS); - - var fun = Loop.rpsLoop(spec.rps, spec.fun), - args = spec.argGenerator ? spec.argGenerator() : spec.args, - conditions = []; - - 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)); - } - - return new Loop(fun, args, conditions); -}; - /** 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 fun an asynchronous function that calls finished(result) when it finishes: +@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) { ... @@ -82,14 +66,44 @@ and 'enditeration'. @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. */ -var Loop = exports.Loop = function Loop(fun, args, conditions) { + 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 = fun; + this.fun = funOrSpec; this.args = args; this.conditions = conditions || []; this.running = false; + this.rps = rps; }; util.inherits(Loop, EventEmitter); @@ -121,18 +135,33 @@ Loop.prototype.checkConditions_ = function() { /** 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, - callback = function(result) { - self.emit('enditeration', result); - self.loop_(); - }, - callFun = 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); - self.fun(callback, 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); + process.nextTick(callfun); } else { self.running = false; self.emit('end'); @@ -185,42 +214,6 @@ Loop.loopWrapper = function(fun, start, finish) { fun(finishFun, args); }; }; -/** Wrapper for executing a Loop function rps times per second. */ -Loop.rpsLoop = function(rps, fun) { - var timeout, running, lagging, restart, - loop = function(finished, args) { - if (timeout === Infinity) { - restart = function() { loop(finished, args); }; - return; - } - running = true; - lagging = (timeout <= 0); - if (!lagging) { - setTimeout(function() { - lagging = running; - if (!lagging) { finished(); } - }, timeout); - } - fun(function() { - running = false; - if (lagging) { finished(); } - }, args); - }; - - loop.__defineGetter__('rps', function() { return rps; }); - loop.__defineSetter__('rps', function(val) { - rps = (val >= 0) ? val : Infinity; - timeout = (rps >= 0) ? Math.floor(1/rps * 1000) : 0; - if (restart && timeout < Infinity) { - var oldRestart = restart; - restart = null; - oldRestart(); - } - }); - loop.rps = rps; - - return loop; -}; // ----------------------------------------- @@ -230,11 +223,11 @@ Loop.rpsLoop = function(rps, fun) { /** 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_DEFAULTS lists the supported specification parameters. */ +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_DEFAULTS)); + 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]]; @@ -283,6 +276,7 @@ MultiLoop.prototype.getProfileValue_ = function(profile, time) { // 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. + if (!profile || profile.length === 0) { return 0; } if (time < 0) { return profile[0][0]; } var lastval = [0,0]; @@ -328,9 +322,8 @@ MultiLoop.prototype.update_ = function() { } else if (concurrency > this.concurrency) { var loops = []; for (i = 0; i < concurrency-this.concurrency; i++) { - var fun = Loop.rpsLoop(0, this.spec.fun), - args = this.spec.argGenerator ? this.spec.argGenerator() : this.spec.args, - loop = new Loop(fun, args, this.loopConditions_).start(); + 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); } @@ -340,7 +333,7 @@ MultiLoop.prototype.update_ = function() { if (concurrency !== this.concurrency || rps !== this.rps) { var rpsPerLoop = (rps / concurrency); - this.loops.forEach(function(l) { l.fun.rps = rpsPerLoop; }); + this.loops.forEach(function(l) { l.rps = rpsPerLoop; }); this.emit('rps', rps); } diff --git a/lib/reporting.js b/lib/reporting.js index a0699f9..466b802 100644 --- a/lib/reporting.js +++ b/lib/reporting.js @@ -65,8 +65,10 @@ Report.prototype = { util.forEach(stat.summary(), function(name, val) { self.summary[self.name + ' ' + monitorname + statname + ' ' + name] = val; }); - self.getChart(monitorname + statname) - .put(monitor.interval[statname].summary()); + if (monitor.interval[statname]) { + self.getChart(monitorname + statname) + .put(monitor.interval[statname].summary()); + } }); } }; diff --git a/lib/summary.tpl b/lib/summary.tpl index 6f70dd3..0bb203f 100644 --- a/lib/summary.tpl +++ b/lib/summary.tpl @@ -33,68 +33,81 @@ - +
-
- <% reports.forEach(function(report) { %> - <% for (var j in report.charts) { %> - <% var chart = report.charts[j]; %> -

<%=report.name%>: <%=chart.name%>

-
-
-
-
-
- <% } %> - <% }); %> -
+
-
- - - + }); + } - - + if(navigator.appName == "Microsoft Internet Explorer") { http = new ActiveXObject("Microsoft.XMLHTTP"); } else { http = new XMLHttpRequest(); } + + setInterval(function() { + http.open("GET", "/reports"); + http.onreadystatechange=function() { + if (http.readyState == 4 && http.status == 200) { + update(JSON.parse(http.responseText)); + } + } + http.send(null); + }, <%=refreshPeriodMs%>); + + graphs = {}; + update(<%=JSON.stringify(reports)%>); + \ No newline at end of file diff --git a/test/loop.test.js b/test/loop.test.js index 5607b92..23de2ab 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -5,7 +5,7 @@ var loop = require('../lib/loop'), module.exports = { 'example: a basic rps loop with set duration': function(assert, beforeExit) { var i = 0, start = new Date(), lasttime = start, duration, - l = loop.create({ + l = new Loop({ fun: function(finished) { var now = new Date(); assert.ok(Math.abs(now - lasttime) < 210, (now - lasttime).toString()); @@ -45,7 +45,7 @@ module.exports = { }, 'test numberOfTimes loop': function(assert, beforeExit) { var i = 0, - l = loop.create({ + l = new Loop({ fun: function(finished) { i++; finished(); }, rps: 5, numberOfTimes: 3 @@ -57,7 +57,7 @@ module.exports = { }, 'test emits start and stop events': function(assert, beforeExit) { var started, ended, - l = loop.create({ + l = new Loop({ fun: function(finished) { finished(); }, rps: 10, numberOfTimes: 3 @@ -107,7 +107,7 @@ module.exports = { }, 'change loop rate': function(assert, beforeExit) { var i = 0, start = new Date(), duration, - l = loop.create({ + l = new Loop({ fun: function(finished) { i++; finished(); @@ -117,8 +117,8 @@ module.exports = { }).start(); l.on('end', function() { duration = new Date() - start; }); - setTimeout(function() { l.fun.rps = 10; }, 1000); - setTimeout(function() { l.fun.rps = 20; }, 1500); + 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 @@ -126,4 +126,29 @@ module.exports = { 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 From 171d2a8c7ccf73cd93a34232458fbad559bc0b56 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 12:35:38 -0500 Subject: [PATCH 09/41] Add loadtesting module --- lib/collectors.js | 5 +- lib/loadtesting.js | 292 +++++++++++++++++++++++++++++++++++++++++++++ lib/summary.tpl | 4 +- 3 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 lib/loadtesting.js diff --git a/lib/collectors.js b/lib/collectors.js index 6909a0d..4674e0d 100644 --- a/lib/collectors.js +++ b/lib/collectors.js @@ -56,8 +56,9 @@ StatsCollectors['request-bytes'] = function RequestBytesCollector() { var self = this; self.stats = new Accumulator(); self.end = function(context, http) { - if (http && http.req && http.req.headers && http.req.headers['content-length']) { - self.stats.put(http.req.headers['content-length']); + 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); } } }; }; diff --git a/lib/loadtesting.js b/lib/loadtesting.js new file mode 100644 index 0000000..31a2b14 --- /dev/null +++ b/lib/loadtesting.js @@ -0,0 +1,292 @@ +// ------------------------------------ +// Main HTTP load testing interface +// ------------------------------------ +// +// This file defines run() and traceableRequest(). +// +// This file defines the public API for using nodeload to construct load tests. 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 START = new Date(); +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 PeriodicUpdater = util.PeriodicUpdater; +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 REPORT_MANAGER = reporting.REPORT_MANAGER; +var HTTP_SERVER = require('./http').HTTP_SERVER; +} + +/** TEST_DEFAULTS defines all of the parameters that can be set in a test specifiction passed to +run(). 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 + + // Specify one of: + host: 'localhost', // 1. (host, port) to connect to via HTTP + port: 8080, // + // + connectionGenerator: null, // 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: null, // 1. requestGenerator: a function + // function(http.Client) -> http.ClientRequest + requestLoop: null, // 2. requestLoop: is a function + // function(loopFun, http.Client) + method: 'GET', // If must call: + path: '/', // loopFun({ + requestData: null, // 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: null, // 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: null, // 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, createClient, requestGeneratorLoop; + +/** run(spec, ...) is the primary method for creating and executing load tests with nodeload. Each spec +can be a test specification or a ramp specification. See TEST_DEFAULTS for a list of the configuration +values in a test specification and RAMP_DEFAULTS in a ramp specification. + +@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 + } + + This object emits these events: + - 'update', stats since last update, overall stats: set the frequency of these events using setWindowSizeSeconds(). + - 'end': all tests finished +*/ +var run = exports.run = function(specs) { + specs = (specs instanceof Array) ? specs : util.argarray(arguments); + var tests = specs.map(function(spec) { + var generateConnection = function() { + return createClient(spec.port, spec.host); + }, + generateRequest = function(client) { + if (spec.requestGenerator) { return spec.requestGenerator(client); } + var request = client.request(spec.method, spec.path, { 'host': spec.host }); + request.end(spec.requestData); + return request; + }, + loop = new MultiLoop({ + fun: spec.requestLoop || requestGeneratorLoop(generateRequest), + argGenerator: spec.connectionGenerator || generateConnection, + 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); + + return { + spec: spec, + loop: loop, + monitor: monitor, + report: report, + }; + }); + + var loadtest = new LoadTest(tests).start(); + loadtest.updateInterval = 2000; + return loadtest; +}; + +var LoadTest = exports.LoadTest = function LoadTest(tests) { + EventEmitter.call(this); + PeriodicUpdater.call(this); + + var self = this; + self.tests = tests; + 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(keepAliveAfterDone) { + var self = this; + self.keepAliveAfterDone_ = keepAliveAfterDone; + + // 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; +}; + +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; + qputs('Done.'); + + if (!this.keepAliveAfterDone_) { + 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 = function() { + var client = http.createClient.apply(this, arguments); + return extendClient(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); + if (request) { + if (request.timeout > 0) { + timeoutId = setTimeout(function() { + running = false; + finished({req: request, res: {statusCode: 0}}); + }, request.timeout); + } + request.on('response', function(response) { + if (running) { + clearTimeout(timeoutId); + finished({req: request, res: response}); + } + }); + request.end(); + } else { + finished(null); + } + }; +} \ No newline at end of file diff --git a/lib/summary.tpl b/lib/summary.tpl index 0bb203f..d073f9d 100644 --- a/lib/summary.tpl +++ b/lib/summary.tpl @@ -46,15 +46,15 @@ - \n \n \n\n \n

Test Results

<%=new Date()%>

\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
\" style=\"float:left;width:660px;height:200px;\">
\n
legend\" style=\"float:left;width:80px;height:200px;\">
\n
\n
\n <% } %>\n <% } %>\n
\n
\n

Cumulative

\n <% for (var i in reports) { %>\n

\"/>

\n \n <% } %>\n
\n
\n
\n \n \n \n\n

generated with nodeload

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

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

\n \n\n \n";var BUILD_AS_SINGLE_FILE,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.setSlavePingIntervalMs=function(milliseconds){NODELOAD_CONFIG.SLAVE_PING_INTERVAL_MS=milliseconds;};var NODELOAD_CONFIG=exports.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 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 START=new Date();var util=require('./util');var PeriodicUpdater=util.PeriodicUpdater;var StatsCollectors=require('./collectors');var EventEmitter=require('events').EventEmitter;var LogFile=require('./stats').LogFile;} +var StatsLogger;var Monitor=exports.Monitor=function Monitor(){EventEmitter.call(this);PeriodicUpdater.call(this);this.targets=[];this.setStats.apply(this,arguments);};util.inherits(Monitor,EventEmitter);Monitor.prototype.setStats=function(stats){var self=this;self.collectors=[];self.stats={};self.interval={};stats=(stats instanceof Array)?stats:[].concat.apply([],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;}});};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.update=function(){this.emit('update',this.interval,this.stats);util.forEach(this.interval,function(name,stats){if(stats.length>0){stats.clear();}});};var MonitorGroup=exports.MonitorGroup=function MonitorGroup(statsNames){EventEmitter.call(this);PeriodicUpdater.call(this);var summaryFun=function(){var summary={};util.forEach(this,function(monitorName,stats){if(monitorName==='summary'){return;} +summary[monitorName]={};util.forEach(stats,function(statName,stat){summary[monitorName][statName]=stat.summary();});});return summary;};this.statsNames=(statsNames instanceof Array)?statsNames:[].concat.apply([],arguments);this.monitors={};this.stats={summary:summaryFun};this.interval={summary:summaryFun};};util.inherits(MonitorGroup,EventEmitter);MonitorGroup.prototype.initMonitors=function(monitorNames){var self=this;monitorNames=(monitorNames instanceof Array)?monitorNames:[].concat.apply([],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 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.log.put('[');this.monitor.on('update',this.logger_);return this;};StatsLogger.prototype.stop=function(){this.log.put(']');if(this.createdLog){this.log.close();this.log=null;} +this.monitor.removeListener('update',this.logger_);return this;};StatsLogger.prototype.log_=function(interval){var summary=interval.summary();summary.ts=new Date();this.log.put(JSON.stringify(summary)+',\n');};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();}} @@ -56,7 +41,7 @@ var sendStatus=function(status){res.writeHead(status,{"Content-Length":0});res.e 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/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 BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var fs=require('fs');} -var Histogram=exports.Histogram=function(params){this.type="Histogram";this.params=params;this.size=params&¶ms.buckets||3000;this.percentiles=params&¶ms.percentiles||[0.95,0.99];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;} @@ -64,50 +49,33 @@ return this.extra[idx];}else{var sum=this.extra.length;for(var i=this.items.leng 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=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]!==undefined){this.items[item]++;}else{this.items[item]=1;this.uniques++;} +this.length+=other.length;}};var Uniques=exports.Uniques=function(){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=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){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=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){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;};function statsClassFromString(name){var types={"Histogram":Histogram,"Accumulator":Accumulator,"ResultsCounter":ResultsCounter,"Uniques":Uniques,"Peak":Peak,"Rate":Rate,"LogFile":LogFile,"NullLog":NullLog,"Reportable":Reportable};return types[name];} -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{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 BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var http=require('http');var fs=require('fs');var util=require('./util');var qputs=util.qputs;} -var HttpServer=exports.HttpServer=function HttpServer(port){this.port=port||8000;this.routes=[];};HttpServer.prototype.start=function(){var self=this;if(self.server){return;} -self.server=http.createServer(function(req,res){self.route_(req,res)});self.server.listen(self.port);qputs('Started HTTP server on port '+port+'.');return self;};HttpServer.prototype.stop=function(){if(!this.server){return;} -this.server.close();this.server=null;qputs('Shutdown HTTP server.');};HttpServer.prototype.on=function(regex,handler){this.routes.push({regex:regex,handler:handler});};HttpServer.prototype.route_=function(req,res){this.routes.forEach(function(r){if(req.url.match(r.regex)){r.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!=null){response.writeHead(404,{"Content-Type":"text/plain"});response.write("Cannot find file: "+file);response.end();return;} +return Math.pow((rnd*(h-l)-h)/-(h*l),-1/shape)-1+min;};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 START=new Date();var REPORT_SUMMARY_TEMPLATE=require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;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){var config=require('./config');var http=require('http');var fs=require('fs');var util=require('./util');var qputs=util.qputs;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;} +var HttpServer=exports.HttpServer=function HttpServer(){this.routes=[];this.running=false;};HttpServer.prototype.start=function(port,hostname){if(this.running){return;} +this.running=true;var self=this;port=port||8000;self.server=http.createServer(function(req,res){self.route_(req,res);});self.server.listen(port,hostname);qputs('Started HTTP server on port '+port+'.');return self;};HttpServer.prototype.stop=function(){if(!this.running){return;} +this.running=false;this.server.close();this.server=null;qputs('Shutdown HTTP server.');};HttpServer.prototype.addRoute=function(regex,handler){this.routes.push({regex:regex,handler:handler});return this;};HttpServer.prototype.route_=function(req,res){this.routes.forEach(function(r){if(req.url.match(r.regex)){r.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();});});};NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);}});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;}} +response.end();});});};var HTTP_SERVER=exports.HTTP_SERVER=new HttpServer();NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);}});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); diff --git a/lib/remote.js b/lib/remote.js index ba060e6..45815db 100644 --- a/lib/remote.js +++ b/lib/remote.js @@ -16,7 +16,7 @@ // nodeloadlib instance // -/** Returns a test that can be scheduled with `remoteStart(spec)` (See TEST_DEFAULTS in api.js for a list +/** Returns a test that can be scheduled with `remoteStart(spec)` (See TEST_OPTIONS in api.js for a list of the configuration values supported in the test specification */ var remoteTest = exports.remoteTest = function(spec) { return "(function() {\n" + diff --git a/test/loop.test.js b/test/loop.test.js index 23de2ab..d5c0d96 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -90,7 +90,7 @@ module.exports = { assert.ok(Math.abs(duration - 1000) < 30, '1000 == ' + duration); }); }, - 'LoopGroup emits events': function(assert, beforeExit) { + 'MultiLoop emits events': function(assert, beforeExit) { var started = false, ended = false, l = new MultiLoop({ fun: function(finished) { finished(); }, From 89aa061157377144ea9b1776d5ca4eb81e97dd78 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 14:56:59 -0500 Subject: [PATCH 13/41] single file build, nodeload.js, works --- Makefile | 2 +- lib/header.js | 5 +- lib/loadtesting.js | 3 +- lib/monitoring/monitor.js | 3 +- lib/monitoring/monitorgroup.js | 3 +- lib/nodeload.js | 116 ++++++++++++++++++--------------- lib/util.js | 3 +- 7 files changed, 72 insertions(+), 63 deletions(-) diff --git a/Makefile b/Makefile index 2f7f803..f1fa302 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/utils.js lib/config.js lib/testapi.js lib/job.js lib/monitoring.js lib/remote.js lib/stats.js lib/log.js lib/reporting.js lib/http.js lib/template.js +SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/monitor.js lib/monitoring/statslogger.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js all: compile diff --git a/lib/header.js b/lib/header.js index eb8a252..22faf4a 100644 --- a/lib/header.js +++ b/lib/header.js @@ -2,10 +2,13 @@ // Header for single file build // ----------------------------------------- -var sys = require('sys'), +var util = require('util'), http = require('http'), 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/loadtesting.js b/lib/loadtesting.js index 8a3f79d..6a2be0c 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -18,7 +18,6 @@ var reporting = require('./reporting'); var qputs = util.qputs; var qprint = util.qprint; var EventEmitter = require('events').EventEmitter; -var PeriodicUpdater = util.PeriodicUpdater; var MultiLoop = require('./loop').MultiLoop; var Monitor = require('./monitoring').Monitor; var Report = reporting.Report; @@ -167,7 +166,7 @@ LoadTest emits these events: */ var LoadTest = exports.LoadTest = function LoadTest(tests) { EventEmitter.call(this); - PeriodicUpdater.call(this); + util.PeriodicUpdater.call(this); var self = this; self.tests = tests; diff --git a/lib/monitoring/monitor.js b/lib/monitoring/monitor.js index a8cc298..a1244ca 100644 --- a/lib/monitoring/monitor.js +++ b/lib/monitoring/monitor.js @@ -4,7 +4,6 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var util = require('../util'); -var PeriodicUpdater = util.PeriodicUpdater; var StatsCollectors = require('./collectors'); var EventEmitter = require('events').EventEmitter; } @@ -30,7 +29,7 @@ allows the statistics to be introspected at regular intervals for things like lo */ var Monitor = exports.Monitor = function Monitor() { // arguments EventEmitter.call(this); - PeriodicUpdater.call(this); // adds updateInterval property and calls update() + util.PeriodicUpdater.call(this); // adds updateInterval property and calls update() this.targets = []; this.setStats.apply(this, arguments); }; diff --git a/lib/monitoring/monitorgroup.js b/lib/monitoring/monitorgroup.js index 4e4930c..51c8845 100644 --- a/lib/monitoring/monitorgroup.js +++ b/lib/monitoring/monitorgroup.js @@ -6,7 +6,6 @@ if (!BUILD_AS_SINGLE_FILE) { var util = require('../util'); var Monitor = require('./monitor').Monitor; var StatsLogger = require('./monitor').StatsLogger; -var PeriodicUpdater = util.PeriodicUpdater; var EventEmitter = require('events').EventEmitter; } @@ -18,7 +17,7 @@ also emit regular 'update' events as well as log the statistics from the interva Monitor.StatsCollectors. */ var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { EventEmitter.call(this); - PeriodicUpdater.call(this); + util.PeriodicUpdater.call(this); var summaryFun = function() { var summary = {}; diff --git a/lib/nodeload.js b/lib/nodeload.js index d8194b6..b6d9659 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -1,46 +1,15 @@ #!/usr/bin/env node -var sys=require('sys'),http=require('http'),fs=require('fs'),events=require('events'),querystring=require('querystring');var BUILD_AS_SINGLE_FILE=true;var DYGRAPH_SOURCE=exports.DYGRAPH_SOURCE="DygraphLayout=function(b,a){this.dygraph_=b;this.options={};Dygraph.update(this.options,a?a:{});this.datasets=new Array()};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.options.dateWindow){this.minxval=this.options.dateWindow[0];this.maxxval=this.options.dateWindow[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var d=this.datasets[c];var b=d[0][0];if(!this.minxval||bthis.maxxval){this.maxxval=a}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!=0?1/this.xrange:1);this.minyval=this.options.yAxis[0];this.maxyval=this.options.yAxis[1];this.yrange=this.maxyval-this.minyval;this.yscale=(this.yrange!=0?1/this.yrange:1)};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=new Array();for(var e in this.datasets){if(!this.datasets.hasOwnProperty(e)){continue}var d=this.datasets[e];for(var b=0;b=1){a.y=1}this.points.push(a)}}};DygraphLayout.prototype._evaluateLineTicks=function(){this.xticks=new Array();for(var c=0;c=0)&&(d<=1)){this.xticks.push([d,a])}}this.yticks=new Array();for(var c=0;c=0)&&(d<=1)){this.yticks.push([d,a])}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!this.options.errorBars){return}var d=0;for(var g in this.datasets){if(!this.datasets.hasOwnProperty(g)){continue}var c=0;var f=this.datasets[g];for(var c=0;c0){for(var e=0;ethis.height){k.style.bottom=\"0px\"}else{k.style.top=h+\"px\"}k.style.left=\"0px\";k.style.textAlign=\"right\";k.style.width=this.options.yAxisLabelWidth+\"px\";this.container.appendChild(k);this.ylabels.push(k)}var m=this.ylabels[0];var n=this.options.axisLabelFontSize;var a=parseInt(m.style.top)+n;if(a>this.height-n){m.style.top=(parseInt(m.style.top)-n/2)+\"px\"}}b.beginPath();b.moveTo(this.area.x,this.area.y);b.lineTo(this.area.x,this.area.y+this.area.h);b.closePath();b.stroke()}if(this.options.drawXAxis){if(this.layout.xticks){for(var e=0;ethis.width){c=this.width-this.options.xAxisLabelWidth;k.style.textAlign=\"right\"}if(c<0){c=0;k.style.textAlign=\"left\"}k.style.left=c+\"px\";k.style.width=this.options.xAxisLabelWidth+\"px\";this.container.appendChild(k);this.xlabels.push(k)}}b.beginPath();b.moveTo(this.area.x,this.area.y+this.area.h);b.lineTo(this.area.x+this.area.w,this.area.y+this.area.h);b.closePath();b.stroke()}b.restore()};DygraphCanvasRenderer.prototype._renderLineChart=function(){var b=this.element.getContext(\"2d\");var d=this.options.colorScheme.length;var n=this.options.colorScheme;var x=this.options.fillAlpha;var C=this.layout.options.errorBars;var q=this.layout.options.fillGraph;var E=[];for(var F in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(F)){E.push(F)}}var y=E.length;this.colors={};for(var A=0;A0){r=E[A-1]}var v=this.colors[g];s.save();s.strokeStyle=v;s.lineWidth=this.options.strokeWidth;var h=NaN;var f=[-1,-1];var k=0;var B=this.layout.yscale;var a=new RGBColor(v);var D=\"rgba(\"+a.r+\",\"+a.g+\",\"+a.b+\",\"+x+\")\";s.fillStyle=D;s.beginPath();for(var w=0;w1){e=1}}var p=[t.y,e];p[0]=this.area.h*p[0]+this.area.y;p[1]=this.area.h*p[1]+this.area.y;if(!isNaN(h)){s.moveTo(h,f[0]);s.lineTo(t.canvasx,p[0]);s.lineTo(t.canvasx,p[1]);s.lineTo(h,f[1]);s.closePath()}f[0]=p[0];f[1]=p[1];h=t.canvasx}}s.fill()}}}for(var A=0;A0){if(arguments.length==4){this.warn(\"Using deprecated four-argument dygraph constructor\");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME=\"Dygraph\";Dygraph.VERSION=\"1.2\";Dygraph.__repr__=function(){return\"[\"+this.NAME+\" \"+this.VERSION+\"]\"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.AXIS_LINE_WIDTH=0.3;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,pixelsPerXLabel:60,pixelsPerYLabel:30,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,yValueFormatter:function(a){return Dygraph.round_(a,2)},strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueFormatter:Dygraph.dateString_,xValueParser:Dygraph.dateParser,xTicker:Dygraph.dateTicker,delimiter:\",\",logScale:false,sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!=null){var a=[\"Date\"];for(var c=0;c=10){n.doZoom_(Math.min(b,m),Math.max(b,m))}else{n.canvas_.getContext(\"2d\").clearRect(0,0,n.canvas_.width,n.canvas_.height)}b=null;a=null}if(e){e=false;l=null;j=null}});Dygraph.addEvent(this.hidden_,\"dblclick\",function(o){if(n.dateWindow_==null){return}n.dateWindow_=null;n.drawGraph_(n.rawData_);var p=n.rawData_[0][0];var q=n.rawData_[n.rawData_.length-1][0];if(n.attr_(\"zoomCallback\")){n.attr_(\"zoomCallback\")(p,q)}})};Dygraph.prototype.drawZoomRect_=function(c,d,b){var a=this.canvas_.getContext(\"2d\");if(b){a.clearRect(Math.min(c,b),0,Math.abs(c-b),this.height_)}if(d&&c){a.fillStyle=\"rgba(128,128,128,0.33)\";a.fillRect(Math.min(c,d),0,Math.abs(d-c),this.height_)}};Dygraph.prototype.doZoom_=function(d,a){var b=this.toDataCoords(d,null);var c=b[0];b=this.toDataCoords(a,null);var e=b[0];this.dateWindow_=[c,e];this.drawGraph_(this.rawData_);if(this.attr_(\"zoomCallback\")){this.attr_(\"zoomCallback\")(c,e)}};Dygraph.prototype.mouseMove_=function(b){var a=Dygraph.pageX(b)-Dygraph.findPosX(this.hidden_);var s=this.layout_.points;var m=-1;var j=-1;var q=1e+100;var r=-1;for(var f=0;fq){continue}q=h;r=f}if(r>=0){m=s[r].xval}if(a>s[s.length-1].canvasx){m=s[s.length-1].xval}this.selPoints_=[];var g=0;var d=s.length;var o=this.attr_(\"stackedGraph\");if(!this.attr_(\"stackedGraph\")){for(var f=0;f=0;f--){if(s[f].xval==m){var c={};for(var e in s[f]){c[e]=s[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}}if(this.attr_(\"highlightCallback\")){var n=this.lastHighlightCallbackX;if(n!==null&&m!=n){this.lastHighlightCallbackX=m;this.attr_(\"highlightCallback\")(b,m,this.selPoints_)}}this.lastx_=m;this.updateSelection_()};Dygraph.prototype.updateSelection_=function(){var a=this.attr_(\"highlightCircleSize\");var n=this.canvas_.getContext(\"2d\");if(this.previousVerticalX_>=0){var l=this.previousVerticalX_;n.clearRect(l-a-1,0,2*a+2,this.height_)}var m=function(c){return c&&!isNaN(c)};if(this.selPoints_.length>0){var b=this.selPoints_[0].canvasx;var d=this.attr_(\"xValueFormatter\")(this.lastx_,this)+\":\";var e=this.attr_(\"yValueFormatter\");var j=this.colors_.length;if(this.attr_(\"showLabelsOnHighlight\")){for(var f=0;f\"}var k=this.selPoints_[f];var h=new RGBColor(this.colors_[f%j]);var g=e(k.yval);d+=\" \"+k.name+\":\"+g}this.attr_(\"labelsDiv\").innerHTML=d}n.save();for(var f=0;f=0){for(var a in this.layout_.datasets){if(b=Dygraph.DAILY){y.push({v:l,label:new Date(l+3600*1000).strftime(u)})}else{y.push({v:l,label:this.hmsString_(l)})}}}else{var f;var o=1;if(a==Dygraph.MONTHLY){f=[0,1,2,3,4,5,6,7,8,9,10,11,12]}else{if(a==Dygraph.QUARTERLY){f=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){f=[0,6]}else{if(a==Dygraph.ANNUAL){f=[0]}else{if(a==Dygraph.DECADAL){f=[0];o=10}}}}}var r=new Date(n).getFullYear();var p=new Date(k).getFullYear();var c=Dygraph.zeropad;for(var s=r;s<=p;s++){if(s%o!=0){continue}for(var q=0;qk){continue}y.push({v:l,label:new Date(l).strftime(\"%b %y\")})}}}return y};Dygraph.dateTicker=function(a,f,d){var b=-1;for(var e=0;e=d.attr_(\"pixelsPerXLabel\")){b=e;break}}if(b>=0){return d.GetXAxis(a,f,b)}else{}};Dygraph.numericTicks=function(v,u,l){if(l.attr_(\"labelsKMG2\")){var f=[1,2,4,8]}else{var f=[1,2,5]}var x,p,a,q;var h=l.attr_(\"pixelsPerYLabel\");for(var t=-10;t<50;t++){if(l.attr_(\"labelsKMG2\")){var c=Math.pow(16,t)}else{var c=Math.pow(10,t)}for(var s=0;sh){break}}if(d>h){break}}var w=[];var r;var o=[];if(l.attr_(\"labelsKMB\")){r=1000;o=[\"K\",\"M\",\"B\",\"T\"]}if(l.attr_(\"labelsKMG2\")){if(r){l.warn(\"Setting both labelsKMB and labelsKMG2. Pick one!\")}r=1024;o=[\"k\",\"M\",\"G\",\"T\"]}if(p>a){x*=-1}for(var t=0;t=0;s--,m/=r){if(b>=m){e=Dygraph.round_(g/m,1)+o[s];break}}}w.push({label:e,v:g})}return w};Dygraph.prototype.addYTicks_=function(c,b){var a=Dygraph.numericTicks(c,b,this);this.layout_.updateOptions({yAxis:[c,b],yTicks:a})};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null;var b=this.attr_(\"errorBars\")||this.attr_(\"customBars\");if(b){for(var c=0;cg){a=g}if(ef){f=e}if(h==null||af){f=g}if(h==null||g=E&&e===null){e=t}if(g[t][0]<=f){D=t}}if(e===null){e=0}if(e>0){e--}if(D===null){D=g.length-1}if(Dx){x=o}if(p){var m=[];for(var u=0;ux){x=d[g[u][0]]}}h.push([this.attr_(\"labels\")[w],m])}else{this.layout_.addDataset(this.attr_(\"labels\")[w],g)}}}if(h.length>0){for(var w=(h.length-1);w>=0;w--){this.layout_.addDataset(h[w][0],h[w][1])}}if(this.valueRange_!=null){this.addYTicks_(this.valueRange_[0],this.valueRange_[1]);this.displayedYRange_=this.valueRange_}else{if(this.attr_(\"includeZero\")&&y>0){y=0}var v=x-y;if(v==0){v=x}var c=x+0.1*v;var B=y-0.1*v;if(B<0&&y>=0){B=0}if(c>0&&x<=0){c=0}if(this.attr_(\"includeZero\")){if(x<0){c=0}if(y>0){B=0}}this.addYTicks_(B,c);this.displayedYRange_=[B,c]}this.addXTicks_();this.layout_.updateOptions({dateWindow:this.dateWindow_});this.layout_.evaluateWithError();this.plotter_.clear();this.plotter_.render();this.canvas_.getContext(\"2d\").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_(\"drawCallback\")!==null){this.attr_(\"drawCallback\")(this,n)}};Dygraph.prototype.rollingAverage=function(m,d){if(m.length<2){return m}var d=Math.min(d,m.length-1);var b=[];var s=this.attr_(\"sigma\");if(this.fractions_){var k=0;var h=0;var e=100;for(var x=0;x=0){k-=m[x-d][1][0];h-=m[x-d][1][1]}var B=m[x][0];var v=h?k/h:0;if(this.attr_(\"errorBars\")){if(this.wilsonInterval_){if(h){var t=v<0?0:v,u=h;var A=s*Math.sqrt(t*(1-t)/u+s*s/(4*u*u));var a=1+s*s/h;var F=(t+s*s/(2*h)-A)/a;var o=(t+s*s/(2*h)+A)/a;b[x]=[B,[t*e,(t-F)*e,(o-t)*e]]}else{b[x]=[B,[0,0,0]]}}else{var z=h?s*Math.sqrt(v*(1-v)/h):1;b[x]=[B,[e*v,e*z,e*z]]}}else{b[x]=[B,e*v]}}}else{if(this.attr_(\"customBars\")){var F=0;var C=0;var o=0;var g=0;for(var x=0;x=0){var r=m[x-d];if(r[1][1]!=null&&!isNaN(r[1][1])){F-=r[1][0];C-=r[1][1];o-=r[1][2];g-=1}}b[x]=[m[x][0],[1*C/g,1*(C-F)/g,1*(o-C)/g]]}}else{var q=Math.min(d-1,m.length-2);if(!this.attr_(\"errorBars\")){if(d==1){return m}for(var x=0;x=0||b.indexOf(\"/\")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>\"19700101\"&&b<\"20371231\"){a=true}}if(a){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker}else{this.attrs_.xValueFormatter=function(c){return c};this.attrs_.xValueParser=function(c){return parseFloat(c)};this.attrs_.xTicker=Dygraph.numericTicks}};Dygraph.prototype.parseCSV_=function(h){var m=[];var q=h.split(\"\\n\");var b=this.attr_(\"delimiter\");if(q[0].indexOf(b)==-1&&q[0].indexOf(\"\\t\")>=0){b=\"\\t\"}var a=0;if(this.labelsFromCSV_){a=1;this.attrs_.labels=q[0].split(b)}var c;var o=false;var d=this.attr_(\"labels\").length;var l=false;for(var g=a;g0&&k[0]0&&k[0]=0){this.loadedEvent_(this.file_)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status==200){a.loadedEvent_(b.responseText)}}};b.open(\"GET\",this.file_,true);b.send(null)}}else{this.error(\"Unknown data format: \"+(typeof this.file_))}}}}};Dygraph.prototype.updateOptions=function(a){if(a.rollPeriod){this.rollPeriod_=a.rollPeriod}if(a.dateWindow){this.dateWindow_=a.dateWindow}if(a.valueRange){this.valueRange_=a.valueRange}Dygraph.update(this.user_attrs_,a);this.labelsFromCSV_=(this.attr_(\"labels\")==null);this.layout_.updateOptions({errorBars:this.attr_(\"errorBars\")});if(a.file){this.file_=a.file;this.start_()}else{this.drawGraph_(this.rawData_)}};Dygraph.prototype.resize=function(b,a){if((b===null)!=(a===null)){this.warn(\"Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.\");b=a=null}this.maindiv_.innerHTML=\"\";this.attrs_.labelsDiv=null;if(b){this.maindiv_.style.width=b+\"px\";this.maindiv_.style.height=a+\"px\";this.width_=b;this.height_=a}else{this.width_=this.maindiv_.offsetWidth;this.height_=this.maindiv_.offsetHeight}this.createInterface_();this.drawGraph_(this.rawData_)};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.drawGraph_(this.rawData_)};Dygraph.prototype.visibility=function(){if(!this.attr_(\"visibility\")){this.attrs_.visibility=[]}while(this.attr_(\"visibility\").length=a.length){this.warn(\"invalid series number in setVisibility: \"+b)}else{a[b]=c;this.drawGraph_(this.rawData_)}};Dygraph.createCanvas=function(){var a=document.createElement(\"canvas\");isIE=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(isIE){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML=\"\";this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var c=this.date_graph.getSelection();if(c<0){return b}col=1;for(var a in this.date_graph.layout_.datasets){b.push({row:c,column:col});col++}return b};DateGraph=Dygraph;function RGBColor(g){this.ok=false;if(g.charAt(0)==\"#\"){g=g.substr(1,6)}g=g.replace(/ /g,\"\");g=g.toLowerCase();var a={aliceblue:\"f0f8ff\",antiquewhite:\"faebd7\",aqua:\"00ffff\",aquamarine:\"7fffd4\",azure:\"f0ffff\",beige:\"f5f5dc\",bisque:\"ffe4c4\",black:\"000000\",blanchedalmond:\"ffebcd\",blue:\"0000ff\",blueviolet:\"8a2be2\",brown:\"a52a2a\",burlywood:\"deb887\",cadetblue:\"5f9ea0\",chartreuse:\"7fff00\",chocolate:\"d2691e\",coral:\"ff7f50\",cornflowerblue:\"6495ed\",cornsilk:\"fff8dc\",crimson:\"dc143c\",cyan:\"00ffff\",darkblue:\"00008b\",darkcyan:\"008b8b\",darkgoldenrod:\"b8860b\",darkgray:\"a9a9a9\",darkgreen:\"006400\",darkkhaki:\"bdb76b\",darkmagenta:\"8b008b\",darkolivegreen:\"556b2f\",darkorange:\"ff8c00\",darkorchid:\"9932cc\",darkred:\"8b0000\",darksalmon:\"e9967a\",darkseagreen:\"8fbc8f\",darkslateblue:\"483d8b\",darkslategray:\"2f4f4f\",darkturquoise:\"00ced1\",darkviolet:\"9400d3\",deeppink:\"ff1493\",deepskyblue:\"00bfff\",dimgray:\"696969\",dodgerblue:\"1e90ff\",feldspar:\"d19275\",firebrick:\"b22222\",floralwhite:\"fffaf0\",forestgreen:\"228b22\",fuchsia:\"ff00ff\",gainsboro:\"dcdcdc\",ghostwhite:\"f8f8ff\",gold:\"ffd700\",goldenrod:\"daa520\",gray:\"808080\",green:\"008000\",greenyellow:\"adff2f\",honeydew:\"f0fff0\",hotpink:\"ff69b4\",indianred:\"cd5c5c\",indigo:\"4b0082\",ivory:\"fffff0\",khaki:\"f0e68c\",lavender:\"e6e6fa\",lavenderblush:\"fff0f5\",lawngreen:\"7cfc00\",lemonchiffon:\"fffacd\",lightblue:\"add8e6\",lightcoral:\"f08080\",lightcyan:\"e0ffff\",lightgoldenrodyellow:\"fafad2\",lightgrey:\"d3d3d3\",lightgreen:\"90ee90\",lightpink:\"ffb6c1\",lightsalmon:\"ffa07a\",lightseagreen:\"20b2aa\",lightskyblue:\"87cefa\",lightslateblue:\"8470ff\",lightslategray:\"778899\",lightsteelblue:\"b0c4de\",lightyellow:\"ffffe0\",lime:\"00ff00\",limegreen:\"32cd32\",linen:\"faf0e6\",magenta:\"ff00ff\",maroon:\"800000\",mediumaquamarine:\"66cdaa\",mediumblue:\"0000cd\",mediumorchid:\"ba55d3\",mediumpurple:\"9370d8\",mediumseagreen:\"3cb371\",mediumslateblue:\"7b68ee\",mediumspringgreen:\"00fa9a\",mediumturquoise:\"48d1cc\",mediumvioletred:\"c71585\",midnightblue:\"191970\",mintcream:\"f5fffa\",mistyrose:\"ffe4e1\",moccasin:\"ffe4b5\",navajowhite:\"ffdead\",navy:\"000080\",oldlace:\"fdf5e6\",olive:\"808000\",olivedrab:\"6b8e23\",orange:\"ffa500\",orangered:\"ff4500\",orchid:\"da70d6\",palegoldenrod:\"eee8aa\",palegreen:\"98fb98\",paleturquoise:\"afeeee\",palevioletred:\"d87093\",papayawhip:\"ffefd5\",peachpuff:\"ffdab9\",peru:\"cd853f\",pink:\"ffc0cb\",plum:\"dda0dd\",powderblue:\"b0e0e6\",purple:\"800080\",red:\"ff0000\",rosybrown:\"bc8f8f\",royalblue:\"4169e1\",saddlebrown:\"8b4513\",salmon:\"fa8072\",sandybrown:\"f4a460\",seagreen:\"2e8b57\",seashell:\"fff5ee\",sienna:\"a0522d\",silver:\"c0c0c0\",skyblue:\"87ceeb\",slateblue:\"6a5acd\",slategray:\"708090\",snow:\"fffafa\",springgreen:\"00ff7f\",steelblue:\"4682b4\",tan:\"d2b48c\",teal:\"008080\",thistle:\"d8bfd8\",tomato:\"ff6347\",turquoise:\"40e0d0\",violet:\"ee82ee\",violetred:\"d02090\",wheat:\"f5deb3\",white:\"ffffff\",whitesmoke:\"f5f5f5\",yellow:\"ffff00\",yellowgreen:\"9acd32\"};for(var c in a){if(g==c){g=a[c]}}var h=[{re:/^rgb\\((\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d{1,3})\\)$/,example:[\"rgb(123, 234, 45)\",\"rgb(255,234,245)\"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\\w{2})(\\w{2})(\\w{2})$/,example:[\"#00ff00\",\"336699\"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\\w{1})(\\w{1})(\\w{1})$/,example:[\"#fb0\",\"f0f\"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var b=0;b255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return\"rgb(\"+this.r+\", \"+this.g+\", \"+this.b+\")\"};this.toHex=function(){var k=this.r.toString(16);var j=this.g.toString(16);var i=this.b.toString(16);if(k.length==1){k=\"0\"+k}if(j.length==1){j=\"0\"+j}if(i.length==1){i=\"0\"+i}return\"#\"+k+j+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)==\"undefined\"){b=10}for(;parseInt(a,10)1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale=\"en-GB\";if(document.getElementsByTagName(\"html\")&&document.getElementsByTagName(\"html\")[0].lang){Date.prototype.locale=document.getElementsByTagName(\"html\")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],A:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],b:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],B:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],c:\"%a %d %b %Y %T %Z\",p:[\"AM\",\"PM\"],P:[\"am\",\"pm\"],x:\"%d/%m/%y\",X:\"%T\"};Date.ext.locales[\"en-US\"]=Date.ext.locales.en;Date.ext.locales[\"en-US\"].c=\"%a %d %b %Y %r %Z\";Date.ext.locales[\"en-US\"].x=\"%D\";Date.ext.locales[\"en-US\"].X=\"%r\";Date.ext.locales[\"en-GB\"]=Date.ext.locales.en;Date.ext.locales[\"en-AU\"]=Date.ext.locales[\"en-GB\"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:\"toLocaleString\",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:[\"getDate\",\"0\"],e:[\"getDate\",\" \"],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:[\"getHours\",\"0\"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(\"\"+c.getFullYear()+\"/1/1 GMT\");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:[\"getMinutes\",\"0\"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:[\"getSeconds\",\"0\"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(\"\"+e.getFullYear()+\"/1/1\")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(\"\"+e.getFullYear()+\"/12/31\")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(\"\"+(e.getFullYear()-1)+\"/12/31\"))}}return Date.ext.util.xPad(b,0)},w:\"getDay\",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:\"getFullYear\",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?\"-\":\"+\")+a+e},Z:function(a){return a.toString().replace(/^.*\\(([^)]+)\\)$/,\"$1\")},\"%\":function(a){return\"%\"}};Date.ext.aggregates={c:\"locale\",D:\"%m/%d/%y\",h:\"%b\",n:\"\\n\",r:\"%I:%M:%S %p\",R:\"%H:%M\",t:\"\\t\",T:\"%H:%M:%S\",x:\"locale\",X:\"locale\"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,\"\") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,\"\")}else{this.locale=\"en-GB\"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g==\"locale\"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)==\"string\"){return c[g]()}else{if(typeof(g)==\"function\"){return g.call(c,c)}else{if(typeof(g)==\"object\"&&typeof(g[0])==\"string\"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};";var REPORT_SUMMARY_TEMPLATE=exports.REPORT_SUMMARY_TEMPLATE="\n \n Test Results\n \n \n \n\n \n
\n

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

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

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

\n \n\n \n";var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var fs=require('fs');} +var template={cache_:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)){if(!callback){fn=this.create(fs.readFileSync(str).toString('utf8'));}else{fs.readFile(str,function(err,buffer){if(err){throw err;} +this.create(buffer.toString('utf8'),data,callback);});return;}}else{if(this.cache_[str]){fn=this.cache_[str];}else{fn=new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};"+"obj=obj||{};"+"with(obj){p.push('"+ +str.split("'").join("\\'").split("\n").join("\\n").replace(/<%([\s\S]*?)%>/mg,function(m,t){return'<%'+t.split("\\'").join("'").split("\\n").join("\n")+'%>';}).replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');");this.cache_[str]=fn;}} +if(callback){callback(data?fn(data):fn);} +else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);var BUILD_AS_SINGLE_FILE,NODELOAD_CONFIG;if(!BUILD_AS_SINGLE_FILE){var EventEmitter=require('events').EventEmitter;} +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=exports.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 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.PeriodicUpdater=function(updateIntervalMs){var self=this,updateTimeoutId,scheduleUpdate=function(milliseconds){clearTimeout(updateTimeoutId);if(milliseconds>0){updateTimeoutId=setTimeout(function(){scheduleUpdate(milliseconds);self.update();},milliseconds);}};this.__defineGetter__('updateInterval',function(){return updateIntervalMs;});this.__defineSetter__('updateInterval',function(milliseconds){scheduleUpdate(milliseconds);updateIntervalMs=milliseconds;});this.updateInterval=updateIntervalMs;};util.extend(exports,util);var BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var fs=require('fs');} var Histogram=exports.Histogram=function(params){this.params=params;this.size=params&¶ms.buckets||3000;this.percentiles=params&¶ms.percentiles||[0.95,0.99];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(item=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){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;};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 START=new Date();var REPORT_SUMMARY_TEMPLATE=require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;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){var config=require('./config');var http=require('http');var fs=require('fs');var util=require('./util');var qputs=util.qputs;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;} +return Math.pow((rnd*(h-l)-h)/-(h*l),-1/shape)-1+min;};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;i0){stats.clear();}});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var START=new Date();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.log.put('[');this.monitor.on('update',this.logger_);return this;};StatsLogger.prototype.stop=function(){this.log.put(']');if(this.createdLog){this.log.close();this.log=null;} +this.monitor.removeListener('update',this.logger_);return this;};StatsLogger.prototype.log_=function(interval){var summary=interval.summary();summary.ts=new Date();this.log.put(JSON.stringify(summary)+',\n');};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var Monitor=require('./monitor').Monitor;var StatsLogger=require('./monitor').StatsLogger;var EventEmitter=require('events').EventEmitter;} +var MonitorGroup=exports.MonitorGroup=function MonitorGroup(statsNames){EventEmitter.call(this);util.PeriodicUpdater.call(this);var summaryFun=function(){var summary={};util.forEach(this,function(monitorName,stats){if(monitorName==='summary'){return;} +summary[monitorName]={};util.forEach(stats,function(statName,stat){summary[monitorName][statName]=stat.summary();});});return summary;};this.statsNames=(statsNames instanceof Array)?statsNames:[].concat.apply([],arguments);this.monitors={};this.stats={summary:summaryFun};this.interval={summary:summaryFun};};util.inherits(MonitorGroup,EventEmitter);MonitorGroup.prototype.initMonitors=function(monitorNames){var self=this;monitorNames=(monitorNames instanceof Array)?monitorNames:[].concat.apply([],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 NODELOAD_CONFIG=config.NODELOAD_CONFIG;} var HttpServer=exports.HttpServer=function HttpServer(){this.routes=[];this.running=false;};HttpServer.prototype.start=function(port,hostname){if(this.running){return;} this.running=true;var self=this;port=port||8000;self.server=http.createServer(function(req,res){self.route_(req,res);});self.server.listen(port,hostname);qputs('Started HTTP server on port '+port+'.');return self;};HttpServer.prototype.stop=function(){if(!this.running){return;} this.running=false;this.server.close();this.server=null;qputs('Shutdown HTTP server.');};HttpServer.prototype.addRoute=function(regex,handler){this.routes.push({regex:regex,handler:handler});return this;};HttpServer.prototype.route_=function(req,res){this.routes.forEach(function(r){if(req.url.match(r.regex)){r.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();});});};var HTTP_SERVER=exports.HTTP_SERVER=new HttpServer();NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);}});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); +response.end();});});};var HTTP_SERVER=exports.HTTP_SERVER=new HttpServer();NODELOAD_CONFIG.on('apply',function(){if(NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);}});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 START=new Date();var REPORT_SUMMARY_TEMPLATE=require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;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 START=new Date();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 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,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} +var request=client.request(spec.method,spec.path,{'host':spec.host});request.end(spec.requestData);return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection,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);return{spec:spec,loop:loop,monitor:monitor,report:report,};});var loadtest=new LoadTest(tests).start();loadtest.updateInterval=2000;return loadtest;};var LoadTest=exports.LoadTest=function LoadTest(tests){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.tests=tests;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;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=function(){var client=http.createClient.apply(this,arguments);return extendClient(client);};function requestGeneratorLoop(generator){return function(finished,client){var running=true,timeoutId,request=generator(client);if(request){if(request.timeout>0){timeoutId=setTimeout(function(){running=false;finished({req:request,res:{statusCode:0}});},request.timeout);} +request.on('response',function(response){if(running){clearTimeout(timeoutId);finished({req:request,res:response});}});request.end();}else{finished(null);}};} diff --git a/lib/util.js b/lib/util.js index b0eb076..2728ecb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -6,10 +6,9 @@ // // Extends node.js util.js with other common functions. // -var util = require('util'); - var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { +var util = require('util'); var NODELOAD_CONFIG = require('./config').NODELOAD_CONFIG; } From bd92374f5a4b9b470ebb3ab405e443896b9e3c41 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 15:03:26 -0500 Subject: [PATCH 14/41] Fix packaging so you can require('nodeload') after npm link --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index c95b59f..255ea1e 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,12 @@ "Benjamin Schmaus ", "Jonathan Lee ", "Robert Newson ", - "Jeremy Volkman " ], "repository": { "type": "git", "url": "http://github.com/benschmaus/nodeload" }, - "main": "nodeload", + "main": "./lib/nodeload", "bin": { "nodeload": "./lib/nodeload.js", "nl": "./lib/nl.js" From 9625d8f344b008b8b176c6313de8b2d2643dd1ac Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 15:48:19 -0500 Subject: [PATCH 15/41] Enable monitor stats logging in loadtest --- Makefile | 2 +- lib/loadtesting.js | 2 ++ lib/monitoring/monitor.js | 37 +++++++++++++++++++++++++++++++++- lib/monitoring/monitorgroup.js | 20 ++++++++++++------ lib/monitoring/statslogger.js | 7 ++----- lib/nodeload.js | 19 ++++++++--------- 6 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index f1fa302..453d64d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/monitor.js lib/monitoring/statslogger.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js +SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js all: compile diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 6a2be0c..5f4503c 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -129,6 +129,8 @@ var run = exports.run = function(specs) { monitor.monitorObjects(loops, 'startiteration', 'enditeration'); }); REPORT_MANAGER.addReport(report); + monitor.name = spec.name; + monitor.setLoggingEnabled(true); return { spec: spec, diff --git a/lib/monitoring/monitor.js b/lib/monitoring/monitor.js index a1244ca..268d0d9 100644 --- a/lib/monitoring/monitor.js +++ b/lib/monitoring/monitor.js @@ -5,6 +5,7 @@ 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; } @@ -38,7 +39,16 @@ util.inherits(Monitor, EventEmitter); /** Set the statistics this monitor should gather. */ Monitor.prototype.setStats = function(stats) { // arguments contains stats names - var self = this; + 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 = {}; @@ -64,6 +74,15 @@ Monitor.prototype.setStats = function(stats) { // arguments contains stats names 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 @@ -116,6 +135,22 @@ Monitor.prototype.monitorObjects = function(objs, startEvent, endEvent) { 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); diff --git a/lib/monitoring/monitorgroup.js b/lib/monitoring/monitorgroup.js index 51c8845..22783ee 100644 --- a/lib/monitoring/monitorgroup.js +++ b/lib/monitoring/monitorgroup.js @@ -5,7 +5,7 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var util = require('../util'); var Monitor = require('./monitor').Monitor; -var StatsLogger = require('./monitor').StatsLogger; +var StatsLogger = require('./statslogger').StatsLogger; var EventEmitter = require('events').EventEmitter; } @@ -19,10 +19,9 @@ var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { EventEmitter.call(this); util.PeriodicUpdater.call(this); - var summaryFun = function() { - var summary = {}; + var summarizeStats = function() { + var summary = {ts: new Date()}; util.forEach(this, function(monitorName, stats) { - if (monitorName === 'summary') { return; } summary[monitorName] = {}; util.forEach(stats, function(statName, stat) { summary[monitorName][statName] = stat.summary(); @@ -33,8 +32,17 @@ var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { this.statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); this.monitors = {}; - this.stats = { summary: summaryFun }; - this.interval = { summary: summaryFun }; + 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); diff --git a/lib/monitoring/statslogger.js b/lib/monitoring/statslogger.js index dce1655..13794c7 100644 --- a/lib/monitoring/statslogger.js +++ b/lib/monitoring/statslogger.js @@ -16,12 +16,10 @@ var StatsLogger = exports.StatsLogger = function StatsLogger(monitor, logNameOrO StatsLogger.prototype.start = function() { this.createdLog = (typeof this.logNameOrObject === 'string'); this.log = this.createdLog ? new LogFile(this.logNameOrObject) : this.logNameOrObject; - this.log.put('['); this.monitor.on('update', this.logger_); return this; }; StatsLogger.prototype.stop = function() { - this.log.put(']'); if (this.createdLog) { this.log.close(); this.log = null; @@ -29,8 +27,7 @@ StatsLogger.prototype.stop = function() { this.monitor.removeListener('update', this.logger_); return this; }; -StatsLogger.prototype.log_ = function(interval) { - var summary = interval.summary(); - summary.ts = new Date(); +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/lib/nodeload.js b/lib/nodeload.js index b6d9659..f916a76 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -59,17 +59,18 @@ this.concurrency=concurrency;this.rps=rps;if(timeout0){stats.clear();}});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var START=new Date();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.log.put('[');this.monitor.on('update',this.logger_);return this;};StatsLogger.prototype.stop=function(){this.log.put(']');if(this.createdLog){this.log.close();this.log=null;} -this.monitor.removeListener('update',this.logger_);return this;};StatsLogger.prototype.log_=function(interval){var summary=interval.summary();summary.ts=new Date();this.log.put(JSON.stringify(summary)+',\n');};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var Monitor=require('./monitor').Monitor;var StatsLogger=require('./monitor').StatsLogger;var EventEmitter=require('events').EventEmitter;} -var MonitorGroup=exports.MonitorGroup=function MonitorGroup(statsNames){EventEmitter.call(this);util.PeriodicUpdater.call(this);var summaryFun=function(){var summary={};util.forEach(this,function(monitorName,stats){if(monitorName==='summary'){return;} -summary[monitorName]={};util.forEach(stats,function(statName,stat){summary[monitorName][statName]=stat.summary();});});return summary;};this.statsNames=(statsNames instanceof Array)?statsNames:[].concat.apply([],arguments);this.monitors={};this.stats={summary:summaryFun};this.interval={summary:summaryFun};};util.inherits(MonitorGroup,EventEmitter);MonitorGroup.prototype.initMonitors=function(monitorNames){var self=this;monitorNames=(monitorNames instanceof Array)?monitorNames:[].concat.apply([],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]);} +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:[].concat.apply([],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:[].concat.apply([],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 NODELOAD_CONFIG=config.NODELOAD_CONFIG;} @@ -84,7 +85,7 @@ row[col]=val;});self.rows.push(row);}};var ReportGroup=exports.ReportGroup=funct 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 START=new Date();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 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,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} -var request=client.request(spec.method,spec.path,{'host':spec.host});request.end(spec.requestData);return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection,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);return{spec:spec,loop:loop,monitor:monitor,report:report,};});var loadtest=new LoadTest(tests).start();loadtest.updateInterval=2000;return loadtest;};var LoadTest=exports.LoadTest=function LoadTest(tests){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.tests=tests;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);} +var request=client.request(spec.method,spec.path,{'host':spec.host});request.end(spec.requestData);return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection,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(true);return{spec:spec,loop:loop,monitor:monitor,report:report,};});var loadtest=new LoadTest(tests).start();loadtest.updateInterval=2000;return loadtest;};var LoadTest=exports.LoadTest=function LoadTest(tests){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.tests=tests;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;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=function(){var client=http.createClient.apply(this,arguments);return extendClient(client);};function requestGeneratorLoop(generator){return function(finished,client){var running=true,timeoutId,request=generator(client);if(request){if(request.timeout>0){timeoutId=setTimeout(function(){running=false;finished({req:request,res:{statusCode:0}});},request.timeout);} From 1ee95bdb878586904bdedb3d9ab29e8a847aaff6 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 16:23:21 -0500 Subject: [PATCH 16/41] Update comments --- examples/loadtesting.test.js | 5 ++--- lib/config.js | 4 ++-- lib/http.js | 6 ++++-- lib/loadtesting.js | 21 +++++++++++++++------ lib/loop/multiloop.js | 10 +++++----- lib/monitoring/collectors.js | 3 +++ lib/monitoring/monitor.js | 3 ++- lib/nodeload.js | 6 +++--- lib/util.js | 2 +- 9 files changed, 37 insertions(+), 23 deletions(-) diff --git a/examples/loadtesting.test.js b/examples/loadtesting.test.js index 469e9c8..6b5dd2f 100755 --- a/examples/loadtesting.test.js +++ b/examples/loadtesting.test.js @@ -3,7 +3,7 @@ /*jslint sub:true */ var http = require('http'), - nl = require('../lib/loadtesting'); + nl = require('../lib/nodeload'); var svr = http.createServer(function (req, res) { res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); @@ -54,10 +54,9 @@ var i = 0, }, loadtest = nl.run(readtest, writetest); -loadtest.udpateInterval = 2000; +loadtest.updateInterval = 1000; loadtest.on('end', function() { loadtest = nl.run(cleanup); - loadtest.keepAlive = true; loadtest.on('end', function() { console.log('Closing test server.'); svr.close(); diff --git a/lib/config.js b/lib/config.js index 976a6ba..17f8e14 100644 --- a/lib/config.js +++ b/lib/config.js @@ -5,10 +5,10 @@ // 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, if using individual modules: +// Or, when using individual modules: // // var nlconfig = require('./lib/config').quiet().usePort(10000); // var reporting = require('./lib/reporting'); diff --git a/lib/http.js b/lib/http.js index c9cac59..af17269 100644 --- a/lib/http.js +++ b/lib/http.js @@ -2,9 +2,11 @@ // HTTP Server // ------------------------------------ // -// This file defines the singleton HTTP_SERVER. +// This file defines HttpServer and the singleton HTTP_SERVER. // -// This file defines and starts the nodeload 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) { diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 5f4503c..374697e 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -2,10 +2,11 @@ // Main HTTP load testing interface // ------------------------------------ // -// This file defines run() and traceableRequest(). +// This file defines run(), LoadTest, createClient() and extendClient(). // -// This file defines the public API for using nodeload to construct load tests. Nodeload modules, -// such as monitoring.js and reporting.js can also be used independently. +// 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; @@ -29,7 +30,8 @@ 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, a test will GET localhost:8080/ as fast as possible with 10 users for 2 minutes. */ +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 @@ -92,6 +94,8 @@ var TEST_OPTIONS = { // - for 'uniques', traceableRequest() must be used // to create the ClientRequest or only 2 will be detected. }; +var DEFAULT_UPDATE_INTERVAL_MS = 2000; // Number of milliseconds between 'update' events generated + // by a LoadTest instance returned by run(). var LoadTest, createClient, requestGeneratorLoop; @@ -141,7 +145,6 @@ var run = exports.run = function(specs) { }); var loadtest = new LoadTest(tests).start(); - loadtest.updateInterval = 2000; return loadtest; }; @@ -150,7 +153,7 @@ started. Stopping LoadTest will shut HTTP_SERVER down. The expectation is that o is normally running at a time, and when the test finishes, you usually want to let the process end, which requires stopping HTTP_SERVER. Use start(keepAlive=true) to not shut down HTTP_SERVER when done. -LoadTest contains: +LoadTest contains 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 @@ -159,6 +162,8 @@ LoadTest contains: report: a Report which is tracked by REPORT_MANAGER holding a chart for every stat in monitor - interval: statistics from this current interval - 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: @@ -172,6 +177,7 @@ var LoadTest = exports.LoadTest = function LoadTest(tests) { var self = this; self.tests = tests; + self.updateInterval = DEFAULT_UPDATE_INTERVAL_MS; self.interval = {}; self.stats = {}; self.tests.forEach(function(test) { @@ -183,6 +189,7 @@ var LoadTest = exports.LoadTest = function LoadTest(tests) { 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; @@ -201,6 +208,7 @@ LoadTest.prototype.start = function(keepAlive) { return self; }; +/** Force the load test to stop. */ LoadTest.prototype.stop = function() { this.tests.forEach(function(t) { t.loop.stop(); }); return this; @@ -216,6 +224,7 @@ 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) { diff --git a/lib/loop/multiloop.js b/lib/loop/multiloop.js index 44c3545..0e5d277 100644 --- a/lib/loop/multiloop.js +++ b/lib/loop/multiloop.js @@ -62,10 +62,10 @@ MultiLoop.prototype.stop = function() { 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) { - // 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. if (!profile || profile.length === 0) { return 0; } if (time < 0) { return profile[0][0]; } @@ -82,9 +82,9 @@ MultiLoop.prototype.getProfileValue_ = function(profile, time) { 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) { - // 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. if (time < 0) { return -time; } var MIN_TIMEOUT = 1000, lastval = [0,0]; diff --git a/lib/monitoring/collectors.js b/lib/monitoring/collectors.js index 31c5968..a94b76e 100644 --- a/lib/monitoring/collectors.js +++ b/lib/monitoring/collectors.js @@ -87,6 +87,9 @@ StatsCollectors['uniques'] = function UniquesCollector() { }; 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(); diff --git a/lib/monitoring/monitor.js b/lib/monitoring/monitor.js index 268d0d9..5d587d0 100644 --- a/lib/monitoring/monitor.js +++ b/lib/monitoring/monitor.js @@ -24,7 +24,8 @@ var EventEmitter = require('events').EventEmitter; 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. +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. */ diff --git a/lib/nodeload.js b/lib/nodeload.js index f916a76..1501ef6 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -84,9 +84,9 @@ return this.charts[name];},updateFromMonitor:function(monitor){monitor.on('updat 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 START=new Date();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 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,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} -var request=client.request(spec.method,spec.path,{'host':spec.host});request.end(spec.requestData);return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection,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(true);return{spec:spec,loop:loop,monitor:monitor,report:report,};});var loadtest=new LoadTest(tests).start();loadtest.updateInterval=2000;return loadtest;};var LoadTest=exports.LoadTest=function LoadTest(tests){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.tests=tests;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);} +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 DEFAULT_UPDATE_INTERVAL_MS=2000;var LoadTest,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} +var request=client.request(spec.method,spec.path,{'host':spec.host});request.end(spec.requestData);return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection,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(true);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=DEFAULT_UPDATE_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;qputs('Done.');if(!this.keepAlive){HTTP_SERVER.stop();} +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=function(){var client=http.createClient.apply(this,arguments);return extendClient(client);};function requestGeneratorLoop(generator){return function(finished,client){var running=true,timeoutId,request=generator(client);if(request){if(request.timeout>0){timeoutId=setTimeout(function(){running=false;finished({req:request,res:{statusCode:0}});},request.timeout);} request.on('response',function(response){if(running){clearTimeout(timeoutId);finished({req:request,res:response});}});request.end();}else{finished(null);}};} diff --git a/lib/util.js b/lib/util.js index 2728ecb..d03bc5f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -2,7 +2,7 @@ // Statistics Manager // ------------------------------------ // -// This file defines qputs, qprint, and Utils. +// This file defines qputs, qprint, and extends the util namespace. // // Extends node.js util.js with other common functions. // From 971df75b033c7c816bd69e246927bbc68e6e3ea9 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 16:53:43 -0500 Subject: [PATCH 17/41] Use NODELOAD_CONFIG.MONITOR_INTERVAL_MS as default update interval for LoadTest --- lib/config.js | 2 +- lib/loadtesting.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/config.js b/lib/config.js index 17f8e14..a294281 100644 --- a/lib/config.js +++ b/lib/config.js @@ -36,7 +36,7 @@ exports.disableServer = function() { 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; diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 374697e..d605b11 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -94,8 +94,6 @@ var TEST_OPTIONS = { // - for 'uniques', traceableRequest() must be used // to create the ClientRequest or only 2 will be detected. }; -var DEFAULT_UPDATE_INTERVAL_MS = 2000; // Number of milliseconds between 'update' events generated - // by a LoadTest instance returned by run(). var LoadTest, createClient, requestGeneratorLoop; @@ -177,7 +175,7 @@ var LoadTest = exports.LoadTest = function LoadTest(tests) { var self = this; self.tests = tests; - self.updateInterval = DEFAULT_UPDATE_INTERVAL_MS; + self.updateInterval = NODELOAD_CONFIG.MONITOR_INTERVAL_MS; self.interval = {}; self.stats = {}; self.tests.forEach(function(test) { From 20cde61e10ad14d8d2d17f53dfb055d95e44cb31 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 18:12:06 -0500 Subject: [PATCH 18/41] update nl.js to use new nodeload Add http client error handling when requestGenerator or method+path+data is used add expresso as dev dependency for npm --- examples/test-server.js | 5 +- lib/loadtesting.js | 24 +++- lib/nodeload.js | 9 +- lib/options.js | 26 ++-- lib/optparse.js | 309 ++++++++++++++++++++++++++++++++++++++++ lib/nl.js => nl.js | 90 ++++++------ package.json | 7 +- 7 files changed, 402 insertions(+), 68 deletions(-) mode change 100644 => 100755 examples/test-server.js create mode 100644 lib/optparse.js rename lib/nl.js => nl.js (55%) 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/lib/loadtesting.js b/lib/loadtesting.js index d605b11..20e2d3f 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -112,7 +112,9 @@ var run = exports.run = function(specs) { generateRequest = function(client) { if (spec.requestGenerator) { return spec.requestGenerator(client); } var request = client.request(spec.method, spec.path, { 'host': spec.host }); - request.end(spec.requestData); + if (spec.requestData) { + request.write(spec.requestData); + } return request; }, loop = new MultiLoop({ @@ -284,18 +286,26 @@ var createClient = function() { 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}); + } + }; + client.on('error', function(err) { + util.debug(err + '. ' + err.stack); + callFinished(new EventEmitter()); + }); if (request) { if (request.timeout > 0) { timeoutId = setTimeout(function() { - running = false; - finished({req: request, res: {statusCode: 0}}); + callFinished(new EventEmitter()); }, request.timeout); } request.on('response', function(response) { - if (running) { - clearTimeout(timeoutId); - finished({req: request, res: response}); - } + callFinished(response); }); request.end(); } else { diff --git a/lib/nodeload.js b/lib/nodeload.js index 1501ef6..75a3c15 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -84,9 +84,10 @@ return this.charts[name];},updateFromMonitor:function(monitor){monitor.on('updat 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 START=new Date();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 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 DEFAULT_UPDATE_INTERVAL_MS=2000;var LoadTest,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} -var request=client.request(spec.method,spec.path,{'host':spec.host});request.end(spec.requestData);return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection,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(true);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=DEFAULT_UPDATE_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);} +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,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},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,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(true);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=function(){var client=http.createClient.apply(this,arguments);return extendClient(client);};function requestGeneratorLoop(generator){return function(finished,client){var running=true,timeoutId,request=generator(client);if(request){if(request.timeout>0){timeoutId=setTimeout(function(){running=false;finished({req:request,res:{statusCode:0}});},request.timeout);} -request.on('response',function(response){if(running){clearTimeout(timeoutId);finished({req:request,res:response});}});request.end();}else{finished(null);}};} +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=function(){var client=http.createClient.apply(this,arguments);return extendClient(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});}};client.on('error',function(err){util.debug(err+'. '+err.stack);callFinished(new EventEmitter());});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);}};} diff --git a/lib/options.js b/lib/options.js index 368a82b..1a9f979 100644 --- a/lib/options.js +++ b/lib/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/optparse.js b/lib/optparse.js new file mode 100644 index 0000000..37e3ee8 --- /dev/null +++ b/lib/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/nl.js b/nl.js similarity index 55% rename from lib/nl.js rename to nl.js index e939f25..54f9745 100755 --- a/lib/nl.js +++ b/nl.js @@ -25,70 +25,78 @@ OTHER DEALINGS IN THE SOFTWARE. */ -var options = require('./options'); +/*jslint sub:true */ + +var options = require('./lib/options'); options.process(); -if (!options.get('url')) +if (!options.get('url')) { options.help(); +} -var nl = require('./nodeload') +var nl = require('./lib/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'] }); -nl.TEST_MONITOR.on('end', function(tests) { - var stats = tests[0].stats; +test.on('start', function(tests) { testStart = new Date(); }); +test.on('update', function(interval, stats) { + puts(pad('Completed ' +stats[host]['result-codes'].length+ ' requests', 40)); +}); +test.on('end', function() { + + 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/package.json b/package.json index 255ea1e..1a2d053 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ }, "main": "./lib/nodeload", "bin": { - "nodeload": "./lib/nodeload.js", - "nl": "./lib/nl.js" + "nodeload.js": "./lib/nodeload.js", + "nl.js": "./nl.js" }, "modules": { "loop": "./lib/loop", @@ -32,6 +32,9 @@ "test": "expresso", "preinstall": "make clean compile" }, + "devDependencies": { + "expresso": ">=0.6.4" + }, "dependencies": { } } \ No newline at end of file From 15db7a9518172dd124f0efe79ad18b937784df7f Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 18:36:48 -0500 Subject: [PATCH 19/41] Move START into config Don't write html page if logs disabled globally --- lib/config.js | 2 ++ lib/loadtesting.js | 4 ++-- lib/monitoring/statslogger.js | 2 +- lib/nodeload.js | 10 +++++----- lib/reporting.js | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/config.js b/lib/config.js index a294281..d3d5839 100644 --- a/lib/config.js +++ b/lib/config.js @@ -64,6 +64,8 @@ exports.setSlavePingIntervalMs = function(milliseconds) { // ================= var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { + START: new Date(), + QUIET: false, HTTP_ENABLED: true, diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 20e2d3f..383d932 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -11,7 +11,6 @@ /*jslint laxbreak: true */ var BUILD_AS_SINGLE_FILE; if (BUILD_AS_SINGLE_FILE === undefined) { -var START = new Date(); var http = require('http'); var util = require('./util'); var stats = require('./stats'); @@ -25,6 +24,7 @@ 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; } @@ -134,7 +134,7 @@ var run = exports.run = function(specs) { }); REPORT_MANAGER.addReport(report); monitor.name = spec.name; - monitor.setLoggingEnabled(true); + monitor.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); return { spec: spec, diff --git a/lib/monitoring/statslogger.js b/lib/monitoring/statslogger.js index 13794c7..4381925 100644 --- a/lib/monitoring/statslogger.js +++ b/lib/monitoring/statslogger.js @@ -3,7 +3,7 @@ // ----------------- var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { -var START = new Date(); +var START = require('../config').NODELOAD_CONFIG.START; var LogFile = require('../stats').LogFile; } diff --git a/lib/nodeload.js b/lib/nodeload.js index 75a3c15..22d3cee 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -6,7 +6,7 @@ this.create(buffer.toString('utf8'),data,callback);});return;}}else{if(this.cach str.split("'").join("\\'").split("\n").join("\\n").replace(/<%([\s\S]*?)%>/mg,function(m,t){return'<%'+t.split("\\'").join("'").split("\\n").join("\n")+'%>';}).replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');");this.cache_[str]=fn;}} if(callback){callback(data?fn(data):fn);} else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);var BUILD_AS_SINGLE_FILE,NODELOAD_CONFIG;if(!BUILD_AS_SINGLE_FILE){var EventEmitter=require('events').EventEmitter;} -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=exports.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 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;} +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=exports.NODELOAD_CONFIG={START:new Date(),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 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.PeriodicUpdater=function(updateIntervalMs){var self=this,updateTimeoutId,scheduleUpdate=function(milliseconds){clearTimeout(updateTimeoutId);if(milliseconds>0){updateTimeoutId=setTimeout(function(){scheduleUpdate(milliseconds);self.update();},milliseconds);}};this.__defineGetter__('updateInterval',function(){return updateIntervalMs;});this.__defineSetter__('updateInterval',function(milliseconds){scheduleUpdate(milliseconds);updateIntervalMs=milliseconds;});this.updateInterval=updateIntervalMs;};util.extend(exports,util);var BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var fs=require('fs');} @@ -59,7 +59,7 @@ this.concurrency=concurrency;this.rps=rps;if(timeout0){timeoutId=setTimeout(function(){callFinished(new EventEmitter());},request.timeout);} diff --git a/lib/reporting.js b/lib/reporting.js index 466b802..0e0f1ea 100644 --- a/lib/reporting.js +++ b/lib/reporting.js @@ -16,9 +16,9 @@ var LogFile = require('./stats').LogFile; var template = require('./template'); var config = require('./config'); -var START = new Date(); 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; } From 80eef7c22a44de701df4df06e188aa23bbd7b65a Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 18:43:47 -0500 Subject: [PATCH 20/41] Remove dependency on google.com for monitoring tests --- test/monitoring.test.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/test/monitoring.test.js b/test/monitoring.test.js index 321dfae..e175c3f 100644 --- a/test/monitoring.test.js +++ b/test/monitoring.test.js @@ -7,6 +7,13 @@ var http = require('http'), 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) { @@ -127,15 +134,11 @@ module.exports = { 'HTTP specific monitors': function(assert, beforeExit) { var q = 0, m = new Monitor('result-codes', 'uniques', 'request-bytes', 'response-bytes'), - client = http.createClient(80, 'www.google.com'), + client = http.createClient(9000, 'localhost'), f = function() { var ctx = m.start(), path = '/search?q=' + q++, - req = client.request( - 'GET', - path, - {'host': 'www.google.com'} - ); + req = client.request('GET', path, {'host': 'localhost'}); req.path = path; req.end(); req.on('response', function(res) { @@ -143,8 +146,6 @@ module.exports = { }); }; - client.on('error', function(e) { assert.fail('This test requires internet connectivity: ' + e); }); - for (var i = 0; i < 2; i++) { f(); } @@ -169,7 +170,7 @@ module.exports = { assert.ok(requestBytesSummary.total > 0); - assert.ok(responseBytesSummary.total > 1000); + assert.ok(responseBytesSummary.total > 20); }); }, 'monitor generates update events with interval and overall stats': function(assert, beforeExit) { From 0751e995de472ec4a467a1526a4d30848141eea3 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Fri, 19 Nov 2010 18:46:00 -0500 Subject: [PATCH 21/41] Update todo PeriodicUpdater uses setInterval --- TODO | 9 +++++---- lib/loadtesting.js | 5 +++++ lib/nodeload.js | 3 ++- lib/util.js | 19 +++++-------------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index acada5c..5e6e8ba 100644 --- a/TODO +++ b/TODO @@ -1,5 +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 @@ -7,5 +5,8 @@ - Use git submodules - Console webpage (stats) - Console webpage (node manager) -- Unit testing with a framework -- Write a DEVELOPERS doc that explains the components \ No newline at end of file +- Write sample reporting app that monitors cpu, mem, disk io read + write + wait +- Better handling for failed http connections (net.Stream.connect currently calls destroy if there's an error) +- Update READMEs +- Write a DEVELOPERS doc that explains the components +- Add an error log diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 383d932..87e44ec 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -296,6 +296,11 @@ function requestGeneratorLoop(generator) { }; client.on('error', function(err) { util.debug(err + '. ' + err.stack); + // Setting the following gets client to call _reconnect, but it does so unsuccessfully. + // When the connection becomes available, the next call to client.request() neither emits + // 'response' nor 'error', so we never get to call callFinished(). + // client.readable = client.writable = false; + // client._connecting = false; callFinished(new EventEmitter()); }); if (request) { diff --git a/lib/nodeload.js b/lib/nodeload.js index 22d3cee..fc026e4 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -9,7 +9,8 @@ else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);v 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=exports.NODELOAD_CONFIG={START:new Date(),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 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.PeriodicUpdater=function(updateIntervalMs){var self=this,updateTimeoutId,scheduleUpdate=function(milliseconds){clearTimeout(updateTimeoutId);if(milliseconds>0){updateTimeoutId=setTimeout(function(){scheduleUpdate(milliseconds);self.update();},milliseconds);}};this.__defineGetter__('updateInterval',function(){return updateIntervalMs;});this.__defineSetter__('updateInterval',function(milliseconds){scheduleUpdate(milliseconds);updateIntervalMs=milliseconds;});this.updateInterval=updateIntervalMs;};util.extend(exports,util);var BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var fs=require('fs');} +return obj;};util.forEach=function(obj,f){for(var i in obj){if(obj.hasOwnProperty(i)){f(i,obj[i]);}}};util.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.PeriodicUpdater=function(updateIntervalMs){var self=this,updateTimeoutId;this.__defineGetter__('updateInterval',function(){return updateIntervalMs;});this.__defineSetter__('updateInterval',function(milliseconds){clearInterval(updateTimeoutId);if(milliseconds>0){updateTimeoutId=setInterval(self.update.bind(self),milliseconds);} +updateIntervalMs=milliseconds;});this.updateInterval=updateIntervalMs;};util.extend(exports,util);var BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var fs=require('fs');} var Histogram=exports.Histogram=function(params){this.params=params;this.size=params&¶ms.buckets||3000;this.percentiles=params&¶ms.percentiles||[0.95,0.99];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(item 0) { - updateTimeoutId = setTimeout( - function() { - scheduleUpdate(milliseconds); - self.update(); - }, - milliseconds); - } - }; - + var self = this, updateTimeoutId; this.__defineGetter__('updateInterval', function() { return updateIntervalMs; }); this.__defineSetter__('updateInterval', function(milliseconds) { - scheduleUpdate(milliseconds); + clearInterval(updateTimeoutId); + if (milliseconds > 0) { + updateTimeoutId = setInterval(self.update.bind(self), milliseconds); + } updateIntervalMs = milliseconds; }); this.updateInterval = updateIntervalMs; From b356deba2f6b79e03537ed336927c2950fd04e30 Mon Sep 17 00:00:00 2001 From: Mike Mattozzi Date: Mon, 22 Nov 2010 16:37:15 -0500 Subject: [PATCH 22/41] Adding slow-responses collector, adding logResBody option to http-errors --- lib/monitoring/collectors.js | 59 ++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/lib/monitoring/collectors.js b/lib/monitoring/collectors.js index a94b76e..4a609d9 100644 --- a/lib/monitoring/collectors.js +++ b/lib/monitoring/collectors.js @@ -94,7 +94,8 @@ 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.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); @@ -103,8 +104,7 @@ StatsCollectors['http-errors'] = function HttpErrorsCollector(params) { var body = ''; http.res.on('data', function(chunk) { body += chunk; }); http.res.on('end', function(chunk) { - self.logfile.put(JSON.stringify({ - ts: new Date(), + 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 @@ -115,13 +115,60 @@ StatsCollectors['http-errors'] = function HttpErrorsCollector(params) { }, res: { statusCode: http.res.statusCode, - headers: http.res.headers, - body: body + headers: http.res.headers } - }) + '\n'); + }; + 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) { + var body = ''; + http.res.on('data', function(chunk) { body += chunk; }); + http.res.on('end', function(chunk) { + 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 + } + }; + 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 From 780b3e7ab7572a1e88cf6d807b43179f6c7136fd Mon Sep 17 00:00:00 2001 From: Mike Mattozzi Date: Mon, 22 Nov 2010 17:42:40 -0500 Subject: [PATCH 23/41] Adding latency to slow-responses logging --- lib/monitoring/collectors.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/monitoring/collectors.js b/lib/monitoring/collectors.js index 4a609d9..c1148ce 100644 --- a/lib/monitoring/collectors.js +++ b/lib/monitoring/collectors.js @@ -160,7 +160,8 @@ StatsCollectors['slow-responses'] = function HttpErrorsCollector(params) { res: { statusCode: http.res.statusCode, headers: http.res.headers - } + }, + latency: runTime }; if (self.logResBody) { logObj.res.body = body; From 7b457b5ea8c3f9ca1b28b790d0ab70bf42c492c5 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Wed, 24 Nov 2010 17:36:56 -0500 Subject: [PATCH 24/41] Reimplementation of remote.js Understand environmental variables: QUIET, LOGS HttpServer emits 'start' and 'end' add util.readStream --- Makefile | 2 +- TODO | 1 + examples/remote.test.js | 57 ++++++ lib/config.js | 10 +- lib/header.js | 1 + lib/http.js | 30 +++- lib/monitoring/collectors.js | 13 +- lib/nodeload.js | 44 +++-- lib/remote.js | 339 ----------------------------------- lib/remote/cluster.js | 112 ++++++++++++ lib/remote/endpoint.js | 229 +++++++++++++++++++++++ lib/remote/http.js | 46 +++++ lib/remote/index.js | 9 + lib/remote/slave.js | 118 ++++++++++++ lib/remote/slavenode.js | 95 ++++++++++ lib/util.js | 14 +- 16 files changed, 747 insertions(+), 373 deletions(-) create mode 100755 examples/remote.test.js delete mode 100644 lib/remote.js create mode 100644 lib/remote/cluster.js create mode 100644 lib/remote/endpoint.js create mode 100644 lib/remote/http.js create mode 100644 lib/remote/index.js create mode 100644 lib/remote/slave.js create mode 100644 lib/remote/slavenode.js diff --git a/Makefile b/Makefile index 453d64d..c7fffbd 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js +SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/slave.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/http.js all: compile diff --git a/TODO b/TODO index 5e6e8ba..c9d3f51 100644 --- a/TODO +++ b/TODO @@ -10,3 +10,4 @@ - Update READMEs - Write a DEVELOPERS doc that explains the components - Add an error log +- Add zipf number generator \ No newline at end of file diff --git a/examples/remote.test.js b/examples/remote.test.js new file mode 100755 index 0000000..2397af2 --- /dev/null +++ b/examples/remote.test.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +var http = require('http'), + remote = require('../lib/remote'), + HTTP_SERVER = require('../lib/http').HTTP_SERVER, + Cluster = remote.Cluster; + +var cluster = new Cluster({ + master: { + sendOutput: function(slaves, slaveId, output) { + console.log('-------------' + slaveId + '-------------\n' + output + '--------------------------'); + } + }, + slaves: { + hosts: ['localhost:8001'], + setup: function(master) { + this.exec = require("child_process").exec; + }, + exec: function(master, cmd) { + var self = this; + self.state = 'running'; + self.child = self.exec(cmd, function(error, stdout) { + if (error === null) { + master.sendOutput(stdout.toString()); + self.state = 'done'; + } else { + self.state = 'error'; + } + }); + } + } +}); + +HTTP_SERVER.on('start', function() { + cluster.on('end', function(slaves) { + console.log('All slaves finished: ' + + JSON.stringify( + slaves.map(function(s) { + return s.id + ': ' + s.result; + }))); + }); + cluster.on('start', function() { + cluster.exec('ls -alh && sleep 3'); + }); + cluster.on('running', function() { + console.log('All slaves running'); + }); + cluster.on('done', function() { + console.log('All slaves done'); + }); + cluster.on('slaveState', function(slave, state) { + if (state === 'error') { + console.log('Slave "' + slave.id + '" encountered an error.'); + } + }); + cluster.start(); +}); \ No newline at end of file diff --git a/lib/config.js b/lib/config.js index d3d5839..8f2bec5 100644 --- a/lib/config.js +++ b/lib/config.js @@ -55,8 +55,8 @@ exports.disableLogs = function() { }; /** 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; }; // ================= @@ -66,7 +66,7 @@ exports.setSlavePingIntervalMs = function(milliseconds) { var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { START: new Date(), - QUIET: false, + QUIET: Boolean(process.env.QUIET) || false, HTTP_ENABLED: true, HTTP_PORT: Number(process.env.HTTP_PORT) || 8000, @@ -75,9 +75,9 @@ var NODELOAD_CONFIG = exports.NODELOAD_CONFIG = { 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 EventEmitter(), on: function(event, fun) { diff --git a/lib/header.js b/lib/header.js index 22faf4a..ff94eae 100644 --- a/lib/header.js +++ b/lib/header.js @@ -4,6 +4,7 @@ var util = require('util'), http = require('http'), + url = require('url'), fs = require('fs'), events = require('events'), querystring = require('querystring'); diff --git a/lib/http.js b/lib/http.js index af17269..686fb42 100644 --- a/lib/http.js +++ b/lib/http.js @@ -15,6 +15,7 @@ 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; } @@ -24,6 +25,7 @@ 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; } @@ -31,9 +33,11 @@ HttpServer.prototype.start = function(port, hostname) { var self = this; port = port || 8000; + self.hostname = hostname || util.localHostname(); + self.port = port; self.server = http.createServer(function(req, res) { self.route_(req, res); }); self.server.listen(port, hostname); - qputs('Started HTTP server on port ' + port + '.'); + self.emit('start', self.hostname, self.port); return self; }; /** Terminate the server */ @@ -42,21 +46,27 @@ HttpServer.prototype.stop = function() { this.running = false; this.server.close(); this.server = null; - qputs('Shutdown HTTP server.'); + 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.push({regex: regex, handler: 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) { - this.routes.forEach(function(r) { - if (req.url.match(r.regex)) { - r.handler(req.url, 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 { @@ -94,6 +104,12 @@ HttpServer.prototype.serveFile_ = function(file, response) { 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/monitoring/collectors.js b/lib/monitoring/collectors.js index 4a609d9..cec7f12 100644 --- a/lib/monitoring/collectors.js +++ b/lib/monitoring/collectors.js @@ -12,6 +12,7 @@ /*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; @@ -101,15 +102,9 @@ StatsCollectors['http-errors'] = function HttpErrorsCollector(params) { self.stats.put(1); if (self.logfile) { - var body = ''; - http.res.on('data', function(chunk) { body += chunk; }); - http.res.on('end', function(chunk) { + 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, }, @@ -145,9 +140,7 @@ StatsCollectors['slow-responses'] = function HttpErrorsCollector(params) { self.stats.put(1); if (self.logfile) { - var body = ''; - http.res.on('data', function(chunk) { body += chunk; }); - http.res.on('end', function(chunk) { + util.readStream(http.res, function(body) { var logObj = { ts: new Date(), req: { // Use the _header "private" member of http.ClientRequest, available as of diff --git a/lib/nodeload.js b/lib/nodeload.js index fc026e4..bdc9802 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -1,15 +1,15 @@ #!/usr/bin/env node -var util=require('util'),http=require('http'),fs=require('fs'),events=require('events'),querystring=require('querystring');var EventEmitter=events.EventEmitter;var START=new Date();var BUILD_AS_SINGLE_FILE=true;var DYGRAPH_SOURCE=exports.DYGRAPH_SOURCE="DygraphLayout=function(b,a){this.dygraph_=b;this.options={};Dygraph.update(this.options,a?a:{});this.datasets=new Array()};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.options.dateWindow){this.minxval=this.options.dateWindow[0];this.maxxval=this.options.dateWindow[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var d=this.datasets[c];var b=d[0][0];if(!this.minxval||bthis.maxxval){this.maxxval=a}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!=0?1/this.xrange:1);this.minyval=this.options.yAxis[0];this.maxyval=this.options.yAxis[1];this.yrange=this.maxyval-this.minyval;this.yscale=(this.yrange!=0?1/this.yrange:1)};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=new Array();for(var e in this.datasets){if(!this.datasets.hasOwnProperty(e)){continue}var d=this.datasets[e];for(var b=0;b=1){a.y=1}this.points.push(a)}}};DygraphLayout.prototype._evaluateLineTicks=function(){this.xticks=new Array();for(var c=0;c=0)&&(d<=1)){this.xticks.push([d,a])}}this.yticks=new Array();for(var c=0;c=0)&&(d<=1)){this.yticks.push([d,a])}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!this.options.errorBars){return}var d=0;for(var g in this.datasets){if(!this.datasets.hasOwnProperty(g)){continue}var c=0;var f=this.datasets[g];for(var c=0;c0){for(var e=0;ethis.height){k.style.bottom=\"0px\"}else{k.style.top=h+\"px\"}k.style.left=\"0px\";k.style.textAlign=\"right\";k.style.width=this.options.yAxisLabelWidth+\"px\";this.container.appendChild(k);this.ylabels.push(k)}var m=this.ylabels[0];var n=this.options.axisLabelFontSize;var a=parseInt(m.style.top)+n;if(a>this.height-n){m.style.top=(parseInt(m.style.top)-n/2)+\"px\"}}b.beginPath();b.moveTo(this.area.x,this.area.y);b.lineTo(this.area.x,this.area.y+this.area.h);b.closePath();b.stroke()}if(this.options.drawXAxis){if(this.layout.xticks){for(var e=0;ethis.width){c=this.width-this.options.xAxisLabelWidth;k.style.textAlign=\"right\"}if(c<0){c=0;k.style.textAlign=\"left\"}k.style.left=c+\"px\";k.style.width=this.options.xAxisLabelWidth+\"px\";this.container.appendChild(k);this.xlabels.push(k)}}b.beginPath();b.moveTo(this.area.x,this.area.y+this.area.h);b.lineTo(this.area.x+this.area.w,this.area.y+this.area.h);b.closePath();b.stroke()}b.restore()};DygraphCanvasRenderer.prototype._renderLineChart=function(){var b=this.element.getContext(\"2d\");var d=this.options.colorScheme.length;var n=this.options.colorScheme;var x=this.options.fillAlpha;var C=this.layout.options.errorBars;var q=this.layout.options.fillGraph;var E=[];for(var F in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(F)){E.push(F)}}var y=E.length;this.colors={};for(var A=0;A0){r=E[A-1]}var v=this.colors[g];s.save();s.strokeStyle=v;s.lineWidth=this.options.strokeWidth;var h=NaN;var f=[-1,-1];var k=0;var B=this.layout.yscale;var a=new RGBColor(v);var D=\"rgba(\"+a.r+\",\"+a.g+\",\"+a.b+\",\"+x+\")\";s.fillStyle=D;s.beginPath();for(var w=0;w1){e=1}}var p=[t.y,e];p[0]=this.area.h*p[0]+this.area.y;p[1]=this.area.h*p[1]+this.area.y;if(!isNaN(h)){s.moveTo(h,f[0]);s.lineTo(t.canvasx,p[0]);s.lineTo(t.canvasx,p[1]);s.lineTo(h,f[1]);s.closePath()}f[0]=p[0];f[1]=p[1];h=t.canvasx}}s.fill()}}}for(var A=0;A0){if(arguments.length==4){this.warn(\"Using deprecated four-argument dygraph constructor\");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME=\"Dygraph\";Dygraph.VERSION=\"1.2\";Dygraph.__repr__=function(){return\"[\"+this.NAME+\" \"+this.VERSION+\"]\"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.AXIS_LINE_WIDTH=0.3;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,pixelsPerXLabel:60,pixelsPerYLabel:30,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,yValueFormatter:function(a){return Dygraph.round_(a,2)},strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueFormatter:Dygraph.dateString_,xValueParser:Dygraph.dateParser,xTicker:Dygraph.dateTicker,delimiter:\",\",logScale:false,sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!=null){var a=[\"Date\"];for(var c=0;c=10){n.doZoom_(Math.min(b,m),Math.max(b,m))}else{n.canvas_.getContext(\"2d\").clearRect(0,0,n.canvas_.width,n.canvas_.height)}b=null;a=null}if(e){e=false;l=null;j=null}});Dygraph.addEvent(this.hidden_,\"dblclick\",function(o){if(n.dateWindow_==null){return}n.dateWindow_=null;n.drawGraph_(n.rawData_);var p=n.rawData_[0][0];var q=n.rawData_[n.rawData_.length-1][0];if(n.attr_(\"zoomCallback\")){n.attr_(\"zoomCallback\")(p,q)}})};Dygraph.prototype.drawZoomRect_=function(c,d,b){var a=this.canvas_.getContext(\"2d\");if(b){a.clearRect(Math.min(c,b),0,Math.abs(c-b),this.height_)}if(d&&c){a.fillStyle=\"rgba(128,128,128,0.33)\";a.fillRect(Math.min(c,d),0,Math.abs(d-c),this.height_)}};Dygraph.prototype.doZoom_=function(d,a){var b=this.toDataCoords(d,null);var c=b[0];b=this.toDataCoords(a,null);var e=b[0];this.dateWindow_=[c,e];this.drawGraph_(this.rawData_);if(this.attr_(\"zoomCallback\")){this.attr_(\"zoomCallback\")(c,e)}};Dygraph.prototype.mouseMove_=function(b){var a=Dygraph.pageX(b)-Dygraph.findPosX(this.hidden_);var s=this.layout_.points;var m=-1;var j=-1;var q=1e+100;var r=-1;for(var f=0;fq){continue}q=h;r=f}if(r>=0){m=s[r].xval}if(a>s[s.length-1].canvasx){m=s[s.length-1].xval}this.selPoints_=[];var g=0;var d=s.length;var o=this.attr_(\"stackedGraph\");if(!this.attr_(\"stackedGraph\")){for(var f=0;f=0;f--){if(s[f].xval==m){var c={};for(var e in s[f]){c[e]=s[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}}if(this.attr_(\"highlightCallback\")){var n=this.lastHighlightCallbackX;if(n!==null&&m!=n){this.lastHighlightCallbackX=m;this.attr_(\"highlightCallback\")(b,m,this.selPoints_)}}this.lastx_=m;this.updateSelection_()};Dygraph.prototype.updateSelection_=function(){var a=this.attr_(\"highlightCircleSize\");var n=this.canvas_.getContext(\"2d\");if(this.previousVerticalX_>=0){var l=this.previousVerticalX_;n.clearRect(l-a-1,0,2*a+2,this.height_)}var m=function(c){return c&&!isNaN(c)};if(this.selPoints_.length>0){var b=this.selPoints_[0].canvasx;var d=this.attr_(\"xValueFormatter\")(this.lastx_,this)+\":\";var e=this.attr_(\"yValueFormatter\");var j=this.colors_.length;if(this.attr_(\"showLabelsOnHighlight\")){for(var f=0;f\"}var k=this.selPoints_[f];var h=new RGBColor(this.colors_[f%j]);var g=e(k.yval);d+=\" \"+k.name+\":\"+g}this.attr_(\"labelsDiv\").innerHTML=d}n.save();for(var f=0;f=0){for(var a in this.layout_.datasets){if(b=Dygraph.DAILY){y.push({v:l,label:new Date(l+3600*1000).strftime(u)})}else{y.push({v:l,label:this.hmsString_(l)})}}}else{var f;var o=1;if(a==Dygraph.MONTHLY){f=[0,1,2,3,4,5,6,7,8,9,10,11,12]}else{if(a==Dygraph.QUARTERLY){f=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){f=[0,6]}else{if(a==Dygraph.ANNUAL){f=[0]}else{if(a==Dygraph.DECADAL){f=[0];o=10}}}}}var r=new Date(n).getFullYear();var p=new Date(k).getFullYear();var c=Dygraph.zeropad;for(var s=r;s<=p;s++){if(s%o!=0){continue}for(var q=0;qk){continue}y.push({v:l,label:new Date(l).strftime(\"%b %y\")})}}}return y};Dygraph.dateTicker=function(a,f,d){var b=-1;for(var e=0;e=d.attr_(\"pixelsPerXLabel\")){b=e;break}}if(b>=0){return d.GetXAxis(a,f,b)}else{}};Dygraph.numericTicks=function(v,u,l){if(l.attr_(\"labelsKMG2\")){var f=[1,2,4,8]}else{var f=[1,2,5]}var x,p,a,q;var h=l.attr_(\"pixelsPerYLabel\");for(var t=-10;t<50;t++){if(l.attr_(\"labelsKMG2\")){var c=Math.pow(16,t)}else{var c=Math.pow(10,t)}for(var s=0;sh){break}}if(d>h){break}}var w=[];var r;var o=[];if(l.attr_(\"labelsKMB\")){r=1000;o=[\"K\",\"M\",\"B\",\"T\"]}if(l.attr_(\"labelsKMG2\")){if(r){l.warn(\"Setting both labelsKMB and labelsKMG2. Pick one!\")}r=1024;o=[\"k\",\"M\",\"G\",\"T\"]}if(p>a){x*=-1}for(var t=0;t=0;s--,m/=r){if(b>=m){e=Dygraph.round_(g/m,1)+o[s];break}}}w.push({label:e,v:g})}return w};Dygraph.prototype.addYTicks_=function(c,b){var a=Dygraph.numericTicks(c,b,this);this.layout_.updateOptions({yAxis:[c,b],yTicks:a})};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null;var b=this.attr_(\"errorBars\")||this.attr_(\"customBars\");if(b){for(var c=0;cg){a=g}if(ef){f=e}if(h==null||af){f=g}if(h==null||g=E&&e===null){e=t}if(g[t][0]<=f){D=t}}if(e===null){e=0}if(e>0){e--}if(D===null){D=g.length-1}if(Dx){x=o}if(p){var m=[];for(var u=0;ux){x=d[g[u][0]]}}h.push([this.attr_(\"labels\")[w],m])}else{this.layout_.addDataset(this.attr_(\"labels\")[w],g)}}}if(h.length>0){for(var w=(h.length-1);w>=0;w--){this.layout_.addDataset(h[w][0],h[w][1])}}if(this.valueRange_!=null){this.addYTicks_(this.valueRange_[0],this.valueRange_[1]);this.displayedYRange_=this.valueRange_}else{if(this.attr_(\"includeZero\")&&y>0){y=0}var v=x-y;if(v==0){v=x}var c=x+0.1*v;var B=y-0.1*v;if(B<0&&y>=0){B=0}if(c>0&&x<=0){c=0}if(this.attr_(\"includeZero\")){if(x<0){c=0}if(y>0){B=0}}this.addYTicks_(B,c);this.displayedYRange_=[B,c]}this.addXTicks_();this.layout_.updateOptions({dateWindow:this.dateWindow_});this.layout_.evaluateWithError();this.plotter_.clear();this.plotter_.render();this.canvas_.getContext(\"2d\").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_(\"drawCallback\")!==null){this.attr_(\"drawCallback\")(this,n)}};Dygraph.prototype.rollingAverage=function(m,d){if(m.length<2){return m}var d=Math.min(d,m.length-1);var b=[];var s=this.attr_(\"sigma\");if(this.fractions_){var k=0;var h=0;var e=100;for(var x=0;x=0){k-=m[x-d][1][0];h-=m[x-d][1][1]}var B=m[x][0];var v=h?k/h:0;if(this.attr_(\"errorBars\")){if(this.wilsonInterval_){if(h){var t=v<0?0:v,u=h;var A=s*Math.sqrt(t*(1-t)/u+s*s/(4*u*u));var a=1+s*s/h;var F=(t+s*s/(2*h)-A)/a;var o=(t+s*s/(2*h)+A)/a;b[x]=[B,[t*e,(t-F)*e,(o-t)*e]]}else{b[x]=[B,[0,0,0]]}}else{var z=h?s*Math.sqrt(v*(1-v)/h):1;b[x]=[B,[e*v,e*z,e*z]]}}else{b[x]=[B,e*v]}}}else{if(this.attr_(\"customBars\")){var F=0;var C=0;var o=0;var g=0;for(var x=0;x=0){var r=m[x-d];if(r[1][1]!=null&&!isNaN(r[1][1])){F-=r[1][0];C-=r[1][1];o-=r[1][2];g-=1}}b[x]=[m[x][0],[1*C/g,1*(C-F)/g,1*(o-C)/g]]}}else{var q=Math.min(d-1,m.length-2);if(!this.attr_(\"errorBars\")){if(d==1){return m}for(var x=0;x=0||b.indexOf(\"/\")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>\"19700101\"&&b<\"20371231\"){a=true}}if(a){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker}else{this.attrs_.xValueFormatter=function(c){return c};this.attrs_.xValueParser=function(c){return parseFloat(c)};this.attrs_.xTicker=Dygraph.numericTicks}};Dygraph.prototype.parseCSV_=function(h){var m=[];var q=h.split(\"\\n\");var b=this.attr_(\"delimiter\");if(q[0].indexOf(b)==-1&&q[0].indexOf(\"\\t\")>=0){b=\"\\t\"}var a=0;if(this.labelsFromCSV_){a=1;this.attrs_.labels=q[0].split(b)}var c;var o=false;var d=this.attr_(\"labels\").length;var l=false;for(var g=a;g0&&k[0]0&&k[0]=0){this.loadedEvent_(this.file_)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status==200){a.loadedEvent_(b.responseText)}}};b.open(\"GET\",this.file_,true);b.send(null)}}else{this.error(\"Unknown data format: \"+(typeof this.file_))}}}}};Dygraph.prototype.updateOptions=function(a){if(a.rollPeriod){this.rollPeriod_=a.rollPeriod}if(a.dateWindow){this.dateWindow_=a.dateWindow}if(a.valueRange){this.valueRange_=a.valueRange}Dygraph.update(this.user_attrs_,a);this.labelsFromCSV_=(this.attr_(\"labels\")==null);this.layout_.updateOptions({errorBars:this.attr_(\"errorBars\")});if(a.file){this.file_=a.file;this.start_()}else{this.drawGraph_(this.rawData_)}};Dygraph.prototype.resize=function(b,a){if((b===null)!=(a===null)){this.warn(\"Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.\");b=a=null}this.maindiv_.innerHTML=\"\";this.attrs_.labelsDiv=null;if(b){this.maindiv_.style.width=b+\"px\";this.maindiv_.style.height=a+\"px\";this.width_=b;this.height_=a}else{this.width_=this.maindiv_.offsetWidth;this.height_=this.maindiv_.offsetHeight}this.createInterface_();this.drawGraph_(this.rawData_)};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.drawGraph_(this.rawData_)};Dygraph.prototype.visibility=function(){if(!this.attr_(\"visibility\")){this.attrs_.visibility=[]}while(this.attr_(\"visibility\").length=a.length){this.warn(\"invalid series number in setVisibility: \"+b)}else{a[b]=c;this.drawGraph_(this.rawData_)}};Dygraph.createCanvas=function(){var a=document.createElement(\"canvas\");isIE=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(isIE){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML=\"\";this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var c=this.date_graph.getSelection();if(c<0){return b}col=1;for(var a in this.date_graph.layout_.datasets){b.push({row:c,column:col});col++}return b};DateGraph=Dygraph;function RGBColor(g){this.ok=false;if(g.charAt(0)==\"#\"){g=g.substr(1,6)}g=g.replace(/ /g,\"\");g=g.toLowerCase();var a={aliceblue:\"f0f8ff\",antiquewhite:\"faebd7\",aqua:\"00ffff\",aquamarine:\"7fffd4\",azure:\"f0ffff\",beige:\"f5f5dc\",bisque:\"ffe4c4\",black:\"000000\",blanchedalmond:\"ffebcd\",blue:\"0000ff\",blueviolet:\"8a2be2\",brown:\"a52a2a\",burlywood:\"deb887\",cadetblue:\"5f9ea0\",chartreuse:\"7fff00\",chocolate:\"d2691e\",coral:\"ff7f50\",cornflowerblue:\"6495ed\",cornsilk:\"fff8dc\",crimson:\"dc143c\",cyan:\"00ffff\",darkblue:\"00008b\",darkcyan:\"008b8b\",darkgoldenrod:\"b8860b\",darkgray:\"a9a9a9\",darkgreen:\"006400\",darkkhaki:\"bdb76b\",darkmagenta:\"8b008b\",darkolivegreen:\"556b2f\",darkorange:\"ff8c00\",darkorchid:\"9932cc\",darkred:\"8b0000\",darksalmon:\"e9967a\",darkseagreen:\"8fbc8f\",darkslateblue:\"483d8b\",darkslategray:\"2f4f4f\",darkturquoise:\"00ced1\",darkviolet:\"9400d3\",deeppink:\"ff1493\",deepskyblue:\"00bfff\",dimgray:\"696969\",dodgerblue:\"1e90ff\",feldspar:\"d19275\",firebrick:\"b22222\",floralwhite:\"fffaf0\",forestgreen:\"228b22\",fuchsia:\"ff00ff\",gainsboro:\"dcdcdc\",ghostwhite:\"f8f8ff\",gold:\"ffd700\",goldenrod:\"daa520\",gray:\"808080\",green:\"008000\",greenyellow:\"adff2f\",honeydew:\"f0fff0\",hotpink:\"ff69b4\",indianred:\"cd5c5c\",indigo:\"4b0082\",ivory:\"fffff0\",khaki:\"f0e68c\",lavender:\"e6e6fa\",lavenderblush:\"fff0f5\",lawngreen:\"7cfc00\",lemonchiffon:\"fffacd\",lightblue:\"add8e6\",lightcoral:\"f08080\",lightcyan:\"e0ffff\",lightgoldenrodyellow:\"fafad2\",lightgrey:\"d3d3d3\",lightgreen:\"90ee90\",lightpink:\"ffb6c1\",lightsalmon:\"ffa07a\",lightseagreen:\"20b2aa\",lightskyblue:\"87cefa\",lightslateblue:\"8470ff\",lightslategray:\"778899\",lightsteelblue:\"b0c4de\",lightyellow:\"ffffe0\",lime:\"00ff00\",limegreen:\"32cd32\",linen:\"faf0e6\",magenta:\"ff00ff\",maroon:\"800000\",mediumaquamarine:\"66cdaa\",mediumblue:\"0000cd\",mediumorchid:\"ba55d3\",mediumpurple:\"9370d8\",mediumseagreen:\"3cb371\",mediumslateblue:\"7b68ee\",mediumspringgreen:\"00fa9a\",mediumturquoise:\"48d1cc\",mediumvioletred:\"c71585\",midnightblue:\"191970\",mintcream:\"f5fffa\",mistyrose:\"ffe4e1\",moccasin:\"ffe4b5\",navajowhite:\"ffdead\",navy:\"000080\",oldlace:\"fdf5e6\",olive:\"808000\",olivedrab:\"6b8e23\",orange:\"ffa500\",orangered:\"ff4500\",orchid:\"da70d6\",palegoldenrod:\"eee8aa\",palegreen:\"98fb98\",paleturquoise:\"afeeee\",palevioletred:\"d87093\",papayawhip:\"ffefd5\",peachpuff:\"ffdab9\",peru:\"cd853f\",pink:\"ffc0cb\",plum:\"dda0dd\",powderblue:\"b0e0e6\",purple:\"800080\",red:\"ff0000\",rosybrown:\"bc8f8f\",royalblue:\"4169e1\",saddlebrown:\"8b4513\",salmon:\"fa8072\",sandybrown:\"f4a460\",seagreen:\"2e8b57\",seashell:\"fff5ee\",sienna:\"a0522d\",silver:\"c0c0c0\",skyblue:\"87ceeb\",slateblue:\"6a5acd\",slategray:\"708090\",snow:\"fffafa\",springgreen:\"00ff7f\",steelblue:\"4682b4\",tan:\"d2b48c\",teal:\"008080\",thistle:\"d8bfd8\",tomato:\"ff6347\",turquoise:\"40e0d0\",violet:\"ee82ee\",violetred:\"d02090\",wheat:\"f5deb3\",white:\"ffffff\",whitesmoke:\"f5f5f5\",yellow:\"ffff00\",yellowgreen:\"9acd32\"};for(var c in a){if(g==c){g=a[c]}}var h=[{re:/^rgb\\((\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d{1,3})\\)$/,example:[\"rgb(123, 234, 45)\",\"rgb(255,234,245)\"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\\w{2})(\\w{2})(\\w{2})$/,example:[\"#00ff00\",\"336699\"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\\w{1})(\\w{1})(\\w{1})$/,example:[\"#fb0\",\"f0f\"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var b=0;b255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return\"rgb(\"+this.r+\", \"+this.g+\", \"+this.b+\")\"};this.toHex=function(){var k=this.r.toString(16);var j=this.g.toString(16);var i=this.b.toString(16);if(k.length==1){k=\"0\"+k}if(j.length==1){j=\"0\"+j}if(i.length==1){i=\"0\"+i}return\"#\"+k+j+i}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)==\"undefined\"){b=10}for(;parseInt(a,10)1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale=\"en-GB\";if(document.getElementsByTagName(\"html\")&&document.getElementsByTagName(\"html\")[0].lang){Date.prototype.locale=document.getElementsByTagName(\"html\")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],A:[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],b:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],B:[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],c:\"%a %d %b %Y %T %Z\",p:[\"AM\",\"PM\"],P:[\"am\",\"pm\"],x:\"%d/%m/%y\",X:\"%T\"};Date.ext.locales[\"en-US\"]=Date.ext.locales.en;Date.ext.locales[\"en-US\"].c=\"%a %d %b %Y %r %Z\";Date.ext.locales[\"en-US\"].x=\"%D\";Date.ext.locales[\"en-US\"].X=\"%r\";Date.ext.locales[\"en-GB\"]=Date.ext.locales.en;Date.ext.locales[\"en-AU\"]=Date.ext.locales[\"en-GB\"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:\"toLocaleString\",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:[\"getDate\",\"0\"],e:[\"getDate\",\" \"],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:[\"getHours\",\"0\"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(\"\"+c.getFullYear()+\"/1/1 GMT\");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:[\"getMinutes\",\"0\"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:[\"getSeconds\",\"0\"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(\"\"+e.getFullYear()+\"/1/1\")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(\"\"+e.getFullYear()+\"/12/31\")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(\"\"+(e.getFullYear()-1)+\"/12/31\"))}}return Date.ext.util.xPad(b,0)},w:\"getDay\",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:\"getFullYear\",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?\"-\":\"+\")+a+e},Z:function(a){return a.toString().replace(/^.*\\(([^)]+)\\)$/,\"$1\")},\"%\":function(a){return\"%\"}};Date.ext.aggregates={c:\"locale\",D:\"%m/%d/%y\",h:\"%b\",n:\"\\n\",r:\"%I:%M:%S %p\",R:\"%H:%M\",t:\"\\t\",T:\"%H:%M:%S\",x:\"locale\",X:\"locale\"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,\"\") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,\"\")}else{this.locale=\"en-GB\"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g==\"locale\"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)==\"string\"){return c[g]()}else{if(typeof(g)==\"function\"){return g.call(c,c)}else{if(typeof(g)==\"object\"&&typeof(g[0])==\"string\"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};";var REPORT_SUMMARY_TEMPLATE=exports.REPORT_SUMMARY_TEMPLATE="\n \n Test Results\n \n \n \n\n \n
\n

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

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

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

\n \n\n \n";var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var fs=require('fs');} var template={cache_:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)){if(!callback){fn=this.create(fs.readFileSync(str).toString('utf8'));}else{fs.readFile(str,function(err,buffer){if(err){throw err;} this.create(buffer.toString('utf8'),data,callback);});return;}}else{if(this.cache_[str]){fn=this.cache_[str];}else{fn=new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};"+"obj=obj||{};"+"with(obj){p.push('"+ str.split("'").join("\\'").split("\n").join("\\n").replace(/<%([\s\S]*?)%>/mg,function(m,t){return'<%'+t.split("\\'").join("'").split("\\n").join("\n")+'%>';}).replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');");this.cache_[str]=fn;}} if(callback){callback(data?fn(data):fn);} else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);var BUILD_AS_SINGLE_FILE,NODELOAD_CONFIG;if(!BUILD_AS_SINGLE_FILE){var EventEmitter=require('events').EventEmitter;} -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=exports.NODELOAD_CONFIG={START:new Date(),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 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;} +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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.PeriodicUpdater=function(updateIntervalMs){var self=this,updateTimeoutId;this.__defineGetter__('updateInterval',function(){return updateIntervalMs;});this.__defineSetter__('updateInterval',function(milliseconds){clearInterval(updateTimeoutId);if(milliseconds>0){updateTimeoutId=setInterval(self.update.bind(self),milliseconds);} +return obj;};util.forEach=function(obj,f){for(var i in obj){if(obj.hasOwnProperty(i)){f(i,obj[i]);}}};util.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.localHostname=function(){return'localhost';};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;} @@ -58,9 +58,11 @@ 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(timeoutself.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}};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;} @@ -74,12 +76,13 @@ return this;};Monitor.prototype.update=function(){this.emit('update',this.interv 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:[].concat.apply([],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:[].concat.apply([],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 NODELOAD_CONFIG=config.NODELOAD_CONFIG;} -var HttpServer=exports.HttpServer=function HttpServer(){this.routes=[];this.running=false;};HttpServer.prototype.start=function(port,hostname){if(this.running){return;} -this.running=true;var self=this;port=port||8000;self.server=http.createServer(function(req,res){self.route_(req,res);});self.server.listen(port,hostname);qputs('Started HTTP server on port '+port+'.');return self;};HttpServer.prototype.stop=function(){if(!this.running){return;} -this.running=false;this.server.close();this.server=null;qputs('Shutdown HTTP server.');};HttpServer.prototype.addRoute=function(regex,handler){this.routes.push({regex:regex,handler:handler});return this;};HttpServer.prototype.route_=function(req,res){this.routes.forEach(function(r){if(req.url.match(r.regex)){r.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;} +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||util.localHostname();self.port=port;self.server=http.createServer(function(req,res){self.route_(req,res);});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.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;i0){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 http=require('http');var url=require('url');var util=require('../util');var EventEmitter=require('events').EventEmitter;var qputs=util.qputs;} +var DEFAULT_RETRY_INTERVAL_MS=2000;var Endpoint=exports.Endpoint=function Endpoint(server){EventEmitter.call(this);var self=this,basepath='',updateUrl=function(){self.url=url.format({protocol:'http',hostname:server.hostname,port:server.port,pathname:basepath});};self.__defineGetter__('basepath',function(){return basepath;});self.__defineSetter__('basepath',function(val){basepath=val;updateUrl();});self.id=util.uid();self.basepath='/remote/'+self.id;self.route='^'+self.basepath+'/?';self.server=server;self.methodNames=[];self.methods={};self.context={};self.setStaticParams([]);self.state='initialized';self.handler_=self.handle.bind(self);self.server.on('start',function(hostname,port){updateUrl();});};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.server.addRoute(this.route,this.handler_);if(this.methods['setup']){this.methods['setup'].apply(this.context,this.staticParams_);} +this.state='started';this.emit('start');};Endpoint.prototype.destroy=function(){if(this.state!=='started'){return;} +this.server.removeRoute(this.route,this.handler_);this.state='destroyed';this.emit('end');};Endpoint.prototype.handle=function(path,req,res){var self=this;if(path===self.basepath){if(req.method==='DELETE'){self.destroy();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='undefined';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?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 EndpointClient=exports.EndpointClient=function EndpointClient(host,port,basepath){EventEmitter.call(this);this.host=host;this.port=port;this.basepath=basepath||'';this.methodNames=[];this.state='disconnected';this.retryInterval=DEFAULT_RETRY_INTERVAL_MS;this.setStaticParams([]);};util.inherits(EndpointClient,EventEmitter);EndpointClient.prototype.start=function(){if(this.state!=='disconnected'&&this.state!=='reconnect'){return;} +var self=this;clearTimeout(self.retryTimeoutId);self.retryTimeoutId=null;if(self.client){self.client.destroy();} +self.client=http.createClient(self.port,self.host);self.client.on('error',function(err){qputs('Communication error with "'+self.host+':'+self.port+'". Reconnecting: '+err.toString());self.state='reconnect';self.client.destroy();self.client=null;self.retryTimeoutId=setTimeout(self.start.bind(self),self.retryInterval);self.emit('clientError',err);});self.state='connected';self.emit('connect');};EndpointClient.prototype.end=function(){if(this.state!=='connected'&&this.state!=='reconnect'){return;} +clearTimeout(this.retryTimeoutId);this.client.destroy();this.state='disconnected';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(){if(self.state!=='connected'){throw new Error('Cannot call method before connect');} +var req=self.client.request('POST',self.basepath+'/'+name),params=self.staticParams_.concat(util.argarray(arguments));req.end(JSON.stringify(params));return req;};self.methodNames.push(name);};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('./endpoint').EndpointClient;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var Slave=exports.Slave=function Slave(id,host,port,masterEndpoint){EventEmitter.call(this);this.id=id;this.client=new EndpointClient(host,port);this.masterEndpoint=masterEndpoint;this.methodDefs=[];this.state='stopped';};util.inherits(Slave,EventEmitter);Slave.prototype.start=function(){var self=this;self.client.on('connect',function(){if(!self.basepath){var req=self.client.rawRequest('POST','/remote');req.end(JSON.stringify({id:self.id,master:self.masterEndpoint.url,masterMethods:self.masterEndpoint.methodNames,slaveMethods:self.methodDefs,updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS}));req.on('response',function(res){self.client.basepath=url.parse(res.headers['location']).pathname;self.state='started';self.emit('start');});}});self.client.on('clientError',function(e){self.emit('clientError',e);});self.client.start();self.state='connecting';};Slave.prototype.end=function(){var self=this,req=self.client.rawrequest('DELETE',self.client.basepath);req.end();req.on('response',function(res){if(res.statusCode===204){self.client.end();self.client.basepath='';self.state='stopped';self.emit('end');}else{self.emit('clientError',new Error('Error stopping slave.'),res);}});};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 Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(Slaves,EventEmitter);Slaves.prototype.add=function(host,port){var self=this,id=host+':'+port,slave=new Slave(id,host,port,self.masterEndpoint);self.slaves.push(slave);self[id]=slave;self[id].on('clientError',function(err){self.emit('clientError',err,slave);});self[id].on('start',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='started'){return;}});self.emit('start');});self[id].on('end',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='stopped'){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');var Endpoint=endpoint.Endpoint;var EndpointClient=endpoint.EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;var endpoint=this.createEndpoint_(server,spec.slaveMethods),masterClient=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.url=endpoint.url;this.masterClient_=masterClient;this.masterClient_.start();this.slaveEndpoint_=endpoint;this.slaveEndpoint_.context.state='initialized';this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.start();this.slaveEndpoint_.on('end',this.end.bind(this));this.updateInterval=(spec.updateInterval>=0)?spec.updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){if(this.slaveEndpoint_.state==='started'){this.slaveEndpoint_.destroy();} +if(this.masterClient_.state==='connected'||this.masterClient_.state==='reconnect'){this.masterClient_.end();} +this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_&&this.masterClient_.state==='connected'){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.destroy();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]);return masterClient;};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var slave=require('./slave');var endpoint=require('./endpoint');var Endpoint=endpoint.Endpoint;var EventEmitter=require('events').EventEmitter;var SlaveNode=require('./slavenode').SlaveNode;var Slaves=slave.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);var self=this,masterSpec=spec.master||{},slavesSpec=spec.slaves||{hosts:[]};self.masterEndpoint=new Endpoint(HTTP_SERVER);self.slaves=new Slaves(self.masterEndpoint,spec.pingInterval);self.slaveState_={};self.masterEndpoint.setStaticParams([self.slaves]);self.masterEndpoint.defineMethod('updateSlaveState_',self.updateSlaveState_.bind(self));util.forEach(masterSpec,function(method,val){self.masterEndpoint.defineMethod(method,val);});slavesSpec.hosts.forEach(function(h){var parts=h.split(':'),host=parts[0],port=Number(parts[1])||8000;self.slaves.add(host,port);});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.state='initialized';self.slaves.on('start',function(){self.state='started';self.emit('start');});};util.inherits(Cluster,EventEmitter);Cluster.prototype.start=function(){if(!HTTP_SERVER.running){throw new Error('A Cluster can only be started after the global HTTP_SERVER is running.');} +var self=this;self.masterEndpoint.start();self.slaves.start();};Cluster.prototype.end=function(){this.masterEndpoint.destroy();this.slaves.end();this.state='stopped';this.emit('end');};Cluster.prototype.updateSlaveState_=function(slaves,slaveId,state){var slave=slaves[slaveId];if(slave){var previousState=this.slaveState_[slaveId];this.slaveState_[slaveId]=state;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){allSlavesInSameState=false;}});if(allSlavesInSameState){this.emit(state);}};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var SlaveNode=require('./cluster').SlaveNode;var HTTP_SERVER=require('../http').HTTP_SERVER;} +var createSlave_,createMaster_,slaveNodes=[];HTTP_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(HTTP_SERVER,body);}catch(e){res.writeHead(400);res.end(e.toString());return;} +slaveNode.on('end',function(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});slaveNodes.push(slaveNode);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();}}); diff --git a/lib/remote.js b/lib/remote.js deleted file mode 100644 index 45815db..0000000 --- a/lib/remote.js +++ /dev/null @@ -1,339 +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_OPTIONS 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.masterhost = master[0]; - this.master = 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) { - var s = JSON.stringify(object); - var req = this.master.request('POST', url, {'host': this.masterhost, '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], - 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/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/lib/remote/cluster.js b/lib/remote/cluster.js new file mode 100644 index 0000000..3520c3b --- /dev/null +++ b/lib/remote/cluster.js @@ -0,0 +1,112 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var slave = require('./slave'); +var endpoint = require('./endpoint'); +var Endpoint = endpoint.Endpoint; +var EventEmitter = require('events').EventEmitter; +var SlaveNode = require('./slavenode').SlaveNode; +var Slaves = slave.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: { + master_remote_function_1: function(slaves, slaveId, args...) { ... }, + }, + slaves: { + host: ['host:port', ...], + setup: function(master) { ... } + slave_remote_function_1: function(master, args...) { ... } + }, + pingInterval: 2000 +} + +Calling cluster.start() will register a master handler on this host. It will connect to every slave, +asking each slave to 1) execute the setup() function, 2) report its current status this host every +pingInterval milliseconds. Calling cluster.slave_remote_function_1(), will execute +slave_remote_function_1 on every slave. + +Within master_remote_function_1 and slave_remote_function_1, this points at an initially empty object +that should be used to store state. + +*/ +var Cluster = exports.Cluster = function Cluster(spec) { + EventEmitter.call(this); + + var self = this, + masterSpec = spec.master || {}, + slavesSpec = spec.slaves || { hosts:[] }; + + self.masterEndpoint = new Endpoint(HTTP_SERVER); + self.slaves = new Slaves(self.masterEndpoint, spec.pingInterval); + self.slaveState_ = {}; + + self.masterEndpoint.setStaticParams([self.slaves]); + self.masterEndpoint.defineMethod('updateSlaveState_', self.updateSlaveState_.bind(self)); + util.forEach(masterSpec, function(method, val) { + self.masterEndpoint.defineMethod(method, val); + }); + + slavesSpec.hosts.forEach(function(h) { + var parts = h.split(':'), host = parts[0], port = Number(parts[1]) || 8000; + self.slaves.add(host, port); + }); + 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.state = 'initialized'; + self.slaves.on('start', function() { + self.state = 'started'; + self.emit('start'); + }); +}; +util.inherits(Cluster, EventEmitter); +Cluster.prototype.start = function() { + if (!HTTP_SERVER.running) { + throw new Error('A Cluster can only be started after the global HTTP_SERVER is running.'); + } + var self = this; + self.masterEndpoint.start(); + self.slaves.start(); +}; +Cluster.prototype.end = function() { + this.masterEndpoint.destroy(); + this.slaves.end(); + this.state = 'stopped'; + this.emit('end'); +}; +Cluster.prototype.updateSlaveState_ = function(slaves, slaveId, state) { + var slave = slaves[slaveId]; + if (slave) { + var previousState = this.slaveState_[slaveId]; + this.slaveState_[slaveId] = state; + 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) { + 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..4a92d93 --- /dev/null +++ b/lib/remote/endpoint.js @@ -0,0 +1,229 @@ +/*jslint sub: true */ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var http = require('http'); +var url = require('url'); +var util = require('../util'); +var EventEmitter = require('events').EventEmitter; +var qputs = util.qputs; +} + +var DEFAULT_RETRY_INTERVAL_MS = 2000; + +/** 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(...). */ +var Endpoint = exports.Endpoint = function Endpoint(server) { + EventEmitter.call(this); + + var self = this, + basepath = '', + updateUrl = function() { + self.url = url.format({ + protocol: 'http', + hostname: server.hostname, + port: server.port, + pathname: basepath + }); + }; + self.__defineGetter__('basepath', function() { return basepath; }); + self.__defineSetter__('basepath', function(val) { + basepath = val; + updateUrl(); + }); + + self.id = util.uid(); + self.basepath = '/remote/' + self.id; + self.route = '^' + self.basepath + '/?'; + self.server = server; + self.methodNames = []; + self.methods = {}; + self.context = {}; + self.setStaticParams([]); + self.state = 'initialized'; + self.handler_ = self.handle.bind(self); + + self.server.on('start', function(hostname, port) { updateUrl(); }); +}; + +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.server.addRoute(this.route, this.handler_); + 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.destroy = function() { + if (this.state !== 'started') { return; } + this.server.removeRoute(this.route, this.handler_); + this.state = 'destroyed'; + 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.destroy(); + 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 = 'undefined'; + + 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 ? 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(); + } +}; + + +/** 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.start(); + 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). */ +var EndpointClient = exports.EndpointClient = function EndpointClient(host, port, basepath) { + EventEmitter.call(this); + this.host = host; + this.port = port; + this.basepath = basepath || ''; + this.methodNames = []; + this.state = 'disconnected'; + this.retryInterval = DEFAULT_RETRY_INTERVAL_MS; + this.setStaticParams([]); +}; +util.inherits(EndpointClient, EventEmitter); +/** Establish an HTTP connection to the target server. Emit 'connect' when connected. */ +EndpointClient.prototype.start = function() { + if (this.state !== 'disconnected' && this.state !== 'reconnect') { return; } + + var self = this; + + clearTimeout(self.retryTimeoutId); + self.retryTimeoutId = null; + + if (self.client) { self.client.destroy(); } + self.client = http.createClient(self.port, self.host); + self.client.on('error', function(err) { + qputs('Communication error with "'+ self.host +':'+ self.port +'". Reconnecting: '+ err.toString()); + self.state = 'reconnect'; + self.client.destroy(); + self.client = null; + self.retryTimeoutId = setTimeout(self.start.bind(self), self.retryInterval); + self.emit('clientError', err); + }); + self.state = 'connected'; + self.emit('connect'); +}; +/** Terminate the HTTP connection. */ +EndpointClient.prototype.end = function() { + if (this.state !== 'connected' && this.state !== 'reconnect') { return; } + clearTimeout(this.retryTimeoutId); + this.client.destroy(); + this.state = 'disconnected'; + 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() { + if (self.state !== 'connected') { throw new Error('Cannot call method before connect'); } + var req = self.client.request('POST', self.basepath + '/' + name), + params = self.staticParams_.concat(util.argarray(arguments)); + + req.end(JSON.stringify(params)); + return req; + }; + self.methodNames.push(name); +}; \ No newline at end of file diff --git a/lib/remote/http.js b/lib/remote/http.js new file mode 100644 index 0000000..aeeb6c9 --- /dev/null +++ b/lib/remote/http.js @@ -0,0 +1,46 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var SlaveNode = require('./cluster').SlaveNode; +var HTTP_SERVER = require('../http').HTTP_SERVER; +} + +var createSlave_, createMaster_, slaveNodes = []; + +/** Global /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 +cluster.js#SlaveNode for a description of the handler defintion. */ +HTTP_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(HTTP_SERVER, body); + } catch(e) { + res.writeHead(400); + res.end(e.toString()); + return; + } + + slaveNode.on('end', function() { + slaveNodes = slaveNodes.filter(function(s) { return s !== slaveNode; }); + }); + slaveNodes.push(slaveNode); + + 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/index.js b/lib/remote/index.js new file mode 100644 index 0000000..4f65185 --- /dev/null +++ b/lib/remote/index.js @@ -0,0 +1,9 @@ +var slave = require('./slave'); +var endpoint = require('./endpoint'); +exports.Cluster = require('./cluster').Cluster; +exports.Slaves = slave.Slaves; +exports.Slave = slave.Slave; +exports.Endpoint = endpoint.Endpoint; +exports.EndpointClient = endpoint.EndpointClient; + +require('./http'); \ No newline at end of file diff --git a/lib/remote/slave.js b/lib/remote/slave.js new file mode 100644 index 0000000..69f1d2a --- /dev/null +++ b/lib/remote/slave.js @@ -0,0 +1,118 @@ +/*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('./endpoint').EndpointClient; +var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; +} + +// ------------------------- +// Slave +// ------------------------- +var Slave = exports.Slave = function Slave(id, host, port, masterEndpoint) { + EventEmitter.call(this); + this.id = id; + this.client = new EndpointClient(host, port); + this.masterEndpoint = masterEndpoint; + this.methodDefs = []; + this.state = 'stopped'; +}; +util.inherits(Slave, EventEmitter); +Slave.prototype.start = function() { + var self = this; + self.client.on('connect', function() { + if (!self.basepath) { + var req = self.client.rawRequest('POST', '/remote'); + req.end(JSON.stringify({ + id: self.id, + master: self.masterEndpoint.url, + masterMethods: self.masterEndpoint.methodNames, + slaveMethods: self.methodDefs, + updateInterval: NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS + })); + req.on('response', function(res) { + self.client.basepath = url.parse(res.headers['location']).pathname; + self.state = 'started'; + self.emit('start'); + }); + } + }); + self.client.on('clientError', function(e) { self.emit('clientError', e); }); + self.client.start(); + self.state = 'connecting'; +}; +Slave.prototype.end = function() { + var self = this, req = self.client.rawrequest('DELETE', self.client.basepath); + req.end(); + req.on('response', function(res) { + if (res.statusCode === 204) { + self.client.end(); + self.client.basepath = ''; + self.state = 'stopped'; + self.emit('end'); + } else { + self.emit('clientError', new Error('Error stopping slave.'), res); + } + }); +}; +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()}); +}; + + +// ------------------------- +// Slaves +// ------------------------- +var Slaves = exports.Slaves = function Slaves(masterEndpoint, pingInterval) { + EventEmitter.call(this); + this.masterEndpoint = masterEndpoint; + this.slaves = []; + this.pingInterval = pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; +}; +util.inherits(Slaves, EventEmitter); +Slaves.prototype.add = function(host, port) { + var self = this, + id = host + ':' + port, + slave = new Slave(id, host, port, self.masterEndpoint); + + self.slaves.push(slave); + self[id] = slave; + self[id].on('clientError', function(err) { + self.emit('clientError', err, slave); + }); + self[id].on('start', function() { + util.forEach(self.slaves, function(id, s) { + if (s.state !== 'started') { return; } + }); + self.emit('start'); + }); + self[id].on('end', function() { + util.forEach(self.slaves, function(id, s) { + if (s.state !== 'stopped') { 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(); }); +}; \ No newline at end of file diff --git a/lib/remote/slavenode.js b/lib/remote/slavenode.js new file mode 100644 index 0000000..3fd51c1 --- /dev/null +++ b/lib/remote/slavenode.js @@ -0,0 +1,95 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var url = require('url'); +var util = require('../util'); +var endpoint = require('./endpoint'); +var Endpoint = endpoint.Endpoint; +var EndpointClient = endpoint.EndpointClient; +var EventEmitter = require('events').EventEmitter; +var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; +} + +/** An instance of SlaveNode is instantiated on each slave node in the Cluster. When a Slave object is +started, it sends a slave specification to the target machine. This specification is used to create +SlaveNode. It 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 }' } + ], + updateInterval: milliseconds between sending the current execution state to master + } + +If the any of the slaveMethods contain invalid Javascript, this constructor will throw an exception. +*/ +var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { + EventEmitter.call(this); + util.PeriodicUpdater.call(this); + + this.id = spec.id; + + var endpoint = this.createEndpoint_(server, spec.slaveMethods), + masterClient = spec.master ? this.createMasterClient_(spec.master, spec.masterMethods) : null; + + this.url = endpoint.url; + this.masterClient_ = masterClient; + this.masterClient_.start(); + this.slaveEndpoint_ = endpoint; + this.slaveEndpoint_.context.state = 'initialized'; + this.slaveEndpoint_.setStaticParams([this.masterClient_]); + this.slaveEndpoint_.start(); + this.slaveEndpoint_.on('end', this.end.bind(this)); + this.updateInterval = (spec.updateInterval >= 0) ? spec.updateInterval : NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; +}; +util.inherits(SlaveNode, EventEmitter); +SlaveNode.prototype.end = function() { + if (this.slaveEndpoint_.state === 'started') { + this.slaveEndpoint_.destroy(); + } + if (this.masterClient_.state === 'connected' || this.masterClient_.state === 'reconnect') { + this.masterClient_.end(); + } + this.emit('end'); +}; +SlaveNode.prototype.update = function() { + if (this.masterClient_ && this.masterClient_.state === 'connected') { + 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.destroy(); + 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]); + + return masterClient; +}; \ No newline at end of file diff --git a/lib/util.js b/lib/util.js index de1b565..ce1dd34 100644 --- a/lib/util.js +++ b/lib/util.js @@ -55,6 +55,18 @@ util.forEach = function(obj, f) { util.argarray = function(args) { return (args instanceof Array) ? args : [].concat.apply([], args); }; +util.localHostname = function() { + return 'localhost'; +}; +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 an UpdateEventGenerator by adding UpdateEventGenerator.call(this) to the constructor. Monitor should gather statistics for each intervalMs period, and generate 'update' events */ @@ -63,7 +75,7 @@ util.PeriodicUpdater = function(updateIntervalMs) { this.__defineGetter__('updateInterval', function() { return updateIntervalMs; }); this.__defineSetter__('updateInterval', function(milliseconds) { clearInterval(updateTimeoutId); - if (milliseconds > 0) { + if (milliseconds > 0 && milliseconds < Infinity) { updateTimeoutId = setInterval(self.update.bind(self), milliseconds); } updateIntervalMs = milliseconds; From a012e9bfa80a832cbae39969b7578aa61bf846ec Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Wed, 24 Nov 2010 21:34:44 -0500 Subject: [PATCH 25/41] add comments split endpoint & endpointclient into separate files remove HTTP_SERVER references from slavenode bugs fixes --- Makefile | 2 +- examples/remote.test.js | 17 +++--- lib/nodeload.js | 32 +++++----- lib/remote/cluster.js | 91 +++++++++++++++++++++-------- lib/remote/endpoint.js | 94 +++++------------------------- lib/remote/endpointclient.js | 93 +++++++++++++++++++++++++++++ lib/remote/http.js | 44 +------------- lib/remote/index.js | 7 ++- lib/remote/slave.js | 110 +++++++++++++++++++++++------------ lib/remote/slavenode.js | 65 ++++++++++++++++++--- 10 files changed, 336 insertions(+), 219 deletions(-) create mode 100644 lib/remote/endpointclient.js diff --git a/Makefile b/Makefile index c7fffbd..2287b37 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/slave.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/http.js +SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/http.js all: compile diff --git a/examples/remote.test.js b/examples/remote.test.js index 2397af2..6eebbea 100755 --- a/examples/remote.test.js +++ b/examples/remote.test.js @@ -31,17 +31,14 @@ var cluster = new Cluster({ } }); -HTTP_SERVER.on('start', function() { - cluster.on('end', function(slaves) { - console.log('All slaves finished: ' + - JSON.stringify( - slaves.map(function(s) { - return s.id + ': ' + s.result; - }))); - }); +cluster.on('init', function() { cluster.on('start', function() { cluster.exec('ls -alh && sleep 3'); }); + cluster.on('end', function(slaves) { + console.log('All slaves terminated.'); + process.exit(0); + }); cluster.on('running', function() { console.log('All slaves running'); }); @@ -54,4 +51,8 @@ HTTP_SERVER.on('start', function() { } }); cluster.start(); +}); + +process.on('SIGINT', function() { + cluster.end(); }); \ No newline at end of file diff --git a/lib/nodeload.js b/lib/nodeload.js index bdc9802..4323ab5 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -95,24 +95,26 @@ return self;};LoadTest.prototype.stop=function(){this.tests.forEach(function(t){ 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=function(){var client=http.createClient.apply(this,arguments);return extendClient(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});}};client.on('error',function(err){util.debug(err+'. '+err.stack);callFinished(new EventEmitter());});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 http=require('http');var url=require('url');var util=require('../util');var EventEmitter=require('events').EventEmitter;var qputs=util.qputs;} -var DEFAULT_RETRY_INTERVAL_MS=2000;var Endpoint=exports.Endpoint=function Endpoint(server){EventEmitter.call(this);var self=this,basepath='',updateUrl=function(){self.url=url.format({protocol:'http',hostname:server.hostname,port:server.port,pathname:basepath});};self.__defineGetter__('basepath',function(){return basepath;});self.__defineSetter__('basepath',function(val){basepath=val;updateUrl();});self.id=util.uid();self.basepath='/remote/'+self.id;self.route='^'+self.basepath+'/?';self.server=server;self.methodNames=[];self.methods={};self.context={};self.setStaticParams([]);self.state='initialized';self.handler_=self.handle.bind(self);self.server.on('start',function(hostname,port){updateUrl();});};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;} +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){EventEmitter.call(this);var self=this,basepath='',updateUrl=function(){self.url=url.format({protocol:'http',hostname:server.hostname,port:server.port,pathname:basepath});};self.__defineGetter__('basepath',function(){return basepath;});self.__defineSetter__('basepath',function(val){basepath=val;updateUrl();});self.id=util.uid();self.basepath='/remote/'+self.id;self.route='^'+self.basepath+'/?';self.server=server;self.methodNames=[];self.methods={};self.context={};self.setStaticParams([]);self.state='initialized';self.handler_=self.handle.bind(self);self.server.on('start',function(hostname,port){updateUrl();});};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.server.addRoute(this.route,this.handler_);if(this.methods['setup']){this.methods['setup'].apply(this.context,this.staticParams_);} this.state='started';this.emit('start');};Endpoint.prototype.destroy=function(){if(this.state!=='started'){return;} this.server.removeRoute(this.route,this.handler_);this.state='destroyed';this.emit('end');};Endpoint.prototype.handle=function(path,req,res){var self=this;if(path===self.basepath){if(req.method==='DELETE'){self.destroy();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='undefined';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?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 EndpointClient=exports.EndpointClient=function EndpointClient(host,port,basepath){EventEmitter.call(this);this.host=host;this.port=port;this.basepath=basepath||'';this.methodNames=[];this.state='disconnected';this.retryInterval=DEFAULT_RETRY_INTERVAL_MS;this.setStaticParams([]);};util.inherits(EndpointClient,EventEmitter);EndpointClient.prototype.start=function(){if(this.state!=='disconnected'&&this.state!=='reconnect'){return;} -var self=this;clearTimeout(self.retryTimeoutId);self.retryTimeoutId=null;if(self.client){self.client.destroy();} -self.client=http.createClient(self.port,self.host);self.client.on('error',function(err){qputs('Communication error with "'+self.host+':'+self.port+'". Reconnecting: '+err.toString());self.state='reconnect';self.client.destroy();self.client=null;self.retryTimeoutId=setTimeout(self.start.bind(self),self.retryInterval);self.emit('clientError',err);});self.state='connected';self.emit('connect');};EndpointClient.prototype.end=function(){if(this.state!=='connected'&&this.state!=='reconnect'){return;} -clearTimeout(this.retryTimeoutId);this.client.destroy();this.state='disconnected';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(){if(self.state!=='connected'){throw new Error('Cannot call method before connect');} -var req=self.client.request('POST',self.basepath+'/'+name),params=self.staticParams_.concat(util.argarray(arguments));req.end(JSON.stringify(params));return req;};self.methodNames.push(name);};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('./endpoint').EndpointClient;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} -var Slave=exports.Slave=function Slave(id,host,port,masterEndpoint){EventEmitter.call(this);this.id=id;this.client=new EndpointClient(host,port);this.masterEndpoint=masterEndpoint;this.methodDefs=[];this.state='stopped';};util.inherits(Slave,EventEmitter);Slave.prototype.start=function(){var self=this;self.client.on('connect',function(){if(!self.basepath){var req=self.client.rawRequest('POST','/remote');req.end(JSON.stringify({id:self.id,master:self.masterEndpoint.url,masterMethods:self.masterEndpoint.methodNames,slaveMethods:self.methodDefs,updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS}));req.on('response',function(res){self.client.basepath=url.parse(res.headers['location']).pathname;self.state='started';self.emit('start');});}});self.client.on('clientError',function(e){self.emit('clientError',e);});self.client.start();self.state='connecting';};Slave.prototype.end=function(){var self=this,req=self.client.rawrequest('DELETE',self.client.basepath);req.end();req.on('response',function(res){if(res.statusCode===204){self.client.end();self.client.basepath='';self.state='stopped';self.emit('end');}else{self.emit('clientError',new Error('Error stopping slave.'),res);}});};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 Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(Slaves,EventEmitter);Slaves.prototype.add=function(host,port){var self=this,id=host+':'+port,slave=new Slave(id,host,port,self.masterEndpoint);self.slaves.push(slave);self[id]=slave;self[id].on('clientError',function(err){self.emit('clientError',err,slave);});self[id].on('start',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='started'){return;}});self.emit('start');});self[id].on('end',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='stopped'){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');var Endpoint=endpoint.Endpoint;var EndpointClient=endpoint.EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} -var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;var endpoint=this.createEndpoint_(server,spec.slaveMethods),masterClient=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.url=endpoint.url;this.masterClient_=masterClient;this.masterClient_.start();this.slaveEndpoint_=endpoint;this.slaveEndpoint_.context.state='initialized';this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.start();this.slaveEndpoint_.on('end',this.end.bind(this));this.updateInterval=(spec.updateInterval>=0)?spec.updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){if(this.slaveEndpoint_.state==='started'){this.slaveEndpoint_.destroy();} -if(this.masterClient_.state==='connected'||this.masterClient_.state==='reconnect'){this.masterClient_.end();} +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.basepath=basepath||'';this.methodNames=[];this.retryInterval=DEFAULT_RETRY_INTERVAL_MS;this.setStaticParams([]);this.state='initialized';this.connect_();};util.inherits(EndpointClient,EventEmitter);EndpointClient.prototype.connect_=function(){var self=this;if(self.state!=='initialized'&&self.state!=='reconnect'){return;} +self.retryTimeoutId=clearTimeout(self.retryTimeoutId);if(self.client){self.client.destroy();} +self.client=http.createClient(self.port,self.host);self.client.on('error',function(err){qputs('Communication error with "'+self.host+':'+self.port+'". Reconnecting: '+err.toString());self.state='reconnect';self.emit('clientError',err);});self.state='connected';self.emit('connect');};EndpointClient.prototype.destroy=function(){if(this.state!=='connected'&&this.state!=='reconnect'){return;} +clearTimeout(this.retryTimeoutId);this.client.destroy();this.state='initialized';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(){if(self.state!=='connected'&&this.state!=='reconnect'){throw new Error('Cannot call method before connect');} +var req=self.client.request('POST',self.basepath+'/'+name),params=self.staticParams_.concat(util.argarray(arguments));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){EventEmitter.call(this);this.id=id;this.client=new EndpointClient(host,port);this.client.on('clientError',this.emit.bind(this,'clientError'));this.masterEndpoint=masterEndpoint;this.methodDefs=[];this.state='initialized';};util.inherits(Slave,EventEmitter);Slave.prototype.start=function(){if(!this.basepath){var self=this,req=self.client.rawRequest('POST','/remote');req.end(JSON.stringify({id:self.id,master:self.masterEndpoint.url,masterMethods:self.masterEndpoint.methodNames,slaveMethods:self.methodDefs,updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS}));req.on('response',function(res){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);req.end();req.on('response',function(res){if(res.statusCode!==204){self.emit('clientError',new Error('Error stopping slave.'),res);} +self.client.destroy();self.client.basepath='';self.state='initialized';self.emit('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 Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};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.slaves.push(slave);self[id]=slave;self[id].on('clientError',function(err){self.emit('clientError',err,slave);});self[id].on('start',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='started'){return;}});self.emit('start');});self[id].on('end',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='stopped'){return;}});self.emit('end');});};Slaves.prototype.defineMethod=function(name,fun){var self=this;self.slaves.forEach(function(slave){slave.defineMethod(name,fun);});self[name]=function(){var args=arguments;return self.slaves.map(function(s){return s[name].apply(s,args);});};};Slaves.prototype.start=function(){this.slaves.forEach(function(s){s.start();});};Slaves.prototype.end=function(){this.slaves.forEach(function(s){s.end();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;var endpoint=this.createEndpoint_(server,spec.slaveMethods),masterClient=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.url=endpoint.url;this.masterClient_=masterClient;this.slaveEndpoint_=endpoint;this.slaveEndpoint_.context.state='initialized';this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start')});this.slaveEndpoint_.on('end',this.end.bind(this));this.updateInterval=(spec.updateInterval>=0)?spec.updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;this.slaveEndpoint_.start();};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){if(this.slaveEndpoint_.state==='started'){this.slaveEndpoint_.destroy();} +if(this.masterClient_.state==='connected'||this.masterClient_.state==='reconnect'){this.masterClient_.destroy();} this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_&&this.masterClient_.state==='connected'){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.destroy();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]);return masterClient;};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var slave=require('./slave');var endpoint=require('./endpoint');var Endpoint=endpoint.Endpoint;var EventEmitter=require('events').EventEmitter;var SlaveNode=require('./slavenode').SlaveNode;var Slaves=slave.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);var self=this,masterSpec=spec.master||{},slavesSpec=spec.slaves||{hosts:[]};self.masterEndpoint=new Endpoint(HTTP_SERVER);self.slaves=new Slaves(self.masterEndpoint,spec.pingInterval);self.slaveState_={};self.masterEndpoint.setStaticParams([self.slaves]);self.masterEndpoint.defineMethod('updateSlaveState_',self.updateSlaveState_.bind(self));util.forEach(masterSpec,function(method,val){self.masterEndpoint.defineMethod(method,val);});slavesSpec.hosts.forEach(function(h){var parts=h.split(':'),host=parts[0],port=Number(parts[1])||8000;self.slaves.add(host,port);});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.state='initialized';self.slaves.on('start',function(){self.state='started';self.emit('start');});};util.inherits(Cluster,EventEmitter);Cluster.prototype.start=function(){if(!HTTP_SERVER.running){throw new Error('A Cluster can only be started after the global HTTP_SERVER is running.');} -var self=this;self.masterEndpoint.start();self.slaves.start();};Cluster.prototype.end=function(){this.masterEndpoint.destroy();this.slaves.end();this.state='stopped';this.emit('end');};Cluster.prototype.updateSlaveState_=function(slaves,slaveId,state){var slave=slaves[slaveId];if(slave){var previousState=this.slaveState_[slaveId];this.slaveState_[slaveId]=state;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){allSlavesInSameState=false;}});if(allSlavesInSameState){this.emit(state);}};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var SlaveNode=require('./cluster').SlaveNode;var HTTP_SERVER=require('../http').HTTP_SERVER;} -var createSlave_,createMaster_,slaveNodes=[];HTTP_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(HTTP_SERVER,body);}catch(e){res.writeHead(400);res.end(e.toString());return;} -slaveNode.on('end',function(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});slaveNodes.push(slaveNode);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();}}); +masterClient.setStaticParams([this.id]);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;} +slaveNode.on('end',function(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});slaveNodes.push(slaveNode);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 slave=require('./slave');var Endpoint=require('./endpoint').Endpoint;var EventEmitter=require('events').EventEmitter;var SlaveNode=require('./slavenode').SlaveNode;var Slaves=slave.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);var self=this,masterSpec=spec.master||{},slavesSpec=spec.slaves||{hosts:[]};self.server=spec.server||HTTP_SERVER;self.masterEndpoint=new Endpoint(self.server);self.slaves=new Slaves(self.masterEndpoint,spec.pingInterval);self.slaveState_={};self.masterEndpoint.setStaticParams([self.slaves]);self.masterEndpoint.defineMethod('updateSlaveState_',self.updateSlaveState_.bind(self));util.forEach(masterSpec,function(method,val){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.on('start',function(){self.state='started';self.emit('start');});self.slaves.on('end',function(){self.masterEndpoint.destroy();self.state='stopped';self.emit('end');});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.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();};Cluster.prototype.end=function(){this.slaves.end();};Cluster.prototype.updateSlaveState_=function(slaves,slaveId,state){var slave=slaves[slaveId];if(slave){var previousState=this.slaveState_[slaveId];this.slaveState_[slaveId]=state;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){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); diff --git a/lib/remote/cluster.js b/lib/remote/cluster.js index 3520c3b..61c7af2 100644 --- a/lib/remote/cluster.js +++ b/lib/remote/cluster.js @@ -2,8 +2,7 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var util = require('../util'); var slave = require('./slave'); -var endpoint = require('./endpoint'); -var Endpoint = endpoint.Endpoint; +var Endpoint = require('./endpoint').Endpoint; var EventEmitter = require('events').EventEmitter; var SlaveNode = require('./slavenode').SlaveNode; var Slaves = slave.Slaves; @@ -22,17 +21,40 @@ var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; setup: function(master) { ... } slave_remote_function_1: function(master, args...) { ... } }, - pingInterval: 2000 + pingInterval: 2000, + server: HttpServer instance (defaults to global HTTP_SERVER) } -Calling cluster.start() will register a master handler on this host. It will connect to every slave, -asking each slave to 1) execute the setup() function, 2) report its current status this host every -pingInterval milliseconds. Calling cluster.slave_remote_function_1(), will execute -slave_remote_function_1 on every slave. +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. -Within master_remote_function_1 and slave_remote_function_1, this points at an initially empty object -that should be used to store state. +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. +- 'running', 'done': when all the slaves 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. +- 'stopped': All of the slaves have been properly shutdown and the master endpoint removed. */ var Cluster = exports.Cluster = function Cluster(spec) { EventEmitter.call(this); @@ -41,48 +63,69 @@ var Cluster = exports.Cluster = function Cluster(spec) { masterSpec = spec.master || {}, slavesSpec = spec.slaves || { hosts:[] }; - self.masterEndpoint = new Endpoint(HTTP_SERVER); + self.server = spec.server || HTTP_SERVER; + self.masterEndpoint = new Endpoint(self.server); self.slaves = new Slaves(self.masterEndpoint, spec.pingInterval); self.slaveState_ = {}; + // Define all master methods on the local endpoint self.masterEndpoint.setStaticParams([self.slaves]); self.masterEndpoint.defineMethod('updateSlaveState_', self.updateSlaveState_.bind(self)); util.forEach(masterSpec, function(method, val) { self.masterEndpoint.defineMethod(method, val); }); - slavesSpec.hosts.forEach(function(h) { - var parts = h.split(':'), host = parts[0], port = Number(parts[1]) || 8000; - self.slaves.add(host, port); - }); + // 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); }; } }); - - self.state = 'initialized'; + + // 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.destroy(); + self.state = 'stopped'; + self.emit('end'); + }); + + // 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); +/** 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 (!HTTP_SERVER.running) { - throw new Error('A Cluster can only be started after the global HTTP_SERVER is running.'); + if (!this.server.running) { + throw new Error('A Cluster can only be started after it has emitted \'init\'.'); } - var self = this; - self.masterEndpoint.start(); - self.slaves.start(); + this.masterEndpoint.start(); + this.slaves.start(); + // 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.masterEndpoint.destroy(); this.slaves.end(); - this.state = 'stopped'; - this.emit('end'); + // this.slaves 'end' event handler emits 'end', destroys masterEndpoint & updates state }; +/** 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) { diff --git a/lib/remote/endpoint.js b/lib/remote/endpoint.js index 4a92d93..4e98832 100644 --- a/lib/remote/endpoint.js +++ b/lib/remote/endpoint.js @@ -1,22 +1,30 @@ /*jslint sub: true */ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { -var http = require('http'); var url = require('url'); var util = require('../util'); var EventEmitter = require('events').EventEmitter; -var qputs = util.qputs; } -var DEFAULT_RETRY_INTERVAL_MS = 2000; - /** 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(...). */ +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 +- 'destroyed': This endpoint has been terminated because Endpoint.destroy() was explicitly called or a + remote host issued a DELETE request at the base URL. +*/ var Endpoint = exports.Endpoint = function Endpoint(server) { EventEmitter.call(this); @@ -150,80 +158,4 @@ Endpoint.prototype.handle = function(path, req, res) { res.writeHead(405); res.end(); } -}; - - -/** 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.start(); - 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). */ -var EndpointClient = exports.EndpointClient = function EndpointClient(host, port, basepath) { - EventEmitter.call(this); - this.host = host; - this.port = port; - this.basepath = basepath || ''; - this.methodNames = []; - this.state = 'disconnected'; - this.retryInterval = DEFAULT_RETRY_INTERVAL_MS; - this.setStaticParams([]); -}; -util.inherits(EndpointClient, EventEmitter); -/** Establish an HTTP connection to the target server. Emit 'connect' when connected. */ -EndpointClient.prototype.start = function() { - if (this.state !== 'disconnected' && this.state !== 'reconnect') { return; } - - var self = this; - - clearTimeout(self.retryTimeoutId); - self.retryTimeoutId = null; - - if (self.client) { self.client.destroy(); } - self.client = http.createClient(self.port, self.host); - self.client.on('error', function(err) { - qputs('Communication error with "'+ self.host +':'+ self.port +'". Reconnecting: '+ err.toString()); - self.state = 'reconnect'; - self.client.destroy(); - self.client = null; - self.retryTimeoutId = setTimeout(self.start.bind(self), self.retryInterval); - self.emit('clientError', err); - }); - self.state = 'connected'; - self.emit('connect'); -}; -/** Terminate the HTTP connection. */ -EndpointClient.prototype.end = function() { - if (this.state !== 'connected' && this.state !== 'reconnect') { return; } - clearTimeout(this.retryTimeoutId); - this.client.destroy(); - this.state = 'disconnected'; - 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() { - if (self.state !== 'connected') { throw new Error('Cannot call method before connect'); } - var req = self.client.request('POST', self.basepath + '/' + name), - params = self.staticParams_.concat(util.argarray(arguments)); - - req.end(JSON.stringify(params)); - return req; - }; - self.methodNames.push(name); }; \ No newline at end of file diff --git a/lib/remote/endpointclient.js b/lib/remote/endpointclient.js new file mode 100644 index 0000000..d371e42 --- /dev/null +++ b/lib/remote/endpointclient.js @@ -0,0 +1,93 @@ +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. +- 'end': The underlying HTTP connect has been terminated. No more events will be emitted. + +EndpointClient.state can be: +- 'initialized': A connection to the remote endpoint has not yet been established +- 'connected': Connection to the remote endpoint is established +- 'reconnect': An error occured in the HTTP connection. It will be re-established if possible. +*/ +var EndpointClient = exports.EndpointClient = function EndpointClient(host, port, basepath) { + EventEmitter.call(this); + this.host = host; + this.port = port; + this.basepath = basepath || ''; + this.methodNames = []; + this.retryInterval = DEFAULT_RETRY_INTERVAL_MS; + this.setStaticParams([]); + this.state = 'initialized'; + this.connect_(); +}; +util.inherits(EndpointClient, EventEmitter); +/** Establish an HTTP connection to the target server. Emit 'connect' when connected. */ +EndpointClient.prototype.connect_ = function() { + var self = this; + if (self.state !== 'initialized' && self.state !== 'reconnect') { return; } + + self.retryTimeoutId = clearTimeout(self.retryTimeoutId); + + if (self.client) { self.client.destroy(); } + self.client = http.createClient(self.port, self.host); + self.client.on('error', function(err) { + qputs('Communication error with "'+ self.host +':'+ self.port +'". Reconnecting: '+ err.toString()); + self.state = 'reconnect'; + self.emit('clientError', err); + }); + self.state = 'connected'; + self.emit('connect'); +}; +/** Terminate the HTTP connection. */ +EndpointClient.prototype.destroy = function() { + if (this.state !== 'connected' && this.state !== 'reconnect') { return; } + clearTimeout(this.retryTimeoutId); + this.client.destroy(); + this.state = 'initialized'; + 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() { + if (self.state !== 'connected' && this.state !== 'reconnect') { + throw new Error('Cannot call method before connect'); + } + var req = self.client.request('POST', self.basepath + '/' + name), + params = self.staticParams_.concat(util.argarray(arguments)); + + req.end(JSON.stringify(params)); + return req; + }; + self.methodNames.push(name); + return self; +}; \ No newline at end of file diff --git a/lib/remote/http.js b/lib/remote/http.js index aeeb6c9..ab379b9 100644 --- a/lib/remote/http.js +++ b/lib/remote/http.js @@ -1,46 +1,8 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { -var util = require('../util'); -var SlaveNode = require('./cluster').SlaveNode; +var installRemoteHandler = require('./slavenode').installRemoteHandler; var HTTP_SERVER = require('../http').HTTP_SERVER; } -var createSlave_, createMaster_, slaveNodes = []; - -/** Global /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 -cluster.js#SlaveNode for a description of the handler defintion. */ -HTTP_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(HTTP_SERVER, body); - } catch(e) { - res.writeHead(400); - res.end(e.toString()); - return; - } - - slaveNode.on('end', function() { - slaveNodes = slaveNodes.filter(function(s) { return s !== slaveNode; }); - }); - slaveNodes.push(slaveNode); - - 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 +// 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 index 4f65185..52d567a 100644 --- a/lib/remote/index.js +++ b/lib/remote/index.js @@ -1,9 +1,10 @@ var slave = require('./slave'); -var endpoint = require('./endpoint'); +var slavenode = require('./slavenode'); exports.Cluster = require('./cluster').Cluster; exports.Slaves = slave.Slaves; exports.Slave = slave.Slave; -exports.Endpoint = endpoint.Endpoint; -exports.EndpointClient = endpoint.EndpointClient; +exports.SlaveNode = slavenode.SlaveNode; +exports.Endpoint = require('./endpoint').Endpoint; +exports.EndpointClient = require('./endpointclient').EndpointClient; require('./http'); \ No newline at end of file diff --git a/lib/remote/slave.js b/lib/remote/slave.js index 69f1d2a..d19acfe 100644 --- a/lib/remote/slave.js +++ b/lib/remote/slave.js @@ -4,59 +4,82 @@ if (!BUILD_AS_SINGLE_FILE) { var url = require('url'); var util = require('../util'); var EventEmitter = require('events').EventEmitter; -var EndpointClient = require('./endpoint').EndpointClient; +var EndpointClient = require('./endpointclient').EndpointClient; var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; } -// ------------------------- -// Slave -// ------------------------- +/** 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: +- 'clientError', error: The underlying HTTP connection returned an error. The connection will be retried. +- 'start': The remote instance accepted the slave definition and slave methods can now be called. +- 'stopped': 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) { EventEmitter.call(this); this.id = id; this.client = new EndpointClient(host, port); + this.client.on('clientError', this.emit.bind(this, 'clientError')); this.masterEndpoint = masterEndpoint; this.methodDefs = []; - this.state = 'stopped'; + 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() { - var self = this; - self.client.on('connect', function() { - if (!self.basepath) { - var req = self.client.rawRequest('POST', '/remote'); - req.end(JSON.stringify({ - id: self.id, - master: self.masterEndpoint.url, - masterMethods: self.masterEndpoint.methodNames, - slaveMethods: self.methodDefs, - updateInterval: NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS - })); - req.on('response', function(res) { - self.client.basepath = url.parse(res.headers['location']).pathname; - self.state = 'started'; - self.emit('start'); - }); - } - }); - self.client.on('clientError', function(e) { self.emit('clientError', e); }); - self.client.start(); - self.state = 'connecting'; + if (!this.basepath) { + var self = this, + req = self.client.rawRequest('POST', '/remote'); + + req.end(JSON.stringify({ + id: self.id, + master: self.masterEndpoint.url, + masterMethods: self.masterEndpoint.methodNames, + slaveMethods: self.methodDefs, + updateInterval: NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS + })); + req.on('response', function(res) { + 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); + var self = this, req = self.client.rawRequest('DELETE', self.client.basepath); req.end(); req.on('response', function(res) { - if (res.statusCode === 204) { - self.client.end(); - self.client.basepath = ''; - self.state = 'stopped'; - self.emit('end'); - } else { + if (res.statusCode !== 204) { self.emit('clientError', new Error('Error stopping slave.'), res); } + + self.client.destroy(); + self.client.basepath = ''; + self.state = 'initialized'; + self.emit('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); @@ -65,9 +88,15 @@ Slave.prototype.defineMethod = function(name, fun) { }; -// ------------------------- -// Slaves -// ------------------------- +/** 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: +- 'clientError', error, slave: The underlying HTTP connection for this slave returned an error. The + connection will be retried. +- '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; @@ -75,8 +104,12 @@ var Slaves = exports.Slaves = function Slaves(masterEndpoint, pingInterval) { this.pingInterval = pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; }; util.inherits(Slaves, EventEmitter); -Slaves.prototype.add = function(host, port) { +/** 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); @@ -98,6 +131,7 @@ Slaves.prototype.add = function(host, port) { self.emit('end'); }); }; +/** Define a method on all the slaves */ Slaves.prototype.defineMethod = function(name, fun) { var self = this; @@ -110,9 +144,11 @@ Slaves.prototype.defineMethod = function(name, fun) { 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/remote/slavenode.js b/lib/remote/slavenode.js index 3fd51c1..4aa2450 100644 --- a/lib/remote/slavenode.js +++ b/lib/remote/slavenode.js @@ -2,16 +2,16 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var url = require('url'); var util = require('../util'); -var endpoint = require('./endpoint'); -var Endpoint = endpoint.Endpoint; -var EndpointClient = endpoint.EndpointClient; +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 is instantiated on each slave node in the Cluster. When a Slave object is -started, it sends a slave specification to the target machine. This specification is used to create -SlaveNode. It contains: +/** 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, @@ -24,6 +24,10 @@ SlaveNode. It contains: } 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 +- '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); @@ -36,13 +40,14 @@ var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { this.url = endpoint.url; this.masterClient_ = masterClient; - this.masterClient_.start(); this.slaveEndpoint_ = endpoint; this.slaveEndpoint_.context.state = 'initialized'; this.slaveEndpoint_.setStaticParams([this.masterClient_]); - this.slaveEndpoint_.start(); + this.slaveEndpoint_.on('start', function() { this.emit.bind(this, 'start') }); this.slaveEndpoint_.on('end', this.end.bind(this)); this.updateInterval = (spec.updateInterval >= 0) ? spec.updateInterval : NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; + + this.slaveEndpoint_.start(); }; util.inherits(SlaveNode, EventEmitter); SlaveNode.prototype.end = function() { @@ -50,7 +55,7 @@ SlaveNode.prototype.end = function() { this.slaveEndpoint_.destroy(); } if (this.masterClient_.state === 'connected' || this.masterClient_.state === 'reconnect') { - this.masterClient_.end(); + this.masterClient_.destroy(); } this.emit('end'); }; @@ -92,4 +97,46 @@ SlaveNode.prototype.createMasterClient_ = function(masterUrl, methods) { masterClient.setStaticParams([this.id]); 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; + } + + slaveNode.on('end', function() { + slaveNodes = slaveNodes.filter(function(s) { return s !== slaveNode; }); + }); + slaveNodes.push(slaveNode); + + 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 From a710f1c522b9b27aec0b8e5fb944949f6cb7d8af Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Thu, 25 Nov 2010 12:14:25 -0500 Subject: [PATCH 26/41] endpointclient emits clientError on non-200 status codes add option to specify master host name for an endpoint detect unresponsive slaves --- examples/remote.test.js | 10 +++++++ lib/http.js | 2 +- lib/nodeload.js | 20 +++++++------ lib/remote/cluster.js | 58 +++++++++++++++++++++++++++++------- lib/remote/endpoint.js | 13 +++++--- lib/remote/endpointclient.js | 10 +++++-- lib/remote/slave.js | 4 +-- lib/remote/slavenode.js | 2 +- lib/util.js | 3 -- 9 files changed, 90 insertions(+), 32 deletions(-) diff --git a/examples/remote.test.js b/examples/remote.test.js index 6eebbea..c810279 100755 --- a/examples/remote.test.js +++ b/examples/remote.test.js @@ -45,6 +45,16 @@ cluster.on('init', function() { cluster.on('done', function() { console.log('All slaves done'); }); + cluster.on('slaveError', function(slave, err) { + if (err === null) { + console.log('Unresponsive slave detected: ' + slave.id); + } else { + console.log('Slave error from ' + slave.id + ': ' + err.toString()); + if (cluster.state === 'stopping') { + process.exit(1); + } + } + }); cluster.on('slaveState', function(slave, state) { if (state === 'error') { console.log('Slave "' + slave.id + '" encountered an error.'); diff --git a/lib/http.js b/lib/http.js index 686fb42..f529585 100644 --- a/lib/http.js +++ b/lib/http.js @@ -33,7 +33,7 @@ HttpServer.prototype.start = function(port, hostname) { var self = this; port = port || 8000; - self.hostname = hostname || util.localHostname(); + self.hostname = hostname || 'localhost'; self.port = port; self.server = http.createServer(function(req, res) { self.route_(req, res); }); self.server.listen(port, hostname); diff --git a/lib/nodeload.js b/lib/nodeload.js index 4323ab5..332462d 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -9,7 +9,7 @@ else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);v 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],args);};util.localHostname=function(){return'localhost';};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&&milliseconds0&&millisecondsthis.max||this.max===-1){this.max=item;} @@ -78,7 +78,7 @@ return this.monitors[monitorName].start(args);};MonitorGroup.prototype.monitorOb 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||util.localHostname();self.port=port;self.server=http.createServer(function(req,res){self.route_(req,res);});self.server.listen(port,hostname);self.emit('start',self.hostname,self.port);return self;};HttpServer.prototype.stop=function(){if(!this.running){return;} +this.running=true;var self=this;port=port||8000;self.hostname=hostname||'localhost';self.port=port;self.server=http.createServer(function(req,res){self.route_(req,res);});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.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;i0){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){EventEmitter.call(this);var self=this,basepath='',updateUrl=function(){self.url=url.format({protocol:'http',hostname:server.hostname,port:server.port,pathname:basepath});};self.__defineGetter__('basepath',function(){return basepath;});self.__defineSetter__('basepath',function(val){basepath=val;updateUrl();});self.id=util.uid();self.basepath='/remote/'+self.id;self.route='^'+self.basepath+'/?';self.server=server;self.methodNames=[];self.methods={};self.context={};self.setStaticParams([]);self.state='initialized';self.handler_=self.handle.bind(self);self.server.on('start',function(hostname,port){updateUrl();});};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;} +var Endpoint=exports.Endpoint=function Endpoint(server,hostAndPort){EventEmitter.call(this);var self=this,hostAndPort=hostAndPort||'';parts=hostAndPort.split(':'),hostname=parts[0],port=parts[1]||8000,basepath='',updateUrl=function(){self.url=url.format({protocol:'http',hostname:hostname||server.hostname,port:port||server.port,pathname:basepath});self.route='^'+basepath+'/?';};self.__defineGetter__('basepath',function(){return basepath;});self.__defineSetter__('basepath',function(val){if(this.state==='started'){throw new Error('Cannot update basepath while endpoint is running.');} +basepath=val;updateUrl();});self.id=util.uid();self.basepath='/remote/'+self.id;self.server=server;self.methodNames=[];self.methods={};self.context={};self.setStaticParams([]);self.state='initialized';self.handler_=self.handle.bind(self);self.server.on('start',function(hostname,port){updateUrl();});};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.server.addRoute(this.route,this.handler_);if(this.methods['setup']){this.methods['setup'].apply(this.context,this.staticParams_);} this.state='started';this.emit('start');};Endpoint.prototype.destroy=function(){if(this.state!=='started'){return;} this.server.removeRoute(this.route,this.handler_);this.state='destroyed';this.emit('end');};Endpoint.prototype.handle=function(path,req,res){var self=this;if(path===self.basepath){if(req.method==='DELETE'){self.destroy();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='undefined';try{params=JSON.parse(params);}catch(e1){res.writeHead(400);res.end();return;} @@ -104,17 +105,18 @@ params=(params instanceof Array)?params:[params];ret=self.methods[method].apply( 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.basepath=basepath||'';this.methodNames=[];this.retryInterval=DEFAULT_RETRY_INTERVAL_MS;this.setStaticParams([]);this.state='initialized';this.connect_();};util.inherits(EndpointClient,EventEmitter);EndpointClient.prototype.connect_=function(){var self=this;if(self.state!=='initialized'&&self.state!=='reconnect'){return;} self.retryTimeoutId=clearTimeout(self.retryTimeoutId);if(self.client){self.client.destroy();} -self.client=http.createClient(self.port,self.host);self.client.on('error',function(err){qputs('Communication error with "'+self.host+':'+self.port+'". Reconnecting: '+err.toString());self.state='reconnect';self.emit('clientError',err);});self.state='connected';self.emit('connect');};EndpointClient.prototype.destroy=function(){if(this.state!=='connected'&&this.state!=='reconnect'){return;} +self.client=http.createClient(self.port,self.host);self.client.on('error',function(err){console.log(this.host+'x'+this.port) +self.state='reconnect';self.emit('clientError',err);});self.state='connected';self.emit('connect');};EndpointClient.prototype.destroy=function(){if(this.state!=='connected'&&this.state!=='reconnect'){return;} clearTimeout(this.retryTimeoutId);this.client.destroy();this.state='initialized';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(){if(self.state!=='connected'&&this.state!=='reconnect'){throw new Error('Cannot call method before connect');} -var req=self.client.request('POST',self.basepath+'/'+name),params=self.staticParams_.concat(util.argarray(arguments));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 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){EventEmitter.call(this);this.id=id;this.client=new EndpointClient(host,port);this.client.on('clientError',this.emit.bind(this,'clientError'));this.masterEndpoint=masterEndpoint;this.methodDefs=[];this.state='initialized';};util.inherits(Slave,EventEmitter);Slave.prototype.start=function(){if(!this.basepath){var self=this,req=self.client.rawRequest('POST','/remote');req.end(JSON.stringify({id:self.id,master:self.masterEndpoint.url,masterMethods:self.masterEndpoint.methodNames,slaveMethods:self.methodDefs,updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS}));req.on('response',function(res){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);req.end();req.on('response',function(res){if(res.statusCode!==204){self.emit('clientError',new Error('Error stopping slave.'),res);} -self.client.destroy();self.client.basepath='';self.state='initialized';self.emit('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 Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};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.slaves.push(slave);self[id]=slave;self[id].on('clientError',function(err){self.emit('clientError',err,slave);});self[id].on('start',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='started'){return;}});self.emit('start');});self[id].on('end',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='stopped'){return;}});self.emit('end');});};Slaves.prototype.defineMethod=function(name,fun){var self=this;self.slaves.forEach(function(slave){slave.defineMethod(name,fun);});self[name]=function(){var args=arguments;return self.slaves.map(function(s){return s[name].apply(s,args);});};};Slaves.prototype.start=function(){this.slaves.forEach(function(s){s.start();});};Slaves.prototype.end=function(){this.slaves.forEach(function(s){s.end();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} -var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;var endpoint=this.createEndpoint_(server,spec.slaveMethods),masterClient=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.url=endpoint.url;this.masterClient_=masterClient;this.slaveEndpoint_=endpoint;this.slaveEndpoint_.context.state='initialized';this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start')});this.slaveEndpoint_.on('end',this.end.bind(this));this.updateInterval=(spec.updateInterval>=0)?spec.updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;this.slaveEndpoint_.start();};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){if(this.slaveEndpoint_.state==='started'){this.slaveEndpoint_.destroy();} +self.client.destroy();self.client.basepath='';self.state='initialized';self.emit('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 Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};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.slaves.push(slave);self[id]=slave;self[id].on('clientError',function(err){self.emit('clientError',slave,err);});self[id].on('start',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='started'){return;}});self.emit('start');});self[id].on('end',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='stopped'){return;}});self.emit('end');});};Slaves.prototype.defineMethod=function(name,fun){var self=this;self.slaves.forEach(function(slave){slave.defineMethod(name,fun);});self[name]=function(){var args=arguments;return self.slaves.map(function(s){return s[name].apply(s,args);});};};Slaves.prototype.start=function(){this.slaves.forEach(function(s){s.start();});};Slaves.prototype.end=function(){this.slaves.forEach(function(s){s.end();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;var endpoint=this.createEndpoint_(server,spec.slaveMethods),masterClient=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.url=endpoint.url;this.masterClient_=masterClient;this.slaveEndpoint_=endpoint;this.slaveEndpoint_.context.state='initialized';this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.updateInterval=(spec.updateInterval>=0)?spec.updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;this.slaveEndpoint_.start();};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){if(this.slaveEndpoint_.state==='started'){this.slaveEndpoint_.destroy();} if(this.masterClient_.state==='connected'||this.masterClient_.state==='reconnect'){this.masterClient_.destroy();} this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_&&this.masterClient_.state==='connected'){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.destroy();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]);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;} slaveNode.on('end',function(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});slaveNodes.push(slaveNode);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 slave=require('./slave');var Endpoint=require('./endpoint').Endpoint;var EventEmitter=require('events').EventEmitter;var SlaveNode=require('./slavenode').SlaveNode;var Slaves=slave.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);var self=this,masterSpec=spec.master||{},slavesSpec=spec.slaves||{hosts:[]};self.server=spec.server||HTTP_SERVER;self.masterEndpoint=new Endpoint(self.server);self.slaves=new Slaves(self.masterEndpoint,spec.pingInterval);self.slaveState_={};self.masterEndpoint.setStaticParams([self.slaves]);self.masterEndpoint.defineMethod('updateSlaveState_',self.updateSlaveState_.bind(self));util.forEach(masterSpec,function(method,val){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.on('start',function(){self.state='started';self.emit('start');});self.slaves.on('end',function(){self.masterEndpoint.destroy();self.state='stopped';self.emit('end');});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.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();};Cluster.prototype.end=function(){this.slaves.end();};Cluster.prototype.updateSlaveState_=function(slaves,slaveId,state){var slave=slaves[slaveId];if(slave){var previousState=this.slaveState_[slaveId];this.slaveState_[slaveId]=state;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){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;} +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',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,pingInterval);self.slaveState_={};self.updateInterval=pingInterval*4;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.destroy();self.state='stopped';self.emit('end');});self.slaves.on('clientError',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.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();};Cluster.prototype.end=function(){this.state='stopping';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;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); diff --git a/lib/remote/cluster.js b/lib/remote/cluster.js index 61c7af2..2418b4e 100644 --- a/lib/remote/cluster.js +++ b/lib/remote/cluster.js @@ -14,6 +14,7 @@ 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: { @@ -36,8 +37,13 @@ Cluster emits the following events: - '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. -- 'running', 'done': when all the slaves report that they are in a 'running' or 'done' state. To set a - slave's the state, install a slave function: +- '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: { @@ -54,25 +60,32 @@ 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:[] }; + slavesSpec = spec.slaves || { hosts:[] }, + masterHost = spec.master && spec.master.host || 'localhost', + pingInterval = spec.pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; self.server = spec.server || HTTP_SERVER; - self.masterEndpoint = new Endpoint(self.server); - self.slaves = new Slaves(self.masterEndpoint, spec.pingInterval); + self.masterEndpoint = new Endpoint(self.server, masterHost); + self.slaves = new Slaves(self.masterEndpoint, pingInterval); self.slaveState_ = {}; + self.updateInterval = pingInterval * 4; // Define all master methods on the local endpoint self.masterEndpoint.setStaticParams([self.slaves]); self.masterEndpoint.defineMethod('updateSlaveState_', self.updateSlaveState_.bind(self)); util.forEach(masterSpec, function(method, val) { - self.masterEndpoint.defineMethod(method, val); + if (typeof val === 'function') { + self.masterEndpoint.defineMethod(method, val); + } }); // Send all slave methods definitions to the remote instances @@ -83,6 +96,13 @@ var Cluster = exports.Cluster = function Cluster(spec) { 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() { @@ -94,6 +114,9 @@ var Cluster = exports.Cluster = function Cluster(spec) { self.state = 'stopped'; self.emit('end'); }); + self.slaves.on('clientError', function(slave, err) { + self.emit('slaveError', slave, err); + }); // Cluster is initialized (can be started) once server is started if (self.server.running) { @@ -121,16 +144,31 @@ Cluster.prototype.start = function() { /** 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.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]; - this.slaveState_[slaveId] = state; + var previousState = this.slaveState_[slaveId].state; + this.slaveState_[slaveId].state = state; if (previousState !== state) { this.emit('slaveState', slave, state); @@ -144,8 +182,8 @@ Cluster.prototype.updateSlaveState_ = function(slaves, slaveId, state) { }; Cluster.prototype.emitWhenAllSlavesInState_ = function(state) { var allSlavesInSameState = true; - util.forEach(this.slaveState, function(id, s) { - if (s !== state) { + util.forEach(this.slaveState_, function(id, s) { + if (s.state !== state && s.alive) { allSlavesInSameState = false; } }); diff --git a/lib/remote/endpoint.js b/lib/remote/endpoint.js index 4e98832..6142d02 100644 --- a/lib/remote/endpoint.js +++ b/lib/remote/endpoint.js @@ -25,28 +25,33 @@ Endpoint.state can be: - 'destroyed': This endpoint has been terminated because Endpoint.destroy() was explicitly called or a remote host issued a DELETE request at the base URL. */ -var Endpoint = exports.Endpoint = function Endpoint(server) { +var Endpoint = exports.Endpoint = function Endpoint(server, hostAndPort) { EventEmitter.call(this); var self = this, + hostAndPort = hostAndPort || ''; + parts = hostAndPort.split(':'), + hostname = parts[0], + port = parts[1] || 8000, basepath = '', updateUrl = function() { self.url = url.format({ protocol: 'http', - hostname: server.hostname, - port: server.port, + hostname: hostname || server.hostname, + port: port || server.port, pathname: basepath }); + self.route = '^' + basepath + '/?'; }; self.__defineGetter__('basepath', function() { return basepath; }); self.__defineSetter__('basepath', function(val) { + if (this.state === 'started') { throw new Error('Cannot update basepath while endpoint is running.'); } basepath = val; updateUrl(); }); self.id = util.uid(); self.basepath = '/remote/' + self.id; - self.route = '^' + self.basepath + '/?'; self.server = server; self.methodNames = []; self.methods = {}; diff --git a/lib/remote/endpointclient.js b/lib/remote/endpointclient.js index d371e42..9b228cc 100644 --- a/lib/remote/endpointclient.js +++ b/lib/remote/endpointclient.js @@ -22,7 +22,8 @@ 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', 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. EndpointClient.state can be: @@ -52,7 +53,6 @@ EndpointClient.prototype.connect_ = function() { if (self.client) { self.client.destroy(); } self.client = http.createClient(self.port, self.host); self.client.on('error', function(err) { - qputs('Communication error with "'+ self.host +':'+ self.port +'". Reconnecting: '+ err.toString()); self.state = 'reconnect'; self.emit('clientError', err); }); @@ -85,7 +85,13 @@ EndpointClient.prototype.defineMethod = function(name) { 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); diff --git a/lib/remote/slave.js b/lib/remote/slave.js index d19acfe..9b6b508 100644 --- a/lib/remote/slave.js +++ b/lib/remote/slave.js @@ -92,7 +92,7 @@ Slave.prototype.defineMethod = function(name, fun) { together and method calls are sent to all the instances. Slaves emits the following events: -- 'clientError', error, slave: The underlying HTTP connection for this slave returned an error. The +- 'clientError', slave, error: The underlying HTTP connection for this slave returned an error. The connection will be retried. - 'start': All of the slave instances are running. - 'stopped': All of the slave instances have been stopped. */ @@ -116,7 +116,7 @@ Slaves.prototype.add = function(hostAndPort) { self.slaves.push(slave); self[id] = slave; self[id].on('clientError', function(err) { - self.emit('clientError', err, slave); + self.emit('clientError', slave, err); }); self[id].on('start', function() { util.forEach(self.slaves, function(id, s) { diff --git a/lib/remote/slavenode.js b/lib/remote/slavenode.js index 4aa2450..4cb2531 100644 --- a/lib/remote/slavenode.js +++ b/lib/remote/slavenode.js @@ -43,7 +43,7 @@ var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { this.slaveEndpoint_ = endpoint; this.slaveEndpoint_.context.state = 'initialized'; this.slaveEndpoint_.setStaticParams([this.masterClient_]); - this.slaveEndpoint_.on('start', function() { this.emit.bind(this, 'start') }); + this.slaveEndpoint_.on('start', function() { this.emit.bind(this, 'start'); }); this.slaveEndpoint_.on('end', this.end.bind(this)); this.updateInterval = (spec.updateInterval >= 0) ? spec.updateInterval : NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; diff --git a/lib/util.js b/lib/util.js index ce1dd34..b97eed4 100644 --- a/lib/util.js +++ b/lib/util.js @@ -55,9 +55,6 @@ util.forEach = function(obj, f) { util.argarray = function(args) { return (args instanceof Array) ? args : [].concat.apply([], args); }; -util.localHostname = function() { - return 'localhost'; -}; util.readStream = function(stream, callback) { var data = []; stream.on('data', function(chunk) { From f15d2add4bbf656c6f7482e9bb0b96e7d6a70cb0 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 29 Nov 2010 11:19:21 -0500 Subject: [PATCH 27/41] Add util.createReconnectingClient, replacing retry code in endpointclient Split slave & slaves into separate files Endpoint sets url & route on start() Endpoint can be started / stopped multiple times --- examples/remote.test.js | 3 + lib/nodeload.js | 42 ++++++------- lib/remote/cluster.js | 14 ++--- lib/remote/endpoint.js | 60 ++++++++---------- lib/remote/endpointclient.js | 31 +--------- lib/remote/index.js | 3 +- lib/remote/slave.js | 115 ++++++++--------------------------- lib/remote/slavenode.js | 37 ++++++----- lib/remote/slaves.js | 70 +++++++++++++++++++++ lib/util.js | 39 +++++++++++- test/util.test.js | 52 ++++++++++++++++ 11 files changed, 260 insertions(+), 206 deletions(-) create mode 100644 lib/remote/slaves.js create mode 100644 test/util.test.js diff --git a/examples/remote.test.js b/examples/remote.test.js index c810279..9c0e3d0 100755 --- a/examples/remote.test.js +++ b/examples/remote.test.js @@ -15,6 +15,9 @@ var cluster = new Cluster({ hosts: ['localhost:8001'], setup: function(master) { this.exec = require("child_process").exec; + master.on('error', function(err) { + console.log('Error communicating with master: ' + err.toString()); + }); }, exec: function(master, cmd) { var self = this; diff --git a/lib/nodeload.js b/lib/nodeload.js index 332462d..0e3905a 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -10,7 +10,9 @@ exports.quiet=function(){NODELOAD_CONFIG.QUIET=true;return exports;};exports.use 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],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(item0){timeoutId=setTimeout(function(){callFinished(new EventEmitter());},request.timeout);} +this.emit('end');};var extendClient=exports.extendClient=function(client){var extendedClient=Object.create(client);extendedClient.request=function(method,url){var request=client.apply(client,arguments),extendedRequest=Object.create(request),track=function(data){if(data){extendedRequest.emit('write',data);if(extendedRequest.captureRequestBody){extendedRequest.body+=data.toString();}}};extendedRequest.method=method;extendedRequest.path=url;extendedRequest.body='';extendedRequest.captureRequestBody=true;extendedRequest.write=function(data,encoding){track(data);return request.write.apply(request,arguments);};extendedRequest.end=function(data,encoding){track(data);return request.end.apply(request,arguments);};return extendedRequest;};return extendedClient;};var createClient=function(){return extendClient(http.createClient.apply(this,arguments));};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});}};client.on('error',function(err){util.debug(err+'. '+err.stack);callFinished(new EventEmitter());});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,hostAndPort=hostAndPort||'';parts=hostAndPort.split(':'),hostname=parts[0],port=parts[1]||8000,basepath='',updateUrl=function(){self.url=url.format({protocol:'http',hostname:hostname||server.hostname,port:port||server.port,pathname:basepath});self.route='^'+basepath+'/?';};self.__defineGetter__('basepath',function(){return basepath;});self.__defineSetter__('basepath',function(val){if(this.state==='started'){throw new Error('Cannot update basepath while endpoint is running.');} -basepath=val;updateUrl();});self.id=util.uid();self.basepath='/remote/'+self.id;self.server=server;self.methodNames=[];self.methods={};self.context={};self.setStaticParams([]);self.state='initialized';self.handler_=self.handle.bind(self);self.server.on('start',function(hostname,port){updateUrl();});};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.server.addRoute(this.route,this.handler_);if(this.methods['setup']){this.methods['setup'].apply(this.context,this.staticParams_);} -this.state='started';this.emit('start');};Endpoint.prototype.destroy=function(){if(this.state!=='started'){return;} -this.server.removeRoute(this.route,this.handler_);this.state='destroyed';this.emit('end');};Endpoint.prototype.handle=function(path,req,res){var self=this;if(path===self.basepath){if(req.method==='DELETE'){self.destroy();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='undefined';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?JSON.stringify(ret):'';}catch(e2){ret=e2.toString();status=500;} +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.basepath=basepath||'';this.methodNames=[];this.retryInterval=DEFAULT_RETRY_INTERVAL_MS;this.setStaticParams([]);this.state='initialized';this.connect_();};util.inherits(EndpointClient,EventEmitter);EndpointClient.prototype.connect_=function(){var self=this;if(self.state!=='initialized'&&self.state!=='reconnect'){return;} -self.retryTimeoutId=clearTimeout(self.retryTimeoutId);if(self.client){self.client.destroy();} -self.client=http.createClient(self.port,self.host);self.client.on('error',function(err){console.log(this.host+'x'+this.port) -self.state='reconnect';self.emit('clientError',err);});self.state='connected';self.emit('connect');};EndpointClient.prototype.destroy=function(){if(this.state!=='connected'&&this.state!=='reconnect'){return;} -clearTimeout(this.retryTimeoutId);this.client.destroy();this.state='initialized';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(){if(self.state!=='connected'&&this.state!=='reconnect'){throw new Error('Cannot call method before connect');} -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){EventEmitter.call(this);this.id=id;this.client=new EndpointClient(host,port);this.client.on('clientError',this.emit.bind(this,'clientError'));this.masterEndpoint=masterEndpoint;this.methodDefs=[];this.state='initialized';};util.inherits(Slave,EventEmitter);Slave.prototype.start=function(){if(!this.basepath){var self=this,req=self.client.rawRequest('POST','/remote');req.end(JSON.stringify({id:self.id,master:self.masterEndpoint.url,masterMethods:self.masterEndpoint.methodNames,slaveMethods:self.methodDefs,updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS}));req.on('response',function(res){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);req.end();req.on('response',function(res){if(res.statusCode!==204){self.emit('clientError',new Error('Error stopping slave.'),res);} -self.client.destroy();self.client.basepath='';self.state='initialized';self.emit('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 Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval||NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};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.slaves.push(slave);self[id]=slave;self[id].on('clientError',function(err){self.emit('clientError',slave,err);});self[id].on('start',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='started'){return;}});self.emit('start');});self[id].on('end',function(){util.forEach(self.slaves,function(id,s){if(s.state!=='stopped'){return;}});self.emit('end');});};Slaves.prototype.defineMethod=function(name,fun){var self=this;self.slaves.forEach(function(slave){slave.defineMethod(name,fun);});self[name]=function(){var args=arguments;return self.slaves.map(function(s){return s[name].apply(s,args);});};};Slaves.prototype.start=function(){this.slaves.forEach(function(s){s.start();});};Slaves.prototype.end=function(){this.slaves.forEach(function(s){s.end();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} -var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;var endpoint=this.createEndpoint_(server,spec.slaveMethods),masterClient=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.url=endpoint.url;this.masterClient_=masterClient;this.slaveEndpoint_=endpoint;this.slaveEndpoint_.context.state='initialized';this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.updateInterval=(spec.updateInterval>=0)?spec.updateInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;this.slaveEndpoint_.start();};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){if(this.slaveEndpoint_.state==='started'){this.slaveEndpoint_.destroy();} -if(this.masterClient_.state==='connected'||this.masterClient_.state==='reconnect'){this.masterClient_.destroy();} -this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_&&this.masterClient_.state==='connected'){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.destroy();endpoint=null;throw e;}} +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){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);req.end();req.on('response',function(res){if(res.statusCode!==204){self.emit('slaveError',new Error('Error stopping slave.'),res);} +self.client.destroy();self.client.basepath='';self.state='initialized';self.emit('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 url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} +var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;this.masterClient_=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.slaveEndpoint_=this.createEndpoint_(server,spec.slaveMethods);this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.slaveEndpoint_.start();this.slaveEndpoint_.context.id=this.id;this.slaveEndpoint_.context.state='initialized';this.url=this.slaveEndpoint_.url;this.updateInterval=(spec.pingInterval>=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.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]);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;} -slaveNode.on('end',function(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});slaveNodes.push(slaveNode);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 slave=require('./slave');var Endpoint=require('./endpoint').Endpoint;var EventEmitter=require('events').EventEmitter;var SlaveNode=require('./slavenode').SlaveNode;var Slaves=slave.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',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,pingInterval);self.slaveState_={};self.updateInterval=pingInterval*4;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.destroy();self.state='stopped';self.emit('end');});self.slaves.on('clientError',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.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();};Cluster.prototype.end=function(){this.state='stopping';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;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;} +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(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});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',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,pingInterval);self.slaveState_={};self.updateInterval=pingInterval*4;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.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();};Cluster.prototype.end=function(){this.state='stopping';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); diff --git a/lib/remote/cluster.js b/lib/remote/cluster.js index 2418b4e..4d6d5c5 100644 --- a/lib/remote/cluster.js +++ b/lib/remote/cluster.js @@ -1,11 +1,10 @@ var BUILD_AS_SINGLE_FILE; if (!BUILD_AS_SINGLE_FILE) { var util = require('../util'); -var slave = require('./slave'); var Endpoint = require('./endpoint').Endpoint; var EventEmitter = require('events').EventEmitter; var SlaveNode = require('./slavenode').SlaveNode; -var Slaves = slave.Slaves; +var Slaves = require('./slaves').Slaves; var qputs = util.qputs; var HTTP_SERVER = require('../http').HTTP_SERVER; var NODELOAD_CONFIG = require('../config').NODELOAD_CONFIG; @@ -77,11 +76,11 @@ var Cluster = exports.Cluster = function Cluster(spec) { self.masterEndpoint = new Endpoint(self.server, masterHost); self.slaves = new Slaves(self.masterEndpoint, pingInterval); self.slaveState_ = {}; - self.updateInterval = pingInterval * 4; + self.updateInterval = pingInterval * 4; // call update() every 4 ping intervals to check for slave aliveness // Define all master methods on the local endpoint - self.masterEndpoint.setStaticParams([self.slaves]); - self.masterEndpoint.defineMethod('updateSlaveState_', self.updateSlaveState_.bind(self)); + 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); @@ -110,11 +109,11 @@ var Cluster = exports.Cluster = function Cluster(spec) { self.emit('start'); }); self.slaves.on('end', function() { - self.masterEndpoint.destroy(); + self.masterEndpoint.end(); self.state = 'stopped'; self.emit('end'); }); - self.slaves.on('clientError', function(slave, err) { + self.slaves.on('slaveError', function(slave, err) { self.emit('slaveError', slave, err); }); @@ -169,6 +168,7 @@ Cluster.prototype.updateSlaveState_ = function(slaves, slaveId, state) { 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); diff --git a/lib/remote/endpoint.js b/lib/remote/endpoint.js index 6142d02..cd7d78c 100644 --- a/lib/remote/endpoint.js +++ b/lib/remote/endpoint.js @@ -22,45 +22,25 @@ Endpoint emits the following events: 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 -- 'destroyed': This endpoint has been terminated because Endpoint.destroy() was explicitly called or a - remote host issued a DELETE request at the base URL. */ var Endpoint = exports.Endpoint = function Endpoint(server, hostAndPort) { EventEmitter.call(this); var self = this, - hostAndPort = hostAndPort || ''; - parts = hostAndPort.split(':'), - hostname = parts[0], - port = parts[1] || 8000, - basepath = '', - updateUrl = function() { - self.url = url.format({ - protocol: 'http', - hostname: hostname || server.hostname, - port: port || server.port, - pathname: basepath - }); - self.route = '^' + basepath + '/?'; - }; - self.__defineGetter__('basepath', function() { return basepath; }); - self.__defineSetter__('basepath', function(val) { - if (this.state === 'started') { throw new Error('Cannot update basepath while endpoint is running.'); } - basepath = val; - updateUrl(); - }); - + parts = hostAndPort ? hostAndPort.split(':') : []; + self.id = util.uid(); - self.basepath = '/remote/' + self.id; self.server = server; self.methodNames = []; self.methods = {}; - self.context = {}; 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); - - self.server.on('start', function(hostname, port) { updateUrl(); }); }; util.inherits(Endpoint, EventEmitter); @@ -98,7 +78,15 @@ Endpoint.prototype.defineMethod = function(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.server.addRoute(this.route, this.handler_); + 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_); } @@ -107,10 +95,10 @@ Endpoint.prototype.start = function() { }; /** Remove the HTTP server route and stop responding to requests */ -Endpoint.prototype.destroy = function() { +Endpoint.prototype.end = function() { if (this.state !== 'started') { return; } - this.server.removeRoute(this.route, this.handler_); - this.state = 'destroyed'; + this.server.removeRoute(this.route_, this.handler_); + this.state = 'initialized'; this.emit('end'); }; @@ -119,9 +107,9 @@ requests are routed to the function set by defineMethod(), applying the HTTP req and sending return value back in the HTTP response. */ Endpoint.prototype.handle = function(path, req, res) { var self = this; - if (path === self.basepath) { + if (path === self.basepath_) { if (req.method === 'DELETE') { - self.destroy(); + self.end(); res.writeHead(204, {'Content-Length': 0}); res.end(); } else { @@ -129,10 +117,10 @@ Endpoint.prototype.handle = function(path, req, res) { res.end(); } } else if (req.method === 'POST') { - var method = path.slice(this.basepath.length+1); + var method = path.slice(this.basepath_.length+1); if (self.methods[method]) { util.readStream(req, function(params) { - var status = 200, ret = 'undefined'; + var status = 200, ret; try { params = JSON.parse(params); @@ -146,7 +134,7 @@ Endpoint.prototype.handle = function(path, req, res) { ret = self.methods[method].apply(self.context, self.staticParams_.concat(params)); try { - ret = ret ? JSON.stringify(ret) : ''; + ret = (ret === undefined) ? '' : JSON.stringify(ret); } catch(e2) { ret = e2.toString(); status = 500; diff --git a/lib/remote/endpointclient.js b/lib/remote/endpointclient.js index 9b228cc..6b0f998 100644 --- a/lib/remote/endpointclient.js +++ b/lib/remote/endpointclient.js @@ -25,46 +25,22 @@ EndpointClient emits the following events: - '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. - -EndpointClient.state can be: -- 'initialized': A connection to the remote endpoint has not yet been established -- 'connected': Connection to the remote endpoint is established -- 'reconnect': An error occured in the HTTP connection. It will be re-established if possible. */ 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([]); - this.state = 'initialized'; - this.connect_(); }; util.inherits(EndpointClient, EventEmitter); -/** Establish an HTTP connection to the target server. Emit 'connect' when connected. */ -EndpointClient.prototype.connect_ = function() { - var self = this; - if (self.state !== 'initialized' && self.state !== 'reconnect') { return; } - - self.retryTimeoutId = clearTimeout(self.retryTimeoutId); - - if (self.client) { self.client.destroy(); } - self.client = http.createClient(self.port, self.host); - self.client.on('error', function(err) { - self.state = 'reconnect'; - self.emit('clientError', err); - }); - self.state = 'connected'; - self.emit('connect'); -}; /** Terminate the HTTP connection. */ EndpointClient.prototype.destroy = function() { - if (this.state !== 'connected' && this.state !== 'reconnect') { return; } - clearTimeout(this.retryTimeoutId); this.client.destroy(); - this.state = 'initialized'; this.emit('end'); }; /** Send an arbitrary HTTP request using the underlying http.Client. */ @@ -79,9 +55,6 @@ endpointClient.method(args...). */ EndpointClient.prototype.defineMethod = function(name) { var self = this; self[name] = function() { - if (self.state !== 'connected' && this.state !== 'reconnect') { - throw new Error('Cannot call method before connect'); - } var req = self.client.request('POST', self.basepath + '/' + name), params = self.staticParams_.concat(util.argarray(arguments)); diff --git a/lib/remote/index.js b/lib/remote/index.js index 52d567a..10d78f3 100644 --- a/lib/remote/index.js +++ b/lib/remote/index.js @@ -1,9 +1,8 @@ var slave = require('./slave'); -var slavenode = require('./slavenode'); exports.Cluster = require('./cluster').Cluster; exports.Slaves = slave.Slaves; exports.Slave = slave.Slave; -exports.SlaveNode = slavenode.SlaveNode; +exports.SlaveNode = require('./slavenode').SlaveNode; exports.Endpoint = require('./endpoint').Endpoint; exports.EndpointClient = require('./endpointclient').EndpointClient; diff --git a/lib/remote/slave.js b/lib/remote/slave.js index 9b6b508..a731fc4 100644 --- a/lib/remote/slave.js +++ b/lib/remote/slave.js @@ -24,7 +24,7 @@ endpoint with those methods. Subsequent calls to Slave simply POST parameters to will POST the definition of method_1 to /remote, followed by ['tom'] to /remote/.../method_1. Slave emits the following events: -- 'clientError', error: The underlying HTTP connection returned an error. The connection will be retried. +- 'slaveError', error: The underlying HTTP connection returned an error. - 'start': The remote instance accepted the slave definition and slave methods can now be called. - 'stopped': The slave endpoint has been removed from the remote instance. @@ -32,37 +32,42 @@ 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) { +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('clientError', this.emit.bind(this, 'clientError')); + 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.basepath) { - var self = this, - req = self.client.rawRequest('POST', '/remote'); - - req.end(JSON.stringify({ - id: self.id, - master: self.masterEndpoint.url, - masterMethods: self.masterEndpoint.methodNames, - slaveMethods: self.methodDefs, - updateInterval: NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS - })); - req.on('response', function(res) { - self.client.basepath = url.parse(res.headers['location']).pathname; - self.state = 'started'; - self.emit('start'); - }); - - self.state = 'connecting'; + 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) { + 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() { @@ -70,7 +75,7 @@ Slave.prototype.end = function() { req.end(); req.on('response', function(res) { if (res.statusCode !== 204) { - self.emit('clientError', new Error('Error stopping slave.'), res); + self.emit('slaveError', new Error('Error stopping slave.'), res); } self.client.destroy(); @@ -85,70 +90,4 @@ Slave.prototype.defineMethod = function(name, fun) { self.client.defineMethod(name, fun); self[name] = function() { return self.client[name].apply(self.client, arguments); }; self.methodDefs.push({name: name, fun: fun.toString()}); -}; - - -/** 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: -- 'clientError', slave, error: The underlying HTTP connection for this slave returned an error. The - connection will be retried. -- '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 || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; -}; -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.slaves.push(slave); - self[id] = slave; - self[id].on('clientError', function(err) { - self.emit('clientError', slave, err); - }); - self[id].on('start', function() { - util.forEach(self.slaves, function(id, s) { - if (s.state !== 'started') { return; } - }); - self.emit('start'); - }); - self[id].on('end', function() { - util.forEach(self.slaves, function(id, s) { - if (s.state !== 'stopped') { 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/remote/slavenode.js b/lib/remote/slavenode.js index 4cb2531..8c976da 100644 --- a/lib/remote/slavenode.js +++ b/lib/remote/slavenode.js @@ -20,13 +20,14 @@ a SlaveNode. The specification contains: slaveMethods: [ { name: 'method-name', fun: 'function() { valid Javascript in a string }' } ], - updateInterval: milliseconds between sending the current execution state to master + 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) { @@ -34,33 +35,29 @@ var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { util.PeriodicUpdater.call(this); this.id = spec.id; - - var endpoint = this.createEndpoint_(server, spec.slaveMethods), - masterClient = spec.master ? this.createMasterClient_(spec.master, spec.masterMethods) : null; - - this.url = endpoint.url; - this.masterClient_ = masterClient; - this.slaveEndpoint_ = endpoint; - this.slaveEndpoint_.context.state = 'initialized'; + 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.updateInterval = (spec.updateInterval >= 0) ? spec.updateInterval : NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; this.slaveEndpoint_.start(); + this.slaveEndpoint_.context.id = this.id; + this.slaveEndpoint_.context.state = 'initialized'; + this.url = this.slaveEndpoint_.url; + + this.updateInterval = (spec.pingInterval >= 0) ? spec.pingInterval : NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; }; util.inherits(SlaveNode, EventEmitter); SlaveNode.prototype.end = function() { - if (this.slaveEndpoint_.state === 'started') { - this.slaveEndpoint_.destroy(); - } - if (this.masterClient_.state === 'connected' || this.masterClient_.state === 'reconnect') { + this.slaveEndpoint_.end(); + if (this.masterClient_) { this.masterClient_.destroy(); } this.emit('end'); }; SlaveNode.prototype.update = function() { - if (this.masterClient_ && this.masterClient_.state === 'connected') { + if (this.masterClient_) { this.masterClient_.updateSlaveState_(this.slaveEndpoint_.context.state); } }; @@ -77,7 +74,7 @@ SlaveNode.prototype.createEndpoint_ = function(server, methods) { endpoint.defineMethod(m.name, fun); }); } catch (e) { - endpoint.destroy(); + endpoint.end(); endpoint = null; throw e; } @@ -94,8 +91,10 @@ SlaveNode.prototype.createMasterClient_ = function(masterUrl, methods) { 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; }; @@ -120,11 +119,11 @@ var installRemoteHandler = exports.installRemoteHandler = function(server) { return; } + slaveNodes.push(slaveNode); slaveNode.on('end', function() { slaveNodes = slaveNodes.filter(function(s) { return s !== slaveNode; }); }); - slaveNodes.push(slaveNode); - + res.writeHead(201, { 'Location': slaveNode.url, 'Content-Length': 0, diff --git a/lib/remote/slaves.js b/lib/remote/slaves.js new file mode 100644 index 0000000..80a0326 --- /dev/null +++ b/lib/remote/slaves.js @@ -0,0 +1,70 @@ +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() { + util.forEach(self.slaves, function(id, s) { + if (s.state !== 'started') { return; } + }); + self.emit('start'); + }); + self[id].on('end', function() { + util.forEach(self.slaves, function(id, s) { + if (s.state !== 'stopped') { 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/util.js b/lib/util.js index b97eed4..2d428d3 100644 --- a/lib/util.js +++ b/lib/util.js @@ -65,8 +65,8 @@ util.readStream = function(stream, callback) { }); }; -/** Make an object an UpdateEventGenerator by adding UpdateEventGenerator.call(this) to the constructor. -Monitor should gather statistics for each intervalMs period, and generate 'update' events */ +/** 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; }); @@ -80,4 +80,39 @@ util.PeriodicUpdater = function(updateIntervalMs) { 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() { + if (client) { client.destroy(); } + client = http.createClient.apply(http, clientArgs); + client._events = util.extend(events, client._events); // EventEmitter._events stores event handlers + client.emit('reconnect'); + }; + + // 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/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); + }); + }, +}; From 23c92f5a7a888c2fa1caa953c8c4fbfd6d0b0b69 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 29 Nov 2010 11:29:47 -0500 Subject: [PATCH 28/41] loadtesting createClient uses reconnectingClient to handle client errors --- examples/loadtesting.test.js | 1 + lib/loadtesting.js | 50 +++++++++++++++++++++++------------- lib/nodeload.js | 12 +++++---- lib/util.js | 5 ++-- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/examples/loadtesting.test.js b/examples/loadtesting.test.js index 6b5dd2f..19793ba 100755 --- a/examples/loadtesting.test.js +++ b/examples/loadtesting.test.js @@ -60,5 +60,6 @@ loadtest.on('end', function() { loadtest.on('end', function() { console.log('Closing test server.'); svr.close(); + process.exit(0); }); }); diff --git a/lib/loadtesting.js b/lib/loadtesting.js index 87e44ec..61fad2c 100644 --- a/lib/loadtesting.js +++ b/lib/loadtesting.js @@ -95,7 +95,7 @@ var TEST_OPTIONS = { // to create the ClientRequest or only 2 will be detected. }; -var LoadTest, createClient, requestGeneratorLoop; +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. @@ -106,10 +106,7 @@ TEST_OPTIONS for a list of the configuration values in each specification. var run = exports.run = function(specs) { specs = (specs instanceof Array) ? specs : util.argarray(arguments); var tests = specs.map(function(spec) { - var generateConnection = function() { - return createClient(spec.port, spec.host); - }, - generateRequest = function(client) { + 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) { @@ -119,7 +116,7 @@ var run = exports.run = function(specs) { }, loop = new MultiLoop({ fun: spec.requestLoop || requestGeneratorLoop(generateRequest), - argGenerator: spec.connectionGenerator || generateConnection, + 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, @@ -266,11 +263,37 @@ var extendClient = exports.extendClient = function(client) { }; /** Same arguments as http.createClient. Returns an extended version of the object (see extendClient) */ -var createClient = function() { - var client = http.createClient.apply(this, arguments); - return extendClient(client); +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: @@ -294,15 +317,6 @@ function requestGeneratorLoop(generator) { finished({req: request, res: response}); } }; - client.on('error', function(err) { - util.debug(err + '. ' + err.stack); - // Setting the following gets client to call _reconnect, but it does so unsuccessfully. - // When the connection becomes available, the next call to client.request() neither emits - // 'response' nor 'error', so we never get to call callFinished(). - // client.readable = client.writable = false; - // client._connecting = false; - callFinished(new EventEmitter()); - }); if (request) { if (request.timeout > 0) { timeoutId = setTimeout(function() { diff --git a/lib/nodeload.js b/lib/nodeload.js index 0e3905a..e45d7b8 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -10,8 +10,8 @@ exports.quiet=function(){NODELOAD_CONFIG.QUIET=true;return exports;};exports.use 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],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;} @@ -90,12 +90,14 @@ return this.charts[name];},updateFromMonitor:function(monitor){monitor.on('updat 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,createClient,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateConnection=function(){return createClient(spec.port,spec.host);},generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} +var TEST_OPTIONS={name:'Debug test',host:'localhost',port:8080,connectionGenerator:undefined,requestGenerator:undefined,requestLoop:undefined,method:'GET',path:'/',requestData:undefined,numUsers:10,loadProfile:undefined,targetRps:Infinity,userProfile:undefined,numRequests:Infinity,timeLimit:120,delay:0,stats:['latency','result-codes'],};var LoadTest,generateConnection,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){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,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 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 extendedClient=Object.create(client);extendedClient.request=function(method,url){var request=client.apply(client,arguments),extendedRequest=Object.create(request),track=function(data){if(data){extendedRequest.emit('write',data);if(extendedRequest.captureRequestBody){extendedRequest.body+=data.toString();}}};extendedRequest.method=method;extendedRequest.path=url;extendedRequest.body='';extendedRequest.captureRequestBody=true;extendedRequest.write=function(data,encoding){track(data);return request.write.apply(request,arguments);};extendedRequest.end=function(data,encoding){track(data);return request.end.apply(request,arguments);};return extendedRequest;};return extendedClient;};var createClient=function(){return extendClient(http.createClient.apply(this,arguments));};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});}};client.on('error',function(err){util.debug(err+'. '+err.stack);callFinished(new EventEmitter());});if(request){if(request.timeout>0){timeoutId=setTimeout(function(){callFinished(new EventEmitter());},request.timeout);} +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;} diff --git a/lib/util.js b/lib/util.js index 2d428d3..51f845a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -92,10 +92,11 @@ util.createReconnectingClient = function() { clientGetter = function(member) { return function() { return client[member]; };}, clientSetter = function(member) { return function(val) { client[member] = val; };}, reconnect = function() { - if (client) { client.destroy(); } + 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'); + client.emit('reconnect', oldclient); }; // Create initial http.Client From 39b702d512ea9adbae692e64d3890748cdb5e5a5 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 29 Nov 2010 13:28:10 -0500 Subject: [PATCH 29/41] HttpServer tracks connections so that HttpServer.end() actually kills server. --- lib/http.js | 15 +++++++++++++++ lib/nodeload.js | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/http.js b/lib/http.js index f529585..9574758 100644 --- a/lib/http.js +++ b/lib/http.js @@ -35,8 +35,22 @@ HttpServer.prototype.start = function(port, hostname) { 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; }; @@ -44,6 +58,7 @@ HttpServer.prototype.start = function(port, hostname) { 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'); diff --git a/lib/nodeload.js b/lib/nodeload.js index e45d7b8..d6a672b 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -63,7 +63,7 @@ if(this.loops.some(function(l){return l.running;})){return false;} this.running=false;this.emit('end');return 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={};} 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);};};StatsCollectors['result-codes']=function ResultCodesCollector(){var self=this;self.stats=new ResultsCounter();self.end=function(context,http){self.stats.put(http.res.statusCode);};};StatsCollectors['concurrency']=function ConcurrencyCollector(){var self=this,c=0;self.stats=new Peak();self.start=function(){c++;};self.end=function(){self.stats.put(c--);};};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);}}};};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);});}};};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;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;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:{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;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:{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;} @@ -80,8 +80,8 @@ return this.monitors[monitorName].start(args);};MonitorGroup.prototype.monitorOb 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.server=http.createServer(function(req,res){self.route_(req,res);});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.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;i Date: Mon, 29 Nov 2010 15:04:44 -0500 Subject: [PATCH 30/41] Fix Slaves start / stop detection Fix SlaveNode.end should disable updateInterval Fix Cluster.end should disable updateInterval Fix Slave.end should still succeed if there is a client error http.test.js does not use global HTTP_SERVER add remote.test.js add util.every for maps rename remote/http.js to httphandler.js --- Makefile | 2 +- TODO | 7 -- ...{loadtesting.test.js => loadtesting.ex.js} | 0 examples/{remote.test.js => remote.ex.js} | 0 lib/nodeload.js | 19 +++-- lib/remote/cluster.js | 9 ++- lib/remote/{http.js => httphandler.js} | 0 lib/remote/index.js | 6 +- lib/remote/slave.js | 27 +++++-- lib/remote/slavenode.js | 4 +- lib/remote/slaves.js | 10 +-- lib/util.js | 10 +++ test/http.test.js | 15 ++-- test/remote.test.js | 81 +++++++++++++++++++ 14 files changed, 146 insertions(+), 44 deletions(-) rename examples/{loadtesting.test.js => loadtesting.ex.js} (100%) rename examples/{remote.test.js => remote.ex.js} (100%) rename lib/remote/{http.js => httphandler.js} (100%) create mode 100755 test/remote.test.js diff --git a/Makefile b/Makefile index 2287b37..a46f8c5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/http.js +SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slaves.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/httphandler.js all: compile diff --git a/TODO b/TODO index c9d3f51..e84fdd8 100644 --- a/TODO +++ b/TODO @@ -1,13 +1,6 @@ -- 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 webpage (stats) - Console webpage (node manager) - Write sample reporting app that monitors cpu, mem, disk io read + write + wait -- Better handling for failed http connections (net.Stream.connect currently calls destroy if there's an error) - Update READMEs - Write a DEVELOPERS doc that explains the components -- Add an error log - Add zipf number generator \ No newline at end of file diff --git a/examples/loadtesting.test.js b/examples/loadtesting.ex.js similarity index 100% rename from examples/loadtesting.test.js rename to examples/loadtesting.ex.js diff --git a/examples/remote.test.js b/examples/remote.ex.js similarity index 100% rename from examples/remote.test.js rename to examples/remote.ex.js diff --git a/lib/nodeload.js b/lib/nodeload.js index d6a672b..f6ac7f1 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -9,7 +9,8 @@ else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);v 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.argarray=function(args){return(args instanceof Array)?args:[].concat.apply([],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&&milliseconds0&&milliseconds=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.slaveEndpoint_.end();if(this.masterClient_){this.masterClient_.destroy();} +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);this.id=spec.id;this.masterClient_=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.slaveEndpoint_=this.createEndpoint_(server,spec.slaveMethods);this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.slaveEndpoint_.start();this.slaveEndpoint_.context.id=this.id;this.slaveEndpoint_.context.state='initialized';this.url=this.slaveEndpoint_.url;this.updateInterval=(spec.pingInterval>=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.updateInterval=0;this.slaveEndpoint_.end();if(this.masterClient_){this.masterClient_.destroy();} 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(){slaveNodes=slaveNodes.filter(function(s){return s!==slaveNode;});});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',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,pingInterval);self.slaveState_={};self.updateInterval=pingInterval*4;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.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();};Cluster.prototype.end=function(){this.state='stopping';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;} +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.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); diff --git a/lib/remote/cluster.js b/lib/remote/cluster.js index 4d6d5c5..aabcea8 100644 --- a/lib/remote/cluster.js +++ b/lib/remote/cluster.js @@ -69,14 +69,13 @@ var Cluster = exports.Cluster = function Cluster(spec) { var self = this, masterSpec = spec.master || {}, slavesSpec = spec.slaves || { hosts:[] }, - masterHost = spec.master && spec.master.host || 'localhost', - pingInterval = spec.pingInterval || NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS; + 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, pingInterval); + self.slaves = new Slaves(self.masterEndpoint, self.pingInterval); self.slaveState_ = {}; - self.updateInterval = pingInterval * 4; // call update() every 4 ping intervals to check for slave aliveness // 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. @@ -138,12 +137,14 @@ Cluster.prototype.start = function() { } 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 }; diff --git a/lib/remote/http.js b/lib/remote/httphandler.js similarity index 100% rename from lib/remote/http.js rename to lib/remote/httphandler.js diff --git a/lib/remote/index.js b/lib/remote/index.js index 10d78f3..8d217ee 100644 --- a/lib/remote/index.js +++ b/lib/remote/index.js @@ -1,9 +1,11 @@ var slave = require('./slave'); +var slavenode = require('./slavenode'); exports.Cluster = require('./cluster').Cluster; exports.Slaves = slave.Slaves; exports.Slave = slave.Slave; -exports.SlaveNode = require('./slavenode').SlaveNode; +exports.SlaveNode = slavenode.SlaveNode; +exports.installRemoteHandler = slavenode.installRemoteHandler; exports.Endpoint = require('./endpoint').Endpoint; exports.EndpointClient = require('./endpointclient').EndpointClient; -require('./http'); \ No newline at end of file +require('./httphandler'); \ No newline at end of file diff --git a/lib/remote/slave.js b/lib/remote/slave.js index a731fc4..af13d9c 100644 --- a/lib/remote/slave.js +++ b/lib/remote/slave.js @@ -26,7 +26,7 @@ will POST the definition of method_1 to /remote, followed by ['tom'] to /remote/ 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. -- 'stopped': The slave endpoint has been removed from the remote instance. +- 'end': The slave endpoint has been removed from the remote instance. Slave.state can be: - 'initialized': The slave is ready to be started. @@ -62,6 +62,9 @@ Slave.prototype.start = function() { 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'); @@ -71,18 +74,26 @@ Slave.prototype.start = function() { }; /** 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); - req.end(); + 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); } - - self.client.destroy(); - self.client.basepath = ''; - self.state = 'initialized'; - self.emit('end'); + done(); }); + req.end(); }; /** Define a method that will be sent to the slave instance */ Slave.prototype.defineMethod = function(name, fun) { diff --git a/lib/remote/slavenode.js b/lib/remote/slavenode.js index 8c976da..0587776 100644 --- a/lib/remote/slavenode.js +++ b/lib/remote/slavenode.js @@ -50,6 +50,7 @@ var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { }; util.inherits(SlaveNode, EventEmitter); SlaveNode.prototype.end = function() { + this.updateInterval = 0; this.slaveEndpoint_.end(); if (this.masterClient_) { this.masterClient_.destroy(); @@ -121,7 +122,8 @@ var installRemoteHandler = exports.installRemoteHandler = function(server) { slaveNodes.push(slaveNode); slaveNode.on('end', function() { - slaveNodes = slaveNodes.filter(function(s) { return s !== slaveNode; }); + var idx = slaveNodes.indexOf(slaveNode); + if (idx !== -1) { slaveNodes.splice(idx, 1); } }); res.writeHead(201, { diff --git a/lib/remote/slaves.js b/lib/remote/slaves.js index 80a0326..a20ec90 100644 --- a/lib/remote/slaves.js +++ b/lib/remote/slaves.js @@ -35,15 +35,13 @@ Slaves.prototype.add = function(hostAndPort) { self.emit('slaveError', slave, err); }); self[id].on('start', function() { - util.forEach(self.slaves, function(id, s) { - if (s.state !== 'started') { return; } - }); + var allStarted = util.every(self.slaves, function(id, s) { return s.state === 'started'; }); + if (!allStarted) { return; } self.emit('start'); }); self[id].on('end', function() { - util.forEach(self.slaves, function(id, s) { - if (s.state !== 'stopped') { return; } - }); + var allStopped = util.every(self.slaves, function(id, s) { return s.state !== 'started'; }); + if (!allStopped) { return; } self.emit('end'); }); }; diff --git a/lib/util.js b/lib/util.js index 51f845a..d3f6541 100644 --- a/lib/util.js +++ b/lib/util.js @@ -52,6 +52,16 @@ util.forEach = function(obj, f) { } } }; +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 (args instanceof Array) ? args : [].concat.apply([], args); }; diff --git a/test/http.test.js b/test/http.test.js index 236c0f2..75e9a84 100644 --- a/test/http.test.js +++ b/test/http.test.js @@ -1,19 +1,18 @@ var http = require('http'), - nlconfig = require('../lib/config').quiet(), - nlhttp = require('../lib/http'), - HTTP_SERVER = nlhttp.HTTP_SERVER; + nlconfig = require('../lib/config').disableServer(), + HttpServer = require('../lib/http').HttpServer; -HTTP_SERVER.start(); -setTimeout(function() { HTTP_SERVER.stop(); }, 1500); +var server = new HttpServer().start(9020); +setTimeout(function() { server.stop(); }, 1500); module.exports = { 'example: add a new route': function(assert, beforeExit) { var done = false; - HTTP_SERVER.addRoute('^/route', function() { + server.addRoute('^/route', function() { done = true; }); - var client = http.createClient(8000, '127.0.0.1'), + var client = http.createClient(9020, '127.0.0.1'), req = client.request('GET', '/route/item'); req.end(); @@ -23,7 +22,7 @@ module.exports = { }, 'test file server finds package.json': function(assert, beforeExit) { var done = false; - var client = http.createClient(8000, '127.0.0.1'), + var client = http.createClient(9020, '127.0.0.1'), req = client.request('GET', '/package.json'); req.end(); req.on('response', function(res) { 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 From 0404ec927054eb29416b379f2e111b467cae10f2 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Mon, 29 Nov 2010 16:16:26 -0500 Subject: [PATCH 31/41] remote.ex.js polls remote iostat and graphs output fix util.argarray so it respects arrays in arguments --- TODO | 2 +- examples/remote.ex.js | 106 +++++++++++++++++++++++++++++++++++------- lib/nodeload.js | 3 +- lib/util.js | 6 ++- 4 files changed, 97 insertions(+), 20 deletions(-) diff --git a/TODO b/TODO index e84fdd8..3be009e 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,6 @@ - Console webpage (stats) - Console webpage (node manager) -- Write sample reporting app that monitors cpu, mem, disk io read + write + wait +- Add mem, disk io read + write + wait monitoring remote.ex.js - Update READMEs - Write a DEVELOPERS doc that explains the components - Add zipf number generator \ No newline at end of file diff --git a/examples/remote.ex.js b/examples/remote.ex.js index 9c0e3d0..19e244d 100755 --- a/examples/remote.ex.js +++ b/examples/remote.ex.js @@ -1,52 +1,125 @@ #!/usr/bin/env node +// This example uses 'iostat' to gather load average from a remote machine and graph it. To run this +// example, first start nodeload on a remote machine: +// +// remote-machine> nodeload.js +// Started HTTP server on remote-machine:8000. +// +// local-machine> examples/remote.ex.js remote-machine:8000 +// +// This example expects a machine with a single disk, so iostat output looks like: +// +// disk0 cpu load average +// KB/t tps MB/s us sy id 1m 5m 15m +// 36.73 2 0.08 8 4 89 0.39 0.43 0.41 +// 0.00 0 0.00 10 4 86 0.39 0.43 0.41 +// var http = require('http'), + util = require('util'), + nlhttp = require('../lib/http'), remote = require('../lib/remote'), - HTTP_SERVER = require('../lib/http').HTTP_SERVER, + REPORT_MANAGER = require('../lib/reporting').REPORT_MANAGER, + HTTP_SERVER = nlhttp.HTTP_SERVER, + HttpServer = nlhttp.HttpServer, Cluster = remote.Cluster; +// Parse remote host from command line arguments +// +var slave, remoteHost; +if (process.argv.length < 3) { + console.log([ + 'No remote host specified, starting slave host locally. To use a separate slave machine, run:\n', + '\n', + ' $ examples/remote.ex.js .\n', + '\n', + 'To start a second host, just run nodeload.js on another machine, optionally specifying the port:\n', + '\n', + ' $ HTTP_PORT=8001 nodeload.js\n', + ].join('')); + + slave = new HttpServer().start(8001); + remote.installRemoteHandler(slave); + remoteHost = 'localhost:8001'; +} else { + remoteHost = process.argv[2]; +} + + +// Initialize the HTML report +var report = REPORT_MANAGER.addReport(remoteHost), + cpuChart = report.getChart('CPU usage'); + + +// Create the Cluster... +// var cluster = new Cluster({ master: { sendOutput: function(slaves, slaveId, output) { - console.log('-------------' + slaveId + '-------------\n' + output + '--------------------------'); + util.print(output); + + // grab fields 4-6 from the iostat output, which assumes output looks like: + // + // disk0 cpu load average + // KB/t tps MB/s us sy id 1m 5m 15m + // 36.73 2 0.08 8 4 89 0.39 0.43 0.41 + // + // not so portable... + var parts = output.trim().split(/\s+/); + if (parts.length > 5) { + cpuChart.put({ + user: parseFloat(parts[3]), + system: parseFloat(parts[4]), + idle: parseFloat(parts[5]) + }); + } } }, slaves: { - hosts: ['localhost:8001'], + hosts: [remoteHost], setup: function(master) { - this.exec = require("child_process").exec; + this.spawn = require("child_process").spawn; master.on('error', function(err) { console.log('Error communicating with master: ' + err.toString()); }); }, - exec: function(master, cmd) { - var self = this; + exec: function(master, cmd, params) { + var self = this, + child = self.spawn(cmd, params); + self.state = 'running'; - self.child = self.exec(cmd, function(error, stdout) { - if (error === null) { - master.sendOutput(stdout.toString()); - self.state = 'done'; - } else { - self.state = 'error'; - } + child.stdout.on('data', function(data) { + master.sendOutput(data.toString()); + }); + child.on('exit', function(data) { + self.state = 'done'; }); } } }); +// ...and start it +// +console.log('Browse to http://localhost:8000 to HTML report'); +console.log('Press ^C to exit.'); cluster.on('init', function() { cluster.on('start', function() { - cluster.exec('ls -alh && sleep 3'); + console.log('----------------- Slave Output -----------------'); + cluster.exec('iostat', ['-w1']); }); cluster.on('end', function(slaves) { console.log('All slaves terminated.'); + if (slave) { + slave.stop(); + } process.exit(0); }); cluster.on('running', function() { console.log('All slaves running'); }); cluster.on('done', function() { - console.log('All slaves done'); + console.log('All slaves done. Stopping cluster...'); + cluster.end(); }); cluster.on('slaveError', function(slave, err) { if (err === null) { @@ -65,7 +138,6 @@ cluster.on('init', function() { }); cluster.start(); }); - -process.on('SIGINT', function() { +process.on('SIGINT', function () { cluster.end(); }); \ No newline at end of file diff --git a/lib/nodeload.js b/lib/nodeload.js index f6ac7f1..705d95e 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -10,7 +10,8 @@ exports.quiet=function(){NODELOAD_CONFIG.QUIET=true;return exports;};exports.use 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(args instanceof Array)?args:[].concat.apply([],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&&milliseconds0&&milliseconds Date: Mon, 29 Nov 2010 18:50:20 -0500 Subject: [PATCH 32/41] Add remote/remotetesting.js --- Makefile | 2 +- examples/remotetesting.ex.js | 54 +++++++++++++++++ lib/nodeload.js | 10 ++- lib/remote/cluster.js | 1 + lib/remote/index.js | 1 + lib/remote/remotetesting.js | 114 +++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100755 examples/remotetesting.ex.js create mode 100644 lib/remote/remotetesting.js diff --git a/Makefile b/Makefile index a46f8c5..ccfc35e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slaves.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/httphandler.js +SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slaves.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/httphandler.js lib/remote/remotetesting.js all: compile diff --git a/examples/remotetesting.ex.js b/examples/remotetesting.ex.js new file mode 100755 index 0000000..36e69b7 --- /dev/null +++ b/examples/remotetesting.ex.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +var http = require('http'), + nl = require('../lib/nodeload'); + +// Start a local HTTP server that we can load test +var svr = http.createServer(function (req, res) { + res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); + res.end(req.url); +}); +svr.listen(9000); +console.log('Started test server.'); + +// Define the tests and nodeload cluster +var i = 0, + readtest = { + name: "Read", + host: 'localhost', + port: 9000, + timeLimit: 40, + loadProfile: [[0,0], [10, 100], [30, 100], [39, 0]], + userProfile: [[0,0], [20, 10]], + stats: ['result-codes', {name: 'latency', percentiles: [0.95, 0.999]}, 'concurrency', 'uniques', 'request-bytes', 'response-bytes'], + requestGenerator: function(client) { + var request = client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); + request.end(); + return request; + } + }, + writetest = { + name: "Write", + host: 'localhost', + port: 9000, + numUsers: 10, + timeLimit: 40, + targetRps: 20, + stats: ['result-codes', 'latency', 'uniques'], + requestGenerator: function(client) { + var request = client.request('PUT', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); + request.end('foo'); + return request; + } + }, + cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8001', 'localhost:8002']); + +// Start the cluster +cluster.run(readtest, writetest); +cluster.on('done', function() { + console.log('All tests complete.'); + cluster.destroy(); + cluster.on('end', function() { + process.exit(0); + }); +}); \ No newline at end of file diff --git a/lib/nodeload.js b/lib/nodeload.js index 705d95e..d90e9e6 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -121,6 +121,12 @@ this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_){ 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.start=function(){if(!this.server.running){throw new Error('A Cluster can only be started after it has emitted \'init\'.');} +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); +installRemoteHandler(HTTP_SERVER);var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var run=require('../loadtesting').run;var Cluster=require('./cluster').Cluster;var EventEmitter=require('events').EventEmitter;var qputs=util.qputs;} +var LoadTestCluster=exports.LoadTestCluster=function LoadTestCluster(masterHost,slaveHosts,masterHttpServer,slaveUpdateInterval){EventEmitter.call(this);var self=this;self.cluster=new Cluster({master:{host:masterHost,sendStats:function(slaves,slaveId,stats){console.log(self.stringify(stats));}},slaves:{hosts: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,loadtest=self.nlrun(specs);self.state='running';loadtest.keepAlive=true;loadtest.on('update',function(interval,stats){master.sendStats(interval);});loadtest.on('end',function(){self.state='done';});}},server:masterHttpServer,pingInterval:slaveUpdateInterval});self.specs=[];self.slaveUpdateInterval=slaveUpdateInterval;self.cluster.on('init',function(){self.cluster.on('start',function(){if(self.specs.length>0){self.cluster.runTests(self.stringify(self.specs));}});self.cluster.start();});self.cluster.on('done',function(){self.emit('done');});self.cluster.on('end',function(){self.emit('end');});};util.inherits(LoadTestCluster,EventEmitter);LoadTestCluster.prototype.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);this.specs=this.specs.concat(specs);if(this.specs.length>0&&this.cluster.started()){this.cluster.runTests(this.stringify(specs));}};LoadTestCluster.prototype.destroy=function(){this.cluster.end();};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);}}; diff --git a/lib/remote/cluster.js b/lib/remote/cluster.js index aabcea8..da6191d 100644 --- a/lib/remote/cluster.js +++ b/lib/remote/cluster.js @@ -129,6 +129,7 @@ var Cluster = exports.Cluster = function Cluster(spec) { } }; 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() { diff --git a/lib/remote/index.js b/lib/remote/index.js index 8d217ee..0fde3c3 100644 --- a/lib/remote/index.js +++ b/lib/remote/index.js @@ -1,6 +1,7 @@ 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; diff --git a/lib/remote/remotetesting.js b/lib/remote/remotetesting.js new file mode 100644 index 0000000..a316937 --- /dev/null +++ b/lib/remote/remotetesting.js @@ -0,0 +1,114 @@ +var BUILD_AS_SINGLE_FILE; +if (!BUILD_AS_SINGLE_FILE) { +var util = require('../util'); +var run = require('../loadtesting').run; +var Cluster = require('./cluster').Cluster; +var EventEmitter = require('events').EventEmitter; +var qputs = util.qputs; +} + +var LoadTestCluster = exports.LoadTestCluster = function LoadTestCluster(masterHost, slaveHosts, masterHttpServer, slaveUpdateInterval) { + EventEmitter.call(this); + + var self = this; + self.cluster = new Cluster({ + master: { + host: masterHost, + sendStats: function(slaves, slaveId, stats) { + } + }, + slaves: { + hosts: 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, + loadtest = self.nlrun(specs); + + self.state = 'running'; + loadtest.keepAlive = true; + loadtest.on('update', function(interval, stats) { + master.sendStats(interval); + }); + loadtest.on('end', function() { + self.state = 'done'; + }); + } + }, + server: masterHttpServer, + pingInterval: slaveUpdateInterval + }); + self.specs = []; + self.slaveUpdateInterval = slaveUpdateInterval; + self.cluster.on('init', function() { + self.cluster.on('start', function() { + if (self.specs.length > 0) { + self.cluster.runTests(self.stringify(self.specs)); + } + }); + self.cluster.start(); + }); + self.cluster.on('done', function() { + self.emit('done'); + }); + self.cluster.on('end', function() { + self.emit('end'); + }); +}; +util.inherits(LoadTestCluster, EventEmitter); +LoadTestCluster.prototype.run = function(specs) { + specs = (specs instanceof Array) ? specs : util.argarray(arguments); + this.specs = this.specs.concat(specs); + if (this.specs.length > 0 && this.cluster.started()) { + this.cluster.runTests(this.stringify(specs)); + } +}; +LoadTestCluster.prototype.destroy = function() { + this.cluster.end(); +}; +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); + } +}; \ No newline at end of file From 28f4882e03e800389f964416a35147da9623ed12 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 09:43:32 -0500 Subject: [PATCH 33/41] remotetesting aggregates stats on master --- examples/remotetesting.ex.js | 9 +- lib/monitoring/monitorgroup.js | 2 +- lib/nodeload.js | 40 +++--- lib/remote/remotetesting.js | 253 ++++++++++++++++++++++++--------- lib/stats.js | 55 ++++--- 5 files changed, 254 insertions(+), 105 deletions(-) diff --git a/examples/remotetesting.ex.js b/examples/remotetesting.ex.js index 36e69b7..c555353 100755 --- a/examples/remotetesting.ex.js +++ b/examples/remotetesting.ex.js @@ -41,14 +41,11 @@ var i = 0, return request; } }, - cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8001', 'localhost:8002']); + cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8002', 'localhost:8001']); // Start the cluster cluster.run(readtest, writetest); -cluster.on('done', function() { +cluster.on('end', function() { console.log('All tests complete.'); - cluster.destroy(); - cluster.on('end', function() { - process.exit(0); - }); + process.exit(0); }); \ No newline at end of file diff --git a/lib/monitoring/monitorgroup.js b/lib/monitoring/monitorgroup.js index 22783ee..f078804 100644 --- a/lib/monitoring/monitorgroup.js +++ b/lib/monitoring/monitorgroup.js @@ -98,7 +98,7 @@ MonitorGroup.prototype.setLogFile = function(logNameOrObject) { this.logNameOrObject = logNameOrObject; }; -/** Log statistics each time an 'update' event is emitted? */ +/** 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(); diff --git a/lib/nodeload.js b/lib/nodeload.js index d90e9e6..53f1f4c 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -14,30 +14,32 @@ return true;};util.argarray=function(args){var arr=[];for(var i=0;i0&&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]!==undefined){sum+=this.items[i];if(sum>=target){return i;}}} +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=exports.Uniques=function(){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+=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=exports.Peak=function(){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){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=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){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;};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var EventEmitter=require('events').EventEmitter;} +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){self.cluster.runTests(self.stringify(self.specs));}});self.cluster.start();});self.cluster.on('done',function(){self.emit('done');});self.cluster.on('end',function(){self.emit('end');});};util.inherits(LoadTestCluster,EventEmitter);LoadTestCluster.prototype.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);this.specs=this.specs.concat(specs);if(this.specs.length>0&&this.cluster.started()){this.cluster.runTests(this.stringify(specs));}};LoadTestCluster.prototype.destroy=function(){this.cluster.end();};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])+',');} +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);}}; +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/lib/remote/remotetesting.js b/lib/remote/remotetesting.js index a316937..cf546fb 100644 --- a/lib/remote/remotetesting.js +++ b/lib/remote/remotetesting.js @@ -1,105 +1,160 @@ +/*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.cluster = new Cluster({ - master: { - host: masterHost, - sendStats: function(slaves, slaveId, stats) { - } - }, - slaves: { - hosts: 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, - loadtest = self.nlrun(specs); + 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.state = 'running'; - loadtest.keepAlive = true; - loadtest.on('update', function(interval, stats) { - master.sendStats(interval); - }); - loadtest.on('end', function() { - self.state = 'done'; - }); - } - }, - server: masterHttpServer, - pingInterval: slaveUpdateInterval - }); - self.specs = []; - self.slaveUpdateInterval = slaveUpdateInterval; + 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() { - if (self.specs.length > 0) { - self.cluster.runTests(self.stringify(self.specs)); - } + self.startTests_(); + self.updateInterval = self.slaveUpdateInterval; + self.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); }); self.cluster.start(); }); - self.cluster.on('done', function() { - self.emit('done'); + 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.cluster.on('end', function() { self.emit('end'); }); }; -util.inherits(LoadTestCluster, EventEmitter); -LoadTestCluster.prototype.run = function(specs) { - specs = (specs instanceof Array) ? specs : util.argarray(arguments); - this.specs = this.specs.concat(specs); - if (this.specs.length > 0 && this.cluster.started()) { - this.cluster.runTests(this.stringify(specs)); +/** 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; }; -LoadTestCluster.prototype.destroy = function() { - this.cluster.end(); +/** 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.stringify = function(obj) { +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(''); + 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(i + ':' + this.stringify_(obj[i]) + ','); } ret.push('}'); return ret.join(''); @@ -111,4 +166,72 @@ LoadTestCluster.prototype.stringify = function(obj) { 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/stats.js b/lib/stats.js index 011ca66..862cf4f 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -6,14 +6,15 @@ // See NODELOADLIB.md for a complete description of the classes and functions. // /*jslint forin:true */ -var BUILD_AS_SINGLE_FILE; +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 + this.type = 'Histogram'; this.params = params; this.size = params && params.buckets || 3000; this.percentiles = params && params.percentiles || [0.95, 0.99]; @@ -76,7 +77,7 @@ Histogram.prototype = { } else { var sum = this.extra.length; for (var i = this.items.length - 1; i >= 0; i--) { - if (this.items[i] !== undefined) { + if (this.items[i] > 0) { sum += this.items[i]; if (sum >= target) { return i; @@ -134,7 +135,8 @@ Histogram.prototype = { } }; -var Accumulator = exports.Accumulator = function() { +var Accumulator = stats.Accumulator = function Accumulator() { + this.type = 'Accumulator'; this.total = 0; this.length = 0; }; @@ -159,7 +161,8 @@ Accumulator.prototype = { } }; -var ResultsCounter = exports.ResultsCounter = function() { +var ResultsCounter = stats.ResultsCounter = function ResultsCounter() { + this.type = 'ResultsCounter'; this.start = new Date(); this.items = {}; this.length = 0; @@ -210,7 +213,8 @@ ResultsCounter.prototype = { } }; -var Uniques = exports.Uniques = function() { +var Uniques = stats.Uniques = function Uniques() { + this.type = 'Uniques'; this.start = new Date(); this.items = {}; this.uniques = 0; @@ -250,7 +254,8 @@ Uniques.prototype = { } }; -var Peak = exports.Peak = function() { +var Peak = stats.Peak = function Peak() { + this.type = 'Peak'; this.peak = 0; this.length = 0; }; @@ -278,7 +283,8 @@ Peak.prototype = { } }; -var Rate = exports.Rate = function() { +var Rate = stats.Rate = function Rate() { + this.type = 'Rate'; this.start = new Date(); this.length = 0; }; @@ -301,7 +307,8 @@ Rate.prototype = { } }; -var LogFile = exports.LogFile = function(filename) { +var LogFile = stats.LogFile = function LogFile(filename) { + this.type = 'LogFile'; this.writepos = null; this.length = 0; this.filename = filename; @@ -339,7 +346,8 @@ LogFile.prototype = { } }; -var NullLog = exports.NullLog = function() { +var NullLog = stats.NullLog = function NullLog() { + this.type = 'NullLog'; this.length = 0; }; NullLog.prototype = { @@ -351,7 +359,8 @@ NullLog.prototype = { summary: function() { return { file: 'null', written: 0 }; } }; -var Reportable = exports.Reportable = function(name, Backend, backendparams) { +var Reportable = stats.Reportable = function Reportable(name, Backend, backendparams) { + this.type = 'Reportable'; this.name = name || ''; this.length = 0; this.interval = new Backend(backendparams); @@ -391,7 +400,18 @@ Reportable.prototype = { } }; -var roundRobin = exports.roundRobin = function(list) { +/** 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() { @@ -401,7 +421,7 @@ var roundRobin = exports.roundRobin = function(list) { 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 @@ -409,7 +429,7 @@ var randomString = exports.randomString = function(length) { return eval("'" + s + "'"); }; -var nextGaussian = exports.nextGaussian = function(mean, stddev) { +var nextGaussian = stats.nextGaussian = function(mean, stddev) { mean = mean || 0; stddev = stddev || 1; var s = 0, z0, z1; @@ -421,9 +441,12 @@ var nextGaussian = exports.nextGaussian = function(mean, stddev) { return z0 * Math.sqrt(-2 * Math.log(s) / s) * stddev + mean; }; -var nextPareto = exports.nextPareto = function(min, max, shape) { +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; -}; \ 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 From 7dc9937e85350b34905dbca7794905ac26198f68 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 09:43:32 -0500 Subject: [PATCH 34/41] remotetesting aggregates stats on master --- examples/remotetesting.ex.js | 9 +- lib/monitoring/monitorgroup.js | 2 +- lib/nodeload.js | 40 +++-- lib/remote/remotetesting.js | 276 +++++++++++++++++++++++++-------- lib/stats.js | 55 +++++-- 5 files changed, 277 insertions(+), 105 deletions(-) diff --git a/examples/remotetesting.ex.js b/examples/remotetesting.ex.js index 36e69b7..c555353 100755 --- a/examples/remotetesting.ex.js +++ b/examples/remotetesting.ex.js @@ -41,14 +41,11 @@ var i = 0, return request; } }, - cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8001', 'localhost:8002']); + cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8002', 'localhost:8001']); // Start the cluster cluster.run(readtest, writetest); -cluster.on('done', function() { +cluster.on('end', function() { console.log('All tests complete.'); - cluster.destroy(); - cluster.on('end', function() { - process.exit(0); - }); + process.exit(0); }); \ No newline at end of file diff --git a/lib/monitoring/monitorgroup.js b/lib/monitoring/monitorgroup.js index 22783ee..f078804 100644 --- a/lib/monitoring/monitorgroup.js +++ b/lib/monitoring/monitorgroup.js @@ -98,7 +98,7 @@ MonitorGroup.prototype.setLogFile = function(logNameOrObject) { this.logNameOrObject = logNameOrObject; }; -/** Log statistics each time an 'update' event is emitted? */ +/** 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(); diff --git a/lib/nodeload.js b/lib/nodeload.js index d90e9e6..53f1f4c 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -14,30 +14,32 @@ return true;};util.argarray=function(args){var arr=[];for(var i=0;i0&&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]!==undefined){sum+=this.items[i];if(sum>=target){return i;}}} +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=exports.Uniques=function(){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+=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=exports.Peak=function(){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){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=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){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;};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var EventEmitter=require('events').EventEmitter;} +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){self.cluster.runTests(self.stringify(self.specs));}});self.cluster.start();});self.cluster.on('done',function(){self.emit('done');});self.cluster.on('end',function(){self.emit('end');});};util.inherits(LoadTestCluster,EventEmitter);LoadTestCluster.prototype.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);this.specs=this.specs.concat(specs);if(this.specs.length>0&&this.cluster.started()){this.cluster.runTests(this.stringify(specs));}};LoadTestCluster.prototype.destroy=function(){this.cluster.end();};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])+',');} +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);}}; +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/lib/remote/remotetesting.js b/lib/remote/remotetesting.js index a316937..332f9ac 100644 --- a/lib/remote/remotetesting.js +++ b/lib/remote/remotetesting.js @@ -1,105 +1,183 @@ +// ------------------------------------ +// 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.cluster = new Cluster({ - master: { - host: masterHost, - sendStats: function(slaves, slaveId, stats) { - } - }, - slaves: { - hosts: 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, - loadtest = self.nlrun(specs); + 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.state = 'running'; - loadtest.keepAlive = true; - loadtest.on('update', function(interval, stats) { - master.sendStats(interval); - }); - loadtest.on('end', function() { - self.state = 'done'; - }); - } - }, - server: masterHttpServer, - pingInterval: slaveUpdateInterval - }); - self.specs = []; - self.slaveUpdateInterval = slaveUpdateInterval; + 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() { - if (self.specs.length > 0) { - self.cluster.runTests(self.stringify(self.specs)); - } + self.startTests_(); + self.updateInterval = self.slaveUpdateInterval; + self.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED); }); self.cluster.start(); }); - self.cluster.on('done', function() { - self.emit('done'); + 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.cluster.on('end', function() { self.emit('end'); }); }; -util.inherits(LoadTestCluster, EventEmitter); -LoadTestCluster.prototype.run = function(specs) { - specs = (specs instanceof Array) ? specs : util.argarray(arguments); - this.specs = this.specs.concat(specs); - if (this.specs.length > 0 && this.cluster.started()) { - this.cluster.runTests(this.stringify(specs)); +/** 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; }; -LoadTestCluster.prototype.destroy = function() { - this.cluster.end(); +/** 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.stringify = function(obj) { +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(''); + 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(i + ':' + this.stringify_(obj[i]) + ','); } ret.push('}'); return ret.join(''); @@ -111,4 +189,72 @@ LoadTestCluster.prototype.stringify = function(obj) { 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/stats.js b/lib/stats.js index 011ca66..862cf4f 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -6,14 +6,15 @@ // See NODELOADLIB.md for a complete description of the classes and functions. // /*jslint forin:true */ -var BUILD_AS_SINGLE_FILE; +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 + this.type = 'Histogram'; this.params = params; this.size = params && params.buckets || 3000; this.percentiles = params && params.percentiles || [0.95, 0.99]; @@ -76,7 +77,7 @@ Histogram.prototype = { } else { var sum = this.extra.length; for (var i = this.items.length - 1; i >= 0; i--) { - if (this.items[i] !== undefined) { + if (this.items[i] > 0) { sum += this.items[i]; if (sum >= target) { return i; @@ -134,7 +135,8 @@ Histogram.prototype = { } }; -var Accumulator = exports.Accumulator = function() { +var Accumulator = stats.Accumulator = function Accumulator() { + this.type = 'Accumulator'; this.total = 0; this.length = 0; }; @@ -159,7 +161,8 @@ Accumulator.prototype = { } }; -var ResultsCounter = exports.ResultsCounter = function() { +var ResultsCounter = stats.ResultsCounter = function ResultsCounter() { + this.type = 'ResultsCounter'; this.start = new Date(); this.items = {}; this.length = 0; @@ -210,7 +213,8 @@ ResultsCounter.prototype = { } }; -var Uniques = exports.Uniques = function() { +var Uniques = stats.Uniques = function Uniques() { + this.type = 'Uniques'; this.start = new Date(); this.items = {}; this.uniques = 0; @@ -250,7 +254,8 @@ Uniques.prototype = { } }; -var Peak = exports.Peak = function() { +var Peak = stats.Peak = function Peak() { + this.type = 'Peak'; this.peak = 0; this.length = 0; }; @@ -278,7 +283,8 @@ Peak.prototype = { } }; -var Rate = exports.Rate = function() { +var Rate = stats.Rate = function Rate() { + this.type = 'Rate'; this.start = new Date(); this.length = 0; }; @@ -301,7 +307,8 @@ Rate.prototype = { } }; -var LogFile = exports.LogFile = function(filename) { +var LogFile = stats.LogFile = function LogFile(filename) { + this.type = 'LogFile'; this.writepos = null; this.length = 0; this.filename = filename; @@ -339,7 +346,8 @@ LogFile.prototype = { } }; -var NullLog = exports.NullLog = function() { +var NullLog = stats.NullLog = function NullLog() { + this.type = 'NullLog'; this.length = 0; }; NullLog.prototype = { @@ -351,7 +359,8 @@ NullLog.prototype = { summary: function() { return { file: 'null', written: 0 }; } }; -var Reportable = exports.Reportable = function(name, Backend, backendparams) { +var Reportable = stats.Reportable = function Reportable(name, Backend, backendparams) { + this.type = 'Reportable'; this.name = name || ''; this.length = 0; this.interval = new Backend(backendparams); @@ -391,7 +400,18 @@ Reportable.prototype = { } }; -var roundRobin = exports.roundRobin = function(list) { +/** 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() { @@ -401,7 +421,7 @@ var roundRobin = exports.roundRobin = function(list) { 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 @@ -409,7 +429,7 @@ var randomString = exports.randomString = function(length) { return eval("'" + s + "'"); }; -var nextGaussian = exports.nextGaussian = function(mean, stddev) { +var nextGaussian = stats.nextGaussian = function(mean, stddev) { mean = mean || 0; stddev = stddev || 1; var s = 0, z0, z1; @@ -421,9 +441,12 @@ var nextGaussian = exports.nextGaussian = function(mean, stddev) { return z0 * Math.sqrt(-2 * Math.log(s) / s) * stddev + mean; }; -var nextPareto = exports.nextPareto = function(min, max, shape) { +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; -}; \ 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 From 1adac097aa764018ce1499bab0d44e636eb85455 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 11:32:16 -0500 Subject: [PATCH 35/41] use Array.prototype.slice.call to convert arguments to array. --- lib/monitoring/monitor.js | 2 +- lib/monitoring/monitorgroup.js | 4 ++-- lib/nodeload.js | 7 +++---- lib/util.js | 6 +----- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/monitoring/monitor.js b/lib/monitoring/monitor.js index 5d587d0..f288b80 100644 --- a/lib/monitoring/monitor.js +++ b/lib/monitoring/monitor.js @@ -53,7 +53,7 @@ Monitor.prototype.setStats = function(stats) { // arguments contains stats names self.collectors = []; self.stats = {}; self.interval = {}; - stats = (stats instanceof Array) ? stats : [].concat.apply([], arguments); + stats = (stats instanceof Array) ? stats : Array.prototype.slice.call(arguments); stats.forEach(function(stat) { var name = stat, params; if (typeof stat === 'object') { diff --git a/lib/monitoring/monitorgroup.js b/lib/monitoring/monitorgroup.js index f078804..63628d8 100644 --- a/lib/monitoring/monitorgroup.js +++ b/lib/monitoring/monitorgroup.js @@ -30,7 +30,7 @@ var MonitorGroup = exports.MonitorGroup = function MonitorGroup(statsNames) { return summary; }; - this.statsNames = (statsNames instanceof Array) ? statsNames : [].concat.apply([], arguments); + this.statsNames = (statsNames instanceof Array) ? statsNames : Array.prototype.slice.call(arguments); this.monitors = {}; this.stats = {}; this.interval = {}; @@ -51,7 +51,7 @@ util.inherits(MonitorGroup, EventEmitter); once if desired. */ MonitorGroup.prototype.initMonitors = function(monitorNames) { var self = this; - monitorNames = (monitorNames instanceof Array) ? monitorNames : [].concat.apply([], arguments); + 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; diff --git a/lib/nodeload.js b/lib/nodeload.js index 53f1f4c..04835e7 100755 --- a/lib/nodeload.js +++ b/lib/nodeload.js @@ -10,8 +10,7 @@ exports.quiet=function(){NODELOAD_CONFIG.QUIET=true;return exports;};exports.use 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){var arr=[];for(var i=0;i0&&milliseconds0&&milliseconds0){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:[].concat.apply([],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:[].concat.apply([],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]);} +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;} diff --git a/lib/util.js b/lib/util.js index 2b24493..9eb0ccb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -63,11 +63,7 @@ util.every = function(obj, f) { return true; }; util.argarray = function(args) { - var arr = []; - for (var i = 0; i < args.length; i++) { - arr[i] = args[i]; - } - return arr; + return Array.prototype.slice.call(args); }; util.readStream = function(stream, callback) { var data = []; From 8ee881b544aa356c91b56d2c3649677fbb0797fd Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 13:15:17 -0500 Subject: [PATCH 36/41] nodeload.js moves into root directory THANKS moves inside package.json --- Makefile | 8 ++++---- THANKS | 5 ----- TODO | 1 + examples/loadtesting.ex.js | 2 +- examples/remotetesting.ex.js | 2 +- lib/nodeload.js => nodeload.js | 0 package.json | 5 +++-- 7 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 THANKS rename lib/nodeload.js => nodeload.js (100%) diff --git a/Makefile b/Makefile index ccfc35e..a4612f2 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ all: compile clean: rm -rf ./lib-cov - rm -f ./lib/nodeload.js ./lib/*.tpl.js + rm -f ./nodeload.js ./lib/*.tpl.js rm -f results-*-err.log results-*-stats.log results-*-summary.html templates: @@ -14,6 +14,6 @@ templates: $(PROCESS_TPL) DYGRAPH_SOURCE lib/dygraph.tpl > lib/dygraph.tpl.js compile: templates - echo "#!/usr/bin/env node" > ./lib/nodeload.js - cat $(SOURCES) | ./scripts/jsmin.js >> ./lib/nodeload.js - chmod +x ./lib/nodeload.js \ No newline at end of file + 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/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 3be009e..0a4ac85 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,7 @@ - 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) - Update READMEs - Write a DEVELOPERS doc that explains the components - Add zipf number generator \ No newline at end of file diff --git a/examples/loadtesting.ex.js b/examples/loadtesting.ex.js index 19793ba..8a00e93 100755 --- a/examples/loadtesting.ex.js +++ b/examples/loadtesting.ex.js @@ -3,7 +3,7 @@ /*jslint sub:true */ var http = require('http'), - nl = require('../lib/nodeload'); + nl = require('../nodeload'); var svr = http.createServer(function (req, res) { res.writeHead((Math.random() < 0.8) ? 200 : 404, {'Content-Type': 'text/plain'}); diff --git a/examples/remotetesting.ex.js b/examples/remotetesting.ex.js index c555353..b617140 100755 --- a/examples/remotetesting.ex.js +++ b/examples/remotetesting.ex.js @@ -1,7 +1,7 @@ #!/usr/bin/env node var http = require('http'), - nl = require('../lib/nodeload'); + nl = require('../nodeload'); // Start a local HTTP server that we can load test var svr = http.createServer(function (req, res) { diff --git a/lib/nodeload.js b/nodeload.js similarity index 100% rename from lib/nodeload.js rename to nodeload.js diff --git a/package.json b/package.json index 1a2d053..fb55e58 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,15 @@ "Benjamin Schmaus ", "Jonathan Lee ", "Robert Newson ", + "Michael Mattozzi " ], "repository": { "type": "git", "url": "http://github.com/benschmaus/nodeload" }, - "main": "./lib/nodeload", + "main": "./nodeload.js", "bin": { - "nodeload.js": "./lib/nodeload.js", + "nodeload.js": "./nodeload.js", "nl.js": "./nl.js" }, "modules": { From a5e3f73c0c520e3459ddd5f5f651bf803f2667a8 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 22:26:07 -0500 Subject: [PATCH 37/41] Update a bunch of docs and examples Move nl.js stuff under lib/nl Move reporting stuff under lib/reporting --- Makefile | 8 +- NODELOADLIB.md | 569 ------------------ README.md | 214 ++++--- doc/developers.md | 19 + doc/loop.md | 102 ++++ doc/monitoring.md | 64 ++ NODELOAD.md => doc/nl.md | 54 +- doc/nodeload.md | 202 +++++++ doc/remote.md | 37 ++ doc/reporting.md | 43 ++ doc/stats.md | 44 ++ doc/tips.md | 64 ++ .../{loadtesting.ex.js => nodeload.ex.js} | 4 +- examples/nodeloadlib-ex.js | 102 ---- examples/nodeloadlib-ex2.js | 33 - examples/riaktest.ex.js | 88 +++ examples/sample-report.html | 141 ++++- examples/simpletest.ex.js | 32 + lib/loadtesting.js | 9 +- lib/monitoring/index.js | 9 +- lib/{ => nl}/options.js | 0 lib/nl/optparse-README.md | 161 +++++ lib/{ => nl}/optparse.js | 0 lib/{ => reporting}/dygraph.tpl | 0 lib/{reporting.js => reporting/index.js} | 8 +- lib/{ => reporting}/summary.tpl | 0 lib/{ => reporting}/template.js | 0 nl.js | 7 +- nodeload.js | 18 +- test/loop.test.js | 2 +- 30 files changed, 1165 insertions(+), 869 deletions(-) delete mode 100644 NODELOADLIB.md create mode 100644 doc/developers.md create mode 100644 doc/loop.md create mode 100644 doc/monitoring.md rename NODELOAD.md => doc/nl.md (53%) create mode 100644 doc/nodeload.md create mode 100644 doc/remote.md create mode 100644 doc/reporting.md create mode 100644 doc/stats.md create mode 100644 doc/tips.md rename examples/{loadtesting.ex.js => nodeload.ex.js} (91%) delete mode 100755 examples/nodeloadlib-ex.js delete mode 100755 examples/nodeloadlib-ex2.js create mode 100755 examples/riaktest.ex.js mode change 100755 => 100644 examples/sample-report.html create mode 100755 examples/simpletest.ex.js rename lib/{ => nl}/options.js (100%) create mode 100644 lib/nl/optparse-README.md rename lib/{ => nl}/optparse.js (100%) rename lib/{ => reporting}/dygraph.tpl (100%) rename lib/{reporting.js => reporting/index.js} (97%) rename lib/{ => reporting}/summary.tpl (100%) rename lib/{ => reporting}/template.js (100%) diff --git a/Makefile b/Makefile index a4612f2..5a65103 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,17 @@ .PHONY: clean templates compile PROCESS_TPL = scripts/process_tpl.js -SOURCES = lib/header.js lib/*.tpl.js lib/template.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slaves.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/httphandler.js lib/remote/remotetesting.js +SOURCES = lib/header.js lib/config.js lib/util.js lib/stats.js lib/loop/loop.js lib/loop/multiloop.js lib/monitoring/collectors.js lib/monitoring/statslogger.js lib/monitoring/monitor.js lib/monitoring/monitorgroup.js lib/http.js lib/reporting/*.tpl.js lib/reporting/template.js lib/reporting/index.js lib/loadtesting.js lib/remote/endpoint.js lib/remote/endpointclient.js lib/remote/slave.js lib/remote/slaves.js lib/remote/slavenode.js lib/remote/cluster.js lib/remote/httphandler.js lib/remote/remotetesting.js all: compile clean: rm -rf ./lib-cov - rm -f ./nodeload.js ./lib/*.tpl.js + rm -f ./nodeload.js ./lib/reporting/*.tpl.js rm -f results-*-err.log results-*-stats.log results-*-summary.html templates: - $(PROCESS_TPL) REPORT_SUMMARY_TEMPLATE lib/summary.tpl > lib/summary.tpl.js - $(PROCESS_TPL) DYGRAPH_SOURCE lib/dygraph.tpl > lib/dygraph.tpl.js + $(PROCESS_TPL) REPORT_SUMMARY_TEMPLATE lib/reporting/summary.tpl > lib/reporting/summary.tpl.js + $(PROCESS_TPL) DYGRAPH_SOURCE lib/reporting/dygraph.tpl > lib/reporting/dygraph.tpl.js compile: templates echo "#!/usr/bin/env node" > ./nodeload.js diff --git a/NODELOADLIB.md b/NODELOADLIB.md deleted file mode 100644 index 0f5bb4c..0000000 --- a/NODELOADLIB.md +++ /dev/null @@ -1,569 +0,0 @@ -OVERVIEW -================ - -`nodeloadlib` is a [node.js](http://nodejs.org/) library containing building blocks to programmatically create load tests for HTTP services. The components are: - -* High-level load testing interface -* Test monitoring -* Distributed testing -* A scheduler which executes functions at a given rate -* Event-based loops -* Statistics classes -* HTTP-specific monitors -* Web-based reporting - -QUICKSTART -================ - -* **Write a load test** - - $ vi example.js ## Add the following text to example.js - - // This test will hit localhost:8080 with 20 concurrent connections for 10 minutes. - var http = require('http'), - nl = require('./lib/nodeloadlib'); - - http.createServer(function (req, res) { res.writeHead(200); res.end(); }).listen(8080); - console.log("Server to load test listening on 8080.") - - nl.runTest({ - host: 'localhost', - port: 8080, - numClients: 20, - timeLimit: 600, - successCodes: [200], - targetRps: 200, - requestLoop: function(loopFun, client) { - var url = '/data/object-' + Math.floor(Math.random()*10000), - req = nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - req.on('response', function(res) { - loopFun({req: req, res: res}); - }); - req.end(); - } - }); - - $ node example.js ## while running, browse to http://localhost:8000 - Listening on 8080. - Opening log files. - Started HTTP server on port 8000. - ......done. - Finishing... - Shutdown HTTP server. - - Browse to http://localhost:8000 during the test for graphs. Non-200 responses are logged to `results-{timestamp}-err.log`, `results-{timestamp}-stats.log` contains statistics, and the summary web page is written to `results-{timestamp}-summary.html`. Check out [examples/nodeloadlib-ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/nodeloadlib-ex.js) for a example of a full read+write test. - -* **Run a function at given rate:** - - // Print 0..19 over 10 seconds - var nl = require('./lib/nodeloadlib').disableServer(); - var i = 0; - - new nl.Job({ - rps: 2, // run 2 times/sec - duration: 10, // run for 10 seconds - fun: function(loopFun) { - console.log(i++); - loopFun(); - } - }).start(); - - -CONFIGURATION -================ - -Use the following functions when calling `require()` to alter nodeload's default behavior: - - var nl = require('./lib/nodeloadlib') - .quiet() // disable all console output - .usePort(10000) // start HTTP server on port 10000. Default: 8000 - .disableServer() // don't start the HTTP server - .setMonitorIntervalMs(1000) // emit 'update' events every second. Default: 2000 - .setAjaxRefreshIntervalMs(1000) // HTML page should update every second. Default: 2000 - .disableLogs() // don't log anything to disk - - -COMPONENTS -================ - -## Load Test Functions ## - -High-level functions useful for quickly building up complex load tests. See `api.js`. - -**Functions:** - -* `runTest(spec, callback, stayAliveAfterDone)`: Run a single test and call `callback` (see **Test Definition** below). -* `addTest(spec)`: Add a test to be run on `startTests()`. Tests are run concurrently. -* `addRamp(rampSpec)`: Gradually ramp up the load generated by a test (see **Ramp Definition** below). -* `startTests(callback, stayAliveAfterDone)`: Run tests added by `addTest()` and `addRamp()` and call `callback`. -* `traceableRequest(...)`: Used instead of built-in node.js `http.Client.request()` to allows proper tracking of unique URLs. - -**Usage**: - -A "test" represents requests being sent at a fixed rate over concurrent connections. Tests are run by calling `runTest()` or calling `addTest()` followed by `startTests()`. The parameters defining a test are detailed in **Test Definition** section. Issue requests using one of three methods: - -* Define `method`, `path`, and `requestData`, leaving `requestGenerator` and `requestLoop` as `null`. - -* Set `requestGenerator` to a `function(http.Client) -> http.ClientRequest`. Requests returned by this function are executed by `nodeloadlib`. For example, you can GET random URLs using a `requestGenerator`: - - nl.addTest({ - requestGenerator: function(client) { - return nl.traceableRequest(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); - } - }); - -* Set `requestLoop` to a `function(loopFun, http.Client)` which calls `loopFun({req: http.ClientRequest, res: http.ClientResponse})` after each request completes. This is the most flexibile, but the function must be sure to call `loopFun()`. For example, issue `PUT` requests with proper `If-Match` headers using a `requestLoop`: - - nl.addTest({ - requestLoop: function(loopFun, client) { - var req = nl.traceableRequest(client, 'GET', '/resource'); - req.on('response', function(response) { - if (response.statusCode != 200 && response.statusCode != 404) { - loopFun({req: req, res: response}); - } else { - var headers = { }; - if (response.headers['etag'] != null) - headers['if-match'] = response.headers['etag']; - req = nl.traceableRequest(client, 'PUT', '/resource', headers, "new value"); - req.on('response', function(response) { - loopFun({req: req, res: response}); - }); - req.end(); - } - }); - req.end(); - } - }); - -A "ramp" increases the load of a particular test over some period of time. Schedule a ramp after scheduling a test by calling `addRamp()`: - - var test1 = nl.addTest({ - targetRps: 100, - requestGenerator: function(client) { - return nl.traceableRequest(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); - } - }); - - // Add 100 requests / second using 10 concurrent connections to test1 between minutes 1 and 2 - nl.addRamp({ - test: test1, - numberOfSteps: 10, - timeLimit: 60, - rpsPerStep: 10, - clientsPerStep: 1, - delay: 60 - }); - -Start all the tests by calling `startTests(...)`. The script terminates 3 seconds after the tests complete unless the parameter `stayAliveAfterDone==true`. - -Check out [examples/nodeloadlib-ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/nodeloadlib-ex.js) for an example of a full read+write test. - -**Test Definition:** The following object defines the parameters and defaults for a test, which is used by `addTest()` or `runTest()`: - - var TEST_DEFAULTS = { - name: 'Debug test', // A descriptive name for the test - - host: 'localhost', // host and port specify where to connect - port: 8080, // - requestGenerator: null, // Specify one of: - // 1. requestGenerator: a function - // function(http.Client) -> http.ClientRequest - requestLoop: null, // 2. requestLoop: is a function - // function(loopFun, http.Client) - // It must call - // loopFun({ - // req: http.ClientRequest, - // res: http.ClientResponse}); - // after each transaction to finishes to schedule the - // next iteration of requestLoop. - method: 'GET', // 3. (method + path + requestData) specify a single URL to - path: '/', // test - requestData: null, // - // - numClients: 10, // Maximum number of concurrent executions of request loop - numRequests: Infinity, // Maximum number of iterations of request loop - timeLimit: 120, // Maximum duration of test in seconds - targetRps: Infinity, // Number of times per second to execute request loop - delay: 0, // Seconds before starting test - // - successCodes: null, // List of success HTTP response codes. Non-success responses - // are logged to the error log. - stats: ['latency', // Specify list of: 'latency', 'result-codes', 'uniques', - 'result-codes'], // 'concurrency'. Note that 'uniques' only shows up in - // Cumulative section of the report. traceableRequest() must - // be used for requets or only 2 uniques will be detected. - latencyConf: { // Set latencyConf.percentiles to percentiles to report for - percentiles: [0.95,0.99] // the 'latency' stat. - } // - }; - -**Ramp Definition:** The following object defines the parameters and defaults for a ramp, which is used by `addRamp()`: - - var RAMP_DEFAULTS = { - test: null, // The test to ramp up, returned from from addTest() - numberOfSteps: 10, // Number of steps in ramp - timeLimit: 10, // The total number of seconds to ramp up - rpsPerStep: 10, // The rps to add to the test at each step - clientsPerStep: 1, // The number of connections to add to the test at each step. - delay: 0 // Number of seconds to wait before ramping up. - } - - - -## Test Monitoring ## - -`TEST_MONITOR` is an EventEmitter that emits 'update' events at regular intervals. This allows tests to be introspected for things like statistics gathering, report generation, etc. See `monitor.js`. - -To set the interval between 'update' events: - - var nl = require('./lib/nodeloadlib').setMonitorIntervalMs(seconds) - -**Events:** - -* `TEST_MONITOR.on('test', callback(test))`: `addTest()` was called. The newly created test is passed to `callback`. -* `TEST_MONITOR.on('start', callback(tests))`: `startTests()` was called. The list of tests being started is passed to `callback`. -* `TEST_MONITOR.on('end', callback(tests))`: All tests finished. -* `TEST_MONITOR.on('update', callback(tests))`: Emitted at regular intervals while tests are running. Default is every 2 seconds. `nodeloadlib` uses this event internally to track statistics and generate the summary webpage. -* `TEST_MONITOR.on('afterUpdate', callback(tests))`: Emitted after the 'update' event. - -**Usage**: - - nl.TEST_MONITOR.on('update', function(tests) { - for (var i in tests) { - console.log(JSON.stringify(tests[i].stats['latency'].summary())) - } - }); - -## Distributed Testing ## - -Functions to distribute tests across multiple slave `nodeload` instances. See `remote.js`. - -**Functions:** - -* `remoteTest(spec)`: Return a test to be scheduled with `remoteStart(...)` (`spec` uses same format as `addTest(spec)`). -* `remoteStart(master, slaves, tests, callback, stayAliveAfterDone)`: Run tests on specified slaves. -* `remoteStartFile(master, slaves, filename, callback, stayAliveAfterDone)`: Execute a `.js` file on specified slaves. - -**Usage**: - -First, start `nodeloadlib.js` on each slave instances. - - $ node dist/nodeloadlib.js # Run on each slave machine - -Then, create tests using `remoteTest(spec)` with the same `spec` fields in the **Test Definition** section above. Pass the created tests as a list to `remoteStart(...)` to execute them on slave `nodeload` instances. `master` must be the `"host:port"` of the `nodeload` which runs `remoteStart(...)`. It will receive and aggregate statistics from the slaves, so the address should be reachable by the slaves. Or, use `master=null` to disable reports from the slaves. - - // This script must be run on master:8000, which will aggregate results. Each slave - // will GET http://internal-service:8080/ at 100 rps. - var t1 = nl.remoteTest({ - name: "Distributed test", - host: 'internal-service', - port: 8080, - timeLimit: 20, - targetRps: 100 - }); - nl.remoteStart('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], [t1]); - -Alternatively, an existing `nodeload` script file can be used: - - // The file /path/to/load-test.js should contain valid javascript and can use any nodeloadlib functions - nl.remoteStartFile('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], '/path/to/load-test.js'); - -When the remote tests complete, the master instance will call the `callback` parameter if non-null. It then automatically terminates after 3 seconds unless the parameter `stayAliveAfterDone==true`. - - - -## Function Scheduler ## - -The `SCHEDULER` object allows a function to be called at a desired rate and concurrency level. See `scheduler.js`. -**Functions:** - -* `SCHEDULER.schedule(spec)`: Schedule a function to be executed (see the **Schedule Definition** below) -* `SCHEDULER.startAll(callback)`: Start running all the scheduled functions and execute callback when they complete -* `SCHEDULER.startSchedule(callback)`: Start a single scheduled function and execute callback when it completes -* `funLoop(fun)`: Wrap functions that do not perform IO so they can be used with SCHEDULER - -**Usage**: - -Call `SCHEDULER.schedule(spec)` to add a job. `spec.fun` must be a `function(loopFun, args)` and call `loopFun(results)` when it completes. Call `SCHEDULER.startAll()` to start running all scheduled jobs. - -If `spec.argGenerator` is non-null, it is called `spec.concurrency` times on startup. One return value is passed as the second parameter to each concurrent execution of `spec.fun`. If null, the value of `spec.args` is passed to all executions of `spec.fun` instead. - -A scheduled job finishes after its target duration or it has been called the maximum number of times. `SCHEDULER` stops *all* jobs once all *monitored* jobs finish. For example, 1 monitored job is scheduled for 5 seconds, and 2 unmonitored jobs are scheduled with no time limits. `SCHEDULER` will start all 3 jobs when `SCHEDULER.startAll()` is called, and stop all 3 jobs 5 seconds later. Unmonitored jobs are useful for running side processes such as statistics gathering and reporting. - -Example: - - var t = 1; - nl.SCHEDULER.schedule({ - fun: nl.LoopUtils.funLoop(function(i) { console.log("Thread " + i) }), - argGenerator: function() { return t++; }, - concurrency: 5, - rps: 10, - duration: 10 - }); - nl.SCHEDULER.startAll(function() { sys.puts("Done.") }); - -Alternatively, a Job can started independently. A Job instance is analogous to a single thread, and does not understand the `concurrency` parameter. - - var i = 0; - var job = new nl.Job({ - fun: nl.LoopUtils.funLoop(function() { console.log(i++) }), - rps: 10, - duration: 10 - }).start(); - -**Job Definition**: The following object defines the parameters and defaults for a job run by `SCHEDULER`: - - var JOB_DEFAULTS = { - fun: null, // A function to execute which accepts the parameters (loopFun, args). - // The value of args is the return value of argGenerator() or the args - // parameter if argGenerator is null. The function must call - // loopFun(results) when it completes. - argGenerator: null, // A function which is called once when the job is started. The return - // value is passed to fun as the "args" parameter. This is useful when - // concurrency > 1, and each "thread" should have its own args. - args: null, // If argGenerator is NOT specified, then this is passed to the fun as "args". - concurrency: 1, // Number of concurrent calls of fun() - rps: Infinity, // Target number of time per second to call fun() - duration: Infinity, // Maximum duration of this job in seconds - numberOfTimes: Infinity, // Maximum number of times to call fun() - delay: 0, // Seconds to wait before calling fun() for the first time - monitored: true // Does this job need to finish in order for SCHEDULER.startAll() to end? - }; - - -## Event-based loops ## - -The `ConditionalLoop` class provides a generic way to write a loop where each iteration is scheduled using `process.nextTick()`. This allows many long running "loops" to be executed concurrently by `node.js`. See `evloops.js`. - -**Functions:** - -* `ConditionalLoop(fun, args, conditions, delay):` Defines a loop (see **Loop Definition** below) -* `ConditionalLoop.start(callback):` Starts executing and call `callback` on termination -* `ConditionalLoop.stop():` Terminate the loop -* `LoopConditions.timeLimit(seconds)`, `LoopConditions.maxExecutions(numberOfTimes)`: useful ConditionalLoop conditions -* `LoopUtils.rpsLoop(rps, fun)`: Wrap a `function(loopFun, args)` so ConditionalLoop calls it a set rate -* `LoopUtils.funLoop(fun)`: Wrap a linearly executing `function(args)` so it can be used with a ConditionalLoop - -**Usage:** - -Create a `ConditionalLoop` instance and call `ConditionalLoop.start()` to execute the loop. A function given to `ConditionalLoop` must be a `function(loopFun, args)` which ends by calling `loopFun()`. - -The `conditions` parameter is a list of functions. When any function returns `false`, the loop terminates. For example, the functions `LoopConditions.timeLimit(seconds)` and `LoopConditions.maxExecutions(numberOfTimes)` are conditions that limit the duration and number of iterations of a loop respectively. - -The loop also terminates if `ConditionalLoop.stop()` is called. - -Example: - - var fun = function(loopFun, startTime) { - console.log("It's been " + (new Date() - startTime) / 1000 + " seconds"); - loopFun(); - }; - var stopOnFriday = function() { - return (new Date()).getDay() < 5; - } - var loop = new nl.ConditionalLoop(nl.LoopUtils.rpsLoop(1, fun), new Date(), [stopOnFriday, nl.LoopConditions.timeLimit(604800 /*1 week*/)], 1); - loop.start(function() { console.log("It's Friday!") }); - -**Loop Definition:** - -The `ConditionalLoop` constructor arguments are: - - fun: Function that takes parameters (loopFun, args) and calls loopFun() after each iteration - args: The args parameter to pass to fun - conditions: A list of functions representing termination conditions. Terminate when any function returns `false`. - delay: Seconds to wait before starting the first iteration - - -## Statistics ## - -Implementations of various statistics. See `stats.js`. - -**Classes:** - -* `Histogram(numBuckets)`: A histogram of integers. If most of the items are between 0 and `numBuckets`, calculating percentiles and stddev is fast. -* `Accumulator`: Calculates the sum of the numbers put in. -* `ResultsCounter`: Tracks results which are be limited to a small set of possible choices. Tracks the total number of results, number of results by value, and results added per second. -* `Uniques`: Tracks the number of unique items added. -* `Peak`: Tracks the max of the numbers put in. -* `Rate`: Tracks the rate at which items are added. -* `LogFile`: Outputs to a file on disk. -* `NullLog`: Ignores all items put in. -* `Reportable`: Wraps any other statistic to store an interval and cumulative version of it. - -**Functions:** - -* `randomString(length)`: Returns a random string of ASCII characters between 32 and 126 of the requested length. -* `nextGaussian(mean, stddev)`: Returns a normally distributed number using the provided mean and standard deviation. -* `nextPareto(min, max, shape)`: Returns a Pareto distributed number between `min` and `max` inclusive using the provided shape. -* `roundRobin(list)`: Returns a copy of the list with a `get()` method. `get()` returns list entries round robin. - -**Usage:** - -All of the statistics classes support the methods: - -* `.length`: The total number of items `put()` into this object. -* `put(item)`: Include an item in the statistic. -* `get()`: Get a specific value from the object, which varies depending on the object. -* `clear()`: Clear out all items. -* `summary()`: Get a object containing a summary of the object, which varies depending on the object. The fields returned are used to generate the trends of the HTML report graphs. - -In addition, these other methods are supported: - -* `Histogram.mean()`: Calculate the mean of the numbers in the histogram. -* `Histogram.percentile(percentile)`: Calculate the given `percentile`, between 0 and 1, of the numbers in the histogram. -* `Histogram.stddev()`: Standard deviation of the numbers in the histogram. -* `LogFile.open()`: Open the file. -* `LogFile.clear(text)`: Truncate the file, and write `text` if specified. -* `LogFile.close()`: Close the file. -* `Reportable.next()`: clear out the interval statistic for the next window. - -Refer to the `stats.js` for the return value of the `get()` and `summary()` functions for the different classes. - - - -## HTTP-specific Monitors ## - -A collection of wrappers for `requestLoop` functions that record statistics for HTTP requests. These functions can be run scheduled with `SCHEDULER` or run with a `ConditionalLoop`. See `evloops.js`. - -**Functions:** - -* `monitorLatenciesLoop(latencies, fun)`: Call `fun()` and put the execution duration in `latencies`, which should be a `Histogram`. -* `monitorResultsLoop(results, fun)`: Call `fun()` and put the HTTP response code in `results`, which should be a `ResultsCounter`. -* `monitorByteReceivedLoop(bytesReceived, fun)`: Call `fun()` and put the number of bytes received in `bytesReceived`, usually an `Accumulator`. -* `monitorConcurrencyLoop(concurrency, fun)`: Call `fun()` and put the number of "threads" currently executing it into `concurrency`, usually a `Peak`. -* `monitorRateLoop(rate, fun)`: Call `fun()` and notify `rate`, which should be a `Rate`, that it was called. -* `monitorHttpFailuresLoop(successCodes, fun, log)`: Call `fun()` and put the HTTP request and response into `log`, which should be a `LogFile`, for every request that does not return an HTTP status code included in the list `successCodes`. -* `monitorUniqueUrlsLoop(uniqs, fun)`: Call `fun()` and put the HTTP request path into `uniqs`, which should be a `Uniques`. -* `loopWrapper(fun, start, finish)`: Create a custom loop wrapper by specifying a functions to execute before and after calling `fun()`. - -**Usage:** - -All of these wrappers return a `function(loopFun, args)` which can be used by `SCHEDULER` and `ConditionalLoop`. The underlying function should have the same signature and execute an HTTP request. It must call `loopFun({req: http.ClientRequest, res: http.ClientResponse})` when it completes the request. - -Example: - - // Issue GET requests to random objects at localhost:8080/data/obj-{0-1000} for 1 minute and - // track the number of unique URLs - var uniq = new nl.Reportable(Uniques, 'Uniques'); - var loop = nl.LoopUtils.monitorUniqueUrlsLoop(uniq, function(loopFun, client) { - var req = nl.traceableRequest(client, 'GET', '/data/obj-' + Math.floor(Math.random()*1000)); - req.on('response', function(res) { - loopFun({req: req, res: res}); - }); - req.end(); - }); - SCHEDULER.schedule({ - fun: loop, - args: http.createClient(8080, 'localhost'), - duration: 60 - }).start(function() { - console.log(JSON.stringify(uniq.summary())); - }); - - - -## Web-based Reports ## - -Functions for manipulating the report that is available during the test at http://localhost:8000/ and that is written to `results-{timestamp}-summary.html`. - -**Interface:** - -* `REPORT_MANAGER.reports`: All of the reports that are displayed in the summary webpage. -* `REPORT_MANAGER.addReport(Report)`: Add a report object to the webpage. -* `Report(name, updater(Report))`: A report consists of a set of charts, displayed in the main body of the webpage, and a summary object displayed on the right side bar. A report has a name and an updater function. Calling `updater(Report)` should update the report's chart and summary. When tests are running, REPORT_MANAGER calls each report's `updater` periodically. -* `Report.summary`: A JSON object displayed in table form in the summary webpage right side bar. -* `Report.getChart(name)`: Gets or creates a chart with the title `name` to the report and returns a `Chart` object. See `Chart.put(data)` below. -* `Chart.put(data)`: Add the data, which is a map of { 'trend-1': value, 'trend-2': value, ... }, to the chart, which tracks the values for each trend over time. - -**Usage:** - -An HTTP server is started on port 8000 by default. Use: - - `var nl = require('./lib/nodeloadlib).disableServer()` - -to disable the HTTP server, or - - `var nl = require('./lib/nodeloadlib).usePort(port)` - -to change the port binding. The file `results-{timestamp}-summary.html` is written to the current directory. Use - - `var nl = require('./lib/nodeloadlib).disableLogs()` - -to disable creation of this file. - -A report is automatically added for each test created by `addTest()` or `runTest()`. To add additional charts to the summary webpage: - - var mycounter = 0; - REPORT_MANAGER.addReport(new Report("My Report", function(report) { - chart = report.getChart("My Chart"); - chart.put({ 'counter': mycounter++ }); - chart.summary = { 'Total increments': mycounter }; - })); - -The webpage automatically issues an AJAX request to refresh the text and chart data every 2 seconds by default. Change the refresh period using: - - `var nl = require('./lib/nodeloadlib).setAjaxRefreshIntervalMs(milliseconds)` - - -TIPS AND TRICKS -================ - -Some handy features worth mentioning. - -1. **Examine and add to stats to the HTML page:** - - addTest().stats and runTest().stats are maps: - - { 'latency': Reportable(Histogram), - 'result-codes': Reportable(ResultsCounter}, - 'uniques': Reportable(Uniques), - 'concurrency': Reportable(Peak) } - - Put `Reportable` instances to this map to have it automatically updated each reporting interval and added to the summary webpage. - -2. **Post-process statistics:** - - Use a `startTests()` callback to examine the final statistics in `test.stats[name].cumulative` at test completion. - - // GET random URLs of the form localhost:8080/data/object-#### for 10 seconds, then - // print out all the URLs that were hit. - var t = addTest({ - timeLimit: 10, - targetRps: 10, - stats: ['uniques'], - requestGenerator: function(client) { - return traceableRequest(client, 'GET', '/data/object-' + Math.floor(Math.random()*100));; - } - }); - function printAllUrls() { - console.log(JSON.stringify(t.stats['uniques'].cumulative)); - } - startTests(printAllUrls); - - -3. **Out-of-the-box file server:** - - Just start `nodeloadlib.js` and it will serve files in the current directory. - - $ node lib/nodeloadlib.js - $ curl -i localhost:8000/lib/nodeloadlib.js # executed in a separate terminal - HTTP/1.1 200 OK - Content-Length: 50763 - Connection: keep-alive - - var sys = require('sys'); - var http = require('http'); - ... - -4. **Run arbitrary Javascript:** - - POST any valid Javascript to `/remote` to have it `eval()`'d. - - $ node dist/nodeloadlib.js - Serving progress report on port 8000. - Opening log files. - Received remote command: - sys.puts("hello!") - hello! - - $ curl -i -d 'sys.puts("hello!")' localhost:8000/remote # executed in a separate terminal diff --git a/README.md b/README.md index 756f6f4..acdd977 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,165 @@ -NODELOAD +INSTALLING ================ -`nodeload` is both a **standalone tool** and a **`node.js` library** for load testing HTTP services. +Using [npm](http://npmjs.org/): + + curl http://npmjs.org/install.sh | sh # install npm if not already installed + npm install nodeload -See [NODELOADLIB.md](http://github.com/benschmaus/nodeload/blob/master/NODELOADLIB.md) for using `nodeload` as a `node.js` library. +From source: -See [NODELOAD.md](http://github.com/benschmaus/nodeload/blob/master/NODELOAD.md) for instructions on using the standalone load test tool. + git clone git://github.com/benschmaus/nodeload.git + cd nodeload + npm link # optional. enables require('nodeload/') instead of require('./lib/'). +Or as a single file (this does not install the `nl.js` tool): + wget https://github.com/benschmaus/nodeload/raw/master/nodeload.js -NODELOAD QUICKSTART +NODELOAD ================ -1. Install node.js. -2. Clone nodeload. -3. cd into nodeload working copy. -4. git submodule update --init -5. Start testing! +`nodeload` is collection of [node.js](http://nodejs.org/) modules for load testing HTTP services. -nodeload contains a toy server that you can use for a quick demo. -Try the following: +As a developer, you should be able write load tests and get informative reports without having to learn another framework. You should be able to build tests by example and selectively use the parts of the tool that fit your task. Being a library means that you can use as much or as little of `nodeload` as makes sense, and you can create load tests with the power of a full programming language. For example, if you need to execute some function at a given rate, just use the [`'nodeload/loop'`](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) module, and write the rest yourself. - [~/code/nodeload] node examples/test-server.js & - [1] 2756 - [~/code/nodeload] Server running at http://127.0.0.1:8080/ - [~/code/nodeload] ./dist/nodeload.js -f -c 10 -n 10000 -i 1 -r ../examples/test-generator.js localhost:8080 +In addition, `nodeload` is built for operability. It can always be deployed by simply copying the single file, `nodeload.js`. -You should now see some test output in your console. The generated webpage contains a graphical chart of test results. +Here are examples of each module, which can be used separately. Look for more examples in the [`examples/`](https://github.com/benschmaus/nodeload/tree/master/examples) directory and in test cases prefixed with "example" in [`test/`](https://github.com/benschmaus/nodeload/tree/master/test): +### [nl](https://github.com/benschmaus/nodeload/tree/master/doc/nl.md) +`nl` is an [Apache Bench (ab)](http://httpd.apache.org/docs/2.0/programs/ab.html) like command line tool for running tests quickly. See the [nl documentation](https://github.com/benschmaus/nodeload/tree/master/doc/nl.md) for details. -NODELOADLIB QUICKSTART -================ + $ examples/test-server.js & # starts a simple server on port 9000 to load test + $ ./nl.js -c 10 -n 10000 -i 2 localhost:9000 + +will send 10,000 queries to http://localhost:9000 using 10 connections. Statistics are printed to the console and graphs can be seen at . + +### [nodeload](https://github.com/benschmaus/nodeload/tree/master/doc/nodeload.md) + +The `nodeload` module is the primary interface for creating load tests. It includes all of the other modules described below, so if you `require('nodeload')`, you don't need to `require()` any of the other ones. Look at the examples in [`examples/loadtesting.ex.js`](https://github.com/benschmaus/nodeload/tree/master/examples/loadtesting.ex.js) and [`examples/riaktest.ex.js`]((https://github.com/benschmaus/nodeload/tree/master/examples/riaktest.ex.js) or read the [nodeload module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/nodeload.md). + + var nl = require('nodeload'); + var loadtest = nl.run({ + host: 'localhost', + port: 9000, + timeLimit: 60, + targetRps: 500, + requestGenerator: function(client) { + var request = client.request('GET', "/" + Math.floor(Math.random()*10000)); + request.end(); + return request; + } + }); + loadtest.on('end', function() { console.log('Load test done.'); }); + +### [remote](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md) -* **Write a load test:** +The `remote` module provides a mechanism for running a distributed load test. See the [remote module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md). - $ vi example.js ## Add the following text to example.js +Start slave instances: + + $ HTTP_PORT=10001 ./nodeload.js # start a local slave instance on :10001 + $ HTTP_PORT=10002 ./nodeload.js # start a 2nd slave instance on :10002 - // This test will hit localhost:8080 with 20 concurrent connections for 10 minutes. - var http = require('http'), - nl = require('./lib/nodeloadlib'); - - http.createServer(function (req, res) { res.writeHead(200); res.end(); }).listen(8080); - console.log("Server to load test listening on 8080.") - - nl.runTest({ - host: 'localhost', - port: 8080, - numClients: 20, - timeLimit: 600, - successCodes: [200], - targetRps: 200, - requestLoop: function(loopFun, client) { - var url = '/data/object-' + Math.floor(Math.random()*10000), - req = nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - req.on('response', function(res) { - loopFun({req: req, res: res}); - }); - req.end(); - } - }); - - $ node example.js ## while running, browse to http://localhost:8000 - Listening on 8080. - Opening log files. - Started HTTP server on port 8000. - ......done. - Finishing... - Shutdown HTTP server. - - Browse to http://localhost:8000 during the test for graphs. Non-200 responses are logged to `results-{timestamp}-err.log`, `results-{timestamp}-stats.log` contains statistics, and the summary web page is written to `results-{timestamp}-summary.html`. Check out [examples/nodeloadlib-ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/nodeloadlib-ex.js) for a example of a full read+write test. - -* **Run a function at given rate:** - - // Print 0..19 over 10 seconds - var nl = require('./lib/nodeloadlib').disableServer(); - var i = 0; - - new nl.Job({ - rps: 2, // run 2 times/sec - duration: 10, // run for 10 seconds - fun: function(loopFun) { - console.log(i++); - loopFun(); - } +Create the distributed load test: + + var nl = require('nodeload/remote'); + var cluster = new nl.LoadTestCluster('localhost:8000', ['localhost:8002', 'localhost:8001']); + cluster.run({ + host: 'localhost', + port: 9000, + timeLimit: 60, + targetRps: 500, + requestGenerator: function(client) { + var request = client.request('GET', "/" + Math.floor(Math.random()*10000)); + request.end(); + return request; + } + }); + cluster.on('end', function() { console.log('Load test done.'); }); + +### [stats](https://github.com/benschmaus/nodeload/tree/master/doc/stats.md) + +The `stats` module provides implementations of various statistics objects, like Histograms and Accumulators, and functions, like randomString(), and nextGaussian(). See the [stats module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/stats.md). + + var stats = require('nodeload/stats'); + var histogram = new stats.Histogram(); + for (var i = 0; i < 1000; i++) + histogram.put(Math.abs(Math.floor(stats.nextGaussian()))); + console.log('Mean: ' + histogram.mean() + ', 99%: ' + histogram.percentile(0.99)); + +will output "`Mean: 0.852, 99%: 3`". + +### [monitoring](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md) + +The `monitoring` module provides a way to track runtime statistics for code that is run concurrently. See the [monitoring module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md). + + var monitoring = require('nodeload/monitoring'); + var monitor = new monitoring.Monitor('runtime'); + function asyncFunction() { + var m = monitor.start(); + setTimeout(function() { m.end(); }, Math.floor(Math.random()*1000)); + } + for (var i = 0; i < 1000; i++) { asyncFunction(); } + process.on('exit', function() { + console.log('Median runtime (ms): ' + monitor.stats['runtime'].percentile(0.5)); + }); + +will output "`Median runtime (ms): 497`". + +### [reporting](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md) + +The `reporting` module provides a way to produce HTML graphs from code. See the [reporting module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md). + + var reporting = require('nodeload/reporting'), stats = require('nodeload/stats'), + report = reporting.REPORT_MANAGER.addReport('Random Numbers'), + chart = report.getChart('Gaussian / Pareto vs. Time (minutes)'); + for (var i = 0; i < 10; i++) { + setTimeout(function() { + chart.put({'Pareto': stats.nextPareto(0, 100), 'Gaussian': stats.nextGaussian()}); + }, i * 500); + } + +will display a graph on http://localhost:8000/ and save it to an HTML file in the local directory. + +### [loop](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) + +The `loop` module provides a way to execute a function at a set rate and concurrency. See [`test/loop.test.js`](https://github.com/benschmaus/nodeload/tree/master/test/loop.test.js) for examples and read the [loop module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) for details. + + var http = require('http'), + loop = require('nodeload/loop'), + requests = 0, + client = http.createClient(80, 'www.google.com'), + l = new loop.MultiLoop({ + fun: function(finished) { + client.request('GET', '/').end(); + requests++; + finished(); + }, + rps: 10, + duration: 3, + concurrency: 5 }).start(); + l.on('end', function() { console.log('Total requests: ' + requests) }); + +will output "`Total requests: 30`". + +### [http](https://github.com/benschmaus/nodeload/tree/master/doc/http.md) + +The `http` module provides a generic HTTP server that serves static files and that can be configured with new routes. See the [http module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/http.md). + + var http = require('nodeload/http'); + var server = new http.HttpServer().start(10000); + server.addRoute('^/hello$', function(url, req, res) { + res.writeHead(200); + res.end("Hello"); + }); + + will output the contents of `./package.json`, and will display "Hello". + + +CONTRIBUTING +================ +Contributions are always welcome. File bugs on [github](https://github.com/benschmaus/nodeload/issues), email any of the authors, and fork away! [developers.md](https://github.com/benschmaus/nodeload/tree/master/doc/developers.md) has brief instructions on getting tests up and running, and will hold more design details in the future. \ No newline at end of file diff --git a/doc/developers.md b/doc/developers.md new file mode 100644 index 0000000..05c2e1a --- /dev/null +++ b/doc/developers.md @@ -0,0 +1,19 @@ +# Setting up + +First, it's recommended that [`npm`](http://npmjs.org/) is installed. Just run: + + [~/]> curl http://npmjs.org/install.sh | sh + +The clone nodeload and run `npm link` + + [~/]> git clone git://github.com/benschmaus/nodeload.git + [~/]> cd nodeload + [~/nodeload]> npm link + +which will installs the unit testing framework [expresso](http://visionmedia.github.com/expresso) and puts a symlink to `nodeload` in the node library path. + +Use expresso to run the tests under test/: + + [~/nodeload]> expresso + + 100% 20 tests diff --git a/doc/loop.md b/doc/loop.md new file mode 100644 index 0000000..fac1818 --- /dev/null +++ b/doc/loop.md @@ -0,0 +1,102 @@ +**This document is out-of-date. See [`lib/loop/loop.js`](https://github.com/benschmaus/nodeload/tree/master/lib/loop/loop.js) and [`lib/loop/multiloop.js`](https://github.com/benschmaus/nodeload/tree/master/lib/loop/multiloop.js).** + +## Function Scheduler ## + +The `SCHEDULER` object allows a function to be called at a desired rate and concurrency level. See `scheduler.js`. +**Functions:** + +* `SCHEDULER.schedule(spec)`: Schedule a function to be executed (see the **Schedule Definition** below) +* `SCHEDULER.startAll(callback)`: Start running all the scheduled functions and execute callback when they complete +* `SCHEDULER.startSchedule(callback)`: Start a single scheduled function and execute callback when it completes +* `funLoop(fun)`: Wrap functions that do not perform IO so they can be used with SCHEDULER + +**Usage**: + +Call `SCHEDULER.schedule(spec)` to add a job. `spec.fun` must be a `function(loopFun, args)` and call `loopFun(results)` when it completes. Call `SCHEDULER.startAll()` to start running all scheduled jobs. + +If `spec.argGenerator` is non-null, it is called `spec.concurrency` times on startup. One return value is passed as the second parameter to each concurrent execution of `spec.fun`. If null, the value of `spec.args` is passed to all executions of `spec.fun` instead. + +A scheduled job finishes after its target duration or it has been called the maximum number of times. `SCHEDULER` stops *all* jobs once all *monitored* jobs finish. For example, 1 monitored job is scheduled for 5 seconds, and 2 unmonitored jobs are scheduled with no time limits. `SCHEDULER` will start all 3 jobs when `SCHEDULER.startAll()` is called, and stop all 3 jobs 5 seconds later. Unmonitored jobs are useful for running side processes such as statistics gathering and reporting. + +Example: + + var t = 1; + nl.SCHEDULER.schedule({ + fun: nl.LoopUtils.funLoop(function(i) { console.log("Thread " + i) }), + argGenerator: function() { return t++; }, + concurrency: 5, + rps: 10, + duration: 10 + }); + nl.SCHEDULER.startAll(function() { sys.puts("Done.") }); + +Alternatively, a Job can started independently. A Job instance is analogous to a single thread, and does not understand the `concurrency` parameter. + + var i = 0; + var job = new nl.Job({ + fun: nl.LoopUtils.funLoop(function() { console.log(i++) }), + rps: 10, + duration: 10 + }).start(); + +**Job Definition**: The following object defines the parameters and defaults for a job run by `SCHEDULER`: + + var JOB_DEFAULTS = { + fun: null, // A function to execute which accepts the parameters (loopFun, args). + // The value of args is the return value of argGenerator() or the args + // parameter if argGenerator is null. The function must call + // loopFun(results) when it completes. + argGenerator: null, // A function which is called once when the job is started. The return + // value is passed to fun as the "args" parameter. This is useful when + // concurrency > 1, and each "thread" should have its own args. + args: null, // If argGenerator is NOT specified, then this is passed to the fun as "args". + concurrency: 1, // Number of concurrent calls of fun() + rps: Infinity, // Target number of time per second to call fun() + duration: Infinity, // Maximum duration of this job in seconds + numberOfTimes: Infinity, // Maximum number of times to call fun() + delay: 0, // Seconds to wait before calling fun() for the first time + monitored: true // Does this job need to finish in order for SCHEDULER.startAll() to end? + }; + + +## Event-based loops ## + +The `ConditionalLoop` class provides a generic way to write a loop where each iteration is scheduled using `process.nextTick()`. This allows many long running "loops" to be executed concurrently by `node.js`. See `evloops.js`. + +**Functions:** + +* `ConditionalLoop(fun, args, conditions, delay):` Defines a loop (see **Loop Definition** below) +* `ConditionalLoop.start(callback):` Starts executing and call `callback` on termination +* `ConditionalLoop.stop():` Terminate the loop +* `LoopConditions.timeLimit(seconds)`, `LoopConditions.maxExecutions(numberOfTimes)`: useful ConditionalLoop conditions +* `LoopUtils.rpsLoop(rps, fun)`: Wrap a `function(loopFun, args)` so ConditionalLoop calls it a set rate +* `LoopUtils.funLoop(fun)`: Wrap a linearly executing `function(args)` so it can be used with a ConditionalLoop + +**Usage:** + +Create a `ConditionalLoop` instance and call `ConditionalLoop.start()` to execute the loop. A function given to `ConditionalLoop` must be a `function(loopFun, args)` which ends by calling `loopFun()`. + +The `conditions` parameter is a list of functions. When any function returns `false`, the loop terminates. For example, the functions `LoopConditions.timeLimit(seconds)` and `LoopConditions.maxExecutions(numberOfTimes)` are conditions that limit the duration and number of iterations of a loop respectively. + +The loop also terminates if `ConditionalLoop.stop()` is called. + +Example: + + var fun = function(loopFun, startTime) { + console.log("It's been " + (new Date() - startTime) / 1000 + " seconds"); + loopFun(); + }; + var stopOnFriday = function() { + return (new Date()).getDay() < 5; + } + var loop = new nl.ConditionalLoop(nl.LoopUtils.rpsLoop(1, fun), new Date(), [stopOnFriday, nl.LoopConditions.timeLimit(604800 /*1 week*/)], 1); + loop.start(function() { console.log("It's Friday!") }); + +**Loop Definition:** + +The `ConditionalLoop` constructor arguments are: + + fun: Function that takes parameters (loopFun, args) and calls loopFun() after each iteration + args: The args parameter to pass to fun + conditions: A list of functions representing termination conditions. Terminate when any function returns `false`. + delay: Seconds to wait before starting the first iteration \ No newline at end of file diff --git a/doc/monitoring.md b/doc/monitoring.md new file mode 100644 index 0000000..ead2c83 --- /dev/null +++ b/doc/monitoring.md @@ -0,0 +1,64 @@ +**This document is out-of-date. See [`lib/monitoring/monitor.js`](https://github.com/benschmaus/nodeload/tree/master/lib/monitoring/monitor.js), [`lib/monitoring/monitorgroup.js`](https://github.com/benschmaus/nodeload/tree/master/lib/monitoring/monitorgroup.js), and [`lib/monitoring/collectors.js`](https://github.com/benschmaus/nodeload/tree/master/lib/monitoring/collectors.js).** + +## Monitoring ## + +`TEST_MONITOR` is an EventEmitter that emits 'update' events at regular intervals. This allows tests to be introspected for things like statistics gathering, report generation, etc. See `monitor.js`. + +To set the interval between 'update' events: + + var nl = require('./lib/nodeloadlib').setMonitorIntervalMs(seconds) + +**Events:** + +* `TEST_MONITOR.on('test', callback(test))`: `addTest()` was called. The newly created test is passed to `callback`. +* `TEST_MONITOR.on('start', callback(tests))`: `startTests()` was called. The list of tests being started is passed to `callback`. +* `TEST_MONITOR.on('end', callback(tests))`: All tests finished. +* `TEST_MONITOR.on('update', callback(tests))`: Emitted at regular intervals while tests are running. Default is every 2 seconds. `nodeloadlib` uses this event internally to track statistics and generate the summary webpage. +* `TEST_MONITOR.on('afterUpdate', callback(tests))`: Emitted after the 'update' event. + +**Usage**: + + nl.TEST_MONITOR.on('update', function(tests) { + for (var i in tests) { + console.log(JSON.stringify(tests[i].stats['latency'].summary())) + } + }); + +## HTTP-specific Monitors ## + +A collection of wrappers for `requestLoop` functions that record statistics for HTTP requests. These functions can be run scheduled with `SCHEDULER` or run with a `ConditionalLoop`. See `evloops.js`. + +**Functions:** + +* `monitorLatenciesLoop(latencies, fun)`: Call `fun()` and put the execution duration in `latencies`, which should be a `Histogram`. +* `monitorResultsLoop(results, fun)`: Call `fun()` and put the HTTP response code in `results`, which should be a `ResultsCounter`. +* `monitorByteReceivedLoop(bytesReceived, fun)`: Call `fun()` and put the number of bytes received in `bytesReceived`, usually an `Accumulator`. +* `monitorConcurrencyLoop(concurrency, fun)`: Call `fun()` and put the number of "threads" currently executing it into `concurrency`, usually a `Peak`. +* `monitorRateLoop(rate, fun)`: Call `fun()` and notify `rate`, which should be a `Rate`, that it was called. +* `monitorHttpFailuresLoop(successCodes, fun, log)`: Call `fun()` and put the HTTP request and response into `log`, which should be a `LogFile`, for every request that does not return an HTTP status code included in the list `successCodes`. +* `monitorUniqueUrlsLoop(uniqs, fun)`: Call `fun()` and put the HTTP request path into `uniqs`, which should be a `Uniques`. +* `loopWrapper(fun, start, finish)`: Create a custom loop wrapper by specifying a functions to execute before and after calling `fun()`. + +**Usage:** + +All of these wrappers return a `function(loopFun, args)` which can be used by `SCHEDULER` and `ConditionalLoop`. The underlying function should have the same signature and execute an HTTP request. It must call `loopFun({req: http.ClientRequest, res: http.ClientResponse})` when it completes the request. + +Example: + + // Issue GET requests to random objects at localhost:8080/data/obj-{0-1000} for 1 minute and + // track the number of unique URLs + var uniq = new nl.Reportable(Uniques, 'Uniques'); + var loop = nl.LoopUtils.monitorUniqueUrlsLoop(uniq, function(loopFun, client) { + var req = nl.traceableRequest(client, 'GET', '/data/obj-' + Math.floor(Math.random()*1000)); + req.on('response', function(res) { + loopFun({req: req, res: res}); + }); + req.end(); + }); + SCHEDULER.schedule({ + fun: loop, + args: http.createClient(8080, 'localhost'), + duration: 60 + }).start(function() { + console.log(JSON.stringify(uniq.summary())); + }); \ No newline at end of file diff --git a/NODELOAD.md b/doc/nl.md similarity index 53% rename from NODELOAD.md rename to doc/nl.md index 6c85092..ff60581 100644 --- a/NODELOAD.md +++ b/doc/nl.md @@ -1,76 +1,64 @@ NAME ---- - nodeload - Load test tool for HTTP APIs. Generates result charts and has hooks for generating requests. + nl - Load test tool for HTTP APIs. Generates result charts and has hooks + for generating requests. SYNOPSIS -------- - nodeload.js [options] :[] + nl.js [options] :[] DESCRIPTION ----------- - nodeload is for generating lots of requests to send to an HTTP API. It is + nl is for generating lots of requests to send to an HTTP API. It is inspired by Apache's ab benchmark tool and is designed to let programmers develop load tests and get informative reports without having to learn a - big and complicated framework.. + big and complicated framework. OPTIONS ------- - -n, --number NUMBER Number of requests to make. Defaults to - value of --concurrency unless a time limit - is specified. + -n, --number NUMBER Number of requests to make. Defaults to + value of --concurrency unless a time limit is specified. -c, --concurrency NUMBER Concurrent number of connections. Defaults to 1. -t, --time-limit NUMBER Number of seconds to spend running test. No timelimit by default. -e, --request-rate NUMBER Target number of requests per seconds. Infinite by default -m, --method STRING HTTP method to use. -d, --data STRING Data to send along with PUT or POST request. - -f, --flot-chart If set, generate an HTML page with a Flot chart of results. -r, --request-generator STRING Path to module that exports getRequest function - -i, --report-interval NUMBER Frequency in seconds to report statistics + -i, --report-interval NUMBER Frequency in seconds to report statistics. Default is 10. -q, --quiet Supress display of progress count info. - -u, --usage Show usage info + -h, --help Show usage info + ENVIRONMENT ----------- - nodeload requires node to be installed somewhere on your path. - - To get a known working combination of nodeload + node grab a release - download or checkout a release tag. - - To find a version of node that's compatible with a tag release do - git show . + nl requires node to be installed somewhere on your path. Get it + from http://nodejs.org/#download. - For example: git show v0.1.1 + To get a known working combination of nodeload + node, be sure + to install using npm: + $ curl http://npmjs.org/install.sh | sh # installs npm + $ npm install nodeload + QUICKSTART ---------- - 1. Install node.js. - 2. Clone nodeload. - 3. cd into nodeload working copy. - 4. git submodule update --init - 5. Start testing! nodeload contains a toy server that you can use for a quick demo. Try the following: - [~/code/nodeload] node examples/test-server.js & - [1] 2756 - [~/code/nodeload] Server running at http://127.0.0.1:8000/ - [~/code/nodeload] ./nodeload.js -f -c 10 -n 10000 -i 1 -r ./examples/test-generator.js localhost:8000 + $ examples/test-server.js & + [1] 2756 + $ Server running at http://127.0.0.1:9000/ + $ nl.js -f -c 10 -n 10000 -i 1 -r examples/test-generator.js localhost:9000 You should now see some test output in your console. The generated HTML report contains a graphical chart of test results. -AUTHORS -------- - - Benjamin Schmaus - Jonathan Lee - THANKS ------ diff --git a/doc/nodeload.md b/doc/nodeload.md new file mode 100644 index 0000000..2850873 --- /dev/null +++ b/doc/nodeload.md @@ -0,0 +1,202 @@ +The `nodeload` module contains a high level interface for constructing load tests for HTTP services. It also includes all of the other modules: [remote](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md), [stats](https://github.com/benschmaus/nodeload/tree/master/doc/stats.md), [monitoring](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md), [reporting](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md), [loop](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md), and [http](https://github.com/benschmaus/nodeload/tree/master/doc/http.md). + +# Quickstart + + $ cat > example.js < http.ClientRequest`. Requests returned by this function are executed by `nodeload`. For example, you can GET random URLs using a `requestGenerator`: + + nl.run({ + requestGenerator: function(client) { + client.request(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); + } + }); + +* Set `requestLoop` to a `function(finished, http.Client)` which calls `finished({req: http.ClientRequest, res: http.ClientResponse})` after each request completes. This is the most flexibile, but the function must be sure to call `finished()`. For example, issue `PUT` requests with proper `If-Match` headers using a `requestLoop`: + + nl.run({ + requestLoop: function(finished, client) { + var req = client.request('GET', '/resource'); + req.on('response', function(res) { + if (res.statusCode !== 200 && res.statusCode !== 404) { + finished({req: req, res: res}); + } else { + var headers = res.headers['etag'] ? {'if-match': res.headers['etag']} : {}; + req = client.request('PUT', '/resource', headers); + req.on('response', function(res) { + finished({req: req, res: res}); + }); + req.end("new value"); + } + }); + req.end(); + } + }); + +Check out [examples/riaktest.ex.js](http://github.com/benschmaus/nodeload/blob/master/examples/riaktest.ex.js) for an example of a full read+write test. + +### Events: + +`run()` returns a `nl.LoadTest` object, which emits these events: + +* `'update', interval, stats`: + + `interval` and `stats` both contains { 'test-name': { 'statistic-name': StatsObject } }. e.g. + + { + 'Read': { + 'latency': [object stats.Histogram], + 'result-codes': [object stats.ResultsCounter] + } + } + + `interval` contains the statistics gathered since the last `'update'` event. `stats` contains cumulative statistics since the beginning of the test. + + Set the frequency of 'update' events in milliseconds by changing loadtest.updateInterval. + +* 'end': all tests finished + + +### Load and User Profiles: + +Profiles can be used to adjust the load and number of users (concurrency) during a load test. The following will linearly ramp up from 0 to 100 req/sec over the first 10 seconds and ramp back down to 0 in the last 10 seconds. It will also ramp up from 0 to 10 users over the first 10 seconds. + + nl.run({ + timeLimit: 40, + loadProfile: [[0,0], [10, 100], [30, 100], [39, 0]], + userProfile: [[0,0], [20, 10]], + }); + + +### Other options: + +The global HTTP server will automatically shutdown after `run(...)` finishes and emits the `'end'` event. This allows the process to terminate after the load test finishes if nothing else is running. To keep the server running, set stayAlive: + + var loadtest = nl.run(...); + loadtest.stayAlive = true; + +### Test Definition: + +The following object defines the parameters and defaults for a test, which is used by `run()`: + + var TEST_OPTIONS = { + name: 'Debug test', // A descriptive name for the test + + // Specify one of: + host: 'localhost', // 1. (host, port) to connect to via HTTP + port: 8080, // + // + connectionGenerator: undefined, // 2. connectionGenerator(), called once for each user. + // The return value is passed as-is to requestGenerator, + // requestLoop, or used internally to generate requests + // when using (method + path + requestData). + + // Specify one of: + requestGenerator: undefined, // 1. requestGenerator: a function + // function(http.Client) -> http.ClientRequest + requestLoop: undefined, // 2. requestLoop: is a function + // function(loopFun, http.Client) + method: 'GET', // If must call: + path: '/', // loopFun({ + requestData: undefined, // req: http.ClientRequest, + // res: http.ClientResponse}); + // after each transaction to finishes to schedule the + // next iteration of requestLoop. + // 3. (method + path + requestData) specify a single URL to + // test + // + + // Specify one of: + numUsers: 10, // 1. numUsers: number of virtual users concurrently + // executing therequest loop + loadProfile: undefined, // 2. loadProfile: array with requests/sec over time: + // [[time (seconds), rps], [time 2, rps], ...] + // For example, ramp up from 100 to 500 rps and then + // down to 0 over 20 seconds: + // [[0, 100], [10, 500], [20, 0]] + + // Specify one of: + targetRps: Infinity, // 1. targetRps: times per second to execute request loop + userProfile: undefined, // 2. userProfile: array with number of users over time: + // [[time (seconds), # users], [time 2, users], ...] + // For example, ramp up from 0 to 100 users and back + // down to 0 over 20 seconds: + // [[0, 0], [10, 100], [20, 0]] + + numRequests: Infinity, // Maximum number of iterations of request loop + timeLimit: 120, // Maximum duration of test in seconds + delay: 0, // Seconds before starting test + + stats: ['latency', // Specify list of: 'latency', 'result-codes', 'uniques', + 'result-codes'], // 'concurrency', 'http-errors'. These following statistics + // may also be specified with parameters: + // + // { name: 'latency', percentiles: [0.9, 0.99] } + // { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' } + // + // Extend this list of statistics by adding to the + // monitor.js#Monitor.Monitors object. + // + // Note: + // - for 'uniques', traceableRequest() must be used + // to create the ClientRequest or only 2 will be detected. + }; \ No newline at end of file diff --git a/doc/remote.md b/doc/remote.md new file mode 100644 index 0000000..d7529a9 --- /dev/null +++ b/doc/remote.md @@ -0,0 +1,37 @@ +**This document is out-of-date. See [`lib/remote/remotetesting.js`](https://github.com/benschmaus/nodeload/tree/master/lib/remote/remotetesting.js) and [`lib/remote/cluster.js`](https://github.com/benschmaus/nodeload/tree/master/lib/remote/cluster.js).** + +## Distributed Testing ## + +Functions to distribute tests across multiple slave `nodeload` instances. See `remote.js`. + +**Functions:** + +* `remoteTest(spec)`: Return a test to be scheduled with `remoteStart(...)` (`spec` uses same format as `addTest(spec)`). +* `remoteStart(master, slaves, tests, callback, stayAliveAfterDone)`: Run tests on specified slaves. +* `remoteStartFile(master, slaves, filename, callback, stayAliveAfterDone)`: Execute a `.js` file on specified slaves. + +**Usage**: + +First, start `nodeloadlib.js` on each slave instances. + + $ node dist/nodeloadlib.js # Run on each slave machine + +Then, create tests using `remoteTest(spec)` with the same `spec` fields in the **Test Definition** section above. Pass the created tests as a list to `remoteStart(...)` to execute them on slave `nodeload` instances. `master` must be the `"host:port"` of the `nodeload` which runs `remoteStart(...)`. It will receive and aggregate statistics from the slaves, so the address should be reachable by the slaves. Or, use `master=null` to disable reports from the slaves. + + // This script must be run on master:8000, which will aggregate results. Each slave + // will GET http://internal-service:8080/ at 100 rps. + var t1 = nl.remoteTest({ + name: "Distributed test", + host: 'internal-service', + port: 8080, + timeLimit: 20, + targetRps: 100 + }); + nl.remoteStart('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], [t1]); + +Alternatively, an existing `nodeload` script file can be used: + + // The file /path/to/load-test.js should contain valid javascript and can use any nodeloadlib functions + nl.remoteStartFile('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], '/path/to/load-test.js'); + +When the remote tests complete, the master instance will call the `callback` parameter if non-null. It then automatically terminates after 3 seconds unless the parameter `stayAliveAfterDone==true`. \ No newline at end of file diff --git a/doc/reporting.md b/doc/reporting.md new file mode 100644 index 0000000..6b265da --- /dev/null +++ b/doc/reporting.md @@ -0,0 +1,43 @@ +**This document is out-of-date. See [`lib/reporting.js`](https://github.com/benschmaus/nodeload/tree/master/lib/reporting.js).** + +## Web-based Reports ## + +Functions for manipulating the report that is available during the test at http://localhost:8000/ and that is written to `results-{timestamp}-summary.html`. + +**Interface:** + +* `REPORT_MANAGER.reports`: All of the reports that are displayed in the summary webpage. +* `REPORT_MANAGER.addReport(Report)`: Add a report object to the webpage. +* `Report(name, updater(Report))`: A report consists of a set of charts, displayed in the main body of the webpage, and a summary object displayed on the right side bar. A report has a name and an updater function. Calling `updater(Report)` should update the report's chart and summary. When tests are running, REPORT_MANAGER calls each report's `updater` periodically. +* `Report.summary`: A JSON object displayed in table form in the summary webpage right side bar. +* `Report.getChart(name)`: Gets or creates a chart with the title `name` to the report and returns a `Chart` object. See `Chart.put(data)` below. +* `Chart.put(data)`: Add the data, which is a map of { 'trend-1': value, 'trend-2': value, ... }, to the chart, which tracks the values for each trend over time. + +**Usage:** + +An HTTP server is started on port 8000 by default. Use: + + `var nl = require('./lib/nodeloadlib).disableServer()` + +to disable the HTTP server, or + + `var nl = require('./lib/nodeloadlib).usePort(port)` + +to change the port binding. The file `results-{timestamp}-summary.html` is written to the current directory. Use + + `var nl = require('./lib/nodeloadlib).disableLogs()` + +to disable creation of this file. + +A report is automatically added for each test created by `addTest()` or `runTest()`. To add additional charts to the summary webpage: + + var mycounter = 0; + REPORT_MANAGER.addReport(new Report("My Report", function(report) { + chart = report.getChart("My Chart"); + chart.put({ 'counter': mycounter++ }); + chart.summary = { 'Total increments': mycounter }; + })); + +The webpage automatically issues an AJAX request to refresh the text and chart data every 2 seconds by default. Change the refresh period using: + + `var nl = require('./lib/nodeloadlib).setAjaxRefreshIntervalMs(milliseconds)` diff --git a/doc/stats.md b/doc/stats.md new file mode 100644 index 0000000..cdc888c --- /dev/null +++ b/doc/stats.md @@ -0,0 +1,44 @@ +## Statistics ## + +Implementations of various statistics. See [`lib/stats.js`](https://github.com/benschmaus/nodeload/tree/master/lib/stats.js). + +**Classes:** + +* `Histogram(numBuckets)`: A histogram of integers. If most of the items are between 0 and `numBuckets`, calculating percentiles and stddev is fast. +* `Accumulator`: Calculates the sum of the numbers put in. +* `ResultsCounter`: Tracks results which are be limited to a small set of possible choices. Tracks the total number of results, number of results by value, and results added per second. +* `Uniques`: Tracks the number of unique items added. +* `Peak`: Tracks the max of the numbers put in. +* `Rate`: Tracks the rate at which items are added. +* `LogFile`: Outputs to a file on disk. +* `NullLog`: Ignores all items put in. +* `Reportable`: Wraps any other statistic to store an interval and cumulative version of it. + +**Functions:** + +* `randomString(length)`: Returns a random string of ASCII characters between 32 and 126 of the requested length. +* `nextGaussian(mean, stddev)`: Returns a normally distributed number using the provided mean and standard deviation. +* `nextPareto(min, max, shape)`: Returns a Pareto distributed number between `min` and `max` inclusive using the provided shape. +* `roundRobin(list)`: Returns a copy of the list with a `get()` method. `get()` returns list entries round robin. + +**Usage:** + +All of the statistics classes support the methods: + +* `.length`: The total number of items `put()` into this object. +* `put(item)`: Include an item in the statistic. +* `get()`: Get a specific value from the object, which varies depending on the object. +* `clear()`: Clear out all items. +* `summary()`: Get a object containing a summary of the object, which varies depending on the object. The fields returned are used to generate the trends of the HTML report graphs. + +In addition, these other methods are supported: + +* `Histogram.mean()`: Calculate the mean of the numbers in the histogram. +* `Histogram.percentile(percentile)`: Calculate the given `percentile`, between 0 and 1, of the numbers in the histogram. +* `Histogram.stddev()`: Standard deviation of the numbers in the histogram. +* `LogFile.open()`: Open the file. +* `LogFile.clear(text)`: Truncate the file, and write `text` if specified. +* `LogFile.close()`: Close the file. +* `Reportable.next()`: clear out the interval statistic for the next window. + +Refer to the [`lib/stats.js`](https://github.com/benschmaus/nodeload/tree/master/lib/stats.js) for the return value of the `get()` and `summary()` functions for the different classes. \ No newline at end of file diff --git a/doc/tips.md b/doc/tips.md new file mode 100644 index 0000000..74e8e44 --- /dev/null +++ b/doc/tips.md @@ -0,0 +1,64 @@ +**This page is out of date** + +TIPS AND TRICKS +================ + +Some handy features of `nodeload` worth mentioning. + +1. **Examine and add to stats to the HTML page:** + + addTest().stats and runTest().stats are maps: + + { 'latency': Reportable(Histogram), + 'result-codes': Reportable(ResultsCounter}, + 'uniques': Reportable(Uniques), + 'concurrency': Reportable(Peak) } + + Put `Reportable` instances to this map to have it automatically updated each reporting interval and added to the summary webpage. + +2. **Post-process statistics:** + + Use a `startTests()` callback to examine the final statistics in `test.stats[name].cumulative` at test completion. + + // GET random URLs of the form localhost:8080/data/object-#### for 10 seconds, then + // print out all the URLs that were hit. + var t = addTest({ + timeLimit: 10, + targetRps: 10, + stats: ['uniques'], + requestGenerator: function(client) { + return traceableRequest(client, 'GET', '/data/object-' + Math.floor(Math.random()*100));; + } + }); + function printAllUrls() { + console.log(JSON.stringify(t.stats['uniques'].cumulative)); + } + startTests(printAllUrls); + + +3. **Out-of-the-box file server:** + + Just start `nodeloadlib.js` and it will serve files in the current directory. + + $ node lib/nodeloadlib.js + $ curl -i localhost:8000/lib/nodeloadlib.js # executed in a separate terminal + HTTP/1.1 200 OK + Content-Length: 50763 + Connection: keep-alive + + var sys = require('sys'); + var http = require('http'); + ... + +4. **Run arbitrary Javascript:** + + POST any valid Javascript to `/remote` to have it `eval()`'d. + + $ node dist/nodeloadlib.js + Serving progress report on port 8000. + Opening log files. + Received remote command: + sys.puts("hello!") + hello! + + $ curl -i -d 'sys.puts("hello!")' localhost:8000/remote # executed in a separate terminal diff --git a/examples/loadtesting.ex.js b/examples/nodeload.ex.js similarity index 91% rename from examples/loadtesting.ex.js rename to examples/nodeload.ex.js index 8a00e93..fbb54c3 100755 --- a/examples/loadtesting.ex.js +++ b/examples/nodeload.ex.js @@ -22,9 +22,7 @@ var i = 0, userProfile: [[0,0], [20, 10]], stats: ['result-codes', {name: 'latency', percentiles: [0.95, 0.999]}, 'concurrency', 'uniques', 'request-bytes', 'response-bytes'], requestGenerator: function(client) { - var request = client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); - request.end(); - return request; + return client.request('GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); } }, writetest = { diff --git a/examples/nodeloadlib-ex.js b/examples/nodeloadlib-ex.js deleted file mode 100755 index fedf833..0000000 --- a/examples/nodeloadlib-ex.js +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env node - -// Instructions: -// -// 1. Get node (http://nodejs.org/#download) -// 2. git clone http://github.com/benschmaus/nodeload.git -// 3. node nodeload/examples/nodeloadlib-ex.js -// -// This example performs a micro-benchmark of Riak (http://riak.basho.com/), a key-value store, -// running on localhost:8098/riak. First, it first loads 2000 objects into the store as quickly -// as possible. Then, it performs a 90% read + 10% update test at total request rate of 300 rps. -// From minutes 5-8, the read load is increased by 100 rps. The test runs for 10 minutes. - -var sys = require('sys'), - nl = require('../dist/nodeloadlib'); - -function riakUpdate(loopFun, client, url, body) { - var req = nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - req.on('response', function(response) { - if (response.statusCode != 200 && response.statusCode != 404) { - loopFun({req: req, res: response}); - } else { - var headers = { - 'host': 'localhost', - 'content-type': 'text/plain', - 'x-riak-client-id': 'bmxpYg==' - }; - if (response.headers['x-riak-vclock'] != null) - headers['x-riak-vclock'] = response.headers['x-riak-vclock']; - - req = nl.traceableRequest(client, 'PUT', url, headers, body); - req.on('response', function(response) { - loopFun({req: req, res: response}); - }); - req.end(); - } - }); - req.end(); -} - -var i=0; -nl.runTest({ - name: "Load Data", - host: 'localhost', - port: 8098, - numClients: 20, - numRequests: 2000, - timeLimit: Infinity, - successCodes: [204], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - requestLoop: function(loopFun, client) { - riakUpdate(loopFun, client, '/riak/b/o' + i++, 'original value'); - } -}, startRWTest); - -function startRWTest() { - console.log("Running read + update test."); - - var reads = nl.addTest({ - name: "Read", - host: 'localhost', - port: 8098, - numClients: 30, - timeLimit: 600, - targetRps: 270, - successCodes: [200,404], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - requestGenerator: function(client) { - var url = '/riak/b/o' + Math.floor(Math.random()*8000); - return nl.traceableRequest(client, 'GET', url, { 'host': 'localhost' }); - } - }); - var writes = nl.addTest({ - name: "Write", - host: 'localhost', - port: 8098, - numClients: 5, - timeLimit: 600, - targetRps: 30, - successCodes: [204], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - requestLoop: function(loopFun, client) { - var url = '/riak/b/o' + Math.floor(Math.random()*8000); - riakUpdate(loopFun, client, url, 'updated value'); - } - }); - - // From minute 5, schedule 10x 10 read requests per second in 3 minutes = adding 100 requests/sec - nl.addRamp({ - test: reads, - numberOfSteps: 10, - rpsPerStep: 10, - clientsPerStep: 2, - timeLimit: 180, - delay: 300 - }); - - nl.startTests(); -} diff --git a/examples/nodeloadlib-ex2.js b/examples/nodeloadlib-ex2.js deleted file mode 100755 index a4be4b7..0000000 --- a/examples/nodeloadlib-ex2.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node - -// Self contained node.js HTTP server and a load test against it. Just run: -// -// node examples/nodeloadlib-ex2.js -// -var http = require('http'); -var sys = require('sys'); -var nl = require('../lib/nodeloadlib'); -sys.puts("Test server on localhost:9000."); -http.createServer(function (req, res) { - res.writeHead((Math.random() < .8) ? 200 : 404, {'Content-Type': 'text/plain'}); - res.write('foo\n'); - res.end(); -}).listen(9000); - -var test = nl.addTest({ - name: "Read", - host: 'localhost', - port: 9000, - numClients: 10, - timeLimit: 600, - targetRps: 500, - successCodes: [200,404], - reportInterval: 2, - stats: ['result-codes', 'latency', 'concurrency', 'uniques'], - latencyConf: {percentiles: [.90, .999]}, - requestGenerator: function(client) { - return nl.traceableRequest(client, 'GET', "/" + Math.floor(Math.random()*8000), { 'host': 'localhost' }); - } -}); - -nl.startTests(); diff --git a/examples/riaktest.ex.js b/examples/riaktest.ex.js new file mode 100755 index 0000000..e9d26a7 --- /dev/null +++ b/examples/riaktest.ex.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +// Instructions: +// +// 1. Get node (http://nodejs.org/#download) +// 2. git clone http://github.com/benschmaus/nodeload.git +// 3. examples/riaktest.ex.js +// +// This example performs a micro-benchmark of Riak (http://riak.basho.com/), a key-value store, +// running on localhost:8098/riak. First, it first loads 2000 objects into the store as quickly +// as possible. Then, it performs a 90% read + 10% update test at total request rate of 300 rps. +// From minutes 5-8, the read load is increased by 100 rps. The test runs for 10 minutes. + +var sys = require('sys'), + nl = require('../nodeload'); + +function riakUpdate(loopFun, client, url, body) { + var req = client.request('GET', url, { 'host': 'localhost' }); + req.on('response', function(res) { + if (res.statusCode !== 200 && res.statusCode !== 404) { + loopFun({req: req, res: res}); + } else { + var headers = { + 'host': 'localhost', + 'content-type': 'text/plain', + 'x-riak-client-id': 'bmxpYg==' + }; + if (res.headers['x-riak-vclock']) { + headers['x-riak-vclock'] = res.headers['x-riak-vclock']; + } + + req = client.request('PUT', url, headers); + req.on('response', function(res) { + loopFun({req: req, res: res}); + }); + req.end(body); + } + }); + req.end(); +} + +var i=0; +var loadData = nl.run({ + name: "Load Data", + host: 'localhost', + port: 8098, + numUsers: 20, + numRequests: 2000, + timeLimit: Infinity, + stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [204], log: 'http-errors.log' }], + requestLoop: function(loopFun, client) { + riakUpdate(loopFun, client, '/riak/b/o' + i++, 'original value'); + } +}); + +loadData.on('end', function() { + console.log("Running read + update test."); + + var reads = { + name: "Read", + host: 'localhost', + port: 8098, + numUsers: 30, + loadProfile: [[0,0],[20,270],[300,270],[480,370],[590,400],[599,0]], // Ramp up to 270, then up to 370, then down to 0 + timeLimit: 600, + stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [200,404], log: 'http-errors.log' }], + requestGenerator: function(client) { + var url = '/riak/b/o' + Math.floor(Math.random()*8000); + return client.request('GET', url, { 'host': 'localhost' }); + } + }, + writes = { + name: "Write", + host: 'localhost', + port: 8098, + numUsers: 5, + timeLimit: 600, + targetRps: 30, + reportInterval: 2, + stats: ['result-codes', 'latency', 'concurrency', 'uniques', { name: 'http-errors', successCodes: [204], log: 'http-errors.log' }], + requestLoop: function(loopFun, client) { + var url = '/riak/b/o' + Math.floor(Math.random()*8000); + riakUpdate(loopFun, client, url, 'updated value'); + } + }; + + nl.run(reads, writes); +}); \ No newline at end of file diff --git a/examples/sample-report.html b/examples/sample-report.html old mode 100755 new mode 100644 index 1fff25b..253f524 --- a/examples/sample-report.html +++ b/examples/sample-report.html @@ -1,32 +1,113 @@ -Response Times over Time - - - - -

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

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

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

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

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

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

Test Results

\n

<%=new Date()%>

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

Cumulative

\n
\n
\n
\n
\n

generated with nodeload

\n \n\n \n";var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var fs=require('fs');} +var template={cache_:{},create:function(str,data,callback){var fn;if(!/[\t\r\n% ]/.test(str)){if(!callback){fn=this.create(fs.readFileSync(str).toString('utf8'));}else{fs.readFile(str,function(err,buffer){if(err){throw err;} +this.create(buffer.toString('utf8'),data,callback);});return;}}else{if(this.cache_[str]){fn=this.cache_[str];}else{fn=new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};"+"obj=obj||{};"+"with(obj){p.push('"+ +str.split("'").join("\\'").split("\n").join("\\n").replace(/<%([\s\S]*?)%>/mg,function(m,t){return'<%'+t.split("\\'").join("'").split("\\n").join("\n")+'%>';}).replace(/<%=(.+?)%>/g,"',$1,'").split("<%").join("');").split("%>").join("p.push('")+"');}return p.join('');");this.cache_[str]=fn;}} +if(callback){callback(data?fn(data):fn);} +else{return data?fn(data):fn;}}};exports.create=template.create.bind(template);var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var util=require('../util');var querystring=require('querystring');var LogFile=require('../stats').LogFile;var template=require('./template');var config=require('../config');var REPORT_SUMMARY_TEMPLATE=require('./summary.tpl.js').REPORT_SUMMARY_TEMPLATE;var NODELOAD_CONFIG=config.NODELOAD_CONFIG;var START=NODELOAD_CONFIG.START;var DYGRAPH_SOURCE=require('./dygraph.tpl.js').DYGRAPH_SOURCE;var HTTP_SERVER=require('../http').HTTP_SERVER;} var Chart,timeFromStart;var Report=exports.Report=function(name){this.name=name;this.uid=util.uid();this.summary={};this.charts={};};Report.prototype={getChart:function(name){if(!this.charts[name]){this.charts[name]=new Chart(name);} return this.charts[name];},updateFromMonitor:function(monitor){monitor.on('update',this.doUpdateFromMonitor_.bind(this,monitor,''));return this;},updateFromMonitorGroup:function(monitorGroup){var self=this;monitorGroup.on('update',function(){util.forEach(monitorGroup.monitors,function(monitorname,monitor){self.doUpdateFromMonitor_(monitor,monitorname);});});return self;},doUpdateFromMonitor_:function(monitor,monitorname){var self=this;monitorname=monitorname?monitorname+' ':'';util.forEach(monitor.stats,function(statname,stat){util.forEach(stat.summary(),function(name,val){self.summary[self.name+' '+monitorname+statname+' '+name]=val;});if(monitor.interval[statname]){self.getChart(monitorname+statname).put(monitor.interval[statname].summary());}});}};var Chart=exports.Chart=function(name){this.name=name;this.uid=util.uid();this.columns=["time"];this.rows=[[timeFromStart()]];};Chart.prototype={put:function(data){var self=this,row=[timeFromStart()];util.forEach(data,function(column,val){var col=self.columns.indexOf(column);if(col<0){col=self.columns.length;self.columns.push(column);self.rows[0].push(0);} row[col]=val;});self.rows.push(row);}};var ReportGroup=exports.ReportGroup=function(){this.reports=[];this.logNameOrObject='results-'+START.getTime()+'.html';};ReportGroup.prototype={addReport:function(report){report=(typeof report==='string')?new Report(report):report;this.reports.push(report);return report;},setLogFile:function(logNameOrObject){this.logNameOrObject=logNameOrObject;},setLoggingEnabled:function(enabled){clearTimeout(this.loggingTimeoutId);if(enabled){this.logger=this.logger||(typeof this.logNameOrObject==='string')?new LogFile(this.logNameOrObject):this.logNameOrObject;this.loggingTimeoutId=setTimeout(this.writeToLog_.bind(this),this.refreshIntervalMs);}else if(this.logger){this.logger.close();this.logger=null;} return this;},reset:function(){this.reports={};},getHtml:function(){var self=this,t=template.create(REPORT_SUMMARY_TEMPLATE);return t({DYGRAPH_SOURCE:DYGRAPH_SOURCE,querystring:querystring,refreshPeriodMs:self.refreshIntervalMs,reports:self.reports});},writeToLog_:function(){this.loggingTimeoutId=setTimeout(this.writeToLog_.bind(this),this.refreshIntervalMs);this.logger.clear(this.getHtml());}};var REPORT_MANAGER=exports.REPORT_MANAGER=new ReportGroup();NODELOAD_CONFIG.on('apply',function(){REPORT_MANAGER.refreshIntervalMs=REPORT_MANAGER.refreshIntervalMs||NODELOAD_CONFIG.AJAX_REFRESH_INTERVAL_MS;REPORT_MANAGER.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED);});HTTP_SERVER.addRoute('^/$',function(url,req,res){var html=REPORT_MANAGER.getHtml();res.writeHead(200,{"Content-Type":"text/html","Content-Length":html.length});res.write(html);res.end();});HTTP_SERVER.addRoute('^/reports$',function(url,req,res){var json=JSON.stringify(REPORT_MANAGER.reports);res.writeHead(200,{"Content-Type":"application/json","Content-Length":json.length});res.write(json);res.end();});function timeFromStart(){return(Math.floor((new Date().getTime()-START)/600)/100);} var BUILD_AS_SINGLE_FILE;if(BUILD_AS_SINGLE_FILE===undefined){var http=require('http');var util=require('./util');var stats=require('./stats');var reporting=require('./reporting');var qputs=util.qputs;var qprint=util.qprint;var EventEmitter=require('events').EventEmitter;var MultiLoop=require('./loop').MultiLoop;var Monitor=require('./monitoring').Monitor;var Report=reporting.Report;var LogFile=stats.LogFile;var NODELOAD_CONFIG=require('./config').NODELOAD_CONFIG;var START=NODELOAD_CONFIG.START;var REPORT_MANAGER=reporting.REPORT_MANAGER;var HTTP_SERVER=require('./http').HTTP_SERVER;} -var TEST_OPTIONS={name:'Debug test',host:'localhost',port:8080,connectionGenerator:undefined,requestGenerator:undefined,requestLoop:undefined,method:'GET',path:'/',requestData:undefined,numUsers:10,loadProfile:undefined,targetRps:Infinity,userProfile:undefined,numRequests:Infinity,timeLimit:120,delay:0,stats:['latency','result-codes'],};var LoadTest,generateConnection,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){var generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} +var TEST_OPTIONS={name:'Debug test',host:'localhost',port:8080,connectionGenerator:undefined,requestGenerator:undefined,requestLoop:undefined,method:'GET',path:'/',requestData:undefined,numUsers:10,loadProfile:undefined,targetRps:Infinity,userProfile:undefined,numRequests:Infinity,timeLimit:120,delay:0,stats:['latency','result-codes'],};var LoadTest,generateConnection,requestGeneratorLoop;var run=exports.run=function(specs){specs=(specs instanceof Array)?specs:util.argarray(arguments);var tests=specs.map(function(spec){spec=util.defaults(spec,TEST_OPTIONS);var generateRequest=function(client){if(spec.requestGenerator){return spec.requestGenerator(client);} var request=client.request(spec.method,spec.path,{'host':spec.host});if(spec.requestData){request.write(spec.requestData);} return request;},loop=new MultiLoop({fun:spec.requestLoop||requestGeneratorLoop(generateRequest),argGenerator:spec.connectionGenerator||generateConnection(spec.host,spec.port,!spec.requestLoop),concurrencyProfile:spec.userProfile||[[0,spec.numUsers]],rpsProfile:spec.loadProfile||[[0,spec.targetRps]],duration:spec.timeLimit,numberOfTimes:spec.numRequests,delay:spec.delay}),monitor=new Monitor(spec.stats),report=new Report(spec.name).updateFromMonitor(monitor);loop.on('add',function(loops){monitor.monitorObjects(loops,'startiteration','enditeration');});REPORT_MANAGER.addReport(report);monitor.name=spec.name;monitor.setLoggingEnabled(NODELOAD_CONFIG.LOGS_ENABLED);return{spec:spec,loop:loop,monitor:monitor,report:report,};});var loadtest=new LoadTest(tests).start();return loadtest;};var LoadTest=exports.LoadTest=function LoadTest(tests){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this;self.tests=tests;self.updateInterval=NODELOAD_CONFIG.MONITOR_INTERVAL_MS;self.interval={};self.stats={};self.tests.forEach(function(test){self.interval[test.spec.name]=test.monitor.interval;self.stats[test.spec.name]=test.monitor.stats;});self.finishChecker_=this.checkFinished_.bind(this);};util.inherits(LoadTest,EventEmitter);LoadTest.prototype.start=function(keepAlive){var self=this;self.keepAlive=keepAlive;process.nextTick(self.emit.bind(self,'start'));self.tests.forEach(function(test){test.loop.start();test.loop.on('end',self.finishChecker_);});if(!HTTP_SERVER.running&&NODELOAD_CONFIG.HTTP_ENABLED){HTTP_SERVER.start(NODELOAD_CONFIG.HTTP_PORT);} return self;};LoadTest.prototype.stop=function(){this.tests.forEach(function(t){t.loop.stop();});return this;};LoadTest.prototype.update=function(){this.emit('update',this.interval,this.stats);this.tests.forEach(function(t){t.monitor.update();});qprint('.');};LoadTest.prototype.checkFinished_=function(){if(this.tests.some(function(t){return t.loop.running;})){return;} @@ -117,7 +117,7 @@ done();});req.end();};Slave.prototype.defineMethod=function(name,fun){var self=t var Slaves=exports.Slaves=function Slaves(masterEndpoint,pingInterval){EventEmitter.call(this);this.masterEndpoint=masterEndpoint;this.slaves=[];this.pingInterval=pingInterval;};util.inherits(Slaves,EventEmitter);Slaves.prototype.add=function(hostAndPort){var self=this,parts=hostAndPort.split(':'),host=parts[0],port=Number(parts[1])||8000,id=host+':'+port,slave=new Slave(id,host,port,self.masterEndpoint,self.pingInterval);self.slaves.push(slave);self[id]=slave;self[id].on('slaveError',function(err){self.emit('slaveError',slave,err);});self[id].on('start',function(){var allStarted=util.every(self.slaves,function(id,s){return s.state==='started';});if(!allStarted){return;} self.emit('start');});self[id].on('end',function(){var allStopped=util.every(self.slaves,function(id,s){return s.state!=='started';});if(!allStopped){return;} self.emit('end');});};Slaves.prototype.defineMethod=function(name,fun){var self=this;self.slaves.forEach(function(slave){slave.defineMethod(name,fun);});self[name]=function(){var args=arguments;return self.slaves.map(function(s){return s[name].apply(s,args);});};};Slaves.prototype.start=function(){this.slaves.forEach(function(s){s.start();});};Slaves.prototype.end=function(){this.slaves.forEach(function(s){s.end();});};var BUILD_AS_SINGLE_FILE;if(!BUILD_AS_SINGLE_FILE){var url=require('url');var util=require('../util');var Endpoint=require('./endpoint').Endpoint;var EndpointClient=require('./endpointclient').EndpointClient;var EventEmitter=require('events').EventEmitter;var NODELOAD_CONFIG=require('../config').NODELOAD_CONFIG;} -var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);this.id=spec.id;this.masterClient_=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.slaveEndpoint_=this.createEndpoint_(server,spec.slaveMethods);this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.slaveEndpoint_.start();this.slaveEndpoint_.context.id=this.id;this.slaveEndpoint_.context.state='initialized';this.url=this.slaveEndpoint_.url;this.updateInterval=(spec.pingInterval>=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.updateInterval=0;this.slaveEndpoint_.end();if(this.masterClient_){this.masterClient_.destroy();} +var SlaveNode=exports.SlaveNode=function SlaveNode(server,spec){EventEmitter.call(this);util.PeriodicUpdater.call(this);var self=this,slaveState='initialized';this.id=spec.id;this.masterClient_=spec.master?this.createMasterClient_(spec.master,spec.masterMethods):null;this.slaveEndpoint_=this.createEndpoint_(server,spec.slaveMethods);this.slaveEndpoint_.setStaticParams([this.masterClient_]);this.slaveEndpoint_.on('start',function(){this.emit.bind(this,'start');});this.slaveEndpoint_.on('end',this.end.bind(this));this.slaveEndpoint_.start();this.slaveEndpoint_.context.id=this.id;this.slaveEndpoint_.context.__defineGetter__('state',function(){return slaveState;});this.slaveEndpoint_.context.__defineSetter__('state',function(val){slaveState=val;self.update();});this.url=this.slaveEndpoint_.url;this.updateInterval=(spec.pingInterval>=0)?spec.pingInterval:NODELOAD_CONFIG.SLAVE_UPDATE_INTERVAL_MS;};util.inherits(SlaveNode,EventEmitter);SlaveNode.prototype.end=function(){this.updateInterval=0;this.slaveEndpoint_.end();if(this.masterClient_){this.masterClient_.destroy();} this.emit('end');};SlaveNode.prototype.update=function(){if(this.masterClient_){this.masterClient_.updateSlaveState_(this.slaveEndpoint_.context.state);}};SlaveNode.prototype.createEndpoint_=function(server,methods){var endpoint=new Endpoint(server);if(methods){try{methods.forEach(function(m){var fun;eval('fun='+m.fun);endpoint.defineMethod(m.name,fun);});}catch(e){endpoint.end();endpoint=null;throw e;}} return endpoint;};SlaveNode.prototype.createMasterClient_=function(masterUrl,methods){var parts=url.parse(masterUrl),masterClient=new EndpointClient(parts.hostname,Number(parts.port)||8000,parts.pathname);masterClient.defineMethod('updateSlaveState_');if(methods&&methods instanceof Array){methods.forEach(function(m){masterClient.defineMethod(m);});} masterClient.setStaticParams([this.id]);masterClient.on('error',this.emit.bind(this,'masterError'));return masterClient;};var installRemoteHandler=exports.installRemoteHandler=function(server){var slaveNodes=[];server.addRoute('^/remote/?$',function(path,req,res){if(req.method==='POST'){util.readStream(req,function(body){var slaveNode;try{body=JSON.parse(body);slaveNode=new SlaveNode(server,body);}catch(e){res.writeHead(400);res.end(e.toString());return;} diff --git a/test/loop.test.js b/test/loop.test.js index d5c0d96..fbb7969 100644 --- a/test/loop.test.js +++ b/test/loop.test.js @@ -23,7 +23,7 @@ module.exports = { beforeExit(function() { assert.equal(i, 5, 'loop executed incorrect number of times: ' + i); assert.ok(!l.running, 'loop still flagged as running'); - assert.ok(Math.abs(duration - 1000) <= 50, '1000 == ' + duration); + assert.ok(Math.abs(duration - 1000) <= 60, '1000 == ' + duration); }); }, 'example: use Scheduler to vary execution rate and concurrency': function (assert, beforeExit) { From 4dba58e1a320e32d12d48268f40dc3478ca53995 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 22:26:41 -0500 Subject: [PATCH 38/41] SlaveNode calls master.updateSlaveState_() immediately when its state changes --- lib/remote/slavenode.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/remote/slavenode.js b/lib/remote/slavenode.js index 0587776..45a0a47 100644 --- a/lib/remote/slavenode.js +++ b/lib/remote/slavenode.js @@ -34,6 +34,7 @@ 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); @@ -43,7 +44,11 @@ var SlaveNode = exports.SlaveNode = function SlaveNode(server, spec) { this.slaveEndpoint_.start(); this.slaveEndpoint_.context.id = this.id; - this.slaveEndpoint_.context.state = 'initialized'; + 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; From 1540ec97835bd1a993f3a4490b720efa90163ab6 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Tue, 30 Nov 2010 22:45:15 -0500 Subject: [PATCH 39/41] Minor doc updates. --- README.md | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index acdd977..560a7c3 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,33 @@ INSTALLING ================ -Using [npm](http://npmjs.org/): +Make sure [node.js](http://nodejs.org/#download) is installed. Then install `nodeload`: - curl http://npmjs.org/install.sh | sh # install npm if not already installed - npm install nodeload +1. Using [npm](http://npmjs.org/): -From source: + curl http://npmjs.org/install.sh | sh # install npm if not already installed + npm install nodeload - git clone git://github.com/benschmaus/nodeload.git - cd nodeload - npm link # optional. enables require('nodeload/') instead of require('./lib/'). +2. From source: -Or as a single file (this does not install the `nl.js` tool): + git clone git://github.com/benschmaus/nodeload.git + cd nodeload + npm link # optional. enables require('nodeload/') instead of require('./lib/'). - wget https://github.com/benschmaus/nodeload/raw/master/nodeload.js +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 ================ -`nodeload` is collection of [node.js](http://nodejs.org/) modules for load testing HTTP services. +`nodeload` is collection of independent [node.js](http://nodejs.org/) modules for load testing HTTP services. -As a developer, you should be able write load tests and get informative reports without having to learn another framework. You should be able to build tests by example and selectively use the parts of the tool that fit your task. Being a library means that you can use as much or as little of `nodeload` as makes sense, and you can create load tests with the power of a full programming language. For example, if you need to execute some function at a given rate, just use the [`'nodeload/loop'`](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) module, and write the rest yourself. +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. In addition, `nodeload` is built for operability. It can always be deployed by simply copying the single file, `nodeload.js`. -Here are examples of each module, which can be used separately. Look for more examples in the [`examples/`](https://github.com/benschmaus/nodeload/tree/master/examples) directory and in test cases prefixed with "example" in [`test/`](https://github.com/benschmaus/nodeload/tree/master/test): +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) @@ -38,7 +40,7 @@ will send 10,000 queries to http://localhost:9000 using 10 connections. Statisti ### [nodeload](https://github.com/benschmaus/nodeload/tree/master/doc/nodeload.md) -The `nodeload` module is the primary interface for creating load tests. It includes all of the other modules described below, so if you `require('nodeload')`, you don't need to `require()` any of the other ones. Look at the examples in [`examples/loadtesting.ex.js`](https://github.com/benschmaus/nodeload/tree/master/examples/loadtesting.ex.js) and [`examples/riaktest.ex.js`]((https://github.com/benschmaus/nodeload/tree/master/examples/riaktest.ex.js) or read the [nodeload module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/nodeload.md). +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({ @@ -56,7 +58,7 @@ The `nodeload` module is the primary interface for creating load tests. It inclu ### [remote](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md) -The `remote` module provides a mechanism for running a distributed load test. See the [remote module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/remote.md). +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). Start slave instances: @@ -94,7 +96,7 @@ will output "`Mean: 0.852, 99%: 3`". ### [monitoring](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md) -The `monitoring` module provides a way to track runtime statistics for code that is run concurrently. See the [monitoring module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/monitoring.md). +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'); @@ -111,22 +113,26 @@ will output "`Median runtime (ms): 497`". ### [reporting](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md) -The `reporting` module provides a way to produce HTML graphs from code. See the [reporting module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/reporting.md). +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'), + var reporting = require('nodeload/reporting'), + stats = require('nodeload/stats'), report = reporting.REPORT_MANAGER.addReport('Random Numbers'), chart = report.getChart('Gaussian / Pareto vs. Time (minutes)'); - for (var i = 0; i < 10; i++) { + for (var timeout = 0; timeout < 5000; timeout+=500) { setTimeout(function() { - chart.put({'Pareto': stats.nextPareto(0, 100), 'Gaussian': stats.nextGaussian()}); - }, i * 500); + 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 and read the [loop module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/loop.md) for details. +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'), @@ -148,7 +154,7 @@ will output "`Total requests: 30`". ### [http](https://github.com/benschmaus/nodeload/tree/master/doc/http.md) -The `http` module provides a generic HTTP server that serves static files and that can be configured with new routes. See the [http module documentation](https://github.com/benschmaus/nodeload/tree/master/doc/http.md). +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); @@ -162,4 +168,4 @@ The `http` module provides a generic HTTP server that serves static files and th CONTRIBUTING ================ -Contributions are always welcome. File bugs on [github](https://github.com/benschmaus/nodeload/issues), email any of the authors, and fork away! [developers.md](https://github.com/benschmaus/nodeload/tree/master/doc/developers.md) has brief instructions on getting tests up and running, and will hold more design details in the future. \ No newline at end of file +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 From 4deb379690f1e8752909223c2b6e44081c4958d3 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Wed, 1 Dec 2010 00:15:58 -0500 Subject: [PATCH 40/41] Add release notes --- RELEASE-NOTES.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 RELEASE-NOTES.md 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 From dd59c5a17402296ef45a31943c38fa7f377c7201 Mon Sep 17 00:00:00 2001 From: Jonathan Lee Date: Wed, 1 Dec 2010 09:33:13 -0500 Subject: [PATCH 41/41] Add stats.StatsGroup --- TODO | 1 + lib/stats.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++ test/stats.test.js | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 test/stats.test.js diff --git a/TODO b/TODO index 0a4ac85..800a6f0 100644 --- a/TODO +++ b/TODO @@ -2,6 +2,7 @@ - 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 \ No newline at end of file diff --git a/lib/stats.js b/lib/stats.js index 862cf4f..2ab7537 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -400,6 +400,66 @@ Reportable.prototype = { } }; +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) { 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); + } +};