@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,2592 @@
1
+ <script>/**
2
+ * The full SSR profile report. Static sections render server-side; the interactive widgets are
3
+ * `wake:'load'` islands (treemap, flame graph, sortable tables, export button). Ports report.ts's
4
+ * render_report — no HTML strings, no minified script blobs.
5
+ *
6
+ * THE ORDER IS THE POINT: what to fix first (the findings, the paths, the one render explained),
7
+ * then the stories that need reading (memory, the data, the islands and the browser, the
8
+ * network), then the raw tables everything above is an aggregate of. Nothing is left out; the
9
+ * raw numbers come last.
10
+ */
11
+ import { derive_findings, span_rows, cold_rows, page_score_of } from "../report.js";
12
+ import { fmt_ms, fmt_pct, fmt_bytes, label_of, CATEGORY_COLOR, CATEGORY_LABEL, ink_on } from "./format.js";
13
+ import { build_treemap, treemap_legend, budget_segments, waiting_rows, top_hosts, waterfall_rows, request_rows, island_rows, seed_rows, hole_rows, spark as build_spark } from "./report-data.js";
14
+ import IslandsTable from "./IslandsTable.svelte" with { wake: "load" };
15
+ import Paths from "./Paths.svelte" with { wake: "load" };
16
+ import Awaits from "./Awaits.svelte" with { wake: "load" };
17
+ import SeedExplainer from "./SeedExplainer.svelte" with { wake: "load" };
18
+ import Shell from "./Shell.svelte";
19
+ import ExportButton from "./ExportButton.svelte" with { wake: "load" };
20
+ // (awake on load: it sits in a closed <details>, and a tick made before a later wake is lost)
21
+ import WhatIf from "./WhatIf.svelte" with { wake: "load" };
22
+ import ScoreCard from "./ScoreCard.svelte";
23
+ import Treemap from "./Treemap.svelte" with { wake: "load" };
24
+ import Flame from "./Flame.svelte" with { wake: "load" };
25
+ import ComponentsTable from "./ComponentsTable.svelte" with { wake: "load" };
26
+ import FunctionsTable from "./FunctionsTable.svelte" with { wake: "load" };
27
+ import WaitingTable from "./WaitingTable.svelte" with { wake: "load" };
28
+ import Waterfall from "./Waterfall.svelte" with { wake: "load" };
29
+ import ShareLink from "./ShareLink.svelte" with { wake: "load" };
30
+ import Timeline from "./Timeline.svelte" with { wake: "load" };
31
+ import Scrub from "./Scrub.svelte" with { wake: "load" };
32
+ import RenderSteps from "./RenderSteps.svelte" with { wake: "load" };
33
+ import AllocStrip from "./AllocStrip.svelte";
34
+ import Ledger from "./Ledger.svelte";
35
+ import Patterns from "./Patterns.svelte";
36
+ import Drill from "./Drill.svelte";
37
+ import { unread_rows } from "../drill.js";
38
+ import { PATTERN_LABEL, fix_impact } from "../patterns.js";
39
+ import OnThisPage from "./OnThisPage.svelte" with { wake: "load" };
40
+ import OneClock from "./OneClock.svelte" with { wake: "load" };
41
+ import ByteStrip from "./ByteStrip.svelte" with { wake: "load" };
42
+ import DataRiver from "./DataRiver.svelte" with { wake: "load" };
43
+ import IslandHeatmap from "./IslandHeatmap.svelte" with { wake: "load" };
44
+ import DomTravel from "./DomTravel.svelte" with { wake: "load" };
45
+ import KeepLocal from "./KeepLocal.svelte" with { wake: "load" };
46
+ import DownloadHtml from "./DownloadHtml.svelte" with { wake: "load" };
47
+ import { OGP_ELEMENT_ID } from "./report-request.js";
48
+ import { island_rows_of, island_name } from "../report.js";
49
+ import { PHASE_LABEL } from "../timeline.js";
50
+ import { sync_io, memo_candidates, span_values } from "../insights.js";
51
+ import { render_steps } from "../steps.js";
52
+ import { row_href } from "./row-anchor.svelte.js";
53
+ // The report renderer, fed the fields directly — from the server (a logged-in `/report/[id]`) OR
54
+ // from a decrypted share-link `#fragment` (PermalinkGate). One body, two sources.
55
+ let { a, meta, base, extras, ogpB64, history = null, prev = null, since = null, dev = false, kept = false } = $props();
56
+ // the one element the export buttons read the page's .ogp bytes from (`\x2f` is `/`: a literal
57
+ // closing script tag would end this script block)
58
+ const ogp_element = (b64) => `<script type="application/octet-stream" id="${OGP_ELEMENT_ID}">${b64}<\x2fscript>`;
59
+ const fmt_kb = (b) => `${Math.round(b / 1024)} KB`;
60
+ // The ogygia page score — derived here from the same meta+extras the findings read, so the card
61
+ // and the findings never disagree (report.ts serializes the same value for agents).
62
+ const score = $derived(page_score_of(meta, extras));
63
+ // ogygia's own cost on the profiled page: the request with the largest side-channels
64
+ const og = $derived([...meta.requests].filter((r) => r.og).sort((x, y) => y.og.seed_bytes + y.og.tail_bytes - (x.og.seed_bytes + x.og.tail_bytes))[0]?.og ?? null);
65
+ // history: this page's medians over time (page mode), newest last
66
+ const hist = $derived(history && history.points.length >= 2 ? history.points : null);
67
+ const hist_geom = $derived.by(() => {
68
+ if (!hist) return null;
69
+ const w = 320, h = 48, pad = 4;
70
+ const max = Math.max(...hist.map((p) => p.median), 1);
71
+ const step = hist.length > 1 ? (w - 2 * pad) / (hist.length - 1) : 0;
72
+ const pts = hist.map((p, i) => [pad + i * step, h - pad - p.median / max * (h - 2 * pad)]);
73
+ return {
74
+ w,
75
+ h,
76
+ max,
77
+ pts,
78
+ poly: pts.map(([x, y]) => `${x},${y}`).join(" ")
79
+ };
80
+ });
81
+ const hist_prev = $derived(hist ? hist[hist.length - 2] : null);
82
+ const hist_last = $derived(hist ? hist[hist.length - 1] : null);
83
+ const hist_delta = $derived(hist_prev && hist_last && hist_prev.median > 0 ? (hist_last.median - hist_prev.median) / hist_prev.median * 100 : 0);
84
+ const busy_pct = $derived(a.duration_ms > 0 ? a.busy_ms / a.duration_ms * 100 : 0);
85
+ const net = $derived(extras.net.filter((c) => c.ms >= 0));
86
+ const net_total = $derived(net.reduce((s, c) => s + c.ms + (c.body_ms ?? 0), 0));
87
+ const findings = $derived(derive_findings(a, meta, extras));
88
+ // what fixing every slow pattern is worth, against the median render (page mode) or the window
89
+ const render_ms = $derived(meta.runs?.length ? [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)] : a.duration_ms);
90
+ // a CPU pattern's numbers cover every profiled render; a wait pattern's, one
91
+ const renders = $derived(meta.runs?.length || 1);
92
+ // the report's own forecast (overlapping fixes counted once, moot ones left out, deleted work in);
93
+ // an older report without one: the plain sum, waits capped at the render's waiting
94
+ const fc = $derived(extras.forecast);
95
+ const old = $derived(fc ? undefined : fix_impact(extras.patterns ?? [], render_ms, renders, a.timeline?.wait_ms));
96
+ const impact = $derived(fc ? {
97
+ render_ms: fc.now_ms,
98
+ after_ms: fc.after_ms,
99
+ pct: fc.now_ms > 0 ? Math.round((fc.now_ms - fc.after_ms) / fc.now_ms * 100) : 0,
100
+ cpu_ms: fc.cpu_ms,
101
+ wait_ms: fc.wait_ms,
102
+ delete_ms: fc.delete_ms
103
+ } : old ? {
104
+ ...old,
105
+ delete_ms: 0
106
+ } : undefined);
107
+ /** fixes whose saving another on the same lines already covers: counted once */
108
+ const covered = $derived((fc?.parts ?? []).filter((p) => p.with?.length));
109
+ const all_patterns = $derived((extras.patterns ?? []).map((p, i) => {
110
+ const s = p.sites[0];
111
+ // the caller that loops, else (a wait in a helper) the first line that called the helper
112
+ const at = s?.via?.find((v) => v.in_loop) ?? (p.wait ? s?.via?.[0] : undefined) ?? s;
113
+ const worth = p.kept_bytes ? `keeps ${fmt_bytes(p.kept_bytes)} a render${p.requests_left !== undefined ? `, heap full in ~${p.requests_left} requests` : ""}` : p.seed_bytes ? `ships ${fmt_bytes(p.seed_bytes)} a page view` : p.kind === "sync-io" && !p.cost_ms ? "blocks the server on every request (too quick to time here)" : p.wait ? p.kind === "same-document" || p.kind === "almost-same-document" ? `~${fmt_ms(p.save_ms)} ms: the whole render, served from a cache` : `~${fmt_ms(p.save_ms)} ms less waiting` : `~${fmt_ms(p.save_ms / renders)} ms less CPU a render`;
114
+ // memory and seed lead where they are (they are sorted first for a reason); time, by ms a render
115
+ // (a leak that crashes the server, 8 MB+ a render, and a whole shipped seed lead; a small kept
116
+ // amount waits behind the time savings — the same rule as the agent's plan)
117
+ const value = (p.kept_bytes ?? 0) >= 8 * 1048576 || p.seed_bytes ? Infinity : p.kept_bytes ? -1 : p.wait ? p.save_ms : p.save_ms / renders;
118
+ return {
119
+ i,
120
+ label: PATTERN_LABEL[p.kind],
121
+ where: at ? `${at.file}:${at.line}` : "",
122
+ worth,
123
+ href: `#pattern-${i}`,
124
+ value
125
+ };
126
+ }));
127
+ const from_patterns = $derived(all_patterns.slice(0, 3));
128
+ // WORK FOR DATA NOTHING READS (the drill-down's rows whose every key is unread): all of it goes,
129
+ // so it ranks with the patterns by its ms in one render
130
+ const unread = $derived(unread_rows(extras.drill));
131
+ const unread_ms = $derived(unread.reduce((s, r) => s + r.ms, 0));
132
+ // a pattern inside that work (the date parsing in a function that builds only an unread key) is
133
+ // moot: deleting it beats speeding it up
134
+ const gone_names = $derived(new Set(unread.map((r) => r.label)));
135
+ const gone_at = $derived(new Set(unread.flatMap((r) => r.at ? [r.at] : [])));
136
+ const moot = (i) => {
137
+ const p = typeof i === "number" ? extras.patterns?.[i] : undefined;
138
+ return !!p && p.sites.every((s) => gone_at.has(`${s.file}:${s.line}`) || (s.fn ? gone_names.has(s.fn.split(" ")[0]) : false));
139
+ };
140
+ // WITH A FORECAST: its order (each saving once, a covered fix folded into the one that covers it)
141
+ // and its running total after each step; memory and a shipped seed still lead
142
+ const from_forecast = $derived(fc?.parts.length ? [...all_patterns.filter((s) => s.value === Infinity || s.value === -1), ...fc.parts.flatMap((p, rank) => {
143
+ if (p.kind === "unread-work") {
144
+ const keys = p.title.slice(p.title.indexOf("(") + 1, -1);
145
+ // the place to edit: a LINE row that fills the key (the load's line), else the biggest row
146
+ const rows = unread.filter((r) => r.keys.join(", ") === keys);
147
+ const row = rows.find((r) => r.kind === "line" && r.at) ?? rows[0];
148
+ return [{
149
+ i: `unread-${rank}`,
150
+ label: "Work for data nothing reads",
151
+ where: row?.at ?? row?.label ?? "",
152
+ worth: `~${fmt_ms(p.ms)} ms a render (${keys})`,
153
+ href: "#drill",
154
+ value: 1e9 - rank,
155
+ ...p.after_ms !== undefined ? { after: p.after_ms } : {}
156
+ }];
157
+ }
158
+ const at = (extras.patterns ?? []).findIndex((x) => x.title === p.title);
159
+ if (at === -1) return [];
160
+ return [{
161
+ ...all_patterns[at],
162
+ value: 1e9 - rank,
163
+ ...p.after_ms !== undefined ? { after: p.after_ms } : {},
164
+ ...p.with?.length ? { also: p.with } : {}
165
+ }];
166
+ })] : undefined);
167
+ const start_here = $derived((from_forecast ?? [...from_patterns.filter((s) => unread_ms < 1 || !moot(s.i)), ...unread_ms >= 1 ? [{
168
+ i: "unread",
169
+ label: "Work for data nothing reads",
170
+ where: unread[0].at ?? unread[0].label,
171
+ worth: `~${fmt_ms(unread_ms)} ms a render (${[...new Set(unread.flatMap((r) => r.keys))].slice(0, 3).join(", ")})`,
172
+ href: "#drill",
173
+ value: unread_ms
174
+ }] : []]).sort((x, y) => y.value - x.value).slice(0, 3));
175
+ // group the findings by the area they touch, so "what to fix" reads as a few labelled clusters,
176
+ // not one wall — warnings float to the top of each cluster
177
+ const FCAT_ORDER = [
178
+ "Network",
179
+ "CPU",
180
+ "Memory",
181
+ "Data & seed",
182
+ "Islands & delivery",
183
+ "Overview"
184
+ ];
185
+ const fcat = (code) => {
186
+ const has = (...ks) => ks.some((k) => code.includes(k));
187
+ if (has("await", "network", "n-plus", "upstream", "self-fetch", "sequential")) return "Network";
188
+ if (has("gc", "alloc", "mem", "retain", "promise")) return "Memory";
189
+ if (has("seed", "key-", "props", "river", "data")) return "Data & seed";
190
+ if (has("island", "wake", "hole", "cold")) return "Islands & delivery";
191
+ if (has("cpu", "path", "component", "deopt", "memo", "span", "loop", "sync", "value")) return "CPU";
192
+ return "Overview";
193
+ };
194
+ const finding_groups = $derived(FCAT_ORDER.map((label) => ({
195
+ label,
196
+ items: findings.filter((f) => fcat(f.code) === label).sort((x, y) => x.severity === y.severity ? 0 : x.severity === "warn" ? -1 : 1)
197
+ })).filter((g) => g.items.length));
198
+ const warn_count = $derived(findings.filter((f) => f.severity === "warn").length);
199
+ // component memory join + counts
200
+ const allocByName = $derived.by(() => {
201
+ const m = new Map();
202
+ for (const h of extras.heap ?? []) m.set(h.name, (m.get(h.name) ?? 0) + h.self_bytes);
203
+ // a component's bytes: everything allocated under it, nested components excluded
204
+ for (const c of extras.heap_components ?? []) m.set(c.name, c.bytes);
205
+ return m;
206
+ });
207
+ const hasAlloc = $derived(allocByName.size > 0);
208
+ const hasCounts = $derived(a.components.some((c) => c.calls != null) || a.functions.some((f) => f.calls != null));
209
+ // PER CALL: the time adds up every render, the call count is ONE render's (the coverage render),
210
+ // so one call is the time per render over the calls in a render
211
+ const withExtras = (f) => {
212
+ const count = f.calls ?? 0;
213
+ return {
214
+ ...f,
215
+ per: f.total_ms / renders / (count > 0 ? count : 1),
216
+ count,
217
+ alloc: allocByName.get(f.name) ?? null
218
+ };
219
+ };
220
+ const compRows = $derived(a.components.map(withExtras));
221
+ const compMaxTotal = $derived(Math.max(...a.components.map((c) => c.total_ms), 1));
222
+ const stats = $derived.by(() => {
223
+ const out = [
224
+ {
225
+ value: fmt_ms(meta.duration_ms) + " ms",
226
+ label: "window"
227
+ },
228
+ {
229
+ value: busy_pct.toFixed(0) + "%",
230
+ label: "CPU busy"
231
+ },
232
+ {
233
+ value: net.length ? fmt_ms(net_total) + " ms" : "0",
234
+ label: `network (${net.length} calls)`
235
+ },
236
+ (
237
+ // per render, the unit of the render time and the lines (the window holds every run)
238
+ {
239
+ value: fmt_ms((extras.gc_attr ? extras.gc_attr.summary.total_ms : a.gc_ms) / renders) + " ms",
240
+ label: renders > 1 ? "garbage collection per render" : "garbage collection"
241
+ }),
242
+ {
243
+ value: String(meta.requests.length),
244
+ label: "requests in window"
245
+ }
246
+ ];
247
+ if (extras.gc) out.push({
248
+ value: fmt_ms(extras.gc.max_ms) + " ms",
249
+ label: `GC pause max (${extras.gc.count})`
250
+ });
251
+ if (meta.loop_delay) out.push({
252
+ value: fmt_ms(meta.loop_delay.p99) + " ms",
253
+ label: "loop delay p99"
254
+ });
255
+ if (meta.elu_percent !== undefined) out.push({
256
+ value: meta.elu_percent.toFixed(0) + "%",
257
+ label: "event loop use"
258
+ });
259
+ if (meta.rss_mb !== undefined) out.push({
260
+ value: meta.rss_mb + " MB",
261
+ label: "memory (rss)"
262
+ });
263
+ out.push({
264
+ value: String(a.sample_count),
265
+ label: "samples"
266
+ });
267
+ if (meta.overhead && meta.overhead.cpu_ms + meta.overhead.gc_ms > 0) out.push({
268
+ value: `−${fmt_ms(meta.overhead.cpu_ms + meta.overhead.gc_ms)} ms`,
269
+ label: `profiler's own cost, taken out${meta.overhead.per_run_ms ? ` (${fmt_ms(meta.overhead.per_run_ms)} ms per render)` : ""}`
270
+ });
271
+ return out;
272
+ });
273
+ const runsMedian = $derived(meta.trigger === "page" && meta.runs?.length ? [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)] : 0);
274
+ const budget = $derived(budget_segments(a));
275
+ const tree = $derived(build_treemap(a));
276
+ const legend = $derived(treemap_legend(a));
277
+ const hosts = $derived(top_hosts(net).slice(0, 12));
278
+ const netSorted = $derived([...net].sort((x, y) => y.ms + (y.body_ms ?? 0) - (x.ms + (x.body_ms ?? 0))).slice(0, 60));
279
+ const wf = $derived(waterfall_rows(net));
280
+ const waiting = $derived(waiting_rows(net, extras.io ?? []));
281
+ const spans = $derived(span_rows(extras.spans));
282
+ const span_runs = $derived(meta.trigger === "page" ? Math.max(meta.runs?.length ?? 1, 1) : 1);
283
+ const tagged = $derived(meta.requests.filter((r) => r.tags && Object.keys(r.tags).length));
284
+ const waitMax = $derived(waiting[0]?.ms || 1);
285
+ const spark = $derived(build_spark(extras.mem));
286
+ const measures = $derived(extras.measures ?? []);
287
+ const heap = $derived(extras.heap ?? []);
288
+ const files = $derived(a.files.filter((f) => f.category !== "idle").slice(0, 25));
289
+ const buckets = $derived(a.buckets.filter((b) => b.category !== "idle").slice(0, 15));
290
+ const maxBucket = $derived(Math.max(...a.buckets.map((b) => b.self_ms), 1));
291
+ // the ogygia side of the page: islands (server + build + browser joined), the seed, the holes
292
+ const islands = $derived(island_rows(a, meta, extras));
293
+ const hasJs = $derived(islands.some((r) => r.js_bytes !== null));
294
+ const hasClient = $derived(islands.some((r) => !!r.client));
295
+ const flagged = $derived(islands.filter((r) => r.advice).length);
296
+ const seed = $derived(seed_rows(meta));
297
+ const holes = $derived(hole_rows(meta));
298
+ const vitals = $derived(extras.vitals ?? null);
299
+ const client_marks = $derived(extras.client_marks ?? []);
300
+ // every island fingerprint on the page → its component name (the browser's pictures are keyed by fingerprint)
301
+ const fp_names = $derived.by(() => {
302
+ const out = {};
303
+ for (const r of island_rows_of(meta)) out[r.fp] = island_name(r);
304
+ return out;
305
+ });
306
+ // each hole id → its component (and props, for a single copy): One clock's hole lanes
307
+ const hole_names = $derived.by(() => {
308
+ const out = {};
309
+ for (const h of holes) if (h.name) out[h.id] = h.props && h.count <= 1 ? `${h.name} ${h.props}` : h.name;
310
+ return out;
311
+ });
312
+ const visit = $derived(extras.visit ?? null);
313
+ const server_clock = $derived(a.timeline ? {
314
+ window_ms: a.timeline.window_ms,
315
+ phases: a.timeline.phases.map((p) => ({
316
+ phase: p.phase,
317
+ label: PHASE_LABEL[p.phase] ?? p.phase,
318
+ cpu_ms: p.cpu_ms,
319
+ wait_ms: p.wait_ms
320
+ }))
321
+ } : null);
322
+ const page_path = $derived(meta.page ?? meta.request?.path ?? null);
323
+ // WHO CAUSED THE GC: the pauses on the timeline's clock (the window starts `window_offset_ms` into the capture)
324
+ const gc_attr = $derived(extras.gc_attr ?? null);
325
+ const gc_ticks = $derived(gc_attr ? gc_attr.pauses.map((p) => ({
326
+ t: p.t - (gc_attr.window_offset_ms ?? 0),
327
+ ms: p.ms,
328
+ kind: p.kind,
329
+ why: p.why,
330
+ top: p.top.slice(0, 3).map((x) => `${x.name}${x.component && x.component !== x.name ? ` in ${x.component}` : ""} ${Math.round(x.share * 100)}%`).join(", ")
331
+ })) : []);
332
+ // the pauses on the capture's clock, for the heap chart (it spans every run)
333
+ const gc_on_capture = $derived(gc_attr ? gc_attr.pauses.map((p) => ({
334
+ t: p.t,
335
+ ms: p.ms,
336
+ kind: p.kind
337
+ })) : []);
338
+ // the scrubber's marks: the pauses inside the window, the few longest
339
+ const scrub_marks = $derived(gc_ticks.filter((g) => g.t >= 0 && (a.timeline ? g.t <= a.timeline.window_ms : true)).sort((x, y) => y.ms - x.ms).slice(0, 8).map((g) => ({
340
+ t: g.t,
341
+ label: `GC ${fmt_ms(g.ms)}`
342
+ })));
343
+ const fmt_mb = (b) => `${Math.round(b / 1048576 * 10) / 10} MB`;
344
+ // more from the same snapshot: deopts, sync I/O, promises, memo candidates, what a render keeps
345
+ const deopt_rows = $derived(a.deopts ?? []);
346
+ const sync_rows = $derived(sync_io(a));
347
+ const memo_rows = $derived(memo_candidates(a, gc_attr?.makers ?? []));
348
+ // THE FUNCTIONS TABLE: the 80 with the most self time, plus every function another part of the
349
+ // report links to (`#fn=<key>`). A slow pattern's line, a path's middle frame or a leak's maker
350
+ // often has little self time; without its row the link opened nothing (43 of 67 on a heavy page).
351
+ const fn_key = (f) => f.key ?? f.name + " " + f.url;
352
+ const linked = $derived.by(() => {
353
+ const out = new Set();
354
+ for (const f of findings) if (f.anchor?.startsWith("fn:")) out.add(f.anchor.slice(3));
355
+ for (const l of extras.ledger ?? []) if (l.fn) out.add(l.fn);
356
+ for (const p of extras.patterns ?? []) for (const s of p.sites) {
357
+ if (s.fn) out.add(s.fn);
358
+ for (const v of s.via ?? []) if (v.fn) out.add(v.fn);
359
+ }
360
+ const walk_path = (n) => {
361
+ if (n.key && n.category !== "component") out.add(n.key);
362
+ for (const c of n.children) walk_path(c);
363
+ };
364
+ for (const g of a.paths ?? []) walk_path(g.tree);
365
+ for (const s of sync_rows) out.add(s.key);
366
+ for (const m of memo_rows) out.add(m.key);
367
+ for (const d of deopt_rows) out.add(d.key);
368
+ return out;
369
+ });
370
+ const fnRows = $derived([...a.functions.slice(0, 80), ...a.functions.slice(80).filter((f) => linked.has(fn_key(f)))].map(withExtras));
371
+ // the rows a `#fn=` link can reach: a key outside them gets no link (a library frame with no
372
+ // time of its own sits on a path but has no row)
373
+ const fn_keys = $derived(fnRows.map(fn_key));
374
+ const fn_known = $derived(new Set(fn_keys));
375
+ const anchor_ok = (anchor) => !anchor.startsWith("fn:") || fn_known.has(anchor.slice(3));
376
+ const promises = $derived(extras.promises ?? null);
377
+ const promises_per_render = $derived(promises ? Math.round(promises.count / Math.max(meta.runs?.length ?? 1, 1)) : 0);
378
+ const retained = $derived(extras.retained ?? null);
379
+ // the render step by step, when the heap grew, the instance's other requests, the data's lineage, the spans' values
380
+ const steps = $derived(a.timeline ? render_steps(a.timeline, a.stacks) ?? null : null);
381
+ const alloc = $derived(extras.alloc ?? null);
382
+ const contention = $derived(extras.contention ?? null);
383
+ const contention_others = $derived(contention ? contention.requests.filter((r) => r.kind === "other").length : 0);
384
+ const lineage = $derived(extras.lineage ?? null);
385
+ const lineage_keys = $derived(lineage ? lineage.keys.slice(0, 40) : []);
386
+ const value_rows = $derived(span_values(extras.spans));
387
+ const cold = $derived(cold_rows(a, meta));
388
+ const holes_seen = $derived(holes.some((h) => h.requests > 0));
389
+ // page mode: N runs of one page → fold identical requests into one row with the count + medians
390
+ const reqs = $derived(request_rows(meta.requests, meta.trigger === "page").slice(0, 60));
391
+ const folded = $derived(meta.trigger === "page" && reqs.some((r) => r.count > 1));
392
+ const title = $derived(`SSR profile — ${label_of(meta)}`);
393
+ const VERDICT_LABEL = {
394
+ island: "read by an island",
395
+ server: "server only, not shipped",
396
+ "server-only": "server only, but shipped",
397
+ unread: "nobody reads it",
398
+ unknown: "unknown"
399
+ };
400
+ const contention_self = $derived(contention ? contention.requests.filter((r) => r.kind === "self").length : 0);
401
+ </script>
402
+
403
+ <svelte:head><title>{title}</title></svelte:head>
404
+
405
+ <Shell {base} current={meta.id} toc>
406
+ <OnThisPage />
407
+
408
+ <h1>
409
+ SSR profile
410
+ <small
411
+ >{label_of(meta)} · {new Date(meta.created).toLocaleString()} · node {meta.node}{a.sourcemapped
412
+ ? ' · sourcemapped'
413
+ : ''}</small
414
+ >
415
+ </h1>
416
+ <div class="actions">
417
+ {#if meta.trigger === 'page' && meta.page}
418
+ <!-- fix the code, then this: the same page profiled again, opened as a comparison with this report -->
419
+ <a
420
+ class="btn primary"
421
+ href="{base}/run?p={encodeURIComponent(meta.page)}&runs={meta.runs?.length ||
422
+ 3}&against={meta.id}"
423
+ title="Profile {meta.page} again and compare it with this report: which patterns and lines your change fixed"
424
+ >Profile again &amp; compare</a
425
+ >
426
+ {/if}
427
+ <!-- the buttons read the page's .ogp bytes from the one element below when clicked
428
+ (report-request.ts page_ogp), never as a prop each: five copies were 1.2 MB -->
429
+ {#if ogpB64 || kept}<ExportButton id={meta.id} {base} />{/if}
430
+ {#if ogpB64 || kept}<ShareLink id={meta.id} {base} />{/if}
431
+ <KeepLocal {base} id={meta.id} />
432
+ <DownloadHtml {base} id={meta.id} />
433
+ <a class="btn" href="{base}/view">Import<span class="sub">.ogp</span></a>
434
+ </div>
435
+ {#if ogpB64}{@html ogp_element(ogpB64)}{/if}
436
+ <p class="hint">
437
+ <a href={base}>← dashboard</a> ·
438
+ <DownloadHtml {base} id={meta.id} as="json" link /> (agents) ·
439
+ <a href="{base}/report/{meta.id}/raw" download="ogygia-profile-{meta.id}.cpuprofile"
440
+ >.cpuprofile</a
441
+ >
442
+ (DevTools / speedscope) · Export is an encrypted <code>.ogp</code> — re-open it with Import (needs
443
+ this profiler's key)
444
+ </p>
445
+
446
+ <ScoreCard {score} />
447
+
448
+ <div class="summary">
449
+ {#each stats as st}<div class="stat"><b>{st.value}</b><span>{st.label}</span></div>{/each}
450
+ </div>
451
+
452
+ <!-- ═══════════════ 1 · WHAT TO FIX ═══════════════ -->
453
+ <div class="part"><span class="part-n">1</span><span class="part-t">What to fix</span></div>
454
+
455
+ {#if since}
456
+ <!-- SINCE THE LAST PROFILE of this page: did the change pay, and which rows moved -->
457
+ <section class="since">
458
+ {#if since.score && since.score.a !== since.score.b}
459
+ {@const ds = since.score.b - since.score.a}
460
+ <p data-since-score>
461
+ <b class={ds < 0 ? 'warn' : 'good'}>Score {since.score.a} → {since.score.b}</b> ({since
462
+ .score.a_grade} → {since.score.b_grade}){#if since.score.moved.length}: {since.score.moved
463
+ .map((m) => `${m.label} ${m.a} → ${m.b}`)
464
+ .join(', ')}{/if}.
465
+ </p>
466
+ {/if}
467
+ {#if since.browser}
468
+ <!-- the browser's findings, each report read with its own visit -->
469
+ <p data-since-browser>
470
+ <b>In the browser</b>{#if since.browser.fixed.length}: <span class="good"
471
+ >fixed {since.browser.fixed.join(' · ')}</span
472
+ >{/if}{#if since.browser.added.length}{since.browser.fixed.length ? ';' : ':'}
473
+ <span class="warn">new {since.browser.added.join(' · ')}</span>{/if}.
474
+ </p>
475
+ {/if}
476
+ {#if since.vitals}
477
+ <!-- the vitals each report's own visit measured, and the part of each that moved most -->
478
+ {@const label = (k) => (k === 'cls' ? 'CLS' : k.toUpperCase())}
479
+ {@const val = (k, v) => (k === 'cls' ? String(v) : `${fmt_ms(v)} ms`)}
480
+ <p data-since-vitals>
481
+ <b class={since.vitals.some((v) => v.b > v.a) ? 'warn' : 'good'}>Vitals</b>: {since.vitals
482
+ .map(
483
+ (v) =>
484
+ `${label(v.key)} ${val(v.key, v.a)} → ${val(v.key, v.b)}${v.part ? ` (${v.part.label} ${fmt_ms(v.part.a)} → ${fmt_ms(v.part.b)} ms)` : ''}`
485
+ )
486
+ .join('; ')}.
487
+ </p>
488
+ {/if}
489
+ {#if since.islands}
490
+ {@const isl = since.islands}
491
+ <!-- what the deploy costs a returning visitor: each island keeps its file (its cache still
492
+ serves it) or moves to a new one (downloaded again) -->
493
+ <p data-since-islands>
494
+ <b class={isl.moved.length ? 'warn' : 'good'}
495
+ >Island files: {isl.moved.length} changed, {isl.kept} kept</b
496
+ >{#if isl.moved.length}{' '}— a returning visitor downloads {isl.bytes
497
+ ? fmt_bytes(isl.bytes) + ' '
498
+ : ''}again for {isl.moved
499
+ .slice(0, 6)
500
+ .map((m) => m.name)
501
+ .join(', ')}{isl.moved.length > 6
502
+ ? ` and ${isl.moved.length - 6} more`
503
+ : ''}{/if}.{#if isl.all_moved}{' '}Every island changed: if their code did not, the
504
+ build's names are not stable. SvelteKit bakes its version (the build time, by default)
505
+ into its client code — set <code>kit.version.name</code> to your commit.{/if}
506
+ </p>
507
+ {/if}
508
+ {#if since.assets}
509
+ {@const d = since.assets}
510
+ {@const line = (c) =>
511
+ `${c.name}${c.contains ? ` (${c.contains.join(', ')})` : ''}`}
512
+ <!-- the files behind a JS move: known by what they hold (hashed names change every build) -->
513
+ <p data-since-assets>
514
+ <b class={d.js.b > d.js.a ? 'warn' : 'good'}
515
+ >JS at start {fmt_bytes(d.js.a)} → {fmt_bytes(d.js.b)}</b
516
+ >{d.lazy_js.a !== d.lazy_js.b
517
+ ? ` (loads later ${fmt_bytes(d.lazy_js.a)} → ${fmt_bytes(d.lazy_js.b)})`
518
+ : ''}{#if d.added.length}; new: {d.added
519
+ .map((c) => `${line(c)} ${fmt_bytes(c.b)}${c.lazy ? ' later' : ''}`)
520
+ .join('; ')}{/if}{#if d.grew.length}; grew: {d.grew
521
+ .map((c) => `${line(c)} ${fmt_bytes(c.a)} → ${fmt_bytes(c.b)}`)
522
+ .join('; ')}{/if}{#if d.shrank.length}; shrank: {d.shrank
523
+ .map((c) => `${line(c)} ${fmt_bytes(c.a)} → ${fmt_bytes(c.b)}`)
524
+ .join('; ')}{/if}{#if d.removed.length}; gone: {d.removed
525
+ .map((c) => `${line(c)} ${fmt_bytes(c.a)}`)
526
+ .join('; ')}{/if}{#if d.to_start.length}; now at start (it loaded later before — an
527
+ island's wake changed?): {d.to_start
528
+ .map((c) => `${line(c)} ${fmt_bytes(c.b)}`)
529
+ .join('; ')}{/if}{#if d.to_later.length}; now loads later: {d.to_later
530
+ .map((c) => `${line(c)} ${fmt_bytes(c.b)}`)
531
+ .join('; ')}{/if}.
532
+ </p>
533
+ {/if}
534
+ {#if since.order}
535
+ <p>
536
+ <b>Run order changes this comparison:</b> both ran on one server after a render that kept {since
537
+ .order.kept_mb} MB alive each time, so the later one ran on a fuller heap. Restart between
538
+ profiles to compare.
539
+ </p>
540
+ {:else if since.same}
541
+ <p>
542
+ <b>No real change since your last profile</b
543
+ >{#if since.a_ms !== undefined && since.b_ms !== undefined}&nbsp;({fmt_ms(since.a_ms)} → {fmt_ms(
544
+ since.b_ms
545
+ )} ms, within the runs' own spread){/if}.
546
+ </p>
547
+ {:else if since.a_ms !== undefined && since.b_ms !== undefined}
548
+ {@const d = since.b_ms - since.a_ms}
549
+ <p>
550
+ <b class={d > 0 ? 'warn' : 'good'}>{d > 0 ? '+' : '−'}{fmt_ms(Math.abs(d))} ms</b> since
551
+ your last profile ({fmt_ms(since.a_ms)} → {fmt_ms(since.b_ms)} ms){#if since.moved.length},
552
+ mostly:{:else}.{/if}
553
+ </p>
554
+ {/if}
555
+ {#if !since.order && !since.same && since.moved.length}
556
+ <ul>
557
+ {#each since.moved as r, i (r.path.join('\0') + i)}
558
+ <li>
559
+ <span class="status {r.status}">{r.status}</span>
560
+ {r.path[r.path.length - 1]}{#if r.at && r.kind !== 'line'}
561
+ <code>{r.at}</code>{/if}
562
+ <b class={r.d_ms > 0 ? 'warn' : 'good'}
563
+ >{r.d_ms > 0 ? '+' : '−'}{fmt_ms(Math.abs(r.d_ms))} ms</b
564
+ >
565
+ </li>
566
+ {/each}
567
+ </ul>
568
+ {/if}
569
+ {#if since.fix_check && !since.same && !since.order}<p class="hint">
570
+ The fixed patterns promised ~{fmt_ms(since.fix_check.predicted_ms)} ms; the render moved {fmt_ms(
571
+ since.fix_check.measured_ms
572
+ )} ms.
573
+ </p>{/if}
574
+ <a class="btn" href="{base}/compare/{since.prev}/{meta.id}">Compare in full</a>
575
+ </section>
576
+ {/if}
577
+
578
+ {#if start_here.length}
579
+ <!-- START HERE: the three biggest fixes, each with the line to open and what it is worth -->
580
+ <ol class="start">
581
+ {#each start_here as s (s.i)}
582
+ <li>
583
+ <a href={s.href}><b>{s.label}</b></a>
584
+ {#if s.where}<code>{s.where}</code>{/if}
585
+ <span class="worth">{s.worth}</span>
586
+ {#if s.after !== undefined}<span
587
+ class="then"
588
+ title="one render once this and every step above it are done"
589
+ >→ ~{fmt_ms(s.after)} ms</span
590
+ >{/if}
591
+ {#if s.also?.length}<span class="hint">also covers {s.also.join('; ')}</span>{/if}
592
+ </li>
593
+ {/each}
594
+ </ol>
595
+ {/if}
596
+
597
+ {#if warn_count}<p class="findings-lead">
598
+ <b>{warn_count}</b> thing{warn_count === 1 ? '' : 's'} worth fixing, grouped by area. The rest are
599
+ context.
600
+ </p>{/if}
601
+ <div class="fgrid">
602
+ {#each finding_groups as g (g.label)}
603
+ <section class="fgroup">
604
+ <h3 class="fgroup-h">{g.label} <span class="fgroup-n">{g.items.length}</span></h3>
605
+ {#each g.items as f, i (f.code + i)}
606
+ <div class="finding" class:warn={f.severity === 'warn'}>
607
+ <p class="fmsg">{f.message}</p>
608
+ {#if f.fix}<p class="ffix">{f.fix}</p>{/if}
609
+ {#if f.pattern !== undefined && extras.patterns?.[f.pattern]}
610
+ <a class="fshow fpat" href="#pattern-{f.pattern}"
611
+ >same problem as “{PATTERN_LABEL[extras.patterns[f.pattern].kind]}” ↓</a
612
+ >
613
+ {/if}
614
+ {#if f.anchor && anchor_ok(f.anchor)}<a class="fshow" href={row_href(f.anchor)}
615
+ >show the row ↓</a
616
+ >{/if}
617
+ {#if f.file && !f.anchor}<span class="ffile"
618
+ >{f.file}{#if f.line}:{f.line}{/if}</span
619
+ >{/if}
620
+ </div>
621
+ {/each}
622
+ </section>
623
+ {/each}
624
+ </div>
625
+
626
+ {#if extras.drill}
627
+ <section class="panel" id="drill">
628
+ <h2>
629
+ Where one render went <span class="hint" style="font-weight:400">(down to the line)</span>
630
+ </h2>
631
+ <Drill root={extras.drill} />
632
+ </section>
633
+ {/if}
634
+
635
+ {#if extras.patterns?.length}
636
+ <section class="panel">
637
+ <h2>Slow patterns</h2>
638
+ {#if impact && impact.render_ms > impact.after_ms}
639
+ <p class="impact">
640
+ Fixing all of these takes about <b>{fmt_ms(impact.render_ms - impact.after_ms)} ms</b> off
641
+ the {fmt_ms(impact.render_ms)} ms render (<b>{impact.pct}%</b>): about {fmt_ms(
642
+ impact.after_ms
643
+ )} ms after.
644
+ <span class="hint">
645
+ {[
646
+ impact.cpu_ms ? `${fmt_ms(impact.cpu_ms)} ms of CPU` : '',
647
+ impact.wait_ms ? `${fmt_ms(impact.wait_ms)} ms of waiting` : '',
648
+ impact.delete_ms
649
+ ? `${fmt_ms(impact.delete_ms)} ms of work for data nothing reads`
650
+ : ''
651
+ ]
652
+ .filter(Boolean)
653
+ .join(', ')} per render.
654
+ {#if covered.length}
655
+ Fixes on the same lines count once: {covered
656
+ .map((p) => `“${p.title}” covers ${p.with.map((w) => `“${w}”`).join(', ')}`)
657
+ .join('; ')}.
658
+ {/if}
659
+ {#if fc?.answers}Keeping the services’ answers between renders too (if they may be a
660
+ little stale): about <b>{fmt_ms(fc.answers.after_ms)} ms</b>{#if fc.answers.measured},
661
+ measured: the page rendered in {fmt_ms(fc.answers.measured.ms)} ms with those answers
662
+ served from memory{/if}.{/if}
663
+ {#if fc?.cpu_now_ms && fc.cpu_after_ms && fc.cpu_after_ms < fc.cpu_now_ms * 0.9}
664
+ {@const lambda_gbs = (ms) =>
665
+ (((ms / 1000) * (meta.lambda_mb ?? 0)) / 1024) * 1e6}
666
+ Capacity: the main thread spends {fmt_ms(fc.cpu_now_ms)} ms of CPU a render, about {fmt_ms(
667
+ fc.cpu_after_ms
668
+ )} ms after the CPU fixes, so one core serves about
669
+ <b>{Math.round(1000 / fc.cpu_now_ms)} → {Math.round(1000 / fc.cpu_after_ms)}</b>
670
+ renders a second (the waiting fixes cut how long a visitor waits, not this).
671
+ {#if meta.lambda_mb}On Lambda at {meta.lambda_mb} MB, billed for the whole render, waits
672
+ included: about {Math.round(lambda_gbs(impact.render_ms)).toLocaleString('en')} → {Math.round(
673
+ lambda_gbs(impact.after_ms)
674
+ ).toLocaleString('en')} GB-seconds per million renders.{/if}
675
+ {/if}
676
+ {#if fc?.cache}A cached copy of the whole document is a separate lever, not in this
677
+ number.{/if}
678
+ {#if fc?.clamped}The savings claimed more waiting or CPU than the render had (they
679
+ overlap more than the lines show): each is held at what the render used.{/if}
680
+ {#if fc?.partial}Built without sourcemaps: the profiler saw fewer lines, so fewer fixes;
681
+ the real time after them is likely lower.{/if}
682
+ </span>
683
+ </p>
684
+ {/if}
685
+ {#if fc?.parts.length}
686
+ <details class="whatif-box">
687
+ <summary>Pick the fixes you will make</summary>
688
+ <WhatIf parts={fc.parts} now_ms={fc.now_ms} caps={fc.caps} />
689
+ </details>
690
+ {/if}
691
+ <p class="hint">
692
+ Known slow habits spotted on your costliest lines, and the numbers that back each one.
693
+ Biggest win first.
694
+ </p>
695
+ <Patterns patterns={extras.patterns} busy_ms={a.busy_ms} {renders} fns={fn_keys} />
696
+ </section>
697
+ {/if}
698
+
699
+ {#if extras.ledger?.length}
700
+ <section class="panel">
701
+ <h2>The exact lines</h2>
702
+ <p class="hint">
703
+ Your own lines that cost the most, with every cost on the line{renders > 1
704
+ ? `, per render (the average of ${renders})`
705
+ : ''}. Click a place to open its function.
706
+ </p>
707
+ <Ledger lines={extras.ledger} patterns={extras.patterns ?? []} {renders} fns={fn_keys} />
708
+ </section>
709
+ {/if}
710
+
711
+ <div class="grid">
712
+ <section class="panel">
713
+ <h2>Where the time went</h2>
714
+ <p class="hint">
715
+ The whole window, wall-clock. If the biggest segment is "idle / waiting", the server was
716
+ blocked on I/O, not computing — look at Network below. If it's "your code", the treemap and
717
+ Components table show exactly where.
718
+ </p>
719
+ <div class="budget">
720
+ {#each budget as s}
721
+ <div
722
+ style="width:{s.pct}%;background:{CATEGORY_COLOR[s.cat]};color:{ink_on(
723
+ CATEGORY_COLOR[s.cat]
724
+ )}"
725
+ title="{s.label} — {fmt_ms(s.ms / renders)} ms{renders > 1
726
+ ? ' per render'
727
+ : ''} ({s.pct.toFixed(1)}%)"
728
+ >
729
+ {s.pct > 8 ? `${s.label} ${s.pct.toFixed(0)}%` : ''}
730
+ </div>
731
+ {/each}
732
+ </div>
733
+ {#if busy_pct < 25}
734
+ <p class="hint" style="color:var(--warn)">
735
+ This window barely used the CPU ({busy_pct.toFixed(0)}% busy) — the bottleneck is
736
+ <b>waiting</b>, not computing. Look at "Waiting by function" below; the treemap is just
737
+ the small slice of real CPU work.
738
+ </p>
739
+ {/if}
740
+ </section>
741
+ {#if hist && hist_geom && hist_last && hist_prev}
742
+ <section class="panel">
743
+ <h2>History of {meta.page}</h2>
744
+ <div class="history">
745
+ <svg width={hist_geom.w} height={hist_geom.h} viewBox="0 0 {hist_geom.w} {hist_geom.h}">
746
+ <polyline points={hist_geom.poly} fill="none" stroke="#6ca8e0" stroke-width="1.5" />
747
+ {#each hist_geom.pts as [x, y], i (i)}
748
+ <circle cx={x} cy={y} r="3" fill={hist[i].id === meta.id ? '#e0834a' : '#6ca8e0'}>
749
+ <title
750
+ >{fmt_ms(hist[i].median)} ms · {new Date(hist[i].created).toLocaleString()}</title
751
+ >
752
+ </circle>
753
+ {/each}
754
+ </svg>
755
+ <div>
756
+ <p class="hint">
757
+ {hist.map((p) => fmt_ms(p.median) + ' ms').join(' → ')} (median render, oldest first).
758
+ {#if Math.abs(hist_delta) >= 5}
759
+ <b class={hist_delta > 0 ? 'warn' : 'good'}
760
+ >{hist_delta > 0 ? '+' : ''}{hist_delta.toFixed(0)}%</b
761
+ > against the previous run.
762
+ {/if}
763
+ </p>
764
+ {#if prev}<a class="btn" href="{base}/compare/{prev}/{meta.id}"
765
+ >Compare with the previous run</a
766
+ >{/if}
767
+ </div>
768
+ </div>
769
+ </section>
770
+ {/if}
771
+ </div>
772
+
773
+ {#if a.paths?.length}
774
+ <section class="panel">
775
+ <h2>
776
+ Paths to fix <span class="hint" style="font-weight:400"
777
+ >({a.paths.length}: hot functions that share one caller)</span
778
+ >
779
+ </h2>
780
+ <p class="hint">
781
+ The hot-functions table ranks them one by one, but several often sit on <b>one call path</b
782
+ >: fix the caller they share and all of them go. Each graph reads left to right: the caller
783
+ to fix, the chain of calls, the hot functions (lit) at the end. Link width is the time that
784
+ flowed through; click a node for its row.
785
+ </p>
786
+ <Paths paths={a.paths} busy={a.busy_ms} fns={fn_keys} />
787
+ </section>
788
+ {/if}
789
+
790
+ <!-- ═══════════════ 2 · WHY IT TOOK THIS LONG ═══════════════ -->
791
+ {#if a.timeline}
792
+ <div class="part">
793
+ <span class="part-n">2</span><span class="part-t">Why it took this long</span>
794
+ </div>
795
+ <section class="panel">
796
+ <h2>Why this render took {fmt_ms(a.timeline.window_ms)} ms</h2>
797
+ <p class="hint">
798
+ One render, left to right, start to end. Each block is a stretch of time: solid means the
799
+ CPU was running the thing named on it, striped means the server was waiting for a call to
800
+ come back. The numbered steps below are the same blocks in order — the biggest is the one to
801
+ fix first.
802
+ </p>
803
+ <Timeline t={a.timeline} gc={gc_ticks} />
804
+ {#if a.timeline.awaits && a.timeline.awaits.nodes.length >= 2}
805
+ <h3 class="sub-h">
806
+ Which call waited for which <span class="hint"
807
+ >the render's calls on one clock, arrows where one started only after another finished</span
808
+ >
809
+ </h3>
810
+ <Awaits
811
+ nodes={a.timeline.awaits.nodes}
812
+ edges={a.timeline.awaits.edges}
813
+ window_ms={a.timeline.window_ms}
814
+ />
815
+ {/if}
816
+ {#if a.stacks}
817
+ <h3 class="sub-h">
818
+ Slice it <span class="hint"
819
+ >the render's CPU samples themselves — drag a range for what ran there, hover for the
820
+ stack at that instant</span
821
+ >
822
+ </h3>
823
+ <p class="hint">
824
+ Every table in this report is an aggregate over the whole render. This is the same data
825
+ before aggregation:
826
+ {a.stacks.raw.toLocaleString()} samples, each with its stack. Slice the busy stretch on the
827
+ strip and the tables answer for that stretch alone; the yellow marks are the collections.
828
+ </p>
829
+ <Scrub stacks={a.stacks} marks={scrub_marks} />
830
+ {/if}
831
+ {#if steps && steps.steps.length > 1}
832
+ <h3 class="sub-h">
833
+ Step through it <span class="hint"
834
+ >the same render as a sequence: this ran, then the server waited for that, then…</span
835
+ >
836
+ </h3>
837
+ <RenderSteps {steps} />
838
+ {/if}
839
+ </section>
840
+ {/if}
841
+
842
+ {#if contention && contention.requests.length}
843
+ <section class="panel">
844
+ <h2>
845
+ The instance was not alone <span class="hint" style="font-weight:400"
846
+ >({contention.requests.length} request{contention.requests.length === 1 ? '' : 's'} ran here
847
+ during the profiled render{(meta.runs?.length ?? 1) > 1 ? 's' : ''}{#if contention_self}: {contention_self}
848
+ the page's own calls to itself{/if}{#if contention_others}, {contention_others} from elsewhere{/if})</span
849
+ >
850
+ </h2>
851
+ <p class="hint">
852
+ One Node process, one event loop: every other request's turn on the CPU is a wait inside the
853
+ render's wall time with no frame of its own. Something else was in flight for <b
854
+ >{Math.round(contention.busy_share * 100)}%</b
855
+ >
856
+ of the window{#if contention.cpu_max_ms > 0}, taking up to <b
857
+ >{fmt_ms(contention.cpu_max_ms)} ms</b
858
+ > of CPU from it (an upper bound — a request's CPU is a process-wide delta, so overlapping requests
859
+ carry some of each other's){/if}.
860
+ {#if contention_others === 0}None of them is a stranger: they are the page's own calls to
861
+ this same server, and its holes — the render waited on itself.{/if}
862
+ The CPU tables above exclude the others; the wall time does not.
863
+ </p>
864
+ <table>
865
+ <thead
866
+ ><tr
867
+ ><th>request</th><th>route</th><th class="num">status</th><th class="num">ms</th><th
868
+ class="num">overlapped</th
869
+ ><th class="num">CPU, at most</th><th>what</th></tr
870
+ ></thead
871
+ >
872
+ <tbody>
873
+ {#each contention.requests.slice(0, 20) as r, i (i)}
874
+ <tr>
875
+ <td class="fn">{r.method} {r.path}</td>
876
+ <td class="file">{r.route ?? '—'}</td>
877
+ <td class="num">{r.status || '—'}</td>
878
+ <td class="num">{fmt_ms(r.ms)}</td>
879
+ <td class="num"><b>{fmt_ms(r.overlap_ms)}</b></td>
880
+ <td class="num">{fmt_ms(r.cpu_max_ms)}</td>
881
+ <td class="hint"
882
+ >{r.kind === 'hole'
883
+ ? "one of the page's own holes"
884
+ : r.kind === 'self'
885
+ ? 'the render calling its own server'
886
+ : 'another request'}</td
887
+ >
888
+ </tr>
889
+ {/each}
890
+ </tbody>
891
+ </table>
892
+ {#if contention.inflight_at_start.some((n) => n > 0)}
893
+ <p class="hint">
894
+ In flight when each profiled render started: {contention.inflight_at_start.join(', ')}.
895
+ </p>
896
+ {/if}
897
+ </section>
898
+ {/if}
899
+
900
+ <!-- ═══════════════ 3 · MEMORY ═══════════════ -->
901
+ {#if spark}
902
+ <div class="part"><span class="part-n">3</span><span class="part-t">Memory</span></div>
903
+ {#if gc_attr && (gc_attr.pauses.length || gc_attr.makers.length)}
904
+ <section class="panel">
905
+ <h2>
906
+ Who caused the GC <span class="hint" style="font-weight:400"
907
+ >(each pause joined to what filled the heap before it)</span
908
+ >
909
+ </h2>
910
+ <div class="summary">
911
+ <div class="stat">
912
+ <b>{gc_attr.summary.count}</b><span
913
+ >pauses in the window · {gc_attr.summary.minor} minor · {gc_attr.summary.major} major{gc_attr
914
+ .summary.incremental
915
+ ? ` · ${gc_attr.summary.incremental} incremental`
916
+ : ''}</span
917
+ >
918
+ </div>
919
+ <div class="stat">
920
+ <b>{fmt_ms(gc_attr.summary.total_ms / renders)} ms</b><span
921
+ >in GC{renders > 1 ? ' per render' : ''} · longest pause {fmt_ms(
922
+ gc_attr.summary.max_ms
923
+ )} ms</span
924
+ >
925
+ </div>
926
+ <div class="stat">
927
+ <b>{Math.round((gc_attr.summary.allocated_mb / renders) * 10) / 10} MB</b><span
928
+ >allocated{renders > 1 ? ' per render' : ' in the window'} · {gc_attr.summary
929
+ .alloc_rate_mb_s} MB/s</span
930
+ >
931
+ </div>
932
+ {#if gc_attr.summary.retained_mb !== undefined}<div class="stat">
933
+ <b>{gc_attr.summary.retained_mb > 0 ? '+' : ''}{gc_attr.summary.retained_mb} MB</b
934
+ ><span
935
+ >heap held at the end vs the start{gc_attr.summary.retained_mb <= 5
936
+ ? ' — the rest was churn'
937
+ : ' — something keeps what it allocates'}</span
938
+ >
939
+ </div>{/if}
940
+ {#if gc_attr.summary.overhead_ms >= 0.5}<div class="stat">
941
+ <b>−{fmt_ms(gc_attr.summary.overhead_ms)} ms</b><span
942
+ >the profiler's own garbage, taken out ({fmt_ms(gc_attr.summary.measured_ms)} ms measured)</span
943
+ >
944
+ </div>{/if}
945
+ </div>
946
+ {#if gc_attr.makers.length}
947
+ {@const with_pauses = gc_attr.makers.some((m) => m.pauses > 0)}
948
+ {@const with_comp = gc_attr.makers.some((m) => m.component)}
949
+ <h3 class="sub-h">
950
+ Garbage makers <span class="hint"
951
+ >allocators by the pause time their allocations caused{renders > 1
952
+ ? ', per render'
953
+ : ''}</span
954
+ >
955
+ </h3>
956
+ <table>
957
+ <thead
958
+ ><tr
959
+ ><th>function</th>{#if with_comp}<th>in</th>{/if}<th>where</th><th class="num"
960
+ >allocated</th
961
+ ><th class="num">share</th><th class="num">GC ms caused</th>{#if with_pauses}<th
962
+ class="num">pauses</th
963
+ >{/if}</tr
964
+ ></thead
965
+ >
966
+ <tbody>
967
+ {#each gc_attr.makers.slice(0, 20) as m (m.key)}
968
+ <tr>
969
+ <td class="fn"
970
+ ><b>{m.name}</b>{#if m.caller && m.caller !== m.name}<span class="hint">
971
+ ← {m.caller}</span
972
+ >{/if}</td
973
+ >
974
+ {#if with_comp}<td class="fn">{m.component ?? '—'}</td>{/if}
975
+ <td class="file"
976
+ >{m.url ? `${m.url.split('/').slice(-3).join('/')}:${m.line}` : ''}
977
+ <span class="hint">{m.category}</span></td
978
+ >
979
+ <td class="num">{fmt_mb(m.allocated / renders)}</td>
980
+ <td class="num">{Math.round(m.share * 100)}%</td>
981
+ <td class="num"><b>{fmt_ms(m.gc_ms / renders)}</b></td>
982
+ {#if with_pauses}<td class="num">{m.pauses}</td>{/if}
983
+ </tr>
984
+ {/each}
985
+ </tbody>
986
+ </table>
987
+ {/if}
988
+ {#if gc_attr.components.length}
989
+ <p class="hint">
990
+ By component: {#each gc_attr.components.slice(0, 6) as c, i (c.name)}{#if i > 0},
991
+ {/if}<b>{c.name}</b>
992
+ {fmt_ms(c.gc_ms / renders)} ms of GC for {fmt_mb(
993
+ c.allocated / renders
994
+ )}{/each}{renders > 1 ? ', per render' : ''}.
995
+ </p>
996
+ {/if}
997
+ {#if gc_attr.pauses.length}
998
+ <h3 class="sub-h">
999
+ The pauses <span class="hint"
1000
+ >when, what kind, and what was allocated since the previous one</span
1001
+ >
1002
+ </h3>
1003
+ <table>
1004
+ <thead
1005
+ ><tr
1006
+ ><th class="num">at</th><th>kind</th><th class="num">ms</th><th class="num"
1007
+ >allocated before</th
1008
+ ><th>why</th><th>running then</th></tr
1009
+ ></thead
1010
+ >
1011
+ <tbody>
1012
+ {#each gc_attr.pauses.slice(0, 40) as p, i (i)}
1013
+ <tr>
1014
+ <td class="num">{fmt_ms(p.t - (gc_attr.window_offset_ms ?? 0))} ms</td>
1015
+ <td class="fn">{p.kind}{p.forced ? ' (forced)' : ''}</td>
1016
+ <td class="num"
1017
+ ><b>{fmt_ms(p.ms)}</b>{#if p.ms_measured - p.ms >= 0.5}<span class="hint">
1018
+ ({fmt_ms(p.ms_measured)} measured)</span
1019
+ >{/if}</td
1020
+ >
1021
+ <td class="num">{p.estimated ? '≈' : ''}{fmt_mb(p.allocated)}</td>
1022
+ <td class="file">{p.why}</td>
1023
+ <td class="fn"
1024
+ >{#if p.top.length}{#each p.top.slice(0, 3) as x, j (x.key)}{#if j > 0},
1025
+ {/if}{x.name}{#if x.component && x.component !== x.name}
1026
+ <span class="hint">in {x.component}</span>{/if}
1027
+ {Math.round(x.share * 100)}%{/each}{:else if p.running}<b
1028
+ >{p.running.label}</b
1029
+ >{#if p.running.file}
1030
+ <span class="hint">{p.running.file.split('/').slice(-2).join('/')}</span
1031
+ >{/if}{:else}—{/if}</td
1032
+ >
1033
+ </tr>
1034
+ {/each}
1035
+ </tbody>
1036
+ </table>
1037
+ {#if gc_attr.pauses.length > 40}<p class="hint">
1038
+ and {gc_attr.pauses.length - 40} more pauses.
1039
+ </p>{/if}
1040
+ {/if}
1041
+ <p class="hint">
1042
+ A minor pause happens when the young space fills; a major one when the old space grows.
1043
+ Who allocated comes from one read of the allocation profile at the end of the window
1044
+ (reading it during the work set off collections of its own), so "GC ms caused" gives each
1045
+ allocator its share of the pauses by its share of everything allocated, and a pause's
1046
+ "allocated before" is the window's rate over the stretch since the previous pause.
1047
+ "Running then" is what the CPU was doing when the pause fell, from the timeline. The
1048
+ profiler's own allocations are taken out of every pause.
1049
+ </p>
1050
+ </section>
1051
+ {/if}
1052
+ {#if alloc}
1053
+ <section class="panel">
1054
+ <h2>
1055
+ When the heap grew <span class="hint" style="font-weight:400"
1056
+ >(the used heap over the whole recording, and the stretches it grew fastest — with what
1057
+ ran then)</span
1058
+ >
1059
+ </h2>
1060
+ <AllocStrip {alloc} gc={gc_on_capture} />
1061
+ </section>
1062
+ {/if}
1063
+ {#if retained}
1064
+ <section class="panel">
1065
+ <h2>
1066
+ What a render leaves behind <span class="hint" style="font-weight:400"
1067
+ >(one more render, a full collection, then what is still alive — by the line that made
1068
+ it)</span
1069
+ >
1070
+ </h2>
1071
+ <div class="summary">
1072
+ <div class="stat">
1073
+ <b>{fmt_mb(retained.total_bytes)}</b><span
1074
+ >still alive after the render and a full collection</span
1075
+ >
1076
+ </div>
1077
+ <div class="stat">
1078
+ <b>{fmt_ms(retained.render_ms)} ms</b><span>that render (outside the runs)</span>
1079
+ </div>
1080
+ </div>
1081
+ {#if retained.sites.length}
1082
+ <table>
1083
+ <thead
1084
+ ><tr
1085
+ ><th>allocated by</th><th>in</th><th>where</th><th class="num">kept</th><th
1086
+ class="num">share</th
1087
+ ></tr
1088
+ ></thead
1089
+ >
1090
+ <tbody>
1091
+ {#each retained.sites.slice(0, 15) as s, i (i)}
1092
+ <tr>
1093
+ <td class="fn"
1094
+ ><b>{s.name}</b>{#if s.caller && s.caller !== s.name}<span class="hint">
1095
+ ← {s.caller}</span
1096
+ >{/if}</td
1097
+ >
1098
+ <td class="fn">{s.component ?? '—'}</td>
1099
+ <td class="file"
1100
+ >{s.url ? `${s.url.split('/').slice(-3).join('/')}:${s.line}` : ''}</td
1101
+ >
1102
+ <td class="num">{fmt_mb(s.bytes)}</td>
1103
+ <td class="num">{Math.round(s.share * 100)}%</td>
1104
+ </tr>
1105
+ {/each}
1106
+ </tbody>
1107
+ </table>
1108
+ {/if}
1109
+ <p class="hint">
1110
+ These objects were made by one render and something still holds them after a full
1111
+ collection: a module-level cache, a registry, a closure kept by a long-lived object. The
1112
+ line is where they were made; their owner is what keeps them. A page that keeps this much
1113
+ per render grows until the instance restarts.
1114
+ </p>
1115
+ </section>
1116
+ {/if}
1117
+ {/if}
1118
+
1119
+ <!-- ═══════════════ 4 · HIDDEN COSTS ═══════════════ -->
1120
+ {#if sync_rows.length || promises_per_render >= 1000 || memo_rows.length || deopt_rows.length}
1121
+ <div class="part">
1122
+ <span class="part-n">4</span><span class="part-t">Hidden costs</span><span class="hint"
1123
+ >what no single hot function shows</span
1124
+ >
1125
+ </div>
1126
+ <div class="grid">
1127
+ {#if sync_rows.length}
1128
+ <section class="panel">
1129
+ <h3>
1130
+ Synchronous I/O in the render <span class="hint"
1131
+ >blocks every other request on the instance while it runs</span
1132
+ >
1133
+ </h3>
1134
+ <table>
1135
+ <thead
1136
+ ><tr
1137
+ ><th>call</th><th>module</th><th class="num">calls</th><th class="num">ms</th><th
1138
+ >from</th
1139
+ ></tr
1140
+ ></thead
1141
+ >
1142
+ <tbody>
1143
+ {#each sync_rows as s (s.key)}
1144
+ <tr
1145
+ ><td class="fn"
1146
+ ><a href={fn_known.has(s.key) ? row_href(`fn:${s.key}`) : undefined}
1147
+ ><b>{s.name}</b></a
1148
+ ></td
1149
+ ><td class="file">{s.module}</td><td class="num">{s.calls ?? '—'}</td><td
1150
+ class="num">{fmt_ms(s.total_ms)}</td
1151
+ ><td class="file">{s.callers.join(' ← ') || '—'}</td></tr
1152
+ >
1153
+ {/each}
1154
+ </tbody>
1155
+ </table>
1156
+ </section>
1157
+ {/if}
1158
+ {#if memo_rows.length}
1159
+ <section class="panel">
1160
+ <h3>
1161
+ Memoization candidates <span class="hint"
1162
+ >called many times per render at a steady cost each — if the result depends only on
1163
+ the argument, cache it</span
1164
+ >
1165
+ </h3>
1166
+ <table>
1167
+ <thead
1168
+ ><tr
1169
+ ><th>function</th><th>where</th><th class="num">/ render</th><th class="num"
1170
+ >per call</th
1171
+ ><th class="num">total ms</th><th class="num">alloc / call</th><th>under</th></tr
1172
+ ></thead
1173
+ >
1174
+ <tbody>
1175
+ {#each memo_rows as m (m.key)}
1176
+ <tr
1177
+ ><td class="fn"
1178
+ ><a href={fn_known.has(m.key) ? row_href(`fn:${m.key}`) : undefined}
1179
+ ><b>{m.name}</b></a
1180
+ ></td
1181
+ ><td class="file">{m.url}:{m.line}</td><td class="num">{m.calls}</td><td
1182
+ class="num">{m.per_call_ms} ms</td
1183
+ ><td class="num">{fmt_ms(m.total_ms)}</td><td class="num"
1184
+ >{m.alloc_per_call ? fmt_bytes(m.alloc_per_call) : '—'}</td
1185
+ ><td class="fn">{m.parent ?? '—'}</td></tr
1186
+ >
1187
+ {/each}
1188
+ </tbody>
1189
+ </table>
1190
+ </section>
1191
+ {/if}
1192
+ {#if deopt_rows.length}
1193
+ <section class="panel">
1194
+ <h3>
1195
+ Deoptimizations <span class="hint"
1196
+ >functions V8 threw out of optimized code, and why</span
1197
+ >
1198
+ </h3>
1199
+ <table>
1200
+ <thead
1201
+ ><tr
1202
+ ><th>function</th><th>where</th><th class="num">times</th><th>reasons</th><th
1203
+ class="num">self ms</th
1204
+ ></tr
1205
+ ></thead
1206
+ >
1207
+ <tbody>
1208
+ {#each deopt_rows.slice(0, 20) as d (d.key)}
1209
+ <tr>
1210
+ <td class="fn"
1211
+ ><a href={fn_known.has(d.key) ? row_href(`fn:${d.key}`) : undefined}
1212
+ ><b>{d.name}</b></a
1213
+ ></td
1214
+ >
1215
+ <td class="file"
1216
+ >{d.url}{#if d.line > 0}:{d.line}{/if}
1217
+ <span class="hint">{CATEGORY_LABEL[d.category]}</span></td
1218
+ >
1219
+ <td class="num">{d.count}</td>
1220
+ <td class="fn"
1221
+ >{Object.entries(d.reasons)
1222
+ .sort((x, y) => y[1] - x[1])
1223
+ .map(([r, n]) => `${r} ×${n}`)
1224
+ .join(', ')}</td
1225
+ >
1226
+ <td class="num">{fmt_ms(d.self_ms)}</td>
1227
+ </tr>
1228
+ {/each}
1229
+ </tbody>
1230
+ </table>
1231
+ <p class="hint">
1232
+ "wrong map": objects of different shapes reach one site — keep the same properties in
1233
+ the same order. "not a Smi": a number changed kind. A function that deoptimizes every
1234
+ render runs slow every render.
1235
+ </p>
1236
+ </section>
1237
+ {/if}
1238
+ {#if promises && promises_per_render >= 1000}
1239
+ <section class="panel">
1240
+ <h3>Promises <span class="hint">each an allocation and a microtask</span></h3>
1241
+ <p>
1242
+ <b class="big">{promises_per_render.toLocaleString()}</b>
1243
+ <span class="hint">created per render</span>
1244
+ </p>
1245
+ {#if promises.top.length}
1246
+ <p class="hint">
1247
+ Mostly from: {#each promises.top.slice(0, 5) as t, i (t.caller)}{#if i > 0},
1248
+ {/if}<b>{t.caller}</b>
1249
+ {Math.round(t.share * 100)}%{/each}
1250
+ <span class="hint"
1251
+ >(one stack in 256 promises is captured; the shares are of those)</span
1252
+ >
1253
+ </p>
1254
+ {/if}
1255
+ </section>
1256
+ {/if}
1257
+ </div>
1258
+ {/if}
1259
+
1260
+ <!-- ═══════════════ 5 · THE DATA ═══════════════ -->
1261
+ {#if lineage || (extras.river && extras.river.nodes.length) || (og && seed && seed.rows.length) || extras.strip}
1262
+ <div class="part"><span class="part-n">5</span><span class="part-t">The data</span></div>
1263
+ {/if}
1264
+ {#if lineage}
1265
+ <section class="panel">
1266
+ <h2>
1267
+ Who reads what <span class="hint" style="font-weight:400"
1268
+ >(every page.data key: the load that made it, the components that read it, from their
1269
+ sources)</span
1270
+ >
1271
+ </h2>
1272
+ <p class="hint">
1273
+ A key read by an island must ship in the seed. A key read only on the server is rendered
1274
+ into the HTML and need not ship. A key nobody reads was fetched for no one — the load's wait
1275
+ behind it is pure waste. The readers come from scanning the route's page and layouts and
1276
+ every component the profile saw; the islands' reads are what the page's own seed explainer
1277
+ recorded, so the two agree. An island that reads the page whole makes every key "unknown".
1278
+ </p>
1279
+ <table>
1280
+ <thead
1281
+ ><tr
1282
+ ><th>key</th><th>from</th><th class="num">shipped</th><th class="num">load waited</th
1283
+ ><th>read by</th><th>verdict</th></tr
1284
+ ></thead
1285
+ >
1286
+ <tbody>
1287
+ {#each lineage_keys as k (k.key)}
1288
+ <tr>
1289
+ <td class="fn"><code>{k.key}</code></td>
1290
+ <td class="file">{k.from ?? '—'}</td>
1291
+ <td class="num">{k.shipped_bytes ? fmt_bytes(k.shipped_bytes) : '—'}</td>
1292
+ <td class="num">{k.load_wait_ms === null ? '—' : fmt_ms(k.load_wait_ms) + ' ms'}</td>
1293
+ <td class="fn"
1294
+ >{#each k.readers.slice(0, 5) as r, i (r.name)}{#if i > 0},
1295
+ {/if}{r.name}{#if r.island}<span class="hint"> (island)</span>{/if}{:else}<span
1296
+ class="hint">—</span
1297
+ >{/each}{#if k.readers.length > 5}<span class="hint">
1298
+ +{k.readers.length - 5}</span
1299
+ >{/if}</td
1300
+ >
1301
+ <td
1302
+ ><span
1303
+ class="lverdict"
1304
+ class:warn={k.verdict === 'unread'}
1305
+ class:note={k.verdict === 'server-only'}
1306
+ class:good={k.verdict === 'island'}>{VERDICT_LABEL[k.verdict] ?? k.verdict}</span
1307
+ ></td
1308
+ >
1309
+ </tr>
1310
+ {/each}
1311
+ </tbody>
1312
+ </table>
1313
+ {#if lineage.keys.length > 40}<p class="hint">
1314
+ and {lineage.keys.length - 40} more keys.
1315
+ </p>{/if}
1316
+ {#if lineage.notes.length}<p class="hint">
1317
+ {#each lineage.notes as n, i (i)}{n}
1318
+ {/each}
1319
+ </p>{/if}
1320
+ <p class="hint scanned">
1321
+ Scanned: {#each lineage.components as c, i (c.file)}{#if i > 0},
1322
+ {/if}<span title={c.file}>{c.name}</span>{#if c.reads === null}<span class="hint">
1323
+ (?)</span
1324
+ >{:else if c.whole}<span class="hint"> (whole)</span>{:else}<span class="hint">
1325
+ ({c.reads.length})</span
1326
+ >{/if}{/each}.
1327
+ </p>
1328
+ </section>
1329
+ {/if}
1330
+ {#if extras.river && extras.river.nodes.length}
1331
+ <section class="panel">
1332
+ <h2>
1333
+ The data river <span class="hint" style="font-weight:400"
1334
+ >(upstream calls → loads → page.data keys → the islands that read them)</span
1335
+ >
1336
+ </h2>
1337
+ <DataRiver river={extras.river} />
1338
+ </section>
1339
+ {/if}
1340
+ {#if og && seed && seed.rows.length}
1341
+ <section class="panel">
1342
+ <h2>
1343
+ The seed, explained <span class="hint" style="font-weight:400"
1344
+ >({fmt_kb(og.seed_bytes)}{#if og.seed_culprit}
1345
+ · devalue because of <code>{og.seed_culprit}</code>{:else if og.seed_json}
1346
+ · json{/if})</span
1347
+ >
1348
+ </h2>
1349
+ <p class="hint">
1350
+ <!-- (a colour swatch beside plain text: the words themselves must stay 4.5:1 in both themes) -->
1351
+ Every top-level <code>page.data</code> key, sized, and why it ships:
1352
+ <i class="sw" style="background:#e8734a"></i>read by an island's client code,
1353
+ <i class="sw" style="background:#5b8fd6"></i>referenced by an island's props, or
1354
+ <i class="sw" style="background:#d9a03d"></i>whole because an island reads page.data without
1355
+ naming keys. Greyed keys were left out by seed shaping.
1356
+ </p>
1357
+ <SeedExplainer
1358
+ rows={seed.rows}
1359
+ whole_by={seed.whole_by}
1360
+ total={seed.total}
1361
+ seed_bytes={og.seed_bytes}
1362
+ />
1363
+ </section>
1364
+ {/if}
1365
+ {#if extras.strip}
1366
+ <section class="panel">
1367
+ <h2>
1368
+ The document, byte by byte <span class="hint" style="font-weight:400"
1369
+ >({fmt_bytes(extras.strip.total)} — what each byte the server wrote is, in the order it left)</span
1370
+ >
1371
+ </h2>
1372
+ <ByteStrip
1373
+ strip={extras.strip}
1374
+ names={fp_names}
1375
+ nav={visit ? { res_start: visit.nav.res_start, res_end: visit.nav.res_end } : null}
1376
+ />
1377
+ </section>
1378
+ {/if}
1379
+ {#if extras.assets}
1380
+ {@const pa = extras.assets}
1381
+ <section class="panel" id="page-weight">
1382
+ <h2>
1383
+ What the page loads <span class="hint" style="font-weight:400"
1384
+ >({fmt_bytes(pa.totals.wire)} on the wire at start · {fmt_bytes(pa.totals.js)} of JS in {pa
1385
+ .totals.js_files} files{#if pa.totals.lazy_js}
1386
+ · {fmt_bytes(pa.totals.lazy_js)} more on demand{/if})</span
1387
+ >
1388
+ </h2>
1389
+ <p class="hint">
1390
+ Every file the document makes the browser fetch at start, weighed through the app: its
1391
+ scripts, each
1392
+ <code>modulepreload</code> and what it imports, a Kit-hydrated page's route code, the
1393
+ islands that wake at load, stylesheets, fonts and images. Code that loads only when an
1394
+ island wakes later is marked <b>on demand</b>.
1395
+ <b>Blocks</b> = must arrive before the first paint.
1396
+ </p>
1397
+ <table class="weight">
1398
+ <thead
1399
+ ><tr
1400
+ ><th>file</th><th>kind</th><th>how</th><th class="num">size</th><th class="num">wire</th
1401
+ ><th>holds</th></tr
1402
+ ></thead
1403
+ >
1404
+ <tbody>
1405
+ {#each pa.assets.slice(0, 25) as x (x.url)}
1406
+ <tr class:lazy={x.lazy}>
1407
+ <td class="file" title={x.url}>{x.url.split('?')[0].split('/').pop()}</td>
1408
+ <td
1409
+ >{x.kind}{#if x.blocking}
1410
+ <b class="warn">blocks</b>{/if}{#if x.lazy}
1411
+ <span class="hint">on demand</span>{/if}</td
1412
+ >
1413
+ <td class="hint">{x.via}</td>
1414
+ <td class="num">{fmt_bytes(x.bytes)}</td>
1415
+ <td class="num">{fmt_bytes(x.wire)}</td>
1416
+ <td class="hint">{x.contains ? x.contains.join(', ') : ''}</td>
1417
+ </tr>
1418
+ {/each}
1419
+ </tbody>
1420
+ </table>
1421
+ {#if pa.assets.length > 25}<p class="hint">
1422
+ …and {pa.assets.length - 25} smaller files.
1423
+ </p>{/if}
1424
+ {#if pa.missed.length}<p class="hint">
1425
+ Not weighed: {pa.missed
1426
+ .slice(0, 5)
1427
+ .map((u) => u.split('/').pop())
1428
+ .join(', ')}{pa.missed.length > 5 ? ` and ${pa.missed.length - 5} more` : ''}.
1429
+ </p>{/if}
1430
+ </section>
1431
+ {:else if extras.assets_missing}
1432
+ <section class="panel" id="page-weight">
1433
+ <h2>What the page loads</h2>
1434
+ <p class="hint">Not weighed: {extras.assets_missing}.</p>
1435
+ </section>
1436
+ {/if}
1437
+
1438
+ <!-- ═══════════════ 6 · ISLANDS AND THE BROWSER ═══════════════ -->
1439
+ {#if og || islands.length || (meta.trigger === 'page' && meta.runs?.length && (page_path || vitals || client_marks.length)) || extras.client_cpu}
1440
+ <div class="part">
1441
+ <span class="part-n">6</span><span class="part-t">Islands and the browser</span>
1442
+ </div>
1443
+ {/if}
1444
+ {#if og}
1445
+ <section class="panel">
1446
+ <h2>ogygia's own cost</h2>
1447
+ <p class="hint">
1448
+ What the handle added to this page — the transform's own time and every byte it put in the
1449
+ document. The seed ships only when an island reads <code>$page</code>; props cross once per
1450
+ island.
1451
+ </p>
1452
+ <div class="summary">
1453
+ <div class="stat"><b>{fmt_ms(og.transform_ms)} ms</b><span>transform</span></div>
1454
+ <div class="stat"><b>{og.islands}</b><span>islands</span></div>
1455
+ {#if og.holes}<div class="stat"><b>{og.holes}</b><span>holes</span></div>{/if}
1456
+ <div class="stat">
1457
+ <b>{fmt_kb(og.seed_bytes)}</b><span
1458
+ >page seed{og.seed_bytes ? (og.seed_json ? ' (json)' : ' (devalue)') : ''}</span
1459
+ >
1460
+ </div>
1461
+ <div class="stat"><b>{fmt_kb(og.tail_bytes)}</b><span>props + hints</span></div>
1462
+ {#if og.remote_seed_bytes}<div class="stat">
1463
+ <b>{fmt_kb(og.remote_seed_bytes)}</b><span>remote seed</span>
1464
+ </div>{/if}
1465
+ {#if og.fnm_bytes}<div class="stat">
1466
+ <b>{fmt_kb(og.fnm_bytes)}</b><span>fn manifest</span>
1467
+ </div>{/if}
1468
+ {#if og.ctx_bytes}<div class="stat">
1469
+ <b>{fmt_kb(og.ctx_bytes)}</b><span>context bridge</span>
1470
+ </div>{/if}
1471
+ </div>
1472
+ </section>
1473
+
1474
+ {#if islands.length}
1475
+ <section class="panel">
1476
+ <h2>
1477
+ Islands <span class="hint" style="font-weight:400"
1478
+ >({islands.length}{#if flagged}, {flagged} with advice{/if}, click a column to sort)</span
1479
+ >
1480
+ </h2>
1481
+ <p class="hint">
1482
+ One row per island — its copies merge, however many props sidecars they ship (<b>×N</b>).
1483
+ <b>SSR ms</b>
1484
+ is the component's server render per page render; <b>props</b> is what the sidecar ships;
1485
+ <b>seed refs</b>
1486
+ are props that point into page.data instead of shipping twice{#if hasJs}; <b>JS</b> is the unique
1487
+ weight of the island's module and its preloads{/if};
1488
+ <b>interactivity</b> is what the build found in its components — an
1489
+ <span style="color:var(--warn)">inert</span>
1490
+ island loads JS for markup that never changes{#if hasClient}; <b>hydrate ms</b> is what your
1491
+ own browser reported (wake to hydrated, p50){:else}. Open the page in this browser while
1492
+ logged in and the runtime reports each island's hydration time back here{/if}. A
1493
+ <span class="dotflag-inline">!</span> marks a row with advice — open it.
1494
+ </p>
1495
+ <IslandsTable rows={islands} {hasJs} {hasClient} />
1496
+ </section>
1497
+ {/if}
1498
+
1499
+ {#if holes.length}
1500
+ <section class="panel">
1501
+ <h2>Holes <span class="hint" style="font-weight:400">({holes.length} deferred)</span></h2>
1502
+ <p class="hint">
1503
+ A hole ships its fallback in the page and renders on the islands endpoint afterwards. Each
1504
+ is named by its component and props, the way it is written in the page.{#if holes_seen}
1505
+ The endpoint requests that landed in this window say what its render cache did.{:else}
1506
+ No endpoint requests landed in this window — open the page in a browser during a
1507
+ recording, or profile the hole's URL, to see hits and misses.{/if}
1508
+ </p>
1509
+ <table>
1510
+ <thead>
1511
+ <tr
1512
+ ><th>hole</th><th>fetch</th><th>then wakes</th><th class="num">maxAge</th><th
1513
+ class="num">copies</th
1514
+ ><th class="num">requests</th><th class="num">avg ms</th><th>cache</th></tr
1515
+ >
1516
+ </thead>
1517
+ <tbody>
1518
+ {#each holes as h (h.id)}
1519
+ <tr>
1520
+ <td class="fn">
1521
+ {#if h.name}<b>{h.name}</b>{#if h.props}
1522
+ <code class="props">{h.props}</code>{/if}<span class="hint">
1523
+ · {h.id}</span
1524
+ >{:else}<code>{h.id}</code>{/if}
1525
+ </td>
1526
+ <td>{h.when}</td>
1527
+ <td>{h.hydrate ?? '—'}</td>
1528
+ <td class="num">{h.ttl ? `${h.ttl}s` : '—'}</td>
1529
+ <td class="num">{h.count}</td>
1530
+ <td class="num">{h.requests || '—'}</td>
1531
+ <td class="num">{h.avg_ms === null ? '—' : fmt_ms(h.avg_ms)}</td>
1532
+ <td>
1533
+ <span
1534
+ class="hverdict"
1535
+ class:warn={h.ttl > 0 && h.requests >= 2 && h.hit === 0}
1536
+ class:good={h.hit > 0}>{h.verdict}</span
1537
+ >
1538
+ {#if h.requests}<span class="hint">
1539
+ · {h.hit} hit · {h.miss} miss{#if h.none}
1540
+ · {h.none} uncached{/if}</span
1541
+ >{/if}
1542
+ </td>
1543
+ </tr>
1544
+ {/each}
1545
+ </tbody>
1546
+ </table>
1547
+ </section>
1548
+ {/if}
1549
+ {/if}
1550
+ {#if islands.length}
1551
+ <section class="panel">
1552
+ <h2>
1553
+ Island cost heatmap <span class="hint" style="font-weight:400"
1554
+ >(every cost of every island on one grid — click a column to sort by it)</span
1555
+ >
1556
+ </h2>
1557
+ <IslandHeatmap rows={islands} {visit} />
1558
+ </section>
1559
+ {/if}
1560
+ {#if page_path && islands.length}
1561
+ <section class="panel">
1562
+ <h2>
1563
+ DOM time travel <span class="hint" style="font-weight:400"
1564
+ >(each island's markup as the server sent it, after it hydrated, when the page left — and
1565
+ the layout shifts, mapped)</span
1566
+ >
1567
+ </h2>
1568
+ <DomTravel page={page_path} names={fp_names} {visit} />
1569
+ </section>
1570
+ {/if}
1571
+
1572
+ {#if meta.trigger === 'page' && meta.runs?.length}
1573
+ {#if page_path}
1574
+ <section class="panel">
1575
+ <h2>
1576
+ One clock <span class="hint" style="font-weight:400"
1577
+ >(the server's render, the HTML arriving, every file, the main thread, each island
1578
+ waking, the paints — from the click)</span
1579
+ >
1580
+ </h2>
1581
+ <OneClock server={server_clock} {visit} page={page_path} names={fp_names} {hole_names} />
1582
+ </section>
1583
+ {/if}
1584
+ {#if vitals}
1585
+ <section class="panel">
1586
+ <h2>
1587
+ In the browser <span class="hint" style="font-weight:400"
1588
+ >(what your own visits to this page measured, p50 of {vitals.n})</span
1589
+ >
1590
+ </h2>
1591
+ <div class="summary">
1592
+ <div class="stat">
1593
+ <b>{vitals.ttfb === null ? '—' : fmt_ms(vitals.ttfb) + ' ms'}</b><span>TTFB</span>
1594
+ </div>
1595
+ <div class="stat">
1596
+ <b>{vitals.fcp === null ? '—' : fmt_ms(vitals.fcp) + ' ms'}</b><span>first paint</span>
1597
+ </div>
1598
+ <div class="stat">
1599
+ <b>{vitals.lcp === null ? '—' : fmt_ms(vitals.lcp) + ' ms'}</b><span>LCP</span>
1600
+ </div>
1601
+ <div class="stat"><b>{vitals.cls === null ? '—' : vitals.cls}</b><span>CLS</span></div>
1602
+ <div class="stat">
1603
+ <b>{vitals.inp === null ? '—' : fmt_ms(vitals.inp) + ' ms'}</b><span>INP</span>
1604
+ </div>
1605
+ <div class="stat"><b>{fmt_ms(runsMedian)} ms</b><span>server render (median)</span></div>
1606
+ </div>
1607
+ <p class="hint">
1608
+ TTFB minus the render is what sits in front of this server (a cold instance, a proxy, the
1609
+ network); LCP minus TTFB is what the browser does after the HTML arrives (assets, fonts,
1610
+ hydration).
1611
+ </p>
1612
+ </section>
1613
+ {/if}
1614
+ {#if client_marks.length}
1615
+ <section class="panel">
1616
+ <h3 class="sub-h">
1617
+ Marked in the browser <span class="hint"
1618
+ >what the app timed itself with <code>mark()</code>, p50 over your visits</span
1619
+ >
1620
+ </h3>
1621
+ <table>
1622
+ <thead
1623
+ ><tr
1624
+ ><th>mark</th><th class="num">seen</th><th class="num">p50 ms</th><th class="num"
1625
+ >max ms</th
1626
+ ><th class="num">failed</th><th>attributes</th></tr
1627
+ ></thead
1628
+ >
1629
+ <tbody>
1630
+ {#each client_marks as m (m.name)}
1631
+ <tr>
1632
+ <td class="fn"><b>{m.name}</b></td>
1633
+ <td class="num">{m.n}</td>
1634
+ <td class="num"><b>{fmt_ms(m.p50_ms)}</b></td>
1635
+ <td class="num">{fmt_ms(m.max_ms)}</td>
1636
+ <td class="num" class:warn={m.errors > 0}>{m.errors || '—'}</td>
1637
+ <td class="hint">{m.attr_keys.join(', ') || '—'}</td>
1638
+ </tr>
1639
+ {/each}
1640
+ </tbody>
1641
+ </table>
1642
+ </section>
1643
+ {/if}
1644
+ {/if}
1645
+
1646
+ {#if extras.client_cpu}
1647
+ {@const c = extras.client_cpu.analysis}
1648
+ {@const cRows = c.components.map((f) => ({
1649
+ ...f,
1650
+ per: f.total_ms / Math.max(f.calls ?? 1, 1),
1651
+ count: f.calls ?? 0,
1652
+ alloc: null
1653
+ }))}
1654
+ {@const fRows = c.functions.slice(0, 60).map((f) => ({
1655
+ ...f,
1656
+ per: f.total_ms / Math.max(f.calls ?? 1, 1),
1657
+ count: f.calls ?? 0,
1658
+ alloc: null
1659
+ }))}
1660
+ <section class="panel">
1661
+ <h2>In the browser: CPU while the page hydrated</h2>
1662
+ <p class="hint">
1663
+ Your own browser's main thread, sampled every 10 ms for the first seconds of the visit ({new Date(
1664
+ extras.client_cpu.at
1665
+ ).toLocaleTimeString()}), the same way the server is: components by their hydration cost,
1666
+ the functions under them, and a flame graph. This is where the islands' hydrate times come
1667
+ from. A visitor is never sampled.
1668
+ </p>
1669
+ <div class="summary">
1670
+ <div class="stat"><b>{fmt_ms(c.duration_ms)} ms</b><span>sampled</span></div>
1671
+ <div class="stat"><b>{fmt_ms(c.busy_ms)} ms</b><span>main thread busy</span></div>
1672
+ <div class="stat">
1673
+ <b>{c.duration_ms > 0 ? ((c.busy_ms / c.duration_ms) * 100).toFixed(0) : 0}%</b><span
1674
+ >busy</span
1675
+ >
1676
+ </div>
1677
+ <div class="stat"><b>{c.components.length}</b><span>components seen</span></div>
1678
+ </div>
1679
+ {#if cRows.length}
1680
+ <h3 class="sub-h">Components <span class="hint">by hydration cost in the browser</span></h3>
1681
+ <ComponentsTable
1682
+ rows={cRows}
1683
+ busy={c.busy_ms}
1684
+ hasAlloc={false}
1685
+ maxTotal={Math.max(...cRows.map((r) => r.total_ms), 1)}
1686
+ {base}
1687
+ dev={false}
1688
+ />
1689
+ {/if}
1690
+ <h3 class="sub-h">Hot functions <span class="hint">in the browser</span></h3>
1691
+ <FunctionsTable rows={fRows} hasAlloc={false} {base} dev={false} />
1692
+ <h3 class="sub-h">Flame graph <span class="hint">the browser's main thread</span></h3>
1693
+ <Flame flame={c.flame} />
1694
+ </section>
1695
+ {/if}
1696
+
1697
+ <!-- ═══════════════ 7 · NETWORK AND WAITS ═══════════════ -->
1698
+ <div class="part"><span class="part-n">7</span><span class="part-t">Network and waits</span></div>
1699
+ <section class="panel">
1700
+ <h2>Network</h2>
1701
+ {#if net.length}
1702
+ <p class="hint">
1703
+ Every outbound call the server made during the window, tied to the route that made it.
1704
+ "wait" = until headers arrived; "body" = reading the response.{#if meta.trigger === 'page' && (meta.runs?.length ?? 0) > 1}
1705
+ Shown for one representative render (of {meta.runs?.length ?? 0}).{/if}
1706
+ </p>
1707
+ {#if wf.length}
1708
+ <p class="hint">
1709
+ Each bar is one request, sized by its response. Click a bar for its trigger, sizes,
1710
+ headers, and payload.
1711
+ </p>
1712
+ <Waterfall rows={wf} />
1713
+ {/if}
1714
+ <table>
1715
+ <thead>
1716
+ <tr
1717
+ ><th>host</th><th class="num">calls</th><th class="num">total ms</th><th class="num"
1718
+ >p50</th
1719
+ ><th class="num">max</th><th class="num">errors</th></tr
1720
+ >
1721
+ </thead>
1722
+ <tbody>
1723
+ {#each hosts as h}
1724
+ <tr>
1725
+ <td class="fn">{h.host || '(same process)'}</td>
1726
+ <td class="num">{h.count}</td>
1727
+ <td class="num"><b>{fmt_ms(h.total)}</b></td>
1728
+ <td class="num">{fmt_ms(h.p50)}</td>
1729
+ <td class="num">{fmt_ms(h.max)}</td>
1730
+ <td class="num">{h.errors || '—'}</td>
1731
+ </tr>
1732
+ {/each}
1733
+ </tbody>
1734
+ </table>
1735
+ {:else}
1736
+ <p class="hint">
1737
+ No outbound HTTP calls seen in this window. If requests are still slow while the CPU is
1738
+ idle, the wait is inside a database/socket driver or a timer.
1739
+ </p>
1740
+ {/if}
1741
+ </section>
1742
+
1743
+ {#if spans.length}
1744
+ <section class="panel">
1745
+ <h2>
1746
+ Spans <span class="hint" style="font-weight:400"
1747
+ >(what the app named with <code>span()</code>)</span
1748
+ >
1749
+ </h2>
1750
+ <p class="hint">
1751
+ The waits the profiler cannot see on its own, named in the code. Per render: count and time
1752
+ are divided by the {span_runs} render{span_runs === 1 ? '' : 's'} in this recording. A span appears
1753
+ on the timeline as "in &lt;name&gt;" wherever a call or a gap sits inside it. <b>total</b>
1754
+ is the spans' summed duration, <b>self</b> is that minus the child spans inside them,
1755
+ <b>wall</b>
1756
+ counts overlaps once (∥ marks spans that ran together, ×N the most at once): wall is what the
1757
+ render paid. When spans overlap, one span's start to end is mostly waiting behind the others,
1758
+ so <b>share</b> under p50 is its fair part of the wall. A span that carries an attribute
1759
+ with a few distinct values (a tag, a table, a key) gets a <b>by</b> table under it: each value's
1760
+ share, and the shares add up to the span's wall.
1761
+ </p>
1762
+ <table>
1763
+ <thead>
1764
+ <tr
1765
+ ><th>span</th><th>called from</th><th class="num">per render</th><th
1766
+ class="num"
1767
+ title="the spans' summed duration per render">total ms</th
1768
+ ><th class="num" title="minus the child spans inside it — its own time">self ms</th><th
1769
+ class="num"
1770
+ title="overlaps counted once — what it cost the render">wall ms</th
1771
+ ><th class="num">p50</th><th class="num">max</th><th>cache</th><th class="num"
1772
+ >errors</th
1773
+ ></tr
1774
+ >
1775
+ </thead>
1776
+ <tbody>
1777
+ {#each spans as s (s.name)}
1778
+ <tr>
1779
+ <td class="fn"
1780
+ ><b>{s.name}</b>{#if s.attr_keys.length}<span class="hint">
1781
+ · {s.attr_keys.join(', ')}</span
1782
+ >{/if}</td
1783
+ >
1784
+ <td class="file"
1785
+ >{s.callers[0] ?? '—'}{#if s.callers.length > 1}<span class="hint">
1786
+ +{s.callers.length - 1}</span
1787
+ >{/if}</td
1788
+ >
1789
+ <td class="num"
1790
+ >{(s.count / span_runs) % 1 === 0
1791
+ ? s.count / span_runs
1792
+ : (s.count / span_runs).toFixed(1)}</td
1793
+ >
1794
+ <td class="num">{fmt_ms(s.total_ms / span_runs)}</td>
1795
+ <td class="num" class:dimcell={s.self_ms < s.total_ms * 0.2}
1796
+ >{fmt_ms(s.self_ms / span_runs)}</td
1797
+ >
1798
+ <td class="num"
1799
+ ><b>{fmt_ms(s.wall_ms / span_runs)}</b
1800
+ >{#if s.count > 1 && s.wall_ms < s.total_ms * 0.6}<span
1801
+ class="hint"
1802
+ title="the spans overlap: they ran together{s.peak
1803
+ ? `, up to ${s.peak} at once`
1804
+ : ''}"
1805
+ >
1806
+ ∥{s.peak ? ` ×${s.peak}` : ''}</span
1807
+ >{/if}</td
1808
+ >
1809
+ <td class="num"
1810
+ >{fmt_ms(
1811
+ s.p50_ms
1812
+ )}{#if s.share_p50_ms !== undefined && s.count > 1 && s.wall_ms < s.total_ms * 0.6}<div
1813
+ class="hint"
1814
+ title="one span's fair part of the wall: its start to end is mostly waiting behind the others"
1815
+ >
1816
+ share {fmt_ms(s.share_p50_ms)}
1817
+ </div>{/if}</td
1818
+ >
1819
+ <td class="num">{fmt_ms(s.max_ms)}</td>
1820
+ <td
1821
+ >{#if s.cache}{s.cache.hit} hit ·
1822
+ <span class:warn={s.cache.miss > 0}>{s.cache.miss} miss</span>{:else}—{/if}</td
1823
+ >
1824
+ <td class="num">{s.errors || (s.open ? `${s.open} open` : '—')}</td>
1825
+ </tr>
1826
+ {#each Object.entries(s.by) as [k, rows] (k)}
1827
+ <tr class="by">
1828
+ <td colspan="10">
1829
+ <div class="bygrid">
1830
+ <div class="byh">by {k}</div>
1831
+ <div class="byh"></div>
1832
+ <div
1833
+ class="byh num"
1834
+ title="its fair part of the span's wall: at each instant the wall is split among the spans open then, so the rows add up to the span's wall"
1835
+ >
1836
+ share ms
1837
+ </div>
1838
+ <div class="byh num" title="summed durations">total ms</div>
1839
+ <div class="byh num">per render</div>
1840
+ <div class="byh num">p50</div>
1841
+ <div class="byh num">max</div>
1842
+ {#each rows as r (r.value)}
1843
+ {@const share = r.share_ms ?? r.wall_ms}
1844
+ <div class="byv"><code>{r.value}</code></div>
1845
+ <div class="bybar">
1846
+ <i
1847
+ style="width:{Math.max(
1848
+ 1,
1849
+ (share / Math.max(...rows.map((x) => x.share_ms ?? x.wall_ms), 0.01)) *
1850
+ 100
1851
+ )}%"
1852
+ ></i>
1853
+ </div>
1854
+ <div class="num"><b>{fmt_ms(share / span_runs)}</b></div>
1855
+ <div class="num">{fmt_ms(r.total_ms / span_runs)}</div>
1856
+ <div class="num">
1857
+ {(r.count / span_runs) % 1 === 0
1858
+ ? r.count / span_runs
1859
+ : (r.count / span_runs).toFixed(1)}×
1860
+ </div>
1861
+ <div class="num">{r.p50_ms ? fmt_ms(r.p50_ms) : '—'}</div>
1862
+ <div class="num">{fmt_ms(r.max_ms)}</div>
1863
+ {/each}
1864
+ </div>
1865
+ </td>
1866
+ </tr>
1867
+ {/each}
1868
+ {/each}
1869
+ </tbody>
1870
+ </table>
1871
+ {#if value_rows.length}
1872
+ <h3 class="sub-h">
1873
+ Values, not just functions <span class="hint"
1874
+ >the numbers the spans carried, and how the time moved with them</span
1875
+ >
1876
+ </h3>
1877
+ <p class="hint">
1878
+ A span that reports a number (<code>rows</code>, <code>tags</code>, <code>bytes</code>) is
1879
+ measured against it here:
1880
+ <b>ms per unit</b> is the slope of a straight line through the spans, <b>fit</b> how well the
1881
+ line holds (1 = the value alone sets the time). A steep, well-fitting line says the cost is
1882
+ the size of the input; cut the input and the span follows.
1883
+ </p>
1884
+ <table>
1885
+ <thead
1886
+ ><tr
1887
+ ><th>span</th><th>attribute</th><th class="num">spans</th><th class="num">min</th><th
1888
+ class="num">p50</th
1889
+ ><th class="num">max</th><th class="num">sum</th><th class="num">ms per unit</th><th
1890
+ class="num">fit</th
1891
+ ></tr
1892
+ ></thead
1893
+ >
1894
+ <tbody>
1895
+ {#each value_rows.slice(0, 30) as v (v.span + '.' + v.attr)}
1896
+ <tr>
1897
+ <td class="fn"><b>{v.span}</b></td>
1898
+ <td class="fn"><code>{v.attr}</code></td>
1899
+ <td class="num">{v.n}</td>
1900
+ <td class="num">{v.min}</td>
1901
+ <td class="num">{v.p50}</td>
1902
+ <td class="num">{v.max}</td>
1903
+ <td class="num">{v.sum}</td>
1904
+ <td class="num"
1905
+ >{v.ms_per_unit === undefined
1906
+ ? '—'
1907
+ : v.ms_per_unit >= 0.01
1908
+ ? v.ms_per_unit
1909
+ : v.ms_per_unit.toExponential(1)}</td
1910
+ >
1911
+ <td class="num" class:good={v.r !== undefined && v.r >= 0.8}
1912
+ >{v.r === undefined ? '—' : v.r}</td
1913
+ >
1914
+ </tr>
1915
+ {/each}
1916
+ </tbody>
1917
+ </table>
1918
+ {/if}
1919
+ </section>
1920
+ {/if}
1921
+
1922
+ {#if waiting.length}
1923
+ <section class="panel">
1924
+ <h2>
1925
+ Waiting by function <span class="hint" style="font-weight:400"
1926
+ >(click a column to sort)</span
1927
+ >
1928
+ </h2>
1929
+ <p class="hint">
1930
+ Where the server WAITED (not computed), attributed to the function that started the I/O. A
1931
+ big number here with idle CPU is your bottleneck.
1932
+ </p>
1933
+ <WaitingTable rows={waiting} maxMs={waitMax} />
1934
+ </section>
1935
+ {/if}
1936
+
1937
+ <!-- ═══════════════ 8 · THE RAW NUMBERS ═══════════════ -->
1938
+ <div class="part">
1939
+ <span class="part-n">8</span><span class="part-t">The raw numbers</span><span class="hint"
1940
+ >everything above is an aggregate of these</span
1941
+ >
1942
+ </div>
1943
+
1944
+ <section class="panel">
1945
+ <h2>CPU by self time</h2>
1946
+ <p class="hint">
1947
+ Every box is real work; the biggest box is the bottleneck. This is <b>self</b> time, so parents
1948
+ like Root/_layout barely show — only code that actually burns CPU. Hover for detail.
1949
+ </p>
1950
+ {#if tree}
1951
+ <Treemap hierarchy={tree} />
1952
+ <div class="legend">
1953
+ {#each legend as c}<span
1954
+ ><i style="background:{CATEGORY_COLOR[c]}"></i>{CATEGORY_LABEL[c]}</span
1955
+ >{/each}
1956
+ </div>
1957
+ {/if}
1958
+ </section>
1959
+
1960
+ <section class="panel">
1961
+ <h2>
1962
+ Components <span class="hint" style="font-weight:400"
1963
+ >({a.components.length}, click a column to sort)</span
1964
+ >
1965
+ </h2>
1966
+ <p class="hint">
1967
+ <b>self</b> = the component's own code, excluding nested components. <b>total</b> = self plus
1968
+ everything it calls. Sort by self to find who burns CPU, by total for the most expensive
1969
+ subtree.{#if renders > 1}
1970
+ Self and total add up all {renders} renders (divide by {renders} for one); <b>per call</b> is
1971
+ one render of the component.{/if}{#if hasCounts}
1972
+ <b>×N</b> is how many times it rendered in one page render.{/if}{#if hasAlloc}
1973
+ <b>alloc</b> is the memory allocated under it (sampled), nested components excluded — sort by
1974
+ it to find who makes the garbage.{/if}
1975
+ </p>
1976
+ {#if compRows.length}
1977
+ <ComponentsTable
1978
+ rows={compRows}
1979
+ busy={a.busy_ms}
1980
+ {hasAlloc}
1981
+ maxTotal={compMaxTotal}
1982
+ {base}
1983
+ {dev}
1984
+ />
1985
+ {:else}
1986
+ <p class="hint">
1987
+ No component frames in this recording — was any page rendered during the window?
1988
+ </p>
1989
+ {/if}
1990
+ </section>
1991
+
1992
+ <section class="panel">
1993
+ <h2>
1994
+ Hot functions <span class="hint" style="font-weight:400">(click a column to sort)</span>
1995
+ </h2>
1996
+ <p class="hint">
1997
+ Every function on the server, by time spent inside it.{#if renders > 1}
1998
+ Self and total add up all {renders} renders (divide by {renders} for one); <b>per call</b> is
1999
+ one call.{/if}{#if hasCounts}
2000
+ <b>×N</b> is the exact call count in one render (from V8 coverage).{/if}
2001
+ </p>
2002
+ {#if !a.sourcemapped}
2003
+ <p class="hint warn">
2004
+ Locations are bundled chunk positions — no sourcemap resolved. Ship .map files next to the
2005
+ server chunks (build with server sourcemaps) and "where" maps back to your source files.
2006
+ </p>
2007
+ {/if}
2008
+ {#if fnRows.length > 80}<p class="hint">
2009
+ The 80 with the most self time, and {fnRows.length - 80} more that other parts of this report
2010
+ link to.
2011
+ </p>{/if}
2012
+ <FunctionsTable rows={fnRows} {hasAlloc} {base} {dev} />
2013
+ </section>
2014
+
2015
+ {#if heap.length}
2016
+ <section class="panel">
2017
+ <h2>Top memory allocators</h2>
2018
+ <p class="hint">
2019
+ Sampled heap allocations during the window — who creates the objects (and the GC pressure).
2020
+ </p>
2021
+ <table>
2022
+ <thead>
2023
+ <tr
2024
+ ><th>function</th><th>where</th><th><span class="sr-only">kind of code</span></th><th
2025
+ class="num">self</th
2026
+ ><th class="num">total</th></tr
2027
+ >
2028
+ </thead>
2029
+ <tbody>
2030
+ {#each heap as h}
2031
+ <tr>
2032
+ <td class="fn"><b>{h.name}</b></td>
2033
+ <td class="file"
2034
+ >{h.url}{#if h.line > 0}:{h.line}{/if}</td
2035
+ >
2036
+ <td
2037
+ ><span
2038
+ class="chip"
2039
+ style="background:{CATEGORY_COLOR[h.category]};color:{ink_on(
2040
+ CATEGORY_COLOR[h.category]
2041
+ )}">{CATEGORY_LABEL[h.category]}</span
2042
+ ></td
2043
+ >
2044
+ <td class="num"><b>{fmt_bytes(h.self_bytes)}</b></td>
2045
+ <td class="num">{fmt_bytes(h.total_bytes)}</td>
2046
+ </tr>
2047
+ {/each}
2048
+ </tbody>
2049
+ </table>
2050
+ </section>
2051
+ {/if}
2052
+
2053
+ {#if spark}
2054
+ <section class="panel">
2055
+ <h2>Memory over the window</h2>
2056
+ <svg class="spark" width={spark.w} height={spark.h} viewBox="0 0 {spark.w} {spark.h}">
2057
+ <polyline points={spark.pts} fill="none" stroke="#6ca8e0" stroke-width="1.5" />
2058
+ <text x={spark.pad} y="12" fill="#6f8378" font-size="11">{spark.max} MB</text>
2059
+ <text x={spark.pad} y={spark.h - 6} fill="#6f8378" font-size="11">{spark.min} MB</text>
2060
+ </svg>
2061
+ <p class="hint">
2062
+ rss {spark.first.rss} → {spark.last.rss} MB · heap {spark.first.heap_used} → {spark.last
2063
+ .heap_used} MB
2064
+ </p>
2065
+ </section>
2066
+ {/if}
2067
+
2068
+ {#if measures.length}
2069
+ <section class="panel">
2070
+ <h2>User timings</h2>
2071
+ <p class="hint">
2072
+ Spans emitted with performance.measure() — captured free during the window. Real wall time.
2073
+ </p>
2074
+ <table>
2075
+ <thead>
2076
+ <tr
2077
+ ><th>name</th><th class="num">count</th><th class="num">total ms</th><th class="num"
2078
+ >avg ms</th
2079
+ ><th class="num">max ms</th></tr
2080
+ >
2081
+ </thead>
2082
+ <tbody>
2083
+ {#each measures as m}
2084
+ <tr>
2085
+ <td class="fn">{m.name}</td>
2086
+ <td class="num">{m.count}</td>
2087
+ <td class="num"><b>{fmt_ms(m.total_ms)}</b></td>
2088
+ <td class="num">{fmt_ms(m.total_ms / m.count)}</td>
2089
+ <td class="num">{fmt_ms(m.max_ms)}</td>
2090
+ </tr>
2091
+ {/each}
2092
+ </tbody>
2093
+ </table>
2094
+ </section>
2095
+ {/if}
2096
+
2097
+ <section class="panel">
2098
+ <h2>Time by file</h2>
2099
+ {#if files.length}
2100
+ <table>
2101
+ <thead
2102
+ ><tr
2103
+ ><th>file</th><th><span class="sr-only">kind of code</span></th><th class="num"
2104
+ >self ms</th
2105
+ ><th class="num">% of busy</th></tr
2106
+ ></thead
2107
+ >
2108
+ <tbody>
2109
+ {#each files as f}
2110
+ <tr>
2111
+ <td class="file">{f.key}</td>
2112
+ <td
2113
+ ><span
2114
+ class="chip"
2115
+ style="background:{CATEGORY_COLOR[f.category]};color:{ink_on(
2116
+ CATEGORY_COLOR[f.category]
2117
+ )}">{CATEGORY_LABEL[f.category]}</span
2118
+ ></td
2119
+ >
2120
+ <td class="num"><b>{fmt_ms(f.self_ms)}</b></td>
2121
+ <td class="num">{fmt_pct(f.self_ms, a.busy_ms)}</td>
2122
+ </tr>
2123
+ {/each}
2124
+ </tbody>
2125
+ </table>
2126
+ {/if}
2127
+ </section>
2128
+
2129
+ <section class="panel">
2130
+ <h2>Where the CPU went</h2>
2131
+ {#each buckets as b}
2132
+ <div class="barrow">
2133
+ <span class="fn">{b.key}</span>
2134
+ <div
2135
+ class="bar"
2136
+ style="width:{Math.max(1, (b.self_ms / maxBucket) * 100)}%;background:{CATEGORY_COLOR[
2137
+ b.category
2138
+ ]}"
2139
+ ></div>
2140
+ <span class="num">{fmt_ms(b.self_ms)} ms</span>
2141
+ </div>
2142
+ {/each}
2143
+ </section>
2144
+
2145
+ {#if meta.trigger === 'page' && meta.runs?.length}
2146
+ <section class="panel">
2147
+ <h2>Renders of {meta.page ?? ''}</h2>
2148
+ <p class="hint">
2149
+ Each run is one full server render, median {fmt_ms(runsMedian)} ms.
2150
+ {#if meta.redirected_from}Followed a redirect from <code>{meta.redirected_from}</code>.{/if}
2151
+ {#if meta.warmup_ms !== undefined}Warm-up render {fmt_ms(meta.warmup_ms)} ms (un-profiled, pays
2152
+ cold module load).{/if}
2153
+ {#if meta.run_status !== undefined}Status {meta.run_status}{#if meta.run_bytes !== undefined},
2154
+ {fmt_bytes(meta.run_bytes)}{/if}.{/if}
2155
+ </p>
2156
+ <p class="fn">{meta.runs.map((r) => fmt_ms(r) + ' ms').join(' · ')}</p>
2157
+ {#if meta.budget_note}<p class="verdict">{meta.budget_note}</p>{/if}
2158
+ {#if meta.cold && cold.length}
2159
+ <h3 class="sub-h">
2160
+ Cold start <span class="hint"
2161
+ >the first render, profiled on its own: {fmt_ms(meta.cold.ms)} ms against {fmt_ms(
2162
+ runsMedian
2163
+ )} ms warm</span
2164
+ >
2165
+ </h3>
2166
+ <p class="hint">
2167
+ What the first render paid over a warm one, per file: module load, compile, first-call
2168
+ caches. On a serverless host every cold instance pays this on its first request.
2169
+ </p>
2170
+ <table>
2171
+ <thead
2172
+ ><tr
2173
+ ><th>file</th><th><span class="sr-only">kind of code</span></th><th class="num"
2174
+ >cold ms</th
2175
+ ><th class="num">warm ms</th><th class="num">extra</th></tr
2176
+ ></thead
2177
+ >
2178
+ <tbody>
2179
+ {#each cold.slice(0, 20) as r (r.file)}
2180
+ <tr>
2181
+ <td class="file">{r.file}</td>
2182
+ <td
2183
+ ><span
2184
+ class="chip"
2185
+ style="background:{CATEGORY_COLOR[r.category]};color:{ink_on(
2186
+ CATEGORY_COLOR[r.category]
2187
+ )}">{CATEGORY_LABEL[r.category]}</span
2188
+ ></td
2189
+ >
2190
+ <td class="num">{fmt_ms(r.cold_ms)}</td>
2191
+ <td class="num">{fmt_ms(r.warm_ms)}</td>
2192
+ <td class="num"><b>+{fmt_ms(r.extra_ms)}</b></td>
2193
+ </tr>
2194
+ {/each}
2195
+ </tbody>
2196
+ </table>
2197
+ {/if}
2198
+ </section>
2199
+ {:else if (meta.trigger === 'request' || meta.trigger === 'trap') && meta.request}
2200
+ <section class="panel">
2201
+ <h2>{meta.trigger === 'trap' ? 'Caught request' : 'Profiled request'}</h2>
2202
+ <p class="fn">
2203
+ {meta.request.method}
2204
+ {meta.request.path} — {fmt_ms(meta.request.ms)} ms (route {meta.request.route ?? '—'})
2205
+ </p>
2206
+ {#if meta.trigger === 'trap'}
2207
+ <p class="hint">
2208
+ The background trap kept this window because the request crossed {meta.trap_over ?? 0} ms. Sampled
2209
+ coarsely (no per-request context, no stacks on calls), so the timeline is the answer here; the
2210
+ tables are rougher than a page profile's.
2211
+ </p>
2212
+ {/if}
2213
+ <p>
2214
+ <a
2215
+ class="btn"
2216
+ href="{base}/replay/{meta.id}"
2217
+ title="Render this request again, five times, under the full profiler — same path and query{extras.replay &&
2218
+ Object.keys(extras.replay.headers).length
2219
+ ? `, with ${Object.keys(extras.replay.headers).join(', ')}`
2220
+ : ''}">Profile this request in full</a
2221
+ >
2222
+ <span class="hint">
2223
+ renders <code>{extras.replay?.path ?? meta.request.path}</code> again as a page profile{#if extras.replay && Object.keys(extras.replay.headers).length}
2224
+ with the kept headers ({Object.keys(extras.replay.headers).join(', ')}){:else}
2225
+ (no headers kept: name them in <code>trap.replay</code> to carry a tenant or a locale along){/if}
2226
+ </span>
2227
+ </p>
2228
+ </section>
2229
+ {/if}
2230
+
2231
+ <section class="panel">
2232
+ <h2>Requests during the window</h2>
2233
+ <p class="hint">
2234
+ Wall-clock time. High total with low net and low CPU = waiting on something we can't see.
2235
+ "inflight" = other requests sharing the CPU at the same time.{#if folded}
2236
+ The page rendered {meta.runs?.length ?? 0} times, so identical requests are folded into one row:
2237
+ <b>×N</b> is how many, the numbers are medians, "max" the slowest of them.{/if}
2238
+ </p>
2239
+ {#if reqs.length}
2240
+ <table>
2241
+ <thead>
2242
+ <tr
2243
+ ><th>method</th><th>path</th><th>route</th>{#if tagged.length}<th>tags</th>{/if}<th
2244
+ class="num">status</th
2245
+ ><th class="num">inflight</th><th class="num">net ms</th><th class="num">cpu ms</th><th
2246
+ class="num">wait ms</th
2247
+ ><th class="num">total ms</th>{#if folded}<th class="num">max ms</th>{/if}</tr
2248
+ >
2249
+ </thead>
2250
+ <tbody>
2251
+ {#each reqs as e}
2252
+ <tr>
2253
+ <td>{e.method}</td>
2254
+ <td class="fn"
2255
+ >{e.path}{#if e.count > 1}<span class="hint">
2256
+ ×{e.count}</span
2257
+ >{/if}{#if e.internal}<span class="warn"> (profiler)</span>{/if}</td
2258
+ >
2259
+ <td class="file">{e.route ?? '—'}</td>
2260
+ {#if tagged.length}<td class="file">{e.tags || '—'}</td>{/if}
2261
+ <td class="num">{e.status || '—'}</td>
2262
+ <td class="num">{e.inflight}</td>
2263
+ <td class="num">{e.net_count ? fmt_ms(e.net_ms) : '—'}</td>
2264
+ <td class="num">{fmt_ms(e.cpu_ms)}</td>
2265
+ <td class="num">{fmt_ms(e.wait_ms)}</td>
2266
+ <td class="num"><b>{fmt_ms(e.ms)}</b></td>
2267
+ {#if folded}<td class="num">{fmt_ms(e.max_ms)}</td>{/if}
2268
+ </tr>
2269
+ {/each}
2270
+ </tbody>
2271
+ </table>
2272
+ {:else}
2273
+ <p class="hint">No requests completed inside the window.</p>
2274
+ {/if}
2275
+ </section>
2276
+
2277
+ <section class="panel">
2278
+ <h2>Flame graph</h2>
2279
+ <p class="hint">
2280
+ Width = time. Click a bar to zoom, click it again to zoom back out. Orange bars are your
2281
+ components.
2282
+ </p>
2283
+ <Flame flame={a.flame} />
2284
+ </section>
2285
+ </Shell>
2286
+
2287
+ <style>
2288
+ .barrow {
2289
+ display: grid;
2290
+ grid-template-columns: minmax(160px, 240px) 1fr 90px;
2291
+ gap: 10px;
2292
+ align-items: center;
2293
+ padding: 3px 0;
2294
+ font-size: 13px;
2295
+ }
2296
+ .barrow .num {
2297
+ text-align: right;
2298
+ font-variant-numeric: tabular-nums;
2299
+ color: var(--text-dim);
2300
+ }
2301
+ .budget {
2302
+ display: flex;
2303
+ height: 34px;
2304
+ border-radius: var(--r-sm);
2305
+ overflow: hidden;
2306
+ border: 1px solid var(--line);
2307
+ margin: 6px 0;
2308
+ }
2309
+ .budget > div {
2310
+ display: flex;
2311
+ align-items: center;
2312
+ justify-content: center;
2313
+ font-size: 10.5px;
2314
+ color: #06120c;
2315
+ font-weight: 600;
2316
+ overflow: hidden;
2317
+ white-space: nowrap;
2318
+ min-width: 0;
2319
+ }
2320
+ .legend {
2321
+ display: flex;
2322
+ flex-wrap: wrap;
2323
+ gap: 6px 14px;
2324
+ margin: 8px 0 0;
2325
+ font-size: 11.5px;
2326
+ color: var(--text-dim);
2327
+ }
2328
+ .legend span {
2329
+ display: inline-flex;
2330
+ align-items: center;
2331
+ gap: 5px;
2332
+ }
2333
+ .legend i {
2334
+ width: 10px;
2335
+ height: 10px;
2336
+ border-radius: 2px;
2337
+ display: inline-block;
2338
+ }
2339
+ .spark {
2340
+ display: block;
2341
+ margin: 6px 0;
2342
+ }
2343
+ .findings-lead {
2344
+ margin: 0 0 12px;
2345
+ font-size: 13.5px;
2346
+ color: var(--text-dim);
2347
+ }
2348
+ .fgrid {
2349
+ display: grid;
2350
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
2351
+ gap: 14px;
2352
+ align-items: start;
2353
+ margin-bottom: 8px;
2354
+ }
2355
+ .fgroup {
2356
+ background: var(--bg-panel);
2357
+ border: 1px solid var(--line);
2358
+ border-radius: var(--r-lg);
2359
+ padding: 12px 14px;
2360
+ min-width: 0;
2361
+ }
2362
+ .fgroup-h {
2363
+ display: flex;
2364
+ align-items: center;
2365
+ gap: 8px;
2366
+ margin: 0 0 8px;
2367
+ font-family: var(--font-body);
2368
+ font-size: 11px;
2369
+ font-weight: 600;
2370
+ text-transform: uppercase;
2371
+ letter-spacing: 0.05em;
2372
+ color: var(--text-faint);
2373
+ }
2374
+ .fgroup-n {
2375
+ font-family: var(--font-mono);
2376
+ font-size: 10.5px;
2377
+ color: var(--text-faint);
2378
+ background: var(--bg-hover);
2379
+ border-radius: 999px;
2380
+ padding: 0 6px;
2381
+ }
2382
+ .finding {
2383
+ padding: 8px 0;
2384
+ border-top: 1px solid var(--line);
2385
+ }
2386
+ .finding:first-of-type {
2387
+ border-top: 0;
2388
+ padding-top: 0;
2389
+ }
2390
+ .fmsg {
2391
+ margin: 0;
2392
+ font-size: 13px;
2393
+ line-height: 1.45;
2394
+ color: var(--text);
2395
+ overflow-wrap: anywhere;
2396
+ }
2397
+ .finding.warn .fmsg {
2398
+ color: var(--text);
2399
+ }
2400
+ .finding.warn {
2401
+ border-left: 2px solid var(--warn);
2402
+ padding-left: 10px;
2403
+ margin-left: -12px;
2404
+ }
2405
+ .ffix {
2406
+ margin: 3px 0 0;
2407
+ font-size: 12.5px;
2408
+ line-height: 1.4;
2409
+ color: var(--text-dim);
2410
+ }
2411
+ .fshow {
2412
+ font-size: 12px;
2413
+ }
2414
+ .since {
2415
+ border: 1px solid var(--line);
2416
+ border-radius: 8px;
2417
+ padding: 10px 14px;
2418
+ margin: 0 0 12px;
2419
+ font-size: 13.5px;
2420
+ }
2421
+ .since p {
2422
+ margin: 0 0 6px;
2423
+ }
2424
+ .since ul {
2425
+ margin: 0 0 8px;
2426
+ padding-left: 18px;
2427
+ }
2428
+ .since .status {
2429
+ font-size: 11px;
2430
+ text-transform: uppercase;
2431
+ color: var(--text-dim);
2432
+ margin-right: 6px;
2433
+ }
2434
+ .start {
2435
+ margin: 0 0 16px;
2436
+ padding: 12px 16px 12px 34px;
2437
+ background: var(--bg-panel);
2438
+ border: 1px solid var(--line);
2439
+ border-radius: var(--r-lg);
2440
+ display: grid;
2441
+ gap: 6px;
2442
+ font-size: 14px;
2443
+ }
2444
+ .start li {
2445
+ display: flex;
2446
+ flex-wrap: wrap;
2447
+ gap: 2px 10px;
2448
+ align-items: baseline;
2449
+ min-width: 0;
2450
+ }
2451
+ .start li::marker {
2452
+ color: var(--c-orange);
2453
+ font-weight: 700;
2454
+ }
2455
+ .start code {
2456
+ font-family: var(--font-mono);
2457
+ font-size: 12.5px;
2458
+ color: var(--text-dim);
2459
+ overflow-wrap: anywhere;
2460
+ }
2461
+ .start .worth {
2462
+ color: var(--c-orange);
2463
+ font-size: 13px;
2464
+ }
2465
+ .start .then {
2466
+ font-family: var(--font-mono, ui-monospace, monospace);
2467
+ font-size: 12px;
2468
+ color: var(--c-green, currentColor);
2469
+ margin-left: 6px;
2470
+ }
2471
+ .impact {
2472
+ margin: 0 0 8px;
2473
+ font-size: 14px;
2474
+ }
2475
+ .impact b {
2476
+ color: var(--c-orange);
2477
+ }
2478
+ .fpat {
2479
+ color: var(--c-orange);
2480
+ margin-right: 10px;
2481
+ }
2482
+ .ffile {
2483
+ font-family: var(--font-mono);
2484
+ font-size: 11.5px;
2485
+ color: var(--text-faint);
2486
+ margin-left: 6px;
2487
+ /* a full file:// path must wrap inside its card, never widen the page */
2488
+ overflow-wrap: anywhere;
2489
+ }
2490
+ .scanned {
2491
+ overflow-wrap: anywhere;
2492
+ }
2493
+ .history {
2494
+ display: flex;
2495
+ gap: 16px;
2496
+ align-items: center;
2497
+ flex-wrap: wrap;
2498
+ }
2499
+ .hverdict.warn,
2500
+ .lverdict.warn {
2501
+ color: var(--warn);
2502
+ }
2503
+ .lverdict.note {
2504
+ color: var(--c-blue);
2505
+ }
2506
+ .lverdict.good {
2507
+ color: var(--good);
2508
+ }
2509
+ code.props {
2510
+ font-size: 11px;
2511
+ color: var(--text-dim);
2512
+ }
2513
+ .sub-h {
2514
+ font-size: 14px;
2515
+ margin: 14px 0 4px;
2516
+ }
2517
+ .sub-h .hint {
2518
+ font-weight: 400;
2519
+ margin-left: 6px;
2520
+ }
2521
+ tr.by td {
2522
+ background: var(--bg-sunken);
2523
+ padding: 6px 14px 10px 28px;
2524
+ border-bottom: 1px solid var(--line);
2525
+ }
2526
+ .dimcell {
2527
+ color: var(--text-faint);
2528
+ }
2529
+ .bygrid {
2530
+ display: grid;
2531
+ grid-template-columns: minmax(120px, max-content) minmax(140px, 1fr) 80px 80px 80px 64px 64px;
2532
+ gap: 3px 14px;
2533
+ align-items: center;
2534
+ font-size: 12px;
2535
+ max-width: 760px;
2536
+ }
2537
+ .bygrid .byh {
2538
+ color: var(--text-faint);
2539
+ font-size: 10.5px;
2540
+ text-transform: uppercase;
2541
+ letter-spacing: 0.04em;
2542
+ padding-bottom: 2px;
2543
+ border-bottom: 1px solid var(--line);
2544
+ }
2545
+ .bygrid .num {
2546
+ text-align: right;
2547
+ font-variant-numeric: tabular-nums;
2548
+ color: var(--text-dim);
2549
+ }
2550
+ .bygrid .byv code {
2551
+ font-size: 11.5px;
2552
+ }
2553
+ .bybar {
2554
+ height: 6px;
2555
+ background: var(--bg-sunken);
2556
+ border-radius: 3px;
2557
+ overflow: hidden;
2558
+ }
2559
+ .bybar i {
2560
+ display: block;
2561
+ height: 100%;
2562
+ background: var(--warn);
2563
+ border-radius: 3px;
2564
+ }
2565
+ .dotflag-inline {
2566
+ display: inline-block;
2567
+ width: 14px;
2568
+ height: 14px;
2569
+ line-height: 14px;
2570
+ text-align: center;
2571
+ border-radius: 50%;
2572
+ background: var(--warn);
2573
+ color: #06120c;
2574
+ font-weight: 700;
2575
+ font-size: 10px;
2576
+ }
2577
+ table.weight td.file {
2578
+ font-family: var(--mono, ui-monospace, monospace);
2579
+ max-width: 16rem;
2580
+ overflow: hidden;
2581
+ text-overflow: ellipsis;
2582
+ white-space: nowrap;
2583
+ }
2584
+ /* files that load later read quieter — in the faint ink, never by fading it below a readable 4.5:1 */
2585
+ table.weight tr.lazy td {
2586
+ color: var(--text-faint);
2587
+ }
2588
+ table.weight .num {
2589
+ text-align: right;
2590
+ white-space: nowrap;
2591
+ }
2592
+ </style>