Repository: GoogleChrome/lighthouse
Pull request #9785 (2019-10-04 15:57:15)

core(tracehouse): allow nested trace events without an end
**Summary**
Addresses one of the 3 remaining cases for `Fatal trace logic error - child cannot end after parent` error.

**Related Issues/PRs**
#7764 



Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 2419204e6..e49f3174c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -15,10 +15,9 @@ module.exports = {
   settings: {
     onlyAudits: [
       'first-meaningful-paint',
-      'speed-index-metric',
-      'estimated-input-latency',
-      'first-interactive',
-      'consistently-interactive',
+      'speed-index',
+      'first-cpu-idle',
+      'interactive',
     ],
   },
 };
@@ -129,7 +128,7 @@ The audits property controls which audits to run and include with your Lighthous
 {
   audits: [
     'first-meaningful-paint',
-    'first-interactive',
+    'first-cpu-idle',
     'byte-efficiency/uses-optimized-images',
   ]
 }
@@ -151,8 +150,8 @@ The categories property controls how to score and organize the audit results in
       description: 'This category judges your performance',
       auditRefs: [
         {id: 'first-meaningful-paint', weight: 2, group: 'metrics'},
-        {id: 'first-interactive', weight: 3, group: 'metrics'},
-        {id: 'consistently-interactive', weight: 5, group: 'metrics'},
+        {id: 'first-cpu-idle', weight: 3, group: 'metrics'},
+        {id: 'interactive', weight: 5, group: 'metrics'},
       ],
     }
   }
diff --git a/docs/readme.md b/docs/readme.md
index a397dbd5a..1d379e616 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -92,10 +92,9 @@ In order to extend the Lighthouse configuration programmatically, you need to pa
   settings: {
     onlyAudits: [
       'first-meaningful-paint',
-      'speed-index-metric',
-      'estimated-input-latency',
-      'first-interactive',
-      'consistently-interactive',
+      'speed-index',
+      'first-cpu-idle',
+      'interactive',
     ],
   },
 }
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
index 9f155f54f..1c81e0afd 100644
--- a/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
+++ b/lighthouse-core/gather/gatherers/dobetterweb/optimized-images.js
@@ -10,6 +10,7 @@
   */
 'use strict';
 
+const log = require('lighthouse-logger');
 const Gatherer = require('../gatherer.js');
 const URL = require('../../../lib/url-shim.js');
 const NetworkRequest = require('../../../lib/network-request.js');
@@ -124,6 +125,8 @@ class OptimizedImages extends Gatherer {
         const image = {failed: false, ...stats, ...record};
         results.push(image);
       } catch (err) {
+        log.warn('optimized-images', err.message);
+
         // Track this with Sentry since these errors aren't surfaced anywhere else, but we don't
         // want to tank the entire run due to a single image.
         Sentry.captureException(err, {
diff --git a/lighthouse-core/lib/tracehouse/main-thread-tasks.js b/lighthouse-core/lib/tracehouse/main-thread-tasks.js
index 78ce49637..653ac3930 100644
--- a/lighthouse-core/lib/tracehouse/main-thread-tasks.js
+++ b/lighthouse-core/lib/tracehouse/main-thread-tasks.js
@@ -31,6 +31,7 @@ const {taskGroups, taskNameToGroup} = require('./task-groups.js');
  * @prop {LH.TraceEvent} event
  * @prop {TaskNode[]} children
  * @prop {TaskNode|undefined} parent
+ * @prop {boolean} unbounded Indicates that the task had an endTime that was inferred rather than specified in the trace. i.e. in the source trace this task was unbounded.
  * @prop {number} startTime
  * @prop {number} endTime
  * @prop {number} duration
@@ -64,6 +65,7 @@ class MainThreadTasks {
       duration: endTime - startTime,
 
       // These properties will be filled in later
+      unbounded: false,
       parent: undefined,
       children: [],
       attributableURLs: [],
@@ -181,10 +183,12 @@ class MainThreadTasks {
 
       /** @type {Pick<LH.TraceEvent, 'ph'|'ts'>} */
       let taskEndEvent;
+      let unbounded = false;
       if (matchedEventIndex === -1) {
         // If we couldn't find an end event, we'll assume it's the end of the trace.
         // If this creates invalid parent/child relationships it will be caught in the next step.
         taskEndEvent = {ph: 'E', ts: traceEndTs};
+        unbounded = true;
       } else if (matchedEventIndex === taskEndEventsReverseQueue.length - 1) {
         // Use .pop() in the common case where the immediately next event is needed.
         // It's ~25x faster, https://jsperf.com/pop-vs-splice.
@@ -193,7 +197,9 @@ class MainThreadTasks {
         taskEndEvent = taskEndEventsReverseQueue.splice(matchedEventIndex, 1)[0];
       }
 
-      tasks.push(MainThreadTasks._createNewTaskNode(taskStartEvent, taskEndEvent));
+      const task = MainThreadTasks._createNewTaskNode(taskStartEvent, taskEndEvent);
+      task.unbounded = unbounded;
+      tasks.push(task);
     }
 
     if (taskEndEventsReverseQueue.length) {
@@ -247,18 +253,29 @@ class MainThreadTasks {
         if (nextTask.endTime > currentTask.endTime) {
           const timeDelta = nextTask.endTime - currentTask.endTime;
           // The child task is taking longer than the parent task, which should be impossible.
-          //    If it's less than 1ms, we'll let it slide by increasing the duration of the parent.
-          //    If it's more, throw an error.
+          // In reality these situations happen, so we allow for some flexibility in trace event times.
           if (timeDelta < 1000) {
+            // It's less than 1ms, we'll let it slide by increasing the duration of the parent.
             currentTask.endTime = nextTask.endTime;
             currentTask.duration += timeDelta;
+          } else if (nextTask.unbounded) {
+            // It's ending at traceEndTs, it means we were missing the end event. We'll truncate it to the parent.
+            nextTask.endTime = currentTask.endTime;
+            nextTask.duration = nextTask.endTime - nextTask.startTime;
           } else {
-            // If we fell into this error, it's usually because of one of three reasons.
-            //    - We were missing an E event for a child task and we assumed the child ended at the end of the trace.
+            // None of our workarounds matched. It's time to throw an error.
+            // When we fall into this error, it's usually because of one of two reasons.
             //    - There was slop in the opposite direction (child started 1ms before parent) and the child was assumed to be parent instead.
             //    - The child timestamp ended more than 1ms after tha parent.
             // These have more complicated fixes, so handling separately https://github.com/GoogleChrome/lighthouse/pull/9491#discussion_r327331204.
-            throw new Error('Fatal trace logic error - child cannot end after parent');
+            /** @type {any} */
+            const error = new Error('Fatal trace logic error - child cannot end after parent');
+            error.timeDelta = timeDelta;
+            error.nextTaskEvent = nextTask.event;
+            error.nextTaskEndTime = nextTask.endTime;
+            error.currentTaskEvent = currentTask.event;
+            error.currentTaskEndTime = currentTask.endTime;
+            throw error;
           }
         }
 
diff --git a/lighthouse-core/scripts/compare-timings.js b/lighthouse-core/scripts/compare-timings.js
index f04b39911..a8f6f8d4a 100644
--- a/lighthouse-core/scripts/compare-timings.js
+++ b/lighthouse-core/scripts/compare-timings.js
@@ -8,6 +8,7 @@
 // Example:
 //     node lighthouse-core/scripts/compare-timings.js --name my-collection --collect -n 3 --lh-flags='--only-audits=unminified-javascript' --urls https://www.example.com https://www.nyt.com
 //     node lighthouse-core/scripts/compare-timings.js --name my-collection --summarize --measure-filter 'loadPage|connect'
+//     node lighthouse-core/scripts/compare-timings.js --name base --name pr --compare
 
 const fs = require('fs');
 const {execSync} = require('child_process');
@@ -19,7 +20,9 @@ const ROOT_OUTPUT_DIR = `${LH_ROOT}/timings-data`;
 const argv = yargs
   .help('help')
   .describe({
+    // common flags
     'name': 'Unique identifier, makes the folder for storing LHRs. Not a path',
+    'report-exclude': 'Regex of properties to exclude. Set to "none" to disable default',
     // --collect
     'collect': 'Saves LHRs to disk',
     'lh-flags': 'Lighthouse flags',
@@ -27,18 +30,33 @@ const argv = yargs
     'n': 'Number of times to run',
     // --summarize
     'summarize': 'Prints statistics report',
-    'measure-filter': 'Regex filter of measures to report. Optional',
+    'measure-filter': 'Regex of measures to include. Optional',
     'output': 'table, json',
+    // --compare
+    'compare': 'Compare two sets of LHRs',
+    'delta-property-sort': 'Property to sort by its delta',
+    'desc': 'Set to override default ascending sort',
   })
   .string('measure-filter')
+  .default('report-exclude', 'min|max|stdev|^n$')
+  .default('delta-property-sort', 'mean')
   .default('output', 'table')
   .array('urls')
   .string('lh-flags')
+  .default('desc', false)
   .default('lh-flags', '')
   .wrap(yargs.terminalWidth())
 .argv;
 
-const outputDir = `${ROOT_OUTPUT_DIR}/${argv.name}`;
+const reportExcludeRegex =
+  argv.reportExclude !== 'none' ? new RegExp(argv.reportExclude, 'i') : null;
+
+/**
+ * @param {string} name
+ */
+function dir(name) {
+  return `${ROOT_OUTPUT_DIR}/${name}`;
+}
 
 /**
  * @param {number[]} values
@@ -72,6 +90,7 @@ function round(value) {
 }
 
 function collect() {
+  const outputDir = dir(argv.name);
   if (!fs.existsSync(ROOT_OUTPUT_DIR)) fs.mkdirSync(ROOT_OUTPUT_DIR);
   if (fs.existsSync(outputDir)) throw new Error(`folder already exists: ${outputDir}`);
   fs.mkdirSync(outputDir);
@@ -91,11 +110,15 @@ function collect() {
   }
 }
 
-function summarize() {
+/**
+ * @param {string} name
+ */
+function aggregateResults(name) {
+  const outputDir = dir(name);
+
   // `${url}@@@${entry.name}` -> duration
   /** @type {Map<string, number[]>} */
   const durationsMap = new Map();
-  /** @type {RegExp|null} */
   const measureFilter = argv.measureFilter ? new RegExp(argv.measureFilter, 'i') : null;
 
   for (const lhrPath of fs.readdirSync(outputDir)) {
@@ -127,13 +150,14 @@ function summarize() {
     }
   }
 
-  const results = [...durationsMap].map(([key, durations]) => {
+  return [...durationsMap].map(([key, durations]) => {
     const [url, entryName] = key.split('@@@');
     const mean = average(durations);
     const min = Math.min(...durations);
     const max = Math.max(...durations);
     const stdev = sampleStdev(durations);
     return {
+      key,
       measure: entryName,
       url,
       n: durations.length,
@@ -148,7 +172,104 @@ function summarize() {
     if (measureComp !== 0) return measureComp;
     return a.url.localeCompare(b.url);
   });
+}
+
+/**
+ * @param {*[]} results
+ */
+function filter(results) {
+  if (!reportExcludeRegex) return;
+
+  for (const result of results) {
+    for (const key in result) {
+      if (reportExcludeRegex.test(key)) delete result[key];
+    }
+  }
+}
+
+/**
+ * @param {number=} value
+ * @return {value is number}
+ */
+function exists(value) {
+  return typeof value !== 'undefined';
+}
+
+function summarize() {
+  const results = aggregateResults(argv.name);
+  filter(results);
+  print(results);
+}
+
+/**
+ * @param {number=} base
+ * @param {number=} other
+ */
+function compareValues(base, other) {
+  const basePart = exists(base) ? base : 'N/A';
+  const otherPart = exists(other) ? other : 'N/A';
+  return {
+    description: `${basePart} -> ${otherPart}`,
+    delta: exists(base) && exists(other) ? (other - base) : undefined,
+  };
+}
+
+function compare() {
+  if (!Array.isArray(argv.name) || argv.name.length !== 2) {
+    throw new Error('expected two entries for name option');
+  }
+
+  const baseResults = aggregateResults(argv.name[0]);
+  const otherResults = aggregateResults(argv.name[1]);
+
+  const keys = [...new Set([...baseResults.map(r => r.key), ...otherResults.map(r => r.key)])];
+  const results = keys.map(key => {
+    const baseResult = baseResults.find(result => result.key === key);
+    const otherResult = otherResults.find(result => result.key === key);
+
+    const someResult = baseResult || otherResult;
+    if (!someResult) throw new Error('impossible');
+
+    const mean = compareValues(baseResult && baseResult.mean, otherResult && otherResult.mean);
+    const stdev = compareValues(baseResult && baseResult.stdev, otherResult && otherResult.stdev);
+    const min = compareValues(baseResult && baseResult.min, otherResult && otherResult.min);
+    const max = compareValues(baseResult && baseResult.max, otherResult && otherResult.max);
+
+    return {
+      'measure': someResult.measure,
+      'url': someResult.url,
+      'mean': mean.description,
+      'mean Δ': exists(mean.delta) ? round(mean.delta) : undefined,
+      'stdev': stdev.description,
+      'stdev Δ': exists(stdev.delta) ? round(stdev.delta) : undefined,
+      'min': min.description,
+      'min Δ': exists(min.delta) ? round(min.delta) : undefined,
+      'max': max.description,
+      'max Δ': exists(max.delta) ? round(max.delta) : undefined,
+    };
+  });
+
+  const sortByKey = `${argv.deltaPropertySort} Δ`;
+  results.sort((a, b) => {
+    // @ts-ignore - shhh tsc.
+    const aValue = a[sortByKey];
+    // @ts-ignore - shhh tsc.
+    const bValue = b[sortByKey];
+
+    // Always put the keys missing a result at the bottom of the table.
+    if (!exists(aValue)) return 1;
+    else if (!exists(bValue)) return -1;
 
+    return (argv.desc ? 1 : -1) * (Math.abs(aValue) - Math.abs(bValue));
+  });
+  filter(results);
+  print(results);
+}
+
+/**
+ * @param {*[]} results
+ */
+function print(results) {
   if (argv.output === 'table') {
     // eslint-disable-next-line no-console
     console.table(results);
@@ -161,6 +282,7 @@ function summarize() {
 function main() {
   if (argv.collect) collect();
   if (argv.summarize) summarize();
+  if (argv.compare) compare();
 }
 
 main();
diff --git a/third-party/snyk/snapshot.json b/third-party/snyk/snapshot.json
index 79284f279..1765ffeba 100644
--- a/third-party/snyk/snapshot.json
+++ b/third-party/snyk/snapshot.json
@@ -1,25 +1,28 @@
 {
   "npm":{
     "angular":[
+      {"id":"SNYK-JS-ANGULAR-471885","severity":"medium","semver":{"vulnerable":["<1.6.3"]}},
+      {"id":"SNYK-JS-ANGULAR-471882","severity":"medium","semver":{"vulnerable":["<1.6.5"]}},
+      {"id":"SNYK-JS-ANGULAR-471879","severity":"medium","semver":{"vulnerable":["<1.6.0-rc.0"]}},
       {"id":"npm:angular:20180202","severity":"medium","semver":{"vulnerable":["<1.6.9"]}},
       {"id":"npm:angular:20171018","severity":"medium","semver":{"vulnerable":["<1.6.7"]}},
-      {"id":"npm:angular:20160527","severity":"medium","semver":{"vulnerable":["<1.2.30 >=1.0.0"]}},
-      {"id":"npm:angular:20160122","severity":"medium","semver":{"vulnerable":["<1.5.0-rc.2 >=1.3.0"]}},
+      {"id":"npm:angular:20160527","severity":"medium","semver":{"vulnerable":[">=1.0.0 <1.2.30"]}},
+      {"id":"npm:angular:20160122","severity":"medium","semver":{"vulnerable":[">=1.3.0 <1.5.0-rc.2"]}},
       {"id":"npm:angular:20140608","severity":"low","semver":{"vulnerable":["<1.3.0"]}},
       {"id":"npm:angular:20131113","severity":"high","semver":{"vulnerable":["<1.2.2"]}},
       {"id":"npm:angular:20140908","severity":"medium","semver":{"vulnerable":["<1.3.0-rc.4"]}},
-      {"id":"npm:angular:20161101","severity":"medium","semver":{"vulnerable":["<1.5.9 >=1.5.0"]}},
+      {"id":"npm:angular:20161101","severity":"medium","semver":{"vulnerable":[">=1.5.0 <1.5.9"]}},
       {"id":"npm:angular:20150909","severity":"high","semver":{"vulnerable":["<1.5.0-beta.2"]}},
       {"id":"npm:angular:20151205","severity":"medium","semver":{"vulnerable":["<1.5.0-rc.0"]}},
       {"id":"npm:angular:20151130","severity":"medium","semver":{"vulnerable":["<1.4.10"]}},
-      {"id":"npm:angular:20130622","severity":"medium","semver":{"vulnerable":["<1.2.0 >=1.0.0"]}},
-      {"id":"npm:angular:20150807-1","severity":"medium","semver":{"vulnerable":["<1.5.0-beta.0 >=1.3.1"]}},
-      {"id":"npm:angular:20150807","severity":"high","semver":{"vulnerable":["<1.5.0-beta.0 >=1.0.0"]}},
+      {"id":"npm:angular:20130622","severity":"medium","semver":{"vulnerable":[">=1.0.0 <1.2.0"]}},
+      {"id":"npm:angular:20150807-1","severity":"medium","semver":{"vulnerable":[">=1.3.1 <1.5.0-beta.0"]}},
+      {"id":"npm:angular:20150807","severity":"high","semver":{"vulnerable":[">=1.0.0 <1.5.0-beta.0"]}},
       {"id":"npm:angular:20150315","severity":"medium","semver":{"vulnerable":["<1.6.1"]}},
       {"id":"npm:angular:20150310","severity":"high","semver":{"vulnerable":["<1.5.0-beta.2"]}},
       {"id":"npm:angular:20141104","severity":"medium","semver":{"vulnerable":["<1.3.2"]}},
-      {"id":"npm:angular:20130621","severity":"medium","semver":{"vulnerable":["<=1.1.5"]}},
-      {"id":"npm:angular:20140909","severity":"high","semver":{"vulnerable":["<1.2.24 >=1.2.19"]}},
+      {"id":"npm:angular:20130621","severity":"medium","semver":{"vulnerable":["<1.2.0"]}},
+      {"id":"npm:angular:20140909","severity":"high","semver":{"vulnerable":[">=1.2.19 <1.2.24"]}},
       {"id":"npm:angular:20130625","severity":"high","semver":{"vulnerable":["<1.1.5"]}}
     ],
     "backbone":[
@@ -36,9 +39,11 @@
       {"id":"npm:bootstrap:20120510","severity":"medium","semver":{"vulnerable":["<2.1.0"]}}
     ],
     "dojo":[
+      {"id":"SNYK-JS-DOJO-174934","severity":"medium","semver":{"vulnerable":[">=1.0.0 <1.0.3",">=1.1.0 <1.1.2",">=1.2.0 <1.2.4",">=1.3.0 <1.3.3",">=1.4.0 <1.4.2"]}},
+      {"id":"SNYK-JS-DOJO-174933","severity":"medium","semver":{"vulnerable":["<1.2.0"]}},
       {"id":"SNYK-JS-DOJO-72305","severity":"medium","semver":{"vulnerable":["<1.14"]}},
       {"id":"npm:dojo:20180818","severity":"medium","semver":{"vulnerable":["<1.10.10 || >=1.11.0 <1.11.6 || >=1.12.0 <1.12.4 || >=1.13.0 <1.13.1"]}},
-      {"id":"npm:dojo:20160523","severity":"medium","semver":{"vulnerable":["<= 1.0.0"]}},
+      {"id":"npm:dojo:20160523","severity":"medium","semver":{"vulnerable":["<1.1.0"]}},
       {"id":"npm:dojo:20100614-6","severity":"medium","semver":{"vulnerable":["<1.4.2"]}},
       {"id":"npm:dojo:20100614","severity":"high","semver":{"vulnerable":[">=0.4 <0.4.4 || >=1.0 <1.0.3 || >=1.1 <1.1.2 || >=1.2 <1.2.4 || >=1.3 <1.3.3 || >=1.4 <1.4.2"]}},
       {"id":"npm:dojo:20090409","severity":"medium","semver":{"vulnerable":["<1.1"]}}
@@ -48,8 +53,12 @@
       {"id":"npm:foundation-sites:20150619","severity":"medium","semver":{"vulnerable":["<5.5.3"]}},
       {"id":"npm:foundation-sites:20120717","severity":"medium","semver":{"vulnerable":["<3.0.6 >=3.0.0"]}}
     ],
+    "google-closure-library":[
+      {"id":"SNYK-JS-GOOGLECLOSURELIBRARY-174519","severity":"medium","semver":{"vulnerable":[">=20190121.0.0 <20190301.0.0"]}}
+    ],
     "handlebars":[
-      {"id":"SNYK-JS-HANDLEBARS-174183","severity":"high","semver":{"vulnerable":[">=4.1.0 <4.1.2","<4.0.14"]}},
+      {"id":"SNYK-JS-HANDLEBARS-469063","severity":"high","semver":{"vulnerable":["<4.3.0"]}},
+      {"id":"SNYK-JS-HANDLEBARS-174183","severity":"high","semver":{"vulnerable":["<3.0.7",">=4.1.0 <4.1.2","<4.0.14"]}},
       {"id":"SNYK-JS-HANDLEBARS-173692","severity":"high","semver":{"vulnerable":["<4.0.13"]}},
       {"id":"npm:handlebars:20151207","severity":"medium","semver":{"vulnerable":["<4.0.0"]}},
       {"id":"npm:handlebars:20110425","severity":"medium","semver":{"vulnerable":["<=1.0.0-beta.3"]}}
@@ -59,25 +68,27 @@
     ],
     "jquery":[
       {"id":"SNYK-JS-JQUERY-174006","severity":"medium","semver":{"vulnerable":["<3.4.0"]}},
-      {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":["=3.0.0-rc1"]}},
+      {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":[">=3.0.0-rc1 <3.0.0"]}},
       {"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.7.1 <1.9.0"]}},
       {"id":"npm:jquery:20110606","severity":"medium","semver":{"vulnerable":["<1.6.3"]}}
     ],
     "jquery-mobile":[
+      {"id":"SNYK-JS-JQUERYMOBILE-174599","severity":"medium","semver":{"vulnerable":["<=1.5.0-alpha.1"]}},
       {"id":"npm:jquery-mobile:20120802","severity":"medium","semver":{"vulnerable":["<1.2.0"]}}
     ],
     "jquery-ui":[
       {"id":"npm:jquery-ui:20121127","severity":"medium","semver":{"vulnerable":["<1.10.0"]}},
       {"id":"npm:jquery-ui:20100903","severity":"medium","semver":{"vulnerable":["<1.10.0"]}},
-      {"id":"npm:jquery-ui:20160721","severity":"high","semver":{"vulnerable":["<=1.11.4"]}}
+      {"id":"npm:jquery-ui:20160721","severity":"high","semver":{"vulnerable":["<1.12.0"]}}
     ],
     "knockout":[
       {"id":"npm:knockout:20180213","severity":"medium","semver":{"vulnerable":["<3.5.0-beta"]}},
       {"id":"npm:knockout:20130701","severity":"medium","semver":{"vulnerable":["<3.0.0 >=2.1.0-pre"]}}
     ],
     "lodash":[
+      {"id":"SNYK-JS-LODASH-450202","severity":"high","semver":{"vulnerable":["<4.17.12"]}},
       {"id":"SNYK-JS-LODASH-73639","severity":"medium","semver":{"vulnerable":["<4.17.11"]}},
       {"id":"SNYK-JS-LODASH-73638","severity":"high","semver":{"vulnerable":["<4.17.11"]}},
       {"id":"npm:lodash:20180130","severity":"medium","semver":{"vulnerable":["<4.17.5"]}}
@@ -85,7 +96,7 @@
     "moment":[
       {"id":"npm:moment:20170905","severity":"low","semver":{"vulnerable":["<2.19.3"]}},
       {"id":"npm:moment:20161019","severity":"medium","semver":{"vulnerable":["<2.15.2"]}},
-      {"id":"npm:moment:20160126","severity":"medium","semver":{"vulnerable":["<=2.11.1"]}}
+      {"id":"npm:moment:20160126","severity":"medium","semver":{"vulnerable":["<2.11.2"]}}
     ],
     "mustache":[
       {"id":"npm:mustache:20151207","severity":"medium","semver":{"vulnerable":["<2.2.1"]}},
@@ -106,7 +117,7 @@
       {"id":"npm:vue:20170829","severity":"medium","semver":{"vulnerable":["<2.4.3"]}},
       {"id":"npm:vue:20170401","severity":"medium","semver":{"vulnerable":["<2.3.0-beta.1"]}},
       {"id":"npm:vue:20180802","severity":"medium","semver":{"vulnerable":["<2.5.17"]}},
-      {"id":"npm:vue:20180222","severity":"low","semver":{"vulnerable":["<=2.5.14"]}}
+      {"id":"npm:vue:20180222","severity":"low","semver":{"vulnerable":["<2.5.14"]}}
     ],
     "yui":[
       {"id":"npm:yui:20130604","severity":"medium","semver":{"vulnerable":[">=3.0.0 <3.10.1 || =3.10.2"]}},


Test patch:
diff --git a/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js b/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js
index a4a28283c..ad8bd8b5f 100644
--- a/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js
+++ b/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js
@@ -99,6 +99,7 @@ describe('Main Thread Tasks', () => {
       duration: 100,
       selfTime: 50,
       group: taskGroups.other,
+      unbounded: false,
     });
 
     expect(taskB).toEqual({
@@ -112,6 +113,7 @@ describe('Main Thread Tasks', () => {
       duration: 50,
       selfTime: 20,
       group: taskGroups.other,
+      unbounded: false,
     });
   });
 
@@ -232,6 +234,7 @@ describe('Main Thread Tasks', () => {
       duration: 110,
       selfTime: 5,
       group: taskGroups.other,
+      unbounded: true,
     });
 
     expect(taskB).toEqual({
@@ -245,6 +248,7 @@ describe('Main Thread Tasks', () => {
       duration: 105,
       selfTime: 95,
       group: taskGroups.other,
+      unbounded: true,
     });
   });
 
@@ -362,6 +366,7 @@ describe('Main Thread Tasks', () => {
         duration: 100,
         selfTime: 50,
         group: taskGroups.other,
+        unbounded: false,
       },
       {
         parent: taskA,
@@ -374,6 +379,7 @@ describe('Main Thread Tasks', () => {
         duration: 50,
         selfTime: 50,
         group: taskGroups.other,
+        unbounded: false,
       },
     ]);
   });
@@ -406,6 +412,7 @@ describe('Main Thread Tasks', () => {
         duration: 100,
         selfTime: 100,
         group: taskGroups.other,
+        unbounded: false,
       },
       {
         parent: undefined,
@@ -418,6 +425,7 @@ describe('Main Thread Tasks', () => {
         duration: 0,
         selfTime: 0,
         group: taskGroups.other,
+        unbounded: false,
       },
     ]);
   });
@@ -452,6 +460,7 @@ describe('Main Thread Tasks', () => {
         duration: 100,
         selfTime: 25,
         group: taskGroups.other,
+        unbounded: false,
       },
       {
         parent: taskA,
@@ -464,10 +473,61 @@ describe('Main Thread Tasks', () => {
         duration: 75,
         selfTime: 75,
         group: taskGroups.other,
+        unbounded: false,
       },
     ]);
   });
 
+  it('should handle child events that extend >1ms beyond parent event because missing E', () => {
+    /*
+    An artistic rendering of the below trace:
+    ████████████████TaskA██████████████████
+            █████████TaskB██████████████████
+    */
+    const traceEvents = [
+      ...boilerplateTrace,
+      {ph: 'B', name: 'TaskA', pid, tid, ts: baseTs, args},
+      {ph: 'B', name: 'TaskB', pid, tid, ts: baseTs + 25e3, args},
+      {ph: 'E', name: 'TaskA', pid, tid, ts: baseTs + 100e3, args},
+      {ph: 'I', name: 'MarkerToPushOutTraceEnd', pid, tid, ts: baseTs + 110e3, args},
+    ];
+
+    traceEvents.forEach(evt => Object.assign(evt, {cat: 'devtools.timeline'}));
+
+    const tasks = run({traceEvents});
+    const [taskA, taskB] = tasks;
+    expect(tasks).toEqual([
+      {
+        parent: undefined,
+        attributableURLs: [],
+
+        children: [taskB],
+        event: traceEvents.find(event => event.name === 'TaskA'),
+        startTime: 0,
+        endTime: 100,
+        duration: 100,
+        selfTime: 25,
+        group: taskGroups.other,
+        unbounded: false,
+      },
+      {
+        parent: taskA,
+        attributableURLs: [],
+
+        children: [],
+        event: traceEvents.find(event => event.name === 'TaskB' && event.ph === 'B'),
+        startTime: 25,
+        endTime: 100,
+        duration: 75,
+        selfTime: 75,
+        group: taskGroups.other,
+        unbounded: true,
+      },
+    ]);
+  });
+
+  // Invalid sets of events.
+  // All of these should have `traceEnd` pushed out to avoid falling into one of our mitigation scenarios.
   const invalidEventSets = [
     [
       // TaskA overlaps with TaskB, X first
@@ -499,12 +559,6 @@ describe('Main Thread Tasks', () => {
       {ph: 'X', name: 'TaskA', pid, tid, ts: baseTs, dur: 100e3, args},
       {ph: 'E', name: 'TaskB', pid, tid, ts: baseTs + 10e3, args},
     ],
-    [
-      {ph: 'I', name: 'MarkerToPushOutTraceEnd', pid, tid, ts: baseTs + 110e3, args},
-      // TaskB is missing an E event within an X
-      {ph: 'X', name: 'TaskA', pid, tid, ts: baseTs, dur: 100e3, args},
-      {ph: 'B', name: 'TaskB', pid, tid, ts: baseTs + 10e3, args},
-    ],
   ];
 
   for (const invalidEvents of invalidEventSets) {