Repository: GoogleChrome/lighthouse
Pull request #6396 (2018-10-25 23:19:42)

core(config): add new PWA category groups
Starting on #6395

This PR just adds groups to the PWA category and deletes the manifest-short-name-length audit. Feel free to come up with better group descriptions or we can defer, I spent no time on them :)


Patch:
diff --git a/docs/hacking-tips.md b/docs/hacking-tips.md
index 7546b7475..33b0754db 100644
--- a/docs/hacking-tips.md
+++ b/docs/hacking-tips.md
@@ -1,5 +1,20 @@
 A few assorted scripts and tips to make hacking on Lighthouse a bit easier
 
+## Evaluate Lighthouse's runtime performance
+
+Lighthouse has instrumentation to collect timing data for its operations. The data is exposed at `LHR.timing.entries`.  You can generate a trace from this data for closer analysis.
+
+![image](https://user-images.githubusercontent.com/39191/47525915-3c477000-d853-11e8-90a2-27036f93e682.png)
+[View example trace](https://ahead-daughter.surge.sh/paulirish.json.timing.trace.html)
+
+To generate, run `yarn timing-trace` with the LHR json:
+```sh
+lighthouse http://example.com --output=json --output-path=lhr.json
+yarn timing-trace lhr.json
+```
+
+That will generate `lhr.json.timing.trace.json`. Then, drag 'n drop that file into `chrome://tracing`.
+
 ## Unhandled promise rejections
 
 Getting errors like these?
diff --git a/lighthouse-core/audits/manifest-short-name-length.js b/lighthouse-core/audits/manifest-short-name-length.js
deleted file mode 100644
index 7a0041d20..000000000
--- a/lighthouse-core/audits/manifest-short-name-length.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * @license Copyright 2016 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');
-const ManifestValues = require('../gather/computed/manifest-values');
-
-class ManifestShortNameLength extends Audit {
-  /**
-   * @return {LH.Audit.Meta}
-   */
-  static get meta() {
-    return {
-      id: 'manifest-short-name-length',
-      title: 'The `short_name` won\'t be truncated on the homescreen',
-      failureTitle: 'The `short_name` will be truncated on the homescreen',
-      description: 'Make your app\'s `short_name` fewer than 12 characters to ' +
-          'ensure that it\'s not truncated on homescreens. [Learn ' +
-          'more](https://developers.google.com/web/tools/lighthouse/audits/manifest-short_name-is-not-truncated).',
-      requiredArtifacts: ['Manifest'],
-    };
-  }
-
-  /**
-   * @param {LH.Artifacts} artifacts
-   * @param {LH.Audit.Context} context
-   * @return {Promise<LH.Audit.Product>}
-   */
-  static async audit(artifacts, context) {
-    const manifestValues = await ManifestValues.request(artifacts.Manifest, context);
-    // If there's no valid manifest, this audit is not applicable
-    if (manifestValues.isParseFailure) {
-      return {
-        rawValue: true,
-        notApplicable: true,
-      };
-    }
-
-    const shortNameCheck = manifestValues.allChecks.find(i => i.id === 'hasShortName');
-    const shortNameLengthCheck = manifestValues.allChecks.find(i => i.id === 'shortNameLength');
-
-    // If there's no short_name present, this audit is not applicable
-    if (shortNameCheck && !shortNameCheck.passing) {
-      return {
-        rawValue: true,
-        notApplicable: true,
-      };
-    }
-    // Shortname is present, but it's too long
-    if (shortNameLengthCheck && !shortNameLengthCheck.passing) {
-      return {
-        rawValue: false,
-        explanation: `Failure: ${shortNameLengthCheck.failureText}.`,
-      };
-    }
-    // Has a shortname that's under the threshold
-    return {
-      rawValue: true,
-    };
-  }
-}
-
-module.exports = ManifestShortNameLength;
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 547a1296b..d26faa2f7 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -31,6 +31,12 @@ const UIStrings = {
   diagnosticsGroupTitle: 'Diagnostics',
   /** Description of the diagnostics section of the Performance category. Within this section are audits with non-imperative titles that provide more detail on the page's page load performance characteristics. Whereas the 'Opportunities' suggest an action along with expected time savings, diagnostics do not. Within this section, the user may read the details and deduce additional actions they could take. */
   diagnosticsGroupDescription: 'More information about the performance of your application.',
+  /** Title of the Fast and Reliable section of the web app category. Within this section are audits that check if the web site loaded quickly and can reliably load even if the internet connection is very slow or goes offline. */
+  pwaFastReliableGroupTitle: 'Fast and reliable',
+  /** Title of the Installable section of the web app category. Within this section are audits that check if Chrome supports installing the web site as an app on their device. */
+  pwaInstallableGroupTitle: 'Installable',
+  /** Title of the Engaging section of the web app category. Within this section are audits that check if the developer has taken advantage of features to make their web page more enjoyable and engaging for the user. */
+  pwaEngagingGroupTitle: 'Engaging',
 };
 
 const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
@@ -116,7 +122,6 @@ const defaultConfig = {
     'webapp-install-banner',
     'splash-screen',
     'themed-omnibox',
-    'manifest-short-name-length',
     'content-width',
     'image-aspect-ratio',
     'deprecations',
@@ -225,6 +230,15 @@ const defaultConfig = {
       title: str_(UIStrings.diagnosticsGroupTitle),
       description: str_(UIStrings.diagnosticsGroupDescription),
     },
+    'pwa-fast-reliable': {
+      title: str_(UIStrings.pwaFastReliableGroupTitle),
+    },
+    'pwa-installable': {
+      title: str_(UIStrings.pwaInstallableGroupTitle),
+    },
+    'pwa-engaging': {
+      title: str_(UIStrings.pwaEngagingGroupTitle),
+    },
     'a11y-color-contrast': {
       title: 'Color Contrast Is Satisfactory',
       description: 'These are opportunities to improve the legibility of your content.',
@@ -312,27 +326,25 @@ const defaultConfig = {
     },
     'pwa': {
       title: 'Progressive Web App',
-      description: 'These checks validate the aspects of a Progressive Web App, as specified by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist).',
+      description: 'These checks validate the aspects of a Progressive Web App. [Learn more](https://developers.google.com/web/progressive-web-apps/checklist).',
       manualDescription: 'These checks are required by the baseline ' +
           '[PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist) but are ' +
           'not automatically checked by Lighthouse. They do not affect your score but it\'s important that you verify them manually.',
       auditRefs: [
-        // Most difficult and critical for good UX
-        {id: 'load-fast-enough-for-pwa', weight: 7}, // can't be green in the category without being fast
-        {id: 'works-offline', weight: 5},
-        // Encompasses most of the other checks
-        {id: 'webapp-install-banner', weight: 3},
-        // Important but not too difficult
-        {id: 'is-on-https', weight: 2},
-        {id: 'redirects-http', weight: 2},
-        {id: 'viewport', weight: 2},
-        // Relatively easy checkboxes to tick with minimal value on their own
-        {id: 'service-worker', weight: 1},
-        {id: 'without-javascript', weight: 1},
-        {id: 'splash-screen', weight: 1},
-        {id: 'themed-omnibox', weight: 1},
-        {id: 'content-width', weight: 1},
-        {id: 'manifest-short-name-length', weight: 0},
+        // Fast and Reliable
+        {id: 'load-fast-enough-for-pwa', weight: 7, group: 'pwa-fast-reliable'},
+        {id: 'works-offline', weight: 5, group: 'pwa-fast-reliable'},
+        // Installable
+        {id: 'is-on-https', weight: 2, group: 'pwa-installable'},
+        {id: 'service-worker', weight: 1, group: 'pwa-installable'},
+        {id: 'webapp-install-banner', weight: 3, group: 'pwa-installable'},
+        // Engaging
+        {id: 'redirects-http', weight: 2, group: 'pwa-engaging'},
+        {id: 'splash-screen', weight: 1, group: 'pwa-engaging'},
+        {id: 'themed-omnibox', weight: 1, group: 'pwa-engaging'},
+        {id: 'content-width', weight: 1, group: 'pwa-engaging'},
+        {id: 'viewport', weight: 2, group: 'pwa-engaging'},
+        {id: 'without-javascript', weight: 1, group: 'pwa-engaging'},
         // Manual audits
         {id: 'pwa-cross-browser', weight: 0},
         {id: 'pwa-page-transitions', weight: 0},
diff --git a/lighthouse-core/gather/computed/trace-of-tab.js b/lighthouse-core/gather/computed/trace-of-tab.js
index 59940da54..f1c7ab92d 100644
--- a/lighthouse-core/gather/computed/trace-of-tab.js
+++ b/lighthouse-core/gather/computed/trace-of-tab.js
@@ -82,10 +82,10 @@ class TraceOfTab {
     });
 
     // Find the inspected frame
-    const {startedInPageEvt, frameId} = TracingProcessor.findTracingStartedEvt(keyEvents);
+    const mainFrameIds = TracingProcessor.findMainFrameIds(keyEvents);
 
     // Filter to just events matching the frame ID for sanity
-    const frameEvents = keyEvents.filter(e => e.args.frame === frameId);
+    const frameEvents = keyEvents.filter(e => e.args.frame === mainFrameIds.frameId);
 
     // Our navStart will be the last frame navigation in the trace
     const navigationStart = frameEvents.filter(TraceOfTab.isNavigationStartOfInterest).pop();
@@ -132,10 +132,10 @@ class TraceOfTab {
     // subset all trace events to just our tab's process (incl threads other than main)
     // stable-sort events to keep them correctly nested.
     const processEvents = TraceOfTab
-      .filteredStableSort(trace.traceEvents, e => e.pid === startedInPageEvt.pid);
+      .filteredStableSort(trace.traceEvents, e => e.pid === mainFrameIds.pid);
 
     const mainThreadEvents = processEvents
-      .filter(e => e.tid === startedInPageEvt.tid);
+      .filter(e => e.tid === mainFrameIds.tid);
 
     // traceEnd must exist since at least navigationStart event was verified as existing.
     const traceEnd = trace.traceEvents.reduce((max, evt) => {
@@ -177,7 +177,7 @@ class TraceOfTab {
       timestamps,
       processEvents,
       mainThreadEvents,
-      startedInPageEvt,
+      mainFrameIds,
       navigationStartEvt: navigationStart,
       firstPaintEvt: firstPaint,
       firstContentfulPaintEvt: firstContentfulPaint,
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index 2c2c65719..7251e2976 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -238,7 +238,7 @@ class GatherRunner {
 
     const status = {
       msg: 'Loading page & waiting for onload',
-      id: 'lh:gather:loadPage',
+      id: `lh:gather:loadPage-${passContext.passConfig.passName}`,
       args: [gatherers.map(g => g.instance.name).join(', ')],
     };
     log.time(status);
@@ -271,8 +271,8 @@ class GatherRunner {
       gathererResult.push(artifactPromise);
       gathererResults[gatherer.name] = gathererResult;
       await artifactPromise.catch(() => {});
-      log.timeEnd(status);
     }
+    log.timeEnd(status);
     log.timeEnd(pStatus);
   }
 
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index 6b3f5e728..cb2b606f7 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -383,6 +383,18 @@
     "message": "Performance",
     "description": "Title of the Performance category of audits. Equivalent to 'Web performance', this term is inclusive of all web page speed and loading optimization topics. Also used as a label of a score gauge; try to limit to 20 characters."
   },
+  "lighthouse-core/config/default-config.js | pwaEngagingGroupTitle": {
+    "message": "Engaging",
+    "description": "Title of the Engaging section of the web app category. Within this section are audits that check if the developer has taken advantage of features to make their web page more enjoyable and engaging for the user."
+  },
+  "lighthouse-core/config/default-config.js | pwaFastReliableGroupTitle": {
+    "message": "Fast and reliable",
+    "description": "Title of the Fast and Reliable section of the web app category. Within this section are audits that check if the web site loaded quickly and can reliably load even if the internet connection is very slow or goes offline."
+  },
+  "lighthouse-core/config/default-config.js | pwaInstallableGroupTitle": {
+    "message": "Installable",
+    "description": "Title of the Installable section of the web app category. Within this section are audits that check if Chrome supports installing the web site as an app on their device."
+  },
   "lighthouse-core/lib/i18n/i18n.js | columnCacheTTL": {
     "message": "Cache TTL",
     "description": "Label for the TTL column in data tables, entries will be the time to live value of the cache header on a web resource"
diff --git a/lighthouse-core/lib/traces/tracing-processor.js b/lighthouse-core/lib/traces/tracing-processor.js
index 21f9ac337..74af61945 100644
--- a/lighthouse-core/lib/traces/tracing-processor.js
+++ b/lighthouse-core/lib/traces/tracing-processor.js
@@ -198,39 +198,46 @@ class TraceProcessor {
 
   /**
    * @param {LH.TraceEvent[]} events
-   * @return {{startedInPageEvt: LH.TraceEvent, frameId: string}}
+   * @return {{pid: number, tid: number, frameId: string}}
    */
-  static findTracingStartedEvt(events) {
-    /** @type {LH.TraceEvent|undefined} */
-    let startedInPageEvt;
-
+  static findMainFrameIds(events) {
     // Prefer the newer TracingStartedInBrowser event first, if it exists
     const startedInBrowserEvt = events.find(e => e.name === 'TracingStartedInBrowser');
     if (startedInBrowserEvt && startedInBrowserEvt.args.data &&
         startedInBrowserEvt.args.data.frames) {
       const mainFrame = startedInBrowserEvt.args.data.frames.find(frame => !frame.parent);
+      const frameId = mainFrame && mainFrame.frame;
       const pid = mainFrame && mainFrame.processId;
+
       const threadNameEvt = events.find(e => e.pid === pid && e.ph === 'M' &&
         e.cat === '__metadata' && e.name === 'thread_name' && e.args.name === 'CrRendererMain');
-      startedInPageEvt = mainFrame && threadNameEvt ?
-        Object.assign({}, startedInBrowserEvt, {
-          pid, tid: threadNameEvt.tid, name: 'TracingStartedInPage',
-          args: {data: {page: mainFrame.frame}}}) :
-        undefined;
+      const tid = threadNameEvt && threadNameEvt.tid;
+
+      if (pid && tid && frameId) {
+        return {
+          pid,
+          tid,
+          frameId,
+        };
+      }
     }
 
     // Support legacy browser versions that do not emit TracingStartedInBrowser event.
-    if (!startedInPageEvt) {
-      // The first TracingStartedInPage in the trace is definitely our renderer thread of interest
-      // Beware: the tracingStartedInPage event can appear slightly after a navigationStart
-      startedInPageEvt = events.find(e => e.name === 'TracingStartedInPage');
+    // The first TracingStartedInPage in the trace is definitely our renderer thread of interest
+    // Beware: the tracingStartedInPage event can appear slightly after a navigationStart
+    const startedInPageEvt = events.find(e => e.name === 'TracingStartedInPage');
+    if (startedInPageEvt && startedInPageEvt.args && startedInPageEvt.args.data) {
+      const frameId = startedInPageEvt.args.data.page;
+      if (frameId) {
+        return {
+          pid: startedInPageEvt.pid,
+          tid: startedInPageEvt.tid,
+          frameId,
+        };
+      }
     }
 
-    if (!startedInPageEvt) throw new LHError(LHError.errors.NO_TRACING_STARTED);
-
-    // @ts-ignore - property chain exists for 'TracingStartedInPage' event.
-    const frameId = /** @type {string} */ (startedInPageEvt.args.data.page);
-    return {startedInPageEvt, frameId};
+    throw new LHError(LHError.errors.NO_TRACING_STARTED);
   }
 
   /**
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index a363974fd..83d84b8fb 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -175,7 +175,8 @@ class Runner {
     const timingEntriesKeyValues = [
       ...timingEntriesFromArtifacts,
       ...timingEntriesFromRunner,
-    ].map(entry => /** @type {[string, PerformanceEntry]} */ ([entry.name, entry]));
+      // As entries can share a name, dedupe based on the startTime timestamp
+    ].map(entry => /** @type {[number, PerformanceEntry]} */ ([entry.startTime, entry]));
     const timingEntries = Array.from(new Map(timingEntriesKeyValues).values());
     const runnerEntry = timingEntries.find(e => e.name === 'lh:runner:run');
     return {entries: timingEntries, total: runnerEntry && runnerEntry.duration || 0};
diff --git a/lighthouse-core/scripts/lantern/minify-trace.js b/lighthouse-core/scripts/lantern/minify-trace.js
index c149429be..d5aede803 100755
--- a/lighthouse-core/scripts/lantern/minify-trace.js
+++ b/lighthouse-core/scripts/lantern/minify-trace.js
@@ -78,11 +78,11 @@ const traceEventsToKeepInProcess = new Set([
  * @param {LH.TraceEvent[]} events
  */
 function filterOutUnnecessaryTasksByNameAndDuration(events) {
-  const {startedInPageEvt} = TracingProcessor.findTracingStartedEvt(events);
+  const {pid} = TracingProcessor.findMainFrameIds(events);
 
   return events.filter(evt => {
     if (toplevelTaskNames.has(evt.name) && evt.dur < 1000) return false;
-    if (evt.pid === startedInPageEvt.pid && traceEventsToKeepInProcess.has(evt.name)) return true;
+    if (evt.pid === pid && traceEventsToKeepInProcess.has(evt.name)) return true;
     return traceEventsToAlwaysKeep.has(evt.name);
   });
 }
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index b45178870..f6c43f781 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -1227,13 +1227,6 @@
             "scoreDisplayMode": "manual", 
             "title": "The user's focus is directed to new content added to the page"
         }, 
-        "manifest-short-name-length": {
-            "description": "Make your app's `short_name` fewer than 12 characters to ensure that it's not truncated on homescreens. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/manifest-short_name-is-not-truncated).", 
-            "id": "manifest-short-name-length", 
-            "score": null, 
-            "scoreDisplayMode": "not_applicable", 
-            "title": "The `short_name` won't be truncated on the homescreen"
-        }, 
         "meta-description": {
             "description": "Meta descriptions may be included in search results to concisely summarize page content. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/description).", 
             "id": "meta-description", 
@@ -3039,52 +3032,59 @@
         "pwa": {
             "auditRefs": [
                 {
+                    "group": "pwa-fast-reliable", 
                     "id": "load-fast-enough-for-pwa", 
                     "weight": 7.0
                 }, 
                 {
+                    "group": "pwa-fast-reliable", 
                     "id": "works-offline", 
                     "weight": 5.0
                 }, 
                 {
-                    "id": "webapp-install-banner", 
-                    "weight": 3.0
-                }, 
-                {
+                    "group": "pwa-installable", 
                     "id": "is-on-https", 
                     "weight": 2.0
                 }, 
                 {
-                    "id": "redirects-http", 
-                    "weight": 2.0
-                }, 
-                {
-                    "id": "viewport", 
-                    "weight": 2.0
-                }, 
-                {
+                    "group": "pwa-installable", 
                     "id": "service-worker", 
                     "weight": 1.0
                 }, 
                 {
-                    "id": "without-javascript", 
-                    "weight": 1.0
+                    "group": "pwa-installable", 
+                    "id": "webapp-install-banner", 
+                    "weight": 3.0
+                }, 
+                {
+                    "group": "pwa-engaging", 
+                    "id": "redirects-http", 
+                    "weight": 2.0
                 }, 
                 {
+                    "group": "pwa-engaging", 
                     "id": "splash-screen", 
                     "weight": 1.0
                 }, 
                 {
+                    "group": "pwa-engaging", 
                     "id": "themed-omnibox", 
                     "weight": 1.0
                 }, 
                 {
+                    "group": "pwa-engaging", 
                     "id": "content-width", 
                     "weight": 1.0
                 }, 
                 {
-                    "id": "manifest-short-name-length", 
-                    "weight": 0.0
+                    "group": "pwa-engaging", 
+                    "id": "viewport", 
+                    "weight": 2.0
+                }, 
+                {
+                    "group": "pwa-engaging", 
+                    "id": "without-javascript", 
+                    "weight": 1.0
                 }, 
                 {
                     "id": "pwa-cross-browser", 
@@ -3099,7 +3099,7 @@
                     "weight": 0.0
                 }
             ], 
-            "description": "These checks validate the aspects of a Progressive Web App, as specified by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist).", 
+            "description": "These checks validate the aspects of a Progressive Web App. [Learn more](https://developers.google.com/web/progressive-web-apps/checklist).", 
             "id": "pwa", 
             "manualDescription": "These checks are required by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist) but are not automatically checked by Lighthouse. They do not affect your score but it's important that you verify them manually.", 
             "score": 0.42, 
@@ -3222,6 +3222,15 @@
         "metrics": {
             "title": "Metrics"
         }, 
+        "pwa-engaging": {
+            "title": "Engaging"
+        }, 
+        "pwa-fast-reliable": {
+            "title": "Fast and reliable"
+        }, 
+        "pwa-installable": {
+            "title": "Installable"
+        }, 
         "seo-content": {
             "description": "Format your HTML in a way that enables crawlers to better understand your app\u2019s content.", 
             "title": "Content Best Practices"
diff --git a/typings/artifacts.d.ts b/typings/artifacts.d.ts
index 56e8e8337..b681ae429 100644
--- a/typings/artifacts.d.ts
+++ b/typings/artifacts.d.ts
@@ -366,8 +366,8 @@ declare global {
         processEvents: Array<TraceEvent>;
         /** The subset of trace events from the page's main thread, sorted by timestamp. */
         mainThreadEvents: Array<TraceEvent>;
-        /** The event marking the start of tracing in the target browser. */
-        startedInPageEvt: TraceEvent;
+        /** IDs for the trace's main frame, process, and thread. */
+        mainFrameIds: {pid: number, tid: number, frameId: string};
         /** The trace event marking navigationStart. */
         navigationStartEvt: TraceEvent;
         /** The trace event marking firstPaint, if it was found. */


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 716e9943a..2c64d0662 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -72,9 +72,6 @@ Object {
     Object {
       "path": "themed-omnibox",
     },
-    Object {
-      "path": "manifest-short-name-length",
-    },
     Object {
       "path": "content-width",
     },
@@ -814,52 +811,59 @@ Object {
     "pwa": Object {
       "auditRefs": Array [
         Object {
+          "group": "pwa-fast-reliable",
           "id": "load-fast-enough-for-pwa",
           "weight": 7,
         },
         Object {
+          "group": "pwa-fast-reliable",
           "id": "works-offline",
           "weight": 5,
         },
         Object {
-          "id": "webapp-install-banner",
-          "weight": 3,
-        },
-        Object {
+          "group": "pwa-installable",
           "id": "is-on-https",
           "weight": 2,
         },
         Object {
-          "id": "redirects-http",
-          "weight": 2,
-        },
-        Object {
-          "id": "viewport",
-          "weight": 2,
-        },
-        Object {
+          "group": "pwa-installable",
           "id": "service-worker",
           "weight": 1,
         },
         Object {
-          "id": "without-javascript",
-          "weight": 1,
+          "group": "pwa-installable",
+          "id": "webapp-install-banner",
+          "weight": 3,
         },
         Object {
+          "group": "pwa-engaging",
+          "id": "redirects-http",
+          "weight": 2,
+        },
+        Object {
+          "group": "pwa-engaging",
           "id": "splash-screen",
           "weight": 1,
         },
         Object {
+          "group": "pwa-engaging",
           "id": "themed-omnibox",
           "weight": 1,
         },
         Object {
+          "group": "pwa-engaging",
           "id": "content-width",
           "weight": 1,
         },
         Object {
-          "id": "manifest-short-name-length",
-          "weight": 0,
+          "group": "pwa-engaging",
+          "id": "viewport",
+          "weight": 2,
+        },
+        Object {
+          "group": "pwa-engaging",
+          "id": "without-javascript",
+          "weight": 1,
         },
         Object {
           "id": "pwa-cross-browser",
@@ -874,7 +878,7 @@ Object {
           "weight": 0,
         },
       ],
-      "description": "These checks validate the aspects of a Progressive Web App, as specified by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist).",
+      "description": "These checks validate the aspects of a Progressive Web App. [Learn more](https://developers.google.com/web/progressive-web-apps/checklist).",
       "manualDescription": "These checks are required by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist) but are not automatically checked by Lighthouse. They do not affect your score but it's important that you verify them manually.",
       "title": "Progressive Web App",
     },
@@ -993,6 +997,15 @@ Object {
     "metrics": Object {
       "title": "Metrics",
     },
+    "pwa-engaging": Object {
+      "title": "Engaging",
+    },
+    "pwa-fast-reliable": Object {
+      "title": "Fast and reliable",
+    },
+    "pwa-installable": Object {
+      "title": "Installable",
+    },
     "seo-content": Object {
       "description": "Format your HTML in a way that enables crawlers to better understand your app’s content.",
       "title": "Content Best Practices",
@@ -1245,6 +1258,15 @@ Object {
     "metrics": Object {
       "title": "Metrics",
     },
+    "pwa-engaging": Object {
+      "title": "Engaging",
+    },
+    "pwa-fast-reliable": Object {
+      "title": "Fast and reliable",
+    },
+    "pwa-installable": Object {
+      "title": "Installable",
+    },
     "seo-content": Object {
       "description": "Format your HTML in a way that enables crawlers to better understand your app’s content.",
       "title": "Content Best Practices",
diff --git a/lighthouse-core/test/audits/manifest-short-name-length-test.js b/lighthouse-core/test/audits/manifest-short-name-length-test.js
deleted file mode 100644
index 705e5b334..000000000
--- a/lighthouse-core/test/audits/manifest-short-name-length-test.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * @license Copyright 2016 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 ManifestShortNameLengthAudit = require('../../audits/manifest-short-name-length.js');
-const assert = require('assert');
-const manifestParser = require('../../lib/manifest-parser');
-
-const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json';
-const EXAMPLE_DOC_URL = 'https://example.com/index.html';
-
-function generateMockArtifacts() {
-  return {
-    Manifest: null,
-  };
-}
-function generateMockAuditContext() {
-  return {
-    computedCache: new Map(),
-  };
-}
-
-/* eslint-env jest */
-
-describe('Manifest: short_name_length audit', () => {
-  it('marked as notApplicable if page had no manifest', () => {
-    const artifacts = generateMockArtifacts();
-    artifacts.Manifest = null;
-    const context = generateMockAuditContext();
-
-    return ManifestShortNameLengthAudit.audit(artifacts, context).then(result => {
-      assert.strictEqual(result.rawValue, true);
-      assert.strictEqual(result.notApplicable, true);
-    });
-  });
-
-  it('marked as notApplicable if manifest is present but empty', () => {
-    const artifacts = generateMockArtifacts();
-    artifacts.Manifest = manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-    const context = generateMockAuditContext();
-    return ManifestShortNameLengthAudit.audit(artifacts, context).then(result => {
-      assert.strictEqual(result.rawValue, true);
-      assert.strictEqual(result.notApplicable, true);
-    });
-  });
-
-  it('marked as notApplicable when a manifest contains no short_name', () => {
-    const artifacts = generateMockArtifacts();
-    const manifestSrc = JSON.stringify({
-      name: 'i\'m much longer than the recommended size',
-    });
-    artifacts.Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-    const context = generateMockAuditContext();
-    return ManifestShortNameLengthAudit.audit(artifacts, context).then(result => {
-      assert.strictEqual(result.rawValue, true);
-      assert.strictEqual(result.notApplicable, true);
-      assert.equal(result.explanation, undefined);
-    });
-  });
-
-  // Need to disable camelcase check for dealing with short_name.
-  /* eslint-disable camelcase */
-  it('fails when a manifest contains a too long short_name', () => {
-    const artifacts = generateMockArtifacts();
-    const manifestSrc = JSON.stringify({
-      short_name: 'i\'m much longer than the recommended size',
-    });
-    artifacts.Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-    const context = generateMockAuditContext();
-    return ManifestShortNameLengthAudit.audit(artifacts, context).then(result => {
-      assert.equal(result.rawValue, false);
-      assert.ok(result.explanation.includes('without truncation'), result.explanation);
-      assert.equal(result.notApplicable, undefined);
-    });
-  });
-
-  it('passes when a manifest contains a short_name', () => {
-    const artifacts = generateMockArtifacts();
-    const manifestSrc = JSON.stringify({
-      short_name: 'Lighthouse',
-    });
-    artifacts.Manifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-    const context = generateMockAuditContext();
-    return ManifestShortNameLengthAudit.audit(artifacts, context).then(result => {
-      assert.equal(result.rawValue, true);
-      assert.equal(result.explanation, undefined);
-      assert.equal(result.notApplicable, undefined);
-    });
-  });
-  /* eslint-enable camelcase */
-});
diff --git a/lighthouse-core/test/gather/computed/main-thread-tasks-test.js b/lighthouse-core/test/gather/computed/main-thread-tasks-test.js
index a48436065..93ce3aa38 100644
--- a/lighthouse-core/test/gather/computed/main-thread-tasks-test.js
+++ b/lighthouse-core/test/gather/computed/main-thread-tasks-test.js
@@ -14,15 +14,18 @@ const TracingProcessor = require('../../../lib/traces/tracing-processor.js');
 const assert = require('assert');
 
 describe('MainResource computed artifact', () => {
-  const args = {data: {}};
+  const pid = 1;
+  const tid = 2;
+  const frameId = 'BLAH';
+  const args = {data: {}, frame: frameId};
   const baseTs = 1241250325;
   let boilerplateTrace;
 
   beforeEach(() => {
     boilerplateTrace = [
-      {ph: 'I', name: 'TracingStartedInPage', ts: baseTs, args},
-      {ph: 'I', name: 'navigationStart', ts: baseTs, args},
-      {ph: 'R', name: 'firstContentfulPaint', ts: baseTs + 1, args},
+      {ph: 'I', name: 'TracingStartedInPage', pid, tid, ts: baseTs, args: {data: {page: frameId}}},
+      {ph: 'I', name: 'navigationStart', pid, tid, ts: baseTs, args},
+      {ph: 'R', name: 'firstContentfulPaint', pid, tid, ts: baseTs + 1, args},
     ];
   });
 
@@ -60,13 +63,13 @@ describe('MainResource computed artifact', () => {
     */
     const traceEvents = [
       ...boilerplateTrace,
-      {ph: 'X', name: 'TaskA', ts: baseTs, dur: 100e3},
-      {ph: 'B', name: 'TaskB', ts: baseTs + 5e3},
-      {ph: 'X', name: 'TaskC', ts: baseTs + 10e3, dur: 30e3},
-      {ph: 'E', name: 'TaskB', ts: baseTs + 55e3},
+      {ph: 'X', name: 'TaskA', pid, tid, ts: baseTs, dur: 100e3, args},
+      {ph: 'B', name: 'TaskB', pid, tid, ts: baseTs + 5e3, args},
+      {ph: 'X', name: 'TaskC', pid, tid, ts: baseTs + 10e3, dur: 30e3, args},
+      {ph: 'E', name: 'TaskB', pid, tid, ts: baseTs + 55e3, args},
     ];
 
-    traceEvents.forEach(evt => Object.assign(evt, {cat: 'devtools.timeline', args}));
+    traceEvents.forEach(evt => Object.assign(evt, {cat: 'devtools.timeline'}));
 
     const context = {computedCache: new Map()};
     const tasks = await MainThreadTasks.request({traceEvents}, context);
@@ -116,11 +119,11 @@ describe('MainResource computed artifact', () => {
     */
     const traceEvents = [
       ...boilerplateTrace,
-      {ph: 'X', name: 'TaskA', ts: baseTs, dur: 100e3, ...url('about:blank')},
-      {ph: 'B', name: 'TaskB', ts: baseTs + 5e3, ...stackFrames(['urlB.1', 'urlB.2'])},
-      {ph: 'X', name: 'EvaluateScript', ts: baseTs + 10e3, dur: 30e3, ...url('urlC')},
-      {ph: 'X', name: 'TaskD', ts: baseTs + 15e3, dur: 5e3, ...stackFrames(['urlD'])},
-      {ph: 'E', name: 'TaskB', ts: baseTs + 55e3},
+      {ph: 'X', name: 'TaskA', pid, tid, ts: baseTs, dur: 100e3, ...url('about:blank')},
+      {ph: 'B', name: 'TaskB', pid, tid, ts: baseTs + 5e3, ...stackFrames(['urlB.1', 'urlB.2'])},
+      {ph: 'X', name: 'EvaluateScript', pid, tid, ts: baseTs + 10e3, dur: 30e3, ...url('urlC')},
+      {ph: 'X', name: 'TaskD', pid, tid, ts: baseTs + 15e3, dur: 5e3, ...stackFrames(['urlD'])},
+      {ph: 'E', name: 'TaskB', pid, tid, ts: baseTs + 55e3},
     ];
 
     traceEvents.forEach(evt => {
diff --git a/lighthouse-core/test/gather/computed/trace-of-tab-test.js b/lighthouse-core/test/gather/computed/trace-of-tab-test.js
index 32ded3b2b..187384026 100644
--- a/lighthouse-core/test/gather/computed/trace-of-tab-test.js
+++ b/lighthouse-core/test/gather/computed/trace-of-tab-test.js
@@ -27,7 +27,7 @@ describe('Trace of Tab computed artifact:', () => {
       assert.equal(evt.pid, firstEvt.pid, 'A traceEvent is found from another process');
     });
 
-    assert.ok(firstEvt.pid === trace.startedInPageEvt.pid);
+    assert.ok(firstEvt.pid === trace.mainFrameIds.pid);
     assert.ok(firstEvt.pid === trace.navigationStartEvt.pid);
     assert.ok(firstEvt.pid === trace.firstContentfulPaintEvt.pid);
     assert.ok(firstEvt.pid === trace.firstMeaningfulPaintEvt.pid);
@@ -54,7 +54,7 @@ describe('Trace of Tab computed artifact:', () => {
   describe('finds correct FMP', () => {
     it('if there was a tracingStartedInPage after the frame\'s navStart', async () => {
       const trace = await TraceOfTab.compute_(lateTracingStartedTrace);
-      assert.equal(trace.startedInPageEvt.ts, 29343544280);
+      assert.equal(trace.mainFrameIds.frameId, '0x163736997740');
       assert.equal(trace.navigationStartEvt.ts, 29343540951);
       assert.equal(trace.firstContentfulPaintEvt.ts, 29343621005);
       assert.equal(trace.firstMeaningfulPaintEvt.ts, 29344070867);
@@ -63,7 +63,7 @@ describe('Trace of Tab computed artifact:', () => {
 
     it('if there was a tracingStartedInPage after the frame\'s navStart #2', async () => {
       const trace = await TraceOfTab.compute_(badNavStartTrace);
-      assert.equal(trace.startedInPageEvt.ts, 8885435611);
+      assert.equal(trace.mainFrameIds.frameId, '0x89915541e48');
       assert.equal(trace.navigationStartEvt.ts, 8885424467);
       assert.equal(trace.firstContentfulPaintEvt.ts, 8886056886);
       assert.equal(trace.firstMeaningfulPaintEvt.ts, 8886056891);
@@ -72,7 +72,7 @@ describe('Trace of Tab computed artifact:', () => {
 
     it('if it appears slightly before the fCP', async () => {
       const trace = await TraceOfTab.compute_(preactTrace);
-      assert.equal(trace.startedInPageEvt.ts, 1805796376829);
+      assert.equal(trace.mainFrameIds.frameId, '0x25edaa521e58');
       assert.equal(trace.navigationStartEvt.ts, 1805796384607);
       assert.equal(trace.firstContentfulPaintEvt.ts, 1805797263653);
       assert.equal(trace.firstMeaningfulPaintEvt.ts, 1805797262960);
@@ -81,7 +81,7 @@ describe('Trace of Tab computed artifact:', () => {
 
     it('from candidates if no defined FMP exists', async () => {
       const trace = await TraceOfTab.compute_(noFMPtrace);
-      assert.equal(trace.startedInPageEvt.ts, 2146735802456);
+      assert.equal(trace.mainFrameIds.frameId, '0x150343381dd0');
       assert.equal(trace.navigationStartEvt.ts, 2146735807738);
       assert.equal(trace.firstContentfulPaintEvt.ts, 2146737302468);
       assert.equal(trace.firstMeaningfulPaintEvt.ts, 2146740268666);
@@ -91,7 +91,7 @@ describe('Trace of Tab computed artifact:', () => {
 
   it('handles traces missing a paints (captured in background tab)', async () => {
     const trace = await TraceOfTab.compute_(backgroundTabTrace);
-    assert.equal(trace.startedInPageEvt.ts, 1966813248134);
+    assert.equal(trace.mainFrameIds.frameId, '0x53965941e30');
     assert.notEqual(trace.navigationStartEvt.ts, 1966813346529, 'picked wrong frame');
     assert.notEqual(trace.navigationStartEvt.ts, 1966813520313, 'picked wrong frame');
     assert.equal(
@@ -158,7 +158,7 @@ describe('Trace of Tab computed artifact:', () => {
         'args': {'name': 'CrRendererMain'},
       }]};
     const trace = await TraceOfTab.compute_(tracingStartedInBrowserTrace);
-    assert.equal(trace.startedInPageEvt.ts, 2193564729582);
+    assert.equal(trace.mainFrameIds.frameId, 'B192D1F3355A6F961EC8F0B01623C1FB');
     assert.equal(trace.navigationStartEvt.ts, 2193564790059);
   });
 
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 77e890be0..0d4cb0751 100644
--- a/lighthouse-core/test/report/html/renderer/category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
@@ -238,8 +238,8 @@ describe('CategoryRenderer', () => {
       const category = JSON.parse(JSON.stringify(origCategory));
       category.auditRefs.forEach(audit => audit.result.score = 0);
       const elem = renderer.render(category, sampleResults.categoryGroups);
-      const passedAudits = elem.querySelectorAll('.lh-passed-audits > .lh-audit');
-      const failedAudits = elem.querySelectorAll('.lh-failed-audits > .lh-audit');
+      const passedAudits = elem.querySelectorAll('.lh-passed-audits .lh-audit');
+      const failedAudits = elem.querySelectorAll('.lh-failed-audits .lh-audit');
 
       assert.equal(passedAudits.length, 0);
       assert.equal(failedAudits.length, 11);
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index fa7f08eb1..2442879ba 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -523,14 +523,6 @@
         ]
       }
     },
-    "manifest-short-name-length": {
-      "id": "manifest-short-name-length",
-      "title": "The `short_name` won't be truncated on the homescreen",
-      "description": "Make your app's `short_name` fewer than 12 characters to ensure that it's not truncated on homescreens. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/manifest-short_name-is-not-truncated).",
-      "score": null,
-      "scoreDisplayMode": "not-applicable",
-      "rawValue": true
-    },
     "content-width": {
       "id": "content-width",
       "title": "Content is sized correctly for the viewport",
@@ -2922,56 +2914,63 @@
     },
     "pwa": {
       "title": "Progressive Web App",
-      "description": "These checks validate the aspects of a Progressive Web App, as specified by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist).",
+      "description": "These checks validate the aspects of a Progressive Web App. [Learn more](https://developers.google.com/web/progressive-web-apps/checklist).",
       "manualDescription": "These checks are required by the baseline [PWA Checklist](https://developers.google.com/web/progressive-web-apps/checklist) but are not automatically checked by Lighthouse. They do not affect your score but it's important that you verify them manually.",
       "auditRefs": [
         {
           "id": "load-fast-enough-for-pwa",
-          "weight": 7
+          "weight": 7,
+          "group": "pwa-fast-reliable"
         },
         {
           "id": "works-offline",
-          "weight": 5
-        },
-        {
-          "id": "webapp-install-banner",
-          "weight": 3
+          "weight": 5,
+          "group": "pwa-fast-reliable"
         },
         {
           "id": "is-on-https",
-          "weight": 2
-        },
-        {
-          "id": "redirects-http",
-          "weight": 2
+          "weight": 2,
+          "group": "pwa-installable"
         },
         {
-          "id": "viewport",
-          "weight": 2
+          "id": "service-worker",
+          "weight": 1,
+          "group": "pwa-installable"
         },
         {
-          "id": "service-worker",
-          "weight": 1
+          "id": "webapp-install-banner",
+          "weight": 3,
+          "group": "pwa-installable"
         },
         {
-          "id": "without-javascript",
-          "weight": 1
+          "id": "redirects-http",
+          "weight": 2,
+          "group": "pwa-engaging"
         },
         {
           "id": "splash-screen",
-          "weight": 1
+          "weight": 1,
+          "group": "pwa-engaging"
         },
         {
           "id": "themed-omnibox",
-          "weight": 1
+          "weight": 1,
+          "group": "pwa-engaging"
         },
         {
           "id": "content-width",
-          "weight": 1
+          "weight": 1,
+          "group": "pwa-engaging"
         },
         {
-          "id": "manifest-short-name-length",
-          "weight": 0
+          "id": "viewport",
+          "weight": 2,
+          "group": "pwa-engaging"
+        },
+        {
+          "id": "without-javascript",
+          "weight": 1,
+          "group": "pwa-engaging"
         },
         {
           "id": "pwa-cross-browser",
@@ -3369,6 +3368,15 @@
       "title": "Diagnostics",
       "description": "More information about the performance of your application."
     },
+    "pwa-fast-reliable": {
+      "title": "Fast and reliable"
+    },
+    "pwa-installable": {
+      "title": "Installable"
+    },
+    "pwa-engaging": {
+      "title": "Engaging"
+    },
     "a11y-color-contrast": {
       "title": "Color Contrast Is Satisfactory",
       "description": "These are opportunities to improve the legibility of your content."
@@ -3800,6 +3808,15 @@
       ],
       "lighthouse-core/config/default-config.js | diagnosticsGroupDescription": [
         "categoryGroups.diagnostics.description"
+      ],
+      "lighthouse-core/config/default-config.js | pwaFastReliableGroupTitle": [
+        "categoryGroups[pwa-fast-reliable].title"
+      ],
+      "lighthouse-core/config/default-config.js | pwaInstallableGroupTitle": [
+        "categoryGroups[pwa-installable].title"
+      ],
+      "lighthouse-core/config/default-config.js | pwaEngagingGroupTitle": [
+        "categoryGroups[pwa-engaging].title"
       ]
     }
   }