lighthouse 11.7.0 → 12.0.0-dev.20240422

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 (334) 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/accesskeys.js +1 -1
  10. package/core/audits/accessibility/aria-allowed-attr.js +1 -1
  11. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  12. package/core/audits/accessibility/aria-command-name.js +1 -1
  13. package/core/audits/accessibility/aria-conditional-attr.d.ts +10 -0
  14. package/core/audits/accessibility/aria-conditional-attr.js +43 -0
  15. package/core/audits/accessibility/{duplicate-id-active.d.ts → aria-deprecated-role.d.ts} +3 -3
  16. package/core/audits/accessibility/aria-deprecated-role.js +43 -0
  17. package/core/audits/accessibility/aria-dialog-name.js +1 -1
  18. package/core/audits/accessibility/aria-hidden-body.js +1 -1
  19. package/core/audits/accessibility/aria-hidden-focus.js +1 -1
  20. package/core/audits/accessibility/aria-input-field-name.js +1 -1
  21. package/core/audits/accessibility/aria-meter-name.js +1 -1
  22. package/core/audits/accessibility/aria-progressbar-name.js +1 -1
  23. package/core/audits/accessibility/aria-prohibited-attr.d.ts +10 -0
  24. package/core/audits/accessibility/aria-prohibited-attr.js +44 -0
  25. package/core/audits/accessibility/aria-required-attr.js +1 -1
  26. package/core/audits/accessibility/aria-required-children.js +1 -1
  27. package/core/audits/accessibility/aria-required-parent.js +1 -1
  28. package/core/audits/accessibility/aria-roles.js +1 -1
  29. package/core/audits/accessibility/aria-text.js +1 -1
  30. package/core/audits/accessibility/aria-toggle-field-name.js +1 -1
  31. package/core/audits/accessibility/aria-tooltip-name.js +1 -1
  32. package/core/audits/accessibility/aria-treeitem-name.js +1 -1
  33. package/core/audits/accessibility/aria-valid-attr-value.js +1 -1
  34. package/core/audits/accessibility/aria-valid-attr.js +1 -1
  35. package/core/audits/accessibility/button-name.js +1 -1
  36. package/core/audits/accessibility/bypass.js +1 -1
  37. package/core/audits/accessibility/color-contrast.js +1 -1
  38. package/core/audits/accessibility/definition-list.js +1 -1
  39. package/core/audits/accessibility/dlitem.js +1 -1
  40. package/core/audits/accessibility/document-title.js +1 -1
  41. package/core/audits/accessibility/duplicate-id-aria.js +1 -1
  42. package/core/audits/accessibility/empty-heading.js +1 -1
  43. package/core/audits/accessibility/form-field-multiple-labels.js +1 -1
  44. package/core/audits/accessibility/frame-title.js +1 -1
  45. package/core/audits/accessibility/heading-order.js +1 -1
  46. package/core/audits/accessibility/html-has-lang.js +1 -1
  47. package/core/audits/accessibility/html-lang-valid.js +1 -1
  48. package/core/audits/accessibility/html-xml-lang-mismatch.js +1 -1
  49. package/core/audits/accessibility/identical-links-same-purpose.js +1 -1
  50. package/core/audits/accessibility/image-alt.js +1 -1
  51. package/core/audits/accessibility/image-redundant-alt.js +1 -1
  52. package/core/audits/accessibility/input-button-name.js +1 -1
  53. package/core/audits/accessibility/input-image-alt.js +1 -1
  54. package/core/audits/accessibility/label-content-name-mismatch.js +1 -1
  55. package/core/audits/accessibility/label.js +1 -1
  56. package/core/audits/accessibility/landmark-one-main.js +1 -1
  57. package/core/audits/accessibility/link-in-text-block.js +1 -1
  58. package/core/audits/accessibility/link-name.js +1 -1
  59. package/core/audits/accessibility/list.js +1 -1
  60. package/core/audits/accessibility/listitem.js +1 -1
  61. package/core/audits/accessibility/meta-refresh.js +1 -1
  62. package/core/audits/accessibility/meta-viewport.js +1 -1
  63. package/core/audits/accessibility/object-alt.js +1 -1
  64. package/core/audits/accessibility/select-name.js +1 -1
  65. package/core/audits/accessibility/skip-link.js +1 -1
  66. package/core/audits/accessibility/tabindex.js +1 -1
  67. package/core/audits/accessibility/table-duplicate-name.js +1 -1
  68. package/core/audits/accessibility/table-fake-caption.js +1 -1
  69. package/core/audits/accessibility/target-size.js +1 -2
  70. package/core/audits/accessibility/td-has-header.js +1 -1
  71. package/core/audits/accessibility/td-headers-attr.js +1 -1
  72. package/core/audits/accessibility/th-has-data-cells.js +1 -1
  73. package/core/audits/accessibility/valid-lang.js +1 -1
  74. package/core/audits/accessibility/video-caption.js +1 -1
  75. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +0 -16
  76. package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -38
  77. package/core/audits/byte-efficiency/offscreen-images.d.ts +0 -11
  78. package/core/audits/byte-efficiency/offscreen-images.js +0 -15
  79. package/core/audits/byte-efficiency/render-blocking-resources.js +27 -26
  80. package/core/audits/byte-efficiency/unminified-css.js +2 -2
  81. package/core/audits/byte-efficiency/unused-css-rules.js +3 -1
  82. package/core/audits/dobetterweb/uses-http2.d.ts +0 -13
  83. package/core/audits/dobetterweb/uses-http2.js +4 -38
  84. package/core/audits/font-display.js +2 -2
  85. package/core/audits/layout-shifts.js +2 -2
  86. package/core/audits/preload-fonts.d.ts +1 -5
  87. package/core/audits/preload-fonts.js +1 -10
  88. package/core/audits/prioritize-lcp-image.js +2 -2
  89. package/core/audits/redirects-http.d.ts +19 -0
  90. package/core/audits/redirects-http.js +75 -0
  91. package/core/audits/resource-summary.d.ts +2 -0
  92. package/core/audits/resource-summary.js +5 -4
  93. package/core/audits/seo/is-crawlable.d.ts +2 -1
  94. package/core/audits/seo/link-text.js +12 -0
  95. package/core/audits/uses-rel-preload.d.ts +1 -5
  96. package/core/audits/uses-rel-preload.js +5 -15
  97. package/core/audits/work-during-interaction.d.ts +2 -2
  98. package/core/audits/work-during-interaction.js +2 -2
  99. package/core/computed/js-bundles.d.ts +1 -1
  100. package/core/computed/metrics/cumulative-layout-shift.js +2 -2
  101. package/core/computed/metrics/lantern-first-contentful-paint.d.ts +13 -82
  102. package/core/computed/metrics/lantern-first-contentful-paint.js +16 -178
  103. package/core/computed/metrics/lantern-first-meaningful-paint.d.ts +13 -12
  104. package/core/computed/metrics/lantern-first-meaningful-paint.js +11 -49
  105. package/core/computed/metrics/lantern-interactive.d.ts +13 -21
  106. package/core/computed/metrics/lantern-interactive.js +12 -84
  107. package/core/computed/metrics/lantern-largest-contentful-paint.d.ts +13 -25
  108. package/core/computed/metrics/lantern-largest-contentful-paint.js +13 -80
  109. package/core/computed/metrics/lantern-max-potential-fid.d.ts +13 -24
  110. package/core/computed/metrics/lantern-max-potential-fid.js +12 -64
  111. package/core/computed/metrics/lantern-metric.d.ts +14 -18
  112. package/core/computed/metrics/lantern-metric.js +34 -30
  113. package/core/computed/metrics/lantern-speed-index.d.ts +13 -32
  114. package/core/computed/metrics/lantern-speed-index.js +13 -116
  115. package/core/computed/metrics/lantern-total-blocking-time.d.ts +13 -25
  116. package/core/computed/metrics/lantern-total-blocking-time.js +10 -94
  117. package/core/computed/metrics/metric.js +1 -3
  118. package/core/computed/module-duplication.d.ts +1 -1
  119. package/core/computed/navigation-insights.d.ts +36 -0
  120. package/core/computed/navigation-insights.js +35 -0
  121. package/core/computed/page-dependency-graph.js +3 -7
  122. package/core/computed/resource-summary.d.ts +10 -12
  123. package/core/computed/resource-summary.js +12 -19
  124. package/core/computed/trace-engine-result.d.ts +16 -3
  125. package/core/computed/trace-engine-result.js +19 -14
  126. package/core/computed/unused-css.d.ts +3 -1
  127. package/core/computed/unused-css.js +6 -5
  128. package/core/config/config-helpers.js +0 -4
  129. package/core/config/constants.js +0 -1
  130. package/core/config/default-config.js +63 -142
  131. package/core/config/experimental-config.js +11 -0
  132. package/core/config/filters.js +0 -1
  133. package/core/gather/driver/navigation.js +4 -1
  134. package/core/gather/driver/prepare.js +1 -1
  135. package/core/gather/driver/target-manager.js +4 -3
  136. package/core/gather/driver/wait-for-condition.js +2 -3
  137. package/core/gather/driver.d.ts +12 -0
  138. package/core/gather/driver.js +27 -0
  139. package/core/gather/gatherers/accessibility.js +4 -6
  140. package/core/gather/gatherers/css-usage.d.ts +0 -21
  141. package/core/gather/gatherers/css-usage.js +9 -119
  142. package/core/gather/gatherers/root-causes.d.ts +2 -2
  143. package/core/gather/gatherers/root-causes.js +6 -5
  144. package/core/gather/gatherers/stylesheets.d.ts +26 -0
  145. package/core/gather/gatherers/stylesheets.js +138 -0
  146. package/core/gather/gatherers/trace-elements.d.ts +4 -13
  147. package/core/gather/gatherers/trace-elements.js +42 -31
  148. package/core/gather/navigation-runner.js +3 -1
  149. package/core/gather/session.d.ts +8 -0
  150. package/core/gather/session.js +14 -1
  151. package/core/lib/emulation.js +2 -2
  152. package/core/lib/i18n/i18n.d.ts +0 -1
  153. package/core/lib/i18n/i18n.js +0 -2
  154. package/core/lib/lantern/cpu-node.d.ts +7 -1
  155. package/core/lib/lantern/cpu-node.js +12 -2
  156. package/core/lib/lantern/lantern-error.d.ts +8 -0
  157. package/core/lib/lantern/lantern-error.js +9 -0
  158. package/core/lib/lantern/metric.js +2 -6
  159. package/core/lib/lantern/metrics/first-contentful-paint.d.ts +79 -0
  160. package/core/lib/lantern/metrics/first-contentful-paint.js +200 -0
  161. package/core/lib/lantern/metrics/first-meaningful-paint.d.ts +6 -0
  162. package/core/lib/lantern/metrics/first-meaningful-paint.js +64 -0
  163. package/core/lib/lantern/metrics/interactive.d.ts +26 -0
  164. package/core/lib/lantern/metrics/interactive.js +112 -0
  165. package/core/lib/lantern/metrics/largest-contentful-paint.d.ts +19 -0
  166. package/core/lib/lantern/metrics/largest-contentful-paint.js +108 -0
  167. package/core/lib/lantern/metrics/max-potential-fid.d.ts +30 -0
  168. package/core/lib/lantern/metrics/max-potential-fid.js +92 -0
  169. package/core/lib/lantern/metrics/speed-index.d.ts +38 -0
  170. package/core/lib/lantern/metrics/speed-index.js +144 -0
  171. package/core/lib/lantern/metrics/total-blocking-time.d.ts +31 -0
  172. package/core/lib/lantern/metrics/total-blocking-time.js +128 -0
  173. package/core/lib/lantern/network-node.js +28 -4
  174. package/core/lib/lantern/page-dependency-graph.d.ts +4 -4
  175. package/core/lib/lantern/page-dependency-graph.js +19 -7
  176. package/core/lib/lantern/simulator/connection-pool.d.ts +2 -12
  177. package/core/lib/lantern/simulator/connection-pool.js +3 -22
  178. package/core/lib/lantern/simulator/network-analyzer.js +1 -12
  179. package/core/lib/lantern/simulator/simulator.d.ts +2 -4
  180. package/core/lib/lantern/simulator/simulator.js +7 -14
  181. package/core/lib/lantern/types/lantern.d.ts +5 -3
  182. package/core/lib/lantern-trace-saver.js +0 -1
  183. package/core/lib/lh-error.d.ts +9 -0
  184. package/core/lib/lh-error.js +13 -3
  185. package/core/lib/lighthouse-compatibility.js +53 -10
  186. package/core/lib/navigation-error.js +3 -0
  187. package/core/lib/network-request.d.ts +1 -0
  188. package/core/lib/network-request.js +29 -1
  189. package/core/lib/proto-preprocessor.js +24 -8
  190. package/core/lib/tracehouse/trace-processor.js +15 -2
  191. package/core/runner.js +18 -25
  192. package/dist/report/bundle.esm.js +11 -112
  193. package/dist/report/flow.js +16 -117
  194. package/dist/report/standalone.js +12 -113
  195. package/flow-report/src/help-dialog.tsx +0 -1
  196. package/flow-report/src/i18n/i18n.d.ts +0 -4
  197. package/flow-report/src/i18n/ui-strings.d.ts +0 -1
  198. package/flow-report/src/i18n/ui-strings.js +0 -2
  199. package/flow-report/src/summary/category.tsx +10 -12
  200. package/package.json +7 -7
  201. package/readme.md +4 -4
  202. package/report/assets/styles.css +1 -60
  203. package/report/assets/templates.html +0 -95
  204. package/report/renderer/category-renderer.js +1 -1
  205. package/report/renderer/components.d.ts +2 -2
  206. package/report/renderer/components.js +2 -103
  207. package/report/renderer/dom.d.ts +1 -1
  208. package/report/renderer/dom.js +1 -1
  209. package/report/renderer/performance-category-renderer.d.ts +0 -7
  210. package/report/renderer/performance-category-renderer.js +1 -32
  211. package/report/renderer/report-renderer.js +3 -13
  212. package/report/renderer/report-ui-features.js +1 -1
  213. package/report/renderer/report-utils.d.ts +0 -1
  214. package/report/renderer/report-utils.js +13 -13
  215. package/shared/localization/locales/ar-XB.json +62 -329
  216. package/shared/localization/locales/ar.json +62 -329
  217. package/shared/localization/locales/bg.json +62 -329
  218. package/shared/localization/locales/ca.json +62 -329
  219. package/shared/localization/locales/cs.json +62 -329
  220. package/shared/localization/locales/da.json +62 -329
  221. package/shared/localization/locales/de.json +62 -329
  222. package/shared/localization/locales/el.json +62 -329
  223. package/shared/localization/locales/en-GB.json +62 -329
  224. package/shared/localization/locales/en-US.json +101 -332
  225. package/shared/localization/locales/en-XA.json +62 -329
  226. package/shared/localization/locales/en-XL.json +101 -332
  227. package/shared/localization/locales/es-419.json +62 -329
  228. package/shared/localization/locales/es.json +62 -329
  229. package/shared/localization/locales/fi.json +62 -329
  230. package/shared/localization/locales/fil.json +61 -328
  231. package/shared/localization/locales/fr.json +62 -329
  232. package/shared/localization/locales/he.json +62 -329
  233. package/shared/localization/locales/hi.json +62 -329
  234. package/shared/localization/locales/hr.json +63 -330
  235. package/shared/localization/locales/hu.json +62 -329
  236. package/shared/localization/locales/id.json +62 -329
  237. package/shared/localization/locales/it.json +62 -329
  238. package/shared/localization/locales/ja.json +62 -329
  239. package/shared/localization/locales/ko.json +62 -329
  240. package/shared/localization/locales/lt.json +62 -329
  241. package/shared/localization/locales/lv.json +62 -329
  242. package/shared/localization/locales/nl.json +62 -329
  243. package/shared/localization/locales/no.json +62 -329
  244. package/shared/localization/locales/pl.json +61 -328
  245. package/shared/localization/locales/pt-PT.json +62 -329
  246. package/shared/localization/locales/pt.json +64 -331
  247. package/shared/localization/locales/ro.json +62 -329
  248. package/shared/localization/locales/ru.json +62 -329
  249. package/shared/localization/locales/sk.json +62 -329
  250. package/shared/localization/locales/sl.json +62 -329
  251. package/shared/localization/locales/sr-Latn.json +62 -329
  252. package/shared/localization/locales/sr.json +62 -329
  253. package/shared/localization/locales/sv.json +62 -329
  254. package/shared/localization/locales/ta.json +61 -328
  255. package/shared/localization/locales/te.json +62 -329
  256. package/shared/localization/locales/th.json +62 -329
  257. package/shared/localization/locales/tr.json +62 -329
  258. package/shared/localization/locales/uk.json +62 -329
  259. package/shared/localization/locales/vi.json +61 -328
  260. package/shared/localization/locales/zh-HK.json +62 -329
  261. package/shared/localization/locales/zh-TW.json +61 -328
  262. package/shared/localization/locales/zh.json +62 -329
  263. package/tsconfig.json +1 -3
  264. package/types/artifacts.d.ts +21 -110
  265. package/types/audit.d.ts +0 -10
  266. package/types/config.d.ts +0 -1
  267. package/types/externs.d.ts +0 -2
  268. package/types/gatherer.d.ts +3 -0
  269. package/types/internal/smokehouse.d.ts +1 -1
  270. package/types/lh.d.ts +0 -2
  271. package/types/lhr/audit-details.d.ts +9 -4
  272. package/types/lhr/lhr.d.ts +0 -4
  273. package/types/lhr/settings.d.ts +0 -4
  274. package/core/audits/accessibility/duplicate-id-active.js +0 -42
  275. package/core/audits/content-width.d.ts +0 -17
  276. package/core/audits/content-width.js +0 -76
  277. package/core/audits/installable-manifest.d.ts +0 -111
  278. package/core/audits/installable-manifest.js +0 -248
  279. package/core/audits/layout-shift-elements.d.ts +0 -16
  280. package/core/audits/layout-shift-elements.js +0 -101
  281. package/core/audits/manual/pwa-cross-browser.d.ts +0 -12
  282. package/core/audits/manual/pwa-cross-browser.js +0 -40
  283. package/core/audits/manual/pwa-each-page-has-url.d.ts +0 -12
  284. package/core/audits/manual/pwa-each-page-has-url.js +0 -38
  285. package/core/audits/manual/pwa-page-transitions.d.ts +0 -12
  286. package/core/audits/manual/pwa-page-transitions.js +0 -38
  287. package/core/audits/maskable-icon.d.ts +0 -25
  288. package/core/audits/maskable-icon.js +0 -70
  289. package/core/audits/multi-check-audit.d.ts +0 -28
  290. package/core/audits/multi-check-audit.js +0 -82
  291. package/core/audits/no-unload-listeners.d.ts +0 -16
  292. package/core/audits/no-unload-listeners.js +0 -86
  293. package/core/audits/performance-budget.d.ts +0 -39
  294. package/core/audits/performance-budget.js +0 -154
  295. package/core/audits/script-elements-test-audit.d.ts +0 -14
  296. package/core/audits/script-elements-test-audit.js +0 -29
  297. package/core/audits/seo/plugins.d.ts +0 -15
  298. package/core/audits/seo/plugins.js +0 -150
  299. package/core/audits/seo/tap-targets.d.ts +0 -50
  300. package/core/audits/seo/tap-targets.js +0 -352
  301. package/core/audits/splash-screen.d.ts +0 -37
  302. package/core/audits/splash-screen.js +0 -98
  303. package/core/audits/themed-omnibox.d.ts +0 -42
  304. package/core/audits/themed-omnibox.js +0 -102
  305. package/core/audits/timing-budget.d.ts +0 -43
  306. package/core/audits/timing-budget.js +0 -175
  307. package/core/computed/manifest-values.d.ts +0 -24
  308. package/core/computed/manifest-values.js +0 -136
  309. package/core/config/budget.d.ts +0 -74
  310. package/core/config/budget.js +0 -340
  311. package/core/config/metrics-to-audits.d.ts +0 -19
  312. package/core/config/metrics-to-audits.js +0 -62
  313. package/core/gather/driver/service-workers.d.ts +0 -16
  314. package/core/gather/driver/service-workers.js +0 -52
  315. package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.d.ts +0 -47
  316. package/core/gather/gatherers/dobetterweb/tags-blocking-first-paint.js +0 -233
  317. package/core/gather/gatherers/global-listeners.d.ts +0 -27
  318. package/core/gather/gatherers/global-listeners.js +0 -108
  319. package/core/gather/gatherers/installability-errors.d.ts +0 -17
  320. package/core/gather/gatherers/installability-errors.js +0 -56
  321. package/core/gather/gatherers/script-elements.d.ts +0 -21
  322. package/core/gather/gatherers/script-elements.js +0 -100
  323. package/core/gather/gatherers/seo/embedded-content.d.ts +0 -10
  324. package/core/gather/gatherers/seo/embedded-content.js +0 -63
  325. package/core/gather/gatherers/seo/tap-targets.d.ts +0 -21
  326. package/core/gather/gatherers/seo/tap-targets.js +0 -389
  327. package/core/gather/gatherers/service-worker.d.ts +0 -10
  328. package/core/gather/gatherers/service-worker.js +0 -32
  329. package/core/gather/gatherers/web-app-manifest.d.ts +0 -32
  330. package/core/gather/gatherers/web-app-manifest.js +0 -106
  331. package/report/renderer/pwa-category-renderer.d.ts +0 -55
  332. package/report/renderer/pwa-category-renderer.js +0 -184
  333. package/third-party/chromium-synchronization/installability-errors-test.js +0 -79
  334. package/types/lhr/budget.d.ts +0 -60
@@ -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
 
@@ -140,15 +140,6 @@ class NetworkAnalyzer {
140
140
  const {timing, connectionReused, record} = info;
141
141
  if (connectionReused) return;
142
142
 
143
- // In LR, network records are missing connection timing, but we've smuggled it in via headers.
144
- if (global.isLightrider && record.lrStatistics) {
145
- if (record.protocol.startsWith('h3')) {
146
- return record.lrStatistics.TCPMs;
147
- } else {
148
- return [record.lrStatistics.TCPMs / 2, record.lrStatistics.TCPMs / 2];
149
- }
150
- }
151
-
152
143
  const {connectStart, sslStart, sslEnd, connectEnd} = timing;
153
144
  if (connectEnd >= 0 && connectStart >= 0 && record.protocol.startsWith('h3')) {
154
145
  // These values are equal to sslStart and sslEnd for h3.
@@ -256,9 +247,7 @@ class NetworkAnalyzer {
256
247
  */
257
248
  static _estimateResponseTimeByOrigin(records, rttByOrigin) {
258
249
  return NetworkAnalyzer._estimateValueByOrigin(records, ({record, timing}) => {
259
- // Lightrider does not have timings for sendEnd, but we do have this timing which should be
260
- // close to the response time.
261
- if (global.isLightrider && record.lrStatistics) return record.lrStatistics.requestMs;
250
+ if (record.serverResponseTime !== undefined) return record.serverResponseTime;
262
251
 
263
252
  if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
264
253
  if (!Number.isFinite(timing.sendEnd) || timing.sendEnd < 0) return;
@@ -29,7 +29,6 @@ export class Simulator<T = any> {
29
29
  _layoutTaskMultiplier: number;
30
30
  /** @type {Array<Node>} */
31
31
  _cachedNodeListByStartPosition: Array<Node>;
32
- _flexibleOrdering: boolean;
33
32
  _nodeTimings: SimulatorTimingMap;
34
33
  /** @type {Map<string, number>} */
35
34
  _numberInProgressByType: Map<string, number>;
@@ -131,17 +130,16 @@ export class Simulator<T = any> {
131
130
  * Estimates the time taken to process all of the graph's nodes, returns the overall time along with
132
131
  * each node annotated by start/end times.
133
132
  *
134
- * If flexibleOrdering is set, simulator/connection pool are allowed to deviate from what was
133
+ * Simulator/connection pool are allowed to deviate from what was
135
134
  * observed in the trace/devtoolsLog and start requests as soon as they are queued (i.e. do not
136
135
  * wait around for a warm connection to be available if the original record was fetched on a warm
137
136
  * connection).
138
137
  *
139
138
  * @param {Node} graph
140
- * @param {{flexibleOrdering?: boolean, label?: string}=} options
139
+ * @param {{label?: string}=} options
141
140
  * @return {Lantern.Simulation.Result<T>}
142
141
  */
143
142
  simulate(graph: Node, options?: {
144
- flexibleOrdering?: boolean;
145
143
  label?: string;
146
144
  } | undefined): Lantern.Simulation.Result<T>;
147
145
  /**
@@ -80,7 +80,6 @@ class Simulator {
80
80
  this._cachedNodeListByStartPosition = [];
81
81
 
82
82
  // Properties reset on every `.simulate` call but duplicated here for type checking
83
- this._flexibleOrdering = false;
84
83
  this._nodeTimings = new SimulatorTimingMap();
85
84
  /** @type {Map<string, number>} */
86
85
  this._numberInProgressByType = new Map();
@@ -197,9 +196,7 @@ class Simulator {
197
196
  * @return {?TcpConnection}
198
197
  */
199
198
  _acquireConnection(record) {
200
- return this._connectionPool.acquire(record, {
201
- ignoreConnectionReused: this._flexibleOrdering,
202
- });
199
+ return this._connectionPool.acquire(record);
203
200
  }
204
201
 
205
202
  /**
@@ -276,7 +273,7 @@ class Simulator {
276
273
  ? this._layoutTaskMultiplier
277
274
  : this._cpuSlowdownMultiplier;
278
275
  const totalDuration = Math.min(
279
- Math.round(cpuNode.event.dur / 1000 * multiplier),
276
+ Math.round(cpuNode.duration / 1000 * multiplier),
280
277
  DEFAULT_MAXIMUM_CPU_TASK_DURATION
281
278
  );
282
279
  const estimatedTimeElapsed = totalDuration - timingData.timeElapsed;
@@ -425,13 +422,13 @@ class Simulator {
425
422
  * Estimates the time taken to process all of the graph's nodes, returns the overall time along with
426
423
  * each node annotated by start/end times.
427
424
  *
428
- * If flexibleOrdering is set, simulator/connection pool are allowed to deviate from what was
425
+ * Simulator/connection pool are allowed to deviate from what was
429
426
  * observed in the trace/devtoolsLog and start requests as soon as they are queued (i.e. do not
430
427
  * wait around for a warm connection to be available if the original record was fetched on a warm
431
428
  * connection).
432
429
  *
433
430
  * @param {Node} graph
434
- * @param {{flexibleOrdering?: boolean, label?: string}=} options
431
+ * @param {{label?: string}=} options
435
432
  * @return {Lantern.Simulation.Result<T>}
436
433
  */
437
434
  simulate(graph, options) {
@@ -441,11 +438,9 @@ class Simulator {
441
438
 
442
439
  options = Object.assign({
443
440
  label: undefined,
444
- flexibleOrdering: false,
445
441
  }, options);
446
442
 
447
443
  // initialize the necessary data containers
448
- this._flexibleOrdering = !!options.flexibleOrdering;
449
444
  this._dns = new DNSCache({rtt: this._rtt});
450
445
  this._initializeConnectionPool(graph);
451
446
  this._initializeAuxiliaryData();
@@ -470,11 +465,9 @@ class Simulator {
470
465
  }
471
466
 
472
467
  if (!nodesInProgress.size) {
473
- // interplay between fromDiskCache and connectionReused can be incorrect
474
- // proceed with flexibleOrdering if we can, otherwise give up
475
- if (this._flexibleOrdering) throw new Error('Failed to start a node');
476
- this._flexibleOrdering = true;
477
- continue;
468
+ // Interplay between fromDiskCache and connectionReused can be incorrect,
469
+ // have to give up.
470
+ throw new Error('Failed to start a node');
478
471
  }
479
472
 
480
473
  // set the available throughput for all connections based on # inflight
@@ -67,9 +67,6 @@ export class NetworkRequest<T = any> {
67
67
  resourceSize: number;
68
68
  fromDiskCache: boolean;
69
69
  fromMemoryCache: boolean;
70
- // TODO(15841): remove from lantern.
71
- /** Extra timing information available only when run in Lightrider. */
72
- lrStatistics: LightriderStatistics | undefined;
73
70
  finished: boolean;
74
71
  statusCode: number;
75
72
  /** The network request that this one redirected to */
@@ -80,6 +77,11 @@ export class NetworkRequest<T = any> {
80
77
  /** The chain of network requests that redirected to this one */
81
78
  redirects: NetworkRequest[] | undefined;
82
79
  timing: LH.Crdp.Network.ResourceTiming | undefined;
80
+ /**
81
+ * Optional value for how long the server took to respond to this request.
82
+ * When not provided, the server response time is derived from the timing object.
83
+ */
84
+ serverResponseTime: number | undefined;
83
85
  resourceType: LH.Crdp.Network.ResourceType | undefined;
84
86
  mimeType: string;
85
87
  priority: LH.Crdp.Network.ResourcePriority;
@@ -255,7 +255,6 @@ export default {
255
255
  'unlabeled',
256
256
  // These node timings should be nearly identical to the ones produced for Interactive
257
257
  'optimisticSpeedIndex',
258
- 'optimisticFlexSpeedIndex',
259
258
  'pessimisticSpeedIndex',
260
259
  ],
261
260
  convertNodeTimingsToTrace,
@@ -105,6 +105,7 @@ export namespace UIStrings {
105
105
  const missingRequiredArtifact: string;
106
106
  const erroredRequiredArtifact: string;
107
107
  const oldChromeDoesNotSupportFeature: string;
108
+ const targetCrashed: string;
108
109
  }
109
110
  declare namespace ERRORS {
110
111
  namespace NO_SPEEDLINE_FRAMES {
@@ -340,6 +341,14 @@ declare namespace ERRORS {
340
341
  import message_30 = UIStrings.erroredRequiredArtifact;
341
342
  export { message_30 as message };
342
343
  }
344
+ namespace TARGET_CRASHED {
345
+ const code_31: string;
346
+ export { code_31 as code };
347
+ import message_31 = UIStrings.targetCrashed;
348
+ export { message_31 as message };
349
+ const lhrRuntimeError_22: boolean;
350
+ export { lhrRuntimeError_22 as lhrRuntimeError };
351
+ }
343
352
  }
344
353
  export {};
345
354
  //# sourceMappingURL=lh-error.d.ts.map
@@ -76,13 +76,13 @@ const UIStrings = {
76
76
  criTimeout: 'Timeout waiting for initial Debugger Protocol connection.',
77
77
  /**
78
78
  * @description Error message explaining that a resource that was required for testing was never collected. "artifactName" will be replaced with the name of the resource that wasn't collected.
79
- * @example {WebAppManifest} artifactName
79
+ * @example {MainDocumentContent} artifactName
80
80
  * */
81
81
  missingRequiredArtifact: 'Required {artifactName} gatherer did not run.',
82
82
  /**
83
83
  * @description Error message explaining that there was an error while trying to collect a resource that was required for testing. "artifactName" will be replaced with the name of the resource that wasn't collected; "errorMessage" will be replaced with a string description of the error that occurred.
84
- * @example {WebAppManifest} artifactName
85
- * @example {Manifest invalid} errorMessage
84
+ * @example {MainDocumentContent} artifactName
85
+ * @example {Could not find main document} errorMessage
86
86
  * */
87
87
  erroredRequiredArtifact: 'Required {artifactName} gatherer encountered an error: {errorMessage}',
88
88
 
@@ -91,6 +91,9 @@ const UIStrings = {
91
91
  * @example {Largest Contentful Paint} featureName
92
92
  * */
93
93
  oldChromeDoesNotSupportFeature: 'This version of Chrome is too old to support \'{featureName}\'. Use a newer version to see full results.',
94
+
95
+ /** Error message explaining that the browser tab that Lighthouse is inspecting has crashed. */
96
+ targetCrashed: 'Browser tab has unexpectedly crashed.',
94
97
  };
95
98
 
96
99
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
@@ -423,6 +426,13 @@ const ERRORS = {
423
426
  message: UIStrings.erroredRequiredArtifact,
424
427
  },
425
428
 
429
+ /** The page has crashed and will no longer respond to 99% of CDP commmands. */
430
+ TARGET_CRASHED: {
431
+ code: 'TARGET_CRASHED',
432
+ message: UIStrings.targetCrashed,
433
+ lhrRuntimeError: true,
434
+ },
435
+
426
436
  // Hey! When adding a new error type, update lighthouse-result.proto too.
427
437
  // Only necessary for runtime errors, which come from artifacts or pageLoadErrors.
428
438
  };
@@ -108,19 +108,62 @@ function upgradeLhrForCompatibility(lhr) {
108
108
  }
109
109
  }
110
110
 
111
- // This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
112
- // Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
113
- // Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
111
+ // This backcompat converts old LHRs to use the "hidden" and "diagnostics" groups.
112
+ // <9.0.0 LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
113
+ // <9.0.0 LHRs used "load-opportunities" for opportunity audits that are now grouped under "diagnostics".
114
+ // >=9.0.0 && <12.0.0 LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
115
+ // >=12.0.0 LHRs use the "hidden" group to identify hidden audits and "diagnostics" to identify diagnostics. "no group" is meaningless.
114
116
  const [majorVersion] = lhr.lighthouseVersion.split('.').map(Number);
115
117
  const perfCategory = lhr.categories['performance'];
116
- if (majorVersion < 9 && perfCategory) {
117
- if (!lhr.categoryGroups) lhr.categoryGroups = {};
118
- lhr.categoryGroups['hidden'] = {title: ''};
118
+ if (perfCategory) {
119
+ if (majorVersion < 9) {
120
+ if (!lhr.categoryGroups) lhr.categoryGroups = {};
121
+ lhr.categoryGroups['hidden'] = {title: ''};
122
+ for (const auditRef of perfCategory.auditRefs) {
123
+ if (!auditRef.group) {
124
+ auditRef.group = 'hidden';
125
+ } else if (auditRef.group === 'load-opportunities') {
126
+ auditRef.group = 'diagnostics';
127
+ }
128
+ }
129
+ } else if (majorVersion < 12) {
130
+ for (const auditRef of perfCategory.auditRefs) {
131
+ if (!auditRef.group) {
132
+ auditRef.group = 'diagnostics';
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ if (majorVersion < 12 && perfCategory) {
139
+ /** @type {Map<string, string[]>} */
140
+ const metricRelevanceMap = new Map();
141
+
119
142
  for (const auditRef of perfCategory.auditRefs) {
120
- if (!auditRef.group) {
121
- auditRef.group = 'hidden';
122
- } else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
123
- delete auditRef.group;
143
+ /** @type {string[]|undefined} */
144
+ // @ts-expect-error Removed in v12
145
+ const relevantAudits = auditRef.relevantAudits;
146
+ if (!relevantAudits || !auditRef.acronym) continue;
147
+
148
+ for (const auditId of relevantAudits) {
149
+ const acronyms = metricRelevanceMap.get(auditId) || [];
150
+ acronyms.push(auditRef.acronym);
151
+ metricRelevanceMap.set(auditId, acronyms);
152
+ }
153
+ }
154
+
155
+ for (const [auditId, acronyms] of metricRelevanceMap) {
156
+ if (!acronyms.length) continue;
157
+
158
+ const audit = lhr.audits[auditId];
159
+ if (!audit) continue;
160
+
161
+ // Old versions can still define metric savings, let's not mess with it.
162
+ if (audit.metricSavings) continue;
163
+
164
+ audit.metricSavings = {};
165
+ for (const acronym of acronyms) {
166
+ audit.metricSavings[acronym] = 0;
124
167
  }
125
168
  }
126
169
  }
@@ -150,6 +150,9 @@ function getPageLoadError(navigationError, context) {
150
150
  let finalRecord;
151
151
  if (mainRecord) {
152
152
  finalRecord = NetworkAnalyzer.resolveRedirects(mainRecord);
153
+ } else {
154
+ // We have no network requests to process, use the navError
155
+ return navigationError;
153
156
  }
154
157
 
155
158
  if (finalRecord?.mimeType === XHTML_MIME_TYPE) {