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