@ogygia/devtools 0.0.0-stage → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -2
  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 +103 -3
@@ -0,0 +1,182 @@
1
+ <script>/**
2
+ * THE DOCUMENT AS A BYTE STRIP — one bar, every byte coloured by what it is (byte-strip.ts),
3
+ * with the time each part left the server (the strip is in write order) and, when this
4
+ * browser's visit is known, the time it arrived. A `wake:'load'` island: hover a segment, click
5
+ * a kind in the legend to light only that kind.
6
+ */
7
+ import { arrival_ms, left_at } from "../byte-strip.js";
8
+ import { fmt_bytes, fmt_ms } from "./format.js";
9
+ let { strip, names = {}, nav = null } = $props();
10
+ const COLOR = {
11
+ head: "#374151",
12
+ style: "#d9a03d",
13
+ "css-link": "#b8860b",
14
+ runtime: "#8b5cf6",
15
+ script: "#f0883e",
16
+ seed: "#58a6ff",
17
+ "remote-seed": "#1f6feb",
18
+ props: "#79c0ff",
19
+ holes: "#a371f7",
20
+ island: "#2ea043",
21
+ lake: "#3fb950",
22
+ hole: "#56d364",
23
+ shadow: "#ff7b72",
24
+ markup: "#4b5563"
25
+ };
26
+ const LABEL = {
27
+ head: "head markup",
28
+ style: "inline styles",
29
+ "css-link": "stylesheet links",
30
+ runtime: "runtime bootstrap",
31
+ script: "other scripts",
32
+ seed: "page seed",
33
+ "remote-seed": "remote seed",
34
+ props: "island props",
35
+ holes: "holes record",
36
+ island: "island markup",
37
+ lake: "lakes",
38
+ hole: "holes",
39
+ shadow: "shadow DOM",
40
+ markup: "markup"
41
+ };
42
+ const kinds = $derived(Object.keys(strip.by_kind).filter((k) => (strip.by_kind[k] ?? 0) > 0 && k !== "shadow").sort((a, b) => (strip.by_kind[b] ?? 0) - (strip.by_kind[a] ?? 0)));
43
+ let lit = $state(null);
44
+ let tip = $state(null);
45
+ const pct = (n) => strip.total ? n / strip.total * 100 : 0;
46
+ const name_of = (s) => s.kind === "island" && s.label ? names[s.label] ?? s.label.slice(0, 8) : s.label ?? "";
47
+ const islands = $derived(strip.segments.filter((s) => s.kind === "island"));
48
+ </script>
49
+
50
+ <div
51
+ class="strip"
52
+ role="img"
53
+ aria-label="the document, byte by byte"
54
+ onmouseleave={() => (tip = null)}
55
+ >
56
+ {#each strip.segments as s, i (i)}
57
+ <div
58
+ class="seg"
59
+ class:dim={lit && s.kind !== lit}
60
+ style="width:{Math.max(pct(s.end - s.start), 0.15)}%;background:{COLOR[s.kind]}"
61
+ role="presentation"
62
+ onmouseenter={(e) => (tip = { s, x: e.clientX, y: e.clientY })}
63
+ onmousemove={(e) => (tip = { s, x: e.clientX, y: e.clientY })}
64
+ ></div>
65
+ {/each}
66
+ </div>
67
+ {#if tip}
68
+ <div class="tip" style="left:{tip.x + 12}px;top:{tip.y + 12}px">
69
+ <b>{LABEL[tip.s.kind]}{name_of(tip.s) ? ` · ${name_of(tip.s)}` : ''}</b><br />
70
+ {fmt_bytes(tip.s.end - tip.s.start)} · {pct(tip.s.end - tip.s.start).toFixed(1)}% of the
71
+ document · bytes {tip.s.start.toLocaleString()}–{tip.s.end.toLocaleString()}
72
+ {#if strip.chunks}{@const l0 = left_at(tip.s.start, strip.chunks)}{@const l1 = left_at(
73
+ Math.max(tip.s.start, tip.s.end - 1),
74
+ strip.chunks
75
+ )}{#if l0 !== undefined && l1 !== undefined}<br />left the server {fmt_ms(l0)}{l1 !== l0
76
+ ? ` → ${fmt_ms(l1)}`
77
+ : ''} ms into the render{/if}{/if}
78
+ {#if nav}{@const a = arrival_ms(tip.s, strip.total, nav.res_start, nav.res_end)}<br />arrives {fmt_ms(
79
+ a.first
80
+ )} → {fmt_ms(a.last)} ms after the click (steady download assumed){/if}
81
+ </div>
82
+ {/if}
83
+ {#if strip.chunks}
84
+ <p class="hint">
85
+ Streamed in {strip.chunks.length} chunks: the first left the server {fmt_ms(strip.chunks[0].t)} ms
86
+ into the render, the last {fmt_ms(strip.chunks[strip.chunks.length - 1].t)} ms. Hover a segment for
87
+ when its bytes left.
88
+ </p>
89
+ {/if}
90
+ <div class="legend">
91
+ {#each kinds as k (k)}
92
+ <button
93
+ class="key"
94
+ class:on={lit === k}
95
+ onclick={() => (lit = lit === k ? null : k)}
96
+ title="click to light only this kind"
97
+ >
98
+ <i style="background:{COLOR[k]}"></i>{LABEL[k]} <b>{fmt_bytes(strip.by_kind[k] ?? 0)}</b>
99
+ <span class="hint">{pct(strip.by_kind[k] ?? 0).toFixed(0)}%</span>
100
+ </button>
101
+ {/each}
102
+ {#if strip.shadow_count}
103
+ <span class="key static"
104
+ ><i style="background:{COLOR.shadow}"></i>shadow DOM inside the above
105
+ <b>{fmt_bytes(strip.shadow_bytes)}</b>
106
+ <span class="hint">{strip.shadow_count} roots</span></span
107
+ >
108
+ {/if}
109
+ <span class="hint">total {fmt_bytes(strip.total)}</span>
110
+ </div>
111
+ {#if islands.length}
112
+ <p class="hint">
113
+ Islands in document order: {#each islands as s, i (i)}{#if i > 0},
114
+ {/if}<b>{name_of(s)}</b>
115
+ {fmt_bytes(s.end - s.start)}{/each}.
116
+ {#if nav}The last byte arrives {fmt_ms(nav.res_end)} ms after the click; everything after the seed
117
+ and the props tail cannot paint before they do.{/if}
118
+ </p>
119
+ {/if}
120
+
121
+ <style>
122
+ .strip {
123
+ display: flex;
124
+ height: 26px;
125
+ border-radius: 4px;
126
+ overflow: hidden;
127
+ border: 1px solid var(--line);
128
+ background: var(--bg-sunken);
129
+ }
130
+ .seg {
131
+ height: 100%;
132
+ min-width: 1px;
133
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.35);
134
+ transition: opacity 0.12s;
135
+ }
136
+ .seg.dim {
137
+ opacity: 0.15;
138
+ }
139
+ .legend {
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ gap: 6px 14px;
143
+ margin: 8px 0 4px;
144
+ font-size: 12px;
145
+ align-items: center;
146
+ }
147
+ .key {
148
+ background: none;
149
+ border: 0;
150
+ color: inherit;
151
+ font: inherit;
152
+ padding: 0;
153
+ cursor: pointer;
154
+ display: inline-flex;
155
+ align-items: center;
156
+ gap: 5px;
157
+ }
158
+ .key.on {
159
+ text-decoration: underline;
160
+ }
161
+ /* a legend line that is not a filter: no hand, nothing to press */
162
+ .key.static {
163
+ cursor: default;
164
+ }
165
+ .key i {
166
+ width: 10px;
167
+ height: 10px;
168
+ border-radius: 2px;
169
+ display: inline-block;
170
+ }
171
+ .tip {
172
+ position: fixed;
173
+ z-index: 20;
174
+ background: var(--bg-raised);
175
+ border: 1px solid var(--line-strong);
176
+ border-radius: 6px;
177
+ padding: 6px 8px;
178
+ font-size: 12px;
179
+ max-width: 380px;
180
+ pointer-events: none;
181
+ }
182
+ </style>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * THE DOCUMENT AS A BYTE STRIP — one bar, every byte coloured by what it is (byte-strip.ts),
3
+ * with the time each part left the server (the strip is in write order) and, when this
4
+ * browser's visit is known, the time it arrived. A `wake:'load'` island: hover a segment, click
5
+ * a kind in the legend to light only that kind.
6
+ */
7
+ import { type ByteStrip } from '../byte-strip.js';
8
+ type $$ComponentProps = {
9
+ strip: ByteStrip;
10
+ names?: Record<string, string>;
11
+ nav?: {
12
+ res_start: number;
13
+ res_end: number;
14
+ } | null;
15
+ };
16
+ declare const ByteStrip: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type ByteStrip = ReturnType<typeof ByteStrip>;
18
+ export default ByteStrip;
@@ -0,0 +1,24 @@
1
+ <script>/**
2
+ * COMPARE two reports (`/compare/[a]/[b]`). The server's comparison when it holds both reports
3
+ * (static render, CompareBody); otherwise the LocalCompare island builds the same comparison
4
+ * from this browser's store — a restarted server or an ephemeral host forgets, the browser does
5
+ * not.
6
+ */
7
+ import Shell from "./Shell.svelte";
8
+ import CompareBody from "./CompareBody.svelte";
9
+ import LocalCompare from "./LocalCompare.svelte" with { wake: "load" };
10
+ let { data } = $props();
11
+ const { base, cmp, a, b } = $derived(data);
12
+ </script>
13
+
14
+ <svelte:head
15
+ ><title>compare — {cmp ? `${cmp.a.label} vs ${cmp.b.label}` : `${a} vs ${b}`}</title></svelte:head
16
+ >
17
+
18
+ <Shell {base}>
19
+ {#if cmp}
20
+ <CompareBody {base} {cmp} />
21
+ {:else}
22
+ <LocalCompare {base} {a} {b} />
23
+ {/if}
24
+ </Shell>
@@ -0,0 +1,16 @@
1
+ declare const Compare: import("svelte").Component<{
2
+ data: {
3
+ base: string;
4
+ cmp: import("../compare.js").Comparison | null;
5
+ a: string;
6
+ b: string;
7
+ };
8
+ form: null;
9
+ params: {
10
+ a: string;
11
+ b: string;
12
+ };
13
+ search: {};
14
+ }, {}, "">;
15
+ type Compare = ReturnType<typeof Compare>;
16
+ export default Compare;
@@ -0,0 +1,429 @@
1
+ <script>/**
2
+ * THE COMPARISON BODY: summary deltas, phases, components and functions side by side, findings
3
+ * that appeared or went away. One renderer, two sources — the server's comparison (Compare
4
+ * page) or one computed in the browser from the store (LocalCompare island). A positive delta
5
+ * is a regression (b slower than a) and reads red.
6
+ */
7
+ import { fmt_ms } from "./format.js";
8
+ let { base, cmp } = $props();
9
+ const when = (ms) => new Date(ms).toLocaleString();
10
+ const fmt = (n, unit) => unit === "ms" ? fmt_ms(n) + " ms" : unit === "KB" ? Math.round(n) + " KB" : String(Math.round(n));
11
+ const sign = (d, unit) => (d > 0 ? "+" : "") + (unit === "ms" ? fmt_ms(d) : Math.round(d));
12
+ const pct = (a, d) => a > 0 ? ` (${d > 0 ? "+" : ""}${(d / a * 100).toFixed(0)}%)` : "";
13
+ const cls = (d, floor = .5) => d > floor ? "worse" : d < -floor ? "better" : "";
14
+ const comps = $derived(cmp.components.filter((r) => Math.abs(r.d_self) >= .05 || Math.abs(r.d_total) >= .5).slice(0, 40));
15
+ const fns = $derived(cmp.functions.filter((r) => Math.abs(r.d_self) >= .05).slice(0, 40));
16
+ const mb = (b) => !b ? "—" : b >= 1048576 ? (b / 1048576).toFixed(1) + " MB" : Math.round(b / 1024) + " KB";
17
+ </script>
18
+
19
+ <h1>Compare <small>{cmp.a.label} → {cmp.b.label}</small></h1>
20
+ <p class="hint">
21
+ <a href={base}>← dashboard</a> ·
22
+ <b>A</b> <a href="{base}/report/{cmp.a.id}">{cmp.a.label}</a> ({when(cmp.a.created)}) ·
23
+ <b>B</b> <a href="{base}/report/{cmp.b.id}">{cmp.b.label}</a> ({when(cmp.b.created)}) ·
24
+ <a href="{base}/compare/{cmp.b.id}/{cmp.a.id}">swap</a>. A positive delta means B is slower.
25
+ </p>
26
+
27
+ <h2>Summary</h2>
28
+ <table>
29
+ <thead
30
+ ><tr
31
+ ><th><span class="sr-only">what</span></th><th class="num">A</th><th class="num">B</th><th
32
+ class="num">Δ</th
33
+ ></tr
34
+ ></thead
35
+ >
36
+ <tbody>
37
+ {#each cmp.summary as r (r.label)}
38
+ <tr>
39
+ <td class="fn">{r.label}</td>
40
+ <td class="num">{fmt(r.a, r.unit)}</td>
41
+ <td class="num">{fmt(r.b, r.unit)}</td>
42
+ <td class="num {cls(r.d, r.unit === 'ms' ? 0.5 : 0)}"
43
+ ><b>{sign(r.d, r.unit)}</b>{pct(r.a, r.d)}</td
44
+ >
45
+ </tr>
46
+ {/each}
47
+ </tbody>
48
+ </table>
49
+
50
+ {#if cmp.order}
51
+ {@const o = cmp.order}
52
+ <!-- the run order trap: the later report ran on the heap the earlier one's renders filled -->
53
+ <p class="same-render">
54
+ <b>Run order changes this result.</b> Both ran on one server, and {o.earlier.toUpperCase()} kept {o.kept_mb}
55
+ MB alive on every render.
56
+ {o.earlier === 'a' ? 'B' : 'A'} ran {o.requests_between} requests later on that fuller heap (more
57
+ garbage collection, slower allocation), so it reads slower than its code is. Restart the server between
58
+ the two, or profile them in the other order, before trusting the difference.
59
+ </p>
60
+ {/if}
61
+
62
+ {#if cmp.render_same}
63
+ <!-- the ground truth: did the render get faster? When not, per-line moves are attribution -->
64
+ <p class="same-render">
65
+ <b>No real change in render time</b> ({fmt_ms(cmp.render_same.a_ms)} ms → {fmt_ms(
66
+ cmp.render_same.b_ms
67
+ )} ms, within the runs' own spread of {fmt_ms(cmp.render_same.noise_ms)} ms). V8 decides what to inline
68
+ differently from run to run, so the same CPU time can be charged to a different line: the moves below
69
+ are marked <i>shifted</i>, not fixed or worse. Waiting and memory keep their verdicts.
70
+ </p>
71
+ {/if}
72
+
73
+ {#if cmp.fix_check && !cmp.render_same}
74
+ {@const f = cmp.fix_check}
75
+ <!-- did the fix pay: what the fixed patterns promised against the render's measured change -->
76
+ <p class="fix-check {f.verdict}">
77
+ <b>
78
+ {#if f.verdict === 'as-expected'}The fix paid off as expected{:else if f.verdict === 'less'}The
79
+ fix saved less than expected{:else}The fix saved more than expected{/if}
80
+ </b>: the patterns that went away or shrank were expected to save about {fmt_ms(f.predicted_ms)} ms
81
+ per render, and the median render got {f.measured_ms >= 0
82
+ ? `${fmt_ms(f.measured_ms)} ms faster`
83
+ : `${fmt_ms(-f.measured_ms)} ms slower`}.
84
+ {#if f.verdict === 'less' && f.skewed}
85
+ Most likely the run order above, not the fix: B ran on the heap A filled, so it reads slower
86
+ than its code is. Restart the server and profile B again first.
87
+ {:else if f.skewed}
88
+ B ran on the heap A filled (see above), so the real saving is likely larger still.
89
+ {:else if f.verdict === 'less'}
90
+ The time most likely moved: look for lines and patterns that got worse below, or a wait that
91
+ grew. If nothing did, profile both again with more renders; the runs may just be noisy.
92
+ {:else if f.verdict === 'more'}
93
+ The change removed more than the patterns saw: a cost that sat under the reporting floor, or
94
+ less garbage to collect.
95
+ {/if}
96
+ </p>
97
+ {/if}
98
+
99
+ {#if cmp.patterns?.length}
100
+ <h2>
101
+ Slow patterns <span class="hint" style="font-weight:400"
102
+ >(what the change fixed, and what it brought in)</span
103
+ >
104
+ </h2>
105
+ <table>
106
+ <thead
107
+ ><tr
108
+ ><th><span class="sr-only">what</span></th><th>pattern</th><th class="num">A</th><th
109
+ class="num">B</th
110
+ ><th class="num">Δ</th></tr
111
+ ></thead
112
+ >
113
+ <tbody>
114
+ {#each cmp.patterns as p (p.kind + '\0' + p.title)}
115
+ <tr>
116
+ <td class="status {p.status}">{p.status}</td>
117
+ <td class="fn">{p.title}</td>
118
+ <td class="num">{p.a_ms ? fmt_ms(p.a_ms) + ' ms' : '—'}</td>
119
+ <td class="num">{p.b_ms ? fmt_ms(p.b_ms) + ' ms' : '—'}</td>
120
+ <td class="num {cls(p.d_ms)}"
121
+ ><b>{sign(p.d_ms, 'ms')}</b>{#if p.wait}&nbsp;waiting{/if}</td
122
+ >
123
+ </tr>
124
+ {/each}
125
+ </tbody>
126
+ </table>
127
+ {/if}
128
+
129
+ {#if cmp.drill?.length}
130
+ <h2>
131
+ Where one render's time moved <span class="hint" style="font-weight:400"
132
+ >(the rows of the drill-down that changed, down to the call or line that did it; one render
133
+ each side)</span
134
+ >
135
+ </h2>
136
+ <table>
137
+ <thead
138
+ ><tr
139
+ ><th><span class="sr-only">what</span></th><th>where</th><th class="num">A</th><th
140
+ class="num">B</th
141
+ ><th class="num">Δ</th></tr
142
+ ></thead
143
+ >
144
+ <tbody>
145
+ {#each cmp.drill as r, i (r.path.join('\0') + '\0' + i)}
146
+ <tr>
147
+ <td class="status {r.status}">{r.status}</td>
148
+ <td class="fn">
149
+ <span class="hint"
150
+ >{r.path.slice(0, -1).join(' › ')}{r.path.length > 1 ? ' › ' : ''}</span
151
+ ><b
152
+ >{r.kind === 'line'
153
+ ? (r.at ?? r.path[r.path.length - 1])
154
+ : r.path[r.path.length - 1]}</b
155
+ >
156
+ {#if r.at && r.kind !== 'line' && r.kind !== 'lane'}<div class="code">{r.at}</div>{/if}
157
+ </td>
158
+ <!-- ≤: that side only had an "N more" fold, so the row is at most that -->
159
+ <td class="num">{r.upto === 'a' ? '≤ ' : ''}{r.a_ms ? fmt_ms(r.a_ms) + ' ms' : '—'}</td>
160
+ <td class="num">{r.upto === 'b' ? '≤ ' : ''}{r.b_ms ? fmt_ms(r.b_ms) + ' ms' : '—'}</td>
161
+ <td class="num {cls(r.d_ms)}"><b>{r.upto ? 'at least ' : ''}{sign(r.d_ms, 'ms')}</b></td>
162
+ </tr>
163
+ {/each}
164
+ </tbody>
165
+ </table>
166
+ {/if}
167
+
168
+ {#if cmp.lines?.filter((l) => l.status !== 'same').length}
169
+ <h2>
170
+ The exact lines <span class="hint" style="font-weight:400"
171
+ >(your costliest lines, per render, matched by their code so an edit above them does not lose
172
+ them)</span
173
+ >
174
+ </h2>
175
+ <table>
176
+ <thead
177
+ ><tr
178
+ ><th><span class="sr-only">what</span></th><th>line</th><th class="num">A</th><th
179
+ class="num">B</th
180
+ ><th class="num">Δ</th><th class="num">made A → B</th><th class="num">kept A → B</th></tr
181
+ ></thead
182
+ >
183
+ <tbody>
184
+ {#each cmp.lines.filter((l) => l.status !== 'same') as l (l.file + '\0' + l.code + '\0' + l.line)}
185
+ <tr>
186
+ <td class="status {l.status}">{l.status}</td>
187
+ <td class="fn"
188
+ ><code>{l.file}:{l.line}</code>
189
+ <div class="code">{l.code}</div></td
190
+ >
191
+ <td class="num">{l.a_ms ? fmt_ms(l.a_ms) + ' ms' : '—'}</td>
192
+ <td class="num">{l.b_ms ? fmt_ms(l.b_ms) + ' ms' : '—'}</td>
193
+ <td class="num {cls(l.d_ms)}"><b>{sign(l.d_ms, 'ms')}</b></td>
194
+ <td class="num">{mb(l.a_bytes)} → {mb(l.b_bytes)}</td>
195
+ <td class="num">{mb(l.a_kept)} → {mb(l.b_kept)}</td>
196
+ </tr>
197
+ {/each}
198
+ </tbody>
199
+ </table>
200
+ {/if}
201
+
202
+ {#if cmp.phases.length}
203
+ <h2>Phases</h2>
204
+ <table>
205
+ <thead
206
+ ><tr><th>phase</th><th class="num">A</th><th class="num">B</th><th class="num">Δ</th></tr
207
+ ></thead
208
+ >
209
+ <tbody>
210
+ {#each cmp.phases as p (p.phase)}
211
+ <tr>
212
+ <td class="fn">{p.label}</td>
213
+ <td class="num">{fmt_ms(p.a)} ms</td>
214
+ <td class="num">{fmt_ms(p.b)} ms</td>
215
+ <td class="num {cls(p.d)}"><b>{sign(p.d, 'ms')}</b>{pct(p.a, p.d)}</td>
216
+ </tr>
217
+ {/each}
218
+ </tbody>
219
+ </table>
220
+ {/if}
221
+
222
+ {#if cmp.findings.added.length || cmp.findings.gone.length}
223
+ <h2>Findings</h2>
224
+ {#each cmp.findings.added as f (f)}<p class="finding worse">+ {f}</p>{/each}
225
+ {#each cmp.findings.gone as f (f)}<p class="finding better">− {f}</p>{/each}
226
+ {/if}
227
+
228
+ {#if cmp.paths.length}
229
+ <h2>Paths to fix <span class="hint" style="font-weight:400">(by change)</span></h2>
230
+ <p class="hint">
231
+ Each path is the hot functions under one caller. A path that shrank is a fix that landed; one
232
+ that appeared is new work under that caller.
233
+ </p>
234
+ <table>
235
+ <thead
236
+ ><tr
237
+ ><th>caller</th><th>where</th><th class="num">A</th><th class="num">B</th><th class="num"
238
+ >Δ</th
239
+ ><th>functions</th></tr
240
+ ></thead
241
+ >
242
+ <tbody>
243
+ {#each cmp.paths as p (p.owner)}
244
+ <tr>
245
+ <td class="fn"
246
+ ><b>{p.owner}</b>{#if p.only}<span class="hint">
247
+ (only in {p.only.toUpperCase()})</span
248
+ >{/if}</td
249
+ >
250
+ <td class="file"
251
+ >{p.file}{#if p.line > 0}:{p.line}{/if}</td
252
+ >
253
+ <td class="num">{p.a_ms ? fmt_ms(p.a_ms) : '—'}</td>
254
+ <td class="num">{p.b_ms ? fmt_ms(p.b_ms) : '—'}</td>
255
+ <td class="num {cls(p.d_ms, 1)}"><b>{sign(p.d_ms, 'ms')}</b>{pct(p.a_ms, p.d_ms)}</td>
256
+ <td class="hint">{(p.b_fns.length ? p.b_fns : p.a_fns).slice(0, 6).join(', ')}</td>
257
+ </tr>
258
+ {/each}
259
+ </tbody>
260
+ </table>
261
+ {/if}
262
+
263
+ {#if cmp.gc_makers.length}
264
+ <h2>
265
+ Garbage makers <span class="hint" style="font-weight:400"
266
+ >(what each line allocated and the GC it caused, by change)</span
267
+ >
268
+ </h2>
269
+ <table>
270
+ <thead
271
+ ><tr
272
+ ><th>line</th><th>in</th><th class="num">A allocated</th><th class="num">B allocated</th><th
273
+ class="num">Δ</th
274
+ ><th class="num">A GC ms</th><th class="num">B GC ms</th><th class="num">Δ</th></tr
275
+ ></thead
276
+ >
277
+ <tbody>
278
+ {#each cmp.gc_makers.slice(0, 25) as m (m.name + m.caller)}
279
+ <tr>
280
+ <td class="fn"
281
+ ><b>{m.name}</b>{#if m.caller}<span class="hint">
282
+ ← {m.caller}</span
283
+ >{/if}{#if m.only}<span class="hint"> (only in {m.only.toUpperCase()})</span>{/if}</td
284
+ >
285
+ <td class="fn">{m.component ?? '—'}</td>
286
+ <td class="num">{m.a_mb} MB</td>
287
+ <td class="num">{m.b_mb} MB</td>
288
+ <td class="num {cls(m.d_mb, 0.5)}"
289
+ ><b>{m.d_mb > 0 ? '+' : ''}{m.d_mb} MB</b>{pct(m.a_mb, m.d_mb)}</td
290
+ >
291
+ <td class="num">{fmt_ms(m.a_gc_ms)}</td>
292
+ <td class="num">{fmt_ms(m.b_gc_ms)}</td>
293
+ <td class="num {cls(m.d_gc_ms, 0.5)}"><b>{sign(m.d_gc_ms, 'ms')}</b></td>
294
+ </tr>
295
+ {/each}
296
+ </tbody>
297
+ </table>
298
+ {/if}
299
+
300
+ <h2>Components <span class="hint" style="font-weight:400">(by change in self time)</span></h2>
301
+ {#if comps.length}
302
+ <table>
303
+ <thead>
304
+ <tr
305
+ ><th>component</th><th class="num">self A</th><th class="num">self B</th><th class="num"
306
+ >Δ self</th
307
+ ><th class="num">total A</th><th class="num">total B</th><th class="num">Δ total</th><th
308
+ class="num">renders</th
309
+ ></tr
310
+ >
311
+ </thead>
312
+ <tbody>
313
+ {#each comps as r (r.name)}
314
+ <tr>
315
+ <td class="fn"
316
+ ><b>{r.name}</b>{#if r.only}<span class="hint">
317
+ only in {r.only.toUpperCase()}</span
318
+ >{/if}</td
319
+ >
320
+ <td class="num">{fmt_ms(r.a_self)}</td>
321
+ <td class="num">{fmt_ms(r.b_self)}</td>
322
+ <td class="num {cls(r.d_self, 0.05)}"><b>{sign(r.d_self, 'ms')}</b></td>
323
+ <td class="num">{fmt_ms(r.a_total)}</td>
324
+ <td class="num">{fmt_ms(r.b_total)}</td>
325
+ <td class="num {cls(r.d_total)}">{sign(r.d_total, 'ms')}</td>
326
+ <td class="num">{r.a_calls ?? '—'} → {r.b_calls ?? '—'}</td>
327
+ </tr>
328
+ {/each}
329
+ </tbody>
330
+ </table>
331
+ {:else}
332
+ <p class="hint">No component changed by more than 0.05 ms.</p>
333
+ {/if}
334
+
335
+ <h2>Functions <span class="hint" style="font-weight:400">(by change in self time)</span></h2>
336
+ {#if fns.length}
337
+ <table>
338
+ <thead>
339
+ <tr
340
+ ><th>function</th><th>where</th><th class="num">self A</th><th class="num">self B</th><th
341
+ class="num">Δ</th
342
+ ><th class="num">calls</th></tr
343
+ >
344
+ </thead>
345
+ <tbody>
346
+ {#each fns as r (r.key ?? r.name + r.file + ':' + r.line)}
347
+ <tr>
348
+ <td class="fn"
349
+ ><b>{r.name}</b>{#if r.only}<span class="hint">
350
+ only in {r.only.toUpperCase()}</span
351
+ >{/if}</td
352
+ >
353
+ <td class="file"
354
+ >{r.file}{#if r.line > 0}:{r.line}{/if}</td
355
+ >
356
+ <td class="num">{fmt_ms(r.a_self)}</td>
357
+ <td class="num">{fmt_ms(r.b_self)}</td>
358
+ <td class="num {cls(r.d_self, 0.05)}"
359
+ ><b>{sign(r.d_self, 'ms')}</b>{pct(r.a_self, r.d_self)}</td
360
+ >
361
+ <td class="num">{r.a_calls ?? '—'} → {r.b_calls ?? '—'}</td>
362
+ </tr>
363
+ {/each}
364
+ </tbody>
365
+ </table>
366
+ {:else}
367
+ <p class="hint">No function changed by more than 0.05 ms.</p>
368
+ {/if}
369
+
370
+ <style>
371
+ .status {
372
+ font-size: 12px;
373
+ font-weight: 600;
374
+ white-space: nowrap;
375
+ color: var(--text-faint);
376
+ }
377
+ .status.fixed,
378
+ .status.better {
379
+ color: var(--good);
380
+ }
381
+ .status.new,
382
+ .status.worse {
383
+ color: var(--bad);
384
+ }
385
+ .status.shifted {
386
+ color: var(--text-faint);
387
+ font-style: italic;
388
+ }
389
+ .fix-check {
390
+ margin: 12px 0;
391
+ padding: 10px 14px;
392
+ border: 1px solid var(--line);
393
+ border-left: 3px solid var(--c-green, #4a9d6e);
394
+ border-radius: 8px;
395
+ background: var(--bg-panel);
396
+ font-size: 13.5px;
397
+ line-height: 1.5;
398
+ }
399
+ .fix-check.less {
400
+ border-left-color: var(--c-amber, #d4a017);
401
+ }
402
+ .same-render {
403
+ margin: 12px 0;
404
+ padding: 10px 14px;
405
+ border: 1px solid var(--line);
406
+ border-radius: 8px;
407
+ background: var(--bg-panel);
408
+ font-size: 13.5px;
409
+ line-height: 1.5;
410
+ }
411
+ .code {
412
+ font-family: ui-monospace, monospace;
413
+ font-size: 11.5px;
414
+ color: var(--text-faint);
415
+ white-space: pre-wrap;
416
+ word-break: break-word;
417
+ max-width: 520px;
418
+ }
419
+ .worse {
420
+ color: var(--bad);
421
+ }
422
+ .better {
423
+ color: var(--good);
424
+ }
425
+ .finding {
426
+ margin: 4px 0;
427
+ font-size: 13px;
428
+ }
429
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { Comparison } from '../compare.js';
2
+ type $$ComponentProps = {
3
+ base: string;
4
+ cmp: Comparison;
5
+ };
6
+ declare const CompareBody: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type CompareBody = ReturnType<typeof CompareBody>;
8
+ export default CompareBody;