Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2010 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
     32 
     33 WebInspector.AuditRules.CacheableResponseCodes =
     34 {
     35     200: true,
     36     203: true,
     37     206: true,
     38     300: true,
     39     301: true,
     40     410: true,
     41 
     42     304: true // Underlying resource is cacheable
     43 }
     44 
     45 WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, needFullResources)
     46 {
     47     var domainToResourcesMap = {};
     48     for (var i = 0, size = resources.length; i < size; ++i) {
     49         var resource = resources[i];
     50         if (types && types.indexOf(resource.type) === -1)
     51             continue;
     52         var parsedURL = resource.url.asParsedURL();
     53         if (!parsedURL)
     54             continue;
     55         var domain = parsedURL.host;
     56         var domainResources = domainToResourcesMap[domain];
     57         if (domainResources === undefined) {
     58           domainResources = [];
     59           domainToResourcesMap[domain] = domainResources;
     60         }
     61         domainResources.push(needFullResources ? resource : resource.url);
     62     }
     63     return domainToResourcesMap;
     64 }
     65 
     66 WebInspector.AuditRules.GzipRule = function()
     67 {
     68     WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
     69 }
     70 
     71 WebInspector.AuditRules.GzipRule.prototype = {
     72     doRun: function(resources, result, callback)
     73     {
     74         var totalSavings = 0;
     75         var compressedSize = 0;
     76         var candidateSize = 0;
     77         var summary = result.addChild("", true);
     78         for (var i = 0, length = resources.length; i < length; ++i) {
     79             var resource = resources[i];
     80             if (resource.statusCode === 304)
     81                 continue; // Do not test 304 Not Modified resources as their contents are always empty.
     82             if (this._shouldCompress(resource)) {
     83                 var size = resource.resourceSize;
     84                 candidateSize += size;
     85                 if (this._isCompressed(resource)) {
     86                     compressedSize += size;
     87                     continue;
     88                 }
     89                 var savings = 2 * size / 3;
     90                 totalSavings += savings;
     91                 summary.addChild(String.sprintf("%s could save ~%s", WebInspector.AuditRuleResult.linkifyDisplayName(resource.url), Number.bytesToString(savings)));
     92                 result.violationCount++;
     93             }
     94         }
     95         if (!totalSavings)
     96             return callback(null);
     97         summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
     98         callback(result);
     99     },
    100 
    101     _isCompressed: function(resource)
    102     {
    103         var encodingHeader = resource.responseHeaders["Content-Encoding"];
    104         if (!encodingHeader)
    105             return false;
    106 
    107         return /\b(?:gzip|deflate)\b/.test(encodingHeader);
    108     },
    109 
    110     _shouldCompress: function(resource)
    111     {
    112         return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150;
    113     }
    114 }
    115 
    116 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    117 
    118 
    119 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
    120 {
    121     WebInspector.AuditRule.call(this, id, name);
    122     this._type = type;
    123     this._resourceTypeName = resourceTypeName;
    124     this._allowedPerDomain = allowedPerDomain;
    125 }
    126 
    127 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
    128     doRun: function(resources, result, callback)
    129     {
    130         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type]);
    131         var penalizedResourceCount = 0;
    132         // TODO: refactor according to the chosen i18n approach
    133         var summary = result.addChild("", true);
    134         for (var domain in domainToResourcesMap) {
    135             var domainResources = domainToResourcesMap[domain];
    136             var extraResourceCount = domainResources.length - this._allowedPerDomain;
    137             if (extraResourceCount <= 0)
    138                 continue;
    139             penalizedResourceCount += extraResourceCount - 1;
    140             summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
    141             result.violationCount += domainResources.length;
    142         }
    143         if (!penalizedResourceCount)
    144             return callback(null);
    145 
    146         summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
    147         callback(result);
    148     }
    149 }
    150 
    151 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    152 
    153 
    154 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
    155     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain);
    156 }
    157 
    158 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
    159 
    160 
    161 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
    162     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain);
    163 }
    164 
    165 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
    166 
    167 
    168 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
    169     WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
    170     this._hostCountThreshold = hostCountThreshold;
    171 }
    172 
    173 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
    174     doRun: function(resources, result, callback)
    175     {
    176         var summary = result.addChild("");
    177         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined);
    178         for (var domain in domainToResourcesMap) {
    179             if (domainToResourcesMap[domain].length > 1)
    180                 continue;
    181             var parsedURL = domain.asParsedURL();
    182             if (!parsedURL)
    183                 continue;
    184             if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
    185                 continue; // an IP address
    186             summary.addSnippet(match[2]);
    187             result.violationCount++;
    188         }
    189         if (!summary.children || summary.children.length <= this._hostCountThreshold)
    190             return callback(null);
    191 
    192         summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
    193         callback(result);
    194     }
    195 }
    196 
    197 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    198 
    199 
    200 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
    201 {
    202     WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
    203     this._optimalHostnameCount = optimalHostnameCount;
    204     this._minRequestThreshold = minRequestThreshold;
    205     this._minBalanceThreshold = minBalanceThreshold;
    206 }
    207 
    208 
    209 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
    210     doRun: function(resources, result, callback)
    211     {
    212         function hostSorter(a, b)
    213         {
    214             var aCount = domainToResourcesMap[a].length;
    215             var bCount = domainToResourcesMap[b].length;
    216             return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
    217         }
    218 
    219         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
    220             resources,
    221             [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
    222             true);
    223 
    224         var hosts = [];
    225         for (var url in domainToResourcesMap)
    226             hosts.push(url);
    227 
    228         if (!hosts.length)
    229             return callback(null); // no hosts (local file or something)
    230 
    231         hosts.sort(hostSorter);
    232 
    233         var optimalHostnameCount = this._optimalHostnameCount;
    234         if (hosts.length > optimalHostnameCount)
    235             hosts.splice(optimalHostnameCount);
    236 
    237         var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
    238         var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
    239         if (resourceCountAboveThreshold <= 0)
    240             return callback(null);
    241 
    242         var avgResourcesPerHost = 0;
    243         for (var i = 0, size = hosts.length; i < size; ++i)
    244             avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
    245 
    246         // Assume optimal parallelization.
    247         avgResourcesPerHost /= optimalHostnameCount;
    248         avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
    249 
    250         var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;
    251         var minBalanceThreshold = this._minBalanceThreshold;
    252         if (pctAboveAvg < minBalanceThreshold)
    253             return callback(null);
    254 
    255         var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
    256         var entry = result.addChild(String.sprintf("This page makes %d parallelizable requests to %s. Increase download parallelization by distributing the following requests across multiple hostnames.", busiestHostResourceCount, hosts[0]), true);
    257         for (var i = 0; i < resourcesOnBusiestHost.length; ++i)
    258             entry.addURL(resourcesOnBusiestHost[i].url);
    259 
    260         result.violationCount = resourcesOnBusiestHost.length;
    261         callback(result);
    262     }
    263 }
    264 
    265 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    266 
    267 
    268 // The reported CSS rule size is incorrect (parsed != original in WebKit),
    269 // so use percentages instead, which gives a better approximation.
    270 WebInspector.AuditRules.UnusedCssRule = function()
    271 {
    272     WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
    273 }
    274 
    275 WebInspector.AuditRules.UnusedCssRule.prototype = {
    276     doRun: function(resources, result, callback)
    277     {
    278         var self = this;
    279 
    280         function evalCallback(styleSheets) {
    281             if (!styleSheets.length)
    282                 return callback(null);
    283 
    284             var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/;
    285             var selectors = [];
    286             var testedSelectors = {};
    287             for (var i = 0; i < styleSheets.length; ++i) {
    288                 var styleSheet = styleSheets[i];
    289                 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
    290                     var selectorText = styleSheet.rules[curRule].selectorText;
    291                     if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText])
    292                         continue;
    293                     selectors.push(selectorText);
    294                     testedSelectors[selectorText] = 1;
    295                 }
    296             }
    297 
    298             function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
    299             {
    300                 var inlineBlockOrdinal = 0;
    301                 var totalStylesheetSize = 0;
    302                 var totalUnusedStylesheetSize = 0;
    303                 var summary;
    304 
    305                 for (var i = 0; i < styleSheets.length; ++i) {
    306                     var styleSheet = styleSheets[i];
    307                     var stylesheetSize = 0;
    308                     var unusedStylesheetSize = 0;
    309                     var unusedRules = [];
    310                     for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
    311                         var rule = styleSheet.rules[curRule];
    312                         // Exact computation whenever source ranges are available.
    313                         var textLength = (rule.selectorRange && rule.style.range && rule.style.range.end) ? rule.style.range.end - rule.selectorRange.start + 1 : 0;
    314                         if (!textLength && rule.style.cssText)
    315                             textLength = rule.style.cssText.length + rule.selectorText.length;
    316                         stylesheetSize += textLength;
    317                         if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
    318                             continue;
    319                         unusedStylesheetSize += textLength;
    320                         unusedRules.push(rule.selectorText);
    321                     }
    322                     totalStylesheetSize += stylesheetSize;
    323                     totalUnusedStylesheetSize += unusedStylesheetSize;
    324 
    325                     if (!unusedRules.length)
    326                         continue;
    327 
    328                     var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
    329                     var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document;
    330                     var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
    331                     var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize);
    332                     if (!summary)
    333                         summary = result.addChild("", true);
    334                     var entry = summary.addChild(String.sprintf("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused));
    335 
    336                     for (var j = 0; j < unusedRules.length; ++j)
    337                         entry.addSnippet(unusedRules[j]);
    338 
    339                     result.violationCount += unusedRules.length;
    340                 }
    341 
    342                 if (!totalUnusedStylesheetSize)
    343                     return callback(null);
    344 
    345                 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
    346                 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent);
    347 
    348                 callback(result);
    349             }
    350 
    351             var foundSelectors = {};
    352             function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId)
    353             {
    354                 if (nodeId)
    355                     foundSelectors[selector] = true;
    356                 if (boundSelectorsCallback)
    357                     boundSelectorsCallback(foundSelectors);
    358             }
    359 
    360             function documentLoaded(selectors, document) {
    361                 for (var i = 0; i < selectors.length; ++i)
    362                     WebInspector.domAgent.querySelector(document.id, selectors[i], queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, callback, styleSheets, testedSelectors) : null, selectors[i], styleSheets, testedSelectors));
    363             }
    364 
    365             WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
    366         }
    367 
    368         function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
    369         {
    370             if (styleSheet) {
    371                 styleSheet.sourceURL = sourceURL;
    372                 styleSheets.push(styleSheet);
    373             }
    374             if (continuation)
    375                 continuation(styleSheets);
    376         }
    377 
    378         function allStylesCallback(error, styleSheetInfos)
    379         {
    380             if (error || !styleSheetInfos || !styleSheetInfos.length)
    381                 return evalCallback([]);
    382             var styleSheets = [];
    383             for (var i = 0; i < styleSheetInfos.length; ++i) {
    384                 var info = styleSheetInfos[i];
    385                 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
    386             }
    387         }
    388 
    389         CSSAgent.getAllStyleSheets(allStylesCallback);
    390     }
    391 }
    392 
    393 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    394 
    395 
    396 WebInspector.AuditRules.CacheControlRule = function(id, name)
    397 {
    398     WebInspector.AuditRule.call(this, id, name);
    399 }
    400 
    401 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
    402 
    403 WebInspector.AuditRules.CacheControlRule.prototype = {
    404 
    405     doRun: function(resources, result, callback)
    406     {
    407         var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
    408         if (cacheableAndNonCacheableResources[0].length)
    409             this.runChecks(cacheableAndNonCacheableResources[0], result);
    410         this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
    411 
    412         callback(result);
    413     },
    414 
    415     handleNonCacheableResources: function()
    416     {
    417     },
    418 
    419     _cacheableAndNonCacheableResources: function(resources)
    420     {
    421         var processedResources = [[], []];
    422         for (var i = 0; i < resources.length; ++i) {
    423             var resource = resources[i];
    424             if (!this.isCacheableResource(resource))
    425                 continue;
    426             if (this._isExplicitlyNonCacheable(resource))
    427                 processedResources[1].push(resource);
    428             else
    429                 processedResources[0].push(resource);
    430         }
    431         return processedResources;
    432     },
    433 
    434     execCheck: function(messageText, resourceCheckFunction, resources, result)
    435     {
    436         var resourceCount = resources.length;
    437         var urls = [];
    438         for (var i = 0; i < resourceCount; ++i) {
    439             if (resourceCheckFunction.call(this, resources[i]))
    440                 urls.push(resources[i].url);
    441         }
    442         if (urls.length) {
    443             var entry = result.addChild(messageText, true);
    444             entry.addURLs(urls);
    445             result.violationCount += urls.length;
    446         }
    447     },
    448 
    449     freshnessLifetimeGreaterThan: function(resource, timeMs)
    450     {
    451         var dateHeader = this.responseHeader(resource, "Date");
    452         if (!dateHeader)
    453             return false;
    454 
    455         var dateHeaderMs = Date.parse(dateHeader);
    456         if (isNaN(dateHeaderMs))
    457             return false;
    458 
    459         var freshnessLifetimeMs;
    460         var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");
    461 
    462         if (maxAgeMatch)
    463             freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
    464         else {
    465             var expiresHeader = this.responseHeader(resource, "Expires");
    466             if (expiresHeader) {
    467                 var expDate = Date.parse(expiresHeader);
    468                 if (!isNaN(expDate))
    469                     freshnessLifetimeMs = expDate - dateHeaderMs;
    470             }
    471         }
    472 
    473         return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
    474     },
    475 
    476     responseHeader: function(resource, header)
    477     {
    478         return resource.responseHeaders[header];
    479     },
    480 
    481     hasResponseHeader: function(resource, header)
    482     {
    483         return resource.responseHeaders[header] !== undefined;
    484     },
    485 
    486     isCompressible: function(resource)
    487     {
    488         return WebInspector.Resource.Type.isTextType(resource.type);
    489     },
    490 
    491     isPubliclyCacheable: function(resource)
    492     {
    493         if (this._isExplicitlyNonCacheable(resource))
    494             return false;
    495 
    496         if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
    497             return true;
    498 
    499         return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
    500     },
    501 
    502     responseHeaderMatch: function(resource, header, regexp)
    503     {
    504         return resource.responseHeaders[header]
    505             ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
    506             : undefined;
    507     },
    508 
    509     hasExplicitExpiration: function(resource)
    510     {
    511         return this.hasResponseHeader(resource, "Date") &&
    512             (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
    513     },
    514 
    515     _isExplicitlyNonCacheable: function(resource)
    516     {
    517         var hasExplicitExp = this.hasExplicitExpiration(resource);
    518         return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
    519             this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
    520             (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
    521             (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
    522             (!hasExplicitExp && !this.isCacheableResource(resource));
    523     },
    524 
    525     isCacheableResource: function(resource)
    526     {
    527         return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
    528     }
    529 }
    530 
    531 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    532 
    533 
    534 WebInspector.AuditRules.BrowserCacheControlRule = function()
    535 {
    536     WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
    537 }
    538 
    539 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
    540     handleNonCacheableResources: function(resources, result)
    541     {
    542         if (resources.length) {
    543             var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
    544             result.violationCount += resources.length;
    545             for (var i = 0; i < resources.length; ++i)
    546                 entry.addURL(resources[i].url);
    547         }
    548     },
    549 
    550     runChecks: function(resources, result, callback)
    551     {
    552         this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
    553             this._missingExpirationCheck, resources, result);
    554         this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
    555             this._varyCheck, resources, result);
    556         this.execCheck("The following cacheable resources have a short freshness lifetime:",
    557             this._oneMonthExpirationCheck, resources, result);
    558 
    559         // Unable to implement the favicon check due to the WebKit limitations.
    560         this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
    561             this._oneYearExpirationCheck, resources, result);
    562     },
    563 
    564     _missingExpirationCheck: function(resource)
    565     {
    566         return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
    567     },
    568 
    569     _varyCheck: function(resource)
    570     {
    571         var varyHeader = this.responseHeader(resource, "Vary");
    572         if (varyHeader) {
    573             varyHeader = varyHeader.replace(/User-Agent/gi, "");
    574             varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
    575             varyHeader = varyHeader.replace(/[, ]*/g, "");
    576         }
    577         return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
    578     },
    579 
    580     _oneMonthExpirationCheck: function(resource)
    581     {
    582         return this.isCacheableResource(resource) &&
    583             !this.hasResponseHeader(resource, "Set-Cookie") &&
    584             !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
    585             this.freshnessLifetimeGreaterThan(resource, 0);
    586     },
    587 
    588     _oneYearExpirationCheck: function(resource)
    589     {
    590         return this.isCacheableResource(resource) &&
    591             !this.hasResponseHeader(resource, "Set-Cookie") &&
    592             !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
    593             this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
    594     }
    595 }
    596 
    597 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
    598 
    599 
    600 WebInspector.AuditRules.ProxyCacheControlRule = function() {
    601     WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
    602 }
    603 
    604 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
    605     runChecks: function(resources, result, callback)
    606     {
    607         this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
    608             this._questionMarkCheck, resources, result);
    609         this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
    610             this._publicCachingCheck, resources, result);
    611         this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
    612             this._setCookieCacheableCheck, resources, result);
    613     },
    614 
    615     _questionMarkCheck: function(resource)
    616     {
    617         return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
    618     },
    619 
    620     _publicCachingCheck: function(resource)
    621     {
    622         return this.isCacheableResource(resource) &&
    623             !this.isCompressible(resource) &&
    624             !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
    625             !this.hasResponseHeader(resource, "Set-Cookie");
    626     },
    627 
    628     _setCookieCacheableCheck: function(resource)
    629     {
    630         return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
    631     }
    632 }
    633 
    634 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
    635 
    636 
    637 WebInspector.AuditRules.ImageDimensionsRule = function()
    638 {
    639     WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
    640 }
    641 
    642 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
    643     doRun: function(resources, result, callback)
    644     {
    645         var urlToNoDimensionCount = {};
    646 
    647         function doneCallback()
    648         {
    649             for (var url in urlToNoDimensionCount) {
    650                 var entry = entry || result.addChild("A width and height should be specified for all images in order to speed up page display. The following image(s) are missing a width and/or height:", true);
    651                 var value = WebInspector.AuditRuleResult.linkifyDisplayName(url);
    652                 if (urlToNoDimensionCount[url] > 1)
    653                     value += String.sprintf(" (%d uses)", urlToNoDimensionCount[url]);
    654                 entry.addChild(value);
    655                 result.violationCount++;
    656             }
    657             callback(entry ? result : null);
    658         }
    659 
    660         function imageStylesReady(imageId, lastCall, styles)
    661         {
    662             const node = WebInspector.domAgent.nodeForId(imageId);
    663             var src = node.getAttribute("src");
    664             if (!src.asParsedURL()) {
    665                 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
    666                     if (frameOwnerCandidate.documentURL) {
    667                         var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src);
    668                         break;
    669                     }
    670                 }
    671             }
    672             if (completeSrc)
    673                 src = completeSrc;
    674 
    675             const computedStyle = styles.computedStyle;
    676             if (computedStyle.getPropertyValue("position") === "absolute") {
    677                 if (lastCall)
    678                     doneCallback();
    679                 return;
    680             }
    681 
    682             var widthFound = "width" in styles.styleAttributes;
    683             var heightFound = "height" in styles.styleAttributes;
    684 
    685             var inlineStyle = styles.inlineStyle;
    686             if (inlineStyle) {
    687                 if (inlineStyle.getPropertyValue("width") !== "")
    688                     widthFound = true;
    689                 if (inlineStyle.getPropertyValue("height") !== "")
    690                     heightFound = true;
    691             }
    692 
    693             for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
    694                 var style = styles.matchedCSSRules[i].style;
    695                 if (style.getPropertyValue("width") !== "")
    696                     widthFound = true;
    697                 if (style.getPropertyValue("height") !== "")
    698                     heightFound = true;
    699             }
    700 
    701             if (!widthFound || !heightFound) {
    702                 if (src in urlToNoDimensionCount)
    703                     ++urlToNoDimensionCount[src];
    704                 else
    705                     urlToNoDimensionCount[src] = 1;
    706             }
    707 
    708             if (lastCall)
    709                 doneCallback();
    710         }
    711 
    712         function getStyles(nodeIds)
    713         {
    714             if (!nodeIds) {
    715                 console.error("Failed to get styles");
    716                 return;
    717             }
    718             for (var i = 0; i < nodeIds.length; ++i)
    719                 WebInspector.cssModel.getStylesAsync(nodeIds[i], imageStylesReady.bind(this, nodeIds[i], i === nodeIds.length - 1));
    720         }
    721 
    722         function onDocumentAvailable(root)
    723         {
    724             WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
    725         }
    726 
    727         WebInspector.domAgent.requestDocument(onDocumentAvailable);
    728     }
    729 }
    730 
    731 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    732 
    733 
    734 WebInspector.AuditRules.CssInHeadRule = function()
    735 {
    736     WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
    737 }
    738 
    739 WebInspector.AuditRules.CssInHeadRule.prototype = {
    740     doRun: function(resources, result, callback)
    741     {
    742         function evalCallback(evalResult)
    743         {
    744             if (!evalResult)
    745                 return callback(null);
    746 
    747             var summary = result.addChild("");
    748 
    749             var outputMessages = [];
    750             for (var url in evalResult) {
    751                 var urlViolations = evalResult[url];
    752                 if (urlViolations[0]) {
    753                     result.addChild(String.sprintf("%s style block(s) in the %s body should be moved to the document head.", urlViolations[0], WebInspector.AuditRuleResult.linkifyDisplayName(url)));
    754                     result.violationCount += urlViolations[0];
    755                 }
    756                 for (var i = 0; i < urlViolations[1].length; ++i)
    757                     result.addChild(String.sprintf("Link node %s should be moved to the document head in %s", WebInspector.AuditRuleResult.linkifyDisplayName(urlViolations[1][i]), WebInspector.AuditRuleResult.linkifyDisplayName(url)));
    758                 result.violationCount += urlViolations[1].length;
    759             }
    760             summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
    761             callback(result);
    762         }
    763 
    764         function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
    765         {
    766             if (!nodeIds) {
    767                 callback(null);
    768                 return;
    769             }
    770 
    771             var externalStylesheetNodeIds = nodeIds;
    772             var result = null;
    773             if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
    774                 var urlToViolationsArray = {};
    775                 var externalStylesheetHrefs = [];
    776                 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
    777                     var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
    778                     var completeHref = WebInspector.completeURL(linkNode.ownerDocument.documentURL, linkNode.getAttribute("href"));
    779                     externalStylesheetHrefs.push(completeHref || "<empty>");
    780                 }
    781                 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
    782                 result = urlToViolationsArray;
    783             }
    784             evalCallback(result);
    785         }
    786 
    787         function inlineStylesReceived(root, nodeIds)
    788         {
    789             if (!nodeIds) {
    790                 callback(null);
    791                 return;
    792             }
    793 
    794             WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
    795         }
    796 
    797         function onDocumentAvailable(root)
    798         {
    799             WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
    800         }
    801 
    802         WebInspector.domAgent.requestDocument(onDocumentAvailable);
    803     }
    804 }
    805 
    806 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    807 
    808 
    809 WebInspector.AuditRules.StylesScriptsOrderRule = function()
    810 {
    811     WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
    812 }
    813 
    814 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
    815     doRun: function(resources, result, callback)
    816     {
    817         function evalCallback(resultValue)
    818         {
    819             if (!resultValue)
    820                 return callback(null);
    821 
    822             var lateCssUrls = resultValue[0];
    823             var cssBeforeInlineCount = resultValue[1];
    824 
    825             var entry = result.addChild("The following external CSS files were included after an external JavaScript file in the document head. To ensure CSS files are downloaded in parallel, always include external CSS before external JavaScript.", true);
    826             entry.addURLs(lateCssUrls);
    827             result.violationCount += lateCssUrls.length;
    828 
    829             if (cssBeforeInlineCount) {
    830                 result.addChild(String.sprintf(" %d inline script block%s found in the head between an external CSS file and another resource. To allow parallel downloading, move the inline script before the external CSS file, or after the next resource.", cssBeforeInlineCount, cssBeforeInlineCount > 1 ? "s were" : " was"));
    831                 result.violationCount += cssBeforeInlineCount;
    832             }
    833             callback(result);
    834         }
    835 
    836         function cssBeforeInlineReceived(lateStyleIds, nodeIds)
    837         {
    838             if (!nodeIds) {
    839                 callback(null);
    840                 return;
    841             }
    842 
    843             var cssBeforeInlineCount = nodeIds.length;
    844             var result = null;
    845             if (lateStyleIds.length || cssBeforeInlineCount) {
    846                 var lateStyleUrls = [];
    847                 for (var i = 0; i < lateStyleIds.length; ++i) {
    848                     var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
    849                     var completeHref = WebInspector.completeURL(lateStyleNode.ownerDocument.documentURL, lateStyleNode.getAttribute("href"));
    850                     lateStyleUrls.push(completeHref || "<empty>");
    851                 }
    852                 result = [ lateStyleUrls, cssBeforeInlineCount ];
    853             }
    854 
    855             evalCallback(result);
    856         }
    857 
    858         function lateStylesReceived(root, nodeIds)
    859         {
    860             if (!nodeIds) {
    861                 callback(null);
    862                 return;
    863             }
    864 
    865             WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
    866         }
    867 
    868         function onDocumentAvailable(root)
    869         {
    870             WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
    871         }
    872 
    873         WebInspector.domAgent.requestDocument(onDocumentAvailable);
    874     }
    875 }
    876 
    877 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
    878 
    879 
    880 WebInspector.AuditRules.CookieRuleBase = function(id, name)
    881 {
    882     WebInspector.AuditRule.call(this, id, name);
    883 }
    884 
    885 WebInspector.AuditRules.CookieRuleBase.prototype = {
    886     doRun: function(resources, result, callback)
    887     {
    888         var self = this;
    889         function resultCallback(receivedCookies, isAdvanced) {
    890             self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
    891             callback(result);
    892         }
    893         WebInspector.Cookies.getCookiesAsync(resultCallback);
    894     },
    895 
    896     mapResourceCookies: function(resourcesByDomain, allCookies, callback)
    897     {
    898         for (var i = 0; i < allCookies.length; ++i) {
    899             for (var resourceDomain in resourcesByDomain) {
    900                 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
    901                     this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
    902             }
    903         }
    904     },
    905 
    906     _callbackForResourceCookiePairs: function(resources, cookie, callback)
    907     {
    908         if (!resources)
    909             return;
    910         for (var i = 0; i < resources.length; ++i) {
    911             if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
    912                 callback(resources[i], cookie);
    913         }
    914     }
    915 }
    916 
    917 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;
    918 
    919 
    920 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
    921 {
    922     WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
    923     this._avgBytesThreshold = avgBytesThreshold;
    924     this._maxBytesThreshold = 1000;
    925 }
    926 
    927 WebInspector.AuditRules.CookieSizeRule.prototype = {
    928     _average: function(cookieArray)
    929     {
    930         var total = 0;
    931         for (var i = 0; i < cookieArray.length; ++i)
    932             total += cookieArray[i].size;
    933         return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
    934     },
    935 
    936     _max: function(cookieArray)
    937     {
    938         var result = 0;
    939         for (var i = 0; i < cookieArray.length; ++i)
    940             result = Math.max(cookieArray[i].size, result);
    941         return result;
    942     },
    943 
    944     processCookies: function(allCookies, resources, result)
    945     {
    946         function maxSizeSorter(a, b)
    947         {
    948             return b.maxCookieSize - a.maxCookieSize;
    949         }
    950 
    951         function avgSizeSorter(a, b)
    952         {
    953             return b.avgCookieSize - a.avgCookieSize;
    954         }
    955 
    956         var cookiesPerResourceDomain = {};
    957 
    958         function collectorCallback(resource, cookie)
    959         {
    960             var cookies = cookiesPerResourceDomain[resource.domain];
    961             if (!cookies) {
    962                 cookies = [];
    963                 cookiesPerResourceDomain[resource.domain] = cookies;
    964             }
    965             cookies.push(cookie);
    966         }
    967 
    968         if (!allCookies.length)
    969             return;
    970 
    971         var sortedCookieSizes = [];
    972 
    973         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
    974                 null,
    975                 true);
    976         var matchingResourceData = {};
    977         this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
    978 
    979         for (var resourceDomain in cookiesPerResourceDomain) {
    980             var cookies = cookiesPerResourceDomain[resourceDomain];
    981             sortedCookieSizes.push({
    982                 domain: resourceDomain,
    983                 avgCookieSize: this._average(cookies),
    984                 maxCookieSize: this._max(cookies)
    985             });
    986         }
    987         var avgAllCookiesSize = this._average(allCookies);
    988 
    989         var hugeCookieDomains = [];
    990         sortedCookieSizes.sort(maxSizeSorter);
    991 
    992         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
    993             var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
    994             if (maxCookieSize > this._maxBytesThreshold)
    995                 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
    996         }
    997 
    998         var bigAvgCookieDomains = [];
    999         sortedCookieSizes.sort(avgSizeSorter);
   1000         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
   1001             var domain = sortedCookieSizes[i].domain;
   1002             var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
   1003             if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
   1004                 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
   1005         }
   1006         result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
   1007 
   1008         var message;
   1009         if (hugeCookieDomains.length) {
   1010             var entry = result.addChild("The following domains have a cookie size in excess of 1KB. This is harmful because requests with cookies larger than 1KB typically cannot fit into a single network packet.", true);
   1011             entry.addURLs(hugeCookieDomains);
   1012             result.violationCount += hugeCookieDomains.length;
   1013         }
   1014 
   1015         if (bigAvgCookieDomains.length) {
   1016             var entry = result.addChild(String.sprintf("The following domains have an average cookie size in excess of %d bytes. Reducing the size of cookies for these domains can reduce the time it takes to send requests.", this._avgBytesThreshold), true);
   1017             entry.addURLs(bigAvgCookieDomains);
   1018             result.violationCount += bigAvgCookieDomains.length;
   1019         }
   1020     }
   1021 }
   1022 
   1023 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
   1024 
   1025 
   1026 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
   1027 {
   1028     WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
   1029     this._minResources = minResources;
   1030 }
   1031 
   1032 WebInspector.AuditRules.StaticCookielessRule.prototype = {
   1033     processCookies: function(allCookies, resources, result)
   1034     {
   1035         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
   1036                 [WebInspector.Resource.Type.Stylesheet,
   1037                  WebInspector.Resource.Type.Image],
   1038                 true);
   1039         var totalStaticResources = 0;
   1040         for (var domain in domainToResourcesMap)
   1041             totalStaticResources += domainToResourcesMap[domain].length;
   1042         if (totalStaticResources < this._minResources)
   1043             return;
   1044         var matchingResourceData = {};
   1045         this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
   1046 
   1047         var badUrls = [];
   1048         var cookieBytes = 0;
   1049         for (var url in matchingResourceData) {
   1050             badUrls.push(url);
   1051             cookieBytes += matchingResourceData[url]
   1052         }
   1053         if (badUrls.length < this._minResources)
   1054             return;
   1055 
   1056         var entry = result.addChild(String.sprintf("%s of cookies were sent with the following static resources. Serve these static resources from a domain that does not set cookies:", Number.bytesToString(cookieBytes)), true);
   1057         entry.addURLs(badUrls);
   1058         result.violationCount = badUrls.length;
   1059     },
   1060 
   1061     _collectorCallback: function(matchingResourceData, resource, cookie)
   1062     {
   1063         matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
   1064     }
   1065 }
   1066 
   1067 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
   1068