Javascript  |  1214行  |  48.25 KB

/*
 * Copyright (C) 2010 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;

WebInspector.AuditRules.CacheableResponseCodes =
{
    200: true,
    203: true,
    206: true,
    300: true,
    301: true,
    410: true,

    304: true // Underlying resource is cacheable
}

/**
 * @param {Array} array Array of Elements (outerHTML is used) or strings (plain value is used as innerHTML)
 */
WebInspector.AuditRules.arrayAsUL = function(array, shouldLinkify)
{
    if (!array.length)
        return "";
    var ulElement = document.createElement("ul");
    for (var i = 0; i < array.length; ++i) {
        var liElement = document.createElement("li");
        if (array[i] instanceof Element)
            liElement.appendChild(array[i]);
        else if (shouldLinkify)
            liElement.appendChild(WebInspector.linkifyURLAsNode(array[i]));
        else
            liElement.innerHTML = array[i];
        ulElement.appendChild(liElement);
    }
    return ulElement.outerHTML;
}

WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, regexp, needFullResources)
{
    var domainToResourcesMap = {};
    for (var i = 0, size = resources.length; i < size; ++i) {
        var resource = resources[i];
        if (types && types.indexOf(resource.type) === -1)
            continue;
        var match = resource.url.match(regexp);
        if (!match)
            continue;
        var domain = match[2];
        var domainResources = domainToResourcesMap[domain];
        if (domainResources === undefined) {
          domainResources = [];
          domainToResourcesMap[domain] = domainResources;
        }
        domainResources.push(needFullResources ? resource : resource.url);
    }
    return domainToResourcesMap;
}

WebInspector.AuditRules.evaluateInTargetWindow = function(func, callback)
{
    InjectedScriptAccess.getDefault().evaluateOnSelf(func.toString(), callback);
}


WebInspector.AuditRules.GzipRule = function()
{
    WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
}

WebInspector.AuditRules.GzipRule.prototype = {
    doRun: function(resources, result, callback)
    {
        try {
            var commonMessage = undefined;
            var totalSavings = 0;
            var compressedSize = 0
            var candidateSize = 0
            var outputResources = [];
            for (var i = 0, length = resources.length; i < length; ++i) {
                var resource = resources[i];
                if (this._shouldCompress(resource)) {
                    var size = resource.contentLength;
                    candidateSize += size;
                    if (this._isCompressed(resource)) {
                        compressedSize += size;
                        continue;
                    }
                    if (!commonMessage)
                        commonMessage = result.appendChild("");
                    var savings = 2 * size / 3;
                    totalSavings += savings;
                    outputResources.push(
                        String.sprintf("Compressing %s could save ~%s",
                        WebInspector.linkifyURL(resource.url), Number.bytesToString(savings)));
                }
            }
            if (commonMessage) {
              commonMessage.value =
                  String.sprintf("Compressing the following resources with gzip could reduce their " +
                      "transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
              commonMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources));
              result.score = 100 * compressedSize / candidateSize;
              result.type = WebInspector.AuditRuleResult.Type.Violation;
            }
        } catch(e) {
            console.log(e);
        } finally {
            callback(result);
        }
    },

    _isCompressed: function(resource)
    {
        var encoding = resource.responseHeaders["Content-Encoding"];
        return encoding === "gzip" || encoding === "deflate";
    },

    _shouldCompress: function(resource)
    {
        return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.contentLength !== undefined && resource.contentLength > 150;
    }
}

WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, parametersObject)
{
    WebInspector.AuditRule.call(this, id, name, parametersObject);
    this._type = type;
    this._resourceTypeName = resourceTypeName;
}

WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
    doRun: function(resources, result, callback)
    {
        try {
            var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type], WebInspector.URLRegExp);
            var penalizedResourceCount = 0;
            // TODO: refactor according to the chosen i18n approach
            for (var domain in domainToResourcesMap) {
                var domainResources = domainToResourcesMap[domain];
                var extraResourceCount = domainResources.length - this.getValue("AllowedPerDomain");
                if (extraResourceCount <= 0)
                    continue;
                penalizedResourceCount += extraResourceCount - 1;
                result.appendChild(
                    String.sprintf("There are %d %s files served from %s. Consider combining them into as few files as possible.",
                    domainResources.length, this._resourceTypeName, domain));
            }
            result.score = 100 - (penalizedResourceCount * this.getValue("ScorePerResource"));
            result.type = WebInspector.AuditRuleResult.Type.Hint;
        } catch(e) {
            console.log(e);
        } finally {
            callback(result);
        }
    }
};

WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.CombineJsResourcesRule = function(parametersObject) {
    WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JS", parametersObject);
}

WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;


WebInspector.AuditRules.CombineCssResourcesRule = function(parametersObject) {
    WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", parametersObject);
}

WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;


WebInspector.AuditRules.MinimizeDnsLookupsRule = function(parametersObject) {
    WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups", parametersObject);
}

WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
    doRun: function(resources, result, callback)
    {
        try {
            var violationDomains = [];
            var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined, WebInspector.URLRegExp);
            for (var domain in domainToResourcesMap) {
                if (domainToResourcesMap[domain].length > 1)
                    continue;
                var match = domain.match(WebInspector.URLRegExp);
                if (!match)
                    continue;
                if (!match[2].search(WebInspector.AuditRules.IPAddressRegexp))
                    continue; // an IP address
                violationDomains.push(match[2]);
            }
            if (violationDomains.length <= this.getValue("HostCountThreshold"))
                return;
            var commonMessage = result.appendChild(
                "The following domains only serve one resource each. If possible, avoid the extra DNS " +
                "lookups by serving these resources from existing domains.");
            commonMessage.appendChild(WebInspector.AuditRules.arrayAsUL(violationDomains));
            result.score = 100 - violationDomains.length * this.getValue("ViolationDomainScore");
        } catch(e) {
            console.log(e);
        } finally {
            callback(result);
        }
    }
}

WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.ParallelizeDownloadRule = function(parametersObject)
{
    WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames", parametersObject);
}


WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
    doRun: function(resources, result, callback)
    {
        function hostSorter(a, b)
        {
            var aCount = domainToResourcesMap[a].length;
            var bCount = domainToResourcesMap[b].length;
            return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
        }

        try {
            var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
                resources,
                [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
                WebInspector.URLRegExp,
                true);

            var hosts = [];
            for (var url in domainToResourcesMap)
                hosts.push(url);

            if (!hosts.length)
                return; // no hosts (local file or something)

            hosts.sort(hostSorter);

            var optimalHostnameCount = this.getValue("OptimalHostnameCount");
            if (hosts.length > optimalHostnameCount)
                hosts.splice(optimalHostnameCount);

            var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
            var resourceCountAboveThreshold = busiestHostResourceCount - this.getValue("MinRequestThreshold");
            if (resourceCountAboveThreshold <= 0)
                return;

            var avgResourcesPerHost = 0;
            for (var i = 0, size = hosts.length; i < size; ++i)
                avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;

            // Assume optimal parallelization.
            avgResourcesPerHost /= optimalHostnameCount;

            avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);

            var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;

            var minBalanceThreshold = this.getValue("MinBalanceThreshold");
            if (pctAboveAvg < minBalanceThreshold) {
                result.score = 100;
                return;
            }

            result.score = (1 - (pctAboveAvg - minBalanceThreshold)) * 100;
            result.type = WebInspector.AuditRuleResult.Type.Hint;

            var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
            var commonMessage = result.appendChild(
                String.sprintf("This page makes %d parallelizable requests to %s" +
                ". Increase download parallelization by distributing the following" +
                " requests across multiple hostnames.", busiestHostResourceCount, hosts[0]));
            var outputResources = [];
            for (var i = 0, size = resourcesOnBusiestHost.length; i < size; ++i)
                outputResources.push(resourcesOnBusiestHost[i].url);
            commonMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources, true));
        } catch(e) {
            console.log(e);
        } finally {
            callback(result);
        }
    }
}

WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


// The reported CSS rule size is incorrect (parsed != original in WebKit),
// so use percentages instead, which gives a better approximation.
WebInspector.AuditRules.UnusedCssRule = function(parametersObject)
{
    WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS", parametersObject);
}

WebInspector.AuditRules.UnusedCssRule.prototype = {
    _getUnusedStylesheetRatioMessage: function(unusedLength, type, location, styleSheetLength)
    {
        var url = type === "href"
            ? WebInspector.linkifyURL(location)
            : String.sprintf("Inline block #%s", location);
        var pctUnused = Math.round(unusedLength / styleSheetLength * 100);
        return String.sprintf("%s: %f%% (estimated) is not used by the current page.", url, pctUnused);
    },

    _getUnusedTotalRatioMessage: function(unusedLength, totalLength)
    {
        var pctUnused = Math.round(unusedLength / totalLength * 100);
        return String.sprintf("%d%% of CSS (estimated) is not used by the current page.", pctUnused);
    },

    doRun: function(resources, result, callback)
    {
        var self = this;
        function evalCallback(evalResult, isException) {
            try {
              if (isException)
                  return;

              var totalLength = 0;
              var totalUnusedLength = 0;
              var topMessage;
              var styleSheetMessage;
              for (var i = 0; i < evalResult.length; ) {
                  var type = evalResult[i++];
                  if (type === "totalLength") {
                      totalLength = evalResult[i++];
                      continue;
                  }

                  var styleSheetLength = evalResult[i++];
                  var location = evalResult[i++];
                  var unusedRules = evalResult[i++];
                  styleSheetMessage = undefined;
                  if (!topMessage)
                      topMessage = result.appendChild("");

                  var totalUnusedRuleLength = 0;
                  var ruleSelectors = [];
                  for (var j = 0; j < unusedRules.length; ++j) {
                      var rule = unusedRules[j];
                      totalUnusedRuleLength += parseInt(rule[1]);
                      if (!styleSheetMessage)
                          styleSheetMessage = result.appendChild("");
                      ruleSelectors.push(rule[0]);
                  }
                  styleSheetMessage.appendChild(WebInspector.AuditRules.arrayAsUL(ruleSelectors));

                  styleSheetMessage.value = self._getUnusedStylesheetRatioMessage(totalUnusedRuleLength, type, location, styleSheetLength);
                  totalUnusedLength += totalUnusedRuleLength;
              }
              if (totalUnusedLength) {
                  var totalUnusedPercent = totalUnusedLength / totalLength;
                  topMessage.value = self._getUnusedTotalRatioMessage(totalUnusedLength, totalLength);
                  var pctMultiplier = Math.log(Math.max(200, totalUnusedLength - 800)) / 7 - 0.6;
                  result.score = (1 - totalUnusedPercent * pctMultiplier) * 100;
                  result.type = WebInspector.AuditRuleResult.Type.Hint;
              } else
                  result.score = 100;
            } catch(e) {
                console.log(e);
            } finally {
                callback(result);
            }
        }

        function routine()
        {
            var styleSheets = document.styleSheets;
            if (!styleSheets)
                return {};
            var styleSheetToUnusedRules = [];
            var inlineBlockOrdinal = 0;
            var totalCSSLength = 0;
            var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus/;
            for (var i = 0; i < styleSheets.length; ++i) {
                var styleSheet = styleSheets[i];
                if (!styleSheet.cssRules)
                    continue;
                var currentStyleSheetSize = 0;
                var unusedRules = [];
                for (var curRule = 0; curRule < styleSheet.cssRules.length; ++curRule) {
                    var rule = styleSheet.cssRules[curRule];
                    var textLength = rule.cssText ? rule.cssText.length : 0;
                    currentStyleSheetSize += textLength;
                    totalCSSLength += textLength;
                    if (rule.type !== 1 || rule.selectorText.match(pseudoSelectorRegexp))
                        continue;
                    var nodes = document.querySelectorAll(rule.selectorText);
                    if (nodes && nodes.length)
                        continue;
                    unusedRules.push([rule.selectorText, textLength]);
                }
                if (unusedRules.length) {
                    styleSheetToUnusedRules.push(styleSheet.href ? "href" : "inline");
                    styleSheetToUnusedRules.push(currentStyleSheetSize);
                    styleSheetToUnusedRules.push(styleSheet.href ? styleSheet.href : ++inlineBlockOrdinal);
                    styleSheetToUnusedRules.push(unusedRules);
                }
            }
            styleSheetToUnusedRules.push("totalLength");
            styleSheetToUnusedRules.push(totalCSSLength);
            return styleSheetToUnusedRules;
        }

        WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback);
    }
}

WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.CacheControlRule = function(id, name, parametersObject)
{
    WebInspector.AuditRule.call(this, id, name, parametersObject);
}

WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;

WebInspector.AuditRules.CacheControlRule.prototype = {

    InfoCheck: -1,
    FailCheck: 0,
    WarningCheck: 1,
    SevereCheck: 2,

    doRun: function(resources, result, callback)
    {
        try {
            var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
            if (cacheableAndNonCacheableResources[0].length) {
                result.score = 100;
                this.runChecks(cacheableAndNonCacheableResources[0], result);
            }
            this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
        } catch(e) {
            console.log(e);
        } finally {
            callback(result);
        }
    },

    handleNonCacheableResources: function()
    {
    },

    _cacheableAndNonCacheableResources: function(resources)
    {
        var processedResources = [[], []];
        for (var i = 0; i < resources.length; ++i) {
            var resource = resources[i];
            if (!this.isCacheableResource(resource))
                continue;
            if (this._isExplicitlyNonCacheable(resource))
                processedResources[1].push(resource);
            else
                processedResources[0].push(resource);
        }
        return processedResources;
    },

    execCheck: function(messageText, resourceCheckFunction, resources, severity, result)
    {
        var topMessage;
        var failingResources = 0;
        var resourceCount = resources.length;
        var outputResources = [];
        for (var i = 0; i < resourceCount; ++i) {
            if (resourceCheckFunction.call(this, resources[i])) {
                ++failingResources;
                if (!topMessage)
                    topMessage = result.appendChild(messageText);
                outputResources.push(resources[i].url);
            }
        }
        if (topMessage)
            topMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources, true));
        if (failingResources) {
            switch (severity) {
                case this.FailCheck:
                    result.score = 0;
                    result.type = WebInspector.AuditRuleResult.Type.Violation;
                    break;
                case this.SevereCheck:
                case this.WarningCheck:
                    result.score -= 50 * severity * failingResources / resourceCount;
                    result.type = WebInspector.AuditRuleResult.Type.Hint;
                    break;
            }
        }
        return topMessage;
    },

    freshnessLifetimeGreaterThan: function(resource, timeMs)
    {
        var dateHeader = this.responseHeader(resource, "Date");
        if (!dateHeader)
            return false;

        var dateHeaderMs = Date.parse(dateHeader);
        if (isNaN(dateHeaderMs))
            return false;

        var freshnessLifetimeMs;
        var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");

        if (maxAgeMatch)
            freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
        else {
            var expiresHeader = this.responseHeader(resource, "Expires");
            if (expiresHeader) {
                var expDate = Date.parse(expiresHeader);
                if (!isNaN(expDate))
                    freshnessLifetimeMs = expDate - dateHeaderMs;
            }
        }

        return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
    },

    responseHeader: function(resource, header)
    {
        return resource.responseHeaders[header];
    },

    hasResponseHeader: function(resource, header)
    {
        return resource.responseHeaders[header] !== undefined;
    },

    isCompressible: function(resource)
    {
        return WebInspector.Resource.Type.isTextType(resource.type);
    },

    isPubliclyCacheable: function(resource)
    {
        if (this._isExplicitlyNonCacheable(resource))
            return false;

        if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
            return true;

        return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
    },

    responseHeaderMatch: function(resource, header, regexp)
    {
        return resource.responseHeaders[header]
            ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
            : undefined;
    },

    hasExplicitExpiration: function(resource)
    {
        return this.hasResponseHeader(resource, "Date") &&
            (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
    },

    _isExplicitlyNonCacheable: function(resource)
    {
        var hasExplicitExp = this.hasExplicitExpiration(resource);
        return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
            this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
            (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
            (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
            (!hasExplicitExp && !this.isCacheableResource(resource));
    },

    isCacheableResource: function(resource)
    {
        return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
    }
}

WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.BrowserCacheControlRule = function(parametersObject)
{
    WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching", parametersObject);
}

WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
    handleNonCacheableResources: function(resources, result)
    {
        if (resources.length) {
            var message = result.appendChild(
                "The following resources are explicitly non-cacheable. Consider making them cacheable if possible:");
            var resourceOutput = [];
            for (var i = 0; i < resources.length; ++i)
                resourceOutput.push(resources[i].url);
            message.appendChild(WebInspector.AuditRules.arrayAsUL(resourceOutput, true));
        }
    },

    runChecks: function(resources, result, callback)
    {
        this.execCheck(
            "The following resources are missing a cache expiration." +
            " Resources that do not specify an expiration may not be" +
            " cached by browsers:",
            this._missingExpirationCheck, resources, this.SevereCheck, result);
        this.execCheck(
            "The following resources specify a \"Vary\" header that" +
            " disables caching in most versions of Internet Explorer:",
            this._varyCheck, resources, this.SevereCheck, result);
        this.execCheck(
            "The following cacheable resources have a short" +
            " freshness lifetime:",
            this._oneMonthExpirationCheck, resources, this.WarningCheck, result);

        // Unable to implement the favicon check due to the WebKit limitations.

        this.execCheck(
            "To further improve cache hit rate, specify an expiration" +
            " one year in the future for the following cacheable" +
            " resources:",
            this._oneYearExpirationCheck, resources, this.InfoCheck, result);
    },

    _missingExpirationCheck: function(resource)
    {
        return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
    },

    _varyCheck: function(resource)
    {
        var varyHeader = this.responseHeader(resource, "Vary");
        if (varyHeader) {
            varyHeader = varyHeader.replace(/User-Agent/gi, "");
            varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
            varyHeader = varyHeader.replace(/[, ]*/g, "");
        }
        return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
    },

    _oneMonthExpirationCheck: function(resource)
    {
        return this.isCacheableResource(resource) &&
            !this.hasResponseHeader(resource, "Set-Cookie") &&
            !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
            this.freshnessLifetimeGreaterThan(resource, 0);
    },

    _oneYearExpirationCheck: function(resource)
    {
        return this.isCacheableResource(resource) &&
            !this.hasResponseHeader(resource, "Set-Cookie") &&
            !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
            this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
    }
}

WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;


WebInspector.AuditRules.ProxyCacheControlRule = function(parametersObject) {
    WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching", parametersObject);
}

WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
    runChecks: function(resources, result, callback)
    {
        this.execCheck(
            "Resources with a \"?\" in the URL are not cached by most" +
            " proxy caching servers:",
            this._questionMarkCheck, resources, this.WarningCheck, result);
        this.execCheck(
            "Consider adding a \"Cache-Control: public\" header to the" +
            " following resources:",
            this._publicCachingCheck, resources, this.InfoCheck, result);
        this.execCheck(
            "The following publicly cacheable resources contain" +
            " a Set-Cookie header. This security vulnerability" +
            " can cause cookies to be shared by multiple users.",
            this._setCookieCacheableCheck, resources, this.FailCheck, result);
    },

    _questionMarkCheck: function(resource)
    {
        return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
    },

    _publicCachingCheck: function(resource)
    {
        return this.isCacheableResource(resource) &&
            !this.isCompressible(resource) &&
            !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
            !this.hasResponseHeader(resource, "Set-Cookie");
    },

    _setCookieCacheableCheck: function(resource)
    {
        return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
    }
}

WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;


WebInspector.AuditRules.ImageDimensionsRule = function(parametersObject)
{
    WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions", parametersObject);
}

WebInspector.AuditRules.ImageDimensionsRule.prototype = {
    doRun: function(resources, result, callback)
    {
        function evalCallback(evalResult, isException)
        {
            try {
                if (isException)
                    return;
                if (!evalResult || !evalResult.totalImages)
                    return;
                result.score = 100;
                var topMessage = result.appendChild(
                    "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:");
                var map = evalResult.map;
                var outputResources = [];
                for (var url in map) {
                    var value = WebInspector.linkifyURL(url);
                    if (map[url] > 1)
                        value += " (" + map[url] + " uses)";
                    outputResources.push(value);
                    result.score -= this.getValue("ScorePerImageUse") * map[url];
                    result.type = WebInspector.AuditRuleResult.Type.Hint;
                }
                topMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources));
            } catch(e) {
                console.log(e);
            } finally {
                callback(result);
            }
        }

        function routine()
        {
            var images = document.getElementsByTagName("img");
            const widthRegExp = /width[^:;]*:/gim;
            const heightRegExp = /height[^:;]*:/gim;

            function hasDimension(element, cssText, rules, regexp, attributeName) {
                if (element.attributes.getNamedItem(attributeName) != null || (cssText && cssText.match(regexp)))
                    return true;

                if (!rules)
                    return false;
                for (var i = 0; i < rules.length; ++i) {
                    if (rules.item(i).style.cssText.match(regexp))
                        return true;
                }
                return false;
            }

            function hasWidth(element, cssText, rules) {
                return hasDimension(element, cssText, rules, widthRegExp, "width");
            }

            function hasHeight(element, cssText, rules) {
                return hasDimension(element, cssText, rules, heightRegExp, "height");
            }

            var urlToNoDimensionCount = {};
            var found = false;
            for (var i = 0; i < images.length; ++i) {
                var image = images[i];
                if (!image.src)
                    continue;
                var position = document.defaultView.getComputedStyle(image).getPropertyValue("position");
                if (position === "absolute")
                    continue;
                var cssText = (image.style && image.style.cssText) ? image.style.cssText : "";
                var rules = document.defaultView.getMatchedCSSRules(image, "", true);
                if (!hasWidth(image, cssText, rules) || !hasHeight(image, cssText, rules)) {
                    found = true;
                    if (urlToNoDimensionCount.hasOwnProperty(image.src))
                        ++urlToNoDimensionCount[image.src];
                    else
                        urlToNoDimensionCount[image.src] = 1;
                }
            }
            return found ? {totalImages: images.length, map: urlToNoDimensionCount} : null;
        }

        WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback.bind(this));
    }
}

WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.CssInHeadRule = function(parametersObject)
{
    WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head", parametersObject);
}

WebInspector.AuditRules.CssInHeadRule.prototype = {
    doRun: function(resources, result, callback)
    {
        function evalCallback(evalResult, isException)
        {
            try {
                if (isException)
                    return;
                if (!evalResult)
                    return;
                result.score = 100;
                var outputMessages = [];
                for (var url in evalResult) {
                    var urlViolations = evalResult[url];
                    var topMessage = result.appendChild(
                        String.sprintf("CSS in the %s document body adversely impacts rendering performance.",
                        WebInspector.linkifyURL(url)));
                    if (urlViolations[0]) {
                        outputMessages.push(
                            String.sprintf("%s style block(s) in the body should be moved to the document head.", urlViolations[0]));
                        result.score -= this.getValue("InlineURLScore") * urlViolations[0];
                    }
                    for (var i = 0; i < urlViolations[1].length; ++i) {
                        outputMessages.push(
                            String.sprintf("Link node %s should be moved to the document head", WebInspector.linkifyURL(urlViolations[1])));
                    }
                    result.score -= this.getValue("InlineStylesheetScore") * urlViolations[1];
                    result.type = WebInspector.AuditRuleResult.Type.Hint;
                }
                topMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputMessages));
            } catch(e) {
                console.log(e);
            } finally {
                callback(result);
            }
        }

        function routine()
        {
            function allViews() {
                var views = [document.defaultView];
                var curView = 0;
                while (curView < views.length) {
                    var view = views[curView];
                    var frames = view.frames;
                    for (var i = 0; i < frames.length; ++i) {
                        if (frames[i] !== view)
                            views.push(frames[i]);
                    }
                    ++curView;
                }
                return views;
            }

            var views = allViews();
            var urlToViolationsArray = {};
            var found = false;
            for (var i = 0; i < views.length; ++i) {
              var view = views[i];
              if (!view.document)
                  continue;

              var inlineStyles = view.document.querySelectorAll("body style");
              var inlineStylesheets = view.document.querySelectorAll(
                  "body link[rel~='stylesheet'][href]");
              if (!inlineStyles.length && !inlineStylesheets.length)
                  continue;

              found = true;
              var inlineStylesheetHrefs = [];
              for (var j = 0; j < inlineStylesheets.length; ++j)
                  inlineStylesheetHrefs.push(inlineStylesheets[j].href);

              urlToViolationsArray[view.location.href] =
                  [inlineStyles.length, inlineStylesheetHrefs];
            }
            return found ? urlToViolationsArray : null;
        }

        WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback);
    }
}

WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.StylesScriptsOrderRule = function(parametersObject)
{
    WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts", parametersObject);
}

WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
    doRun: function(resources, result, callback)
    {
        function evalCallback(evalResult, isException)
        {
            try {
                if (isException)
                    return;
                if (!evalResult)
                    return;

                result.score = 100;
                var lateCssUrls = evalResult['late'];
                if (lateCssUrls) {
                    var lateMessage = result.appendChild(
                        '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.');
                    lateMessage.appendChild(WebInspector.AuditRules.arrayAsUL(lateCssUrls, true));
                    result.score -= this.getValue("InlineBetweenResourcesScore") * lateCssUrls.length;
                    result.type = WebInspector.AuditRuleResult.Type.Violation;
                }
                if (evalResult['cssBeforeInlineCount']) {
                  var count = evalResult['cssBeforeInlineCount'];
                  result.appendChild(count + ' inline script block' +
                      (count > 1 ? 's were' : ' was') + ' 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.');
                  result.score -= this.getValue("CSSAfterJSURLScore") * count;
                  result.type = WebInspector.AuditRuleResult.Type.Violation;
                }
            } catch(e) {
                console.log(e);
            } finally {
                callback(result);
            }
        }

        function routine()
        {
            var lateStyles = document.querySelectorAll(
                "head script[src] ~ link[rel~='stylesheet'][href]");
            var stylesBeforeInlineScript = document.querySelectorAll(
                "head link[rel~='stylesheet'][href] ~ script:not([src])");

            var resultObject;
            if (!lateStyles.length && !stylesBeforeInlineScript.length)
                resultObject = null;
            else {
                resultObject = {};
                if (lateStyles.length) {
                  lateStyleUrls = [];
                  for (var i = 0; i < lateStyles.length; ++i)
                      lateStyleUrls.push(lateStyles[i].href);
                  resultObject["late"] = lateStyleUrls;
                }
                resultObject["cssBeforeInlineCount"] = stylesBeforeInlineScript.length;
            }
            return resultObject;
        }

        WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback.bind(this));
    }
}

WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.CookieRuleBase = function(id, name, parametersObject)
{
    WebInspector.AuditRule.call(this, id, name, parametersObject);
}

WebInspector.AuditRules.CookieRuleBase.prototype = {
    doRun: function(resources, result, callback)
    {
        var self = this;
        function resultCallback(receivedCookies, isAdvanced) {
            try {
                self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
            } catch(e) {
                console.log(e);
            } finally {
                callback(result);
            }
        }
        WebInspector.Cookies.getCookiesAsync(resultCallback);
    },

    mapResourceCookies: function(resourcesByDomain, allCookies, callback)
    {
        for (var i = 0; i < allCookies.length; ++i) {
            for (var resourceDomain in resourcesByDomain) {
                if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
                    this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
            }
        }
    },

    _callbackForResourceCookiePairs: function(resources, cookie, callback)
    {
        if (!resources)
            return;
        for (var i = 0; i < resources.length; ++i) {
            if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
                callback(resources[i], cookie);
        }
    }
}

WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;


WebInspector.AuditRules.CookieSizeRule = function(parametersObject)
{
    WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size", parametersObject);
}

WebInspector.AuditRules.CookieSizeRule.prototype = {
    _average: function(cookieArray)
    {
        var total = 0;
        for (var i = 0; i < cookieArray.length; ++i)
            total += cookieArray[i].size;
        return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
    },

    _max: function(cookieArray)
    {
        var result = 0;
        for (var i = 0; i < cookieArray.length; ++i)
            result = Math.max(cookieArray[i].size, result);
        return result;
    },

    processCookies: function(allCookies, resources, result)
    {
        function maxSizeSorter(a, b)
        {
            return b.maxCookieSize - a.maxCookieSize;
        }

        function avgSizeSorter(a, b)
        {
            return b.avgCookieSize - a.avgCookieSize;
        }

        var cookiesPerResourceDomain = {};

        function collectorCallback(resource, cookie)
        {
            var cookies = cookiesPerResourceDomain[resource.domain];
            if (!cookies) {
                cookies = [];
                cookiesPerResourceDomain[resource.domain] = cookies;
            }
            cookies.push(cookie);
        }

        if (!allCookies.length)
            return;

        var sortedCookieSizes = [];

        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
                null,
                WebInspector.URLRegExp,
                true);
        var matchingResourceData = {};
        this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));

        result.score = 100;
        for (var resourceDomain in cookiesPerResourceDomain) {
            var cookies = cookiesPerResourceDomain[resourceDomain];
            sortedCookieSizes.push({
                domain: resourceDomain,
                avgCookieSize: this._average(cookies),
                maxCookieSize: this._max(cookies)
            });
        }
        var avgAllCookiesSize = this._average(allCookies);

        var hugeCookieDomains = [];
        sortedCookieSizes.sort(maxSizeSorter);

        var maxBytesThreshold = this.getValue("MaxBytesThreshold");
        var minBytesThreshold = this.getValue("MinBytesThreshold");

        for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
            var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
            if (maxCookieSize > maxBytesThreshold)
                hugeCookieDomains.push(sortedCookieSizes[i].domain + ": " + Number.bytesToString(maxCookieSize));
        }

        var bigAvgCookieDomains = [];
        sortedCookieSizes.sort(avgSizeSorter);
        for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
            var domain = sortedCookieSizes[i].domain;
            var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
            if (avgCookieSize > minBytesThreshold && avgCookieSize < maxBytesThreshold)
                bigAvgCookieDomains.push(domain + ": " + Number.bytesToString(avgCookieSize));
        }
        result.appendChild("The average cookie size for all requests on this page is " + Number.bytesToString(avgAllCookiesSize));

        var message;
        if (hugeCookieDomains.length) {
            result.score = 75;
            result.type = WebInspector.AuditRuleResult.Type.Violation;
            message = result.appendChild(
                String.sprintf("The following domains have a cookie size in excess of %d " +
                " bytes. This is harmful because requests with cookies larger than 1KB" +
                " typically cannot fit into a single network packet.", maxBytesThreshold));
            message.appendChild(WebInspector.AuditRules.arrayAsUL(hugeCookieDomains));
        }

        if (bigAvgCookieDomains.length) {
            this.score -= Math.max(0, avgAllCookiesSize - minBytesThreshold) /
                (minBytesThreshold - minBytesThreshold) / this.getValue("TotalPoints");
            if (!result.type)
                result.type = WebInspector.AuditRuleResult.Type.Hint;
            message = result.appendChild(
                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.", minBytesThreshold));
            message.appendChild(WebInspector.AuditRules.arrayAsUL(bigAvgCookieDomains));
        }

        if (!bigAvgCookieDomains.length && !hugeCookieDomains.length)
            result.score = WebInspector.AuditCategoryResult.ScoreNA;
    }
}

WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;


WebInspector.AuditRules.StaticCookielessRule = function(parametersObject)
{
    WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain", parametersObject);
}

WebInspector.AuditRules.StaticCookielessRule.prototype = {
    processCookies: function(allCookies, resources, result)
    {
        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
                [WebInspector.Resource.Type.Stylesheet,
                 WebInspector.Resource.Type.Image],
                WebInspector.URLRegExp,
                true);
        var totalStaticResources = 0;
        var minResources = this.getValue("MinResources");
        for (var domain in domainToResourcesMap)
            totalStaticResources += domainToResourcesMap[domain].length;
        if (totalStaticResources < minResources)
            return;
        var matchingResourceData = {};
        this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));

        var badUrls = [];
        var cookieBytes = 0;
        for (var url in matchingResourceData) {
            badUrls.push(url);
            cookieBytes += matchingResourceData[url]
        }
        if (badUrls.length < minResources)
            return;

        result.score = 100;
        var badPoints = cookieBytes / 75;
        var violationPct = Math.max(badUrls.length / totalStaticResources, 0.6);
        badPoints *= violationPct;
        result.score -= badPoints;
        result.score = Math.max(result.score, 0);
        result.type = WebInspector.AuditRuleResult.Type.Violation;
        result.appendChild(String.sprintf("%s of cookies were sent with the following static resources.", Number.bytesToString(cookieBytes)));
        var message = result.appendChild("Serve these static resources from a domain that does not set cookies:");
        message.appendChild(WebInspector.AuditRules.arrayAsUL(badUrls, true));
    },

    _collectorCallback: function(matchingResourceData, resource, cookie)
    {
        matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
    }
}

WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;