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