UNPKG

yslow

Version:

Commnad line version of YSlow web performance analysis tool from HAR files

1,518 lines (1,340 loc) 210 kB
/*global YSLOW:true*/ /*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */ /** * @module YSLOW * @class YSLOW * @static */ if (typeof YSLOW === 'undefined') { YSLOW = {}; } /** * Enable/disable debbuging messages */ YSLOW.DEBUG = true; /** * * Adds a new rule to the pool of rules. * * Rule objects must implement the rule interface or an error will be thrown. The interface * of a rule object is as follows: * <ul> * <li><code>id</code>, e.g. "numreq"</li> * <li><code>name</code>, e.g. "Minimize HTTP requests"</li> * <li><code>url</code>, more info about the rule</li> * <li><code>config</code>, configuration object with defaults</li> * <li><code>lint()</code> a method that accepts a document, array of components and a config object and returns a reuslt object</li> * </ul> * * @param {YSLOW.Rule} rule A new rule object to add */ YSLOW.registerRule = function (rule) { YSLOW.controller.addRule(rule); }; /** * * Adds a new ruleset (new grading algorithm). * * Ruleset objects must implement the ruleset interface or an error will be thrown. The interface * of a ruleset object is as follows: * <ul> * <li><code>id</code>, e.g. "ydefault"</li> * <li><code>name</code>, e.g. "Yahoo! Default"</li> * <li><code>rules</code> a hash of ruleID => ruleconfig </li> * <li><code>weights</code> a hash of ruleID => ruleweight </li> * </ul> * * @param {YSLOW.Ruleset} ruleset The new ruleset object to be registered */ YSLOW.registerRuleset = function (ruleset) { YSLOW.controller.addRuleset(ruleset); }; /** * Register a renderer. * * Renderer objects must implement the renderer interface. * The interface is as follows: * <ul> * <li><code>id</code></li> * <li><code>supports</code> a hash of view_name => 1 or 0 to indicate what views are supported</li> * <li>and the methods</li> * </ul> * * For instance if you define a JSON renderer that only render grade. Your renderer object will look like this: * { id: 'json', * supports: { reportcard: 1, components: 0, stats: 0, cookies: 0}, * reportcardView: function(resultset) { ... } * } * * Refer to YSLOW.HTMLRenderer for the function prototype. * * * @param {YSLOW.renderer} renderer The new renderer object to be registered. */ YSLOW.registerRenderer = function (renderer) { YSLOW.controller.addRenderer(renderer); }; /** * Adds a new tool. * * Tool objects must implement the tool interface or an error will be thrown. * The interface of a tool object is as follows: * <ul> * <li><code>id</code>, e.g. 'mytool'</li> * <li><code>name</code>, eg. 'Custom tool #3'</li> * <li><code>print_output</code>, whether this tool will produce output.</li> * <li><code>run</code>, function that takes doc and componentset object, return content to be output</li> * </ul> * * @param {YSLOW.Tool} tool The new tool object to be registered */ YSLOW.registerTool = function (tool) { YSLOW.Tools.addCustomTool(tool); }; /** * Register an event listener * * @param {String} event_name Name of the event * @param {Function} callback A function to be called when the event fires * @param {Object} that Object to be assigned to the "this" value of the callback function */ YSLOW.addEventListener = function (event_name, callback, that) { YSLOW.util.event.addListener(event_name, callback, that); }; /** * Unregister an event listener. * * @param {String} event_name Name of the event * @param {Function} callback The callback function that was added as a listener * @return {Boolean} TRUE is the listener was removed successfully, FALSE otherwise (for example in cases when the listener doesn't exist) */ YSLOW.removeEventListener = function (event_name, callback) { return YSLOW.util.event.removeListener(event_name, callback); }; /** * @namespace YSLOW * @constructor * @param {String} name Error type * @param {String} message Error description */ YSLOW.Error = function (name, message) { /** * Type of error, e.g. "Interface error" * @type String */ this.name = name; /** * Error description * @type String */ this.message = message; }; YSLOW.Error.prototype = { toString: function () { return this.name + "\n" + this.message; } }; YSLOW.version = '3.1.0'; /** * Copyright (c) 2012, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*global YSLOW,MutationEvent*/ /*jslint browser: true, continue: true, sloppy: true, maxerr: 50, indent: 4 */ /** * ComponentSet holds an array of all the components and get the response info from net module for each component. * * @constructor * @param {DOMElement} node DOM Element * @param {Number} onloadTimestamp onload timestamp */ YSLOW.ComponentSet = function (node, onloadTimestamp) { // // properties // this.root_node = node; this.components = []; this.outstanding_net_request = 0; this.component_info = []; this.onloadTimestamp = onloadTimestamp; this.nextID = 1; this.notified_fetch_done = false; }; YSLOW.ComponentSet.prototype = { /** * Call this function when you don't use the component set any more. * A chance to do proper clean up, e.g. remove event listener. */ clear: function () { this.components = []; this.component_info = []; this.cleared = true; if (this.outstanding_net_request > 0) { YSLOW.util.dump("YSLOW.ComponentSet.Clearing component set before all net requests finish."); } }, /** * Add a new component to the set. * @param {String} url URL of component * @param {String} type type of component * @param {String} base_href base href of document that the component belongs. * @param {Object} obj DOMElement (for image type only) * @return Component object that was added to ComponentSet * @type ComponentObject */ addComponent: function (url, type, base_href, o) { var comp, found, isDoc; if (!url) { if (!this.empty_url) { this.empty_url = []; } this.empty_url[type] = (this.empty_url[type] || 0) + 1; } if (url && type) { // check if url is valid. if (!YSLOW.ComponentSet.isValidProtocol(url) || !YSLOW.ComponentSet.isValidURL(url)) { return comp; } // Make sure url is absolute url. url = YSLOW.util.makeAbsoluteUrl(url, base_href); // For security purpose url = YSLOW.util.escapeHtml(url); found = typeof this.component_info[url] !== 'undefined'; isDoc = type === 'doc'; // make sure this component is not already in this component set, // but also check if a doc is coming after a redirect using same url if (!found || isDoc) { this.component_info[url] = { 'state': 'NONE', 'count': found ? this.component_info[url].count : 0 }; comp = new YSLOW.Component(url, type, this, o); if (comp) { comp.id = this.nextID += 1; this.components[this.components.length] = comp; // shortcup for document component if (!this.doc_comp && isDoc) { this.doc_comp = comp; } if (this.component_info[url].state === 'NONE') { // net.js has probably made an async request. this.component_info[url].state = 'REQUESTED'; this.outstanding_net_request += 1; } } else { this.component_info[url].state = 'ERROR'; YSLOW.util.event.fire("componentFetchError"); } } this.component_info[url].count += 1; } return comp; }, /** * Add a new component to the set, ignore duplicate. * @param {String} url url of component * @param {String} type type of component * @param {String} base_href base href of document that the component belongs. */ addComponentNoDuplicate: function (url, type, base_href) { if (url && type) { // For security purpose url = YSLOW.util.escapeHtml(url); url = YSLOW.util.makeAbsoluteUrl(url, base_href); if (this.component_info[url] === undefined) { return this.addComponent(url, type, base_href); } } }, /** * Get components by type. * * @param {String|Array} type The type of component to get, e.g. "js" or * ['js', 'css'] * @param {Boolean} include_after_onload If component loaded after onload * should be included in the returned results, default is FALSE, * don't include * @param {Boolean} include_beacons If image beacons (1x1 images) should * be included in the returned results, default is FALSE, don't * include * @return An array of matching components * @type Array */ getComponentsByType: function (type, includeAfterOnload, includeBeacons) { var i, j, len, lenJ, t, comp, info, components = this.components, compInfo = this.component_info, comps = [], types = {}; if (typeof includeAfterOnload === 'undefined') { includeAfterOnload = !(YSLOW.util.Preference.getPref( 'excludeAfterOnload', true )); } if (typeof includeBeacons === 'undefined') { includeBeacons = !(YSLOW.util.Preference.getPref( 'excludeBeaconsFromLint', true )); } if (typeof type === 'string') { types[type] = 1; } else { for (i = 0, len = type.length; i < len; i += 1) { t = type[i]; if (t) { types[t] = 1; } } } for (i = 0, len = components.length; i < len; i += 1) { comp = components[i]; if (!comp || (comp && !types[comp.type]) || (comp.is_beacon && !includeBeacons) || (comp.after_onload && !includeAfterOnload)) { continue; } comps[comps.length] = comp; info = compInfo[i]; if (!info || (info && info.count <= 1)) { continue; } for (j = 1, lenJ = info.count; j < lenJ; j += 1) { comps[comps.length] = comp; } } return comps; }, /** * @private * Get fetching progress. * @return { 'total' => total number of component, 'received' => number of components fetched } */ getProgress: function () { var i, total = 0, received = 0; for (i in this.component_info) { if (this.component_info.hasOwnProperty(i) && this.component_info[i]) { if (this.component_info[i].state === 'RECEIVED') { received += 1; } total += 1; } } return { 'total': total, 'received': received }; }, /** * Event callback when component's GetInfoState changes. * @param {Object} event object */ onComponentGetInfoStateChange: function (event_object) { var comp, state, progress; if (event_object) { if (typeof event_object.comp !== 'undefined') { comp = event_object.comp; } if (typeof event_object.state !== 'undefined') { state = event_object.state; } } if (typeof this.component_info[comp.url] === 'undefined') { // this should not happen. YSLOW.util.dump("YSLOW.ComponentSet.onComponentGetInfoStateChange(): Unexpected component: " + comp.url); return; } if (this.component_info[comp.url].state === "NONE" && state === 'DONE') { this.component_info[comp.url].state = "RECEIVED"; } else if (this.component_info[comp.url].state === "REQUESTED" && state === 'DONE') { this.component_info[comp.url].state = "RECEIVED"; this.outstanding_net_request -= 1; // Got all component detail info. if (this.outstanding_net_request === 0) { this.notified_fetch_done = true; YSLOW.util.event.fire("componentFetchDone", { 'component_set': this }); } } else { // how does this happen? YSLOW.util.dump("Unexpected component info state: [" + comp.type + "]" + comp.url + "state: " + state + " comp_info_state: " + this.component_info[comp.url].state); } // fire event. progress = this.getProgress(); YSLOW.util.event.fire("componentFetchProgress", { 'total': progress.total, 'current': progress.received, 'last_component_url': comp.url }); }, /** * This is called when peeler is done. * If ComponentSet has all the component info, fire componentFetchDone event. */ notifyPeelDone: function () { if (this.outstanding_net_request === 0 && !this.notified_fetch_done) { this.notified_fetch_done = true; YSLOW.util.event.fire("componentFetchDone", { 'component_set': this }); } }, /** * After onload guess (simple version) * Checkes for elements with src or href attributes within * the original document html source */ setSimpleAfterOnload: function (callback, obj) { var i, j, comp, doc_el, doc_comps, src, indoc, url, el, type, len, lenJ, docBody, doc, components, that; if (obj) { docBody = obj.docBody; doc = obj.doc; components = obj.components; that = obj.components; } else { docBody = this.doc_comp && this.doc_comp.body; doc = this.root_node; components = this.components; that = this; } // skip testing when doc not found if (!docBody) { YSLOW.util.dump('doc body is empty'); return callback(that); } doc_el = doc.createElement('div'); doc_el.innerHTML = docBody; doc_comps = doc_el.getElementsByTagName('*'); for (i = 0, len = components.length; i < len; i += 1) { comp = components[i]; type = comp.type; if (type === 'cssimage' || type === 'doc') { // docs are ignored // css images are likely to be loaded before onload continue; } indoc = false; url = comp.url; for (j = 0, lenJ = doc_comps.length; !indoc && j < lenJ; j += 1) { el = doc_comps[j]; src = el.src || el.href || el.getAttribute('src') || el.getAttribute('href') || (el.nodeName === 'PARAM' && el.value); indoc = (src === url); } // if component wasn't found on original html doc // assume it was loaded after onload comp.after_onload = !indoc; } callback(that); }, /** * After onload guess * Checkes for inserted elements with src or href attributes after the * page onload event triggers using an iframe with original doc html */ setAfterOnload: function (callback, obj) { var ifrm, idoc, iwin, timer, done, noOnloadTimer, that, docBody, doc, components, ret, enough, triggered, util = YSLOW.util, addEventListener = util.addEventListener, removeEventListener = util.removeEventListener, setTimer = setTimeout, clearTimer = clearTimeout, comps = [], compsHT = {}, // get changed component and push to comps array // reset timer for 1s after the last dom change getTarget = function (e) { var type, attr, target, src, oldSrc; clearTimer(timer); type = e.type; attr = e.attrName; target = e.target; src = target.src || target.href || (target.getAttribute && ( target.getAttribute('src') || target.getAttribute('href') )); oldSrc = target.dataOldSrc; if (src && (type === 'DOMNodeInserted' || (type === 'DOMSubtreeModified' && src !== oldSrc) || (type === 'DOMAttrModified' && (attr === 'src' || attr === 'href'))) && !compsHT[src]) { compsHT[src] = 1; comps.push(target); } timer = setTimer(done, 1000); }, // temp iframe onload listener // - cancel noOnload timer since onload was fired // - wait 3s before calling done if no dom mutation happens // - set enough timer, limit is 10 seconds for mutations, this is // for edge cases when page inserts/removes nodes within a loop iframeOnload = function () { var i, len, all, el, src; clearTimer(noOnloadTimer); all = idoc.getElementsByTagName('*'); for (i = 0, len = all.length; i < len; i += 1) { el = all[i]; src = el.src || el.href; if (src) { el.dataOldSrc = src; } } addEventListener(iwin, 'DOMSubtreeModified', getTarget); addEventListener(iwin, 'DOMNodeInserted', getTarget); addEventListener(iwin, 'DOMAttrModified', getTarget); timer = setTimer(done, 3000); enough = setTimer(done, 10000); }; if (obj) { that = YSLOW.ComponentSet.prototype; docBody = obj.docBody; doc = obj.doc; components = obj.components; ret = components; } else { that = this; docBody = that.doc_comp && that.doc_comp.body; doc = that.root_node; components = that.components; ret = that; } // check for mutation event support or anti-iframe option if (typeof MutationEvent === 'undefined' || YSLOW.antiIframe) { return that.setSimpleAfterOnload(callback, obj); } // skip testing when doc not found if (!docBody) { util.dump('doc body is empty'); return callback(ret); } // set afteronload properties for all components loaded after window onlod done = function () { var i, j, len, lenJ, comp, src, cmp; // to avoid executing this function twice // due to ifrm iwin double listeners if (triggered) { return; } // cancel timers clearTimer(enough); clearTimer(timer); // remove listeners removeEventListener(iwin, 'DOMSubtreeModified', getTarget); removeEventListener(iwin, 'DOMNodeInserted', getTarget); removeEventListener(iwin, 'DOMAttrModified', getTarget); removeEventListener(ifrm, 'load', iframeOnload); removeEventListener(iwin, 'load', iframeOnload); // changed components loop for (i = 0, len = comps.length; i < len; i += 1) { comp = comps[i]; src = comp.src || comp.href || (comp.getAttribute && (comp.getAttribute('src') || comp.getAttribute('href'))); if (!src) { continue; } for (j = 0, lenJ = components.length; j < lenJ; j += 1) { cmp = components[j]; if (cmp.url === src) { cmp.after_onload = true; } } } // remove temp iframe and invoke callback passing cset ifrm.parentNode.removeChild(ifrm); triggered = 1; callback(ret); }; // create temp iframe with doc html ifrm = doc.createElement('iframe'); ifrm.style.cssText = 'position:absolute;top:-999em;'; doc.body.appendChild(ifrm); iwin = ifrm.contentWindow; // set a fallback when onload is not triggered noOnloadTimer = setTimer(done, 3000); // set onload and ifram content if (iwin) { idoc = iwin.document; } else { iwin = idoc = ifrm.contentDocument; } addEventListener(iwin, 'load', iframeOnload); addEventListener(ifrm, 'load', iframeOnload); idoc.open().write(docBody); idoc.close(); addEventListener(iwin, 'load', iframeOnload); } }; /* * List of protocols to ignore in component set. */ YSLOW.ComponentSet.ignoreProtocols = ['data', 'chrome', 'javascript', 'about', 'resource', 'jar', 'chrome-extension', 'file']; /** * @private * Check if url has an allowed protocol (no chrome://, about:) * @param url * @return false if url does not contain hostname. */ YSLOW.ComponentSet.isValidProtocol = function (s) { var i, index, protocol, ignoreProtocols = this.ignoreProtocols, len = ignoreProtocols.length; s = s.toLowerCase(); index = s.indexOf(':'); if (index > 0) { protocol = s.substr(0, index); for (i = 0; i < len; i += 1) { if (protocol === ignoreProtocols[i]) { return false; } } } return true; }; /** * @private * Check if passed url has hostname specified. * @param url * @return false if url does not contain hostname. */ YSLOW.ComponentSet.isValidURL = function (url) { var arr, host; url = url.toLowerCase(); // all url is in the format of <protocol>:<the rest of the url> arr = url.split(":"); // for http protocol, we want to make sure there is a host in the url. if (arr[0] === "http" || arr[0] === "https") { if (arr[1].substr(0, 2) !== "//") { return false; } host = arr[1].substr(2); if (host.length === 0 || host.indexOf("/") === 0) { // no host specified. return false; } } return true; }; /** * Copyright (c) 2012, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*global YSLOW*/ /*jslint white: true, onevar: true, undef: true, newcap: true, nomen: true, plusplus: true, bitwise: true, browser: true, maxerr: 50, indent: 4 */ /** * @namespace YSLOW * @class Component * @constructor */ YSLOW.Component = function (url, type, parent_set, o) { var obj = o && o.obj, comp = (o && o.comp) || {}; /** * URL of the component * @type String */ this.url = url; /** * Component type, one of the following: * <ul> * <li>doc</li> * <li>js</li> * <li>css</li> * <li>...</li> * </ul> * @type String */ this.type = type; /** * Parent component set. */ this.parent = parent_set; this.headers = {}; this.raw_headers = ''; this.req_headers = null; this.body = ''; this.compressed = false; this.expires = undefined; // to be replaced by a Date object this.size = 0; this.status = 0; this.is_beacon = false; this.method = 'unknown'; this.cookie = ''; this.respTime = null; this.after_onload = false; // component object properties // e.g. for image, image element width, image element height, actual width, actual height this.object_prop = undefined; // construction part if (type === undefined) { this.type = 'unknown'; } this.get_info_state = 'NONE'; if (obj && type === 'image' && obj.width && obj.height) { this.object_prop = { 'width': obj.width, 'height': obj.height }; } if (comp.containerNode) { this.containerNode = comp.containerNode; } this.setComponentDetails(o); }; YSLOW.Component.prototype.setComponentDetails = function (o) { var comp = this, // firefox parseResponse = function (response) { var headerName; // copy from the response object comp.status = response.status; for (headerName in response.headers) { if (response.headers.hasOwnProperty(headerName)) { comp.headers[headerName.toLowerCase()] = response.headers[headerName]; } } comp.raw_headers = response.raw_headers; if (response.req_headers) { comp.req_headers = {}; for (headerName in response.req_headers) { if (response.req_headers.hasOwnProperty(headerName)) { comp.req_headers[headerName.toLowerCase()] = response.req_headers[headerName]; } } } comp.body = (response.body !== null) ? response.body : ''; if (typeof response.method === 'string') { comp.method = response.method; } if ((comp.type === 'unknown' && response.type !== undefined) || (comp.type === 'doc' && (response.type !== undefined && response.type !== 'unknown'))) { comp.type = response.type; } // for security checking comp.response_type = response.type; if (typeof response.cookie === 'string') { comp.cookie = response.cookie; } if (typeof response.size === 'number' && response.size > 0) { comp.nsize = response.size; } if (typeof response.respTime !== 'undefined') { comp.respTime = response.respTime; } if (typeof response.startTimestamp !== 'undefined' && comp.parent.onloadTimestamp !== null) { comp.after_onload = (response.startTimestamp > comp.parent.onloadTimestamp); } else if (typeof response.after_onload !== 'undefined') { comp.after_onload = response.after_onload; } comp.populateProperties(true); comp.get_info_state = 'DONE'; // notify parent ComponentSet that this component has gotten net response. comp.parent.onComponentGetInfoStateChange({ 'comp': comp, 'state': 'DONE' }); }, // parse HAR entry parseEntry = function (entry) { var i, header, cookie, len, response = entry.response, request = entry.request; // copy from the response object comp.status = response.status; comp.headers = {}; comp.raw_headers = ''; for (i = 0, len = response.headers.length; i < len; i += 1) { header = response.headers[i]; comp.headers[header.name.toLowerCase()] = header.value; comp.raw_headers += header.name + ': ' + header.value + '\n'; } comp.req_headers = {}; for (i = 0, len = request.headers.length; i < len; i += 1) { header = request.headers[i]; comp.req_headers[header.name.toLowerCase()] = header.value; } comp.method = request.method; if (response.content && response.content.text) { comp.body = response.content.text; } else { // no body provided, getting size at least and mocking empty string content comp.body = { toString: function () {return '';}, length: response.content.size || 0 }; } // for security checking comp.response_type = comp.type; comp.cookie = (comp.headers['set-cookie'] || '') + (comp.req_headers['cookie'] || ''); comp.nsize = parseInt(comp.headers['content-length'], 10) || response.bodySize || response.content.size; comp.respTime = entry.time; comp.after_onload = (new Date(entry.startedDateTime) .getTime()) > comp.parent.onloadTimestamp; // populate properties ignoring redirect // resolution and image request comp.populateProperties(false, true); comp.get_info_state = 'DONE'; // notify parent ComponentSet that this component has gotten net response. comp.parent.onComponentGetInfoStateChange({ 'comp': comp, 'state': 'DONE' }); }, // parse component (chrome and bookmarklet) parseComponent = function (component) { var headerName, h, i, len, m, reHeader = /^([^:]+):\s*([\s\S]+)$/, headers = component.rawHeaders; // copy from the response object comp.status = component.status; comp.raw_headers = headers; if (component.headers) { for (headerName in component.headers) { if (component.headers.hasOwnProperty(headerName)) { comp.headers[headerName.toLowerCase()] = component.headers[headerName]; } } } else if (typeof headers === 'string') { h = headers.split('\n'); for (i = 0, len = h.length; i < len; i += 1) { m = reHeader.exec(h[i]); if (m) { comp.headers[m[1].toLowerCase()] = m[2]; } } } comp.req_headers = {}; comp.method = 'GET'; comp.body = component.content || component.body || ''; comp.type = component.type; // for security checking comp.response_type = comp.type; comp.cookie = comp.headers['set-cookie'] || ''; comp.nsize = parseInt(comp.headers['content-length'], 10) || comp.body.length; comp.respTime = 0; if (component.after_onload) { comp.after_onload = component.after_onload; } if (typeof component.injected !== 'undefined') { comp.injected = component.injected; } if (typeof component.defer !== 'undefined') { comp.defer = component.defer; } if (typeof component.async !== 'undefined') { comp.async = component.async; } comp.populateProperties(); comp.get_info_state = 'DONE'; // notify parent ComponentSet that this // component has gotten net response. comp.parent.onComponentGetInfoStateChange({ 'comp': comp, 'state': 'DONE' }); }; if (o && o.component) { // chrome and bookmarklet parseComponent(o.component); } else if (o && o.entry) { // har parseEntry(o.entry); } else { // firefox YSLOW.net.getInfo(this.url, parseResponse, (this.type.indexOf('image') > -1)); } }; /** * Return the state of getting detail info from the net. */ YSLOW.Component.prototype.getInfoState = function () { return this.get_info_state; }; YSLOW.Component.prototype.populateProperties = function (resolveRedirect, ignoreImgReq) { var comp, encoding, expires, content_length, img_src, obj, dataUri, that = this, NULL = null, UNDEF = 'undefined'; // check location // bookmarklet and har already handle redirects if (that.headers.location && resolveRedirect) { // Add a new component. comp = that.parent.addComponentNoDuplicate(that.headers.location, (that.type !== 'redirect' ? that.type : 'unknown'), that.url); if (comp && that.after_onload) { comp.after_onload = true; } that.type = 'redirect'; } content_length = that.headers['content-length']; // gzip, deflate encoding = YSLOW.util.trim(that.headers['content-encoding']); if (encoding === 'gzip' || encoding === 'deflate') { that.compressed = encoding; that.size = (that.body.length) ? that.body.length : NULL; if (content_length) { that.size_compressed = parseInt(content_length, 10) || content_length; } else if (typeof that.nsize !== UNDEF) { that.size_compressed = that.nsize; } else { // a hack that.size_compressed = that.size / 3; } } else { that.compressed = false; that.size_compressed = NULL; if (content_length) { that.size = parseInt(content_length, 10); } else if (typeof that.nsize !== UNDEF) { that.size = parseInt(that.nsize, 10); } else { that.size = that.body.length; } } // size check/correction, @todo be more precise here if (!that.size) { if (typeof that.nsize !== UNDEF) { that.size = that.nsize; } else { that.size = that.body.length; } } that.uncompressed_size = that.body.length; // expiration based on either Expires or Cache-Control headers expires = that.headers.expires; if (expires && expires.length > 0) { // set expires as a JS object that.expires = new Date(expires); if (that.expires.toString() === 'Invalid Date') { that.expires = that.getMaxAge(); } } else { that.expires = that.getMaxAge(); } // compare image original dimensions with actual dimensions, data uri is // first attempted to get the orginal dimension, if it fails (btoa) then // another request to the orginal image is made if (that.type === 'image' && !ignoreImgReq) { if (typeof Image !== UNDEF) { obj = new Image(); } else { obj = document.createElement('img'); } if (that.body.length) { img_src = 'data:' + that.headers['content-type'] + ';base64,' + YSLOW.util.base64Encode(that.body); dataUri = 1; } else { img_src = that.url; } obj.onerror = function () { obj.onerror = NULL; if (dataUri) { obj.src = that.url; } }; obj.onload = function () { obj.onload = NULL; if (obj && obj.width && obj.height) { if (that.object_prop) { that.object_prop.actual_width = obj.width; that.object_prop.actual_height = obj.height; } else { that.object_prop = { 'width': obj.width, 'height': obj.height, 'actual_width': obj.width, 'actual_height': obj.height }; } if (obj.width < 2 && obj.height < 2) { that.is_beacon = true; } } }; obj.src = img_src; } }; /** * Return true if this object has a last-modified date significantly in the past. */ YSLOW.Component.prototype.hasOldModifiedDate = function () { var now = Number(new Date()), modified_date = this.headers['last-modified']; if (typeof modified_date !== 'undefined') { // at least 1 day in the past return ((now - Number(new Date(modified_date))) > (24 * 60 * 60 * 1000)); } return false; }; /** * Return true if this object has a far future Expires. * @todo: make the "far" interval configurable * @param expires Date object * @return true if this object has a far future Expires. */ YSLOW.Component.prototype.hasFarFutureExpiresOrMaxAge = function () { var expires_in_seconds, now = Number(new Date()), minSeconds = YSLOW.util.Preference.getPref('minFutureExpiresSeconds', 2 * 24 * 60 * 60), minMilliSeconds = minSeconds * 1000; if (typeof this.expires === 'object') { expires_in_seconds = Number(this.expires); if ((expires_in_seconds - now) > minMilliSeconds) { return true; } } return false; }; YSLOW.Component.prototype.getEtag = function () { return this.headers.etag || ''; }; YSLOW.Component.prototype.getMaxAge = function () { var index, maxage, expires, cache_control = this.headers['cache-control']; if (cache_control) { index = cache_control.indexOf('max-age'); if (index > -1) { maxage = parseInt(cache_control.substring(index + 8), 10); if (maxage > 0) { expires = YSLOW.util.maxAgeToDate(maxage); } } } return expires; }; /** * Return total size of Set-Cookie headers of this component. * @return total size of Set-Cookie headers of this component. * @type Number */ YSLOW.Component.prototype.getSetCookieSize = function () { // only return total size of cookie received. var aCookies, k, size = 0; if (this.headers && this.headers['set-cookie']) { aCookies = this.headers['set-cookie'].split('\n'); if (aCookies.length > 0) { for (k = 0; k < aCookies.length; k += 1) { size += aCookies[k].length; } } } return size; }; /** * Return total size of Cookie HTTP Request headers of this component. * @return total size of Cookie headers Request of this component. * @type Number */ YSLOW.Component.prototype.getReceivedCookieSize = function () { // only return total size of cookie sent. var aCookies, k, size = 0; if (this.cookie && this.cookie.length > 0) { aCookies = this.cookie.split('\n'); if (aCookies.length > 0) { for (k = 0; k < aCookies.length; k += 1) { size += aCookies[k].length; } } } return size; }; /** * Copyright (c) 2012, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*global YSLOW*/ /*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */ /** * YSlow context object that holds components set, result set and statistics of current page. * * @constructor * @param {Document} doc Document object of current page. */ YSLOW.context = function (doc) { this.document = doc; this.component_set = null; this.result_set = null; this.onloadTimestamp = null; // reset renderer variables if (YSLOW.renderer) { YSLOW.renderer.reset(); } this.PAGE = { totalSize: 0, totalRequests: 0, totalObjCount: {}, totalObjSize: {}, totalSizePrimed: 0, totalRequestsPrimed: 0, totalObjCountPrimed: {}, totalObjSizePrimed: {}, canvas_data: {}, statusbar: false, overallScore: 0, t_done: undefined, loaded: false }; }; YSLOW.context.prototype = { defaultview: "ysPerfButton", /** * @private * Compute statistics of current page. * @param {Boolean} bCacheFull set to true if based on primed cache, false for empty cache. * @return stats object * @type Object */ computeStats: function (bCacheFull) { var comps, comp, compType, i, len, size, totalSize, aTypes, canvas_data, sType, hCount = {}, hSize = {}, // hashes where the key is the object type nHttpRequests = 0; if (!this.component_set) { /* need to run peeler first */ return; } comps = this.component_set.components; if (!comps) { return; } // SUMMARY - Find the number and total size for the categories. // Iterate over all the components and add things up. for (i = 0, len = comps.length; i < len; i += 1) { comp = comps[i]; if (!bCacheFull || !comp.hasFarFutureExpiresOrMaxAge()) { // If the object has a far future Expires date it won't add any HTTP requests nor size to the page. // It adds to the HTTP requests (at least a condition GET request). nHttpRequests += 1; compType = comp.type; hCount[compType] = (typeof hCount[compType] === 'undefined' ? 1 : hCount[compType] + 1); size = 0; if (!bCacheFull || !comp.hasOldModifiedDate()) { // If we're doing EMPTY cache stats OR this component is newly modified (so is probably changing). if (comp.compressed === 'gzip' || comp.compressed === 'deflate') { if (comp.size_compressed) { size = parseInt(comp.size_compressed, 10); } } else { size = comp.size; } } hSize[compType] = (typeof hSize[compType] === 'undefined' ? size : hSize[compType] + size); } } totalSize = 0; aTypes = YSLOW.peeler.types; canvas_data = {}; for (i = 0; i < aTypes.length; i += 1) { sType = aTypes[i]; if (typeof hCount[sType] !== "undefined") { // canvas if (hSize[sType] > 0) { canvas_data[sType] = hSize[sType]; } totalSize += hSize[sType]; } } return { 'total_size': totalSize, 'num_requests': nHttpRequests, 'count_obj': hCount, 'size_obj': hSize, 'canvas_data': canvas_data }; }, /** * Collect Statistics of the current page. */ collectStats: function () { var stats = this.computeStats(); if (stats !== undefined) { this.PAGE.totalSize = stats.total_size; this.PAGE.totalRequests = stats.num_requests; this.PAGE.totalObjCount = stats.count_obj; this.PAGE.totalObjSize = stats.size_obj; this.PAGE.canvas_data.empty = stats.canvas_data; } stats = this.computeStats(true); if (stats) { this.PAGE.totalSizePrimed = stats.total_size; this.PAGE.totalRequestsPrimed = stats.num_requests; this.PAGE.totalObjCountPrimed = stats.count_obj; this.PAGE.totalObjSizePrimed = stats.size_obj; this.PAGE.canvas_data.primed = stats.canvas_data; } }, /** * Call registered renderer to generate Grade view with the passed output format. * * @param {String} output_format output format, e.g. 'html', 'xml' * @return Grade in the passed output format. */ genPerformance: function (output_format, doc) { if (this.result_set === null) { if (!doc) { doc = this.document; } YSLOW.controller.lint(doc, this); } return YSLOW.controller.render(output_format, 'reportcard', { 'result_set': this.result_set }); }, /** * Call registered renderer to generate Stats view with the passed output format. * * @param {String} output_format output format, e.g. 'html', 'xml' * @return Stats in the passed output format. */ genStats: function (output_format) { var stats = {}; if (!this.PAGE.totalSize) { // collect stats this.collectStats(); } stats.PAGE = this.PAGE; return YSLOW.controller.render(output_format, 'stats', { 'stats': stats }); }, /** * Call registered renderer to generate Components view with the passed output format. * * @param {String} output_format output format, e.g. 'html', 'xml' * @return Components in the passed output format. */ genComponents: function (output_format) { if (!this.PAGE.totalSize) { // collect stats this.collectStats(); } return YSLOW.controller.render(output_format, 'components', { 'comps': this.component_set.components, 'total_size': this.PAGE.totalSize }); }, /** * Call registered renderer to generate Tools view with the passed output format. * * @param {String} output_format output format, e.g. 'html' * @return Tools in the passed output format. */ genToolsView: function (output_format) { var tools = YSLOW.Tools.getAllTools(); return YSLOW.controller.render(output_format, 'tools', { 'tools': tools }); }, /** * Call registered renderer to generate Ruleset Settings view with the passed output format. * * @param {String} output_format output format, e.g. 'html' * @return Ruleset Settings in the passed output format. */ genRulesetEditView: function (output_format) { return YSLOW.controller.render(output_format, 'rulesetEdit', { 'rulesets': YSLOW.controller.getRegisteredRuleset() }); } }; /** * Copyright (c) 2012, Yahoo! Inc. All rights reserved. * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*global YSLOW*/ /*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */ /** * @namespace YSLOW * @class controller * @static */ YSLOW.controller = { rules: {}, rulesets: {}, onloadTimestamp: null, renderers: {}, default_ruleset_id: 'ydefault', run_pending: 0, /** * Init code. Add event listeners. */ init: function () { var arr_rulesets, i, obj, value; // listen to onload event. YSLOW.util.event.addListener("onload", function (e) { this.onloadTimestamp = e.time; YSLOW.util.setTimer(function () { YSLOW.controller.run_pending_event(); }); }, this); // listen to onunload event. YSLOW.util.event.addListener("onUnload", function (e) { this.run_pending = 0; this.onloadTimestamp = null; }, this); // load custom ruleset arr_rulesets = YSLOW.util.Preference.getPrefList("customRuleset.", undefined); if (arr_rulesets && arr_rulesets.length > 0) { for (i = 0; i < arr_rulesets.length; i += 1) { value = arr_rulesets[i].value; if (typeof value === "string" && value.length > 0) { obj = JSON.parse(value, null); obj.custom = true; this.addRuleset(obj); } } } this.default_ruleset_id = YSLOW.util.Preference.getPref("defaultRuleset", 'ydefault'); // load rule config preference this.loadRulePreference(); }, /** * Run controller to start peeler. Don't start if the page is not done loading. * Delay the running until onload event. * * @param {Window} win window object * @param {YSLOW.context} yscontext YSlow context to use. * @param {Boolean} autorun value to indicate if triggered by autorun */ run: function (win, yscontext, autorun) { var cset, line, doc = win.document; if (!doc || !doc.location || doc.location.href.indexOf("about:") === 0 || "undefined" === typeof doc.location.hostname) { if (!autorun) { line = 'Please enter a valid website address before running YSlow.'; YSLOW.ysview.openDialog(YSLOW.ysview.panel_doc, 389, 150, line, '', 'Ok'); } return; } // Since firebug 1.4, onload event is not passed to YSlow if firebug // panel is not opened. Recommendation from firebug dev team is to // refresh the page before running yslow, which is unnecessary from // yslow point of view. For now, just don't enforce running YSlow // on a page has finished loading. if (!yscontext.PAGE.loaded) { this.run_pending = { 'win': win, 'yscontext': yscontext }; // @todo: put up spining logo to indicate waiting for page finish loading. return; } YSLOW.util.eve