Repository: GoogleChrome/lighthouse
Pull request #7332 (2019-02-26 22:47:34)

docs: update throttling docs to public version
Thanks @paulirish for making them public.


Patch:
diff --git a/docs/throttling.md b/docs/throttling.md
index a09033894..41981648d 100644
--- a/docs/throttling.md
+++ b/docs/throttling.md
@@ -12,7 +12,7 @@ These exact figures are used as [Lighthouse's throttling default](https://github
 ## Throttling basics
 
 1. Simulated throttling, which Lighthouse uses by default, is computed throttling _after a trace has been recorded_ which makes it very fast and deterministic. However, due to the imperfect nature of predicting alternate execution paths, there is inherent inaccuracy that is summarized in this doc: [Lighthouse Metric Variability and Accuracy](https://docs.google.com/document/d/1BqtL-nG53rxWOI5RO0pItSRPowZVnYJ_gBEQCJ5EeUE/edit). The TLDR: while it's roughly as accurate or better than DevTools throttling for most sites, it suffers from edge cases and a deep investigation to performance should use _Packet-level_ throttling tools.
-1. The DevTools network throttling, which Lighthouse uses for `throttlingMethod='devtools'`, is implemented within Chrome at the _request-level_. As a result, it has some downsides that are now summarized in this doc: [Network Throttling & Chrome - status](https://docs.google.com/document/d/1TwWLaLAfnBfbk5_ZzpGXegPapCIfyzT4MWuZgspKUAQ/edit). The TLDR: while it's a [decent approximation](https://docs.google.com/document/d/1uS9SH1KpVH31JAmf-iIZ61VazwAF9MrCVwETshBC4UQ/edit), it's not a sufficient model of a slow connection. The [multipliers used in Lighthouse](https://github.com/GoogleChrome/lighthouse/blob/3be483287a530fb560c843b7299ef9cfe91ce1cc/lighthouse-core/lib/emulation.js#L33-L39) attempt to correct for the differences.
+1. The DevTools network throttling, which Lighthouse uses for `throttlingMethod='devtools'`, is implemented within Chrome at the _request-level_. As a result, it has some downsides that are now summarized in this doc: [Network Throttling & Chrome - status](https://docs.google.com/document/d/1TwWLaLAfnBfbk5_ZzpGXegPapCIfyzT4MWuZgspKUAQ/edit). The TLDR: while it's a [decent approximation](https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit), it's not a sufficient model of a slow connection. The [multipliers used in Lighthouse](https://github.com/GoogleChrome/lighthouse/blob/3be483287a530fb560c843b7299ef9cfe91ce1cc/lighthouse-core/lib/emulation.js#L33-L39) attempt to correct for the differences.
 1. _Proxy-level_ throttling tools do not affect UDP data, so they're not recommended.
 1. _Packet-level_ throttling tools are able to make the most accurate network simulation.
 
diff --git a/lighthouse-core/audits/bootup-time.js b/lighthouse-core/audits/bootup-time.js
index c709e5e03..c2cc645b6 100644
--- a/lighthouse-core/audits/bootup-time.js
+++ b/lighthouse-core/audits/bootup-time.js
@@ -22,7 +22,7 @@ const UIStrings = {
     'You may find delivering smaller JS payloads helps with this. [Learn ' +
     'more](https://developers.google.com/web/tools/lighthouse/audits/bootup).',
   /** Label for the total time column in a data table; entries will be the number of milliseconds spent executing per resource loaded by the page. */
-  columnTotal: 'Total',
+  columnTotal: 'Total CPU Time',
   /** Label for a time column in a data table; entries will be the number of milliseconds spent evaluating script for every script loaded by the page. */
   columnScriptEval: 'Script Evaluation',
   /** Label for a time column in a data table; entries will be the number of milliseconds spent parsing script files for every script loaded by the page. */
@@ -89,8 +89,9 @@ class BootupTime extends Audit {
     for (const task of tasks) {
       const jsURL = task.attributableURLs.find(url => jsURLs.has(url));
       const fallbackURL = task.attributableURLs[0];
-      const attributableURL = jsURL || fallbackURL;
-      if (!attributableURL || attributableURL === 'about:blank') continue;
+      let attributableURL = jsURL || fallbackURL;
+      // If we can't find what URL was responsible for this execution, just attribute it to the root page.
+      if (!attributableURL || attributableURL === 'about:blank') attributableURL = 'Other';
 
       const timingByGroupId = result.get(attributableURL) || {};
       const originalTime = timingByGroupId[task.group.id] || 0;
@@ -122,27 +123,27 @@ class BootupTime extends Audit {
     let totalBootupTime = 0;
     const results = Array.from(executionTimings)
       .map(([url, timingByGroupId]) => {
-        // Add up the totalBootupTime for all the taskGroups
-        let bootupTimeForURL = 0;
+        // Add up the totalExecutionTime for all the taskGroups
+        let totalExecutionTimeForURL = 0;
         for (const [groupId, timespanMs] of Object.entries(timingByGroupId)) {
           timingByGroupId[groupId] = timespanMs * multiplier;
-          bootupTimeForURL += timespanMs * multiplier;
-        }
-
-        // Add up all the execution time of shown URLs
-        if (bootupTimeForURL >= context.options.thresholdInMs) {
-          totalBootupTime += bootupTimeForURL;
+          totalExecutionTimeForURL += timespanMs * multiplier;
         }
 
         const scriptingTotal = timingByGroupId[taskGroups.scriptEvaluation.id] || 0;
         const parseCompileTotal = timingByGroupId[taskGroups.scriptParseCompile.id] || 0;
 
+        // Add up all the JavaScript time of shown URLs
+        if (totalExecutionTimeForURL >= context.options.thresholdInMs) {
+          totalBootupTime += scriptingTotal + parseCompileTotal;
+        }
+
         hadExcessiveChromeExtension = hadExcessiveChromeExtension ||
           (url.startsWith('chrome-extension:') && scriptingTotal > 100);
 
         return {
           url: url,
-          total: bootupTimeForURL,
+          total: totalExecutionTimeForURL,
           // Highlight the JavaScript task costs
           scripting: scriptingTotal,
           scriptParseCompile: parseCompileTotal,
diff --git a/lighthouse-core/computed/main-thread-tasks.js b/lighthouse-core/computed/main-thread-tasks.js
index 5b18eb33b..e66eab62c 100644
--- a/lighthouse-core/computed/main-thread-tasks.js
+++ b/lighthouse-core/computed/main-thread-tasks.js
@@ -41,6 +41,8 @@ const TraceOfTab = require('./trace-of-tab.js');
  * @prop {TaskGroup} group
  */
 
+/** @typedef {{timers: Map<string, TaskNode>}} PriorTaskData */
+
 class MainThreadTasks {
   /**
    * @param {LH.TraceEvent} event
@@ -71,15 +73,24 @@ class MainThreadTasks {
 
   /**
    * @param {LH.TraceEvent[]} mainThreadEvents
+   * @param {PriorTaskData} priorTaskData
    * @return {TaskNode[]}
    */
-  static _createTasksFromEvents(mainThreadEvents) {
+  static _createTasksFromEvents(mainThreadEvents, priorTaskData) {
     /** @type {TaskNode[]} */
     const tasks = [];
     /** @type {TaskNode|undefined} */
     let currentTask;
 
     for (const event of mainThreadEvents) {
+      // Save the timer data, TimerInstall events are instant events `ph === 'I'` so process them first.
+      if (event.name === 'TimerInstall' && currentTask) {
+        /** @type {string} */
+        // @ts-ignore - timerId exists when name is TimerInstall
+        const timerId = event.args.data.timerId;
+        priorTaskData.timers.set(timerId, currentTask);
+      }
+
       // Only look at X (Complete), B (Begin), and E (End) events as they have most data
       if (event.ph !== 'X' && event.ph !== 'B' && event.ph !== 'E') continue;
 
@@ -141,11 +152,13 @@ class MainThreadTasks {
   /**
    * @param {TaskNode} task
    * @param {string[]} parentURLs
+   * @param {PriorTaskData} priorTaskData
    */
-  static _computeRecursiveAttributableURLs(task, parentURLs) {
+  static _computeRecursiveAttributableURLs(task, parentURLs, priorTaskData) {
     const argsData = task.event.args.data || {};
     const stackFrameURLs = (argsData.stackTrace || []).map(entry => entry.url);
 
+    /** @type {Array<string|undefined>} */
     let taskURLs = [];
     switch (task.event.name) {
       /**
@@ -161,6 +174,15 @@ class MainThreadTasks {
       case 'v8.compileModule':
         taskURLs = [task.event.args.fileName].concat(stackFrameURLs);
         break;
+      case 'TimerFire': {
+        /** @type {string} */
+        // @ts-ignore - timerId exists when name is TimerFire
+        const timerId = task.event.args.data.timerId;
+        const timerInstallerTaskNode = priorTaskData.timers.get(timerId);
+        if (!timerInstallerTaskNode) break;
+        taskURLs = timerInstallerTaskNode.attributableURLs.concat(stackFrameURLs);
+        break;
+      }
       default:
         taskURLs = stackFrameURLs;
         break;
@@ -178,7 +200,7 @@ class MainThreadTasks {
 
     task.attributableURLs = attributableURLs;
     task.children.forEach(child =>
-      MainThreadTasks._computeRecursiveAttributableURLs(child, attributableURLs));
+      MainThreadTasks._computeRecursiveAttributableURLs(child, attributableURLs, priorTaskData));
   }
 
   /**
@@ -196,14 +218,16 @@ class MainThreadTasks {
    * @return {TaskNode[]}
    */
   static getMainThreadTasks(traceEvents) {
-    const tasks = MainThreadTasks._createTasksFromEvents(traceEvents);
+    const timers = new Map();
+    const priorTaskData = {timers};
+    const tasks = MainThreadTasks._createTasksFromEvents(traceEvents, priorTaskData);
 
     // Compute the recursive properties we couldn't compute earlier, starting at the toplevel tasks
     for (const task of tasks) {
       if (task.parent) continue;
 
       MainThreadTasks._computeRecursiveSelfTime(task);
-      MainThreadTasks._computeRecursiveAttributableURLs(task, []);
+      MainThreadTasks._computeRecursiveAttributableURLs(task, [], priorTaskData);
       MainThreadTasks._computeRecursiveTaskGroup(task);
     }
 
diff --git a/lighthouse-core/config/constants.js b/lighthouse-core/config/constants.js
index 3c4f21fb2..85c36cd31 100644
--- a/lighthouse-core/config/constants.js
+++ b/lighthouse-core/config/constants.js
@@ -8,7 +8,8 @@
 /**
  * Adjustments needed for DevTools network throttling to simulate
  * more realistic network conditions.
- * See: crbug.com/721112
+ * @see https://crbug.com/721112
+ * @see https://docs.google.com/document/d/10lfVdS1iDWCRKQXPfbxEn4Or99D64mvNlugP1AQuFlE/edit
  */
 const DEVTOOLS_RTT_ADJUSTMENT_FACTOR = 3.75;
 const DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR = 0.9;
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index 299a8c78e..bcb475ced 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -353,6 +353,10 @@ class Driver {
    * @return {Promise<*>}
    */
   async _evaluateInContext(expression, contextId) {
+    // Use a higher than default timeout if the user hasn't specified a specific timeout.
+    // Otherwise, use whatever was requested.
+    const timeout = this._nextProtocolTimeout === DEFAULT_PROTOCOL_TIMEOUT ?
+      60000 : this._nextProtocolTimeout;
     const evaluationParams = {
       // We need to explicitly wrap the raw expression for several purposes:
       // 1. Ensure that the expression will be a native Promise and not a polyfill/non-Promise.
@@ -372,11 +376,11 @@ class Driver {
       includeCommandLineAPI: true,
       awaitPromise: true,
       returnByValue: true,
-      timeout: 60000,
+      timeout,
       contextId,
     };
 
-    this.setNextProtocolTimeout(60000);
+    this.setNextProtocolTimeout(timeout);
     const response = await this.sendCommand('Runtime.evaluate', evaluationParams);
     if (response.exceptionDetails) {
       // An error occurred before we could even create a Promise, should be *very* rare
diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js
index ebb01908b..25e8eb10c 100644
--- a/lighthouse-core/gather/gatherers/image-elements.js
+++ b/lighthouse-core/gather/gatherers/image-elements.js
@@ -87,8 +87,8 @@ function collectImageElementInfo() {
       displayedHeight: element.clientHeight,
       clientRect: getClientRect(element),
       // CSS Images do not expose natural size, we'll determine the size later
-      naturalWidth: Number.MAX_VALUE,
-      naturalHeight: Number.MAX_VALUE,
+      naturalWidth: 0,
+      naturalHeight: 0,
       isCss: true,
       isPicture: false,
       usesObjectFit: false,
@@ -130,12 +130,14 @@ class ImageElements extends Gatherer {
   async fetchElementWithSizeInformation(driver, element) {
     const url = JSON.stringify(element.src);
     try {
+      // We don't want this to take forever, 250ms should be enough for images that are cached
+      driver.setNextProtocolTimeout(250);
       /** @type {{naturalWidth: number, naturalHeight: number}} */
       const size = await driver.evaluateAsync(`(${determineNaturalSize.toString()})(${url})`);
       return Object.assign(element, size);
     } catch (_) {
       // determineNaturalSize fails on invalid images, which we treat as non-visible
-      return Object.assign(element, {naturalWidth: 0, naturalHeight: 0});
+      return element;
     }
   }
 
@@ -147,7 +149,9 @@ class ImageElements extends Gatherer {
   async afterPass(passContext, loadData) {
     const driver = passContext.driver;
     const indexedNetworkRecords = loadData.networkRecords.reduce((map, record) => {
-      if (/^image/.test(record.mimeType) && record.finished) {
+      // The network record is only valid for size information if it finished with a successful status
+      // code that indicates a complete resource response.
+      if (/^image/.test(record.mimeType) && record.finished && record.statusCode === 200) {
         map[record.url] = record;
       }
 
@@ -163,6 +167,9 @@ class ImageElements extends Gatherer {
     const elements = await driver.evaluateAsync(expression);
 
     const imageUsage = [];
+    const top50Images = Object.values(indexedNetworkRecords)
+      .sort((a, b) => b.resourceSize - a.resourceSize)
+      .slice(0, 50);
     for (let element of elements) {
       // Pull some of our information directly off the network record.
       const networkRecord = indexedNetworkRecords[element.src] || {};
@@ -177,8 +184,13 @@ class ImageElements extends Gatherer {
 
       // Images within `picture` behave strangely and natural size information isn't accurate,
       // CSS images have no natural size information at all. Try to get the actual size if we can.
-      // Additional fetch is expensive; don't bother if we don't have a networkRecord for the image.
-      if ((element.isPicture || element.isCss) && networkRecord) {
+      // Additional fetch is expensive; don't bother if we don't have a networkRecord for the image,
+      // or it's not in the top 50 largest images.
+      if (
+        (element.isPicture || element.isCss) &&
+        networkRecord &&
+        top50Images.includes(networkRecord)
+      ) {
         element = await this.fetchElementWithSizeInformation(driver, element);
       }
 
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index 86bddc9be..c95422e88 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -436,7 +436,7 @@
     "description": "Label for a time column in a data table; entries will be the number of milliseconds spent parsing script files for every script loaded by the page."
   },
   "lighthouse-core/audits/bootup-time.js | columnTotal": {
-    "message": "Total",
+    "message": "Total CPU Time",
     "description": "Label for the total time column in a data table; entries will be the number of milliseconds spent executing per resource loaded by the page."
   },
   "lighthouse-core/audits/bootup-time.js | description": {
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 0b6944e44..54862b0e5 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -89,7 +89,7 @@
                         "granularity": 1.0, 
                         "itemType": "ms", 
                         "key": "total", 
-                        "text": "Total"
+                        "text": "Total CPU Time"
                     }, 
                     {
                         "granularity": 1.0, 
@@ -111,6 +111,12 @@
                         "total": 966.9800000000001, 
                         "url": "http://localhost:10200/dobetterweb/dbw_tester.html"
                     }, 
+                    {
+                        "scriptParseCompile": 0.092, 
+                        "scripting": 17.790999999999997, 
+                        "total": 391.76699999999977, 
+                        "url": "Other"
+                    }, 
                     {
                         "scriptParseCompile": 1.674, 
                         "scripting": 89.50799999999998, 
@@ -125,7 +131,7 @@
                     }
                 ], 
                 "summary": {
-                    "wastedMs": 1150.932
+                    "wastedMs": 1150.573
                 }, 
                 "type": "table"
             }, 
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 9dca79bb4..9fe2deb61 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -81,7 +81,7 @@ declare global {
       HTMLWithoutJavaScript: {bodyText: string, hasNoScript: boolean};
       /** Whether the page ended up on an HTTPS page after attempting to load the HTTP version. */
       HTTPRedirect: {value: boolean};
-      /** Information on size and loading for all the images in the page. */
+      /** Information on size and loading for all the images in the page. Natural size information for `picture` and CSS images is only available if the image was one of the largest 50 images. */
       ImageElements: Artifacts.ImageElement[];
       /** Information on JS libraries and versions used by the page. */
       JSLibraries: {name: string, version: string, npmPkgName: string}[];


Test patch:
diff --git a/lighthouse-cli/test/fixtures/tricky-main-thread-consumer.js b/lighthouse-cli/test/fixtures/tricky-main-thread-consumer.js
new file mode 100644
index 000000000..f233672fe
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/tricky-main-thread-consumer.js
@@ -0,0 +1,20 @@
+/**
+ * @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';
+
+/* eslint-disable */
+
+if (window.location.search.includes('setTimeout')) {
+  window.library.setTimeout(() => {
+    window.library.stall(3050);
+  }, 0);
+}
+
+if (window.location.search.includes('fetch')) {
+  window.library.fetch('http://localhost:10200/tricky-main-thread.html').then(() => {
+    window.library.stall(3050);
+  });
+}
diff --git a/lighthouse-cli/test/fixtures/tricky-main-thread-library.js b/lighthouse-cli/test/fixtures/tricky-main-thread-library.js
new file mode 100644
index 000000000..a8cc4a3dd
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/tricky-main-thread-library.js
@@ -0,0 +1,37 @@
+/**
+ * @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';
+
+/* eslint-disable */
+
+window.library = {};
+window.library.setTimeout = (fn, time) => {
+  setTimeout(() => {
+    window.library.stall(50);
+    console.log('Custom timeout hook');
+    fn();
+  }, time);
+};
+
+window.library.fetch = (...args) => {
+  console.log('Custom fetch hook 1');
+  return fetch(...args).then(response => {
+    console.log('Custom fetch hook 2');
+    window.library.stall(50);
+    return response;
+  });
+};
+
+/**
+ * Stalls the main thread for timeInMs
+ */
+window.library.stall = function(timeInMs) {
+  const start = performance.now();
+  while (performance.now() - start < timeInMs) {
+    for (let i = 0; i < 1000000; i++) ;
+  }
+};
+
diff --git a/lighthouse-cli/test/fixtures/tricky-main-thread.html b/lighthouse-cli/test/fixtures/tricky-main-thread.html
new file mode 100644
index 000000000..6238a0df5
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/tricky-main-thread.html
@@ -0,0 +1,8 @@
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
+<body>
+  Just try and figure out my scripts
+  <script src="tricky-main-thread-library.js"></script>
+  <script src="tricky-main-thread-consumer.js"></script>
+</body>
+</html>
diff --git a/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js b/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js
index 07d165fcc..8a67e4228 100644
--- a/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js
+++ b/lighthouse-cli/test/smokehouse/tricky-metrics/expectations.js
@@ -6,7 +6,7 @@
 'use strict';
 
 /**
- * Expected Lighthouse audit values for tricky TTI tests
+ * Expected Lighthouse audit values for tricky metrics tests
  */
 module.exports = [
   {
@@ -32,4 +32,37 @@ module.exports = [
       },
     },
   },
+  {
+    requestedUrl: 'http://localhost:10200/tricky-main-thread.html?setTimeout',
+    finalUrl: 'http://localhost:10200/tricky-main-thread.html?setTimeout',
+    audits: {
+      'bootup-time': {
+        details: {
+          items: {
+            0: {
+              url: /main-thread-consumer/,
+              scripting: '>1000',
+            },
+          },
+        },
+      },
+    },
+  },
+  {
+    requestedUrl: 'http://localhost:10200/tricky-main-thread.html?fetch',
+    finalUrl: 'http://localhost:10200/tricky-main-thread.html?fetch',
+    audits: {
+      'bootup-time': {
+        details: {
+          items: {
+            0: {
+              // TODO: requires async stacks, https://github.com/GoogleChrome/lighthouse/pull/5504
+              // url: /main-thread-consumer/,
+              scripting: '>1000',
+            },
+          },
+        },
+      },
+    },
+  },
 ];
diff --git a/lighthouse-core/test/audits/bootup-time-test.js b/lighthouse-core/test/audits/bootup-time-test.js
index ca240f3ab..a91cb3be5 100644
--- a/lighthouse-core/test/audits/bootup-time-test.js
+++ b/lighthouse-core/test/audits/bootup-time-test.js
@@ -32,12 +32,13 @@ describe('Performance: bootup-time audit', () => {
 
     return BootupTime.audit(artifacts, {options: auditOptions, computedCache}).then(output => {
       assert.deepEqual(roundedValueOf(output, 'https://pwa.rocks/script.js'), {scripting: 31.8, scriptParseCompile: 1.3, total: 36.8});
-      assert.deepEqual(roundedValueOf(output, 'https://www.googletagmanager.com/gtm.js?id=GTM-Q5SW'), {scripting: 96.9, scriptParseCompile: 6.5, total: 104.4});
+      assert.deepEqual(roundedValueOf(output, 'https://www.googletagmanager.com/gtm.js?id=GTM-Q5SW'), {scripting: 97.2, scriptParseCompile: 6.5, total: 104.7});
       assert.deepEqual(roundedValueOf(output, 'https://www.google-analytics.com/plugins/ua/linkid.js'), {scripting: 25.2, scriptParseCompile: 1.2, total: 26.4});
       assert.deepEqual(roundedValueOf(output, 'https://www.google-analytics.com/analytics.js'), {scripting: 40.6, scriptParseCompile: 9.6, total: 53.4});
+      assert.deepEqual(roundedValueOf(output, 'Other'), {scripting: 11.7, scriptParseCompile: 0, total: 1123.8}); // eslint-disable-line max-len
 
-      assert.equal(Math.round(output.rawValue), 221);
-      assert.equal(output.details.items.length, 4);
+      assert.equal(Math.round(output.rawValue), 225);
+      assert.equal(output.details.items.length, 5);
       assert.equal(output.score, 1);
     });
   }, 10000);
@@ -55,9 +56,9 @@ describe('Performance: bootup-time audit', () => {
 
     assert.deepEqual(roundedValueOf(output, 'https://pwa.rocks/script.js'), {scripting: 95.3, scriptParseCompile: 3.9, total: 110.5});
 
-    assert.equal(output.details.items.length, 7);
+    assert.equal(output.details.items.length, 8);
     assert.equal(output.score, 0.98);
-    assert.equal(Math.round(output.rawValue), 709);
+    assert.equal(Math.round(output.rawValue), 720);
   });
 
   it('should get no data when no events are present', () => {
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index 817dac10c..5befd18dd 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -31,13 +31,14 @@ function createMockSendCommandFn() {
   const mockFn = jest.fn().mockImplementation(command => {
     const indexOfResponse = mockResponses.findIndex(entry => entry.command === command);
     if (indexOfResponse === -1) throw new Error(`${command} unimplemented`);
-    const {response} = mockResponses[indexOfResponse];
+    const {response, delay} = mockResponses[indexOfResponse];
     mockResponses.splice(indexOfResponse, 1);
+    if (delay) return new Promise(resolve => setTimeout(() => resolve(response), delay));
     return Promise.resolve(response);
   });
 
-  mockFn.mockResponse = (command, response) => {
-    mockResponses.push({command, response});
+  mockFn.mockResponse = (command, response, delay) => {
+    mockResponses.push({command, response, delay});
     return mockFn;
   };
 
@@ -260,6 +261,34 @@ describe('.evaluateAsync', () => {
     connectionStub.sendCommand.findInvocation('Runtime.evaluate');
   });
 
+  it('uses a high default timeout', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+    .mockResponse('Runtime.evaluate', {result: {value: 2}}, 65000);
+
+    const evaluatePromise = makePromiseInspectable(driver.evaluateAsync('1 + 1'));
+    jest.advanceTimersByTime(30000);
+    await flushAllTimersAndMicrotasks();
+    expect(evaluatePromise).not.toBeDone();
+
+    jest.advanceTimersByTime(30000);
+    await flushAllTimersAndMicrotasks();
+    expect(evaluatePromise).toBeDone();
+    await expect(evaluatePromise).rejects.toBeTruthy();
+  });
+
+  it('uses the specific timeout given', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+    .mockResponse('Runtime.evaluate', {result: {value: 2}}, 10000);
+
+    driver.setNextProtocolTimeout(5000);
+    const evaluatePromise = makePromiseInspectable(driver.evaluateAsync('1 + 1'));
+
+    jest.advanceTimersByTime(5001);
+    await flushAllTimersAndMicrotasks();
+    expect(evaluatePromise).toBeDone();
+    await expect(evaluatePromise).rejects.toBeTruthy();
+  });
+
   it('evaluates an expression in isolation', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
       .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 166f4359a..7a8e7e8a2 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -701,7 +701,7 @@
       "description": "Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/bootup).",
       "score": 0.92,
       "scoreDisplayMode": "numeric",
-      "rawValue": 1150.932,
+      "rawValue": 1150.573,
       "displayValue": "1.2 s",
       "details": {
         "type": "table",
@@ -715,7 +715,7 @@
             "key": "total",
             "granularity": 1,
             "itemType": "ms",
-            "text": "Total"
+            "text": "Total CPU Time"
           },
           {
             "key": "scripting",
@@ -737,6 +737,12 @@
             "scripting": 957.494,
             "scriptParseCompile": 2.777
           },
+          {
+            "url": "Other",
+            "total": 391.76699999999977,
+            "scripting": 17.790999999999997,
+            "scriptParseCompile": 0.092
+          },
           {
             "url": "http://localhost:10200/zone.js",
             "total": 101.21099999999998,
@@ -751,7 +757,7 @@
           }
         ],
         "summary": {
-          "wastedMs": 1150.932
+          "wastedMs": 1150.573
         }
       }
     },
@@ -4878,7 +4884,7 @@
         },
         {
           "values": {
-            "timeInMs": 1150.932
+            "timeInMs": 1150.573
           },
           "path": "audits[bootup-time].displayValue"
         }