Repository: GoogleChrome/lighthouse
Pull request #7613 (2019-03-21 03:33:58)

clients(lr): allow for custom config
**Summary**

Main motivator: Smokerider needs to pass a custom config to Lightrider, otherwise the results do not correspond to the expectations.

But also, hey, a way to configure plugins in Lightrider! Just provide a config that extends the LR preset + adds your plugin coolness.

I can open a WIP internal CL for the LR branch that uses these changes, if necessary to review.

**Related Issues/PRs**

Just the proto changes: #7614
[Internal Googler bug](b/128444698)



Hoten: Forgot about the round trip test :)

Patch:
diff --git a/clients/lightrider-entry.js b/clients/lightrider-entry.js
index 36bc65210..01223693c 100644
--- a/clients/lightrider-entry.js
+++ b/clients/lightrider-entry.js
@@ -21,14 +21,16 @@ const LR_PRESETS = {
 
 /**
  * Run lighthouse for connection and provide similar results as in CLI.
+ *
+ * If configOverride is provided, lrDevice and categoryIDs are ignored.
  * @param {Connection} connection
  * @param {string} url
  * @param {LH.Flags} flags Lighthouse flags, including `output`
- * @param {{lrDevice?: 'desktop'|'mobile', categoryIDs?: Array<string>, logAssets: boolean, keepRawValues: boolean}} lrOpts Options coming from Lightrider
+ * @param {{lrDevice?: 'desktop'|'mobile', categoryIDs?: Array<string>, logAssets: boolean, keepRawValues: boolean, configOverride?: LH.Config.Json}} lrOpts Options coming from Lightrider
  * @return {Promise<string|Array<string>|void>}
  */
 async function runLighthouseInLR(connection, url, flags, lrOpts) {
-  const {lrDevice, categoryIDs, logAssets, keepRawValues} = lrOpts;
+  const {lrDevice, categoryIDs, logAssets, keepRawValues, configOverride} = lrOpts;
 
   // Certain fixes need to kick in under LR, see https://github.com/GoogleChrome/lighthouse/issues/5839
   global.isLightrider = true;
@@ -38,10 +40,15 @@ async function runLighthouseInLR(connection, url, flags, lrOpts) {
   flags.logLevel = flags.logLevel || 'info';
   flags.channel = 'lr';
 
-  const config = lrDevice === 'desktop' ? LR_PRESETS.desktop : LR_PRESETS.mobile;
-  if (categoryIDs) {
-    config.settings = config.settings || {};
-    config.settings.onlyCategories = categoryIDs;
+  let config;
+  if (configOverride) {
+    config = configOverride;
+  } else {
+    config = lrDevice === 'desktop' ? LR_PRESETS.desktop : LR_PRESETS.mobile;
+    if (categoryIDs) {
+      config.settings = config.settings || {};
+      config.settings.onlyCategories = categoryIDs;
+    }
   }
 
   try {
diff --git a/lighthouse-core/computed/main-thread-tasks.js b/lighthouse-core/computed/main-thread-tasks.js
index af38bd441..f824bb44b 100644
--- a/lighthouse-core/computed/main-thread-tasks.js
+++ b/lighthouse-core/computed/main-thread-tasks.js
@@ -109,7 +109,7 @@ class MainThreadTasks {
       if (!currentTask) {
         // We can't start a task with an end event
         if (event.ph === 'E') {
-          throw new Error('Fatal trace logic error');
+          throw new Error('Fatal trace logic error - unexpected end event');
         }
 
         currentTask = MainThreadTasks._createNewTaskNode(event);
@@ -125,7 +125,8 @@ class MainThreadTasks {
         currentTask = newTask;
       } else {
         if (currentTask.event.ph !== 'B') {
-          throw new Error('Fatal trace logic error');
+          throw new Error(
+            `Fatal trace logic error - expected start event, got ${currentTask.event.ph}`);
         }
 
         // We're ending an event, update the end time and the currentTask to its parent
@@ -147,11 +148,16 @@ class MainThreadTasks {
 
   /**
    * @param {TaskNode} task
+   * @param {TaskNode|undefined} parent
    * @return {number}
    */
-  static _computeRecursiveSelfTime(task) {
+  static _computeRecursiveSelfTime(task, parent) {
+    if (parent && task.endTime > parent.endTime) {
+      throw new Error('Fatal trace logic error - child cannot end after parent');
+    }
+
     const childTime = task.children
-      .map(MainThreadTasks._computeRecursiveSelfTime)
+      .map(child => MainThreadTasks._computeRecursiveSelfTime(child, task))
       .reduce((sum, child) => sum + child, 0);
     task.duration = task.endTime - task.startTime;
     task.selfTime = task.duration - childTime;
@@ -236,7 +242,7 @@ class MainThreadTasks {
     for (const task of tasks) {
       if (task.parent) continue;
 
-      MainThreadTasks._computeRecursiveSelfTime(task);
+      MainThreadTasks._computeRecursiveSelfTime(task, undefined);
       MainThreadTasks._computeRecursiveAttributableURLs(task, [], priorTaskData);
       MainThreadTasks._computeRecursiveTaskGroup(task);
     }
diff --git a/lighthouse-core/computed/network-analysis.js b/lighthouse-core/computed/network-analysis.js
index 07784eb78..3db08d2ff 100644
--- a/lighthouse-core/computed/network-analysis.js
+++ b/lighthouse-core/computed/network-analysis.js
@@ -57,7 +57,7 @@ class NetworkAnalysis {
     const records = await NetworkRecords.request(devtoolsLog, context);
     const throughput = NetworkAnalyzer.estimateThroughput(records);
     const rttAndServerResponseTime = NetworkAnalysis.computeRTTAndServerResponseTime(records);
-    return {records, throughput, ...rttAndServerResponseTime};
+    return {throughput, ...rttAndServerResponseTime};
   }
 }
 
diff --git a/lighthouse-core/computed/page-dependency-graph.js b/lighthouse-core/computed/page-dependency-graph.js
index e575940c8..2a9a93442 100644
--- a/lighthouse-core/computed/page-dependency-graph.js
+++ b/lighthouse-core/computed/page-dependency-graph.js
@@ -133,9 +133,9 @@ class PageDependencyGraph {
         rootNode.addDependent(node);
       }
 
-      const redirects = Array.from(node.record.redirects || []);
-      redirects.push(node.record);
+      if (!node.record.redirects) return;
 
+      const redirects = [...node.record.redirects, node.record];
       for (let i = 1; i < redirects.length; i++) {
         const redirectNode = networkNodeOutput.idToNodeMap.get(redirects[i - 1].requestId);
         const actualNode = networkNodeOutput.idToNodeMap.get(redirects[i].requestId);
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index b800c9adf..d0d8176f7 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -295,7 +295,7 @@ class Driver {
    * @param {string[]} parentSessionIds The list of session ids of the parents from youngest to oldest.
    */
   async _handleReceivedMessageFromTarget(event, parentSessionIds) {
-    const {sessionId, message} = event;
+    const {targetId, sessionId, message} = event;
     /** @type {LH.Protocol.RawMessage} */
     const protocolMessage = JSON.parse(message);
 
@@ -317,7 +317,7 @@ class Driver {
     }
 
     if (protocolMessage.method.startsWith('Network')) {
-      this._handleProtocolEvent(protocolMessage);
+      this._handleProtocolEvent({...protocolMessage, source: {targetId, sessionId}});
     }
   }
 
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
index 0a96d98b3..a9a533cb5 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
@@ -45,7 +45,8 @@ class OptimizedImages extends Gatherer {
     /** @type {Set<string>} */
     const seenUrls = new Set();
     return networkRecords.reduce((prev, record) => {
-      if (seenUrls.has(record.url) || !record.finished) {
+      // Skip records that we've seen before, never finished, or came from OOPIFs.
+      if (seenUrls.has(record.url) || !record.finished || record.sessionId) {
         return prev;
       }
 
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js b/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js
index 235845617..ed58dcb26 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js
@@ -40,6 +40,9 @@ class ResponseCompression extends Gatherer {
     const unoptimizedResponses = [];
 
     networkRecords.forEach(record => {
+      // Ignore records from OOPIFs
+      if (record.sessionId) return;
+
       const mimeType = record.mimeType;
       const resourceType = record.resourceType || NetworkRequest.TYPES.Other;
       const resourceSize = record.resourceSize;
diff --git a/lighthouse-core/gather/gatherers/scripts.js b/lighthouse-core/gather/gatherers/scripts.js
index 537ed29c4..fa0969fb4 100644
--- a/lighthouse-core/gather/gatherers/scripts.js
+++ b/lighthouse-core/gather/gatherers/scripts.js
@@ -58,6 +58,9 @@ class Scripts extends Gatherer {
     }
 
     const scriptRecords = loadData.networkRecords
+      // Ignore records from OOPIFs
+      .filter(record => !record.sessionId)
+      // Only get the content of script requests
       .filter(record => record.resourceType === NetworkRequest.TYPES.Script);
 
     for (const record of scriptRecords) {
diff --git a/lighthouse-core/lib/dependency-graph/simulator/connection-pool.js b/lighthouse-core/lib/dependency-graph/simulator/connection-pool.js
index caa4f7e73..8f3e29d2c 100644
--- a/lighthouse-core/lib/dependency-graph/simulator/connection-pool.js
+++ b/lighthouse-core/lib/dependency-graph/simulator/connection-pool.js
@@ -139,14 +139,10 @@ module.exports = class ConnectionPool {
    * @return {?TcpConnection}
    */
   acquire(record, options = {}) {
-    if (this._connectionsByRecord.has(record)) {
-      // @ts-ignore
-      return this._connectionsByRecord.get(record);
-    }
+    if (this._connectionsByRecord.has(record)) throw new Error('Record already has a connection');
 
-    const origin = String(record.parsedURL.securityOrigin);
+    const origin = record.parsedURL.securityOrigin;
     const observedConnectionWasReused = !!this._connectionReusedByRequestId.get(record.requestId);
-    /** @type {TcpConnection[]} */
     const connections = this._connectionsByOrigin.get(origin) || [];
     const connectionToUse = this._findAvailableConnectionWithLargestCongestionWindow(connections, {
       ignoreConnectionReused: options.ignoreConnectionReused,
@@ -160,6 +156,20 @@ module.exports = class ConnectionPool {
     return connectionToUse;
   }
 
+  /**
+   * Return the connection currently being used to fetch a record. If no connection
+   * currently being used for this record, an error will be thrown.
+   *
+   * @param {LH.Artifacts.NetworkRequest} record
+   * @return {TcpConnection}
+   */
+  acquireActiveConnectionFromRecord(record) {
+    const activeConnection = this._connectionsByRecord.get(record);
+    if (!activeConnection) throw new Error('Could not find an active connection for record');
+
+    return activeConnection;
+  }
+
   /**
    * @param {LH.Artifacts.NetworkRequest} record
    */
diff --git a/lighthouse-core/lib/dependency-graph/simulator/simulator.js b/lighthouse-core/lib/dependency-graph/simulator/simulator.js
index 9c6651520..714e43f48 100644
--- a/lighthouse-core/lib/dependency-graph/simulator/simulator.js
+++ b/lighthouse-core/lib/dependency-graph/simulator/simulator.js
@@ -300,8 +300,7 @@ class Simulator {
       const sizeInMb = (record.resourceSize || 0) / 1024 / 1024;
       timeElapsed = 8 + 20 * sizeInMb - timingData.timeElapsed;
     } else {
-      // If we're estimating time remaining, we already acquired a connection for this record, definitely non-null
-      const connection = /** @type {TcpConnection} */ (this._acquireConnection(record));
+      const connection = this._connectionPool.acquireActiveConnectionFromRecord(record);
       const dnsResolutionTime = this._dns.getTimeUntilResolution(record, {
         requestedAt: timingData.startTime,
         shouldUpdateCache: true,
@@ -352,8 +351,7 @@ class Simulator {
     if (node.type !== BaseNode.TYPES.NETWORK) throw new Error('Unsupported');
 
     const record = node.record;
-    // If we're updating the progress, we already acquired a connection for this record, definitely non-null
-    const connection = /** @type {TcpConnection} */ (this._acquireConnection(record));
+    const connection = this._connectionPool.acquireActiveConnectionFromRecord(record);
     const dnsResolutionTime = this._dns.getTimeUntilResolution(record, {
       requestedAt: timingData.startTime,
       shouldUpdateCache: true,
@@ -503,7 +501,7 @@ module.exports = Simulator;
  * @typedef NodeTimingIntermediate
  * @property {number} [startTime]
  * @property {number} [endTime]
- * @property {number} [queuedTime]
+ * @property {number} [queuedTime] Helpful for debugging.
  * @property {number} [estimatedTimeElapsed]
  * @property {number} [timeElapsed]
  * @property {number} [timeElapsedOvershoot]
diff --git a/lighthouse-core/lib/network-recorder.js b/lighthouse-core/lib/network-recorder.js
index f0f05e64a..070a3357f 100644
--- a/lighthouse-core/lib/network-recorder.js
+++ b/lighthouse-core/lib/network-recorder.js
@@ -196,10 +196,11 @@ class NetworkRecorder extends EventEmitter {
   // DevTools SDK network layer.
 
   /**
-   * @param {LH.Crdp.Network.RequestWillBeSentEvent} data
+   * @param {{params: LH.Crdp.Network.RequestWillBeSentEvent, source?: LH.Protocol.RawSource}} event
    */
-  onRequestWillBeSent(data) {
-    const originalRequest = this._findRealRequest(data.requestId);
+  onRequestWillBeSent(event) {
+    const data = event.params;
+    const originalRequest = this._findRealRequestAndSetSource(data.requestId, event.source);
     // This is a simple new request, create the NetworkRequest object and finish.
     if (!originalRequest) {
       const request = new NetworkRequest();
@@ -235,57 +236,63 @@ class NetworkRecorder extends EventEmitter {
   }
 
   /**
-   * @param {LH.Crdp.Network.RequestServedFromCacheEvent} data
+   * @param {{params: LH.Crdp.Network.RequestServedFromCacheEvent, source?: LH.Protocol.RawSource}} event
    */
-  onRequestServedFromCache(data) {
-    const request = this._findRealRequest(data.requestId);
+  onRequestServedFromCache(event) {
+    const data = event.params;
+    const request = this._findRealRequestAndSetSource(data.requestId, event.source);
     if (!request) return;
     request.onRequestServedFromCache();
   }
 
   /**
-   * @param {LH.Crdp.Network.ResponseReceivedEvent} data
+   * @param {{params: LH.Crdp.Network.ResponseReceivedEvent, source?: LH.Protocol.RawSource}} event
    */
-  onResponseReceived(data) {
-    const request = this._findRealRequest(data.requestId);
+  onResponseReceived(event) {
+    const data = event.params;
+    const request = this._findRealRequestAndSetSource(data.requestId, event.source);
     if (!request) return;
     request.onResponseReceived(data);
   }
 
   /**
-   * @param {LH.Crdp.Network.DataReceivedEvent} data
+   * @param {{params: LH.Crdp.Network.DataReceivedEvent, source?: LH.Protocol.RawSource}} event
    */
-  onDataReceived(data) {
-    const request = this._findRealRequest(data.requestId);
+  onDataReceived(event) {
+    const data = event.params;
+    const request = this._findRealRequestAndSetSource(data.requestId, event.source);
     if (!request) return;
     request.onDataReceived(data);
   }
 
   /**
-   * @param {LH.Crdp.Network.LoadingFinishedEvent} data
+   * @param {{params: LH.Crdp.Network.LoadingFinishedEvent, source?: LH.Protocol.RawSource}} event
    */
-  onLoadingFinished(data) {
-    const request = this._findRealRequest(data.requestId);
+  onLoadingFinished(event) {
+    const data = event.params;
+    const request = this._findRealRequestAndSetSource(data.requestId, event.source);
     if (!request) return;
     request.onLoadingFinished(data);
     this.onRequestFinished(request);
   }
 
   /**
-   * @param {LH.Crdp.Network.LoadingFailedEvent} data
+   * @param {{params: LH.Crdp.Network.LoadingFailedEvent, source?: LH.Protocol.RawSource}} event
    */
-  onLoadingFailed(data) {
-    const request = this._findRealRequest(data.requestId);
+  onLoadingFailed(event) {
+    const data = event.params;
+    const request = this._findRealRequestAndSetSource(data.requestId, event.source);
     if (!request) return;
     request.onLoadingFailed(data);
     this.onRequestFinished(request);
   }
 
   /**
-   * @param {LH.Crdp.Network.ResourceChangedPriorityEvent} data
+   * @param {{params: LH.Crdp.Network.ResourceChangedPriorityEvent, source?: LH.Protocol.RawSource}} event
    */
-  onResourceChangedPriority(data) {
-    const request = this._findRealRequest(data.requestId);
+  onResourceChangedPriority(event) {
+    const data = event.params;
+    const request = this._findRealRequestAndSetSource(data.requestId, event.source);
     if (!request) return;
     request.onResourceChangedPriority(data);
   }
@@ -296,13 +303,13 @@ class NetworkRecorder extends EventEmitter {
    */
   dispatch(event) {
     switch (event.method) {
-      case 'Network.requestWillBeSent': return this.onRequestWillBeSent(event.params);
-      case 'Network.requestServedFromCache': return this.onRequestServedFromCache(event.params);
-      case 'Network.responseReceived': return this.onResponseReceived(event.params);
-      case 'Network.dataReceived': return this.onDataReceived(event.params);
-      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 'Network.requestWillBeSent': return this.onRequestWillBeSent(event);
+      case 'Network.requestServedFromCache': return this.onRequestServedFromCache(event);
+      case 'Network.responseReceived': return this.onResponseReceived(event);
+      case 'Network.dataReceived': return this.onDataReceived(event);
+      case 'Network.loadingFinished': return this.onLoadingFinished(event);
+      case 'Network.loadingFailed': return this.onLoadingFailed(event);
+      case 'Network.resourceChangedPriority': return this.onResourceChangedPriority(event);
       default: return;
     }
   }
@@ -314,9 +321,10 @@ class NetworkRecorder extends EventEmitter {
    * message is referring.
    *
    * @param {string} requestId
+   * @param {LH.Protocol.RawSource|undefined} source
    * @return {NetworkRequest|undefined}
    */
-  _findRealRequest(requestId) {
+  _findRealRequestAndSetSource(requestId, source) {
     let request = this._recordsById.get(requestId);
     if (!request || !request.isValid) return undefined;
 
@@ -324,6 +332,7 @@ class NetworkRecorder extends EventEmitter {
       request = request.redirectDestination;
     }
 
+    request.setSource(source);
     return request;
   }
 
diff --git a/lighthouse-core/lib/network-request.js b/lighthouse-core/lib/network-request.js
index 687924e0b..bfd64351e 100644
--- a/lighthouse-core/lib/network-request.js
+++ b/lighthouse-core/lib/network-request.js
@@ -103,6 +103,18 @@ module.exports = class NetworkRequest {
     this.fetchedViaServiceWorker = false;
     /** @type {string|undefined} */
     this.frameId = '';
+    /**
+     * @type {string|undefined}
+     * Only set for OOPIFs. This is the targetId of the protocol target from which this
+     * request came. Undefined means it came from the root.
+     */
+    this.targetId = undefined;
+    /**
+     * @type {string|undefined}
+     * Only set for OOPIFs. This is the sessionId of the protocol connection on which this
+     * request was discovered. Undefined means it came from the root.
+     */
+    this.sessionId = undefined;
     this.isLinkPreload = false;
   }
 
@@ -233,6 +245,19 @@ module.exports = class NetworkRequest {
     this._updateResponseReceivedTimeIfNecessary();
   }
 
+  /**
+   * @param {LH.Protocol.RawSource|undefined} source
+   */
+  setSource(source) {
+    if (source) {
+      this.targetId = source.targetId;
+      this.sessionId = source.sessionId;
+    } else {
+      this.targetId = undefined;
+      this.sessionId = undefined;
+    }
+  }
+
   /**
    * @param {LH.Crdp.Network.Response} response
    * @param {number} timestamp
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index b021b81cb..4f0ea5985 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -179,7 +179,16 @@ class Runner {
       ...timingEntriesFromRunner,
       // 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 timingEntries = Array.from(new Map(timingEntriesKeyValues).values())
+    // Truncate timestamps to hundredths of a millisecond saves ~4KB. No need for microsecond
+    // resolution.
+    .map(entry => {
+      return /** @type {PerformanceEntry} */ ({
+        ...entry,
+        duration: parseFloat(entry.duration.toFixed(2)),
+        startTime: parseFloat(entry.startTime.toFixed(2)),
+      });
+    });
     const runnerEntry = timingEntries.find(e => e.name === 'lh:runner:run');
     return {entries: timingEntries, total: runnerEntry && runnerEntry.duration || 0};
   }
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index ed7cd2fe6..29131c631 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -1853,13 +1853,13 @@
                             "url": "https://snyk.io/vuln/npm:jquery?lh=2.1.1&utm_source=lighthouse&utm_medium=ref&utm_campaign=audit"
                         }, 
                         "highestSeverity": "Medium", 
-                        "vulnCount": 1.0
+                        "vulnCount": 2.0
                     }
                 ], 
                 "summary": null, 
                 "type": "table"
             }, 
-            "displayValue": "1 vulnerability detected", 
+            "displayValue": "2 vulnerabilities detected", 
             "id": "no-vulnerable-libraries", 
             "score": 0.0, 
             "scoreDisplayMode": "binary", 
diff --git a/third-party/snyk/snapshot.json b/third-party/snyk/snapshot.json
index 19702dea0..087bbd432 100644
--- a/third-party/snyk/snapshot.json
+++ b/third-party/snyk/snapshot.json
@@ -57,10 +57,11 @@
       {"id":"npm:highcharts:20180225","severity":"high","semver":{"vulnerable":["<6.1.0"]}}
     ],
     "jquery":[
+      {"id":"SNYK-JS-JQUERY-174006","severity":"medium","semver":{"vulnerable":["*"]}},
       {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":["=3.0.0-rc1"]}},
       {"id":"npm:jquery:20150627","severity":"medium","semver":{"vulnerable":["<1.12.2",">=1.12.3 <2.2.2",">=2.2.3 <3.0.0"]}},
       {"id":"npm:jquery:20140902","severity":"medium","semver":{"vulnerable":[">=1.4.2 <1.6.2"]}},
-      {"id":"npm:jquery:20120206","severity":"medium","semver":{"vulnerable":["<1.9.0 >=1.7.1"]}},
+      {"id":"npm:jquery:20120206","severity":"medium","semver":{"vulnerable":[">=1.7.1 <1.9.0"]}},
       {"id":"npm:jquery:20110606","severity":"medium","semver":{"vulnerable":["<1.6.3"]}}
     ],
     "jquery-mobile":[
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 6bd614d4d..c1ae0516e 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -376,7 +376,6 @@ declare global {
       }
 
       export interface NetworkAnalysis {
-        records: Array<NetworkRequest>;
         rtt: number;
         additionalRttByOrigin: Map<string, number>;
         serverResponseTimeByOrigin: Map<string, number>;
diff --git a/types/protocol.d.ts b/types/protocol.d.ts
index e80525c24..61c3ec580 100644
--- a/types/protocol.d.ts
+++ b/types/protocol.d.ts
@@ -12,6 +12,11 @@ declare global {
      */
     export type RawEventMessage = RawEventMessageRecord[keyof RawEventMessageRecord];
 
+    export interface RawSource {
+      targetId?: string;
+      sessionId: string;
+    }
+
     /**
      * Raw (over the wire) message format of all possible Crdp command responses.
      */
@@ -65,6 +70,8 @@ type RawEventMessageRecord = {
     method: K,
     // Drop [] for `undefined` (so a JS value is valid).
     params: LH.CrdpEvents[K] extends [] ? undefined: LH.CrdpEvents[K][number]
+    // If the source is not set, it means the event was from the root target.
+    source?: LH.Protocol.RawSource
   };
 }
 


Test patch:
diff --git a/clients/test/lightrider-entry-test.js b/clients/test/lightrider-entry-test.js
index 1d93b5b25..35fb0f0b3 100644
--- a/clients/test/lightrider-entry-test.js
+++ b/clients/test/lightrider-entry-test.js
@@ -70,5 +70,53 @@ describe('lightrider-entry', () => {
 
       runStub.mockRestore();
     });
+
+    it('uses the desktop config preset when device is desktop', async () => {
+      const runStub = jest.spyOn(Runner, 'run');
+
+      const mockConnection = {};
+      const url = 'https://example.com';
+
+      const lrDevice = 'desktop';
+      await lhBackground.runLighthouseInLR(mockConnection, url, {}, {lrDevice});
+      const config = runStub.mock.calls[0][1].config;
+      assert.equal(config.settings.emulatedFormFactor, 'desktop');
+
+      runStub.mockRestore();
+    });
+
+    it('uses the mobile config preset when device is mobile', async () => {
+      const runStub = jest.spyOn(Runner, 'run');
+
+      const mockConnection = {};
+      const url = 'https://example.com';
+
+      const lrDevice = 'mobile';
+      await lhBackground.runLighthouseInLR(mockConnection, url, {}, {lrDevice});
+      const config = runStub.mock.calls[0][1].config;
+      assert.equal(config.settings.emulatedFormFactor, 'mobile');
+
+      runStub.mockRestore();
+    });
+
+    it('overrides the default config when one is provided', async () => {
+      const runStub = jest.spyOn(Runner, 'run');
+
+      const mockConnection = {};
+      const url = 'https://example.com';
+
+      const configOverride = {
+        default: 'lighthouse:default',
+        settings: {
+          onlyAudits: ['network-requests'],
+        },
+      };
+      await lhBackground.runLighthouseInLR(mockConnection, url, {}, {configOverride});
+      const config = runStub.mock.calls[0][1].config;
+      assert.equal(config.settings.onlyAudits.length, 1);
+      assert.equal(config.settings.onlyAudits[0], 'network-requests');
+
+      runStub.mockRestore();
+    });
   });
 });
diff --git a/lighthouse-core/test/computed/main-thread-tasks-test.js b/lighthouse-core/test/computed/main-thread-tasks-test.js
index 7dbca9dd7..42b17b5aa 100644
--- a/lighthouse-core/test/computed/main-thread-tasks-test.js
+++ b/lighthouse-core/test/computed/main-thread-tasks-test.js
@@ -195,4 +195,45 @@ describe('MainResource computed artifact', () => {
       group: taskGroups.other,
     });
   });
+
+  const invalidEventSets = [
+    [
+      // TaskA overlaps with TaskB, X first
+      {ph: 'X', name: 'TaskA', pid, tid, ts: baseTs, dur: 100e3, args},
+      {ph: 'B', name: 'TaskB', pid, tid, ts: baseTs + 5e3, args},
+      {ph: 'E', name: 'TaskB', pid, tid, ts: baseTs + 115e3, args},
+    ],
+    [
+      // TaskA overlaps with TaskB, B first
+      {ph: 'B', name: 'TaskA', pid, tid, ts: baseTs, args},
+      {ph: 'X', name: 'TaskB', pid, tid, ts: baseTs + 5e3, dur: 100e3, args},
+      {ph: 'E', name: 'TaskA', pid, tid, ts: baseTs + 90e3, args},
+    ],
+    [
+      // TaskA is missing a B event
+      {ph: 'E', name: 'TaskA', pid, tid, ts: baseTs, args},
+      {ph: 'B', name: 'TaskB', pid, tid, ts: baseTs + 5e3, args},
+      {ph: 'E', name: 'TaskB', pid, tid, ts: baseTs + 115e3, args},
+    ],
+    [
+      // TaskB is missing a B event after an X
+      {ph: 'X', name: 'TaskA', pid, tid, ts: baseTs, dur: 100e3, args},
+      {ph: 'E', name: 'TaskB', pid, tid, ts: baseTs + 10e3, args},
+    ],
+  ];
+
+  for (const invalidEvents of invalidEventSets) {
+    it('should throw on invalid task input', async () => {
+      const traceEvents = [
+        ...boilerplateTrace,
+        ...invalidEvents,
+      ];
+
+      traceEvents.forEach(evt => Object.assign(evt, {cat: 'devtools.timeline'}));
+
+      const context = {computedCache: new Map()};
+      const promise = MainThreadTasks.request({traceEvents}, context);
+      await expect(promise).rejects.toBeTruthy();
+    });
+  }
 });
diff --git a/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js b/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js
index 7af9f3e10..ed1efec97 100644
--- a/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js
+++ b/lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js
@@ -68,6 +68,16 @@ const traceData = {
       resourceType: 'Image',
       finished: true,
     },
+    {
+      requestId: '1',
+      url: 'http://gmail.com/image.jpg',
+      mimeType: 'image/jpeg',
+      resourceSize: 15000,
+      transferSize: 20000,
+      resourceType: 'Image',
+      finished: true,
+      sessionId: 'oopif', // ignore for being an oopif
+    },
     {
       requestId: '1',
       url: 'data: image/jpeg ; base64 ,SgVcAT32587935321...',
diff --git a/lighthouse-core/test/gather/gatherers/dobetterweb/response-compression-test.js b/lighthouse-core/test/gather/gatherers/dobetterweb/response-compression-test.js
index 954dd908e..d885acf95 100644
--- a/lighthouse-core/test/gather/gatherers/dobetterweb/response-compression-test.js
+++ b/lighthouse-core/test/gather/gatherers/dobetterweb/response-compression-test.js
@@ -55,6 +55,19 @@ const traceData = {
       content: '1234567',
       finished: true,
     },
+    {
+      url: 'http://google.com/index.json',
+      _statusCode: 200,
+      mimeType: 'application/json',
+      requestId: 27,
+      resourceSize: 7,
+      transferSize: 8,
+      resourceType: 'XHR',
+      responseHeaders: [],
+      content: '1234567',
+      finished: true,
+      sessionId: 'oopif', // ignore for being from oopif
+    },
     {
       url: 'http://google.com/index.json',
       _statusCode: 304, // ignore for being a cache not modified response
diff --git a/lighthouse-core/test/lib/dependency-graph/simulator/connection-pool-test.js b/lighthouse-core/test/lib/dependency-graph/simulator/connection-pool-test.js
index 601334af8..23157362c 100644
--- a/lighthouse-core/test/lib/dependency-graph/simulator/connection-pool-test.js
+++ b/lighthouse-core/test/lib/dependency-graph/simulator/connection-pool-test.js
@@ -85,8 +85,8 @@ describe('DependencyGraph/Simulator/ConnectionPool', () => {
       const connectionForA = pool.acquire(recordA);
       const connectionForB = pool.acquire(recordB);
       for (let i = 0; i < 10; i++) {
-        assert.equal(pool.acquire(recordA), connectionForA);
-        assert.equal(pool.acquire(recordB), connectionForB);
+        assert.equal(pool.acquireActiveConnectionFromRecord(recordA), connectionForA);
+        assert.equal(pool.acquireActiveConnectionFromRecord(recordB), connectionForB);
       }
 
       assert.deepStrictEqual(pool.connectionsInUse(), [connectionForA, connectionForB]);
@@ -152,7 +152,8 @@ describe('DependencyGraph/Simulator/ConnectionPool', () => {
       }
 
       assert.ok(pool.acquire(coldRecord, opts), 'should have acquired connection');
-      assert.ok(pool.acquire(warmRecord, opts), 'should have acquired connection');
+      assert.ok(pool.acquireActiveConnectionFromRecord(warmRecord, opts),
+        'should have acquired connection');
     });
 
     it('should acquire in order of warmness', () => {
diff --git a/lighthouse-core/test/lib/network-recorder-test.js b/lighthouse-core/test/lib/network-recorder-test.js
index 671ad53a8..1c47eea76 100644
--- a/lighthouse-core/test/lib/network-recorder-test.js
+++ b/lighthouse-core/test/lib/network-recorder-test.js
@@ -180,6 +180,36 @@ describe('network recorder', function() {
     });
   });
 
+  it('should set the source of network records', () => {
+    const devtoolsLogs = networkRecordsToDevtoolsLog([
+      {url: 'http://example.com'},
+      {url: 'http://iframe.com'},
+      {url: 'http://other-iframe.com'},
+    ]);
+
+    const requestId1 = devtoolsLogs.find(
+      log => log.params.request && log.params.request.url === 'http://iframe.com'
+    ).params.requestId;
+    const requestId2 = devtoolsLogs.find(
+      log => log.params.request && log.params.request.url === 'http://other-iframe.com'
+    ).params.requestId;
+
+    for (const log of devtoolsLogs) {
+      if (log.params.requestId === requestId1) log.source = {sessionId: '1', targetId: 'a'};
+
+      if (log.params.requestId === requestId2 && log.method === 'Network.loadingFinished') {
+        log.source = {sessionId: '2', targetId: 'b'};
+      }
+    }
+
+    const records = NetworkRecorder.recordsFromLogs(devtoolsLogs);
+    expect(records).toMatchObject([
+      {url: 'http://example.com', sessionId: undefined, targetId: undefined},
+      {url: 'http://iframe.com', sessionId: '1', targetId: 'a'},
+      {url: 'http://other-iframe.com', sessionId: '2', targetId: 'b'},
+    ]);
+  });
+
   describe('#findNetworkQuietPeriods', () => {
     function record(data) {
       const url = data.url || 'https://example.com';
diff --git a/lighthouse-core/test/report/proto-test.js b/lighthouse-core/test/report/proto-test.js
index 0593a3d19..a90811ee7 100644
--- a/lighthouse-core/test/report/proto-test.js
+++ b/lighthouse-core/test/report/proto-test.js
@@ -55,21 +55,28 @@ describe('round trip JSON comparison subsets', () => {
   });
 
   it('has the same config values', () => {
-    expect(roundTripJson.configSettings).toMatchObject(sampleJson.configSettings);
+    // Config settings from proto round trip should be a subset of the actual settings.
+    expect(sampleJson.configSettings).toMatchObject(roundTripJson.configSettings);
   });
 });
 
-// Note: In a failing diff, if you see details.summary going from {} to null, it's OK.
-// Jest considers this not a failure, and neither do we, here in the python roundtrip
-// Meanwhile, The PSI roundtrip maintains {} to {}.
 describe('round trip JSON comparison to everything', () => {
   let sampleJson;
 
   beforeEach(() => {
     sampleJson = JSON.parse(preprocessor.processForProto(sample));
+
+    // Proto conversion turns empty summaries into null. This is OK,
+    // and is handled in the PSI roundtrip just fine, but messes up the easy
+    // jest sub-object matcher. So, we put the empty object back in its place.
+    for (const audit of Object.values(roundTripJson.audits)) {
+      if (audit.details && audit.details.summary === null) {
+        audit.details.summary = {};
+      }
+    }
   });
 
   it('has the same JSON overall', () => {
-    expect(roundTripJson).toMatchObject(sampleJson);
+    expect(sampleJson).toMatchObject(roundTripJson);
   });
 });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 7f803ce0e..3e19bc2a3 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -2629,7 +2629,7 @@
       "score": 0,
       "scoreDisplayMode": "binary",
       "rawValue": false,
-      "displayValue": "1 vulnerability detected",
+      "displayValue": "2 vulnerabilities detected",
       "details": {
         "type": "table",
         "headings": [
@@ -2652,7 +2652,7 @@
         "items": [
           {
             "highestSeverity": "Medium",
-            "vulnCount": 1,
+            "vulnCount": 2,
             "detectedLib": {
               "text": "jQuery@2.1.1",
               "url": "https://snyk.io/vuln/npm:jquery?lh=2.1.1&utm_source=lighthouse&utm_medium=ref&utm_campaign=audit",