@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,1122 @@
1
+ <!-- (its CSS injected into the dock's shadow root: see Devtools.svelte) -->
2
+ <svelte:options css="injected" />
3
+
4
+ <script>
5
+ /**
6
+ * The Page tab: what the BROWSER saw of this visit — the profiler beacon's data, read live (no tag,
7
+ * no server; runtime/beacon.ts records it on a devtools build). The vitals rated, what the page
8
+ * found wrong (each finding names its islands: hover lights them up, click opens the island), a
9
+ * waterfall of every island's wake (module → turn → hydrate, against FCP / LCP / DCL / load), the
10
+ * files that blocked the first paint, and the bytes by type. The analysis is page-insights.ts.
11
+ */
12
+ import { read_page, cpu_of, summary_lines } from './page.js';
13
+ import { first_difference, rate } from '@ogygia/core/insights';
14
+ import { beacon_record_cpu } from '@ogygia/core/internal/instruments';
15
+ import { read_styles, scan_unscoped, styles_findings } from './styles.js';
16
+ import { hole_segments, HOLE_SEG_LABEL } from '@ogygia/core/insights';
17
+ import { cache_findings, probe_cache } from './cache-headers.js';
18
+
19
+ let { tick = 0, focus = $bindable(null), selected = $bindable(null) } = $props();
20
+
21
+ // THE PAGE'S STYLES: read once the tab opens (selectors tried against the page, the build's
22
+ // unscoped markers fetched from the cache), after each in-app navigation, and on "check again" —
23
+ // not on every tick
24
+ let styles = $state(/** @type {import('./styles.js').StylesReport | null} */ (null));
25
+ let styles_busy = $state(false);
26
+ async function check_styles() {
27
+ styles_busy = true;
28
+ try {
29
+ const s = read_styles(document, { match: true });
30
+ s.unscoped = await scan_unscoped(document);
31
+ styles = s;
32
+ } catch {
33
+ styles = null;
34
+ }
35
+ styles_busy = false;
36
+ }
37
+ // HOW THE HOST CACHES THE ISLANDS' FILES (cache-headers.ts): asked once per page, beside the styles
38
+ let cache = $state(/** @type {import('./cache-headers.js').CacheProbe[] | null} */ (null));
39
+ async function check_cache() {
40
+ try {
41
+ cache = await probe_cache(document);
42
+ } catch {
43
+ cache = null;
44
+ }
45
+ }
46
+ // again after an in-app navigation (a new page, new sheets): keyed on the navigation's time — a
47
+ // number, so the ticks that rebuild the view do not re-run a check that queries every selector
48
+ const nav_key = $derived(view?.nav?.t ?? 0);
49
+ $effect(() => {
50
+ void nav_key;
51
+ const id = setTimeout(
52
+ () => {
53
+ check_styles();
54
+ check_cache();
55
+ },
56
+ nav_key ? 400 : 60
57
+ );
58
+ return () => clearTimeout(id);
59
+ });
60
+ const findings = $derived.by(() => {
61
+ if (!view) return [];
62
+ const extra = [
63
+ ...(styles ? styles_findings(styles) : []),
64
+ ...(cache ? cache_findings(cache) : [])
65
+ ].map((f) => ({ ...f, fps: /** @type {string[]} */ ([]) }));
66
+ // errors, then warnings, then notes: the report's own order (a stable sort keeps the rest)
67
+ const order = /** @type {Record<string, number>} */ ({ error: 0, warn: 1, info: 2 });
68
+ return [...view.report.findings, ...extra].sort(
69
+ (a, b) => order[a.severity] - order[b.severity]
70
+ );
71
+ });
72
+
73
+ let wrapW = $state(560);
74
+ let diff_fp = $state(/** @type {string | null} */ (null));
75
+ const LABEL_W = 132;
76
+ const trackW = $derived(Math.max(160, wrapW - LABEL_W - 70));
77
+
78
+ const view = $derived.by(() => {
79
+ void tick;
80
+ try {
81
+ return read_page();
82
+ } catch {
83
+ return null;
84
+ }
85
+ });
86
+
87
+ const axis = $derived.by(() => {
88
+ if (!view) return null;
89
+ const { report, page } = view;
90
+ const nav = page.visit?.nav ?? {};
91
+ const paints = page.visit?.paints ?? {};
92
+ const lines = [
93
+ { k: 'FCP', t: paints.fcp, c: '#38bdf8' },
94
+ { k: 'LCP', t: paints.lcp, c: '#f472b6' },
95
+ { k: 'DCL', t: nav.dcl, c: '#94a3b8' },
96
+ { k: 'load', t: nav.load, c: '#64748b' }
97
+ ].filter((l) => typeof l.t === 'number' && l.t > 0);
98
+ let end = 1;
99
+ for (const r of report.rows) end = Math.max(end, r.done);
100
+ for (const h of view.holes ?? []) end = Math.max(end, (h.shown_at ?? 0) + h.wait_ms);
101
+ for (const l of lines) end = Math.max(end, l.t);
102
+ end *= 1.04;
103
+ const x = (/** @type {number} */ t) => Math.max(0, Math.min(trackW, (t / end) * trackW));
104
+ return { end, lines, x };
105
+ });
106
+
107
+ // a hole's bar, cut where the browser and the server timed it (the profiler's One clock cuts it
108
+ // the same way: hole-segments.ts)
109
+ const hole_segs = hole_segments;
110
+ const SEG_TITLE = HOLE_SEG_LABEL;
111
+
112
+ const kb = (/** @type {number} */ n) =>
113
+ n < 1024
114
+ ? n + ' B'
115
+ : n < 1048576
116
+ ? (n / 1024).toFixed(1) + ' KB'
117
+ : (n / 1048576).toFixed(2) + ' MB';
118
+ const ms = (/** @type {number} */ n) =>
119
+ n >= 1000 ? (n / 1000).toFixed(2) + ' s' : Math.round(n) + ' ms';
120
+
121
+ function el_of(/** @type {string} */ fp) {
122
+ return document.querySelector(`ogygia-region[data-og-fp="${fp}"]`);
123
+ }
124
+ function light(/** @type {string[]} */ fps) {
125
+ focus = fps.length ? el_of(fps[0]) : null;
126
+ }
127
+ function open(/** @type {string} */ fp) {
128
+ const el = el_of(fp);
129
+ if (el) selected = el;
130
+ }
131
+ // a hole has no fingerprint: its endpoint finds it
132
+ function hole_el(/** @type {string | undefined} */ endpoint) {
133
+ return endpoint
134
+ ? document.querySelector(`ogygia-region[endpoint="${CSS.escape(endpoint)}"]`)
135
+ : null;
136
+ }
137
+ function vital_text(v) {
138
+ return v.key === 'cls' ? String(v.value) : ms(v.value);
139
+ }
140
+ // the server's own account of THIS document (its Server-Timing: ogygia's profiler sends it in dev)
141
+ const doc_server = (() => {
142
+ try {
143
+ const nav = performance.getEntriesByType('navigation')[0];
144
+ const st = nav?.serverTiming ?? [];
145
+ const get = (n) => st.find((x) => x.name === n);
146
+ const ssr = get('ssr');
147
+ if (!ssr) return null;
148
+ const net = get('net');
149
+ const ups = ['up1', 'up2', 'up3'].map(get).filter(Boolean);
150
+ return {
151
+ ssr: ssr.duration,
152
+ net: net?.duration ?? null,
153
+ calls: net?.description ?? '',
154
+ ups: ups.map((u) => ({ desc: u.description, ms: u.duration }))
155
+ };
156
+ } catch {
157
+ return null;
158
+ }
159
+ })();
160
+ // the document's vitals (after a navigation the report drops them; they still show, labelled)
161
+ const LABEL = { ttfb: 'TTFB', fcp: 'FCP', lcp: 'LCP', cls: 'CLS', inp: 'INP' };
162
+ const doc_vitals = $derived.by(() => {
163
+ if (!view) return [];
164
+ if (!view.nav) return view.report.vitals;
165
+ const out = [];
166
+ for (const key of ['ttfb', 'fcp', 'lcp', 'cls', 'inp']) {
167
+ const v = view.page.vitals[key];
168
+ if (typeof v === 'number')
169
+ out.push({ key, label: LABEL[key], value: v, rating: rate(key, v) });
170
+ }
171
+ return out;
172
+ });
173
+ // ── the main thread (the browser's sampler): the load trace, or a recording made here ──
174
+ let pick = $state(0);
175
+ let recording = $state(false);
176
+ const traces = $derived(view?.page.cpu.traces ?? []);
177
+ const cpu = $derived.by(() => {
178
+ const t = traces[Math.min(pick, traces.length - 1)];
179
+ // (with the source's lines: the served code's are not; `view` changes on each tick, so a file's
180
+ // map read since the last one shows up then)
181
+ return t && view ? summary_lines(cpu_of(view.page, t.trace)) : null;
182
+ });
183
+ async function record() {
184
+ recording = true;
185
+ try {
186
+ await beacon_record_cpu(5000, `recording ${new Date().toLocaleTimeString()}`);
187
+ pick = 0; // the newest
188
+ } finally {
189
+ recording = false;
190
+ }
191
+ }
192
+ const KIND_COLOR = {
193
+ app: '#22c55e',
194
+ ogygia: '#14b8a6',
195
+ svelte: '#f97316',
196
+ dependency: '#a78bfa',
197
+ 'page script': '#f472b6',
198
+ browser: '#64748b',
199
+ devtools: '#334155'
200
+ };
201
+ const diff = $derived.by(() => {
202
+ if (!view || !diff_fp) return null;
203
+ const s = view.page.snapshots.find((x) => x.fp === diff_fp);
204
+ return s ? first_difference(s.ssr, s.hydrated, 120) : null;
205
+ });
206
+ </script>
207
+
208
+ <div class="pg" bind:clientWidth={wrapW}>
209
+ {#if !view}
210
+ <p class="muted" data-og-page-unmeasured>
211
+ This page load was not measured: in a build, devtools measures only for a browser that opened
212
+ it before the page loaded (so other visitors pay nothing). <b>Reload</b> and it is measured from
213
+ the start.
214
+ </p>
215
+ {:else}
216
+ <div class="cap">
217
+ page <span class="muted"
218
+ >· what the browser saw of this visit (dev server: modules load one by one, so load times
219
+ run high)</span
220
+ >
221
+ </div>
222
+
223
+ {#if view.since && (view.since.fixed.length || view.since.added.length || view.since.moved.length || view.since.note)}
224
+ {@const s = view.since}
225
+ <!-- the dev loop: changed the code, reloaded — what changed on this page -->
226
+ <div class="since" data-og-page-since>
227
+ <b>Since your last load</b> <span class="muted">({ms(s.ago_ms)} ago)</span>
228
+ {#if s.fixed.length}<p class="good">fixed: {s.fixed.join(' · ')}</p>{/if}
229
+ {#if s.added.length}<p class="bad">new: {s.added.join(' · ')}</p>{/if}
230
+ {#if s.moved.length}
231
+ <p>
232
+ {#each s.moved as m, i (m.what)}{i ? ' · ' : ''}<span class={m.better ? 'good' : 'bad'}
233
+ >{m.what}
234
+ {m.unit ? ms(m.a) : m.a} → {m.unit ? ms(m.b) : m.b}{#if m.part}<span
235
+ class="muted"
236
+ data-og-page-since-part
237
+ >, mostly {m.part.label} {ms(m.part.a)} → {ms(m.part.b)}</span
238
+ >{/if}</span
239
+ >{/each}
240
+ </p>
241
+ {/if}
242
+ {#if s.note}<p class="muted" data-og-page-since-note>{s.note}</p>{/if}
243
+ </div>
244
+ {/if}
245
+
246
+ {#if view.unmeasured.length}
247
+ <p class="navnote" data-og-page-browser-limits>
248
+ This browser does not report {view.unmeasured.join(', ')}. Findings that need them cannot
249
+ appear here, so their absence is not a clean bill: check those in a Chromium browser.
250
+ </p>
251
+ {/if}
252
+
253
+ {#if view.nav}
254
+ <p class="navnote" data-og-page-nav>
255
+ Since the in-app navigation to <code>{view.nav.to}</code> ({ms(
256
+ performance.now() - view.nav.t
257
+ )} ago): the findings and island rows below are this page's. The vitals are the first page's load
258
+ (a navigation paints nothing the browser reports); record the main thread again to see this page's.
259
+ </p>
260
+ {/if}
261
+ <div class="vitals" data-og-vitals>
262
+ {#each doc_vitals as v (v.key)}
263
+ <div
264
+ class="vital {v.rating}"
265
+ title="{v.label}: {v.rating === 'good'
266
+ ? 'good'
267
+ : v.rating === 'fair'
268
+ ? 'needs work'
269
+ : 'poor'}"
270
+ >
271
+ <span class="k">{v.label}</span><span class="v">{vital_text(v)}</span>
272
+ </div>
273
+ {:else}
274
+ <span class="muted">no vitals yet</span>
275
+ {/each}
276
+ {#if view.report.longtask_ms}
277
+ <div class="vital neutral" title="main-thread time in tasks over 50 ms">
278
+ <span class="k">long tasks</span><span class="v">{ms(view.report.longtask_ms)}</span>
279
+ </div>
280
+ {/if}
281
+ </div>
282
+
283
+ {#if doc_server}
284
+ <p class="srvline" data-og-page-server>
285
+ The server rendered this page in <b>{ms(doc_server.ssr)}</b>{#if doc_server.net !== null}, {ms(
286
+ doc_server.net
287
+ )} of it waiting on {doc_server.calls.split('(')[1]?.split(')')[0] ?? 'its'} outbound calls{#if doc_server.ups.length}
288
+ — the slowest {doc_server.ups.map((u) => `${u.desc} ${ms(u.ms)}`).join(', ')}{/if}{/if}.
289
+ </p>
290
+ {/if}
291
+
292
+ <h3>Findings <span class="muted">{findings.length || 'none'}</span></h3>
293
+ {#if findings.length}
294
+ <ul class="findings" data-og-page-findings>
295
+ {#each findings as f, i (f.code + i)}
296
+ <li
297
+ class={f.severity}
298
+ data-code={f.code}
299
+ onmouseenter={() => light(f.fps)}
300
+ onmouseleave={() => (focus = null)}
301
+ >
302
+ <p class="msg"><span class="sev">{f.severity}</span>{f.message}</p>
303
+ {#if f.fix}<p class="fix">{f.fix}</p>{/if}
304
+ {#if f.fps.length}
305
+ <p class="acts">
306
+ {#each f.fps.slice(0, 6) as fp (fp)}
307
+ <button class="chip" onclick={() => open(fp)}
308
+ >{view.regions.find((r) => r.fp === fp)?.name ?? fp.slice(0, 8)}</button
309
+ >
310
+ {/each}
311
+ {#if f.code === 'markup-changed'}
312
+ <button
313
+ class="chip alt"
314
+ onclick={() => (diff_fp = diff_fp === f.fps[0] ? null : f.fps[0])}
315
+ >{diff_fp ? 'hide' : 'show'} difference</button
316
+ >
317
+ {/if}
318
+ </p>
319
+ {/if}
320
+ {#if f.code === 'markup-changed' && diff && diff_fp === f.fps[0]}
321
+ <div class="diff">
322
+ <div><span class="lbl">server</span><code>{diff.server}</code></div>
323
+ <div><span class="lbl">browser</span><code>{diff.now}</code></div>
324
+ </div>
325
+ {/if}
326
+ </li>
327
+ {/each}
328
+ </ul>
329
+ {:else}
330
+ <p class="muted ok">Nothing the browser measured looks wrong on this visit.</p>
331
+ {/if}
332
+
333
+ <!-- (a page with no islands — a Kit-hydrated one — has no wakes to show: no empty heading) -->
334
+ {#if view.report.rows.length || view.kept?.length || view.regions.some((r) => r.kind === 'island')}
335
+ <h3>
336
+ Island wakes <span class="muted"
337
+ >{view.report.rows.length}{view.kept?.length ? ` · ${view.kept.length} kept` : ''}</span
338
+ >
339
+ </h3>
340
+ {/if}
341
+ {#if view.kept?.length}
342
+ <!-- the router reused them: the same island with the same props on both pages -->
343
+ <p class="kept" data-og-page-kept>
344
+ Kept from the page before, still awake (no code to load, no hydrate):
345
+ {#each view.kept.slice(0, 12) as k, i (k.fp)}{i ? ', ' : ''}<button
346
+ class="chip"
347
+ onclick={() => open(k.fp)}
348
+ onmouseenter={() => light([k.fp])}
349
+ onmouseleave={() => (focus = null)}>{k.name}</button
350
+ >{/each}{view.kept.length > 12 ? ` and ${view.kept.length - 12} more` : ''}.
351
+ </p>
352
+ {/if}
353
+ {#if axis && view.report.rows.length}
354
+ <div class="legend">
355
+ <span><i class="sw load"></i>module</span>
356
+ <span><i class="sw queue"></i>waiting for its turn</span>
357
+ <span><i class="sw hyd"></i>hydrate</span>
358
+ {#each axis.lines as l (l.k)}<span
359
+ ><i class="ln" style:background={l.c}></i>{l.k} {ms(l.t)}</span
360
+ >{/each}
361
+ </div>
362
+ <div class="wf">
363
+ {#each view.report.rows as r (r.fp)}
364
+ {@const lo = r.queue_ms === null ? r.t0 + r.load_ms : r.t0 + r.load_ms + r.queue_ms}
365
+ <div
366
+ class="row"
367
+ class:bad={r.recovered || r.changed || r.early_ms !== null || r.shift >= 0.01}
368
+ role="button"
369
+ tabindex="0"
370
+ onmouseenter={() => light([r.fp])}
371
+ onmouseleave={() => (focus = null)}
372
+ onclick={() => open(r.fp)}
373
+ onkeydown={(e) => e.key === 'Enter' && open(r.fp)}
374
+ >
375
+ <span class="name" style:width="{LABEL_W}px" title={r.name}
376
+ >{r.name}<span class="wake">{r.wake}</span></span
377
+ >
378
+ <span class="track" style:width="{trackW}px">
379
+ {#each axis.lines as l (l.k)}<i
380
+ class="ln abs"
381
+ style:left="{axis.x(l.t)}px"
382
+ style:background={l.c}
383
+ ></i>{/each}
384
+ <i
385
+ class="seg load"
386
+ style:left="{axis.x(r.t0)}px"
387
+ style:width="{Math.max(1, axis.x(r.t0 + r.load_ms) - axis.x(r.t0))}px"
388
+ ></i>
389
+ {#if r.queue_ms !== null}<i
390
+ class="seg queue"
391
+ style:left="{axis.x(r.t0 + r.load_ms)}px"
392
+ style:width="{Math.max(0, axis.x(lo) - axis.x(r.t0 + r.load_ms))}px"
393
+ ></i>{/if}
394
+ <i
395
+ class="seg hyd"
396
+ style:left="{axis.x(lo)}px"
397
+ style:width="{Math.max(2, axis.x(r.done) - axis.x(lo))}px"
398
+ ></i>
399
+ </span>
400
+ <span
401
+ class="t"
402
+ title="module {r.load_ms} ms{r.queue_ms !== null
403
+ ? ` · waited ${r.queue_ms} ms`
404
+ : ''} · hydrate {r.hydrate_ms} ms">{ms(r.done)}</span
405
+ >
406
+ <span class="tags">
407
+ {#if r.recovered}<b class="tag err">recovered</b>{/if}
408
+ {#if r.changed && !r.recovered}<b class="tag warn">markup changed</b>{/if}
409
+ {#if r.early_ms !== null}<b class="tag warn"
410
+ >clicked {Math.round(r.early_ms)} ms early</b
411
+ >{/if}
412
+ {#if r.shift >= 0.01}<b class="tag warn">CLS {r.shift}</b>{/if}
413
+ {#if r.hydrate_ms >= 50}<b class="tag warn">{Math.round(r.hydrate_ms)} ms task</b
414
+ >{/if}
415
+ {#if r.cpu_ms && r.cpu_ms >= 20}<b
416
+ class="tag info"
417
+ title="main-thread CPU sampled in its hydrate window"
418
+ >{Math.round(r.cpu_ms)} ms CPU</b
419
+ >{/if}
420
+ {#if r.below_fold && (r.wake === 'load' || r.wake === 'idle')}<b class="tag info"
421
+ >below the fold</b
422
+ >{/if}
423
+ </span>
424
+ </div>
425
+ {/each}
426
+ </div>
427
+ {:else}
428
+ <p class="muted">No island has woken yet.</p>
429
+ {/if}
430
+
431
+ {#if axis && (view.holes?.length || view.holes_failed?.length)}
432
+ <!-- each hole's first answer: how long its fallback stood, and where that time went -->
433
+ <h3>
434
+ Hole answers <span class="muted"
435
+ >{view.holes?.length ?? 0}{view.holes_failed?.length
436
+ ? ` · ${view.holes_failed.length} never came`
437
+ : ''}</span
438
+ >
439
+ </h3>
440
+ <div class="legend">
441
+ <span><i class="sw h-before"></i>before its request left</span>
442
+ <span><i class="sw h-slot"></i>render slot</span>
443
+ <span><i class="sw h-render"></i>server render</span>
444
+ <span><i class="sw h-server"></i>waiting on the server</span>
445
+ <span><i class="sw h-rest"></i>network + swap</span>
446
+ </div>
447
+ <div class="wf" data-og-page-holes>
448
+ {#each view.holes_failed ?? [] as f, i (f.endpoint ?? i)}
449
+ <!-- no answer: the fallback still stands; the Findings say why and where to look -->
450
+ <div
451
+ class="row bad"
452
+ data-og-hole-failed
453
+ role="button"
454
+ tabindex="0"
455
+ onmouseenter={() => (focus = hole_el(f.endpoint))}
456
+ onmouseleave={() => (focus = null)}
457
+ onclick={() => (selected = hole_el(f.endpoint) ?? selected)}
458
+ onkeydown={(e) => e.key === 'Enter' && (selected = hole_el(f.endpoint) ?? selected)}
459
+ >
460
+ <span class="name" style:width="{LABEL_W}px" title={f.name}>{f.name}</span>
461
+ <span class="track" style:width="{trackW}px"
462
+ ><i class="seg h-failed" style:left="0px" style:width="{trackW}px"></i></span
463
+ >
464
+ <span class="t">none</span>
465
+ <span class="tags"
466
+ ><b class="tag err"
467
+ >{f.reason === 'error'
468
+ ? `failed ${f.attempts}×${f.message ? ` (${f.message})` : ''}`
469
+ : f.reason === 'forbidden'
470
+ ? `refused${f.message ? ` (${f.message})` : ''}`
471
+ : f.reason === 'redirected'
472
+ ? 'redirected'
473
+ : 'answered with a page'}</b
474
+ ></span
475
+ >
476
+ </div>
477
+ {/each}
478
+ {#each view.holes ?? [] as h, i (h.endpoint ?? i)}
479
+ {@const segs = hole_segs(h)}
480
+ <div
481
+ class="row"
482
+ class:bad={!h.below_fold && h.wait_ms >= 1000}
483
+ role="button"
484
+ tabindex="0"
485
+ onmouseenter={() => (focus = hole_el(h.endpoint))}
486
+ onmouseleave={() => (focus = null)}
487
+ onclick={() => (selected = hole_el(h.endpoint) ?? selected)}
488
+ onkeydown={(e) => e.key === 'Enter' && (selected = hole_el(h.endpoint) ?? selected)}
489
+ >
490
+ <span class="name" style:width="{LABEL_W}px" title={h.name}>{h.name}</span>
491
+ <span class="track" style:width="{trackW}px">
492
+ {#each axis.lines as l (l.k)}<i
493
+ class="ln abs"
494
+ style:left="{axis.x(l.t)}px"
495
+ style:background={l.c}
496
+ ></i>{/each}
497
+ {#each segs as s (s.k)}<i
498
+ class="seg h-{s.k}"
499
+ title="{SEG_TITLE[s.k]}: {ms(s.b - s.a)}"
500
+ style:left="{axis.x(s.a)}px"
501
+ style:width="{Math.max(1, axis.x(s.b) - axis.x(s.a))}px"
502
+ ></i>{/each}
503
+ </span>
504
+ <span
505
+ class="t"
506
+ title="its fallback stood this long{h.below_fold ? ' (below the first screen)' : ''}"
507
+ >{ms(h.wait_ms)}</span
508
+ >
509
+ <span class="tags">
510
+ {#if h.server_queue_ms && h.server_queue_ms >= 200}<b class="tag warn"
511
+ >{ms(h.server_queue_ms)} for a slot</b
512
+ >{/if}
513
+ {#if h.below_fold}<b class="tag info">below the fold</b>{/if}
514
+ </span>
515
+ </div>
516
+ {/each}
517
+ </div>
518
+ {/if}
519
+
520
+ <h3>
521
+ Main thread <span class="muted"
522
+ >{cpu ? `${ms(cpu.busy_ms)} busy in ${ms(cpu.window_ms)}` : ''}</span
523
+ >
524
+ </h3>
525
+ <div class="cpu" data-og-cpu>
526
+ <div class="cpu-bar">
527
+ {#if traces.length > 1}
528
+ <select bind:value={pick} aria-label="which trace">
529
+ {#each traces as t, i (t.from)}<option value={i}>{t.label}</option>{/each}
530
+ </select>
531
+ {:else if traces.length}
532
+ <span class="muted">{traces[0].label}</span>
533
+ {/if}
534
+ <button
535
+ class="chip alt"
536
+ data-og-cpu-record
537
+ disabled={recording ||
538
+ view.page.cpu.off === 'unsupported' ||
539
+ view.page.cpu.off === 'no-policy'}
540
+ onclick={record}
541
+ >
542
+ {recording ? 'recording 5 s… use the page now' : 'record 5 s'}
543
+ </button>
544
+ </div>
545
+ {#if !traces.length}
546
+ <p class="muted">
547
+ {view.page.cpu.state === 'recording'
548
+ ? 'Sampling the page load (8 s)…'
549
+ : view.page.cpu.off === 'unsupported'
550
+ ? 'This browser has no JS sampler (the JS Self-Profiling API is Chromium-only).'
551
+ : view.page.cpu.off === 'no-policy'
552
+ ? 'The browser would not sample this document: it needs the header Document-Policy: js-profiling (the devtools dev server sends it; a proxy may drop it).'
553
+ : 'No trace yet.'}
554
+ </p>
555
+ {:else if cpu}
556
+ <div class="kinds" title="where the busy time went">
557
+ {#each cpu.by_kind as k (k.kind)}
558
+ <i
559
+ style:width="{(k.ms / Math.max(1, cpu.busy_ms)) * 100}%"
560
+ style:background={KIND_COLOR[k.kind]}
561
+ title="{k.kind} {ms(k.ms)}"
562
+ ></i>
563
+ {/each}
564
+ </div>
565
+ <div class="legend">
566
+ {#each cpu.by_kind as k (k.kind)}<span
567
+ ><i class="sw" style:background={KIND_COLOR[k.kind]}></i>{k.kind} {ms(k.ms)}</span
568
+ >{/each}
569
+ </div>
570
+ <table data-og-cpu-fns>
571
+ <thead
572
+ ><tr
573
+ ><th>function</th><th>where</th><th class="num">own</th><th class="num"
574
+ >with callees</th
575
+ ></tr
576
+ ></thead
577
+ >
578
+ <tbody>
579
+ {#each cpu.fns.slice(0, 10) as f, i (f.name + f.file + f.line + i)}
580
+ <tr>
581
+ <td><i class="sw" style:background={KIND_COLOR[f.kind]}></i>{f.name}</td>
582
+ <td class="url" title={f.file}
583
+ >{f.file ? f.file.slice(f.file.lastIndexOf('/') + 1) : '—'}{f.line !== null
584
+ ? `:${f.line}`
585
+ : ''}</td
586
+ >
587
+ <td class="num">{ms(f.self_ms)}</td>
588
+ <td class="num">{ms(f.total_ms)}</td>
589
+ </tr>
590
+ {/each}
591
+ </tbody>
592
+ </table>
593
+ <p class="muted">
594
+ Sampled every {cpu.interval_ms} ms by the browser: a function under ~{Math.round(
595
+ cpu.interval_ms * 2
596
+ )} ms may not show.
597
+ </p>
598
+ {/if}
599
+ </div>
600
+
601
+ {#if view.report.blocking.length}
602
+ <h3>Blocked the first paint <span class="muted">{view.report.blocking.length}</span></h3>
603
+ <table>
604
+ <tbody>
605
+ {#each view.report.blocking.slice(0, 8) as b, i (b.url + i)}
606
+ <tr
607
+ ><td class="url" title={b.url}>{b.url.split('?')[0].split('/').pop() || b.url}</td><td
608
+ class="num">{ms(b.ms)}</td
609
+ ><td class="num">{b.bytes ? kb(b.bytes) : ''}</td></tr
610
+ >
611
+ {/each}
612
+ </tbody>
613
+ </table>
614
+ {/if}
615
+
616
+ <h3>
617
+ Styles
618
+ <span class="muted"
619
+ >{styles
620
+ ? `${styles.sheets.length} sheet${styles.sheets.length === 1 ? '' : 's'} · ${styles.rules} rules`
621
+ : styles_busy
622
+ ? 'reading…'
623
+ : ''}</span
624
+ >
625
+ <button
626
+ class="chip alt"
627
+ onclick={check_styles}
628
+ disabled={styles_busy}
629
+ title="try every selector against the page as it is now (open a menu first to count its rules)"
630
+ >check again</button
631
+ >
632
+ </h3>
633
+ {#if styles}
634
+ {#if styles.unscoped.length}
635
+ <p class="unscoped" data-og-page-unscoped>
636
+ Unscoped: {#each styles.unscoped as u, i (u.file)}{i ? ', ' : ''}<code>{u.file}</code
637
+ >{/each}. Their rules apply to every element on the page.
638
+ </p>
639
+ {/if}
640
+ <table data-og-page-styles>
641
+ <thead
642
+ ><tr
643
+ ><th>sheet</th><th class="num">rules</th><th class="num">match nothing</th><th
644
+ class="num">size</th
645
+ ></tr
646
+ ></thead
647
+ >
648
+ <tbody>
649
+ {#each styles.sheets.slice(0, 10) as s, i (s.label + i)}
650
+ <tr>
651
+ <td class="url" title={s.href ?? 'a style element in the document'}
652
+ >{s.label}{#if s.blocking}
653
+ <span class="tag">blocked paint</span>{/if}{#if s.ogygia}
654
+ <span class="tag og">ogygia</span>{/if}</td
655
+ >
656
+ <td class="num">{s.rules}</td>
657
+ <td class="num"
658
+ >{s.unmatched === null
659
+ ? '—'
660
+ : s.rules
661
+ ? `${s.unmatched} (${Math.round((s.unmatched / s.rules) * 100)}%)`
662
+ : '0'}</td
663
+ >
664
+ <td class="num">{s.bytes ? kb(s.bytes) : '—'}</td>
665
+ </tr>
666
+ {/each}
667
+ </tbody>
668
+ </table>
669
+ {#if styles.sheets.length > 10}<p class="muted">
670
+ and {styles.sheets.length - 10} more sheets
671
+ </p>{/if}
672
+ {#if styles.examples.length}
673
+ <p class="muted">
674
+ Match nothing now, e.g. {#each styles.examples.slice(0, 4) as e, i (e)}{i
675
+ ? ', '
676
+ : ''}<code>{e.length > 60 ? e.slice(0, 60) + '…' : e}</code>{/each}. Content that
677
+ shows up later (menus, dialogs) counts here until it is open.
678
+ </p>
679
+ {/if}
680
+ {#if styles.unreadable}<p class="muted">
681
+ {styles.unreadable} sheet{styles.unreadable === 1 ? '' : 's'} from another origin could not
682
+ be read.
683
+ </p>{/if}
684
+ {/if}
685
+
686
+ {#if view.report.bytes.length}
687
+ <h3>Files by type</h3>
688
+ <table>
689
+ <thead
690
+ ><tr
691
+ ><th>type</th><th class="num">files</th><th class="num">sent</th><th class="num"
692
+ >size</th
693
+ ></tr
694
+ ></thead
695
+ >
696
+ <tbody>
697
+ {#each view.report.bytes as b (b.type)}
698
+ <tr
699
+ ><td>{b.type}</td><td class="num">{b.count}</td><td class="num"
700
+ >{b.transfer ? kb(b.transfer) : '—'}</td
701
+ ><td class="num">{b.size ? kb(b.size) : '—'}</td></tr
702
+ >
703
+ {/each}
704
+ </tbody>
705
+ </table>
706
+ {/if}
707
+ {/if}
708
+ </div>
709
+
710
+ <style>
711
+ .pg {
712
+ display: flex;
713
+ flex-direction: column;
714
+ gap: 8px;
715
+ }
716
+ .cap {
717
+ font-size: 12px;
718
+ color: #5eead4;
719
+ }
720
+ .srvline {
721
+ margin: 0;
722
+ color: #cbd5e1;
723
+ line-height: 1.5;
724
+ }
725
+ .srvline b {
726
+ color: #5eead4;
727
+ }
728
+ .since {
729
+ padding: 6px 9px;
730
+ border-radius: 8px;
731
+ background: rgba(56, 189, 248, 0.08);
732
+ border: 1px solid rgba(56, 189, 248, 0.25);
733
+ }
734
+ .since p {
735
+ margin: 3px 0 0;
736
+ }
737
+ .since .good {
738
+ color: #4ade80;
739
+ }
740
+ .since .bad {
741
+ color: #fca5a5;
742
+ }
743
+ .navnote {
744
+ margin: 0;
745
+ padding: 6px 9px;
746
+ border-radius: 8px;
747
+ background: rgba(236, 72, 153, 0.1);
748
+ border: 1px solid rgba(236, 72, 153, 0.3);
749
+ color: #fbcfe8;
750
+ line-height: 1.5;
751
+ }
752
+ .navnote code {
753
+ color: #f9a8d4;
754
+ }
755
+ .kept {
756
+ margin: 0;
757
+ color: #cbd5e1;
758
+ line-height: 1.8;
759
+ }
760
+ .muted {
761
+ color: #94a3b8;
762
+ }
763
+ .ok {
764
+ color: #86efac;
765
+ }
766
+ h3 {
767
+ margin: 8px 0 0;
768
+ font-size: 11px;
769
+ color: #cbd5e1;
770
+ text-transform: uppercase;
771
+ letter-spacing: 0.06em;
772
+ }
773
+ .vitals {
774
+ display: flex;
775
+ flex-wrap: wrap;
776
+ gap: 6px;
777
+ }
778
+ .vital {
779
+ display: flex;
780
+ flex-direction: column;
781
+ padding: 5px 10px;
782
+ border-radius: 8px;
783
+ border: 1px solid rgba(148, 163, 184, 0.25);
784
+ min-width: 64px;
785
+ }
786
+ .vital .k {
787
+ color: #94a3b8;
788
+ font-size: 10px;
789
+ }
790
+ .vital .v {
791
+ font-size: 13px;
792
+ font-weight: 700;
793
+ }
794
+ .vital.good {
795
+ border-color: rgba(34, 197, 94, 0.5);
796
+ }
797
+ .vital.good .v {
798
+ color: #4ade80;
799
+ }
800
+ .vital.fair {
801
+ border-color: rgba(251, 191, 36, 0.5);
802
+ }
803
+ .vital.fair .v {
804
+ color: #fbbf24;
805
+ }
806
+ .vital.poor {
807
+ border-color: rgba(239, 68, 68, 0.55);
808
+ }
809
+ .vital.poor .v {
810
+ color: #f87171;
811
+ }
812
+ .findings {
813
+ list-style: none;
814
+ margin: 0;
815
+ padding: 0;
816
+ display: flex;
817
+ flex-direction: column;
818
+ gap: 6px;
819
+ }
820
+ .findings li {
821
+ padding: 7px 9px;
822
+ border-radius: 8px;
823
+ border-left: 3px solid #64748b;
824
+ background: rgba(148, 163, 184, 0.07);
825
+ }
826
+ .findings li.error {
827
+ border-left-color: #ef4444;
828
+ }
829
+ .findings li.warn {
830
+ border-left-color: #f59e0b;
831
+ }
832
+ .findings li.info {
833
+ border-left-color: #38bdf8;
834
+ }
835
+ .msg,
836
+ .fix,
837
+ .acts {
838
+ margin: 0;
839
+ line-height: 1.5;
840
+ }
841
+ .fix {
842
+ color: #94a3b8;
843
+ margin-top: 3px;
844
+ }
845
+ .sev {
846
+ display: inline-block;
847
+ font-size: 9px;
848
+ text-transform: uppercase;
849
+ letter-spacing: 0.06em;
850
+ color: #94a3b8;
851
+ margin-right: 6px;
852
+ }
853
+ .acts {
854
+ display: flex;
855
+ flex-wrap: wrap;
856
+ gap: 4px;
857
+ margin-top: 5px;
858
+ }
859
+ .chip {
860
+ font: inherit;
861
+ font-size: 10px;
862
+ padding: 1px 7px;
863
+ border-radius: 999px;
864
+ border: 1px solid rgba(94, 234, 212, 0.4);
865
+ background: none;
866
+ color: #5eead4;
867
+ cursor: pointer;
868
+ }
869
+ .chip.alt {
870
+ border-color: rgba(148, 163, 184, 0.35);
871
+ color: #cbd5e1;
872
+ }
873
+ .diff {
874
+ margin-top: 6px;
875
+ display: flex;
876
+ flex-direction: column;
877
+ gap: 3px;
878
+ }
879
+ .diff div {
880
+ display: flex;
881
+ gap: 6px;
882
+ }
883
+ .diff .lbl {
884
+ width: 52px;
885
+ flex: none;
886
+ color: #94a3b8;
887
+ }
888
+ .diff code {
889
+ white-space: pre-wrap;
890
+ word-break: break-all;
891
+ color: #e2e8f0;
892
+ background: rgba(148, 163, 184, 0.1);
893
+ padding: 2px 5px;
894
+ border-radius: 4px;
895
+ }
896
+ .legend {
897
+ display: flex;
898
+ flex-wrap: wrap;
899
+ gap: 10px;
900
+ color: #94a3b8;
901
+ font-size: 10px;
902
+ }
903
+ .legend span {
904
+ display: inline-flex;
905
+ align-items: center;
906
+ gap: 4px;
907
+ }
908
+ .sw {
909
+ display: inline-block;
910
+ width: 10px;
911
+ height: 6px;
912
+ border-radius: 2px;
913
+ }
914
+ .sw.load,
915
+ .seg.load {
916
+ background: #38bdf8;
917
+ }
918
+ .sw.queue,
919
+ .seg.queue {
920
+ background: repeating-linear-gradient(90deg, #475569 0 3px, transparent 3px 5px);
921
+ }
922
+ .sw.hyd,
923
+ .seg.hyd {
924
+ background: #22c55e;
925
+ }
926
+ /* a hole's answer: before it left, the server's slot wait, its render, the rest */
927
+ .sw.h-before,
928
+ .seg.h-before,
929
+ .seg.h-wait {
930
+ background: repeating-linear-gradient(90deg, #475569 0 3px, transparent 3px 5px);
931
+ }
932
+ .sw.h-slot,
933
+ .seg.h-slot {
934
+ background: #f59e0b;
935
+ }
936
+ .sw.h-render,
937
+ .seg.h-render {
938
+ background: #a78bfa;
939
+ }
940
+ .sw.h-server,
941
+ .seg.h-server {
942
+ background: #c4b5fd;
943
+ }
944
+ .sw.h-rest,
945
+ .seg.h-rest {
946
+ background: #7dd3fc;
947
+ }
948
+ .seg.h-failed {
949
+ background: repeating-linear-gradient(
950
+ 90deg,
951
+ rgba(248, 113, 113, 0.55) 0 4px,
952
+ transparent 4px 8px
953
+ );
954
+ }
955
+ .ln {
956
+ display: inline-block;
957
+ width: 2px;
958
+ height: 10px;
959
+ }
960
+ .wf {
961
+ display: flex;
962
+ flex-direction: column;
963
+ gap: 2px;
964
+ }
965
+ .row {
966
+ display: flex;
967
+ align-items: center;
968
+ gap: 6px;
969
+ cursor: pointer;
970
+ padding: 1px 0;
971
+ border-radius: 4px;
972
+ flex-wrap: wrap;
973
+ }
974
+ .row:hover {
975
+ background: rgba(148, 163, 184, 0.1);
976
+ }
977
+ .row.bad .name {
978
+ color: #fbbf24;
979
+ }
980
+ .name {
981
+ flex: none;
982
+ overflow: hidden;
983
+ text-overflow: ellipsis;
984
+ white-space: nowrap;
985
+ }
986
+ .wake {
987
+ color: #94a3b8;
988
+ margin-left: 5px;
989
+ font-size: 10px;
990
+ }
991
+ .track {
992
+ position: relative;
993
+ height: 12px;
994
+ flex: none;
995
+ background: rgba(148, 163, 184, 0.06);
996
+ border-radius: 3px;
997
+ }
998
+ .seg {
999
+ position: absolute;
1000
+ top: 2px;
1001
+ height: 8px;
1002
+ border-radius: 2px;
1003
+ }
1004
+ .ln.abs {
1005
+ position: absolute;
1006
+ top: 0;
1007
+ height: 12px;
1008
+ width: 1px;
1009
+ opacity: 0.8;
1010
+ }
1011
+ .t {
1012
+ width: 56px;
1013
+ text-align: right;
1014
+ color: #94a3b8;
1015
+ flex: none;
1016
+ }
1017
+ .tags {
1018
+ display: flex;
1019
+ gap: 4px;
1020
+ flex-wrap: wrap;
1021
+ padding-left: 4px;
1022
+ }
1023
+ .tag {
1024
+ font-weight: 400;
1025
+ font-size: 10px;
1026
+ padding: 0 5px;
1027
+ border-radius: 4px;
1028
+ }
1029
+ .tag.err {
1030
+ background: rgba(239, 68, 68, 0.2);
1031
+ color: #fca5a5;
1032
+ }
1033
+ .tag.warn {
1034
+ background: rgba(245, 158, 11, 0.18);
1035
+ color: #fcd34d;
1036
+ }
1037
+ .tag.info {
1038
+ background: rgba(56, 189, 248, 0.15);
1039
+ color: #7dd3fc;
1040
+ }
1041
+ .tag.og {
1042
+ background: rgba(94, 234, 212, 0.14);
1043
+ color: #5eead4;
1044
+ }
1045
+ .url .tag {
1046
+ background: rgba(148, 163, 184, 0.16);
1047
+ color: #cbd5e1;
1048
+ }
1049
+ .url .tag.og {
1050
+ background: rgba(94, 234, 212, 0.14);
1051
+ color: #5eead4;
1052
+ }
1053
+ h3 .chip {
1054
+ margin-left: 6px;
1055
+ text-transform: none;
1056
+ letter-spacing: 0;
1057
+ }
1058
+ .unscoped {
1059
+ margin: 0;
1060
+ padding: 6px 9px;
1061
+ border-radius: 8px;
1062
+ background: rgba(245, 158, 11, 0.1);
1063
+ border: 1px solid rgba(245, 158, 11, 0.35);
1064
+ color: #fde68a;
1065
+ }
1066
+ .unscoped code {
1067
+ color: #fcd34d;
1068
+ }
1069
+ table {
1070
+ border-collapse: collapse;
1071
+ width: 100%;
1072
+ }
1073
+ .cpu {
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ gap: 6px;
1077
+ }
1078
+ .cpu-bar {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ gap: 8px;
1082
+ }
1083
+ .cpu-bar select {
1084
+ background: #0d1526;
1085
+ color: #e2e8f0;
1086
+ border: 1px solid rgba(148, 163, 184, 0.25);
1087
+ border-radius: 6px;
1088
+ font: inherit;
1089
+ padding: 2px 5px;
1090
+ }
1091
+ .kinds {
1092
+ display: flex;
1093
+ height: 8px;
1094
+ border-radius: 4px;
1095
+ overflow: hidden;
1096
+ background: rgba(148, 163, 184, 0.1);
1097
+ }
1098
+ .kinds i {
1099
+ display: block;
1100
+ height: 100%;
1101
+ }
1102
+ th {
1103
+ text-align: left;
1104
+ color: #94a3b8;
1105
+ font-weight: 600;
1106
+ border-bottom: 1px solid rgba(148, 163, 184, 0.2);
1107
+ padding: 3px 4px;
1108
+ }
1109
+ td {
1110
+ padding: 2px 4px;
1111
+ border-bottom: 1px solid rgba(148, 163, 184, 0.08);
1112
+ }
1113
+ .num {
1114
+ text-align: right;
1115
+ }
1116
+ .url {
1117
+ max-width: 280px;
1118
+ overflow: hidden;
1119
+ text-overflow: ellipsis;
1120
+ white-space: nowrap;
1121
+ }
1122
+ </style>