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