sitespeed.io/lib/collectors/pages.js

323 lines
8.6 KiB
JavaScript

/**
* Sitespeed.io - How speedy is your site? (https://www.sitespeed.io)
* Copyright (c) 2014, Peter Hedenskog, Tobias Lidskog
* and other contributors
* Released under the Apache 2.0 License
*/
'use strict';
var util = require('../util/util'),
yslowUtil = require('../util/yslowUtil');
var pages = [];
var navigationTimingNames = ['navigationStart',
'unloadEventStart',
'unloadEventEnd',
'redirectStart',
'redirectEnd',
'fetchStart',
'domainLookupStart',
'domainLookupEnd',
'connectStart',
'connectEnd',
'secureConnectionStart',
'requestStart',
'responseStart',
'responseEnd',
'domLoading',
'domInteractive',
'domContentLoadedEventStart',
'domContentLoadedEventEnd',
'domComplete',
'loadEventStart',
'loadEventEnd'
];
var isDoc = function(comp) {
return (comp.type === 'doc');
};
exports.processPage = function(pageData) {
var p = {};
if (pageData.yslow) {
p = collectYSlowMetrics(pageData, p);
collectYSlowRules(pageData, p);
}
if (pageData.gpsi) {
collectGPSI(pageData, p);
}
if (pageData.browsertime) {
collectBrowserTime(pageData, p);
collectHAR(pageData, p);
}
if (pageData.webpagetest) {
collectWPT(pageData, p);
collectHAR(pageData, p);
}
if (pageData.headless) {
collectHeadlessData(pageData, p);
}
p.url = util.getURLFromPageData(pageData);
// TODO fix a cleaner check for this
// if an analyzed failed, skip it
if (p.url !== 'undefined') {
pages.push(p);
}
};
function collectYSlowMetrics(pageData, p) {
var docs = pageData.yslow.comps.filter(isDoc);
var assetTypes = ['js', 'css', 'image', 'cssimage', 'font', 'flash', 'iframe', 'doc'];
docs.forEach(function(doc) {
p = {
score: pageData.yslow.o,
// strip to only store response headers to save space?
headers: doc.headers,
yslow: {
requests: {
'v': pageData.yslow.comps.length,
'unit': ''
},
requestsMissingExpire: {
'v': pageData.yslow.comps.filter(function(c) {
return yslowUtil.getCacheTime(c) === 0;
}).length,
'unit': ''
},
timeSinceLastModification: {
'v': yslowUtil.getTimeSinceLastMod(doc),
'unit': 'seconds'
},
cacheTime: {
'v': yslowUtil.getCacheTime(doc),
'unit': 'seconds'
},
docWeight: {
'v': doc.size,
'unit': 'bytes'
},
pageWeight: {
'v': yslowUtil.getSize(pageData.yslow.comps),
'unit': 'bytes'
}
}
};
p.yslow.assets = {};
assetTypes.forEach(function(asset) {
p.yslow.assets[asset] = {
'v': pageData.yslow.comps.filter(function(c) {
return c.type === asset;
}).length,
'unit': ''
};
p.yslow.assets[asset + 'Weight'] = {
'v': yslowUtil.getSize(pageData.yslow.comps.filter(function(c) {
return c.type === asset;
})),
'unit': 'bytes'
};
});
});
return p;
}
function collectYSlowRules(pageData, p) {
p.rules = {};
// add all rule scores as fields
Object.keys(pageData.yslow.g).forEach(function(rule) {
p.rules[rule] = {
'v': pageData.yslow.g[rule].score,
'unit': ''
};
// TODO how should we name them
p.rules[rule].items = {
'v': pageData.yslow.g[rule].components.length,
'unit': ''
};
});
}
function collectGPSI(pageData, p) {
p.gpsi = {};
p.gpsi.gscore = {
'v': pageData.gpsi.score,
'unit': ''
};
}
function collectHeadlessData(pageData, p) {
var timingsWeWillPush = ['min', 'mean', 'median', 'p90', 'p99', 'max'];
p.headless = {};
pageData.headless.getStats().forEach(function(timing) {
p.headless[timing.id] = {};
timingsWeWillPush.forEach(function(number) {
p.headless[timing.id][number] = {
'v': timing.stats[number],
'unit': 'milliseconds'
};
});
});
}
function collectHAR(pageData, p) {
p.har = [];
if (pageData.browsertime) {
Array.prototype.push.apply(p.har, pageData.browsertime.har);
} else if (pageData.webpagetest) {
Array.prototype.push.apply(p.har, pageData.webpagetest.har);
}
}
function collectWPT(pageData, p) {
p.wpt = {};
// the views we will test, we will add the repeated view later if we have it
var views = ['firstView'];
// the different kind of data that we will fetch from WPT and add to the page
var sizes = ['image_savings', 'image_total', 'bytesIn', 'bytesInDoc'];
var timings = ['SpeedIndex', 'firstPaint', 'render', 'TTFB', 'visualComplete', 'domContentLoadedEventEnd',
'loadTime'
];
var others = ['requests'];
// for all browsers/locations and connections
pageData.webpagetest.wpt.forEach(function(browserAndLocation) {
var connectivity = browserAndLocation.response.data.connectivity.toLowerCase();
var locationAndBrowser = browserAndLocation.response.data.location.split(':');
var location = locationAndBrowser[0].toLowerCase();
var browser = locationAndBrowser[1].toLowerCase();
// if we don't have it, setup a clean object
p.wpt[location] = p.wpt[location] || {};
p.wpt[location][browser] = p.wpt[location][browser] || {};
p.wpt[location][browser][connectivity] = p.wpt[location][browser][connectivity] || {};
// only collect repeat view when we have the data
if (browserAndLocation.response.data.median.repeatView) {
views.push('repeatView');
}
views.forEach(function(view) {
p.wpt[location][browser][connectivity][view] = {};
sizes.forEach(function(size) {
p.wpt[location][browser][connectivity][view][size] = {
'v': browserAndLocation.response.data.median[view][size],
'unit': 'bytes'
};
});
timings.forEach(function(timing) {
p.wpt[location][browser][connectivity][view][timing] = {
'v': browserAndLocation.response.data.median[view][timing],
'unit': 'milliseconds'
};
});
// also fetch all user timings!
var userTimings = browserAndLocation.response.data.median[view].userTimes;
if (userTimings) {
Object.keys(userTimings).forEach(function(userTiming) {
p.wpt[location][browser][connectivity][view][userTiming] = {
'v': browserAndLocation.response.data.median[view].userTimes[userTiming],
'unit': 'milliseconds'
};
});
}
// and custom metrics
var customMetrics = browserAndLocation.response.data.median[view].custom;
if (customMetrics) {
customMetrics.value.forEach(function(metricName) {
p.wpt[location][browser][connectivity][view][metricName] = {
'v': browserAndLocation.response.data.median[view][metricName],
'unit': ''
};
});
}
others.forEach(function(metric) {
p.wpt[location][browser][connectivity][view][metric] = {
'v': browserAndLocation.response.data.median[view][metric],
'unit': ''
};
});
});
});
}
function collectBrowserTime(pageData, p) {
var types = ['default', 'custom'];
var statsWeWillPush = ['min', 'mean', 'median', 'p90', 'p99', 'max'];
p.timings = {};
p.navigationtiming = {};
p.extras = {};
p.custom = {};
types.forEach(function(type) {
pageData.browsertime.browsertime.forEach(function(runPerBrowser) {
var browser = runPerBrowser.browserName;
p.timings[browser] = p.timings[browser] || {};
p.extras[browser] = p.extras[browser] || {};
p.custom[browser] = p.custom[browser] || {};
p.navigationtiming[browser] = p.navigationtiming[browser] || {};
for (var stats in runPerBrowser[type].statistics) {
var a = p.custom;
if (type === 'default') {
// if it is a timing
if (stats.indexOf('Time') > -1 || stats === 'speedIndex' || stats === 'firstPaint') {
a = p.timings;
} else if (navigationTimingNames.indexOf(stats) > -1) {
a = p.navigationtiming;
} else {
a = p.extras;
}
}
a[browser][stats] = {};
a[stats] = {};
for (var number in statsWeWillPush) {
a[browser][stats][statsWeWillPush[number]] = {
'v': runPerBrowser[type].statistics[stats][statsWeWillPush[number]],
'unit': 'milliseconds'
};
a[stats][statsWeWillPush[number]] = {
'v': runPerBrowser[type].statistics[stats][statsWeWillPush[number]],
'unit': 'milliseconds'
};
}
}
});
});
}
exports.generateResults = function() {
return {
id: 'pages',
list: pages
};
};
exports.clear = function() {
pages = [];
};