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