lighthouse 13.5.0 → 13.6.0-dev.20261027

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 (266) hide show
  1. package/.agents/skills/update-ard-port/SKILL.md +171 -0
  2. package/.agents/skills/update-trace-engine/SKILL.md +154 -0
  3. package/cli/bin.d.ts +2 -2
  4. package/cli/bin.js +5 -4
  5. package/cli/cli-flags.d.ts +16 -1
  6. package/cli/cli-flags.js +53 -2
  7. package/cli/printer.js +5 -8
  8. package/cli/run.d.ts +5 -5
  9. package/cli/run.js +97 -47
  10. package/cli/sentry-prompt.js +3 -4
  11. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +1 -1
  12. package/cli/test/smokehouse/config/exclusions.js +3 -1
  13. package/cli/test/smokehouse/frontends/smokehouse-bin.js +5 -3
  14. package/cli/test/smokehouse/report-assert-test.js +65 -0
  15. package/cli/test/smokehouse/report-assert.js +24 -39
  16. package/cli/test/smokehouse/smokehouse-test.d.ts +2 -0
  17. package/cli/test/smokehouse/smokehouse-test.js +120 -0
  18. package/cli/test/smokehouse/smokehouse.js +37 -15
  19. package/cli/test/smokehouse/version-check-test.js +5 -0
  20. package/cli/test/smokehouse/version-check.d.ts +1 -1
  21. package/cli/test/smokehouse/version-check.js +1 -1
  22. package/commitlint.config.js +1 -1
  23. package/core/audits/accessibility/accesskeys.js +1 -1
  24. package/core/audits/accessibility/aria-allowed-attr.js +1 -1
  25. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  26. package/core/audits/accessibility/aria-command-name.js +1 -1
  27. package/core/audits/accessibility/aria-conditional-attr.js +1 -1
  28. package/core/audits/accessibility/aria-deprecated-role.js +1 -1
  29. package/core/audits/accessibility/aria-dialog-name.js +1 -1
  30. package/core/audits/accessibility/aria-hidden-body.js +1 -1
  31. package/core/audits/accessibility/aria-hidden-focus.js +1 -1
  32. package/core/audits/accessibility/aria-input-field-name.js +1 -1
  33. package/core/audits/accessibility/aria-meter-name.js +1 -1
  34. package/core/audits/accessibility/aria-progressbar-name.js +1 -1
  35. package/core/audits/accessibility/aria-prohibited-attr.js +1 -1
  36. package/core/audits/accessibility/aria-required-attr.js +1 -1
  37. package/core/audits/accessibility/aria-required-children.js +1 -1
  38. package/core/audits/accessibility/aria-required-parent.js +1 -1
  39. package/core/audits/accessibility/aria-roles.js +1 -1
  40. package/core/audits/accessibility/aria-text.js +1 -1
  41. package/core/audits/accessibility/aria-toggle-field-name.js +1 -1
  42. package/core/audits/accessibility/aria-tooltip-name.js +1 -1
  43. package/core/audits/accessibility/aria-treeitem-name.js +1 -1
  44. package/core/audits/accessibility/aria-valid-attr-value.js +1 -1
  45. package/core/audits/accessibility/aria-valid-attr.js +1 -1
  46. package/core/audits/accessibility/autocomplete-valid.js +1 -1
  47. package/core/audits/accessibility/button-name.js +1 -1
  48. package/core/audits/accessibility/bypass.js +1 -1
  49. package/core/audits/accessibility/color-contrast.js +1 -1
  50. package/core/audits/accessibility/definition-list.js +1 -1
  51. package/core/audits/accessibility/dlitem.js +1 -1
  52. package/core/audits/accessibility/document-title.js +1 -1
  53. package/core/audits/accessibility/duplicate-id-aria.js +1 -1
  54. package/core/audits/accessibility/empty-heading.js +1 -1
  55. package/core/audits/accessibility/form-field-multiple-labels.js +1 -1
  56. package/core/audits/accessibility/frame-title.js +1 -1
  57. package/core/audits/accessibility/heading-order.js +1 -1
  58. package/core/audits/accessibility/html-has-lang.js +1 -1
  59. package/core/audits/accessibility/html-lang-valid.js +1 -1
  60. package/core/audits/accessibility/html-xml-lang-mismatch.js +1 -1
  61. package/core/audits/accessibility/identical-links-same-purpose.js +1 -1
  62. package/core/audits/accessibility/image-alt.js +1 -1
  63. package/core/audits/accessibility/image-redundant-alt.js +1 -1
  64. package/core/audits/accessibility/input-button-name.js +1 -1
  65. package/core/audits/accessibility/input-image-alt.js +1 -1
  66. package/core/audits/accessibility/label-content-name-mismatch.js +1 -1
  67. package/core/audits/accessibility/label.js +1 -1
  68. package/core/audits/accessibility/landmark-one-main.js +1 -1
  69. package/core/audits/accessibility/link-in-text-block.js +1 -1
  70. package/core/audits/accessibility/link-name.js +1 -1
  71. package/core/audits/accessibility/list.js +1 -1
  72. package/core/audits/accessibility/listitem.js +1 -1
  73. package/core/audits/accessibility/meta-refresh.js +1 -1
  74. package/core/audits/accessibility/meta-viewport.js +1 -1
  75. package/core/audits/accessibility/object-alt.js +1 -1
  76. package/core/audits/accessibility/presentation-role-conflict.js +1 -1
  77. package/core/audits/accessibility/select-name.js +1 -1
  78. package/core/audits/accessibility/skip-link.js +1 -1
  79. package/core/audits/accessibility/svg-img-alt.js +1 -1
  80. package/core/audits/accessibility/tabindex.js +1 -1
  81. package/core/audits/accessibility/table-duplicate-name.js +1 -1
  82. package/core/audits/accessibility/table-fake-caption.js +1 -1
  83. package/core/audits/accessibility/target-size.js +1 -1
  84. package/core/audits/accessibility/td-has-header.js +1 -1
  85. package/core/audits/accessibility/td-headers-attr.js +1 -1
  86. package/core/audits/accessibility/th-has-data-cells.js +1 -1
  87. package/core/audits/accessibility/valid-lang.js +1 -1
  88. package/core/audits/accessibility/video-caption.js +1 -1
  89. package/core/audits/agentic/ard-schema.d.ts +13 -0
  90. package/core/audits/agentic/ard-schema.js +81 -33
  91. package/core/audits/agentic/llms-txt.js +5 -2
  92. package/core/audits/baseline.d.ts +29 -7
  93. package/core/audits/baseline.js +19 -6
  94. package/core/audits/byte-efficiency/byte-efficiency-audit.js +2 -2
  95. package/core/audits/insights/insight-audit.d.ts +9 -0
  96. package/core/audits/insights/insight-audit.js +35 -28
  97. package/core/audits/long-tasks.js +2 -1
  98. package/core/audits/predictive-perf.js +2 -2
  99. package/core/audits/screenshot-thumbnails.d.ts +0 -13
  100. package/core/audits/screenshot-thumbnails.js +0 -45
  101. package/core/audits/script-treemap-data.js +26 -6
  102. package/core/audits/seo/robots-txt.js +3 -8
  103. package/core/audits/unsized-images.d.ts +0 -7
  104. package/core/audits/unsized-images.js +7 -26
  105. package/core/computed/lcp-image-record.d.ts +2 -23
  106. package/core/computed/lcp-image-record.js +3 -67
  107. package/core/computed/lcp-network-record.d.ts +24 -0
  108. package/core/computed/lcp-network-record.js +74 -0
  109. package/core/computed/metrics/lantern-metric.js +1 -1
  110. package/core/computed/metrics/lcp-breakdown.js +6 -6
  111. package/core/computed/metrics/time-to-first-byte.js +5 -3
  112. package/core/computed/navigation-insights.js +3 -2
  113. package/core/computed/page-dependency-graph.js +55 -5
  114. package/core/computed/speedline.js +20 -5
  115. package/core/computed/trace-engine-result.js +25 -5
  116. package/core/computed/unused-css.js +15 -2
  117. package/core/config/config-helpers.d.ts +5 -0
  118. package/core/config/config-helpers.js +8 -28
  119. package/core/config/config.js +1 -2
  120. package/core/config/constants.d.ts +2 -28
  121. package/core/config/lr-desktop-config.js +13 -0
  122. package/core/config/lr-mobile-config.js +13 -0
  123. package/core/gather/base-artifacts.js +7 -1
  124. package/core/gather/driver/storage.js +3 -2
  125. package/core/gather/gatherers/agentic/ard.d.ts +59 -4
  126. package/core/gather/gatherers/agentic/ard.js +218 -56
  127. package/core/gather/gatherers/full-page-screenshot.js +2 -2
  128. package/core/gather/gatherers/image-elements.d.ts +7 -0
  129. package/core/gather/gatherers/image-elements.js +75 -24
  130. package/core/gather/gatherers/inspector-issues.js +2 -0
  131. package/core/gather/gatherers/js-usage.js +2 -0
  132. package/core/gather/gatherers/scripts.d.ts +16 -3
  133. package/core/gather/gatherers/scripts.js +157 -14
  134. package/core/gather/gatherers/stylesheets.js +2 -0
  135. package/core/gather/gatherers/trace-elements.js +2 -1
  136. package/core/gather/gatherers/trace.d.ts +6 -0
  137. package/core/gather/gatherers/trace.js +56 -16
  138. package/core/lib/arbitrary-equality-map.d.ts +14 -3
  139. package/core/lib/arbitrary-equality-map.js +113 -6
  140. package/core/lib/baseline/web-features-data.json +32 -11
  141. package/core/lib/baseline/web-features-metadata.json +1 -1
  142. package/core/lib/bf-cache-strings.js +237 -254
  143. package/core/lib/cdt/Common.d.ts +1 -1
  144. package/core/lib/cdt/Common.js +2 -2
  145. package/core/lib/cdt/Platform.d.ts +7 -7
  146. package/core/lib/cdt/Platform.js +11 -8
  147. package/core/lib/cdt/SDK.d.ts +5 -1
  148. package/core/lib/cdt/SDK.js +4 -2
  149. package/core/lib/cdt/generated/ParsedURL.d.ts +0 -1
  150. package/core/lib/cdt/generated/ParsedURL.js +3 -9
  151. package/core/lib/cdt/generated/SourceMap.d.ts +24 -25
  152. package/core/lib/cdt/generated/SourceMap.js +7 -15
  153. package/core/lib/deprecations-strings.d.ts +93 -88
  154. package/core/lib/deprecations-strings.js +20 -14
  155. package/core/lib/i18n/i18n.d.ts +0 -1
  156. package/core/lib/i18n/i18n.js +17 -11
  157. package/core/lib/legacy-javascript/legacy-javascript.d.ts +3 -2
  158. package/core/lib/legacy-javascript/legacy-javascript.js +235 -111
  159. package/core/lib/legacy-javascript/polyfill-graph-data.json +94 -89
  160. package/core/lib/legacy-javascript/polyfill-module-data.json +48 -7
  161. package/core/lib/minification-estimator.js +10 -10
  162. package/core/lib/network-request.d.ts +2 -0
  163. package/core/lib/network-request.js +21 -0
  164. package/core/lib/page-functions.js +2 -5
  165. package/core/lib/sentry.d.ts +1 -0
  166. package/core/lib/sentry.js +3 -0
  167. package/core/lib/trace-engine.d.ts +1 -0
  168. package/core/lib/trace-engine.js +2 -0
  169. package/core/runner.js +9 -3
  170. package/dist/report/bundle.esm.js +10 -10
  171. package/dist/report/flow.js +13 -13
  172. package/dist/report/standalone.js +5 -5
  173. package/flow-report/src/common.tsx +5 -2
  174. package/flow-report/src/i18n/i18n.d.ts +2 -0
  175. package/flow-report/src/summary/summary.tsx +9 -1
  176. package/flow-report/src/util.d.ts +1 -1
  177. package/package.json +62 -98
  178. package/readme.md +13 -18
  179. package/report/generator/file-namer.d.ts +1 -1
  180. package/report/generator/file-namer.js +5 -4
  181. package/report/generator/flow-report-assets.js +1 -3
  182. package/report/generator/report-assets.js +1 -2
  183. package/report/renderer/category-renderer.js +2 -2
  184. package/report/renderer/details-renderer.js +3 -2
  185. package/report/renderer/dom.d.ts +8 -2
  186. package/report/renderer/dom.js +29 -1
  187. package/report/renderer/element-screenshot-renderer.js +3 -0
  188. package/report/renderer/performance-category-renderer.js +3 -1
  189. package/report/renderer/report-renderer.js +4 -0
  190. package/report/renderer/report-utils.d.ts +1 -0
  191. package/report/renderer/report-utils.js +2 -0
  192. package/report/renderer/text-encoding.js +2 -2
  193. package/report/types/augment-dom.d.ts +4 -0
  194. package/shared/esm-utils.d.ts +0 -8
  195. package/shared/esm-utils.js +0 -18
  196. package/shared/localization/format.js +1 -2
  197. package/shared/localization/locales/ar-XB.json +157 -142
  198. package/shared/localization/locales/ar.json +157 -142
  199. package/shared/localization/locales/bg.json +154 -139
  200. package/shared/localization/locales/ca.json +157 -142
  201. package/shared/localization/locales/cs.json +148 -133
  202. package/shared/localization/locales/da.json +153 -138
  203. package/shared/localization/locales/de.json +152 -137
  204. package/shared/localization/locales/el.json +153 -138
  205. package/shared/localization/locales/en-GB.json +160 -145
  206. package/shared/localization/locales/en-US.json +239 -218
  207. package/shared/localization/locales/en-XL.json +239 -218
  208. package/shared/localization/locales/es-419.json +150 -135
  209. package/shared/localization/locales/es.json +149 -134
  210. package/shared/localization/locales/fi.json +151 -136
  211. package/shared/localization/locales/fil.json +154 -139
  212. package/shared/localization/locales/fr.json +150 -135
  213. package/shared/localization/locales/he.json +160 -145
  214. package/shared/localization/locales/hi.json +156 -141
  215. package/shared/localization/locales/hr.json +154 -139
  216. package/shared/localization/locales/hu.json +111 -96
  217. package/shared/localization/locales/id.json +153 -138
  218. package/shared/localization/locales/it.json +153 -138
  219. package/shared/localization/locales/ja.json +150 -135
  220. package/shared/localization/locales/ko.json +42 -27
  221. package/shared/localization/locales/lt.json +152 -137
  222. package/shared/localization/locales/lv.json +93 -78
  223. package/shared/localization/locales/nl.json +149 -134
  224. package/shared/localization/locales/no.json +152 -137
  225. package/shared/localization/locales/pl.json +153 -138
  226. package/shared/localization/locales/pt-PT.json +152 -137
  227. package/shared/localization/locales/pt.json +157 -142
  228. package/shared/localization/locales/ro.json +149 -134
  229. package/shared/localization/locales/ru.json +76 -61
  230. package/shared/localization/locales/sk.json +153 -138
  231. package/shared/localization/locales/sl.json +149 -134
  232. package/shared/localization/locales/sr-Latn.json +151 -136
  233. package/shared/localization/locales/sr.json +151 -136
  234. package/shared/localization/locales/sv.json +152 -137
  235. package/shared/localization/locales/ta.json +156 -141
  236. package/shared/localization/locales/te.json +157 -142
  237. package/shared/localization/locales/th.json +111 -96
  238. package/shared/localization/locales/tr.json +153 -138
  239. package/shared/localization/locales/uk.json +149 -134
  240. package/shared/localization/locales/vi.json +162 -147
  241. package/shared/localization/locales/zh-HK.json +150 -135
  242. package/shared/localization/locales/zh-TW.json +154 -139
  243. package/shared/localization/locales/zh.json +152 -137
  244. package/shared/localization/swap-locale.js +10 -5
  245. package/shared/root.js +1 -3
  246. package/third-party/ard/README.md +19 -10
  247. package/third-party/ard/ard-test.js +189 -266
  248. package/third-party/ard/ard.d.ts +18 -1
  249. package/third-party/ard/ard.js +282 -45
  250. package/third-party/ard/schema-validator.d.ts +14 -5
  251. package/third-party/ard/schema-validator.js +921 -1177
  252. package/third-party/ard/spec/schemas/ard-entry.schema.json +176 -0
  253. package/tsconfig.json +0 -2
  254. package/types/artifacts.d.ts +43 -7
  255. package/types/externs.d.ts +4 -0
  256. package/types/internal/test.d.ts +1 -0
  257. package/build-tracker.config.js +0 -16
  258. package/core/lib/cdt/package.json +0 -4
  259. package/eslint-local-rules.d.cts +0 -9
  260. package/eslint.config.mjs +0 -242
  261. package/third-party/ard/spec/schemas/ai-catalog.schema.json +0 -292
  262. package/third-party/esbuild-plugins-polyfills/LICENSE +0 -3
  263. package/third-party/esbuild-plugins-polyfills/README.md +0 -7
  264. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.d.ts +0 -9
  265. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.js +0 -129
  266. package/types/internal/lookup-closest-locale.d.ts +0 -13
@@ -5,19 +5,31 @@
5
5
  */
6
6
 
7
7
  import BaseGatherer from '../base-gatherer.js';
8
+ import Trace from './trace.js';
9
+
10
+ /**
11
+ * @typedef {Object} TraceScript
12
+ * @property {string} scriptId
13
+ * @property {string} [sourceText]
14
+ * @property {string[]} [chunks]
15
+ * @property {string} [url]
16
+ * @property {string} [sourceUrl]
17
+ * @property {number} [executionContextId]
18
+ * @property {string} [frame]
19
+ */
8
20
 
9
21
  /**
10
22
  * @template T, U
11
23
  * @param {Array<T>} values
12
- * @param {(value: T) => Promise<U>} promiseMapper
24
+ * @param {(value: T, index: number) => Promise<U>} promiseMapper
13
25
  * @param {boolean} runInSeries
14
26
  * @return {Promise<Array<U>>}
15
27
  */
16
28
  async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
17
29
  if (runInSeries) {
18
30
  const results = [];
19
- for (const value of values) {
20
- const result = await promiseMapper(value);
31
+ for (let i = 0; i < values.length; i++) {
32
+ const result = await promiseMapper(values[i], i);
21
33
  results.push(result);
22
34
  }
23
35
  return results;
@@ -27,6 +39,121 @@ async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
27
39
  }
28
40
  }
29
41
 
42
+ /**
43
+ * Extracts script source text and disambiguating metadata from trace rundown events,
44
+ * grouped by `scriptId`.
45
+ * @param {LH.TraceEvent[]} traceEvents
46
+ * @return {Map<string, TraceScript[]>}
47
+ */
48
+ function collectTraceScripts(traceEvents) {
49
+ /** @type {Map<string, TraceScript>} */
50
+ const byIsolateAndScriptId = new Map();
51
+
52
+ /**
53
+ * @param {NonNullable<LH.TraceEvent['args']['data']>} data
54
+ * @return {TraceScript}
55
+ */
56
+ const getOrCreateRecord = (data) => {
57
+ const scriptId = String(data.scriptId);
58
+ const key = `${data.isolate ?? ''}.${scriptId}`;
59
+ let record = byIsolateAndScriptId.get(key);
60
+ if (!record) {
61
+ record = {scriptId};
62
+ byIsolateAndScriptId.set(key, record);
63
+ }
64
+ return record;
65
+ };
66
+
67
+ for (const event of traceEvents) {
68
+ const data = event.args?.data;
69
+ if (!data || data.scriptId === undefined) continue;
70
+
71
+ if (event.cat === 'disabled-by-default-devtools.target-rundown') {
72
+ if (typeof data.frame === 'string') {
73
+ getOrCreateRecord(data).frame = data.frame;
74
+ }
75
+ } else if (event.cat === 'disabled-by-default-devtools.v8-source-rundown' &&
76
+ event.name === 'ScriptCatchup') {
77
+ const record = getOrCreateRecord(data);
78
+ if (typeof data.url === 'string') record.url = data.url;
79
+ if (typeof data.sourceUrl === 'string') record.sourceUrl = data.sourceUrl;
80
+ if (typeof data.executionContextId === 'number') {
81
+ record.executionContextId = data.executionContextId;
82
+ }
83
+ } else if (event.cat === 'disabled-by-default-devtools.v8-source-rundown-sources' &&
84
+ typeof data.sourceText === 'string') {
85
+ const record = getOrCreateRecord(data);
86
+ if (event.name === 'ScriptCatchup') {
87
+ record.sourceText = data.sourceText;
88
+ } else if (event.name === 'LargeScriptCatchup') {
89
+ if (!record.chunks || data.splitIndex === 0) record.chunks = [];
90
+ record.chunks.push(data.sourceText);
91
+ }
92
+ }
93
+ }
94
+
95
+ /** @type {Map<string, TraceScript[]>} */
96
+ const byScriptId = new Map();
97
+ for (const record of byIsolateAndScriptId.values()) {
98
+ if (record.chunks) record.sourceText = record.chunks.join('');
99
+ if (record.sourceText === undefined) continue;
100
+
101
+ let list = byScriptId.get(record.scriptId);
102
+ if (!list) {
103
+ list = [];
104
+ byScriptId.set(record.scriptId, list);
105
+ }
106
+ list.push(record);
107
+ }
108
+
109
+ return byScriptId;
110
+ }
111
+
112
+ /**
113
+ * @param {TraceScript} traceScript
114
+ * @param {LH.Crdp.Debugger.ScriptParsedEvent} script
115
+ * @return {boolean}
116
+ */
117
+ function matchesScriptParsedEvent(traceScript, script) {
118
+ const frameId = script.executionContextAuxData?.frameId;
119
+ if (frameId && traceScript.frame !== undefined && traceScript.frame !== frameId) {
120
+ return false;
121
+ }
122
+ const scriptUrl = script.embedderName || script.url;
123
+ if (traceScript.url !== undefined &&
124
+ traceScript.url !== scriptUrl &&
125
+ traceScript.sourceUrl !== script.url) {
126
+ return false;
127
+ }
128
+ if (script.executionContextId !== undefined &&
129
+ traceScript.executionContextId !== undefined &&
130
+ traceScript.executionContextId !== script.executionContextId) {
131
+ return false;
132
+ }
133
+ return true;
134
+ }
135
+
136
+ /**
137
+ * @param {LH.Crdp.Debugger.ScriptParsedEvent} script
138
+ * @param {Map<string, TraceScript[]>} traceScriptsByScriptId
139
+ * @return {string|undefined}
140
+ */
141
+ function findTraceScriptSource(script, traceScriptsByScriptId) {
142
+ const candidates = traceScriptsByScriptId.get(script.scriptId);
143
+ if (!candidates) return undefined;
144
+
145
+ const matches = candidates.filter((candidate) => matchesScriptParsedEvent(candidate, script));
146
+ if (matches.length === 1) return matches[0].sourceText;
147
+
148
+ const frameId = script.executionContextAuxData?.frameId;
149
+ if (matches.length > 1 && frameId) {
150
+ const frameMatches = matches.filter((candidate) => candidate.frame === frameId);
151
+ if (frameMatches.length === 1) return frameMatches[0].sourceText;
152
+ }
153
+
154
+ return undefined;
155
+ }
156
+
30
157
  /**
31
158
  * Returns true if the script was created via our own calls
32
159
  * to Runtime.evaluate.
@@ -50,18 +177,16 @@ function isLighthouseRuntimeEvaluateScript(script) {
50
177
  class Scripts extends BaseGatherer {
51
178
  static symbol = Symbol('Scripts');
52
179
 
53
- /** @type {LH.Gatherer.GathererMeta} */
180
+ /** @type {LH.Gatherer.GathererMeta<'Trace'>} */
54
181
  meta = {
55
182
  symbol: Scripts.symbol,
56
183
  supportedModes: ['timespan', 'navigation'],
184
+ dependencies: {Trace: Trace.symbol},
57
185
  };
58
186
 
59
187
  /** @type {LH.Crdp.Debugger.ScriptParsedEvent[]} */
60
188
  _scriptParsedEvents = [];
61
189
 
62
- /** @type {Array<string | undefined>} */
63
- _scriptContents = [];
64
-
65
190
  constructor() {
66
191
  super();
67
192
  this.onScriptParsed = this.onScriptParsed.bind(this);
@@ -89,26 +214,42 @@ class Scripts extends BaseGatherer {
89
214
  * @param {LH.Gatherer.Context} context
90
215
  */
91
216
  async stopInstrumentation(context) {
217
+ const session = context.driver.defaultSession;
218
+ session.off('Debugger.scriptParsed', this.onScriptParsed);
219
+ }
220
+
221
+ /**
222
+ * @param {LH.Gatherer.Context<'Trace'>} context
223
+ * @return {Promise<LH.Artifacts['Scripts']>}
224
+ */
225
+ async getArtifact(context) {
92
226
  const session = context.driver.defaultSession;
93
227
  const formFactor = context.baseArtifacts.HostFormFactor;
228
+ const traceScriptsByScriptId = collectTraceScripts(context.dependencies.Trace.traceEvents);
229
+ const traceSources = this._scriptParsedEvents.map(
230
+ (script) => findTraceScriptSource(script, traceScriptsByScriptId)
231
+ );
94
232
 
95
- session.off('Debugger.scriptParsed', this.onScriptParsed);
233
+ if (traceSources.some((source) => source === undefined)) {
234
+ // `disableAsyncStacks()` in `prepare.js` runs after `stopInstrumentation` and calls
235
+ // `Debugger.disable`, so re-enable `Debugger` if we need to call `Debugger.getScriptSource`.
236
+ await session.sendCommand('Debugger.enable');
237
+ }
96
238
 
97
239
  // If run on a mobile device, be sensitive to memory limitations and only
98
240
  // request one at a time.
99
- this._scriptContents = await runInSeriesOrParallel(
241
+ const scriptContents = await runInSeriesOrParallel(
100
242
  this._scriptParsedEvents,
101
- ({scriptId}) => {
102
- return session.sendCommand('Debugger.getScriptSource', {scriptId})
243
+ (script, i) => {
244
+ if (traceSources[i] !== undefined) return Promise.resolve(traceSources[i]);
245
+ return session.sendCommand('Debugger.getScriptSource', {scriptId: script.scriptId})
103
246
  .then((resp) => resp.scriptSource)
104
247
  .catch(() => undefined);
105
248
  },
106
249
  formFactor === 'mobile' /* runInSeries */
107
250
  );
108
251
  await session.sendCommand('Debugger.disable');
109
- }
110
252
 
111
- async getArtifact() {
112
253
  /** @type {LH.Artifacts['Scripts']} */
113
254
  const scripts = this._scriptParsedEvents.map((event, i) => {
114
255
  // 'embedderName' and 'url' are confusingly named, so we rewrite them here.
@@ -123,10 +264,12 @@ class Scripts extends BaseGatherer {
123
264
  // embedderName is optional on the protocol because backends like Node may not set it.
124
265
  // For our purposes, it is always set. But just in case it isn't... fallback to the url.
125
266
  url: event.embedderName || event.url,
126
- content: this._scriptContents[i],
267
+ content: scriptContents[i],
127
268
  };
128
269
  });
129
270
 
271
+ this._scriptParsedEvents = [];
272
+
130
273
  return scripts;
131
274
  }
132
275
  }
@@ -82,6 +82,8 @@ class Stylesheets extends BaseGatherer {
82
82
 
83
83
  // Ensure we finish fetching all stylesheet contents before disabling the CSS domain
84
84
  const sheets = await Promise.all(this._sheetPromises.values());
85
+ this._sheetPromises.clear();
86
+ this._session = undefined;
85
87
 
86
88
  await session.sendCommand('CSS.disable');
87
89
  await session.sendCommand('DOM.disable');
@@ -21,6 +21,7 @@ import {ProcessedTrace} from '../../computed/processed-trace.js';
21
21
  import {Responsiveness} from '../../computed/metrics/responsiveness.js';
22
22
  import {CumulativeLayoutShift} from '../../computed/metrics/cumulative-layout-shift.js';
23
23
  import {TraceEngineResult} from '../../computed/trace-engine-result.js';
24
+ import {Helpers} from '../../lib/trace-engine.js';
24
25
  import SourceMaps from './source-maps.js';
25
26
 
26
27
  /** @typedef {{nodeId: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */
@@ -80,7 +81,7 @@ class TraceElements extends BaseGatherer {
80
81
  }
81
82
 
82
83
  if (navigationId) {
83
- if (insightSet.navigation?.args.data?.navigationId !== navigationId) {
84
+ if (Helpers.Trace.navigationIDForEvent(insightSet.navigation) !== navigationId) {
84
85
  return [];
85
86
  }
86
87
  } else {
@@ -1,6 +1,12 @@
1
1
  export default Trace;
2
2
  declare class Trace extends BaseGatherer {
3
3
  static getDefaultTraceCategories(): string[];
4
+ /**
5
+ * @param {LH.Gatherer.ProtocolSession} session
6
+ * @param {string} handle
7
+ * @return {Promise<LH.Trace>}
8
+ */
9
+ static _readTraceFromStream(session: LH.Gatherer.ProtocolSession, handle: string): Promise<LH.Trace>;
4
10
  /**
5
11
  * @param {LH.Gatherer.ProtocolSession} session
6
12
  * @return {Promise<LH.Trace>}
@@ -73,29 +73,68 @@ class Trace extends BaseGatherer {
73
73
 
74
74
  /**
75
75
  * @param {LH.Gatherer.ProtocolSession} session
76
+ * @param {string} handle
76
77
  * @return {Promise<LH.Trace>}
77
78
  */
78
- static async endTraceAndCollectEvents(session) {
79
+ static async _readTraceFromStream(session, handle) {
79
80
  /** @type {Array<LH.TraceEvent>} */
80
81
  const traceEvents = [];
82
+ const decoder = new TextDecoder();
83
+ let isEof = false;
84
+ let remainder = '';
85
+
86
+ try {
87
+ while (!isEof) {
88
+ const response = await session.sendCommand('IO.read', {handle});
89
+ isEof = Boolean(response.eof);
90
+ remainder += response.base64Encoded ?
91
+ decoder.decode(Buffer.from(response.data, 'base64'), {stream: !isEof}) :
92
+ response.data;
93
+
94
+ // Perfetto's `ExportJson` serializes each trace event and trailing `metadata`
95
+ // as compact single-line JSON (with string newlines escaped as `\\n`), emitting
96
+ // raw `,\n` only between top-level elements of `traceEvents`.
97
+ // Maintain the invariant that `remainder` always starts with `{"traceEvents":[`.
98
+ const lastEventSep = remainder.lastIndexOf('},\n');
99
+ if (lastEventSep !== -1) {
100
+ const {traceEvents: batch} = JSON.parse(`${remainder.slice(0, lastEventSep + 1)}]}`);
101
+ for (const event of batch) {
102
+ traceEvents.push(event);
103
+ }
104
+ remainder = `{"traceEvents":[${remainder.slice(lastEventSep + 3)}`;
105
+ }
106
+ }
107
+ } finally {
108
+ await session.sendCommand('IO.close', {handle});
109
+ }
81
110
 
82
- /**
83
- * Listener for when dataCollected events fire for each trace chunk
84
- * @param {LH.Crdp.Tracing.DataCollectedEvent} data
85
- */
86
- const dataListener = function(data) {
87
- traceEvents.push(...data.value);
88
- };
89
- session.on('Tracing.dataCollected', dataListener);
90
-
91
- return new Promise((resolve, reject) => {
92
- session.once('Tracing.tracingComplete', _ => {
93
- session.off('Tracing.dataCollected', dataListener);
94
- resolve({traceEvents});
95
- });
111
+ const traceJson = JSON.parse(remainder);
112
+ for (const event of traceJson.traceEvents) {
113
+ traceEvents.push(event);
114
+ }
115
+ traceJson.traceEvents = traceEvents;
116
+ return traceJson;
117
+ }
96
118
 
97
- session.sendCommand('Tracing.end').catch(reject);
119
+ /**
120
+ * @param {LH.Gatherer.ProtocolSession} session
121
+ * @return {Promise<LH.Trace>}
122
+ */
123
+ static async endTraceAndCollectEvents(session) {
124
+ /** @type {LH.Crdp.Tracing.TracingCompleteEvent} */
125
+ const completeEvent = await new Promise((resolve, reject) => {
126
+ session.once('Tracing.tracingComplete', resolve);
127
+ session.sendCommand('Tracing.end').catch(err => {
128
+ session.off('Tracing.tracingComplete', resolve);
129
+ reject(err);
130
+ });
98
131
  });
132
+
133
+ if (!completeEvent.stream) {
134
+ throw new Error('Tracing.tracingComplete did not return a stream handle');
135
+ }
136
+
137
+ return Trace._readTraceFromStream(session, completeEvent.stream);
99
138
  }
100
139
 
101
140
  static symbol = Symbol('Trace');
@@ -115,6 +154,7 @@ class Trace extends BaseGatherer {
115
154
  await driver.defaultSession.sendCommand('Page.enable');
116
155
  await driver.defaultSession.sendCommand('Tracing.start', {
117
156
  categories: traceCategories.join(','),
157
+ transferMode: 'ReturnAsStream',
118
158
  options: 'sampling-frequency=10000', // 1000 is default and too slow.
119
159
  });
120
160
 
@@ -5,9 +5,7 @@
5
5
  */
6
6
  export class ArbitraryEqualityMap {
7
7
  /**
8
- * Determines whether two objects are deeply equal. Defers to lodash isEqual, but is kept here for
9
- * easy usage by consumers.
10
- * See https://lodash.com/docs/4.17.5#isEqual.
8
+ * Determines whether two objects are deeply equal.
11
9
  * @param {*} objA
12
10
  * @param {*} objB
13
11
  * @return {boolean}
@@ -44,4 +42,17 @@ export class ArbitraryEqualityMap {
44
42
  */
45
43
  _findIndexOf(key: any): number;
46
44
  }
45
+ /**
46
+ * @license
47
+ * Copyright 2018 Google LLC
48
+ * SPDX-License-Identifier: Apache-2.0
49
+ */
50
+ /**
51
+ * @param {*} a
52
+ * @param {*} b
53
+ * @param {WeakMap<object, WeakSet<object>>=} seen
54
+ * @return {boolean}
55
+ */
56
+ declare function deepEquals(a: any, b: any, seen?: WeakMap<object, WeakSet<object>> | undefined): boolean;
57
+ export { deepEquals as isEqual };
47
58
  //# sourceMappingURL=arbitrary-equality-map.d.ts.map
@@ -4,7 +4,116 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import {isEqual} from 'lodash-es';
7
+ /**
8
+ * @param {*} a
9
+ * @param {*} b
10
+ * @param {WeakMap<object, WeakSet<object>>=} seen
11
+ * @return {boolean}
12
+ */
13
+ function deepEquals(a, b, seen) {
14
+ if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true;
15
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
16
+ return false;
17
+ }
18
+ if (a.constructor !== b.constructor) return false;
19
+
20
+ if (a instanceof Date && b instanceof Date) {
21
+ return a.getTime() === b.getTime() || (Number.isNaN(a.getTime()) && Number.isNaN(b.getTime()));
22
+ }
23
+ if (a instanceof RegExp && b instanceof RegExp) {
24
+ return a.source === b.source && a.flags === b.flags;
25
+ }
26
+ if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
27
+ if (a.byteLength !== b.byteLength) return false;
28
+ const viewA = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
29
+ const viewB = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
30
+ for (let i = 0; i < viewA.length; i++) {
31
+ if (viewA[i] !== viewB[i]) return false;
32
+ }
33
+ return true;
34
+ }
35
+ if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) {
36
+ if (a.byteLength !== b.byteLength) return false;
37
+ const viewA = new Uint8Array(a);
38
+ const viewB = new Uint8Array(b);
39
+ for (let i = 0; i < viewA.length; i++) {
40
+ if (viewA[i] !== viewB[i]) return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ if (!seen) {
46
+ seen = new WeakMap();
47
+ }
48
+ let seenB = seen.get(a);
49
+ if (seenB) {
50
+ if (seenB.has(b)) return true;
51
+ } else {
52
+ seenB = new WeakSet();
53
+ seen.set(a, seenB);
54
+ }
55
+ seenB.add(b);
56
+
57
+ if (Array.isArray(a)) {
58
+ const arrB = /** @type {unknown[]} */ (b);
59
+ if (a.length !== arrB.length) return false;
60
+ for (let i = 0; i < a.length; i++) {
61
+ if (!deepEquals(a[i], arrB[i], seen)) return false;
62
+ }
63
+ return true;
64
+ }
65
+
66
+ if (a instanceof Map && b instanceof Map) {
67
+ if (a.size !== b.size) return false;
68
+ for (const [k, v] of a) {
69
+ if (!b.has(k) || !deepEquals(v, b.get(k), seen)) return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ if (a instanceof Set && b instanceof Set) {
75
+ if (a.size !== b.size) return false;
76
+ /** @type {unknown[]} */
77
+ const unmatchedA = [];
78
+ for (const v of a) {
79
+ if (!b.has(v)) {
80
+ if (typeof v !== 'object' || v === null) return false;
81
+ unmatchedA.push(v);
82
+ }
83
+ }
84
+ if (unmatchedA.length === 0) return true;
85
+ /** @type {unknown[]} */
86
+ const unmatchedB = [];
87
+ for (const v of b) {
88
+ if (!a.has(v)) {
89
+ if (typeof v !== 'object' || v === null) return false;
90
+ unmatchedB.push(v);
91
+ }
92
+ }
93
+ if (unmatchedA.length !== unmatchedB.length) return false;
94
+ for (const itemA of unmatchedA) {
95
+ const matchIndex = unmatchedB.findIndex(itemB => deepEquals(itemA, itemB, seen));
96
+ if (matchIndex === -1) return false;
97
+ unmatchedB.splice(matchIndex, 1);
98
+ }
99
+ return true;
100
+ }
101
+
102
+ if (a instanceof Error && b instanceof Error) {
103
+ if (a.name !== b.name || a.message !== b.message || !deepEquals(a.cause, b.cause, seen)) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ const keysA = Object.keys(a);
109
+ const keysB = Object.keys(b);
110
+ if (keysA.length !== keysB.length) return false;
111
+ for (let i = 0; i < keysA.length; i++) {
112
+ const key = keysA[i];
113
+ if (!Object.hasOwn(b, key) || !deepEquals(a[key], b[key], seen)) return false;
114
+ }
115
+ return true;
116
+ }
8
117
 
9
118
  /**
10
119
  * @fileoverview This class is designed to allow maps with arbitrary equality functions.
@@ -65,16 +174,14 @@ class ArbitraryEqualityMap {
65
174
  }
66
175
 
67
176
  /**
68
- * Determines whether two objects are deeply equal. Defers to lodash isEqual, but is kept here for
69
- * easy usage by consumers.
70
- * See https://lodash.com/docs/4.17.5#isEqual.
177
+ * Determines whether two objects are deeply equal.
71
178
  * @param {*} objA
72
179
  * @param {*} objB
73
180
  * @return {boolean}
74
181
  */
75
182
  static deepEquals(objA, objB) {
76
- return isEqual(objA, objB);
183
+ return deepEquals(objA, objB);
77
184
  }
78
185
  }
79
186
 
80
- export {ArbitraryEqualityMap};
187
+ export {ArbitraryEqualityMap, deepEquals as isEqual};