• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
31WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
32
33WebInspector.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 */
51WebInspector.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 */
76WebInspector.AuditRules.GzipRule = function()
77{
78    WebInspector.AuditRule.call(this, "network-gzip", WebInspector.UIString("Enable gzip compression"));
79}
80
81WebInspector.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 */
147WebInspector.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
155WebInspector.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 */
194WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
195    WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", WebInspector.UIString("Combine external JavaScript"), WebInspector.resourceTypes.Script, "JavaScript", allowedPerDomain);
196}
197
198WebInspector.AuditRules.CombineJsResourcesRule.prototype = {
199    __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
200}
201
202/**
203 * @constructor
204 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
205 */
206WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
207    WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", WebInspector.UIString("Combine external CSS"), WebInspector.resourceTypes.Stylesheet, "CSS", allowedPerDomain);
208}
209
210WebInspector.AuditRules.CombineCssResourcesRule.prototype = {
211    __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
212}
213
214/**
215 * @constructor
216 * @extends {WebInspector.AuditRule}
217 */
218WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
219    WebInspector.AuditRule.call(this, "network-minimizelookups", WebInspector.UIString("Minimize DNS lookups"));
220    this._hostCountThreshold = hostCountThreshold;
221}
222
223WebInspector.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 */
262WebInspector.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
270WebInspector.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 */
351WebInspector.AuditRules.UnusedCssRule = function()
352{
353    WebInspector.AuditRule.call(this, "page-unusedcss", WebInspector.UIString("Remove unused CSS rules"));
354}
355
356WebInspector.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 */
488WebInspector.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 */
496WebInspector.AuditRules.StyleSheetProcessor = function(styleSheetHeaders, progress, styleSheetsParsedCallback)
497{
498    this._styleSheetHeaders = styleSheetHeaders;
499    this._progress = progress;
500    this._styleSheets = [];
501    this._styleSheetsParsedCallback = styleSheetsParsedCallback;
502}
503
504WebInspector.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 */
563WebInspector.AuditRules.CacheControlRule = function(id, name)
564{
565    WebInspector.AuditRule.call(this, id, name);
566}
567
568WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
569
570WebInspector.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 */
751WebInspector.AuditRules.BrowserCacheControlRule = function()
752{
753    WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", WebInspector.UIString("Leverage browser caching"));
754}
755
756WebInspector.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 */
820WebInspector.AuditRules.ProxyCacheControlRule = function() {
821    WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", WebInspector.UIString("Leverage proxy caching"));
822}
823
824WebInspector.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 */
860WebInspector.AuditRules.ImageDimensionsRule = function()
861{
862    WebInspector.AuditRule.call(this, "page-imagedims", WebInspector.UIString("Specify image dimensions"));
863}
864
865WebInspector.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 */
996WebInspector.AuditRules.CssInHeadRule = function()
997{
998    WebInspector.AuditRule.call(this, "page-cssinhead", WebInspector.UIString("Put CSS in the document head"));
999}
1000
1001WebInspector.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 */
1099WebInspector.AuditRules.StylesScriptsOrderRule = function()
1100{
1101    WebInspector.AuditRule.call(this, "page-stylescriptorder", WebInspector.UIString("Optimize the order of styles and scripts"));
1102}
1103
1104WebInspector.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 */
1201WebInspector.AuditRules.CSSRuleBase = function(id, name)
1202{
1203    WebInspector.AuditRule.call(this, id, name);
1204}
1205
1206WebInspector.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 */
1329WebInspector.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
1335WebInspector.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
1341WebInspector.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 */
1397WebInspector.AuditRules.CookieRuleBase = function(id, name)
1398{
1399    WebInspector.AuditRule.call(this, id, name);
1400}
1401
1402WebInspector.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 */
1451WebInspector.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
1458WebInspector.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 */
1560WebInspector.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
1566WebInspector.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