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