2011-01-13 Mikhail Naganov <mnaganov@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / 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 resource is cacheable
43 }
44
45 WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, needFullResources)
46 {
47     var domainToResourcesMap = {};
48     for (var i = 0, size = resources.length; i < size; ++i) {
49         var resource = resources[i];
50         if (types && types.indexOf(resource.type) === -1)
51             continue;
52         var parsedURL = resource.url.asParsedURL();
53         if (!parsedURL)
54             continue;
55         var domain = parsedURL.host;
56         var domainResources = domainToResourcesMap[domain];
57         if (domainResources === undefined) {
58           domainResources = [];
59           domainToResourcesMap[domain] = domainResources;
60         }
61         domainResources.push(needFullResources ? resource : resource.url);
62     }
63     return domainToResourcesMap;
64 }
65
66 WebInspector.AuditRules.evaluateInTargetWindow = function(func, args, callback)
67 {
68     InjectedScriptAccess.getDefault().evaluateOnSelf(func.toString(), args, callback);
69 }
70
71
72 WebInspector.AuditRules.GzipRule = function()
73 {
74     WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
75 }
76
77 WebInspector.AuditRules.GzipRule.prototype = {
78     doRun: function(resources, result, callback)
79     {
80         var totalSavings = 0;
81         var compressedSize = 0;
82         var candidateSize = 0;
83         var summary = result.addChild("", true);
84         for (var i = 0, length = resources.length; i < length; ++i) {
85             var resource = resources[i];
86             if (this._shouldCompress(resource)) {
87                 var size = resource.resourceSize;
88                 candidateSize += size;
89                 if (this._isCompressed(resource)) {
90                     compressedSize += size;
91                     continue;
92                 }
93                 var savings = 2 * size / 3;
94                 totalSavings += savings;
95                 summary.addChild(String.sprintf("%s could save ~%s", WebInspector.AuditRuleResult.linkifyDisplayName(resource.url), Number.bytesToString(savings)));
96                 result.violationCount++;
97             }
98         }
99         if (!totalSavings)
100             return callback(null);
101         summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
102         callback(result);
103     },
104
105     _isCompressed: function(resource)
106     {
107         var encoding = resource.responseHeaders["Content-Encoding"];
108         return encoding === "gzip" || encoding === "deflate";
109     },
110
111     _shouldCompress: function(resource)
112     {
113         return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150;
114     }
115 }
116
117 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
118
119
120 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
121 {
122     WebInspector.AuditRule.call(this, id, name);
123     this._type = type;
124     this._resourceTypeName = resourceTypeName;
125     this._allowedPerDomain = allowedPerDomain;
126 }
127
128 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
129     doRun: function(resources, result, callback)
130     {
131         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type]);
132         var penalizedResourceCount = 0;
133         // TODO: refactor according to the chosen i18n approach
134         var summary = result.addChild("", true);
135         for (var domain in domainToResourcesMap) {
136             var domainResources = domainToResourcesMap[domain];
137             var extraResourceCount = domainResources.length - this._allowedPerDomain;
138             if (extraResourceCount <= 0)
139                 continue;
140             penalizedResourceCount += extraResourceCount - 1;
141             summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
142             result.violationCount += domainResources.length;
143         }
144         if (!penalizedResourceCount)
145             return callback(null);
146
147         summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
148         callback(result);
149     }
150 }
151
152 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
153
154
155 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
156     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain);
157 }
158
159 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
160
161
162 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
163     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain);
164 }
165
166 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
167
168
169 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
170     WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
171     this._hostCountThreshold = hostCountThreshold;
172 }
173
174 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
175     doRun: function(resources, result, callback)
176     {
177         var summary = result.addChild("");
178         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined);
179         for (var domain in domainToResourcesMap) {
180             if (domainToResourcesMap[domain].length > 1)
181                 continue;
182             var parsedURL = domain.asParsedURL();
183             if (!parsedURL)
184                 continue;
185             if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
186                 continue; // an IP address
187             summary.addSnippet(match[2]);
188             result.violationCount++;
189         }
190         if (!summary.children || summary.children.length <= this._hostCountThreshold)
191             return callback(null);
192
193         summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
194         callback(result);
195     }
196 }
197
198 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
199
200
201 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
202 {
203     WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
204     this._optimalHostnameCount = optimalHostnameCount;
205     this._minRequestThreshold = minRequestThreshold;
206     this._minBalanceThreshold = minBalanceThreshold;
207 }
208
209
210 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
211     doRun: function(resources, result, callback)
212     {
213         function hostSorter(a, b)
214         {
215             var aCount = domainToResourcesMap[a].length;
216             var bCount = domainToResourcesMap[b].length;
217             return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
218         }
219
220         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
221             resources,
222             [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
223             true);
224
225         var hosts = [];
226         for (var url in domainToResourcesMap)
227             hosts.push(url);
228
229         if (!hosts.length)
230             return callback(null); // no hosts (local file or something)
231
232         hosts.sort(hostSorter);
233
234         var optimalHostnameCount = this._optimalHostnameCount;
235         if (hosts.length > optimalHostnameCount)
236             hosts.splice(optimalHostnameCount);
237
238         var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
239         var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
240         if (resourceCountAboveThreshold <= 0)
241             return callback(null);
242
243         var avgResourcesPerHost = 0;
244         for (var i = 0, size = hosts.length; i < size; ++i)
245             avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
246
247         // Assume optimal parallelization.
248         avgResourcesPerHost /= optimalHostnameCount;
249         avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
250
251         var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;
252         var minBalanceThreshold = this._minBalanceThreshold;
253         if (pctAboveAvg < minBalanceThreshold)
254             return callback(null);
255
256         var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
257         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);
258         for (var i = 0; i < resourcesOnBusiestHost.length; ++i)
259             entry.addURL(resourcesOnBusiestHost[i].url);
260
261         result.violationCount = resourcesOnBusiestHost.length;
262         callback(result);
263     }
264 }
265
266 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
267
268
269 // The reported CSS rule size is incorrect (parsed != original in WebKit),
270 // so use percentages instead, which gives a better approximation.
271 WebInspector.AuditRules.UnusedCssRule = function()
272 {
273     WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
274 }
275
276 WebInspector.AuditRules.UnusedCssRule.prototype = {
277     doRun: function(resources, result, callback)
278     {
279         var self = this;
280
281         function evalCallback(styleSheets) {
282             if (!styleSheets.length)
283                 return callback(null);
284
285             var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/;
286             var selectors = [];
287             var testedSelectors = {};
288             for (var i = 0; i < styleSheets.length; ++i) {
289                 var styleSheet = styleSheets[i];
290                 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
291                     var selectorText = styleSheet.rules[curRule].selectorText;
292                     if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText])
293                         continue;
294                     selectors.push(selectorText);
295                     testedSelectors[selectorText] = 1;
296                 }
297             }
298
299             function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
300             {
301                 var inlineBlockOrdinal = 0;
302                 var totalStylesheetSize = 0;
303                 var totalUnusedStylesheetSize = 0;
304                 var summary;
305
306                 for (var i = 0; i < styleSheets.length; ++i) {
307                     var styleSheet = styleSheets[i];
308                     var stylesheetSize = 0;
309                     var unusedStylesheetSize = 0;
310                     var unusedRules = [];
311                     for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
312                         var rule = styleSheet.rules[curRule];
313                         // Exact computation whenever source ranges are available.
314                         var textLength = (rule.selectorRange && rule.style.properties.endOffset) ? rule.style.properties.endOffset - rule.selectorRange.start + 1 : 0;
315                         if (!textLength && rule.style.cssText)
316                             textLength = rule.style.cssText.length + rule.selectorText.length;
317                         stylesheetSize += textLength;
318                         if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
319                             continue;
320                         unusedStylesheetSize += textLength;
321                         unusedRules.push(rule.selectorText);
322                     }
323                     totalStylesheetSize += stylesheetSize;
324                     totalUnusedStylesheetSize += unusedStylesheetSize;
325
326                     if (!unusedRules.length)
327                         continue;
328
329                     var resource = WebInspector.resourceTreeModel.resourceForURL(styleSheet.sourceURL);
330                     var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document;
331                     var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
332                     var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize);
333                     if (!summary)
334                         summary = result.addChild("", true);
335                     var entry = summary.addChild(String.sprintf("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused));
336
337                     for (var j = 0; j < unusedRules.length; ++j)
338                         entry.addSnippet(unusedRules[j]);
339
340                     result.violationCount += unusedRules.length;
341                 }
342
343                 if (!totalUnusedStylesheetSize)
344                     return callback(null);
345
346                 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
347                 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent);
348
349                 callback(result);
350             }
351
352             function routine(selectorArray)
353             {
354                 var result = {};
355                 for (var i = 0; i < selectorArray.length; ++i) {
356                     try {
357                         if (document.querySelector(selectorArray[i]))
358                             result[selectorArray[i]] = true;
359                     } catch(e) {
360                         // Ignore and mark as unused.
361                     }
362                 }
363                 return result;
364             }
365
366             WebInspector.AuditRules.evaluateInTargetWindow(routine, [selectors], selectorsCallback.bind(null, callback, styleSheets, testedSelectors));
367         }
368
369         function styleSheetCallback(styleSheets, continuation, styleSheet)
370         {
371             if (styleSheet)
372                 styleSheets.push(styleSheet);
373             if (continuation)
374                 continuation(styleSheets);
375         }
376
377         function allStylesCallback(styleSheetIds)
378         {
379             if (!styleSheetIds || !styleSheetIds.length)
380                 return evalCallback([]);
381             var styleSheets = [];
382             for (var i = 0; i < styleSheetIds.length; ++i)
383                 WebInspector.CSSStyleSheet.createForId(styleSheetIds[i], styleSheetCallback.bind(null, styleSheets, i == styleSheetIds.length - 1 ? evalCallback : null));
384         }
385
386         InspectorBackend.getAllStyles2(allStylesCallback);
387     }
388 }
389
390 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
391
392
393 WebInspector.AuditRules.CacheControlRule = function(id, name)
394 {
395     WebInspector.AuditRule.call(this, id, name);
396 }
397
398 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
399
400 WebInspector.AuditRules.CacheControlRule.prototype = {
401
402     doRun: function(resources, result, callback)
403     {
404         var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
405         if (cacheableAndNonCacheableResources[0].length)
406             this.runChecks(cacheableAndNonCacheableResources[0], result);
407         this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
408
409         callback(result);
410     },
411
412     handleNonCacheableResources: function()
413     {
414     },
415
416     _cacheableAndNonCacheableResources: function(resources)
417     {
418         var processedResources = [[], []];
419         for (var i = 0; i < resources.length; ++i) {
420             var resource = resources[i];
421             if (!this.isCacheableResource(resource))
422                 continue;
423             if (this._isExplicitlyNonCacheable(resource))
424                 processedResources[1].push(resource);
425             else
426                 processedResources[0].push(resource);
427         }
428         return processedResources;
429     },
430
431     execCheck: function(messageText, resourceCheckFunction, resources, result)
432     {
433         var resourceCount = resources.length;
434         var urls = [];
435         for (var i = 0; i < resourceCount; ++i) {
436             if (resourceCheckFunction.call(this, resources[i]))
437                 urls.push(resources[i].url);
438         }
439         if (urls.length) {
440             var entry = result.addChild(messageText, true);
441             entry.addURLs(urls);
442             result.violationCount += urls.length;
443         }
444     },
445
446     freshnessLifetimeGreaterThan: function(resource, timeMs)
447     {
448         var dateHeader = this.responseHeader(resource, "Date");
449         if (!dateHeader)
450             return false;
451
452         var dateHeaderMs = Date.parse(dateHeader);
453         if (isNaN(dateHeaderMs))
454             return false;
455
456         var freshnessLifetimeMs;
457         var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");
458
459         if (maxAgeMatch)
460             freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
461         else {
462             var expiresHeader = this.responseHeader(resource, "Expires");
463             if (expiresHeader) {
464                 var expDate = Date.parse(expiresHeader);
465                 if (!isNaN(expDate))
466                     freshnessLifetimeMs = expDate - dateHeaderMs;
467             }
468         }
469
470         return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
471     },
472
473     responseHeader: function(resource, header)
474     {
475         return resource.responseHeaders[header];
476     },
477
478     hasResponseHeader: function(resource, header)
479     {
480         return resource.responseHeaders[header] !== undefined;
481     },
482
483     isCompressible: function(resource)
484     {
485         return WebInspector.Resource.Type.isTextType(resource.type);
486     },
487
488     isPubliclyCacheable: function(resource)
489     {
490         if (this._isExplicitlyNonCacheable(resource))
491             return false;
492
493         if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
494             return true;
495
496         return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
497     },
498
499     responseHeaderMatch: function(resource, header, regexp)
500     {
501         return resource.responseHeaders[header]
502             ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
503             : undefined;
504     },
505
506     hasExplicitExpiration: function(resource)
507     {
508         return this.hasResponseHeader(resource, "Date") &&
509             (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
510     },
511
512     _isExplicitlyNonCacheable: function(resource)
513     {
514         var hasExplicitExp = this.hasExplicitExpiration(resource);
515         return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
516             this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
517             (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
518             (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
519             (!hasExplicitExp && !this.isCacheableResource(resource));
520     },
521
522     isCacheableResource: function(resource)
523     {
524         return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
525     }
526 }
527
528 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
529
530
531 WebInspector.AuditRules.BrowserCacheControlRule = function()
532 {
533     WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
534 }
535
536 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
537     handleNonCacheableResources: function(resources, result)
538     {
539         if (resources.length) {
540             var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
541             result.violationCount += resources.length;
542             for (var i = 0; i < resources.length; ++i)
543                 entry.addURL(resources[i].url);
544         }
545     },
546
547     runChecks: function(resources, result, callback)
548     {
549         this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
550             this._missingExpirationCheck, resources, result);
551         this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
552             this._varyCheck, resources, result);
553         this.execCheck("The following cacheable resources have a short freshness lifetime:",
554             this._oneMonthExpirationCheck, resources, result);
555
556         // Unable to implement the favicon check due to the WebKit limitations.
557         this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
558             this._oneYearExpirationCheck, resources, result);
559     },
560
561     _missingExpirationCheck: function(resource)
562     {
563         return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
564     },
565
566     _varyCheck: function(resource)
567     {
568         var varyHeader = this.responseHeader(resource, "Vary");
569         if (varyHeader) {
570             varyHeader = varyHeader.replace(/User-Agent/gi, "");
571             varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
572             varyHeader = varyHeader.replace(/[, ]*/g, "");
573         }
574         return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
575     },
576
577     _oneMonthExpirationCheck: function(resource)
578     {
579         return this.isCacheableResource(resource) &&
580             !this.hasResponseHeader(resource, "Set-Cookie") &&
581             !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
582             this.freshnessLifetimeGreaterThan(resource, 0);
583     },
584
585     _oneYearExpirationCheck: function(resource)
586     {
587         return this.isCacheableResource(resource) &&
588             !this.hasResponseHeader(resource, "Set-Cookie") &&
589             !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
590             this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
591     }
592 }
593
594 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
595
596
597 WebInspector.AuditRules.ProxyCacheControlRule = function() {
598     WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
599 }
600
601 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
602     runChecks: function(resources, result, callback)
603     {
604         this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
605             this._questionMarkCheck, resources, result);
606         this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
607             this._publicCachingCheck, resources, result);
608         this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
609             this._setCookieCacheableCheck, resources, result);
610     },
611
612     _questionMarkCheck: function(resource)
613     {
614         return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
615     },
616
617     _publicCachingCheck: function(resource)
618     {
619         return this.isCacheableResource(resource) &&
620             !this.isCompressible(resource) &&
621             !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
622             !this.hasResponseHeader(resource, "Set-Cookie");
623     },
624
625     _setCookieCacheableCheck: function(resource)
626     {
627         return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
628     }
629 }
630
631 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
632
633
634 WebInspector.AuditRules.ImageDimensionsRule = function()
635 {
636     WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
637 }
638
639 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
640     doRun: function(resources, result, callback)
641     {
642         function doneCallback(context)
643         {
644             var map = context.urlToNoDimensionCount;
645             for (var url in map) {
646                 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);
647                 var value = WebInspector.AuditRuleResult.linkifyDisplayName(url);
648                 if (map[url] > 1)
649                     value += String.sprintf(" (%d uses)", map[url]);
650                 entry.addChild(value);
651                 result.violationCount++;
652             }
653             callback(entry ? result : null);
654         }
655
656         function imageStylesReady(imageId, context, styles)
657         {
658             --context.imagesLeft;
659
660             const node = WebInspector.domAgent.nodeForId(imageId);
661             var src = node.getAttribute("src");
662             if (!src.asParsedURL()) {
663                 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
664                     if (frameOwnerCandidate.documentURL) {
665                         var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src);
666                         break;
667                     }
668                 }
669             }
670             if (completeSrc)
671                 src = completeSrc;
672
673             const computedStyle = styles.computedStyle;
674             if (computedStyle.getPropertyValue("position") === "absolute") {
675                 if (!context.imagesLeft)
676                     doneCallback(context);
677                 return;
678             }
679
680             var widthFound = "width" in styles.styleAttributes;
681             var heightFound = "height" in styles.styleAttributes;
682
683             for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
684                 var style = styles.matchedCSSRules[i].style;
685                 if (style.getPropertyValue("width") !== "")
686                     widthFound = true;
687                 if (style.getPropertyValue("height") !== "")
688                     heightFound = true;
689             }
690             
691             if (!widthFound || !heightFound) {
692                 if (src in context.urlToNoDimensionCount)
693                     ++context.urlToNoDimensionCount[src];
694                 else
695                     context.urlToNoDimensionCount[src] = 1;
696             }
697
698             if (!context.imagesLeft)
699                 doneCallback(context);
700         }
701
702         function receivedImages(imageIds)
703         {
704             if (!imageIds || !imageIds.length)
705                 return callback(null);
706             var context = {imagesLeft: imageIds.length, urlToNoDimensionCount: {}};
707             for (var i = imageIds.length - 1; i >= 0; --i)
708                 WebInspector.cssModel.getStylesAsync(imageIds[i], imageStylesReady.bind(this, imageIds[i], context));
709         }
710
711         function pushImageNodes()
712         {
713             const nodeIds = [];
714             var nodes = document.getElementsByTagName("img");
715             for (var i = 0; i < nodes.length; ++i) {
716                 if (!nodes[i].src)
717                     continue;
718                 var nodeId = this.getNodeId(nodes[i]);
719                 nodeIds.push(nodeId);
720             }
721             return nodeIds;
722         }
723
724         WebInspector.AuditRules.evaluateInTargetWindow(pushImageNodes, null, receivedImages);
725     }
726 }
727
728 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
729
730
731 WebInspector.AuditRules.CssInHeadRule = function()
732 {
733     WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
734 }
735
736 WebInspector.AuditRules.CssInHeadRule.prototype = {
737     doRun: function(resources, result, callback)
738     {
739         function evalCallback(evalResult)
740         {
741             if (!evalResult)
742                 return callback(null);
743
744             var summary = result.addChild("");
745
746             var outputMessages = [];
747             for (var url in evalResult) {
748                 var urlViolations = evalResult[url];
749                 if (urlViolations[0]) {
750                     result.addChild(String.sprintf("%s style block(s) in the %s body should be moved to the document head.", urlViolations[0], WebInspector.AuditRuleResult.linkifyDisplayName(url)));
751                     result.violationCount += urlViolations[0];
752                 }
753                 for (var i = 0; i < urlViolations[1].length; ++i)
754                     result.addChild(String.sprintf("Link node %s should be moved to the document head in %s", WebInspector.AuditRuleResult.linkifyDisplayName(urlViolations[1][i]), WebInspector.AuditRuleResult.linkifyDisplayName(url)));
755                 result.violationCount += urlViolations[1].length;
756             }
757             summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
758             callback(result);
759         }
760
761         function routine()
762         {
763             function allViews() {
764                 var views = [document.defaultView];
765                 var curView = 0;
766                 while (curView < views.length) {
767                     var view = views[curView];
768                     var frames = view.frames;
769                     for (var i = 0; i < frames.length; ++i) {
770                         if (frames[i] !== view)
771                             views.push(frames[i]);
772                     }
773                     ++curView;
774                 }
775                 return views;
776             }
777
778             var views = allViews();
779             var urlToViolationsArray = {};
780             var found = false;
781             for (var i = 0; i < views.length; ++i) {
782                 var view = views[i];
783                 if (!view.document)
784                     continue;
785
786                 var inlineStyles = view.document.querySelectorAll("body style");
787                 var inlineStylesheets = view.document.querySelectorAll("body link[rel~='stylesheet'][href]");
788                 if (!inlineStyles.length && !inlineStylesheets.length)
789                     continue;
790
791                 found = true;
792                 var inlineStylesheetHrefs = [];
793                 for (var j = 0; j < inlineStylesheets.length; ++j)
794                     inlineStylesheetHrefs.push(inlineStylesheets[j].href);
795                 urlToViolationsArray[view.location.href] = [inlineStyles.length, inlineStylesheetHrefs];
796             }
797             return found ? urlToViolationsArray : null;
798         }
799
800         WebInspector.AuditRules.evaluateInTargetWindow(routine, null, evalCallback);
801     }
802 }
803
804 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
805
806
807 WebInspector.AuditRules.StylesScriptsOrderRule = function()
808 {
809     WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
810 }
811
812 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
813     doRun: function(resources, result, callback)
814     {
815         function evalCallback(resultValue)
816         {
817             if (!resultValue)
818                 return callback(null);
819
820             var lateCssUrls = resultValue[0];
821             var cssBeforeInlineCount = resultValue[1];
822
823             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);
824             entry.addURLs(lateCssUrls);
825             result.violationCount += lateCssUrls.length;
826
827             if (cssBeforeInlineCount) {
828                 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"));
829                 result.violationCount += cssBeforeInlineCount;
830             }
831             callback(result);
832         }
833
834         function routine()
835         {
836             var lateStyles = document.querySelectorAll("head script[src] ~ link[rel~='stylesheet'][href]");
837             var cssBeforeInlineCount = document.querySelectorAll("head link[rel~='stylesheet'][href] ~ script:not([src])").length;
838             if (!lateStyles.length && !cssBeforeInlineCount)
839                 return null;
840
841             var lateStyleUrls = [];
842             for (var i = 0; i < lateStyles.length; ++i)
843                 lateStyleUrls.push(lateStyles[i].href);
844             return [ lateStyleUrls, cssBeforeInlineCount ];
845         }
846
847         WebInspector.AuditRules.evaluateInTargetWindow(routine, null, evalCallback.bind(this));
848     }
849 }
850
851 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
852
853
854 WebInspector.AuditRules.CookieRuleBase = function(id, name)
855 {
856     WebInspector.AuditRule.call(this, id, name);
857 }
858
859 WebInspector.AuditRules.CookieRuleBase.prototype = {
860     doRun: function(resources, result, callback)
861     {
862         var self = this;
863         function resultCallback(receivedCookies, isAdvanced) {
864             self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
865             callback(result);
866         }
867         WebInspector.Cookies.getCookiesAsync(resultCallback);
868     },
869
870     mapResourceCookies: function(resourcesByDomain, allCookies, callback)
871     {
872         for (var i = 0; i < allCookies.length; ++i) {
873             for (var resourceDomain in resourcesByDomain) {
874                 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
875                     this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
876             }
877         }
878     },
879
880     _callbackForResourceCookiePairs: function(resources, cookie, callback)
881     {
882         if (!resources)
883             return;
884         for (var i = 0; i < resources.length; ++i) {
885             if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
886                 callback(resources[i], cookie);
887         }
888     }
889 }
890
891 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;
892
893
894 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
895 {
896     WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
897     this._avgBytesThreshold = avgBytesThreshold;
898     this._maxBytesThreshold = 1000;
899 }
900
901 WebInspector.AuditRules.CookieSizeRule.prototype = {
902     _average: function(cookieArray)
903     {
904         var total = 0;
905         for (var i = 0; i < cookieArray.length; ++i)
906             total += cookieArray[i].size;
907         return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
908     },
909
910     _max: function(cookieArray)
911     {
912         var result = 0;
913         for (var i = 0; i < cookieArray.length; ++i)
914             result = Math.max(cookieArray[i].size, result);
915         return result;
916     },
917
918     processCookies: function(allCookies, resources, result)
919     {
920         function maxSizeSorter(a, b)
921         {
922             return b.maxCookieSize - a.maxCookieSize;
923         }
924
925         function avgSizeSorter(a, b)
926         {
927             return b.avgCookieSize - a.avgCookieSize;
928         }
929
930         var cookiesPerResourceDomain = {};
931
932         function collectorCallback(resource, cookie)
933         {
934             var cookies = cookiesPerResourceDomain[resource.domain];
935             if (!cookies) {
936                 cookies = [];
937                 cookiesPerResourceDomain[resource.domain] = cookies;
938             }
939             cookies.push(cookie);
940         }
941
942         if (!allCookies.length)
943             return;
944
945         var sortedCookieSizes = [];
946
947         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
948                 null,
949                 true);
950         var matchingResourceData = {};
951         this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
952
953         for (var resourceDomain in cookiesPerResourceDomain) {
954             var cookies = cookiesPerResourceDomain[resourceDomain];
955             sortedCookieSizes.push({
956                 domain: resourceDomain,
957                 avgCookieSize: this._average(cookies),
958                 maxCookieSize: this._max(cookies)
959             });
960         }
961         var avgAllCookiesSize = this._average(allCookies);
962
963         var hugeCookieDomains = [];
964         sortedCookieSizes.sort(maxSizeSorter);
965
966         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
967             var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
968             if (maxCookieSize > this._maxBytesThreshold)
969                 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
970         }
971
972         var bigAvgCookieDomains = [];
973         sortedCookieSizes.sort(avgSizeSorter);
974         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
975             var domain = sortedCookieSizes[i].domain;
976             var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
977             if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
978                 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
979         }
980         result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
981
982         var message;
983         if (hugeCookieDomains.length) {
984             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);
985             entry.addURLs(hugeCookieDomains);
986             result.violationCount += hugeCookieDomains.length;
987         }
988
989         if (bigAvgCookieDomains.length) {
990             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);
991             entry.addURLs(bigAvgCookieDomains);
992             result.violationCount += bigAvgCookieDomains.length;
993         }
994     }
995 }
996
997 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
998
999
1000 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1001 {
1002     WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1003     this._minResources = minResources;
1004 }
1005
1006 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1007     processCookies: function(allCookies, resources, result)
1008     {
1009         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1010                 [WebInspector.Resource.Type.Stylesheet,
1011                  WebInspector.Resource.Type.Image],
1012                 true);
1013         var totalStaticResources = 0;
1014         for (var domain in domainToResourcesMap)
1015             totalStaticResources += domainToResourcesMap[domain].length;
1016         if (totalStaticResources < this._minResources)
1017             return;
1018         var matchingResourceData = {};
1019         this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1020
1021         var badUrls = [];
1022         var cookieBytes = 0;
1023         for (var url in matchingResourceData) {
1024             badUrls.push(url);
1025             cookieBytes += matchingResourceData[url]
1026         }
1027         if (badUrls.length < this._minResources)
1028             return;
1029
1030         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);
1031         entry.addURLs(badUrls);
1032         result.violationCount = badUrls.length;
1033     },
1034
1035     _collectorCallback: function(matchingResourceData, resource, cookie)
1036     {
1037         matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
1038     }
1039 }
1040
1041 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;