var fs = require('fs');
var WebPage = require('webpage');
var process = {
argv:require('system').args,
argc:require('system').args.length,
exit:function(){
phantom.exit();
}
}
var Getopt = require("./node_modules/node-getopt/lib/getopt.js");
var getopt = new Getopt([
['u' , 'url=ARG' , 'the URL of the site to load test'],
['t' , 'task=ARG' , 'the task to perform'],
['' , 'config[=CONFIG_FILE]'],
['' , 'format[=OUTPUT_FORMAT]'],
['' , 'output[=CONFIG_FILE]'],
['' , 'wait[=WAIT]'],
['' , 'cachewait[=CACHE_WAIT]'],
['' , 'user_agent[=USER_AGENT]'],
['' , 'file_suffix[=FILE_SUFFIX]'],
['w' , 'wipe'],
['h' , 'help'],
['v' , 'verbose']
]).bindHelp();
var opt = getopt.parse(require('system').args);
if( !opt.options.url ){
console.log('Usage: loadreport.js --url=[url] --task=[task]');
phantom.exit();
}
if( opt.options.url ){
if( ! opt.options.task ) opt.options.task = 'performance';
}
if( !opt.options.task.match(/^(performance|performancecache)$/) ){
console.log("task must be one of (performance|performancecache)");
phantom.exit("task must be one of (performance|performancecache)");
}
if( opt.options.format && !opt.options.format.match(/^(json|csv|junit)$/) ){
console.log("format must be one of (json|csv|junit)");
phantom.exit("format must be one of (json|csv|junit)");
}
if( opt.options.config ){
if( ! fs.isFile(opt.options.config) ) throw "config file does not exists\n"+opt.options.config;
}
if( !opt.options.output ){
opt.options.output = '';
}
var loadreport = {
run: function () {
var cliConfig = {
url:opt.options.url,
wait:opt.options.wait,
cacheWait:opt.options.cachewait,
output:opt.options.output,
task:opt.options.task,
format:opt.options.format,
wipe:opt.options.wipe,
verbose:opt.options.verbose,
userAgent:opt.options.user_agent
};
loadreport.performancecache = this.clone(loadreport.performance);
this.config = this.mergeConfig(cliConfig, cliConfig.configFile);
var task = this[this.config.task];
this.load(this.config, task, this);
},
performance: {
resources: [],
count1 : 100,
count2 : 1,
timer : 0,
evalConsole : {},
evalConsoleErrors : [],
onInitialized: function(page, config) {
var pageeval = page.evaluate(function(startTime) {
var now = new Date().getTime();
check the readystate within the page being loaded
Returns “loading” while the document is loading
var _timer3=setInterval(function(){
if(/loading/.test(document.readyState)){
console.log('loading-' + (new Date().getTime() - startTime));
don’t clear the interval until we get last measurement
}
}, 5);
“interactive” once it is finished parsing but still loading sub-resources
var _timer1=setInterval(function(){
if(/interactive/.test(document.readyState)){
console.log('interactive-' + (new Date().getTime() - startTime));
clearInterval(_timer1);
clear loading interval
clearInterval(_timer3);
}
}, 5);
“complete” once it has loaded - same as load event below var _timer2=setInterval(function(){ if(/complete/.test(document.readyState)){ console.log(‘complete-‘ + (new Date().getTime() - startTime)); clearInterval(_timer2); } }, 5);
The DOMContentLoaded event is fired when the document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading
document.addEventListener("DOMContentLoaded", function() {
console.log('DOMContentLoaded-' + (new Date().getTime() - startTime));
}, false);
detect a fully-loaded page
window.addEventListener("load", function() {
console.log('onload-' + (new Date().getTime() - startTime));
}, false);
check for JS errors
window.onerror = function(message, url, linenumber) {
console.log("jserror-JavaScript error: " + message + " on line " + linenumber + " for " + url);
};
},this.performance.start);
},
onLoadStarted: function (page, config) {
if (!this.performance.start) {
this.performance.start = new Date().getTime();
}
},
onResourceRequested: function (page, config, request) {
var now = new Date().getTime();
this.performance.resources[request.id] = {
id: request.id,
url: request.url,
request: request,
responses: {},
duration: '',
times: {
request: now
}
};
if (!this.performance.start || now < this.performance.start) {
this.performance.start = now;
}
},
onResourceReceived: function (page, config, response) {
var now = new Date().getTime(),
resource = this.performance.resources[response.id];
resource.responses[response.stage] = response;
if (!resource.times[response.stage]) {
resource.times[response.stage] = now;
resource.duration = now - resource.times.request;
}
if (response.bodySize) {
resource.size = response.bodySize;
response.headers.forEach(function (header) {
});
} else if (!resource.size) {
response.headers.forEach(function (header) {
if (header.name.toLowerCase()=='content-length' && header.value != 0) {
console.log(‘backup———-‘ + header.name + ‘:’ + header.value);
resource.size = parseInt(header.value);
}
});
}
},
onLoadFinished: function (page, config, status) {
var start = this.performance.start,
finish = new Date().getTime(),
resources = this.performance.resources,
slowest, fastest, totalDuration = 0,
largest, smallest, totalSize = 0,
missingList = [],
missingSize = false,
elapsed = finish - start,
now = new Date();
resources.forEach(function (resource) {
if (!resource.times.start) {
resource.times.start = resource.times.end;
}
if (!slowest || resource.duration > slowest.duration) {
slowest = resource;
}
if (!fastest || resource.duration < fastest.duration) {
fastest = resource;
}
console.log(totalDuration);
totalDuration += resource.duration;
if (resource.size) {
if (!largest || resource.size > largest.size) {
largest = resource;
}
if (!smallest || resource.size < smallest.size) {
smallest = resource;
}
totalSize += resource.size;
} else {
resource.size = 0;
missingSize = true;
missingList.push(resource.url);
}
});
if (config.verbose) {
console.log('');
this.emitConfig(config, '');
}
var report = {};
report.url = phantom.args[0];
report.phantomCacheEnabled = phantom.args.indexOf('yes') >= 0 ? 'yes' : 'no';
report.taskName = config.task;
var drsi = parseInt(this.performance.evalConsole.interactive);
var drsl = parseInt(this.performance.evalConsole.loading);
var wo = parseInt(this.performance.evalConsole.onload);
var drsc = parseInt(this.performance.evalConsole.complete);
report.domReadystateLoading = isNaN(drsl) == false ? drsl : 0;
report.domReadystateInteractive = isNaN(drsi) == false ? drsi : 0;
report.domReadystateComplete = isNaN(drsc) == false ? drsc : 0;
report.windowOnload = isNaN(wo) == false ? wo : 0;
report.elapsedLoadTime = elapsed;
report.numberOfResources = resources.length-1;
report.totalResourcesTime = totalDuration;
report.slowestResource = slowest.url;
report.largestResource = largest.url;
report.totalResourcesSize = (totalSize / 1000);
report.nonReportingResources = missingList.length;
report.timeStamp = now.getTime();
report.date = now.getDate() + "/" + now.getMonth() + "/" + now.getFullYear();
report.time = now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds();
report.errors = this.performance.evalConsoleErrors;
console.log(JSON.stringify(report));
console.log('Elapsed load time: ' + this.pad(elapsed, 6) + 'ms');
if(config.format == 'csv'){
this.printToFile(config,report,'loadreport','csv',config.wipe);
}
if(config.format == 'json'){
this.printToFile(config,report,'loadreport','json',config.wipe);
}
if(config.format == 'junit'){
this.printToFile(config,report,'loadreport','xml',config.wipe);
}
}
},
filmstrip: {
onInitialized: function(page, config) {
this.screenshot(new Date().getTime(),page);
},
onLoadStarted: function (page, config) {
if (!this.performance.start) {
this.performance.start = new Date().getTime();
}
this.screenshot(new Date().getTime(),page);
},
onResourceRequested: function (page, config, request) {
this.screenshot(new Date().getTime(),page);
},
onResourceReceived: function (page, config, response) {
this.screenshot(new Date().getTime(),page);
},
onLoadFinished: function (page, config, status) {
this.screenshot(new Date().getTime(),page);
}
},
getFinalUrl: function (page) {
return page.evaluate(function () {
return document.location.toString();
});
},
emitConfig: function (config, prefix) {
console.log(prefix + 'Config:');
for (key in config) {
if (config[key].constructor === Object) {
if (key===config.task) {
console.log(prefix + ' ' + key + ':');
for (key2 in config[key]) {
console.log(prefix + ' ' + key2 + ': ' + config[key][key2]);
}
}
} else {
console.log(prefix + ' ' + key + ': ' + config[key]);
}
}
},
load: function (config, task, scope) {
var page = WebPage.create(),
pagetemp = WebPage.create(),
event;
if (config.userAgent && config.userAgent != "default") {
if (config.userAgentAliases[config.userAgent]) {
config.userAgent = config.userAgentAliases[config.userAgent];
}
page.settings.userAgent = config.userAgent;
}
['onInitialized', 'onLoadStarted', 'onResourceRequested', 'onResourceReceived']
.forEach(function (event) {
if (task[event]) {
page[event] = function () {
var args = [page, config],
a, aL;
for (a = 0, aL = arguments.length; a < aL; a++) {
args.push(arguments[a]);
}
task[event].apply(scope, args);
};
}
});
if (task.onLoadFinished) {
page.onLoadFinished = function (status) {
if (config.wait) {
setTimeout(
function () {
task.onLoadFinished.call(scope, page, config, status);
},
config.wait
);
} else {
task.onLoadFinished.call(scope, page, config, status);
}
phantom.exit();
page = WebPage.create();
doPageLoad();
};
} else {
page.onLoadFinished = function (status) {
phantom.exit();
};
}
page.settings.localToRemoteUrlAccessEnabled = true;
page.settings.webSecurityEnabled = false;
page.onConsoleMessage = function (msg) {
console.log(msg)
if (msg.indexOf('jserror-') >= 0){
loadreport.performance.evalConsoleErrors.push(msg.substring('jserror-'.length,msg.length));
}else{
if (msg.indexOf('loading-') >= 0){
loadreport.performance.evalConsole.loading = msg.substring('loading-'.length,msg.length);
} else if (msg.indexOf('interactive-') >= 0){
loadreport.performance.evalConsole.interactive = msg.substring('interactive-'.length,msg.length);
} else if (msg.indexOf(‘complete-‘) >= 0){ loadreport.performance.evalConsole.complete = msg.substring(‘complete-‘.length,msg.length);
} else if (msg.indexOf('onload-') >= 0){
loadreport.performance.evalConsole.onload = msg.substring('onload-'.length,msg.length);
}
loadreport.performance.evalConsole.push(msg);
}
};
page.onError = function (msg, trace) {
console.log(“+++++ “ + msg);
trace.forEach(function(item) {
loadreport.performance.evalConsoleErrors.push(msg + ':' + item.file + ':' + item.line);
})
};
function doPageLoad(){
setTimeout(function(){page.open(config.url);},config.cacheWait);
}
if(config.task == 'performancecache'){
pagetemp.open(config.url,function(status) {
if (status === 'success') {
pagetemp.release();
doPageLoad();
}
});
}else{
doPageLoad();
}
},
processArgs: function (config, contract) {
var a = 0;
var ok = true;
contract.forEach(function(argument) {
if (a < phantom.args.length) {
config[argument.name] = phantom.args[a];
} else {
if (argument.req) {
console.log('"' + argument.name + '" argument is required. This ' + argument.desc + '.');
ok = false;
} else {
config[argument.name] = argument.def;
}
}
if (argument.oneof && argument.oneof.indexOf(config[argument.name])==-1) {
console.log('"' + argument.name + '" argument must be one of: ' + argument.oneof.join(', '));
ok = false;
}
a++;
});
return ok;
},
mergeConfig: function (config, configFile) {
var result = {};
if (!fs.exists(configFile)) {
configFile = "loadreport/config.json";
}
if (!fs.exists(configFile)) {
configFile = "config.json";
}
if (fs.exists(configFile)) {
result = JSON.parse(fs.read(configFile));
for (var key in config) {
result[key] = config[key];
}
}
return result;
},
truncate: function (str, length) {
length = length || 80;
if (str.length <= length) {
return str;
}
var half = length / 2;
return str.substr(0, half-2) + '...' + str.substr(str.length-half+1);
},
pad: function (str, length) {
var padded = str.toString();
if (padded.length > length) {
return this.pad(padded, length * 2);
}
return this.repeat(' ', length - padded.length) + padded;
},
repeat: function (chr, length) {
for (var str = '', l = 0; l < length; l++) {
str += chr;
}
return str;
},
clone: function(obj) {
var target = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
target[i] = obj[i];
}
}
return target;
},
timerStart: function () {
return (new Date()).getTime();
},
timerEnd: function (start) {
return ((new Date()).getTime() - start);
},
/*worker: function(now,page){
var currentTime = now - this.performance.start;
var ths = this;
if((currentTime) >= this.performance.count1){
var worker = new Worker('file:///Users/wesleyhales/phantom-test/worker.js');
worker.addEventListener('message', function (event) {
getting errors after 3rd thread with… _this.workerTask.callback(event); mycallback(event);
console.log('message' + event.data);
}, false);
worker.postMessage(page);
this.performance.count2++;
this.performance.count1 = currentTime + (this.performance.count2 * 100);
}
},*/
screenshot: function(now,page){
var start = this.timerStart();
var currentTime = now - this.performance.start;
var ths = this;
if((currentTime) >= this.performance.count1){
var ashot = page.renderBase64();
page.render('filmstrip/screenshot' + this.performance.timer + '.png');
this.performance.count2++;
this.performance.count1 = currentTime + (this.performance.count2 * 100);
subtract the time it took to render this image
this.performance.timer = this.timerEnd(start) - this.performance.count1;
}
},
/**
* Format test results as JUnit XML for CI
* @see: http://www.junit.org/
* @param {Array} tests the arrays containing the test results from testResults.
* @return {String} the results as JUnit XML text
*/
formatAsJUnit: function (keys, values) {
var junitable = ['domReadystateLoading','domReadystateInteractive','windowOnload','elapsedLoadTime','numberOfResources','totalResourcesTime','totalResourcesSize','nonReportingResources'];
var i, n = 0, key, value, suite,
junit = [],
suites = [];
for (i = 0; i < keys.length; i++) {
key = keys[i];
if (junitable.indexOf(key) === -1) {
continue;
}
value = values[i];
open test suite w/ summary
suite = ' <testsuite name="' + key + '" tests="1">\n';
suite += ' <testcase name="' + key + '" time="' + value + '"/>\n';
suite +=' </testsuite>';
suites.push(suite);
n++;
}
xml
junit.push('<?xml version="1.0" encoding="UTF-8" ?>');
open test suites wrapper
junit.push('<testsuites>');
concat test cases
junit = junit.concat(suites);
close test suites wrapper
junit.push('</testsuites>');
return junit.join('\n');
},
printToFile: function(config,report,filename,extension,createNew) {
var f, myfile,
keys = [], values = [];
for(var key in report)
{
if(report.hasOwnProperty(key))
{
keys.push(key);
values.push(report[key]);
}
}
if(config.file_suffix){
myfile = config.output+'reports/' + filename + '-' + config.file_suffix + '.' + extension;
}else{
myfile = config.output+'reports/' + filename + '.' + extension;
}
Given localhost:8880/some Transforms to localhost_8880/some
myfile = myfile.replace(":","_");
if(!createNew && fs.exists(myfile)){
file exists so append line
try{
switch (extension) {
case 'json':
var phantomLog = [];
var tempLine = null;
var json_content = fs.read(myfile);
if( json_content != "" ){
tempLine = JSON.parse(json_content);
}
if(Object.prototype.toString.call( tempLine ) === '[object Array]'){
phantomLog = tempLine;
}
phantomLog.push(report);
fs.remove(myfile);
f = fs.open(myfile, "w");
f.writeLine(JSON.stringify(phantomLog));
f.close();
break;
case 'xml':
console.log("cannot append report to xml file");
break;
default:
f = fs.open(myfile, "a");
f.writeLine(values);
f.close();
break;
}
} catch (e) {
console.log("problem appending to file",e);
}
}else{
if(fs.exists(myfile)){
fs.remove(myfile);
}
write the headers and first line
try {
f = fs.open(myfile, "w");
switch (extension) {
case 'json':
f.writeLine(JSON.stringify(report));
break;
case 'xml':
f.writeLine(this.formatAsJUnit(keys, values));
break;
default:
f.writeLine(keys);
f.writeLine(values);
break;
}
f.close();
} catch (e) {
console.log("problem writing to file",e);
}
}
}
};
loadreport.run();