lighthouse 9.5.0-dev.20221128 → 9.5.0-dev.20221129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,7 +47,7 @@ async function setup() {
47
47
  */
48
48
  async function runLighthouse(url, configJson, testRunnerOptions = {}) {
49
49
  const chromeFlags = [
50
- `--custom-devtools-frontend=file://${devtoolsDir}/out/Default/gen/front_end`,
50
+ `--custom-devtools-frontend=file://${devtoolsDir}/out/LighthouseIntegration/gen/front_end`,
51
51
  ];
52
52
  const {lhr, artifacts, logs} = await testUrlFromDevtools(url, {
53
53
  config: configJson,
@@ -151,7 +151,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
151
151
  return images.filter(image => {
152
152
  if (image.wastedBytes < IGNORE_THRESHOLD_IN_BYTES) return false;
153
153
  if (image.wastedPercent < IGNORE_THRESHOLD_IN_PERCENT) return false;
154
- return image.requestStartTime < interactiveTimestamp / 1e6 - IGNORE_THRESHOLD_IN_MS / 1000;
154
+ return image.requestStartTime < interactiveTimestamp / 1000 - IGNORE_THRESHOLD_IN_MS;
155
155
  });
156
156
  }
157
157
 
@@ -158,7 +158,7 @@ class RenderBlockingResources extends Audit {
158
158
  const deferredNodeIds = new Set();
159
159
  for (const resource of artifacts.TagsBlockingFirstPaint) {
160
160
  // Ignore any resources that finished after observed FCP (they're clearly not render-blocking)
161
- if (resource.endTime * 1000 > fcpTsInMs) continue;
161
+ if (resource.endTime > fcpTsInMs) continue;
162
162
  // TODO: beacon to Sentry, https://github.com/GoogleChrome/lighthouse/issues/7041
163
163
  if (!nodesByUrl[resource.tag.url]) continue;
164
164
 
@@ -70,8 +70,8 @@ class CriticalRequestChains extends Audit {
70
70
  depth,
71
71
  id,
72
72
  node: child,
73
- chainDuration: (child.request.endTime - startTime) * 1000,
74
- chainTransferSize: (transferSize + child.request.transferSize),
73
+ chainDuration: child.request.endTime - startTime,
74
+ chainTransferSize: transferSize + child.request.transferSize,
75
75
  });
76
76
 
77
77
  // Carry on walking.
@@ -96,7 +96,7 @@ class CriticalRequestChains extends Audit {
96
96
  transferSize: 0,
97
97
  };
98
98
  CriticalRequestChains._traverse(tree, opts => {
99
- const duration = opts.chainDuration;
99
+ const duration = opts.chainDuration * 1000;
100
100
  if (duration > longest.duration) {
101
101
  longest.duration = duration;
102
102
  longest.transferSize = opts.chainTransferSize;
@@ -123,9 +123,9 @@ class CriticalRequestChains extends Audit {
123
123
  const request = opts.node.request;
124
124
  const simpleRequest = {
125
125
  url: request.url,
126
- startTime: request.startTime,
127
- endTime: request.endTime,
128
- responseReceivedTime: request.responseReceivedTime,
126
+ startTime: request.startTime / 1000,
127
+ endTime: request.endTime / 1000,
128
+ responseReceivedTime: request.responseReceivedTime / 1000,
129
129
  transferSize: request.transferSize,
130
130
  };
131
131
 
@@ -165,7 +165,7 @@ class FontDisplay extends Audit {
165
165
  .map(record => {
166
166
  // In reality the end time should be calculated with paint time included
167
167
  // all browsers wait 3000ms to block text so we make sure 3000 is our max wasted time
168
- const wastedMs = Math.min((record.endTime - record.startTime) * 1000, 3000);
168
+ const wastedMs = Math.min(record.endTime - record.startTime, 3000);
169
169
 
170
170
  return {
171
171
  url: record.url,
@@ -47,8 +47,8 @@ class NetworkRequests extends Audit {
47
47
  }
48
48
 
49
49
  /** @param {number} time */
50
- const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
51
- undefined : (time - earliestStartTime) * 1000;
50
+ const normalizeTime = time => time < earliestStartTime || !Number.isFinite(time) ?
51
+ undefined : (time - earliestStartTime);
52
52
 
53
53
  const results = records.map(record => {
54
54
  const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
@@ -64,8 +64,8 @@ class NetworkRequests extends Audit {
64
64
  return {
65
65
  url: UrlUtils.elideDataURI(record.url),
66
66
  protocol: record.protocol,
67
- startTime: timeToMs(record.startTime),
68
- endTime: timeToMs(record.endTime),
67
+ startTime: normalizeTime(record.startTime),
68
+ endTime: normalizeTime(record.endTime),
69
69
  finished: record.finished,
70
70
  transferSize: record.transferSize,
71
71
  resourceSize: record.resourceSize,
@@ -112,7 +112,7 @@ class NetworkRequests extends Audit {
112
112
 
113
113
  // Include starting timestamp to allow syncing requests with navStart/metric timestamps.
114
114
  const networkStartTimeTs = Number.isFinite(earliestStartTime) ?
115
- earliestStartTime * 1_000_000 : undefined;
115
+ earliestStartTime * 1000 : undefined;
116
116
  tableDetails.debugData = {
117
117
  type: 'debugdata',
118
118
  networkStartTimeTs,
@@ -125,9 +125,9 @@ class Redirects extends Audit {
125
125
  }
126
126
 
127
127
  const lanternTimingDeltaMs = redirectedTiming.startTime - initialTiming.startTime;
128
- const observedTimingDeltaS = redirectedRequest.startTime - initialRequest.startTime;
128
+ const observedTimingDeltaMs = redirectedRequest.startTime - initialRequest.startTime;
129
129
  const wastedMs = settings.throttlingMethod === 'simulate' ?
130
- lanternTimingDeltaMs : observedTimingDeltaS * 1000;
130
+ lanternTimingDeltaMs : observedTimingDeltaMs;
131
131
  totalWastedMs += wastedMs;
132
132
 
133
133
  tableRows.push({
@@ -20,7 +20,7 @@ import {LanternLargestContentfulPaint} from '../computed/metrics/lantern-largest
20
20
  // around for 10s. Meaning, the time delta between processing preconnect a request should be <10s,
21
21
  // otherwise it's wasted. We add a 5s margin so we are sure to capture all key requests.
22
22
  // @see https://github.com/GoogleChrome/lighthouse/issues/3106#issuecomment-333653747
23
- const PRECONNECT_SOCKET_MAX_IDLE = 15;
23
+ const PRECONNECT_SOCKET_MAX_IDLE_IN_MS = 15_000;
24
24
 
25
25
  const IGNORE_THRESHOLD_IN_MS = 50;
26
26
 
@@ -96,7 +96,7 @@ class UsesRelPreconnectAudit extends Audit {
96
96
  * @return {boolean}
97
97
  */
98
98
  static socketStartTimeIsBelowThreshold(record, mainResource) {
99
- return Math.max(0, record.startTime - mainResource.endTime) < PRECONNECT_SOCKET_MAX_IDLE;
99
+ return Math.max(0, record.startTime - mainResource.endTime) < PRECONNECT_SOCKET_MAX_IDLE_IN_MS;
100
100
  }
101
101
 
102
102
  /**
@@ -150,7 +150,7 @@ class UsesRelPreconnectAudit extends Audit {
150
150
  !lcpGraphURLs.has(record.url) ||
151
151
  // Filter out all resources where origins are already resolved.
152
152
  UsesRelPreconnectAudit.hasAlreadyConnectedToOrigin(record) ||
153
- // Make sure the requests are below the PRECONNECT_SOCKET_MAX_IDLE (15s) mark.
153
+ // Make sure the requests are below the PRECONNECT_SOCKET_MAX_IDLE_IN_MS (15s) mark.
154
154
  !UsesRelPreconnectAudit.socketStartTimeIsBelowThreshold(record, mainResource)
155
155
  ) {
156
156
  return;
@@ -189,8 +189,8 @@ class UsesRelPreconnectAudit extends Audit {
189
189
  if (firstRecordOfOrigin.parsedURL.scheme === 'https') connectionTime = connectionTime * 2;
190
190
 
191
191
  const timeBetweenMainResourceAndDnsStart =
192
- firstRecordOfOrigin.startTime * 1000 -
193
- mainResource.endTime * 1000 +
192
+ firstRecordOfOrigin.startTime -
193
+ mainResource.endTime +
194
194
  firstRecordOfOrigin.timing.dnsStart;
195
195
 
196
196
  const wastedMs = Math.min(connectionTime, timeBetweenMainResourceAndDnsStart);
@@ -6,6 +6,7 @@
6
6
 
7
7
  import path from 'path';
8
8
  import {createRequire} from 'module';
9
+ import url from 'url';
9
10
 
10
11
  import isDeepEqual from 'lodash/isEqual.js';
11
12
 
@@ -217,6 +218,11 @@ const bundledModules = new Map(/* BUILD_REPLACE_BUNDLED_MODULES */);
217
218
  * @param {string} requirePath
218
219
  */
219
220
  async function requireWrapper(requirePath) {
221
+ // For windows.
222
+ if (path.isAbsolute(requirePath)) {
223
+ requirePath = url.pathToFileURL(requirePath).href;
224
+ }
225
+
220
226
  /** @type {any} */
221
227
  let module;
222
228
  if (bundledModules.has(requirePath)) {
@@ -231,7 +237,6 @@ async function requireWrapper(requirePath) {
231
237
  if (module.default) return module.default;
232
238
 
233
239
  // Find a valid named export.
234
- // TODO(esmodules): actually make all the audits/gatherers use named exports
235
240
  const methods = new Set(['meta']);
236
241
  const possibleNamedExports = Object.keys(module).filter(key => {
237
242
  if (!(module[key] && module[key] instanceof Object)) return false;
@@ -287,8 +292,12 @@ function requireAudit(auditPath, coreAuditList, configDir) {
287
292
  } else {
288
293
  // Otherwise, attempt to find it elsewhere. This throws if not found.
289
294
  const absolutePath = resolveModulePath(auditPath, configDir, 'audit');
290
- // Use a relative path so bundler can easily expose it.
291
- requirePath = path.relative(getModuleDirectory(import.meta), absolutePath);
295
+ if (isBundledEnvironment()) {
296
+ // Use a relative path so bundler can easily expose it.
297
+ requirePath = path.relative(getModuleDirectory(import.meta), absolutePath);
298
+ } else {
299
+ requirePath = absolutePath;
300
+ }
292
301
  }
293
302
  }
294
303
 
@@ -128,6 +128,7 @@ const artifacts = {
128
128
  Trace: '',
129
129
  Accessibility: '',
130
130
  AnchorElements: '',
131
+ BFCacheFailures: '',
131
132
  CacheContents: '',
132
133
  ConsoleMessages: '',
133
134
  CSSUsage: '',
@@ -103,5 +103,4 @@ class FRGatherer {
103
103
  }
104
104
  }
105
105
 
106
- // TODO(esmodules): do not use default export
107
106
  export default FRGatherer;
@@ -58,15 +58,22 @@ class TargetManager extends ProtocolEventEmitter {
58
58
  async _onFrameNavigated(frameNavigatedEvent) {
59
59
  // Child frames are handled in `_onSessionAttached`.
60
60
  if (frameNavigatedEvent.frame.parentId) return;
61
+ if (!this._enabled) return;
61
62
 
62
63
  // It's not entirely clear when this is necessary, but when the page switches processes on
63
64
  // navigating from about:blank to the `requestedUrl`, resetting `setAutoAttach` has been
64
65
  // necessary in the past.
65
- await this._rootCdpSession.send('Target.setAutoAttach', {
66
- autoAttach: true,
67
- flatten: true,
68
- waitForDebuggerOnStart: true,
69
- });
66
+ try {
67
+ await this._rootCdpSession.send('Target.setAutoAttach', {
68
+ autoAttach: true,
69
+ flatten: true,
70
+ waitForDebuggerOnStart: true,
71
+ });
72
+ } catch (err) {
73
+ // The page can be closed at the end of the run before this CDP function returns.
74
+ // In these cases, just ignore the error since we won't need the page anyway.
75
+ if (this._enabled) throw err;
76
+ }
70
77
  }
71
78
 
72
79
  /**
@@ -84,7 +84,7 @@ class Driver {
84
84
  /** @return {Promise<void>} */
85
85
  async disconnect() {
86
86
  if (this.defaultSession === throwingSession) return;
87
- this._targetManager?.disable();
87
+ await this._targetManager?.disable();
88
88
  await this.defaultSession.dispose();
89
89
  }
90
90
  }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * 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
4
+ * 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.
5
+ */
6
+
7
+ import FRGatherer from '../base-gatherer.js';
8
+ import {waitForFrameNavigated, waitForLoadEvent} from '../driver/wait-for-condition.js';
9
+ import DevtoolsLog from './devtools-log.js';
10
+
11
+ class BFCacheFailures extends FRGatherer {
12
+ /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
13
+ meta = {
14
+ supportedModes: ['navigation', 'timespan'],
15
+ dependencies: {DevtoolsLog: DevtoolsLog.symbol},
16
+ };
17
+
18
+ /**
19
+ * @param {LH.Crdp.Page.BackForwardCacheNotRestoredExplanation[]} errorList
20
+ * @return {LH.Artifacts.BFCacheFailure}
21
+ */
22
+ static processBFCacheEventList(errorList) {
23
+ /** @type {LH.Artifacts.BFCacheNotRestoredReasonsTree} */
24
+ const notRestoredReasonsTree = {
25
+ Circumstantial: {},
26
+ PageSupportNeeded: {},
27
+ SupportPending: {},
28
+ };
29
+
30
+ for (const err of errorList) {
31
+ const bfCacheErrorsMap = notRestoredReasonsTree[err.type];
32
+ bfCacheErrorsMap[err.reason] = [];
33
+ }
34
+
35
+ return {notRestoredReasonsTree};
36
+ }
37
+
38
+ /**
39
+ * @param {LH.Crdp.Page.BackForwardCacheNotRestoredExplanationTree} errorTree
40
+ * @return {LH.Artifacts.BFCacheFailure}
41
+ */
42
+ static processBFCacheEventTree(errorTree) {
43
+ /** @type {LH.Artifacts.BFCacheNotRestoredReasonsTree} */
44
+ const notRestoredReasonsTree = {
45
+ Circumstantial: {},
46
+ PageSupportNeeded: {},
47
+ SupportPending: {},
48
+ };
49
+
50
+ /**
51
+ * @param {LH.Crdp.Page.BackForwardCacheNotRestoredExplanationTree} node
52
+ */
53
+ function traverse(node) {
54
+ for (const error of node.explanations) {
55
+ const bfCacheErrorsMap = notRestoredReasonsTree[error.type];
56
+ const frameUrls = bfCacheErrorsMap[error.reason] || [];
57
+ frameUrls.push(node.url);
58
+ bfCacheErrorsMap[error.reason] = frameUrls;
59
+ }
60
+
61
+ for (const child of node.children) {
62
+ traverse(child);
63
+ }
64
+ }
65
+
66
+ traverse(errorTree);
67
+
68
+ return {notRestoredReasonsTree};
69
+ }
70
+
71
+ /**
72
+ * @param {LH.Crdp.Page.BackForwardCacheNotUsedEvent|undefined} event
73
+ * @return {LH.Artifacts.BFCacheFailure}
74
+ */
75
+ static processBFCacheEvent(event) {
76
+ if (event?.notRestoredExplanationsTree) {
77
+ return BFCacheFailures.processBFCacheEventTree(event.notRestoredExplanationsTree);
78
+ }
79
+ return BFCacheFailures.processBFCacheEventList(event?.notRestoredExplanations || []);
80
+ }
81
+
82
+ /**
83
+ * @param {LH.Gatherer.FRTransitionalContext} context
84
+ * @return {Promise<LH.Crdp.Page.BackForwardCacheNotUsedEvent|undefined>}
85
+ */
86
+ async activelyCollectBFCacheEvent(context) {
87
+ const session = context.driver.defaultSession;
88
+
89
+ /** @type {LH.Crdp.Page.BackForwardCacheNotUsedEvent|undefined} */
90
+ let bfCacheEvent = undefined;
91
+
92
+ /**
93
+ * @param {LH.Crdp.Page.BackForwardCacheNotUsedEvent} event
94
+ */
95
+ function onBfCacheNotUsed(event) {
96
+ bfCacheEvent = event;
97
+ }
98
+
99
+ session.on('Page.backForwardCacheNotUsed', onBfCacheNotUsed);
100
+
101
+ const history = await session.sendCommand('Page.getNavigationHistory');
102
+ const entry = history.entries[history.currentIndex];
103
+
104
+ await Promise.all([
105
+ session.sendCommand('Page.navigate', {url: 'about:blank'}),
106
+ waitForLoadEvent(session, 0).promise,
107
+ ]);
108
+
109
+ await Promise.all([
110
+ session.sendCommand('Page.navigateToHistoryEntry', {entryId: entry.id}),
111
+ waitForFrameNavigated(session).promise,
112
+ ]);
113
+
114
+ session.off('Page.backForwardCacheNotUsed', onBfCacheNotUsed);
115
+
116
+ return bfCacheEvent;
117
+ }
118
+
119
+ /**
120
+ * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context
121
+ * @return {LH.Crdp.Page.BackForwardCacheNotUsedEvent[]}
122
+ */
123
+ passivelyCollectBFCacheEvents(context) {
124
+ const events = [];
125
+ for (const event of context.dependencies.DevtoolsLog) {
126
+ if (event.method === 'Page.backForwardCacheNotUsed') {
127
+ events.push(event.params);
128
+ }
129
+ }
130
+ return events;
131
+ }
132
+
133
+ /**
134
+ * @param {LH.Gatherer.FRTransitionalContext<'DevtoolsLog'>} context
135
+ * @return {Promise<LH.Artifacts['BFCacheFailures']>}
136
+ */
137
+ async getArtifact(context) {
138
+ const events = this.passivelyCollectBFCacheEvents(context);
139
+ if (context.gatherMode === 'navigation') {
140
+ const activelyCollectedEvent = await this.activelyCollectBFCacheEvent(context);
141
+ if (activelyCollectedEvent) events.push(activelyCollectedEvent);
142
+ }
143
+
144
+ return events.map(BFCacheFailures.processBFCacheEvent);
145
+ }
146
+
147
+ /**
148
+ * @param {LH.Gatherer.PassContext} passContext
149
+ * @param {LH.Gatherer.LoadData} loadData
150
+ * @return {Promise<LH.Artifacts['BFCacheFailures']>}
151
+ */
152
+ async afterPass(passContext, loadData) {
153
+ return this.getArtifact({...passContext, dependencies: {DevtoolsLog: loadData.devtoolsLog}});
154
+ }
155
+ }
156
+
157
+ export default BFCacheFailures;
158
+
@@ -34,38 +34,6 @@ function hasFontSizeDeclaration(style) {
34
34
  return !!style && !!style.cssProperties.find(({name}) => name === FONT_SIZE_PROPERTY_NAME);
35
35
  }
36
36
 
37
- /**
38
- * Computes the CSS specificity of a given selector, i.e. #id > .class > div
39
- * TODO: Handle pseudo selectors (:not(), :where, :nth-child) and attribute selectors
40
- * LIMITATION: !important is not respected
41
- *
42
- * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
43
- * @see https://www.smashingmagazine.com/2010/04/css-specificity-and-inheritance/
44
- * @see https://drafts.csswg.org/selectors-4/#specificity-rules
45
- *
46
- * @param {string} selector
47
- * @return {number}
48
- */
49
- function computeSelectorSpecificity(selector) {
50
- // Remove universal selector and separator characters, then split.
51
- const tokens = selector.replace(/[*\s+>~]/g, ' ').split(' ');
52
-
53
- let numIDs = 0;
54
- let numClasses = 0;
55
- let numTypes = 0;
56
-
57
- for (const token of tokens) {
58
- const ids = token.match(/(\b|^)#[a-z0-9_-]+/gi) || [];
59
- const classes = token.match(/(\b|^)\.[a-z0-9_-]+/gi) || [];
60
- const types = token.match(/^[a-z]+/i) ? [1] : [];
61
- numIDs += ids.length;
62
- numClasses += classes.length;
63
- numTypes += types.length;
64
- }
65
-
66
- return Math.min(9, numIDs) * 100 + Math.min(9, numClasses) * 10 + Math.min(9, numTypes);
67
- }
68
-
69
37
  /**
70
38
  * Finds the most specific directly matched CSS font-size rule from the list.
71
39
  *
@@ -74,31 +42,21 @@ function computeSelectorSpecificity(selector) {
74
42
  * @return {NodeFontData['cssRule']|undefined}
75
43
  */
76
44
  function findMostSpecificMatchedCSSRule(matchedCSSRules = [], isDeclarationOfInterest) {
77
- let maxSpecificity = -Infinity;
78
- /** @type {LH.Crdp.CSS.CSSRule|undefined} */
79
- let maxSpecificityRule;
80
-
81
- for (const {rule, matchingSelectors} of matchedCSSRules) {
82
- if (isDeclarationOfInterest(rule.style)) {
83
- const specificities = matchingSelectors.map(idx =>
84
- computeSelectorSpecificity(rule.selectorList.selectors[idx].text)
85
- );
86
- const specificity = Math.max(...specificities);
87
- // Use greater OR EQUAL so that the last rule wins in the event of a tie
88
- if (specificity >= maxSpecificity) {
89
- maxSpecificity = specificity;
90
- maxSpecificityRule = rule;
91
- }
45
+ let mostSpecificRule;
46
+ for (let i = matchedCSSRules.length - 1; i >= 0; i--) {
47
+ if (isDeclarationOfInterest(matchedCSSRules[i].rule.style)) {
48
+ mostSpecificRule = matchedCSSRules[i].rule;
49
+ break;
92
50
  }
93
51
  }
94
52
 
95
- if (maxSpecificityRule) {
53
+ if (mostSpecificRule) {
96
54
  return {
97
55
  type: 'Regular',
98
- ...maxSpecificityRule.style,
56
+ ...mostSpecificRule.style,
99
57
  parentRule: {
100
- origin: maxSpecificityRule.origin,
101
- selectors: maxSpecificityRule.selectorList.selectors,
58
+ origin: mostSpecificRule.origin,
59
+ selectors: mostSpecificRule.selectorList.selectors,
102
60
  },
103
61
  };
104
62
  }
@@ -376,7 +334,6 @@ class FontSize extends FRGatherer {
376
334
 
377
335
  export default FontSize;
378
336
  export {
379
- computeSelectorSpecificity,
380
337
  getEffectiveFontRule,
381
338
  findMostSpecificMatchedCSSRule,
382
339
  };
@@ -71,6 +71,7 @@ legacyDefaultConfig.passes = [{
71
71
  'inspector-issues',
72
72
  'source-maps',
73
73
  'full-page-screenshot',
74
+ 'bf-cache-failures',
74
75
  ],
75
76
  },
76
77
  {
@@ -20,7 +20,7 @@ import {LighthouseError} from '../lib/lh-error.js';
20
20
 
21
21
  const {promisify} = util;
22
22
 
23
- // TODO(esmodules): Rollup does not support `promisfy` or `stream.pipeline`. Bundled files
23
+ // Rollup does not support `promisfy` or `stream.pipeline`. Bundled files
24
24
  // don't need anything in this file except for `stringifyReplacer`, so a check for
25
25
  // truthiness before using is enough.
26
26
  // TODO: Can remove promisify(pipeline) in Node 15.
@@ -50,6 +50,7 @@ class BaseNode {
50
50
  }
51
51
 
52
52
  /**
53
+ * In microseconds
53
54
  * @return {number}
54
55
  */
55
56
  get startTime() {
@@ -57,6 +58,7 @@ class BaseNode {
57
58
  }
58
59
 
59
60
  /**
61
+ * In microseconds
60
62
  * @return {number}
61
63
  */
62
64
  get endTime() {
@@ -25,14 +25,14 @@ class NetworkNode extends BaseNode {
25
25
  * @return {number}
26
26
  */
27
27
  get startTime() {
28
- return this._record.startTime * 1000 * 1000;
28
+ return this._record.startTime * 1000;
29
29
  }
30
30
 
31
31
  /**
32
32
  * @return {number}
33
33
  */
34
34
  get endTime() {
35
- return this._record.endTime * 1000 * 1000;
35
+ return this._record.endTime * 1000;
36
36
  }
37
37
 
38
38
  /**
@@ -150,7 +150,7 @@ class NetworkAnalyzer {
150
150
  if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
151
151
 
152
152
  // Compute the amount of time downloading everything after the first congestion window took
153
- const totalTime = (record.endTime - record.startTime) * 1000;
153
+ const totalTime = record.endTime - record.startTime;
154
154
  const downloadTimeAfterFirstByte = totalTime - timing.receiveHeadersEnd;
155
155
  const numberOfRoundTrips = Math.log2(record.transferSize / INITIAL_CWD);
156
156
 
@@ -399,8 +399,8 @@ class NetworkAnalyzer {
399
399
 
400
400
  // If we've made it this far, all the times we need should be valid (i.e. not undefined/-1).
401
401
  totalBytes += record.transferSize;
402
- boundaries.push({time: record.responseReceivedTime, isStart: true});
403
- boundaries.push({time: record.endTime, isStart: false});
402
+ boundaries.push({time: record.responseReceivedTime / 1000, isStart: true});
403
+ boundaries.push({time: record.endTime / 1000, isStart: false});
404
404
  return boundaries;
405
405
  }, /** @type {Array<{time: number, isStart: boolean}>} */([])).sort((a, b) => a.time - b.time);
406
406
 
@@ -236,8 +236,11 @@ class Simulator {
236
236
  * currently in flight.
237
237
  */
238
238
  _updateNetworkCapacity() {
239
+ const inFlight = this._numberInProgress(BaseNode.TYPES.NETWORK);
240
+ if (inFlight === 0) return;
241
+
239
242
  for (const connection of this._connectionPool.connectionsInUse()) {
240
- connection.setThroughput(this._throughput / this._nodes[NodeState.InProgress].size);
243
+ connection.setThroughput(this._throughput / inFlight);
241
244
  }
242
245
  }
243
246