lighthouse 11.7.1 → 12.0.0

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.
Files changed (272) hide show
  1. package/CONTRIBUTING.md +2 -2
  2. package/cli/bin.js +0 -7
  3. package/cli/cli-flags.d.ts +1 -5
  4. package/cli/cli-flags.js +4 -8
  5. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +14 -14
  6. package/cli/test/smokehouse/config/exclusions.js +3 -0
  7. package/cli/test/smokehouse/core-tests.js +6 -20
  8. package/cli/test/smokehouse/frontends/smokehouse-bin.js +2 -2
  9. package/core/audits/accessibility/aria-conditional-attr.d.ts +10 -0
  10. package/core/audits/accessibility/aria-conditional-attr.js +43 -0
  11. package/core/audits/accessibility/{duplicate-id-active.d.ts → aria-deprecated-role.d.ts} +3 -3
  12. package/core/audits/accessibility/aria-deprecated-role.js +43 -0
  13. package/core/audits/accessibility/aria-prohibited-attr.d.ts +10 -0
  14. package/core/audits/accessibility/aria-prohibited-attr.js +44 -0
  15. package/core/audits/accessibility/target-size.js +0 -1
  16. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +0 -16
  17. package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -38
  18. package/core/audits/byte-efficiency/offscreen-images.d.ts +0 -11
  19. package/core/audits/byte-efficiency/offscreen-images.js +0 -15
  20. package/core/audits/byte-efficiency/render-blocking-resources.js +27 -26
  21. package/core/audits/byte-efficiency/unminified-css.js +2 -2
  22. package/core/audits/byte-efficiency/unused-css-rules.js +3 -1
  23. package/core/audits/dobetterweb/uses-http2.d.ts +0 -13
  24. package/core/audits/dobetterweb/uses-http2.js +4 -38
  25. package/core/audits/font-display.js +2 -2
  26. package/core/audits/layout-shifts.js +1 -1
  27. package/core/audits/preload-fonts.d.ts +1 -5
  28. package/core/audits/preload-fonts.js +1 -10
  29. package/core/audits/prioritize-lcp-image.js +2 -2
  30. package/core/audits/redirects-http.d.ts +19 -0
  31. package/core/audits/redirects-http.js +75 -0
  32. package/core/audits/resource-summary.d.ts +2 -0
  33. package/core/audits/resource-summary.js +5 -4
  34. package/core/audits/seo/is-crawlable.d.ts +2 -1
  35. package/core/audits/seo/link-text.js +12 -0
  36. package/core/audits/uses-rel-preload.d.ts +1 -5
  37. package/core/audits/uses-rel-preload.js +5 -15
  38. package/core/audits/work-during-interaction.d.ts +2 -2
  39. package/core/audits/work-during-interaction.js +2 -2
  40. package/core/computed/js-bundles.d.ts +1 -1
  41. package/core/computed/metrics/lantern-first-contentful-paint.d.ts +13 -82
  42. package/core/computed/metrics/lantern-first-contentful-paint.js +16 -178
  43. package/core/computed/metrics/lantern-first-meaningful-paint.d.ts +13 -12
  44. package/core/computed/metrics/lantern-first-meaningful-paint.js +11 -49
  45. package/core/computed/metrics/lantern-interactive.d.ts +13 -21
  46. package/core/computed/metrics/lantern-interactive.js +12 -84
  47. package/core/computed/metrics/lantern-largest-contentful-paint.d.ts +13 -25
  48. package/core/computed/metrics/lantern-largest-contentful-paint.js +13 -80
  49. package/core/computed/metrics/lantern-max-potential-fid.d.ts +13 -24
  50. package/core/computed/metrics/lantern-max-potential-fid.js +12 -64
  51. package/core/computed/metrics/lantern-metric.d.ts +14 -18
  52. package/core/computed/metrics/lantern-metric.js +34 -30
  53. package/core/computed/metrics/lantern-speed-index.d.ts +13 -32
  54. package/core/computed/metrics/lantern-speed-index.js +13 -116
  55. package/core/computed/metrics/lantern-total-blocking-time.d.ts +13 -25
  56. package/core/computed/metrics/lantern-total-blocking-time.js +10 -94
  57. package/core/computed/metrics/metric.js +1 -3
  58. package/core/computed/module-duplication.d.ts +1 -1
  59. package/core/computed/navigation-insights.d.ts +36 -0
  60. package/core/computed/navigation-insights.js +35 -0
  61. package/core/computed/page-dependency-graph.js +3 -7
  62. package/core/computed/resource-summary.d.ts +10 -12
  63. package/core/computed/resource-summary.js +12 -19
  64. package/core/computed/trace-engine-result.d.ts +16 -3
  65. package/core/computed/trace-engine-result.js +19 -14
  66. package/core/computed/unused-css.d.ts +3 -1
  67. package/core/computed/unused-css.js +6 -5
  68. package/core/config/config-helpers.js +0 -4
  69. package/core/config/constants.js +0 -1
  70. package/core/config/default-config.js +63 -142
  71. package/core/config/experimental-config.js +11 -0
  72. package/core/config/filters.js +0 -1
  73. package/core/gather/driver/navigation.js +4 -1
  74. package/core/gather/driver/prepare.js +1 -1
  75. package/core/gather/driver/target-manager.js +4 -3
  76. package/core/gather/driver/wait-for-condition.js +2 -3
  77. package/core/gather/driver.d.ts +12 -0
  78. package/core/gather/driver.js +27 -0
  79. package/core/gather/gatherers/accessibility.js +4 -6
  80. package/core/gather/gatherers/css-usage.d.ts +0 -21
  81. package/core/gather/gatherers/css-usage.js +9 -119
  82. package/core/gather/gatherers/root-causes.d.ts +2 -2
  83. package/core/gather/gatherers/root-causes.js +6 -5
  84. package/core/gather/gatherers/stylesheets.d.ts +26 -0
  85. package/core/gather/gatherers/stylesheets.js +138 -0
  86. package/core/gather/gatherers/trace-elements.d.ts +2 -12
  87. package/core/gather/gatherers/trace-elements.js +2 -21
  88. package/core/gather/navigation-runner.js +3 -1
  89. package/core/gather/session.d.ts +8 -0
  90. package/core/gather/session.js +14 -1
  91. package/core/lib/emulation.js +2 -2
  92. package/core/lib/i18n/i18n.d.ts +0 -1
  93. package/core/lib/i18n/i18n.js +0 -2
  94. package/core/lib/lantern/cpu-node.d.ts +7 -1
  95. package/core/lib/lantern/cpu-node.js +12 -2
  96. package/core/lib/lantern/lantern-error.d.ts +8 -0
  97. package/core/lib/lantern/lantern-error.js +9 -0
  98. package/core/lib/lantern/metric.js +2 -6
  99. package/core/lib/lantern/metrics/first-contentful-paint.d.ts +79 -0
  100. package/core/lib/lantern/metrics/first-contentful-paint.js +200 -0
  101. package/core/lib/lantern/metrics/first-meaningful-paint.d.ts +6 -0
  102. package/core/lib/lantern/metrics/first-meaningful-paint.js +64 -0
  103. package/core/lib/lantern/metrics/interactive.d.ts +26 -0
  104. package/core/lib/lantern/metrics/interactive.js +112 -0
  105. package/core/lib/lantern/metrics/largest-contentful-paint.d.ts +19 -0
  106. package/core/lib/lantern/metrics/largest-contentful-paint.js +108 -0
  107. package/core/lib/lantern/metrics/max-potential-fid.d.ts +30 -0
  108. package/core/lib/lantern/metrics/max-potential-fid.js +92 -0
  109. package/core/lib/lantern/metrics/speed-index.d.ts +38 -0
  110. package/core/lib/lantern/metrics/speed-index.js +144 -0
  111. package/core/lib/lantern/metrics/total-blocking-time.d.ts +31 -0
  112. package/core/lib/lantern/metrics/total-blocking-time.js +128 -0
  113. package/core/lib/lantern/network-node.js +28 -4
  114. package/core/lib/lantern/page-dependency-graph.d.ts +4 -4
  115. package/core/lib/lantern/page-dependency-graph.js +19 -7
  116. package/core/lib/lantern/simulator/connection-pool.d.ts +2 -12
  117. package/core/lib/lantern/simulator/connection-pool.js +3 -22
  118. package/core/lib/lantern/simulator/network-analyzer.js +1 -12
  119. package/core/lib/lantern/simulator/simulator.d.ts +2 -4
  120. package/core/lib/lantern/simulator/simulator.js +7 -14
  121. package/core/lib/lantern/types/lantern.d.ts +5 -3
  122. package/core/lib/lantern-trace-saver.js +0 -1
  123. package/core/lib/lh-error.d.ts +9 -0
  124. package/core/lib/lh-error.js +13 -3
  125. package/core/lib/lighthouse-compatibility.js +53 -10
  126. package/core/lib/navigation-error.js +3 -0
  127. package/core/lib/network-request.d.ts +1 -0
  128. package/core/lib/network-request.js +28 -0
  129. package/core/runner.js +18 -25
  130. package/dist/report/bundle.esm.js +11 -112
  131. package/dist/report/flow.js +16 -117
  132. package/dist/report/standalone.js +12 -113
  133. package/flow-report/src/help-dialog.tsx +0 -1
  134. package/flow-report/src/i18n/i18n.d.ts +0 -4
  135. package/flow-report/src/i18n/ui-strings.d.ts +0 -1
  136. package/flow-report/src/i18n/ui-strings.js +0 -2
  137. package/flow-report/src/summary/category.tsx +10 -12
  138. package/package.json +5 -5
  139. package/readme.md +4 -4
  140. package/report/assets/styles.css +1 -60
  141. package/report/assets/templates.html +0 -95
  142. package/report/renderer/category-renderer.js +1 -1
  143. package/report/renderer/components.d.ts +2 -2
  144. package/report/renderer/components.js +2 -103
  145. package/report/renderer/dom.d.ts +1 -1
  146. package/report/renderer/dom.js +1 -1
  147. package/report/renderer/performance-category-renderer.d.ts +0 -7
  148. package/report/renderer/performance-category-renderer.js +1 -32
  149. package/report/renderer/report-renderer.js +3 -13
  150. package/report/renderer/report-ui-features.js +1 -1
  151. package/report/renderer/report-utils.d.ts +0 -1
  152. package/report/renderer/report-utils.js +13 -13
  153. package/shared/localization/locales/ar-XB.json +62 -329
  154. package/shared/localization/locales/ar.json +62 -329
  155. package/shared/localization/locales/bg.json +62 -329
  156. package/shared/localization/locales/ca.json +62 -329
  157. package/shared/localization/locales/cs.json +62 -329
  158. package/shared/localization/locales/da.json +62 -329
  159. package/shared/localization/locales/de.json +62 -329
  160. package/shared/localization/locales/el.json +62 -329
  161. package/shared/localization/locales/en-GB.json +62 -329
  162. package/shared/localization/locales/en-US.json +41 -272
  163. package/shared/localization/locales/en-XA.json +62 -329
  164. package/shared/localization/locales/en-XL.json +41 -272
  165. package/shared/localization/locales/es-419.json +62 -329
  166. package/shared/localization/locales/es.json +62 -329
  167. package/shared/localization/locales/fi.json +62 -329
  168. package/shared/localization/locales/fil.json +61 -328
  169. package/shared/localization/locales/fr.json +62 -329
  170. package/shared/localization/locales/he.json +62 -329
  171. package/shared/localization/locales/hi.json +62 -329
  172. package/shared/localization/locales/hr.json +63 -330
  173. package/shared/localization/locales/hu.json +62 -329
  174. package/shared/localization/locales/id.json +62 -329
  175. package/shared/localization/locales/it.json +62 -329
  176. package/shared/localization/locales/ja.json +62 -329
  177. package/shared/localization/locales/ko.json +62 -329
  178. package/shared/localization/locales/lt.json +62 -329
  179. package/shared/localization/locales/lv.json +62 -329
  180. package/shared/localization/locales/nl.json +62 -329
  181. package/shared/localization/locales/no.json +62 -329
  182. package/shared/localization/locales/pl.json +61 -328
  183. package/shared/localization/locales/pt-PT.json +62 -329
  184. package/shared/localization/locales/pt.json +64 -331
  185. package/shared/localization/locales/ro.json +62 -329
  186. package/shared/localization/locales/ru.json +62 -329
  187. package/shared/localization/locales/sk.json +62 -329
  188. package/shared/localization/locales/sl.json +62 -329
  189. package/shared/localization/locales/sr-Latn.json +62 -329
  190. package/shared/localization/locales/sr.json +62 -329
  191. package/shared/localization/locales/sv.json +62 -329
  192. package/shared/localization/locales/ta.json +61 -328
  193. package/shared/localization/locales/te.json +62 -329
  194. package/shared/localization/locales/th.json +62 -329
  195. package/shared/localization/locales/tr.json +62 -329
  196. package/shared/localization/locales/uk.json +62 -329
  197. package/shared/localization/locales/vi.json +61 -328
  198. package/shared/localization/locales/zh-HK.json +62 -329
  199. package/shared/localization/locales/zh-TW.json +61 -328
  200. package/shared/localization/locales/zh.json +62 -329
  201. package/tsconfig.json +1 -3
  202. package/types/artifacts.d.ts +21 -110
  203. package/types/audit.d.ts +0 -10
  204. package/types/config.d.ts +0 -1
  205. package/types/externs.d.ts +0 -2
  206. package/types/gatherer.d.ts +3 -0
  207. package/types/internal/smokehouse.d.ts +1 -1
  208. package/types/lh.d.ts +0 -2
  209. package/types/lhr/audit-details.d.ts +9 -4
  210. package/types/lhr/lhr.d.ts +0 -4
  211. package/types/lhr/settings.d.ts +0 -4
  212. package/core/audits/accessibility/duplicate-id-active.js +0 -42
  213. package/core/audits/content-width.d.ts +0 -17
  214. package/core/audits/content-width.js +0 -76
  215. package/core/audits/installable-manifest.d.ts +0 -111
  216. package/core/audits/installable-manifest.js +0 -248
  217. package/core/audits/layout-shift-elements.d.ts +0 -16
  218. package/core/audits/layout-shift-elements.js +0 -101
  219. package/core/audits/manual/pwa-cross-browser.d.ts +0 -12
  220. package/core/audits/manual/pwa-cross-browser.js +0 -40
  221. package/core/audits/manual/pwa-each-page-has-url.d.ts +0 -12
  222. package/core/audits/manual/pwa-each-page-has-url.js +0 -38
  223. package/core/audits/manual/pwa-page-transitions.d.ts +0 -12
  224. package/core/audits/manual/pwa-page-transitions.js +0 -38
  225. package/core/audits/maskable-icon.d.ts +0 -25
  226. package/core/audits/maskable-icon.js +0 -70
  227. package/core/audits/multi-check-audit.d.ts +0 -28
  228. package/core/audits/multi-check-audit.js +0 -82
  229. package/core/audits/no-unload-listeners.d.ts +0 -16
  230. package/core/audits/no-unload-listeners.js +0 -86
  231. package/core/audits/performance-budget.d.ts +0 -39
  232. package/core/audits/performance-budget.js +0 -154
  233. package/core/audits/script-elements-test-audit.d.ts +0 -14
  234. package/core/audits/script-elements-test-audit.js +0 -29
  235. package/core/audits/seo/plugins.d.ts +0 -15
  236. package/core/audits/seo/plugins.js +0 -150
  237. package/core/audits/seo/tap-targets.d.ts +0 -50
  238. package/core/audits/seo/tap-targets.js +0 -352
  239. package/core/audits/splash-screen.d.ts +0 -37
  240. package/core/audits/splash-screen.js +0 -98
  241. package/core/audits/themed-omnibox.d.ts +0 -42
  242. package/core/audits/themed-omnibox.js +0 -102
  243. package/core/audits/timing-budget.d.ts +0 -43
  244. package/core/audits/timing-budget.js +0 -175
  245. package/core/computed/manifest-values.d.ts +0 -24
  246. package/core/computed/manifest-values.js +0 -136
  247. package/core/config/budget.d.ts +0 -74
  248. package/core/config/budget.js +0 -340
  249. package/core/config/metrics-to-audits.d.ts +0 -19
  250. package/core/config/metrics-to-audits.js +0 -62
  251. package/core/gather/driver/service-workers.d.ts +0 -16
  252. package/core/gather/driver/service-workers.js +0 -52
  253. package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.d.ts +0 -47
  254. package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.js +0 -233
  255. package/core/gather/gatherers/global-listeners.d.ts +0 -27
  256. package/core/gather/gatherers/global-listeners.js +0 -108
  257. package/core/gather/gatherers/installability-errors.d.ts +0 -17
  258. package/core/gather/gatherers/installability-errors.js +0 -56
  259. package/core/gather/gatherers/script-elements.d.ts +0 -21
  260. package/core/gather/gatherers/script-elements.js +0 -100
  261. package/core/gather/gatherers/seo/embedded-content.d.ts +0 -10
  262. package/core/gather/gatherers/seo/embedded-content.js +0 -63
  263. package/core/gather/gatherers/seo/tap-targets.d.ts +0 -21
  264. package/core/gather/gatherers/seo/tap-targets.js +0 -389
  265. package/core/gather/gatherers/service-worker.d.ts +0 -10
  266. package/core/gather/gatherers/service-worker.js +0 -32
  267. package/core/gather/gatherers/web-app-manifest.d.ts +0 -32
  268. package/core/gather/gatherers/web-app-manifest.js +0 -106
  269. package/report/renderer/pwa-category-renderer.d.ts +0 -55
  270. package/report/renderer/pwa-category-renderer.js +0 -184
  271. package/third-party/chromium-synchronization/installability-errors-test.js +0 -79
  272. package/types/lhr/budget.d.ts +0 -60
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2024 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import * as Lantern from '../types/lantern.js';
8
+ import {Metric} from '../metric.js';
9
+ import {BaseNode} from '../base-node.js';
10
+
11
+ /** @typedef {import('../base-node.js').Node} Node */
12
+
13
+ class MaxPotentialFID extends Metric {
14
+ /**
15
+ * @return {LH.Gatherer.Simulation.MetricCoefficients}
16
+ */
17
+ static get COEFFICIENTS() {
18
+ return {
19
+ intercept: 0,
20
+ optimistic: 0.5,
21
+ pessimistic: 0.5,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * @param {Node} dependencyGraph
27
+ * @return {Node}
28
+ */
29
+ static getOptimisticGraph(dependencyGraph) {
30
+ return dependencyGraph;
31
+ }
32
+
33
+ /**
34
+ * @param {Node} dependencyGraph
35
+ * @return {Node}
36
+ */
37
+ static getPessimisticGraph(dependencyGraph) {
38
+ return dependencyGraph;
39
+ }
40
+
41
+ /**
42
+ * @param {LH.Gatherer.Simulation.Result} simulation
43
+ * @param {import('../metric.js').Extras} extras
44
+ * @return {LH.Gatherer.Simulation.Result}
45
+ */
46
+ static getEstimateFromSimulation(simulation, extras) {
47
+ if (!extras.fcpResult) throw new Error('missing fcpResult');
48
+
49
+ // Intentionally use the opposite FCP estimate, a more pessimistic FCP means that more tasks
50
+ // are excluded from the FID computation, so a higher FCP means lower FID for same work.
51
+ const fcpTimeInMs = extras.optimistic
52
+ ? extras.fcpResult.pessimisticEstimate.timeInMs
53
+ : extras.fcpResult.optimisticEstimate.timeInMs;
54
+
55
+ const timings = MaxPotentialFID.getTimingsAfterFCP(
56
+ simulation.nodeTimings,
57
+ fcpTimeInMs
58
+ );
59
+
60
+ return {
61
+ timeInMs: Math.max(...timings.map(timing => timing.duration), 16),
62
+ nodeTimings: simulation.nodeTimings,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * @param {Lantern.Simulation.MetricComputationDataInput} data
68
+ * @param {Omit<import('../metric.js').Extras, 'optimistic'>=} extras
69
+ * @return {Promise<LH.Artifacts.LanternMetric>}
70
+ */
71
+ static compute(data, extras) {
72
+ const fcpResult = extras?.fcpResult;
73
+ if (!fcpResult) {
74
+ throw new Error('FCP is required to calculate the Max Potential FID metric');
75
+ }
76
+
77
+ return super.compute(data, extras);
78
+ }
79
+
80
+ /**
81
+ * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
82
+ * @param {number} fcpTimeInMs
83
+ * @return {Array<{duration: number}>}
84
+ */
85
+ static getTimingsAfterFCP(nodeTimings, fcpTimeInMs) {
86
+ return Array.from(nodeTimings.entries())
87
+ .filter(([node, timing]) => node.type === BaseNode.TYPES.CPU && timing.endTime > fcpTimeInMs)
88
+ .map(([_, timing]) => timing);
89
+ }
90
+ }
91
+
92
+ export {MaxPotentialFID};
@@ -0,0 +1,38 @@
1
+ export type Node = import('../base-node.js').Node;
2
+ /** @typedef {import('../base-node.js').Node} Node */
3
+ export class SpeedIndex extends Metric {
4
+ /**
5
+ * @param {Node} dependencyGraph
6
+ * @return {Node}
7
+ */
8
+ static getOptimisticGraph(dependencyGraph: Node): Node;
9
+ /**
10
+ * @param {Node} dependencyGraph
11
+ * @return {Node}
12
+ */
13
+ static getPessimisticGraph(dependencyGraph: Node): Node;
14
+ /**
15
+ * @param {LH.Gatherer.Simulation.Result} simulationResult
16
+ * @param {import('../metric.js').Extras} extras
17
+ * @return {LH.Gatherer.Simulation.Result}
18
+ */
19
+ static getEstimateFromSimulation(simulationResult: LH.Gatherer.Simulation.Result, extras: import('../metric.js').Extras): LH.Gatherer.Simulation.Result;
20
+ /**
21
+ * Approximate speed index using layout events from the simulated node timings.
22
+ * The layout-based speed index is the weighted average of the endTime of CPU nodes that contained
23
+ * a 'Layout' task. log(duration) is used as the weight to stand for "significance" to the page.
24
+ *
25
+ * If no layout events can be found or the endTime of a CPU task is too early, FCP is used instead.
26
+ *
27
+ * This approach was determined after evaluating the accuracy/complexity tradeoff of many
28
+ * different methods. Read more in the evaluation doc.
29
+ *
30
+ * @see https://docs.google.com/document/d/1qJWXwxoyVLVadezIp_Tgdk867G3tDNkkVRvUJSH3K1E/edit#
31
+ * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
32
+ * @param {number} fcpTimeInMs
33
+ * @return {number}
34
+ */
35
+ static computeLayoutBasedSpeedIndex(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], fcpTimeInMs: number): number;
36
+ }
37
+ import { Metric } from '../metric.js';
38
+ //# sourceMappingURL=speed-index.d.ts.map
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2024 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import * as Lantern from '../types/lantern.js';
8
+ import {Metric} from '../metric.js';
9
+ import {BaseNode} from '../base-node.js';
10
+
11
+ const mobileSlow4GRtt = 150;
12
+
13
+ /** @typedef {import('../base-node.js').Node} Node */
14
+
15
+ class SpeedIndex extends Metric {
16
+ /**
17
+ * @return {LH.Gatherer.Simulation.MetricCoefficients}
18
+ */
19
+ static get COEFFICIENTS() {
20
+ return {
21
+ // Note that the optimistic estimate is based on the real observed speed index rather than a
22
+ // real lantern graph (and the final estimate will be Math.max(FCP, Speed Index)).
23
+ intercept: 0,
24
+ optimistic: 1.4,
25
+ pessimistic: 0.4,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * @param {number} rttMs
31
+ * @return {LH.Gatherer.Simulation.MetricCoefficients}
32
+ */
33
+ static getScaledCoefficients(rttMs) { // eslint-disable-line no-unused-vars
34
+ // We want to scale our default coefficients based on the speed of the connection.
35
+ // We will linearly interpolate coefficients for the passed-in rttMs based on two pre-determined points:
36
+ // 1. Baseline point of 30 ms RTT where Speed Index should be a ~50/50 blend of optimistic/pessimistic.
37
+ // 30 ms was based on a typical home WiFi connection's actual RTT.
38
+ // Coefficients here follow from the fact that the optimistic estimate should be very close
39
+ // to reality at this connection speed and the pessimistic estimate compensates for minor
40
+ // connection speed differences.
41
+ // 2. Default throttled point of 150 ms RTT where the default coefficients have been determined to be most accurate.
42
+ // Coefficients here were determined through thorough analysis and linear regression on the
43
+ // lantern test data set. See core/scripts/test-lantern.sh for more detail.
44
+ // While the coefficients haven't been analyzed at the interpolated points, it's our current best effort.
45
+ const defaultCoefficients = this.COEFFICIENTS;
46
+ const defaultRttExcess = mobileSlow4GRtt - 30;
47
+ const multiplier = Math.max((rttMs - 30) / defaultRttExcess, 0);
48
+
49
+ return {
50
+ intercept: defaultCoefficients.intercept * multiplier,
51
+ optimistic: 0.5 + (defaultCoefficients.optimistic - 0.5) * multiplier,
52
+ pessimistic: 0.5 + (defaultCoefficients.pessimistic - 0.5) * multiplier,
53
+ };
54
+ }
55
+
56
+ /**
57
+ * @param {Node} dependencyGraph
58
+ * @return {Node}
59
+ */
60
+ static getOptimisticGraph(dependencyGraph) {
61
+ return dependencyGraph;
62
+ }
63
+
64
+ /**
65
+ * @param {Node} dependencyGraph
66
+ * @return {Node}
67
+ */
68
+ static getPessimisticGraph(dependencyGraph) {
69
+ return dependencyGraph;
70
+ }
71
+
72
+ /**
73
+ * @param {LH.Gatherer.Simulation.Result} simulationResult
74
+ * @param {import('../metric.js').Extras} extras
75
+ * @return {LH.Gatherer.Simulation.Result}
76
+ */
77
+ static getEstimateFromSimulation(simulationResult, extras) {
78
+ if (!extras.fcpResult) throw new Error('missing fcpResult');
79
+ if (!extras.speedline) throw new Error('missing speedline');
80
+
81
+ const fcpTimeInMs = extras.fcpResult.pessimisticEstimate.timeInMs;
82
+ const estimate = extras.optimistic
83
+ ? extras.speedline.speedIndex
84
+ : SpeedIndex.computeLayoutBasedSpeedIndex(simulationResult.nodeTimings, fcpTimeInMs);
85
+ return {
86
+ timeInMs: estimate,
87
+ nodeTimings: simulationResult.nodeTimings,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * @param {Lantern.Simulation.MetricComputationDataInput} data
93
+ * @param {Omit<import('../metric.js').Extras, 'optimistic'>=} extras
94
+ * @return {Promise<LH.Artifacts.LanternMetric>}
95
+ */
96
+ static async compute(data, extras) {
97
+ const fcpResult = extras?.fcpResult;
98
+ if (!fcpResult) {
99
+ throw new Error('FCP is required to calculate the SpeedIndex metric');
100
+ }
101
+
102
+ const metricResult = await super.compute(data, extras);
103
+ metricResult.timing = Math.max(metricResult.timing, fcpResult.timing);
104
+ return metricResult;
105
+ }
106
+
107
+ /**
108
+ * Approximate speed index using layout events from the simulated node timings.
109
+ * The layout-based speed index is the weighted average of the endTime of CPU nodes that contained
110
+ * a 'Layout' task. log(duration) is used as the weight to stand for "significance" to the page.
111
+ *
112
+ * If no layout events can be found or the endTime of a CPU task is too early, FCP is used instead.
113
+ *
114
+ * This approach was determined after evaluating the accuracy/complexity tradeoff of many
115
+ * different methods. Read more in the evaluation doc.
116
+ *
117
+ * @see https://docs.google.com/document/d/1qJWXwxoyVLVadezIp_Tgdk867G3tDNkkVRvUJSH3K1E/edit#
118
+ * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
119
+ * @param {number} fcpTimeInMs
120
+ * @return {number}
121
+ */
122
+ static computeLayoutBasedSpeedIndex(nodeTimings, fcpTimeInMs) {
123
+ /** @type {Array<{time: number, weight: number}>} */
124
+ const layoutWeights = [];
125
+ for (const [node, timing] of nodeTimings.entries()) {
126
+ if (node.type !== BaseNode.TYPES.CPU) continue;
127
+
128
+ if (node.childEvents.some(x => x.name === 'Layout')) {
129
+ const timingWeight = Math.max(Math.log2(timing.endTime - timing.startTime), 0);
130
+ layoutWeights.push({time: timing.endTime, weight: timingWeight});
131
+ }
132
+ }
133
+
134
+ const totalWeightedTime = layoutWeights
135
+ .map(evt => evt.weight * Math.max(evt.time, fcpTimeInMs))
136
+ .reduce((a, b) => a + b, 0);
137
+ const totalWeight = layoutWeights.map(evt => evt.weight).reduce((a, b) => a + b, 0);
138
+
139
+ if (!totalWeight) return fcpTimeInMs;
140
+ return totalWeightedTime / totalWeight;
141
+ }
142
+ }
143
+
144
+ export {SpeedIndex};
@@ -0,0 +1,31 @@
1
+ export type Node = import('../base-node.js').Node;
2
+ /** @typedef {import('../base-node.js').Node} Node */
3
+ export class TotalBlockingTime extends Metric {
4
+ /**
5
+ * @param {Node} dependencyGraph
6
+ * @return {Node}
7
+ */
8
+ static getOptimisticGraph(dependencyGraph: Node): Node;
9
+ /**
10
+ * @param {Node} dependencyGraph
11
+ * @return {Node}
12
+ */
13
+ static getPessimisticGraph(dependencyGraph: Node): Node;
14
+ /**
15
+ * @param {LH.Gatherer.Simulation.Result} simulation
16
+ * @param {import('../metric.js').Extras} extras
17
+ * @return {LH.Gatherer.Simulation.Result}
18
+ */
19
+ static getEstimateFromSimulation(simulation: LH.Gatherer.Simulation.Result, extras: import('../metric.js').Extras): LH.Gatherer.Simulation.Result;
20
+ /**
21
+ * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
22
+ * @param {number} minDurationMs
23
+ */
24
+ static getTopLevelEvents(nodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], minDurationMs: number): {
25
+ start: number;
26
+ end: number;
27
+ duration: number;
28
+ }[];
29
+ }
30
+ import { Metric } from '../metric.js';
31
+ //# sourceMappingURL=total-blocking-time.d.ts.map
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2024 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import * as Lantern from '../types/lantern.js';
8
+ import {Metric} from '../metric.js';
9
+ import {BaseNode} from '../base-node.js';
10
+ import {BLOCKING_TIME_THRESHOLD, calculateSumOfBlockingTime} from '../../../computed/metrics/tbt-utils.js';
11
+
12
+ /** @typedef {import('../base-node.js').Node} Node */
13
+
14
+ class TotalBlockingTime extends Metric {
15
+ /**
16
+ * @return {LH.Gatherer.Simulation.MetricCoefficients}
17
+ */
18
+ static get COEFFICIENTS() {
19
+ return {
20
+ intercept: 0,
21
+ optimistic: 0.5,
22
+ pessimistic: 0.5,
23
+ };
24
+ }
25
+
26
+ /**
27
+ * @param {Node} dependencyGraph
28
+ * @return {Node}
29
+ */
30
+ static getOptimisticGraph(dependencyGraph) {
31
+ return dependencyGraph;
32
+ }
33
+
34
+ /**
35
+ * @param {Node} dependencyGraph
36
+ * @return {Node}
37
+ */
38
+ static getPessimisticGraph(dependencyGraph) {
39
+ return dependencyGraph;
40
+ }
41
+
42
+ /**
43
+ * @param {LH.Gatherer.Simulation.Result} simulation
44
+ * @param {import('../metric.js').Extras} extras
45
+ * @return {LH.Gatherer.Simulation.Result}
46
+ */
47
+ static getEstimateFromSimulation(simulation, extras) {
48
+ if (!extras.fcpResult) throw new Error('missing fcpResult');
49
+ if (!extras.interactiveResult) throw new Error('missing interactiveResult');
50
+
51
+ // Intentionally use the opposite FCP estimate. A pessimistic FCP is higher than equal to an
52
+ // optimistic FCP, which means potentially more tasks are excluded from the Total Blocking Time
53
+ // computation. So a more pessimistic FCP gives a more optimistic Total Blocking Time for the
54
+ // same work.
55
+ const fcpTimeInMs = extras.optimistic
56
+ ? extras.fcpResult.pessimisticEstimate.timeInMs
57
+ : extras.fcpResult.optimisticEstimate.timeInMs;
58
+
59
+ // Similarly, we always have pessimistic TTI >= optimistic TTI. Therefore, picking optimistic
60
+ // TTI means our window of interest is smaller and thus potentially more tasks are excluded from
61
+ // Total Blocking Time computation, yielding a lower (more optimistic) Total Blocking Time value
62
+ // for the same work.
63
+ const interactiveTimeMs = extras.optimistic
64
+ ? extras.interactiveResult.optimisticEstimate.timeInMs
65
+ : extras.interactiveResult.pessimisticEstimate.timeInMs;
66
+
67
+ const minDurationMs = BLOCKING_TIME_THRESHOLD;
68
+
69
+ const events = TotalBlockingTime.getTopLevelEvents(
70
+ simulation.nodeTimings,
71
+ minDurationMs
72
+ );
73
+
74
+ return {
75
+ timeInMs: calculateSumOfBlockingTime(
76
+ events,
77
+ fcpTimeInMs,
78
+ interactiveTimeMs
79
+ ),
80
+ nodeTimings: simulation.nodeTimings,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * @param {Lantern.Simulation.MetricComputationDataInput} data
86
+ * @param {Omit<import('../metric.js').Extras, 'optimistic'>=} extras
87
+ * @return {Promise<LH.Artifacts.LanternMetric>}
88
+ */
89
+ static async compute(data, extras) {
90
+ const fcpResult = extras?.fcpResult;
91
+ if (!fcpResult) {
92
+ throw new Error('FCP is required to calculate the TBT metric');
93
+ }
94
+
95
+ const interactiveResult = extras?.fcpResult;
96
+ if (!interactiveResult) {
97
+ throw new Error('Interactive is required to calculate the TBT metric');
98
+ }
99
+
100
+ return super.compute(data, extras);
101
+ }
102
+
103
+ /**
104
+ * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
105
+ * @param {number} minDurationMs
106
+ */
107
+ static getTopLevelEvents(nodeTimings, minDurationMs) {
108
+ /** @type {Array<{start: number, end: number, duration: number}>}
109
+ */
110
+ const events = [];
111
+
112
+ for (const [node, timing] of nodeTimings.entries()) {
113
+ if (node.type !== BaseNode.TYPES.CPU) continue;
114
+ // Filtering out events below minimum duration.
115
+ if (timing.duration < minDurationMs) continue;
116
+
117
+ events.push({
118
+ start: timing.startTime,
119
+ end: timing.endTime,
120
+ duration: timing.duration,
121
+ });
122
+ }
123
+
124
+ return events;
125
+ }
126
+ }
127
+
128
+ export {TotalBlockingTime};
@@ -7,8 +7,32 @@
7
7
  import * as Lantern from './types/lantern.js';
8
8
  import {NetworkRequestTypes} from './lantern.js';
9
9
  import {BaseNode} from './base-node.js';
10
- // TODO(15841): bring impl of isNonNetworkRequest inside lantern and remove this.
11
- import UrlUtils from '../url-utils.js';
10
+
11
+ const NON_NETWORK_SCHEMES = [
12
+ 'blob', // @see https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
13
+ 'data', // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
14
+ 'intent', // @see https://developer.chrome.com/docs/multidevice/android/intents/
15
+ 'file', // @see https://en.wikipedia.org/wiki/File_URI_scheme
16
+ 'filesystem', // @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystem
17
+ 'chrome-extension',
18
+ ];
19
+
20
+ /**
21
+ * Use `NetworkRequest.isNonNetworkRequest(req)` if working with a request.
22
+ * Note: the `protocol` field from CDP can be 'h2', 'http', (not 'https'!) or it'll be url's scheme.
23
+ * https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/network_handler.cc;l=598-611;drc=56d4a9a9deb30be73adcee8737c73bcb2a5ab64f
24
+ * However, a `new URL(href).protocol` has a colon suffix.
25
+ * https://url.spec.whatwg.org/#dom-url-protocol
26
+ * A URL's `scheme` is specced as the `protocol` sans-colon, but isn't exposed on a URL object.
27
+ * This method can take all 3 of these string types as a parameter.
28
+ * @param {string} protocol Either a networkRequest's `protocol` per CDP or a `new URL(href).protocol`
29
+ * @return {boolean}
30
+ */
31
+ function isNonNetworkProtocol(protocol) {
32
+ // Strip off any colon
33
+ const urlScheme = protocol.includes(':') ? protocol.slice(0, protocol.indexOf(':')) : protocol;
34
+ return NON_NETWORK_SCHEMES.includes(urlScheme);
35
+ }
12
36
 
13
37
  /**
14
38
  * @template [T=any]
@@ -75,9 +99,9 @@ class NetworkNode extends BaseNode {
75
99
  */
76
100
  get isNonNetworkProtocol() {
77
101
  // The 'protocol' field in devtools a string more like a `scheme`
78
- return UrlUtils.isNonNetworkProtocol(this.request.protocol) ||
102
+ return isNonNetworkProtocol(this.request.protocol) ||
79
103
  // But `protocol` can fail to be populated if the request fails, so fallback to scheme.
80
- UrlUtils.isNonNetworkProtocol(this.request.parsedURL.scheme);
104
+ isNonNetworkProtocol(this.request.parsedURL.scheme);
81
105
  }
82
106
 
83
107
  /**
@@ -18,10 +18,10 @@ export class PageDependencyGraph {
18
18
  */
19
19
  static getNetworkNodeOutput(networkRecords: Array<Lantern.NetworkRequest>): NetworkNodeOutput;
20
20
  /**
21
- * @param {LH.Artifacts.ProcessedTrace} processedTrace
21
+ * @param {LH.TraceEvent[]} mainThreadEvents
22
22
  * @return {Array<CPUNode>}
23
23
  */
24
- static getCPUNodes({ mainThreadEvents }: LH.Artifacts.ProcessedTrace): Array<CPUNode>;
24
+ static getCPUNodes(mainThreadEvents: LH.TraceEvent[]): Array<CPUNode>;
25
25
  /**
26
26
  * @param {NetworkNode} rootNode
27
27
  * @param {NetworkNodeOutput} networkNodeOutput
@@ -40,12 +40,12 @@ export class PageDependencyGraph {
40
40
  */
41
41
  static _pruneNode(node: Node): void;
42
42
  /**
43
- * @param {LH.Artifacts.ProcessedTrace} processedTrace
43
+ * @param {LH.TraceEvent[]} mainThreadEvents
44
44
  * @param {Array<Lantern.NetworkRequest>} networkRecords
45
45
  * @param {URLArtifact} URL
46
46
  * @return {Node}
47
47
  */
48
- static createGraph(processedTrace: LH.Artifacts.ProcessedTrace, networkRecords: Array<Lantern.NetworkRequest>, URL: URLArtifact): Node;
48
+ static createGraph(mainThreadEvents: LH.TraceEvent[], networkRecords: Array<Lantern.NetworkRequest>, URL: URLArtifact): Node;
49
49
  /**
50
50
  *
51
51
  * @param {Node} rootNode
@@ -109,10 +109,10 @@ class PageDependencyGraph {
109
109
  }
110
110
 
111
111
  /**
112
- * @param {LH.Artifacts.ProcessedTrace} processedTrace
112
+ * @param {LH.TraceEvent[]} mainThreadEvents
113
113
  * @return {Array<CPUNode>}
114
114
  */
115
- static getCPUNodes({mainThreadEvents}) {
115
+ static getCPUNodes(mainThreadEvents) {
116
116
  /** @type {Array<CPUNode>} */
117
117
  const nodes = [];
118
118
  let i = 0;
@@ -128,6 +128,9 @@ class PageDependencyGraph {
128
128
  continue;
129
129
  }
130
130
 
131
+ /** @type {number|undefined} */
132
+ let correctedEndTs = undefined;
133
+
131
134
  // Capture all events that occurred within the task
132
135
  /** @type {Array<LH.TraceEvent>} */
133
136
  const children = [];
@@ -136,10 +139,19 @@ class PageDependencyGraph {
136
139
  i < mainThreadEvents.length && mainThreadEvents[i].ts < endTime;
137
140
  i++
138
141
  ) {
142
+ // Temporary fix for a Chrome bug where some RunTask events can be overlapping.
143
+ // We correct that here be ensuring each RunTask ends at least 1 microsecond before the next
144
+ // https://github.com/GoogleChrome/lighthouse/issues/15896
145
+ // https://issues.chromium.org/issues/329678173
146
+ if (TraceProcessor.isScheduleableTask(mainThreadEvents[i]) && mainThreadEvents[i].dur) {
147
+ correctedEndTs = mainThreadEvents[i].ts - 1;
148
+ break;
149
+ }
150
+
139
151
  children.push(mainThreadEvents[i]);
140
152
  }
141
153
 
142
- nodes.push(new CPUNode(evt, children));
154
+ nodes.push(new CPUNode(evt, children, correctedEndTs));
143
155
  }
144
156
 
145
157
  return nodes;
@@ -359,7 +371,7 @@ class PageDependencyGraph {
359
371
  isFirst = foundFirstParse = true;
360
372
  }
361
373
 
362
- if (isFirst || node.event.dur >= minimumEvtDur) {
374
+ if (isFirst || node.duration >= minimumEvtDur) {
363
375
  // Don't prune this node. The task is long / important so it will impact simulation.
364
376
  continue;
365
377
  }
@@ -393,14 +405,14 @@ class PageDependencyGraph {
393
405
  }
394
406
 
395
407
  /**
396
- * @param {LH.Artifacts.ProcessedTrace} processedTrace
408
+ * @param {LH.TraceEvent[]} mainThreadEvents
397
409
  * @param {Array<Lantern.NetworkRequest>} networkRecords
398
410
  * @param {URLArtifact} URL
399
411
  * @return {Node}
400
412
  */
401
- static createGraph(processedTrace, networkRecords, URL) {
413
+ static createGraph(mainThreadEvents, networkRecords, URL) {
402
414
  const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
403
- const cpuNodes = PageDependencyGraph.getCPUNodes(processedTrace);
415
+ const cpuNodes = PageDependencyGraph.getCPUNodes(mainThreadEvents);
404
416
  const {requestedUrl, mainDocumentUrl} = URL;
405
417
  if (!requestedUrl) throw new Error('requestedUrl is required to get the root request');
406
418
  if (!mainDocumentUrl) throw new Error('mainDocumentUrl is required to get the main resource');
@@ -19,27 +19,17 @@ export class ConnectionPool {
19
19
  _initializeConnections(): void;
20
20
  /**
21
21
  * @param {Array<TcpConnection>} connections
22
- * @param {{ignoreConnectionReused?: boolean, observedConnectionWasReused: boolean}} options
23
22
  */
24
- _findAvailableConnectionWithLargestCongestionWindow(connections: Array<TcpConnection>, options: {
25
- ignoreConnectionReused?: boolean | undefined;
26
- observedConnectionWasReused: boolean;
27
- }): TcpConnection | null;
23
+ _findAvailableConnectionWithLargestCongestionWindow(connections: Array<TcpConnection>): TcpConnection | null;
28
24
  /**
29
25
  * This method finds an available connection to the origin specified by the network record or null
30
26
  * if no connection was available. If returned, connection will not be available for other network
31
27
  * records until release is called.
32
28
  *
33
- * If ignoreConnectionReused is true, acquire will consider all connections not in use as available.
34
- * Otherwise, only connections that have matching "warmth" are considered available.
35
- *
36
29
  * @param {Lantern.NetworkRequest} record
37
- * @param {{ignoreConnectionReused?: boolean}} options
38
30
  * @return {?TcpConnection}
39
31
  */
40
- acquire(record: Lantern.NetworkRequest, options?: {
41
- ignoreConnectionReused?: boolean;
42
- }): TcpConnection | null;
32
+ acquire(record: Lantern.NetworkRequest): TcpConnection | null;
43
33
  /**
44
34
  * Return the connection currently being used to fetch a record. If no connection
45
35
  * currently being used for this record, an error will be thrown.
@@ -85,24 +85,13 @@ export class ConnectionPool {
85
85
 
86
86
  /**
87
87
  * @param {Array<TcpConnection>} connections
88
- * @param {{ignoreConnectionReused?: boolean, observedConnectionWasReused: boolean}} options
89
88
  */
90
- _findAvailableConnectionWithLargestCongestionWindow(connections, options) {
91
- const {ignoreConnectionReused, observedConnectionWasReused} = options;
92
-
89
+ _findAvailableConnectionWithLargestCongestionWindow(connections) {
93
90
  /** @type {TcpConnection|null} */
94
91
  let maxConnection = null;
95
92
  for (let i = 0; i < connections.length; i++) {
96
93
  const connection = connections[i];
97
94
 
98
- // Normally, we want to make sure the connection warmth matches the state of the record
99
- // we're acquiring for. Do this check first since it's the common case and cheaper than our
100
- // "in use" check below.
101
- // Use the _warmed property instead of the getter because this is a surprisingly hot code path.
102
- if (!ignoreConnectionReused && connection._warmed !== observedConnectionWasReused) {
103
- continue;
104
- }
105
-
106
95
  // Connections that are in use are never available.
107
96
  if (this._connectionsInUse.has(connection)) {
108
97
  continue;
@@ -121,23 +110,15 @@ export class ConnectionPool {
121
110
  * if no connection was available. If returned, connection will not be available for other network
122
111
  * records until release is called.
123
112
  *
124
- * If ignoreConnectionReused is true, acquire will consider all connections not in use as available.
125
- * Otherwise, only connections that have matching "warmth" are considered available.
126
- *
127
113
  * @param {Lantern.NetworkRequest} record
128
- * @param {{ignoreConnectionReused?: boolean}} options
129
114
  * @return {?TcpConnection}
130
115
  */
131
- acquire(record, options = {}) {
116
+ acquire(record) {
132
117
  if (this._connectionsByRecord.has(record)) throw new Error('Record already has a connection');
133
118
 
134
119
  const origin = record.parsedURL.securityOrigin;
135
- const observedConnectionWasReused = !!this._connectionReusedByRequestId.get(record.requestId);
136
120
  const connections = this._connectionsByOrigin.get(origin) || [];
137
- const connectionToUse = this._findAvailableConnectionWithLargestCongestionWindow(connections, {
138
- ignoreConnectionReused: options.ignoreConnectionReused,
139
- observedConnectionWasReused,
140
- });
121
+ const connectionToUse = this._findAvailableConnectionWithLargestCongestionWindow(connections);
141
122
 
142
123
  if (!connectionToUse) return null;
143
124