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