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