Repository: GoogleChrome/lighthouse
Pull request #8760 (2019-05-01 02:55:52)

core(runner): audits can only use requiredArtifacts
**Summary**
One more breaking change I want to get in we discussed in the meeting that I misunderstood and thought was already on someone's plate :) 

**an audit can only use its `requiredArtifacts`**

With plugins it feels like folks aren't going to remember to use `requiredArtifacts` and if we ever want to enforce it, seems like we need to do it now.

This also fixes the dozen or so audits that were cheating :)



Patch:
diff --git a/lighthouse-core/audits/bootup-time.js b/lighthouse-core/audits/bootup-time.js
index c5e178fef..726de4370 100644
--- a/lighthouse-core/audits/bootup-time.js
+++ b/lighthouse-core/audits/bootup-time.js
@@ -45,7 +45,7 @@ class BootupTime extends Audit {
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
index bcd9de011..570df5246 100644
--- a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
+++ b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
@@ -70,10 +70,9 @@ class RenderBlockingResources extends Audit {
       title: str_(UIStrings.title),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
       description: str_(UIStrings.description),
-      // This audit also looks at CSSUsage but has a graceful fallback if it failed, so do not mark
-      // it as a "requiredArtifact".
-      // TODO: look into adding an `optionalArtifacts` property that captures this
-      requiredArtifacts: ['URL', 'TagsBlockingFirstPaint', 'traces'],
+      // TODO: look into adding an `optionalArtifacts` property that captures the non-required nature
+      // of CSSUsage
+      requiredArtifacts: ['URL', 'TagsBlockingFirstPaint', 'traces', 'devtoolsLogs', 'CSSUsage'],
     };
   }
 
diff --git a/lighthouse-core/audits/byte-efficiency/unminified-css.js b/lighthouse-core/audits/byte-efficiency/unminified-css.js
index 87604f5de..b9730b5a5 100644
--- a/lighthouse-core/audits/byte-efficiency/unminified-css.js
+++ b/lighthouse-core/audits/byte-efficiency/unminified-css.js
@@ -36,7 +36,7 @@ class UnminifiedCSS extends ByteEfficiencyAudit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['CSSUsage', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['CSSUsage', 'devtoolsLogs', 'traces', 'URL'],
     };
   }
 
diff --git a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
index 9350b2782..86df60232 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
@@ -35,7 +35,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['OptimizedImages', 'ImageElements', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['OptimizedImages', 'ImageElements', 'devtoolsLogs', 'traces', 'URL'],
     };
   }
 
diff --git a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
index 50865298f..6da263b8b 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
@@ -35,7 +35,7 @@ class UsesWebPImages extends ByteEfficiencyAudit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['OptimizedImages', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['OptimizedImages', 'devtoolsLogs', 'traces', 'URL', 'ImageElements'],
     };
   }
 
diff --git a/lighthouse-core/audits/metrics/estimated-input-latency.js b/lighthouse-core/audits/metrics/estimated-input-latency.js
index 38efde2b3..5f99e4cb4 100644
--- a/lighthouse-core/audits/metrics/estimated-input-latency.js
+++ b/lighthouse-core/audits/metrics/estimated-input-latency.js
@@ -31,7 +31,7 @@ class EstimatedInputLatency extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/metrics/first-cpu-idle.js b/lighthouse-core/audits/metrics/first-cpu-idle.js
index 6557ba152..5bc9bfa95 100644
--- a/lighthouse-core/audits/metrics/first-cpu-idle.js
+++ b/lighthouse-core/audits/metrics/first-cpu-idle.js
@@ -30,7 +30,7 @@ class FirstCPUIdle extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/metrics/first-meaningful-paint.js b/lighthouse-core/audits/metrics/first-meaningful-paint.js
index fe83fc0f7..0c160319a 100644
--- a/lighthouse-core/audits/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/audits/metrics/first-meaningful-paint.js
@@ -29,7 +29,7 @@ class FirstMeaningfulPaint extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/metrics/max-potential-fid.js b/lighthouse-core/audits/metrics/max-potential-fid.js
index 3a81771bf..c53521b83 100644
--- a/lighthouse-core/audits/metrics/max-potential-fid.js
+++ b/lighthouse-core/audits/metrics/max-potential-fid.js
@@ -13,8 +13,8 @@ const UIStrings = {
   /** The name of the metric "Maximum Potential First Input Delay" that marks the maximum estimated time between the page receiving input (a user clicking, tapping, or typing) and the page responding. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
   title: 'Max Potential FID',
   /** Description of the Maximum Potential First Input Delay metric that marks the maximum estimated time between the page receiving input (a user clicking, tapping, or typing) and the page responding. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
-  description: 'The potential First Input Delay that your users could experience is the ' +
-      'duration, in milliseconds, of the longest task.',
+  description: 'The maximum potential First Input Delay that your users could experience is the ' +
+      'duration, in milliseconds, of the longest task. [Learn more](https://developers.google.com/web/updates/2018/05/first-input-delay).',
 };
 
 const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
@@ -34,7 +34,7 @@ class MaxPotentialFID extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces'],
+      requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/resource-summary.js b/lighthouse-core/audits/resource-summary.js
new file mode 100644
index 000000000..b39add3ae
--- /dev/null
+++ b/lighthouse-core/audits/resource-summary.js
@@ -0,0 +1,103 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const Audit = require('./audit.js');
+const ComputedResourceSummary = require('../computed/resource-summary.js');
+const i18n = require('../lib/i18n/i18n.js');
+
+const UIStrings = {
+  /** Imperative title of a Lighthouse audit that tells the user to minimize the size and quantity of resources used to load the page. */
+  title: 'Keep request counts low and transfer sizes small',
+  /** Description of a Lighthouse audit that tells the user that they can setup a budgets for the quantity and size of page resources. No character length limits. */
+  description: 'To set budgets for the quantity and size of page resources,' +
+    ' add a budget.json file.',
+  /** [ICU Syntax] Label for an audit identifying the number of requests and kilobytes used to load the page. */
+  displayValue: `{requestCount, plural, =1 {1 request} other {# requests}}` +
+    ` • { byteCount, number, bytes } KB`,
+};
+
+const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
+
+class ResourceSummary extends Audit {
+  /**
+   * @return {LH.Audit.Meta}
+   */
+  static get meta() {
+    return {
+      id: 'resource-summary',
+      title: str_(UIStrings.title),
+      description: str_(UIStrings.description),
+      scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
+      requiredArtifacts: ['devtoolsLogs', 'URL'],
+    };
+  }
+
+  /**
+   * @param {LH.Artifacts} artifacts
+   * @param {LH.Audit.Context} context
+   * @return {Promise<LH.Audit.Product>}
+   */
+  static async audit(artifacts, context) {
+    const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
+    const summary = await ComputedResourceSummary
+      .request({devtoolsLog, URL: artifacts.URL}, context);
+
+    /** @type {LH.Audit.Details.Table['headings']} */
+    const headings = [
+      {key: 'label', itemType: 'text', text: 'Resource Type'},
+      {key: 'requestCount', itemType: 'numeric', text: 'Requests'},
+      {key: 'size', itemType: 'bytes', text: 'Transfer Size'},
+    ];
+
+
+    /** @type {Record<LH.Budget.ResourceType,string>} */
+    const strMappings = {
+      'total': str_(i18n.UIStrings.totalResourceType),
+      'document': str_(i18n.UIStrings.documentResourceType),
+      'script': str_(i18n.UIStrings.scriptResourceType),
+      'stylesheet': str_(i18n.UIStrings.stylesheetResourceType),
+      'image': str_(i18n.UIStrings.imageResourceType),
+      'media': str_(i18n.UIStrings.mediaResourceType),
+      'font': str_(i18n.UIStrings.fontResourceType),
+      'other': str_(i18n.UIStrings.otherResourceType),
+      'third-party': str_(i18n.UIStrings.thirdPartyResourceType),
+    };
+
+    const types = /** @type {Array<LH.Budget.ResourceType>} */ (Object.keys(summary));
+    const rows = types.map(type => {
+      return {
+        // ResourceType is included as an "id" for ease of use.
+        // It does not appear directly in the table.
+        resourceType: type,
+        label: strMappings[type],
+        requestCount: summary[type].count,
+        size: summary[type].size,
+      };
+    });
+    // Force third-party to be last, descending by size otherwise
+    const thirdPartyRow = rows.find(r => r.resourceType === 'third-party') || [];
+    const otherRows = rows.filter(r => r.resourceType !== 'third-party')
+      .sort((a, b) => {
+        return b.size - a.size;
+      });
+    const tableItems = otherRows.concat(thirdPartyRow);
+
+    const tableDetails = Audit.makeTableDetails(headings, tableItems);
+
+    return {
+      details: tableDetails,
+      score: 1,
+      displayValue: str_(UIStrings.displayValue, {
+        requestCount: summary.total.count,
+        byteCount: summary.total.size,
+      }),
+    };
+  }
+}
+
+module.exports = ResourceSummary;
+module.exports.UIStrings = UIStrings;
diff --git a/lighthouse-core/audits/seo/canonical.js b/lighthouse-core/audits/seo/canonical.js
index 75ad89955..2306c209a 100644
--- a/lighthouse-core/audits/seo/canonical.js
+++ b/lighthouse-core/audits/seo/canonical.js
@@ -53,7 +53,7 @@ class Canonical extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
-      requiredArtifacts: ['LinkElements', 'URL'],
+      requiredArtifacts: ['LinkElements', 'URL', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/seo/font-size.js b/lighthouse-core/audits/seo/font-size.js
index 8498fda2c..f6808289c 100644
--- a/lighthouse-core/audits/seo/font-size.js
+++ b/lighthouse-core/audits/seo/font-size.js
@@ -225,7 +225,7 @@ class FontSize extends Audit {
       };
     }
 
-    const viewportMeta = await ComputedViewportMeta.request(artifacts, context);
+    const viewportMeta = await ComputedViewportMeta.request(artifacts.MetaElements, context);
     if (!viewportMeta.isMobileOptimized) {
       return {
         score: 0,
diff --git a/lighthouse-core/audits/seo/is-crawlable.js b/lighthouse-core/audits/seo/is-crawlable.js
index efacb2c52..8126cca66 100644
--- a/lighthouse-core/audits/seo/is-crawlable.js
+++ b/lighthouse-core/audits/seo/is-crawlable.js
@@ -81,7 +81,7 @@ class IsCrawlable extends Audit {
       title: str_(UIStrings.title),
       failureTitle: str_(UIStrings.failureTitle),
       description: str_(UIStrings.description),
-      requiredArtifacts: ['MetaElements', 'RobotsTxt', 'URL'],
+      requiredArtifacts: ['MetaElements', 'RobotsTxt', 'URL', 'devtoolsLogs'],
     };
   }
 
diff --git a/lighthouse-core/audits/seo/tap-targets.js b/lighthouse-core/audits/seo/tap-targets.js
index b8b72c805..25afeaaf9 100644
--- a/lighthouse-core/audits/seo/tap-targets.js
+++ b/lighthouse-core/audits/seo/tap-targets.js
@@ -283,7 +283,7 @@ class TapTargets extends Audit {
       };
     }
 
-    const viewportMeta = await ComputedViewportMeta.request(artifacts, context);
+    const viewportMeta = await ComputedViewportMeta.request(artifacts.MetaElements, context);
     if (!viewportMeta.isMobileOptimized) {
       return {
         score: 0,
diff --git a/lighthouse-core/audits/viewport.js b/lighthouse-core/audits/viewport.js
index 6839f56d7..adef0f7c9 100644
--- a/lighthouse-core/audits/viewport.js
+++ b/lighthouse-core/audits/viewport.js
@@ -30,7 +30,7 @@ class Viewport extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
-    const viewportMeta = await ComputedViewportMeta.request(artifacts, context);
+    const viewportMeta = await ComputedViewportMeta.request(artifacts.MetaElements, context);
 
     if (!viewportMeta.hasViewportTag) {
       return {
diff --git a/lighthouse-core/computed/viewport-meta.js b/lighthouse-core/computed/viewport-meta.js
index 7bf2703aa..a75cecaea 100644
--- a/lighthouse-core/computed/viewport-meta.js
+++ b/lighthouse-core/computed/viewport-meta.js
@@ -11,10 +11,10 @@ const makeComputedArtifact = require('./computed-artifact.js');
 
 class ViewportMeta {
   /**
-   * @param {{MetaElements: LH.GathererArtifacts['MetaElements']}} artifacts
+   * @param {LH.GathererArtifacts['MetaElements']} MetaElements
    * @return {Promise<ViewportMetaResult>}
   */
-  static async compute_({MetaElements}) {
+  static async compute_(MetaElements) {
     const viewportMeta = MetaElements.find(meta => meta.name === 'viewport');
 
     if (!viewportMeta) {
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 48951810b..5f5beb393 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -192,6 +192,7 @@ const defaultConfig = {
     'main-thread-tasks',
     'metrics',
     'offline-start-url',
+    'resource-summary',
     'manual/pwa-cross-browser',
     'manual/pwa-page-transitions',
     'manual/pwa-each-page-has-url',
@@ -353,8 +354,8 @@ const defaultConfig = {
         {id: 'speed-index', weight: 4, group: 'metrics'},
         {id: 'interactive', weight: 5, group: 'metrics'},
         {id: 'first-cpu-idle', weight: 2, group: 'metrics'},
-        {id: 'estimated-input-latency', weight: 0, group: 'metrics'},
-        {id: 'max-potential-fid', weight: 0}, // intentionally left out of metrics so it won't be displayed yet
+        {id: 'max-potential-fid', weight: 0, group: 'metrics'},
+        {id: 'estimated-input-latency', weight: 0}, // intentionally left out of metrics so it won't be displayed
 
         {id: 'render-blocking-resources', weight: 0, group: 'load-opportunities'},
         {id: 'uses-responsive-images', weight: 0, group: 'load-opportunities'},
@@ -378,6 +379,7 @@ const defaultConfig = {
         {id: 'bootup-time', weight: 0, group: 'diagnostics'},
         {id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
         {id: 'font-display', weight: 0, group: 'diagnostics'},
+        {id: 'resource-summary', weight: 0, group: 'diagnostics'},
         // Audits past this point don't belong to a group and will not be shown automatically
         {id: 'network-requests', weight: 0},
         {id: 'network-rtt', weight: 0},
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index 16d73ca6a..a0c155cf8 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -712,7 +712,7 @@
     "description": "The name of the metric that marks the time at which the page is fully loaded and is able to quickly respond to user input (clicks, taps, and keypresses feel responsive). Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit."
   },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
-    "message": "The potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task.",
+    "message": "The maximum potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task. [Learn more](https://developers.google.com/web/updates/2018/05/first-input-delay).",
     "description": "Description of the Maximum Potential First Input Delay metric that marks the maximum estimated time between the page receiving input (a user clicking, tapping, or typing) and the page responding. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation."
   },
   "lighthouse-core/audits/metrics/max-potential-fid.js | title": {
@@ -751,6 +751,18 @@
     "message": "Avoid multiple page redirects",
     "description": "Imperative title of a Lighthouse audit that tells the user to eliminate the redirects taken through multiple URLs to load the page. This is shown in a list of audits that Lighthouse generates."
   },
+  "lighthouse-core/audits/resource-summary.js | description": {
+    "message": "To set budgets for the quantity and size of page resources, add a budget.json file.",
+    "description": "Description of a Lighthouse audit that tells the user that they can setup a budgets for the quantity and size of page resources. No character length limits."
+  },
+  "lighthouse-core/audits/resource-summary.js | displayValue": {
+    "message": "{requestCount, plural, =1 {1 request} other {# requests}} • { byteCount, number, bytes } KB",
+    "description": "[ICU Syntax] Label for an audit identifying the number of requests and kilobytes used to load the page."
+  },
+  "lighthouse-core/audits/resource-summary.js | title": {
+    "message": "Keep request counts low and transfer sizes small",
+    "description": "Imperative title of a Lighthouse audit that tells the user to minimize the size and quantity of resources used to load the page."
+  },
   "lighthouse-core/audits/seo/canonical.js | description": {
     "message": "Canonical links suggest which URL to show in search results. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/canonical).",
     "description": "Description of a Lighthouse audit that tells the user *why* they need to have a valid rel=canonical link. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation."
@@ -1223,14 +1235,50 @@
     "message": "Potential savings of {wastedMs, number, milliseconds} ms",
     "description": "Label shown per-audit to show how many milliseconds faster the page load could be if the user implemented the suggestions. The `{wastedMs}` placeholder will be replaced with the time duration, shown in milliseconds (e.g. 140 ms)"
   },
+  "lighthouse-core/lib/i18n/i18n.js | documentResourceType": {
+    "message": "Document",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all 'Document' resources loaded by a web page."
+  },
+  "lighthouse-core/lib/i18n/i18n.js | fontResourceType": {
+    "message": "Font",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all 'Font' resources loaded by a web page."
+  },
+  "lighthouse-core/lib/i18n/i18n.js | imageResourceType": {
+    "message": "Image",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all 'Image' resources loaded by a web page."
+  },
+  "lighthouse-core/lib/i18n/i18n.js | mediaResourceType": {
+    "message": "Media",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all 'Media' resources loaded by a web page. 'Media' refers to audio and video files."
+  },
   "lighthouse-core/lib/i18n/i18n.js | ms": {
     "message": "{timeInMs, number, milliseconds} ms",
     "description": "Used to show the duration in milliseconds that something lasted. The `{timeInMs}` placeholder will be replaced with the time duration, shown in milliseconds (e.g. 63 ms)"
   },
+  "lighthouse-core/lib/i18n/i18n.js | otherResourceType": {
+    "message": "Other",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all resources loaded by a web page that don't fit into the categories of Document, Script, Stylesheet, Image, Media, & Font."
+  },
+  "lighthouse-core/lib/i18n/i18n.js | scriptResourceType": {
+    "message": "Script",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all 'Script' resources loaded by a web page. 'Script' refers to JavaScript or other files that are executable by a browser."
+  },
   "lighthouse-core/lib/i18n/i18n.js | seconds": {
     "message": "{timeInMs, number, seconds} s",
     "description": "Used to show the duration in seconds that something lasted. The {timeInMs} placeholder will be replaced with the time duration, shown in seconds (e.g. 5.2 s)"
   },
+  "lighthouse-core/lib/i18n/i18n.js | stylesheetResourceType": {
+    "message": "Stylesheet",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all 'Stylesheet' resources loaded by a web page. 'Stylesheet' refers to CSS stylesheets."
+  },
+  "lighthouse-core/lib/i18n/i18n.js | thirdPartyResourceType": {
+    "message": "Third-party",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all third-party resources loaded by a web page. 'Third-party resources are items loaded from URLs that aren't controlled by the owner of the web page."
+  },
+  "lighthouse-core/lib/i18n/i18n.js | totalResourceType": {
+    "message": "Total",
+    "description": "Label for a row in a data table; entries will be the total number and byte size of all resources loaded by a web page."
+  },
   "lighthouse-core/lib/lh-error.js | badTraceRecording": {
     "message": "Something went wrong with recording the trace over your page load. Please run Lighthouse again. ({errorCode})",
     "description": "Error message explaining that the network trace was not able to be recorded for the Lighthouse run."
diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js
index 9c4ba3934..b93423b4a 100644
--- a/lighthouse-core/lib/i18n/i18n.js
+++ b/lighthouse-core/lib/i18n/i18n.js
@@ -56,6 +56,24 @@ const UIStrings = {
   columnWastedMs: 'Potential Savings',
   /** Label for the time spent column in data tables, entries will be the number of milliseconds spent during a particular activity */
   columnTimeSpent: 'Time Spent',
+  /** Label for a row in a data table; entries will be the total number and byte size of all resources loaded by a web page. */
+  totalResourceType: 'Total',
+  /** Label for a row in a data table; entries will be the total number and byte size of all 'Document' resources loaded by a web page. */
+  documentResourceType: 'Document',
+  /** Label for a row in a data table; entries will be the total number and byte size of all 'Script' resources loaded by a web page. 'Script' refers to JavaScript or other files that are executable by a browser. */
+  scriptResourceType: 'Script',
+  /** Label for a row in a data table; entries will be the total number and byte size of all 'Stylesheet' resources loaded by a web page. 'Stylesheet' refers to CSS stylesheets. */
+  stylesheetResourceType: 'Stylesheet',
+  /** Label for a row in a data table; entries will be the total number and byte size of all 'Image' resources loaded by a web page. */
+  imageResourceType: 'Image',
+  /** Label for a row in a data table; entries will be the total number and byte size of all 'Media' resources loaded by a web page. 'Media' refers to audio and video files. */
+  mediaResourceType: 'Media',
+  /** Label for a row in a data table; entries will be the total number and byte size of all 'Font' resources loaded by a web page. */
+  fontResourceType: 'Font',
+  /** Label for a row in a data table; entries will be the total number and byte size of all resources loaded by a web page that don't fit into the categories of Document, Script, Stylesheet, Image, Media, & Font.*/
+  otherResourceType: 'Other',
+  /** Label for a row in a data table; entries will be the total number and byte size of all third-party resources loaded by a web page. 'Third-party resources are items loaded from URLs that aren't controlled by the owner of the web page. */
+  thirdPartyResourceType: 'Third-party',
 };
 
 const formats = {
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index adbfcc063..e61b4b3b5 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -158,7 +158,10 @@ class CategoryRenderer {
    */
   _setRatingClass(element, score, scoreDisplayMode) {
     const rating = Util.calculateRating(score, scoreDisplayMode);
-    element.classList.add(`lh-audit--${rating}`, `lh-audit--${scoreDisplayMode.toLowerCase()}`);
+    element.classList.add(`lh-audit--${scoreDisplayMode.toLowerCase()}`);
+    if (scoreDisplayMode !== 'informative') {
+      element.classList.add(`lh-audit--${rating}`);
+    }
     return element;
   }
 
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 552ca9d64..6bbb06095 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -439,6 +439,16 @@
   display: none;
 }
 
+.lh-audit--informative .lh-audit__display-text {
+  color: var(--color-black-600);
+}
+
+.lh-audit--informative .lh-audit__score-icon {
+  border: none;
+  border-radius: 100%;
+  background: var(--color-black-400);
+}
+
 .lh-audit__description,
 .lh-audit__stackpack {
   color: var(--secondary-text-color);
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index 1504607bc..924145bf9 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -325,7 +325,15 @@ class Runner {
         ...sharedAuditContext,
       };
 
-      const product = await audit.audit(artifacts, auditContext);
+      // Only pass the declared `requiredArtifacts` to the audit
+      // The type is masquerading as `LH.Artifacts` but will only contain a subset of the keys
+      // to prevent consumers from unnecessary type assertions.
+      const requiredArtifacts = audit.meta.requiredArtifacts
+        .reduce((requiredArtifacts, artifactName) => {
+          requiredArtifacts[artifactName] = artifacts[artifactName];
+          return requiredArtifacts;
+        }, /** @type {LH.Artifacts} */ ({}));
+      const product = await audit.audit(requiredArtifacts, auditContext);
       auditResult = Audit.generateAuditResult(audit, product);
     } catch (err) {
       log.warn(audit.meta.id, `Caught exception: ${err.message}`);
diff --git a/lighthouse-core/scripts/update-report-fixtures.js b/lighthouse-core/scripts/update-report-fixtures.js
index 39b8b689b..216a07bb8 100644
--- a/lighthouse-core/scripts/update-report-fixtures.js
+++ b/lighthouse-core/scripts/update-report-fixtures.js
@@ -12,6 +12,25 @@ const {server} = require('../../lighthouse-cli/test/fixtures/static-server.js');
 
 /** @typedef {import('net').AddressInfo} AddressInfo */
 
+/** @type {LH.Config.Json} */
+const budgetedConfig = {
+  extends: 'lighthouse:default',
+  settings: {
+    budgets: [{
+      resourceSizes: [
+        {resourceType: 'script', budget: 125},
+        {resourceType: 'total', budget: 500},
+      ],
+      timings: [
+        {metric: 'interactive', budget: 5000, tolerance: 1000},
+      ],
+      resourceCounts: [
+        {resourceType: 'third-party', budget: 0},
+      ],
+    }],
+  },
+};
+
 /**
  * Update the report artifacts
  */
@@ -31,7 +50,7 @@ async function update() {
     url,
   ].join(' ');
   const flags = cliFlags.getFlags(rawFlags);
-  await cli.runLighthouse(url, flags, undefined);
+  await cli.runLighthouse(url, flags, budgetedConfig);
   await new Promise(res => server.close(res));
 }
 
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 5c36f363d..fe1ca60ab 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -1425,7 +1425,7 @@
             "title": "The user's focus is directed to new content added to the page"
         }, 
         "max-potential-fid": {
-            "description": "The potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task.", 
+            "description": "The maximum potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task. [Learn more](https://developers.google.com/web/updates/2018/05/first-input-delay).", 
             "displayValue": "120\u00a0ms", 
             "id": "max-potential-fid", 
             "numericValue": 122.537, 
@@ -2141,7 +2141,91 @@
             "score": 0.46, 
             "scoreDisplayMode": "numeric", 
             "title": "Eliminate render-blocking resources"
-        }, 
+        },
+        "resource-summary": {
+            "description": "To set budgets for the quantity and size of page resources, add a budget.json file.",
+            "details": {
+                "type": "table",
+                "headings": [
+                    {
+                        "key": "label",
+                        "itemType": "text",
+                        "text": "Resource Type"
+                    },
+                    {
+                        "key": "requestCount",
+                        "itemType": "numeric",
+                        "text": "Requests"
+                    },
+                    {
+                        "key": "size",
+                        "itemType": "bytes",
+                        "text": "Transfer Size"
+                    }
+                ],
+                "items": [
+                    {
+                        "resourceType": "total",
+                        "label": "Total",
+                        "requestCount": 18,
+                        "size": 160738
+                    },
+                    {
+                        "resourceType": "script",
+                        "label": "Script",
+                        "requestCount": 4,
+                        "size": 103675
+                    },
+                    {
+                        "resourceType": "image",
+                        "label": "Image",
+                        "requestCount": 2,
+                        "size": 24741
+                    },
+                    {
+                        "resourceType": "document",
+                        "label": "Document",
+                        "requestCount": 3,
+                        "size": 14109
+                    },
+                    {
+                        "resourceType": "other",
+                        "label": "Other",
+                        "requestCount": 2,
+                        "size": 12861
+                    },
+                    {
+                        "resourceType": "stylesheet",
+                        "label": "Stylesheet",
+                        "requestCount": 7,
+                        "size": 5352
+                    },
+                    {
+                        "resourceType": "media",
+                        "label": "Media",
+                        "requestCount": 0,
+                        "size": 0
+                    },
+                    {
+                        "resourceType": "font",
+                        "label": "Font",
+                        "requestCount": 0,
+                        "size": 0
+                    },
+                    {
+                        "resourceType": "third-party",
+                        "label": "Third-party",
+                        "requestCount": 2,
+                        "size": 30174
+                    }
+                ]
+            },
+            "displayValue": "18 requests • 157 KB",
+            "id": "resource-summary",
+            "score": null,
+            "scoreDisplayMode": "informative",
+            "title": "Keep request counts low and transfer sizes small"
+        },
         "robots-txt": {
             "description": "If your robots.txt file is malformed, crawlers may not be able to understand how you want your website to be crawled or indexed.", 
             "id": "robots-txt", 
@@ -3235,11 +3319,11 @@
                 }, 
                 {
                     "group": "metrics", 
-                    "id": "estimated-input-latency", 
+                    "id": "max-potential-fid", 
                     "weight": 0.0
                 }, 
                 {
-                    "id": "max-potential-fid", 
+                    "id": "estimated-input-latency", 
                     "weight": 0.0
                 }, 
                 {
@@ -3351,7 +3435,12 @@
                     "group": "diagnostics", 
                     "id": "font-display", 
                     "weight": 0.0
-                }, 
+                },
+                {
+                    "group": "diagnostics",
+                    "id": "resource-summary",
+                    "weight": 0
+                },
                 {
                     "id": "network-requests", 
                     "weight": 0.0
@@ -3629,24 +3718,24 @@
     "finalUrl": "http://localhost:10200/dobetterweb/dbw_tester.html", 
     "i18n": {
         "rendererFormattedStrings": {
-            "auditGroupExpandTooltip": "Show audits", 
-            "crcInitialNavigation": "Initial Navigation", 
-            "crcLongestDurationLabel": "Maximum critical path latency:", 
-            "errorLabel": "Error!", 
-            "errorMissingAuditInfo": "Report error: no audit information", 
-            "labDataTitle": "Lab Data", 
-            "lsPerformanceCategoryDescription": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.", 
-            "manualAuditsGroupTitle": "Additional items to manually check", 
-            "notApplicableAuditsGroupTitle": "Not applicable", 
-            "opportunityResourceColumnLabel": "Opportunity", 
-            "opportunitySavingsColumnLabel": "Estimated Savings", 
-            "passedAuditsGroupTitle": "Passed audits", 
-            "snippetCollapseButtonLabel": "Collapse snippet", 
-            "snippetExpandButtonLabel": "Expand snippet", 
-            "thirdPartyResourcesLabel": "Show 3rd-party resources", 
-            "toplevelWarningsMessage": "There were issues affecting this run of Lighthouse:", 
-            "varianceDisclaimer": "Values are estimated and may vary.", 
-            "warningAuditsGroupTitle": "Passed audits but with warnings", 
+            "auditGroupExpandTooltip": "Show audits",
+            "crcInitialNavigation": "Initial Navigation",
+            "crcLongestDurationLabel": "Maximum critical path latency:",
+            "errorLabel": "Error!",
+            "errorMissingAuditInfo": "Report error: no audit information",
+            "labDataTitle": "Lab Data",
+            "lsPerformanceCategoryDescription": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.",
+            "manualAuditsGroupTitle": "Additional items to manually check",
+            "notApplicableAuditsGroupTitle": "Not applicable",
+            "opportunityResourceColumnLabel": "Opportunity",
+            "opportunitySavingsColumnLabel": "Estimated Savings",
+            "passedAuditsGroupTitle": "Passed audits",
+            "snippetCollapseButtonLabel": "Collapse snippet",
+            "snippetExpandButtonLabel": "Expand snippet",
+            "thirdPartyResourcesLabel": "Show 3rd-party resources",
+            "toplevelWarningsMessage": "There were issues affecting this run of Lighthouse:",
+            "varianceDisclaimer": "Values are estimated and may vary.",
+            "warningAuditsGroupTitle": "Passed audits but with warnings",
             "warningHeader": "Warnings: "
         }
     }, 
@@ -3671,7 +3760,7 @@
                 "uses-text-compression": "You can enable text compression in your web server configuration.", 
                 "uses-webp-images": "Consider using a [plugin](https://wordpress.org/plugins/search/convert+webp/) or service that will automatically convert your uploaded images to the optimal formats."
             }, 
-            "iconDataURL": "data:image/svg+xml,%3Csvg viewBox='0 0 122.5 122.5' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%2321759b'%3E%3Cpath d='M8.7 61.3c0 20.8 12.1 38.7 29.6 47.3l-25-68.7c-3 6.5-4.6 13.7-4.6 21.4zM96.7 58.6c0-6.5-2.3-11-4.3-14.5-2.7-4.3-5.2-8-5.2-12.3 0-4.8 3.7-9.3 8.9-9.3h.7a52.4 52.4 0 0 0-79.4 9.9h3.3c5.5 0 14-.6 14-.6 2.9-.2 3.2 4 .4 4.3 0 0-2.9.4-6 .5l19.1 57L59.7 59l-8.2-22.5c-2.8-.1-5.5-.5-5.5-.5-2.8-.1-2.5-4.5.3-4.3 0 0 8.7.7 13.9.7 5.5 0 14-.7 14-.7 2.8-.2 3.2 4 .3 4.3 0 0-2.8.4-6 .5l19 56.5 5.2-17.5c2.3-7.3 4-12.5 4-17z'/%3E%3Cpath d='M62.2 65.9l-15.8 45.8a52.6 52.6 0 0 0 32.3-.9l-.4-.7zM107.4 36a49.6 49.6 0 0 1-3.6 24.2l-16.1 46.5A52.5 52.5 0 0 0 107.4 36z'/%3E%3Cpath d='M61.3 0a61.3 61.3 0 1 0 .1 122.7A61.3 61.3 0 0 0 61.3 0zm0 119.7a58.5 58.5 0 1 1 .1-117 58.5 58.5 0 0 1-.1 117z'/%3E%3C/g%3E%3C/svg%3E", 
+            "iconDataURL": "data:image/svg+xml,%3Csvg viewBox='0 0 122.5 122.5' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%232f3439'%3E%3Cpath d='M8.7 61.3c0 20.8 12.1 38.7 29.6 47.3l-25-68.7c-3 6.5-4.6 13.7-4.6 21.4zM96.7 58.6c0-6.5-2.3-11-4.3-14.5-2.7-4.3-5.2-8-5.2-12.3 0-4.8 3.7-9.3 8.9-9.3h.7a52.4 52.4 0 0 0-79.4 9.9h3.3c5.5 0 14-.6 14-.6 2.9-.2 3.2 4 .4 4.3 0 0-2.9.4-6 .5l19.1 57L59.7 59l-8.2-22.5c-2.8-.1-5.5-.5-5.5-.5-2.8-.1-2.5-4.5.3-4.3 0 0 8.7.7 13.9.7 5.5 0 14-.7 14-.7 2.8-.2 3.2 4 .3 4.3 0 0-2.8.4-6 .5l19 56.5 5.2-17.5c2.3-7.3 4-12.5 4-17z'/%3E%3Cpath d='M62.2 65.9l-15.8 45.8a52.6 52.6 0 0 0 32.3-.9l-.4-.7zM107.4 36a49.6 49.6 0 0 1-3.6 24.2l-16.1 46.5A52.5 52.5 0 0 0 107.4 36z'/%3E%3Cpath d='M61.3 0a61.3 61.3 0 1 0 .1 122.7A61.3 61.3 0 0 0 61.3 0zm0 119.7a58.5 58.5 0 1 1 .1-117 58.5 58.5 0 0 1-.1 117z'/%3E%3C/g%3E%3C/svg%3E", 
             "id": "wordpress", 
             "title": "WordPress"
         }
@@ -4085,7 +4174,19 @@
                 "entryType": "measure", 
                 "name": "lh:audit:offline-start-url", 
                 "startTime": 0.0
-            }, 
+            },
+            {
+                "duration": 100,
+                "entryType": "measure",
+                "name": "lh:audit:resource-summary",
+                "startTime": 0.0
+            },
+            {
+                "duration": 100,
+                "entryType": "measure",
+                "name": "lh:computed:ResourceSummary",
+                "startTime": 0.0
+            },
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
diff --git a/stack-packs/packs/wordpress.js b/stack-packs/packs/wordpress.js
index a15194aca..706f7465c 100644
--- a/stack-packs/packs/wordpress.js
+++ b/stack-packs/packs/wordpress.js
@@ -9,7 +9,7 @@
 
 const i18n = require('../../lighthouse-core/lib/i18n/i18n.js');
 
-const wordpressIcon = `data:image/svg+xml,%3Csvg viewBox='0 0 122.5 122.5' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%2321759b'%3E%3Cpath d='M8.7 61.3c0 20.8 12.1 38.7 29.6 47.3l-25-68.7c-3 6.5-4.6 13.7-4.6 21.4zM96.7 58.6c0-6.5-2.3-11-4.3-14.5-2.7-4.3-5.2-8-5.2-12.3 0-4.8 3.7-9.3 8.9-9.3h.7a52.4 52.4 0 0 0-79.4 9.9h3.3c5.5 0 14-.6 14-.6 2.9-.2 3.2 4 .4 4.3 0 0-2.9.4-6 .5l19.1 57L59.7 59l-8.2-22.5c-2.8-.1-5.5-.5-5.5-.5-2.8-.1-2.5-4.5.3-4.3 0 0 8.7.7 13.9.7 5.5 0 14-.7 14-.7 2.8-.2 3.2 4 .3 4.3 0 0-2.8.4-6 .5l19 56.5 5.2-17.5c2.3-7.3 4-12.5 4-17z'/%3E%3Cpath d='M62.2 65.9l-15.8 45.8a52.6 52.6 0 0 0 32.3-.9l-.4-.7zM107.4 36a49.6 49.6 0 0 1-3.6 24.2l-16.1 46.5A52.5 52.5 0 0 0 107.4 36z'/%3E%3Cpath d='M61.3 0a61.3 61.3 0 1 0 .1 122.7A61.3 61.3 0 0 0 61.3 0zm0 119.7a58.5 58.5 0 1 1 .1-117 58.5 58.5 0 0 1-.1 117z'/%3E%3C/g%3E%3C/svg%3E`;
+const wordpressIcon = `data:image/svg+xml,%3Csvg viewBox='0 0 122.5 122.5' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%232f3439'%3E%3Cpath d='M8.7 61.3c0 20.8 12.1 38.7 29.6 47.3l-25-68.7c-3 6.5-4.6 13.7-4.6 21.4zM96.7 58.6c0-6.5-2.3-11-4.3-14.5-2.7-4.3-5.2-8-5.2-12.3 0-4.8 3.7-9.3 8.9-9.3h.7a52.4 52.4 0 0 0-79.4 9.9h3.3c5.5 0 14-.6 14-.6 2.9-.2 3.2 4 .4 4.3 0 0-2.9.4-6 .5l19.1 57L59.7 59l-8.2-22.5c-2.8-.1-5.5-.5-5.5-.5-2.8-.1-2.5-4.5.3-4.3 0 0 8.7.7 13.9.7 5.5 0 14-.7 14-.7 2.8-.2 3.2 4 .3 4.3 0 0-2.8.4-6 .5l19 56.5 5.2-17.5c2.3-7.3 4-12.5 4-17z'/%3E%3Cpath d='M62.2 65.9l-15.8 45.8a52.6 52.6 0 0 0 32.3-.9l-.4-.7zM107.4 36a49.6 49.6 0 0 1-3.6 24.2l-16.1 46.5A52.5 52.5 0 0 0 107.4 36z'/%3E%3Cpath d='M61.3 0a61.3 61.3 0 1 0 .1 122.7A61.3 61.3 0 0 0 61.3 0zm0 119.7a58.5 58.5 0 1 1 .1-117 58.5 58.5 0 0 1-.1 117z'/%3E%3C/g%3E%3C/svg%3E`;
 
 const UIStrings = {
   /** Additional description of a Lighthouse audit that tells the user how they can improve performance by removing unused CSS, in the context of the Wordpress CMS platform. This is displayed after a user expands the section to see more. No character length limits. Links in (parenthesis) become link texts to additional documentation. */


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index dd4636eb9..23fece289 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -120,6 +120,9 @@ Object {
     Object {
       "path": "offline-start-url",
     },
+    Object {
+      "path": "resource-summary",
+    },
     Object {
       "path": "manual/pwa-cross-browser",
     },
@@ -694,11 +697,11 @@ Object {
         },
         Object {
           "group": "metrics",
-          "id": "estimated-input-latency",
+          "id": "max-potential-fid",
           "weight": 0,
         },
         Object {
-          "id": "max-potential-fid",
+          "id": "estimated-input-latency",
           "weight": 0,
         },
         Object {
@@ -811,6 +814,11 @@ Object {
           "id": "font-display",
           "weight": 0,
         },
+        Object {
+          "group": "diagnostics",
+          "id": "resource-summary",
+          "weight": 0,
+        },
         Object {
           "id": "network-requests",
           "weight": 0,
diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
index 2b0b8335a..230403e1b 100644
--- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
+++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
@@ -25,7 +25,7 @@ module.exports = [
           score: 0,
           details: {
             items: {
-              length: 6,
+              length: 8,
             },
           },
         },
diff --git a/lighthouse-core/test/audits/resource-summary-test.js b/lighthouse-core/test/audits/resource-summary-test.js
new file mode 100644
index 000000000..0b745ff38
--- /dev/null
+++ b/lighthouse-core/test/audits/resource-summary-test.js
@@ -0,0 +1,87 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const ResourceSummaryAudit = require('../../audits/resource-summary.js');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
+
+/* eslint-env jest */
+
+describe('Performance: Resource summary audit', () => {
+  let artifacts;
+  let context;
+  beforeEach(() => {
+    context = {computedCache: new Map()};
+
+    artifacts = {
+      devtoolsLogs: {
+        defaultPass: networkRecordsToDevtoolsLog([
+          {url: 'http://example.com/file.html', resourceType: 'Document', transferSize: 30},
+          {url: 'http://example.com/app.js', resourceType: 'Script', transferSize: 10},
+          {url: 'http://third-party.com/script.js', resourceType: 'Script', transferSize: 50},
+          {url: 'http://third-party.com/file.jpg', resourceType: 'Image', transferSize: 70},
+        ])},
+      URL: {requestedUrl: 'https://example.com', finalUrl: 'https://example.com'},
+    };
+  });
+
+  it('has three table columns', async () => {
+    const result = await ResourceSummaryAudit.audit(artifacts, context);
+    expect(result.details.headings).toHaveLength(3);
+  });
+
+  it('has the correct score', async () => {
+    const result = await ResourceSummaryAudit.audit(artifacts, context);
+    expect(result.score).toBe(1);
+  });
+
+  it('has the correct display value', async () => {
+    const result = await ResourceSummaryAudit.audit(artifacts, context);
+    expect(result.displayValue).toBeDisplayString('4 requests • 0 KB');
+  });
+
+  it('includes the correct properties for each table item', async () => {
+    const result = await ResourceSummaryAudit.audit(artifacts, context);
+    const item = result.details.items[0];
+    expect(item.resourceType).toEqual('total');
+    expect(item.label).toBeDisplayString('Total');
+    expect(item.requestCount).toBe(4);
+    expect(item.size).toBe(160);
+  });
+
+  it('includes all resource types, regardless of whether page contains them', async () => {
+    const result = await ResourceSummaryAudit.audit(artifacts, context);
+    expect(Object.keys(result.details.items)).toHaveLength(9);
+  });
+
+  it('it displays "0" if there are no resources of that type', async () => {
+    const result = await ResourceSummaryAudit.audit(artifacts, context);
+    const fontItem = result.details.items.find(item => item.resourceType === 'font');
+    expect(fontItem.requestCount).toBe(0);
+    expect(fontItem.size).toBe(0);
+  });
+
+  describe('table ordering', () => {
+    it('except for the last row, it sorts items by size (descending)', async () => {
+      const result = await ResourceSummaryAudit.audit(artifacts, context);
+      const items = result.details.items;
+      items.slice(0, -2).forEach((item, index) => {
+        expect(item.size).toBeGreaterThanOrEqual(items[index + 1].size);
+      });
+    });
+
+    it('"Total" is the first row', async () => {
+      const result = await ResourceSummaryAudit.audit(artifacts, context);
+      expect(result.details.items[0].resourceType).toBe('total');
+    });
+
+    it('"Third-party" is the last-row', async () => {
+      const result = await ResourceSummaryAudit.audit(artifacts, context);
+      const items = result.details.items;
+      expect(items[items.length - 1].resourceType).toBe('third-party');
+    });
+  });
+});
diff --git a/lighthouse-core/test/computed/viewport-meta-test.js b/lighthouse-core/test/computed/viewport-meta-test.js
index 62b89c7ec..3b2108011 100644
--- a/lighthouse-core/test/computed/viewport-meta-test.js
+++ b/lighthouse-core/test/computed/viewport-meta-test.js
@@ -14,7 +14,7 @@ describe('ViewportMeta computed artifact', () => {
   const makeMetaElements = viewport => [{name: 'viewport', content: viewport}];
 
   it('is not mobile optimized when page does not contain a viewport meta tag', async () => {
-    const {hasViewportTag, isMobileOptimized} = await ViewportMeta.compute_({MetaElements: []});
+    const {hasViewportTag, isMobileOptimized} = await ViewportMeta.compute_([]);
     assert.equal(hasViewportTag, false);
     assert.equal(isMobileOptimized, false);
   });
@@ -23,7 +23,7 @@ describe('ViewportMeta computed artifact', () => {
   it('is not mobile optimized when HTML contains a non-mobile friendly viewport meta tag', async () => {
     const viewport = 'maximum-scale=1';
     const {hasViewportTag, isMobileOptimized} =
-      await ViewportMeta.compute_({MetaElements: makeMetaElements(viewport)});
+      await ViewportMeta.compute_(makeMetaElements(viewport));
     assert.equal(hasViewportTag, true);
     assert.equal(isMobileOptimized, false);
   });
@@ -31,7 +31,7 @@ describe('ViewportMeta computed artifact', () => {
   it('is not mobile optimized when HTML contains an invalid viewport meta tag key', async () => {
     const viewport = 'nonsense=true';
     const {hasViewportTag, isMobileOptimized} =
-      await ViewportMeta.compute_({MetaElements: makeMetaElements(viewport)});
+      await ViewportMeta.compute_(makeMetaElements(viewport));
     assert.equal(hasViewportTag, true);
     assert.equal(isMobileOptimized, false);
   });
@@ -39,7 +39,7 @@ describe('ViewportMeta computed artifact', () => {
   it('is not mobile optimized when HTML contains an invalid viewport meta tag value', async () => {
     const viewport = 'initial-scale=microscopic';
     const {isMobileOptimized, parserWarnings} =
-      await ViewportMeta.compute_({MetaElements: makeMetaElements(viewport)});
+      await ViewportMeta.compute_(makeMetaElements(viewport));
     assert.equal(isMobileOptimized, false);
     assert.equal(parserWarnings[0], 'Invalid values found: {"initial-scale":"microscopic"}');
   });
@@ -48,7 +48,7 @@ describe('ViewportMeta computed artifact', () => {
   it('is not mobile optimized when HTML contains an invalid viewport meta tag key and value', async () => {
     const viewport = 'nonsense=true, initial-scale=microscopic';
     const {isMobileOptimized, parserWarnings} =
-      await ViewportMeta.compute_({MetaElements: makeMetaElements(viewport)});
+      await ViewportMeta.compute_(makeMetaElements(viewport));
     assert.equal(isMobileOptimized, false);
     assert.equal(parserWarnings[0], 'Invalid properties found: {"nonsense":"true"}');
     assert.equal(parserWarnings[1], 'Invalid values found: {"initial-scale":"microscopic"}');
@@ -64,7 +64,7 @@ describe('ViewportMeta computed artifact', () => {
 
     await Promise.all(viewports.map(async viewport => {
       const {isMobileOptimized} =
-        await ViewportMeta.compute_({MetaElements: makeMetaElements(viewport)});
+        await ViewportMeta.compute_(makeMetaElements(viewport));
       assert.equal(isMobileOptimized, true);
     }));
   });
@@ -76,7 +76,7 @@ describe('ViewportMeta computed artifact', () => {
     ];
     await Promise.all(viewports.map(async viewport => {
       const {isMobileOptimized, parserWarnings} =
-        await ViewportMeta.compute_({MetaElements: makeMetaElements(viewport)});
+        await ViewportMeta.compute_(makeMetaElements(viewport));
       assert.equal(isMobileOptimized, true);
       assert.equal(parserWarnings[0], undefined);
     }));
diff --git a/lighthouse-core/test/fixtures/artifacts/alphabet-artifacts/artifacts.json b/lighthouse-core/test/fixtures/artifacts/alphabet-artifacts/artifacts.json
new file mode 100644
index 000000000..d02ccb860
--- /dev/null
+++ b/lighthouse-core/test/fixtures/artifacts/alphabet-artifacts/artifacts.json
@@ -0,0 +1,11 @@
+{
+  "URL": {
+    "requestedUrl": "https://example.com/",
+    "finalUrl": "https://example.com/"
+  },
+  "ArtifactA": "apple",
+  "ArtifactB": "banana",
+  "ArtifactC": "cherry",
+  "ArtifactD": "date",
+  "Stacks": []
+}
diff --git a/lighthouse-core/test/report/html/renderer/category-renderer-test.js b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
index 28086f1bb..6ac2722b5 100644
--- a/lighthouse-core/test/report/html/renderer/category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
@@ -96,7 +96,34 @@ describe('CategoryRenderer', () => {
         score: 0,
       },
     });
+    assert.ok(auditDOM.matches('.lh-audit--informative'));
+  });
+
+  it('adds a pass/average/fail class for non-informative audits', () => {
+    const auditDOM = renderer.renderAudit({
+      id: 'not-informative',
+      result: {
+        title: 'Not informative',
+        description: 'help text',
+        scoreDisplayMode: 'numeric',
+        score: 0,
+      },
+    });
+    assert.ok(auditDOM.matches('.lh-audit--fail'));
+  });
 
+  it('does not add a pass/average/fail class for informative audits', () => {
+    const auditDOM = renderer.renderAudit({
+      id: 'informative',
+      result: {
+        title: 'It informs',
+        description: 'help text',
+        scoreDisplayMode: 'informative',
+        score: 0,
+      },
+    });
+    assert.equal(auditDOM.classList.length, 2);
+    assert.ok(auditDOM.matches('.lh-audit'));
     assert.ok(auditDOM.matches('.lh-audit--informative'));
   });
 
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index 00e6117bf..a65e10c93 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -41,18 +41,18 @@
             "budget": 500
           }
         ],
+        "resourceCounts": [
+          {
+            "resourceType": "third-party",
+            "budget": 0
+          }
+        ],
         "timings": [
           {
             "metric": "interactive",
             "budget": 5000,
             "tolerance": 1000
           }
-        ],
-         "resourceCounts": [
-          {
-           "resourceType": "third-party",
-           "budget": 0
-          }
         ]
       }
     ],
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index f96b06073..5dcc45032 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -194,7 +194,7 @@
     "max-potential-fid": {
       "id": "max-potential-fid",
       "title": "Max Potential FID",
-      "description": "The potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task.",
+      "description": "The maximum potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task. [Learn more](https://developers.google.com/web/updates/2018/05/first-input-delay).",
       "score": 0.92,
       "scoreDisplayMode": "numeric",
       "numericValue": 122.537,
@@ -1273,6 +1273,90 @@
       "explanation": "No usable web app manifest found on page.",
       "warnings": []
     },
+    "resource-summary": {
+      "id": "resource-summary",
+      "title": "Keep request counts low and transfer sizes small",
+      "description": "To set budgets for the quantity and size of page resources, add a budget.json file.",
+      "score": null,
+      "scoreDisplayMode": "informative",
+      "displayValue": "18 requests • 157 KB",
+      "details": {
+        "type": "table",
+        "headings": [
+          {
+            "key": "label",
+            "itemType": "text",
+            "text": "Resource Type"
+          },
+          {
+            "key": "requestCount",
+            "itemType": "numeric",
+            "text": "Requests"
+          },
+          {
+            "key": "size",
+            "itemType": "bytes",
+            "text": "Transfer Size"
+          }
+        ],
+        "items": [
+          {
+            "resourceType": "total",
+            "label": "Total",
+            "requestCount": 18,
+            "size": 160738
+          },
+          {
+            "resourceType": "script",
+            "label": "Script",
+            "requestCount": 4,
+            "size": 103675
+          },
+          {
+            "resourceType": "image",
+            "label": "Image",
+            "requestCount": 2,
+            "size": 24741
+          },
+          {
+            "resourceType": "document",
+            "label": "Document",
+            "requestCount": 3,
+            "size": 14109
+          },
+          {
+            "resourceType": "other",
+            "label": "Other",
+            "requestCount": 2,
+            "size": 12861
+          },
+          {
+            "resourceType": "stylesheet",
+            "label": "Stylesheet",
+            "requestCount": 7,
+            "size": 5352
+          },
+          {
+            "resourceType": "media",
+            "label": "Media",
+            "requestCount": 0,
+            "size": 0
+          },
+          {
+            "resourceType": "font",
+            "label": "Font",
+            "requestCount": 0,
+            "size": 0
+          },
+          {
+            "resourceType": "third-party",
+            "label": "Third-party",
+            "requestCount": 2,
+            "size": 30174
+          }
+        ]
+      }
+    },
     "pwa-cross-browser": {
       "id": "pwa-cross-browser",
       "title": "Site works cross-browser",
@@ -2998,12 +3082,12 @@
           "group": "metrics"
         },
         {
-          "id": "estimated-input-latency",
+          "id": "max-potential-fid",
           "weight": 0,
           "group": "metrics"
         },
         {
-          "id": "max-potential-fid",
+          "id": "estimated-input-latency",
           "weight": 0
         },
         {
@@ -3116,6 +3200,11 @@
           "weight": 0,
           "group": "diagnostics"
         },
+        {
+          "id": "resource-summary",
+          "weight": 0,
+          "group": "diagnostics"
+        },
         {
           "id": "network-requests",
           "weight": 0
@@ -4083,6 +4172,18 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:audit:resource-summary",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:ResourceSummary",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:pwa-cross-browser",
@@ -4859,6 +4960,48 @@
       "lighthouse-core/audits/network-server-latency.js | description": [
         "audits[network-server-latency].description"
       ],
+      "lighthouse-core/audits/resource-summary.js | title": [
+        "audits[resource-summary].title"
+      ],
+      "lighthouse-core/audits/resource-summary.js | description": [
+        "audits[resource-summary].description"
+      ],
+      "lighthouse-core/audits/resource-summary.js | displayValue": [
+        {
+          "values": {
+            "requestCount": 18,
+            "byteCount": 160738
+          },
+          "path": "audits[resource-summary].displayValue"
+        }
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | totalResourceType": [
+        "audits[resource-summary].details.items[0].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | scriptResourceType": [
+        "audits[resource-summary].details.items[1].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | imageResourceType": [
+        "audits[resource-summary].details.items[2].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | documentResourceType": [
+        "audits[resource-summary].details.items[3].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | otherResourceType": [
+        "audits[resource-summary].details.items[4].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | stylesheetResourceType": [
+        "audits[resource-summary].details.items[5].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | mediaResourceType": [
+        "audits[resource-summary].details.items[6].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | fontResourceType": [
+        "audits[resource-summary].details.items[7].label"
+      ],
+      "lighthouse-core/lib/i18n/i18n.js | thirdPartyResourceType": [
+        "audits[resource-summary].details.items[8].label"
+      ],
       "lighthouse-core/audits/accessibility/accesskeys.js | title": [
         "audits.accesskeys.title"
       ],
@@ -5483,7 +5626,7 @@
     {
       "id": "wordpress",
       "title": "WordPress",
-      "iconDataURL": "data:image/svg+xml,%3Csvg viewBox='0 0 122.5 122.5' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%2321759b'%3E%3Cpath d='M8.7 61.3c0 20.8 12.1 38.7 29.6 47.3l-25-68.7c-3 6.5-4.6 13.7-4.6 21.4zM96.7 58.6c0-6.5-2.3-11-4.3-14.5-2.7-4.3-5.2-8-5.2-12.3 0-4.8 3.7-9.3 8.9-9.3h.7a52.4 52.4 0 0 0-79.4 9.9h3.3c5.5 0 14-.6 14-.6 2.9-.2 3.2 4 .4 4.3 0 0-2.9.4-6 .5l19.1 57L59.7 59l-8.2-22.5c-2.8-.1-5.5-.5-5.5-.5-2.8-.1-2.5-4.5.3-4.3 0 0 8.7.7 13.9.7 5.5 0 14-.7 14-.7 2.8-.2 3.2 4 .3 4.3 0 0-2.8.4-6 .5l19 56.5 5.2-17.5c2.3-7.3 4-12.5 4-17z'/%3E%3Cpath d='M62.2 65.9l-15.8 45.8a52.6 52.6 0 0 0 32.3-.9l-.4-.7zM107.4 36a49.6 49.6 0 0 1-3.6 24.2l-16.1 46.5A52.5 52.5 0 0 0 107.4 36z'/%3E%3Cpath d='M61.3 0a61.3 61.3 0 1 0 .1 122.7A61.3 61.3 0 0 0 61.3 0zm0 119.7a58.5 58.5 0 1 1 .1-117 58.5 58.5 0 0 1-.1 117z'/%3E%3C/g%3E%3C/svg%3E",
+      "iconDataURL": "data:image/svg+xml,%3Csvg viewBox='0 0 122.5 122.5' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%232f3439'%3E%3Cpath d='M8.7 61.3c0 20.8 12.1 38.7 29.6 47.3l-25-68.7c-3 6.5-4.6 13.7-4.6 21.4zM96.7 58.6c0-6.5-2.3-11-4.3-14.5-2.7-4.3-5.2-8-5.2-12.3 0-4.8 3.7-9.3 8.9-9.3h.7a52.4 52.4 0 0 0-79.4 9.9h3.3c5.5 0 14-.6 14-.6 2.9-.2 3.2 4 .4 4.3 0 0-2.9.4-6 .5l19.1 57L59.7 59l-8.2-22.5c-2.8-.1-5.5-.5-5.5-.5-2.8-.1-2.5-4.5.3-4.3 0 0 8.7.7 13.9.7 5.5 0 14-.7 14-.7 2.8-.2 3.2 4 .3 4.3 0 0-2.8.4-6 .5l19 56.5 5.2-17.5c2.3-7.3 4-12.5 4-17z'/%3E%3Cpath d='M62.2 65.9l-15.8 45.8a52.6 52.6 0 0 0 32.3-.9l-.4-.7zM107.4 36a49.6 49.6 0 0 1-3.6 24.2l-16.1 46.5A52.5 52.5 0 0 0 107.4 36z'/%3E%3Cpath d='M61.3 0a61.3 61.3 0 1 0 .1 122.7A61.3 61.3 0 0 0 61.3 0zm0 119.7a58.5 58.5 0 1 1 .1-117 58.5 58.5 0 0 1-.1 117z'/%3E%3C/g%3E%3C/svg%3E",
       "descriptions": {
         "unused-css-rules": "Consider reducing, or switching, the number of [WordPress plugins](https://wordpress.org/plugins/) loading unused CSS in your page. To identify plugins that are adding extraneous CSS, try running [code coverage](https://developers.google.com/web/updates/2017/04/devtools-release-notes#coverage) in Chrome DevTools. You can identify the theme/plugin responsible from the URL of the stylesheet. Look out for plugins that have many stylesheets in the list which have a lot of red in code coverage. A plugin should only enqueue a stylesheet if it is actually used on the page.",
         "uses-webp-images": "Consider using a [plugin](https://wordpress.org/plugins/search/convert+webp/) or service that will automatically convert your uploaded images to the optimal formats.",
diff --git a/lighthouse-core/test/runner-test.js b/lighthouse-core/test/runner-test.js
index 720c85dd7..d7c30af49 100644
--- a/lighthouse-core/test/runner-test.js
+++ b/lighthouse-core/test/runner-test.js
@@ -339,6 +339,38 @@ describe('Runner', () => {
         assert.ok(auditResult.errorMessage.includes(errorMessage));
       });
     });
+
+    it('only passes the required artifacts to the audit', async () => {
+      class SimpleAudit extends Audit {
+        static get meta() {
+          return {
+            id: 'simple',
+            title: 'Requires some artifacts',
+            failureTitle: 'Artifacts',
+            description: 'Test for always throwing',
+            requiredArtifacts: ['ArtifactA', 'ArtifactC'],
+          };
+        }
+      }
+
+      const auditMockFn = SimpleAudit.audit = jest.fn().mockReturnValue({score: 1});
+      const config = new Config({
+        settings: {
+          auditMode: __dirname + '/fixtures/artifacts/alphabet-artifacts/',
+        },
+        audits: [
+          SimpleAudit,
+        ],
+      });
+
+      const results = await Runner.run({}, {config});
+      expect(results.lhr).toMatchObject({audits: {simple: {score: 1}}});
+      expect(auditMockFn).toHaveBeenCalled();
+      expect(auditMockFn.mock.calls[0][0]).toEqual({
+        ArtifactA: 'apple',
+        ArtifactC: 'cherry',
+      });
+    });
   });
 
   describe('Bad audit behavior handling', () => {