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