@ogygia/devtools 0.0.0 → 0.11.1

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 (310) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -1
  3. package/dist/client.d.ts +3 -0
  4. package/dist/client.js +24 -0
  5. package/dist/devtools/BoundaryOverlay.svelte +396 -0
  6. package/dist/devtools/BoundaryOverlay.svelte.d.ts +19 -0
  7. package/dist/devtools/Devtools.svelte +663 -0
  8. package/dist/devtools/Devtools.svelte.d.ts +15 -0
  9. package/dist/devtools/HubTab.svelte +206 -0
  10. package/dist/devtools/HubTab.svelte.d.ts +11 -0
  11. package/dist/devtools/HydrationTab.svelte +237 -0
  12. package/dist/devtools/HydrationTab.svelte.d.ts +13 -0
  13. package/dist/devtools/IslandDetail.svelte +615 -0
  14. package/dist/devtools/IslandDetail.svelte.d.ts +15 -0
  15. package/dist/devtools/LedgerTab.svelte +287 -0
  16. package/dist/devtools/LedgerTab.svelte.d.ts +11 -0
  17. package/dist/devtools/LensTab.svelte +290 -0
  18. package/dist/devtools/LensTab.svelte.d.ts +19 -0
  19. package/dist/devtools/NavTab.svelte +229 -0
  20. package/dist/devtools/NavTab.svelte.d.ts +11 -0
  21. package/dist/devtools/PageTab.svelte +1122 -0
  22. package/dist/devtools/PageTab.svelte.d.ts +15 -0
  23. package/dist/devtools/ProfilerTab.svelte +755 -0
  24. package/dist/devtools/ProfilerTab.svelte.d.ts +15 -0
  25. package/dist/devtools/PropsTree.svelte +155 -0
  26. package/dist/devtools/PropsTree.svelte.d.ts +15 -0
  27. package/dist/devtools/SessionTab.svelte +796 -0
  28. package/dist/devtools/SessionTab.svelte.d.ts +11 -0
  29. package/dist/devtools/TimelineTab.svelte +267 -0
  30. package/dist/devtools/TimelineTab.svelte.d.ts +11 -0
  31. package/dist/devtools/WireTab.svelte +149 -0
  32. package/dist/devtools/WireTab.svelte.d.ts +11 -0
  33. package/dist/devtools/bytes.d.ts +70 -0
  34. package/dist/devtools/bytes.js +82 -0
  35. package/dist/devtools/cache-headers.d.ts +20 -0
  36. package/dist/devtools/cache-headers.js +81 -0
  37. package/dist/devtools/highlight.d.ts +7 -0
  38. package/dist/devtools/highlight.js +83 -0
  39. package/dist/devtools/hydration.d.ts +48 -0
  40. package/dist/devtools/hydration.js +93 -0
  41. package/dist/devtools/index.d.ts +3 -0
  42. package/dist/devtools/index.js +3 -0
  43. package/dist/devtools/ledger-dom.d.ts +7 -0
  44. package/dist/devtools/ledger-dom.js +151 -0
  45. package/dist/devtools/leftovers.d.ts +77 -0
  46. package/dist/devtools/leftovers.js +319 -0
  47. package/dist/devtools/names.d.ts +1 -0
  48. package/dist/devtools/names.js +34 -0
  49. package/dist/devtools/page.d.ts +96 -0
  50. package/dist/devtools/page.js +957 -0
  51. package/dist/devtools/profile-anchors.d.ts +22 -0
  52. package/dist/devtools/profile-anchors.js +135 -0
  53. package/dist/devtools/profile-store.d.ts +175 -0
  54. package/dist/devtools/profile-store.js +239 -0
  55. package/dist/devtools/regions.d.ts +77 -0
  56. package/dist/devtools/regions.js +132 -0
  57. package/dist/devtools/session.d.ts +31 -0
  58. package/dist/devtools/session.js +455 -0
  59. package/dist/devtools/since-load.d.ts +50 -0
  60. package/dist/devtools/since-load.js +89 -0
  61. package/dist/devtools/source-lines.d.ts +24 -0
  62. package/dist/devtools/source-lines.js +106 -0
  63. package/dist/devtools/styles.d.ts +52 -0
  64. package/dist/devtools/styles.js +248 -0
  65. package/dist/devtools/testing-engine.d.ts +83 -0
  66. package/dist/devtools/testing-engine.js +220 -0
  67. package/dist/devtools/ui-app.d.ts +5 -0
  68. package/dist/devtools/ui-app.js +69 -0
  69. package/dist/devtools/ui.d.ts +50 -0
  70. package/dist/devtools/ui.js +175 -0
  71. package/dist/profiler/alloc.d.ts +75 -0
  72. package/dist/profiler/alloc.js +76 -0
  73. package/dist/profiler/analyze.d.ts +437 -0
  74. package/dist/profiler/analyze.js +1941 -0
  75. package/dist/profiler/app-path.d.ts +7 -0
  76. package/dist/profiler/app-path.js +26 -0
  77. package/dist/profiler/async-io.d.ts +58 -0
  78. package/dist/profiler/async-io.js +187 -0
  79. package/dist/profiler/beacon-standalone.d.ts +4 -0
  80. package/dist/profiler/beacon-standalone.js +409 -0
  81. package/dist/profiler/browser-findings.d.ts +25 -0
  82. package/dist/profiler/browser-findings.js +260 -0
  83. package/dist/profiler/byte-strip.d.ts +58 -0
  84. package/dist/profiler/byte-strip.js +228 -0
  85. package/dist/profiler/client-files.d.ts +16 -0
  86. package/dist/profiler/client-files.js +60 -0
  87. package/dist/profiler/client-windows.d.ts +49 -0
  88. package/dist/profiler/client-windows.js +95 -0
  89. package/dist/profiler/client.d.ts +40 -0
  90. package/dist/profiler/client.js +82 -0
  91. package/dist/profiler/compare.d.ts +308 -0
  92. package/dist/profiler/compare.js +733 -0
  93. package/dist/profiler/compress.d.ts +10 -0
  94. package/dist/profiler/compress.js +36 -0
  95. package/dist/profiler/contention.d.ts +60 -0
  96. package/dist/profiler/contention.js +86 -0
  97. package/dist/profiler/crypto.d.ts +19 -0
  98. package/dist/profiler/crypto.js +101 -0
  99. package/dist/profiler/doc-diff.d.ts +47 -0
  100. package/dist/profiler/doc-diff.js +214 -0
  101. package/dist/profiler/dom-diff.d.ts +43 -0
  102. package/dist/profiler/dom-diff.js +121 -0
  103. package/dist/profiler/drill.d.ts +89 -0
  104. package/dist/profiler/drill.js +553 -0
  105. package/dist/profiler/embedded-maps.d.ts +37 -0
  106. package/dist/profiler/embedded-maps.js +135 -0
  107. package/dist/profiler/env.d.ts +6 -0
  108. package/dist/profiler/env.js +10 -0
  109. package/dist/profiler/forecast.d.ts +76 -0
  110. package/dist/profiler/forecast.js +225 -0
  111. package/dist/profiler/frames.d.ts +11 -0
  112. package/dist/profiler/frames.js +42 -0
  113. package/dist/profiler/gc.d.ts +144 -0
  114. package/dist/profiler/gc.js +271 -0
  115. package/dist/profiler/hole-slots.d.ts +31 -0
  116. package/dist/profiler/hole-slots.js +90 -0
  117. package/dist/profiler/index.d.ts +288 -0
  118. package/dist/profiler/index.js +4921 -0
  119. package/dist/profiler/insights.d.ts +96 -0
  120. package/dist/profiler/insights.js +239 -0
  121. package/dist/profiler/ledger.d.ts +121 -0
  122. package/dist/profiler/ledger.js +440 -0
  123. package/dist/profiler/lineage.d.ts +81 -0
  124. package/dist/profiler/lineage.js +359 -0
  125. package/dist/profiler/module-map.d.ts +12 -0
  126. package/dist/profiler/module-map.js +94 -0
  127. package/dist/profiler/net.d.ts +169 -0
  128. package/dist/profiler/net.js +586 -0
  129. package/dist/profiler/on-demand.d.ts +11 -0
  130. package/dist/profiler/on-demand.js +15 -0
  131. package/dist/profiler/own-requests.d.ts +9 -0
  132. package/dist/profiler/own-requests.js +17 -0
  133. package/dist/profiler/patterns.d.ts +476 -0
  134. package/dist/profiler/patterns.js +2203 -0
  135. package/dist/profiler/profiler-router.d.ts +245 -0
  136. package/dist/profiler/profiler-router.js +113 -0
  137. package/dist/profiler/refresh-stub.d.ts +2 -0
  138. package/dist/profiler/refresh-stub.js +37 -0
  139. package/dist/profiler/report.d.ts +1404 -0
  140. package/dist/profiler/report.js +2638 -0
  141. package/dist/profiler/river.d.ts +65 -0
  142. package/dist/profiler/river.js +298 -0
  143. package/dist/profiler/route-files.d.ts +22 -0
  144. package/dist/profiler/route-files.js +55 -0
  145. package/dist/profiler/route-imports.d.ts +14 -0
  146. package/dist/profiler/route-imports.js +83 -0
  147. package/dist/profiler/saving.d.ts +22 -0
  148. package/dist/profiler/saving.js +42 -0
  149. package/dist/profiler/score.d.ts +109 -0
  150. package/dist/profiler/score.js +261 -0
  151. package/dist/profiler/session-cookie.d.ts +28 -0
  152. package/dist/profiler/session-cookie.js +45 -0
  153. package/dist/profiler/sink.d.ts +81 -0
  154. package/dist/profiler/sink.js +124 -0
  155. package/dist/profiler/site-fixes.d.ts +25 -0
  156. package/dist/profiler/site-fixes.js +62 -0
  157. package/dist/profiler/source-scan.d.ts +210 -0
  158. package/dist/profiler/source-scan.js +1724 -0
  159. package/dist/profiler/span.d.ts +90 -0
  160. package/dist/profiler/span.js +109 -0
  161. package/dist/profiler/stacks.d.ts +43 -0
  162. package/dist/profiler/stacks.js +107 -0
  163. package/dist/profiler/standalone.d.ts +27 -0
  164. package/dist/profiler/standalone.js +98 -0
  165. package/dist/profiler/steps.d.ts +39 -0
  166. package/dist/profiler/steps.js +74 -0
  167. package/dist/profiler/storage/index.d.ts +87 -0
  168. package/dist/profiler/storage/index.js +76 -0
  169. package/dist/profiler/storage/postgres.d.ts +12 -0
  170. package/dist/profiler/storage/postgres.js +94 -0
  171. package/dist/profiler/storage/redis.d.ts +19 -0
  172. package/dist/profiler/storage/redis.js +148 -0
  173. package/dist/profiler/storage/sqlite.d.ts +2 -0
  174. package/dist/profiler/storage/sqlite.js +85 -0
  175. package/dist/profiler/storage/visits.d.ts +4 -0
  176. package/dist/profiler/storage/visits.js +6 -0
  177. package/dist/profiler/stream-tail.d.ts +26 -0
  178. package/dist/profiler/stream-tail.js +74 -0
  179. package/dist/profiler/timeline.d.ts +279 -0
  180. package/dist/profiler/timeline.js +690 -0
  181. package/dist/profiler/ui/AllocStrip.svelte +146 -0
  182. package/dist/profiler/ui/AllocStrip.svelte.d.ts +17 -0
  183. package/dist/profiler/ui/Awaits.svelte +363 -0
  184. package/dist/profiler/ui/Awaits.svelte.d.ts +16 -0
  185. package/dist/profiler/ui/ByteStrip.svelte +182 -0
  186. package/dist/profiler/ui/ByteStrip.svelte.d.ts +18 -0
  187. package/dist/profiler/ui/Compare.svelte +24 -0
  188. package/dist/profiler/ui/Compare.svelte.d.ts +16 -0
  189. package/dist/profiler/ui/CompareBody.svelte +429 -0
  190. package/dist/profiler/ui/CompareBody.svelte.d.ts +8 -0
  191. package/dist/profiler/ui/ComponentsTable.svelte +265 -0
  192. package/dist/profiler/ui/ComponentsTable.svelte.d.ts +20 -0
  193. package/dist/profiler/ui/Dashboard.svelte +492 -0
  194. package/dist/profiler/ui/Dashboard.svelte.d.ts +31 -0
  195. package/dist/profiler/ui/DataRiver.svelte +206 -0
  196. package/dist/profiler/ui/DataRiver.svelte.d.ts +13 -0
  197. package/dist/profiler/ui/DomTravel.svelte +207 -0
  198. package/dist/profiler/ui/DomTravel.svelte.d.ts +9 -0
  199. package/dist/profiler/ui/DownloadHtml.svelte +52 -0
  200. package/dist/profiler/ui/DownloadHtml.svelte.d.ts +10 -0
  201. package/dist/profiler/ui/Drill.svelte +276 -0
  202. package/dist/profiler/ui/Drill.svelte.d.ts +12 -0
  203. package/dist/profiler/ui/ExportButton.svelte +59 -0
  204. package/dist/profiler/ui/ExportButton.svelte.d.ts +8 -0
  205. package/dist/profiler/ui/Flame.svelte +222 -0
  206. package/dist/profiler/ui/Flame.svelte.d.ts +12 -0
  207. package/dist/profiler/ui/FrameDetails.svelte +308 -0
  208. package/dist/profiler/ui/FrameDetails.svelte.d.ts +16 -0
  209. package/dist/profiler/ui/FunctionsTable.svelte +156 -0
  210. package/dist/profiler/ui/FunctionsTable.svelte.d.ts +17 -0
  211. package/dist/profiler/ui/IslandHeatmap.svelte +151 -0
  212. package/dist/profiler/ui/IslandHeatmap.svelte.d.ts +16 -0
  213. package/dist/profiler/ui/IslandsTable.svelte +448 -0
  214. package/dist/profiler/ui/IslandsTable.svelte.d.ts +16 -0
  215. package/dist/profiler/ui/KeepLocal.svelte +46 -0
  216. package/dist/profiler/ui/KeepLocal.svelte.d.ts +8 -0
  217. package/dist/profiler/ui/Ledger.svelte +196 -0
  218. package/dist/profiler/ui/Ledger.svelte.d.ts +18 -0
  219. package/dist/profiler/ui/LocalCompare.svelte +48 -0
  220. package/dist/profiler/ui/LocalCompare.svelte.d.ts +8 -0
  221. package/dist/profiler/ui/LocalReports.svelte +68 -0
  222. package/dist/profiler/ui/LocalReports.svelte.d.ts +7 -0
  223. package/dist/profiler/ui/Login.svelte +13 -0
  224. package/dist/profiler/ui/Login.svelte.d.ts +12 -0
  225. package/dist/profiler/ui/LoginForm.svelte +42 -0
  226. package/dist/profiler/ui/LoginForm.svelte.d.ts +7 -0
  227. package/dist/profiler/ui/Message.svelte +10 -0
  228. package/dist/profiler/ui/Message.svelte.d.ts +12 -0
  229. package/dist/profiler/ui/OnThisPage.svelte +137 -0
  230. package/dist/profiler/ui/OnThisPage.svelte.d.ts +3 -0
  231. package/dist/profiler/ui/OneClock.svelte +297 -0
  232. package/dist/profiler/ui/OneClock.svelte.d.ts +27 -0
  233. package/dist/profiler/ui/Panel.svelte +10 -0
  234. package/dist/profiler/ui/Panel.svelte.d.ts +16 -0
  235. package/dist/profiler/ui/Paths.svelte +269 -0
  236. package/dist/profiler/ui/Paths.svelte.d.ts +15 -0
  237. package/dist/profiler/ui/Patterns.svelte +364 -0
  238. package/dist/profiler/ui/Patterns.svelte.d.ts +15 -0
  239. package/dist/profiler/ui/PermalinkGate.svelte +143 -0
  240. package/dist/profiler/ui/PermalinkGate.svelte.d.ts +10 -0
  241. package/dist/profiler/ui/RenderSteps.svelte +218 -0
  242. package/dist/profiler/ui/RenderSteps.svelte.d.ts +13 -0
  243. package/dist/profiler/ui/Report.svelte +28 -0
  244. package/dist/profiler/ui/Report.svelte.d.ts +20 -0
  245. package/dist/profiler/ui/ReportBody.svelte +2592 -0
  246. package/dist/profiler/ui/ReportBody.svelte.d.ts +21 -0
  247. package/dist/profiler/ui/Run.svelte +17 -0
  248. package/dist/profiler/ui/Run.svelte.d.ts +14 -0
  249. package/dist/profiler/ui/RunView.svelte +146 -0
  250. package/dist/profiler/ui/RunView.svelte.d.ts +10 -0
  251. package/dist/profiler/ui/ScoreCard.svelte +197 -0
  252. package/dist/profiler/ui/ScoreCard.svelte.d.ts +7 -0
  253. package/dist/profiler/ui/Scrub.svelte +293 -0
  254. package/dist/profiler/ui/Scrub.svelte.d.ts +18 -0
  255. package/dist/profiler/ui/SeedExplainer.svelte +193 -0
  256. package/dist/profiler/ui/SeedExplainer.svelte.d.ts +16 -0
  257. package/dist/profiler/ui/ShareLink.svelte +148 -0
  258. package/dist/profiler/ui/ShareLink.svelte.d.ts +8 -0
  259. package/dist/profiler/ui/Shell.svelte +50 -0
  260. package/dist/profiler/ui/Shell.svelte.d.ts +27 -0
  261. package/dist/profiler/ui/Sidebar.svelte +312 -0
  262. package/dist/profiler/ui/Sidebar.svelte.d.ts +7 -0
  263. package/dist/profiler/ui/Site.svelte +20 -0
  264. package/dist/profiler/ui/Site.svelte.d.ts +23 -0
  265. package/dist/profiler/ui/SiteView.svelte +385 -0
  266. package/dist/profiler/ui/SiteView.svelte.d.ts +27 -0
  267. package/dist/profiler/ui/ThemeSwitch.svelte +78 -0
  268. package/dist/profiler/ui/ThemeSwitch.svelte.d.ts +3 -0
  269. package/dist/profiler/ui/Timeline.svelte +672 -0
  270. package/dist/profiler/ui/Timeline.svelte.d.ts +22 -0
  271. package/dist/profiler/ui/Treemap.svelte +278 -0
  272. package/dist/profiler/ui/Treemap.svelte.d.ts +20 -0
  273. package/dist/profiler/ui/Upload.svelte +19 -0
  274. package/dist/profiler/ui/Upload.svelte.d.ts +10 -0
  275. package/dist/profiler/ui/UploadForm.svelte +59 -0
  276. package/dist/profiler/ui/UploadForm.svelte.d.ts +6 -0
  277. package/dist/profiler/ui/WaitingTable.svelte +55 -0
  278. package/dist/profiler/ui/WaitingTable.svelte.d.ts +14 -0
  279. package/dist/profiler/ui/Waterfall.svelte +371 -0
  280. package/dist/profiler/ui/Waterfall.svelte.d.ts +7 -0
  281. package/dist/profiler/ui/WhatIf.svelte +87 -0
  282. package/dist/profiler/ui/WhatIf.svelte.d.ts +18 -0
  283. package/dist/profiler/ui/fake-progress.svelte.d.ts +15 -0
  284. package/dist/profiler/ui/fake-progress.svelte.js +38 -0
  285. package/dist/profiler/ui/format.d.ts +20 -0
  286. package/dist/profiler/ui/format.js +71 -0
  287. package/dist/profiler/ui/permalink.d.ts +9 -0
  288. package/dist/profiler/ui/permalink.js +108 -0
  289. package/dist/profiler/ui/press.d.ts +5 -0
  290. package/dist/profiler/ui/press.js +19 -0
  291. package/dist/profiler/ui/profiler.css +650 -0
  292. package/dist/profiler/ui/report-data.d.ts +189 -0
  293. package/dist/profiler/ui/report-data.js +393 -0
  294. package/dist/profiler/ui/report-request.d.ts +6 -0
  295. package/dist/profiler/ui/report-request.js +50 -0
  296. package/dist/profiler/ui/row-anchor.svelte.d.ts +12 -0
  297. package/dist/profiler/ui/row-anchor.svelte.js +44 -0
  298. package/dist/profiler/ui/sort.svelte.d.ts +14 -0
  299. package/dist/profiler/ui/sort.svelte.js +37 -0
  300. package/dist/profiler/ui/store.d.ts +45 -0
  301. package/dist/profiler/ui/store.js +92 -0
  302. package/dist/profiler/visit.d.ts +389 -0
  303. package/dist/profiler/visit.js +1002 -0
  304. package/dist/report.d.ts +5 -0
  305. package/dist/report.js +4 -0
  306. package/dist/server.d.ts +11 -0
  307. package/dist/server.js +38 -0
  308. package/dist/vite.d.ts +12 -0
  309. package/dist/vite.js +78 -0
  310. package/package.json +99 -6
@@ -0,0 +1,4921 @@
1
+ import { analyze, analyze_heap, categorize, chunk_component_renamer, heap_by_component, learn_script_urls, sourcemap_resolver } from "./analyze.js";
2
+ import { another_routes_file, route_dirs } from "./route-files.js";
3
+ import { TRACE_HEADER, encode_trace } from "./net.js";
4
+ import { derive_findings, is_dump, island_host_renamer, island_name, island_rows_of, page_score_of, path_template, report_dump, report_json } from "./report.js";
5
+ import { is_ogp, ogp_decode, ogp_encode, recover_ogp_bytes } from "./crypto.js";
6
+ import { AFTER_LOGIN_PARAM, SESSION_COOKIE, describe_cookie_diagnosis, raw_cookie_values, session_cookie_values } from "./session-cookie.js";
7
+ import { label_call, load_lane_of, phase_of_frame } from "./timeline.js";
8
+ import { merge_visits, parse_visit } from "./visit.js";
9
+ import { own_requests } from "./own-requests.js";
10
+ import { MAX_VISITS_PER_KEY } from "./storage/visits.js";
11
+ import { hole_slots } from "./hole-slots.js";
12
+ import { client_windows, interaction_windows } from "./client-windows.js";
13
+ import { byte_strip } from "./byte-strip.js";
14
+ import { stream_tail } from "./stream-tail.js";
15
+ import { BEACON_STANDALONE_TAG } from "./beacon-standalone.js";
16
+ import { build_river, load_return_keys, seed_key_bytes } from "./river.js";
17
+ import { SinkBuffer } from "./sink.js";
18
+ import { attribute_gc, gc_kind, heap_sites } from "./gc.js";
19
+ import { alloc_timeline } from "./alloc.js";
20
+ import { contention } from "./contention.js";
21
+ import { build_lineage, data_prop_names, data_reads, whole_read_line } from "./lineage.js";
22
+ import { build_ledger, pkg_of } from "./ledger.js";
23
+ import { at_line, build_drill, call_group, tag_fills } from "./drill.js";
24
+ import { forecast_of } from "./forecast.js";
25
+ import { site_fixes } from "./site-fixes.js";
26
+ import { may_be_chunk, module_category, module_lookup } from "./module-map.js";
27
+ import { embedded_chunk, embedded_files, embedded_map_of, embedded_source, load_embedded_maps } from "./embedded-maps.js";
28
+ import { module_imports, route_imports } from "./route-imports.js";
29
+ import { app_relative } from "./app-path.js";
30
+ import { attr_sites, doc_diff } from "./doc-diff.js";
31
+ import { almost_same_document_pattern, counted_sync_io, find_cold_caches, find_patterns, find_same_answers, find_wait_patterns, heap_growth, is_clear_leak, is_flat, late_island_pattern, render_per_item_pattern, same_document_pattern, same_work_pattern, seed_whole_pattern, stable_answers, varied_answers, wait_save_on_path } from "./patterns.js";
32
+ import { constant_work, each_blocks, formatter_rewrite, hoist_plan, key_sources, late_island_keys, line_rw, load_inputs, names_on, parent_use, tokens } from "./source-scan.js";
33
+ import { compare_reports, island_files_diff, page_history, vitals_moved as vitals_moved$1 } from "./compare.js";
34
+ import { gzip_large } from "./compress.js";
35
+ import { io_kind, mark_left_each_run } from "./async-io.js";
36
+ import { set_span_recorder } from "./span.js";
37
+ import { register_profiler_file } from "./frames.js";
38
+ import { app_asset_rel, client_dir_candidates, client_file_finder } from "./client-files.js";
39
+ import { build_profiler_router } from "./profiler-router.js";
40
+ import { detect_dev } from "./env.js";
41
+ import { refresh_target } from "./refresh-stub.js";
42
+ import { assets_diff, engine_of, hydration_hazards, vital_parts, weigh_assets } from "@ogygia/core/insights";
43
+ import { hole_stats_of, mark_personal, request_stats_of, set_batch_hole_listener, set_request_stats_detail } from "@ogygia/core/internal/server";
44
+ import { fnv1a32 } from "@ogygia/core/plugin/utils";
45
+ import { chunkBarrels, chunkContents, chunkDuplicates, chunkHeavy, islandHazards, islandPageKeys, islandPageWhy } from "virtual:ogygia/island-deps";
46
+ import { error, isHttpError, isRedirect } from "@ogygia/router";
47
+ // src/profiler/index.ts
48
+ /** Set on the login page's URL when a login just succeeded and the session still did not arrive. */
49
+ const LOGIN_FAILED_PARAM = "session";
50
+ /** I/O resources a line can wait on one after another, as a readable target. Timers and ticks
51
+ * are left out: a timer awaited per turn is the `yield-per-item` pattern, read from the code. */
52
+ const WAIT_TARGET = {
53
+ FSREQCALLBACK: "file access",
54
+ FSREQPROMISE: "file access",
55
+ GETADDRINFOREQWRAP: "DNS lookup",
56
+ GETNAMEINFOREQWRAP: "DNS lookup",
57
+ QUERYWRAP: "DNS query",
58
+ TCPCONNECTWRAP: "socket connect",
59
+ PIPECONNECTWRAP: "pipe connect"
60
+ };
61
+ /** A response body read chunk by chunk, each chunk's end offset stamped with when it arrived
62
+ * (ms after `t0`): the byte strip's "left the server at". */
63
+ async function read_timed(res, t0) {
64
+ const reader = res.body?.getReader();
65
+ if (!reader) return {
66
+ text: await res.text(),
67
+ chunks: []
68
+ };
69
+ const dec = new TextDecoder();
70
+ let text = "";
71
+ const chunks = [];
72
+ for (;;) {
73
+ const { value, done } = await reader.read();
74
+ if (done) break;
75
+ text += dec.decode(value, { stream: true });
76
+ if (chunks.length < 4e3) chunks.push({
77
+ end: text.length,
78
+ t: round2(performance.now() - t0)
79
+ });
80
+ }
81
+ text += dec.decode();
82
+ return {
83
+ text,
84
+ chunks
85
+ };
86
+ }
87
+ register_profiler_file();
88
+ const MAX_VITALS_PATHS = 200;
89
+ const MAX_VITALS_SAMPLES = 30;
90
+ const MAX_SAMPLED_WINDOWS = 60;
91
+ const SAMPLED_FUNCTIONS_PER_WINDOW = 150;
92
+ const MAX_BEACON_FPS = 2e3;
93
+ const MAX_BEACON_SAMPLES = 64;
94
+ const MAX_BEACON_BODY = 65536;
95
+ const MAX_BEACON_CPU_BODY = 6291456;
96
+ const MAX_BEACON_VISIT_BODY = 524288;
97
+ /** the allocation sampler's interval while attributing GC: coarse on purpose (64 KB, against the
98
+ * 16 KB the allocators table used) — the attribution needs shares, not every object, and every
99
+ * sample is a record V8 keeps until the read */
100
+ const GC_SAMPLING_BYTES = 65536;
101
+ /** the live-objects sampler of the memory passes after the timed runs (what one render keeps, and
102
+ * the growth check): finer, because those are VERDICTS on a line, not shares. At 64 KB a 1 MB
103
+ * cache entry is ~14 samples, and a 4-entry cache's halfway-to-end ratio swung 1.10–2.00 around
104
+ * its true 1.33 (the leak line is 1.85): the slow-patterns decoy read "grows" 1 run in ~17, and a
105
+ * real leak was missed now and then. At 16 KB it stays 1.16–1.57. These renders are not timed:
106
+ * it costs about half a second of the profile's wall time on the heaviest test page, no timing. */
107
+ const LIVE_SAMPLING_BYTES = 16384;
108
+ /** the fine heap series (alloc.ts): the timer's period and the most samples one window keeps */
109
+ const HEAP_SERIES_PERIOD_MS = 20;
110
+ const HEAP_SERIES_MAX = 2e3;
111
+ /** the sampler keeps objects already collected: the ALLOCATION stream, what drives the collector,
112
+ * not just what is still alive when the profile is read */
113
+ const GC_SAMPLING_FLAGS = {
114
+ includeObjectsCollectedByMajorGC: true,
115
+ includeObjectsCollectedByMinorGC: true
116
+ };
117
+ /** how long a page profile waits for a background window (trap / sampler) to finish — the
118
+ * longest such window is the trap's 5 s */
119
+ const PAGE_WAITS_FOR_BACKGROUND_MS = 8e3;
120
+ /** a started window stops itself after this, even with no stop (it holds the recorder) */
121
+ const WINDOW_MAX_MS = 6e4;
122
+ /** the header a test's driver sends with each request of a window (its signed id) */
123
+ const WINDOW_HEADER = "x-og-window";
124
+ /** a window's requests sit in the store's visit rows under this page key (never a real path) */
125
+ const WINDOW_PAGE_PREFIX = "\0window:";
126
+ const MAX_VISITS_PER_PAGE = 5;
127
+ /** a screen narrower than this is a phone's (its own first screen, its own hidden islands) */
128
+ const NARROW_SCREEN_PX = 768;
129
+ /** Svelte's client runtime, by the names that show up under a component while it hydrates */
130
+ /** the package in a CDN script's path: `/npm/@scope/name@8/dist/x.js`, `/name@1.2.3/x.js`, `/gh/user/repo@v/` */
131
+ const CDN_PKG_RE = /\/(?:npm\/)?((?:@[\w.-]+\/)?[\w.-]+)@[\w.^~-]+\//;
132
+ const SVELTE_CLIENT_FN_RE = /^(?:set_text|set_attribute|set_class|set_style|template|from_html|child|sibling|first_child|append|mount|hydrate|hydrate_template|each|if_block|render_effect|template_effect|effect|derived|proxy|get|set|push|pop|init|reset|next|skip_nodes|create_text|remove_nodes|event|delegate|bind_value|component|snippet|slot|head)$/;
133
+ /**
134
+ * Chromium's JS Self-Profiling trace (`Profiler.stop()`) as a V8 cpuprofile: a stack is a chain of
135
+ * frames, so every distinct stack becomes one node whose parent is its parent stack; the samples
136
+ * are timestamps (ms) → time deltas (µs); a sample with no stack is idle. Bounded (samples,
137
+ * stacks); `null` for anything not shaped like a trace. Exported for the tests.
138
+ */
139
+ function self_profile_to_cpuprofile(trace) {
140
+ const t = trace;
141
+ if (!t || !Array.isArray(t.frames) || !Array.isArray(t.stacks) || !Array.isArray(t.samples)) return null;
142
+ const resources = Array.isArray(t.resources) ? t.resources.map((r) => typeof r === "string" ? r : "") : [];
143
+ const frames = t.frames.slice(0, 5e4);
144
+ const stacks = t.stacks.slice(0, 1e5);
145
+ const samples = t.samples.slice(0, 2e5);
146
+ const frame_of = (i) => {
147
+ const f = frames[i];
148
+ if (!f) return {
149
+ functionName: "(unknown)",
150
+ url: "",
151
+ lineNumber: 0,
152
+ columnNumber: 0
153
+ };
154
+ const rid = typeof f.resourceId === "number" ? f.resourceId : -1;
155
+ return {
156
+ functionName: typeof f.name === "string" ? f.name : "",
157
+ url: rid >= 0 ? resources[rid] ?? "" : "",
158
+ lineNumber: typeof f.line === "number" ? Math.max(0, f.line - 1) : 0,
159
+ columnNumber: typeof f.column === "number" ? Math.max(0, f.column - 1) : 0
160
+ };
161
+ };
162
+ const nodes = [{
163
+ id: 1,
164
+ callFrame: {
165
+ functionName: "(root)",
166
+ url: "",
167
+ lineNumber: 0,
168
+ columnNumber: 0
169
+ },
170
+ children: [2]
171
+ }, {
172
+ id: 2,
173
+ callFrame: {
174
+ functionName: "(idle)",
175
+ url: "",
176
+ lineNumber: 0,
177
+ columnNumber: 0
178
+ }
179
+ }];
180
+ const children = /* @__PURE__ */ new Map([[1, [2]]]);
181
+ for (let i = 0; i < stacks.length; i++) {
182
+ const s = stacks[i];
183
+ const fid = typeof s?.frameId === "number" ? s.frameId : -1;
184
+ const parent = typeof s?.parentId === "number" && s.parentId >= 0 && s.parentId < stacks.length ? s.parentId + 3 : 1;
185
+ nodes.push({
186
+ id: i + 3,
187
+ callFrame: frame_of(fid)
188
+ });
189
+ (children.get(parent) ?? children.set(parent, []).get(parent)).push(i + 3);
190
+ }
191
+ for (const n of nodes) {
192
+ const ch = children.get(n.id);
193
+ if (ch?.length) n.children = ch;
194
+ }
195
+ const sample_ids = [];
196
+ const deltas = [];
197
+ let last = null;
198
+ let first = 0;
199
+ for (const s of samples) {
200
+ const ts = typeof s?.timestamp === "number" ? s.timestamp : null;
201
+ if (ts === null) continue;
202
+ if (last === null) first = ts;
203
+ const sid = typeof s?.stackId === "number" && s.stackId >= 0 && s.stackId < stacks.length ? s.stackId + 3 : 2;
204
+ sample_ids.push(sid);
205
+ deltas.push(last === null ? 0 : Math.max(0, Math.round((ts - last) * 1e3)));
206
+ last = ts;
207
+ }
208
+ if (!sample_ids.length) return null;
209
+ return {
210
+ nodes,
211
+ startTime: Math.round(first * 1e3),
212
+ endTime: Math.round((last ?? first) * 1e3),
213
+ samples: sample_ids,
214
+ timeDeltas: deltas
215
+ };
216
+ }
217
+ const MAX_BEACON_ISLANDS = 400;
218
+ /** the tag the runtime looks for: its content is the beacon endpoint */
219
+ const BEACON_META = "ogygia-profiler-beacon";
220
+ /** an island fingerprint: 8–32 lowercase hex chars, checked without a regex (per beacon sample) */
221
+ function is_hex_id(s) {
222
+ if (s.length < 8 || s.length > 32) return false;
223
+ for (let i = 0; i < s.length; i++) {
224
+ const c = s.charCodeAt(i);
225
+ if (!(c >= 48 && c <= 57 || c >= 97 && c <= 102)) return false;
226
+ }
227
+ return true;
228
+ }
229
+ /** GET each URL through the app's own fetch and count its bytes — the JS weight of an island's
230
+ * closure. Cached per process (immutable hashed assets), a handful at a time, capped in time so
231
+ * a slow static host can never hold a report hostage. */
232
+ async function weigh_urls(urls, fetch_url, cache, budget_ms = 4e3) {
233
+ const todo = [...new Set(urls)].filter((u) => u && !cache.has(u));
234
+ const deadline = Date.now() + budget_ms;
235
+ let i = 0;
236
+ const worker = async () => {
237
+ while (i < todo.length && Date.now() < deadline) {
238
+ const u = todo[i++];
239
+ try {
240
+ const res = await with_timeout(fetch_url(u), Math.max(200, deadline - Date.now()), "weigh");
241
+ if (!res.ok) {
242
+ cache.set(u, null);
243
+ continue;
244
+ }
245
+ const len = Number(res.headers.get("content-length"));
246
+ const bytes = !res.headers.get("content-encoding") && Number.isFinite(len) && len > 0 ? len : (await res.arrayBuffer()).byteLength;
247
+ cache.set(u, bytes);
248
+ } catch {
249
+ cache.set(u, null);
250
+ }
251
+ }
252
+ };
253
+ await Promise.all(Array.from({ length: Math.min(6, todo.length) }, worker));
254
+ const out = {};
255
+ for (const u of new Set(urls)) {
256
+ const b = cache.get(u);
257
+ if (typeof b === "number") out[u] = b;
258
+ }
259
+ return out;
260
+ }
261
+ const percentile = (sorted, p) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] ?? 0;
262
+ function summarize_measures(entries) {
263
+ const by_name = /* @__PURE__ */ new Map();
264
+ for (const e of entries) {
265
+ let t = by_name.get(e.name);
266
+ if (!t) by_name.set(e.name, t = {
267
+ name: e.name,
268
+ count: 0,
269
+ total_ms: 0,
270
+ max_ms: 0
271
+ });
272
+ t.count++;
273
+ t.total_ms += e.ms;
274
+ if (e.ms > t.max_ms) t.max_ms = e.ms;
275
+ }
276
+ return [...by_name.values()].map((t) => ({
277
+ ...t,
278
+ total_ms: round2(t.total_ms),
279
+ max_ms: round2(t.max_ms)
280
+ })).sort((a, b) => b.total_ms - a.total_ms).slice(0, 30);
281
+ }
282
+ const MAX_SPANS_PER_REQUEST = 2e3;
283
+ const MAX_WINDOW_SPANS = 2e4;
284
+ const MAX_TAGS = 16;
285
+ /** the process's live profiler (the newest): an older one's background timers stand down */
286
+ const LIVE_PROFILER = Symbol.for("ogygia.profiler.live");
287
+ const MAX_NET_PER_REQUEST = 100;
288
+ const MAX_WINDOW_NET = 2e3;
289
+ const MAX_BACKGROUND_NET = 300;
290
+ const RECORDING_MAX_MS = 35e3;
291
+ const RECORDING_HARD_CAP_MS = 3e4;
292
+ const PER_RENDER_TIMEOUT_MS = 15e3;
293
+ /** the heap's fill (of its limit) past which no timed render is made: one more could end the process */
294
+ const HEAP_STOP = .85;
295
+ /** …and past which the extra renders (what a render keeps, the growth check) are skipped */
296
+ const HEAP_EXTRAS = .7;
297
+ /** …and past which a profile does not start: the warm-up renders come before any check, and a
298
+ * render's own peak (not only what it keeps) needs the room */
299
+ const HEAP_START = .75;
300
+ /** the main thread's work in a render: the code on the sampled stacks and the collector's pauses */
301
+ const MAIN_THREAD_CPU = /* @__PURE__ */ new Set([
302
+ "app",
303
+ "component",
304
+ "dependency",
305
+ "node",
306
+ "svelte",
307
+ "gc"
308
+ ]);
309
+ /** app modules whose built lines are their source lines, near enough to match by text */
310
+ const SCRIPT_MODULE_EXTS = [
311
+ ".ts",
312
+ ".js",
313
+ ".mts",
314
+ ".mjs",
315
+ ".cts",
316
+ ".cjs"
317
+ ];
318
+ /** marks the profiler's keep-the-answers render (its value: the recording's token) */
319
+ const ANSWERS_HEADER = "x-og-profiler-answers";
320
+ /** renders the keep-the-answers measure takes at most (their median is the figure) */
321
+ const ANSWER_RENDERS = 3;
322
+ /** an inspector-unavailable recording error (edge runtimes) vs a real profiling failure */
323
+ const INSPECTOR_ERR = /inspector/;
324
+ const WIN_DRIVE_RE = /^[A-Za-z]:[\\/]/;
325
+ const QUERY_SUFFIX_RE = /\?.*$/;
326
+ /** the seed script in a rendered document (report time, one document: not a hot path) */
327
+ const SEED_SCRIPT_RE = /<script\b[^>]*type="application\/ogygia-page"[^>]*>([\s\S]*?)<\/script>/i;
328
+ const SRC_RELATIVE_RE = /(?:^|[/\\])src[/\\](.*)$/;
329
+ const BACKSLASH_G = /\\/g;
330
+ const PATH_SEP_RE = /[/\\]/;
331
+ const TRAILING_SLASH_RE = /\/$/;
332
+ /** a WebAssembly script "file" as V8 names it: `/wasm/00034eea`, `wasm://wasm/…` */
333
+ const WASM_FRAME_RE = /^(?:wasm:\/\/|\/wasm\/)|[/\\]wasm\/[0-9a-f]+$/;
334
+ /** a resolved caller that lives in node_modules or Kit's runtime source: the framework, not the app */
335
+ const FRAMEWORK_SOURCE_RE = /[/\\]node_modules[/\\]|(?:^|[/\\])runtime[/\\](?:server|app|telemetry)[/\\]/;
336
+ const NODE_MODULES_RE = /[/\\]node_modules[/\\]/;
337
+ /** a Kit endpoint file in a resolved caller string (`+server.ts`, or its built `_server.ts.js`) */
338
+ const SERVER_ROUTE_FILE_RE = /[/\\](?:\+server\.[jt]s|_server\.[jt]s\.js):\d+/;
339
+ /** the file inside a resolved caller string: `name (file:line)` or `file:line` */
340
+ const CALLER_FILE_RE = /(?:\(|^)([^()]+):\d+\)?$/;
341
+ /** what the dev source peek may read */
342
+ const SOURCE_EXT_RE = /\.(?:svelte|ts|js|mjs|cjs|tsx|jsx|svx|md|json)$/;
343
+ const SERVERLESS_RESERVE_MS = 5e3;
344
+ /** the app lines the pattern finder reads, and the ones the report keeps and shows */
345
+ const LEDGER_FOR_PATTERNS = 200;
346
+ const LEDGER_SHOWN = 40;
347
+ /** how long a page recording waits for its store write before answering (inside the reserve) */
348
+ const PERSIST_WAIT_MS = 3e3;
349
+ /** The whole-request wall budget (ms) before the platform's gateway kills a /page profile — Infinity
350
+ * on a real server (adapter-node, dev). An explicit OGYGIA_PROFILER_BUDGET_MS wins over detection. */
351
+ function detect_request_budget_ms() {
352
+ const override = Number(process.env.OGYGIA_PROFILER_BUDGET_MS);
353
+ if (Number.isFinite(override) && override > 0) return override;
354
+ const env = process.env;
355
+ if (env.VERCEL) return 3e5;
356
+ if (env.NETLIFY) return 1e4;
357
+ if (env.AWS_LAMBDA_FUNCTION_NAME || env.LAMBDA_TASK_ROOT) return 3e4;
358
+ return Infinity;
359
+ }
360
+ /** Time allotted to the profiling work itself (warm-up + runs + coverage), keeping RESERVE for the
361
+ * rest of the request. Halves as a floor so a very small budget still spends most of itself working. */
362
+ function serverless_work_budget_ms() {
363
+ const req = detect_request_budget_ms();
364
+ if (!Number.isFinite(req)) return Infinity;
365
+ return Math.max(req - SERVERLESS_RESERVE_MS, Math.floor(req * .5));
366
+ }
367
+ /**
368
+ * Page mode renders the SAME page N times, so its outbound calls are the same handful repeated N
369
+ * times — a waterfall of 5×3 identical bars is noise. Given each run's [start,end] window, keep only
370
+ * ONE representative render's net + io (the run that captured the most calls — the fullest picture;
371
+ * ties go to the later, warmer run) so the network view reads as "one request + its leaf calls". CPU
372
+ * stays merged across all runs (more samples = steadier median); network is inherently per-render.
373
+ */
374
+ function scope_net_to_one_run(cap, windows) {
375
+ if (windows.length <= 1) return windows[0] ?? null;
376
+ const within = (t, w) => t >= w.start && t <= w.end;
377
+ let best = windows[0];
378
+ let best_n = -1;
379
+ for (const w of windows) {
380
+ const n = cap.net.reduce((s, c) => s + (within(c.start, w) ? 1 : 0), 0);
381
+ if (n >= best_n) {
382
+ best_n = n;
383
+ best = w;
384
+ }
385
+ }
386
+ cap.net = cap.net.filter((c) => within(c.start, best));
387
+ cap.io_ops = cap.io_ops.filter((o) => within(o.start, best));
388
+ return best;
389
+ }
390
+ /**
391
+ * The profiler as a class — state lives in fields, not closures, matching the
392
+ * `OgygiaHandle` house style. `profiler(options)` (below) constructs one and
393
+ * returns its bound `handle`.
394
+ */
395
+ var Profiler = class {
396
+ base;
397
+ sample_interval;
398
+ max_reports;
399
+ ring_size;
400
+ want_network;
401
+ want_heap;
402
+ dev;
403
+ want_server_timing;
404
+ secret;
405
+ ui_enabled;
406
+ #disabled;
407
+ #ring = [];
408
+ #reports = /* @__PURE__ */ new Map();
409
+ /** the browser's hydration timings, by island fingerprint (`/beacon`) — joined into reports at view time */
410
+ #beacons = /* @__PURE__ */ new Map();
411
+ /** the browser's web vitals per page path (`/beacon`), the profiler user's own visits */
412
+ #vitals = /* @__PURE__ */ new Map();
413
+ /** the app's own browser marks per page path, per name (`mark()` from ogygia/profiler/client) */
414
+ #marks = /* @__PURE__ */ new Map();
415
+ /** the trap (catch the slow one) and the always-on sampler: their state */
416
+ #trap;
417
+ #client_cpu;
418
+ /** the browser's CPU profile of hydration, per page path (the profiler user's latest visit) */
419
+ #client_cpus = /* @__PURE__ */ new Map();
420
+ /** per page, the slowest interaction's CPU (its wait and its handlers), the latest trace */
421
+ #interaction_cpus = /* @__PURE__ */ new Map();
422
+ /** the beacon's visits per page (the browser's picture of a page load), a few kept each */
423
+ #visits = /* @__PURE__ */ new Map();
424
+ /** THE SINK: rows an ephemeral host posts out before it dies (see sink.ts) */
425
+ #sink = new SinkBuffer();
426
+ #sink_url = null;
427
+ #sink_key = null;
428
+ #sink_every = 1e4;
429
+ #sink_timer = null;
430
+ #sink_inflight = false;
431
+ #sink_last = null;
432
+ #trap_caught = 0;
433
+ #trap_running = false;
434
+ #trap_timer = null;
435
+ #sample;
436
+ #sampled = [];
437
+ #sample_timer = null;
438
+ #background_note = null;
439
+ /** bytes per asset URL, measured once per process */
440
+ #weights = /* @__PURE__ */ new Map();
441
+ /** every page asset weighed so far (hashed assets never change): page-assets.ts */
442
+ #asset_weights = /* @__PURE__ */ new Map();
443
+ #background_net = [];
444
+ #window_net = null;
445
+ #inflight = 0;
446
+ #recording_since = 0;
447
+ /** performance.now() when this instance's first request arrived (its cold start, on Lambda) */
448
+ #first_request_pt;
449
+ /** requests this instance has handled */
450
+ #requests_seen = 0;
451
+ #recorder_busy = false;
452
+ /** page profiles waiting for a background window to give the recorder back */
453
+ #page_waiting = 0;
454
+ /** the durable report store (SQLite/Redis/Postgres/custom), when configured — else null and the
455
+ * in-memory map is all there is (the prior behaviour) */
456
+ #store = null;
457
+ #als = null;
458
+ #init_done = null;
459
+ /** Re-assert the `globalThis.fetch` patch before a profile (self-heal if it was replaced). */
460
+ #ensure_net = null;
461
+ /** wraps Kit's `event.fetch` (in-process same-origin dispatch) with the recorder, per attributed request */
462
+ #wrap_event_fetch = null;
463
+ /** the network layer's kept answers (net.ts): what the keep-the-answers render is served */
464
+ #kept = null;
465
+ /** the keep-the-answers render in flight: its request carries this token, and only these calls
466
+ * (`GET <url>`, the same answer in every timed render) are served from memory */
467
+ #answers = null;
468
+ constructor(options = {}) {
469
+ this.base = options.path ?? "/__profiler";
470
+ this.sample_interval = clamp(options.sampleInterval ?? 500, 50, 1e4);
471
+ this.max_reports = options.maxReports ?? 6;
472
+ this.ring_size = options.recentRequests ?? 500;
473
+ this.want_network = options.network !== false;
474
+ this.want_heap = options.heap !== false;
475
+ this.dev = detect_dev();
476
+ this.want_server_timing = options.serverTiming ?? this.dev;
477
+ this.secret = options.secret ?? process.env.OGYGIA_PROFILER_SECRET ?? "";
478
+ this.ui_enabled = options.enabled !== false && (this.dev || this.secret !== "");
479
+ this.#disabled = options.enabled === false;
480
+ this.#trap = options.trap && options.trap.over > 0 ? options.trap : null;
481
+ if (options.store) {
482
+ this.#store = options.store;
483
+ (async () => this.#store.init?.())().catch((e) => {
484
+ console.error("[ogygia/profiler] store init failed; falling back to in-memory:", e);
485
+ this.#store = null;
486
+ });
487
+ }
488
+ this.#client_cpu = options.clientCpu !== false;
489
+ if (options.sink?.url) {
490
+ this.#sink_url = options.sink.url;
491
+ this.#sink_key = options.sink.key ?? this.secret ?? null;
492
+ this.#sink_every = Math.max(1e3, options.sink.every ?? 1e4);
493
+ if (!Number.isFinite(serverless_work_budget_ms())) {
494
+ this.#sink_timer = setInterval(() => void this.#sink_flush(), this.#sink_every);
495
+ this.#sink_timer.unref?.();
496
+ }
497
+ }
498
+ this.#sample = options.sample ? options.sample : null;
499
+ globalThis[LIVE_PROFILER] = this;
500
+ if (!this.#disabled) set_batch_hole_listener((request, s) => this.#batch_hole(request, s));
501
+ }
502
+ /** Logged requests by their Request (a batch's holes find their request here), and the holes that
503
+ * settled before their request was logged. */
504
+ #logged = /* @__PURE__ */ new WeakMap();
505
+ #early_holes = /* @__PURE__ */ new WeakMap();
506
+ /** ONE HOLE OF A BATCH, logged as its own hole request — the shape every hole reading here knows
507
+ * (the cache table, the render slots, a visit's holes): its own time within the batch, no CPU of
508
+ * its own (the batch request carries that). */
509
+ #batch_hole(request, s) {
510
+ const base = this.#logged.get(request);
511
+ if (!base) {
512
+ const early = this.#early_holes.get(request);
513
+ if (early) early.push(s);
514
+ else this.#early_holes.set(request, [s]);
515
+ return;
516
+ }
517
+ const { ms, status, ...hole } = s;
518
+ this.#ring.push({
519
+ ...base,
520
+ ms: round2(ms),
521
+ cpu_ms: 0,
522
+ status,
523
+ hole,
524
+ og: void 0
525
+ });
526
+ if (this.#ring.length > this.ring_size) this.#ring.shift();
527
+ }
528
+ /** A newer profiler took this process over (a dev server that re-ran the app's hooks). */
529
+ #superseded() {
530
+ return globalThis[LIVE_PROFILER] !== this;
531
+ }
532
+ /** Stand down: the background timers stop (a sink sends what it holds first). */
533
+ #retire() {
534
+ if (this.#trap_timer) clearTimeout(this.#trap_timer);
535
+ if (this.#sample_timer) clearTimeout(this.#sample_timer);
536
+ this.#trap_timer = null;
537
+ this.#sample_timer = null;
538
+ if (this.#sink_timer) {
539
+ clearInterval(this.#sink_timer);
540
+ this.#sink_timer = null;
541
+ this.#sink_flush();
542
+ }
543
+ }
544
+ /** Arm the background recorders (once, from the first request). Both need a real server: on a
545
+ * serverless host an instance lives for one request and the inspector is often unavailable. */
546
+ #arm_background() {
547
+ if (!this.#trap && !this.#sample) return;
548
+ if (Number.isFinite(serverless_work_budget_ms())) {
549
+ this.#background_note = "trap / sampling are off on this serverless host (an instance lives for one request).";
550
+ return;
551
+ }
552
+ if (this.#trap && !this.#trap_timer) this.#schedule_trap(1e3);
553
+ if (this.#sample && !this.#sample_timer) this.#schedule_sample((this.#sample.every ?? 60) * 1e3);
554
+ }
555
+ #schedule_trap(delay_ms) {
556
+ this.#trap_timer = setTimeout(() => void this.#trap_cycle(), delay_ms);
557
+ this.#trap_timer.unref?.();
558
+ }
559
+ #schedule_sample(delay_ms) {
560
+ this.#sample_timer = setTimeout(() => void this.#sample_cycle(), delay_ms);
561
+ this.#sample_timer.unref?.();
562
+ }
563
+ /** One rolling trap window: sample coarsely, then keep the window only if a request in it blew
564
+ * past the threshold. The slowest such request is the report's request; the timeline is its. */
565
+ async #trap_cycle() {
566
+ const t = this.#trap;
567
+ this.#trap_timer = null;
568
+ if (this.#superseded()) return this.#retire();
569
+ const keep = t.keep ?? 3;
570
+ if (this.#trap_caught >= keep) return;
571
+ if (this.#page_waiting) return this.#schedule_trap(1e3);
572
+ if (!this.#try_acquire_recorder()) return this.#schedule_trap(2e3);
573
+ this.#trap_running = true;
574
+ this.#recording_since = Date.now();
575
+ try {
576
+ const window_ms = t.window ?? 5e3;
577
+ const cap = await this.#capture_window((t.interval ?? 10) * 1e3, () => new Promise((r) => setTimeout(r, window_ms)), { light: true });
578
+ const slow = this.#ring.filter((e) => !e.internal && e.pt !== void 0 && e.ts >= cap.t0 && e.ts + e.ms <= cap.t1 && e.ms >= t.over).sort((x, y) => y.ms - x.ms)[0];
579
+ if (slow) {
580
+ cap.window = {
581
+ start: slow.pt,
582
+ end: slow.pt + slow.ms
583
+ };
584
+ const id = await this.#finish_report(cap, {
585
+ trigger: "trap",
586
+ request: {
587
+ method: slow.method,
588
+ path: slow.path,
589
+ route: slow.route,
590
+ ms: slow.ms
591
+ },
592
+ trap_over: t.over
593
+ });
594
+ const stored = this.#reports.get(id);
595
+ if (stored) stored.replay = {
596
+ path: slow.path + (slow.search ?? ""),
597
+ headers: slow.replay_headers ?? {}
598
+ };
599
+ if (stored && this.#sink_url) {
600
+ this.#sink.push({
601
+ k: "trap",
602
+ t: stored.meta.created,
603
+ path: slow.path,
604
+ ms: slow.ms,
605
+ id,
606
+ dump: report_dump(stored.analysis, stored.meta, this.#report_extras(stored))
607
+ });
608
+ this.#sink_flush();
609
+ }
610
+ this.#trap_caught++;
611
+ }
612
+ } catch {
613
+ this.#trap_caught = keep;
614
+ } finally {
615
+ this.#trap_running = false;
616
+ this.#recording_since = 0;
617
+ this.#release_als();
618
+ this.#release_recorder();
619
+ }
620
+ if (this.#trap_caught < keep) this.#schedule_trap(1e3);
621
+ }
622
+ /** One always-on sample: a short coarse window folded into the rolling hot-functions table. */
623
+ async #sample_cycle() {
624
+ const s = this.#sample;
625
+ this.#sample_timer = null;
626
+ if (this.#superseded()) return this.#retire();
627
+ const every_ms = (s.every ?? 60) * 1e3;
628
+ if (this.#page_waiting) return this.#schedule_sample(Math.min(every_ms, 2e3));
629
+ if (!this.#try_acquire_recorder()) return this.#schedule_sample(Math.min(every_ms, 5e3));
630
+ this.#trap_running = true;
631
+ this.#recording_since = Date.now();
632
+ try {
633
+ const window_ms = s.window ?? 2e3;
634
+ const cap = await this.#capture_window((s.interval ?? 10) * 1e3, () => new Promise((r) => setTimeout(r, window_ms)), { light: true });
635
+ const resolver = await this.#make_resolver();
636
+ const a = analyze(cap.profile, resolver, void 0, void 0, void 0, this.#module_hint);
637
+ this.#sampled.push({
638
+ at: cap.t0,
639
+ ms: cap.duration_ms,
640
+ busy_ms: a.busy_ms,
641
+ functions: a.functions.slice(0, SAMPLED_FUNCTIONS_PER_WINDOW).map((f) => ({
642
+ key: f.key,
643
+ name: f.name,
644
+ url: f.url,
645
+ line: f.line,
646
+ category: f.category,
647
+ pkg: f.pkg,
648
+ self_ms: f.self_ms
649
+ }))
650
+ });
651
+ while (this.#sampled.length > MAX_SAMPLED_WINDOWS) this.#sampled.shift();
652
+ if (this.#sink_url) {
653
+ const w = this.#sampled[this.#sampled.length - 1];
654
+ this.#sink.push({
655
+ k: "win",
656
+ t0: w.at,
657
+ t1: w.at + w.ms,
658
+ fns: w.functions.slice(0, 40).map((f) => ({
659
+ name: f.name,
660
+ file: f.url,
661
+ self_ms: f.self_ms,
662
+ category: f.category
663
+ }))
664
+ });
665
+ }
666
+ } catch {
667
+ return;
668
+ } finally {
669
+ this.#trap_running = false;
670
+ this.#recording_since = 0;
671
+ this.#release_als();
672
+ this.#release_recorder();
673
+ }
674
+ this.#schedule_sample(every_ms);
675
+ }
676
+ /** The always-on table: every kept window folded, hot functions by summed self time. */
677
+ #sampled_summary() {
678
+ if (!this.#sample) return null;
679
+ const by_key = /* @__PURE__ */ new Map();
680
+ let sampled_ms = 0;
681
+ let busy_ms = 0;
682
+ for (const w of this.#sampled) {
683
+ sampled_ms += w.ms;
684
+ busy_ms += w.busy_ms;
685
+ for (const f of w.functions) {
686
+ const g = by_key.get(f.key);
687
+ if (g) {
688
+ g.self_ms = round2(g.self_ms + f.self_ms);
689
+ g.windows++;
690
+ } else by_key.set(f.key, {
691
+ ...f,
692
+ windows: 1
693
+ });
694
+ }
695
+ }
696
+ return {
697
+ every_s: this.#sample.every ?? 60,
698
+ window_ms: this.#sample.window ?? 2e3,
699
+ windows: this.#sampled.length,
700
+ since: this.#sampled[0]?.at ?? null,
701
+ sampled_ms: round2(sampled_ms),
702
+ busy_ms: round2(busy_ms),
703
+ functions: [...by_key.values()].sort((x, y) => y.self_ms - x.self_ms).slice(0, 40)
704
+ };
705
+ }
706
+ #collect_window(call) {
707
+ if (this.#window_net && this.#window_net.length < MAX_WINDOW_NET) this.#window_net.push(call);
708
+ }
709
+ /** An attributed request's context: its own calls (capped), each also offered to the window. */
710
+ #request_ctx(entry) {
711
+ const collect_window = (call) => this.#collect_window(call);
712
+ return {
713
+ entry,
714
+ net: [],
715
+ spans: [],
716
+ route: entry.route,
717
+ path: entry.path,
718
+ on_net(call) {
719
+ if (this.net.length < MAX_NET_PER_REQUEST) this.net.push(call);
720
+ collect_window(call);
721
+ }
722
+ };
723
+ }
724
+ #init() {
725
+ return this.#init_done ??= (async () => {
726
+ this.#arm_background();
727
+ if (!this.want_network) return;
728
+ try {
729
+ const { AsyncLocalStorage } = await import("node:async_hooks");
730
+ this.#als = new AsyncLocalStorage();
731
+ } catch {
732
+ this.#als = null;
733
+ }
734
+ if (this.#als) try {
735
+ const { install_net_capture, ensure_fetch_patched, wrap_event_fetch, keep_answers, kept_answer, drop_answers, answer_diff } = await import("./net.js");
736
+ this.#wrap_event_fetch = wrap_event_fetch;
737
+ this.#kept = {
738
+ keep: keep_answers,
739
+ get: kept_answer,
740
+ drop: drop_answers,
741
+ diff: answer_diff
742
+ };
743
+ await install_net_capture(this.#als, (call) => {
744
+ this.#collect_window(call);
745
+ this.#background_net.push(call);
746
+ if (this.#background_net.length > MAX_BACKGROUND_NET) this.#background_net.shift();
747
+ });
748
+ this.#ensure_net = ensure_fetch_patched;
749
+ } catch {}
750
+ })();
751
+ }
752
+ /** A recording is "active" only if one started recently — a run abandoned by a frozen/killed
753
+ * serverless invocation ages out after the cap instead of wedging the profiler forever. */
754
+ #recording_active() {
755
+ return this.#recording_since > 0 && Date.now() - this.#recording_since < RECORDING_MAX_MS;
756
+ }
757
+ /** Take the recorder slot without waiting: true if it was free (now ours), false if busy. Recordings
758
+ * serialize PER WORKER — one V8 inspector CPU/heap sampler + one attribution context per process — so
759
+ * a second recording on the same process is refused, NOT queued in memory: on serverless the process
760
+ * can be recycled at any moment (Amplify freezes a worker after ~30 s), so a waiter parked in memory
761
+ * is unreliable, and it isn't needed — a retry lands on a different, free worker. The "queue" is the
762
+ * CLIENT polling `/page` until a worker says yes (see RunView). */
763
+ #try_acquire_recorder() {
764
+ if (this.#recorder_busy) return false;
765
+ this.#recorder_busy = true;
766
+ return true;
767
+ }
768
+ /** Free the recorder slot. Called only when the recording is truly finished (its inspector session
769
+ * closed) — never from the site-unblock watchdog, which may fire while an orphaned run still holds
770
+ * the inspector. */
771
+ #release_recorder() {
772
+ this.#recorder_busy = false;
773
+ }
774
+ /** Production: detach the profiler's AsyncLocalStorage once a recording ends. An ALS that has
775
+ * run once stays enabled — every async hop in the process keeps paying a store copy for it — until
776
+ * `disable()`; the next `run()` re-enables it. Dev keeps it on (attribution is always on there). */
777
+ #release_als() {
778
+ if (!this.dev) this.#als?.disable();
779
+ }
780
+ async #capture_window(interval_us, work, opts = {}) {
781
+ const { Session } = await import("node:inspector/promises");
782
+ const perf_hooks = await import("node:perf_hooks");
783
+ await load_embedded_maps();
784
+ const session = new Session();
785
+ session.connect();
786
+ const histogram = perf_hooks.monitorEventLoopDelay({ resolution: 10 });
787
+ histogram.enable();
788
+ const elu0 = perf_hooks.performance.eventLoopUtilization();
789
+ const cpu0 = process.cpuUsage();
790
+ const gc_pauses = [];
791
+ const gc_raw = [];
792
+ const measures = [];
793
+ const ingest = (entries) => {
794
+ for (const e of entries) if (e.entryType === "gc") {
795
+ gc_pauses.push(e.duration);
796
+ const d = e.detail ?? {};
797
+ if (gc_raw.length < 5e3) gc_raw.push({
798
+ start: e.startTime ?? 0,
799
+ ms: e.duration,
800
+ kind: d.kind ?? 0,
801
+ flags: d.flags ?? 0
802
+ });
803
+ } else if (e.entryType === "measure" && measures.length < 5e3) measures.push({
804
+ name: e.name,
805
+ ms: e.duration
806
+ });
807
+ };
808
+ let observer;
809
+ try {
810
+ observer = new perf_hooks.PerformanceObserver((list) => ingest(list.getEntries()));
811
+ observer.observe({ entryTypes: ["gc", "measure"] });
812
+ } catch {}
813
+ const mem = [];
814
+ const t0 = Date.now();
815
+ const sample_mem = () => {
816
+ const m = process.memoryUsage();
817
+ mem.push({
818
+ t: Date.now() - t0,
819
+ rss: Math.round(m.rss / 1048576),
820
+ heap_used: Math.round(m.heapUsed / 1048576)
821
+ });
822
+ };
823
+ sample_mem();
824
+ const mem_timer = setInterval(sample_mem, 250);
825
+ mem_timer.unref?.();
826
+ const heap_series = [];
827
+ let heap_timer;
828
+ if (!opts.light) try {
829
+ const v8 = await import("node:v8");
830
+ const sample_heap = () => {
831
+ if (heap_series.length >= HEAP_SERIES_MAX) return;
832
+ heap_series.push({
833
+ t: perf_hooks.performance.now(),
834
+ mb: v8.getHeapStatistics().used_heap_size / 1048576
835
+ });
836
+ };
837
+ heap_timer = setInterval(sample_heap, HEAP_SERIES_PERIOD_MS);
838
+ heap_timer.unref?.();
839
+ } catch {}
840
+ this.#window_net = [];
841
+ const netmod = await import("./net.js").catch(() => null);
842
+ if (!opts.light) netmod?.set_stack_capture(true);
843
+ const spans = [];
844
+ let span_seq = 0;
845
+ const als = this.#als;
846
+ const { AsyncLocalStorage: SpanALS } = await import("node:async_hooks");
847
+ const span_als = new SpanALS();
848
+ set_span_recorder({
849
+ begin: (name, attrs) => {
850
+ const ctx = als?.getStore();
851
+ const parent = ctx?.span ?? span_als.getStore();
852
+ const rec = {
853
+ id: ++span_seq,
854
+ name: String(name).slice(0, 120),
855
+ start: perf_hooks.performance.now(),
856
+ ms: -1,
857
+ ...attrs ? { attrs: { ...attrs } } : {},
858
+ ...parent !== void 0 ? { parent } : {},
859
+ caller_site: netmod?.nearest_app_site(),
860
+ route: ctx?.route ?? null,
861
+ path: ctx?.path ?? null
862
+ };
863
+ if (spans.length < MAX_WINDOW_SPANS) spans.push(rec);
864
+ if (ctx && ctx.spans.length < MAX_SPANS_PER_REQUEST) ctx.spans.push(rec);
865
+ return rec;
866
+ },
867
+ within: (s, fn) => {
868
+ const ctx = als?.getStore();
869
+ return ctx && als ? als.run({
870
+ ...ctx,
871
+ span: s.id
872
+ }, fn) : span_als.run(s.id, fn);
873
+ },
874
+ end: (s, attrs, error) => {
875
+ s.ms = round2(perf_hooks.performance.now() - s.start);
876
+ if (attrs) s.attrs = {
877
+ ...s.attrs,
878
+ ...attrs
879
+ };
880
+ if (error !== void 0) s.error = error instanceof Error ? error.message : String(error);
881
+ },
882
+ tag: (key, value) => {
883
+ const ctx = als?.getStore();
884
+ if (!ctx) return;
885
+ const tags = ctx.entry.tags ??= {};
886
+ if (key in tags || Object.keys(tags).length < MAX_TAGS) tags[key] = value;
887
+ }
888
+ });
889
+ const iomod = opts.light ? null : await import("./async-io.js").catch(() => null);
890
+ let io_rec = iomod ? await iomod.record_async_io() : null;
891
+ let heap_head = null;
892
+ set_request_stats_detail(true);
893
+ try {
894
+ await session.post("Profiler.enable");
895
+ await session.post("Profiler.setSamplingInterval", { interval: interval_us });
896
+ let heap_on = false;
897
+ if (this.want_heap && !opts.light) try {
898
+ await session.post("HeapProfiler.enable");
899
+ if (opts.gc_attr !== false) try {
900
+ await session.post("HeapProfiler.startSampling", {
901
+ samplingInterval: GC_SAMPLING_BYTES,
902
+ ...GC_SAMPLING_FLAGS
903
+ });
904
+ heap_on = true;
905
+ } catch {
906
+ try {
907
+ await session.post("HeapProfiler.stopSampling");
908
+ await session.post("HeapProfiler.startSampling", {
909
+ samplingInterval: GC_SAMPLING_BYTES,
910
+ ...GC_SAMPLING_FLAGS
911
+ });
912
+ heap_on = true;
913
+ } catch (e) {
914
+ if (process.env.OGYGIA_PROFILER_DEBUG) console.error("[ogygia/profiler] allocation sampling unavailable, GC attribution off:", e);
915
+ await session.post("HeapProfiler.startSampling", { samplingInterval: 16384 });
916
+ }
917
+ }
918
+ else await session.post("HeapProfiler.startSampling", { samplingInterval: 16384 });
919
+ } catch {}
920
+ const perf_start = perf_hooks.performance.now();
921
+ await session.post("Profiler.start");
922
+ const gc_slices = [];
923
+ const gc_dict = {};
924
+ await work();
925
+ const perf_end = perf_hooks.performance.now();
926
+ const { profile } = await session.post("Profiler.stop");
927
+ learn_script_urls(profile);
928
+ if (this.want_heap && !opts.light) try {
929
+ heap_head = (await session.post("HeapProfiler.stopSampling")).profile?.head ?? null;
930
+ if (heap_on && heap_head) {
931
+ const cur = heap_sites(heap_head, gc_dict);
932
+ const bytes = {};
933
+ for (const [k, v] of cur) if (v > 0) bytes[k] = v;
934
+ gc_slices.push({
935
+ t0: 0,
936
+ t1: round2(perf_end - perf_start),
937
+ bytes
938
+ });
939
+ }
940
+ } catch {
941
+ heap_head = null;
942
+ }
943
+ const t1 = Date.now();
944
+ sample_mem();
945
+ try {
946
+ if (observer) ingest(observer.takeRecords());
947
+ } catch {}
948
+ const cpu = process.cpuUsage(cpu0);
949
+ const elu = perf_hooks.performance.eventLoopUtilization(elu0);
950
+ const duration_ms = t1 - t0;
951
+ const net = this.#window_net ?? [];
952
+ for (const s of spans) if (s.ms < 0) s.open = true;
953
+ return {
954
+ profile,
955
+ heap: heap_head,
956
+ mem,
957
+ net,
958
+ spans,
959
+ gc_pauses,
960
+ gc_events: gc_raw.filter((g) => g.start >= perf_start && g.start <= perf_end).map((g) => ({
961
+ t: round2(g.start - perf_start),
962
+ ms: round2(g.ms),
963
+ kind: gc_kind(g.kind),
964
+ flags: g.flags
965
+ })),
966
+ gc_slices,
967
+ gc_dict,
968
+ ...io_rec ? { promises: io_rec.promises() } : {},
969
+ io_ops: io_rec ? io_rec.stop() : [],
970
+ call_counts: {},
971
+ measures: summarize_measures(measures),
972
+ loop: {
973
+ p50: round2(histogram.percentile(50) / 1e6),
974
+ p99: round2(histogram.percentile(99) / 1e6),
975
+ max: round2(histogram.max / 1e6)
976
+ },
977
+ cpu_percent: duration_ms > 0 ? round2((cpu.user + cpu.system) / 1e3 / duration_ms * 100) : void 0,
978
+ elu_percent: round2(elu.utilization * 100),
979
+ t0,
980
+ t1,
981
+ duration_ms,
982
+ perf_start,
983
+ ...heap_series.length >= 3 ? { heap_series: heap_series.filter((h) => h.t >= perf_start && h.t <= perf_end).map((h) => ({
984
+ t: round2(h.t - perf_start),
985
+ mb: h.mb
986
+ })) } : {}
987
+ };
988
+ } finally {
989
+ clearInterval(mem_timer);
990
+ if (heap_timer) clearInterval(heap_timer);
991
+ histogram.disable();
992
+ set_span_recorder(null);
993
+ span_als.disable();
994
+ set_request_stats_detail(false);
995
+ netmod?.set_stack_capture(false);
996
+ io_rec?.stop();
997
+ try {
998
+ observer?.disconnect();
999
+ } catch {}
1000
+ this.#window_net = null;
1001
+ try {
1002
+ session.disconnect();
1003
+ } catch {}
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Exact per-function call counts (the ×N), from V8 precise coverage. Run as
1008
+ * its OWN render, not during the CPU sampling window — coverage disables
1009
+ * inlining, so mixing it into the sampled profile makes tiny hot functions
1010
+ * (svelte's `child`, `push`) look like the bottleneck. Counts are per this
1011
+ * single render.
1012
+ */
1013
+ /** Call counts from one render under precise coverage. That render runs with V8's inlining off,
1014
+ * so a CPU profile taken during it (`with_profile`) keeps every app function in its own frame:
1015
+ * the report reads it for WHO a merged line's time belongs to (`unsmear`), never for how long
1016
+ * anything takes (no inlining makes everything slower). Same render, no extra one. */
1017
+ async #count_calls(work, with_profile = false) {
1018
+ const counts = {};
1019
+ let profile;
1020
+ try {
1021
+ const { Session } = await import("node:inspector/promises");
1022
+ const session = new Session();
1023
+ session.connect();
1024
+ try {
1025
+ await session.post("Profiler.enable");
1026
+ await session.post("Profiler.startPreciseCoverage", {
1027
+ callCount: true,
1028
+ detailed: false
1029
+ });
1030
+ if (with_profile) {
1031
+ await session.post("Profiler.setSamplingInterval", { interval: 200 });
1032
+ await session.post("Profiler.start");
1033
+ }
1034
+ try {
1035
+ await work();
1036
+ } finally {
1037
+ if (with_profile) try {
1038
+ profile = (await session.post("Profiler.stop")).profile;
1039
+ } catch {
1040
+ profile = void 0;
1041
+ }
1042
+ }
1043
+ const cov = await session.post("Profiler.takePreciseCoverage");
1044
+ await session.post("Profiler.stopPreciseCoverage");
1045
+ for (const script of cov.result) for (const fn of script.functions) {
1046
+ const nm = fn.functionName;
1047
+ if (!nm || nm.startsWith("(")) continue;
1048
+ const c = fn.ranges[0]?.count ?? 0;
1049
+ if (c > 0) {
1050
+ const k = nm + "\0" + script.url;
1051
+ counts[k] = (counts[k] ?? 0) + c;
1052
+ if (script.scriptId) {
1053
+ const ki = nm + "\0#" + script.scriptId;
1054
+ counts[ki] = (counts[ki] ?? 0) + c;
1055
+ }
1056
+ }
1057
+ }
1058
+ } finally {
1059
+ session.disconnect();
1060
+ }
1061
+ } catch {}
1062
+ return {
1063
+ counts,
1064
+ ...profile ? { profile } : {}
1065
+ };
1066
+ }
1067
+ /** URL → local client file, for the browser CPU profile (see client-files.ts); undefined off Node */
1068
+ async #client_file_finder(origin) {
1069
+ try {
1070
+ const fs = await import("node:fs");
1071
+ const path = await import("node:path");
1072
+ const entry = process.argv[1] ? path.dirname(process.argv[1]) : void 0;
1073
+ const dirs = client_dir_candidates(entry, process.cwd(), path.join);
1074
+ return client_file_finder(origin, dirs, (p) => fs.existsSync(p), path.join);
1075
+ } catch {
1076
+ return;
1077
+ }
1078
+ }
1079
+ /** THE MODULE MAP's word on a bundled server frame (no sourcemap needed): the profiler's own code,
1080
+ * ogygia's runtime or a bundled package, else nothing (the app's) — `analyze`'s `url_category` */
1081
+ #module_find = module_lookup();
1082
+ /** A BUILD WITHOUT SOURCEMAPS: a line of a built chunk named by the source module it came from
1083
+ * (the build's module map), so `chunks/format.js:131` reads as src/lib/format.ts */
1084
+ /** a position in a BUILT chunk that the module map files under the app (no sourcemap to resolve
1085
+ * it): the place to charge memory to, as the ledger names chunk lines. Undefined otherwise */
1086
+ #chunk_at(url, line) {
1087
+ if (!url || !line || line <= 0) return void 0;
1088
+ let path = url;
1089
+ if (path.startsWith("file://")) try {
1090
+ path = decodeURIComponent(new URL(path).pathname);
1091
+ } catch {
1092
+ return;
1093
+ }
1094
+ if (!may_be_chunk(path)) return void 0;
1095
+ const id = this.#module_find(path, line);
1096
+ return id && !module_category(id) ? {
1097
+ path,
1098
+ line
1099
+ } : void 0;
1100
+ }
1101
+ #label_module(x) {
1102
+ if (x.module || !may_be_chunk(x.path)) return;
1103
+ const id = this.#module_find(x.path, x.line);
1104
+ if (id && !module_category(id)) x.module = id;
1105
+ }
1106
+ #module_hint = (url, _name, line) => {
1107
+ const id = this.#module_find(url, line);
1108
+ return id ? module_category(id) : void 0;
1109
+ };
1110
+ async #make_resolver() {
1111
+ try {
1112
+ const fs = await import("node:fs");
1113
+ const path = await import("node:path");
1114
+ const entry = process.argv[1] ? path.dirname(process.argv[1]) : void 0;
1115
+ const bases = entry ? [
1116
+ entry,
1117
+ path.dirname(entry),
1118
+ "."
1119
+ ] : ["."];
1120
+ const is_abs = (p) => p.startsWith("/") || WIN_DRIVE_RE.test(p);
1121
+ await load_embedded_maps();
1122
+ const dev_map = this.dev ? globalThis[Symbol.for("ogygia.profiler.dev-maps")] : void 0;
1123
+ const dev_client_map = this.dev ? globalThis[Symbol.for("ogygia.profiler.dev-client-maps")] : void 0;
1124
+ return sourcemap_resolver((p) => {
1125
+ if (dev_map && p.endsWith(".map")) {
1126
+ const m = dev_map(p.slice(0, -4));
1127
+ if (m) return m;
1128
+ }
1129
+ if (dev_client_map && p.startsWith("/") && p.endsWith(".map")) {
1130
+ const m = dev_client_map(p.slice(0, -4));
1131
+ if (m) return m;
1132
+ }
1133
+ for (const base of is_abs(p) ? [""] : bases) try {
1134
+ return fs.readFileSync(base ? path.join(base, p) : p, "utf8");
1135
+ } catch {}
1136
+ return p.endsWith(".map") ? embedded_map_of(p.slice(0, -4)) : embedded_source(p) ?? embedded_chunk(p);
1137
+ }, (chunk, line) => {
1138
+ const id = this.#module_find(chunk, line);
1139
+ if (!id || id.startsWith("\0") || id.startsWith("virtual:")) return void 0;
1140
+ if (id.startsWith("src/")) {
1141
+ if (!SCRIPT_MODULE_EXTS.some((e) => id.endsWith(e))) return void 0;
1142
+ return {
1143
+ source: path.resolve(process.cwd(), id),
1144
+ text: true
1145
+ };
1146
+ }
1147
+ return {
1148
+ source: path.resolve(process.cwd(), id),
1149
+ text: false
1150
+ };
1151
+ }, { any_ext: !!dev_map });
1152
+ } catch {
1153
+ return;
1154
+ }
1155
+ }
1156
+ async #finish_report(cap, meta_partial, fetch_url) {
1157
+ const own = `${this.base}/`;
1158
+ const path_of = (u) => {
1159
+ if (u.startsWith("/")) return u;
1160
+ try {
1161
+ return new URL(u).pathname;
1162
+ } catch {
1163
+ return u;
1164
+ }
1165
+ };
1166
+ cap.net = cap.net.filter((c) => !path_of(c.url).startsWith(own));
1167
+ const resolver = await this.#make_resolver();
1168
+ const heap = cap.heap ? analyze_heap(cap.heap) : null;
1169
+ const heap_components = cap.heap ? heap_by_component(cap.heap) : null;
1170
+ const rel_src = (f) => {
1171
+ const clean = f.replace(QUERY_SUFFIX_RE, "");
1172
+ const m = SRC_RELATIVE_RE.exec(clean);
1173
+ if (m) return m[1].replace(BACKSLASH_G, "/");
1174
+ return clean.split(PATH_SEP_RE).slice(-3).join("/");
1175
+ };
1176
+ /** the source position a caller resolved to (absolute when the map gave one): what the
1177
+ * pattern finder reads the call site's code from */
1178
+ let last_at;
1179
+ const resolve_caller = (site) => {
1180
+ last_at = void 0;
1181
+ if (!site) return void 0;
1182
+ const name = site.fn && site.fn !== "(anonymous)" && !site.fn.includes(".") ? site.fn : void 0;
1183
+ let file = rel_src(site.file);
1184
+ let line = site.line;
1185
+ const m = resolver?.resolve(site.file, Math.max(0, site.line - 1), Math.max(0, site.column - 1));
1186
+ if (m) {
1187
+ file = rel_src(m.source);
1188
+ line = m.line;
1189
+ }
1190
+ if (file.startsWith("profiler/") || file.includes("/profiler/")) return void 0;
1191
+ if (WASM_FRAME_RE.test(file)) return void 0;
1192
+ if (FRAMEWORK_SOURCE_RE.test(file) || m && NODE_MODULES_RE.test(m.source)) return void 0;
1193
+ last_at = {
1194
+ path: m ? m.source : site.file,
1195
+ line
1196
+ };
1197
+ return name ? `${name} (${file}:${line})` : `${file}:${line}`;
1198
+ };
1199
+ for (const c of cap.net) {
1200
+ const ats = [];
1201
+ const chain = (c.caller_chain ?? (c.caller_site ? [c.caller_site] : [])).map((site) => {
1202
+ const text = resolve_caller(site);
1203
+ if (text && last_at && ats.length < 2) ats.push(last_at);
1204
+ return text;
1205
+ }).filter((s) => !!s);
1206
+ c.caller = chain[0] ?? c.caller;
1207
+ if (ats[0]) c.caller_at = ats[0];
1208
+ if (ats[1]) c.outer_at = ats[1];
1209
+ if (chain.length > 1) c.callers = chain;
1210
+ delete c.caller_site;
1211
+ delete c.caller_chain;
1212
+ }
1213
+ for (const o of cap.io_ops) {
1214
+ o.caller = resolve_caller(o.caller_site);
1215
+ if (o.caller && last_at) o.caller_at = last_at;
1216
+ delete o.caller_site;
1217
+ }
1218
+ for (const s of cap.spans) {
1219
+ s.caller = resolve_caller(s.caller_site);
1220
+ delete s.caller_site;
1221
+ }
1222
+ if (cap.promises) {
1223
+ const merged = /* @__PURE__ */ new Map();
1224
+ for (const t of cap.promises.top) {
1225
+ const m = t.site ? resolver?.resolve(t.site.file, Math.max(0, t.site.line - 1), Math.max(0, t.site.column - 1)) : void 0;
1226
+ const src = m?.source ?? t.site?.file ?? "";
1227
+ const nm = src.lastIndexOf("/node_modules/");
1228
+ const fn = m?.name ?? t.site?.fn ?? "";
1229
+ const caller = !t.site ? t.caller : nm !== -1 ? `${fn} (${pkg_of(src.slice(nm + 14))})` : m ? `${fn} (${rel_src(m.source)}:${m.line})` : t.caller;
1230
+ merged.set(caller, (merged.get(caller) ?? 0) + t.share);
1231
+ }
1232
+ cap.promises = {
1233
+ count: cap.promises.count,
1234
+ top: [...merged].sort((a, b) => b[1] - a[1]).map(([caller, share]) => ({
1235
+ caller,
1236
+ share: Math.round(share * 100) / 100
1237
+ }))
1238
+ };
1239
+ }
1240
+ const requests = this.#ring.filter((e) => e.ts + e.ms >= cap.t0 && e.ts <= cap.t1);
1241
+ const page_path = meta_partial.page?.split("?")[0];
1242
+ const own_route = meta_partial.trigger === "page" ? requests.find((e) => e.internal && e.path === page_path && e.route)?.route : meta_partial.request?.route;
1243
+ const timeline_input = cap.window ? {
1244
+ ...this.#timeline_input(cap),
1245
+ ...own_route ? { route: own_route } : {}
1246
+ } : void 0;
1247
+ const analysis = analyze(cap.profile, resolver, cap.call_counts, timeline_input, island_host_renamer(requests), this.#module_hint);
1248
+ const id = Math.random().toString(36).slice(2, 10);
1249
+ const meta = {
1250
+ id,
1251
+ created: cap.t0,
1252
+ duration_ms: round2(cap.duration_ms),
1253
+ sample_interval_us: this.sample_interval,
1254
+ requests,
1255
+ loop_delay: cap.loop,
1256
+ cpu_percent: cap.cpu_percent,
1257
+ elu_percent: cap.elu_percent,
1258
+ rss_mb: cap.mem.at(-1)?.rss,
1259
+ node: process.version,
1260
+ dev: this.dev || void 0,
1261
+ ...meta_partial
1262
+ };
1263
+ let gc_attr;
1264
+ if (cap.gc_slices?.length && (cap.gc_events?.length || Object.keys(cap.gc_dict ?? {}).length)) try {
1265
+ if (resolver && cap.gc_dict) {
1266
+ const short = (u) => u.replace(/^file:\/\//, "").split("/").slice(-2).join("/");
1267
+ for (const site of Object.values(cap.gc_dict)) {
1268
+ const m = site.url ? resolver.resolve(site.url, site.line - 1, 0) : void 0;
1269
+ if (m) {
1270
+ site.url = m.source;
1271
+ site.line = m.line;
1272
+ const c = categorize({
1273
+ functionName: site.name,
1274
+ url: m.source,
1275
+ lineNumber: m.line - 1,
1276
+ columnNumber: 0,
1277
+ scriptId: ""
1278
+ });
1279
+ if (site.category !== "component" && c.category !== "component") site.category = c.category;
1280
+ }
1281
+ let caller_at;
1282
+ if (site.caller_url && site.caller_name) {
1283
+ const c = resolver.resolve(site.caller_url, (site.caller_line ?? 1) - 1, 0);
1284
+ if (c) {
1285
+ site.caller = `${site.caller_name} (${short(c.source)}:${c.line})`;
1286
+ caller_at = {
1287
+ path: c.source,
1288
+ line: c.line
1289
+ };
1290
+ if (site.category !== "component" && categorize({
1291
+ functionName: site.caller_name,
1292
+ url: c.source,
1293
+ lineNumber: 0,
1294
+ columnNumber: 0,
1295
+ scriptId: ""
1296
+ }).category === "profiler") site.category = "profiler";
1297
+ }
1298
+ }
1299
+ if ((site.category === "app" || site.category === "component") && m) site.at = {
1300
+ path: m.source,
1301
+ line: m.line
1302
+ };
1303
+ else if (caller_at) site.at = caller_at;
1304
+ else if (!m) {
1305
+ const at = this.#chunk_at(site.url, site.line) ?? this.#chunk_at(site.caller_url, site.caller_line);
1306
+ if (at) site.at = at;
1307
+ }
1308
+ }
1309
+ }
1310
+ const first = cap.mem[0]?.heap_used;
1311
+ const last = cap.mem[cap.mem.length - 1]?.heap_used;
1312
+ const window_offset_ms = cap.window ? round2(cap.window.start - cap.perf_start) : 0;
1313
+ gc_attr = attribute_gc({
1314
+ slices: cap.gc_slices,
1315
+ events: cap.gc_events ?? [],
1316
+ dict: cap.gc_dict ?? {},
1317
+ window_ms: cap.duration_ms,
1318
+ ...analysis.capture_cpu ? { running: analysis.capture_cpu } : analysis.timeline ? { running: analysis.timeline.segments.filter((s) => s.kind === "cpu").map((s) => ({
1319
+ t0: s.t0 + window_offset_ms,
1320
+ t1: s.t1 + window_offset_ms,
1321
+ label: s.label,
1322
+ category: s.category,
1323
+ file: s.file
1324
+ })) } : {},
1325
+ ...first !== void 0 && last !== void 0 ? { retained_mb: last - first } : {}
1326
+ });
1327
+ gc_attr.window_offset_ms = window_offset_ms;
1328
+ } catch (e) {
1329
+ if (process.env.OGYGIA_PROFILER_DEBUG) console.error("[ogygia/profiler] GC attribution failed:", e);
1330
+ gc_attr = void 0;
1331
+ }
1332
+ {
1333
+ const cpu_over = analysis.overhead_ms;
1334
+ const gc_over = gc_attr?.summary.overhead_ms ?? 0;
1335
+ const n = meta.runs?.length ?? 0;
1336
+ const per_run = Array.from({ length: n }, () => 0);
1337
+ const cpu_in_runs = analysis.overhead_by_run_ms ?? [];
1338
+ for (let i = 0; i < n; i++) per_run[i] += cpu_in_runs[i] ?? 0;
1339
+ if (gc_attr && cap.runs?.length) for (const p of gc_attr.pauses) {
1340
+ const t_abs = cap.perf_start + p.t;
1341
+ const i = cap.runs.findIndex((r) => t_abs >= r.start && t_abs <= r.end);
1342
+ if (i !== -1) per_run[i] += p.ms_measured - p.ms;
1343
+ }
1344
+ const in_runs = per_run.reduce((a, b) => a + b, 0);
1345
+ meta.overhead = {
1346
+ cpu_ms: round2(cpu_over),
1347
+ gc_ms: round2(gc_over),
1348
+ per_run_ms: n ? round2(in_runs / n) : 0,
1349
+ ...n ? { per_run: per_run.map(round2) } : {},
1350
+ note: "the profiler’s own CPU frames and its share of the GC pauses, taken out of every number in this report: each run loses exactly what fell inside it; the start-up scan of compiled code and the final reads sit outside the runs. The sampler’s own thread and the inspector’s are not visible from inside and are not taken out."
1351
+ };
1352
+ if (n && in_runs > 0) {
1353
+ meta.runs_measured = [...meta.runs];
1354
+ meta.runs = meta.runs.map((r, i) => round2(Math.max(0, r - per_run[i])));
1355
+ }
1356
+ }
1357
+ const gc = gc_attr ? {
1358
+ count: gc_attr.summary.count,
1359
+ total_ms: gc_attr.summary.total_ms,
1360
+ max_ms: gc_attr.summary.max_ms
1361
+ } : cap.gc_pauses.length ? {
1362
+ count: cap.gc_pauses.length,
1363
+ total_ms: round2(cap.gc_pauses.reduce((a, c) => a + c, 0)),
1364
+ max_ms: round2(Math.max(...cap.gc_pauses))
1365
+ } : null;
1366
+ const raw_json = JSON.stringify(cap.profile);
1367
+ let raw = raw_json;
1368
+ try {
1369
+ const { gzipSync } = await import("node:zlib");
1370
+ raw = gzipSync(raw_json);
1371
+ } catch {}
1372
+ let weights;
1373
+ let contents;
1374
+ let heavy;
1375
+ let barrels;
1376
+ let hazards;
1377
+ if (fetch_url && !this.dev) {
1378
+ const urls = [];
1379
+ for (const r of island_rows_of(meta)) urls.push(r.module_url, ...r.hints);
1380
+ if (urls.length) weights = await weigh_urls(urls, fetch_url, this.#weights);
1381
+ for (const u of new Set(urls)) {
1382
+ const inside = chunkContents(u);
1383
+ if (inside?.length) (contents ??= {})[u] = inside;
1384
+ const h = chunkHeavy(u);
1385
+ if (h?.top.length) (heavy ??= {})[u] = h;
1386
+ const b = chunkBarrels(u);
1387
+ if (b?.length) (barrels ??= {})[u] = b;
1388
+ }
1389
+ for (const r of island_rows_of(meta)) {
1390
+ const built = islandHazards(r.entry);
1391
+ if (built?.length) (hazards ??= {})[r.entry] = built;
1392
+ }
1393
+ if (contents) {
1394
+ const read = await this.#source_reader();
1395
+ for (const r of island_rows_of(meta)) {
1396
+ if (hazards?.[r.entry]) continue;
1397
+ const files = /* @__PURE__ */ new Set();
1398
+ for (const u of [r.module_url, ...r.hints]) for (const f of contents[u] ?? []) if (f.endsWith(".svelte") && f.startsWith("src/")) files.add(f);
1399
+ const found = [];
1400
+ for (const f of files) {
1401
+ const src = read(f.slice(4)) ?? read(f);
1402
+ if (src) for (const h of hydration_hazards(src).slice(0, 3)) found.push({
1403
+ file: f,
1404
+ ...h
1405
+ });
1406
+ }
1407
+ if (found.length) (hazards ??= {})[r.entry] = found.slice(0, 5);
1408
+ }
1409
+ }
1410
+ }
1411
+ let alloc;
1412
+ if (cap.heap_series?.length) try {
1413
+ const window_offset_ms = cap.window ? round2(cap.window.start - cap.perf_start) : 0;
1414
+ alloc = alloc_timeline({
1415
+ samples: cap.heap_series,
1416
+ ...analysis.capture_cpu ? { running: analysis.capture_cpu } : analysis.timeline ? { running: analysis.timeline.segments.filter((s) => s.kind === "cpu").map((s) => ({
1417
+ t0: s.t0 + window_offset_ms,
1418
+ t1: s.t1 + window_offset_ms,
1419
+ label: s.label,
1420
+ category: s.category,
1421
+ file: s.file
1422
+ })) } : {},
1423
+ gc: (cap.gc_events ?? []).map((g) => ({
1424
+ t: g.t,
1425
+ ms: g.ms
1426
+ })),
1427
+ ...cap.window ? { window: {
1428
+ offset_ms: window_offset_ms,
1429
+ ms: round2(cap.window.end - cap.window.start)
1430
+ } } : {}
1431
+ });
1432
+ } catch {
1433
+ alloc = void 0;
1434
+ }
1435
+ let contended;
1436
+ {
1437
+ const windows = cap.runs?.length ? cap.runs : cap.window ? [cap.window] : [];
1438
+ if (windows.length) try {
1439
+ const self_paths = /* @__PURE__ */ new Set();
1440
+ for (const c of cap.net) {
1441
+ const at = c.url.indexOf("://");
1442
+ const rest = at === -1 ? c.url : c.url.slice(c.url.indexOf("/", at + 3));
1443
+ const q = rest.indexOf("?");
1444
+ self_paths.add(q === -1 ? rest : rest.slice(0, q));
1445
+ }
1446
+ contended = contention({
1447
+ requests: this.#ring,
1448
+ windows,
1449
+ own: this.#ring.filter((e) => e.internal && e.pt !== void 0 && windows.some((w) => e.pt >= w.start - 1 && e.pt <= w.end)),
1450
+ self_paths
1451
+ });
1452
+ } catch {
1453
+ contended = void 0;
1454
+ }
1455
+ }
1456
+ this.#reports.set(id, {
1457
+ meta,
1458
+ analysis,
1459
+ heap,
1460
+ heap_components,
1461
+ net: cap.net,
1462
+ spans: cap.spans,
1463
+ mem: cap.mem,
1464
+ measures: cap.measures,
1465
+ gc,
1466
+ io: cap.io_ops,
1467
+ ...cap.promises ? { promises: cap.promises } : {},
1468
+ call_counts: cap.call_counts,
1469
+ raw,
1470
+ ...weights ? { weights } : {},
1471
+ ...contents ? { contents } : {},
1472
+ ...heavy ? { heavy } : {},
1473
+ ...barrels ? { barrels } : {},
1474
+ ...hazards ? { hazards } : {},
1475
+ ...gc_attr ? { gc_attr } : {},
1476
+ ...alloc ? { alloc } : {},
1477
+ ...contended ? { contention: contended } : {}
1478
+ });
1479
+ if (meta_partial.trigger !== "page") {
1480
+ const s = this.#reports.get(id);
1481
+ try {
1482
+ await this.#lines_for(s, void 0, cap.window?.start);
1483
+ } catch {}
1484
+ }
1485
+ if (meta_partial.trigger !== "page") this.#persist(this.#reports.get(id));
1486
+ while (this.#reports.size > this.max_reports) {
1487
+ const oldest = this.#reports.keys().next().value;
1488
+ if (oldest === void 0) break;
1489
+ this.#reports.delete(oldest);
1490
+ }
1491
+ return id;
1492
+ }
1493
+ async #key_matches(provided) {
1494
+ if (this.dev) return true;
1495
+ if (!this.secret || !provided) return false;
1496
+ try {
1497
+ const { createHash, timingSafeEqual } = await import("node:crypto");
1498
+ const h = (str) => createHash("sha256").update(str).digest();
1499
+ return timingSafeEqual(h(provided), h(this.secret)) || timingSafeEqual(h(provided), h(this.#cookie_token(createHash)));
1500
+ } catch {
1501
+ return provided === this.secret;
1502
+ }
1503
+ }
1504
+ #cookie_token(createHash) {
1505
+ return createHash("sha256").update("og-profiler:" + this.secret).digest("hex");
1506
+ }
1507
+ /** Cookie header that starts / clears a profiler session (Set-Cookie value). */
1508
+ #session_cookie(token, event) {
1509
+ const secure = event.url.protocol === "https:" ? "; Secure" : "";
1510
+ const clear = token === "" ? "; Max-Age=0" : "";
1511
+ const age = token === "" ? "" : "; Max-Age=31536000";
1512
+ return `${SESSION_COOKIE}=${token}; Path=${this.base}; HttpOnly; SameSite=Lax${secure}${clear}${age}`;
1513
+ }
1514
+ /** The BEACON FLAG: a second cookie, site-wide, carrying no secret — it only tells the handle
1515
+ * "this browser is the profiler's user, put the beacon tag in its documents". The session
1516
+ * cookie stays scoped to the profiler's own path (the `/beacon` POST is under it, so the
1517
+ * runtime's post still carries the real session). Set with the session, cleared with it. */
1518
+ #beacon_cookie(on, event) {
1519
+ const secure = event.url.protocol === "https:" ? "; Secure" : "";
1520
+ return `og_profiler_beacon=${on ? "1" : ""}; Path=/; HttpOnly; SameSite=Lax${secure}${on ? "" : "; Max-Age=0"}`;
1521
+ }
1522
+ /**
1523
+ * Validate + set up a session. The unlock page POSTs the key here; a match sets the session cookie
1524
+ * and returns to `next`. No HTTP Basic dialog — a plain form + cookie, so the `Authorization` header
1525
+ * is never spent on profiler auth (it stays free, and there's a real Logout).
1526
+ */
1527
+ async #login(ctx) {
1528
+ const body = await ctx.request.json().catch(() => null);
1529
+ const key = String(body?.key ?? "");
1530
+ const next = this.#safe_next(String(body?.next ?? this.base));
1531
+ if (!await this.#key_matches(key)) return ctx.json({ ok: false }, { status: 401 });
1532
+ const { createHash } = await import("node:crypto");
1533
+ const res = ctx.json({
1534
+ ok: true,
1535
+ next: next + (next.includes("?") ? "&" : "?") + AFTER_LOGIN_PARAM + "=1"
1536
+ });
1537
+ res.headers.set("cache-control", "no-store, private");
1538
+ res.headers.append("set-cookie", this.#session_cookie(this.#cookie_token(createHash), ctx.event));
1539
+ return res;
1540
+ }
1541
+ /** false = denied; true = authed; string = authed AND set this cookie */
1542
+ async #authed(event) {
1543
+ if (!this.ui_enabled) return false;
1544
+ if (this.dev) return true;
1545
+ for (const value of session_cookie_values(event)) if (await this.#key_matches(value)) return true;
1546
+ return this.#key_matches(event.request.headers.get("x-profiler-key"));
1547
+ }
1548
+ /**
1549
+ * What a request that failed auth carried, for a browser that loops on login — no cookie values,
1550
+ * only facts: did a Cookie header arrive, how big, how many `og_profiler` pairs were in it, and
1551
+ * did Kit's own parser find one. 0 pairs with a header = the cookie never reached the server
1552
+ * (dropped on the way, or the jar is past a proxy's limit); pairs but no parse = a parser problem;
1553
+ * more than one pair = duplicates, one of them stale.
1554
+ */
1555
+ #cookie_diagnosis(event) {
1556
+ const raw = event.request.headers.get("cookie");
1557
+ return {
1558
+ header: raw != null,
1559
+ bytes: raw?.length ?? 0,
1560
+ pairs: raw ? raw_cookie_values(raw, SESSION_COOKIE).length : 0,
1561
+ parsed: event.cookies.get(SESSION_COOKIE) != null
1562
+ };
1563
+ }
1564
+ async #ui(event) {
1565
+ const res = await this.#router().fetch(event.request, event) ?? new Response("Not found", { status: 404 });
1566
+ return this.#flag_cookie(event, await gzip_large(event.request, res));
1567
+ }
1568
+ /** THE BEACON FLAG follows the session, one response later: set on the first response to a
1569
+ * browser whose session cookie is good and that lacks the flag, cleared on one to a browser with
1570
+ * the flag and no session. Never beside another Set-Cookie (the login's, the logout's): a host
1571
+ * that keeps one per response must not lose the session for it. */
1572
+ async #flag_cookie(event, res) {
1573
+ if (this.dev || !this.secret || res.headers.has("set-cookie")) return res;
1574
+ const flag = event.cookies.get("og_profiler_beacon") === "1";
1575
+ const values = session_cookie_values(event);
1576
+ let session = false;
1577
+ for (const v of values) if (await this.#key_matches(v)) session = true;
1578
+ if (session === flag) return res;
1579
+ const out = new Response(res.body, res);
1580
+ out.headers.append("set-cookie", this.#beacon_cookie(session, event));
1581
+ return out;
1582
+ }
1583
+ async #auth_guard(ctx) {
1584
+ const rel = ctx.url.pathname.slice(this.base.length).replace(TRAILING_SLASH_RE, "") || "/";
1585
+ if (rel === "/login" || rel === "/logout" || /^\/report\/[^/.]+$/.test(rel)) return;
1586
+ if (await this.#authed(ctx.event)) return;
1587
+ if (this.ui_enabled && this.secret && !this.dev) {
1588
+ const after_login = ctx.url.searchParams.has(AFTER_LOGIN_PARAM);
1589
+ const diag = this.#cookie_diagnosis(ctx.event);
1590
+ if (after_login || diag.pairs > 0) console.warn(`[ogygia profiler] session not accepted on ${ctx.url.pathname}: ${JSON.stringify(diag)} — ${describe_cookie_diagnosis(diag)}`);
1591
+ const target = new URL(ctx.url);
1592
+ target.searchParams.delete(AFTER_LOGIN_PARAM);
1593
+ const next = encodeURIComponent(target.pathname + target.search);
1594
+ return ctx.redirect(`${this.base}/login?next=${next}${after_login ? `&${LOGIN_FAILED_PARAM}=1` : ""}`);
1595
+ }
1596
+ return new Response("Not found", { status: 404 });
1597
+ }
1598
+ #routes;
1599
+ #router() {
1600
+ return this.#routes ??= build_profiler_router({
1601
+ base: this.base,
1602
+ auth_guard: (c) => this.#auth_guard(c),
1603
+ authed: (c) => this.#authed(c.event),
1604
+ beacon: (c) => this.#beacon(c),
1605
+ replay: (id, c) => this.#replay(id, c),
1606
+ dashboard: (c) => this.#dashboard(c.url.searchParams.get("by")),
1607
+ run_page: (c) => this.#run_page(c),
1608
+ record_page: (c) => this.#record_page(c),
1609
+ window_start: (c) => this.#window_start(c),
1610
+ window_stop: (c) => this.#window_stop(c),
1611
+ reset: (c) => this.#reset(c),
1612
+ status: () => ({
1613
+ recording: this.#recorder_busy || this.#recording_active(),
1614
+ inflight: this.#inflight,
1615
+ rss_mb: Math.round(process.memoryUsage().rss / 1048576),
1616
+ reports: this.#reports.size
1617
+ }),
1618
+ login_props: (c) => ({
1619
+ base: this.base,
1620
+ next: this.#safe_next(c.url.searchParams.get("next")),
1621
+ session_problem: c.url.searchParams.has(LOGIN_FAILED_PARAM) ? describe_cookie_diagnosis(this.#cookie_diagnosis(c.event)) : null
1622
+ }),
1623
+ login: (c) => this.#login(c),
1624
+ logout: (c) => this.#logout(c),
1625
+ upload: (c) => this.#upload(c),
1626
+ report_stored: (id) => this.#reports.get(id ?? ""),
1627
+ report_load: (id) => this.#report_load(id),
1628
+ list_stored: (limit) => this.#list_stored(limit),
1629
+ login_url: (c) => this.ui_enabled && this.secret && !this.dev ? `${this.base}/login?next=${encodeURIComponent(c.url.pathname)}` : null,
1630
+ report_view: (stored) => this.#report_view(stored),
1631
+ report_json: (stored) => this.#report_json(stored),
1632
+ report_dump_json: (stored) => this.#report_dump_json(stored),
1633
+ report_html: (stored, c) => this.#report_html(stored, c),
1634
+ report_posted: (c, as) => this.#report_posted(c, as),
1635
+ report_raw: (stored) => this.#report_raw(stored),
1636
+ compare: (a, b) => this.#compare(a, b),
1637
+ site: (c) => this.#site(c),
1638
+ source: (c) => this.#source(c)
1639
+ });
1640
+ }
1641
+ /**
1642
+ * THE TIMELINE INPUT: the one request's window + every call it waited on. Net calls as they
1643
+ * are. I/O primitives only when they are the app's own wait — the same rule as the "Waiting by
1644
+ * function" table: the op must have a resolved app caller (an unattributed one is undici's
1645
+ * socket timer, a response compressor's zlib stream, the recorder's own timer; a WASM caller
1646
+ * is a library's internals) — and an op that sits inside a net call's span is the other end of
1647
+ * that same call (an in-process API's timer on a self-fetch); the net call already says it.
1648
+ * Each call carries the phase of the code that started it, from the caller's file.
1649
+ */
1650
+ #timeline_input(cap) {
1651
+ const w = cap.window;
1652
+ const phase_of = (caller) => {
1653
+ const m = caller ? CALLER_FILE_RE.exec(caller) : null;
1654
+ return m ? phase_of_frame({
1655
+ name: "",
1656
+ url: m[1],
1657
+ line: 0,
1658
+ category: "app"
1659
+ }) ?? void 0 : void 0;
1660
+ };
1661
+ const net = cap.net.filter((c) => c.ms >= 0).map((c) => ({
1662
+ start: c.start,
1663
+ end: c.start + c.ms + (c.body_ms ?? 0),
1664
+ c
1665
+ }));
1666
+ const inside_a_call = (s, e) => net.some((n) => s >= n.start - 2 && e <= n.end + 2);
1667
+ const overlaps_a_call = (s, e) => net.some((n) => s < n.end && e > n.start);
1668
+ const is_handler_side = (o) => !!o.caller && SERVER_ROUTE_FILE_RE.test(o.caller) && overlaps_a_call(o.start, o.start + o.ms);
1669
+ return {
1670
+ perf_start: cap.perf_start,
1671
+ window: w,
1672
+ ...cap.runs ? { runs: cap.runs } : {},
1673
+ ...cap.set_aside?.length ? { set_aside: cap.set_aside } : {},
1674
+ pending: cap.io_ops.filter((o) => o.start <= w.end && o.start + Math.max(o.ms, 0) >= w.start).map((o) => ({
1675
+ start: o.start,
1676
+ end: o.open ? w.end : o.start + o.ms,
1677
+ label: o.caller ? `${o.type} from ${o.caller}` : o.type,
1678
+ kind: io_kind(o.type)
1679
+ })),
1680
+ calls: [
1681
+ ...cap.spans.filter((s) => !s.open && s.ms >= 0).map((s) => ({
1682
+ start: s.start,
1683
+ ms: s.ms,
1684
+ label: s.name,
1685
+ kind: "span",
1686
+ caller: s.caller,
1687
+ phase: phase_of(s.caller)
1688
+ })),
1689
+ ...cap.net.map((c) => ({
1690
+ start: c.start,
1691
+ ms: c.ms < 0 ? -1 : c.ms + (c.body_ms ?? 0),
1692
+ label: label_call(c.method, c.url),
1693
+ kind: "net",
1694
+ caller: c.caller,
1695
+ phase: phase_of(c.caller),
1696
+ ...c.callers ? { callers: c.callers } : {},
1697
+ ...c.timings ? { timings: c.timings } : {},
1698
+ ...c.ms >= 0 && c.body_ms ? { body_ms: c.body_ms } : {}
1699
+ })),
1700
+ ...cap.io_ops.filter((o) => !o.open && o.ms >= .5 && o.type !== "Immediate" && o.type !== "TickObject" && !!o.caller && !o.caller.startsWith("/wasm/") && !o.caller.includes("(/wasm/") && !inside_a_call(o.start, o.start + o.ms) && !is_handler_side(o)).map((o) => ({
1701
+ start: o.start,
1702
+ ms: o.ms,
1703
+ label: `${io_kind(o.type)}${o.caller ? ` · ${o.caller}` : ""}`,
1704
+ kind: io_kind(o.type),
1705
+ caller: o.caller,
1706
+ phase: phase_of(o.caller)
1707
+ }))
1708
+ ]
1709
+ };
1710
+ }
1711
+ /**
1712
+ * The report as ONE self-contained HTML file (profiler/standalone.ts): the page is rendered
1713
+ * through this very server (a self-request, marked internal so it is not logged as traffic),
1714
+ * then every stylesheet, the runtime and every island chunk it reaches are fetched the same way
1715
+ * and inlined. Needs a BUILT app: a dev server's module graph is Vite's, hundreds of unbundled
1716
+ * files with transforms — not a thing one file can carry.
1717
+ */
1718
+ async #report_html(stored, ctx) {
1719
+ if (this.dev) return new Response("The standalone HTML export needs a built app (vite build + preview, or your production deploy): on the dev server the page’s code is hundreds of unbundled modules. Export the .ogp here and open it on a built server, or run the export there.", {
1720
+ status: 400,
1721
+ headers: { "content-type": "text/plain; charset=utf-8" }
1722
+ });
1723
+ const page_url = `${ctx.url.origin}${this.base}/report/${stored.meta.id}`;
1724
+ const headers = { "x-og-profiler-internal": "1" };
1725
+ if (this.secret) headers["x-profiler-key"] = this.secret;
1726
+ const load = async (url) => {
1727
+ for (const f of ctx.fetch === fetch ? [fetch] : [ctx.fetch, fetch]) try {
1728
+ const res = await f(url, { headers });
1729
+ if (res.ok) return await res.text();
1730
+ } catch {}
1731
+ return null;
1732
+ };
1733
+ const html = await load(page_url);
1734
+ if (html === null) return new Response("Could not render the report page.", { status: 502 });
1735
+ const { build_standalone } = await import("./standalone.js");
1736
+ try {
1737
+ const out = await build_standalone(html, {
1738
+ page_url,
1739
+ load
1740
+ });
1741
+ return new Response(out, { headers: {
1742
+ "content-type": "text/html; charset=utf-8",
1743
+ "content-disposition": `attachment; filename="ogygia-profile-${stored.meta.id}.html"`,
1744
+ "cache-control": "no-store"
1745
+ } });
1746
+ } catch (e) {
1747
+ return new Response(e instanceof Error ? e.message : "standalone export failed", { status: 500 });
1748
+ }
1749
+ }
1750
+ /** A report's representations (the standalone file, the agent JSON, the plain dump) from a report
1751
+ * the request carries: the page's .ogp bytes or the browser's kept copy. On a serverless host the
1752
+ * instance that made the report is rarely the one a later request lands on; with the report in
1753
+ * the request, any instance answers. It is held in this instance's memory under its own id, so
1754
+ * the in-process render of the report page (the standalone file) finds it too. */
1755
+ async #report_posted(ctx, as) {
1756
+ const id = ctx.params.id;
1757
+ if (!id) return new Response("No report id.", { status: 400 });
1758
+ let dump;
1759
+ try {
1760
+ const raw = new Uint8Array(await ctx.request.arrayBuffer());
1761
+ if ((ctx.request.headers.get("content-type") ?? "").startsWith("application/octet-stream")) {
1762
+ const bytes = recover_ogp_bytes(raw);
1763
+ if (!is_ogp(bytes)) return new Response("That is not an ogygia .ogp report.", { status: 400 });
1764
+ dump = await ogp_decode(bytes, this.secret);
1765
+ } else if (ctx.request.headers.get("x-og-encoding") === "gzip") {
1766
+ const { gunzipSync } = await import("node:zlib");
1767
+ dump = JSON.parse(new TextDecoder().decode(gunzipSync(raw)));
1768
+ } else dump = JSON.parse(new TextDecoder().decode(raw));
1769
+ } catch {
1770
+ return new Response("The request carried no report this server can open.", { status: 400 });
1771
+ }
1772
+ if (!is_dump(dump)) return new Response("That is not an ogygia profiler report.", { status: 400 });
1773
+ let stored = this.#reports.get(id);
1774
+ if (!stored) {
1775
+ stored = this.#report_from_dump({
1776
+ ...dump,
1777
+ meta: {
1778
+ ...dump.meta,
1779
+ id
1780
+ }
1781
+ });
1782
+ this.#reports.set(id, stored);
1783
+ while (this.#reports.size > this.max_reports) {
1784
+ const oldest = this.#reports.keys().next().value;
1785
+ if (oldest === void 0) break;
1786
+ this.#reports.delete(oldest);
1787
+ }
1788
+ }
1789
+ if (as === "html") return this.#report_html(stored, ctx);
1790
+ if (as === "json") return this.#report_json(stored);
1791
+ if (as === "ogp") return this.#ogp_response(id, stored);
1792
+ return this.#report_dump_json(stored);
1793
+ }
1794
+ /** Two stored reports side by side (compare.ts); a 404 when either has expired. */
1795
+ /** THE DATA RIVER for a page report: the last run's calls on their load lanes, what each load's
1796
+ * source returns, what each island reads (the build's page keys), the seed's bytes per key. */
1797
+ async #river_for(stored, body, last_window) {
1798
+ const tl = stored.analysis.timeline;
1799
+ if (!tl) return void 0;
1800
+ const lanes = tl.lanes ?? [];
1801
+ if (!lanes.length && !stored.net.length) return void 0;
1802
+ const read = await this.#source_reader();
1803
+ const lane_files = [...new Set(lanes.map((l) => l.file))];
1804
+ const river_lanes = lane_files.map((file) => {
1805
+ const src = read(file);
1806
+ return {
1807
+ file,
1808
+ keys: src ? load_return_keys(src) : null
1809
+ };
1810
+ });
1811
+ const calls = stored.net.filter((c) => !last_window || c.start >= last_window.start && c.start <= last_window.end).map((c) => {
1812
+ const text = [
1813
+ c.caller_site?.file ?? "",
1814
+ ...c.callers ?? [],
1815
+ c.caller ?? ""
1816
+ ].join(" ");
1817
+ const lane = load_lane_of(text)?.file ?? null;
1818
+ return {
1819
+ lane: lane && lane_files.includes(lane) ? lane : lane_files.find((f) => lane && (f.endsWith(lane) || lane.endsWith(f))) ?? null,
1820
+ label: `${c.method} ${c.url}`,
1821
+ ms: Math.max(c.ms, 0) + (c.body_ms ?? 0)
1822
+ };
1823
+ });
1824
+ const by_entry = /* @__PURE__ */ new Map();
1825
+ for (const r of island_rows_of(stored.meta)) {
1826
+ if (by_entry.has(r.entry)) continue;
1827
+ let keys = null;
1828
+ const dev = await this.#dev_island_keys(r.entry);
1829
+ if (dev) keys = dev.keys === "all" ? null : dev.keys ?? [];
1830
+ else try {
1831
+ keys = islandPageKeys(r.entry);
1832
+ } catch {
1833
+ keys = null;
1834
+ }
1835
+ by_entry.set(r.entry, {
1836
+ name: island_name(r),
1837
+ keys
1838
+ });
1839
+ }
1840
+ const seed_m = SEED_SCRIPT_RE.exec(body);
1841
+ const seed = seed_m ? seed_key_bytes(seed_m[1]) : {};
1842
+ if (!calls.length && !Object.keys(seed).length && !by_entry.size) return void 0;
1843
+ return build_river({
1844
+ calls,
1845
+ lanes: river_lanes,
1846
+ islands: [...by_entry.values()],
1847
+ seed
1848
+ });
1849
+ }
1850
+ /** THE LINE LEDGER (ledger.ts): the app lines that cost the most — CPU per line, the bytes each
1851
+ * allocated and the GC that caused, what a render kept — joined per line, with the code read
1852
+ * from the source map's embedded copy (so it shows on a deployed host). Then THE PATTERNS
1853
+ * (patterns.ts): the known slow shapes recognised on those lines. Both land on the report. */
1854
+ /** `w0`: the profiled window's start (performance.now()), the clock the timeline's await graph
1855
+ * counts from; the waits' critical-path check lines the chains up on it */
1856
+ async #lines_for(s, uninlined_profile, w0, stable, wait_runs, almost) {
1857
+ const resolver = await this.#make_resolver();
1858
+ let uninlined;
1859
+ if (uninlined_profile) try {
1860
+ uninlined = analyze(uninlined_profile, resolver, void 0, void 0, void 0, this.#module_hint).functions;
1861
+ } catch {
1862
+ uninlined = void 0;
1863
+ }
1864
+ const fns = /* @__PURE__ */ new Map();
1865
+ for (const f of s.analysis.functions) fns.set(f.key, f);
1866
+ for (const c of s.analysis.components) {
1867
+ const e = fns.get(c.key);
1868
+ if (!e || !e.lines?.length && c.lines?.length) fns.set(c.key, c);
1869
+ }
1870
+ const ledger = build_ledger({
1871
+ functions: [...fns.values()],
1872
+ makers: s.gc_attr?.makers,
1873
+ retained: s.retained?.sites,
1874
+ ...s.growth?.sites ? { grown: {
1875
+ renders: s.growth.renders,
1876
+ sites: s.growth.sites,
1877
+ ...s.growth.half_sites && s.growth.half_renders ? { half: {
1878
+ renders: s.growth.half_renders,
1879
+ sites: s.growth.half_sites
1880
+ } } : {}
1881
+ } } : {},
1882
+ code: (p, l) => resolver?.source_lines(p, l, l)?.lines[0],
1883
+ source: resolver ? (p, a, b) => resolver.source_lines(p, a, b) : void 0,
1884
+ ...uninlined ? {
1885
+ uninlined,
1886
+ renders: s.meta.trigger === "page" ? s.meta.runs?.length || 1 : 1
1887
+ } : {},
1888
+ limit: LEDGER_FOR_PATTERNS
1889
+ });
1890
+ const source = resolver ? (p, a, b) => resolver.source_lines(p, a, b) : void 0;
1891
+ for (const l of ledger) this.#label_module(l);
1892
+ if (ledger.length) s.ledger = ledger.slice(0, LEDGER_SHOWN);
1893
+ const renders = s.meta.trigger === "page" ? s.meta.runs?.length || 1 : 1;
1894
+ let heap;
1895
+ try {
1896
+ const st = (await import("node:v8")).getHeapStatistics();
1897
+ heap = {
1898
+ limit_bytes: st.heap_size_limit,
1899
+ used_bytes: st.used_heap_size
1900
+ };
1901
+ const fn_mb = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE);
1902
+ if (fn_mb > 0 && fn_mb * 1024 * 1024 < heap.limit_bytes) heap = {
1903
+ limit_bytes: fn_mb * 1024 * 1024,
1904
+ used_bytes: process.memoryUsage().rss,
1905
+ function: true
1906
+ };
1907
+ } catch {}
1908
+ const patterns = ledger.length ? find_patterns({
1909
+ ledger,
1910
+ functions: [...fns.values()],
1911
+ source,
1912
+ renders,
1913
+ heap,
1914
+ growth: s.growth
1915
+ }) : [];
1916
+ const calls = [];
1917
+ for (const c of s.net) {
1918
+ if (c.ms < 0 || !c.caller_at) continue;
1919
+ const { host, tpl } = path_template(c.url);
1920
+ calls.push({
1921
+ start: c.start,
1922
+ ms: c.ms + (c.body_ms ?? 0),
1923
+ target: `${c.method} ${host}${tpl}`,
1924
+ at: c.caller_at,
1925
+ ...c.outer_at ? { outer: c.outer_at } : {},
1926
+ scope: c.path,
1927
+ ...c.method === "GET" || c.method === "HEAD" ? { exact: `${c.method} ${c.url}` } : {},
1928
+ ...c.bytes !== void 0 ? { bytes: c.bytes } : {},
1929
+ ...c.personal ? { personal: true } : {},
1930
+ ...c.headers?.["cache-control"] ? { cache: c.headers["cache-control"] } : {}
1931
+ });
1932
+ }
1933
+ for (const o of s.io ?? []) {
1934
+ const target = WAIT_TARGET[o.type];
1935
+ if (!target || o.open || !o.caller_at) continue;
1936
+ calls.push({
1937
+ start: o.start,
1938
+ ms: o.ms,
1939
+ target,
1940
+ at: o.caller_at
1941
+ });
1942
+ }
1943
+ const waits = find_wait_patterns({
1944
+ calls,
1945
+ source
1946
+ });
1947
+ const tl = s.analysis.timeline;
1948
+ for (const p of waits) {
1949
+ if (p.kind !== "waits-in-a-row" || w0 === void 0 || !tl) continue;
1950
+ const on_path = wait_save_on_path(p, tl.awaits, tl.window_ms, w0);
1951
+ if (!on_path || on_path.save_ms >= p.save_ms * .8) continue;
1952
+ p.naive_save_ms = p.save_ms;
1953
+ p.save_ms = on_path.save_ms;
1954
+ p.evidence += `. Started together they would save ${p.naive_save_ms} ms of waiting, but only about ${on_path.save_ms} ms comes off the render` + (on_path.bound_by ? `: ${on_path.bound_by} runs about as long beside them and still sets the end` : ": other work runs as long beside them");
1955
+ }
1956
+ patterns.push(...waits);
1957
+ if (stable?.size) {
1958
+ const same = find_same_answers({
1959
+ calls,
1960
+ stable,
1961
+ source,
1962
+ ...tl?.awaits && w0 !== void 0 ? {
1963
+ awaits: tl.awaits,
1964
+ w0
1965
+ } : {}
1966
+ });
1967
+ if (same) patterns.push(same);
1968
+ }
1969
+ if (almost?.size) {
1970
+ const near = find_same_answers({
1971
+ calls,
1972
+ stable: new Set(almost.keys()),
1973
+ almost,
1974
+ source,
1975
+ ...tl?.awaits && w0 !== void 0 ? {
1976
+ awaits: tl.awaits,
1977
+ w0
1978
+ } : {}
1979
+ });
1980
+ if (near) patterns.push(near);
1981
+ }
1982
+ if (source && Object.keys(s.call_counts).length) {
1983
+ const cold = find_cold_caches({
1984
+ functions: [...fns.values()],
1985
+ call_counts: s.call_counts,
1986
+ source
1987
+ });
1988
+ if (cold) patterns.push(cold);
1989
+ }
1990
+ const og = own_requests(s.meta).find((r) => r.og?.seed)?.og;
1991
+ const whole = s.lineage?.components.filter((c) => c.island && c.whole && c.whole_line) ?? [];
1992
+ if (og?.seed && whole.length) {
1993
+ const read = await this.#source_reader();
1994
+ const readers = [];
1995
+ for (const c of whole) {
1996
+ const lines = read(c.file)?.split("\n");
1997
+ const at = c.whole_line;
1998
+ if (!lines?.[at - 1]) continue;
1999
+ const from = Math.max(1, at - 2);
2000
+ readers.push({
2001
+ name: c.name,
2002
+ file: c.file,
2003
+ line: at,
2004
+ code: lines[at - 1].trim(),
2005
+ context: {
2006
+ start: from,
2007
+ lines: lines.slice(from - 1, at + 2)
2008
+ }
2009
+ });
2010
+ }
2011
+ const top = og.seed.keys.find((k) => k.shipped);
2012
+ const p = seed_whole_pattern(readers, og.seed_bytes, top ? {
2013
+ key: top.key,
2014
+ bytes: top.bytes
2015
+ } : void 0);
2016
+ if (p) patterns.push(p);
2017
+ }
2018
+ const load_files = [...new Set((s.analysis.timeline?.lanes ?? []).map((l) => l.file))];
2019
+ if (load_files.length) {
2020
+ const read = await this.#source_reader();
2021
+ const work = [];
2022
+ const unread_keys = new Set((s.lineage?.keys ?? []).filter((k) => k.verdict === "unread").map((k) => k.key));
2023
+ const feeds_only_unread = (file, line) => {
2024
+ const fed = (s.lineage?.sources ?? []).filter((x) => x.from === file && x.lines.includes(line));
2025
+ return fed.length > 0 && fed.every((x) => unread_keys.has(x.key));
2026
+ };
2027
+ for (const file of load_files) {
2028
+ const src = read(file);
2029
+ if (!src) continue;
2030
+ const lines = src.split("\n");
2031
+ for (const w of constant_work(src)) {
2032
+ if (feeds_only_unread(file, w.line)) continue;
2033
+ const own = ledger.filter((l) => l.line === w.line && (l.file.endsWith(file) || file.endsWith(l.file))).reduce((t, l) => t + l.cpu_ms + l.lib_ms, 0);
2034
+ const called = w.calls.reduce((t, c) => {
2035
+ const mod = imported_from(src, c, file);
2036
+ return t + [...fns.values()].filter((f) => f.name === c && f.category === "app" && !!mod && f.path.includes(`/${mod}.`)).reduce((u, f) => u + f.total_ms, 0);
2037
+ }, 0);
2038
+ const from = Math.max(1, w.line - 2);
2039
+ work.push({
2040
+ path: file,
2041
+ file,
2042
+ line: w.line,
2043
+ code: lines[w.line - 1].trim(),
2044
+ names: w.names,
2045
+ calls: w.calls,
2046
+ cpu_ms: own + called,
2047
+ context: {
2048
+ start: from,
2049
+ lines: lines.slice(from - 1, w.line + 2)
2050
+ }
2051
+ });
2052
+ }
2053
+ }
2054
+ const p = same_work_pattern(work, renders);
2055
+ if (p) {
2056
+ const file = p.sites[0].path;
2057
+ const src = read(file);
2058
+ const plan = src ? hoist_plan(src) : null;
2059
+ if (src && plan) {
2060
+ const lines = src.split("\n");
2061
+ const stmt = (x) => lines.slice(x.line - 1, x.end);
2062
+ const dedent = (ls) => ls.map((l) => l.startsWith(" ") ? l.slice(1) : l.startsWith(" ") ? l.slice(2) : l);
2063
+ p.sites[0].rewrite = {
2064
+ file,
2065
+ line: plan.stmts[0].line,
2066
+ before: plan.stmts.map((x) => stmt(x).join("\n")).join("\n // …\n"),
2067
+ after: `// at the top level, above line ${plan.above} (\`export … load\`), unchanged, and gone from load():\n${plan.stmts.map((x) => dedent(stmt(x)).join("\n")).join("\n")}`
2068
+ };
2069
+ }
2070
+ patterns.push(p);
2071
+ }
2072
+ }
2073
+ if (s.meta.doc_diff?.groups.some((g) => g.attr && !g.sources && !g.library)) {
2074
+ const read = await this.#source_reader();
2075
+ const norm = (f) => {
2076
+ const at = f.lastIndexOf("/src/");
2077
+ return at !== -1 ? f.slice(at + 5) : f.startsWith("src/") ? f.slice(4) : f;
2078
+ };
2079
+ const files = [.../* @__PURE__ */ new Set([...(s.lineage?.components ?? []).map((c) => norm(c.file)), ...s.analysis.components.filter((c) => c.url.endsWith(".svelte")).map((c) => norm(c.url))])];
2080
+ for (const g of s.meta.doc_diff.groups) {
2081
+ if (!g.attr || g.attr.includes(",")) continue;
2082
+ const tags = g.tag.split(", ");
2083
+ const found = [];
2084
+ let mentioned = false;
2085
+ for (const f of files) {
2086
+ const src = read(f);
2087
+ if (!src) continue;
2088
+ if (src.includes(g.attr)) mentioned = true;
2089
+ for (const x of attr_sites(src, g.attr, tags)) if (found.length < 3) found.push({
2090
+ file: f,
2091
+ ...x
2092
+ });
2093
+ }
2094
+ if (found.length) g.sources = found;
2095
+ else if (!mentioned) g.library = true;
2096
+ }
2097
+ }
2098
+ if (s.meta.trigger === "page" && s.meta.prerendered === void 0) {
2099
+ const read = await this.#source_reader();
2100
+ const reqs = s.meta.requests ?? [];
2101
+ const route = s.meta.run_route ?? (reqs.find((r) => r.route && r.path.split("?")[0] === s.meta.page?.split("?")[0]) ?? reqs.find((r) => r.route))?.route;
2102
+ const leaf = [...new Set((s.analysis.timeline?.lanes ?? []).map((l) => l.file))].find((f) => f.includes("+page")) ?? (s.lineage?.components ?? []).find((c) => c.file.endsWith("+page.svelte"))?.file;
2103
+ const dir = route ? `routes${route === "/" ? "" : route}` : leaf ? leaf.slice(0, leaf.lastIndexOf("/")) : "";
2104
+ if (s.meta.run_static || dir && route_prerendered(read, dir)) s.meta.prerendered = true;
2105
+ }
2106
+ const near = s.meta.doc_diff && s.meta.doc_diff.differing <= s.meta.doc_diff.tokens * .05 && s.meta.doc_diff.groups.length <= 6 ? s.meta.doc_diff : void 0;
2107
+ if (!s.meta.prerendered && (s.meta.same_document || near) && s.meta.runs?.length && Math.min(...s.meta.runs) >= 5 && (s.meta.run_status ?? 200) === 200) {
2108
+ const read = await this.#source_reader();
2109
+ const files = [...new Set((s.analysis.timeline?.lanes ?? []).map((l) => l.file))];
2110
+ const VISITOR = /* @__PURE__ */ new Set([
2111
+ "cookies",
2112
+ "locals",
2113
+ "request",
2114
+ "getClientAddress",
2115
+ "platform"
2116
+ ]);
2117
+ let visitor = s.net.some((c) => c.personal);
2118
+ const route_files = new Set(files);
2119
+ const leaf = files.find((f) => f.includes("+page")) ?? (s.lineage?.components ?? []).find((c) => c.file.endsWith("+page.svelte"))?.file;
2120
+ if (leaf) {
2121
+ let dir = leaf.slice(0, leaf.lastIndexOf("/"));
2122
+ let first = true;
2123
+ while (dir) {
2124
+ const names = [
2125
+ ...first ? [
2126
+ "+page.server.ts",
2127
+ "+page.server.js",
2128
+ "+page.ts",
2129
+ "+page.js"
2130
+ ] : [],
2131
+ "+layout.server.ts",
2132
+ "+layout.server.js",
2133
+ "+layout.ts",
2134
+ "+layout.js"
2135
+ ];
2136
+ for (const n of names) if (read(`${dir}/${n}`) !== void 0) route_files.add(`${dir}/${n}`);
2137
+ first = false;
2138
+ const up = dir.lastIndexOf("/");
2139
+ if (up === -1) break;
2140
+ dir = dir.slice(0, up);
2141
+ }
2142
+ }
2143
+ for (const f of route_files) {
2144
+ const src = read(f);
2145
+ if ((src ? load_inputs(src) : null)?.some((n) => VISITOR.has(n) || n.startsWith("*"))) visitor = true;
2146
+ }
2147
+ if (!visitor) {
2148
+ const page_file = files.find((f) => f.includes("+page")) ?? files[0] ?? s.meta.page ?? "";
2149
+ const code = page_file ? read(page_file)?.split("\n").find((l) => l.includes("export") && l.includes("load"))?.trim() : void 0;
2150
+ const runs = [...s.meta.runs].sort((a, b) => a - b);
2151
+ const render_ms = runs[Math.floor(runs.length / 2)];
2152
+ const cc = s.meta.run_cache_control;
2153
+ const shut = cc?.toLowerCase().split(",").some((d) => d.trim() === "private" || d.trim() === "no-store");
2154
+ if (s.meta.same_document) patterns.push(same_document_pattern({
2155
+ file: page_file,
2156
+ render_ms,
2157
+ bytes: s.meta.run_bytes ?? 0,
2158
+ ...code ? { code } : {},
2159
+ ...s.meta.run_sets_cookie || shut ? { blocked: {
2160
+ ...s.meta.run_sets_cookie ? { cookie: true } : {},
2161
+ ...shut ? { cache_control: cc } : {}
2162
+ } } : {}
2163
+ }));
2164
+ else if (near) patterns.push(almost_same_document_pattern({
2165
+ file: page_file,
2166
+ render_ms,
2167
+ diff: near,
2168
+ ...code ? { code } : {}
2169
+ }));
2170
+ }
2171
+ }
2172
+ {
2173
+ const read = await this.#source_reader();
2174
+ const comps = s.analysis.components;
2175
+ const norm = (f) => f.startsWith("src/") ? f.slice(4) : f;
2176
+ const blocks_of = /* @__PURE__ */ new Map();
2177
+ const items = [];
2178
+ for (const c of comps) {
2179
+ if (!c.calls || c.calls < 20 || !c.parent) continue;
2180
+ let file = "";
2181
+ let src;
2182
+ let b;
2183
+ let up = c.parent;
2184
+ for (let hop = 0; up && hop < 4 && !b; hop++) {
2185
+ const parent = comps.find((p) => p.name === up);
2186
+ up = parent?.parent;
2187
+ if (!parent) continue;
2188
+ let own = parent.url.endsWith(".svelte") ? parent.url : void 0;
2189
+ if (!own && parent.path) {
2190
+ const id = this.#module_find(parent.path, parent.line);
2191
+ if (id?.endsWith(".svelte")) own = id;
2192
+ }
2193
+ if (!own) continue;
2194
+ file = norm(own);
2195
+ src = read(file);
2196
+ if (!src) continue;
2197
+ if (!blocks_of.has(file)) blocks_of.set(file, each_blocks(src));
2198
+ b = blocks_of.get(file).find((x) => x.components.includes(c.name));
2199
+ }
2200
+ if (!b || !src) continue;
2201
+ const lines = src.split("\n");
2202
+ const from = Math.max(1, b.line - 2);
2203
+ let made_at;
2204
+ for (const x of b.key ? (s.lineage?.sources ?? []).filter((y) => y.key === b.key) : []) {
2205
+ const sorted = [...x.lines].sort((m, n) => n - m);
2206
+ const at = sorted[1] ?? sorted[0];
2207
+ const code = at ? read(x.from)?.split("\n")[at - 1]?.trim() : void 0;
2208
+ if (at && code) made_at = {
2209
+ path: x.from,
2210
+ file: x.from,
2211
+ line: at,
2212
+ code
2213
+ };
2214
+ }
2215
+ items.push({
2216
+ component: c.name,
2217
+ instances: c.calls,
2218
+ total_ms: c.total_ms,
2219
+ each: {
2220
+ path: file,
2221
+ file,
2222
+ line: b.line,
2223
+ code: lines[b.line - 1].trim(),
2224
+ context: {
2225
+ start: from,
2226
+ lines: lines.slice(from - 1, b.line + 2)
2227
+ }
2228
+ },
2229
+ ...b.key ? { key: b.key } : {},
2230
+ ...made_at ? { made_at } : {}
2231
+ });
2232
+ }
2233
+ const p = render_per_item_pattern(items, renders);
2234
+ if (p) patterns.push(p);
2235
+ }
2236
+ if (source) {
2237
+ const files = /* @__PURE__ */ new Map();
2238
+ const opts_for = (path) => {
2239
+ let o = files.get(path);
2240
+ if (o) return o;
2241
+ const all = source(path, 1, Number.MAX_SAFE_INTEGER)?.lines;
2242
+ if (!all) return void 0;
2243
+ const svelte = path.endsWith(".svelte");
2244
+ const tag = svelte ? all.find((l) => l.includes("<script") && l.includes("lang=")) : void 0;
2245
+ const ts = path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") || !!tag && (tag.includes("ts\"") || tag.includes("ts'") || tag.includes("=ts") || tag.includes("typescript"));
2246
+ const taken = /* @__PURE__ */ new Set();
2247
+ for (const l of all) for (const t of tokens(l)) taken.add(t);
2248
+ o = {
2249
+ ts,
2250
+ svelte,
2251
+ taken,
2252
+ made: /* @__PURE__ */ new Map()
2253
+ };
2254
+ files.set(path, o);
2255
+ return o;
2256
+ };
2257
+ for (const p of patterns) {
2258
+ if (p.kind !== "formatter-per-call") continue;
2259
+ for (const site of p.sites) {
2260
+ if (site.rewrite) continue;
2261
+ const full = source(site.path, site.line, site.line)?.lines[0];
2262
+ const opts = opts_for(site.path);
2263
+ if (!full || !opts) continue;
2264
+ const rw = formatter_rewrite(full, opts);
2265
+ if (rw) {
2266
+ site.rewrite = {
2267
+ file: site.file,
2268
+ line: site.line,
2269
+ ...rw
2270
+ };
2271
+ continue;
2272
+ }
2273
+ const above = source(site.path, Math.max(1, site.line - 3), site.line - 1);
2274
+ if (!above) continue;
2275
+ const used = tokens(full);
2276
+ for (let k = above.lines.length - 1; k >= 0; k--) {
2277
+ const l = above.lines[k];
2278
+ if (!l.includes("new Intl.")) continue;
2279
+ const name = line_rw(l).declared[0];
2280
+ if (!name || !used.some((t, i) => t === name && used[i + 1] === "." && used[i - 1] !== ".")) continue;
2281
+ const r2 = formatter_rewrite(l, opts);
2282
+ if (r2) site.rewrite = {
2283
+ file: site.file,
2284
+ line: above.start + k,
2285
+ ...r2
2286
+ };
2287
+ break;
2288
+ }
2289
+ }
2290
+ }
2291
+ }
2292
+ const route = own_requests(s.meta).find((r) => r.route)?.route ?? s.meta.request?.route;
2293
+ if (route) for (let i = patterns.length - 1; i >= 0; i--) {
2294
+ const sites = patterns[i].sites;
2295
+ if (sites.length && sites.every((x) => another_routes_file(route, x.path || x.file))) patterns.splice(i, 1);
2296
+ }
2297
+ if (route && s.call_counts && !patterns.some((p) => p.kind === "sync-io")) try {
2298
+ const read = await this.#source_reader();
2299
+ const { join } = await import("node:path");
2300
+ const dirs = route_dirs(route);
2301
+ const rels = [];
2302
+ const add = (dir, names) => {
2303
+ for (const n of names) for (const ext of [".ts", ".js"]) rels.push(`routes/${dir ? dir + "/" : ""}${n}${ext}`);
2304
+ };
2305
+ for (const d of dirs.above("src")) add(d, ["+layout.server", "+layout"]);
2306
+ add(dirs.own("src"), ["+page.server", "+page"]);
2307
+ for (const ext of [".ts", ".js"]) rels.push(`hooks.server${ext}`);
2308
+ const files = [];
2309
+ for (const rel of rels) {
2310
+ const text = read(rel);
2311
+ if (text) files.push({
2312
+ path: join(process.cwd(), "src", rel),
2313
+ file: rel,
2314
+ text
2315
+ });
2316
+ }
2317
+ const own = new Set(files.map((f) => f.file));
2318
+ for (const f of files.slice()) for (const rel of module_imports(f.file, f.text)) {
2319
+ if (own.size >= 30 + rels.length) break;
2320
+ if (own.has(rel)) continue;
2321
+ const text = read(rel);
2322
+ if (!text) continue;
2323
+ own.add(rel);
2324
+ files.push({
2325
+ path: join(process.cwd(), "src", rel),
2326
+ file: rel,
2327
+ text
2328
+ });
2329
+ }
2330
+ const p = counted_sync_io(s.call_counts, files);
2331
+ if (p) patterns.push(p);
2332
+ } catch {}
2333
+ const severe = (p) => (p.kept_bytes ?? 0) >= 8388608 && p.growth !== "levels-off";
2334
+ const per_render = (p) => p.wait ? p.save_ms : p.save_ms / renders;
2335
+ patterns.sort((a, b) => Number(severe(b)) - Number(severe(a)) || per_render(b) - per_render(a));
2336
+ if (patterns.length) s.patterns = patterns;
2337
+ const label = (x) => this.#label_module(x);
2338
+ for (const p of s.patterns ?? []) for (const site of p.sites) {
2339
+ label(site);
2340
+ for (const v of site.via ?? []) label(v);
2341
+ }
2342
+ const tl_drill = s.analysis.timeline;
2343
+ if (tl_drill) {
2344
+ const renders_of = /* @__PURE__ */ new Map();
2345
+ for (const c of s.analysis.components) if (c.calls) renders_of.set(c.name, c.calls);
2346
+ const cold_owners = new Map((s.meta.cold?.owners ?? []).map((o) => [o.label, o.ms]));
2347
+ const drill = build_drill(tl_drill, ledger, patterns, s.gc_attr?.makers ?? [], renders_of, s.analysis.owner_runs_ms ?? {}, cold_owners, wait_runs ?? {});
2348
+ if (drill && source) {
2349
+ const with_code = (n) => {
2350
+ for (const c of n.children ?? []) {
2351
+ const fl = c.kind === "line" && c.at ? at_line(c.at) : void 0;
2352
+ const code = fl ? source(fl.file, fl.line, fl.line)?.lines[0]?.trim() : void 0;
2353
+ if (code) c.code = code.slice(0, 120);
2354
+ with_code(c);
2355
+ }
2356
+ };
2357
+ with_code(drill);
2358
+ }
2359
+ if (drill && s.lineage?.sources) {
2360
+ const verdict = new Map(s.lineage.keys.map((k) => [k.key, k.verdict]));
2361
+ tag_fills(drill, s.lineage.sources.map((x) => ({
2362
+ ...x,
2363
+ verdict: verdict.get(x.key) ?? "unknown"
2364
+ })));
2365
+ const read = await this.#source_reader();
2366
+ const late = /* @__PURE__ */ new Map();
2367
+ for (const c of s.lineage.components) {
2368
+ if (c.island || !c.file.endsWith(".svelte")) continue;
2369
+ const src = read(c.file);
2370
+ if (!src) continue;
2371
+ const lines = src.split("\n");
2372
+ for (const k of late_island_keys(src)) if (!s.lineage.components.some((o) => o.file !== c.file && (o.whole || (o.reads ?? []).includes(k.key)))) late.set(k.key, {
2373
+ island: k.island,
2374
+ wake: k.wake,
2375
+ file: c.file,
2376
+ line: k.line,
2377
+ code: lines[k.line - 1]?.trim() ?? ""
2378
+ });
2379
+ }
2380
+ if (late.size) {
2381
+ const items = [];
2382
+ const rows = [];
2383
+ const walk = (n) => {
2384
+ if (n.kind === "wait" && n.fills?.length && n.fills.every((f) => late.has(f.key))) rows.push(n);
2385
+ for (const c of n.children ?? []) walk(c);
2386
+ };
2387
+ walk(drill);
2388
+ for (const r of rows) {
2389
+ const line = r.children?.find((c) => c.kind === "line");
2390
+ const at = line?.children?.find((c) => c.kind === "line")?.at ?? line?.at;
2391
+ if (!at) continue;
2392
+ const colon = at.lastIndexOf(":");
2393
+ const file = at.slice(0, colon);
2394
+ const n = Number(at.slice(colon + 1));
2395
+ const k = late.get(r.fills[0].key);
2396
+ items.push({
2397
+ call: r.label,
2398
+ ms: r.ms,
2399
+ alone: r.alone ?? 0,
2400
+ at: {
2401
+ path: file,
2402
+ file,
2403
+ line: n,
2404
+ code: read(file)?.split("\n")[n - 1]?.trim() ?? ""
2405
+ },
2406
+ key: r.fills[0].key,
2407
+ island: k.island,
2408
+ wake: k.wake,
2409
+ use: {
2410
+ path: k.file,
2411
+ file: k.file,
2412
+ line: k.line,
2413
+ code: k.code
2414
+ }
2415
+ });
2416
+ r.why = [...r.why ?? [], "late-island-wait"];
2417
+ }
2418
+ const p = late_island_pattern(items);
2419
+ if (p) {
2420
+ const list = s.patterns ??= [];
2421
+ list.push(p);
2422
+ list.sort((a, b) => Number(severe(b)) - Number(severe(a)) || per_render(b) - per_render(a));
2423
+ }
2424
+ }
2425
+ }
2426
+ if (drill) s.drill = drill;
2427
+ }
2428
+ if (s.meta.trigger === "page" && (s.meta.run_status ?? 200) === 200) {
2429
+ const by_key = new Map(s.analysis.functions.map((fn) => [fn.key, fn]));
2430
+ const f = forecast_of(s.meta.runs, s.patterns, s.drill, {
2431
+ wait_cap: s.analysis.timeline?.wait_ms,
2432
+ cpu_cap: s.analysis.timeline?.cpu_ms,
2433
+ stacks: (k) => by_key.get(k)?.stacks,
2434
+ ...s.meta.answers?.runs_ms.length ? { answers_runs: s.meta.answers.runs_ms } : {},
2435
+ cpu_main: s.analysis.functions.filter((fn) => MAIN_THREAD_CPU.has(fn.category)).reduce((t, fn) => t + fn.self_ms, 0) / Math.max(1, s.meta.runs?.length ?? 1)
2436
+ });
2437
+ if (f) s.forecast = s.analysis.sourcemapped || s.analysis.text_mapped ? f : {
2438
+ ...f,
2439
+ partial: true
2440
+ };
2441
+ }
2442
+ }
2443
+ /** A reader of app sources at report time (a file relative to `src/` or the cwd, or absolute);
2444
+ * undefined where there is no file system, or when the file is not on this machine. */
2445
+ /** THE DEV SERVER: which page.data keys an island's code reads, from the plugin's run of the build's
2446
+ * analysis (vite/dev-maps.ts), by the island id in its entry (`…/virtual:ogygia/island/<id>.js`).
2447
+ * Undefined off the dev server or when the plugin cannot say */
2448
+ async #dev_island_keys(entry) {
2449
+ if (!this.dev) return void 0;
2450
+ const ask = globalThis[Symbol.for("ogygia.profiler.dev-page-keys")];
2451
+ const at = entry.indexOf("virtual:ogygia/island/");
2452
+ if (!ask || at === -1) return void 0;
2453
+ const rest = entry.slice(at + 22);
2454
+ const dot = rest.indexOf(".");
2455
+ try {
2456
+ return await ask(dot === -1 ? rest : rest.slice(0, dot));
2457
+ } catch {
2458
+ return;
2459
+ }
2460
+ }
2461
+ async #source_reader() {
2462
+ let fs;
2463
+ let path;
2464
+ try {
2465
+ fs = await import("node:fs");
2466
+ path = await import("node:path");
2467
+ } catch {
2468
+ fs = void 0;
2469
+ }
2470
+ const cache = /* @__PURE__ */ new Map();
2471
+ return (file) => {
2472
+ if (!fs || !path) return void 0;
2473
+ if (cache.has(file)) return cache.get(file);
2474
+ const candidates = file.startsWith("/") ? [file] : [path.join(process.cwd(), "src", file), path.join(process.cwd(), file)];
2475
+ let out;
2476
+ for (const c of candidates) try {
2477
+ out = fs.readFileSync(c, "utf8");
2478
+ break;
2479
+ } catch {}
2480
+ out ??= embedded_source(file);
2481
+ cache.set(file, out);
2482
+ return out;
2483
+ };
2484
+ }
2485
+ /** DATA LINEAGE FROM THE CODE (lineage.ts): the route's page and layouts, every component the
2486
+ * profile saw with a `.svelte` source on this machine, and the islands (their keys from the
2487
+ * build), each with the page.data keys it reads — joined to the loads' keys and the seed. */
2488
+ /** every `.svelte` under the app's `src/`, relative to it, listed once (null: no src here) */
2489
+ #svelte_files;
2490
+ /** The `src/`-relative paths of `<name>.svelte` files (a component is named after its file). */
2491
+ async #component_files(name) {
2492
+ this.#svelte_files ??= (async () => {
2493
+ try {
2494
+ const fs = await import("node:fs");
2495
+ const path = await import("node:path");
2496
+ const root = path.join(process.cwd(), "src");
2497
+ const out = [];
2498
+ for (const e of fs.readdirSync(root, {
2499
+ recursive: true,
2500
+ withFileTypes: true
2501
+ })) {
2502
+ if (!e.isFile() || !e.name.endsWith(".svelte")) continue;
2503
+ const rel = path.relative(root, path.join(e.parentPath, e.name)).split(path.sep).join("/");
2504
+ if (!rel.includes("node_modules/")) out.push(rel);
2505
+ }
2506
+ return out;
2507
+ } catch {
2508
+ const carried = embedded_files().filter((f) => f.endsWith(".svelte"));
2509
+ return carried.length ? carried : null;
2510
+ }
2511
+ })();
2512
+ const all = await this.#svelte_files;
2513
+ const leaf = "/" + name + ".svelte";
2514
+ return all ? all.filter((f) => f === name + ".svelte" || f.endsWith(leaf)) : [];
2515
+ }
2516
+ async #lineage_for(stored, body) {
2517
+ const a = stored.analysis;
2518
+ const tl = a.timeline;
2519
+ const read = await this.#source_reader();
2520
+ const lanes = tl?.lanes ?? [];
2521
+ const lane_files = [...new Set(lanes.map((l) => l.file))];
2522
+ if (tl?.chain) {
2523
+ const src = read(tl.chain.page);
2524
+ const use = src ? parent_use(src) : null;
2525
+ if (use) tl.chain.parent_use = use;
2526
+ }
2527
+ const runs = Math.max(stored.meta.runs?.length ?? 1, 1);
2528
+ const wait_by_lane = /* @__PURE__ */ new Map();
2529
+ for (const c of stored.net) {
2530
+ const text = [
2531
+ c.caller_site?.file ?? "",
2532
+ ...c.callers ?? [],
2533
+ c.caller ?? ""
2534
+ ].join(" ");
2535
+ const lane = load_lane_of(text)?.file;
2536
+ if (!lane) continue;
2537
+ const file = lane_files.includes(lane) ? lane : lane_files.find((f) => f.endsWith(lane) || lane.endsWith(f));
2538
+ if (!file) continue;
2539
+ wait_by_lane.set(file, (wait_by_lane.get(file) ?? 0) + (Math.max(c.ms, 0) + (c.body_ms ?? 0)) / runs);
2540
+ }
2541
+ const lineage_lanes = lane_files.map((file) => {
2542
+ const src = read(file);
2543
+ return {
2544
+ file,
2545
+ keys: src ? load_return_keys(src) : null,
2546
+ ...wait_by_lane.has(file) ? { wait_ms: round2(wait_by_lane.get(file)) } : {}
2547
+ };
2548
+ });
2549
+ const files = /* @__PURE__ */ new Map();
2550
+ const route_dirs = /* @__PURE__ */ new Set();
2551
+ for (const f of lane_files) {
2552
+ const at = f.lastIndexOf("/");
2553
+ if (at === -1) continue;
2554
+ let dir = f.slice(0, at);
2555
+ while (dir) {
2556
+ route_dirs.add(dir);
2557
+ const up = dir.lastIndexOf("/");
2558
+ if (up === -1 || dir === "routes") break;
2559
+ dir = dir.slice(0, up);
2560
+ }
2561
+ }
2562
+ for (const dir of route_dirs) for (const leaf of ["+page.svelte", "+layout.svelte"]) {
2563
+ const file = `${dir}/${leaf}`;
2564
+ if (read(file) !== void 0 && !files.has(file)) files.set(file, {
2565
+ name: `${dir.slice(dir.lastIndexOf("/") + 1)}/${leaf}`,
2566
+ island: false
2567
+ });
2568
+ }
2569
+ const norm = (f) => f.startsWith("src/") ? f.slice(4) : f;
2570
+ for (const c of a.components) {
2571
+ if (!c.url.endsWith(".svelte")) continue;
2572
+ const file = norm(c.url);
2573
+ if (files.has(file)) continue;
2574
+ if (read(file) !== void 0) files.set(file, {
2575
+ name: c.name,
2576
+ island: false
2577
+ });
2578
+ }
2579
+ const seed_stats = own_requests(stored.meta).find((r) => r.og?.seed)?.og?.seed;
2580
+ const seen_entries = /* @__PURE__ */ new Set();
2581
+ const islands = [];
2582
+ for (const r of island_rows_of(stored.meta)) {
2583
+ if (seen_entries.has(r.entry)) continue;
2584
+ seen_entries.add(r.entry);
2585
+ const name = island_name(r);
2586
+ let keys;
2587
+ const dev = await this.#dev_island_keys(r.entry);
2588
+ if (dev) keys = dev.keys === "all" ? ["*"] : dev.keys ?? [];
2589
+ else if (seed_stats) {
2590
+ keys = seed_stats.keys.filter((k) => k.readers.includes(name)).map((k) => k.key);
2591
+ if (seed_stats.whole_by.includes(name)) keys.push("*");
2592
+ } else {
2593
+ try {
2594
+ keys = islandPageKeys(r.entry);
2595
+ } catch {
2596
+ keys = void 0;
2597
+ }
2598
+ if (keys === void 0) {
2599
+ const src = read(r.entry);
2600
+ keys = src ? data_reads(src, data_prop_names(src)) : null;
2601
+ } else if (keys === null) keys = ["*"];
2602
+ }
2603
+ let file = norm(r.entry);
2604
+ let whole_line;
2605
+ if (keys?.includes("*") || keys === null) {
2606
+ const comp = a.components.find((c) => c.name === name && c.url.endsWith(".svelte"));
2607
+ const named = await this.#component_files(name);
2608
+ const wanted = route_imports(files.keys(), read, name);
2609
+ const candidates = [
2610
+ ...comp ? [norm(comp.url)] : [],
2611
+ ...named.filter((f) => wanted.has(f)),
2612
+ ...named.filter((f) => !wanted.has(f)),
2613
+ file
2614
+ ];
2615
+ for (const f of candidates) {
2616
+ const src = read(f);
2617
+ const line = src ? whole_read_line(src, data_prop_names(src)) : null;
2618
+ if (line) {
2619
+ file = f;
2620
+ whole_line = line;
2621
+ break;
2622
+ }
2623
+ }
2624
+ if (!whole_line) {
2625
+ const whys = [...islandPageWhy(r.entry) ?? [], ...(dev?.why ?? []).map((w) => ({
2626
+ ...w,
2627
+ file: app_relative(w.file) ?? w.file
2628
+ }))];
2629
+ for (const w of whys) {
2630
+ if (!w.line) continue;
2631
+ const f = norm(w.file);
2632
+ if (read(f)?.split("\n")[w.line - 1] === void 0) continue;
2633
+ file = f;
2634
+ whole_line = w.line;
2635
+ break;
2636
+ }
2637
+ }
2638
+ }
2639
+ islands.push({
2640
+ name,
2641
+ file,
2642
+ island: true,
2643
+ reads: keys,
2644
+ ...whole_line ? { whole_line } : {}
2645
+ });
2646
+ files.delete(norm(r.entry));
2647
+ }
2648
+ const components = [...files.entries()].map(([file, info]) => {
2649
+ const src = read(file);
2650
+ return {
2651
+ name: info.name,
2652
+ file,
2653
+ island: false,
2654
+ reads: data_reads(src, data_prop_names(src))
2655
+ };
2656
+ });
2657
+ if (!components.length && !islands.length) return void 0;
2658
+ const seed_m = SEED_SCRIPT_RE.exec(body);
2659
+ const seed = seed_m ? seed_key_bytes(seed_m[1]) : {};
2660
+ const lineage = build_lineage({
2661
+ components: [...components, ...islands],
2662
+ lanes: lineage_lanes,
2663
+ seed
2664
+ });
2665
+ if (lineage) {
2666
+ const sources = [];
2667
+ for (const file of lane_files) {
2668
+ const src = read(file);
2669
+ const m = src ? key_sources(src) : null;
2670
+ for (const [key, lines] of m ?? []) sources.push({
2671
+ key,
2672
+ from: file,
2673
+ lines,
2674
+ names: names_on(src, lines)
2675
+ });
2676
+ }
2677
+ if (sources.length) lineage.sources = sources;
2678
+ }
2679
+ return lineage;
2680
+ }
2681
+ /** One more render under a LIVE-objects sampler (no collected-objects flag), then a full
2682
+ * collection, then the profile: every sampled object still alive was allocated by that
2683
+ * render and kept by something — the leak, by allocation site. Its own session, after the
2684
+ * runs, so nothing here is charged to them; the sites are source-mapped like the makers. */
2685
+ /** `n` renders with a full collection after each: the used heap after every one — and, from a
2686
+ * live-objects sampler left on across all of them, what each LINE still holds at the end (a
2687
+ * leaking line holds about `n` renders' worth; a line filling a bounded cache, about one). */
2688
+ async #growth_check(render, n, until = Infinity) {
2689
+ const { Session } = await import("node:inspector/promises");
2690
+ const session = new Session();
2691
+ session.connect();
2692
+ try {
2693
+ const settled = async () => {
2694
+ await session.post("HeapProfiler.collectGarbage");
2695
+ return process.memoryUsage().heapUsed;
2696
+ };
2697
+ const series = [await settled()];
2698
+ await session.post("HeapProfiler.enable");
2699
+ await session.post("HeapProfiler.startSampling", { samplingInterval: LIVE_SAMPLING_BYTES });
2700
+ const half = Math.floor(n / 2);
2701
+ let half_head;
2702
+ let last = 0;
2703
+ for (let i = 0; i < n; i++) {
2704
+ if (i >= 3 && Date.now() + last > until) break;
2705
+ const t = Date.now();
2706
+ await render();
2707
+ series.push(await settled());
2708
+ if (i + 1 === 3 && is_flat(series)) break;
2709
+ if (i + 1 === 3 && is_clear_leak(series)) {
2710
+ half_head = void 0;
2711
+ break;
2712
+ }
2713
+ if (i + 1 === half) half_head = (await session.post("HeapProfiler.getSamplingProfile")).profile?.head;
2714
+ last = Date.now() - t;
2715
+ }
2716
+ const res = await session.post("HeapProfiler.getSamplingProfile");
2717
+ await session.post("HeapProfiler.stopSampling");
2718
+ const g = heap_growth(series);
2719
+ if (!g) return void 0;
2720
+ const head = res.profile?.head;
2721
+ if (head) g.sites = (await this.#live_sites(head)).rows;
2722
+ if (head && half_head) {
2723
+ g.half_renders = half;
2724
+ g.half_sites = (await this.#live_sites(half_head)).rows;
2725
+ }
2726
+ return g;
2727
+ } finally {
2728
+ try {
2729
+ session.disconnect();
2730
+ } catch {}
2731
+ }
2732
+ }
2733
+ async #retention_pass(render) {
2734
+ const { Session } = await import("node:inspector/promises");
2735
+ const session = new Session();
2736
+ session.connect();
2737
+ try {
2738
+ await session.post("HeapProfiler.enable");
2739
+ await session.post("HeapProfiler.startSampling", { samplingInterval: LIVE_SAMPLING_BYTES });
2740
+ const t = performance.now();
2741
+ await render();
2742
+ const render_ms = round2(performance.now() - t);
2743
+ await session.post("HeapProfiler.collectGarbage");
2744
+ const res = await session.post("HeapProfiler.getSamplingProfile");
2745
+ await session.post("HeapProfiler.stopSampling");
2746
+ const head = res.profile?.head;
2747
+ if (!head) return void 0;
2748
+ const { total, rows } = await this.#live_sites(head);
2749
+ return {
2750
+ total_bytes: total,
2751
+ render_ms,
2752
+ sites: rows
2753
+ };
2754
+ } finally {
2755
+ try {
2756
+ session.disconnect();
2757
+ } catch {}
2758
+ }
2759
+ }
2760
+ /** A live-objects heap sample, after a full collection, as rows: one per allocation line (the
2761
+ * same builtin from several stacks is one site), each charged to its app line (`at`). */
2762
+ async #live_sites(head) {
2763
+ {
2764
+ const dict = {};
2765
+ const sites = heap_sites(head, dict);
2766
+ const resolver = await this.#make_resolver();
2767
+ const short = (u) => u.replace(/^file:\/\//, "").split("/").slice(-2).join("/");
2768
+ const by_ident = /* @__PURE__ */ new Map();
2769
+ let total = 0;
2770
+ for (const [key, bytes] of sites) {
2771
+ const d = dict[key];
2772
+ if (!d || d.category === "profiler" || d.category === "v8" || d.category === "gc") continue;
2773
+ let url = d.url;
2774
+ let line = d.line;
2775
+ let caller = d.caller;
2776
+ let at;
2777
+ if (resolver) {
2778
+ const m = url ? resolver.resolve(url, line - 1, 0) : void 0;
2779
+ if (m) {
2780
+ url = m.source;
2781
+ line = m.line;
2782
+ }
2783
+ let caller_at;
2784
+ if (d.caller_url && d.caller_name) {
2785
+ const c = resolver.resolve(d.caller_url, (d.caller_line ?? 1) - 1, 0);
2786
+ if (c) {
2787
+ caller = `${d.caller_name} (${short(c.source)}:${c.line})`;
2788
+ caller_at = {
2789
+ path: c.source,
2790
+ line: c.line
2791
+ };
2792
+ }
2793
+ }
2794
+ const cat = m ? categorize({
2795
+ functionName: d.name,
2796
+ url: m.source,
2797
+ lineNumber: m.line - 1,
2798
+ columnNumber: 0,
2799
+ scriptId: ""
2800
+ }).category : d.category;
2801
+ if ((cat === "app" || cat === "component") && m) at = {
2802
+ path: m.source,
2803
+ line: m.line
2804
+ };
2805
+ else at = caller_at;
2806
+ if (!at && !m) at = this.#chunk_at(d.url, d.line) ?? this.#chunk_at(d.caller_url, d.caller_line);
2807
+ }
2808
+ const ident = `${d.name}|${url}|${line}|${caller ?? ""}`;
2809
+ const row = by_ident.get(ident) ?? {
2810
+ name: d.name,
2811
+ url,
2812
+ line,
2813
+ ...caller ? { caller } : {},
2814
+ ...at ? { at } : {},
2815
+ component: d.component,
2816
+ bytes: 0,
2817
+ share: 0
2818
+ };
2819
+ row.bytes += bytes;
2820
+ by_ident.set(ident, row);
2821
+ total += bytes;
2822
+ }
2823
+ const rows = [...by_ident.values()].sort((a, b) => b.bytes - a.bytes).slice(0, 30);
2824
+ for (const r of rows) r.share = total ? round2(r.bytes / total) : 0;
2825
+ return {
2826
+ total,
2827
+ rows
2828
+ };
2829
+ }
2830
+ }
2831
+ #compare(a, b) {
2832
+ const A = this.#reports.get(a ?? "");
2833
+ const B = this.#reports.get(b ?? "");
2834
+ if (!A || !B) return {
2835
+ base: this.base,
2836
+ cmp: null,
2837
+ a: a ?? "",
2838
+ b: b ?? ""
2839
+ };
2840
+ return {
2841
+ base: this.base,
2842
+ cmp: compare_reports(this.#pack(A), this.#pack(B)),
2843
+ a: A.meta.id,
2844
+ b: B.meta.id
2845
+ };
2846
+ }
2847
+ /** a stored report as the comparison reads it */
2848
+ #pack(s) {
2849
+ return {
2850
+ meta: s.meta,
2851
+ analysis: s.analysis,
2852
+ findings: derive_findings(s.analysis, s.meta, this.#report_extras(s)).filter((f) => f.severity === "warn").map((f) => `${f.code}: ${f.message}`),
2853
+ ...s.gc_attr ? { gc: s.gc_attr } : {},
2854
+ ...s.patterns ? { patterns: s.patterns } : {},
2855
+ ...s.ledger ? { ledger: s.ledger } : {},
2856
+ ...s.drill ? { drill: s.drill } : {},
2857
+ ...s.forecast ? { forecast: s.forecast } : {}
2858
+ };
2859
+ }
2860
+ /** DEV ONLY: nine lines of a local source file around `l` — the source peek an expanded row
2861
+ * shows. Behind the login like every route; the path must resolve inside the project (the
2862
+ * process cwd, symlinks followed) and be a source file. Anything else is a 404. */
2863
+ async #source(ctx) {
2864
+ if (!this.dev) return new Response("Not found", { status: 404 });
2865
+ const p = ctx.url.searchParams.get("p") ?? "";
2866
+ const line = Math.max(1, Number(ctx.url.searchParams.get("l")) || 1);
2867
+ const to = Math.min(line + 60, Math.max(line, Number(ctx.url.searchParams.get("to")) || line));
2868
+ try {
2869
+ const fs = await import("node:fs");
2870
+ const path = await import("node:path");
2871
+ const real = fs.realpathSync(p);
2872
+ const root = fs.realpathSync(process.cwd());
2873
+ if (!real.startsWith(root + path.sep)) return new Response("Not found", { status: 404 });
2874
+ if (!SOURCE_EXT_RE.test(real)) return new Response("Not found", { status: 404 });
2875
+ const lines = fs.readFileSync(real, "utf8").split("\n");
2876
+ const start = Math.max(1, line - 4);
2877
+ const end = Math.min(lines.length, to + 4);
2878
+ return ctx.json({
2879
+ path: real,
2880
+ start,
2881
+ line,
2882
+ lines: lines.slice(start - 1, end)
2883
+ });
2884
+ } catch {
2885
+ return new Response("Not found", { status: 404 });
2886
+ }
2887
+ }
2888
+ /** No open redirect — a `next` only bounces back into the profiler. */
2889
+ #safe_next(next) {
2890
+ const n = next ?? this.base;
2891
+ return n.startsWith(this.base) ? n : this.base;
2892
+ }
2893
+ /** Clear the session cookie and bounce to the dashboard. */
2894
+ #logout(ctx) {
2895
+ const res = ctx.redirect(this.base);
2896
+ res.headers.append("set-cookie", this.#session_cookie("", ctx.event));
2897
+ return res;
2898
+ }
2899
+ #dashboard(by) {
2900
+ const tag_keys = /* @__PURE__ */ new Set();
2901
+ for (const e of this.#ring) for (const k of Object.keys(e.tags ?? {})) tag_keys.add(k);
2902
+ return {
2903
+ base: this.base,
2904
+ recent: this.#ring,
2905
+ routes: route_aggregates(this.#ring, by && tag_keys.has(by) ? by : null),
2906
+ by: by && tag_keys.has(by) ? by : null,
2907
+ tag_keys: [...tag_keys].sort(),
2908
+ reports: [...this.#reports.values()].map((r) => r.meta).reverse(),
2909
+ top_fix: Object.fromEntries([...this.#reports.values()].filter((r) => r.patterns?.length).map((r) => {
2910
+ const p = r.patterns[0];
2911
+ const n = r.meta.trigger === "page" ? r.meta.runs?.length || 1 : 1;
2912
+ return [r.meta.id, {
2913
+ title: p.title,
2914
+ save_ms: round2(p.wait ? p.save_ms : p.save_ms / n),
2915
+ wait: !!p.wait,
2916
+ more: r.patterns.length - 1
2917
+ }];
2918
+ })),
2919
+ site_fixes: this.#site_fixes(),
2920
+ holes: hole_slots(this.#ring),
2921
+ recording: this.#recorder_busy || this.#recording_active(),
2922
+ dev: this.dev,
2923
+ rss_mb: Math.round(process.memoryUsage().rss / 1048576),
2924
+ inflight: this.#inflight,
2925
+ history: page_history([...this.#reports.values()].map((r) => r.meta), (id) => {
2926
+ const s = this.#reports.get(id);
2927
+ if (!s || s.meta.trigger !== "page") return void 0;
2928
+ try {
2929
+ return page_score_of(s.meta, this.#report_extras(s)).score;
2930
+ } catch {
2931
+ return;
2932
+ }
2933
+ }),
2934
+ trap: this.#trap ? {
2935
+ over: this.#trap.over,
2936
+ window_ms: this.#trap.window ?? 5e3,
2937
+ caught: this.#trap_caught,
2938
+ keep: this.#trap.keep ?? 3,
2939
+ armed: !!this.#trap_timer || this.#trap_running
2940
+ } : null,
2941
+ sampled: this.#sampled_summary(),
2942
+ background_note: this.#background_note
2943
+ };
2944
+ }
2945
+ #run_page(ctx) {
2946
+ const q = ctx.url.searchParams;
2947
+ let path = (q.get("p") ?? "").trim();
2948
+ if (path && !path.startsWith("/") && !path.includes("://")) path = "/" + path;
2949
+ if (!path.startsWith("/") || path.startsWith("//")) return error(400, "Give a path on this site, like /docs/overview.");
2950
+ const runs = clamp(Number(q.get("runs")) || 5, 1, 50);
2951
+ const format = q.get("format") === "ogp" ? "ogp" : "";
2952
+ const against_raw = q.get("against") ?? "";
2953
+ let against = "";
2954
+ for (const ch of against_raw) {
2955
+ const c = ch.charCodeAt(0);
2956
+ if (!(c >= 48 && c <= 57 || c >= 97 && c <= 122)) {
2957
+ against = "";
2958
+ break;
2959
+ }
2960
+ against += ch;
2961
+ }
2962
+ return {
2963
+ base: this.base,
2964
+ path,
2965
+ runs,
2966
+ format,
2967
+ ...against && against.length <= 32 ? { against } : {}
2968
+ };
2969
+ }
2970
+ #window = null;
2971
+ async #window_sign(nonce) {
2972
+ const { createHmac } = await import("node:crypto");
2973
+ return createHmac("sha256", this.secret || "ogygia-dev-window").update("window:" + nonce).digest("hex").slice(0, 20);
2974
+ }
2975
+ /** A window id from a request, when its signature holds; else null. */
2976
+ async #window_id_of(request) {
2977
+ const raw = request.headers.get(WINDOW_HEADER);
2978
+ if (!raw || raw.length > 80) return null;
2979
+ const dot = raw.indexOf(".");
2980
+ if (dot <= 0) return null;
2981
+ const nonce = raw.slice(0, dot);
2982
+ return await this.#window_sign(nonce) === raw.slice(dot + 1) ? raw : null;
2983
+ }
2984
+ /** A request in a window: written to the store, awaited (see above). */
2985
+ async #window_note(entry, request) {
2986
+ if (!this.#store?.putVisit || entry.internal) return;
2987
+ const id = await this.#window_id_of(request);
2988
+ if (!id) return;
2989
+ const rec = {
2990
+ method: entry.method,
2991
+ path: entry.path,
2992
+ route: entry.route ?? null,
2993
+ ms: entry.ms,
2994
+ status: entry.status ?? null,
2995
+ cpu_ms: entry.cpu_ms,
2996
+ net_ms: entry.net_ms ?? null,
2997
+ net_count: entry.net_count ?? 0,
2998
+ at: Date.now(),
2999
+ instance: this.#instance_id
3000
+ };
3001
+ try {
3002
+ await this.#store.putVisit({
3003
+ key: `${WINDOW_PAGE_PREFIX}${id}\0${rec.at}\0${Math.random().toString(36).slice(2, 8)}`,
3004
+ page: WINDOW_PAGE_PREFIX + id,
3005
+ at: rec.at,
3006
+ visit: JSON.stringify(rec)
3007
+ });
3008
+ } catch {}
3009
+ }
3010
+ /** The requests every instance wrote for a window. */
3011
+ async #window_requests(id) {
3012
+ if (!this.#store?.listVisits) return [];
3013
+ try {
3014
+ return (await this.#store.listVisits(WINDOW_PAGE_PREFIX + id, MAX_VISITS_PER_KEY)).map((r) => JSON.parse(r.visit)).sort((a, b) => a.at - b.at);
3015
+ } catch {
3016
+ return [];
3017
+ }
3018
+ }
3019
+ /** This instance, told apart from the others in a window's requests. */
3020
+ #instance_id = Math.random().toString(36).slice(2, 10);
3021
+ async #window_start(ctx) {
3022
+ const busy = (message) => new Response(JSON.stringify({
3023
+ busy: true,
3024
+ message
3025
+ }), {
3026
+ status: 409,
3027
+ headers: {
3028
+ "content-type": "application/json; charset=utf-8",
3029
+ "retry-after": "2",
3030
+ "cache-control": "no-store"
3031
+ }
3032
+ });
3033
+ if (this.#window) return busy("A window is already recording: stop it first.");
3034
+ if (this.#recorder_busy && this.#trap_running) {
3035
+ this.#page_waiting++;
3036
+ try {
3037
+ const until = Date.now() + PAGE_WAITS_FOR_BACKGROUND_MS;
3038
+ while (this.#recorder_busy && this.#trap_running && Date.now() < until) await new Promise((r) => setTimeout(r, 50));
3039
+ } finally {
3040
+ this.#page_waiting--;
3041
+ }
3042
+ }
3043
+ if (!this.#try_acquire_recorder()) return busy("Another recording holds the recorder. Try again in a moment.");
3044
+ this.#recording_since = Date.now();
3045
+ this.#trap_running = true;
3046
+ this.#ensure_net?.();
3047
+ let stop;
3048
+ const stopped = new Promise((r) => stop = r);
3049
+ const nonce = Math.random().toString(36).slice(2, 12) + Date.now().toString(36);
3050
+ const id = `${nonce}.${await this.#window_sign(nonce)}`;
3051
+ const timer = setTimeout(() => {
3052
+ stop();
3053
+ done.finally(() => {
3054
+ if (this.#window?.id === id) this.#window_release();
3055
+ });
3056
+ }, WINDOW_MAX_MS);
3057
+ timer.unref?.();
3058
+ const done = this.#capture_window(Math.min(this.sample_interval, 1e3), () => stopped, { light: true });
3059
+ done.catch(() => this.#window_release());
3060
+ this.#window = {
3061
+ id,
3062
+ stop,
3063
+ done,
3064
+ timer
3065
+ };
3066
+ return ctx.json({
3067
+ ok: true,
3068
+ id,
3069
+ header: WINDOW_HEADER,
3070
+ shared: !!(this.#store?.putVisit && this.#store.listVisits),
3071
+ max_ms: WINDOW_MAX_MS
3072
+ });
3073
+ }
3074
+ #window_release() {
3075
+ if (this.#window) clearTimeout(this.#window.timer);
3076
+ this.#window = null;
3077
+ this.#trap_running = false;
3078
+ this.#recording_since = 0;
3079
+ this.#release_als();
3080
+ this.#release_recorder();
3081
+ }
3082
+ async #window_stop(ctx) {
3083
+ const asked = ctx.event ? await this.#window_id_of(ctx.event.request) : null;
3084
+ const w = this.#window && (!asked || this.#window.id === asked) ? this.#window : null;
3085
+ if (!w) {
3086
+ if (asked && this.#store?.listVisits) {
3087
+ const reqs = await this.#window_requests(asked);
3088
+ const busy = round2(reqs.reduce((a, r) => a + (r.cpu_ms || 0), 0));
3089
+ return ctx.json({
3090
+ kind: "window",
3091
+ id: null,
3092
+ target: {
3093
+ page: "",
3094
+ runs: []
3095
+ },
3096
+ score: null,
3097
+ forecast: null,
3098
+ findings: [],
3099
+ assets: null,
3100
+ links: {
3101
+ html: "",
3102
+ json: ""
3103
+ },
3104
+ summary: { busy_ms: busy },
3105
+ cpu: "the instance that started the window sampled it; this one only gathered the requests",
3106
+ window: {
3107
+ ms: reqs.length ? reqs[reqs.length - 1].at - reqs[0].at : 0,
3108
+ requests: reqs,
3109
+ instances: new Set(reqs.map((r) => r.instance)).size
3110
+ }
3111
+ });
3112
+ }
3113
+ return ctx.json({ error: "No window is recording (start one with POST /window/start)." }, { status: 400 });
3114
+ }
3115
+ w.stop();
3116
+ let cap;
3117
+ try {
3118
+ cap = await w.done;
3119
+ } catch (e) {
3120
+ this.#window_release();
3121
+ return ctx.json({ error: `The window failed: ${e instanceof Error ? e.message : String(e)}` }, { status: 500 });
3122
+ }
3123
+ const own = `${this.base}/`;
3124
+ const shared = await this.#window_requests(w.id);
3125
+ const others = shared.filter((e) => e.instance !== this.#instance_id && !e.path.startsWith(own) && e.path !== this.base);
3126
+ const requests = [...this.#ring.filter((e) => !e.internal && e.ts >= cap.t0 && e.ts <= cap.t1 && !e.path.startsWith(own) && e.path !== this.base).map((e) => ({
3127
+ method: e.method,
3128
+ path: e.path,
3129
+ route: e.route ?? null,
3130
+ ms: round2(e.ms),
3131
+ status: e.status ?? null,
3132
+ cpu_ms: e.cpu_ms,
3133
+ instance: this.#instance_id
3134
+ })), ...others];
3135
+ try {
3136
+ const id = await this.#finish_report(cap, {
3137
+ trigger: "window",
3138
+ ...requests[0] ? { request: {
3139
+ method: requests[0].method,
3140
+ path: requests[0].path,
3141
+ route: requests[0].route,
3142
+ ms: requests[0].ms
3143
+ } } : {}
3144
+ });
3145
+ const s = this.#reports.get(id);
3146
+ return ctx.json({
3147
+ ...report_json(s.analysis, s.meta, this.base, this.#report_extras(s)),
3148
+ window: {
3149
+ ms: round2(cap.t1 - cap.t0),
3150
+ requests,
3151
+ ...shared.length ? { instances: new Set(requests.map((r) => r.instance)).size } : {}
3152
+ }
3153
+ });
3154
+ } finally {
3155
+ this.#window_release();
3156
+ }
3157
+ }
3158
+ #reset(ctx) {
3159
+ this.#recording_since = 0;
3160
+ this.#release_als();
3161
+ this.#recorder_busy = false;
3162
+ return ctx.redirect(this.base);
3163
+ }
3164
+ async #record_page(ctx, replay) {
3165
+ const event = ctx.event;
3166
+ if (this.#recorder_busy && this.#trap_running) {
3167
+ this.#page_waiting++;
3168
+ try {
3169
+ const until = Date.now() + PAGE_WAITS_FOR_BACKGROUND_MS;
3170
+ while (this.#recorder_busy && this.#trap_running && Date.now() < until) await new Promise((r) => setTimeout(r, 50));
3171
+ } finally {
3172
+ this.#page_waiting--;
3173
+ }
3174
+ }
3175
+ if (!this.#try_acquire_recorder()) return new Response(JSON.stringify({
3176
+ busy: true,
3177
+ message: "A profile is already running on this worker. Waiting for it to finish (or for a free worker)."
3178
+ }), {
3179
+ status: 409,
3180
+ headers: {
3181
+ "content-type": "application/json; charset=utf-8",
3182
+ "retry-after": "2",
3183
+ "cache-control": "no-store"
3184
+ }
3185
+ });
3186
+ const q = ctx.url.searchParams;
3187
+ this.#recording_since = Date.now();
3188
+ const watchdog = setTimeout(() => {
3189
+ this.#recording_since = 0;
3190
+ this.#release_als();
3191
+ }, 32e3);
3192
+ watchdog.unref?.();
3193
+ this.#ensure_net?.();
3194
+ try {
3195
+ const path = replay?.path ?? q.get("p") ?? "";
3196
+ if (!path.startsWith("/") || path.startsWith("//")) return error(400, "Give a path on this site, like /docs/overview.");
3197
+ const runs = clamp(Number(q.get("runs")) || 5, 1, 50);
3198
+ const interval = clamp(Number(q.get("interval")) || 200, 50, 1e4);
3199
+ let heap_now = null;
3200
+ try {
3201
+ heap_now = (await import("node:v8")).getHeapStatistics();
3202
+ } catch {
3203
+ heap_now = null;
3204
+ }
3205
+ if (heap_now && heap_now.heap_size_limit > 0 && heap_now.used_heap_size / heap_now.heap_size_limit >= HEAP_START) return error(503, `The server's heap is ${Math.round(heap_now.used_heap_size / heap_now.heap_size_limit * 100)}% full (${Math.round(heap_now.used_heap_size / 1048576)} of ${Math.round(heap_now.heap_size_limit / 1048576)} MB): profiling now could run it out of memory and end the process. Something keeps what each render makes (the last profile's Kept per render names the line). Restart the server, then profile once.`);
3206
+ const work_budget = serverless_work_budget_ms();
3207
+ const requests_before = Math.max(0, this.#requests_seen - 1);
3208
+ const budget_deadline = Number.isFinite(work_budget) ? Date.now() + work_budget : Infinity;
3209
+ const request_deadline = Date.now() + detect_request_budget_ms();
3210
+ const deadline = Math.min(budget_deadline, Date.now() + RECORDING_HARD_CAP_MS);
3211
+ const fetch_render = (p, extra) => {
3212
+ const left = deadline - Date.now();
3213
+ const ms = Number.isFinite(left) ? Math.max(1, Math.min(PER_RENDER_TIMEOUT_MS, left)) : PER_RENDER_TIMEOUT_MS;
3214
+ return with_timeout(event.fetch(p, { headers: {
3215
+ "sec-fetch-mode": "navigate",
3216
+ "sec-fetch-dest": "document",
3217
+ accept: "text/html",
3218
+ ...replay?.headers,
3219
+ ...extra,
3220
+ "x-og-profiler-internal": "1"
3221
+ } }), ms, "render timed out (the page or one of its upstream calls did not return in time)");
3222
+ };
3223
+ let target = path;
3224
+ let redirected_from;
3225
+ /** a hop was a prerendered redirect's stub (a meta refresh answered 200) */
3226
+ let redirect_stub = false;
3227
+ let warmup_ms;
3228
+ let warm_status = 0;
3229
+ let warm_bytes = 0;
3230
+ let cold_cap;
3231
+ const want_cold = q.get("cold") !== "0";
3232
+ mem_mark("before the warm-up render");
3233
+ for (let hop = 0; hop < 5; hop++) {
3234
+ let t = performance.now();
3235
+ let res;
3236
+ let body = "";
3237
+ try {
3238
+ if (hop === 0 && want_cold) {
3239
+ let t1 = 0;
3240
+ cold_cap = await this.#capture_window(interval, async () => {
3241
+ t = performance.now();
3242
+ res = await fetch_render(target);
3243
+ body = await res.text();
3244
+ t1 = performance.now();
3245
+ }, { light: true });
3246
+ if (res) warmup_ms = round2(t1 - t);
3247
+ } else {
3248
+ res = await fetch_render(target);
3249
+ body = await res.text();
3250
+ warmup_ms = round2(performance.now() - t);
3251
+ }
3252
+ } catch {
3253
+ break;
3254
+ }
3255
+ if (!res) break;
3256
+ warm_status = res.status;
3257
+ warm_bytes = body.length;
3258
+ const stub = res.ok && !res.redirected ? refresh_target(body, event.url.origin) : null;
3259
+ const next = res.redirected ? new URL(res.url).pathname + new URL(res.url).search : res.status >= 300 && res.status < 400 ? (() => {
3260
+ const loc = res.headers.get("location");
3261
+ if (!loc) return null;
3262
+ const u = new URL(loc, event.url.origin);
3263
+ return u.pathname + u.search;
3264
+ })() : stub;
3265
+ if (!next || next === target) break;
3266
+ redirected_from ??= path;
3267
+ if (stub && next === stub) redirect_stub = true;
3268
+ target = next;
3269
+ }
3270
+ const run_ms = [];
3271
+ const run_windows = [];
3272
+ let run_status = warm_status;
3273
+ let run_cache_control;
3274
+ let run_sets_cookie = false;
3275
+ let run_bytes = warm_bytes;
3276
+ let last_body = "";
3277
+ /** the first render's document, set against the last: what changes between renders */
3278
+ let first_body = "";
3279
+ const body_prints = [];
3280
+ let last_chunks = [];
3281
+ let budget_note;
3282
+ let slowest = warmup_ms ?? 0;
3283
+ const coverage_reserve = () => Math.max(slowest * 4.5, 300);
3284
+ const gc_attr = q.get("gc") !== "0";
3285
+ const retention_reserve = () => gc_attr && this.want_heap ? slowest * 1.5 + 500 : 0;
3286
+ let v8_stats = null;
3287
+ try {
3288
+ v8_stats = (await import("node:v8")).getHeapStatistics;
3289
+ } catch {
3290
+ v8_stats = null;
3291
+ }
3292
+ const heap_fill = () => {
3293
+ const s = v8_stats?.();
3294
+ return s && s.heap_size_limit > 0 ? s.used_heap_size / s.heap_size_limit : 0;
3295
+ };
3296
+ const heap_skipped = [];
3297
+ const heap_guard = () => {
3298
+ const s = v8_stats?.();
3299
+ return s ? {
3300
+ used_mb: Math.round(s.used_heap_size / 1048576),
3301
+ limit_mb: Math.round(s.heap_size_limit / 1048576),
3302
+ skipped: heap_skipped
3303
+ } : void 0;
3304
+ };
3305
+ this.#kept?.keep(true);
3306
+ mem_mark("before the timed renders");
3307
+ const cap = await this.#capture_window(interval, async () => {
3308
+ const path_only = target.split("?")[0];
3309
+ const mixed = (w) => this.#ring.some((e) => !e.internal && e.pt !== void 0 && e.path === path_only && e.pt < w.end && (e.ms > 0 ? e.pt + e.ms : Infinity) > w.start);
3310
+ const want_clean = Math.min(2, runs);
3311
+ for (let i = 0; i < runs * 2; i++) {
3312
+ if (i >= runs && run_windows.filter((w) => !mixed(w)).length >= want_clean) break;
3313
+ if (i > 0 && heap_fill() >= HEAP_STOP) {
3314
+ if (i < runs) {
3315
+ budget_note = `Ran ${i} of ${runs} renders — stopped: the server's heap was ${Math.round(heap_fill() * 100)}% full, and each render of this page keeps more of it.`;
3316
+ heap_skipped.push(`${runs - i} of the timed renders`);
3317
+ }
3318
+ break;
3319
+ }
3320
+ if (i > 0 && Date.now() + slowest + retention_reserve() + coverage_reserve() > deadline) {
3321
+ const serverless = Number.isFinite(budget_deadline) && deadline === budget_deadline;
3322
+ const limit_ms = serverless ? work_budget : RECORDING_HARD_CAP_MS;
3323
+ const budget_label = limit_ms >= 1e3 ? `${Math.round(limit_ms / 1e3)} s` : `${Math.round(limit_ms)} ms`;
3324
+ if (i < runs) budget_note = `Ran ${i} of ${runs} renders — trimmed to fit the ` + (serverless ? `${budget_label} serverless budget` : `${budget_label} limit on one recording`) + ` (the page renders in ~${Math.round(warmup_ms ?? 0)} ms). Fewer runs, same accuracy per run.`;
3325
+ break;
3326
+ }
3327
+ const t = performance.now();
3328
+ let res;
3329
+ try {
3330
+ res = await fetch_render(target);
3331
+ } catch {
3332
+ budget_note = i > 0 ? `Ran ${i} of ${runs} renders — stopped early, a later render hung (the page or an upstream call did not return within ${Math.round(PER_RENDER_TIMEOUT_MS / 1e3)}s).` : void 0;
3333
+ break;
3334
+ }
3335
+ const { text: body, chunks } = await read_timed(res, t);
3336
+ const done = performance.now();
3337
+ run_status = res.status;
3338
+ run_bytes = body.length;
3339
+ run_cache_control = res.headers.get("cache-control") ?? void 0;
3340
+ if (res.headers.has("set-cookie")) run_sets_cookie = true;
3341
+ body_prints.push(`${fnv1a32(body)}:${body.length}`);
3342
+ if (!first_body) first_body = body;
3343
+ last_body = body;
3344
+ last_chunks = chunks;
3345
+ run_ms.push(round2(done - t));
3346
+ slowest = Math.max(slowest, done - t);
3347
+ run_windows.push({
3348
+ start: t,
3349
+ end: done
3350
+ });
3351
+ await new Promise((r) => setImmediate(r));
3352
+ }
3353
+ }, { gc_attr });
3354
+ if (run_ms.length === 0) return error(504, "The page did not return in time to profile it. It (or an upstream call it makes) is either hung or slower than the recording budget — try a lighter path, or check that page for a stuck request.");
3355
+ const target_path = target.split("?")[0];
3356
+ const own_calls = /* @__PURE__ */ new Set();
3357
+ for (const c of cap.net) try {
3358
+ own_calls.add(new URL(c.url, "http://x").pathname);
3359
+ } catch {}
3360
+ const outside = this.#ring.filter((e) => !e.internal && e.pt !== void 0 && (e.path === target_path || !own_calls.has(e.path.split("?")[0]) && !e.path.startsWith(this.base)));
3361
+ const mixed_at = run_windows.map((w) => outside.some((e) => e.pt < w.end && (e.ms > 0 ? e.pt + e.ms : Infinity) > w.start));
3362
+ let runs_set_aside = 0;
3363
+ if (mixed_at.some(Boolean) && mixed_at.some((m) => !m)) {
3364
+ const aside = [];
3365
+ for (let i = run_windows.length - 1; i >= 0; i--) {
3366
+ if (!mixed_at[i]) continue;
3367
+ aside.push(run_windows[i]);
3368
+ run_windows.splice(i, 1);
3369
+ run_ms.splice(i, 1);
3370
+ body_prints.splice(i, 1);
3371
+ runs_set_aside++;
3372
+ }
3373
+ cap.set_aside = aside;
3374
+ }
3375
+ const stable = stable_answers(cap.net, run_windows);
3376
+ this.#kept?.keep(false);
3377
+ const almost = /* @__PURE__ */ new Map();
3378
+ if (this.#kept) for (const key of varied_answers(cap.net, run_windows)) {
3379
+ const d = this.#kept.diff(key);
3380
+ if (d && d.size >= 64 && d.bytes <= Math.max(48, d.size * .02)) almost.set(key, d.text);
3381
+ }
3382
+ mem_mark("after the timed renders");
3383
+ let answers;
3384
+ if (stable.size && this.#kept) {
3385
+ const state = {
3386
+ token: `${fnv1a32(`${Math.random()}:${Date.now()}`).toString(36)}${fnv1a32(String(performance.now())).toString(36)}`,
3387
+ keys: stable
3388
+ };
3389
+ this.#answers = state;
3390
+ try {
3391
+ const ms = [];
3392
+ for (let i = 0; i < ANSWER_RENDERS; i++) {
3393
+ if (Date.now() + slowest + retention_reserve() + coverage_reserve() > deadline) break;
3394
+ const t = performance.now();
3395
+ const res = await fetch_render(target, { [ANSWERS_HEADER]: state.token });
3396
+ await res.text();
3397
+ if (res.status !== run_status) break;
3398
+ ms.push(round2(performance.now() - t));
3399
+ }
3400
+ if (ms.length) answers = {
3401
+ runs_ms: ms,
3402
+ calls: stable.size
3403
+ };
3404
+ } catch {
3405
+ answers = void 0;
3406
+ } finally {
3407
+ this.#answers = null;
3408
+ this.#kept.drop();
3409
+ }
3410
+ }
3411
+ const wait_runs = {};
3412
+ for (const c of cap.net) {
3413
+ if (c.ms < 0) continue;
3414
+ const ri = run_windows.findIndex((w) => c.start >= w.start && c.start <= w.end);
3415
+ if (ri === -1) continue;
3416
+ const k = call_group(label_call(c.method, c.url));
3417
+ (wait_runs[k] ??= Array.from({ length: run_windows.length }, () => 0))[ri] += c.ms + (c.body_ms ?? 0);
3418
+ }
3419
+ mark_left_each_run(cap.io_ops, run_windows);
3420
+ cap.window = scope_net_to_one_run(cap, run_windows) ?? void 0;
3421
+ cap.runs = run_windows;
3422
+ let cold;
3423
+ mem_mark("after the answer renders");
3424
+ const cold_route = this.#ring.find((e) => e.internal && e.path === target_path && e.route)?.route;
3425
+ if (cold_cap && warmup_ms !== void 0) try {
3426
+ const p = cold_cap.profile;
3427
+ const whole = {
3428
+ start: cold_cap.perf_start,
3429
+ end: cold_cap.perf_start + (p.endTime - p.startTime) / 1e3
3430
+ };
3431
+ const a0 = analyze(p, await this.#make_resolver(), void 0, {
3432
+ perf_start: cold_cap.perf_start,
3433
+ window: whole,
3434
+ calls: [],
3435
+ runs: [whole],
3436
+ ...cold_route ? { route: cold_route } : {}
3437
+ }, void 0, this.#module_hint);
3438
+ const owners = Object.entries(a0.owner_runs_ms ?? {}).map(([label, ms]) => ({
3439
+ label,
3440
+ ms: ms[0]
3441
+ })).filter((o) => o.ms >= .5).sort((x, y) => y.ms - x.ms).slice(0, 40);
3442
+ cold = {
3443
+ ms: warmup_ms,
3444
+ busy_ms: a0.busy_ms,
3445
+ files: a0.files.filter((f) => f.category !== "idle" && f.category !== "gc" && f.category !== "profiler" && f.self_ms >= .5).slice(0, 40).map((f) => ({
3446
+ file: f.key,
3447
+ category: f.category,
3448
+ ms: f.self_ms
3449
+ })),
3450
+ ...owners.length ? { owners } : {},
3451
+ calls: cold_cap.net.length
3452
+ };
3453
+ } catch {
3454
+ cold = void 0;
3455
+ }
3456
+ mem_mark("after the answer renders and the cold analysis");
3457
+ let retained;
3458
+ let retain_ms = 0;
3459
+ const heap_ok = heap_fill() < HEAP_EXTRAS;
3460
+ if (gc_attr && this.want_heap && !heap_ok) heap_skipped.push("the render that measures what a render keeps");
3461
+ if (gc_attr && this.want_heap && heap_ok && Date.now() + retention_reserve() + coverage_reserve() < deadline) try {
3462
+ const t = Date.now();
3463
+ retained = await this.#retention_pass(async () => {
3464
+ await fetch_render(target).then((r) => r.text());
3465
+ });
3466
+ retain_ms = Date.now() - t;
3467
+ } catch {
3468
+ retained = void 0;
3469
+ }
3470
+ mem_mark("after the retention render");
3471
+ let growth;
3472
+ if (retained && retained.total_bytes >= 262144) {
3473
+ const each = Math.max(retain_ms, slowest + 300);
3474
+ const room = deadline - Date.now() - coverage_reserve() - 1500;
3475
+ const s = v8_stats?.();
3476
+ const heap_n = s ? Math.floor((s.heap_size_limit * HEAP_STOP - s.used_heap_size) / retained.total_bytes) : Infinity;
3477
+ const n = Math.min(6, Math.floor(room / each), heap_n);
3478
+ if (heap_n < 3) heap_skipped.push("the growth check (a few more renders, to tell a leak from a cache filling)");
3479
+ if (n >= 3) try {
3480
+ growth = await this.#growth_check(async () => {
3481
+ await fetch_render(target).then((r) => r.text());
3482
+ }, n, Date.now() + room);
3483
+ } catch {
3484
+ growth = void 0;
3485
+ }
3486
+ }
3487
+ mem_mark("after the retention and growth renders");
3488
+ const counted = heap_fill() < HEAP_STOP ? await this.#count_calls(async () => {
3489
+ await fetch_render(target).then((r) => r.text());
3490
+ }, true) : (heap_skipped.push("the call-count render (the ×N counts)"), { counts: {} });
3491
+ cap.call_counts = counted.counts;
3492
+ mem_mark("after the coverage render");
3493
+ const app_fetch = async (u) => {
3494
+ try {
3495
+ const r = await event.fetch(u, { headers: { "x-og-profiler-internal": "1" } });
3496
+ if (r.ok) return r;
3497
+ } catch {}
3498
+ return fetch(new URL(u, event.url.origin), { headers: { "x-og-profiler-internal": "1" } });
3499
+ };
3500
+ const id = await this.#finish_report(cap, {
3501
+ trigger: "page",
3502
+ page: target,
3503
+ redirected_from,
3504
+ ...redirect_stub ? { redirect_stub: true } : {},
3505
+ ...process.env.AWS_LAMBDA_FUNCTION_NAME && !process.env.VERCEL && !process.env.NETLIFY ? {
3506
+ lambda: true,
3507
+ ...Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE) > 0 ? { lambda_mb: Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE) } : {}
3508
+ } : {},
3509
+ heap_limit_mb: Math.round((await import("node:v8")).getHeapStatistics().heap_size_limit / 1048576),
3510
+ instance: {
3511
+ first_request_ms: round2(this.#first_request_pt ?? 0),
3512
+ node_ms: round2(performance.nodeTiming?.bootstrapComplete ?? 0),
3513
+ age_s: Math.round(performance.now() / 100) / 10,
3514
+ requests_before
3515
+ },
3516
+ warmup_ms,
3517
+ run_status,
3518
+ run_bytes,
3519
+ ...run_cache_control ? { run_cache_control } : {},
3520
+ ...run_sets_cookie ? { run_sets_cookie: true } : {},
3521
+ ...(() => {
3522
+ const path = target.split("?")[0];
3523
+ for (let k = this.#ring.length - 1; k >= 0; k--) {
3524
+ const e = this.#ring[k];
3525
+ if (e.path === path) return e.route ? { run_route: e.route } : {};
3526
+ }
3527
+ return run_status === 200 && !this.dev ? { run_static: true } : {};
3528
+ })(),
3529
+ budget_note,
3530
+ ...heap_skipped.length ? { heap_guard: heap_guard() } : {},
3531
+ runs: run_ms,
3532
+ ...runs_set_aside ? { runs_set_aside } : {},
3533
+ ...body_prints.length >= 2 && body_prints.every((p) => p === body_prints[0]) ? { same_document: true } : {},
3534
+ ...cold ? { cold } : {},
3535
+ ...answers ? { answers } : {}
3536
+ }, app_fetch);
3537
+ mem_mark("after the report was built");
3538
+ const s = this.#reports.get(id);
3539
+ if (retained) s.retained = retained;
3540
+ if (growth) s.growth = growth;
3541
+ if (!s.meta.same_document && first_body && last_body && run_ms.length >= 2) try {
3542
+ const rows = island_rows_of(s.meta);
3543
+ const name_of = (entry) => {
3544
+ const tail = entry.startsWith(".") ? entry.slice(1) : entry;
3545
+ const r = rows.find((x) => x.entry === tail || x.entry.endsWith(tail));
3546
+ return r ? island_name(r) : void 0;
3547
+ };
3548
+ const diff = doc_diff(first_body, last_body, name_of);
3549
+ if (diff) s.meta.doc_diff = diff;
3550
+ } catch {}
3551
+ if (last_body) {
3552
+ try {
3553
+ s.strip = byte_strip(last_body);
3554
+ if (last_chunks.length > 1) {
3555
+ s.strip.chunks = last_chunks;
3556
+ const tail = stream_tail(last_body, s.strip, last_chunks);
3557
+ if (tail) s.strip.tail = tail;
3558
+ }
3559
+ } catch {}
3560
+ if (this.dev) s.assets_missing = "the dev server serves modules one by one, so its JS says nothing about a build: profile a build (preview or deploy) to weigh the page";
3561
+ else try {
3562
+ const extra = [];
3563
+ for (const r of island_rows_of(s.meta)) {
3564
+ const lazy = r.wake !== "load" && r.wake !== "idle";
3565
+ for (const u of [r.module_url, ...r.hints]) if (u) extra.push({
3566
+ url: u,
3567
+ kind: "script",
3568
+ blocking: false,
3569
+ via: "island",
3570
+ ...lazy ? { lazy: true } : {}
3571
+ });
3572
+ }
3573
+ const { gzipSync } = await import("node:zlib");
3574
+ s.assets = await weigh_assets({
3575
+ html: last_body,
3576
+ page_url: new URL(target, event.url.origin).href,
3577
+ fetch_url: app_fetch,
3578
+ cache: this.#asset_weights,
3579
+ gzip: (b) => gzipSync(b, { level: 6 }).byteLength,
3580
+ budget_ms: q.get("weigh") === "full" ? Math.max(500, Math.min(12e4, request_deadline - Date.now() - 4e3)) : Math.max(500, Math.min(6e3, request_deadline - Date.now() - 8e3)),
3581
+ ...q.get("weigh") === "full" ? { max_files: 2e3 } : {},
3582
+ extra,
3583
+ contents_of: (u) => {
3584
+ try {
3585
+ const path = new URL(u).pathname;
3586
+ const rel = app_asset_rel(path);
3587
+ return chunkContents(rel ? "/" + rel : path);
3588
+ } catch {
3589
+ return null;
3590
+ }
3591
+ }
3592
+ });
3593
+ } catch {
3594
+ s.assets_missing = "the page could not be weighed";
3595
+ }
3596
+ try {
3597
+ s.river = await this.#river_for(s, last_body, run_windows[run_windows.length - 1]);
3598
+ } catch {}
3599
+ try {
3600
+ s.lineage = await this.#lineage_for(s, last_body);
3601
+ } catch {}
3602
+ }
3603
+ try {
3604
+ await this.#lines_for(s, counted.profile, cap.window?.start, stable, wait_runs, almost);
3605
+ } catch {}
3606
+ const persist_wait = Math.max(200, Math.min(PERSIST_WAIT_MS, request_deadline - Date.now() - 2e3));
3607
+ await Promise.race([this.#persist(s), new Promise((r) => setTimeout(r, persist_wait))]);
3608
+ if (q.get("format") === "keep") return ctx.json({
3609
+ id,
3610
+ url: ctx.href("/report/[id]", { id }),
3611
+ dump: report_dump(s.analysis, s.meta, this.#report_extras(s))
3612
+ });
3613
+ if (q.get("format") === "ogp") return this.#ogp_response(id, s);
3614
+ if (q.get("format") === "json" || (ctx.request.headers.get("accept") ?? "").includes("application/json")) {
3615
+ const since = await this.#since_for(s, this.#prev_in_memory(s));
3616
+ const shared = this.#shared_of(s.meta);
3617
+ const full = {
3618
+ ...report_json(s.analysis, s.meta, this.base, this.#report_extras(s)),
3619
+ ...since ? { since } : {},
3620
+ ...shared.length ? { shared } : {}
3621
+ };
3622
+ return ctx.json(pick_fields(full, q.get("fields")));
3623
+ }
3624
+ return ctx.redirect(ctx.href("/report/[id]", { id }));
3625
+ } catch (e) {
3626
+ if (isHttpError(e) || isRedirect(e)) throw e;
3627
+ return error(500, e instanceof Error && INSPECTOR_ERR.test(e.message) ? "CPU profiling needs a Node.js server (adapter-node or dev). This platform does not expose the V8 inspector." : `Profiling failed: ${e instanceof Error ? e.message : String(e)}`);
3628
+ } finally {
3629
+ clearTimeout(watchdog);
3630
+ this.#recording_since = 0;
3631
+ this.#kept?.keep(false);
3632
+ this.#kept?.drop();
3633
+ this.#answers = null;
3634
+ this.#release_als();
3635
+ this.#release_recorder();
3636
+ }
3637
+ }
3638
+ async #upload(ctx) {
3639
+ try {
3640
+ const bytes = recover_ogp_bytes(new Uint8Array(await ctx.request.arrayBuffer()));
3641
+ if (!is_ogp(bytes)) return ctx.json({ error: "That file is not an ogygia .ogp profile. If you moved it between machines, copy it as a binary file — a text editor or chat can silently re-encode it." }, { status: 400 });
3642
+ const ogp_key = ctx.request.headers.get("x-ogp-key") || this.secret;
3643
+ const dump = await ogp_decode(bytes, ogp_key);
3644
+ if (!is_dump(dump)) return ctx.json({ error: "That file is not an ogygia profiler dump." }, { status: 400 });
3645
+ return ctx.json({ url: ctx.href("/report/[id]", { id: this.#store_uploaded(dump) }) });
3646
+ } catch {
3647
+ return ctx.json({ error: "That file isn't a profile, or it was made with a different key." }, { status: 400 });
3648
+ }
3649
+ }
3650
+ /** the lines two or more profiled pages share, over the latest report of each page this server holds */
3651
+ #site_fixes(limit = 8) {
3652
+ return site_fixes([...this.#reports.values()].filter((r) => r.meta.trigger === "page" && r.meta.page && (r.meta.run_status ?? 200) === 200).map((r) => ({
3653
+ id: r.meta.id,
3654
+ page: r.meta.page,
3655
+ created: r.meta.created,
3656
+ runs: r.meta.runs?.length || 1,
3657
+ patterns: r.patterns
3658
+ })), limit);
3659
+ }
3660
+ /** of those, the ones this report's page is among: its fixes that make other pages faster too */
3661
+ #shared_of(meta) {
3662
+ if (meta.trigger !== "page" || !meta.page) return [];
3663
+ return this.#site_fixes(Infinity).filter((f) => f.pages.some((p) => p.page === meta.page)).slice(0, 5);
3664
+ }
3665
+ async #report_json(stored) {
3666
+ const since = await this.#since_for(stored, this.#prev_in_memory(stored));
3667
+ const shared = this.#shared_of(stored.meta);
3668
+ const body = {
3669
+ ...report_json(stored.analysis, stored.meta, this.base, this.#report_extras(stored)),
3670
+ ...since ? { since } : {},
3671
+ ...shared.length ? { shared } : {}
3672
+ };
3673
+ return new Response(JSON.stringify(body), { headers: {
3674
+ "content-type": "application/json",
3675
+ "cache-control": "no-store"
3676
+ } });
3677
+ }
3678
+ /** The RAW dump JSON (`{ kind, version, meta, analysis, extras }`) the share-link viewer renders —
3679
+ * the same shape `.ogp` carries. Authed (only the owner mints a link); the recipient never hits it. */
3680
+ #report_dump_json(stored) {
3681
+ const body = report_dump(stored.analysis, stored.meta, this.#report_extras(stored));
3682
+ return new Response(JSON.stringify(body), { headers: {
3683
+ "content-type": "application/json",
3684
+ "cache-control": "no-store"
3685
+ } });
3686
+ }
3687
+ async #report_raw(stored) {
3688
+ let body;
3689
+ if (typeof stored.raw === "string") body = stored.raw;
3690
+ else try {
3691
+ const { gunzipSync } = await import("node:zlib");
3692
+ body = gunzipSync(stored.raw).toString("utf8");
3693
+ } catch {
3694
+ return new Response(new Uint8Array(stored.raw), { headers: {
3695
+ "content-type": "application/json",
3696
+ "content-encoding": "gzip",
3697
+ "content-disposition": `attachment; filename="ssr-${stored.meta.id}.cpuprofile"`,
3698
+ "cache-control": "no-store"
3699
+ } });
3700
+ }
3701
+ return new Response(body, { headers: {
3702
+ "content-type": "application/json",
3703
+ "content-disposition": `attachment; filename="ssr-${stored.meta.id}.cpuprofile"`,
3704
+ "cache-control": "no-store"
3705
+ } });
3706
+ }
3707
+ /**
3708
+ * Render a report page with its dump embedded as an encrypted `.ogp` (base64), so the Export button
3709
+ * downloads it with no server round-trip (the report may be evicted by then) and no key in the page.
3710
+ * Encoding is async (Brotli + scrypt off-thread — never blocks the event loop) and best-effort: if
3711
+ * Node crypto/zlib is unavailable (a stripped edge runtime), the Export button is simply omitted and
3712
+ * the JSON / .cpuprofile links still work.
3713
+ */
3714
+ async #report_view(stored) {
3715
+ const { analysis: a, meta } = stored;
3716
+ const extras = this.#report_extras(stored);
3717
+ let ogpB64;
3718
+ try {
3719
+ const bytes = await ogp_encode(report_dump(a, meta, extras), this.secret);
3720
+ ogpB64 = Buffer.from(bytes).toString("base64");
3721
+ } catch {
3722
+ ogpB64 = void 0;
3723
+ }
3724
+ const history = meta.trigger === "page" && meta.page ? page_history([...this.#reports.values()].map((r) => r.meta)).find((h) => h.page === meta.page) ?? null : null;
3725
+ const i = history ? history.points.findIndex((p) => p.id === meta.id) : -1;
3726
+ const since = await this.#since_for(stored, i > 0 ? history.points[i - 1].id : null);
3727
+ return {
3728
+ a,
3729
+ meta,
3730
+ base: this.base,
3731
+ extras,
3732
+ ogpB64,
3733
+ history,
3734
+ prev: since?.prev ?? (i > 0 ? history.points[i - 1].id : null),
3735
+ since,
3736
+ dev: this.dev
3737
+ };
3738
+ }
3739
+ /**
3740
+ * SINCE THE LAST PROFILE of this page: the comparison, cut to what a reader needs at the top — the
3741
+ * render's change, the rows that really moved (inlining shifts left out), the order trap, the fix
3742
+ * check. The previous report from this instance's memory, else (it restarted; a serverless
3743
+ * instance is new) the configured store's newest earlier report of the same page.
3744
+ */
3745
+ async #since_for(stored, prev_in_memory) {
3746
+ const { meta } = stored;
3747
+ let prev = prev_in_memory;
3748
+ if (!prev && meta.trigger === "page" && meta.page) try {
3749
+ const earlier = (await this.#list_stored(50)).filter((r) => r.page === meta.page && r.id !== meta.id && r.created < meta.created).sort((a, b) => b.created - a.created)[0];
3750
+ if (earlier) prev = earlier.id;
3751
+ } catch {}
3752
+ const P = prev ? this.#reports.get(prev) ?? await this.#report_load(prev) : void 0;
3753
+ if (!P) return null;
3754
+ try {
3755
+ const cmp = compare_reports(this.#pack(P), this.#pack(stored));
3756
+ const render = cmp.summary.find((r) => r.label.startsWith("render"));
3757
+ let score;
3758
+ try {
3759
+ const sa = page_score_of(P.meta, this.#report_extras(P));
3760
+ const sb = page_score_of(stored.meta, this.#report_extras(stored));
3761
+ const moved = sb.categories.map((c) => ({
3762
+ key: c.key,
3763
+ label: c.label,
3764
+ b: c.score,
3765
+ a: sa.categories.find((x) => x.key === c.key)?.score ?? NaN
3766
+ })).filter((m) => Number.isFinite(m.a) && Math.abs(m.b - m.a) >= 3).sort((x, y) => Math.abs(y.b - y.a) - Math.abs(x.b - x.a)).slice(0, 3);
3767
+ score = {
3768
+ a: sa.score,
3769
+ b: sb.score,
3770
+ a_grade: sa.grade,
3771
+ b_grade: sb.grade,
3772
+ moved
3773
+ };
3774
+ } catch {
3775
+ score = void 0;
3776
+ }
3777
+ let assets;
3778
+ try {
3779
+ if (P.assets && stored.assets) {
3780
+ const d = assets_diff(P.assets, stored.assets);
3781
+ if (d.added.length || d.removed.length || d.grew.length || d.shrank.length || d.to_start.length || d.to_later.length) assets = d;
3782
+ }
3783
+ } catch {
3784
+ assets = void 0;
3785
+ }
3786
+ let browser;
3787
+ try {
3788
+ const bf = (s) => {
3789
+ const e = this.#report_extras(s);
3790
+ if (!e.visit) return null;
3791
+ const names = new Map(island_rows_of(s.meta).map((r) => [r.fp, r.name]));
3792
+ return derive_findings(s.analysis, s.meta, e).filter((f) => f.fps).map((f) => `${f.code}${f.fps.length ? ` (${[...new Set(f.fps.map((fp) => names.get(fp) ?? fp.slice(0, 6)))].sort().join(", ")})` : ""}`);
3793
+ };
3794
+ const was = bf(P);
3795
+ const now = bf(stored);
3796
+ if (was && now) {
3797
+ const fixed = was.filter((x) => !now.includes(x));
3798
+ const added = now.filter((x) => !was.includes(x));
3799
+ if (fixed.length || added.length) browser = {
3800
+ fixed,
3801
+ added
3802
+ };
3803
+ }
3804
+ } catch {
3805
+ browser = void 0;
3806
+ }
3807
+ let vitals;
3808
+ try {
3809
+ const va = this.#report_extras(P).visit;
3810
+ const vb = this.#report_extras(stored).visit;
3811
+ if (va && vb) {
3812
+ const input = (v) => ({
3813
+ vitals: v.vitals ?? {},
3814
+ visit: {
3815
+ nav: v.nav,
3816
+ paints: v.paints,
3817
+ resources: v.resources,
3818
+ interaction: v.interaction,
3819
+ font_faces: v.font_faces
3820
+ },
3821
+ islands: [],
3822
+ firsts: [],
3823
+ shifts: [],
3824
+ longtasks: []
3825
+ });
3826
+ const moved = vitals_moved$1(va.vitals ?? {}, vb.vitals ?? {}, (side, key) => vital_parts(input(side === "a" ? va : vb), key));
3827
+ if (moved.length) vitals = moved;
3828
+ }
3829
+ } catch {
3830
+ vitals = void 0;
3831
+ }
3832
+ let islands;
3833
+ try {
3834
+ islands = island_files_diff(island_rows_of(P.meta), island_rows_of(stored.meta), (u) => stored.weights?.[u]) ?? void 0;
3835
+ } catch {
3836
+ islands = void 0;
3837
+ }
3838
+ return {
3839
+ prev: P.meta.id,
3840
+ ...score ? { score } : {},
3841
+ ...browser ? { browser } : {},
3842
+ ...vitals ? { vitals } : {},
3843
+ ...islands ? { islands } : {},
3844
+ ...assets ? { assets } : {},
3845
+ ...render ? {
3846
+ a_ms: render.a,
3847
+ b_ms: render.b
3848
+ } : {},
3849
+ ...cmp.render_same ? { same: true } : {},
3850
+ moved: (cmp.drill ?? []).filter((r) => r.status !== "shifted").slice(0, 3),
3851
+ ...cmp.order ? { order: cmp.order } : {},
3852
+ ...cmp.fix_check ? { fix_check: cmp.fix_check } : {}
3853
+ };
3854
+ } catch {
3855
+ return null;
3856
+ }
3857
+ }
3858
+ /** this page's report just before `stored`, in this instance's memory */
3859
+ #prev_in_memory(stored) {
3860
+ const { meta } = stored;
3861
+ if (meta.trigger !== "page" || !meta.page) return null;
3862
+ const h = page_history([...this.#reports.values()].map((r) => r.meta)).find((x) => x.page === meta.page);
3863
+ const i = h ? h.points.findIndex((p) => p.id === meta.id) : -1;
3864
+ return i > 0 ? h.points[i - 1].id : null;
3865
+ }
3866
+ /** Store an uploaded dump as a report so it renders (+ hydrates) at its own /report/<id> URL — an
3867
+ * islands page can't be swapped in via document.write. No cpuprofile in a dump, so `raw` is empty
3868
+ * (its /raw link just 404s; everything else works). */
3869
+ #store_uploaded(dump) {
3870
+ const id = Math.random().toString(36).slice(2, 10);
3871
+ this.#reports.set(id, this.#report_from_dump({
3872
+ ...dump,
3873
+ meta: {
3874
+ ...dump.meta,
3875
+ id
3876
+ }
3877
+ }));
3878
+ while (this.#reports.size > this.max_reports) {
3879
+ const oldest = this.#reports.keys().next().value;
3880
+ if (oldest === void 0) break;
3881
+ this.#reports.delete(oldest);
3882
+ }
3883
+ return id;
3884
+ }
3885
+ /** A StoredReport rebuilt from a portable dump (an uploaded `.ogp`, or one read back from the
3886
+ * storage backend). No cpuprofile survives a dump, so `raw` is empty (the `/raw` link 404s). */
3887
+ #report_from_dump(dump) {
3888
+ const e = dump.extras;
3889
+ return {
3890
+ meta: dump.meta,
3891
+ analysis: dump.analysis,
3892
+ heap: e.heap,
3893
+ ...e.heap_components ? { heap_components: e.heap_components } : {},
3894
+ net: e.net,
3895
+ mem: e.mem,
3896
+ measures: e.measures ?? [],
3897
+ gc: e.gc ?? null,
3898
+ io: e.io ?? [],
3899
+ call_counts: e.call_counts ?? {},
3900
+ raw: "",
3901
+ ...e.weights ? { weights: e.weights } : {},
3902
+ ...e.contents ? { contents: e.contents } : {},
3903
+ ...e.heavy ? { heavy: e.heavy } : {},
3904
+ ...e.barrels ? { barrels: e.barrels } : {},
3905
+ ...e.hazards ? { hazards: e.hazards } : {},
3906
+ ...e.client ? { client: e.client } : {},
3907
+ ...e.vitals ? { vitals: e.vitals } : {},
3908
+ ...e.client_marks ? { client_marks: e.client_marks } : {},
3909
+ ...e.client_cpu ? { client_cpu: e.client_cpu } : {},
3910
+ ...e.interaction_cpu ? { interaction_cpu: e.interaction_cpu } : {},
3911
+ ...e.alloc ? { alloc: e.alloc } : {},
3912
+ ...e.contention ? { contention: e.contention } : {},
3913
+ ...e.lineage ? { lineage: e.lineage } : {},
3914
+ ...e.spans ? { spans: e.spans } : {},
3915
+ ...e.visit ? { visit: e.visit } : {},
3916
+ ...e.strip ? { strip: e.strip } : {},
3917
+ ...e.assets ? { assets: e.assets } : {},
3918
+ ...e.assets_missing ? { assets_missing: e.assets_missing } : {},
3919
+ ...e.river ? { river: e.river } : {},
3920
+ ...e.gc_attr ? { gc_attr: e.gc_attr } : {},
3921
+ ...e.promises ? { promises: e.promises } : {},
3922
+ ...e.retained ? { retained: e.retained } : {},
3923
+ ...e.ledger ? { ledger: e.ledger } : {},
3924
+ ...e.patterns ? { patterns: e.patterns } : {},
3925
+ ...e.drill ? { drill: e.drill } : {},
3926
+ ...e.forecast ? { forecast: e.forecast } : {},
3927
+ ...e.growth ? { growth: e.growth } : {}
3928
+ };
3929
+ }
3930
+ /** Write a finished report to the durable store, when one is configured (a store failure never
3931
+ * fails the recording). The dump is the same portable JSON the `.ogp` export uses. The returned
3932
+ * promise settles when the write did: a serverless host freezes the process once the response
3933
+ * is sent, so a write left running after it may never land — the page recording awaits it. */
3934
+ #persist(stored) {
3935
+ if (!this.#store) return Promise.resolve();
3936
+ const meta = stored.meta;
3937
+ const median = meta.runs?.length ? [...meta.runs].sort((a, b) => a - b)[Math.floor(meta.runs.length / 2)] : null;
3938
+ const rec = {
3939
+ id: meta.id,
3940
+ created: meta.created,
3941
+ page: meta.page ?? null,
3942
+ trigger: meta.trigger,
3943
+ label: meta.page ?? meta.request?.path ?? meta.id,
3944
+ median,
3945
+ dump: JSON.stringify(report_dump(stored.analysis, meta, this.#report_extras(stored)))
3946
+ };
3947
+ return Promise.resolve().then(async () => {
3948
+ await this.#store.putReport(rec);
3949
+ await this.#store.prune?.(Math.max(this.max_reports * 8, 200));
3950
+ }).catch((err) => {
3951
+ if (process.env.OGYGIA_PROFILER_DEBUG) console.error("[ogygia/profiler] store.putReport failed:", err);
3952
+ });
3953
+ }
3954
+ /** Memory first, then the durable store: read the dump back, reconstruct, cache in memory. */
3955
+ async #report_load(id) {
3956
+ if (!id) return void 0;
3957
+ const hit = this.#reports.get(id);
3958
+ if (hit) return hit;
3959
+ if (!this.#store) return void 0;
3960
+ try {
3961
+ const json = await this.#store.getReport(id);
3962
+ if (!json) return void 0;
3963
+ const dump = JSON.parse(json);
3964
+ if (!dump?.meta || !dump.analysis) return void 0;
3965
+ const stored = this.#report_from_dump({
3966
+ ...dump,
3967
+ meta: {
3968
+ ...dump.meta,
3969
+ id
3970
+ }
3971
+ });
3972
+ this.#reports.set(id, stored);
3973
+ return stored;
3974
+ } catch (err) {
3975
+ if (process.env.OGYGIA_PROFILER_DEBUG) console.error("[ogygia/profiler] store.getReport failed:", err);
3976
+ return;
3977
+ }
3978
+ }
3979
+ /** The reports the durable store holds (summaries), newest first — the sidebar's shared list. */
3980
+ async #list_stored(limit = 20) {
3981
+ if (!this.#store) return [];
3982
+ try {
3983
+ return (await this.#store.listReports(limit)).map((r) => ({
3984
+ id: r.id,
3985
+ label: r.label,
3986
+ page: r.page ?? void 0,
3987
+ created: r.created
3988
+ }));
3989
+ } catch {
3990
+ return [];
3991
+ }
3992
+ }
3993
+ /** The full profile as an encrypted `.ogp` download (Brotli + AES-GCM, async — never blocks). */
3994
+ async #ogp_response(id, stored) {
3995
+ const bytes = await ogp_encode(report_dump(stored.analysis, stored.meta, this.#report_extras(stored)), this.secret);
3996
+ return new Response(new Uint8Array(bytes), { headers: {
3997
+ "content-type": "application/octet-stream",
3998
+ "content-disposition": `attachment; filename="profile-${id}.ogp"`,
3999
+ "cache-control": "no-store"
4000
+ } });
4001
+ }
4002
+ #report_extras(stored) {
4003
+ const client = stored.client ?? this.#client_for(stored);
4004
+ const browser_page = stored.meta.page?.split("?")[0];
4005
+ const vitals = stored.vitals ?? this.#vitals_for(browser_page);
4006
+ const client_marks = stored.client_marks ?? this.#marks_for(browser_page);
4007
+ const client_cpu = stored.client_cpu ?? (browser_page ? this.#client_cpus.get(browser_page) : void 0);
4008
+ const own_visit = () => {
4009
+ const created = stored.meta.created - 1e3;
4010
+ if (stored.visit && stored.visit.at >= created) return stored.visit;
4011
+ const list = browser_page ? this.#visits.get(browser_page) ?? [] : [];
4012
+ let first;
4013
+ for (const v of list) if (v.at >= created && (!first || v.at < first.at)) first = v;
4014
+ return first ?? stored.visit ?? list.at(-1);
4015
+ };
4016
+ const visit = own_visit();
4017
+ const answered = new Set((visit?.holes_answered ?? []).map((h) => h.id));
4018
+ const hole_requests = answered.size ? this.#ring.filter((e) => e.hole && answered.has(e.hole.id) && e.ts >= visit.at - 1e3 && e.ts <= visit.at + 6e4).map((e) => ({
4019
+ id: e.hole.id,
4020
+ ms: Math.max(0, e.ms - (e.hole.queue_ms ?? 0)),
4021
+ ...e.hole.name ? { name: e.hole.name } : {},
4022
+ ...e.hole.p ? { p: e.hole.p } : {}
4023
+ })) : [];
4024
+ const nav_requests = (visit?.navs ?? []).map((n) => {
4025
+ const lo = visit.at + n.t - 1e4;
4026
+ const hi = visit.at + n.fetched + 50;
4027
+ let hit;
4028
+ for (const e of this.#ring) {
4029
+ if (e.internal || e.method !== "GET" || e.path + (e.search ?? "") !== n.to || e.ts < lo || e.ts > hi) continue;
4030
+ if (!hit || e.ts > hit.ts) hit = e;
4031
+ }
4032
+ return hit ? {
4033
+ t: n.t,
4034
+ ms: Math.round(hit.ms),
4035
+ cpu_ms: Math.round(hit.cpu_ms),
4036
+ net_ms: Math.round(hit.net_ms),
4037
+ net_count: hit.net_count,
4038
+ ...hit.inflight ? { inflight: hit.inflight } : {}
4039
+ } : null;
4040
+ }).filter((x) => !!x);
4041
+ let doc_request;
4042
+ if (visit?.nav && stored.meta.page) {
4043
+ const path = stored.meta.page.split("?")[0];
4044
+ for (const e of this.#ring) {
4045
+ if (e.internal || e.method !== "GET" || e.path !== path) continue;
4046
+ if (e.ts < visit.at - 500 || e.ts > visit.at + visit.nav.res_start + 500) continue;
4047
+ doc_request = { ms: Math.round(e.ms) };
4048
+ }
4049
+ }
4050
+ const icpu = stored.interaction_cpu ?? (browser_page ? this.#interaction_cpus.get(browser_page) : void 0);
4051
+ const interaction_cpu = icpu && visit?.interaction && Math.abs(icpu.t - visit.interaction.t) < 2 ? icpu : void 0;
4052
+ const narrow = (v) => (v.viewport?.[0] ?? 0) < NARROW_SCREEN_PX;
4053
+ const other_screen_visit = visit?.viewport && browser_page ? (this.#visits.get(browser_page) ?? []).filter((v) => v !== visit && v.viewport && narrow(v) !== narrow(visit)).at(-1) : void 0;
4054
+ const engine = engine_of(visit?.ua);
4055
+ const other_engine_visit = engine && browser_page ? (this.#visits.get(browser_page) ?? []).filter((v) => v !== visit && engine_of(v.ua) && engine_of(v.ua) !== engine).at(-1) : void 0;
4056
+ const dupes = chunkDuplicates();
4057
+ return {
4058
+ ...visit ? { visit } : {},
4059
+ ...other_screen_visit ? { other_screen_visit } : {},
4060
+ ...other_engine_visit ? { other_engine_visit } : {},
4061
+ ...dupes ? { dupes } : {},
4062
+ ...interaction_cpu ? { interaction_cpu } : {},
4063
+ ...hole_requests.length ? { hole_requests } : {},
4064
+ ...nav_requests.length ? { nav_requests } : {},
4065
+ ...doc_request ? { doc_request } : {},
4066
+ ...stored.strip ? { strip: stored.strip } : {},
4067
+ ...stored.assets ? { assets: stored.assets } : {},
4068
+ ...stored.assets_missing ? { assets_missing: stored.assets_missing } : {},
4069
+ ...stored.river ? { river: stored.river } : {},
4070
+ ...stored.gc_attr ? { gc_attr: stored.gc_attr } : {},
4071
+ ...stored.promises ? { promises: stored.promises } : {},
4072
+ ...stored.retained ? { retained: stored.retained } : {},
4073
+ ...stored.alloc ? { alloc: stored.alloc } : {},
4074
+ ...stored.contention ? { contention: stored.contention } : {},
4075
+ ...stored.lineage ? { lineage: stored.lineage } : {},
4076
+ ...stored.ledger ? { ledger: stored.ledger } : {},
4077
+ ...stored.patterns ? { patterns: stored.patterns } : {},
4078
+ ...stored.drill ? { drill: stored.drill } : {},
4079
+ ...stored.forecast ? { forecast: stored.forecast } : {},
4080
+ ...stored.growth ? { growth: stored.growth } : {},
4081
+ net: stored.net,
4082
+ spans: stored.spans ?? [],
4083
+ heap: stored.heap,
4084
+ ...stored.heap_components ? { heap_components: stored.heap_components } : {},
4085
+ mem: stored.mem,
4086
+ measures: stored.measures,
4087
+ gc: stored.gc,
4088
+ io: stored.io,
4089
+ call_counts: stored.call_counts,
4090
+ ...stored.weights ? { weights: stored.weights } : {},
4091
+ ...stored.contents ? { contents: stored.contents } : {},
4092
+ ...stored.heavy ? { heavy: stored.heavy } : {},
4093
+ ...stored.barrels ? { barrels: stored.barrels } : {},
4094
+ ...stored.hazards ? { hazards: stored.hazards } : {},
4095
+ ...client.length ? { client } : {},
4096
+ ...vitals ? { vitals } : {},
4097
+ ...client_marks?.length ? { client_marks } : {},
4098
+ ...client_cpu ? { client_cpu } : {},
4099
+ ...stored.replay ? { replay: stored.replay } : {}
4100
+ };
4101
+ }
4102
+ /** The app's own browser marks for the page, per name (p50 / max over the visits). */
4103
+ #marks_for(page) {
4104
+ if (!page) return void 0;
4105
+ const by_name = this.#marks.get(page);
4106
+ if (!by_name?.size) return void 0;
4107
+ const out = [];
4108
+ for (const [name, a] of by_name) {
4109
+ const ms = [...a.ms].sort((x, y) => x - y);
4110
+ out.push({
4111
+ name,
4112
+ n: ms.length,
4113
+ p50_ms: round2(percentile(ms, .5)),
4114
+ max_ms: round2(ms[ms.length - 1] ?? 0),
4115
+ errors: a.errors,
4116
+ attr_keys: [...a.attr_keys].sort()
4117
+ });
4118
+ }
4119
+ return out.sort((x, y) => y.p50_ms - x.p50_ms);
4120
+ }
4121
+ /** The page's web vitals as the profiler user's browser reported them (p50 over the visits). */
4122
+ #vitals_for(page) {
4123
+ if (!page) return void 0;
4124
+ const e = this.#vitals.get(page);
4125
+ if (!e || !e.samples.length) return void 0;
4126
+ const p50 = (k) => {
4127
+ const xs = e.samples.map((s) => s[k]).filter((x) => x !== void 0).sort((a, b) => a - b);
4128
+ return xs.length ? round2(percentile(xs, .5)) : null;
4129
+ };
4130
+ return {
4131
+ n: e.samples.length,
4132
+ ttfb: p50("ttfb"),
4133
+ fcp: p50("fcp"),
4134
+ lcp: p50("lcp"),
4135
+ cls: p50("cls"),
4136
+ inp: p50("inp")
4137
+ };
4138
+ }
4139
+ /** The browser's hydration timings for THIS report's islands: the beacon ring joined by
4140
+ * fingerprint (the same props → the same fingerprint, so a visit after the recording matches),
4141
+ * then merged per island — a list of 48 cards is 48 fingerprints and one row. */
4142
+ #client_for(stored) {
4143
+ const per_entry = /* @__PURE__ */ new Map();
4144
+ const by_entry = /* @__PURE__ */ new Map();
4145
+ for (const b of this.#beacons.values()) if (b.entry && b.ms.length) by_entry.set(entry_path(b.entry), b);
4146
+ for (const r of island_rows_of(stored.meta)) {
4147
+ const b = this.#beacons.get(r.fp) ?? by_entry.get(entry_path(r.entry));
4148
+ if (!b || !b.ms.length) continue;
4149
+ let e = per_entry.get(r.entry);
4150
+ if (!e) per_entry.set(r.entry, e = {
4151
+ fp: r.fp,
4152
+ name: r.name,
4153
+ ms: [],
4154
+ load: [],
4155
+ recovered: 0
4156
+ });
4157
+ e.ms.push(...b.ms);
4158
+ e.load.push(...b.load);
4159
+ e.recovered += b.recovered;
4160
+ if (b.reason) e.reason = b.reason;
4161
+ }
4162
+ const out = [];
4163
+ for (const [entry, e] of per_entry) {
4164
+ const ms = e.ms.sort((x, y) => x - y);
4165
+ const load = e.load.sort((x, y) => x - y);
4166
+ out.push({
4167
+ fp: e.fp,
4168
+ entry,
4169
+ name: e.name,
4170
+ n: ms.length,
4171
+ p50_ms: round2(percentile(ms, .5)),
4172
+ max_ms: round2(ms[ms.length - 1]),
4173
+ load_p50_ms: round2(percentile(load, .5)),
4174
+ recovered: e.recovered,
4175
+ ...e.reason ? { reason: e.reason } : {}
4176
+ });
4177
+ }
4178
+ return out;
4179
+ }
4180
+ /** `/replay/[id]`: profile a caught (or header-profiled) request again, in full page mode, with
4181
+ * the inputs it was caught with — its path and query, and the headers the trap config kept. */
4182
+ async #replay(id, ctx) {
4183
+ const stored = this.#reports.get(id ?? "");
4184
+ if (!stored?.meta.request) return new Response("Nothing to replay: that report has no request.", { status: 404 });
4185
+ const replay = stored.replay ?? {
4186
+ path: stored.meta.request.path,
4187
+ headers: {}
4188
+ };
4189
+ return this.#record_page(ctx, replay);
4190
+ }
4191
+ /** The browser's CPU profile (Chromium's JS Self-Profiling trace) → the same analysis as the
4192
+ * server's: components, functions, flame. Categories come from the chunk contents the build
4193
+ * recorded, since a client chunk's URL says nothing about what it holds. */
4194
+ async #ingest_client_cpu(page, trace, origin, interaction) {
4195
+ const profile = self_profile_to_cpuprofile(trace);
4196
+ if (!profile) return false;
4197
+ const find = await this.#client_file_finder(origin);
4198
+ let rewritten = 0;
4199
+ if (find) for (const n of profile.nodes) {
4200
+ const file = find(n.callFrame.url);
4201
+ if (file) {
4202
+ n.callFrame.url = file;
4203
+ rewritten++;
4204
+ }
4205
+ }
4206
+ if (this.dev && origin) for (const n of profile.nodes) {
4207
+ const u = n.callFrame.url;
4208
+ if (u.startsWith(origin + "/")) {
4209
+ n.callFrame.url = u.slice(origin.length);
4210
+ rewritten++;
4211
+ }
4212
+ }
4213
+ const resolver = rewritten || !this.dev ? await this.#make_resolver() : void 0;
4214
+ const url_hint = (url, name) => {
4215
+ let path;
4216
+ try {
4217
+ const u = new URL(url, "http://x");
4218
+ path = u.pathname;
4219
+ if (origin && u.host !== "x" && u.origin !== origin) return {
4220
+ category: "dependency",
4221
+ pkg: CDN_PKG_RE.exec(path)?.[1] ?? u.host
4222
+ };
4223
+ } catch {
4224
+ return;
4225
+ }
4226
+ const rel = app_asset_rel(path);
4227
+ if (rel) path = "/" + rel;
4228
+ const inside = chunkContents(path);
4229
+ if (inside) {
4230
+ if (inside.every((s) => s === "svelte runtime")) return "svelte";
4231
+ if (!inside.some((s) => s.startsWith("src/") || s.endsWith(".svelte") || s.startsWith("+"))) {
4232
+ const pkgs = inside.map((s) => s.replace(/ runtime$/, "")).filter((s) => !s.startsWith("+"));
4233
+ return {
4234
+ category: "dependency",
4235
+ pkg: pkgs.length > 2 ? `${pkgs[0]} +${pkgs.length - 1}` : pkgs.join(" + ")
4236
+ };
4237
+ }
4238
+ return;
4239
+ }
4240
+ return SVELTE_CLIENT_FN_RE.test(name) ? "svelte" : void 0;
4241
+ };
4242
+ const served = (p) => {
4243
+ const rel = app_asset_rel(p);
4244
+ return rel ? "/" + rel : p;
4245
+ };
4246
+ const renamer = chunk_component_renamer((p) => chunkContents(served(p)));
4247
+ const run = (p) => analyze(p, resolver, void 0, void 0, renamer, url_hint);
4248
+ if (interaction) {
4249
+ if (this.#interaction_cpus.size >= 50 && !this.#interaction_cpus.has(page)) this.#interaction_cpus.delete(this.#interaction_cpus.keys().next().value);
4250
+ this.#interaction_cpus.set(page, {
4251
+ ...interaction_windows(profile, interaction, run),
4252
+ at: Date.now()
4253
+ });
4254
+ return true;
4255
+ }
4256
+ const analysis = run(profile);
4257
+ const visit = this.#visits.get(page)?.at(-1);
4258
+ const windows = visit && Date.now() - visit.at < 12e4 ? client_windows(profile, visit, run) : void 0;
4259
+ const i = visit && Date.now() - visit.at < 12e4 ? visit.interaction : void 0;
4260
+ if (i && i.ms >= 200 && i.t * 1e3 >= profile.startTime && (i.t + i.ms) * 1e3 <= profile.endTime) {
4261
+ const at = i.t + i.delay;
4262
+ if (this.#interaction_cpus.size >= 50 && !this.#interaction_cpus.has(page)) this.#interaction_cpus.delete(this.#interaction_cpus.keys().next().value);
4263
+ this.#interaction_cpus.set(page, {
4264
+ ...interaction_windows(profile, {
4265
+ t: i.t,
4266
+ wait: [i.t, at],
4267
+ handler: [at, at + i.processing]
4268
+ }, run),
4269
+ at: Date.now()
4270
+ });
4271
+ }
4272
+ if (this.#client_cpus.size >= 50 && !this.#client_cpus.has(page)) this.#client_cpus.delete(this.#client_cpus.keys().next().value);
4273
+ this.#client_cpus.set(page, {
4274
+ analysis,
4275
+ at: Date.now(),
4276
+ sample_ms: analysis.duration_ms,
4277
+ ...windows ? { windows } : {}
4278
+ });
4279
+ return true;
4280
+ }
4281
+ /** `/beacon` (POST, authed): the runtime's hydration timings — `{ islands: [{ fp, entry, ms, load }] }`.
4282
+ * Bounded every way (body, islands per post, fingerprints kept, samples per fingerprint). */
4283
+ async #beacon(ctx) {
4284
+ let body;
4285
+ try {
4286
+ const text = await ctx.request.text();
4287
+ if (text.length > (text.includes("\"cpu\"") ? MAX_BEACON_CPU_BODY : text.includes("\"visit\"") ? MAX_BEACON_VISIT_BODY : MAX_BEACON_BODY)) return new Response(null, { status: 413 });
4288
+ body = JSON.parse(text);
4289
+ } catch {
4290
+ return new Response(null, { status: 400 });
4291
+ }
4292
+ const islands = body?.islands;
4293
+ const visit_body = body?.visit;
4294
+ const vitals = body?.vitals ?? (visit_body && typeof visit_body === "object" ? visit_body.vitals : void 0);
4295
+ const vitals_at_raw = Number(body?.at ?? (visit_body && typeof visit_body === "object" ? visit_body.at : void 0));
4296
+ const vitals_at = Number.isFinite(vitals_at_raw) && vitals_at_raw > 0 ? Math.round(vitals_at_raw) : void 0;
4297
+ const marks = body?.marks;
4298
+ const cpu = body?.cpu;
4299
+ const page = body?.page;
4300
+ const visit_raw = body?.visit;
4301
+ if (!Array.isArray(islands) && !(vitals && typeof vitals === "object") && !Array.isArray(marks) && !(cpu && typeof cpu === "object") && !(visit_raw && typeof visit_raw === "object")) return new Response(null, { status: 400 });
4302
+ if (visit_raw && typeof visit_raw === "object") {
4303
+ const visit = parse_visit(page, visit_raw);
4304
+ if (!visit) return new Response(null, { status: 400 });
4305
+ let list = this.#visits.get(visit.page);
4306
+ if (!list) {
4307
+ if (this.#visits.size >= MAX_VITALS_PATHS) this.#visits.delete(this.#visits.keys().next().value);
4308
+ this.#visits.set(visit.page, list = []);
4309
+ }
4310
+ const same = list.findIndex((v) => v.at === visit.at);
4311
+ if (same !== -1) list[same] = merge_visits(list[same], visit);
4312
+ else {
4313
+ list.push(visit);
4314
+ if (list.length > MAX_VISITS_PER_PAGE) list.shift();
4315
+ }
4316
+ }
4317
+ if (cpu && typeof cpu === "object" && typeof page === "string" && page.startsWith("/") && page.length <= 500 && this.#client_cpu) {
4318
+ const it = body?.interaction;
4319
+ const pair = (v) => Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n < 36e5) && v[1] >= v[0] ? [v[0], v[1]] : null;
4320
+ const wait = pair(it?.wait);
4321
+ const handler = pair(it?.handler);
4322
+ const spans = it && typeof it.t === "number" && Number.isFinite(it.t) && wait && handler ? {
4323
+ t: it.t,
4324
+ wait,
4325
+ handler
4326
+ } : void 0;
4327
+ if (it && !spans) return new Response(null, { status: 400 });
4328
+ if (!await this.#ingest_client_cpu(page, cpu, ctx.url.origin, spans)) return new Response(null, { status: 400 });
4329
+ }
4330
+ const now = Date.now();
4331
+ if (Array.isArray(marks) && typeof page === "string" && page.startsWith("/") && page.length <= 500) {
4332
+ let by_name = this.#marks.get(page);
4333
+ if (!by_name) {
4334
+ if (this.#marks.size >= MAX_VITALS_PATHS) this.#marks.delete(this.#marks.keys().next().value);
4335
+ this.#marks.set(page, by_name = /* @__PURE__ */ new Map());
4336
+ }
4337
+ for (const m of marks.slice(0, MAX_BEACON_ISLANDS)) {
4338
+ const name = typeof m?.name === "string" ? m.name.slice(0, 120) : "";
4339
+ const ms = Number(m?.ms);
4340
+ if (!name || !Number.isFinite(ms) || ms < 0 || ms > 6e5) continue;
4341
+ let agg = by_name.get(name);
4342
+ if (!agg) {
4343
+ if (by_name.size >= 100) continue;
4344
+ by_name.set(name, agg = {
4345
+ ms: [],
4346
+ errors: 0,
4347
+ attr_keys: /* @__PURE__ */ new Set()
4348
+ });
4349
+ }
4350
+ agg.ms.push(round2(ms));
4351
+ if (agg.ms.length > MAX_BEACON_SAMPLES) agg.ms.shift();
4352
+ const attrs = m?.attrs && typeof m.attrs === "object" ? m.attrs : null;
4353
+ if (attrs?.error === true) agg.errors++;
4354
+ for (const k of Object.keys(attrs ?? {}).slice(0, 12)) if (k !== "error") agg.attr_keys.add(k.slice(0, 40));
4355
+ }
4356
+ }
4357
+ if (vitals && typeof vitals === "object" && typeof page === "string" && page.startsWith("/") && page.length <= 500) {
4358
+ const v = vitals;
4359
+ const num = (k, max) => {
4360
+ const n = Number(v[k]);
4361
+ return Number.isFinite(n) && n >= 0 && n <= max ? round2(n) : void 0;
4362
+ };
4363
+ const sample = vitals_at !== void 0 ? { at: vitals_at } : {};
4364
+ const ttfb = num("ttfb", 6e5);
4365
+ const fcp = num("fcp", 6e5);
4366
+ const lcp = num("lcp", 6e5);
4367
+ const cls = num("cls", 100);
4368
+ const inp = num("inp", 6e5);
4369
+ if (ttfb !== void 0) sample.ttfb = ttfb;
4370
+ if (fcp !== void 0) sample.fcp = fcp;
4371
+ if (lcp !== void 0) sample.lcp = lcp;
4372
+ if (cls !== void 0) sample.cls = cls;
4373
+ if (inp !== void 0) sample.inp = inp;
4374
+ if (Object.keys(sample).some((k) => k !== "at")) {
4375
+ let e = this.#vitals.get(page);
4376
+ if (!e) {
4377
+ if (this.#vitals.size >= MAX_VITALS_PATHS) {
4378
+ let oldest;
4379
+ let t = Infinity;
4380
+ for (const [k, x] of this.#vitals) if (x.last < t) {
4381
+ t = x.last;
4382
+ oldest = k;
4383
+ }
4384
+ if (oldest !== void 0) this.#vitals.delete(oldest);
4385
+ }
4386
+ this.#vitals.set(page, e = {
4387
+ samples: [],
4388
+ last: now
4389
+ });
4390
+ }
4391
+ e.last = now;
4392
+ const same = sample.at !== void 0 ? e.samples.findIndex((s) => s.at === sample.at) : -1;
4393
+ if (same !== -1) e.samples[same] = {
4394
+ ...e.samples[same],
4395
+ ...sample
4396
+ };
4397
+ else e.samples.push(sample);
4398
+ if (e.samples.length > MAX_VITALS_SAMPLES) e.samples.shift();
4399
+ }
4400
+ }
4401
+ for (const it of Array.isArray(islands) ? islands.slice(0, MAX_BEACON_ISLANDS) : []) {
4402
+ const fp = typeof it?.fp === "string" ? it.fp : "";
4403
+ const ms = Number(it?.ms);
4404
+ const load = Number(it?.load);
4405
+ if (!is_hex_id(fp) || !Number.isFinite(ms) || ms < 0 || ms > 6e5) continue;
4406
+ let agg = this.#beacons.get(fp);
4407
+ if (!agg) {
4408
+ if (this.#beacons.size >= MAX_BEACON_FPS) {
4409
+ let oldest;
4410
+ let t = Infinity;
4411
+ for (const [k, v] of this.#beacons) if (v.last < t) {
4412
+ t = v.last;
4413
+ oldest = k;
4414
+ }
4415
+ if (oldest !== void 0) this.#beacons.delete(oldest);
4416
+ }
4417
+ this.#beacons.set(fp, agg = {
4418
+ entry: typeof it?.entry === "string" ? it.entry.slice(0, 300) : "",
4419
+ ms: [],
4420
+ load: [],
4421
+ recovered: 0,
4422
+ last: now
4423
+ });
4424
+ }
4425
+ agg.last = now;
4426
+ if (it?.recovered === true) agg.recovered++;
4427
+ if (typeof it?.reason === "string" && it.reason) agg.reason = it.reason.slice(0, 300);
4428
+ agg.ms.push(round2(ms));
4429
+ agg.load.push(Number.isFinite(load) && load >= 0 ? round2(Math.min(load, ms)) : 0);
4430
+ if (agg.ms.length > MAX_BEACON_SAMPLES) {
4431
+ agg.ms.shift();
4432
+ agg.load.shift();
4433
+ }
4434
+ }
4435
+ return new Response(null, {
4436
+ status: 204,
4437
+ headers: { "cache-control": "no-store" }
4438
+ });
4439
+ }
4440
+ /** The page the profiler's own user is about to view gets the beacon tag: the runtime posts its
4441
+ * hydration timings to `/beacon` when it sees it, and does nothing otherwise. The flag cookie
4442
+ * (set at login, site-wide, no secret) or the key header says it is that user; dev is open. The
4443
+ * tag goes at the end of `<head>` so the runtime bootstrap stays the first script. A visitor
4444
+ * never sees it; a stray flag with no session posts to a guarded route and gets nothing. */
4445
+ async #beacon_tag(event) {
4446
+ if (!this.ui_enabled) return null;
4447
+ if (event.request.headers.get("x-og-profiler-internal") === "1") return null;
4448
+ const dest = event.request.headers.get("sec-fetch-dest");
4449
+ if (dest ? dest !== "document" : !(event.request.headers.get("accept") ?? "").includes("text/html")) return null;
4450
+ if (!this.dev) {
4451
+ const key = event.request.headers.get("x-profiler-key");
4452
+ if (key ? !await this.#key_matches(key) : event.cookies.get("og_profiler_beacon") !== "1") return null;
4453
+ }
4454
+ return `<meta name="${BEACON_META}" content="${this.base}/beacon">${BEACON_STANDALONE_TAG}`;
4455
+ }
4456
+ handle = async ({ event, resolve }) => {
4457
+ if (this.#disabled) return resolve(event);
4458
+ this.#first_request_pt ??= performance.now();
4459
+ this.#requests_seen++;
4460
+ await this.#init();
4461
+ const on_ui_path = event.url.pathname === this.base || event.url.pathname.startsWith(this.base + "/");
4462
+ if (this.ui_enabled && on_ui_path) return this.#ui(event);
4463
+ if (!this.#disabled && !this.ui_enabled && !this.dev && this.secret === "" && on_ui_path) return new Response("ogygia profiler is installed but disabled: set OGYGIA_PROFILER_SECRET (or `ogygia({ profiler: { secret } })`) to enable the UI in production.", {
4464
+ status: 404,
4465
+ headers: {
4466
+ "content-type": "text/plain; charset=utf-8",
4467
+ "cache-control": "no-store"
4468
+ }
4469
+ });
4470
+ const remote = event.isRemoteRequest === true;
4471
+ let remote_url = null;
4472
+ if (remote) try {
4473
+ remote_url = new URL(event.request.url);
4474
+ } catch {
4475
+ remote_url = null;
4476
+ }
4477
+ const entry = {
4478
+ ts: Date.now(),
4479
+ method: event.request.method,
4480
+ path: remote_url ? remote_url.pathname : event.url.pathname,
4481
+ ...remote ? { remote: true } : {},
4482
+ route: event.route?.id ?? null,
4483
+ status: 0,
4484
+ ms: 0,
4485
+ cpu_ms: 0,
4486
+ inflight: this.#inflight,
4487
+ net_ms: 0,
4488
+ net_count: 0,
4489
+ internal: event.request.headers.get("x-og-profiler-internal") === "1" || void 0
4490
+ };
4491
+ try {
4492
+ const search = remote_url ? remote_url.search : event.url.search;
4493
+ if (search) entry.search = search;
4494
+ } catch {}
4495
+ if (this.#trap?.replay?.length) {
4496
+ const kept = {};
4497
+ for (const h of this.#trap.replay) {
4498
+ const v = event.request.headers.get(h);
4499
+ if (v !== null) kept[h.toLowerCase()] = v.slice(0, 2048);
4500
+ }
4501
+ if (Object.keys(kept).length) entry.replay_headers = kept;
4502
+ }
4503
+ const profile_header = event.request.headers.get("x-profile");
4504
+ const header_profile = !!profile_header && this.ui_enabled && await this.#key_matches(profile_header) && this.#try_acquire_recorder();
4505
+ const ctx = !!this.#als && (this.dev || this.#recording_active() && !this.#trap_running || header_profile) ? this.#request_ctx(entry) : null;
4506
+ const answers = this.#answers;
4507
+ if (ctx && answers && this.#kept && event.request.headers.get(ANSWERS_HEADER) === answers.token) {
4508
+ const kept = this.#kept;
4509
+ ctx.answer = (method, url) => {
4510
+ const key = `${method} ${url}`;
4511
+ return answers.keys.has(key) ? kept.get(key) : void 0;
4512
+ };
4513
+ }
4514
+ const start = performance.now();
4515
+ entry.pt = start;
4516
+ const cpu0 = process.cpuUsage();
4517
+ this.#inflight++;
4518
+ if (ctx && this.#wrap_event_fetch && typeof event.fetch === "function") try {
4519
+ event.fetch = this.#wrap_event_fetch(event.fetch);
4520
+ } catch {}
4521
+ const beacon_tag = await this.#beacon_tag(event);
4522
+ const resolve_page = () => beacon_tag ? resolve(event, { transformPageChunk: ({ html }) => {
4523
+ let at = html.indexOf("</head>");
4524
+ if (at === -1) at = html.indexOf("</HEAD>");
4525
+ return at === -1 ? html : html.slice(0, at) + beacon_tag + html.slice(at);
4526
+ } }) : resolve(event);
4527
+ const run = async () => {
4528
+ const res = ctx && this.#als ? await this.#als.run(ctx, resolve_page) : await resolve_page();
4529
+ entry.status = res.status;
4530
+ if (beacon_tag && this.#client_cpu) try {
4531
+ res.headers.set("document-policy", "js-profiling");
4532
+ } catch {}
4533
+ if (beacon_tag && !this.dev) mark_personal(res);
4534
+ if (this.want_server_timing) try {
4535
+ const ms = performance.now() - start;
4536
+ res.headers.append("Server-Timing", `ssr;desc="SvelteKit render";dur=${ms.toFixed(1)}`);
4537
+ try {
4538
+ const c = process.cpuUsage(cpu0);
4539
+ res.headers.append("Server-Timing", `cpu;desc="process CPU while it ran";dur=${((c.user + c.system) / 1e3).toFixed(1)}`);
4540
+ } catch {}
4541
+ if (ctx?.net.length) {
4542
+ const net_ms = ctx.net.reduce((a, c) => a + Math.max(c.ms, 0) + (c.body_ms ?? 0), 0);
4543
+ res.headers.append("Server-Timing", `net;desc="outbound (${ctx.net.length})";dur=${net_ms.toFixed(1)}`);
4544
+ [...ctx.net].sort((x, y) => y.ms + (y.body_ms ?? 0) - (x.ms + (x.body_ms ?? 0))).slice(0, 3).forEach((x, i) => {
4545
+ let where = x.url.split("?")[0].split("#")[0];
4546
+ try {
4547
+ const u = new URL(x.url, "http://relative.invalid");
4548
+ where = (u.host === "relative.invalid" ? "" : u.host) + u.pathname;
4549
+ } catch {}
4550
+ const desc = `${x.method ?? "GET"} ${where}`.slice(0, 120).split("\"").join("'").split("\\").join("/");
4551
+ res.headers.append("Server-Timing", `up${i + 1};desc="${desc}";dur=${(Math.max(x.ms, 0) + (x.body_ms ?? 0)).toFixed(1)}`);
4552
+ });
4553
+ }
4554
+ if (event.request.headers.get("x-og-trace")) {
4555
+ const c = process.cpuUsage(cpu0);
4556
+ const net = ctx?.net ?? [];
4557
+ res.headers.set(TRACE_HEADER, encode_trace({
4558
+ ms: round2(ms),
4559
+ cpu_ms: round2((c.user + c.system) / 1e3),
4560
+ wait_ms: round2(net.reduce((a, x) => a + Math.max(x.ms, 0) + (x.body_ms ?? 0), 0)),
4561
+ calls: net.length,
4562
+ route: event.route?.id ?? null,
4563
+ top: [...net].sort((x, y) => y.ms - x.ms).slice(0, 5).map((x) => ({
4564
+ url: x.url.slice(0, 200),
4565
+ ms: round2(x.ms + (x.body_ms ?? 0))
4566
+ })),
4567
+ profiler: this.base
4568
+ }));
4569
+ }
4570
+ } catch {}
4571
+ return res;
4572
+ };
4573
+ if (header_profile) {
4574
+ this.#recording_since = Date.now();
4575
+ const watchdog = setTimeout(() => {
4576
+ this.#recording_since = 0;
4577
+ this.#release_als();
4578
+ }, 32e3);
4579
+ watchdog.unref?.();
4580
+ try {
4581
+ let res;
4582
+ const cap = await this.#capture_window(100, async () => {
4583
+ res = await run();
4584
+ });
4585
+ this.#finalize(entry, start, cpu0, ctx, event.request);
4586
+ cap.window = {
4587
+ start,
4588
+ end: start + entry.ms
4589
+ };
4590
+ const id = await this.#finish_report(cap, {
4591
+ trigger: "request",
4592
+ request: {
4593
+ method: entry.method,
4594
+ path: entry.path,
4595
+ route: entry.route,
4596
+ ms: entry.ms
4597
+ }
4598
+ });
4599
+ const stored = this.#reports.get(id);
4600
+ if (stored) stored.replay = {
4601
+ path: entry.path + (entry.search ?? ""),
4602
+ headers: entry.replay_headers ?? {}
4603
+ };
4604
+ try {
4605
+ res?.headers.append("x-profile-report", `${this.base}/report/${id}`);
4606
+ } catch {}
4607
+ return res;
4608
+ } finally {
4609
+ clearTimeout(watchdog);
4610
+ this.#recording_since = 0;
4611
+ this.#release_als();
4612
+ this.#release_recorder();
4613
+ }
4614
+ }
4615
+ try {
4616
+ return await run();
4617
+ } finally {
4618
+ this.#finalize(entry, start, cpu0, ctx, event.request);
4619
+ if (event.request.headers.has(WINDOW_HEADER)) await this.#window_note(entry, event.request);
4620
+ }
4621
+ };
4622
+ #finalize(entry, start, cpu0, ctx, request) {
4623
+ this.#inflight--;
4624
+ entry.ms = round2(performance.now() - start);
4625
+ const c = process.cpuUsage(cpu0);
4626
+ entry.cpu_ms = round2((c.user + c.system) / 1e3);
4627
+ const og = request ? request_stats_of(request) : void 0;
4628
+ if (og) entry.og = og;
4629
+ const hole = request ? hole_stats_of(request) : void 0;
4630
+ if (hole) entry.hole = hole;
4631
+ if (ctx) {
4632
+ entry.net_count = ctx.net.length;
4633
+ entry.net_ms = round2(ctx.net.reduce((a, c) => a + Math.max(c.ms, 0) + (c.body_ms ?? 0), 0));
4634
+ if (ctx.spans.length) {
4635
+ entry.span_count = ctx.spans.length;
4636
+ entry.span_ms = round2(ctx.spans.filter((s) => s.parent === void 0 && s.ms >= 0).reduce((a, s) => a + s.ms, 0));
4637
+ }
4638
+ }
4639
+ this.#ring.push(entry);
4640
+ if (this.#ring.length > this.ring_size) this.#ring.shift();
4641
+ if (request) {
4642
+ this.#logged.set(request, entry);
4643
+ const early = this.#early_holes.get(request);
4644
+ if (early) {
4645
+ this.#early_holes.delete(request);
4646
+ for (const s of early) this.#batch_hole(request, s);
4647
+ }
4648
+ }
4649
+ if (this.#sink_url && !entry.internal) {
4650
+ this.#sink.push({
4651
+ k: "req",
4652
+ t: entry.ts,
4653
+ path: entry.path,
4654
+ route: entry.route,
4655
+ ms: entry.ms,
4656
+ cpu: entry.cpu_ms,
4657
+ wait: entry.net_ms,
4658
+ status: entry.status,
4659
+ ...entry.og ? { og: entry.og.seed_bytes + entry.og.tail_bytes } : {}
4660
+ });
4661
+ if (Number.isFinite(serverless_work_budget_ms())) this.#sink_flush();
4662
+ }
4663
+ }
4664
+ /** Post the buffered rows as NDJSON to the sink. One post in flight at a time; a failure keeps
4665
+ * the rows for the next try (the buffer drops its oldest request rows when full). */
4666
+ async #sink_flush() {
4667
+ if (this.#sink_timer && this.#superseded()) {
4668
+ clearInterval(this.#sink_timer);
4669
+ this.#sink_timer = null;
4670
+ }
4671
+ if (!this.#sink_url || this.#sink_inflight || !this.#sink.size) return;
4672
+ this.#sink_inflight = true;
4673
+ const rows = this.#sink.size;
4674
+ const body = this.#sink.drain();
4675
+ try {
4676
+ const res = await fetch(this.#sink_url, {
4677
+ method: "POST",
4678
+ body,
4679
+ headers: {
4680
+ "content-type": "application/x-ndjson",
4681
+ "x-og-profiler-internal": "1",
4682
+ ...this.#sink_key ? { authorization: `Bearer ${this.#sink_key}` } : {}
4683
+ },
4684
+ signal: AbortSignal.timeout(5e3)
4685
+ });
4686
+ this.#sink_last = {
4687
+ at: Date.now(),
4688
+ ok: res.ok,
4689
+ rows,
4690
+ ...res.ok ? {} : { error: `the sink answered ${res.status}` }
4691
+ };
4692
+ if (!res.ok) {
4693
+ for (const line of body.split("\n")) if (line) this.#sink.push(JSON.parse(line));
4694
+ }
4695
+ } catch (e) {
4696
+ this.#sink_last = {
4697
+ at: Date.now(),
4698
+ ok: false,
4699
+ rows,
4700
+ error: e instanceof Error ? e.message : String(e)
4701
+ };
4702
+ for (const line of body.split("\n")) if (line) this.#sink.push(JSON.parse(line));
4703
+ } finally {
4704
+ this.#sink_inflight = false;
4705
+ }
4706
+ }
4707
+ /** The site view's rows from THIS instance: the request ring and the sampler's windows — what a
4708
+ * long-lived host has without a sink. */
4709
+ #site_rows() {
4710
+ const rows = [];
4711
+ for (const e of this.#ring) if (!e.internal) rows.push({
4712
+ k: "req",
4713
+ t: e.ts,
4714
+ path: e.path,
4715
+ route: e.route,
4716
+ ms: e.ms,
4717
+ cpu: e.cpu_ms,
4718
+ wait: e.net_ms,
4719
+ status: e.status,
4720
+ ...e.og ? { og: e.og.seed_bytes + e.og.tail_bytes } : {}
4721
+ });
4722
+ for (const w of this.#sampled) rows.push({
4723
+ k: "win",
4724
+ t0: w.at,
4725
+ t1: w.at + w.ms,
4726
+ fns: w.functions.slice(0, 40).map((f) => ({
4727
+ name: f.name,
4728
+ file: f.url,
4729
+ self_ms: f.self_ms,
4730
+ category: f.category
4731
+ }))
4732
+ });
4733
+ for (const r of this.#reports.values()) if (r.meta.trigger === "trap" && r.meta.request) rows.push({
4734
+ k: "trap",
4735
+ t: r.meta.created,
4736
+ path: r.meta.request.path,
4737
+ ms: r.meta.request.ms,
4738
+ id: r.meta.id
4739
+ });
4740
+ return rows;
4741
+ }
4742
+ /** `/site`: the whole-site pictures (the request cloud, the layer cake) from this instance's
4743
+ * rows, or from a sink the browser reads (`?from=<url>`, or the configured sink). */
4744
+ #site(ctx) {
4745
+ const from = ctx.url.searchParams.get("from");
4746
+ return {
4747
+ base: this.base,
4748
+ rows: this.#site_rows(),
4749
+ sink: this.#sink_url ? {
4750
+ url: this.#sink_url,
4751
+ last: this.#sink_last,
4752
+ buffered: this.#sink.size
4753
+ } : null,
4754
+ from: from && /^https?:\/\//.test(from) ? from.slice(0, 2e3) : null,
4755
+ ephemeral: Number.isFinite(serverless_work_budget_ms())
4756
+ };
4757
+ }
4758
+ };
4759
+ /**
4760
+ * Build a profiler handle. INTERNAL — the profiler is configured in vite.config.ts
4761
+ * (`ogygia({ profiler: true })`) and `ogygia.handle()` constructs + mounts this itself.
4762
+ * Kept as a named export only so the handle's lazy import can reach it; it is not a
4763
+ * public entry point.
4764
+ *
4765
+ * See the class doc and README for what it captures. Visit `<path>` (default
4766
+ * /__profiler) — in prod, log in (or send the x-profiler-key header).
4767
+ */
4768
+ function profiler(options = {}) {
4769
+ const instance = new Profiler(options);
4770
+ return (args) => instance.handle(args);
4771
+ }
4772
+ function route_aggregates(ring, by = null) {
4773
+ const by_route = /* @__PURE__ */ new Map();
4774
+ for (const e of ring) {
4775
+ if (e.internal) continue;
4776
+ const key = (e.route ?? "(no route)") + (by ? ` · ${by}=${e.tags?.[by] ?? "—"}` : "");
4777
+ let list = by_route.get(key);
4778
+ if (!list) by_route.set(key, list = []);
4779
+ list.push(e);
4780
+ }
4781
+ const aggs = [];
4782
+ for (const [route, list] of by_route) {
4783
+ const sorted = list.map((e) => e.ms).sort((a, b) => a - b);
4784
+ const net = list.map((e) => e.net_ms).sort((a, b) => a - b);
4785
+ aggs.push({
4786
+ route,
4787
+ count: list.length,
4788
+ p50: pct(sorted, 50),
4789
+ p95: pct(sorted, 95),
4790
+ max: sorted.at(-1) ?? 0,
4791
+ avg: round2(sorted.reduce((a, c) => a + c, 0) / (sorted.length || 1)),
4792
+ net_p50: pct(net, 50)
4793
+ });
4794
+ }
4795
+ return aggs.sort((a, b) => b.p95 - a.p95);
4796
+ }
4797
+ function pct(sorted, p) {
4798
+ if (!sorted.length) return 0;
4799
+ return sorted[Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length))];
4800
+ }
4801
+ function clamp(n, lo, hi) {
4802
+ return Math.max(lo, Math.min(hi, Number.isFinite(n) ? n : lo));
4803
+ }
4804
+ /** Race a promise against a wall-time so a hung render can never pin a recording (and, through the
4805
+ * attribution AsyncLocalStorage, the whole site). Rejects with `label` on timeout; the underlying work
4806
+ * is left to settle on its own — the caller has already moved on. */
4807
+ function with_timeout(p, ms, label) {
4808
+ if (!Number.isFinite(ms) || ms <= 0) return p;
4809
+ let timer;
4810
+ const timeout = new Promise((_, reject) => {
4811
+ timer = setTimeout(() => reject(new Error(label)), ms);
4812
+ timer.unref?.();
4813
+ });
4814
+ return Promise.race([p.finally(() => clearTimeout(timer)), timeout]);
4815
+ }
4816
+ /** OGYGIA_PROFILER_DEBUG: the process's memory at a step of a recording (what the profiler itself
4817
+ * holds on a small serverless instance) */
4818
+ function mem_mark(step) {
4819
+ if (!process.env.OGYGIA_PROFILER_DEBUG) return;
4820
+ const m = process.memoryUsage();
4821
+ const mb = (n) => Math.round(n / 1048576);
4822
+ console.error(`[ogygia/profiler] memory ${step} @${Math.round(performance.now())} ms: rss ${mb(m.rss)} heap ${mb(m.heapUsed)} external ${mb(m.external)} buffers ${mb(m.arrayBuffers)} MB`);
4823
+ }
4824
+ function round2(n) {
4825
+ return Math.round(n * 100) / 100;
4826
+ }
4827
+ /** What names a report: kept whatever `fields` asks for. */
4828
+ const REPORT_IDENTITY = [
4829
+ "schema",
4830
+ "version",
4831
+ "engine",
4832
+ "kind",
4833
+ "id",
4834
+ "created",
4835
+ "target",
4836
+ "links"
4837
+ ];
4838
+ /** `report` with only the top-level keys in `fields` (comma-separated) and its identity; whole when unset. */
4839
+ function pick_fields(report, fields) {
4840
+ if (!fields) return report;
4841
+ const want = /* @__PURE__ */ new Set([...REPORT_IDENTITY, ...fields.split(",").map((f) => f.trim()).filter(Boolean)]);
4842
+ const out = {};
4843
+ for (const k in report) if (want.has(k)) out[k] = report[k];
4844
+ return out;
4845
+ }
4846
+ /**
4847
+ * The module (src-relative, no extension: `lib/inferno/catalog`) a file imports `name` from — its
4848
+ * `import { name } from '$lib/…'` or `'./…'` — or the file itself when it declares it. Undefined for
4849
+ * a package import, or a name it cannot place. Plain text, no regex.
4850
+ */
4851
+ /**
4852
+ * Is the route in `dir` prerendered? Kit takes the NEAREST `export const prerender`: the page's own
4853
+ * files first (`+page.server`, `+page`), then each layout up the tree. `true` and `'auto'` prerender;
4854
+ * `false` (or none found) does not. String search over the files (no regex, no parse).
4855
+ */
4856
+ function route_prerendered(read, dir) {
4857
+ let at = dir;
4858
+ let first = true;
4859
+ while (at) {
4860
+ const names = [
4861
+ ...first ? [
4862
+ "+page.server.ts",
4863
+ "+page.server.js",
4864
+ "+page.ts",
4865
+ "+page.js"
4866
+ ] : [],
4867
+ "+layout.server.ts",
4868
+ "+layout.server.js",
4869
+ "+layout.ts",
4870
+ "+layout.js"
4871
+ ];
4872
+ for (const n of names) {
4873
+ const src = read(`${at}/${n}`);
4874
+ if (!src) continue;
4875
+ const k = src.indexOf("export const prerender");
4876
+ if (k === -1) continue;
4877
+ const eq = src.indexOf("=", k);
4878
+ const v = eq === -1 ? "" : src.slice(eq + 1, eq + 12).trim();
4879
+ return v.startsWith("true") || v.startsWith("'auto'") || v.startsWith("\"auto\"");
4880
+ }
4881
+ first = false;
4882
+ const up = at.lastIndexOf("/");
4883
+ if (up === -1) break;
4884
+ at = at.slice(0, up);
4885
+ }
4886
+ return false;
4887
+ }
4888
+ /** An island entry as a path, however it was written (`./_app/x.js`, `/_app/x.js`, an absolute URL). */
4889
+ function entry_path(entry) {
4890
+ try {
4891
+ return new URL(entry, "http://x/").pathname;
4892
+ } catch {
4893
+ return entry;
4894
+ }
4895
+ }
4896
+ function imported_from(src, name, file) {
4897
+ const self = file.slice(0, file.lastIndexOf("."));
4898
+ for (const line of src.split("\n")) {
4899
+ const l = line.trim();
4900
+ if (!l.startsWith("import")) {
4901
+ if ((l.startsWith(`function ${name}(`) || l.startsWith(`export function ${name}(`) || l.startsWith(`async function ${name}(`) || l.startsWith(`export async function ${name}(`) || l.startsWith(`const ${name} =`)) && !l.startsWith("import")) return self;
4902
+ continue;
4903
+ }
4904
+ const open = l.indexOf("{");
4905
+ const close = l.indexOf("}");
4906
+ if (!(open !== -1 && close > open ? l.slice(open + 1, close).split(",").map((x) => x.trim().split(" as ").pop().trim()) : []).includes(name)) continue;
4907
+ const q = Math.max(l.lastIndexOf("'"), l.lastIndexOf("\""));
4908
+ const q0 = Math.max(l.lastIndexOf("'", q - 1), l.lastIndexOf("\"", q - 1));
4909
+ const spec = q0 !== -1 && q > q0 ? l.slice(q0 + 1, q) : "";
4910
+ const bare = (p) => p.endsWith(".ts") || p.endsWith(".js") ? p.slice(0, -3) : p;
4911
+ if (spec.startsWith("$lib/")) return bare(`lib/${spec.slice(5)}`);
4912
+ if (spec.startsWith(".")) {
4913
+ const parts = file.split("/").slice(0, -1);
4914
+ for (const seg of spec.split("/")) if (seg === "..") parts.pop();
4915
+ else if (seg !== ".") parts.push(seg);
4916
+ return bare(parts.join("/"));
4917
+ }
4918
+ return;
4919
+ }
4920
+ }
4921
+ export { pick_fields, profiler, route_prerendered, self_profile_to_cpuprofile };