@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,90 @@
1
+ import { CallerSite } from "./net.js";
2
+ export type SpanAttrs = Record<string, string | number | boolean>;
3
+ export interface SpanRecord {
4
+ id: number;
5
+ name: string;
6
+ /** performance.now() at start */
7
+ start: number;
8
+ /** duration; -1 while running (an `open` span at the end of a recording keeps -1) */
9
+ ms: number;
10
+ attrs?: SpanAttrs;
11
+ /** the thrown error's message, when `fn` threw / the handle was `fail`ed */
12
+ error?: string;
13
+ /** never ended before the recording stopped */
14
+ open?: boolean;
15
+ /** the enclosing span's id, when nested */
16
+ parent?: number;
17
+ /** resolved at report time from `caller_site` (same as an outbound call's) */
18
+ caller?: string;
19
+ caller_site?: CallerSite;
20
+ /** the page render that ran it, when known */
21
+ route: string | null;
22
+ path: string | null;
23
+ }
24
+ /** What the profiler host installs while a recording runs. */
25
+ export interface SpanRecorder {
26
+ begin(name: string, attrs?: SpanAttrs): SpanRecord;
27
+ /** run `fn` with `s` as the current span (nesting); the host binds it to its async context */
28
+ within<T>(s: SpanRecord, fn: () => T): T;
29
+ end(s: SpanRecord, attrs?: SpanAttrs, error?: unknown): void;
30
+ tag(key: string, value: string): void;
31
+ }
32
+ /** The profiler host installs a recorder for a recording; `null` uninstalls. */
33
+ export declare function set_span_recorder(r: SpanRecorder | null): void;
34
+ /** Whether a recorder is installed (tests / the host). */
35
+ export declare function span_recording(): boolean;
36
+ /** A span opened by hand (`span.start`) for what a callback cannot wrap: a stream, an event. */
37
+ export interface SpanHandle {
38
+ /** add or change an attribute while the span runs */
39
+ set(key: string, value: string | number | boolean): void;
40
+ /** close it; ending twice is a no-op */
41
+ end(attrs?: SpanAttrs): void;
42
+ /** close it as failed */
43
+ fail(error: unknown): void;
44
+ }
45
+ /**
46
+ * Run `fn` as a named span. Sync in, sync out; a promise in, a promise out — the wrapper never
47
+ * changes the shape of what it wraps. A throw (or rejection) is recorded on the span and rethrown.
48
+ * `attrs` is a plain record, or a function of the result for what is only known afterwards
49
+ * (`(r) => ({ cache: r.fromCache ? 'hit' : 'miss', rows: r.length })`). Reserved keys the report
50
+ * reads: `cache` ('hit' | 'miss'), `rows`, `bytes`, `key`; anything else is carried and shown.
51
+ */
52
+ export declare function span<T>(name: string, fn: () => T, attrs?: SpanAttrs | ((result: Awaited<T>) => SpanAttrs | undefined)): T;
53
+ export declare namespace span {
54
+ export var start: (name: string, attrs?: SpanAttrs) => SpanHandle;
55
+ }
56
+ /** Stamp the current request with `key = value` (string, number or boolean; kept to 64 chars, at
57
+ * most 16 keys). Shown on the request's log row and in the report; the dashboard splits the
58
+ * slowest routes by any tag. A no-op outside a recording, and always on the client. */
59
+ export declare function tag(key: string, value: string | number | boolean): void;
60
+ /**
61
+ * INSTRUMENT a function once, so every call is a `span` — for a renderer or a client you call
62
+ * from many places and cannot wrap at each site: a design system's `renderToString`, a database
63
+ * client's `query`, an SDK method.
64
+ *
65
+ * Two forms:
66
+ * • `instrument(fn, name, attrs?)` returns a wrapped function (an ESM import is read-only, so
67
+ * bind the result: `const renderToString = instrument(renderToStringCore, 'ds.render')`).
68
+ * • `instrument(obj, 'method', name, attrs?)` patches the method in place (a client instance,
69
+ * a prototype) and returns a function that restores it.
70
+ *
71
+ * `attrs` gets the result AND the call's arguments, so a span can carry the tag it rendered or
72
+ * the bytes it produced: `(html, tag) => ({ tag, bytes: html.length })`. Sync in, sync out; a
73
+ * promise in, a promise out; `this` preserved. Outside a recording the wrapper is one `if`.
74
+ *
75
+ * ```ts
76
+ * import { instrument } from '@ogygia/devtools/profiler';
77
+ * import { renderToString as core } from '@acme/design-system/hydrate';
78
+ * const renderToString = instrument(core, 'ds.render', (r, tag) => ({ tag, bytes: r.html.length }));
79
+ * ```
80
+ */
81
+ /** The attrs callback of `instrument`: the result, then the call's arguments. Declared method-style
82
+ * so a callback naming fewer arguments than the function takes (`(r, tag) => …` for a two-argument
83
+ * renderer) is accepted, and so it never narrows the wrapped signature. `NoInfer` wraps the whole
84
+ * callback, not its rest argument: on the rest tuple alone it made a shorter callback a tuple-length
85
+ * mismatch, so the `(r, tag) => …` form above did not type-check. */
86
+ export type InstrumentAttrs<A extends unknown[], R> = NoInfer<{
87
+ bivariant(result: Awaited<R>, ...args: A): SpanAttrs | undefined;
88
+ }["bivariant"]>;
89
+ export declare function instrument<A extends unknown[], R>(fn: (...args: A) => R, name: string, attrs?: InstrumentAttrs<A, R>): (...args: A) => R;
90
+ export declare function instrument<T extends object, K extends keyof T>(target: T, method: K, name: string, attrs?: T[K] extends ((...args: infer A) => infer R) ? InstrumentAttrs<A, R> : never): () => void;
@@ -0,0 +1,109 @@
1
+ import { register_profiler_file } from "./frames.js";
2
+ // src/profiler/span.ts
3
+ register_profiler_file();
4
+ let recorder = null;
5
+ /** The profiler host installs a recorder for a recording; `null` uninstalls. */
6
+ function set_span_recorder(r) {
7
+ recorder = r;
8
+ }
9
+ /** Whether a recorder is installed (tests / the host). */
10
+ function span_recording() {
11
+ return recorder !== null;
12
+ }
13
+ const NOOP_HANDLE = {
14
+ set() {},
15
+ end() {},
16
+ fail() {}
17
+ };
18
+ const is_thenable = (v) => !!v && typeof v.then === "function";
19
+ /**
20
+ * Run `fn` as a named span. Sync in, sync out; a promise in, a promise out — the wrapper never
21
+ * changes the shape of what it wraps. A throw (or rejection) is recorded on the span and rethrown.
22
+ * `attrs` is a plain record, or a function of the result for what is only known afterwards
23
+ * (`(r) => ({ cache: r.fromCache ? 'hit' : 'miss', rows: r.length })`). Reserved keys the report
24
+ * reads: `cache` ('hit' | 'miss'), `rows`, `bytes`, `key`; anything else is carried and shown.
25
+ */
26
+ function span(name, fn, attrs) {
27
+ const r = recorder;
28
+ if (r === null) return fn();
29
+ const s = r.begin(name, typeof attrs === "function" ? void 0 : attrs);
30
+ const finish = (result, error) => {
31
+ let extra;
32
+ if (typeof attrs === "function" && error === void 0) try {
33
+ extra = attrs(result);
34
+ } catch {}
35
+ r.end(s, extra, error);
36
+ };
37
+ let out;
38
+ try {
39
+ out = r.within(s, fn);
40
+ } catch (e) {
41
+ finish(void 0, e ?? /* @__PURE__ */ new Error("span failed"));
42
+ throw e;
43
+ }
44
+ if (is_thenable(out)) return out.then((v) => {
45
+ finish(v);
46
+ return v;
47
+ }, (e) => {
48
+ finish(void 0, e ?? /* @__PURE__ */ new Error("span failed"));
49
+ throw e;
50
+ });
51
+ finish(out);
52
+ return out;
53
+ }
54
+ /** Open a span by hand; close it with `end` / `fail`. Nesting: spans started while another span's
55
+ * `fn` runs record it as their parent; a bare `span.start` at the top level has none. */
56
+ span.start = function start(name, attrs) {
57
+ const r = recorder;
58
+ if (r === null) return NOOP_HANDLE;
59
+ const s = r.begin(name, attrs);
60
+ let done = false;
61
+ return {
62
+ set(key, value) {
63
+ (s.attrs ??= {})[key] = value;
64
+ },
65
+ end(extra) {
66
+ if (done) return;
67
+ done = true;
68
+ r.end(s, extra);
69
+ },
70
+ fail(error) {
71
+ if (done) return;
72
+ done = true;
73
+ r.end(s, void 0, error ?? /* @__PURE__ */ new Error("span failed"));
74
+ }
75
+ };
76
+ };
77
+ /** Stamp the current request with `key = value` (string, number or boolean; kept to 64 chars, at
78
+ * most 16 keys). Shown on the request's log row and in the report; the dashboard splits the
79
+ * slowest routes by any tag. A no-op outside a recording, and always on the client. */
80
+ function tag(key, value) {
81
+ const r = recorder;
82
+ if (r === null || !key) return;
83
+ r.tag(String(key).slice(0, 64), String(value).slice(0, 64));
84
+ }
85
+ function instrument(...args) {
86
+ if (typeof args[0] === "function") {
87
+ const [fn, name, attrs] = args;
88
+ return wrap_call(fn, name, attrs);
89
+ }
90
+ const [target, method, name, attrs] = args;
91
+ const original = target[method];
92
+ if (typeof original !== "function") throw new Error(`instrument: ${String(method)} is not a function`);
93
+ target[method] = wrap_call(original, name, attrs);
94
+ return () => {
95
+ target[method] = original;
96
+ };
97
+ }
98
+ function wrap_call(fn, name, attrs) {
99
+ const wrapped = function(...a) {
100
+ if (recorder === null) return fn.apply(this, a);
101
+ return span(name, () => fn.apply(this, a), attrs ? (result) => attrs(result, ...a) : void 0);
102
+ };
103
+ try {
104
+ Object.defineProperty(wrapped, "name", { value: fn.name || name });
105
+ Object.defineProperty(wrapped, "length", { value: fn.length });
106
+ } catch {}
107
+ return wrapped;
108
+ }
109
+ export { instrument, set_span_recorder, span, span_recording, tag };
@@ -0,0 +1,43 @@
1
+ import { FrameCategory, StackIndex } from "./analyze.js";
2
+ export interface RangeRow {
3
+ /** the frame's name */
4
+ name: string;
5
+ file?: string;
6
+ category: FrameCategory;
7
+ /** ms the frame was the leaf (running itself) inside the range */
8
+ self_ms: number;
9
+ /** ms the frame was anywhere on the stack inside the range */
10
+ total_ms: number;
11
+ }
12
+ export interface RangeSummary {
13
+ t0: number;
14
+ t1: number;
15
+ /** CPU inside the range (idle samples excluded) */
16
+ cpu_ms: number;
17
+ idle_ms: number;
18
+ /** by self time, desc */
19
+ self: RangeRow[];
20
+ /** by inclusive time, desc — the owners of the range */
21
+ total: RangeRow[];
22
+ /** the components on the stacks in the range, by inclusive time */
23
+ components: RangeRow[];
24
+ }
25
+ /** What ran between `t0` and `t1` (ms from the window's start): self and inclusive time per
26
+ * frame, the components among them. A sample that straddles an edge counts by its overlap. */
27
+ export declare function range_hot(index: StackIndex, t0: number, t1: number, limit?: number): RangeSummary;
28
+ export interface StackAt {
29
+ t: number;
30
+ /** the sample's length */
31
+ ms: number;
32
+ /** the stack, outermost first; empty when idle */
33
+ frames: {
34
+ name: string;
35
+ file?: string;
36
+ category: FrameCategory;
37
+ }[];
38
+ }
39
+ /** The stack at instant `t` (ms from the window's start). */
40
+ export declare function stack_at(index: StackIndex, t: number): StackAt | null;
41
+ /** The CPU of the window as `bins` equal slices: ms of CPU per slice, for a density strip.
42
+ * `by`: 'all' for any frame; a category to count only samples whose leaf is that category. */
43
+ export declare function density(index: StackIndex, bins?: number, by?: "all" | FrameCategory): number[];
@@ -0,0 +1,107 @@
1
+ // src/profiler/stacks.ts
2
+ const round2 = (n) => Math.round(n * 100) / 100;
3
+ /** the first sample index whose end reaches `t` (binary search on the sorted starts) */
4
+ function first_at(index, t) {
5
+ let lo = 0;
6
+ let hi = index.t.length;
7
+ while (lo < hi) {
8
+ const mid = lo + hi >> 1;
9
+ if (index.t[mid] + index.d[mid] < t) lo = mid + 1;
10
+ else hi = mid;
11
+ }
12
+ return lo;
13
+ }
14
+ /** What ran between `t0` and `t1` (ms from the window's start): self and inclusive time per
15
+ * frame, the components among them. A sample that straddles an edge counts by its overlap. */
16
+ function range_hot(index, t0, t1, limit = 30) {
17
+ const self = /* @__PURE__ */ new Map();
18
+ const total = /* @__PURE__ */ new Map();
19
+ let cpu = 0;
20
+ let idle = 0;
21
+ const seen = [];
22
+ for (let i = first_at(index, t0); i < index.t.length; i++) {
23
+ const s = index.t[i];
24
+ if (s > t1) break;
25
+ const e = s + index.d[i];
26
+ const ms = Math.min(e, t1) - Math.max(s, t0);
27
+ if (ms <= 0) continue;
28
+ const leaf = index.leaf[i];
29
+ if (leaf < 0) {
30
+ idle += ms;
31
+ continue;
32
+ }
33
+ cpu += ms;
34
+ self.set(leaf, (self.get(leaf) ?? 0) + ms);
35
+ seen.length = 0;
36
+ for (let f = leaf; f >= 0; f = index.frames[f].p) {
37
+ if (seen.includes(f)) continue;
38
+ seen.push(f);
39
+ total.set(f, (total.get(f) ?? 0) + ms);
40
+ }
41
+ }
42
+ const row = (f) => {
43
+ const fr = index.frames[f];
44
+ return {
45
+ name: fr.n,
46
+ ...fr.f ? { file: fr.f } : {},
47
+ category: fr.c,
48
+ self_ms: round2(self.get(f) ?? 0),
49
+ total_ms: round2(total.get(f) ?? 0)
50
+ };
51
+ };
52
+ const by_self = [...self.keys()].sort((a, b) => self.get(b) - self.get(a)).slice(0, limit).map(row);
53
+ const by_total = [...total.keys()].sort((a, b) => total.get(b) - total.get(a));
54
+ const components = by_total.filter((f) => index.frames[f].c === "component").slice(0, limit).map(row);
55
+ return {
56
+ t0: round2(t0),
57
+ t1: round2(t1),
58
+ cpu_ms: round2(cpu),
59
+ idle_ms: round2(idle),
60
+ self: by_self,
61
+ total: by_total.slice(0, limit).map(row),
62
+ components
63
+ };
64
+ }
65
+ /** The stack at instant `t` (ms from the window's start). */
66
+ function stack_at(index, t) {
67
+ const i = first_at(index, t);
68
+ if (i >= index.t.length || index.t[i] > t) return null;
69
+ const frames = [];
70
+ for (let f = index.leaf[i]; f >= 0; f = index.frames[f].p) {
71
+ const fr = index.frames[f];
72
+ frames.push({
73
+ name: fr.n,
74
+ ...fr.f ? { file: fr.f } : {},
75
+ category: fr.c
76
+ });
77
+ }
78
+ frames.reverse();
79
+ return {
80
+ t: round2(index.t[i]),
81
+ ms: round2(index.d[i]),
82
+ frames
83
+ };
84
+ }
85
+ /** The CPU of the window as `bins` equal slices: ms of CPU per slice, for a density strip.
86
+ * `by`: 'all' for any frame; a category to count only samples whose leaf is that category. */
87
+ function density(index, bins = 200, by = "all") {
88
+ const out = Array.from({ length: bins }, () => 0);
89
+ const w = index.window_ms || 1;
90
+ for (let i = 0; i < index.t.length; i++) {
91
+ const leaf = index.leaf[i];
92
+ if (leaf < 0) continue;
93
+ if (by !== "all" && index.frames[leaf].c !== by) continue;
94
+ const s = index.t[i];
95
+ const e = s + index.d[i];
96
+ let b0 = Math.floor(s / w * bins);
97
+ const b1 = Math.min(bins - 1, Math.floor(e / w * bins));
98
+ if (b0 < 0) b0 = 0;
99
+ for (let b = b0; b <= b1; b++) {
100
+ const bs = b / bins * w;
101
+ const be = (b + 1) / bins * w;
102
+ out[b] += Math.max(0, Math.min(e, be) - Math.max(s, bs));
103
+ }
104
+ }
105
+ return out.map(round2);
106
+ }
107
+ export { density, range_hot, stack_at };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * STANDALONE REPORT — one HTML file that opens from disk, islands and all.
3
+ *
4
+ * The report page is an ogygia document: server markup, a runtime `<script type="module" src>`,
5
+ * `<ogygia-region entry="…">` islands whose chunks are `import()`ed by URL, stylesheet links. None
6
+ * of that survives `file://`. This turns it into a single file:
7
+ *
8
+ * • every stylesheet link becomes a `<style>`;
9
+ * • every JS chunk the page can reach (the runtime, each island entry, and everything they
10
+ * import, transitively) is fetched once, its relative imports rewritten to bare `og:<path>`
11
+ * specifiers, and shipped as a `data:` URL in ONE import map — dynamic `import('og:…')` from
12
+ * the runtime resolves through the map, so islands hydrate exactly as on the server;
13
+ * • the runtime script is inlined as a module; modulepreload hints are dropped (nothing to fetch);
14
+ * • island `entry` attributes are rewritten to their `og:` keys.
15
+ *
16
+ * Pure: the caller supplies `load(url)` (a self-fetch in the profiler host, a map in tests). Dev
17
+ * servers are refused upstream — their module graph is Vite's, not a bundle.
18
+ */
19
+ export interface StandaloneOptions {
20
+ /** the report page's own URL (relative hrefs resolve against it) */
21
+ page_url: string;
22
+ /** fetch a same-origin asset by absolute URL; `null` when it cannot be read */
23
+ load: (url: string) => Promise<string | null>;
24
+ /** refuse past this many bytes of inlined assets (a runaway graph) */
25
+ max_bytes?: number;
26
+ }
27
+ export declare function build_standalone(html: string, opts: StandaloneOptions): Promise<string>;
@@ -0,0 +1,98 @@
1
+ // src/profiler/standalone.ts
2
+ const STYLESHEET_RE = /<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi;
3
+ const MODULEPRELOAD_RE = /<link\b[^>]*\brel=["']modulepreload["'][^>]*>/gi;
4
+ const HREF_RE = /\bhref=["']([^"']+)["']/i;
5
+ const MODULE_SCRIPT_RE = /<script\b([^>]*\btype=["']module["'][^>]*)\bsrc=["']([^"']+)["']([^>]*)><\/script>/gi;
6
+ const ENTRY_ATTR_RE = /(<ogygia-region\b[^>]*\bentry=["'])([^"']+)(["'])/gi;
7
+ /** a static or dynamic import of a RELATIVE specifier inside a built chunk — quoted, or a
8
+ * template literal with no interpolation (Vite emits `import(\`./chunks/x.js\`)`) */
9
+ const IMPORT_RE = /((?:\bfrom|\bimport)\s*\(?\s*)(["'`])(\.{1,2}\/[^"'`$]+)\2/g;
10
+ /** any other relative chunk path in a string: Vite's `__vite__mapDeps` preload list. Pointed at
11
+ * the same keys so a preload link never resolves against the document (a `file://` fetch). */
12
+ const DEP_STRING_RE = /(["'])(\.{1,2}\/[^"'`\s]+\.(?:js|css))\1/g;
13
+ /** The import-map key for a chunk. An absolute URL with a `//` authority on purpose: the runtime's
14
+ * `island_module_url` hands an `entry` with a URL scheme through untouched (a bare `og:/x` would
15
+ * be resolved against the document and lose the scheme), and a `data:` module can import an
16
+ * absolute URL where it cannot import a relative one. */
17
+ const key_of = (u) => `og://chunks${u.pathname}`;
18
+ async function build_standalone(html, opts) {
19
+ const base = new URL(opts.page_url);
20
+ const max = opts.max_bytes ?? 12582912;
21
+ let bytes = 0;
22
+ const chunks = /* @__PURE__ */ new Map();
23
+ const missing = [];
24
+ const fetch_text = async (u) => {
25
+ const text = await opts.load(u.href);
26
+ if (text === null) {
27
+ missing.push(u.pathname);
28
+ return null;
29
+ }
30
+ bytes += text.length;
31
+ if (bytes > max) throw new Error(`standalone report would exceed ${Math.round(max / 1048576)} MB of inlined assets`);
32
+ return text;
33
+ };
34
+ /** rewrite a chunk's relative imports to `og:` keys and pull those chunks in too */
35
+ const add_chunk = async (u) => {
36
+ const key = key_of(u);
37
+ if (chunks.get(key) !== void 0) return key;
38
+ chunks.set(key, "");
39
+ const src = await fetch_text(u);
40
+ if (src === null) return key;
41
+ const deps = [];
42
+ const rewritten = src.replace(IMPORT_RE, (_, lead, q, spec) => {
43
+ const dep = new URL(spec, u);
44
+ deps.push(dep);
45
+ return `${lead}${q}${key_of(dep)}${q}`;
46
+ }).replace(DEP_STRING_RE, (_, q, spec) => {
47
+ const dep = new URL(spec, u);
48
+ if (spec.endsWith(".js")) deps.push(dep);
49
+ return `${q}${key_of(dep)}${q}`;
50
+ });
51
+ chunks.set(key, rewritten);
52
+ for (const dep of deps) await add_chunk(dep);
53
+ return key;
54
+ };
55
+ const sheets = [];
56
+ for (const m of html.matchAll(STYLESHEET_RE)) {
57
+ const href = HREF_RE.exec(m[0])?.[1];
58
+ if (href) sheets.push({
59
+ tag: m[0],
60
+ url: new URL(href, base)
61
+ });
62
+ }
63
+ for (const s of sheets) {
64
+ const css = await fetch_text(s.url);
65
+ html = html.replace(s.tag, css === null ? "" : `<style data-standalone="${s.url.pathname}">${css.replaceAll("</style", "<\\/style")}</style>`);
66
+ }
67
+ html = html.replace(MODULEPRELOAD_RE, "");
68
+ const scripts = [];
69
+ for (const m of html.matchAll(MODULE_SCRIPT_RE)) scripts.push({
70
+ tag: m[0],
71
+ attrs: (m[1] + " " + m[3]).replace(/\s+/g, " ").trim(),
72
+ url: new URL(m[2], base)
73
+ });
74
+ for (const s of scripts) {
75
+ const key = await add_chunk(s.url);
76
+ const code = chunks.get(key) ?? "";
77
+ html = html.replace(s.tag, `<script type="module" ${s.attrs.replace(/\btype=["']module["']/i, "").trim()} data-standalone="${s.url.pathname}">${code.replaceAll("<\/script", "<\\/script")}<\/script>`);
78
+ chunks.delete(key);
79
+ }
80
+ const entries = [];
81
+ html = html.replace(ENTRY_ATTR_RE, (_, open, href, close) => {
82
+ const u = new URL(href, base);
83
+ entries.push(u);
84
+ return `${open}${key_of(u)}${close}`;
85
+ });
86
+ for (const u of entries) await add_chunk(u);
87
+ const imports = {};
88
+ for (const [key, code] of chunks) {
89
+ if (!code) continue;
90
+ imports[key] = "data:text/javascript;base64," + Buffer.from(code, "utf8").toString("base64");
91
+ }
92
+ const map = `<script type="importmap">${JSON.stringify({ imports }).replaceAll("<\/script", "<\\/script")}<\/script>`;
93
+ const note = `<!-- ogygia profiler · standalone report · ${Object.keys(imports).length} chunks inlined${missing.length ? ` · missing: ${missing.join(", ")}` : ""} -->`;
94
+ const at = html.search(/<script\b/i);
95
+ html = at === -1 ? html.replace(/<\/head>/i, `${map}</head>`) : html.slice(0, at) + map + html.slice(at);
96
+ return html.replace(/<\/head>/i, `<meta name="ogygia-standalone" content="1">${note}</head>`);
97
+ }
98
+ export { build_standalone };
@@ -0,0 +1,39 @@
1
+ import { Timeline } from "./timeline.js";
2
+ import { StackIndex } from "./analyze.js";
3
+ export interface RenderStep {
4
+ i: number;
5
+ t0: number;
6
+ t1: number;
7
+ ms: number;
8
+ kind: "cpu" | "wait";
9
+ /** cpu: the component / function that ran; wait: what the server waited for */
10
+ label: string;
11
+ category: string;
12
+ file?: string;
13
+ /** cpu: the leaf that burned inside it, when it differs */
14
+ detail?: string;
15
+ /** wait: every call in flight, with its own length */
16
+ calls?: {
17
+ label: string;
18
+ ms: number;
19
+ }[];
20
+ /** wait: the innermost span the wait sat inside */
21
+ within?: string;
22
+ /** the render so far, at the step's end */
23
+ at_ms: number;
24
+ cpu_so_far_ms: number;
25
+ /** the stack at the step's middle, outermost first (from the index; absent when it has none) */
26
+ stack?: string[];
27
+ /** small consecutive steps folded into this one: how many */
28
+ folded?: number;
29
+ }
30
+ export interface RenderSteps {
31
+ steps: RenderStep[];
32
+ window_ms: number;
33
+ cpu_ms: number;
34
+ wait_ms: number;
35
+ /** the step the render spent the longest in */
36
+ longest: number;
37
+ }
38
+ /** `max` steps at most: the smallest consecutive steps fold into their neighbour, counted. */
39
+ export declare function render_steps(t: Timeline, stacks?: StackIndex, max?: number): RenderSteps | undefined;
@@ -0,0 +1,74 @@
1
+ import { stack_at } from "./stacks.js";
2
+ // src/profiler/steps.ts
3
+ const round2 = (n) => Math.round(n * 100) / 100;
4
+ /** `max` steps at most: the smallest consecutive steps fold into their neighbour, counted. */
5
+ function render_steps(t, stacks, max = 80) {
6
+ const segs = t.segments.filter((s) => s.kind === "cpu" || s.kind === "wait" || s.kind === "gap");
7
+ if (!segs.length) return void 0;
8
+ const merged = [];
9
+ for (const s of segs) {
10
+ const kind = s.kind === "gap" ? "wait" : s.kind;
11
+ const label = s.kind === "gap" ? s.pending?.length ? s.pending.slice(0, 3).join(", ") : "nothing on the CPU, nothing named in flight" : s.label;
12
+ const last = merged[merged.length - 1];
13
+ if (last && last.kind === kind && last.label === label && kind === "cpu" && Math.abs(last.t1 - s.t0) < .05) {
14
+ last.t1 = s.t1;
15
+ if (!last.detail && s.detail) last.detail = s.detail;
16
+ continue;
17
+ }
18
+ merged.push({
19
+ ...s,
20
+ kind,
21
+ label,
22
+ folded: 0
23
+ });
24
+ }
25
+ while (merged.length > max) {
26
+ let min_i = 0;
27
+ for (let i = 1; i < merged.length; i++) if (merged[i].t1 - merged[i].t0 < merged[min_i].t1 - merged[min_i].t0) min_i = i;
28
+ const victim = merged[min_i];
29
+ const into = min_i > 0 ? merged[min_i - 1] : merged[min_i + 1];
30
+ if (!into) break;
31
+ if (min_i > 0) into.t1 = victim.t1;
32
+ else into.t0 = victim.t0;
33
+ into.folded += 1 + victim.folded;
34
+ merged.splice(min_i, 1);
35
+ }
36
+ let cpu = 0;
37
+ let wait = 0;
38
+ let longest = 0;
39
+ return {
40
+ steps: merged.map((s, i) => {
41
+ const ms = s.t1 - s.t0;
42
+ if (s.kind === "cpu") cpu += ms;
43
+ else wait += ms;
44
+ if (ms > merged[longest].t1 - merged[longest].t0) longest = i;
45
+ const mid = (s.t0 + s.t1) / 2;
46
+ const at = s.kind === "cpu" && stacks ? stack_at(stacks, mid) : null;
47
+ return {
48
+ i,
49
+ t0: round2(s.t0),
50
+ t1: round2(s.t1),
51
+ ms: round2(ms),
52
+ kind: s.kind,
53
+ label: s.label,
54
+ category: s.category,
55
+ ...s.file ? { file: s.file } : {},
56
+ ...s.detail ? { detail: s.detail } : {},
57
+ ...s.calls?.length ? { calls: s.calls.map((c) => ({
58
+ label: c.label,
59
+ ms: round2(c.ms)
60
+ })) } : {},
61
+ ...s.within ? { within: s.within } : {},
62
+ at_ms: round2(s.t1),
63
+ cpu_so_far_ms: round2(cpu),
64
+ ...at && at.frames.length ? { stack: at.frames.map((f) => f.name) } : {},
65
+ ...s.folded ? { folded: s.folded } : {}
66
+ };
67
+ }),
68
+ window_ms: t.window_ms,
69
+ cpu_ms: round2(cpu),
70
+ wait_ms: round2(wait),
71
+ longest
72
+ };
73
+ }
74
+ export { render_steps };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * PROFILER STORAGE — where finished reports live. By default the profiler keeps them in the
3
+ * instance's memory (and the browser keeps its own copies), which is all a single long-lived
4
+ * server needs. But on an ephemeral host (Amplify, Vercel, Netlify) the instance that recorded a
5
+ * report is often gone before the report page is opened, and a team wants every teammate's
6
+ * recordings in one place. A `ProfilerStore` is the seam for that: point the profiler at one and
7
+ * every report is written there and read back from there, across instances and across a restart.
8
+ *
9
+ * The contract is deliberately tiny — put, get, list, delete a report, and (optionally) put and list
10
+ * visit rows, which carry a server window's requests across instances — so a backend is a few
11
+ * dozen lines. Adapters ship for
12
+ * SQLite (a file, or `:memory:`), Redis and Postgres; a custom one is any object of this shape.
13
+ *
14
+ * // src/hooks.server.ts
15
+ * import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
16
+ * import { sqliteStore } from '@ogygia/devtools/profiler/storage/sqlite';
17
+ * setProfilerStore(sqliteStore('.ogygia/profiles.db'));
18
+ *
19
+ * A report is stored as its portable dump (the same JSON the `.ogp` export and the agent view are
20
+ * built from): `meta` + `analysis` + `extras`, plus a few summary columns for the list. Nothing
21
+ * here is encrypted — a store is trusted infrastructure the profiler owner runs; share links stay
22
+ * the encrypted path for anything that leaves it.
23
+ */
24
+ /** One report as a store holds it: summary columns for the list, and the portable dump as JSON. */
25
+ export interface ProfilerRecord {
26
+ id: string;
27
+ /** epoch ms the report was created */
28
+ created: number;
29
+ /** the profiled page (page mode), else null */
30
+ page: string | null;
31
+ /** `window` | `page` | `request` | `trap` */
32
+ trigger: string;
33
+ /** a human label for the list (the page, or the request path) */
34
+ label: string;
35
+ /** the median render in ms (page mode), else null — shown in the list */
36
+ median: number | null;
37
+ /** the portable dump as a JSON string: `{ kind, version, meta, analysis, extras }` */
38
+ dump: string;
39
+ }
40
+ /** What the list needs — every column of a record except the (large) dump. */
41
+ export type ProfilerSummary = Omit<ProfilerRecord, "dump">;
42
+ /** One visit row, as a store holds it: today, one request of a server window, written by whichever
43
+ * instance served it and listed together under the window's `page` key. Optional to support. */
44
+ export interface ProfilerVisitRecord {
45
+ /** unique per row (it starts with `page`) */
46
+ key: string;
47
+ page: string;
48
+ /** epoch ms */
49
+ at: number;
50
+ /** the visit payload as a JSON string */
51
+ visit: string;
52
+ }
53
+ /**
54
+ * The storage contract. Report methods are required; visit methods are optional (with a store that
55
+ * omits them, a server window sees only its own instance's requests). All async so a network
56
+ * backend fits; a memory or SQLite
57
+ * store just resolves immediately.
58
+ */
59
+ export interface ProfilerStore {
60
+ /** Called once before first use — create tables, connect, etc. Optional. */
61
+ init?(): Promise<void> | void;
62
+ /** Write (or overwrite) a report. */
63
+ putReport(rec: ProfilerRecord): Promise<void> | void;
64
+ /** Read a report's full dump JSON, or null when absent. */
65
+ getReport(id: string): Promise<string | null> | string | null;
66
+ /** The most recent reports (summaries), newest first. */
67
+ listReports(limit?: number): Promise<ProfilerSummary[]> | ProfilerSummary[];
68
+ /** Remove a report. */
69
+ deleteReport(id: string): Promise<void> | void;
70
+ /** Keep at most `keep` reports, dropping the oldest — the store's own LRU, called after a put. */
71
+ prune?(keep: number): Promise<void> | void;
72
+ putVisit?(rec: ProfilerVisitRecord): Promise<void> | void;
73
+ listVisits?(page: string, limit?: number): Promise<ProfilerVisitRecord[]> | ProfilerVisitRecord[];
74
+ /** Release connections. Optional. */
75
+ close?(): Promise<void> | void;
76
+ /** A name for the dashboard ("sqlite", "redis", "postgres", "memory"). */
77
+ readonly kind: string;
78
+ }
79
+ /** An in-memory store — a Map with an LRU cap. The profiler without a store keeps reports in its own
80
+ * memory already, so this adds nothing there; it is for tests and a model for a custom store.
81
+ * Nothing survives a restart. */
82
+ export declare function memoryStore(cap?: number): ProfilerStore;
83
+ /** Pull the summary columns off a record for storing them alongside the dump. */
84
+ export declare function recordColumns(rec: ProfilerRecord): ProfilerSummary;
85
+ export declare function setProfilerStore(store: ProfilerStore): void;
86
+ /** The store set by {@link setProfilerStore}, if any — read by the profiler host on mount. */
87
+ export declare function getProfilerStore(): ProfilerStore | undefined;