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 request is cacheable
     43 }
     44 
     45 /**
     46  * @param {!Array.<!WebInspector.NetworkRequest>} requests
     47  * @param {Array.<!WebInspector.resourceTypes>} types
     48  * @param {boolean} needFullResources
     49  * @return {(Object.<string, !Array.<!WebInspector.NetworkRequest>>|Object.<string, !Array.<string>>)}
     50  */
     51 WebInspector.AuditRules.getDomainToResourcesMap = function(requests, types, needFullResources)
     52 {
     53     var domainToResourcesMap = {};
     54     for (var i = 0, size = requests.length; i < size; ++i) {
     55         var request = requests[i];
     56         if (types && types.indexOf(request.type) === -1)
     57             continue;
     58         var parsedURL = request.url.asParsedURL();
     59         if (!parsedURL)
     60             continue;
     61         var domain = parsedURL.host;
     62         var domainResources = domainToResourcesMap[domain];
     63         if (domainResources === undefined) {
     64           domainResources = [];
     65           domainToResourcesMap[domain] = domainResources;
     66         }
     67         domainResources.push(needFullResources ? request : request.url);
     68     }
     69     return domainToResourcesMap;
     70 }
     71 
     72 /**
     73  * @constructor
     74  * @extends {WebInspector.AuditRule}
     75  */
     76 WebInspector.AuditRules.GzipRule = function()
     77 {
     78     WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
     79 }
     80 
     81 WebInspector.AuditRules.GzipRule.prototype = {
     82     /**
     83      * @param {!Array.<!WebInspector.NetworkRequest>} requests
     84      * @param {!WebInspector.AuditRuleResult} result
     85      * @param {function(WebInspector.AuditRuleResult)} callback
     86      * @param {!WebInspector.Progress} progress
     87      */
     88     doRun: function(requests, result, callback, progress)
     89     {
     90         var totalSavings = 0;
     91         var compressedSize = 0;
     92         var candidateSize = 0;
     93         var summary = result.addChild("", true);
     94         for (var i = 0, length = requests.length; i < length; ++i) {
     95             var request = requests[i];
     96             if (request.statusCode === 304)
     97                 continue; // Do not test 304 Not Modified requests as their contents are always empty.
     98             if (this._shouldCompress(request)) {
     99                 var size = request.resourceSize;
    100                 candidateSize += size;
    101                 if (this._isCompressed(request)) {
    102                     compressedSize += size;
    103                     continue;
    104                 }
    105                 var savings = 2 * size / 3;
    106                 totalSavings += savings;
    107                 summary.addFormatted("%r could save ~%s", request.url, Number.bytesToString(savings));
    108                 result.violationCount++;
    109             }
    110         }
    111         if (!totalSavings)
    112             return callback(null);
    113         summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
    114         callback(result);
    115     },
    116 
    117     _isCompressed: function(request)
    118     {
    119         var encodingHeader = request.responseHeaderValue("Content-Encoding");
    120         if (!encodingHeader)
    121             return false;
    122 
    123         return /\b(?:gzip|deflate)\b/.test(encodingHeader);
    124     },
    125 
    126     _shouldCompress: function(request)
    127     {
    128         return request.type.isTextType() && request.parsedURL.host && request.resourceSize !== undefined && request.resourceSize > 150;
    129     },
    130 
    131     __proto__: WebInspector.AuditRule.prototype
    132 }
    133 
    134 /**
    135  * @constructor
    136  * @extends {WebInspector.AuditRule}
    137  */
    138 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
    139 {
    140     WebInspector.AuditRule.call(this, id, name);
    141     this._type = type;
    142     this._resourceTypeName = resourceTypeName;
    143     this._allowedPerDomain = allowedPerDomain;
    144 }
    145 
    146 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
    147     /**
    148      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    149      * @param {!WebInspector.AuditRuleResult} result
    150      * @param {function(WebInspector.AuditRuleResult)} callback
    151      * @param {!WebInspector.Progress} progress
    152      */
    153     doRun: function(requests, result, callback, progress)
    154     {
    155         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, [this._type], false);
    156         var penalizedResourceCount = 0;
    157         // TODO: refactor according to the chosen i18n approach
    158         var summary = result.addChild("", true);
    159         for (var domain in domainToResourcesMap) {
    160             var domainResources = domainToResourcesMap[domain];
    161             var extraResourceCount = domainResources.length - this._allowedPerDomain;
    162             if (extraResourceCount <= 0)
    163                 continue;
    164             penalizedResourceCount += extraResourceCount - 1;
    165             summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
    166             result.violationCount += domainResources.length;
    167         }
    168         if (!penalizedResourceCount)
    169             return callback(null);
    170 
    171         summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
    172         callback(result);
    173     },
    174 
    175     __proto__: WebInspector.AuditRule.prototype
    176 }
    177 
    178 /**
    179  * @constructor
    180  * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
    181  */
    182 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
    183     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.resourceTypes.Script, "JavaScript", allowedPerDomain);
    184 }
    185 
    186 WebInspector.AuditRules.CombineJsResourcesRule.prototype = {
    187     __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
    188 }
    189 
    190 /**
    191  * @constructor
    192  * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
    193  */
    194 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
    195     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.resourceTypes.Stylesheet, "CSS", allowedPerDomain);
    196 }
    197 
    198 WebInspector.AuditRules.CombineCssResourcesRule.prototype = {
    199     __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
    200 }
    201 
    202 /**
    203  * @constructor
    204  * @extends {WebInspector.AuditRule}
    205  */
    206 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
    207     WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
    208     this._hostCountThreshold = hostCountThreshold;
    209 }
    210 
    211 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
    212     /**
    213      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    214      * @param {!WebInspector.AuditRuleResult} result
    215      * @param {function(WebInspector.AuditRuleResult)} callback
    216      * @param {!WebInspector.Progress} progress
    217      */
    218     doRun: function(requests, result, callback, progress)
    219     {
    220         var summary = result.addChild("");
    221         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, null, false);
    222         for (var domain in domainToResourcesMap) {
    223             if (domainToResourcesMap[domain].length > 1)
    224                 continue;
    225             var parsedURL = domain.asParsedURL();
    226             if (!parsedURL)
    227                 continue;
    228             if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
    229                 continue; // an IP address
    230             summary.addSnippet(domain);
    231             result.violationCount++;
    232         }
    233         if (!summary.children || summary.children.length <= this._hostCountThreshold)
    234             return callback(null);
    235 
    236         summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
    237         callback(result);
    238     },
    239 
    240     __proto__: WebInspector.AuditRule.prototype
    241 }
    242 
    243 /**
    244  * @constructor
    245  * @extends {WebInspector.AuditRule}
    246  */
    247 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
    248 {
    249     WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
    250     this._optimalHostnameCount = optimalHostnameCount;
    251     this._minRequestThreshold = minRequestThreshold;
    252     this._minBalanceThreshold = minBalanceThreshold;
    253 }
    254 
    255 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
    256     /**
    257      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    258      * @param {!WebInspector.AuditRuleResult} result
    259      * @param {function(WebInspector.AuditRuleResult)} callback
    260      * @param {!WebInspector.Progress} progress
    261      */
    262     doRun: function(requests, result, callback, progress)
    263     {
    264         function hostSorter(a, b)
    265         {
    266             var aCount = domainToResourcesMap[a].length;
    267             var bCount = domainToResourcesMap[b].length;
    268             return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
    269         }
    270 
    271         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
    272             requests,
    273             [WebInspector.resourceTypes.Stylesheet, WebInspector.resourceTypes.Image],
    274             true);
    275 
    276         var hosts = [];
    277         for (var url in domainToResourcesMap)
    278             hosts.push(url);
    279 
    280         if (!hosts.length)
    281             return callback(null); // no hosts (local file or something)
    282 
    283         hosts.sort(hostSorter);
    284 
    285         var optimalHostnameCount = this._optimalHostnameCount;
    286         if (hosts.length > optimalHostnameCount)
    287             hosts.splice(optimalHostnameCount);
    288 
    289         var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
    290         var requestCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
    291         if (requestCountAboveThreshold <= 0)
    292             return callback(null);
    293 
    294         var avgResourcesPerHost = 0;
    295         for (var i = 0, size = hosts.length; i < size; ++i)
    296             avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
    297 
    298         // Assume optimal parallelization.
    299         avgResourcesPerHost /= optimalHostnameCount;
    300         avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
    301 
    302         var pctAboveAvg = (requestCountAboveThreshold / avgResourcesPerHost) - 1.0;
    303         var minBalanceThreshold = this._minBalanceThreshold;
    304         if (pctAboveAvg < minBalanceThreshold)
    305             return callback(null);
    306 
    307         var requestsOnBusiestHost = domainToResourcesMap[hosts[0]];
    308         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);
    309         for (var i = 0; i < requestsOnBusiestHost.length; ++i)
    310             entry.addURL(requestsOnBusiestHost[i].url);
    311 
    312         result.violationCount = requestsOnBusiestHost.length;
    313         callback(result);
    314     },
    315 
    316     __proto__: WebInspector.AuditRule.prototype
    317 }
    318 
    319 /**
    320  * The reported CSS rule size is incorrect (parsed != original in WebKit),
    321  * so use percentages instead, which gives a better approximation.
    322  * @constructor
    323  * @extends {WebInspector.AuditRule}
    324  */
    325 WebInspector.AuditRules.UnusedCssRule = function()
    326 {
    327     WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
    328 }
    329 
    330 WebInspector.AuditRules.UnusedCssRule.prototype = {
    331     /**
    332      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    333      * @param {!WebInspector.AuditRuleResult} result
    334      * @param {function(WebInspector.AuditRuleResult)} callback
    335      * @param {!WebInspector.Progress} progress
    336      */
    337     doRun: function(requests, result, callback, progress)
    338     {
    339         var self = this;
    340 
    341         function evalCallback(styleSheets) {
    342             if (progress.isCanceled())
    343                 return;
    344 
    345             if (!styleSheets.length)
    346                 return callback(null);
    347 
    348             var selectors = [];
    349             var testedSelectors = {};
    350             for (var i = 0; i < styleSheets.length; ++i) {
    351                 var styleSheet = styleSheets[i];
    352                 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
    353                     var selectorText = styleSheet.rules[curRule].selectorText;
    354                     if (testedSelectors[selectorText])
    355                         continue;
    356                     selectors.push(selectorText);
    357                     testedSelectors[selectorText] = 1;
    358                 }
    359             }
    360 
    361             function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
    362             {
    363                 if (progress.isCanceled())
    364                     return;
    365 
    366                 var inlineBlockOrdinal = 0;
    367                 var totalStylesheetSize = 0;
    368                 var totalUnusedStylesheetSize = 0;
    369                 var summary;
    370 
    371                 for (var i = 0; i < styleSheets.length; ++i) {
    372                     var styleSheet = styleSheets[i];
    373                     var unusedRules = [];
    374                     for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
    375                         var rule = styleSheet.rules[curRule];
    376                         if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
    377                             continue;
    378                         unusedRules.push(rule.selectorText);
    379                     }
    380                     totalStylesheetSize += styleSheet.rules.length;
    381                     totalUnusedStylesheetSize += unusedRules.length;
    382 
    383                     if (!unusedRules.length)
    384                         continue;
    385 
    386                     var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
    387                     var isInlineBlock = resource && resource.request && resource.request.type == WebInspector.resourceTypes.Document;
    388                     var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
    389                     var pctUnused = Math.round(100 * unusedRules.length / styleSheet.rules.length);
    390                     if (!summary)
    391                         summary = result.addChild("", true);
    392                     var entry = summary.addFormatted("%s: %d% is not used by the current page.", url, pctUnused);
    393 
    394                     for (var j = 0; j < unusedRules.length; ++j)
    395                         entry.addSnippet(unusedRules[j]);
    396 
    397                     result.violationCount += unusedRules.length;
    398                 }
    399 
    400                 if (!totalUnusedStylesheetSize)
    401                     return callback(null);
    402 
    403                 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
    404                 summary.value = String.sprintf("%s rules (%d%) of CSS not used by the current page.", totalUnusedStylesheetSize, totalUnusedPercent);
    405 
    406                 callback(result);
    407             }
    408 
    409             var foundSelectors = {};
    410             function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId)
    411             {
    412                 if (nodeId)
    413                     foundSelectors[selector] = true;
    414                 if (boundSelectorsCallback)
    415                     boundSelectorsCallback(foundSelectors);
    416             }
    417 
    418             function documentLoaded(selectors, document) {
    419                 var pseudoSelectorRegexp = /::?(?:[\w-]+)(?:\(.*?\))?/g;
    420                 for (var i = 0; i < selectors.length; ++i) {
    421                     if (progress.isCanceled())
    422                         return;
    423                     var effectiveSelector = selectors[i].replace(pseudoSelectorRegexp, "");
    424                     WebInspector.domAgent.querySelector(document.id, effectiveSelector, queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, callback, styleSheets, testedSelectors) : null, selectors[i], styleSheets, testedSelectors));
    425                 }
    426             }
    427 
    428             WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
    429         }
    430 
    431         function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
    432         {
    433             if (progress.isCanceled())
    434                 return;
    435 
    436             if (styleSheet) {
    437                 styleSheet.sourceURL = sourceURL;
    438                 styleSheets.push(styleSheet);
    439             }
    440             if (continuation)
    441                 continuation(styleSheets);
    442         }
    443 
    444         function allStylesCallback(error, styleSheetInfos)
    445         {
    446             if (progress.isCanceled())
    447                 return;
    448 
    449             if (error || !styleSheetInfos || !styleSheetInfos.length)
    450                 return evalCallback([]);
    451             var styleSheets = [];
    452             for (var i = 0; i < styleSheetInfos.length; ++i) {
    453                 var info = styleSheetInfos[i];
    454                 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
    455             }
    456         }
    457 
    458         CSSAgent.getAllStyleSheets(allStylesCallback);
    459     },
    460 
    461     __proto__: WebInspector.AuditRule.prototype
    462 }
    463 
    464 /**
    465  * @constructor
    466  * @extends {WebInspector.AuditRule}
    467  */
    468 WebInspector.AuditRules.CacheControlRule = function(id, name)
    469 {
    470     WebInspector.AuditRule.call(this, id, name);
    471 }
    472 
    473 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
    474 
    475 WebInspector.AuditRules.CacheControlRule.prototype = {
    476     /**
    477      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    478      * @param {!WebInspector.AuditRuleResult} result
    479      * @param {function(WebInspector.AuditRuleResult)} callback
    480      * @param {!WebInspector.Progress} progress
    481      */
    482     doRun: function(requests, result, callback, progress)
    483     {
    484         var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(requests);
    485         if (cacheableAndNonCacheableResources[0].length)
    486             this.runChecks(cacheableAndNonCacheableResources[0], result);
    487         this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
    488 
    489         callback(result);
    490     },
    491 
    492     handleNonCacheableResources: function(requests, result)
    493     {
    494     },
    495 
    496     _cacheableAndNonCacheableResources: function(requests)
    497     {
    498         var processedResources = [[], []];
    499         for (var i = 0; i < requests.length; ++i) {
    500             var request = requests[i];
    501             if (!this.isCacheableResource(request))
    502                 continue;
    503             if (this._isExplicitlyNonCacheable(request))
    504                 processedResources[1].push(request);
    505             else
    506                 processedResources[0].push(request);
    507         }
    508         return processedResources;
    509     },
    510 
    511     execCheck: function(messageText, requestCheckFunction, requests, result)
    512     {
    513         var requestCount = requests.length;
    514         var urls = [];
    515         for (var i = 0; i < requestCount; ++i) {
    516             if (requestCheckFunction.call(this, requests[i]))
    517                 urls.push(requests[i].url);
    518         }
    519         if (urls.length) {
    520             var entry = result.addChild(messageText, true);
    521             entry.addURLs(urls);
    522             result.violationCount += urls.length;
    523         }
    524     },
    525 
    526     freshnessLifetimeGreaterThan: function(request, timeMs)
    527     {
    528         var dateHeader = this.responseHeader(request, "Date");
    529         if (!dateHeader)
    530             return false;
    531 
    532         var dateHeaderMs = Date.parse(dateHeader);
    533         if (isNaN(dateHeaderMs))
    534             return false;
    535 
    536         var freshnessLifetimeMs;
    537         var maxAgeMatch = this.responseHeaderMatch(request, "Cache-Control", "max-age=(\\d+)");
    538 
    539         if (maxAgeMatch)
    540             freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
    541         else {
    542             var expiresHeader = this.responseHeader(request, "Expires");
    543             if (expiresHeader) {
    544                 var expDate = Date.parse(expiresHeader);
    545                 if (!isNaN(expDate))
    546                     freshnessLifetimeMs = expDate - dateHeaderMs;
    547             }
    548         }
    549 
    550         return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
    551     },
    552 
    553     responseHeader: function(request, header)
    554     {
    555         return request.responseHeaderValue(header);
    556     },
    557 
    558     hasResponseHeader: function(request, header)
    559     {
    560         return request.responseHeaderValue(header) !== undefined;
    561     },
    562 
    563     isCompressible: function(request)
    564     {
    565         return request.type.isTextType();
    566     },
    567 
    568     isPubliclyCacheable: function(request)
    569     {
    570         if (this._isExplicitlyNonCacheable(request))
    571             return false;
    572 
    573         if (this.responseHeaderMatch(request, "Cache-Control", "public"))
    574             return true;
    575 
    576         return request.url.indexOf("?") == -1 && !this.responseHeaderMatch(request, "Cache-Control", "private");
    577     },
    578 
    579     responseHeaderMatch: function(request, header, regexp)
    580     {
    581         return request.responseHeaderValue(header)
    582             ? request.responseHeaderValue(header).match(new RegExp(regexp, "im"))
    583             : undefined;
    584     },
    585 
    586     hasExplicitExpiration: function(request)
    587     {
    588         return this.hasResponseHeader(request, "Date") &&
    589             (this.hasResponseHeader(request, "Expires") || this.responseHeaderMatch(request, "Cache-Control", "max-age"));
    590     },
    591 
    592     _isExplicitlyNonCacheable: function(request)
    593     {
    594         var hasExplicitExp = this.hasExplicitExpiration(request);
    595         return this.responseHeaderMatch(request, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
    596             this.responseHeaderMatch(request, "Pragma", "no-cache") ||
    597             (hasExplicitExp && !this.freshnessLifetimeGreaterThan(request, 0)) ||
    598             (!hasExplicitExp && request.url && request.url.indexOf("?") >= 0) ||
    599             (!hasExplicitExp && !this.isCacheableResource(request));
    600     },
    601 
    602     isCacheableResource: function(request)
    603     {
    604         return request.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[request.statusCode];
    605     },
    606 
    607     __proto__: WebInspector.AuditRule.prototype
    608 }
    609 
    610 /**
    611  * @constructor
    612  * @extends {WebInspector.AuditRules.CacheControlRule}
    613  */
    614 WebInspector.AuditRules.BrowserCacheControlRule = function()
    615 {
    616     WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
    617 }
    618 
    619 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
    620     handleNonCacheableResources: function(requests, result)
    621     {
    622         if (requests.length) {
    623             var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
    624             result.violationCount += requests.length;
    625             for (var i = 0; i < requests.length; ++i)
    626                 entry.addURL(requests[i].url);
    627         }
    628     },
    629 
    630     runChecks: function(requests, result, callback)
    631     {
    632         this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
    633             this._missingExpirationCheck, requests, result);
    634         this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
    635             this._varyCheck, requests, result);
    636         this.execCheck("The following cacheable resources have a short freshness lifetime:",
    637             this._oneMonthExpirationCheck, requests, result);
    638 
    639         // Unable to implement the favicon check due to the WebKit limitations.
    640         this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
    641             this._oneYearExpirationCheck, requests, result);
    642     },
    643 
    644     _missingExpirationCheck: function(request)
    645     {
    646         return this.isCacheableResource(request) && !this.hasResponseHeader(request, "Set-Cookie") && !this.hasExplicitExpiration(request);
    647     },
    648 
    649     _varyCheck: function(request)
    650     {
    651         var varyHeader = this.responseHeader(request, "Vary");
    652         if (varyHeader) {
    653             varyHeader = varyHeader.replace(/User-Agent/gi, "");
    654             varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
    655             varyHeader = varyHeader.replace(/[, ]*/g, "");
    656         }
    657         return varyHeader && varyHeader.length && this.isCacheableResource(request) && this.freshnessLifetimeGreaterThan(request, 0);
    658     },
    659 
    660     _oneMonthExpirationCheck: function(request)
    661     {
    662         return this.isCacheableResource(request) &&
    663             !this.hasResponseHeader(request, "Set-Cookie") &&
    664             !this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
    665             this.freshnessLifetimeGreaterThan(request, 0);
    666     },
    667 
    668     _oneYearExpirationCheck: function(request)
    669     {
    670         return this.isCacheableResource(request) &&
    671             !this.hasResponseHeader(request, "Set-Cookie") &&
    672             !this.freshnessLifetimeGreaterThan(request, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
    673             this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
    674     },
    675 
    676     __proto__: WebInspector.AuditRules.CacheControlRule.prototype
    677 }
    678 
    679 /**
    680  * @constructor
    681  * @extends {WebInspector.AuditRules.CacheControlRule}
    682  */
    683 WebInspector.AuditRules.ProxyCacheControlRule = function() {
    684     WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
    685 }
    686 
    687 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
    688     runChecks: function(requests, result, callback)
    689     {
    690         this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
    691             this._questionMarkCheck, requests, result);
    692         this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
    693             this._publicCachingCheck, requests, result);
    694         this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
    695             this._setCookieCacheableCheck, requests, result);
    696     },
    697 
    698     _questionMarkCheck: function(request)
    699     {
    700         return request.url.indexOf("?") >= 0 && !this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request);
    701     },
    702 
    703     _publicCachingCheck: function(request)
    704     {
    705         return this.isCacheableResource(request) &&
    706             !this.isCompressible(request) &&
    707             !this.responseHeaderMatch(request, "Cache-Control", "public") &&
    708             !this.hasResponseHeader(request, "Set-Cookie");
    709     },
    710 
    711     _setCookieCacheableCheck: function(request)
    712     {
    713         return this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request);
    714     },
    715 
    716     __proto__: WebInspector.AuditRules.CacheControlRule.prototype
    717 }
    718 
    719 /**
    720  * @constructor
    721  * @extends {WebInspector.AuditRule}
    722  */
    723 WebInspector.AuditRules.ImageDimensionsRule = function()
    724 {
    725     WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
    726 }
    727 
    728 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
    729     /**
    730      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    731      * @param {!WebInspector.AuditRuleResult} result
    732      * @param {function(WebInspector.AuditRuleResult)} callback
    733      * @param {!WebInspector.Progress} progress
    734      */
    735     doRun: function(requests, result, callback, progress)
    736     {
    737         var urlToNoDimensionCount = {};
    738 
    739         function doneCallback()
    740         {
    741             for (var url in urlToNoDimensionCount) {
    742                 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);
    743                 var format = "%r";
    744                 if (urlToNoDimensionCount[url] > 1)
    745                     format += " (%d uses)";
    746                 entry.addFormatted(format, url, urlToNoDimensionCount[url]);
    747                 result.violationCount++;
    748             }
    749             callback(entry ? result : null);
    750         }
    751 
    752         function imageStylesReady(imageId, styles, isLastStyle, computedStyle)
    753         {
    754             if (progress.isCanceled())
    755                 return;
    756 
    757             const node = WebInspector.domAgent.nodeForId(imageId);
    758             var src = node.getAttribute("src");
    759             if (!src.asParsedURL()) {
    760                 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
    761                     if (frameOwnerCandidate.baseURL) {
    762                         var completeSrc = WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, src);
    763                         break;
    764                     }
    765                 }
    766             }
    767             if (completeSrc)
    768                 src = completeSrc;
    769 
    770             if (computedStyle.getPropertyValue("position") === "absolute") {
    771                 if (isLastStyle)
    772                     doneCallback();
    773                 return;
    774             }
    775 
    776             if (styles.attributesStyle) {
    777                 var widthFound = !!styles.attributesStyle.getLiveProperty("width");
    778                 var heightFound = !!styles.attributesStyle.getLiveProperty("height");
    779             }
    780 
    781             var inlineStyle = styles.inlineStyle;
    782             if (inlineStyle) {
    783                 if (inlineStyle.getPropertyValue("width") !== "")
    784                     widthFound = true;
    785                 if (inlineStyle.getPropertyValue("height") !== "")
    786                     heightFound = true;
    787             }
    788 
    789             for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
    790                 var style = styles.matchedCSSRules[i].style;
    791                 if (style.getPropertyValue("width") !== "")
    792                     widthFound = true;
    793                 if (style.getPropertyValue("height") !== "")
    794                     heightFound = true;
    795             }
    796 
    797             if (!widthFound || !heightFound) {
    798                 if (src in urlToNoDimensionCount)
    799                     ++urlToNoDimensionCount[src];
    800                 else
    801                     urlToNoDimensionCount[src] = 1;
    802             }
    803 
    804             if (isLastStyle)
    805                 doneCallback();
    806         }
    807 
    808         function getStyles(nodeIds)
    809         {
    810             if (progress.isCanceled())
    811                 return;
    812             var targetResult = {};
    813 
    814             function inlineCallback(inlineStyle, attributesStyle)
    815             {
    816                 targetResult.inlineStyle = inlineStyle;
    817                 targetResult.attributesStyle = attributesStyle;
    818             }
    819 
    820             function matchedCallback(result)
    821             {
    822                 if (result)
    823                     targetResult.matchedCSSRules = result.matchedCSSRules;
    824             }
    825 
    826             if (!nodeIds || !nodeIds.length)
    827                 doneCallback();
    828 
    829             for (var i = 0; nodeIds && i < nodeIds.length; ++i) {
    830                 WebInspector.cssModel.getMatchedStylesAsync(nodeIds[i], false, false, matchedCallback);
    831                 WebInspector.cssModel.getInlineStylesAsync(nodeIds[i], inlineCallback);
    832                 WebInspector.cssModel.getComputedStyleAsync(nodeIds[i], imageStylesReady.bind(null, nodeIds[i], targetResult, i === nodeIds.length - 1));
    833             }
    834         }
    835 
    836         function onDocumentAvailable(root)
    837         {
    838             if (progress.isCanceled())
    839                 return;
    840             WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
    841         }
    842 
    843         if (progress.isCanceled())
    844             return;
    845         WebInspector.domAgent.requestDocument(onDocumentAvailable);
    846     },
    847 
    848     __proto__: WebInspector.AuditRule.prototype
    849 }
    850 
    851 /**
    852  * @constructor
    853  * @extends {WebInspector.AuditRule}
    854  */
    855 WebInspector.AuditRules.CssInHeadRule = function()
    856 {
    857     WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
    858 }
    859 
    860 WebInspector.AuditRules.CssInHeadRule.prototype = {
    861     /**
    862      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    863      * @param {!WebInspector.AuditRuleResult} result
    864      * @param {function(WebInspector.AuditRuleResult)} callback
    865      * @param {!WebInspector.Progress} progress
    866      */
    867     doRun: function(requests, result, callback, progress)
    868     {
    869         function evalCallback(evalResult)
    870         {
    871             if (progress.isCanceled())
    872                 return;
    873 
    874             if (!evalResult)
    875                 return callback(null);
    876 
    877             var summary = result.addChild("");
    878 
    879             var outputMessages = [];
    880             for (var url in evalResult) {
    881                 var urlViolations = evalResult[url];
    882                 if (urlViolations[0]) {
    883                     result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url);
    884                     result.violationCount += urlViolations[0];
    885                 }
    886                 for (var i = 0; i < urlViolations[1].length; ++i)
    887                     result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url);
    888                 result.violationCount += urlViolations[1].length;
    889             }
    890             summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
    891             callback(result);
    892         }
    893 
    894         function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
    895         {
    896             if (progress.isCanceled())
    897                 return;
    898 
    899             if (!nodeIds)
    900                 return;
    901             var externalStylesheetNodeIds = nodeIds;
    902             var result = null;
    903             if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
    904                 var urlToViolationsArray = {};
    905                 var externalStylesheetHrefs = [];
    906                 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
    907                     var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
    908                     var completeHref = WebInspector.ParsedURL.completeURL(linkNode.ownerDocument.baseURL, linkNode.getAttribute("href"));
    909                     externalStylesheetHrefs.push(completeHref || "<empty>");
    910                 }
    911                 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
    912                 result = urlToViolationsArray;
    913             }
    914             evalCallback(result);
    915         }
    916 
    917         function inlineStylesReceived(root, nodeIds)
    918         {
    919             if (progress.isCanceled())
    920                 return;
    921 
    922             if (!nodeIds)
    923                 return;
    924             WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
    925         }
    926 
    927         function onDocumentAvailable(root)
    928         {
    929             if (progress.isCanceled())
    930                 return;
    931 
    932             WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
    933         }
    934 
    935         WebInspector.domAgent.requestDocument(onDocumentAvailable);
    936     },
    937 
    938     __proto__: WebInspector.AuditRule.prototype
    939 }
    940 
    941 /**
    942  * @constructor
    943  * @extends {WebInspector.AuditRule}
    944  */
    945 WebInspector.AuditRules.StylesScriptsOrderRule = function()
    946 {
    947     WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
    948 }
    949 
    950 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
    951     /**
    952      * @param {!Array.<!WebInspector.NetworkRequest>} requests
    953      * @param {!WebInspector.AuditRuleResult} result
    954      * @param {function(WebInspector.AuditRuleResult)} callback
    955      * @param {!WebInspector.Progress} progress
    956      */
    957     doRun: function(requests, result, callback, progress)
    958     {
    959         function evalCallback(resultValue)
    960         {
    961             if (progress.isCanceled())
    962                 return;
    963 
    964             if (!resultValue)
    965                 return callback(null);
    966 
    967             var lateCssUrls = resultValue[0];
    968             var cssBeforeInlineCount = resultValue[1];
    969 
    970             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);
    971             entry.addURLs(lateCssUrls);
    972             result.violationCount += lateCssUrls.length;
    973 
    974             if (cssBeforeInlineCount) {
    975                 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"));
    976                 result.violationCount += cssBeforeInlineCount;
    977             }
    978             callback(result);
    979         }
    980 
    981         function cssBeforeInlineReceived(lateStyleIds, nodeIds)
    982         {
    983             if (progress.isCanceled())
    984                 return;
    985 
    986             if (!nodeIds)
    987                 return;
    988 
    989             var cssBeforeInlineCount = nodeIds.length;
    990             var result = null;
    991             if (lateStyleIds.length || cssBeforeInlineCount) {
    992                 var lateStyleUrls = [];
    993                 for (var i = 0; i < lateStyleIds.length; ++i) {
    994                     var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
    995                     var completeHref = WebInspector.ParsedURL.completeURL(lateStyleNode.ownerDocument.baseURL, lateStyleNode.getAttribute("href"));
    996                     lateStyleUrls.push(completeHref || "<empty>");
    997                 }
    998                 result = [ lateStyleUrls, cssBeforeInlineCount ];
    999             }
   1000 
   1001             evalCallback(result);
   1002         }
   1003 
   1004         function lateStylesReceived(root, nodeIds)
   1005         {
   1006             if (progress.isCanceled())
   1007                 return;
   1008 
   1009             if (!nodeIds)
   1010                 return;
   1011 
   1012             WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
   1013         }
   1014 
   1015         function onDocumentAvailable(root)
   1016         {
   1017             if (progress.isCanceled())
   1018                 return;
   1019 
   1020             WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
   1021         }
   1022 
   1023         WebInspector.domAgent.requestDocument(onDocumentAvailable);
   1024     },
   1025 
   1026     __proto__: WebInspector.AuditRule.prototype
   1027 }
   1028 
   1029 /**
   1030  * @constructor
   1031  * @extends {WebInspector.AuditRule}
   1032  */
   1033 WebInspector.AuditRules.CSSRuleBase = function(id, name)
   1034 {
   1035     WebInspector.AuditRule.call(this, id, name);
   1036 }
   1037 
   1038 WebInspector.AuditRules.CSSRuleBase.prototype = {
   1039     /**
   1040      * @param {!Array.<!WebInspector.NetworkRequest>} requests
   1041      * @param {!WebInspector.AuditRuleResult} result
   1042      * @param {function(WebInspector.AuditRuleResult)} callback
   1043      * @param {!WebInspector.Progress} progress
   1044      */
   1045     doRun: function(requests, result, callback, progress)
   1046     {
   1047         CSSAgent.getAllStyleSheets(sheetsCallback.bind(this));
   1048 
   1049         function sheetsCallback(error, headers)
   1050         {
   1051             if (error)
   1052                 return callback(null);
   1053 
   1054             if (!headers.length)
   1055                 return callback(null);
   1056             for (var i = 0; i < headers.length; ++i) {
   1057                 var header = headers[i];
   1058                 if (header.disabled)
   1059                     continue; // Do not check disabled stylesheets.
   1060 
   1061                 this._visitStyleSheet(header.styleSheetId, i === headers.length - 1 ? finishedCallback : null, result, progress);
   1062             }
   1063         }
   1064 
   1065         function finishedCallback()
   1066         {
   1067             callback(result);
   1068         }
   1069     },
   1070 
   1071     _visitStyleSheet: function(styleSheetId, callback, result, progress)
   1072     {
   1073         WebInspector.CSSStyleSheet.createForId(styleSheetId, sheetCallback.bind(this));
   1074 
   1075         function sheetCallback(styleSheet)
   1076         {
   1077             if (progress.isCanceled())
   1078                 return;
   1079 
   1080             if (!styleSheet) {
   1081                 if (callback)
   1082                     callback();
   1083                 return;
   1084             }
   1085 
   1086             this.visitStyleSheet(styleSheet, result);
   1087 
   1088             for (var i = 0; i < styleSheet.rules.length; ++i)
   1089                 this._visitRule(styleSheet, styleSheet.rules[i], result);
   1090 
   1091             this.didVisitStyleSheet(styleSheet, result);
   1092 
   1093             if (callback)
   1094                 callback();
   1095         }
   1096     },
   1097 
   1098     _visitRule: function(styleSheet, rule, result)
   1099     {
   1100         this.visitRule(styleSheet, rule, result);
   1101         var allProperties = rule.style.allProperties;
   1102         for (var i = 0; i < allProperties.length; ++i)
   1103             this.visitProperty(styleSheet, allProperties[i], result);
   1104         this.didVisitRule(styleSheet, rule, result);
   1105     },
   1106 
   1107     visitStyleSheet: function(styleSheet, result)
   1108     {
   1109         // Subclasses can implement.
   1110     },
   1111 
   1112     didVisitStyleSheet: function(styleSheet, result)
   1113     {
   1114         // Subclasses can implement.
   1115     },
   1116 
   1117     visitRule: function(styleSheet, rule, result)
   1118     {
   1119         // Subclasses can implement.
   1120     },
   1121 
   1122     didVisitRule: function(styleSheet, rule, result)
   1123     {
   1124         // Subclasses can implement.
   1125     },
   1126 
   1127     visitProperty: function(styleSheet, property, result)
   1128     {
   1129         // Subclasses can implement.
   1130     },
   1131 
   1132     __proto__: WebInspector.AuditRule.prototype
   1133 }
   1134 
   1135 /**
   1136  * @constructor
   1137  * @extends {WebInspector.AuditRules.CSSRuleBase}
   1138  */
   1139 WebInspector.AuditRules.VendorPrefixedCSSProperties = function()
   1140 {
   1141     WebInspector.AuditRules.CSSRuleBase.call(this, "page-vendorprefixedcss", "Use normal CSS property names instead of vendor-prefixed ones");
   1142     this._webkitPrefix = "-webkit-";
   1143 }
   1144 
   1145 WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties = [
   1146     "background-clip", "background-origin", "background-size",
   1147     "border-radius", "border-bottom-left-radius", "border-bottom-right-radius", "border-top-left-radius", "border-top-right-radius",
   1148     "box-shadow", "box-sizing", "opacity", "text-shadow"
   1149 ].keySet();
   1150 
   1151 WebInspector.AuditRules.VendorPrefixedCSSProperties.prototype = {
   1152     didVisitStyleSheet: function(styleSheet)
   1153     {
   1154         delete this._styleSheetResult;
   1155     },
   1156 
   1157     visitRule: function(rule)
   1158     {
   1159         this._mentionedProperties = {};
   1160     },
   1161 
   1162     didVisitRule: function()
   1163     {
   1164         delete this._ruleResult;
   1165         delete this._mentionedProperties;
   1166     },
   1167 
   1168     visitProperty: function(styleSheet, property, result)
   1169     {
   1170         if (!property.name.startsWith(this._webkitPrefix))
   1171             return;
   1172 
   1173         var normalPropertyName = property.name.substring(this._webkitPrefix.length).toLowerCase(); // Start just after the "-webkit-" prefix.
   1174         if (WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties[normalPropertyName] && !this._mentionedProperties[normalPropertyName]) {
   1175             var style = property.ownerStyle;
   1176             var liveProperty = style.getLiveProperty(normalPropertyName);
   1177             if (liveProperty && !liveProperty.styleBased)
   1178                 return; // WebCore can provide normal versions of prefixed properties automatically, so be careful to skip only normal source-based properties.
   1179 
   1180             var rule = style.parentRule;
   1181             this._mentionedProperties[normalPropertyName] = true;
   1182             if (!this._styleSheetResult)
   1183                 this._styleSheetResult = result.addChild(rule.sourceURL ? WebInspector.linkifyResourceAsNode(rule.sourceURL) : "<unknown>");
   1184             if (!this._ruleResult) {
   1185                 var anchor = WebInspector.linkifyURLAsNode(rule.sourceURL, rule.selectorText);
   1186                 anchor.preferredPanel = "resources";
   1187                 anchor.lineNumber = rule.lineNumberInSource();
   1188                 this._ruleResult = this._styleSheetResult.addChild(anchor);
   1189             }
   1190             ++result.violationCount;
   1191             this._ruleResult.addSnippet(String.sprintf("\"" + this._webkitPrefix + "%s\" is used, but \"%s\" is supported.", normalPropertyName, normalPropertyName));
   1192         }
   1193     },
   1194 
   1195     __proto__: WebInspector.AuditRules.CSSRuleBase.prototype
   1196 }
   1197 
   1198 /**
   1199  * @constructor
   1200  * @extends {WebInspector.AuditRule}
   1201  */
   1202 WebInspector.AuditRules.CookieRuleBase = function(id, name)
   1203 {
   1204     WebInspector.AuditRule.call(this, id, name);
   1205 }
   1206 
   1207 WebInspector.AuditRules.CookieRuleBase.prototype = {
   1208     /**
   1209      * @param {!Array.<!WebInspector.NetworkRequest>} requests
   1210      * @param {!WebInspector.AuditRuleResult} result
   1211      * @param {function(WebInspector.AuditRuleResult)} callback
   1212      * @param {!WebInspector.Progress} progress
   1213      */
   1214     doRun: function(requests, result, callback, progress)
   1215     {
   1216         var self = this;
   1217         function resultCallback(receivedCookies) {
   1218             if (progress.isCanceled())
   1219                 return;
   1220 
   1221             self.processCookies(receivedCookies, requests, result);
   1222             callback(result);
   1223         }
   1224 
   1225         WebInspector.Cookies.getCookiesAsync(resultCallback);
   1226     },
   1227 
   1228     mapResourceCookies: function(requestsByDomain, allCookies, callback)
   1229     {
   1230         for (var i = 0; i < allCookies.length; ++i) {
   1231             for (var requestDomain in requestsByDomain) {
   1232                 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain(), requestDomain))
   1233                     this._callbackForResourceCookiePairs(requestsByDomain[requestDomain], allCookies[i], callback);
   1234             }
   1235         }
   1236     },
   1237 
   1238     _callbackForResourceCookiePairs: function(requests, cookie, callback)
   1239     {
   1240         if (!requests)
   1241             return;
   1242         for (var i = 0; i < requests.length; ++i) {
   1243             if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, requests[i].url))
   1244                 callback(requests[i], cookie);
   1245         }
   1246     },
   1247 
   1248     __proto__: WebInspector.AuditRule.prototype
   1249 }
   1250 
   1251 /**
   1252  * @constructor
   1253  * @extends {WebInspector.AuditRules.CookieRuleBase}
   1254  */
   1255 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
   1256 {
   1257     WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
   1258     this._avgBytesThreshold = avgBytesThreshold;
   1259     this._maxBytesThreshold = 1000;
   1260 }
   1261 
   1262 WebInspector.AuditRules.CookieSizeRule.prototype = {
   1263     _average: function(cookieArray)
   1264     {
   1265         var total = 0;
   1266         for (var i = 0; i < cookieArray.length; ++i)
   1267             total += cookieArray[i].size();
   1268         return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
   1269     },
   1270 
   1271     _max: function(cookieArray)
   1272     {
   1273         var result = 0;
   1274         for (var i = 0; i < cookieArray.length; ++i)
   1275             result = Math.max(cookieArray[i].size(), result);
   1276         return result;
   1277     },
   1278 
   1279     processCookies: function(allCookies, requests, result)
   1280     {
   1281         function maxSizeSorter(a, b)
   1282         {
   1283             return b.maxCookieSize - a.maxCookieSize;
   1284         }
   1285 
   1286         function avgSizeSorter(a, b)
   1287         {
   1288             return b.avgCookieSize - a.avgCookieSize;
   1289         }
   1290 
   1291         var cookiesPerResourceDomain = {};
   1292 
   1293         function collectorCallback(request, cookie)
   1294         {
   1295             var cookies = cookiesPerResourceDomain[request.parsedURL.host];
   1296             if (!cookies) {
   1297                 cookies = [];
   1298                 cookiesPerResourceDomain[request.parsedURL.host] = cookies;
   1299             }
   1300             cookies.push(cookie);
   1301         }
   1302 
   1303         if (!allCookies.length)
   1304             return;
   1305 
   1306         var sortedCookieSizes = [];
   1307 
   1308         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
   1309                 null,
   1310                 true);
   1311         var matchingResourceData = {};
   1312         this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
   1313 
   1314         for (var requestDomain in cookiesPerResourceDomain) {
   1315             var cookies = cookiesPerResourceDomain[requestDomain];
   1316             sortedCookieSizes.push({
   1317                 domain: requestDomain,
   1318                 avgCookieSize: this._average(cookies),
   1319                 maxCookieSize: this._max(cookies)
   1320             });
   1321         }
   1322         var avgAllCookiesSize = this._average(allCookies);
   1323 
   1324         var hugeCookieDomains = [];
   1325         sortedCookieSizes.sort(maxSizeSorter);
   1326 
   1327         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
   1328             var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
   1329             if (maxCookieSize > this._maxBytesThreshold)
   1330                 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
   1331         }
   1332 
   1333         var bigAvgCookieDomains = [];
   1334         sortedCookieSizes.sort(avgSizeSorter);
   1335         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
   1336             var domain = sortedCookieSizes[i].domain;
   1337             var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
   1338             if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
   1339                 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
   1340         }
   1341         result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
   1342 
   1343         var message;
   1344         if (hugeCookieDomains.length) {
   1345             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);
   1346             entry.addURLs(hugeCookieDomains);
   1347             result.violationCount += hugeCookieDomains.length;
   1348         }
   1349 
   1350         if (bigAvgCookieDomains.length) {
   1351             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);
   1352             entry.addURLs(bigAvgCookieDomains);
   1353             result.violationCount += bigAvgCookieDomains.length;
   1354         }
   1355     },
   1356 
   1357     __proto__: WebInspector.AuditRules.CookieRuleBase.prototype
   1358 }
   1359 
   1360 /**
   1361  * @constructor
   1362  * @extends {WebInspector.AuditRules.CookieRuleBase}
   1363  */
   1364 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
   1365 {
   1366     WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
   1367     this._minResources = minResources;
   1368 }
   1369 
   1370 WebInspector.AuditRules.StaticCookielessRule.prototype = {
   1371     processCookies: function(allCookies, requests, result)
   1372     {
   1373         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
   1374                 [WebInspector.resourceTypes.Stylesheet,
   1375                  WebInspector.resourceTypes.Image],
   1376                 true);
   1377         var totalStaticResources = 0;
   1378         for (var domain in domainToResourcesMap)
   1379             totalStaticResources += domainToResourcesMap[domain].length;
   1380         if (totalStaticResources < this._minResources)
   1381             return;
   1382         var matchingResourceData = {};
   1383         this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
   1384 
   1385         var badUrls = [];
   1386         var cookieBytes = 0;
   1387         for (var url in matchingResourceData) {
   1388             badUrls.push(url);
   1389             cookieBytes += matchingResourceData[url]
   1390         }
   1391         if (badUrls.length < this._minResources)
   1392             return;
   1393 
   1394         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);
   1395         entry.addURLs(badUrls);
   1396         result.violationCount = badUrls.length;
   1397     },
   1398 
   1399     _collectorCallback: function(matchingResourceData, request, cookie)
   1400     {
   1401         matchingResourceData[request.url] = (matchingResourceData[request.url] || 0) + cookie.size();
   1402     },
   1403 
   1404     __proto__: WebInspector.AuditRules.CookieRuleBase.prototype
   1405 }
   1406