autobench 0.0.1alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/README.md +123 -0
- data/bin/autobench +180 -0
- data/bin/autobench-config +162 -0
- data/lib/autobench.rb +28 -0
- data/lib/autobench/client.rb +78 -0
- data/lib/autobench/common.rb +49 -0
- data/lib/autobench/config.rb +62 -0
- data/lib/autobench/render.rb +102 -0
- data/lib/autobench/version.rb +3 -0
- data/lib/autobench/yslow.rb +75 -0
- data/lib/phantomas/README.md +296 -0
- data/lib/phantomas/core/formatter.js +65 -0
- data/lib/phantomas/core/helper.js +64 -0
- data/lib/phantomas/core/modules/requestsMonitor/requestsMonitor.js +214 -0
- data/lib/phantomas/core/pads.js +16 -0
- data/lib/phantomas/core/phantomas.js +418 -0
- data/lib/phantomas/lib/args.js +27 -0
- data/lib/phantomas/lib/modules/_coffee-script.js +2 -0
- data/lib/phantomas/lib/modules/assert.js +326 -0
- data/lib/phantomas/lib/modules/events.js +216 -0
- data/lib/phantomas/lib/modules/http.js +55 -0
- data/lib/phantomas/lib/modules/path.js +441 -0
- data/lib/phantomas/lib/modules/punycode.js +510 -0
- data/lib/phantomas/lib/modules/querystring.js +214 -0
- data/lib/phantomas/lib/modules/tty.js +7 -0
- data/lib/phantomas/lib/modules/url.js +625 -0
- data/lib/phantomas/lib/modules/util.js +520 -0
- data/lib/phantomas/modules/ajaxRequests/ajaxRequests.js +15 -0
- data/lib/phantomas/modules/assetsTypes/assetsTypes.js +21 -0
- data/lib/phantomas/modules/cacheHits/cacheHits.js +28 -0
- data/lib/phantomas/modules/caching/caching.js +66 -0
- data/lib/phantomas/modules/cookies/cookies.js +54 -0
- data/lib/phantomas/modules/domComplexity/domComplexity.js +130 -0
- data/lib/phantomas/modules/domQueries/domQueries.js +148 -0
- data/lib/phantomas/modules/domains/domains.js +49 -0
- data/lib/phantomas/modules/globalVariables/globalVariables.js +44 -0
- data/lib/phantomas/modules/headers/headers.js +48 -0
- data/lib/phantomas/modules/localStorage/localStorage.js +14 -0
- data/lib/phantomas/modules/requestsStats/requestsStats.js +71 -0
- data/lib/phantomas/modules/staticAssets/staticAssets.js +40 -0
- data/lib/phantomas/modules/waterfall/waterfall.js +62 -0
- data/lib/phantomas/modules/windowPerformance/windowPerformance.js +36 -0
- data/lib/phantomas/package.json +27 -0
- data/lib/phantomas/phantomas.js +35 -0
- data/lib/phantomas/run-multiple.js +177 -0
- data/lib/yslow.js +5 -0
- metadata +135 -0
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            /**
         | 
| 2 | 
            +
             * Results formatter
         | 
| 3 | 
            +
             */
         | 
| 4 | 
            +
            var formatter = function(results, format) {
         | 
| 5 | 
            +
            	function render() {
         | 
| 6 | 
            +
            		switch(format) {
         | 
| 7 | 
            +
            			case 'json':
         | 
| 8 | 
            +
            				return formatJson();
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            			case 'csv':
         | 
| 11 | 
            +
            				return formatCsv();
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            			case 'plain':
         | 
| 14 | 
            +
            			default:
         | 
| 15 | 
            +
            				return formatPlain();
         | 
| 16 | 
            +
            		}
         | 
| 17 | 
            +
            	}
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            	function formatJson() {
         | 
| 20 | 
            +
            		return JSON.stringify(results);
         | 
| 21 | 
            +
            	}
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            	function formatCsv() {
         | 
| 24 | 
            +
            		var obj = results.metrics,
         | 
| 25 | 
            +
            			key,
         | 
| 26 | 
            +
            			keys = [],
         | 
| 27 | 
            +
            			values = [];
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            		for (key in obj) {
         | 
| 30 | 
            +
            			keys.push(key);
         | 
| 31 | 
            +
            			values.push(obj[key]);
         | 
| 32 | 
            +
            		}
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            		return keys.join(',') + "\n" + values.join(',');
         | 
| 35 | 
            +
            	}
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            	function formatPlain() {
         | 
| 38 | 
            +
            		var res = '',
         | 
| 39 | 
            +
            			obj = results.metrics,
         | 
| 40 | 
            +
            			key;
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            		// header
         | 
| 43 | 
            +
            		res += 'phantomas metrics for <' + results.url + '>:\n\n';
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            		// metrics
         | 
| 46 | 
            +
            		for (key in obj) {
         | 
| 47 | 
            +
            			res += '* ' + key + ': ' + obj[key]+ '\n';
         | 
| 48 | 
            +
            		}
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            		res += '\n';
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            		// notices
         | 
| 53 | 
            +
            		results.notices.forEach(function(msg) {
         | 
| 54 | 
            +
            			res += '> ' + msg + "\n";
         | 
| 55 | 
            +
            		});
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            		return res.trim();
         | 
| 58 | 
            +
            	}
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            	// public interface
         | 
| 61 | 
            +
            	this.render = render;
         | 
| 62 | 
            +
            };
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            exports.formatter = formatter;
         | 
| 65 | 
            +
             | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            /**
         | 
| 2 | 
            +
             * phantomas helper code
         | 
| 3 | 
            +
             *
         | 
| 4 | 
            +
             * Executed in page window
         | 
| 5 | 
            +
             */
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            (function(window) {
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            	// NodeRunner
         | 
| 10 | 
            +
            	var nodeRunner = function() {
         | 
| 11 | 
            +
            		// "Beep, Beep"
         | 
| 12 | 
            +
            	};
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            	nodeRunner.prototype = {
         | 
| 15 | 
            +
            		// call callback for each child of node
         | 
| 16 | 
            +
            		walk: function(node, callback, depth) {
         | 
| 17 | 
            +
            			if (this.isSkipped(node)) {
         | 
| 18 | 
            +
            				return;
         | 
| 19 | 
            +
            			}
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            			var childNode,
         | 
| 22 | 
            +
            				childNodes = node.childNodes || [];
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            			depth = (depth || 1);
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            			for (var n=0, len = childNodes.length; n < len; n++) {
         | 
| 27 | 
            +
            				childNode = childNodes[n];
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            				// callback can return false to stop recursive
         | 
| 30 | 
            +
            				if (callback(childNode, depth) !== false) {
         | 
| 31 | 
            +
            					this.walk(childNode, callback, depth + 1);
         | 
| 32 | 
            +
            				}
         | 
| 33 | 
            +
            			}
         | 
| 34 | 
            +
            		},
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            		// override this function when you create an object of class phantomas.nodeRunner
         | 
| 37 | 
            +
            		// by default only iterate over HTML elements
         | 
| 38 | 
            +
            		isSkipped: function(node) {
         | 
| 39 | 
            +
            			return !node || (node.nodeType !== Node.ELEMENT_NODE);
         | 
| 40 | 
            +
            		}
         | 
| 41 | 
            +
            	};
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            	function getCaller() {
         | 
| 44 | 
            +
            		var caller = {};
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            		try {
         | 
| 47 | 
            +
            			throw new Error('backtrace');
         | 
| 48 | 
            +
            		} catch(e) {
         | 
| 49 | 
            +
            			caller = (e.stackArray && e.stackArray[3]) || {};
         | 
| 50 | 
            +
            		}
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            		return caller;
         | 
| 53 | 
            +
            	}
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            	// create a scope
         | 
| 56 | 
            +
            	var phantomas = (window.phantomas = window.phantomas || {});
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            	// exports
         | 
| 59 | 
            +
            	phantomas.nodeRunner = nodeRunner;
         | 
| 60 | 
            +
            	phantomas.getCaller = getCaller;
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            	console.log('phantomas scope injected');
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            })(window);
         | 
| @@ -0,0 +1,214 @@ | |
| 1 | 
            +
            /**
         | 
| 2 | 
            +
             * Simple HTTP requests monitor and analyzer
         | 
| 3 | 
            +
             */
         | 
| 4 | 
            +
            exports.version = '1.1';
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            exports.module = function(phantomas) {
         | 
| 7 | 
            +
            	// imports
         | 
| 8 | 
            +
            	var HTTP_STATUS_CODES = phantomas.require('http').STATUS_CODES,
         | 
| 9 | 
            +
            		parseUrl = phantomas.require('url').parse;
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            	var requests = [];
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            	// register metric
         | 
| 14 | 
            +
            	phantomas.setMetric('requests');
         | 
| 15 | 
            +
            	phantomas.setMetric('gzipRequests');
         | 
| 16 | 
            +
            	phantomas.setMetric('postRequests');
         | 
| 17 | 
            +
            	phantomas.setMetric('redirects');
         | 
| 18 | 
            +
            	phantomas.setMetric('notFound');
         | 
| 19 | 
            +
            	phantomas.setMetric('timeToFirstByte');
         | 
| 20 | 
            +
            	phantomas.setMetric('timeToLastByte');
         | 
| 21 | 
            +
            	phantomas.setMetric('bodySize'); // content only
         | 
| 22 | 
            +
            	phantomas.setMetric('contentLength'); // content only
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            	// parse given URL to get protocol and domain
         | 
| 25 | 
            +
            	function parseEntryUrl(entry) {
         | 
| 26 | 
            +
            		var parsed;
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            		if (entry.url.indexOf('data:') !== 0) {
         | 
| 29 | 
            +
            			// @see http://nodejs.org/api/url.html#url_url
         | 
| 30 | 
            +
            			parsed = parseUrl(entry.url) || {};
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            			entry.protocol = parsed.protocol.replace(':', '');
         | 
| 33 | 
            +
            			entry.domain = parsed.hostname;
         | 
| 34 | 
            +
            			entry.query = parsed.query;
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            			if (entry.protocol === 'https') {
         | 
| 37 | 
            +
            				entry.isSSL = true;
         | 
| 38 | 
            +
            			}
         | 
| 39 | 
            +
            		}
         | 
| 40 | 
            +
            		else {
         | 
| 41 | 
            +
            			// base64 encoded data
         | 
| 42 | 
            +
            			entry.domain = false;
         | 
| 43 | 
            +
            			entry.protocol = false;
         | 
| 44 | 
            +
            			entry.isBase64 = true;
         | 
| 45 | 
            +
            		}
         | 
| 46 | 
            +
            	}
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            	// when the monitoring started?
         | 
| 49 | 
            +
            	var start;
         | 
| 50 | 
            +
            	phantomas.on('pageOpen', function(res) {
         | 
| 51 | 
            +
            		start = Date.now();
         | 
| 52 | 
            +
            	});
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            	phantomas.on('onResourceRequested', function(res) {
         | 
| 55 | 
            +
            		// store current request data
         | 
| 56 | 
            +
            		var entry = requests[res.id] = {
         | 
| 57 | 
            +
            			id: res.id,
         | 
| 58 | 
            +
            			url: res.url,
         | 
| 59 | 
            +
            			method: res.method,
         | 
| 60 | 
            +
            			requestHeaders: {},
         | 
| 61 | 
            +
            			sendTime: res.time,
         | 
| 62 | 
            +
            			bodySize: 0
         | 
| 63 | 
            +
            		};
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            		res.headers.forEach(function(header) {
         | 
| 66 | 
            +
            			entry.requestHeaders[header.name] = header.value;
         | 
| 67 | 
            +
            		});
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            		parseEntryUrl(entry);
         | 
| 70 | 
            +
             | 
| 71 | 
            +
            		if (!entry.isBase64) {
         | 
| 72 | 
            +
            			phantomas.emit('send', entry, res);
         | 
| 73 | 
            +
            		}
         | 
| 74 | 
            +
            	});
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            	phantomas.on('onResourceReceived', function(res) {
         | 
| 77 | 
            +
            		// current request data
         | 
| 78 | 
            +
            		var entry = requests[res.id];
         | 
| 79 | 
            +
             | 
| 80 | 
            +
            		switch(res.stage) {
         | 
| 81 | 
            +
            			// the beginning of response
         | 
| 82 | 
            +
            			case 'start':
         | 
| 83 | 
            +
            				entry.recvStartTime = res.time;
         | 
| 84 | 
            +
            				entry.timeToFirstByte = res.time - entry.sendTime;
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            				// FIXME: buggy
         | 
| 87 | 
            +
            				// @see http://code.google.com/p/phantomjs/issues/detail?id=169
         | 
| 88 | 
            +
            				entry.bodySize += res.bodySize || 0;
         | 
| 89 | 
            +
            				break;
         | 
| 90 | 
            +
             | 
| 91 | 
            +
            			// the end of response
         | 
| 92 | 
            +
            			case 'end':
         | 
| 93 | 
            +
            				// timing
         | 
| 94 | 
            +
            				entry.recvEndTime = res.time;
         | 
| 95 | 
            +
            				entry.timeToLastByte = res.time - entry.sendTime;
         | 
| 96 | 
            +
            				entry.receiveTime = entry.recvEndTime - entry.recvStartTime;
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            				// request method
         | 
| 99 | 
            +
            				switch(entry.method) {
         | 
| 100 | 
            +
            					case 'POST':
         | 
| 101 | 
            +
            						phantomas.incrMetric('postRequests');
         | 
| 102 | 
            +
            						break;
         | 
| 103 | 
            +
            				}
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            				// HTTP code
         | 
| 106 | 
            +
            				entry.status = res.status || 200 /* for base64 data */;
         | 
| 107 | 
            +
            				entry.statusText = HTTP_STATUS_CODES[entry.status];
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            				switch(entry.status) {
         | 
| 110 | 
            +
            					case 301:
         | 
| 111 | 
            +
            					case 302:
         | 
| 112 | 
            +
            						phantomas.incrMetric('redirects');
         | 
| 113 | 
            +
            						phantomas.addNotice(entry.url + ' is a redirect (HTTP ' + entry.status + ')');
         | 
| 114 | 
            +
            						break;
         | 
| 115 | 
            +
             | 
| 116 | 
            +
            					case 404:
         | 
| 117 | 
            +
            						phantomas.incrMetric('notFound');
         | 
| 118 | 
            +
            						phantomas.addNotice(entry.url + ' was not found (HTTP 404)');
         | 
| 119 | 
            +
            						break;
         | 
| 120 | 
            +
            				}
         | 
| 121 | 
            +
             | 
| 122 | 
            +
            				parseEntryUrl(entry);
         | 
| 123 | 
            +
             | 
| 124 | 
            +
            				// asset type
         | 
| 125 | 
            +
            				entry.type = 'other';
         | 
| 126 | 
            +
             | 
| 127 | 
            +
            				// analyze HTTP headers
         | 
| 128 | 
            +
            				entry.headers = {};
         | 
| 129 | 
            +
            				res.headers.forEach(function(header) {
         | 
| 130 | 
            +
            					entry.headers[header.name] = header.value;
         | 
| 131 | 
            +
             | 
| 132 | 
            +
            					switch (header.name.toLowerCase()) {
         | 
| 133 | 
            +
            						// TODO: why it's not gzipped?
         | 
| 134 | 
            +
            						// because: http://code.google.com/p/phantomjs/issues/detail?id=156
         | 
| 135 | 
            +
            						// should equal bodySize
         | 
| 136 | 
            +
            						case 'content-length':
         | 
| 137 | 
            +
            							entry.contentLength = parseInt(header.value, 10);
         | 
| 138 | 
            +
            							break;
         | 
| 139 | 
            +
             | 
| 140 | 
            +
            						// detect content type
         | 
| 141 | 
            +
            						case 'content-type':
         | 
| 142 | 
            +
            							// parse header value
         | 
| 143 | 
            +
            							var value = header.value.split(';').shift().toLowerCase();
         | 
| 144 | 
            +
             | 
| 145 | 
            +
            							switch(value) {
         | 
| 146 | 
            +
            								case 'text/html':
         | 
| 147 | 
            +
            									entry.type = 'html';
         | 
| 148 | 
            +
            									entry.isHTML = true;
         | 
| 149 | 
            +
            									break;
         | 
| 150 | 
            +
             | 
| 151 | 
            +
            								case 'text/css':
         | 
| 152 | 
            +
            									entry.type = 'css';
         | 
| 153 | 
            +
            									entry.isCSS = true;
         | 
| 154 | 
            +
            									break;
         | 
| 155 | 
            +
             | 
| 156 | 
            +
            								case 'application/x-javascript':
         | 
| 157 | 
            +
            								case 'application/javascript':
         | 
| 158 | 
            +
            								case 'text/javascript':
         | 
| 159 | 
            +
            									entry.type = 'js';
         | 
| 160 | 
            +
            									entry.isJS = true;
         | 
| 161 | 
            +
            									break;
         | 
| 162 | 
            +
             | 
| 163 | 
            +
            								case 'image/png':
         | 
| 164 | 
            +
            								case 'image/jpeg':
         | 
| 165 | 
            +
            								case 'image/gif':
         | 
| 166 | 
            +
            									entry.type = 'image';
         | 
| 167 | 
            +
            									entry.isImage = true;
         | 
| 168 | 
            +
            									break;
         | 
| 169 | 
            +
             | 
| 170 | 
            +
            								default:
         | 
| 171 | 
            +
            									phantomas.addNotice('Unknown content type found: ' + value);
         | 
| 172 | 
            +
            							}
         | 
| 173 | 
            +
            							break;
         | 
| 174 | 
            +
             | 
| 175 | 
            +
            						// detect content encoding
         | 
| 176 | 
            +
            						case 'content-encoding':
         | 
| 177 | 
            +
            							if (header.value === 'gzip') {
         | 
| 178 | 
            +
            								entry.gzip = true;
         | 
| 179 | 
            +
            							}
         | 
| 180 | 
            +
            							break;
         | 
| 181 | 
            +
            					}
         | 
| 182 | 
            +
            				});
         | 
| 183 | 
            +
             | 
| 184 | 
            +
            				// requests stats
         | 
| 185 | 
            +
            				if (!entry.isBase64) {
         | 
| 186 | 
            +
            					phantomas.incrMetric('requests');
         | 
| 187 | 
            +
             | 
| 188 | 
            +
            					phantomas.incrMetric('bodySize', entry.bodySize); // content only
         | 
| 189 | 
            +
            					phantomas.incrMetric('contentLength', entry.contentLength || entry.bodySize); // content only
         | 
| 190 | 
            +
            				}
         | 
| 191 | 
            +
             | 
| 192 | 
            +
            				if (entry.gzip) {
         | 
| 193 | 
            +
            					phantomas.incrMetric('gzipRequests');
         | 
| 194 | 
            +
            				}
         | 
| 195 | 
            +
             | 
| 196 | 
            +
            				// emit an event for other modules
         | 
| 197 | 
            +
            				phantomas.emit(entry.isBase64 ? 'base64recv' : 'recv' , entry, res);
         | 
| 198 | 
            +
            				//phantomas.log(entry);
         | 
| 199 | 
            +
            				break;
         | 
| 200 | 
            +
            		}
         | 
| 201 | 
            +
            	});
         | 
| 202 | 
            +
             | 
| 203 | 
            +
            	// TTFB / TTLB metrics
         | 
| 204 | 
            +
            	phantomas.on('recv', function(entry, res) {
         | 
| 205 | 
            +
            		// check the first request
         | 
| 206 | 
            +
            		if (entry.id === 1) {
         | 
| 207 | 
            +
            			phantomas.setMetric('timeToFirstByte', entry.timeToFirstByte);
         | 
| 208 | 
            +
            			phantomas.setMetric('timeToLastByte', entry.timeToLastByte);
         | 
| 209 | 
            +
            		}
         | 
| 210 | 
            +
             | 
| 211 | 
            +
            		// completion of the last HTTP request
         | 
| 212 | 
            +
            		phantomas.setMetric('httpTrafficCompleted', entry.recvEndTime - start);
         | 
| 213 | 
            +
            	});
         | 
| 214 | 
            +
            };
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            /**
         | 
| 2 | 
            +
             * Helper functions for string formatting
         | 
| 3 | 
            +
             */
         | 
| 4 | 
            +
            function lpad(str, len) {
         | 
| 5 | 
            +
            	var fill = new Array( Math.max(1, len - str.toString().length + 1) ).join(' ');
         | 
| 6 | 
            +
            	return fill + str;
         | 
| 7 | 
            +
            }
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            function rpad(str, len) {
         | 
| 10 | 
            +
            	var fill = new Array( Math.max(1, len - str.toString().length + 1) ).join(' ');
         | 
| 11 | 
            +
            	return str + fill;
         | 
| 12 | 
            +
            }
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            exports.lpad = lpad;
         | 
| 15 | 
            +
            exports.rpad = rpad;
         | 
| 16 | 
            +
             | 
| @@ -0,0 +1,418 @@ | |
| 1 | 
            +
            /**
         | 
| 2 | 
            +
             * phantomas main file
         | 
| 3 | 
            +
             */
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            var VERSION = '0.4.1';
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            var getDefaultUserAgent = function() {
         | 
| 8 | 
            +
            	var version = phantom.version,
         | 
| 9 | 
            +
            		system = require('system'),
         | 
| 10 | 
            +
            		os = system.os;
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            	return "phantomas/" + VERSION + " (PhantomJS/" + version.major + "." + version.minor + "." + version.patch + "; " + os.name + " " + os.architecture + ")";
         | 
| 13 | 
            +
            }
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            var phantomas = function(params) {
         | 
| 16 | 
            +
            	// parse script CLI parameters
         | 
| 17 | 
            +
            	this.params = params;
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            	// --url=http://example.com
         | 
| 20 | 
            +
            	this.url = this.params.url;
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            	// --format=[csv|json]
         | 
| 23 | 
            +
            	this.resultsFormat = params.format || 'plain';
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            	// --viewport=1280x1024
         | 
| 26 | 
            +
            	this.viewport = params.viewport || '1280x1024';
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            	// --verbose
         | 
| 29 | 
            +
            	this.verboseMode = params.verbose === true;
         | 
| 30 | 
            +
            	
         | 
| 31 | 
            +
            	// --silent
         | 
| 32 | 
            +
            	this.silentMode = params.silent === true;
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            	// --timeout (in seconds)
         | 
| 35 | 
            +
            	this.timeout = (params.timeout > 0 && parseInt(params.timeout, 10)) || 15;
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            	// --modules=localStorage,cookies
         | 
| 38 | 
            +
            	this.modules = (params.modules) ? params.modules.split(',') : [];
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            	// --user-agent=custom-agent
         | 
| 41 | 
            +
            	this.userAgent = params['user-agent'] || getDefaultUserAgent();
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            	// setup the stuff
         | 
| 44 | 
            +
            	this.emitter = new (this.require('events').EventEmitter)();
         | 
| 45 | 
            +
            	this.emitter.setMaxListeners(200);
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            	this.page = require('webpage').create();
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            	// current HTTP requests counter
         | 
| 50 | 
            +
            	this.currentRequests = 0;
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            	this.log('phantomas v' + VERSION);
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            	// load core modules
         | 
| 55 | 
            +
            	this.addCoreModule('requestsMonitor');
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            	// load 3rd party modules
         | 
| 58 | 
            +
            	var modules = (this.modules.length > 0) ? this.modules : this.listModules(),
         | 
| 59 | 
            +
            		self = this;
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            	modules.forEach(function(moduleName) {
         | 
| 62 | 
            +
            		self.addModule(moduleName);
         | 
| 63 | 
            +
            	});
         | 
| 64 | 
            +
            };
         | 
| 65 | 
            +
             | 
| 66 | 
            +
            phantomas.version = VERSION;
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            phantomas.prototype = {
         | 
| 69 | 
            +
            	metrics: {},
         | 
| 70 | 
            +
            	notices: [],
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            	// simple version of jQuery.proxy
         | 
| 73 | 
            +
            	proxy: function(fn, scope) {
         | 
| 74 | 
            +
            		scope = scope || this;
         | 
| 75 | 
            +
            		return function () {
         | 
| 76 | 
            +
            			return fn.apply(scope, arguments);
         | 
| 77 | 
            +
            		};
         | 
| 78 | 
            +
            	},
         | 
| 79 | 
            +
             | 
| 80 | 
            +
            	// emit given event
         | 
| 81 | 
            +
            	emit: function(/* eventName, arg1, arg2, ... */) {
         | 
| 82 | 
            +
            		this.log('Event ' + arguments[0] + ' emitted');
         | 
| 83 | 
            +
            		this.emitter.emit.apply(this.emitter, arguments);
         | 
| 84 | 
            +
            	},
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            	// bind to a given event
         | 
| 87 | 
            +
            	on: function(ev, fn) {
         | 
| 88 | 
            +
            		this.emitter.on(ev, fn);
         | 
| 89 | 
            +
            	},
         | 
| 90 | 
            +
             | 
| 91 | 
            +
            	once: function(ev, fn) {
         | 
| 92 | 
            +
            		this.emitter.once(ev, fn);
         | 
| 93 | 
            +
            	},
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            	// returns "wrapped" version of phantomas object with public methods / fields only
         | 
| 96 | 
            +
            	getPublicWrapper: function() {
         | 
| 97 | 
            +
            		var self = this;
         | 
| 98 | 
            +
             | 
| 99 | 
            +
            		// modules API
         | 
| 100 | 
            +
            		return {
         | 
| 101 | 
            +
            			url: this.params.url,
         | 
| 102 | 
            +
            			params: this.params,
         | 
| 103 | 
            +
             | 
| 104 | 
            +
            			// events
         | 
| 105 | 
            +
            			on: function() {self.on.apply(self, arguments);},
         | 
| 106 | 
            +
            			once: function() {self.once.apply(self, arguments);},
         | 
| 107 | 
            +
            			emit: function() {self.emit.apply(self, arguments);},
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            			// metrics
         | 
| 110 | 
            +
            			setMetric: function() {self.setMetric.apply(self, arguments);},
         | 
| 111 | 
            +
            			setMetricEvaluate: function() {self.setMetricEvaluate.apply(self, arguments);},
         | 
| 112 | 
            +
            			incrMetric: function() {self.incrMetric.apply(self, arguments);},
         | 
| 113 | 
            +
             | 
| 114 | 
            +
            			// debug
         | 
| 115 | 
            +
            			addNotice: function(msg) {self.addNotice(msg);},
         | 
| 116 | 
            +
            			log: function(msg) {self.log(msg);},
         | 
| 117 | 
            +
            			echo: function(msg) {self.echo(msg);},
         | 
| 118 | 
            +
             | 
| 119 | 
            +
            			// phantomJS
         | 
| 120 | 
            +
            			evaluate: function(fn) {return self.page.evaluate(fn);},
         | 
| 121 | 
            +
            			injectJs: function(src) {return self.page.injectJs(src);},
         | 
| 122 | 
            +
            			require: function(module) {return self.require(module);},
         | 
| 123 | 
            +
            			getPageContent: function() {return self.page.content;}
         | 
| 124 | 
            +
            		};
         | 
| 125 | 
            +
            	},
         | 
| 126 | 
            +
             | 
| 127 | 
            +
            	// initialize given core phantomas module
         | 
| 128 | 
            +
            	addCoreModule: function(name) {
         | 
| 129 | 
            +
            		var pkg = require('./modules/' + name + '/' + name);
         | 
| 130 | 
            +
             | 
| 131 | 
            +
            		// init a module
         | 
| 132 | 
            +
            		pkg.module(this.getPublicWrapper());
         | 
| 133 | 
            +
             | 
| 134 | 
            +
            		this.log('Core module ' + name + (pkg.version ? ' v' + pkg.version : '') + ' initialized');
         | 
| 135 | 
            +
            	},
         | 
| 136 | 
            +
             | 
| 137 | 
            +
            	// initialize given phantomas module
         | 
| 138 | 
            +
            	addModule: function(name) {
         | 
| 139 | 
            +
            		var pkg;
         | 
| 140 | 
            +
            		try {
         | 
| 141 | 
            +
            			pkg = require('./../modules/' + name + '/' + name);
         | 
| 142 | 
            +
            		}
         | 
| 143 | 
            +
            		catch (e) {
         | 
| 144 | 
            +
            			this.log('Unable to load module "' + name + '"!');
         | 
| 145 | 
            +
            			return false;
         | 
| 146 | 
            +
            		}
         | 
| 147 | 
            +
             | 
| 148 | 
            +
            		if (pkg.skip) {
         | 
| 149 | 
            +
            			this.log('Module ' + name + ' skipped!');
         | 
| 150 | 
            +
            			return false;
         | 
| 151 | 
            +
            		}
         | 
| 152 | 
            +
             | 
| 153 | 
            +
            		// init a module
         | 
| 154 | 
            +
            		pkg.module(this.getPublicWrapper());
         | 
| 155 | 
            +
             | 
| 156 | 
            +
            		this.log('Module ' + name + (pkg.version ? ' v' + pkg.version : '') + ' initialized');
         | 
| 157 | 
            +
            		return true;
         | 
| 158 | 
            +
            	},
         | 
| 159 | 
            +
             | 
| 160 | 
            +
            	// returns list of 3rd party modules located in modules directory
         | 
| 161 | 
            +
            	listModules: function() {
         | 
| 162 | 
            +
            		this.log('Getting the list of all modules...');
         | 
| 163 | 
            +
             | 
| 164 | 
            +
            		var fs = require('fs'),
         | 
| 165 | 
            +
            			modulesDir = fs.workingDirectory + '/modules',
         | 
| 166 | 
            +
            			ls = fs.list(modulesDir) || [],
         | 
| 167 | 
            +
            			modules = [];
         | 
| 168 | 
            +
             | 
| 169 | 
            +
            		ls.forEach(function(entry) {
         | 
| 170 | 
            +
            			if (fs.isFile(modulesDir + '/' + entry + '/' + entry + '.js')) {
         | 
| 171 | 
            +
            				modules.push(entry);
         | 
| 172 | 
            +
            			}
         | 
| 173 | 
            +
            		});
         | 
| 174 | 
            +
             | 
| 175 | 
            +
            		return modules;
         | 
| 176 | 
            +
            	},
         | 
| 177 | 
            +
             
         | 
| 178 | 
            +
            	// runs phantomas
         | 
| 179 | 
            +
            	run: function(callback) {
         | 
| 180 | 
            +
             | 
| 181 | 
            +
            		// check required params
         | 
| 182 | 
            +
            		if (!this.url) {
         | 
| 183 | 
            +
            			throw '--url argument must be provided!';
         | 
| 184 | 
            +
            		}
         | 
| 185 | 
            +
             | 
| 186 | 
            +
            		// to be called by tearDown
         | 
| 187 | 
            +
            		this.onDoneCallback = callback;
         | 
| 188 | 
            +
             | 
| 189 | 
            +
            		this.start = Date.now();
         | 
| 190 | 
            +
             | 
| 191 | 
            +
            		// setup viewport
         | 
| 192 | 
            +
            		var parsedViewport = this.viewport.split('x');
         | 
| 193 | 
            +
             | 
| 194 | 
            +
            		if (parsedViewport.length === 2) {
         | 
| 195 | 
            +
            			this.page.viewportSize = {
         | 
| 196 | 
            +
            				height: parseInt(parsedViewport[0], 10) || 1280,
         | 
| 197 | 
            +
            				width: parseInt(parsedViewport[1], 10) || 1024
         | 
| 198 | 
            +
            			};
         | 
| 199 | 
            +
            		}
         | 
| 200 | 
            +
             | 
| 201 | 
            +
            		// setup user agent
         | 
| 202 | 
            +
            		if (this.userAgent) {
         | 
| 203 | 
            +
            			this.page.settings.userAgent = this.userAgent;
         | 
| 204 | 
            +
            		}
         | 
| 205 | 
            +
             | 
| 206 | 
            +
            		// print out debug messages
         | 
| 207 | 
            +
            		this.log('Opening <' + this.url + '>...');
         | 
| 208 | 
            +
            		this.log('Using ' + this.page.settings.userAgent + ' as user agent');
         | 
| 209 | 
            +
            		this.log('Viewport set to ' + this.page.viewportSize.height + 'x' + this.page.viewportSize.width);
         | 
| 210 | 
            +
             | 
| 211 | 
            +
            		// bind basic events
         | 
| 212 | 
            +
            		this.page.onInitialized = this.proxy(this.onInitialized);
         | 
| 213 | 
            +
            		this.page.onLoadStarted = this.proxy(this.onLoadStarted);
         | 
| 214 | 
            +
            		this.page.onLoadFinished = this.proxy(this.onLoadFinished);
         | 
| 215 | 
            +
            		this.page.onResourceRequested = this.proxy(this.onResourceRequested);
         | 
| 216 | 
            +
            		this.page.onResourceReceived = this.proxy(this.onResourceReceived);
         | 
| 217 | 
            +
             | 
| 218 | 
            +
            		// debug
         | 
| 219 | 
            +
            		this.page.onAlert = this.proxy(this.onAlert);
         | 
| 220 | 
            +
            		this.page.onConsoleMessage = this.proxy(this.onConsoleMessage);
         | 
| 221 | 
            +
             | 
| 222 | 
            +
            		// observe HTTP requests
         | 
| 223 | 
            +
            		// finish when the last request is completed
         | 
| 224 | 
            +
            		
         | 
| 225 | 
            +
            		// update HTTP requests counter
         | 
| 226 | 
            +
            		this.on('send', this.proxy(function(entry) {
         | 
| 227 | 
            +
            			this.currentRequests++;
         | 
| 228 | 
            +
            		}));
         | 
| 229 | 
            +
            	
         | 
| 230 | 
            +
            		this.on('recv', this.proxy(function(entry) {
         | 
| 231 | 
            +
            			this.currentRequests--;
         | 
| 232 | 
            +
             | 
| 233 | 
            +
            			this.enqueueReport();
         | 
| 234 | 
            +
            		}));
         | 
| 235 | 
            +
             | 
| 236 | 
            +
            		// last time changes?
         | 
| 237 | 
            +
            		this.emit('pageBeforeOpen', this.page);
         | 
| 238 | 
            +
             | 
| 239 | 
            +
            		// open the page
         | 
| 240 | 
            +
            		this.page.open(this.url);
         | 
| 241 | 
            +
             | 
| 242 | 
            +
            		this.emit('pageOpen');
         | 
| 243 | 
            +
             | 
| 244 | 
            +
            		// fallback - always timeout after TIMEOUT seconds
         | 
| 245 | 
            +
            		this.log('Run timeout set to ' + this.timeout + ' s');
         | 
| 246 | 
            +
            		setTimeout(this.proxy(function() {
         | 
| 247 | 
            +
            			this.log('Timeout of ' + this.timeout + ' s was reached!');
         | 
| 248 | 
            +
            			this.report();
         | 
| 249 | 
            +
            		}), this.timeout * 1000);
         | 
| 250 | 
            +
            	},
         | 
| 251 | 
            +
             | 
| 252 | 
            +
            	/**
         | 
| 253 | 
            +
            	 * Wait a second before finishing the monitoring (i.e. report generation)
         | 
| 254 | 
            +
            	 *
         | 
| 255 | 
            +
            	 * This one is called when response is received. Previously scheduled reporting is removed and the new is created.
         | 
| 256 | 
            +
            	 */
         | 
| 257 | 
            +
            	enqueueReport: function() {
         | 
| 258 | 
            +
            		clearTimeout(this.lastRequestTimeout);
         | 
| 259 | 
            +
             | 
| 260 | 
            +
            		if (this.currentRequests < 1) {
         | 
| 261 | 
            +
            			this.lastRequestTimeout = setTimeout(this.proxy(this.report), 1000);
         | 
| 262 | 
            +
            		}
         | 
| 263 | 
            +
            	},
         | 
| 264 | 
            +
             | 
| 265 | 
            +
            	// called when all HTTP requests are completed
         | 
| 266 | 
            +
            	report: function() {
         | 
| 267 | 
            +
            		this.emit('report');
         | 
| 268 | 
            +
             | 
| 269 | 
            +
            		var time = Date.now() - this.start;
         | 
| 270 | 
            +
            		this.log('phantomas work done in ' + time + ' ms');
         | 
| 271 | 
            +
             | 
| 272 | 
            +
            		// format results
         | 
| 273 | 
            +
            		var results = {
         | 
| 274 | 
            +
            			url: this.url,
         | 
| 275 | 
            +
            			metrics: this.metrics,
         | 
| 276 | 
            +
            			notices: this.notices
         | 
| 277 | 
            +
            		};
         | 
| 278 | 
            +
             | 
| 279 | 
            +
            		this.emit('results', results);
         | 
| 280 | 
            +
             | 
| 281 | 
            +
            		// count all metrics
         | 
| 282 | 
            +
            		var metricsCount = 0,
         | 
| 283 | 
            +
            			i;
         | 
| 284 | 
            +
             | 
| 285 | 
            +
            		for (i in this.metrics) {
         | 
| 286 | 
            +
            			metricsCount++;
         | 
| 287 | 
            +
            		}
         | 
| 288 | 
            +
             | 
| 289 | 
            +
            		this.log('Formatting results (' + this.resultsFormat + ') with ' + metricsCount+ ' metric(s)...');
         | 
| 290 | 
            +
             | 
| 291 | 
            +
            		// render results
         | 
| 292 | 
            +
            		var formatter = require('./formatter').formatter,
         | 
| 293 | 
            +
            			renderer = new formatter(results, this.resultsFormat);
         | 
| 294 | 
            +
             | 
| 295 | 
            +
            		this.echo(renderer.render());
         | 
| 296 | 
            +
            		this.tearDown(0);
         | 
| 297 | 
            +
            	},
         | 
| 298 | 
            +
             | 
| 299 | 
            +
            	tearDown: function(exitCode) {
         | 
| 300 | 
            +
            		exitCode = exitCode || 0;
         | 
| 301 | 
            +
             | 
| 302 | 
            +
            		if (exitCode > 0) {
         | 
| 303 | 
            +
            			this.log('Exiting with code #' + exitCode);
         | 
| 304 | 
            +
            		}
         | 
| 305 | 
            +
             | 
| 306 | 
            +
            		this.page.release();
         | 
| 307 | 
            +
             | 
| 308 | 
            +
            		// call function provided to run() method
         | 
| 309 | 
            +
            		if (typeof this.onDoneCallback === 'function') {
         | 
| 310 | 
            +
            			this.onDoneCallback();
         | 
| 311 | 
            +
            		}
         | 
| 312 | 
            +
            		else {
         | 
| 313 | 
            +
            			phantom.exit(exitCode);
         | 
| 314 | 
            +
            		}
         | 
| 315 | 
            +
            	},
         | 
| 316 | 
            +
             | 
| 317 | 
            +
            	// core events
         | 
| 318 | 
            +
            	onInitialized: function() {
         | 
| 319 | 
            +
            		// add helper tools into window.phantomas "namespace"
         | 
| 320 | 
            +
            		this.page.injectJs('./core/helper.js');
         | 
| 321 | 
            +
             | 
| 322 | 
            +
            		this.log('Page object initialized');
         | 
| 323 | 
            +
            		this.emit('init');
         | 
| 324 | 
            +
            	},
         | 
| 325 | 
            +
             | 
| 326 | 
            +
            	onLoadStarted: function() {
         | 
| 327 | 
            +
            		this.log('Page loading started');
         | 
| 328 | 
            +
            		this.emit('loadStarted');
         | 
| 329 | 
            +
            	},
         | 
| 330 | 
            +
             | 
| 331 | 
            +
            	onResourceRequested: function(res) {
         | 
| 332 | 
            +
            		this.emit('onResourceRequested', res);
         | 
| 333 | 
            +
            		//this.log(JSON.stringify(res));
         | 
| 334 | 
            +
            	},
         | 
| 335 | 
            +
             | 
| 336 | 
            +
            	onResourceReceived: function(res) {
         | 
| 337 | 
            +
            		this.emit('onResourceReceived', res);
         | 
| 338 | 
            +
            		//this.log(JSON.stringify(res));
         | 
| 339 | 
            +
            	},
         | 
| 340 | 
            +
             | 
| 341 | 
            +
            	onLoadFinished: function(status) {
         | 
| 342 | 
            +
            		// trigger this only once
         | 
| 343 | 
            +
            		if (this.onLoadFinishedEmitted) {
         | 
| 344 | 
            +
            			return;
         | 
| 345 | 
            +
            		}
         | 
| 346 | 
            +
            		this.onLoadFinishedEmitted = true;
         | 
| 347 | 
            +
             | 
| 348 | 
            +
            		// we're done
         | 
| 349 | 
            +
            		this.log('Page loading finished ("' + status + '")');
         | 
| 350 | 
            +
             | 
| 351 | 
            +
            		switch(status) {
         | 
| 352 | 
            +
            			case 'success':
         | 
| 353 | 
            +
            				this.emit('loadFinished', status);
         | 
| 354 | 
            +
            				this.enqueueReport();
         | 
| 355 | 
            +
            				break;
         | 
| 356 | 
            +
             | 
| 357 | 
            +
            			default:
         | 
| 358 | 
            +
            				this.emit('loadFailed', status);
         | 
| 359 | 
            +
            				this.tearDown(2);
         | 
| 360 | 
            +
            				break;
         | 
| 361 | 
            +
            		}
         | 
| 362 | 
            +
            	},
         | 
| 363 | 
            +
             | 
| 364 | 
            +
            	// debug
         | 
| 365 | 
            +
            	onAlert: function(msg) {
         | 
| 366 | 
            +
            		this.log('Alert: ' + msg);
         | 
| 367 | 
            +
            		this.emit('alert', msg);
         | 
| 368 | 
            +
            	},
         | 
| 369 | 
            +
             | 
| 370 | 
            +
            	onConsoleMessage: function(msg) {
         | 
| 371 | 
            +
            		this.log('console.log: ' + msg);
         | 
| 372 | 
            +
            		this.emit('consoleLog', msg);
         | 
| 373 | 
            +
            	},
         | 
| 374 | 
            +
             | 
| 375 | 
            +
            	// metrics reporting
         | 
| 376 | 
            +
            	setMetric: function(name, value) {
         | 
| 377 | 
            +
            		this.metrics[name] = (typeof value !== 'undefined') ? value : 0;
         | 
| 378 | 
            +
            	},
         | 
| 379 | 
            +
             | 
| 380 | 
            +
            	setMetricEvaluate: function(name, fn) {
         | 
| 381 | 
            +
            		this.setMetric(name, this.page.evaluate(fn));
         | 
| 382 | 
            +
            	},
         | 
| 383 | 
            +
             | 
| 384 | 
            +
            	// increements given metric by given number (default is one)
         | 
| 385 | 
            +
            	incrMetric: function(name, incr /* =1 */) {
         | 
| 386 | 
            +
            		this.metrics[name] = (this.metrics[name] || 0) + (incr || 1);
         | 
| 387 | 
            +
            	},
         | 
| 388 | 
            +
             | 
| 389 | 
            +
            	// adds a notice that will be emitted after results
         | 
| 390 | 
            +
            	addNotice: function(msg) {
         | 
| 391 | 
            +
            		this.notices.push(msg || '');
         | 
| 392 | 
            +
            	},
         | 
| 393 | 
            +
             | 
| 394 | 
            +
            	// add log message
         | 
| 395 | 
            +
            	// will be printed out only when --verbose
         | 
| 396 | 
            +
            	log: function(msg) {
         | 
| 397 | 
            +
            		if (this.verboseMode) {
         | 
| 398 | 
            +
            			msg = (typeof msg === 'object') ? JSON.stringify(msg) : msg;
         | 
| 399 | 
            +
             | 
| 400 | 
            +
            			this.echo('> ' + msg);
         | 
| 401 | 
            +
            		}
         | 
| 402 | 
            +
            	},
         | 
| 403 | 
            +
             | 
| 404 | 
            +
            	// console.log wrapper obeying --silent mode
         | 
| 405 | 
            +
            	echo: function(msg) {
         | 
| 406 | 
            +
            		if (!this.silentMode) {
         | 
| 407 | 
            +
            			console.log(msg);
         | 
| 408 | 
            +
            		}
         | 
| 409 | 
            +
            	},
         | 
| 410 | 
            +
             | 
| 411 | 
            +
            	// require CommonJS module from lib/modules
         | 
| 412 | 
            +
            	require: function(module) {
         | 
| 413 | 
            +
            		return require('../lib/modules/' + module);
         | 
| 414 | 
            +
            	}
         | 
| 415 | 
            +
            };
         | 
| 416 | 
            +
             | 
| 417 | 
            +
            exports.phantomas = phantomas;
         | 
| 418 | 
            +
             |