Repository: GoogleChrome/lighthouse
Pull request #7358 (2019-03-02 00:40:20)

core: remove NO_ERROR from default responses
<!-- Thank you for submitting a pull request! -->

**Summary**
Removes NO_ERROR codes from default responses that contain no errors.
From some initial testing I did, this should have no effect on LR.

**Related Issues/PRs**
part of: #6336 



Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index d54eb3aeb..d6cf85be5 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -196,7 +196,11 @@ The groups property controls how to visually group audits within a category. For
 
 ## Config Extension
 
-The stock Lighthouse configurations can be extended if you only need to make small tweaks, such as adding an audit or skipping an audit, but wish to still run most of what Lighthouse offers. When adding the `extends: 'lighthouse:default'` property to your config, the default passes, audits, groups, and categories will be automatically included, allowing you modify settings or add additional audits to a pass. See [more examples below](#more-examples) to see different types of extension in action.
+The stock Lighthouse configurations can be extended if you only need to make small tweaks, such as adding an audit or skipping an audit, but wish to still run most of what Lighthouse offers. When adding the `extends: 'lighthouse:default'` property to your config, the default passes, audits, groups, and categories will be automatically included, allowing you modify settings or add additional audits to a pass.
+
+Please note that you can only extend from `lighthouse:default` or `lighthouse:full` using the `extends` property. Other internal configs found in the [lighthouse-core/config](https://github.com/GoogleChrome/lighthouse/tree/master/lighthouse-core/config) directory can be used by importing the config object from file reference, or by using the [`--preset`](https://github.com/GoogleChrome/lighthouse#cli-options) CLI flag.
+
+See [more examples below](#more-examples) to view different types of extensions in action.
 
 **Config extension is the recommended way to run custom Lighthouse**. If there's a use case that extension doesn't currently solve, we'd love to [hear from you](https://github.com/GoogleChrome/lighthouse/issues/new)!
 
@@ -205,7 +209,11 @@ The stock Lighthouse configurations can be extended if you only need to make sma
 The best examples are the ones Lighthouse uses itself! There are several reference configuration files that are maintained as part of Lighthouse.
 
 * [lighthouse-core/config/default-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/default-config.js)
+* [lighthouse-core/config/full-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/full-config.js)
+* [lighthouse-core/config/lr-desktop-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/lr-desktop-config.js)
+* [lighthouse-core/config/lr-mobile-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/lr-mobile-config.js)
 * [lighthouse-core/config/perf-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/perf-config.js)
+* [lighthouse-core/config/mixed-content-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/mixed-content-config.js)
 * [docs/recipes/custom-audit/custom-config.js](https://github.com/GoogleChrome/lighthouse/blob/master/docs/recipes/custom-audit/custom-config.js)
 * [pwmetrics](https://github.com/paulirish/pwmetrics/blob/v4.1.1/lib/perf-config.ts)
 
diff --git a/docs/readme.md b/docs/readme.md
index 51cf8fbe8..531157b73 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -82,6 +82,29 @@ log.setLevel(flags.logLevel);
 launchChromeAndRunLighthouse('https://example.com', flags).then(...);
 ```
 
+## Configuration
+In order to extend the Lighthouse configuration programmatically, you need to pass the config object as the 3rd argument. If omitted, a default configuration is used.
+
+**Example:**
+```js
+{
+  extends: 'lighthouse:default',
+  settings: {
+    onlyAudits: [
+      'first-meaningful-paint',
+      'speed-index-metric',
+      'estimated-input-latency',
+      'first-interactive',
+      'consistently-interactive',
+    ],
+  },
+}
+```
+
+You can extend base configuration from either [lighthouse:default](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/default-config.js) or [lighthouse:full](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/full-config.js). Alternatively, you can build up your own configuration from scratch to have complete control.
+
+For more information on the types of config you can provide, see [Lighthouse Configuration](https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md).
+
 ## Testing on a site with authentication
 
 When installed globally via `npm i -g lighthouse` or `yarn global add lighthouse`,
diff --git a/docs/throttling.md b/docs/throttling.md
index 41981648d..761c660ae 100644
--- a/docs/throttling.md
+++ b/docs/throttling.md
@@ -48,7 +48,7 @@ Currently, `comcast` will also throttle the websocket port that Lighthouse uses
 
 ```sh
 # Enable system traffic throttling
-comcast --latency=150 --target-bw=1600
+comcast --latency=150 --target-bw=1638
 
 # Run Lighthouse with its own throttling disabled
 lighthouse --throttling.requestLatencyMs=0 --throttling.downloadThroughputKbps=0 --throttling.uploadThroughputKbps=0 # ...
diff --git a/lighthouse-cli/cli-flags.js b/lighthouse-cli/cli-flags.js
index 6f9f0a9a6..a808cfd39 100644
--- a/lighthouse-cli/cli-flags.js
+++ b/lighthouse-cli/cli-flags.js
@@ -87,8 +87,10 @@ function getFlags(manualArgv) {
         'list-trace-categories': 'Prints a list of all required trace categories and exits',
         'additional-trace-categories':
             'Additional categories to capture with the trace (comma-delimited).',
-        'config-path': 'The path to the config JSON.',
-        'preset': 'Use a built-in configuration.',
+        'config-path': `The path to the config JSON. 
+            An example config file: lighthouse-core/config/lr-desktop-config.js`,
+        'preset': `Use a built-in configuration.
+            WARNING: If the --config-path flag is provided, this preset will be ignored.`,
         'chrome-flags':
             `Custom flags to pass to Chrome (space-delimited). For a full list of flags, see https://bit.ly/chrome-flags
             Additionally, use the CHROME_PATH environment variable to use a specific Chrome binary. Requires Chromium version 66.0 or later. If omitted, any detected Chrome Canary or Chrome stable will be used.`,
diff --git a/lighthouse-core/config/constants.js b/lighthouse-core/config/constants.js
index 85c36cd31..2a050b12b 100644
--- a/lighthouse-core/config/constants.js
+++ b/lighthouse-core/config/constants.js
@@ -43,6 +43,7 @@ const throttling = {
 /** @type {LH.Config.Settings} */
 const defaultSettings = {
   output: 'json',
+  maxWaitForFcp: 15 * 1000,
   maxWaitForLoad: 45 * 1000,
   throttlingMethod: 'simulate',
   throttling: throttling.mobileSlow4G,
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index bcb475ced..e93e9d016 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -47,8 +47,8 @@ class Driver {
      */
     this._eventEmitter = /** @type {CrdpEventEmitter} */ (new EventEmitter());
     this._connection = connection;
-    // currently only used by WPT where just Page and Network are needed
-    this._devtoolsLog = new DevtoolsLog(/^(Page|Network)\./);
+    // Used to save network and lifecycle protocol traffic. Just Page, Network, and Target are needed.
+    this._devtoolsLog = new DevtoolsLog(/^(Page|Network|Target)\./);
     this.online = true;
     /** @type {Map<string, number>} */
     this._domainEnabledCounts = new Map();
@@ -69,6 +69,20 @@ class Driver {
      */
     this._monitoredUrl = null;
 
+    let targetProxyMessageId = 0;
+    this.on('Target.attachedToTarget', event => {
+      targetProxyMessageId++;
+      // We're only interested in network requests from iframes for now as those are "part of the page".
+      if (event.targetInfo.type !== 'iframe') return;
+
+      // We want to receive information about network requests from iframes, so enable the Network domain.
+      // Network events from subtargets will be stringified and sent back on `Target.receivedMessageFromTarget`.
+      this.sendCommand('Target.sendMessageToTarget', {
+        message: JSON.stringify({id: targetProxyMessageId, method: 'Network.enable'}),
+        sessionId: event.sessionId,
+      });
+    });
+
     connection.on('protocolevent', event => {
       this._devtoolsLog.record(event);
       if (this._networkStatusMonitor) {
@@ -548,20 +562,25 @@ class Driver {
 
   /**
    * Returns a promise that resolve when a frame has a FCP.
+   * @param {number} maxWaitForFCPMs
    * @return {{promise: Promise<void>, cancel: function(): void}}
    */
-  _waitForFCP() {
+  _waitForFCP(maxWaitForFCPMs) {
     /** @type {(() => void)} */
     let cancel = () => {
       throw new Error('_waitForFCP.cancel() called before it was defined');
     };
 
-    const promise = new Promise(resolve => {
+    const promise = new Promise((resolve, reject) => {
+      const maxWaitTimeout = setTimeout(() => {
+        reject(new LHError(LHError.errors.NO_FCP));
+      }, maxWaitForFCPMs);
+
       /** @param {LH.Crdp.Page.LifecycleEventEvent} e */
       const lifecycleListener = e => {
         if (e.name === 'firstContentfulPaint') {
-          cancel();
           resolve();
+          cancel();
         }
       };
 
@@ -572,6 +591,8 @@ class Driver {
         if (canceled) return;
         canceled = true;
         this.off('Page.lifecycleEvent', lifecycleListener);
+        maxWaitTimeout && clearTimeout(maxWaitTimeout);
+        reject(new Error('Wait for FCP canceled'));
       };
     });
 
@@ -843,17 +864,17 @@ class Driver {
    * @param {number} networkQuietThresholdMs
    * @param {number} cpuQuietThresholdMs
    * @param {number} maxWaitForLoadedMs
-   * @param {boolean} shouldWaitForFCP
+   * @param {number=} maxWaitForFCPMs
    * @return {Promise<void>}
    * @private
    */
   async _waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs,
-      maxWaitForLoadedMs, shouldWaitForFCP) {
+      maxWaitForLoadedMs, maxWaitForFCPMs) {
     /** @type {NodeJS.Timer|undefined} */
     let maxTimeoutHandle;
 
     // Listener for onload. Resolves on first FCP event.
-    const waitForFCP = shouldWaitForFCP ? this._waitForFCP() : this._waitForNothing();
+    const waitForFCP = maxWaitForFCPMs ? this._waitForFCP(maxWaitForFCPMs) : this._waitForNothing();
     // Listener for onload. Resolves pauseAfterLoadMs ms after load.
     const waitForLoadEvent = this._waitForLoadEvent(pauseAfterLoadMs);
     // Network listener. Resolves when the network has been idle for networkQuietThresholdMs.
@@ -881,6 +902,11 @@ class Driver {
       return function() {
         log.verbose('Driver', 'loadEventFired and network considered idle');
       };
+    }).catch(err => {
+      // Throw the error in the cleanupFn so we still cleanup all our handlers.
+      return function() {
+        throw err;
+      };
     });
 
     // Last resort timeout. Resolves maxWaitForLoadedMs ms from now on
@@ -1015,10 +1041,16 @@ class Driver {
     await this._beginNetworkStatusMonitoring(url);
     await this._clearIsolatedContextId();
 
+    // Enable auto-attaching to subtargets so we receive iframe information
+    await this.sendCommand('Target.setAutoAttach', {
+      autoAttach: true,
+      waitForDebuggerOnStart: false,
+    });
+
     await this.sendCommand('Page.enable');
     await this.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
     await this.sendCommand('Emulation.setScriptExecutionDisabled', {value: disableJS});
-    // No timeout needed for Page.navigate. See #6413.
+    // No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413.
     const waitforPageNavigateCmd = this._innerSendCommand('Page.navigate', {url});
 
     if (waitForNavigated) {
@@ -1027,19 +1059,22 @@ class Driver {
       const passConfig = /** @type {Partial<LH.Config.Pass>} */ (passContext.passConfig || {});
       let {pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs} = passConfig;
       let maxWaitMs = passContext.settings && passContext.settings.maxWaitForLoad;
+      let maxFCPMs = passContext.settings && passContext.settings.maxWaitForFcp;
 
       /* eslint-disable max-len */
       if (typeof pauseAfterLoadMs !== 'number') pauseAfterLoadMs = DEFAULT_PAUSE_AFTER_LOAD;
       if (typeof networkQuietThresholdMs !== 'number') networkQuietThresholdMs = DEFAULT_NETWORK_QUIET_THRESHOLD;
       if (typeof cpuQuietThresholdMs !== 'number') cpuQuietThresholdMs = DEFAULT_CPU_QUIET_THRESHOLD;
       if (typeof maxWaitMs !== 'number') maxWaitMs = constants.defaultSettings.maxWaitForLoad;
+      if (typeof maxFCPMs !== 'number') maxFCPMs = constants.defaultSettings.maxWaitForFcp;
       /* eslint-enable max-len */
 
+      if (!waitForFCP) maxFCPMs = undefined;
       await this._waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs,
-          maxWaitMs, waitForFCP);
+          maxWaitMs, maxFCPMs);
     }
 
-    // Bring `Page.navigate` errors back into the promise chain. See #6739.
+    // Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
     await waitforPageNavigateCmd;
 
     return this._endNetworkStatusMonitoring();
diff --git a/lighthouse-core/lib/network-recorder.js b/lighthouse-core/lib/network-recorder.js
index 4264e34eb..ed38a3498 100644
--- a/lighthouse-core/lib/network-recorder.js
+++ b/lighthouse-core/lib/network-recorder.js
@@ -303,15 +303,26 @@ class NetworkRecorder extends EventEmitter {
     request.onResourceChangedPriority(data);
   }
 
+  /**
+   * Events from targets other than the main frame are proxied through `Target.receivedMessageFromTarget`.
+   * Their payloads are JSON-stringified into the `.message` property
+   * @param {LH.Crdp.Target.ReceivedMessageFromTargetEvent} data
+   */
+  onReceivedMessageFromTarget(data) {
+    /** @type {LH.Protocol.RawMessage} */
+    const protocolMessage = JSON.parse(data.message);
+
+    // Message was a response to some command, not an event, so we'll ignore it.
+    if ('id' in protocolMessage) return;
+    // Message was an event, replay it through our normal dispatch process.
+    this.dispatch(protocolMessage);
+  }
+
   /**
    * Routes network events to their handlers, so we can construct networkRecords
    * @param {LH.Protocol.RawEventMessage} event
    */
   dispatch(event) {
-    if (!event.method.startsWith('Network.')) {
-      return;
-    }
-
     switch (event.method) {
       case 'Network.requestWillBeSent': return this.onRequestWillBeSent(event.params);
       case 'Network.requestServedFromCache': return this.onRequestServedFromCache(event.params);
@@ -320,6 +331,7 @@ class NetworkRecorder extends EventEmitter {
       case 'Network.loadingFinished': return this.onLoadingFinished(event.params);
       case 'Network.loadingFailed': return this.onLoadingFailed(event.params);
       case 'Network.resourceChangedPriority': return this.onResourceChangedPriority(event.params);
+      case 'Target.receivedMessageFromTarget': return this.onReceivedMessageFromTarget(event.params); // eslint-disable-line max-len
       default: return;
     }
   }
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index e08294dc1..b021b81cb 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -331,7 +331,7 @@ class Runner {
   /**
    * Returns first runtimeError found in artifacts.
    * @param {LH.Artifacts} artifacts
-   * @return {LH.Result['runtimeError']}
+   * @return {LH.Result['runtimeError']|undefined}
    */
   static getArtifactRuntimeError(artifacts) {
     for (const possibleErrorArtifact of Object.values(artifacts)) {
@@ -345,10 +345,7 @@ class Runner {
       }
     }
 
-    return {
-      code: LHError.NO_ERROR,
-      message: '',
-    };
+    return undefined;
   }
 
   /**
diff --git a/readme.md b/readme.md
index bb4aa2061..5a0e99c2c 100644
--- a/readme.md
+++ b/readme.md
@@ -56,6 +56,7 @@ Configuration:
   --print-config                 Print the normalized config for the given config and options, then exit.                                  [boolean]
   --additional-trace-categories  Additional categories to capture with the trace (comma-delimited).
   --config-path                  The path to the config JSON.
+                                 An example config file: lighthouse-core/config/lr-desktop-config.js
   --chrome-flags                 Custom flags to pass to Chrome (space-delimited). For a full list of flags, see
                                  http://peter.sh/experiments/chromium-command-line-switches/.
 
@@ -65,6 +66,7 @@ Configuration:
                                                                                                                                        [default: ""]
   --port                         The port to use for the debugging protocol. Use 0 for a random port                                    [default: 0]
   --preset                       Use a built-in configuration.                                            [choices: "full", "perf", "mixed-content"]
+                                 WARNING: If the --config-path flag is provided, this preset will be ignored.
   --hostname                     The hostname to use for the debugging protocol.                                              [default: "localhost"]
   --max-wait-for-load            The timeout (in milliseconds) to wait before the page is considered done loading and the run should continue.
                                  WARNING: Very high values can lead to large traces and instability                                 [default: 45000]
@@ -164,6 +166,12 @@ lighthouse -GA=./gmailartifacts https://gmail.com
 
 The first time you run the CLI you will be prompted with a message asking you if Lighthouse can anonymously report runtime exceptions. The Lighthouse team uses this information to detect new bugs and avoid regressions. Opting out will not affect your ability to use Lighthouse in any way. [Learn more](https://github.com/GoogleChrome/lighthouse/blob/master/docs/error-reporting.md).
 
+## Using the Node module
+You can also use Lighthouse programmatically with the Node module.
+
+Read [Using Lighthouse programmatically](./docs/readme.md#using-programmatically) for help getting started.\
+Read [Lighthouse Configuration](./docs/configuration.md) to learn more about the configuration options available.
+
 ## Viewing a report
 
 Lighthouse can produce a report as JSON or HTML.
diff --git a/types/externs.d.ts b/types/externs.d.ts
index 455587626..d4d5e5192 100644
--- a/types/externs.d.ts
+++ b/types/externs.d.ts
@@ -59,19 +59,19 @@ declare global {
 
     /** Simulation settings that control the amount of network & cpu throttling in the run. */
     interface ThrottlingSettings {
-      // The round trip time in milliseconds
+      /** The round trip time in milliseconds. */
       rttMs?: number;
-      // The network throughput in kilobytes per second
+      /** The network throughput in kilobits per second. */
       throughputKbps?: number;
       // devtools settings
-      // The network request latency in milliseconds
+      /** The network request latency in milliseconds. */
       requestLatencyMs?: number;
-      // The network download throughput in kilobytes per second
+      /** The network download throughput in kilobits per second. */
       downloadThroughputKbps?: number;
-      // The network upload throughput in kilobytes per second
+      /** The network upload throughput in kilobits per second. */
       uploadThroughputKbps?: number;
       // used by both
-      // The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>)
+      /** The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>). */
       cpuSlowdownMultiplier?: number
     }
 
@@ -89,39 +89,41 @@ declare global {
      * interface and the Config's `settings` object.
      */
     interface SharedFlagsSettings {
-      // The output types (json, html, ...)
+      /** The type(s) of report output to be produced. */
       output?: OutputMode|OutputMode[];
-      // The locale setting
+      /** The locale to use for the output. */
       locale?: Locale;
-      // The maximum amount of time to wait for a page to load in ms
+      /** The maximum amount of time to wait for a page content render, in ms. If no content is rendered within this limit, the run is aborted with an error. */
+      maxWaitForFcp?: number;
+      /** The maximum amount of time to wait for a page to load, in ms. */
       maxWaitForLoad?: number;
-      // List of URL patterns to block
+      /** List of URL patterns to block. */
       blockedUrlPatterns?: string[] | null;
-      // Comma-delimited list of trace categories to include
+      /** Comma-delimited list of trace categories to include. */
       additionalTraceCategories?: string | null;
-      // Flag indicating the run should only audit
+      /** Flag indicating the run should only audit. */
       auditMode?: boolean | string;
-      // Flag indicating the run should only gather
+      /** Flag indicating the run should only gather. */
       gatherMode?: boolean | string;
-      // Flag indicating that the browser storage should not be reset for the audit
+      /** Flag indicating that the browser storage should not be reset for the audit. */
       disableStorageReset?: boolean;
-      // Flag indicating that there shouldn't be any emulation during the run
+      /** Flag indicating that there shouldn't be any emulation during the run. */
       disableDeviceEmulation?: boolean;
-      // The form factor the emulation should use
+      /** The form factor the emulation should use. */
       emulatedFormFactor?: 'mobile'|'desktop'|'none';
-      // The method used to throttle the network
+      /** The method used to throttle the network. */
       throttlingMethod?: 'devtools'|'simulate'|'provided';
-      // The throttling config settings
+      /** The throttling config settings. */
       throttling?: ThrottlingSettings;
-      // List of audits that the run should conduct
+      /** If present, the run should only conduct this list of audits. */
       onlyAudits?: string[] | null;
-      // List of categories of audits the run should conduct
+      /** If present, the run should only conduct this list of categories. */
       onlyCategories?: string[] | null;
-      // List of audits that the run should skip
+      /** If present, the run should skip this list of audits. */
       skipAudits?: string[] | null;
-      // List of extra HTTP Headers to include
+      /** List of extra HTTP Headers to include. */
       extraHeaders?: Crdp.Network.Headers | null; // See extraHeaders TODO in bin.js
-      // Precomputed lantern estimates to use instead of observed analysis
+      /** Precomputed lantern estimates to use instead of observed analysis. */
       precomputedLanternData?: PrecomputedLanternData | null;
     }
 
@@ -130,9 +132,13 @@ declare global {
      * Lighthouse module but will not end up in the Config settings.
      */
     export interface Flags extends SharedFlagsSettings {
+      /** The port to use for the debugging protocol, if manually connecting. */
       port?: number;
+      /** The hostname to use for the debugging protocol, if manually connecting. */
       hostname?: string;
+      /** The level of logging to enable. */
       logLevel?: 'silent'|'error'|'info'|'verbose';
+      /** The path to the config JSON. */
       configPath?: string;
     }
 
@@ -143,31 +149,35 @@ declare global {
     export interface CliFlags extends Flags {
       _: string[];
       chromeFlags: string;
-      // Output path for the generated assets
+      /** Output path for the generated results. */
       outputPath: string;
-      // Flag to save the trace contents and screenshots to disk
+      /** Flag to save the trace contents and screenshots to disk. */
       saveAssets: boolean;
-      // Flag to open the report immediately
+      /** Flag to open the report immediately. */
       view: boolean;
-      // Enables error reporting
+      /** Flag to enable error reporting. */
       enableErrorReporting?: boolean;
-      // Flag to print out a list of all audits + categories
+      /** Flag to print a list of all audits + categories. */
       listAllAudits: boolean;
-      // Flag to print out a list of all required trace categories
+      /** Flag to print a list of all required trace categories. */
       listTraceCategories: boolean;
-      // A preset audit of selected audit categories to run
+      /** A preset audit of selected audit categories to run. */
       preset?: 'full'|'mixed-content'|'perf';
+      /** A flag to enable logLevel 'verbose'. */
       verbose: boolean;
+      /** A flag to enable logLevel 'silent'. */
       quiet: boolean;
-      // following are given defaults in cli-flags, so are not optional like in Flags or SharedFlagsSettings
-      output: OutputMode[];
-      port: number;
-      hostname: string;
+      /** A flag to print the normalized config for the given config and options, then exit. */
       printConfig: boolean;
-      // Path to the file where precomputed lantern data should be read from.
+      /** Path to the file where precomputed lantern data should be read from. */
       precomputedLanternDataPath?: string;
-      // Path to the file where precomputed lantern data should be written to.
+      /** Path to the file where precomputed lantern data should be written to. */
       lanternDataOutputPath?: string;
+
+      // The following are given defaults in cli-flags, so are not optional like in Flags or SharedFlagsSettings.
+      output: OutputMode[];
+      port: number;
+      hostname: string;
     }
 
     export interface RunnerResult {
diff --git a/types/lhr.d.ts b/types/lhr.d.ts
index d1b25612d..6e7193fe2 100644
--- a/types/lhr.d.ts
+++ b/types/lhr.d.ts
@@ -53,7 +53,7 @@ declare global {
       /** List of top-level warnings for this Lighthouse run. */
       runWarnings: string[];
       /** A top-level error message that, if present, indicates a serious enough problem that this Lighthouse result may need to be discarded. */
-      runtimeError: {code: string, message: string};
+      runtimeError?: {code: string, message: string};
       /** The User-Agent string of the browser used run Lighthouse for these results. */
       userAgent: string;
       /** Information about the environment in which Lighthouse was run. */


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 7e6e5f7b5..111adf17a 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -1208,6 +1208,7 @@ Object {
     "extraHeaders": null,
     "gatherMode": false,
     "locale": "en-US",
+    "maxWaitForFcp": 15000,
     "maxWaitForLoad": 45000,
     "onlyAudits": null,
     "onlyCategories": null,
@@ -1336,6 +1337,7 @@ Object {
     "extraHeaders": null,
     "gatherMode": false,
     "locale": "en-US",
+    "maxWaitForFcp": 15000,
     "maxWaitForLoad": 45000,
     "onlyAudits": Array [
       "metrics",
diff --git a/lighthouse-cli/test/fixtures/byte-efficiency/gzip.html b/lighthouse-cli/test/fixtures/byte-efficiency/gzip.html
index d6ba29c44..033b3020c 100644
--- a/lighthouse-cli/test/fixtures/byte-efficiency/gzip.html
+++ b/lighthouse-cli/test/fixtures/byte-efficiency/gzip.html
@@ -13,5 +13,6 @@
 <script src="script.js"></script>
 </head>
 <body>
+  GZIP FTW!
 </body>
 </html>
diff --git a/lighthouse-cli/test/fixtures/oopif.html b/lighthouse-cli/test/fixtures/oopif.html
new file mode 100644
index 000000000..b27db2d3c
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/oopif.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Where is my iframe?</title>
+  </head>
+  <body>
+    <h1>Hello frames</h1>
+    <iframe name="oopif" src="https://airhorner.com"></iframe>
+  </body>
+</html>
diff --git a/lighthouse-cli/test/smokehouse/oopif-config.js b/lighthouse-cli/test/smokehouse/oopif-config.js
new file mode 100644
index 000000000..ebc5fa22b
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/oopif-config.js
@@ -0,0 +1,18 @@
+/**
+ * @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';
+
+/**
+ * Config file for running the OOPIF tests
+ */
+module.exports = {
+  extends: 'lighthouse:default',
+  settings: {
+    onlyAudits: [
+      'network-requests',
+    ],
+  },
+};
diff --git a/lighthouse-cli/test/smokehouse/oopif-expectations.js b/lighthouse-cli/test/smokehouse/oopif-expectations.js
new file mode 100644
index 000000000..94eac432d
--- /dev/null
+++ b/lighthouse-cli/test/smokehouse/oopif-expectations.js
@@ -0,0 +1,28 @@
+/**
+ * @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';
+
+/**
+ * Expected Lighthouse audit values for sites with OOPIFS.
+ */
+module.exports = [
+  {
+    requestedUrl: 'http://localhost:10200/oopif.html',
+    finalUrl: 'http://localhost:10200/oopif.html',
+    audits: {
+      'network-requests': {
+        details: {
+          items: {
+            // The page itself only makes a few requests.
+            // We want to make sure we are finding the iframe's requests (airhorner) while being flexible enough
+            // to allow changes to the live site.
+            length: '>10',
+          },
+        },
+      },
+    },
+  },
+];
diff --git a/lighthouse-cli/test/smokehouse/run-smoke.js b/lighthouse-cli/test/smokehouse/run-smoke.js
index fd891c093..5399eb77b 100644
--- a/lighthouse-cli/test/smokehouse/run-smoke.js
+++ b/lighthouse-cli/test/smokehouse/run-smoke.js
@@ -35,6 +35,11 @@ const SMOKETESTS = [{
   expectations: smokehouseDir + 'error-expectations.js',
   config: smokehouseDir + 'error-config.js',
   batch: 'errors',
+}, {
+  id: 'oopif',
+  expectations: smokehouseDir + 'oopif-expectations.js',
+  config: smokehouseDir + 'oopif-config.js',
+  batch: 'parallel-first',
 }, {
   id: 'pwa',
   expectations: smokehouseDir + 'pwa-expectations.js',
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index 5befd18dd..89ec69f61 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -487,6 +487,7 @@ describe('.gotoURL', () => {
       .mockResponse('Page.setLifecycleEventsEnabled', {})
       .mockResponse('Emulation.setScriptExecutionDisabled', {})
       .mockResponse('Page.navigate', {})
+      .mockResponse('Target.setAutoAttach', {})
       .mockResponse('Runtime.evaluate', {});
   });
 
@@ -662,6 +663,19 @@ describe('.gotoURL', () => {
       expect(driver._waitForCPUIdle.getMockCancelFn()).toHaveBeenCalled();
     });
 
+    it('should cleanup listeners even when waits reject', async () => {
+      driver._waitForLoadEvent = createMockWaitForFn();
+
+      const loadPromise = makePromiseInspectable(driver.gotoURL(url, {waitForLoad: true}));
+
+      driver._waitForLoadEvent.mockReject();
+      await flushAllTimersAndMicrotasks();
+      expect(loadPromise).toBeDone('Did not reject load promise when load rejected');
+      await expect(loadPromise).rejects.toBeTruthy();
+      // Make sure we still cleaned up our listeners
+      expect(driver._waitForLoadEvent.getMockCancelFn()).toHaveBeenCalled();
+    });
+
     it('does not reject when page is secure', async () => {
       const secureSecurityState = {
         explanations: [],
@@ -739,6 +753,58 @@ describe('.gotoURL', () => {
   });
 });
 
+describe('._waitForFCP', () => {
+  it('should not resolve until FCP fires', async () => {
+    driver.on = driver.once = createMockOnceFn();
+
+    const waitPromise = makePromiseInspectable(driver._waitForFCP(60 * 1000).promise);
+    const listener = driver.on.findListener('Page.lifecycleEvent');
+
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).not.toBeDone('Resolved without FCP');
+
+    listener({name: 'domContentLoaded'});
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).not.toBeDone('Resolved on wrong event');
+
+    listener({name: 'firstContentfulPaint'});
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).toBeDone('Did not resolve with FCP');
+    await waitPromise;
+  });
+
+  it('should timeout', async () => {
+    driver.on = driver.once = createMockOnceFn();
+
+    const waitPromise = makePromiseInspectable(driver._waitForFCP(5000).promise);
+
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).not.toBeDone('Resolved before timeout');
+
+    jest.advanceTimersByTime(5001);
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).toBeDone('Did not resolve after timeout');
+    await expect(waitPromise).rejects.toMatchObject({code: 'NO_FCP'});
+  });
+
+  it('should be cancellable', async () => {
+    driver.on = driver.once = createMockOnceFn();
+    driver.off = jest.fn();
+
+    const {promise: rawPromise, cancel} = driver._waitForFCP(5000);
+    const waitPromise = makePromiseInspectable(rawPromise);
+
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).not.toBeDone('Resolved before timeout');
+
+    cancel();
+    await flushAllTimersAndMicrotasks();
+    expect(waitPromise).toBeDone('Did not cancel promise');
+    expect(driver.off).toHaveBeenCalled();
+    await expect(waitPromise).rejects.toMatchObject({message: 'Wait for FCP canceled'});
+  });
+});
+
 describe('.assertNoSameOriginServiceWorkerClients', () => {
   beforeEach(() => {
     connectionStub.sendCommand = createMockSendCommandFn()
@@ -930,3 +996,36 @@ describe('.goOnline', () => {
     });
   });
 });
+
+describe('Multi-target management', () => {
+  it('enables the Network domain for iframes', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Target.sendMessageToTarget', {});
+
+    driver._eventEmitter.emit('Target.attachedToTarget', {
+      sessionId: 123,
+      targetInfo: {type: 'iframe'},
+    });
+    await flushAllTimersAndMicrotasks();
+
+    const sendMessageArgs = connectionStub.sendCommand
+      .findInvocation('Target.sendMessageToTarget');
+    expect(sendMessageArgs).toEqual({
+      message: '{"id":1,"method":"Network.enable"}',
+      sessionId: 123,
+    });
+  });
+
+  it('ignores other target types', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+    .mockResponse('Target.sendMessageToTarget', {});
+
+    driver._eventEmitter.emit('Target.attachedToTarget', {
+      sessionId: 123,
+      targetInfo: {type: 'service_worker'},
+    });
+    await flushAllTimersAndMicrotasks();
+
+    expect(connectionStub.sendCommand).not.toHaveBeenCalled();
+  });
+});
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index fb0b83114..172934d9c 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -10,10 +10,6 @@
   "requestedUrl": "http://localhost:10200/dobetterweb/dbw_tester.html",
   "finalUrl": "http://localhost:10200/dobetterweb/dbw_tester.html",
   "runWarnings": [],
-  "runtimeError": {
-    "code": "NO_ERROR",
-    "message": ""
-  },
   "audits": {
     "is-on-https": {
       "id": "is-on-https",
@@ -3164,6 +3160,7 @@
     "output": [
       "json"
     ],
+    "maxWaitForFcp": 15000,
     "maxWaitForLoad": 45000,
     "throttlingMethod": "devtools",
     "throttling": {
diff --git a/lighthouse-core/test/runner-test.js b/lighthouse-core/test/runner-test.js
index 52230e3a6..18a05dab6 100644
--- a/lighthouse-core/test/runner-test.js
+++ b/lighthouse-core/test/runner-test.js
@@ -112,6 +112,21 @@ describe('Runner', () => {
       }
     });
 
+    it('does not include a top-level runtimeError when gatherers were successful', async () => {
+      const config = new Config({
+        settings: {
+          auditMode: __dirname + '/fixtures/artifacts/perflog/',
+
+        },
+        audits: [
+          'content-width',
+        ],
+      });
+
+      const {lhr} = await Runner.run(undefined, {config});
+      assert.strictEqual(lhr.runtimeError, undefined);
+    });
+
     it('-GA is a normal run but it saves artifacts to disk', () => {
       const settings = {auditMode: artifactsPath, gatherMode: artifactsPath};
       const opts = {url, config: generateConfig(settings), driverMock};