@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,220 @@
1
+ import { read_page, unmeasured } from "./page.js";
2
+ import { hydration_report, hydration_rows } from "./hydration.js";
3
+ import { recording, session_names, set_last_session, start_session } from "./session.js";
4
+ import { region_name_by_fp } from "./regions.js";
5
+ import { ensure_region_names } from "./names.js";
6
+ import { TESTING_ENGINE_VERSION, analyze_session, apply_attr_rules, session_timeline, testing_lean } from "@ogygia/core/insights";
7
+ import { charge, nearest_attr, own_cost } from "@ogygia/core/internal/instruments";
8
+ import { snapshot } from "@ogygia/core/events";
9
+ // src/devtools/testing-engine.ts
10
+ /**
11
+ * What `window.__ogygia_testing` answers (ogygia/playwright calls it through `page.evaluate`). Loaded
12
+ * on the first call, like the dock: plain JSON out (no elements, no WeakRefs), so a test can assert
13
+ * on it and attach it to its report.
14
+ */
15
+ /** Which engine answered, and whether in lean testing (switch.ts): on every report. */
16
+ const engine = () => ({
17
+ version: TESTING_ENGINE_VERSION,
18
+ lean: testing_lean()
19
+ });
20
+ /** The decoded size of the engine's own files: what the page fetched while its import ran (ui.ts). */
21
+ let engine_bytes = 0;
22
+ function note_engine_files(from, to) {
23
+ for (const r of performance.getEntriesByType("resource")) if (r.startTime >= from && r.startTime <= to && (r.initiatorType === "script" || r.initiatorType === "link" || r.initiatorType === "other")) engine_bytes += r.decodedBodySize || 0;
24
+ }
25
+ /** What ogygia's measuring cost this page, for a test to subtract: the main-thread time of its own work
26
+ * (checking islands as they hydrate, building the visit, the testing reports — runtime/own-cost.ts)
27
+ * and the decoded bytes of the testing engine's files. No visitor with devtools off pays either. */
28
+ function self_cost() {
29
+ const c = own_cost();
30
+ return {
31
+ script_ms: c.ms,
32
+ bytes: engine_bytes,
33
+ phases: c.phases
34
+ };
35
+ }
36
+ async function page_json(cpu_fns = 15) {
37
+ await ensure_region_names();
38
+ const c0 = performance.now();
39
+ const v = read_page();
40
+ charge("report", c0);
41
+ if (!v) return null;
42
+ let route = null;
43
+ try {
44
+ route = performance.getEntriesByType("navigation")[0]?.serverTiming?.find((s) => s.name === "og-route")?.description || null;
45
+ } catch {
46
+ route = null;
47
+ }
48
+ return {
49
+ url: location.pathname,
50
+ route,
51
+ nav: v.nav,
52
+ vitals: v.report.vitals,
53
+ islands: v.report.rows,
54
+ findings: v.report.findings.map((f) => ({
55
+ ...f,
56
+ names: f.fps.map((fp) => region_name_by_fp(fp))
57
+ })),
58
+ blocking: v.report.blocking,
59
+ bytes: v.report.bytes,
60
+ longtask_ms: v.report.longtask_ms,
61
+ cpu: v.cpu ? {
62
+ busy_ms: v.cpu.busy_ms,
63
+ by_kind: v.cpu.by_kind,
64
+ fns: v.cpu.fns.slice(0, Math.max(1, Math.min(500, cpu_fns)))
65
+ } : null,
66
+ cpu_state: v.page.cpu.state,
67
+ cpu_off: v.page.cpu.off ?? null,
68
+ unmeasured: v.unmeasured,
69
+ self: self_cost(),
70
+ engine: engine()
71
+ };
72
+ }
73
+ /** Each island's report, keyed per INSTANCE: the value of `key_attr` on the island or its NEAREST
74
+ * ancestor that has it, across shadow roots (an id the app renders, e.g. its CMS block's), else
75
+ * `Name#n`, the n-th island of that name in document order. Two islands under the same keyed
76
+ * element are told apart as `key`, `key#2`, … in document order. */
77
+ async function hydration_json(key_attr, ignore = []) {
78
+ await ensure_region_names();
79
+ const base = hydration_report();
80
+ const report = {
81
+ ...base,
82
+ islands: base.islands.map((i) => {
83
+ if (!i.attributesOnly || !ignore.length || !i.attributes) return i;
84
+ const { left, ignored } = apply_attr_rules(i.attributes, ignore);
85
+ if (!ignored.length) return i;
86
+ return left.length ? {
87
+ ...i,
88
+ attributes: left,
89
+ ignored
90
+ } : {
91
+ ...i,
92
+ status: "clean",
93
+ attributesOnly: false,
94
+ attributes: null,
95
+ diff: null,
96
+ ignored
97
+ };
98
+ })
99
+ };
100
+ const els = new Map(hydration_rows().rows.map((r) => [r.fp, r.el]));
101
+ const in_order = report.islands.map((i) => ({
102
+ i,
103
+ el: els.get(i.fp)
104
+ })).sort((a, b) => a.el && b.el && a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1);
105
+ const keys = /* @__PURE__ */ new Map();
106
+ const count = /* @__PURE__ */ new Map();
107
+ const used = /* @__PURE__ */ new Map();
108
+ for (const { i, el } of in_order) {
109
+ const n = (count.get(i.name) ?? 0) + 1;
110
+ count.set(i.name, n);
111
+ const own = key_attr && el ? nearest_attr(el, key_attr) : null;
112
+ if (own) {
113
+ const k = (used.get(own) ?? 0) + 1;
114
+ used.set(own, k);
115
+ keys.set(i.fp, k === 1 ? own : `${own}#${k}`);
116
+ } else keys.set(i.fp, `${i.name}#${n}`);
117
+ }
118
+ return {
119
+ ...report,
120
+ engine: engine(),
121
+ islands: report.islands.map((i) => ({
122
+ ...i,
123
+ key: keys.get(i.fp)
124
+ }))
125
+ };
126
+ }
127
+ function record_start() {
128
+ if (recording()) return false;
129
+ start_session();
130
+ return true;
131
+ }
132
+ async function record_stop(blocked = []) {
133
+ const rec = recording();
134
+ if (!rec) return null;
135
+ const data = await rec.stop();
136
+ await ensure_region_names();
137
+ const names = session_names(data, (fp) => region_name_by_fp(fp));
138
+ const report = analyze_session(data, names, location.href);
139
+ const inputs = [...data.clicks, ...data.interactions].sort((a, b) => a.t - b.t);
140
+ const seen = [...data.navs ?? []];
141
+ const navigations = blocked.map((n) => {
142
+ let best = -1;
143
+ for (let k = 0; k < seen.length; k++) {
144
+ if (seen[k].url !== n.url) continue;
145
+ if (best === -1 || n.t !== null && Math.abs(seen[k].t - n.t) < Math.abs(seen[best].t - n.t)) best = k;
146
+ }
147
+ if (best !== -1) {
148
+ const [nav] = seen.splice(best, 1);
149
+ return {
150
+ url: n.url,
151
+ t: nav.t,
152
+ after: nav.source,
153
+ island: nav.fp ? names(nav.fp) : null,
154
+ approx: false
155
+ };
156
+ }
157
+ const before = n.t === null ? void 0 : inputs.filter((x) => x.t <= n.t).at(-1);
158
+ return {
159
+ url: n.url,
160
+ t: n.t,
161
+ after: before?.target ?? null,
162
+ island: before?.fp ? names(before.fp) : null,
163
+ approx: true
164
+ };
165
+ });
166
+ for (const n of navigations) report.findings.push({
167
+ code: "navigation-blocked",
168
+ severity: "warn",
169
+ message: `The page tried to load another document (${n.url})${n.after ? ` after ${n.after}${n.island ? ` in ${n.island}` : ""}` : ""}; it was blocked, so the recording kept going.`,
170
+ fix: "If that input should keep the page, find what navigates: a link, a form, or a reload in a handler.",
171
+ refs: []
172
+ });
173
+ set_last_session({
174
+ data,
175
+ report
176
+ });
177
+ window.__ogygia_session = report;
178
+ const fp_of = (ref) => ref.kind === "island" ? ref.fp : (ref.kind === "interaction" ? data.interactions : ref.kind === "click" ? data.clicks : data.shifts)[ref.i]?.fp ?? null;
179
+ let nav_i = 0;
180
+ return {
181
+ ...report,
182
+ findings: report.findings.map((f) => {
183
+ const named = f.refs.map(fp_of).filter((fp) => !!fp).map(names);
184
+ if (f.code === "navigation-blocked") {
185
+ const island = navigations[nav_i++]?.island;
186
+ if (island) named.push(island);
187
+ }
188
+ return {
189
+ ...f,
190
+ names: [...new Set(named)]
191
+ };
192
+ }),
193
+ navigations,
194
+ engine: engine(),
195
+ timeline: session_timeline(data, names).map((l) => ({
196
+ ...l,
197
+ items: l.items.map(({ ref: _ref, ...it }) => it)
198
+ })),
199
+ unmeasured: unmeasured(data.cpu_off)
200
+ };
201
+ }
202
+ /** Resolves when no island is mid-hydration and nothing woke for `quiet` ms (or `timeout` passed). */
203
+ async function settled(quiet = 300, timeout = 1e4) {
204
+ const until = performance.now() + timeout;
205
+ for (;;) {
206
+ const ev = snapshot();
207
+ const started = /* @__PURE__ */ new Set();
208
+ let last = 0;
209
+ for (const e of ev) {
210
+ if (e.realm !== "client") continue;
211
+ if (e.name === "region.hydrate.start" && e.fp) started.add(e.fp);
212
+ if ((e.name === "region.hydrate.done" || e.name === "region.hydrate.failed") && e.fp) started.delete(e.fp);
213
+ if (e.name === "region.hydrate.start" || e.name === "region.hydrate.done" || e.name === "wake.fired") last = Math.max(last, e.t);
214
+ }
215
+ if (!started.size && performance.now() - last >= quiet && document.readyState === "complete") return true;
216
+ if (performance.now() > until) return false;
217
+ await new Promise((r) => setTimeout(r, 50));
218
+ }
219
+ }
220
+ export { hydration_json, note_engine_files, page_json, record_start, record_stop, settled };
@@ -0,0 +1,5 @@
1
+ export declare function mount_app(root: ShadowRoot, opts: {
2
+ csr_true: boolean;
3
+ start_open: boolean;
4
+ hide?: () => void;
5
+ }): void;
@@ -0,0 +1,69 @@
1
+ import { install_page_hook } from "./page.js";
2
+ import { set_highlight_root } from "./highlight.js";
3
+ import { names } from "virtual:ogygia/devtools-meta";
4
+ import { mount } from "svelte";
5
+ import Devtools from "./Devtools.svelte";
6
+ // src/devtools/ui-app.ts
7
+ /**
8
+ * The devtools DOCK — one `mount()` of the {@link ./Devtools.svelte root app} into the shadow root
9
+ * the launcher (ui.ts) made. Its own module so a build with devtools (`devtools: 'always'`) ships
10
+ * only the launcher in the runtime chunk: this file, the components and their CSS load when someone
11
+ * opens the dock (a dynamic import the bundler splits off). On the dev server it loads at once.
12
+ */
13
+ function mount_app(root, opts) {
14
+ install_page_hook();
15
+ set_highlight_root(root);
16
+ if (names && Object.keys(names).length && typeof window !== "undefined") window.__ogygia_region_names = {
17
+ ...names,
18
+ ...window.__ogygia_region_names
19
+ };
20
+ mount(Devtools, {
21
+ target: root,
22
+ props: {
23
+ csrTrue: opts.csr_true,
24
+ startOpen: opts.start_open,
25
+ hide: opts.hide
26
+ }
27
+ });
28
+ adopt_scoped_styles(root);
29
+ const mo = new MutationObserver(() => adopt_scoped_styles(root));
30
+ mo.observe(document.head, { childList: true });
31
+ mo.observe(root, {
32
+ childList: true,
33
+ subtree: true
34
+ });
35
+ }
36
+ /** the dock's `<style>`s a page's Content-Security-Policy refused, already re-made as sheets */
37
+ const remade = /* @__PURE__ */ new WeakSet();
38
+ /** A page whose Content-Security-Policy has no `'unsafe-inline'` for styles refuses every `<style>`
39
+ * element — the dock's own among them (the element stays, with its text, but no sheet). A sheet made
40
+ * through the CSSOM is not an inline style: the same text, adopted into the shadow root, applies. */
41
+ function remake_refused_styles(root) {
42
+ if (typeof CSSStyleSheet === "undefined" || !("adoptedStyleSheets" in root)) return;
43
+ const add = [];
44
+ for (const style of root.querySelectorAll("style")) {
45
+ if (style.sheet || remade.has(style) || !style.textContent) continue;
46
+ remade.add(style);
47
+ try {
48
+ const sheet = new CSSStyleSheet();
49
+ sheet.replaceSync(style.textContent);
50
+ add.push(sheet);
51
+ } catch {}
52
+ }
53
+ if (add.length) root.adoptedStyleSheets = [...root.adoptedStyleSheets, ...add];
54
+ }
55
+ function adopt_scoped_styles(root) {
56
+ remake_refused_styles(root);
57
+ const hashes = /* @__PURE__ */ new Set();
58
+ for (const el of root.querySelectorAll("[class*=\"svelte-\"]")) for (const c of el.classList) if (c.startsWith("svelte-")) hashes.add(c);
59
+ if (hashes.size === 0) return;
60
+ for (const style of document.querySelectorAll("style")) {
61
+ if (style.getRootNode() === root) continue;
62
+ const css = style.textContent || "";
63
+ for (const h of hashes) if (css.includes(h)) {
64
+ root.appendChild(style);
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ export { mount_app };
@@ -0,0 +1,50 @@
1
+ export declare function install_devtools_ui(opts?: {
2
+ csr_true?: boolean;
3
+ }): void;
4
+ /**
5
+ * `window.__ogygia_testing` — what ogygia/playwright drives through `page.evaluate`: the page report,
6
+ * each island's hydration, a recorded session, "settled". Tiny here: each call loads the engine
7
+ * (testing-engine.ts) on first use, like the dock. Version-stamped so the fixture can refuse a
8
+ * mismatch.
9
+ */
10
+ export interface OgygiaTestingApi {
11
+ /** the report shape (testing-version.ts) */
12
+ version: number;
13
+ /** `cpu_fns`: how many of the page load's hottest functions (default 15) */
14
+ page(cpu_fns?: number): Promise<unknown>;
15
+ /** `key_attr`: the attribute that names an island instance; `ignore`: attribute changes to leave out
16
+ * (see testing-engine.ts) */
17
+ hydration(key_attr?: string, ignore?: unknown[]): Promise<unknown>;
18
+ record_start(): Promise<boolean>;
19
+ /** `blocked`: the navigations the test's driver held back during the recording */
20
+ record_stop(blocked?: {
21
+ url: string;
22
+ t: number | null;
23
+ }[]): Promise<unknown>;
24
+ settled(quiet?: number, timeout?: number): Promise<boolean>;
25
+ }
26
+ declare global {
27
+ interface Window {
28
+ __ogygia_testing?: OgygiaTestingApi;
29
+ }
30
+ }
31
+ /** Turn devtools on in this browser, now: the launcher shows (measuring starts with the next load). */
32
+ export declare function devtools_on(): void;
33
+ /** Turn devtools off in this browser, now: the launcher and the dock go, and nothing is measured from
34
+ * the next load on. */
35
+ export declare function devtools_off(): void;
36
+ /**
37
+ * `ogygia.devtools.on()` / `.off()` in the console (and `.status()`). On the dev server devtools is
38
+ * always on, and they say so.
39
+ */
40
+ declare global {
41
+ interface Window {
42
+ ogygia?: {
43
+ devtools?: {
44
+ on(): string;
45
+ off(): string;
46
+ status(): string;
47
+ };
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,175 @@
1
+ import { TESTING_ENGINE_VERSION, devtools_switched_on, switch_in_url, testing_lean, without_switch, write_switch } from "@ogygia/core/insights";
2
+ // src/devtools/ui.ts
3
+ /**
4
+ * The devtools LAUNCHER — the only part of devtools the runtime chunk carries. Called from the runtime
5
+ * boot on a devtools build (behind the `__OGYGIA_DEVTOOLS__` gate: off, all of it tree-shakes away).
6
+ * It makes the shadow host and, in a build, one small plain-DOM button: the dock itself (ui-app.ts,
7
+ * Svelte, every tab, their CSS) loads when someone opens it — a dynamic import the bundler splits
8
+ * off. In a build even the button waits for the browser's DEVTOOLS SWITCH (switch.ts: `?og-devtools=on`,
9
+ * `ogygia.devtools.on()`): a QA or preview deploy's visitors see nothing and pay nothing. On the dev
10
+ * server the button is always there and the dock loads at once.
11
+ *
12
+ * The panel mounts on the LIVE page (not an iframe), so its host element carries a **shadow root** for
13
+ * total CSS isolation: host-page globals can't reach in (a stray `.spacer { height: 140vh }` once
14
+ * ballooned the header) and the devtools' own styles can't leak out. The host is attached to
15
+ * `<html>` (not `<body>`) so a full-body SPA swap never tears the panel out.
16
+ *
17
+ * The switch's cookie also starts the rest, for that browser only: the server writes its event
18
+ * side-channel and the page is measured from the start (runtime/beacon.ts).
19
+ */
20
+ const DEVTOOLS = typeof __OGYGIA_DEVTOOLS__ !== "undefined" ? __OGYGIA_DEVTOOLS__ : false;
21
+ const LAZY = typeof __OGYGIA_DEVTOOLS_LAZY__ !== "undefined" ? __OGYGIA_DEVTOOLS_LAZY__ : false;
22
+ const LAYOUT_KEY = "ogygia:devtools:layout:v4";
23
+ let mounted = false;
24
+ let launch_opts;
25
+ function install_devtools_ui(opts) {
26
+ if (!DEVTOOLS || typeof document === "undefined" || mounted) return;
27
+ if (document.querySelector("meta[name=\"ogygia-devtools\"][content=\"off\"]")) return;
28
+ launch_opts = opts;
29
+ install_testing_api();
30
+ install_console_api();
31
+ if (LAZY) {
32
+ apply_url_switch();
33
+ if (!devtools_switched_on()) return;
34
+ }
35
+ if (testing_lean()) return;
36
+ mount_launcher(opts);
37
+ }
38
+ /** The launcher (and, on the dev server or for a dock left open, the dock). */
39
+ function mount_launcher(opts) {
40
+ if (mounted || document.querySelector("[data-ogygia-devtools-host]")) return;
41
+ mounted = true;
42
+ try {
43
+ if (sessionStorage.getItem("ogygia:devtools:session")) import("./session.js").then((m) => m.resume_session_if_any());
44
+ } catch {}
45
+ const host = document.createElement("div");
46
+ host.setAttribute("data-ogygia-devtools-host", "");
47
+ host.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647";
48
+ document.documentElement.appendChild(host);
49
+ const root = host.attachShadow({ mode: "open" });
50
+ const csr_true = opts?.csr_true ?? false;
51
+ let loading = false;
52
+ let want_open = false;
53
+ let button = null;
54
+ let on_key = null;
55
+ const load = () => {
56
+ if (loading) return;
57
+ loading = true;
58
+ import("./ui-app.js").then((m) => {
59
+ button?.remove();
60
+ if (on_key) removeEventListener("keydown", on_key);
61
+ m.mount_app(root, {
62
+ csr_true,
63
+ start_open: want_open,
64
+ ...LAZY ? { hide: devtools_off } : {}
65
+ });
66
+ });
67
+ };
68
+ button = document.createElement("button");
69
+ button.setAttribute("data-og-panel-toggle", "");
70
+ button.title = "ogygia devtools (Alt+O)";
71
+ button.textContent = "og devtools";
72
+ button.style.cssText = "position:fixed;right:24px;bottom:8px;padding:7px 12px;border-radius:999px;border:1px solid rgba(148,163,184,.35);background:#0b1220;color:#e2e8f0;font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;cursor:pointer;opacity:.55;box-shadow:0 4px 16px rgba(0,0,0,.4)";
73
+ const b = button;
74
+ b.onmouseenter = () => b.style.opacity = "1";
75
+ b.onmouseleave = () => b.style.opacity = ".55";
76
+ const open = () => {
77
+ want_open = true;
78
+ b.textContent = "loading…";
79
+ load();
80
+ };
81
+ b.onclick = open;
82
+ on_key = (e) => {
83
+ if (e.altKey && (e.key === "o" || e.key === "O")) {
84
+ e.preventDefault();
85
+ open();
86
+ }
87
+ };
88
+ addEventListener("keydown", on_key);
89
+ root.appendChild(b);
90
+ if (was_open()) load();
91
+ else if (!LAZY) after_load_idle(load);
92
+ }
93
+ function after_load_idle(fn) {
94
+ const idle = () => {
95
+ const ric = globalThis.requestIdleCallback;
96
+ if (ric) ric(fn, { timeout: 3e3 });
97
+ else setTimeout(fn, 1e3);
98
+ };
99
+ if (document.readyState === "complete") setTimeout(idle, 500);
100
+ else addEventListener("load", () => setTimeout(idle, 500), { once: true });
101
+ }
102
+ function install_testing_api() {
103
+ if (window.__ogygia_testing) return;
104
+ let loading = null;
105
+ const engine = () => loading ??= (async () => {
106
+ const from = performance.now();
107
+ const m = await import("./testing-engine.js");
108
+ m.note_engine_files(from, performance.now());
109
+ return m;
110
+ })();
111
+ window.__ogygia_testing = {
112
+ version: TESTING_ENGINE_VERSION,
113
+ page: async (cpu_fns) => (await engine()).page_json(cpu_fns),
114
+ hydration: async (key_attr, ignore) => (await engine()).hydration_json(key_attr, ignore),
115
+ record_start: async () => (await engine()).record_start(),
116
+ record_stop: async (blocked) => (await engine()).record_stop(blocked),
117
+ settled: async (quiet, timeout) => (await engine()).settled(quiet, timeout)
118
+ };
119
+ }
120
+ /** Has this browser opened the dock before (from then on the page is measured from the start). */
121
+ /** `?og-devtools=on|off` on this page: flip the switch and drop the param from the address. */
122
+ function apply_url_switch() {
123
+ try {
124
+ const url = new URL(location.href);
125
+ const want = switch_in_url(url);
126
+ if (want === null) return;
127
+ write_switch(want);
128
+ history.replaceState(history.state, "", without_switch(url).href);
129
+ } catch {}
130
+ }
131
+ /** Turn devtools on in this browser, now: the launcher shows (measuring starts with the next load). */
132
+ function devtools_on() {
133
+ if (!LAZY) return;
134
+ write_switch(true);
135
+ mount_launcher(launch_opts);
136
+ }
137
+ /** Turn devtools off in this browser, now: the launcher and the dock go, and nothing is measured from
138
+ * the next load on. */
139
+ function devtools_off() {
140
+ if (!LAZY) return;
141
+ write_switch(false);
142
+ try {
143
+ localStorage.removeItem(LAYOUT_KEY);
144
+ } catch {}
145
+ document.querySelector("[data-ogygia-devtools-host]")?.remove();
146
+ mounted = false;
147
+ }
148
+ function install_console_api() {
149
+ const say = (on) => on ? "ogygia devtools: on in this browser (stays on; ogygia.devtools.off() to hide it)" : "ogygia devtools: off in this browser";
150
+ const g = window.ogygia ??= {};
151
+ if (g.devtools) return;
152
+ g.devtools = {
153
+ on() {
154
+ if (!LAZY) return "ogygia devtools: always on on the dev server";
155
+ devtools_on();
156
+ return say(true);
157
+ },
158
+ off() {
159
+ if (!LAZY) return "ogygia devtools: always on on the dev server";
160
+ devtools_off();
161
+ return say(false);
162
+ },
163
+ status() {
164
+ return !LAZY ? "ogygia devtools: always on on the dev server" : say(devtools_switched_on());
165
+ }
166
+ };
167
+ }
168
+ function was_open() {
169
+ try {
170
+ return !!JSON.parse(localStorage.getItem(LAYOUT_KEY) || "{}")?.open;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+ export { devtools_off, devtools_on, install_devtools_ui };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * ALLOCATION ON THE TIMELINE — the heap's size sampled finely while the recording ran, turned
3
+ * into bursts: the stretches where the heap grew fastest, each joined to what the CPU was
4
+ * running at that moment. The makers table (gc.ts) says WHO allocated over the whole window;
5
+ * this says WHEN, and what ran when it happened. Pure: the host supplies the samples (on the
6
+ * capture's clock, ms from the profiler's start), the CPU segments on the same clock, and the
7
+ * GC pauses (a pause inside an interval shrinks the heap, so that interval's growth is a floor,
8
+ * not the allocation).
9
+ *
10
+ * The samples come from a timer: a synchronous stretch cannot be sampled while it runs, so an
11
+ * interval is at least one yield of the event loop long — "between two awaits", never inside one.
12
+ */
13
+ export interface HeapSample {
14
+ /** ms on the capture's clock */
15
+ t: number;
16
+ /** used heap, MB */
17
+ mb: number;
18
+ }
19
+ export interface AllocBurst {
20
+ t0: number;
21
+ t1: number;
22
+ /** MB the heap grew across the interval (a floor when `gc` is set) */
23
+ mb: number;
24
+ /** MB per second over the interval */
25
+ rate: number;
26
+ /** what the CPU was running across the interval, by share of its CPU time */
27
+ running: {
28
+ label: string;
29
+ category: string;
30
+ file?: string;
31
+ share: number;
32
+ }[];
33
+ /** a GC pause fell inside the interval: the growth is what survived it, the allocation was more */
34
+ gc: boolean;
35
+ }
36
+ export interface AllocTimeline {
37
+ /** the samples on the capture's clock (kept for the chart) */
38
+ samples: HeapSample[];
39
+ /** the fastest-growing intervals, rate desc */
40
+ bursts: AllocBurst[];
41
+ /** MB the heap grew over every interval that grew (what the render put on the heap between
42
+ * collections; the GC attribution's allocated bytes are the same thing measured by sampling) */
43
+ grown_mb: number;
44
+ /** ms between samples as recorded (the timer's period; longer where the loop was blocked) */
45
+ period_ms: number;
46
+ /** the longest gap between two samples: the longest synchronous stretch the loop saw */
47
+ longest_gap_ms: number;
48
+ /** the one window the report explains, on the same clock */
49
+ window?: {
50
+ offset_ms: number;
51
+ ms: number;
52
+ };
53
+ }
54
+ export interface AllocInput {
55
+ samples: HeapSample[];
56
+ /** CPU segments on the capture's clock */
57
+ running?: {
58
+ t0: number;
59
+ t1: number;
60
+ label: string;
61
+ category: string;
62
+ file?: string;
63
+ }[];
64
+ /** GC pauses on the capture's clock */
65
+ gc?: {
66
+ t: number;
67
+ ms: number;
68
+ }[];
69
+ window?: {
70
+ offset_ms: number;
71
+ ms: number;
72
+ };
73
+ limit?: number;
74
+ }
75
+ export declare function alloc_timeline(input: AllocInput): AllocTimeline | undefined;
@@ -0,0 +1,76 @@
1
+ // src/profiler/alloc.ts
2
+ const round1 = (n) => Math.round(n * 10) / 10;
3
+ const round2 = (n) => Math.round(n * 100) / 100;
4
+ function alloc_timeline(input) {
5
+ const s = input.samples;
6
+ if (s.length < 3) return void 0;
7
+ const running = input.running ?? [];
8
+ const gc = input.gc ?? [];
9
+ let ri = 0;
10
+ let gi = 0;
11
+ let grown = 0;
12
+ let longest = 0;
13
+ let period_sum = 0;
14
+ const raw = [];
15
+ for (let i = 1; i < s.length; i++) {
16
+ const t0 = s[i - 1].t;
17
+ const t1 = s[i].t;
18
+ const len = t1 - t0;
19
+ if (len <= 0) continue;
20
+ period_sum += len;
21
+ if (len > longest) longest = len;
22
+ const mb = s[i].mb - s[i - 1].mb;
23
+ while (gi < gc.length && gc[gi].t + gc[gi].ms < t0) gi++;
24
+ let had_gc = false;
25
+ for (let g = gi; g < gc.length && gc[g].t <= t1; g++) had_gc = true;
26
+ if (mb <= 0) continue;
27
+ grown += mb;
28
+ while (ri < running.length && running[ri].t1 < t0) ri++;
29
+ const by = /* @__PURE__ */ new Map();
30
+ let cpu = 0;
31
+ for (let r = ri; r < running.length && running[r].t0 <= t1; r++) {
32
+ const seg = running[r];
33
+ const ov = Math.min(seg.t1, t1) - Math.max(seg.t0, t0);
34
+ if (ov <= 0) continue;
35
+ cpu += ov;
36
+ const k = seg.label + "\0" + (seg.file ?? "");
37
+ const hit = by.get(k);
38
+ if (hit) hit.ms += ov;
39
+ else by.set(k, {
40
+ label: seg.label,
41
+ category: seg.category,
42
+ ...seg.file ? { file: seg.file } : {},
43
+ ms: ov
44
+ });
45
+ }
46
+ const top = [...by.values()].sort((a, b) => b.ms - a.ms).slice(0, 4);
47
+ raw.push({
48
+ t0: round2(t0),
49
+ t1: round2(t1),
50
+ mb: round1(mb),
51
+ rate: round1(mb / len * 1e3),
52
+ running: top.map((x) => ({
53
+ label: x.label,
54
+ category: x.category,
55
+ ...x.file ? { file: x.file } : {},
56
+ share: cpu > 0 ? round2(x.ms / cpu) : 0
57
+ })),
58
+ gc: had_gc
59
+ });
60
+ }
61
+ const limit = input.limit ?? 12;
62
+ const floor = Math.max(.5, grown * .02);
63
+ const bursts = raw.filter((b) => b.mb >= floor).sort((a, b) => b.rate - a.rate).slice(0, limit);
64
+ return {
65
+ samples: s.map((x) => ({
66
+ t: round2(x.t),
67
+ mb: round1(x.mb)
68
+ })),
69
+ bursts,
70
+ grown_mb: round1(grown),
71
+ period_ms: round1(period_sum / Math.max(1, s.length - 1)),
72
+ longest_gap_ms: round1(longest),
73
+ ...input.window ? { window: input.window } : {}
74
+ };
75
+ }
76
+ export { alloc_timeline };