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|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.cached || request.statusCode === 304) 97 continue; // Do not test cached resources. 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 /** 342 * @param {!Array.<!WebInspector.CSSStyleSheet>} styleSheets 343 */ 344 function evalCallback(styleSheets) { 345 if (progress.isCanceled()) 346 return; 347 348 if (!styleSheets.length) 349 return callback(null); 350 351 var selectors = []; 352 var testedSelectors = {}; 353 for (var i = 0; i < styleSheets.length; ++i) { 354 var styleSheet = styleSheets[i]; 355 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) { 356 var selectorText = styleSheet.rules[curRule].selectorText; 357 if (testedSelectors[selectorText]) 358 continue; 359 selectors.push(selectorText); 360 testedSelectors[selectorText] = 1; 361 } 362 } 363 364 var foundSelectors = {}; 365 366 /** 367 * @param {!Array.<!WebInspector.CSSStyleSheet>} styleSheets 368 */ 369 function selectorsCallback(styleSheets) 370 { 371 if (progress.isCanceled()) 372 return; 373 374 var inlineBlockOrdinal = 0; 375 var totalStylesheetSize = 0; 376 var totalUnusedStylesheetSize = 0; 377 var summary; 378 379 for (var i = 0; i < styleSheets.length; ++i) { 380 var styleSheet = styleSheets[i]; 381 var unusedRules = []; 382 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) { 383 var rule = styleSheet.rules[curRule]; 384 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText]) 385 continue; 386 unusedRules.push(rule.selectorText); 387 } 388 totalStylesheetSize += styleSheet.rules.length; 389 totalUnusedStylesheetSize += unusedRules.length; 390 391 if (!unusedRules.length) 392 continue; 393 394 var resource = WebInspector.resourceForURL(styleSheet.sourceURL); 395 var isInlineBlock = resource && resource.request && resource.request.type == WebInspector.resourceTypes.Document; 396 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal); 397 var pctUnused = Math.round(100 * unusedRules.length / styleSheet.rules.length); 398 if (!summary) 399 summary = result.addChild("", true); 400 var entry = summary.addFormatted("%s: %d% is not used by the current page.", url, pctUnused); 401 402 for (var j = 0; j < unusedRules.length; ++j) 403 entry.addSnippet(unusedRules[j]); 404 405 result.violationCount += unusedRules.length; 406 } 407 408 if (!totalUnusedStylesheetSize) 409 return callback(null); 410 411 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize); 412 summary.value = String.sprintf("%s rules (%d%) of CSS not used by the current page.", totalUnusedStylesheetSize, totalUnusedPercent); 413 414 callback(result); 415 } 416 417 /** 418 * @param {?function()} boundSelectorsCallback 419 * @param {string} selector 420 * @param {?DOMAgent.NodeId} nodeId 421 */ 422 function queryCallback(boundSelectorsCallback, selector, nodeId) 423 { 424 if (nodeId) 425 foundSelectors[selector] = true; 426 if (boundSelectorsCallback) 427 boundSelectorsCallback(); 428 } 429 430 /** 431 * @param {!Array.<string>} selectors 432 * @param {!WebInspector.DOMDocument} document 433 */ 434 function documentLoaded(selectors, document) { 435 var pseudoSelectorRegexp = /::?(?:[\w-]+)(?:\(.*?\))?/g; 436 if (!selectors.length) { 437 selectorsCallback([]); 438 return; 439 } 440 for (var i = 0; i < selectors.length; ++i) { 441 if (progress.isCanceled()) 442 return; 443 var effectiveSelector = selectors[i].replace(pseudoSelectorRegexp, ""); 444 WebInspector.domAgent.querySelector(document.id, effectiveSelector, queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, styleSheets) : null, selectors[i])); 445 } 446 } 447 448 WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors)); 449 } 450 451 /** 452 * @param {!Array.<!WebInspector.CSSStyleSheet>} styleSheets 453 * @param {string} sourceURL 454 * @param {?function(!Array.<!WebInspector.CSSStyleSheet>)} continuation 455 * @param {?WebInspector.CSSStyleSheet} styleSheet 456 */ 457 function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet) 458 { 459 if (progress.isCanceled()) 460 return; 461 462 if (styleSheet) { 463 styleSheet.sourceURL = sourceURL; 464 styleSheets.push(styleSheet); 465 } 466 if (continuation) 467 continuation(styleSheets); 468 } 469 470 /** 471 * @param {?Protocol.Error} error 472 * @param {!Array.<!CSSAgent.CSSStyleSheetHeader>} styleSheetInfos 473 */ 474 function allStylesCallback(error, styleSheetInfos) 475 { 476 if (progress.isCanceled()) 477 return; 478 479 if (error || !styleSheetInfos || !styleSheetInfos.length) 480 return evalCallback([]); 481 var styleSheets = []; 482 for (var i = 0; i < styleSheetInfos.length; ++i) { 483 var info = styleSheetInfos[i]; 484 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null)); 485 } 486 } 487 488 CSSAgent.getAllStyleSheets(allStylesCallback); 489 }, 490 491 __proto__: WebInspector.AuditRule.prototype 492} 493 494/** 495 * @constructor 496 * @extends {WebInspector.AuditRule} 497 */ 498WebInspector.AuditRules.CacheControlRule = function(id, name) 499{ 500 WebInspector.AuditRule.call(this, id, name); 501} 502 503WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30; 504 505WebInspector.AuditRules.CacheControlRule.prototype = { 506 /** 507 * @param {!Array.<!WebInspector.NetworkRequest>} requests 508 * @param {!WebInspector.AuditRuleResult} result 509 * @param {function(!WebInspector.AuditRuleResult)} callback 510 * @param {!WebInspector.Progress} progress 511 */ 512 doRun: function(requests, result, callback, progress) 513 { 514 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(requests); 515 if (cacheableAndNonCacheableResources[0].length) 516 this.runChecks(cacheableAndNonCacheableResources[0], result); 517 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result); 518 519 callback(result); 520 }, 521 522 handleNonCacheableResources: function(requests, result) 523 { 524 }, 525 526 _cacheableAndNonCacheableResources: function(requests) 527 { 528 var processedResources = [[], []]; 529 for (var i = 0; i < requests.length; ++i) { 530 var request = requests[i]; 531 if (!this.isCacheableResource(request)) 532 continue; 533 if (this._isExplicitlyNonCacheable(request)) 534 processedResources[1].push(request); 535 else 536 processedResources[0].push(request); 537 } 538 return processedResources; 539 }, 540 541 execCheck: function(messageText, requestCheckFunction, requests, result) 542 { 543 var requestCount = requests.length; 544 var urls = []; 545 for (var i = 0; i < requestCount; ++i) { 546 if (requestCheckFunction.call(this, requests[i])) 547 urls.push(requests[i].url); 548 } 549 if (urls.length) { 550 var entry = result.addChild(messageText, true); 551 entry.addURLs(urls); 552 result.violationCount += urls.length; 553 } 554 }, 555 556 freshnessLifetimeGreaterThan: function(request, timeMs) 557 { 558 var dateHeader = this.responseHeader(request, "Date"); 559 if (!dateHeader) 560 return false; 561 562 var dateHeaderMs = Date.parse(dateHeader); 563 if (isNaN(dateHeaderMs)) 564 return false; 565 566 var freshnessLifetimeMs; 567 var maxAgeMatch = this.responseHeaderMatch(request, "Cache-Control", "max-age=(\\d+)"); 568 569 if (maxAgeMatch) 570 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0; 571 else { 572 var expiresHeader = this.responseHeader(request, "Expires"); 573 if (expiresHeader) { 574 var expDate = Date.parse(expiresHeader); 575 if (!isNaN(expDate)) 576 freshnessLifetimeMs = expDate - dateHeaderMs; 577 } 578 } 579 580 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs; 581 }, 582 583 responseHeader: function(request, header) 584 { 585 return request.responseHeaderValue(header); 586 }, 587 588 hasResponseHeader: function(request, header) 589 { 590 return request.responseHeaderValue(header) !== undefined; 591 }, 592 593 isCompressible: function(request) 594 { 595 return request.type.isTextType(); 596 }, 597 598 isPubliclyCacheable: function(request) 599 { 600 if (this._isExplicitlyNonCacheable(request)) 601 return false; 602 603 if (this.responseHeaderMatch(request, "Cache-Control", "public")) 604 return true; 605 606 return request.url.indexOf("?") == -1 && !this.responseHeaderMatch(request, "Cache-Control", "private"); 607 }, 608 609 responseHeaderMatch: function(request, header, regexp) 610 { 611 return request.responseHeaderValue(header) 612 ? request.responseHeaderValue(header).match(new RegExp(regexp, "im")) 613 : undefined; 614 }, 615 616 hasExplicitExpiration: function(request) 617 { 618 return this.hasResponseHeader(request, "Date") && 619 (this.hasResponseHeader(request, "Expires") || this.responseHeaderMatch(request, "Cache-Control", "max-age")); 620 }, 621 622 _isExplicitlyNonCacheable: function(request) 623 { 624 var hasExplicitExp = this.hasExplicitExpiration(request); 625 return this.responseHeaderMatch(request, "Cache-Control", "(no-cache|no-store|must-revalidate)") || 626 this.responseHeaderMatch(request, "Pragma", "no-cache") || 627 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(request, 0)) || 628 (!hasExplicitExp && request.url && request.url.indexOf("?") >= 0) || 629 (!hasExplicitExp && !this.isCacheableResource(request)); 630 }, 631 632 isCacheableResource: function(request) 633 { 634 return request.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[request.statusCode]; 635 }, 636 637 __proto__: WebInspector.AuditRule.prototype 638} 639 640/** 641 * @constructor 642 * @extends {WebInspector.AuditRules.CacheControlRule} 643 */ 644WebInspector.AuditRules.BrowserCacheControlRule = function() 645{ 646 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching"); 647} 648 649WebInspector.AuditRules.BrowserCacheControlRule.prototype = { 650 handleNonCacheableResources: function(requests, result) 651 { 652 if (requests.length) { 653 var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true); 654 result.violationCount += requests.length; 655 for (var i = 0; i < requests.length; ++i) 656 entry.addURL(requests[i].url); 657 } 658 }, 659 660 runChecks: function(requests, result, callback) 661 { 662 this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:", 663 this._missingExpirationCheck, requests, result); 664 this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:", 665 this._varyCheck, requests, result); 666 this.execCheck("The following cacheable resources have a short freshness lifetime:", 667 this._oneMonthExpirationCheck, requests, result); 668 669 // Unable to implement the favicon check due to the WebKit limitations. 670 this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:", 671 this._oneYearExpirationCheck, requests, result); 672 }, 673 674 _missingExpirationCheck: function(request) 675 { 676 return this.isCacheableResource(request) && !this.hasResponseHeader(request, "Set-Cookie") && !this.hasExplicitExpiration(request); 677 }, 678 679 _varyCheck: function(request) 680 { 681 var varyHeader = this.responseHeader(request, "Vary"); 682 if (varyHeader) { 683 varyHeader = varyHeader.replace(/User-Agent/gi, ""); 684 varyHeader = varyHeader.replace(/Accept-Encoding/gi, ""); 685 varyHeader = varyHeader.replace(/[, ]*/g, ""); 686 } 687 return varyHeader && varyHeader.length && this.isCacheableResource(request) && this.freshnessLifetimeGreaterThan(request, 0); 688 }, 689 690 _oneMonthExpirationCheck: function(request) 691 { 692 return this.isCacheableResource(request) && 693 !this.hasResponseHeader(request, "Set-Cookie") && 694 !this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) && 695 this.freshnessLifetimeGreaterThan(request, 0); 696 }, 697 698 _oneYearExpirationCheck: function(request) 699 { 700 return this.isCacheableResource(request) && 701 !this.hasResponseHeader(request, "Set-Cookie") && 702 !this.freshnessLifetimeGreaterThan(request, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) && 703 this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth); 704 }, 705 706 __proto__: WebInspector.AuditRules.CacheControlRule.prototype 707} 708 709/** 710 * @constructor 711 * @extends {WebInspector.AuditRules.CacheControlRule} 712 */ 713WebInspector.AuditRules.ProxyCacheControlRule = function() { 714 WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching"); 715} 716 717WebInspector.AuditRules.ProxyCacheControlRule.prototype = { 718 runChecks: function(requests, result, callback) 719 { 720 this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:", 721 this._questionMarkCheck, requests, result); 722 this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:", 723 this._publicCachingCheck, requests, result); 724 this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.", 725 this._setCookieCacheableCheck, requests, result); 726 }, 727 728 _questionMarkCheck: function(request) 729 { 730 return request.url.indexOf("?") >= 0 && !this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request); 731 }, 732 733 _publicCachingCheck: function(request) 734 { 735 return this.isCacheableResource(request) && 736 !this.isCompressible(request) && 737 !this.responseHeaderMatch(request, "Cache-Control", "public") && 738 !this.hasResponseHeader(request, "Set-Cookie"); 739 }, 740 741 _setCookieCacheableCheck: function(request) 742 { 743 return this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request); 744 }, 745 746 __proto__: WebInspector.AuditRules.CacheControlRule.prototype 747} 748 749/** 750 * @constructor 751 * @extends {WebInspector.AuditRule} 752 */ 753WebInspector.AuditRules.ImageDimensionsRule = function() 754{ 755 WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions"); 756} 757 758WebInspector.AuditRules.ImageDimensionsRule.prototype = { 759 /** 760 * @param {!Array.<!WebInspector.NetworkRequest>} requests 761 * @param {!WebInspector.AuditRuleResult} result 762 * @param {function(?WebInspector.AuditRuleResult)} callback 763 * @param {!WebInspector.Progress} progress 764 */ 765 doRun: function(requests, result, callback, progress) 766 { 767 var urlToNoDimensionCount = {}; 768 769 function doneCallback() 770 { 771 for (var url in urlToNoDimensionCount) { 772 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); 773 var format = "%r"; 774 if (urlToNoDimensionCount[url] > 1) 775 format += " (%d uses)"; 776 entry.addFormatted(format, url, urlToNoDimensionCount[url]); 777 result.violationCount++; 778 } 779 callback(entry ? result : null); 780 } 781 782 function imageStylesReady(imageId, styles, isLastStyle, computedStyle) 783 { 784 if (progress.isCanceled()) 785 return; 786 787 const node = WebInspector.domAgent.nodeForId(imageId); 788 var src = node.getAttribute("src"); 789 if (!src.asParsedURL()) { 790 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) { 791 if (frameOwnerCandidate.baseURL) { 792 var completeSrc = WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, src); 793 break; 794 } 795 } 796 } 797 if (completeSrc) 798 src = completeSrc; 799 800 if (computedStyle.getPropertyValue("position") === "absolute") { 801 if (isLastStyle) 802 doneCallback(); 803 return; 804 } 805 806 if (styles.attributesStyle) { 807 var widthFound = !!styles.attributesStyle.getLiveProperty("width"); 808 var heightFound = !!styles.attributesStyle.getLiveProperty("height"); 809 } 810 811 var inlineStyle = styles.inlineStyle; 812 if (inlineStyle) { 813 if (inlineStyle.getPropertyValue("width") !== "") 814 widthFound = true; 815 if (inlineStyle.getPropertyValue("height") !== "") 816 heightFound = true; 817 } 818 819 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) { 820 var style = styles.matchedCSSRules[i].style; 821 if (style.getPropertyValue("width") !== "") 822 widthFound = true; 823 if (style.getPropertyValue("height") !== "") 824 heightFound = true; 825 } 826 827 if (!widthFound || !heightFound) { 828 if (src in urlToNoDimensionCount) 829 ++urlToNoDimensionCount[src]; 830 else 831 urlToNoDimensionCount[src] = 1; 832 } 833 834 if (isLastStyle) 835 doneCallback(); 836 } 837 838 /** 839 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds 840 */ 841 function getStyles(nodeIds) 842 { 843 if (progress.isCanceled()) 844 return; 845 var targetResult = {}; 846 847 function inlineCallback(inlineStyle, attributesStyle) 848 { 849 targetResult.inlineStyle = inlineStyle; 850 targetResult.attributesStyle = attributesStyle; 851 } 852 853 function matchedCallback(result) 854 { 855 if (result) 856 targetResult.matchedCSSRules = result.matchedCSSRules; 857 } 858 859 if (!nodeIds || !nodeIds.length) 860 doneCallback(); 861 862 for (var i = 0; nodeIds && i < nodeIds.length; ++i) { 863 WebInspector.cssModel.getMatchedStylesAsync(nodeIds[i], false, false, matchedCallback); 864 WebInspector.cssModel.getInlineStylesAsync(nodeIds[i], inlineCallback); 865 WebInspector.cssModel.getComputedStyleAsync(nodeIds[i], imageStylesReady.bind(null, nodeIds[i], targetResult, i === nodeIds.length - 1)); 866 } 867 } 868 869 function onDocumentAvailable(root) 870 { 871 if (progress.isCanceled()) 872 return; 873 WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles); 874 } 875 876 if (progress.isCanceled()) 877 return; 878 WebInspector.domAgent.requestDocument(onDocumentAvailable); 879 }, 880 881 __proto__: WebInspector.AuditRule.prototype 882} 883 884/** 885 * @constructor 886 * @extends {WebInspector.AuditRule} 887 */ 888WebInspector.AuditRules.CssInHeadRule = function() 889{ 890 WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head"); 891} 892 893WebInspector.AuditRules.CssInHeadRule.prototype = { 894 /** 895 * @param {!Array.<!WebInspector.NetworkRequest>} requests 896 * @param {!WebInspector.AuditRuleResult} result 897 * @param {function(?WebInspector.AuditRuleResult)} callback 898 * @param {!WebInspector.Progress} progress 899 */ 900 doRun: function(requests, result, callback, progress) 901 { 902 function evalCallback(evalResult) 903 { 904 if (progress.isCanceled()) 905 return; 906 907 if (!evalResult) 908 return callback(null); 909 910 var summary = result.addChild(""); 911 912 var outputMessages = []; 913 for (var url in evalResult) { 914 var urlViolations = evalResult[url]; 915 if (urlViolations[0]) { 916 result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url); 917 result.violationCount += urlViolations[0]; 918 } 919 for (var i = 0; i < urlViolations[1].length; ++i) 920 result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url); 921 result.violationCount += urlViolations[1].length; 922 } 923 summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance."); 924 callback(result); 925 } 926 927 /** 928 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds 929 */ 930 function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds) 931 { 932 if (progress.isCanceled()) 933 return; 934 935 if (!nodeIds) 936 return; 937 var externalStylesheetNodeIds = nodeIds; 938 var result = null; 939 if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) { 940 var urlToViolationsArray = {}; 941 var externalStylesheetHrefs = []; 942 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) { 943 var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]); 944 var completeHref = WebInspector.ParsedURL.completeURL(linkNode.ownerDocument.baseURL, linkNode.getAttribute("href")); 945 externalStylesheetHrefs.push(completeHref || "<empty>"); 946 } 947 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs]; 948 result = urlToViolationsArray; 949 } 950 evalCallback(result); 951 } 952 953 /** 954 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds 955 */ 956 function inlineStylesReceived(root, nodeIds) 957 { 958 if (progress.isCanceled()) 959 return; 960 961 if (!nodeIds) 962 return; 963 WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds)); 964 } 965 966 function onDocumentAvailable(root) 967 { 968 if (progress.isCanceled()) 969 return; 970 971 WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root)); 972 } 973 974 WebInspector.domAgent.requestDocument(onDocumentAvailable); 975 }, 976 977 __proto__: WebInspector.AuditRule.prototype 978} 979 980/** 981 * @constructor 982 * @extends {WebInspector.AuditRule} 983 */ 984WebInspector.AuditRules.StylesScriptsOrderRule = function() 985{ 986 WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts"); 987} 988 989WebInspector.AuditRules.StylesScriptsOrderRule.prototype = { 990 /** 991 * @param {!Array.<!WebInspector.NetworkRequest>} requests 992 * @param {!WebInspector.AuditRuleResult} result 993 * @param {function(?WebInspector.AuditRuleResult)} callback 994 * @param {!WebInspector.Progress} progress 995 */ 996 doRun: function(requests, result, callback, progress) 997 { 998 function evalCallback(resultValue) 999 { 1000 if (progress.isCanceled()) 1001 return; 1002 1003 if (!resultValue) 1004 return callback(null); 1005 1006 var lateCssUrls = resultValue[0]; 1007 var cssBeforeInlineCount = resultValue[1]; 1008 1009 if (lateCssUrls.length) { 1010 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); 1011 entry.addURLs(lateCssUrls); 1012 result.violationCount += lateCssUrls.length; 1013 } 1014 1015 if (cssBeforeInlineCount) { 1016 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")); 1017 result.violationCount += cssBeforeInlineCount; 1018 } 1019 callback(result); 1020 } 1021 1022 /** 1023 * @param {!Array.<!DOMAgent.NodeId>} lateStyleIds 1024 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds 1025 */ 1026 function cssBeforeInlineReceived(lateStyleIds, nodeIds) 1027 { 1028 if (progress.isCanceled()) 1029 return; 1030 1031 if (!nodeIds) 1032 return; 1033 1034 var cssBeforeInlineCount = nodeIds.length; 1035 var result = null; 1036 if (lateStyleIds.length || cssBeforeInlineCount) { 1037 var lateStyleUrls = []; 1038 for (var i = 0; i < lateStyleIds.length; ++i) { 1039 var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]); 1040 var completeHref = WebInspector.ParsedURL.completeURL(lateStyleNode.ownerDocument.baseURL, lateStyleNode.getAttribute("href")); 1041 lateStyleUrls.push(completeHref || "<empty>"); 1042 } 1043 result = [ lateStyleUrls, cssBeforeInlineCount ]; 1044 } 1045 1046 evalCallback(result); 1047 } 1048 1049 /** 1050 * @param {!WebInspector.DOMDocument} root 1051 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds 1052 */ 1053 function lateStylesReceived(root, nodeIds) 1054 { 1055 if (progress.isCanceled()) 1056 return; 1057 1058 if (!nodeIds) 1059 return; 1060 1061 WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds)); 1062 } 1063 1064 /** 1065 * @param {!WebInspector.DOMDocument} root 1066 */ 1067 function onDocumentAvailable(root) 1068 { 1069 if (progress.isCanceled()) 1070 return; 1071 1072 WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root)); 1073 } 1074 1075 WebInspector.domAgent.requestDocument(onDocumentAvailable); 1076 }, 1077 1078 __proto__: WebInspector.AuditRule.prototype 1079} 1080 1081/** 1082 * @constructor 1083 * @extends {WebInspector.AuditRule} 1084 */ 1085WebInspector.AuditRules.CSSRuleBase = function(id, name) 1086{ 1087 WebInspector.AuditRule.call(this, id, name); 1088} 1089 1090WebInspector.AuditRules.CSSRuleBase.prototype = { 1091 /** 1092 * @param {!Array.<!WebInspector.NetworkRequest>} requests 1093 * @param {!WebInspector.AuditRuleResult} result 1094 * @param {function(?WebInspector.AuditRuleResult)} callback 1095 * @param {!WebInspector.Progress} progress 1096 */ 1097 doRun: function(requests, result, callback, progress) 1098 { 1099 CSSAgent.getAllStyleSheets(sheetsCallback.bind(this)); 1100 1101 /** 1102 * @param {?Protocol.Error} error 1103 * @param {!Array.<!CSSAgent.CSSStyleSheetHeader>} headers 1104 * @this {WebInspector.AuditRules.CSSRuleBase} 1105 */ 1106 function sheetsCallback(error, headers) 1107 { 1108 if (error) 1109 return callback(null); 1110 1111 if (!headers.length) 1112 return callback(null); 1113 for (var i = 0; i < headers.length; ++i) { 1114 var header = headers[i]; 1115 if (header.disabled) 1116 continue; // Do not check disabled stylesheets. 1117 1118 this._visitStyleSheet(header.styleSheetId, i === headers.length - 1 ? finishedCallback : null, result, progress); 1119 } 1120 } 1121 1122 function finishedCallback() 1123 { 1124 callback(result); 1125 } 1126 }, 1127 1128 _visitStyleSheet: function(styleSheetId, callback, result, progress) 1129 { 1130 WebInspector.CSSStyleSheet.createForId(styleSheetId, sheetCallback.bind(this)); 1131 1132 /** 1133 * @param {?WebInspector.CSSStyleSheet} styleSheet 1134 * @this {WebInspector.AuditRules.CSSRuleBase} 1135 */ 1136 function sheetCallback(styleSheet) 1137 { 1138 if (progress.isCanceled()) 1139 return; 1140 1141 if (!styleSheet) { 1142 if (callback) 1143 callback(); 1144 return; 1145 } 1146 1147 this.visitStyleSheet(styleSheet, result); 1148 1149 for (var i = 0; i < styleSheet.rules.length; ++i) 1150 this._visitRule(styleSheet, styleSheet.rules[i], result); 1151 1152 this.didVisitStyleSheet(styleSheet, result); 1153 1154 if (callback) 1155 callback(); 1156 } 1157 }, 1158 1159 _visitRule: function(styleSheet, rule, result) 1160 { 1161 this.visitRule(styleSheet, rule, result); 1162 var allProperties = rule.style.allProperties; 1163 for (var i = 0; i < allProperties.length; ++i) 1164 this.visitProperty(styleSheet, allProperties[i], result); 1165 this.didVisitRule(styleSheet, rule, result); 1166 }, 1167 1168 visitStyleSheet: function(styleSheet, result) 1169 { 1170 // Subclasses can implement. 1171 }, 1172 1173 didVisitStyleSheet: function(styleSheet, result) 1174 { 1175 // Subclasses can implement. 1176 }, 1177 1178 visitRule: function(styleSheet, rule, result) 1179 { 1180 // Subclasses can implement. 1181 }, 1182 1183 didVisitRule: function(styleSheet, rule, result) 1184 { 1185 // Subclasses can implement. 1186 }, 1187 1188 visitProperty: function(styleSheet, property, result) 1189 { 1190 // Subclasses can implement. 1191 }, 1192 1193 __proto__: WebInspector.AuditRule.prototype 1194} 1195 1196/** 1197 * @constructor 1198 * @extends {WebInspector.AuditRules.CSSRuleBase} 1199 */ 1200WebInspector.AuditRules.VendorPrefixedCSSProperties = function() 1201{ 1202 WebInspector.AuditRules.CSSRuleBase.call(this, "page-vendorprefixedcss", "Use normal CSS property names instead of vendor-prefixed ones"); 1203 this._webkitPrefix = "-webkit-"; 1204} 1205 1206WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties = [ 1207 "background-clip", "background-origin", "background-size", 1208 "border-radius", "border-bottom-left-radius", "border-bottom-right-radius", "border-top-left-radius", "border-top-right-radius", 1209 "box-shadow", "box-sizing", "opacity", "text-shadow" 1210].keySet(); 1211 1212WebInspector.AuditRules.VendorPrefixedCSSProperties.prototype = { 1213 didVisitStyleSheet: function(styleSheet) 1214 { 1215 delete this._styleSheetResult; 1216 }, 1217 1218 visitRule: function(rule) 1219 { 1220 this._mentionedProperties = {}; 1221 }, 1222 1223 didVisitRule: function() 1224 { 1225 delete this._ruleResult; 1226 delete this._mentionedProperties; 1227 }, 1228 1229 visitProperty: function(styleSheet, property, result) 1230 { 1231 if (!property.name.startsWith(this._webkitPrefix)) 1232 return; 1233 1234 var normalPropertyName = property.name.substring(this._webkitPrefix.length).toLowerCase(); // Start just after the "-webkit-" prefix. 1235 if (WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties[normalPropertyName] && !this._mentionedProperties[normalPropertyName]) { 1236 var style = property.ownerStyle; 1237 var liveProperty = style.getLiveProperty(normalPropertyName); 1238 if (liveProperty && !liveProperty.styleBased) 1239 return; // WebCore can provide normal versions of prefixed properties automatically, so be careful to skip only normal source-based properties. 1240 1241 var rule = style.parentRule; 1242 this._mentionedProperties[normalPropertyName] = true; 1243 if (!this._styleSheetResult) 1244 this._styleSheetResult = result.addChild(rule.sourceURL ? WebInspector.linkifyResourceAsNode(rule.sourceURL) : "<unknown>"); 1245 if (!this._ruleResult) { 1246 var anchor = WebInspector.linkifyURLAsNode(rule.sourceURL, rule.selectorText); 1247 anchor.preferredPanel = "resources"; 1248 anchor.lineNumber = rule.lineNumberInSource(); 1249 this._ruleResult = this._styleSheetResult.addChild(anchor); 1250 } 1251 ++result.violationCount; 1252 this._ruleResult.addSnippet(String.sprintf("\"" + this._webkitPrefix + "%s\" is used, but \"%s\" is supported.", normalPropertyName, normalPropertyName)); 1253 } 1254 }, 1255 1256 __proto__: WebInspector.AuditRules.CSSRuleBase.prototype 1257} 1258 1259/** 1260 * @constructor 1261 * @extends {WebInspector.AuditRule} 1262 */ 1263WebInspector.AuditRules.CookieRuleBase = function(id, name) 1264{ 1265 WebInspector.AuditRule.call(this, id, name); 1266} 1267 1268WebInspector.AuditRules.CookieRuleBase.prototype = { 1269 /** 1270 * @param {!Array.<!WebInspector.NetworkRequest>} requests 1271 * @param {!WebInspector.AuditRuleResult} result 1272 * @param {function(!WebInspector.AuditRuleResult)} callback 1273 * @param {!WebInspector.Progress} progress 1274 */ 1275 doRun: function(requests, result, callback, progress) 1276 { 1277 var self = this; 1278 function resultCallback(receivedCookies) { 1279 if (progress.isCanceled()) 1280 return; 1281 1282 self.processCookies(receivedCookies, requests, result); 1283 callback(result); 1284 } 1285 1286 WebInspector.Cookies.getCookiesAsync(resultCallback); 1287 }, 1288 1289 mapResourceCookies: function(requestsByDomain, allCookies, callback) 1290 { 1291 for (var i = 0; i < allCookies.length; ++i) { 1292 for (var requestDomain in requestsByDomain) { 1293 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain(), requestDomain)) 1294 this._callbackForResourceCookiePairs(requestsByDomain[requestDomain], allCookies[i], callback); 1295 } 1296 } 1297 }, 1298 1299 _callbackForResourceCookiePairs: function(requests, cookie, callback) 1300 { 1301 if (!requests) 1302 return; 1303 for (var i = 0; i < requests.length; ++i) { 1304 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, requests[i].url)) 1305 callback(requests[i], cookie); 1306 } 1307 }, 1308 1309 __proto__: WebInspector.AuditRule.prototype 1310} 1311 1312/** 1313 * @constructor 1314 * @extends {WebInspector.AuditRules.CookieRuleBase} 1315 */ 1316WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold) 1317{ 1318 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size"); 1319 this._avgBytesThreshold = avgBytesThreshold; 1320 this._maxBytesThreshold = 1000; 1321} 1322 1323WebInspector.AuditRules.CookieSizeRule.prototype = { 1324 _average: function(cookieArray) 1325 { 1326 var total = 0; 1327 for (var i = 0; i < cookieArray.length; ++i) 1328 total += cookieArray[i].size(); 1329 return cookieArray.length ? Math.round(total / cookieArray.length) : 0; 1330 }, 1331 1332 _max: function(cookieArray) 1333 { 1334 var result = 0; 1335 for (var i = 0; i < cookieArray.length; ++i) 1336 result = Math.max(cookieArray[i].size(), result); 1337 return result; 1338 }, 1339 1340 processCookies: function(allCookies, requests, result) 1341 { 1342 function maxSizeSorter(a, b) 1343 { 1344 return b.maxCookieSize - a.maxCookieSize; 1345 } 1346 1347 function avgSizeSorter(a, b) 1348 { 1349 return b.avgCookieSize - a.avgCookieSize; 1350 } 1351 1352 var cookiesPerResourceDomain = {}; 1353 1354 function collectorCallback(request, cookie) 1355 { 1356 var cookies = cookiesPerResourceDomain[request.parsedURL.host]; 1357 if (!cookies) { 1358 cookies = []; 1359 cookiesPerResourceDomain[request.parsedURL.host] = cookies; 1360 } 1361 cookies.push(cookie); 1362 } 1363 1364 if (!allCookies.length) 1365 return; 1366 1367 var sortedCookieSizes = []; 1368 1369 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, 1370 null, 1371 true); 1372 var matchingResourceData = {}; 1373 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this)); 1374 1375 for (var requestDomain in cookiesPerResourceDomain) { 1376 var cookies = cookiesPerResourceDomain[requestDomain]; 1377 sortedCookieSizes.push({ 1378 domain: requestDomain, 1379 avgCookieSize: this._average(cookies), 1380 maxCookieSize: this._max(cookies) 1381 }); 1382 } 1383 var avgAllCookiesSize = this._average(allCookies); 1384 1385 var hugeCookieDomains = []; 1386 sortedCookieSizes.sort(maxSizeSorter); 1387 1388 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) { 1389 var maxCookieSize = sortedCookieSizes[i].maxCookieSize; 1390 if (maxCookieSize > this._maxBytesThreshold) 1391 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize)); 1392 } 1393 1394 var bigAvgCookieDomains = []; 1395 sortedCookieSizes.sort(avgSizeSorter); 1396 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) { 1397 var domain = sortedCookieSizes[i].domain; 1398 var avgCookieSize = sortedCookieSizes[i].avgCookieSize; 1399 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold) 1400 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize)); 1401 } 1402 result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize))); 1403 1404 var message; 1405 if (hugeCookieDomains.length) { 1406 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); 1407 entry.addURLs(hugeCookieDomains); 1408 result.violationCount += hugeCookieDomains.length; 1409 } 1410 1411 if (bigAvgCookieDomains.length) { 1412 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); 1413 entry.addURLs(bigAvgCookieDomains); 1414 result.violationCount += bigAvgCookieDomains.length; 1415 } 1416 }, 1417 1418 __proto__: WebInspector.AuditRules.CookieRuleBase.prototype 1419} 1420 1421/** 1422 * @constructor 1423 * @extends {WebInspector.AuditRules.CookieRuleBase} 1424 */ 1425WebInspector.AuditRules.StaticCookielessRule = function(minResources) 1426{ 1427 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain"); 1428 this._minResources = minResources; 1429} 1430 1431WebInspector.AuditRules.StaticCookielessRule.prototype = { 1432 processCookies: function(allCookies, requests, result) 1433 { 1434 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, 1435 [WebInspector.resourceTypes.Stylesheet, 1436 WebInspector.resourceTypes.Image], 1437 true); 1438 var totalStaticResources = 0; 1439 for (var domain in domainToResourcesMap) 1440 totalStaticResources += domainToResourcesMap[domain].length; 1441 if (totalStaticResources < this._minResources) 1442 return; 1443 var matchingResourceData = {}; 1444 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData)); 1445 1446 var badUrls = []; 1447 var cookieBytes = 0; 1448 for (var url in matchingResourceData) { 1449 badUrls.push(url); 1450 cookieBytes += matchingResourceData[url] 1451 } 1452 if (badUrls.length < this._minResources) 1453 return; 1454 1455 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); 1456 entry.addURLs(badUrls); 1457 result.violationCount = badUrls.length; 1458 }, 1459 1460 _collectorCallback: function(matchingResourceData, request, cookie) 1461 { 1462 matchingResourceData[request.url] = (matchingResourceData[request.url] || 0) + cookie.size(); 1463 }, 1464 1465 __proto__: WebInspector.AuditRules.CookieRuleBase.prototype 1466} 1467