lighthouse 12.0.0-dev.20240602 → 12.0.0-dev.20240603

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.
@@ -53,7 +53,7 @@ function getNodesAndTimingByRequestId(nodeTimings) {
53
53
  for (const [node, nodeTiming] of nodeTimings) {
54
54
  if (node.type !== 'network') continue;
55
55
 
56
- requestIdToNode.set(node.record.requestId, {node, nodeTiming});
56
+ requestIdToNode.set(node.request.requestId, {node, nodeTiming});
57
57
  }
58
58
 
59
59
  return requestIdToNode;
@@ -89,7 +89,7 @@ class UsesHTTP2Audit extends Audit {
89
89
  if (node.type !== 'network') return;
90
90
  if (!urlsToChange.has(node.record.url)) return;
91
91
 
92
- originalProtocols.set(node.record.requestId, node.record.protocol);
92
+ originalProtocols.set(node.request.requestId, node.record.protocol);
93
93
  node.request.protocol = 'h2';
94
94
  });
95
95
 
@@ -98,7 +98,7 @@ class UsesHTTP2Audit extends Audit {
98
98
  // Restore the original protocol after we've done our simulation
99
99
  graph.traverse(node => {
100
100
  if (node.type !== 'network') return;
101
- const originalProtocol = originalProtocols.get(node.record.requestId);
101
+ const originalProtocol = originalProtocols.get(node.request.requestId);
102
102
  if (originalProtocol === undefined) return;
103
103
  node.request.protocol = originalProtocol;
104
104
  });
@@ -69,7 +69,7 @@ class PrioritizeLcpImage extends Audit {
69
69
  static findLCPNode(graph, lcpRecord) {
70
70
  for (const {node} of graph.traverseGenerator()) {
71
71
  if (node.type !== 'network') continue;
72
- if (node.record.requestId === lcpRecord.requestId) {
72
+ if (node.request.requestId === lcpRecord.requestId) {
73
73
  return node;
74
74
  }
75
75
  }
@@ -98,7 +98,7 @@ class Redirects extends Audit {
98
98
  const nodeTimingsById = new Map();
99
99
  for (const [node, timing] of metricResult.pessimisticEstimate.nodeTimings.entries()) {
100
100
  if (node.type === 'network') {
101
- nodeTimingsById.set(node.record.requestId, timing);
101
+ nodeTimingsById.set(node.request.requestId, timing);
102
102
  }
103
103
  }
104
104
 
@@ -2,11 +2,29 @@
2
2
  * @param {LH.Artifacts.MetricComputationDataInput} data
3
3
  * @param {LH.Artifacts.ComputedContext} context
4
4
  */
5
- export function getComputationDataParams(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
5
+ export function getComputationDataParamsFromTrace(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
6
+ simulator: import("../../lib/lantern/simulator/simulator.js").Simulator<any>;
7
+ graph: import("../../lib/lantern/page-dependency-graph.js").Node;
8
+ processedNavigation: import("../../index.js").Artifacts.ProcessedNavigation;
9
+ }>;
10
+ /**
11
+ * @param {LH.Artifacts.MetricComputationDataInput} data
12
+ * @param {LH.Artifacts.ComputedContext} context
13
+ */
14
+ export function getComputationDataParamsFromDevtoolsLog(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
6
15
  simulator: import("../../lib/lantern/simulator/simulator.js").Simulator<any>;
7
16
  graph: import("../page-dependency-graph.js").Node;
8
17
  processedNavigation: import("../../index.js").Artifacts.ProcessedNavigation;
9
18
  }>;
19
+ /**
20
+ * @param {LH.Artifacts.MetricComputationDataInput} data
21
+ * @param {LH.Artifacts.ComputedContext} context
22
+ */
23
+ export function getComputationDataParams(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
24
+ simulator: import("../../lib/lantern/simulator/simulator.js").Simulator<any>;
25
+ graph: import("../../lib/lantern/page-dependency-graph.js").Node;
26
+ processedNavigation: import("../../index.js").Artifacts.ProcessedNavigation;
27
+ }>;
10
28
  /**
11
29
  * @param {unknown} err
12
30
  * @return {never}
@@ -5,16 +5,20 @@
5
5
  */
6
6
 
7
7
  import {LanternError} from '../../lib/lantern/lantern-error.js';
8
+ import {PageDependencyGraph as LanternPageDependencyGraph} from '../../lib/lantern/page-dependency-graph.js';
8
9
  import {LighthouseError} from '../../lib/lh-error.js';
9
10
  import {LoadSimulator} from '../load-simulator.js';
10
- import {PageDependencyGraph} from '../page-dependency-graph.js';
11
11
  import {ProcessedNavigation} from '../processed-navigation.js';
12
+ import {ProcessedTrace} from '../processed-trace.js';
13
+ import {TraceEngineResult} from '../trace-engine-result.js';
14
+ import {PageDependencyGraph} from '../page-dependency-graph.js';
12
15
 
16
+ // TODO: we need to update all test traces (that use lantern) before this can be removed
13
17
  /**
14
18
  * @param {LH.Artifacts.MetricComputationDataInput} data
15
19
  * @param {LH.Artifacts.ComputedContext} context
16
20
  */
17
- async function getComputationDataParams(data, context) {
21
+ async function getComputationDataParamsFromDevtoolsLog(data, context) {
18
22
  if (data.gatherContext.gatherMode !== 'navigation') {
19
23
  throw new Error(`Lantern metrics can only be computed on navigations`);
20
24
  }
@@ -26,6 +30,35 @@ async function getComputationDataParams(data, context) {
26
30
  return {simulator, graph, processedNavigation};
27
31
  }
28
32
 
33
+ /**
34
+ * @param {LH.Artifacts.URL} theURL
35
+ * @param {LH.Trace} trace
36
+ * @param {LH.Artifacts.ComputedContext} context
37
+ */
38
+ async function createGraphFromTrace(theURL, trace, context) {
39
+ const {mainThreadEvents} = await ProcessedTrace.request(trace, context);
40
+ const traceEngineResult = await TraceEngineResult.request({trace}, context);
41
+ return LanternPageDependencyGraph.createGraphFromTrace(
42
+ mainThreadEvents, trace, traceEngineResult, theURL);
43
+ }
44
+
45
+ /**
46
+ * @param {LH.Artifacts.MetricComputationDataInput} data
47
+ * @param {LH.Artifacts.ComputedContext} context
48
+ */
49
+ async function getComputationDataParamsFromTrace(data, context) {
50
+ if (data.gatherContext.gatherMode !== 'navigation') {
51
+ throw new Error(`Lantern metrics can only be computed on navigations`);
52
+ }
53
+
54
+ const {trace, URL} = data;
55
+ const {graph} = await createGraphFromTrace(URL, trace, context);
56
+ const processedNavigation = await ProcessedNavigation.request(data.trace, context);
57
+ const simulator = data.simulator || (await LoadSimulator.request(data, context));
58
+
59
+ return {simulator, graph, processedNavigation};
60
+ }
61
+
29
62
  /**
30
63
  * @param {unknown} err
31
64
  * @return {never}
@@ -43,7 +76,23 @@ function lanternErrorAdapter(err) {
43
76
  throw err;
44
77
  }
45
78
 
79
+ /**
80
+ * @param {LH.Artifacts.MetricComputationDataInput} data
81
+ * @param {LH.Artifacts.ComputedContext} context
82
+ */
83
+ function getComputationDataParams(data, context) {
84
+ // TODO(15841): remove when all tests use the trace, and we want to remove CDT graph impl.
85
+ if (process.env.INTERNAL_LANTERN_USE_TRACE !== undefined) {
86
+ return getComputationDataParamsFromTrace(data, context);
87
+ } else {
88
+ // This is the default behavior.
89
+ return getComputationDataParamsFromDevtoolsLog(data, context);
90
+ }
91
+ }
92
+
46
93
  export {
94
+ getComputationDataParamsFromTrace,
95
+ getComputationDataParamsFromDevtoolsLog,
47
96
  getComputationDataParams,
48
97
  lanternErrorAdapter,
49
98
  };
@@ -11,6 +11,7 @@ declare const NavigationInsightsComputed: typeof NavigationInsights & {
11
11
  Samples: typeof import("@paulirish/trace_engine/models/trace/handlers/SamplesHandler.js");
12
12
  Screenshots: typeof import("@paulirish/trace_engine/models/trace/handlers/ScreenshotsHandler.js");
13
13
  PageLoadMetrics: typeof import("@paulirish/trace_engine/models/trace/handlers/PageLoadMetricsHandler.js");
14
+ Workers: typeof import("@paulirish/trace_engine/models/trace/handlers/WorkersHandler.js");
14
15
  }>>;
15
16
  };
16
17
  /**
@@ -31,6 +32,7 @@ declare class NavigationInsights {
31
32
  Samples: typeof import("@paulirish/trace_engine/models/trace/handlers/SamplesHandler.js");
32
33
  Screenshots: typeof import("@paulirish/trace_engine/models/trace/handlers/ScreenshotsHandler.js");
33
34
  PageLoadMetrics: typeof import("@paulirish/trace_engine/models/trace/handlers/PageLoadMetricsHandler.js");
35
+ Workers: typeof import("@paulirish/trace_engine/models/trace/handlers/WorkersHandler.js");
34
36
  }>>;
35
37
  }
36
38
  //# sourceMappingURL=navigation-insights.d.ts.map
@@ -16,6 +16,7 @@ declare namespace ENABLED_HANDLERS {
16
16
  const Samples: typeof import("@paulirish/trace_engine/models/trace/handlers/SamplesHandler.js");
17
17
  const Screenshots: typeof import("@paulirish/trace_engine/models/trace/handlers/ScreenshotsHandler.js");
18
18
  const PageLoadMetrics: typeof import("@paulirish/trace_engine/models/trace/handlers/PageLoadMetricsHandler.js");
19
+ const Workers: typeof import("@paulirish/trace_engine/models/trace/handlers/WorkersHandler.js");
19
20
  }
20
21
  /**
21
22
  * @fileoverview Processes trace with the shared trace engine.
@@ -21,6 +21,7 @@ const ENABLED_HANDLERS = {
21
21
  Samples: TraceEngine.TraceHandlers.Samples,
22
22
  Screenshots: TraceEngine.TraceHandlers.Screenshots,
23
23
  PageLoadMetrics: TraceEngine.TraceHandlers.PageLoadMetrics,
24
+ Workers: TraceEngine.TraceHandlers.Workers,
24
25
  };
25
26
 
26
27
  /**
@@ -6,7 +6,7 @@
6
6
 
7
7
  import * as Lantern from './types/lantern.js';
8
8
  import {BaseNode} from '../../lib/lantern/base-node.js';
9
- import {NetworkRequest} from '../../lib/network-request.js';
9
+ import {RESOURCE_TYPES} from '../../lib/network-request.js';
10
10
 
11
11
  /** @typedef {import('./base-node.js').Node} Node */
12
12
  /** @typedef {import('./network-node.js').NetworkNode} NetworkNode */
@@ -33,9 +33,9 @@ class Metric {
33
33
 
34
34
  dependencyGraph.traverse(node => {
35
35
  if (node.type !== BaseNode.TYPES.NETWORK) return;
36
- if (node.record.resourceType !== NetworkRequest.TYPES.Script) return;
36
+ if (node.request.resourceType !== RESOURCE_TYPES.Script) return;
37
37
  if (treatNodeAsRenderBlocking?.(node)) {
38
- scriptUrls.add(node.record.url);
38
+ scriptUrls.add(node.request.url);
39
39
  }
40
40
  });
41
41
 
@@ -153,7 +153,7 @@ class FirstContentfulPaint extends Metric {
153
153
  const endedAfterPaint = node.endTime > cutoffTimestamp || node.startTime > cutoffTimestamp;
154
154
  if (endedAfterPaint && !node.isMainDocument()) return false;
155
155
 
156
- const url = node.record.url;
156
+ const url = node.request.url;
157
157
  // If the URL definitely wasn't render-blocking then we filter it out.
158
158
  if (definitelyNotRenderBlockingScriptUrls.has(url)) {
159
159
  return false;
@@ -41,13 +41,13 @@ class Interactive extends Metric {
41
41
  }
42
42
 
43
43
  // Include all scripts and high priority requests, exclude all images
44
- const isImage = node.record.resourceType === NetworkRequestTypes.Image;
45
- const isScript = node.record.resourceType === NetworkRequestTypes.Script;
44
+ const isImage = node.request.resourceType === NetworkRequestTypes.Image;
45
+ const isScript = node.request.resourceType === NetworkRequestTypes.Script;
46
46
  return (
47
47
  !isImage &&
48
48
  (isScript ||
49
- node.record.priority === 'High' ||
50
- node.record.priority === 'VeryHigh')
49
+ node.request.priority === 'High' ||
50
+ node.request.priority === 'VeryHigh')
51
51
  );
52
52
  });
53
53
  }
@@ -32,8 +32,8 @@ class LargestContentfulPaint extends Metric {
32
32
  */
33
33
  static isNotLowPriorityImageNode(node) {
34
34
  if (node.type !== 'network') return true;
35
- const isImage = node.record.resourceType === 'Image';
36
- const isLowPriority = node.record.priority === 'Low' || node.record.priority === 'VeryLow';
35
+ const isImage = node.request.resourceType === 'Image';
36
+ const isLowPriority = node.request.priority === 'Low' || node.request.priority === 'VeryLow';
37
37
  return !isImage || !isLowPriority;
38
38
  }
39
39
 
@@ -39,6 +39,20 @@ export class PageDependencyGraph {
39
39
  * @param {Node} node
40
40
  */
41
41
  static _pruneNode(node: Node): void;
42
+ /**
43
+ * TODO(15841): remove when CDT backend is gone. until then, this is a useful debugging tool
44
+ * to find delta between using CDP or the trace to create the network requests.
45
+ *
46
+ * When a test fails using the trace backend, I enabled this debug method and copied the network
47
+ * requests when CDP was used, then when trace is used, and diff'd them. This method helped
48
+ * remove non-logical differences from the comparison (order of properties, slight rounding
49
+ * discrepancies, removing object cycles, etc).
50
+ *
51
+ * When using for a unit test, make sure to do `.only` so you are getting what you expect.
52
+ * @param {Lantern.NetworkRequest[]} lanternRequests
53
+ * @return {never}
54
+ */
55
+ static _debugNormalizeRequests(lanternRequests: Lantern.NetworkRequest[]): never;
42
56
  /**
43
57
  * @param {LH.TraceEvent[]} mainThreadEvents
44
58
  * @param {Array<Lantern.NetworkRequest>} networkRecords
@@ -46,6 +60,48 @@ export class PageDependencyGraph {
46
60
  * @return {Node}
47
61
  */
48
62
  static createGraph(mainThreadEvents: LH.TraceEvent[], networkRecords: Array<Lantern.NetworkRequest>, URL: URLArtifact): Node;
63
+ /**
64
+ * @param {Lantern.NetworkRequest} record The record to find the initiator of
65
+ * @param {Map<string, Lantern.NetworkRequest[]>} recordsByURL
66
+ * @return {Lantern.NetworkRequest|null}
67
+ */
68
+ static chooseInitiatorRequest(record: Lantern.NetworkRequest, recordsByURL: Map<string, Lantern.NetworkRequest[]>): Lantern.NetworkRequest | null;
69
+ /**
70
+ * Returns a map of `pid` -> `tid[]`.
71
+ * @param {LH.Trace} trace
72
+ * @return {Map<number, number[]>}
73
+ */
74
+ static _findWorkerThreads(trace: LH.Trace): Map<number, number[]>;
75
+ /**
76
+ * @param {URL|string} url
77
+ */
78
+ static _createParsedUrl(url: URL | string): {
79
+ scheme: string;
80
+ host: string;
81
+ securityOrigin: string;
82
+ };
83
+ /**
84
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
85
+ * @param {Map<number, number[]>} workerThreads
86
+ * @param {import('@paulirish/trace_engine/models/trace/types/TraceEvents.js').SyntheticNetworkRequest} request
87
+ * @return {Lantern.NetworkRequest=}
88
+ */
89
+ static _createLanternRequest(traceEngineResult: LH.Artifacts.TraceEngineResult, workerThreads: Map<number, number[]>, request: import('@paulirish/trace_engine/models/trace/types/TraceEvents.js').SyntheticNetworkRequest): Lantern.NetworkRequest | undefined;
90
+ /**
91
+ *
92
+ * @param {Lantern.NetworkRequest[]} lanternRequests
93
+ */
94
+ static _linkInitiators(lanternRequests: Lantern.NetworkRequest[]): void;
95
+ /**
96
+ * @param {LH.TraceEvent[]} mainThreadEvents
97
+ * @param {LH.Trace} trace
98
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
99
+ * @param {LH.Artifacts.URL} URL
100
+ */
101
+ static createGraphFromTrace(mainThreadEvents: LH.TraceEvent[], trace: LH.Trace, traceEngineResult: LH.Artifacts.TraceEngineResult, URL: LH.Artifacts.URL): Promise<{
102
+ graph: Node;
103
+ records: Lantern.NetworkRequest<any>[];
104
+ }>;
49
105
  /**
50
106
  *
51
107
  * @param {Node} rootNode
@@ -10,6 +10,7 @@ import {NetworkNode} from './network-node.js';
10
10
  import {CPUNode} from './cpu-node.js';
11
11
  import {TraceProcessor} from '../tracehouse/trace-processor.js';
12
12
  import {NetworkAnalyzer} from './simulator/network-analyzer.js';
13
+ import {RESOURCE_TYPES} from '../network-request.js';
13
14
 
14
15
  /** @typedef {import('./base-node.js').Node} Node */
15
16
  /** @typedef {Omit<LH.Artifacts['URL'], 'finalDisplayedUrl'>} URLArtifact */
@@ -75,7 +76,7 @@ class PageDependencyGraph {
75
76
 
76
77
  networkRecords.forEach(record => {
77
78
  if (IGNORED_MIME_TYPES_REGEX.test(record.mimeType)) return;
78
- if (record.sessionTargetType === 'worker') return;
79
+ if (record.fromWorker) return;
79
80
 
80
81
  // Network record requestIds can be duplicated for an unknown reason
81
82
  // Suffix all subsequent records with `:duplicate` until it's unique
@@ -404,6 +405,107 @@ class PageDependencyGraph {
404
405
  }
405
406
  }
406
407
 
408
+ /**
409
+ * TODO(15841): remove when CDT backend is gone. until then, this is a useful debugging tool
410
+ * to find delta between using CDP or the trace to create the network requests.
411
+ *
412
+ * When a test fails using the trace backend, I enabled this debug method and copied the network
413
+ * requests when CDP was used, then when trace is used, and diff'd them. This method helped
414
+ * remove non-logical differences from the comparison (order of properties, slight rounding
415
+ * discrepancies, removing object cycles, etc).
416
+ *
417
+ * When using for a unit test, make sure to do `.only` so you are getting what you expect.
418
+ * @param {Lantern.NetworkRequest[]} lanternRequests
419
+ * @return {never}
420
+ */
421
+ static _debugNormalizeRequests(lanternRequests) {
422
+ for (const request of lanternRequests) {
423
+ request.rendererStartTime = Math.round(request.rendererStartTime * 1000) / 1000;
424
+ request.networkRequestTime = Math.round(request.networkRequestTime * 1000) / 1000;
425
+ request.responseHeadersEndTime = Math.round(request.responseHeadersEndTime * 1000) / 1000;
426
+ request.networkEndTime = Math.round(request.networkEndTime * 1000) / 1000;
427
+ }
428
+
429
+ for (const r of lanternRequests) {
430
+ delete r.record;
431
+ if (r.initiatorRequest) {
432
+ // @ts-expect-error
433
+ r.initiatorRequest = {id: r.initiatorRequest.requestId};
434
+ }
435
+ if (r.redirectDestination) {
436
+ // @ts-expect-error
437
+ r.redirectDestination = {id: r.redirectDestination.requestId};
438
+ }
439
+ if (r.redirectSource) {
440
+ // @ts-expect-error
441
+ r.redirectSource = {id: r.redirectSource.requestId};
442
+ }
443
+ if (r.redirects) {
444
+ // @ts-expect-error
445
+ r.redirects = r.redirects.map(r2 => r2.requestId);
446
+ }
447
+ }
448
+ /** @type {Lantern.NetworkRequest[]} */
449
+ const requests = lanternRequests.map(r => ({
450
+ requestId: r.requestId,
451
+ connectionId: r.connectionId,
452
+ connectionReused: r.connectionReused,
453
+ url: r.url,
454
+ protocol: r.protocol,
455
+ parsedURL: r.parsedURL,
456
+ documentURL: r.documentURL,
457
+ rendererStartTime: r.rendererStartTime,
458
+ networkRequestTime: r.networkRequestTime,
459
+ responseHeadersEndTime: r.responseHeadersEndTime,
460
+ networkEndTime: r.networkEndTime,
461
+ transferSize: r.transferSize,
462
+ resourceSize: r.resourceSize,
463
+ fromDiskCache: r.fromDiskCache,
464
+ fromMemoryCache: r.fromMemoryCache,
465
+ finished: r.finished,
466
+ statusCode: r.statusCode,
467
+ redirectSource: r.redirectSource,
468
+ redirectDestination: r.redirectDestination,
469
+ redirects: r.redirects,
470
+ failed: r.failed,
471
+ initiator: r.initiator,
472
+ timing: r.timing ? {
473
+ requestTime: r.timing.requestTime,
474
+ proxyStart: r.timing.proxyStart,
475
+ proxyEnd: r.timing.proxyEnd,
476
+ dnsStart: r.timing.dnsStart,
477
+ dnsEnd: r.timing.dnsEnd,
478
+ connectStart: r.timing.connectStart,
479
+ connectEnd: r.timing.connectEnd,
480
+ sslStart: r.timing.sslStart,
481
+ sslEnd: r.timing.sslEnd,
482
+ workerStart: r.timing.workerStart,
483
+ workerReady: r.timing.workerReady,
484
+ workerFetchStart: r.timing.workerFetchStart,
485
+ workerRespondWithSettled: r.timing.workerRespondWithSettled,
486
+ sendStart: r.timing.sendStart,
487
+ sendEnd: r.timing.sendEnd,
488
+ pushStart: r.timing.pushStart,
489
+ pushEnd: r.timing.pushEnd,
490
+ receiveHeadersStart: r.timing.receiveHeadersStart,
491
+ receiveHeadersEnd: r.timing.receiveHeadersEnd,
492
+ } : r.timing,
493
+ resourceType: r.resourceType,
494
+ mimeType: r.mimeType,
495
+ priority: r.priority,
496
+ initiatorRequest: r.initiatorRequest,
497
+ frameId: r.frameId,
498
+ fromWorker: r.fromWorker,
499
+ isLinkPreload: r.isLinkPreload,
500
+ serverResponseTime: r.serverResponseTime,
501
+ })).filter(r => !r.fromWorker);
502
+ // eslint-disable-next-line no-unused-vars
503
+ const debug = requests;
504
+ // Set breakpoint here.
505
+ // Copy `debug` and compare with https://www.diffchecker.com/text-compare/
506
+ process.exit(1);
507
+ }
508
+
407
509
  /**
408
510
  * @param {LH.TraceEvent[]} mainThreadEvents
409
511
  * @param {Array<Lantern.NetworkRequest>} networkRecords
@@ -411,6 +513,8 @@ class PageDependencyGraph {
411
513
  * @return {Node}
412
514
  */
413
515
  static createGraph(mainThreadEvents, networkRecords, URL) {
516
+ // This is for debugging trace/devtoolslog network records.
517
+ // const debug = PageDependencyGraph._debugNormalizeRequests(networkRecords);
414
518
  const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
415
519
  const cpuNodes = PageDependencyGraph.getCPUNodes(mainThreadEvents);
416
520
  const {requestedUrl, mainDocumentUrl} = URL;
@@ -421,7 +525,6 @@ class PageDependencyGraph {
421
525
  if (!rootRequest) throw new Error('rootRequest not found');
422
526
  const rootNode = networkNodeOutput.idToNodeMap.get(rootRequest.requestId);
423
527
  if (!rootNode) throw new Error('rootNode not found');
424
-
425
528
  const mainDocumentRequest =
426
529
  NetworkAnalyzer.findLastDocumentForUrl(networkRecords, mainDocumentUrl);
427
530
  if (!mainDocumentRequest) throw new Error('mainDocumentRequest not found');
@@ -439,6 +542,314 @@ class PageDependencyGraph {
439
542
  return rootNode;
440
543
  }
441
544
 
545
+ /**
546
+ * @param {Lantern.NetworkRequest} record The record to find the initiator of
547
+ * @param {Map<string, Lantern.NetworkRequest[]>} recordsByURL
548
+ * @return {Lantern.NetworkRequest|null}
549
+ */
550
+ static chooseInitiatorRequest(record, recordsByURL) {
551
+ if (record.redirectSource) {
552
+ return record.redirectSource;
553
+ }
554
+
555
+ const initiatorURL = PageDependencyGraph.getNetworkInitiators(record)[0];
556
+ let candidates = recordsByURL.get(initiatorURL) || [];
557
+ // The (valid) initiator must come before the initiated request.
558
+ candidates = candidates.filter(c => {
559
+ return c.responseHeadersEndTime <= record.rendererStartTime &&
560
+ c.finished && !c.failed;
561
+ });
562
+ if (candidates.length > 1) {
563
+ // Disambiguate based on prefetch. Prefetch requests have type 'Other' and cannot
564
+ // initiate requests, so we drop them here.
565
+ const nonPrefetchCandidates = candidates.filter(
566
+ cand => cand.resourceType !== RESOURCE_TYPES.Other);
567
+ if (nonPrefetchCandidates.length) {
568
+ candidates = nonPrefetchCandidates;
569
+ }
570
+ }
571
+ if (candidates.length > 1) {
572
+ // Disambiguate based on frame. It's likely that the initiator comes from the same frame.
573
+ const sameFrameCandidates = candidates.filter(cand => cand.frameId === record.frameId);
574
+ if (sameFrameCandidates.length) {
575
+ candidates = sameFrameCandidates;
576
+ }
577
+ }
578
+ if (candidates.length > 1 && record.initiator.type === 'parser') {
579
+ // Filter to just Documents when initiator type is parser.
580
+ const documentCandidates = candidates.filter(cand =>
581
+ cand.resourceType === RESOURCE_TYPES.Document);
582
+ if (documentCandidates.length) {
583
+ candidates = documentCandidates;
584
+ }
585
+ }
586
+ if (candidates.length > 1) {
587
+ // If all real loads came from successful preloads (url preloaded and
588
+ // loads came from the cache), filter to link rel=preload request(s).
589
+ const linkPreloadCandidates = candidates.filter(c => c.isLinkPreload);
590
+ if (linkPreloadCandidates.length) {
591
+ const nonPreloadCandidates = candidates.filter(c => !c.isLinkPreload);
592
+ const allPreloaded = nonPreloadCandidates.every(c => c.fromDiskCache || c.fromMemoryCache);
593
+ if (nonPreloadCandidates.length && allPreloaded) {
594
+ candidates = linkPreloadCandidates;
595
+ }
596
+ }
597
+ }
598
+
599
+ // Only return an initiator if the result is unambiguous.
600
+ return candidates.length === 1 ? candidates[0] : null;
601
+ }
602
+
603
+ /**
604
+ * Returns a map of `pid` -> `tid[]`.
605
+ * @param {LH.Trace} trace
606
+ * @return {Map<number, number[]>}
607
+ */
608
+ static _findWorkerThreads(trace) {
609
+ // TODO: WorkersHandler in TraceEngine needs to be updated to also include `pid` (only had `tid`).
610
+ const workerThreads = new Map();
611
+ const workerCreationEvents = ['ServiceWorker thread', 'DedicatedWorker thread'];
612
+
613
+ for (const event of trace.traceEvents) {
614
+ if (event.name !== 'thread_name' || !event.args.name) {
615
+ continue;
616
+ }
617
+ if (!workerCreationEvents.includes(event.args.name)) {
618
+ continue;
619
+ }
620
+
621
+ const tids = workerThreads.get(event.pid);
622
+ if (tids) {
623
+ tids.push(event.tid);
624
+ } else {
625
+ workerThreads.set(event.pid, [event.tid]);
626
+ }
627
+ }
628
+
629
+ return workerThreads;
630
+ }
631
+
632
+ /**
633
+ * @param {URL|string} url
634
+ */
635
+ static _createParsedUrl(url) {
636
+ if (typeof url === 'string') {
637
+ url = new URL(url);
638
+ }
639
+ return {
640
+ scheme: url.protocol.split(':')[0],
641
+ // Intentional, DevTools uses different terminology
642
+ host: url.hostname,
643
+ securityOrigin: url.origin,
644
+ };
645
+ }
646
+
647
+ /**
648
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
649
+ * @param {Map<number, number[]>} workerThreads
650
+ * @param {import('@paulirish/trace_engine/models/trace/types/TraceEvents.js').SyntheticNetworkRequest} request
651
+ * @return {Lantern.NetworkRequest=}
652
+ */
653
+ static _createLanternRequest(traceEngineResult, workerThreads, request) {
654
+ if (request.args.data.connectionId === undefined ||
655
+ request.args.data.connectionReused === undefined) {
656
+ throw new Error('Trace is too old');
657
+ }
658
+
659
+ let url;
660
+ try {
661
+ url = new URL(request.args.data.url);
662
+ } catch (e) {
663
+ return;
664
+ }
665
+
666
+ const timing = request.args.data.timing ? {
667
+ // These two timings are not included in the trace.
668
+ workerFetchStart: -1,
669
+ workerRespondWithSettled: -1,
670
+ ...request.args.data.timing,
671
+ } : undefined;
672
+
673
+ const networkRequestTime = timing ?
674
+ timing.requestTime * 1000 :
675
+ request.args.data.syntheticData.downloadStart / 1000;
676
+
677
+ let fromWorker = false;
678
+ const tids = workerThreads.get(request.pid);
679
+ if (tids?.includes(request.tid)) {
680
+ fromWorker = true;
681
+ }
682
+
683
+ // TraceEngine collects worker thread ids in a different manner than `workerThreads` does.
684
+ // AFAIK these should be equivalent, but in case they are not let's also check this for now.
685
+ if (traceEngineResult.data.Workers.workerIdByThread.has(request.tid)) {
686
+ fromWorker = true;
687
+ }
688
+
689
+ // `initiator` in the trace does not contain the stack trace for JS-initiated
690
+ // requests. Instead, that is stored in the `stackTrace` property of the SyntheticNetworkRequest.
691
+ // There are some minor differences in the fields, accounted for here.
692
+ // Most importantly, there seems to be fewer frames in the trace than the equivalent
693
+ // events over the CDP. This results in less accuracy in determining the initiator request,
694
+ // which means less edges in the graph, which mean worse results.
695
+ // TODO: Should fix in Chromium.
696
+ /** @type {Lantern.NetworkRequest['initiator']} */
697
+ const initiator = request.args.data.initiator ?? {type: 'other'};
698
+ if (request.args.data.stackTrace) {
699
+ const callFrames = request.args.data.stackTrace.map(f => {
700
+ return {
701
+ scriptId: String(f.scriptId),
702
+ url: f.url,
703
+ lineNumber: f.lineNumber - 1,
704
+ columnNumber: f.columnNumber - 1,
705
+ functionName: f.functionName,
706
+ };
707
+ });
708
+ initiator.stack = {callFrames};
709
+ }
710
+
711
+ let resourceType = request.args.data.resourceType;
712
+ if (request.args.data.initiator?.fetchType === 'xmlhttprequest') {
713
+ // @ts-expect-error yes XHR is a valid ResourceType. TypeScript const enums are so unhelpful.
714
+ resourceType = 'XHR';
715
+ }
716
+
717
+ return {
718
+ requestId: request.args.data.requestId,
719
+ connectionId: request.args.data.connectionId,
720
+ connectionReused: request.args.data.connectionReused,
721
+ url: request.args.data.url,
722
+ protocol: request.args.data.protocol,
723
+ parsedURL: this._createParsedUrl(url),
724
+ documentURL: request.args.data.requestingFrameUrl,
725
+ rendererStartTime: request.ts / 1000,
726
+ networkRequestTime,
727
+ responseHeadersEndTime: request.args.data.syntheticData.downloadStart / 1000,
728
+ networkEndTime: request.args.data.syntheticData.finishTime / 1000,
729
+ transferSize: request.args.data.encodedDataLength,
730
+ resourceSize: request.args.data.decodedBodyLength,
731
+ fromDiskCache: request.args.data.syntheticData.isDiskCached,
732
+ fromMemoryCache: request.args.data.syntheticData.isMemoryCached,
733
+ isLinkPreload: request.args.data.isLinkPreload,
734
+ finished: request.args.data.finished,
735
+ failed: request.args.data.failed,
736
+ statusCode: request.args.data.statusCode,
737
+ initiator,
738
+ timing,
739
+ resourceType,
740
+ mimeType: request.args.data.mimeType,
741
+ priority: request.args.data.priority,
742
+ frameId: request.args.data.frame,
743
+ fromWorker,
744
+ record: request,
745
+ // Set later.
746
+ redirects: undefined,
747
+ redirectSource: undefined,
748
+ redirectDestination: undefined,
749
+ initiatorRequest: undefined,
750
+ };
751
+ }
752
+
753
+ /**
754
+ *
755
+ * @param {Lantern.NetworkRequest[]} lanternRequests
756
+ */
757
+ static _linkInitiators(lanternRequests) {
758
+ /** @type {Map<string, Lantern.NetworkRequest[]>} */
759
+ const requestsByURL = new Map();
760
+ for (const request of lanternRequests) {
761
+ const requests = requestsByURL.get(request.url) || [];
762
+ requests.push(request);
763
+ requestsByURL.set(request.url, requests);
764
+ }
765
+
766
+ for (const request of lanternRequests) {
767
+ const initiatorRequest = PageDependencyGraph.chooseInitiatorRequest(request, requestsByURL);
768
+ if (initiatorRequest) {
769
+ request.initiatorRequest = initiatorRequest;
770
+ }
771
+ }
772
+ }
773
+
774
+ /**
775
+ * @param {LH.TraceEvent[]} mainThreadEvents
776
+ * @param {LH.Trace} trace
777
+ * @param {LH.Artifacts.TraceEngineResult} traceEngineResult
778
+ * @param {LH.Artifacts.URL} URL
779
+ */
780
+ static async createGraphFromTrace(mainThreadEvents, trace, traceEngineResult, URL) {
781
+ const workerThreads = this._findWorkerThreads(trace);
782
+
783
+ /** @type {Lantern.NetworkRequest[]} */
784
+ const lanternRequests = [];
785
+ for (const request of traceEngineResult.data.NetworkRequests.byTime) {
786
+ const lanternRequest = this._createLanternRequest(traceEngineResult, workerThreads, request);
787
+ if (lanternRequest) {
788
+ lanternRequests.push(lanternRequest);
789
+ }
790
+ }
791
+
792
+ // TraceEngine consolidates all redirects into a single request object, but lantern needs
793
+ // an entry for each redirected request.
794
+ for (const request of [...lanternRequests]) {
795
+ if (!request.record) continue;
796
+
797
+ const redirects = request.record.args.data.redirects;
798
+ if (!redirects.length) continue;
799
+
800
+ const requestChain = [];
801
+ for (const redirect of redirects) {
802
+ const redirectedRequest = structuredClone(request);
803
+ if (redirectedRequest.timing) {
804
+ // TODO: These are surely wrong for when there is more than one redirect. Would be
805
+ // simpler if each redirect remembered it's `timing` object in this `redirects` array.
806
+ redirectedRequest.timing.requestTime = redirect.ts / 1000 / 1000;
807
+ redirectedRequest.timing.receiveHeadersStart -= redirect.dur / 1000 / 1000;
808
+ redirectedRequest.timing.receiveHeadersEnd -= redirect.dur / 1000 / 1000;
809
+ redirectedRequest.rendererStartTime = redirect.ts / 1000;
810
+ redirectedRequest.networkRequestTime = redirect.ts / 1000;
811
+ redirectedRequest.networkEndTime = (redirect.ts + redirect.dur) / 1000;
812
+ redirectedRequest.responseHeadersEndTime =
813
+ redirectedRequest.timing.requestTime * 1000 +
814
+ redirectedRequest.timing.receiveHeadersEnd;
815
+ }
816
+ redirectedRequest.url = redirect.url;
817
+ redirectedRequest.parsedURL = this._createParsedUrl(redirect.url);
818
+ // TODO: TraceEngine is not retaining the actual status code.
819
+ redirectedRequest.statusCode = 302;
820
+ requestChain.push(redirectedRequest);
821
+ lanternRequests.push(redirectedRequest);
822
+ }
823
+ requestChain.push(request);
824
+
825
+ for (let i = 0; i < requestChain.length; i++) {
826
+ const request = requestChain[i];
827
+ if (i > 0) {
828
+ request.redirectSource = requestChain[i - 1];
829
+ request.redirects = requestChain.slice(0, i);
830
+ }
831
+ if (i !== requestChain.length - 1) {
832
+ request.redirectDestination = requestChain[i + 1];
833
+ }
834
+ }
835
+
836
+ // Apply the `:redirect` requestId convention: only redirects[0].requestId is the actual
837
+ // requestId, all the rest have n occurences of `:redirect` as a suffix.
838
+ for (let i = 1; i < requestChain.length; i++) {
839
+ requestChain[i].requestId = `${requestChain[i - 1].requestId}:redirect`;
840
+ }
841
+ }
842
+
843
+ this._linkInitiators(lanternRequests);
844
+
845
+ // This would already be sorted by rendererStartTime, if not for the redirect unwrapping done
846
+ // above.
847
+ lanternRequests.sort((a, b) => a.rendererStartTime - b.rendererStartTime);
848
+
849
+ const graph = PageDependencyGraph.createGraph(mainThreadEvents, lanternRequests, URL);
850
+ return {graph, records: lanternRequests};
851
+ }
852
+
442
853
  /**
443
854
  *
444
855
  * @param {Node} rootNode
@@ -10,7 +10,7 @@ export class Simulator<T = any> {
10
10
  /**
11
11
  * @param {Lantern.Simulation.Settings} settings
12
12
  */
13
- static createSimulator(settings: Lantern.Simulation.Settings): Promise<Simulator<any>>;
13
+ static createSimulator(settings: Lantern.Simulation.Settings): Simulator<any>;
14
14
  /** @return {Map<string, Map<Node, CompleteNodeTiming>>} */
15
15
  static get ALL_NODE_TIMINGS(): Map<string, Map<Node, CompleteNodeTiming>>;
16
16
  /**
@@ -53,7 +53,7 @@ class Simulator {
53
53
  /**
54
54
  * @param {Lantern.Simulation.Settings} settings
55
55
  */
56
- static async createSimulator(settings) {
56
+ static createSimulator(settings) {
57
57
  const {throttlingMethod, throttling, precomputedLanternData, networkAnalysis} = settings;
58
58
 
59
59
  /** @type {Lantern.Simulation.Options} */
@@ -67,11 +67,14 @@ export class NetworkRequest<T = any> {
67
67
  resourceSize: number;
68
68
  fromDiskCache: boolean;
69
69
  fromMemoryCache: boolean;
70
+ isLinkPreload: boolean;
70
71
  finished: boolean;
72
+ failed: boolean;
71
73
  statusCode: number;
74
+ /** The network request that redirected to this one */
75
+ redirectSource: NetworkRequest<T> | undefined;
72
76
  /** The network request that this one redirected to */
73
77
  redirectDestination: NetworkRequest<T> | undefined;
74
- failed: boolean;
75
78
  initiator: LH.Crdp.Network.Initiator;
76
79
  initiatorRequest: NetworkRequest<T> | undefined;
77
80
  /** The chain of network requests that redirected to this one */
@@ -81,12 +84,12 @@ export class NetworkRequest<T = any> {
81
84
  * Optional value for how long the server took to respond to this request.
82
85
  * When not provided, the server response time is derived from the timing object.
83
86
  */
84
- serverResponseTime: number | undefined;
87
+ serverResponseTime?: number;
85
88
  resourceType: LH.Crdp.Network.ResourceType | undefined;
86
89
  mimeType: string;
87
90
  priority: LH.Crdp.Network.ResourcePriority;
88
91
  frameId: string | undefined;
89
- sessionTargetType: LH.Protocol.TargetType | undefined;
92
+ fromWorker: boolean;
90
93
  }
91
94
 
92
95
  export namespace Simulation {
@@ -149,6 +149,7 @@ export class NetworkRequest {
149
149
  sessionId: string | undefined;
150
150
  /** @type {LH.Protocol.TargetType|undefined} */
151
151
  sessionTargetType: LH.Protocol.TargetType | undefined;
152
+ fromWorker: boolean;
152
153
  isLinkPreload: boolean;
153
154
  /**
154
155
  * @return {boolean}
@@ -258,6 +259,26 @@ export namespace NetworkRequest {
258
259
  export { HEADER_FETCHED_SIZE };
259
260
  export { HEADER_PROTOCOL_IS_H2 };
260
261
  }
262
+ /**
263
+ * @typedef HeaderEntry
264
+ * @property {string} name
265
+ * @property {string} value
266
+ */
267
+ /**
268
+ * @typedef ParsedURL
269
+ * @property {string} scheme Equivalent to a `new URL(url).protocol` BUT w/o the trailing colon (:)
270
+ * @property {string} host Equivalent to a `new URL(url).hostname`
271
+ * @property {string} securityOrigin
272
+ */
273
+ /**
274
+ * @typedef LightriderStatistics
275
+ * @property {number} endTimeDeltaMs The difference in networkEndTime between the observed Lighthouse networkEndTime and Lightrider's derived networkEndTime.
276
+ * @property {number} TCPMs The time spent making a TCP connection (connect + SSL). Note: this is poorly named.
277
+ * @property {number} requestMs The time spent requesting a resource from a remote server, we use this to approx RTT. Note: this is poorly names, it really should be "server response time".
278
+ * @property {number} responseMs Time to receive the entire response payload starting the clock on receiving the first fragment (first non-header byte).
279
+ */
280
+ /** @type {LH.Util.SelfMap<LH.Crdp.Network.ResourceType>} */
281
+ export const RESOURCE_TYPES: LH.Util.SelfMap<LH.Crdp.Network.ResourceType>;
261
282
  import * as LH from '../../types/lh.js';
262
283
  import * as Lantern from './lantern/types/lantern.js';
263
284
  declare const HEADER_TCP: "X-TCPMs";
@@ -180,6 +180,7 @@ class NetworkRequest {
180
180
  this.sessionId = undefined;
181
181
  /** @type {LH.Protocol.TargetType|undefined} */
182
182
  this.sessionTargetType = undefined;
183
+ this.fromWorker = false;
183
184
  this.isLinkPreload = false;
184
185
  }
185
186
 
@@ -600,6 +601,8 @@ class NetworkRequest {
600
601
  }
601
602
  }
602
603
 
604
+ record.fromWorker = record.sessionTargetType === 'worker';
605
+
603
606
  return {
604
607
  ...record,
605
608
  timing,
@@ -691,4 +694,4 @@ NetworkRequest.HEADER_TOTAL = HEADER_TOTAL;
691
694
  NetworkRequest.HEADER_FETCHED_SIZE = HEADER_FETCHED_SIZE;
692
695
  NetworkRequest.HEADER_PROTOCOL_IS_H2 = HEADER_PROTOCOL_IS_H2;
693
696
 
694
- export {NetworkRequest};
697
+ export {NetworkRequest, RESOURCE_TYPES};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "12.0.0-dev.20240602",
4
+ "version": "12.0.0-dev.20240603",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -51,17 +51,19 @@
51
51
  "test-proto": "yarn compile-proto && yarn build-proto-roundtrip",
52
52
  "unit-core": "yarn mocha core",
53
53
  "unit-cli": "yarn mocha --testMatch cli/**/*-test.js",
54
+ "unit-lantern-trace": "INTERNAL_LANTERN_USE_TRACE=1 yarn mocha core/test/computed/metrics core/test/audits",
54
55
  "unit-report": "yarn mocha --testMatch report/**/*-test.js",
55
56
  "unit-treemap": "yarn mocha --testMatch treemap/**/*-test.js",
56
57
  "unit-viewer": "yarn mocha --testMatch viewer/**/*-test.js",
57
58
  "unit-flow": "bash flow-report/test/run-flow-report-tests.sh",
58
- "unit": "yarn unit-flow && yarn mocha",
59
+ "unit": "yarn unit-flow && yarn mocha && yarn unit-lantern-trace",
59
60
  "unit:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit",
61
+ "unit-lantern-trace:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit-lantern-trace",
60
62
  "core-unit": "yarn unit-core",
61
63
  "cli-unit": "yarn unit-cli",
62
64
  "viewer-unit": "yarn unit-viewer",
63
65
  "watch": "yarn unit-core --watch",
64
- "unit:cicoverage": "yarn c8 --all yarn unit:ci",
66
+ "unit:cicoverage": "yarn c8 --all yarn unit:ci && yarn c8 --all yarn unit-lantern-trace:ci",
65
67
  "coverage": "yarn unit:cicoverage && c8 report --reporter html",
66
68
  "coverage:smoke": "yarn c8 yarn smoke -j=1 && c8 report --reporter html",
67
69
  "devtools": "bash core/scripts/roll-to-devtools.sh",
@@ -179,7 +181,7 @@
179
181
  "webtreemap-cdt": "^3.2.1"
180
182
  },
181
183
  "dependencies": {
182
- "@paulirish/trace_engine": "^0.0.19",
184
+ "@paulirish/trace_engine": "^0.0.23",
183
185
  "@sentry/node": "^6.17.4",
184
186
  "axe-core": "^4.9.0",
185
187
  "chrome-launcher": "^1.1.1",
package/tsconfig.json CHANGED
@@ -109,5 +109,6 @@
109
109
  "core/test/computed/metrics/interactive-test.js",
110
110
  "core/test/computed/tbt-impact-tasks-test.js",
111
111
  "core/test/fixtures/config-plugins/lighthouse-plugin-simple/plugin-simple.js",
112
+ "core/test/lib/lantern/metrics/metric-test-utils.js",
112
113
  ],
113
114
  }
@@ -942,6 +942,22 @@ export interface TraceEvent {
942
942
  name?: string;
943
943
  duration?: number;
944
944
  blockingDuration?: number;
945
+ candidateIndex?: number;
946
+ priority?: string;
947
+ requestMethod?: string;
948
+ resourceType?: string;
949
+ fromCache?: boolean;
950
+ fromServiceWorker?: boolean;
951
+ mimeType?: string;
952
+ statusCode?: number;
953
+ timing?: any;
954
+ connectionId?: number;
955
+ connectionReused?: boolean;
956
+ encodedDataLength?: number;
957
+ decodedBodyLength?: number;
958
+ initiator?: {type: string, url?: string, stack?: any};
959
+ protocol?: string;
960
+ finishTime?: number;
945
961
  };
946
962
  frame?: string;
947
963
  name?: string;